Preorder/add new filter

This commit is contained in:
Luca Haid
2025-12-02 14:46:22 +00:00
parent 1985865d1f
commit 09ffd911fc
20 changed files with 1486 additions and 66 deletions
+3 -3
View File
@@ -871,9 +871,9 @@ class PreorderModel
if (is_array($tool_building_type) && count($tool_building_type)) {
$where .= " AND adb_hausnummer.tool_building_type IN ('" . implode("','", $tool_building_type) . "')";
} else {
$tool_building_type = FronkDB::singleton()->escape($filter['connection_type']);
if ($tool_building_type) {
$where .= " AND adb_hausnummer.tool_building_type like '%$tool_building_type%'";
$tool_building_type = FronkDB::singleton()->escape($filter['tool_building_type']);
if ($tool_building_type === '0' || $tool_building_type) {
$where .= " AND adb_hausnummer.tool_building_type = $tool_building_type ";
}
}
}
@@ -1,5 +1,8 @@
<?php
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
class WarehouseOfferController extends TTCrud
{
protected string $headerTitle = 'Angebote';
@@ -53,6 +56,7 @@ class WarehouseOfferController extends TTCrud
$this->postData['offerNumber'] = 'AN' . date('Y') . '-' . str_pad($currentCount + 1, 4, '0', STR_PAD_LEFT);
$this->postData['status'] = 'new';
$this->postData['version'] = 1;
$this->postData['validity'] = 14;
$this->postData['alternativePositions'] = json_encode([]);
return true;
}
@@ -182,11 +186,16 @@ class WarehouseOfferController extends TTCrud
// E-Mail Actions
public function sendOfferEmailAction()
{
//display errors for debugging
error_reporting(E_ALL);
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
$_POST = json_decode(file_get_contents('php://input'), true);
$id = $_POST['id'] ?? null;
$recipientEmail = $_POST['email'] ?? null;
$subject = $_POST['subject'] ?? 'Ihr Angebot von XINON GmbH';
$body = $_POST['body'] ?? 'Anbei finden Sie Ihr angefordertes Angebot.';
$bodyText = $_POST['body'] ?? 'Anbei finden Sie Ihr angefordertes Angebot.';
if (!$id || !$recipientEmail) {
self::sendError("ID oder E-Mail-Adresse fehlt.");
@@ -208,16 +217,66 @@ class WarehouseOfferController extends TTCrud
// Generate PDF
$pdfContent = $this->createPDFAction(true, $id, true);
// Send Email
$mail = new Mail();
$mail->addAddress($recipientEmail, $offer->contactPerson ?? $offer->customerName);
$mail->setSubject($subject);
$mail->setBody($body);
$mail->addStringAttachment($pdfContent, $offer->offerNumber . '_Angebot.pdf', 'base64', 'application/pdf');
// --- HTML Email Generation ---
$logoToolPath = BASEDIR . '/public/assets/images/the-tool-logo.png';
$logoXinonPath = BASEDIR . '/public/assets/images/xinon-full.png';
$logoToolExists = file_exists($logoToolPath);
$logoXinonExists = file_exists($logoXinonPath);
// Construct HTML Body
$html = '<!DOCTYPE html><html lang="de"><head><meta charset="UTF-8"><title>Angebot</title><style>body { font-family: Arial, sans-serif; color: #333; }</style></head><body style="margin:0;padding:20px;background-color:#f3f4f6;">';
$html .= '<div style="background-color:#fff;padding:20px;border-radius:8px;max-width:600px;margin:0 auto;box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);">';
// Logos
$html .= '<div style="text-align:center;margin-bottom:20px;border-bottom: 1px solid #e5e7eb;padding-bottom: 15px;">';
if ($logoToolExists) $html .= '<img src="cid:logo_thetool" alt="The Tool" style="height:40px;margin-right:15px;vertical-align:middle;">';
if ($logoXinonExists) $html .= '<img src="cid:logo_xinon" alt="Xinon" style="height:40px;vertical-align:middle;">';
$html .= '</div>';
$html .= '<h2 style="color:#00558c;text-align:center;font-size:20px;margin-bottom:20px;">' . htmlspecialchars($subject) . '</h2>';
$html .= '<div style="font-size:14px;line-height:1.6;color:#333;">';
$html .= nl2br(htmlspecialchars($bodyText));
$html .= '</div>';
$html .= '<br><div style="border-top:1px solid #eee;padding-top:20px;font-size:12px;color:#999;text-align:center;">';
$html .= 'XINON GmbH | <a href="https://www.xinon.at" style="color:#00558c;text-decoration:none;">www.xinon.at</a>';
$html .= '</div></div></body></html>';
$mail = new PHPMailer(true);
try {
// Server settings
$mail->isSMTP();
$mail->Host = TT_PIPEWORK_SMTP_HOST;
$mail->SMTPAuth = true;
$mail->Username = TT_PIPEWORK_SMTP_USER;
$mail->Password = TT_PIPEWORK_SMTP_PASS;
$mail->CharSet = PHPMailer::CHARSET_UTF8;
$mail->Encoding = 'base64';
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$mail->Port = 587;
// Logos
if ($logoToolExists) $mail->addEmbeddedImage($logoToolPath, 'logo_thetool');
if ($logoXinonExists) $mail->addEmbeddedImage($logoXinonPath, 'logo_xinon');
$mail->addReplyTo('backoffice@xinon.at', 'XINON Backoffice');
$mail->setFrom('thetool@xinon.at', 'XINON TheTool');
// set replyto to backoffice@xinon.at
$mail->addAddress($recipientEmail, $offer->contactPerson ?? $offer->customerName);
$mail->Subject = ($subject);
$mail->isHTML(true);
$mail->Body = $html;
$mail->AltBody = strip_tags($bodyText);
$mail->addStringAttachment($pdfContent, $offer->offerNumber . '_Angebot.pdf', 'base64', 'application/pdf');
$mail->send();
if ($mail->send()) {
// Update offer status and last sent date
WarehouseOfferModel::update($id, ['status' => 'sent', 'lastSentDate' => time()]);
$WarehouseOffer = (array) WarehouseOfferModel::get($id);
$WarehouseOffer['status'] = 'sent';
$WarehouseOffer['lastSentDate'] = time();
WarehouseOfferModel::update($WarehouseOffer);
// Add Journal Entry
WarehouseOfferJournalModel::create([
@@ -228,7 +287,7 @@ class WarehouseOfferController extends TTCrud
]);
self::returnJson(['success' => true, 'message' => 'E-Mail erfolgreich versendet.']);
} else {
} catch (Exception $e) {
self::sendError('E-Mail konnte nicht gesendet werden. Fehler: ' . $mail->ErrorInfo);
}
}
@@ -377,6 +436,7 @@ class WarehouseOfferController extends TTCrud
"includeTax" => true,
"vatRate" => 0.20,
"offerText" => $offerData->notes ?? '',
"validity" => $offerData->validity ?? 14,
"closingText" => $offerData->closingText ?? '',
"bank_iban" => TT_INVOICE_BANK_IBAN,
"bank_bic" => TT_INVOICE_BANK_BIC,
@@ -24,6 +24,7 @@ class WarehouseOfferModel extends TTCrudBaseModel {
public string $closingText;
public string $notes;
public string $status;
public ?int $validity; // New field
public ?int $lastSentDate; // New field
public float $totalAmount;
public int $create;
@@ -2,46 +2,378 @@
class WarehouseProjectController extends TTCrud {
protected string $headerTitle = 'Projekte';
protected string $createText = 'Neues Projekt erstellen';
protected string $singleText = 'Projekt';
protected bool $createText = false;
//@formatter:off
protected array $columns = [
['key' => 'title', 'text' => 'Titel', 'required' => true],
['key' => 'description', 'text' => 'Projektbeschreibung', 'modal' => ['type' => 'textarea']],
['key' => 'startDate', 'text' => 'Startdatum', 'required' => true, 'modal' => ['type' => 'datepicker']],
['key' => 'endDate', 'text' => 'Enddatum', 'required' => true, 'modal' => ['type' => 'datepicker']],
['key' => 'positions', 'text' => 'Positionen', 'required' => true, 'modal' => ['type' => 'positions-manager', 'config' => [
'header' => 'Positionen',
'fields' => [
'articleId' => ['apiUrl' => '/WarehouseArticle/autoComplete','type' => 'autocomplete','customFieldReference' => 'WarehouseArticle','label' => 'Artikel'],
'amount' => ['type' => 'input', 'label' => 'Menge', 'inputType' => 'number'],
'purpose' => ['type' => 'input', 'label' => 'Zweck'],
],
'validateFormOptions' => [
['key' => 'articleId', 'message' => 'Bitte füllen Sie den Artikel aus'],
['key' => 'amount', 'message' => 'Bitte füllen Sie die Menge aus'],
['key' => 'purpose', 'message' => 'Bitte füllen Sie den Zweck aus'],
],
]], 'table' => false],
['key' => 'linkedOrderIds', 'text' => 'Verlinkte Bestellung', 'modal' => false],
//
['key' => 'assignedPersons', 'text' => 'Zugewiesene Personen', 'modal' => ['type' => 'positions-manager', 'config' => [
'header' => 'Zugewiesene Personen',
'fields' => [
'userId' => ['apiUrl' => '/WarehouseShippingNote/userAutoComplete','type' => 'autocomplete','label' => 'Person','customFieldReference' => 'User']
],
'validateFormOptions' => [
['key' => 'userId', 'message' => 'Bitte füllen Sie die Person aus'],
],
]], 'table' => false],
['key' => 'storageLocation', 'text' => 'Lagerort', 'modal' => ['type' => 'input']],
['key' => 'note', 'text' => 'Notiz', 'modal' => ['type' => 'textarea']],
['key' => 'createBy', 'text' => 'Erstellt von', 'required' => true, 'modal' => ['visible' => false, 'type' => 'select'], 'table' => ['filter' => 'select']],
['key' => 'create', 'text' => 'Erstellt', 'required' => true, 'modal' => false],
['key' => 'id', 'text' => 'ID', 'table' => false, 'modal' => false],
['key' => 'projectNumber', 'text' => 'Projekt-Nr.', 'required' => false, 'modal' => ['disabled' => true, 'placeholder' => 'Wird automatisch generiert']],
['key' => 'title', 'text' => 'Bezeichnung', 'required' => true],
['key' => 'status', 'text' => 'Status', 'required' => true, 'modal' => ['type' => 'select', 'items' => [
['value' => 'new', 'text' => 'Neu'],
['value' => 'wip', 'text' => 'In Bearbeitung'],
['value' => 'finished', 'text' => 'Abgeschlossen'],
['value' => 'cancelled', 'text' => 'Storniert'],
]], 'table' => ['filter' => 'select']],
['key' => 'startDate', 'text' => 'Startdatum', 'required' => true, 'modal' => ['type' => 'date'], 'table' => ['filter' => 'date']],
['key' => 'endDate', 'text' => 'Enddatum', 'required' => true, 'modal' => ['type' => 'date'], 'table' => ['filter' => 'date']],
['key' => 'financials', 'text' => 'Gesamtsumme', 'required' => false, 'modal' => ['disabled' => true], 'table' => ['formatter' => 'formatPrice']],
['key' => 'storageLocation', 'text' => 'Lagerort', 'required' => false],
['key' => 'externalTeam', 'text' => 'Externes Team', 'required' => false, 'modal' => ['type' => 'textarea'], 'table' => false],
['key' => 'description', 'text' => 'Beschreibung', 'required' => false, 'modal' => ['type' => 'textarea'], 'table' => false],
['key' => 'create', 'text' => 'Erstellt', 'required' => false, 'modal' => false, 'table' => ['formatter' => 'formatDate']],
['key' => 'actions', 'text' => 'Aktionen', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center']],
];
//@formatter:on
}
protected array $permissionCheck = ['WarehouseUser'];
protected array $additionalJSVariables = ['WAREHOUSE_ADMIN' => false];
protected function prepareCrudConfig(): void {
if ($this->user->can('WarehouseAdmin')) {
$this->additionalJSVariables['WAREHOUSE_ADMIN'] = true;
}
}
private array $tempInternalTeam = [];
protected function beforeCreate($postData): bool {
$json = json_decode(file_get_contents('php://input'), true);
if ($json) {
$this->postData = array_merge($this->postData ?? [], $json);
}
if (isset($this->postData['internalTeam'])) {
$this->tempInternalTeam = $this->postData['internalTeam'];
unset($this->postData['internalTeam']);
}
$this->postData['projectNumber'] = WarehouseProjectModel::getNextProjectNumber();
// Ensure defaults if not provided
if (!isset($this->postData['status'])) $this->postData['status'] = 'new';
if (!isset($this->postData['financials'])) $this->postData['financials'] = 0.00;
return true;
}
protected function afterCreate($id, $postData): void
{
WarehouseProjectJournalModel::create([
'projectId' => $id,
'text' => 'Projekt erstellt.',
'createBy' => $this->user->id,
'create' => time()
]);
// Handle initial Internal Team
if (!empty($this->tempInternalTeam) && is_array($this->tempInternalTeam)) {
foreach ($this->tempInternalTeam as $userId) {
WarehouseProjectMemberModel::create([
'projectId' => $id,
'userId' => $userId,
'create' => time()
]);
$u = UserModel::getOne($userId);
$this->logJournal($id, "Teammitglied initial hinzugefügt: " . ($u ? $u->name : $userId));
}
}
}
protected function afterUpdate($id, $postData): void
{
// Simple journaling of main record update
WarehouseProjectJournalModel::create([
'projectId' => $id,
'text' => 'Projektstammdaten aktualisiert.',
'createBy' => $this->user->id,
'create' => time()
]);
}
// --- API for Vue ---
public function getProjectDetailsAction() {
$id = $this->request->id;
if (!$id) self::sendError("Projekt ID fehlt");
$project = WarehouseProjectModel::get($id);
if (!$project) self::sendError("Projekt nicht gefunden");
self::returnJson(['project' => $project]);
}
public function getTasksAction() {
$projectId = $this->request->id;
if (!$projectId) self::sendError("Projekt ID fehlt");
$tasks = WarehouseProjectTaskModel::getAll(['projectId' => $projectId], null, 0, ['key' => 'order', 'order' => 'ASC']);
foreach ($tasks as $task) {
if ($task->assignedUserId) {
$user = UserModel::getOne($task->assignedUserId);
$task->assignedUserName = $user ? $user->name : 'Unbekannt';
} else {
$task->assignedUserName = null;
}
}
self::returnJson($tasks);
}
public function saveTaskAction() {
$data = json_decode(file_get_contents('php://input'), true);
$projectId = $data['projectId'] ?? null;
if (!$projectId) self::sendError("Projekt ID fehlt");
$taskData = [
'projectId' => $projectId,
'title' => $data['title'],
'description' => $data['description'] ?? '',
'status' => $data['status'] ?? 'todo',
'assignedUserId' => !empty($data['assignedUserId']) ? $data['assignedUserId'] : null,
'createBy' => $this->user->id,
'create' => time()
];
if (!empty($data['id'])) {
$existingTask = WarehouseProjectTaskModel::get($data['id']);
if (!$existingTask) self::sendError("Aufgabe nicht gefunden");
// Merge existing data with new data to ensure all required fields are present
$updatedData = array_merge((array)$existingTask, $taskData);
$updatedData['id'] = $data['id']; // Ensure ID is in the data for update
// update method expects an array with 'id' key for update.
WarehouseProjectTaskModel::update($updatedData);
$this->logJournal($projectId, "Aufgabe aktualisiert: {$data['title']}");
} else {
// Get max order to append
$count = WarehouseProjectTaskModel::count(['projectId' => $projectId]);
$taskData['order'] = $count + 1;
WarehouseProjectTaskModel::create($taskData);
$this->logJournal($projectId, "Aufgabe erstellt: {$data['title']}");
}
self::returnJson(['success' => true]);
}
public function updateTaskStatusAction() {
$data = json_decode(file_get_contents('php://input'), true);
if (empty($data['id']) || empty($data['status'])) self::sendError("Daten fehlen");
$task = WarehouseProjectTaskModel::get($data['id']);
if ($task) {
// Retrieve existing task data to preserve projectId and other required fields
$updatedData = (array)$task;
$updatedData['status'] = $data['status'];
// WarehouseProjectTaskModel::update expects an array with 'id' key for update.
WarehouseProjectTaskModel::update($updatedData);
$this->logJournal($task->projectId, "Aufgabenstatus '{$task->title}' geändert auf {$data['status']}");
}
self::returnJson(['success' => true]);
}
public function deleteTaskAction() {
$id = $this->request->id;
if (!$id) self::sendError("ID fehlt");
$task = WarehouseProjectTaskModel::get($id);
if ($task) {
WarehouseProjectTaskModel::delete($id);
$this->logJournal($task->projectId, "Aufgabe gelöscht: {$task->title}");
}
self::returnJson(['success' => true]);
}
public function getTeamAction() {
$projectId = $this->request->id;
if (!$projectId) self::sendError("ID fehlt");
$members = WarehouseProjectMemberModel::getAll(['projectId' => $projectId]);
$users = [];
foreach($members as $m) {
$u = UserModel::getOne($m->userId);
if ($u) {
$users[] = [
'memberId' => $m->id,
'userId' => $u->id,
'name' => $u->name,
'role' => $m->role
];
}
}
self::returnJson($users);
}
public function addTeamMemberAction() {
$data = json_decode(file_get_contents('php://input'), true);
if (empty($data['projectId']) || empty($data['userId'])) self::sendError("Daten fehlen");
$exists = WarehouseProjectMemberModel::count(['projectId' => $data['projectId'], 'userId' => $data['userId']]);
if ($exists > 0) self::sendError("Benutzer bereits im Team");
WarehouseProjectMemberModel::create([
'projectId' => $data['projectId'],
'userId' => $data['userId'],
'role' => $data['role'] ?? null,
'create' => time()
]);
$u = UserModel::getOne($data['userId']);
$this->logJournal($data['projectId'], "Teammitglied hinzugefügt: " . ($u ? $u->name : $data['userId']));
self::returnJson(['success' => true]);
}
public function removeTeamMemberAction() {
$id = $this->request->id;
$member = WarehouseProjectMemberModel::get($id);
if ($member) {
$u = UserModel::getOne($member->userId);
WarehouseProjectMemberModel::delete($id);
$this->logJournal($member->projectId, "Teammitglied entfernt: " . ($u ? $u->name : $member->userId));
}
self::returnJson(['success' => true]);
}
public function getAvailableOrderRequestsAction() {
// Return open requests (not done, not cancelled)
// You might want to filter out ones already linked to THIS project, but maybe not strictly necessary.
$requests = WarehouseOrderRequest::getAll([], 100, 0, ['key' => 'create', 'order' => 'DESC']);
$available = [];
foreach($requests as $r) {
if (!$r->done && !$r->cancelled) {
$available[] = [
'id' => $r->id,
'purpose' => $r->purpose,
'create' => $r->create
];
}
}
self::returnJson($available);
}
public function getLinkedOrdersAction() {
$projectId = $this->request->id;
if (!$projectId) self::sendError("ID fehlt");
$links = WarehouseProjectOrderRequestModel::getAll(['projectId' => $projectId]);
$result = [];
foreach($links as $l) {
$req = WarehouseOrderRequest::get($l->orderRequestId);
if ($req) {
$positions = json_decode($req->positions, true);
// Resolve actual Orders
$orders = [];
$ids = [];
if (!empty($req->linkedOrderIds)) {
// Check if it is JSON array
$decoded = json_decode($req->linkedOrderIds, true);
if (is_array($decoded)) {
$ids = $decoded;
} else {
// Fallback to comma separated
$ids = explode(',', $req->linkedOrderIds);
}
}
foreach($ids as $oid) {
$oid = trim($oid);
if (empty($oid)) continue;
$o = WarehouseOrderModel::get($oid);
if ($o) {
$orders[] = [
'id' => $o->id,
'orderNumber' => $o->orderNumber,
'status' => $o->status,
'distributorId' => $o->distributorId
];
}
}
$result[] = [
'linkId' => $l->id,
'requestId' => $req->id,
'purpose' => $req->purpose,
'create' => $req->create,
'status' => $req->done ? 'done' : ($req->cancelled ? 'cancelled' : 'open'),
'positionsCount' => is_array($positions) ? count($positions) : 0,
'orders' => $orders
];
}
}
self::returnJson($result);
}
public function linkOrderAction() {
$data = json_decode(file_get_contents('php://input'), true);
if (empty($data['projectId']) || empty($data['orderId'])) self::sendError("Daten fehlen");
$order = WarehouseOrderRequest::get($data['orderId']);
if (!$order) self::sendError("Bestellwunsch nicht gefunden");
WarehouseProjectOrderRequestModel::create([
'projectId' => $data['projectId'],
'orderRequestId' => $data['orderId'],
'create' => time()
]);
$this->logJournal($data['projectId'], "Bestellwunsch #{$data['orderId']} verknüpft.");
self::returnJson(['success' => true]);
}
public function unlinkOrderAction() {
$id = $this->request->id;
$link = WarehouseProjectOrderRequestModel::get($id);
if ($link) {
WarehouseProjectOrderRequestModel::delete($id);
$this->logJournal($link->projectId, "Bestellwunsch #{$link->orderRequestId} Verknüpfung aufgehoben.");
}
self::returnJson(['success' => true]);
}
public function createJournalEntryAction() {
$data = json_decode(file_get_contents('php://input'), true);
if (empty($data['projectId'])) self::sendError("Projekt ID fehlt");
$this->logJournal($data['projectId'], $data['message'] ?? '');
self::returnJson(['success' => true]);
}
public function getJournalAction() {
$projectId = $this->request->id;
$logs = WarehouseProjectJournalModel::getAll(['projectId' => $projectId], null, 0, ['order' => 'DESC', 'key' => 'create']);
foreach($logs as $log) {
$u = UserModel::getOne($log->createBy);
$log->userName = $u ? $u->name : 'System';
}
self::returnJson($logs);
}
private function logJournal($projectId, $text) {
WarehouseProjectJournalModel::create([
'projectId' => $projectId,
'text' => $text,
'createBy' => $this->user->id,
'create' => time()
]);
}
// Users for Team Selection
public function getUsersAction() {
$users = array_map(function($u) {
return ['id' => $u->id, 'name' => $u->name];
}, UserModel::search(['employee' => true]));
self::returnJson($users);
}
}
@@ -2,14 +2,35 @@
class WarehouseProjectModel extends TTCrudBaseModel {
public int $id;
public string $projectNumber;
public string $title;
public string $description;
public string $startDate;
public string $endDate;
public ?string $description;
public ?int $startDate;
public ?int $endDate;
public string $status;
public string $priority;
public int $assignedTo;
public float $financials;
public ?string $storageLocation;
public ?string $externalTeam;
public ?int $createdFromOrderId;
public int $createBy;
public int $create;
public static function getNextProjectNumber(): string {
$year = date('Y');
$prefix = "XP-$year-";
$db = self::getDB();
$tableName = self::getFullyQualifiedTable();
$sql = "SELECT projectNumber FROM $tableName WHERE projectNumber LIKE '$prefix%' ORDER BY projectNumber DESC LIMIT 1";
$result = $db->query($sql);
$nextNum = 1;
if ($row = $result->fetch_assoc()) {
$lastNumStr = substr($row['projectNumber'], strrpos($row['projectNumber'], '-') + 1);
$nextNum = intval($lastNumStr) + 1;
}
return $prefix . str_pad((string)$nextNum, 4, '0', STR_PAD_LEFT);
}
}
@@ -0,0 +1,10 @@
<?php
class WarehouseProjectJournalModel extends TTCrudBaseModel {
public int $id;
public int $projectId;
public ?string $text;
public ?string $data; // json
public int $createBy;
public int $create;
}
@@ -0,0 +1,9 @@
<?php
class WarehouseProjectMemberModel extends TTCrudBaseModel {
public int $id;
public int $projectId;
public int $userId;
public ?string $role;
public int $create;
}
@@ -0,0 +1,8 @@
<?php
class WarehouseProjectOrderRequestModel extends TTCrudBaseModel {
public int $id;
public int $projectId;
public int $orderRequestId;
public int $create;
}
@@ -0,0 +1,14 @@
<?php
class WarehouseProjectTaskModel extends TTCrudBaseModel {
public int $id;
public int $projectId;
public ?int $parentTaskId;
public string $title;
public ?string $description;
public string $status; // todo, in_progress, done
public ?int $assignedUserId;
public int $order;
public int $createBy;
public int $create;
}