From 09ffd911fc332947c29a1dc073472f2e487843f2 Mon Sep 17 00:00:00 2001 From: Luca Haid Date: Tue, 2 Dec 2025 14:46:22 +0000 Subject: [PATCH] Preorder/add new filter --- Layout/default/Preorder/Index.php | 10 + Layout/default/WarehouseOffer/PDF_MAIN.php | 7 +- Layout/default/menu.php | 6 +- application/Preorder/PreorderModel.php | 6 +- .../WarehouseOfferController.php | 80 +- .../WarehouseOffer/WarehouseOfferModel.php | 1 + .../WarehouseProjectController.php | 404 +++++++++- .../WarehouseProjectModel.php | 33 +- .../WarehouseProjectJournalModel.php | 10 + .../WarehouseProjectMemberModel.php | 9 + .../WarehouseProjectOrderRequestModel.php | 8 + .../WarehouseProjectTaskModel.php | 14 + ...125100000_warehouse_offer_add_validity.php | 33 + ...0251125110000_create_warehouse_project.php | 62 ++ ...20000_create_warehouse_project_journal.php | 21 + .../WarehouseBasicOfferModal.js | 2 +- .../WarehouseOffer/WarehouseOfferModal.js | 3 +- .../WarehouseProject/WarehouseProject.js | 696 +++++++++++++++++- .../WarehouseProject/WarehouseProjectModal.js | 146 ++++ public/plugins/vue/tt-components/tt-table.js | 1 + 20 files changed, 1486 insertions(+), 66 deletions(-) create mode 100644 application/WarehouseProjectJournal/WarehouseProjectJournalModel.php create mode 100644 application/WarehouseProjectMember/WarehouseProjectMemberModel.php create mode 100644 application/WarehouseProjectOrderRequest/WarehouseProjectOrderRequestModel.php create mode 100644 application/WarehouseProjectTask/WarehouseProjectTaskModel.php create mode 100644 db/migrations/20251125100000_warehouse_offer_add_validity.php create mode 100644 db/migrations/20251125110000_create_warehouse_project.php create mode 100644 db/migrations/20251125120000_create_warehouse_project_journal.php create mode 100644 public/js/pages/WarehouseProject/WarehouseProjectModal.js diff --git a/Layout/default/Preorder/Index.php b/Layout/default/Preorder/Index.php index d97168da2..10367c511 100644 --- a/Layout/default/Preorder/Index.php +++ b/Layout/default/Preorder/Index.php @@ -856,6 +856,16 @@ $pagination_entity_name = "Vorbestellungen"; +
+ + +
+
diff --git a/Layout/default/WarehouseOffer/PDF_MAIN.php b/Layout/default/WarehouseOffer/PDF_MAIN.php index 365cd1dfc..88888077d 100644 --- a/Layout/default/WarehouseOffer/PDF_MAIN.php +++ b/Layout/default/WarehouseOffer/PDF_MAIN.php @@ -61,7 +61,8 @@ if ($includeTax) { } $formattedOfferDate = date("d.m.Y", $offerDate); -$formattedValidUntil = date("d.m.Y", strtotime("+14 days", $offerDate)); +$validityDays = isset($validity) ? (int)$validity : 14; +$formattedValidUntil = date("d.m.Y", strtotime("+$validityDays days", $offerDate)); ?> @@ -116,7 +117,7 @@ $formattedValidUntil = date("d.m.Y", strtotime("+14 days", $offerDate)); - + @@ -173,7 +174,7 @@ $formattedValidUntil = date("d.m.Y", strtotime("+14 days", $offerDate)); - + diff --git a/Layout/default/menu.php b/Layout/default/menu.php index 4169b5266..afd636c90 100644 --- a/Layout/default/menu.php +++ b/Layout/default/menu.php @@ -179,9 +179,9 @@ can("WarehouseAdmin")): ?>can("WarehouseAdmin")): ?>can("WarehouseAdmin")): ?>
  • "> Bestellungen
  • - can("WarehouseAdmin")): ?>
  • "> Angebote
  • can("WarehouseAdmin") || $me->can("WarehouseUser")): ?>
  • "> Bestellwünsche
  • can("WarehouseAdmin") || $me->can("WarehouseUser")): ?>
  • "> Lieferscheine
  • + can("WarehouseAdmin") || $me->can("WarehouseUser")): ?>
  • "> Projekte
  • can("WarehouseAdmin")): ?>
  • "> Administration
  • @@ -209,7 +209,7 @@ - is(["Admin","netowner","salespartner"]) || $me->can(["Order", "Preorder"])): ?> + is(["Admin","netowner","salespartner"]) || $me->can(["Order", "Preorder", "WarehouseAdmin"])): ?>
  • Verkauf
    @@ -220,6 +220,8 @@
  • "> RIMO Typen-Karte
  • + can("WarehouseAdmin")): ?>
  • "> Angebote
  • + is(["Admin","salespartner"]) && $me->can("Order")): ?>
  • "> Bestellungen
  • diff --git a/application/Preorder/PreorderModel.php b/application/Preorder/PreorderModel.php index 143cf91ea..67485c017 100644 --- a/application/Preorder/PreorderModel.php +++ b/application/Preorder/PreorderModel.php @@ -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 "; } } } diff --git a/application/WarehouseOffer/WarehouseOfferController.php b/application/WarehouseOffer/WarehouseOfferController.php index 39211f47d..18f76c01a 100644 --- a/application/WarehouseOffer/WarehouseOfferController.php +++ b/application/WarehouseOffer/WarehouseOfferController.php @@ -1,5 +1,8 @@ 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 = 'Angebot'; + $html .= '
    '; + + // Logos + $html .= '
    '; + if ($logoToolExists) $html .= 'The Tool'; + if ($logoXinonExists) $html .= 'Xinon'; + $html .= '
    '; + + $html .= '

    ' . htmlspecialchars($subject) . '

    '; + $html .= '
    '; + $html .= nl2br(htmlspecialchars($bodyText)); + $html .= '
    '; + + $html .= '
    '; + $html .= 'XINON GmbH | www.xinon.at'; + $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, diff --git a/application/WarehouseOffer/WarehouseOfferModel.php b/application/WarehouseOffer/WarehouseOfferModel.php index 239e66d03..62d06c748 100644 --- a/application/WarehouseOffer/WarehouseOfferModel.php +++ b/application/WarehouseOffer/WarehouseOfferModel.php @@ -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; diff --git a/application/WarehouseProject/WarehouseProjectController.php b/application/WarehouseProject/WarehouseProjectController.php index 7da44ce9d..47a4ed463 100644 --- a/application/WarehouseProject/WarehouseProjectController.php +++ b/application/WarehouseProject/WarehouseProjectController.php @@ -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 -} \ No newline at end of file + + 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); + } +} diff --git a/application/WarehouseProject/WarehouseProjectModel.php b/application/WarehouseProject/WarehouseProjectModel.php index a1aa8f680..42bb92e2d 100644 --- a/application/WarehouseProject/WarehouseProjectModel.php +++ b/application/WarehouseProject/WarehouseProjectModel.php @@ -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); + } } \ No newline at end of file diff --git a/application/WarehouseProjectJournal/WarehouseProjectJournalModel.php b/application/WarehouseProjectJournal/WarehouseProjectJournalModel.php new file mode 100644 index 000000000..d747af1ec --- /dev/null +++ b/application/WarehouseProjectJournal/WarehouseProjectJournalModel.php @@ -0,0 +1,10 @@ +getEnvironment() == "thetool") { + $table = $this->table('WarehouseOffer'); + if (!$table->hasColumn('validity')) { + $table->addColumn('validity', 'integer', [ + 'null' => true, + 'default' => null, + 'after' => 'status', + ]); + $table->update(); + } + } + } + + public function down(): void + { + if ($this->getEnvironment() == "thetool") { + $table = $this->table('WarehouseOffer'); + if ($table->hasColumn('validity')) { + $table->removeColumn('validity'); + $table->update(); + } + } + } +} diff --git a/db/migrations/20251125110000_create_warehouse_project.php b/db/migrations/20251125110000_create_warehouse_project.php new file mode 100644 index 000000000..c8f4c0508 --- /dev/null +++ b/db/migrations/20251125110000_create_warehouse_project.php @@ -0,0 +1,62 @@ +getEnvironment() == "thetool") { + // Main Project Table + $projects = $this->table('WarehouseProject'); + $projects->addColumn('projectNumber', 'string', ['limit' => 20, 'comment' => 'XP-YYYY-NNNN']) + ->addColumn('title', 'string', ['limit' => 255]) + ->addColumn('description', 'text', ['null' => true]) + ->addColumn('startDate', 'integer', ['null' => true]) + ->addColumn('endDate', 'integer', ['null' => true]) + ->addColumn('status', 'enum', ['values' => ['new', 'wip', 'finished', 'cancelled'], 'default' => 'new']) + ->addColumn('financials', 'decimal', ['precision' => 10, 'scale' => 2, 'default' => 0.00]) + ->addColumn('storageLocation', 'string', ['limit' => 255, 'null' => true]) + ->addColumn('externalTeam', 'text', ['null' => true, 'comment' => 'Free text for external participants']) + ->addColumn('createdFromOrderId', 'integer', ['null' => true]) + ->addColumn('createBy', 'integer') + ->addColumn('create', 'integer') + ->addIndex(['projectNumber'], ['unique' => true]) + ->create(); + + // Tasks Table + $tasks = $this->table('WarehouseProjectTask'); + $tasks->addColumn('projectId', 'integer') + ->addColumn('parentTaskId', 'integer', ['null' => true]) + ->addColumn('title', 'string', ['limit' => 255]) + ->addColumn('description', 'text', ['null' => true]) + ->addColumn('status', 'enum', ['values' => ['todo', 'in_progress', 'done'], 'default' => 'todo']) + ->addColumn('assignedUserId', 'integer', ['null' => true]) + ->addColumn('order', 'integer', ['default' => 0]) + ->addColumn('createBy', 'integer') + ->addColumn('create', 'integer') + ->addForeignKey('projectId', 'WarehouseProject', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE']) + ->create(); + + // Project Members (Internal Team) + $members = $this->table('WarehouseProjectMember'); + $members->addColumn('projectId', 'integer') + ->addColumn('userId', 'integer') + ->addColumn('role', 'string', ['limit' => 50, 'null' => true]) + ->addColumn('create', 'integer') + ->addForeignKey('projectId', 'WarehouseProject', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE']) + ->create(); + + // Link table for Order Requests + $orderRequests = $this->table('WarehouseProjectOrderRequest'); + $orderRequests->addColumn('projectId', 'integer') + ->addColumn('orderRequestId', 'integer') + ->addColumn('create', 'integer') + ->addForeignKey('projectId', 'WarehouseProject', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE']) + // Assuming WarehouseOrderRequest table exists based on controller analysis + ->addForeignKey('orderRequestId', 'WarehouseOrderRequest', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE']) + ->create(); + } + } +} diff --git a/db/migrations/20251125120000_create_warehouse_project_journal.php b/db/migrations/20251125120000_create_warehouse_project_journal.php new file mode 100644 index 000000000..cb2ebfc1d --- /dev/null +++ b/db/migrations/20251125120000_create_warehouse_project_journal.php @@ -0,0 +1,21 @@ +getEnvironment() == "thetool") { + $table = $this->table('WarehouseProjectJournal'); + $table->addColumn('projectId', 'integer') + ->addColumn('text', 'text', ['null' => true]) + ->addColumn('data', 'json', ['null' => true, 'comment' => 'Stores changes/diffs']) + ->addColumn('createBy', 'integer') + ->addColumn('create', 'integer') + ->addForeignKey('projectId', 'WarehouseProject', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE']) + ->create(); + } + } +} diff --git a/public/js/pages/WarehouseOffer/WarehouseBasicOfferModal.js b/public/js/pages/WarehouseOffer/WarehouseBasicOfferModal.js index 0559f8157..ed4f25c40 100644 --- a/public/js/pages/WarehouseOffer/WarehouseBasicOfferModal.js +++ b/public/js/pages/WarehouseOffer/WarehouseBasicOfferModal.js @@ -54,7 +54,7 @@ Vue.component('warehouse-offer-create-basic-offer-modal', { totalDiscount: 0, paymentTerms: 'net30', deliveryTerms: 'ex_works', - closingText: 'Sollten sich die Rohstoffpreise bzw. die Preise unserer Zulieferer um mehr als 10% innerhalb der Angebots bzw.\n\nAuftragsgültigkeit erhöhen (Stichtag Datum), sind wir gezwungen die Preise anzupassen.\n\nDiese Angebot hat eine Gültigkeit von 4 Wochen.\n\nVerrechnung erfolgt nach tatsächlichem Aufwand.\n\nWir sind sicher, Ihnen ein konkurenzfähiges Angebot unterbreitet zu haben und sehen gern Ihrer Bestellung entgegen.\n\nSollten Sie noch Fragen oder weitere Informationen benötigen stehen wir Ihnen jederzeit gern zu Verfügung.', + closingText: 'Sollten sich die Rohstoffpreise bzw. die Preise unserer Zulieferer um mehr als 10% innerhalb der Angebots bzw.\n\nAuftragsgültigkeit erhöhen (Stichtag Datum), sind wir gezwungen die Preise anzupassen.\n\nVerrechnung erfolgt nach tatsächlichem Aufwand.\n\nWir sind sicher, Ihnen ein konkurenzfähiges Angebot unterbreitet zu haben und sehen gern Ihrer Bestellung entgegen.\n\nSollten Sie noch Fragen oder weitere Informationen benötigen stehen wir Ihnen jederzeit gern zu Verfügung.', notes: '', }, diff --git a/public/js/pages/WarehouseOffer/WarehouseOfferModal.js b/public/js/pages/WarehouseOffer/WarehouseOfferModal.js index 3a41899f5..5612e994e 100644 --- a/public/js/pages/WarehouseOffer/WarehouseOfferModal.js +++ b/public/js/pages/WarehouseOffer/WarehouseOfferModal.js @@ -147,8 +147,7 @@ Vue.component('warehouse-offer-modal', { totalDiscount: 0, paymentTerms: 'net30', deliveryTerms: 'ex_works', - closingText: 'Sollten sich die Rohstoffpreise bzw. die Preise unserer Zulieferer um mehr als 10% innerhalb der Angebots bzw.\nAuftragsgültigkeit erhöhen (Stichtag Datum), sind wir gezwungen die Preise anzupassen.\nDiese Angebot hat eine Gültigkeit von 4 Wochen.\nVerrechnung erfolgt nach tatsächlichem Aufwand.\nWir sind sicher, Ihnen ein konkurenzfähiges Angebot unterbreitet zu haben und sehen gern Ihrer Bestellung entgegen.\nSollten Sie noch Fragen oder weitere Informationen benötigen stehen wir Ihnen jederzeit gern zu Verfügung.', - notes: '', + closingText: 'Sollten sich die Rohstoffpreise bzw. die Preise unserer Zulieferer um mehr als 10% innerhalb der Angebots bzw.\nAuftragsgültigkeit erhöhen (Stichtag Datum), sind wir gezwungen die Preise anzupassen.\nVerrechnung erfolgt nach tatsächlichem Aufwand.\nWir sind sicher, Ihnen ein konkurenzfähiges Angebot unterbreitet zu haben und sehen gern Ihrer Bestellung entgegen.\nSollten Sie noch Fragen oder weitere Informationen benötigen stehen wir Ihnen jederzeit gern zu Verfügung.', notes: '', }, templateName: '', ignoreFirstAddressChange: false, diff --git a/public/js/pages/WarehouseProject/WarehouseProject.js b/public/js/pages/WarehouseProject/WarehouseProject.js index b1bd519e7..5fd942de0 100644 --- a/public/js/pages/WarehouseProject/WarehouseProject.js +++ b/public/js/pages/WarehouseProject/WarehouseProject.js @@ -1,11 +1,701 @@ Vue.component('warehouse-project', { template: ` - - +
    + +
    + + + + + + + +
    `, data() { - return {window: window} + return { + additionalActions: [], + projectModalId: null + }; }, + methods: { + openProjectModal(id) { + this.projectModalId = id; + }, + closeProjectModal() { + this.projectModalId = null; + this.$refs.table.$refs.table.refreshTable(); + } + } +}); + +Vue.component('warehouse-project-details', { + props: ['project'], + template: ` +
    + +
    +
    + +
    +
    + + +
    + +
    +
    +
    +
    +
    Projektdaten
    +
    +
    Status
    +
    + {{ statusText(project.status) }} +
    + +
    Projekt-Nr.
    +
    {{ project.projectNumber }}
    + +
    Zeitraum
    +
    + {{ formatDate(project.startDate) }} - {{ formatDate(project.endDate) }} +
    + +
    Lagerort
    +
    {{ project.storageLocation || '-' }}
    + +
    Budget
    +
    {{ formatPrice(project.financials) }}
    +
    +
    +
    +
    Details
    +

    Beschreibung

    +
    {{ project.description || 'Keine Beschreibung vorhanden.' }}
    + +

    Externes Team / Partner

    +
    {{ project.externalTeam || '-' }}
    +
    +
    +
    +
    + + +
    +
    +
    +
    + Fortschritt: +
    +
    + {{ taskProgress }}% +
    +
    +
    + +
    + +
    + +
    +
    +
    + Zu Erledigen + {{ tasksByStatus('todo').length }} +
    +
    +
    +
    +
    {{ task.title }}
    + +
    +

    {{ task.description }}

    +
    + {{ task.assignedUserName || 'Niemand' }} +
    +
    +
    +
    Keine Aufgaben
    +
    +
    + + +
    +
    +
    + In Arbeit + {{ tasksByStatus('in_progress').length }} +
    +
    +
    +
    +
    {{ task.title }}
    + +
    +

    {{ task.description }}

    +
    + {{ task.assignedUserName || 'Niemand' }} +
    +
    +
    +
    +
    + + +
    +
    +
    + Erledigt + {{ tasksByStatus('done').length }} +
    +
    +
    +
    +
    {{ task.title }}
    + +
    +
    + Abgeschlossen +
    +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    Projektteam
    + {{ team.length }} Mitglieder +
    +
    +
    + +
    + +
    +
    + +
      +
    • +
      +
      + {{ member.name.charAt(0) }} +
      +
      +
      {{ member.name }}
      + {{ member.role || 'Projektmitarbeiter' }} +
      +
      + +
    • +
    • +
      + +

      Noch keine Teammitglieder zugewiesen.

      +
      +
    • +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    Logistik & Bestellungen
    + +
    + +
    +
    +
    +
    + + + + +
    + {{ request.requestId }} + | + {{ request.purpose }} +
    +
    +
    + Erledigt + Storniert + Offen + + + +
    +
    + +
    +
    +
    +
    Enthaltene Bestellungen
    +
    +
    +
    - vversion) ? $offer->version : 1 ?>
    + + + + + + + + + + + + + + +
    Order Nr.StatusAktion
    {{ order.orderNumber }} + {{ order.status || 'Unbekannt' }} + + Öffnen +
    +
    + +
    + Keine direkten Bestellungen verknüpft oder Daten noch nicht verfügbar. +
    + + + + + +
    + +

    Keine verknüpften Bestellwünsche.

    + +
    + + + + + +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    {{ new Date(log.create * 1000).toLocaleDateString('de-DE', {day: '2-digit', month: '2-digit'}) }}
    +
    {{ new Date(log.create * 1000).toLocaleTimeString('de-DE', {hour: '2-digit', minute: '2-digit'}) }}
    +
    +
    +
    + {{ log.userName }} +
    +
    {{ log.text }}
    +
    +
    +
    + +

    Noch keine Journal-Einträge.

    +
    +
    +
    +
    +
    +
    +
    Dokumente
    +
    +
    + +

    Dateien hierher ziehen

    +

    (WIP: Upload Funktion)

    +
    +
    +
    +
    +
    +
    + + + + + + + + + `, + data() { + return { + activeTab: 'overview', + tasks: [], + team: [], + linkedOrders: [], + journal: [], + allUsers: [], + availableOrders: [], + selectedUserId: '', + newJournalMessage: '', + currentTask: { + id: null, + title: '', + description: '', + assignedUserId: '', + status: 'todo' + } + }; + }, + computed: { + taskProgress() { + if (!this.tasks || !this.tasks.length) return 0; + const done = this.tasks.filter(t => t.status === 'done').length; + return Math.round((done / this.tasks.length) * 100); + }, + allUsersSelectItems() { + return this.allUsers.map(u => ({ value: u.id, text: u.name })); + } + }, + mounted() { + this.loadData(); + }, + methods: { + async loadData() { + try { + await Promise.all([ + this.fetchTasks(), + this.fetchTeam(), + this.fetchLinkedOrders(), + this.fetchJournal(), + this.fetchUsers() + ]); + } catch (e) { + console.error("Error loading project data", e); + } + }, + async fetchTasks() { + try { + const res = await axios.get('/WarehouseProject/getTasks?id=' + this.project.id); + this.tasks = Array.isArray(res.data) ? res.data : []; + } catch(e) { + this.tasks = []; + } + }, + async fetchTeam() { + try { + const res = await axios.get('/WarehouseProject/getTeam?id=' + this.project.id); + this.team = Array.isArray(res.data) ? res.data : []; + } catch(e) { + this.team = []; + } + }, + async fetchLinkedOrders() { + try { + const res = await axios.get('/WarehouseProject/getLinkedOrders?id=' + this.project.id); + this.linkedOrders = Array.isArray(res.data) ? res.data : []; + } catch(e) { + this.linkedOrders = []; + } + }, + async fetchJournal() { + try { + const res = await axios.get('/WarehouseProject/getJournal?id=' + this.project.id); + this.journal = Array.isArray(res.data) ? res.data : []; + } catch(e) { + this.journal = []; + } + }, + async fetchUsers() { + try { + const res = await axios.get('/WarehouseProject/getUsers'); + this.allUsers = Array.isArray(res.data) ? res.data : []; + } catch(e) { + this.allUsers = []; + } + }, + async fetchAvailableOrders() { + try { + const res = await axios.get('/WarehouseProject/getAvailableOrderRequests'); + this.availableOrders = Array.isArray(res.data) ? res.data : []; + } catch(e) { + this.availableOrders = []; + } + }, + // Helpers + tasksByStatus(status) { + if (!this.tasks) return []; + return this.tasks.filter(t => t.status === status); + }, + statusLabel(status) { + const map = { todo: 'ToDo', in_progress: 'In Arbeit', done: 'Erledigt' }; + return map[status] || status; + }, + statusText(status) { + const map = { new: 'Neu', wip: 'In Bearbeitung', finished: 'Abgeschlossen', cancelled: 'Storniert' }; + return map[status] || status; + }, + statusBadgeClass(status) { + const map = { new: 'badge badge-primary', wip: 'badge badge-warning', finished: 'badge badge-success', cancelled: 'badge badge-danger' }; + return map[status] || 'badge badge-secondary'; + }, + formatDate(ts) { + if (!ts) return '-'; + return new Date(ts * 1000).toLocaleDateString('de-DE'); + }, + formatDateTime(ts) { + if (!ts) return '-'; + return new Date(ts * 1000).toLocaleString('de-DE'); + }, + formatPrice(val) { + return new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(val || 0); + }, + // Actions + openTaskModal(task = null) { + if (task) { + this.currentTask = { ...task }; + } else { + this.currentTask = { id: null, title: '', description: '', assignedUserId: '', status: 'todo' }; + } + $(this.$refs.taskModal).modal('show'); + }, + async saveTask() { + if (!this.currentTask.title) return alert("Titel erforderlich"); + + try { + await axios.post('/WarehouseProject/saveTask', { ...this.currentTask, projectId: this.project.id }); + $(this.$refs.taskModal).modal('hide'); + this.fetchTasks(); + this.fetchJournal(); + } catch(e) { + console.error(e); + alert("Fehler beim Speichern: " + (e.response?.data?.error || 'Unbekannt')); + } + }, + async updateTaskStatus(id, status) { + await axios.post('/WarehouseProject/updateTaskStatus', { id, status }); + this.fetchTasks(); + this.fetchJournal(); + }, + async deleteTask(id) { + if(!confirm("Wirklich löschen?")) return; + await axios.get('/WarehouseProject/deleteTask?id=' + id); + this.fetchTasks(); + this.fetchJournal(); + }, + async addMember() { + if (!this.selectedUserId) return; + try { + await axios.post('/WarehouseProject/addTeamMember', { projectId: this.project.id, userId: this.selectedUserId }); + this.fetchTeam(); + this.fetchJournal(); + this.selectedUserId = ''; + } catch (e) { + alert("Fehler beim Hinzufügen (evtl. bereits im Team?)"); + } + }, + async removeMember(id) { + await axios.get('/WarehouseProject/removeTeamMember?id=' + id); + this.fetchTeam(); + this.fetchJournal(); + }, + async openLinkOrderModal() { + await this.fetchAvailableOrders(); + $(this.$refs.linkOrderModal).modal('show'); + }, + async linkOrder(orderId) { + if (!orderId) return; + try { + await axios.post('/WarehouseProject/linkOrder', { projectId: this.project.id, orderId }); + $(this.$refs.linkOrderModal).modal('hide'); + this.fetchLinkedOrders(); + this.fetchJournal(); + } catch (e) { + alert("Fehler beim Verknüpfen"); + } + }, + async unlinkOrder(id) { + await axios.get('/WarehouseProject/unlinkOrder?id=' + id); + this.fetchLinkedOrders(); + this.fetchJournal(); + }, + async postJournal() { + if (!this.newJournalMessage.trim()) return; + await axios.post('/WarehouseProject/createJournalEntry', { projectId: this.project.id, message: this.newJournalMessage }); + this.newJournalMessage = ''; + this.fetchJournal(); + } + } }); diff --git a/public/js/pages/WarehouseProject/WarehouseProjectModal.js b/public/js/pages/WarehouseProject/WarehouseProjectModal.js new file mode 100644 index 000000000..d3e11e6b4 --- /dev/null +++ b/public/js/pages/WarehouseProject/WarehouseProjectModal.js @@ -0,0 +1,146 @@ +Vue.component('warehouse-project-modal', { + props: { + id: { type: [String, Number], required: true }, + }, + data() { + return { + window: window, + loading: false, + allUsers: [], + project: { + title: '', + description: '', + startDate: null, + endDate: null, + storageLocation: '', + externalTeam: '', + internalTeam: [], // Array of user IDs + status: 'new', + financials: 0.00 + }, + statusOptions: [ + { text: 'Neu', value: 'new' }, + { text: 'In Bearbeitung', value: 'wip' }, + { text: 'Abgeschlossen', value: 'finished' }, + { text: 'Storniert', value: 'cancelled' } + ] + }; + }, + //language=Vue + template: ` + +
    + +
    +
    + +
    +
    + +
    +
    + + + +
    + +
    + +
    +
    + + +
    + +
    + + Strg+Klick für Mehrfachauswahl +
    +
    + + + + +
    + +
    + + Wird automatisch berechnet. +
    +
    +
    +
    + `, + async mounted() { + this.fetchUsers(); + if (this.id !== 'create') { + this.loading = true; + try { + const { data } = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseProject/getById`, { params: { id: this.id } }); + if (data) { + this.project = { ...this.project, ...data }; + // Ensure dates are in YYYY-MM-DD format for input type="date" + if (this.project.startDate) this.project.startDate = this.formatDateForInput(this.project.startDate); + if (this.project.endDate) this.project.endDate = this.formatDateForInput(this.project.endDate); + } + } catch (e) { + console.error(e); + window.notify('error', 'Projekt konnte nicht geladen werden.'); + } finally { + this.loading = false; + } + } + }, + methods: { + async fetchUsers() { + try { + const res = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseProject/getUsers`); + this.allUsers = res.data; + } catch(e) { console.error(e); } + }, + formatDateForInput(timestamp) { + if (!timestamp) return null; + return new Date(timestamp * 1000).toISOString().split('T')[0]; + }, + formatPrice(val) { + return new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(val || 0); + }, + async submit() { + if (!this.project.title || !this.project.startDate || !this.project.endDate) { + return window.notify('error', 'Bitte füllen Sie alle Pflichtfelder aus (*).'); + } + + this.loading = true; + try { + const payload = { ...this.project }; + if (payload.startDate) payload.startDate = Math.floor(new Date(payload.startDate).getTime() / 1000); + if (payload.endDate) payload.endDate = Math.floor(new Date(payload.endDate).getTime() / 1000); + + const url = `${window.TT_CONFIG["BASE_PATH"]}/WarehouseProject/${this.id === 'create' ? 'create' : 'update'}`; + const response = await axios.post(url, payload); + + if (response.data.success) { + this.$emit('close'); + window.notify('success', response.data.message || 'Gespeichert.'); + } else { + window.notify('error', response.data.message || 'Fehler beim Speichern.'); + } + } catch (e) { + console.error(e); + window.notify('error', 'Ein Systemfehler ist aufgetreten.'); + } finally { + this.loading = false; + } + } + } +}); diff --git a/public/plugins/vue/tt-components/tt-table.js b/public/plugins/vue/tt-components/tt-table.js index 3580a29f2..b50dc6fd8 100644 --- a/public/plugins/vue/tt-components/tt-table.js +++ b/public/plugins/vue/tt-components/tt-table.js @@ -364,6 +364,7 @@ Vue.component('tt-table', { const fetchTimestamp = Date.now(); this.latestFetchTimestamp = fetchTimestamp; + console.log(this.fetchUrl); const response = await axios.post(this.fetchUrl, { pagination: { page: Math.max(page, 1), per_page: this.pagination?.per_page ? this.pagination.per_page : 10,