From 7e3b1af54ab349a4cb85c54eac8f135e2a06089e Mon Sep 17 00:00:00 2001 From: Luca Haid Date: Fri, 7 Mar 2025 08:04:36 +0100 Subject: [PATCH] added new warehouseorder features --- Layout/default/WarehouseOrder/PDF_MAIN.php | 12 +- .../WarehouseOrderController.php | 247 +++++++++++++----- .../WarehouseOrder/WarehouseOrderModel.php | 1 + .../WarehouseOrderRequestController.php | 48 ++-- .../WarehouseOrderRequestModel.php | 2 + .../WarehouseShippingNoteController.php | 7 +- composer.json | 3 +- .../20250307080000_warehouse_modify_14.php | 37 +++ .../pages/WarehouseOrder/WarehouseOrder.css | 53 ++++ .../js/pages/WarehouseOrder/WarehouseOrder.js | 140 ++++++++-- .../WarehouseOrderRequest.js | 29 +- 11 files changed, 460 insertions(+), 119 deletions(-) create mode 100644 db/migrations/20250307080000_warehouse_modify_14.php diff --git a/Layout/default/WarehouseOrder/PDF_MAIN.php b/Layout/default/WarehouseOrder/PDF_MAIN.php index 3c40a3c8c..08a341682 100644 --- a/Layout/default/WarehouseOrder/PDF_MAIN.php +++ b/Layout/default/WarehouseOrder/PDF_MAIN.php @@ -4,6 +4,7 @@ * @var WarehouseOrderModel $order * @var Array $positions * @var Array $textElements + * @var string $distributorCountryText */ $this->setReturnValue(['filename' => $order["id"] . ".pdf"]); @@ -16,6 +17,7 @@ $texts = [ 'total' => 'Total', 'taxFree' => 'Please provide tax-free delivery according to § 6a UStG (Austrian Sales tax law).
Our VAT ID number: ATU68711968. Delivery to Austria.', 'orderConfirmation' => 'Please send the order confirmation to einkauf@xinon.at', + 'sendShippingNote' => 'Please send the delivery note with our shipping note and neutral packaging.', 'table' => [ 'pos' => 'POS', 'article' => 'Article', @@ -32,6 +34,7 @@ $texts = [ 'total' => 'Gesamt', 'taxFree' => 'Bitte um steuerfreie Lieferung gemäß § 6a UStG.
Unsere UID-Nr.: ATU68711968. Lieferung nach Österreich.', 'orderConfirmation' => 'Wir bitten um Zusendung der Auftragsbestätigung für die Bestellung an einkauf@xinon.at.', + 'sendShippingNote' => 'Bitte senden Sie den Lieferschein mit unserem Versandschein und neutraler Verpackung.', 'table' => [ 'pos' => 'POS', 'article' => 'Artikel', @@ -182,10 +185,17 @@ $text = $texts[in_array($distributorCountryText, ["Österreich", "Deutschland",
- +


+ + 0): ?> +

+ + + +
\ No newline at end of file diff --git a/application/WarehouseOrder/WarehouseOrderController.php b/application/WarehouseOrder/WarehouseOrderController.php index 2bc3843c1..61801ca9a 100644 --- a/application/WarehouseOrder/WarehouseOrderController.php +++ b/application/WarehouseOrder/WarehouseOrderController.php @@ -89,85 +89,210 @@ class WarehouseOrderController extends TTCrud { return $order; } - protected function createPDFAction() { + protected function createPDFAction($returnFilename = false) { $order = (array) WarehouseOrderModel::get($this->request->id); $order['positions'] = json_decode($order['positions'], true); - // check if all positions have the same distributor - $distributorId = $order['positions'][0]['distributorId']; - foreach ($order['positions'] as $key => $position) { - if ($position['distributorId'] !== $distributorId) { - self::returnJson(['error' => 'Die Bestellung enthält Positionen von verschiedenen Lieferanten.']); - } - // we need to get the article name and distributor name for the pdf - $position['distributorName'] = WarehouseDistributorModel::get($position['distributorId'])->name; - $position['articleName'] = WarehouseArticleModel::get($position['article'])->title; - $position['articleDescription'] = WarehouseArticleModel::get($position['article'])->description; - - $order['positions'][$key] = $position; + // Validate distributor consistency + $distributorIds = array_column($order['positions'], 'distributorId'); + if (count(array_unique($distributorIds)) > 1) { + self::returnJson(['error' => 'Die Bestellung enthält Positionen von verschiedenen Lieferanten.']); } - $pdf_vars = ['order' => $order, - 'distributor' => WarehouseDistributorModel::get($distributorId), - 'distributorCountryText' => (new Country(WarehouseDistributorModel::get($distributorId)->countryId))->name, - "bank_iban" => TT_INVOICE_BANK_IBAN, - "bank_bic" => TT_INVOICE_BANK_BIC, - "bank_bank" => TT_INVOICE_BANK_BANK, - "bank_owner" => TT_INVOICE_BANK_OWNER]; + // Prepare data + $distributor = WarehouseDistributorModel::get($distributorIds[0]); + $countryText = (new Country($distributor->countryId))->name; + $isGermanSpeaking = in_array($countryText, ['Österreich', 'Deutschland', 'Schweiz']); + $isDefaultAddress = $order['delAddrLine'] === "Fladnitz im Raabtal 150"; - $countryText = (new Country(WarehouseDistributorModel::get($distributorId)->countryId))->name; - $shouldGenerateEnglisch = !in_array($countryText, ['Österreich', 'Deutschland', 'Schweiz']); + // Enhance positions + foreach ($order['positions'] as &$position) { + $article = WarehouseArticleModel::get($position['article']); + $position += [ + 'distributorName' => $distributor->name, + 'articleName' => $article->title, + 'articleDescription' => $article->description + ]; + } - $headerHtml = file_get_contents(BASEDIR . "/Layout/default/WarehouseOrder/PDF_HEADER.html"); - $headerHtml = str_replace("{{ basedir }}", BASEDIR, $headerHtml); - $headerHtml = str_replace("{{ externalReference }}", !empty($order['extReference']) && count($order['extReference']) > 0 ? "Ext. Ref.: ". $order['extReference'] : "", $headerHtml); - $headerHtml = str_replace("{{ addressLine_header }}", $shouldGenerateEnglisch ? "Supplier" : "Lieferant", $headerHtml); - $headerHtml = str_replace("{{ addressLine_1 }}", WarehouseDistributorModel::get($distributorId)->name, $headerHtml); - $headerHtml = str_replace("{{ addressLine_2 }}", WarehouseDistributorModel::get($distributorId)->address, $headerHtml); - $headerHtml = str_replace("{{ addressLine_3 }}", WarehouseDistributorModel::get($distributorId)->plz . " " . WarehouseDistributorModel::get($distributorId)->city, $headerHtml); - $headerHtml = str_replace("{{ addressLine_4 }}", $countryText, $headerHtml); + // Prepare PDF variables + $pdfVars = [ + 'order' => $order, + 'distributor' => $distributor, + 'distributorCountryText' => $countryText, + 'bank' => [ + 'iban' => TT_INVOICE_BANK_IBAN, + 'bic' => TT_INVOICE_BANK_BIC, + 'bank' => TT_INVOICE_BANK_BANK, + 'owner' => TT_INVOICE_BANK_OWNER + ] + ]; - $headerHtml = str_replace("{{ billingAddressLine_header }}", $shouldGenerateEnglisch ? "Billing Address" : "Rechnungsadresse", $headerHtml); - $headerHtml = str_replace("{{ billingAddressLine_1 }}", "Xinon GmbH", $headerHtml); - $headerHtml = str_replace("{{ billingAddressLine_2 }}", "Fladnitz im Raabtal 150", $headerHtml); - $headerHtml = str_replace("{{ billingAddressLine_3 }}", "A-8322 Studenzen", $headerHtml); - $headerHtml = str_replace("{{ billingAddressLine_4 }}", "UID: ATU68711968", $headerHtml); - $headerHtml = str_replace("{{ billingAddressLine_5 }}", "EORI-Nr.: ATEOS1000085074", $headerHtml); - $headerHtml = str_replace("{{ billingAddressLine_6 }}", "Referenz: ". $order["orderNumber"] . "", $headerHtml); - - $chk = $order['delAddrLine'] == "Fladnitz im Raabtal 150"; - - $headerHtml = str_replace("{{ shippingAddressLine_header }}", $chk ? "" : ($shouldGenerateEnglisch ? "Shipping Address" : "Lieferadresse"), $headerHtml); - $headerHtml = str_replace("{{ shippingAddressLine_1 }}", $chk ? "" : $order['delAddrName'], $headerHtml); - $headerHtml = str_replace("{{ shippingAddressLine_2 }}", $chk ? "" : $order['delAddrLine'], $headerHtml); - $headerHtml = str_replace("{{ shippingAddressLine_3 }}", $chk ? "" : $order['delAddrPLZ'] . " " . $order['delAddrCity'], $headerHtml); - $headerHtml = str_replace("{{ shippingAddressLine_4 }}", $chk ? "" : $order['delAddrEMail'], $headerHtml); + // Generate HTML templates + $replacements = [ + 'basedir' => BASEDIR, + 'externalReference' => !empty($order['extReference']) ? + "Ext. Ref.: ".$order['extReference'] : "", + 'isGermanSpeaking' => $isGermanSpeaking, // Required for conditional logic + 'isDefaultAddress' => $isDefaultAddress, // Needed for address display rules + 'distributor' => $distributor, + 'countryText' => $countryText, + 'order' => $order, // Full order data access + 'orderNumber' => $order['orderNumber'], + 'bank_iban' => TT_INVOICE_BANK_IBAN_FORMATTED, // Direct footer access + 'bank_bic' => TT_INVOICE_BANK_BIC, + 'bank_bank' => TT_INVOICE_BANK_BANK, + 'bank_owner' => TT_INVOICE_BANK_OWNER + ]; + // put header and footer into temp files $headerFile = BASEDIR . "/var/temp/order_header-" . date("U") . "-" . rand(1000, 9999) . ".html"; - file_put_contents($headerFile, $headerHtml); - - $footerHtml = file_get_contents(BASEDIR . "/Layout/default/WarehouseOrder/PDF_FOOTER.html"); - $footerHtml = str_replace("{{ bank_iban }}", TT_INVOICE_BANK_IBAN_FORMATTED, $footerHtml); - $footerHtml = str_replace("{{ bank_bic }}", TT_INVOICE_BANK_BIC, $footerHtml); - $footerHtml = str_replace("{{ bank_bank }}", TT_INVOICE_BANK_BANK, $footerHtml); - $footerHtml = str_replace("{{ bank_owner }}", TT_INVOICE_BANK_OWNER, $footerHtml); - + file_put_contents($headerFile, $this->generateTemplate('PDF_HEADER', $replacements)); $footerFile = BASEDIR . "/var/temp/order_footer-" . date("U") . "-" . rand(1000, 9999) . ".html"; - file_put_contents($footerFile, $footerHtml); + file_put_contents($footerFile, $this->generateTemplate('PDF_FOOTER', $replacements)); + // Generate PDF + $pdf = new PdfForm("WarehouseOrder/PDF_MAIN", $pdfVars); + $filename = $pdf->render("--header-html $headerFile --footer-html $footerFile"); - $pdf = new PdfForm("WarehouseOrder/PDF_MAIN", $pdf_vars); - $wkhtmltopdfArgs = "--header-html $headerFile --footer-html $footerFile"; - $filename = $pdf->render($wkhtmltopdfArgs); + if ($returnFilename === true) return $filename; - // return the pdf and die so the client sees the pdf not the filename + // Output PDF header('Content-Type: application/pdf'); - header('Content-Disposition: inline; filename="' . $filename . '"'); + header('Content-Disposition: inline; filename="' . basename($filename) . '"'); readfile($filename); - + exit; } + protected function sendEmailAction() { + $pdfFilename = $this->createPDFAction(true); + + // Get order details for email content + $order = (array) WarehouseOrderModel::get($this->request->id); + $orderNumber = $order['orderNumber']; + $order['positions'] = json_decode($order['positions'], true); + $distributorName = WarehouseDistributorModel::get($order['distributorId'])->name; + + // Instantiate PHPMailer + $mail = new PHPMailer\PHPMailer\PHPMailer(true); + + try { + // Server settings + $mail->isSMTP(); + $mail->Host = TT_WAREHOUSE_ORDER_SMTP_HOST; + $mail->SMTPAuth = true; + $mail->Username = TT_WAREHOUSE_ORDER_SMTP_USER; + $mail->Password = TT_WAREHOUSE_ORDER_SMTP_PASS; + $mail->SMTPSecure = PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_STARTTLS; + $mail->Port = 587; + + // Recipients + $mail->setFrom('einkauf@xinon.at', 'XINON Einkauf'); + $mail->addAddress($this->request->email, $this->request->email); + // $mail->addCC('cc@example.com'); // Uncomment for CC + + // Content + $mail->isHTML(true); + $mail->Subject = "Neue Bestellung #$orderNumber"; + $mail->Body = " + + + XINON E-Mail Template + + + +

Neue Bestellung

+

+ Bestellnummer: $orderNumber
+ Datum: " . date('d.m.Y H:i', $order['create']) . "
+ Lieferant: $distributorName +

+

Die Bestellung liegt als PDF-Anhang bei.

+ +"; + + // Attach PDF + $mail->addAttachment($pdfFilename, "Bestellung_$orderNumber.pdf"); + + if ($order['sendShippingNote'] > 0) { + $shippingNoteController = new WarehouseShippingNoteController(); + $shippingNoteFilename = $shippingNoteController->createPDFAction(true, $this->request->shippingNote); + $mail->addAttachment($shippingNoteFilename, "Lieferschein_$orderNumber.pdf"); + } + + $mail->send(); + + $log = [ + "table" => "WarehouseOrder", + "rowId" => $this->request->id, + "type" => "noChanges", + "message" => "Bestellung per Mail versendet", + "createBy" => intval($this->user->id), + "create" => time() + ]; + WarehouseLogModel::create($log); + + // Cleanup temporary files + if (file_exists($pdfFilename)) { + unlink($pdfFilename); + } + + self::returnJson(['success' => true, 'message' => 'E-Mail erfolgreich versendet']); + exit; + } catch (Exception $e) { + http_response_code(500); + self::returnJson([ + 'success' => false, + 'message' => "E-Mail konnte nicht versendet werden: {$mail->ErrorInfo}", + ]); + exit; + } + } + + + private function generateTemplate(string $type, array $data): string { + $template = file_get_contents(BASEDIR . "/Layout/default/WarehouseOrder/{$type}.html"); + + $replacements = [ + '{{ basedir }}' => BASEDIR, + '{{ externalReference }}' => !empty($data['order']['extReference']) ? + "Ext. Ref.: ".$data['order']['extReference'] : "", + '{{ addressLine_header }}' => $data['isGermanSpeaking'] ? 'Lieferant' : 'Supplier', + '{{ addressLine_1 }}' => $data['distributor']->name, + '{{ addressLine_2 }}' => $data['distributor']->address, + '{{ addressLine_3 }}' => $data['distributor']->plz . ' ' . $data['distributor']->city, + '{{ addressLine_4 }}' => $data['countryText'], + '{{ billingAddressLine_header }}' => $this->getBillingHeader($data), + '{{ billingAddressLine_1 }}' => 'Xinon GmbH', + '{{ billingAddressLine_2 }}' => 'Fladnitz im Raabtal 150', + '{{ billingAddressLine_3 }}' => 'A-8322 Studenzen', + '{{ billingAddressLine_4 }}' => 'UID: ATU68711968', + '{{ billingAddressLine_5 }}' => 'EORI-Nr.: ATEOS1000085074', + '{{ billingAddressLine_6 }}' => "Referenz: ".$data['order']['orderNumber']."", + '{{ shippingAddressLine_header }}' => $data['isDefaultAddress'] ? '' : + ($data['isGermanSpeaking'] ? 'Lieferadresse' : 'Shipping Address'), + '{{ shippingAddressLine_1 }}' => $data['isDefaultAddress'] ? '' : $data['order']['delAddrName'], + '{{ shippingAddressLine_2 }}' => $data['isDefaultAddress'] ? '' : $data['order']['delAddrLine'], + '{{ shippingAddressLine_3 }}' => $data['isDefaultAddress'] ? '' : + $data['order']['delAddrPLZ'] . ' ' . $data['order']['delAddrCity'], + '{{ shippingAddressLine_4 }}' => $data['isDefaultAddress'] ? '' : $data['order']['delAddrEMail'], + '{{ bank_iban }}' => TT_INVOICE_BANK_IBAN_FORMATTED, + '{{ bank_bic }}' => TT_INVOICE_BANK_BIC, + '{{ bank_bank }}' => TT_INVOICE_BANK_BANK, + '{{ bank_owner }}' => TT_INVOICE_BANK_OWNER + ]; + + return strtr($template, $replacements); + } + + private function getBillingHeader(array $data): string { + $base = $data['isGermanSpeaking'] ? 'Rechnungsadresse' : 'Billing Address'; + return $data['isDefaultAddress'] ? $base . ' / ' . + ($data['isGermanSpeaking'] ? 'Lieferadresse' : 'Shipping Address'):$base; + } + + protected function getLogAction() { $orderId = $this->request->orderId; if (empty($orderId)) { @@ -198,7 +323,7 @@ class WarehouseOrderController extends TTCrud { ]; try { - $order = WarehouseOrderModel::get($log['orderId']); + $order = WarehouseOrderModel::get($log['rowId']); if ($postData['status'] !== 'noChanges') { $oldStatusText = array_values(array_filter($this->columns, fn($c) => $c['key'] === 'status'))[0]['modal']['items'][array_search($order->status, array_column(array_values(array_filter($this->columns, fn($c) => $c['key'] === 'status'))[0]['modal']['items'], 'value'))]['text']; $newStatusText = array_values(array_filter($this->columns, fn($c) => $c['key'] === 'status'))[0]['modal']['items'][array_search($postData['status'], array_column(array_values(array_filter($this->columns, fn($c) => $c['key'] === 'status'))[0]['modal']['items'], 'value'))]['text']; diff --git a/application/WarehouseOrder/WarehouseOrderModel.php b/application/WarehouseOrder/WarehouseOrderModel.php index 9c66e75ca..4e14082c7 100644 --- a/application/WarehouseOrder/WarehouseOrderModel.php +++ b/application/WarehouseOrder/WarehouseOrderModel.php @@ -33,6 +33,7 @@ class WarehouseOrderModel extends TTCrudBaseModel { public int $editor; public ?string $note; public string $positions; + public ?int $sendShippingNote; public int $create; public int $createBy; } diff --git a/application/WarehouseOrderRequest/WarehouseOrderRequestController.php b/application/WarehouseOrderRequest/WarehouseOrderRequestController.php index 2452a67b0..feb051528 100644 --- a/application/WarehouseOrderRequest/WarehouseOrderRequestController.php +++ b/application/WarehouseOrderRequest/WarehouseOrderRequestController.php @@ -8,6 +8,7 @@ class WarehouseOrderRequestController extends TTCrud { //@formatter:off protected array $columns = [ ['key' => 'id', 'text' => 'Bestellnummer', 'table' => ['filter' => false], 'modal' => false], + ['key' => 'addressId', 'text' => 'Kunde', 'required' => false, 'type' => 'autocomplete', 'table' => ['class' => 'text-nowrap', 'filter' => 'autocomplete'], 'modal' => ['apiUrl' => 'Address/api?do=findAddress', 'items' => '/Address/Api?do=findAddress', 'type' => 'autocomplete']], ['key' => 'purpose', 'text' => 'Verwendungszweck', 'required' => true], ['key' => 'positions', 'text' => 'Positionen', 'required' => true, 'modal' => ['type' => 'positions-manager', 'config' => [ 'header' => 'Positionen', @@ -35,11 +36,16 @@ class WarehouseOrderRequestController extends TTCrud { ['value' => 0, 'text' => 'Bestellwunsch nicht storniert', 'icon' => 'fa-regular fa-circle-check text-success'], ['value' => 1, 'text' => 'Bestellwunsch storniert', 'icon' => 'fa-regular fa-circle-xmark text-danger']]], 'table' => ['filter' => 'iconSelect'] ], + ['key' => 'done', 'text' => 'Erledigt', 'modal' => ['visible' => false, 'type' => 'icon-select', 'items' => [ + ['value' => 0, 'text' => 'Bestellwunsch nicht erledigt', 'icon' => 'fa-regular fa-circle-check text-success'], + ['value' => 1, 'text' => 'Bestellwunsch erledigt', 'icon' => 'fa-regular fa-circle-xmark text-danger']]], 'table' => ['filter' => 'iconSelect'] + ], ['key' => 'actions', 'text' => 'Aktionen', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center']], ]; //@formatter:on protected array $permissionCheck = ['WarehouseUser']; + protected array $defaultOrder = ['key' => 'create', 'order' => 'DESC']; protected array $additionalActions = [ ['key' => 'openHistory', 'title' => 'Historie', 'class' => 'fas fa-history text-primary'], @@ -59,37 +65,6 @@ class WarehouseOrderRequestController extends TTCrud { return true; } - protected function afterCreate($postData): void { - return; - //TODO: this should be working again - if ($_SERVER['HTTP_HOST'] === 'localhost') return; - die("TODO we need this to work with new positions manager"); - - $email = new Emailnotification(); - $postData['ware'] = is_numeric($postData['ware']) ? WarehouseArticleModel::get((int) $postData['ware'])->title : $postData['ware']; - $paddedId = str_pad($postData['id'], 5, '0', STR_PAD_LEFT); - - $email->setSubject("TheTool: Neue Interne Bestellung #$paddedId") - ->setBody(<<user->name} -Beauftragt am: {date('d.m.Y H:i')} -Notiz: {$postData['note']} - -BODY - ) - ->setFrom(TT_OUTGOING_EMAIL_2FA, TT_OUTGOING_EMAIL_2FA) - ->setTo("einkauf@xinon.at", "Einkauf") - ->send(); - } - protected function cancelAction() { $id = filter_var($this->request->id, FILTER_VALIDATE_INT); $cancel = filter_var($this->request->cancel, FILTER_VALIDATE_INT, ['options' => ['min_range' => 0, 'max_range' => 1]]); @@ -101,6 +76,17 @@ BODY self::returnJson(['success' => true]); } + protected function doneAction() { + $id = filter_var($this->request->id, FILTER_VALIDATE_INT); + $done = filter_var($this->request->done, FILTER_VALIDATE_INT, ['options' => ['min_range' => 0, 'max_range' => 1]]); + + if (!$id || $done === false) self::returnJson(['error' => 'Ungültige Anfrage']); + if (!(WarehouseOrderRequestModel::get($id))) self::returnJson(['error' => 'Bestellwunsch nicht gefunden']); + + WarehouseOrderRequestModel::update(['id' => $id, 'done' => $done]); + self::returnJson(['success' => true]); + } + protected function createNewLogAction() { $postData = json_decode(file_get_contents('php://input'), true); diff --git a/application/WarehouseOrderRequest/WarehouseOrderRequestModel.php b/application/WarehouseOrderRequest/WarehouseOrderRequestModel.php index 63f93a823..c1ff10ad2 100644 --- a/application/WarehouseOrderRequest/WarehouseOrderRequestModel.php +++ b/application/WarehouseOrderRequest/WarehouseOrderRequestModel.php @@ -2,11 +2,13 @@ class WarehouseOrderRequestModel extends TTCrudBaseModel { public int $id; + public ?int $addressId; public string $purpose; public string $positions; public ?string $note; public ?string $linkedOrderIds; public ?int $cancelled; + public ?int $done; public int $create; public int $createBy; } diff --git a/application/WarehouseShippingNote/WarehouseShippingNoteController.php b/application/WarehouseShippingNote/WarehouseShippingNoteController.php index 2861ac418..38c6f7123 100644 --- a/application/WarehouseShippingNote/WarehouseShippingNoteController.php +++ b/application/WarehouseShippingNote/WarehouseShippingNoteController.php @@ -223,8 +223,8 @@ class WarehouseShippingNoteController extends TTCrud { } } - protected function createPDFAction() { - $id = $this->request->id; + public function createPDFAction($returnFilename = false, $idOverride = null) { + $id = $idOverride ?? $this->request->id; if (strlen($id) < 1) { http_response_code(500); self::returnJson(['success' => false, 'message' => 'Lieferschein wurde nicht gefunden']); @@ -376,10 +376,13 @@ class WarehouseShippingNoteController extends TTCrud { $wkhtmltopdfArgs = "--header-html $headerFile --footer-html $footerFile"; $filename = $pdf->render($wkhtmltopdfArgs); + if ($returnFilename === true) return $filename; + // return the pdf and die so the client sees the pdf not the filename header('Content-Type: application/pdf'); header('Content-Disposition: inline; filename="' . $filename . '"'); readfile($filename); + die(); } // TODO: either move this to UserController or make it better diff --git a/composer.json b/composer.json index 9aa8489ea..f50ae1f49 100644 --- a/composer.json +++ b/composer.json @@ -5,6 +5,7 @@ "textalk/websocket": "^1.6", "chillerlan/php-qrcode": "^4", "phpseclib/phpseclib": "^3.0", - "stomp-php/stomp-php": "^5" + "stomp-php/stomp-php": "^5", + "phpmailer/phpmailer": "^6.9" } } diff --git a/db/migrations/20250307080000_warehouse_modify_14.php b/db/migrations/20250307080000_warehouse_modify_14.php new file mode 100644 index 000000000..de4e52b27 --- /dev/null +++ b/db/migrations/20250307080000_warehouse_modify_14.php @@ -0,0 +1,37 @@ +getEnvironment() == "thetool") { + // Modify WarehouseOrderRequest + $orderRequestTable = $this->table("WarehouseOrderRequest"); + $orderRequestTable + ->addColumn("done", "integer", ['default' => 0]) + ->addColumn("addressId", "integer", ['default' => null, 'null' => true]) + ->save(); + + // Modify WarehouseOrder + $orderTable = $this->table("WarehouseOrder"); + $orderTable + ->addColumn("sendShippingNote", "integer", ['default' => 0]) + ->save(); + } + } + + public function down(): void { + if ($this->getEnvironment() == "thetool") { + $orderRequestTable = $this->table("WarehouseOrderRequest"); + $orderRequestTable + ->removeColumn("done") + ->removeColumn("addressId") + ->save(); + + $orderTable = $this->table("WarehouseOrder"); + $orderTable + ->removeColumn("sendShippingNote") + ->save(); + } + } +} diff --git a/public/js/pages/WarehouseOrder/WarehouseOrder.css b/public/js/pages/WarehouseOrder/WarehouseOrder.css index 36dd1f290..01feb970c 100644 --- a/public/js/pages/WarehouseOrder/WarehouseOrder.css +++ b/public/js/pages/WarehouseOrder/WarehouseOrder.css @@ -127,3 +127,56 @@ margin-bottom: 0.5rem; } + +.ios-switch-wrapper { + position: relative; + display: inline-block; + width: 50px; + height: 28px; +} + +.ios-switch-wrapper.disabled { + opacity: 0.6; +} + +.ios-switch-wrapper input { + opacity: 0; + width: 0; + height: 0; +} + +.ios-switch-slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + transition: .4s; + border-radius: 34px; +} + +.ios-switch-slider:before { + position: absolute; + content: ""; + height: 20px; + width: 20px; + left: 4px; + bottom: 4px; + background-color: white; + transition: .4s; + border-radius: 50%; +} + +input:checked + .ios-switch-slider { + background-color: #4cd964; +} + +input:checked + .ios-switch-slider:before { + transform: translateX(22px); +} + +input:disabled + .ios-switch-slider { + cursor: not-allowed; +} diff --git a/public/js/pages/WarehouseOrder/WarehouseOrder.js b/public/js/pages/WarehouseOrder/WarehouseOrder.js index af9c9651e..cb3152675 100644 --- a/public/js/pages/WarehouseOrder/WarehouseOrder.js +++ b/public/js/pages/WarehouseOrder/WarehouseOrder.js @@ -5,11 +5,15 @@ Vue.component('change-status-modal', { }, data() { return { - order: null, - newStatus: 'noChanges', - note: '', - file: null, - uploadedFiles: [] + window: window, + order: null, + newStatus: 'noChanges', + note: '', + file: null, + uploadedFiles: [], + sendEmail: false, + sendEmailViewedPDF: false, + sendEmailMail: '', }; }, async mounted() { @@ -101,12 +105,25 @@ Vue.component('change-status-modal', { }, removeFile: index => this.uploadedFiles.splice(index, 1), async submit() { + if (this.newStatus === 'accepted' && this.sendEmail && !this.sendEmailMail) { + window.notify('error', 'Bitte geben Sie eine E-Mail-Adresse ein'); + return; + } else if (this.newStatus === 'accepted' && this.sendEmail && !this.sendEmailViewedPDF) { + window.notify('error', 'Bitte öffnen Sie das PDF bevor Sie die E-Mail senden'); + return; + } else if (this.newStatus === 'accepted' && this.sendEmail && this.sendEmailMail) { + if (this.order.sendShippingNote > 0) { + const shippingNoteId = await this.generateShippingNote(); + await this.submitEmail(shippingNoteId); + } else await this.submitEmail(); + } + const fileIds = this.uploadedFiles.map(file => file.id); const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/createNewLogAction`, { orderId: this.order.id, status: this.newStatus, note: this.note, - fileIds: JSON.stringify(fileIds) + fileIds: JSON.stringify(fileIds), }); if (response.data.success) { @@ -116,6 +133,39 @@ Vue.component('change-status-modal', { window.notify('error', response.data.errors ? Object.values(response.data.errors).join('
') : response.data.message || 'Ein Fehler ist aufgetreten'); } + }, + async generateShippingNote() { + const positions = this.order.positions.map(position => ({ + article: position.article, + amount: position.amount, + price: position.buyPrice + })); + + const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseShippingNote/create`, + { + "billingAddressId": this.order.sendShippingNote, + "deliveryAddressName": this.order.delAddrName, + "deliveryAddressLine": this.order.delAddrLine, + "deliveryAddressPLZ": this.order.delAddrPLZ, + "deliveryAddressCity": this.order.delAddrCity, + "deliveryAddressEMail": this.order.delAddrEMail, + "textElements": [], + "hoursEntries": [], + "positions": positions, + "note": "Bestellung #" + this.order.orderNumber, + "status": "new" + }); + + return response.data.id; + }, + async submitEmail(shippingNote = null) { + const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/sendEmail?id=${this.order.id}&email=${this.sendEmailMail}${shippingNote ? '&shippingNote=' + shippingNote : ''}`); + + if (response.data.success) { + window.notify('success', response.data.message); + } else { + window.notify('error', response.data.message || 'Ein Fehler ist aufgetreten'); + } } }, template: ` @@ -163,6 +213,32 @@ Vue.component('change-status-modal', { +
+

E-Mail verschicken?

+ + + +
+
E-Mail senden:
+ +
+ Bitte erst PDF ansehen +
+
+ +
@@ -210,6 +286,24 @@ Vue.component('warehouse-order-modal', { + +
@@ -218,8 +312,9 @@ Vue.component('warehouse-order-modal', { data() { return { - window: window, - positionsConfig: { + window: window, + showSendShippingNote: false, + positionsConfig: { customOrdering: 'distributorId', fields: { article: { @@ -252,16 +347,17 @@ Vue.component('warehouse-order-modal', { return true; }, }, - order: { - extReference: '', - delAddrName: 'XINON GmbH', - delAddrLine: 'Fladnitz im Raabtal 150', - delAddrPLZ: '8322', - delAddrCity: 'Studenzen', - delAddrEMail: 'einkauf@xinon.at', - note: '', - editor: window.TT_CONFIG['USER_ID'], - positions: [], + order: { + extReference: '', + delAddrName: 'XINON GmbH', + delAddrLine: 'Fladnitz im Raabtal 150', + delAddrPLZ: '8322', + delAddrCity: 'Studenzen', + delAddrEMail: 'einkauf@xinon.at', + note: '', + editor: window.TT_CONFIG['USER_ID'], + sendShippingNote: false, + positions: [], } } }, @@ -365,20 +461,20 @@ Vue.component('warehouse-order-modal', { }); Vue.component('tt-file', { - props: ['id'], - data: () => ({file: null}), + props: ['id'], + data: () => ({file: null}), async mounted() { const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/File/getById`, {params: {id: this.id}}); this.file = response.data; }, template: `
- {{ file.filename }} + {{ file.filename }}
- ` + ` }) diff --git a/public/js/pages/WarehouseOrderRequest/WarehouseOrderRequest.js b/public/js/pages/WarehouseOrderRequest/WarehouseOrderRequest.js index 97f32059b..c973762fc 100644 --- a/public/js/pages/WarehouseOrderRequest/WarehouseOrderRequest.js +++ b/public/js/pages/WarehouseOrderRequest/WarehouseOrderRequest.js @@ -19,7 +19,19 @@ window.TT_CONFIG["CRUD_CONFIG"]["additionalActions"] = [ condition: (row) => window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1' && row.cancelled === 0 && (!row.linkedOrderIds || row.linkedOrderIds.length === 0) && JSON.parse(row.positions).filter(position => position.articleId_text).length === 0, - } + }, + { + key: "doneOrder", + title: "Bestellwunsch erledigt", + class: "fas fa-check text-success", + condition: (row) => window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1' && row.done === 0, + }, + { + key: "undoneOrder", + title: "Bestellwunsch wieder offen", + class: "fas fa-times text-danger", + condition: (row) => window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1' && row.done === 1, + }, ] Vue.component('add-log-modal', { @@ -188,6 +200,8 @@ Vue.component('warehouse-order-request', { @@ -232,6 +246,19 @@ Vue.component('warehouse-order-request', { res.data.message || (res.data.success ? 'Erfolgreich aktualisiert' : 'Fehler beim aktualisieren')); if (res.data.success) this.$refs.crud.$refs.table.refreshTable(); }, + async doneRequest(row, done) { + if (!confirm('Bestellwunsch wirklich als erledigt markieren?')) return; + const res = await axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrderRequest/done?id=${row.id}&done=${done}`); + window.notify(res.data.success ? 'success' : 'error', + res.data.message || (res.data.success ? 'Erfolgreich aktualisiert' : 'Fehler beim aktualisieren')); + if (res.data.success) this.$refs.crud.$refs.table.refreshTable(); + }, + async doneOrder(row) { + this.doneRequest(row, '1'); + }, + async undoneOrder(row) { + this.doneRequest(row, '0'); + }, async createLog(row) { this.addLogModal = true; this.addLogModalId = row.id;