From 7c8af7aed48eecb0b6f3d44eed611745c9d41230 Mon Sep 17 00:00:00 2001 From: Luca Haid Date: Tue, 4 Feb 2025 19:06:08 +0100 Subject: [PATCH] Updated Warehouse --- Layout/default/WarehouseOrder/PDF_FOOTER.html | 43 +++++ Layout/default/WarehouseOrder/PDF_HEADER.html | 92 +++++++++ Layout/default/WarehouseOrder/PDF_MAIN.php | 133 +++++++++++++ .../WarehouseArticleController.php | 2 + application/WarehouseOffer/WarehouseOffer.php | 9 + .../WarehouseOfferController.php | 56 ++++++ .../WarehouseOffer/WarehouseOfferModel.php | 52 +++++ .../WarehouseOrderController.php | 182 +++++++++++++----- .../WarehouseOrder/WarehouseOrderModel.php | 4 +- .../WarehouseProject/WarehouseProject.php | 4 + .../WarehouseProjectController.php | 36 ++++ .../WarehouseProjectModel.php | 15 ++ .../20250204190000_warehouse_modify_12.php | 21 ++ lib/Helper/Helper.php | 5 +- lib/TTCrud/TTCrud.php | 22 ++- lib/TTCrudBaseModel/TTCrudBaseModel.php | 22 +-- .../pages/WarehouseOffer/WarehouseOffer.css | 14 ++ .../js/pages/WarehouseOffer/WarehouseOffer.js | 178 +++++++++++++++++ .../pages/WarehouseOrder/WarehouseOrder.css | 27 ++- .../js/pages/WarehouseOrder/WarehouseOrder.js | 125 +++++++++--- .../WarehouseProject/WarehouseProject.css | 14 ++ .../WarehouseProject/WarehouseProject.js | 161 ++++++++++++++++ .../vue/tt-components/tt-position-manager.js | 7 + public/plugins/vue/tt-components/tt-table.js | 1 + 24 files changed, 1126 insertions(+), 99 deletions(-) create mode 100644 Layout/default/WarehouseOrder/PDF_FOOTER.html create mode 100644 Layout/default/WarehouseOrder/PDF_HEADER.html create mode 100644 Layout/default/WarehouseOrder/PDF_MAIN.php create mode 100644 application/WarehouseOffer/WarehouseOffer.php create mode 100644 application/WarehouseOffer/WarehouseOfferController.php create mode 100644 application/WarehouseOffer/WarehouseOfferModel.php create mode 100644 application/WarehouseProject/WarehouseProject.php create mode 100644 application/WarehouseProject/WarehouseProjectController.php create mode 100644 application/WarehouseProject/WarehouseProjectModel.php create mode 100644 db/migrations/20250204190000_warehouse_modify_12.php create mode 100644 public/js/pages/WarehouseOffer/WarehouseOffer.css create mode 100644 public/js/pages/WarehouseOffer/WarehouseOffer.js create mode 100644 public/js/pages/WarehouseProject/WarehouseProject.css create mode 100644 public/js/pages/WarehouseProject/WarehouseProject.js diff --git a/Layout/default/WarehouseOrder/PDF_FOOTER.html b/Layout/default/WarehouseOrder/PDF_FOOTER.html new file mode 100644 index 000000000..0a984b7d6 --- /dev/null +++ b/Layout/default/WarehouseOrder/PDF_FOOTER.html @@ -0,0 +1,43 @@ + + + + Xinon Rechnung + + + + + + +
+
+ XINON GmbH | Fladnitz 150 | 8322 Studenzen
+ Tel.: +43 3115 40800 | E-Mail: office@xinon.at
+ UID: ATU68711968 | FN: 416556h | LG: Feldbach
+ IBAN: {{ bank_iban }} | BIC: {{ bank_bic }}
+
+ +
Seite von
+ +
+ + diff --git a/Layout/default/WarehouseOrder/PDF_HEADER.html b/Layout/default/WarehouseOrder/PDF_HEADER.html new file mode 100644 index 000000000..21802f9d5 --- /dev/null +++ b/Layout/default/WarehouseOrder/PDF_HEADER.html @@ -0,0 +1,92 @@ + + + + XINON Shipping Note Header + + + + + +
+ +
+ Xinon Logo +
+ + + + + + + +
+

Lieferant

+
{{ addressLine_1 }}
+
{{ addressLine_2 }}
+
{{ addressLine_3 }}
+
{{ addressLine_4 }}
+
+
{{ externalReference }}
+
+

Rechnungsadresse

+
{{ billingAddressLine_1 }}
+
{{ billingAddressLine_2 }}
+
{{ billingAddressLine_3 }}
+
{{ billingAddressLine_4 }}
+
{{ billingAddressLine_5 }}
+
{{ billingAddressLine_6 }}
+
+
{{ shippingAddressLine_1 }}
+
{{ shippingAddressLine_2 }}
+
{{ shippingAddressLine_3 }}
+
{{ shippingAddressLine_4 }}
+
+ + +
+ + + diff --git a/Layout/default/WarehouseOrder/PDF_MAIN.php b/Layout/default/WarehouseOrder/PDF_MAIN.php new file mode 100644 index 000000000..144555322 --- /dev/null +++ b/Layout/default/WarehouseOrder/PDF_MAIN.php @@ -0,0 +1,133 @@ +setReturnValue(['filename' => $order["id"] . ".pdf"]); +?> + + + + + Bestellung + + + + + + +
+ +

XINON Lieferantenbestellung vom

+ + + + + + + + + + + + + "> + + + + + + + + + + + + + + + + + +
PositionArtikelArt.-Nr. LieferantMengeEinzelpreisGesamtpreis
Summe
+ +
+

Anmerkungen

+

+ +

+
+ + \ No newline at end of file diff --git a/application/WarehouseArticle/WarehouseArticleController.php b/application/WarehouseArticle/WarehouseArticleController.php index 77c80a59e..93eae95cc 100644 --- a/application/WarehouseArticle/WarehouseArticleController.php +++ b/application/WarehouseArticle/WarehouseArticleController.php @@ -24,6 +24,8 @@ class WarehouseArticleController extends TTCrud { ['key' => 'actions', 'text' => 'Aktionen', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center', 'priority' => 8]] ]; + protected array $autocompleteColumns = ['articleNumber', 'title', 'description', 'category']; + protected array $additionalActions = [ ['key' => 'openHistory','title' => 'Historie','class' => 'fas fa-history text-secondary'], ['key' => 'editDistributorEntries','title' => 'Lieferanten','class' => 'fas fa-truck text-cyan'], diff --git a/application/WarehouseOffer/WarehouseOffer.php b/application/WarehouseOffer/WarehouseOffer.php new file mode 100644 index 000000000..7aff40082 --- /dev/null +++ b/application/WarehouseOffer/WarehouseOffer.php @@ -0,0 +1,9 @@ + 'id', 'text' => 'ID', 'modal' => false], + ['key' => 'offerNumber', 'text' => 'Angebotsnummer', 'required' => true, 'modal' => false], + ['key' => 'customerNumber', 'text' => 'Kundennummer', 'required' => true, 'modal' => false], + ['key' => 'customerName', 'text' => 'Kundenname', 'required' => true, 'modal' => false], + ['key' => 'customerCity', 'text' => 'Stadt', 'required' => true, 'modal' => false], + ['key' => 'customerVAT', 'text' => 'UID', 'required' => true, 'modal' => false], + ['key' => 'editor', 'text' => 'Sachbearbeiter', 'required' => true, 'modal' => false], + ['key' => 'totalAmount', 'text' => 'Gesamtbetrag', 'required' => true, 'modal' => false], + ['key' => 'status', 'text' => 'Status', 'required' => true, 'modal' => ['type' => 'select']], + ['key' => 'create', 'text' => 'Erstellt', 'required' => true, 'modal' => false], + ['key' => 'createBy', 'text' => 'Erstellt von', 'required' => true, 'modal' => ['type' => 'select']], + ['key' => 'actions', + 'text' => 'Aktionen', + 'required' => false, + 'modal' => false, + 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center']], + ]; + + protected array $permissionCheck = ['WarehouseAdmin']; + + protected array $additionalActions = [ + ['key' => 'openHistory', 'title' => 'Historie', 'class' => 'fas fa-history text-primary'], + ['key' => 'sendOffer', 'title' => 'Angebot senden', 'class' => 'fas fa-paper-plane text-success'] + ]; + + protected array $infoMessages = [ + 'create' => 'Angebot wurde erfolgreich erstellt.', + 'update' => 'Angebot wurde aktualisiert.', + 'delete' => 'Angebot wurde gelöscht', + 'noChanges' => 'Keine Änderungen', + 'sent' => 'Angebot wurde erfolgreich gesendet', + ]; + + protected function beforeCreate(): bool { + $currentCount = WarehouseOfferModel::count(['create' => ['from' => strtotime(date('Y-01-01'))]]); + $this->postData['offerNumber'] = 'AN' . date('Y') . '-' . str_pad($currentCount + 1, 4, '0', STR_PAD_LEFT); + + return true; + } + + protected function beforeUpdate($postData): bool { + (new WarehouseHistoryController)->create($postData, $this->mod); + return true; + } + + protected function getHistoryAction() { + self::returnJson((new WarehouseHistoryController)->getHistory($this->request->id, $this->mod, $this->columns)); + } +} diff --git a/application/WarehouseOffer/WarehouseOfferModel.php b/application/WarehouseOffer/WarehouseOfferModel.php new file mode 100644 index 000000000..c49405483 --- /dev/null +++ b/application/WarehouseOffer/WarehouseOfferModel.php @@ -0,0 +1,52 @@ + 'id', 'text' => 'ID', 'modal' => false], - ['key' => 'orderNumber', 'text' => 'Bestellnummer', 'required' => true, 'modal' => false], - ['key' => 'delAddrCity', 'text' => 'Stadt', 'required' => true, 'modal' => false], - ['key' => 'delAddrEMail', 'text' => 'E-Mail', 'required' => true, 'modal' => false], - ['key' => 'delAddrLine', 'text' => 'Adresse', 'required' => true, 'modal' => false], - ['key' => 'delAddrName', 'text' => 'Name', 'required' => true, 'modal' => false], - ['key' => 'delAddrPLZ', 'text' => 'PLZ', 'required' => true, 'modal' => false], - ['key' => 'editor', 'text' => 'Bearbeiter', 'required' => true, 'modal' => false], - ['key' => 'note', 'text' => 'Notiz', 'required' => true, 'modal' => false], - ['key' => 'positions', 'text' => 'Positionen', 'required' => true, 'modal' => false], - ['key' => 'create', 'text' => 'Erstellt', 'required' => true, 'modal' => false], - ['key' => 'createBy', 'text' => 'Erstellt von', 'required' => true, 'modal' => ['type' => 'select']], - ['key' => 'actions', - 'text' => 'Aktionen', - 'required' => false, - 'modal' => false, - 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center']], - ]; - protected array $permissionCheck = ['WarehouseAdmin']; - protected array $additionalActions = [['key' => 'openHistory', 'title' => 'Historie', 'class' => 'fas fa-history text-primary']]; + //@formatter:off + protected array $columns = [ + ['key' => 'id', 'text' => 'ID', 'modal' => false, 'table' => false], + ['key' => 'orderNumber', 'text' => 'Bestellnummer', 'required' => true, 'modal' => false], + ['key' => 'distributor', 'text' => 'Lieferant', 'required' => false, 'modal' => false, 'table' => ['filter' => false]], + ['key' => 'delAddrCity', 'text' => 'Stadt', 'required' => true, 'modal' => false, 'table' => false], + ['key' => 'delAddrEMail', 'text' => 'E-Mail', 'required' => true, 'modal' => false, 'table' => false], + ['key' => 'delAddrLine', 'text' => 'Adresse', 'required' => true, 'modal' => false, 'table' => false], + ['key' => 'delAddrName', 'text' => 'Name', 'required' => true, 'modal' => false, 'table' => false], + ['key' => 'delAddrPLZ', 'text' => 'PLZ', 'required' => true, 'modal' => false, 'table' => false], + ['key' => 'editor', 'text' => 'Bearbeiter', 'required' => true, 'modal' => ['type' => 'select'], 'table' => ['filter' => 'select']], + ['key' => 'note', 'text' => 'Notiz', 'required' => true, 'modal' => false, 'table' => false], + ['key' => 'sum', 'text' => 'Summe', 'required' => false, 'modal' => false, 'table' => ['class' => 'text-right']], + ['key' => 'status', 'text' => 'Status', 'required' => false, 'modal' => ['type' => 'select', 'items' => []], 'table' => ['filter' => 'select']], + ['key' => 'positions', 'text' => 'Positionen', 'required' => true, 'modal' => false, 'table' => false], + ['key' => 'extReference', 'text' => 'Externe Referenz', 'required' => true, 'modal' => false], + ['key' => 'createBy', 'text' => 'Erstellt von', 'required' => true, 'modal' => ['type' => 'select'], 'table' => ['filter' => 'select']], + ['key' => 'create', 'text' => 'Erstellt', 'required' => true, 'modal' => false], + ['key' => 'actions', 'text' => 'Aktionen', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center']], + ]; + //@formatter:on protected array $infoMessages = ['create' => 'Bestellung wurde erfolgreich erstellt.', 'update' => 'Bestellung wurde aktualisiert.', 'delete' => 'Bestellung wurde gelöscht', 'noChanges' => 'Keine Änderungen',]; + protected function prepareCrudConfig(): void { + $editorColumnIndex = array_search('editor', array_column($this->columns, 'key')); + $this->columns[$editorColumnIndex]['modal']['items'] = array_map(function ($user) { + return ['value' => intval($user->id), 'text' => $user->name]; + }, UserModel::search(['employee' => true])); + + $statusIndex = array_search('status', array_column($this->columns, 'key')); + $this->columns[$statusIndex]['modal']['items'] = [ + ['value' => 'new', 'text' => 'Neu'], + ['value' => 'accepted', 'text' => 'Akzeptiert'], + ['value' => 'ordered', 'text' => 'Bestellt'], + ['value' => 'sent', 'text' => 'Versendet'], + ['value' => 'partiallyDelivered', 'text' => 'Teilweise geliefert'], + ['value' => 'fullyDelivered', 'text' => 'Geliefert'], + ['value' => 'cancelled', 'text' => 'Storniert'], + ]; + } + protected function beforeCreate(): bool { - $currentCount = WarehouseOrderModel::count(['create' => ['from' => strtotime(date('Y-01-01'))]]); - $this->postData['orderNumber'] = 'PO' . date('Y') . '-' . str_pad($currentCount + 1, 4, '0', STR_PAD_LEFT); + $this->postData['orderNumber'] = 'PO' . date('Y') . '-' . str_pad(WarehouseOrderModel::count(['create' => ['from' => strtotime(date('Y-01-01'))]]) + 1, 4, '0', STR_PAD_LEFT); return true; } protected function getArticleDistributorDataAction() { - $data = []; - $article = $this->request->articleId; + $articleId = $this->request->articleId; + if ($this->request->allDistributor === 'true') self::returnJson(array_map(fn($d) => ['id' => $d->id, + 'name' => $d->name], WarehouseDistributorModel::getAll())); + else if (!empty($articleId)) self::returnJson(array_map(fn($d) => ['id' => $d->distributorId, + 'name' => WarehouseDistributorModel::get($d->distributorId)->name, + 'purchasePrice' => $d->purchasePrice, + 'externalArticleNumber' => $d->externalArticleNumber], WarehouseArticleDistributorModel::getAll(['articleId' => $articleId]))); + else self::returnJson([]); + } - if ($this->request->allDistributor === 'true') { - foreach (WarehouseDistributorModel::getAll() as $distributor) { - $data[] = [ - 'id' => $distributor->id, - 'name' => $distributor->name, - ]; - } - } elseif (!empty($article)) { - foreach (WarehouseArticleDistributorModel::getAll(['articleId' => $this->request->articleId]) as $distributor) { - $data[] = [ - 'id' => $distributor->distributorId, - 'name' => WarehouseDistributorModel::get($distributor->distributorId)->name, - 'purchasePrice' => $distributor->purchasePrice, - 'externalArticleNumber' => $distributor->externalArticleNumber, - ]; - } + protected function getByIdParse(array $order): array { + $order['positions'] = json_decode($order['positions'], true); + + foreach ($order['positions'] as &$position) { + $position['distributorName'] = WarehouseDistributorModel::get($position['distributorId'])->name; + $position['articleName'] = WarehouseArticleModel::get($position['article'])->title; } - self::returnJson($data); + return $order; } - protected function beforeUpdate($postData): bool { - (new WarehouseHistoryController)->create($postData, $this->mod); - return true; + protected function createPDFAction() { + $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; + + $order['positions'][$key] = $position; + } + + $pdf_vars = ['order' => $order, + 'distributor' => WarehouseDistributorModel::get($distributorId), + "bank_iban" => TT_INVOICE_BANK_IBAN, + "bank_bic" => TT_INVOICE_BANK_BIC, + "bank_bank" => TT_INVOICE_BANK_BANK, + "bank_owner" => TT_INVOICE_BANK_OWNER]; + + + $countryText = CountryModel::search(['id' => WarehouseDistributorModel::get($distributorId)->countryId])[0]->name; + + $headerHtml = file_get_contents(BASEDIR . "/Layout/default/WarehouseOrder/PDF_HEADER.html"); + $headerHtml = str_replace("{{ basedir }}", BASEDIR, $headerHtml); + $headerHtml = str_replace("{{ externalReference }}","Ihre Referenz: ". $order['extReference'], $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); + + $headerHtml = str_replace("{{ billingAddressLine_1 }}", "Xinon GmbH", $headerHtml); + $headerHtml = str_replace("{{ billingAddressLine_2 }}", "Fladnitz im Raabtal 150", $headerHtml); + $headerHtml = str_replace("{{ billingAddressLine_3 }}", "8322 Studenzen", $headerHtml); + $headerHtml = str_replace("{{ billingAddressLine_4 }}", "Österreich", $headerHtml); + $headerHtml = str_replace("{{ billingAddressLine_5 }}", "einkauf@xinon.at", $headerHtml); + $headerHtml = str_replace("{{ billingAddressLine_6 }}", "Referenz: ". $order["orderNumber"] . "", $headerHtml); + + // if order dellAddrLine is Fladnitz im Raabtal 150 we need to set all template strings to empty + + $chk = $order['delAddrLine'] == "Fladnitz im Raabtal 150"; + + $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); + + + $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); + + $footerFile = BASEDIR . "/var/temp/order_footer-" . date("U") . "-" . rand(1000, 9999) . ".html"; + file_put_contents($footerFile, $footerHtml); + + + $pdf = new PdfForm("WarehouseOrder/PDF_MAIN", $pdf_vars); + $wkhtmltopdfArgs = "--header-html $headerFile --footer-html $footerFile"; + $filename = $pdf->render($wkhtmltopdfArgs); + + // 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); + } - protected function getHistoryAction() { - self::returnJson((new WarehouseHistoryController)->getHistory($this->request->id, $this->mod, $this->columns)); - } + } \ No newline at end of file diff --git a/application/WarehouseOrder/WarehouseOrderModel.php b/application/WarehouseOrder/WarehouseOrderModel.php index 84070625f..ac4677800 100644 --- a/application/WarehouseOrder/WarehouseOrderModel.php +++ b/application/WarehouseOrder/WarehouseOrderModel.php @@ -1,5 +1,5 @@ 'title', 'text' => 'Titel', 'required' => true, 'table' => ['class' => 'text-nowrap', 'priority' => 9]], + ['key' => 'description', 'text' => 'Beschreibung', 'required' => true, 'table' => ['class' => 'text-nowrap']], + ['key' => 'createBy', 'text' => 'Erstellt von', 'required' => true, 'type' => 'select', 'table' => ['class' => 'text-nowrap', 'filter' => 'select'], 'modal' => ['items' => [], 'type' => 'select']], + ['key' => 'create', 'text' => 'Erstellt am', 'required' => true, 'table' => ['filter' => 'date', 'class' => 'text-center']], + ['key' => 'address', 'text' => 'Adresse', 'required' => true, 'type' => 'autocomplete', 'table' => ['class' => 'text-nowrap', 'filter' => false], 'modal' => ['apiUrl' => '/Address/api?do=findAddress', 'items' => '/Address/api?do=findAddress', 'type' => 'autocomplete']], + ['key' => 'status', 'text' => 'Status', 'required' => true, 'table' => ['filter' => 'select'], 'modal' => [ 'type' => 'select', 'items' => [ ['value' => 'erstellt', 'text' => 'Erstellt'], ['value' => 'in_bearbeitung', 'text' => 'In Bearbeitung'], ['value' => 'erledigt', 'text' => 'Erledigt'], ['value' => 'verrechnet', 'text' => 'Verrechnet']]]] + ]; + + + protected array $additionalActions = [ + ]; + + protected array $infoMessages = [ + 'create' => 'Projekt wurde erstellt', + 'update' => 'Projekt wurde aktualisiert', + 'delete' => 'Projekt wurde gelöscht', + 'noChanges' => 'Keine Änderungen', + ]; + //@formatter:on + + public function prepareCrudConfig() { + $users = array_map(function($user) { + return ['value' => $user->id, 'text' => $user->name]; + }, UserModel::search(['employee' => true])); + + $this->columns[1]['modal']['items'] = $users; + } +} \ No newline at end of file diff --git a/application/WarehouseProject/WarehouseProjectModel.php b/application/WarehouseProject/WarehouseProjectModel.php new file mode 100644 index 000000000..a1aa8f680 --- /dev/null +++ b/application/WarehouseProject/WarehouseProjectModel.php @@ -0,0 +1,15 @@ +getEnvironment() == "thetool") { + $table = $this->table("WarehouseOrder"); + $table->addColumn("extReference", "string", ["length" => 255, "null" => true]); + $table->save();; + } + } + + public function down(): void { + if ($this->getEnvironment() == "thetool") { + $table = $this->table("WarehouseOrder"); + $table->removeColumn("extReference"); + $table->save();; + } + } +} diff --git a/lib/Helper/Helper.php b/lib/Helper/Helper.php index 3668e1b8d..aa31b8c6e 100644 --- a/lib/Helper/Helper.php +++ b/lib/Helper/Helper.php @@ -8,7 +8,7 @@ class Helper { * @param string $columnName The name of the column in the database table. * @return string The SQL condition generated based on the filter value and column name. */ - public static function generateFilterCondition($filterValue, string $columnName, bool $exactMatch = false): string { + public static function generateFilterCondition($filterValue, $columnName, bool $exactMatch = false): string { $sql = ""; if (is_array($filterValue)) { @@ -30,6 +30,9 @@ class Helper { } else if (!empty($filterValue)) { if ($exactMatch) { $sql .= " AND `$columnName` = '" . $filterValue . "'"; + } else if (strpos($columnName, "|") !== false) { + foreach (explode(" ", $filterValue) as $item) + $sql .= " AND CONCAT(" . join(",", explode("|", $columnName)) . ") LIKE '%" . str_replace("%", "", $item) . "%'"; } else if ($filterValue[0] === "%") { $sql .= " AND `$columnName` LIKE '" . $filterValue . "'"; } else if ($filterValue[strlen($filterValue) - 1] === "%") { diff --git a/lib/TTCrud/TTCrud.php b/lib/TTCrud/TTCrud.php index 8288b6123..a79537acb 100644 --- a/lib/TTCrud/TTCrud.php +++ b/lib/TTCrud/TTCrud.php @@ -10,6 +10,8 @@ * @property array $additionalJSVariables * @property array $infoMessages * @property bool $onlyView + * @property array $defaultOrder + * @property array $autocompleteColumns */ class TTCrud extends mfBaseController { public User $user; @@ -251,18 +253,20 @@ class TTCrud extends mfBaseController { } protected function autocompleteAction() { - $searchedID = $this->request->searchedID; - $textKey = property_exists($this->model, 'name') ? 'name' : 'title'; - - if (strlen($searchedID) > 0) { - $filter = ['id' => $searchedID]; + if (strlen($this->request->searchedID) > 0) { + $filter = ['id' => $this->request->searchedID]; $data = $this->model::getAll($filter, 10); } else { - $filter = [$textKey => $this->request->q . '%']; - $data = $this->model::getAll($filter, 10); + if (isset($this->autocompleteColumns) && is_array($this->autocompleteColumns)) { + $filterKey = join('|', $this->autocompleteColumns); + } else { + $filterKey = $textKey; + } + + $data = []; if (count($data) < 11) { - $filter = [$textKey => $this->request->q]; + $filter = [$filterKey => '%' . $this->request->q . '%']; $lazyData = $this->model::getAll($filter, 10); $data = array_merge($data, $lazyData); $data = array_unique($data, SORT_REGULAR); @@ -270,7 +274,6 @@ class TTCrud extends mfBaseController { } } - self::returnJson(array_map(function ($item) use ($textKey) { return ['value' => $item->id, 'text' => $item->$textKey]; }, $data)); @@ -285,6 +288,7 @@ class TTCrud extends mfBaseController { } $data = (array) $this->model::get($id); + if (method_exists($this, 'getByIdParse') && !isset($_GET['disableParse'])) $data = $this->getByIdParse($data); self::returnJson($data); } } diff --git a/lib/TTCrudBaseModel/TTCrudBaseModel.php b/lib/TTCrudBaseModel/TTCrudBaseModel.php index 3184ffa10..4815dde9f 100644 --- a/lib/TTCrudBaseModel/TTCrudBaseModel.php +++ b/lib/TTCrudBaseModel/TTCrudBaseModel.php @@ -81,17 +81,13 @@ class TTCrudBaseModel { } - public static function get($id, $die= false): TTCrudBaseModel { + public static function get($id): TTCrudBaseModel { $FronkDB = FronkDB::singleton(); $db = $FronkDB->link; $id = $db->real_escape_string($id); $table = self::getTable(); $sql = "SELECT * FROM `$table` WHERE `id` = $id"; - if($die) { - die($sql); - } - $result = $db->query($sql); // as TTCRudBaseModel is abstract, we need to get the class name of the child class $class = get_called_class(); @@ -109,16 +105,16 @@ class TTCrudBaseModel { return $result->fetch_assoc()['count']; } - public static function getSQLFilter($filter): string { - if (empty($filter)) { - return ""; - } + public static function getSQLFilter(array $filter): string { + if (empty($filter)) return ''; + $sql = 'WHERE 1=1'; + $calledClass = get_called_class(); - $sql = "WHERE 1=1"; foreach ($filter as $key => $value) { - if (!property_exists(get_called_class(), $key)) { - http_response_code(500); - throw new Exception("Field $key does not exist in " . get_called_class()); + foreach (explode('|', $key) as $column) { + if (!property_exists($calledClass, $column)) { + throw new InvalidArgumentException("Field $column does not exist in $calledClass"); + } } $sql .= Helper::generateFilterCondition($value, $key, gettype($value) === "integer"); } diff --git a/public/js/pages/WarehouseOffer/WarehouseOffer.css b/public/js/pages/WarehouseOffer/WarehouseOffer.css new file mode 100644 index 000000000..7951745f0 --- /dev/null +++ b/public/js/pages/WarehouseOffer/WarehouseOffer.css @@ -0,0 +1,14 @@ +@media (min-width: 992px) { + .modal-lg, .modal-xl { + /*max width either 90% or 1120px*/ + max-width: min(90vw, 1120px) !important; + } +} + +@media (max-width: 992px) { + .warehouse-order-modal-positions-entry-container { + display: grid; + grid-template-columns: 1fr 1fr !important; + grid-gap: 10px; + } +} \ No newline at end of file diff --git a/public/js/pages/WarehouseOffer/WarehouseOffer.js b/public/js/pages/WarehouseOffer/WarehouseOffer.js new file mode 100644 index 000000000..fe1365051 --- /dev/null +++ b/public/js/pages/WarehouseOffer/WarehouseOffer.js @@ -0,0 +1,178 @@ +Vue.component('warehouse-offer-modal', { + props: { + id: {type: [String, Number], required: true}, + mode: {type: String, default: 'edit'} + }, + template: ` + +

Angebotdetails

+ + + + +
+

Kundenadresse

+
+ + + + + +
+
+

Positionen

+ +
+

Alternative Artikel

+ +
+ + + + +
+ +
+
+ `, + data() { + return { + window: window, + positionsConfig: { + fields: { + article: { + type: 'autocomplete', + label: 'Artikel', + apiUrl: '/WarehouseArticle/autoComplete', + customFieldReference: 'WarehouseArticle', + }, + amount: {type: 'input', label: 'Menge', inputType: 'number'}, + unit: {type: 'input', label: 'Einheit'}, + articleNumber: {type: 'input', label: 'Artikelnummer'}, + unitPrice: {type: 'input', label: 'Einzelpreis', inputType: 'number'}, + discount: {type: 'input', label: 'Rabatt (%)', inputType: 'number'}, + }, + validateForm: (formData) => { + const requiredFields = ['article', 'amount', 'unitPrice']; + for (const field of requiredFields) { + if (!formData[field]) { + window.notify('error', `Bitte füllen Sie ${this.positionsConfig.fields[field].label} aus`); + return false; + } + } + return true; + }, + }, + alternativePositionsConfig: { + fields: { + article: {type: 'input', label: 'Artikel'}, + description: {type: 'textarea', label: 'Beschreibung'}, + }, + }, + paymentTerms: [ + {value: 'net30', text: '30 Tage netto'}, + {value: 'net60', text: '60 Tage netto'}, + {value: 'immediate', text: 'Sofort fällig'}, + ], + deliveryTerms: [ + {value: 'ex_works', text: 'Ab Werk'}, + {value: 'free_delivery', text: 'Frei Haus'}, + {value: 'fob', text: 'FOB'}, + ], + closingTexts: [ + {value: 'standard', text: 'Standardtext'}, + {value: 'custom1', text: 'Angepasster Text 1'}, + {value: 'custom2', text: 'Angepasster Text 2'}, + ], + offer: { + editor: window.TT_CONFIG['USER_ID'], + customerNumber: '', + reference: '', + purpose: '', + customerName: '', + customerStreet: '', + customerZip: '', + customerCity: '', + customerVAT: '', + positions: [], + alternativePositions: [], + totalDiscount: 0, + paymentTerms: 'net30', + deliveryTerms: 'ex_works', + closingText: 'standard', + notes: '', + } + } + }, + async mounted() { + if (this.id !== 'create') { + const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOffer/getById`, {params: {id: this.id}}); + this.offer = response.data; + this.offer.positions = JSON.parse(this.offer.positions); + this.offer.alternativePositions = JSON.parse(this.offer.alternativePositions); + } + }, + methods: { + async submit() { + if (this.offer.positions.length === 0) return window.notify('error', 'Bitte fügen Sie mindestens eine Position hinzu.'); + + const url = this.id === 'create' + ? `${window.TT_CONFIG["BASE_PATH"]}/WarehouseOffer/create` + : `${window.TT_CONFIG["BASE_PATH"]}/WarehouseOffer/update`; + + const response = await axios.post(url, this.offer); + + if (response.data.success) { + window.notify('success', response.data.message ?? 'Angebot erfolgreich gespeichert'); + this.$emit('close'); + } else { + window.notify('error', + response.data.errors ? Object.values(response.data.errors).join('
') : response.data.message || 'Ein Fehler ist aufgetreten'); + } + }, + async fetchArticleData(article) { + if (typeof article === 'number') { + const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseArticle/getById`, {params: {id: article}}); + this.$refs.positionsManager.updateField('articleNumber', response.data.articleNumber); + this.$refs.positionsManager.updateField('unitPrice', + Object.values(JSON.parse(response.data.cheapestSellPrice)).find(price => price.title === 'Verkauf').price); + this.$refs.positionsManager.updateField('unit', response.data.unit); + } + }, + }, +}); + +Vue.component('warehouse-offer', { + template: ` + + + + + + + + `, + data() { + return { + window: window, + offerModalId: null, + } + }, +}); diff --git a/public/js/pages/WarehouseOrder/WarehouseOrder.css b/public/js/pages/WarehouseOrder/WarehouseOrder.css index 878a34a77..9b3746d82 100644 --- a/public/js/pages/WarehouseOrder/WarehouseOrder.css +++ b/public/js/pages/WarehouseOrder/WarehouseOrder.css @@ -24,4 +24,29 @@ grid-template-columns: 1fr 1fr !important; grid-gap: 10px; } -} \ No newline at end of file +} + + +/* Expanded Row Styling */ +.order-summary { + padding: 1rem; +} +.position-item { + margin-bottom: 1rem; + border: 1px solid #ddd; + border-radius: 4px; +} +.position-header { + background-color: #f0f0f0; + padding: 0.5rem; + font-weight: bold; +} +.position-details { + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr; + padding: 0.5rem; +} +.field-item { + margin-bottom: 0.5rem; +} + diff --git a/public/js/pages/WarehouseOrder/WarehouseOrder.js b/public/js/pages/WarehouseOrder/WarehouseOrder.js index fcd97373a..c9b937d86 100644 --- a/public/js/pages/WarehouseOrder/WarehouseOrder.js +++ b/public/js/pages/WarehouseOrder/WarehouseOrder.js @@ -16,6 +16,7 @@ Vue.component('warehouse-order-modal', { sm row v-model="order.editor"/> +

Positionen

@@ -80,6 +81,7 @@ Vue.component('warehouse-order-modal', { }, }, order: { + extReference: '', delAddrName: 'XINON GmbH', delAddrLine: 'Fladnitz im Raabtal 150', delAddrPLZ: '8322', @@ -92,11 +94,13 @@ Vue.component('warehouse-order-modal', { } }, async mounted() { - if (this.id !== 'create') { - const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/getById`, {params: {id: this.id}}); - response.data.positions = JSON.parse(response.data.positions); - this.order = response.data; - } + if (this.id === 'create') return; + + console.log(this.id); + + const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/getById?disableParse`, {params: {id: this.id}}); + response.data.positions = JSON.parse(response.data.positions); + this.order = response.data; }, methods: { async submit() { @@ -112,18 +116,20 @@ Vue.component('warehouse-order-modal', { positions: this.order.positions.filter(position => position.distributorId === distributorId) } ); - if (response.data.success) window.notify('success', response.data.message ?? 'Bestellung erfolgreich erstellt'); - else window.notify('error', + if (response.data.success) { + this.$emit('close'); + window.notify('success', response.data.message ?? 'Bestellung erfolgreich erstellt'); + } else window.notify('error', response.data.errors ? Object.values(response.data.errors).join('
') : response.data.message || 'Ein Fehler ist aufgetreten'); } } else { const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/update`, this.order); - if (response.data.success) window.notify('success', response.data.message ?? 'Bestellung erfolgreich aktualisiert'); - else window.notify('error', + if (response.data.success) { + this.$emit('close'); + window.notify('success', response.data.message ?? 'Bestellung erfolgreich aktualisiert'); + } else window.notify('error', response.data.errors ? Object.values(response.data.errors).join('
') : response.data.message || 'Ein Fehler ist aufgetreten'); } - this.$emit('close'); - }, async fetchDistributors(article) { const url = `${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/getArticleDistributorData`; @@ -148,22 +154,93 @@ Vue.component('warehouse-order-modal', { }, }); - -Vue.component('warehouse-order', { +Vue.component('warehouse-order-detail', { //language=Vue template: ` - - - - - - - `, data() { - return { - window: window, - orderModalId: null, + + + + + + + `, + props: { + id: {type: [String, Number], required: true} + }, + data() { + return { + window: window, + order: {}, + loading: true } }, -}) \ No newline at end of file + async mounted() { + const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/getById`, {params: {id: this.id}}); + this.order = response.data; + this.loading = false; + }, +}); + +Vue.component('warehouse-order', { + template: ` + + + + + + + + + + + + `, + data() { + return { + orderModalId: null, + } + }, + methods: { + closeOrderModal() { + this.orderModalId = null; + this.$refs.table.$refs.table.refreshTable(); + }, + calculateSum(positions) { + return positions.reduce((sum, position) => sum + position.amount * position.buyPrice, 0); + } + } +}); diff --git a/public/js/pages/WarehouseProject/WarehouseProject.css b/public/js/pages/WarehouseProject/WarehouseProject.css new file mode 100644 index 000000000..7951745f0 --- /dev/null +++ b/public/js/pages/WarehouseProject/WarehouseProject.css @@ -0,0 +1,14 @@ +@media (min-width: 992px) { + .modal-lg, .modal-xl { + /*max width either 90% or 1120px*/ + max-width: min(90vw, 1120px) !important; + } +} + +@media (max-width: 992px) { + .warehouse-order-modal-positions-entry-container { + display: grid; + grid-template-columns: 1fr 1fr !important; + grid-gap: 10px; + } +} \ No newline at end of file diff --git a/public/js/pages/WarehouseProject/WarehouseProject.js b/public/js/pages/WarehouseProject/WarehouseProject.js new file mode 100644 index 000000000..3bfeb5578 --- /dev/null +++ b/public/js/pages/WarehouseProject/WarehouseProject.js @@ -0,0 +1,161 @@ +Vue.component('warehouse-project-modal', { + props: { + id: { type: [String, Number], required: true }, + mode: { type: String, default: 'edit' } + }, + template: ` + +
+

Projektübersicht

+ + + +
+

Zeitraum

+
+ + +
+ +
+

Beteiligte Personen

+ + + +
+

Projektübersicht

+ + + +
+

Lagerort

+ + +
+ +
+
+ `, + data() { + return { + window: window, + participantsOptions: [ + { value: 1, text: 'Person A' }, + { value: 2, text: 'Person B' }, + { value: 3, text: 'Person C' } + // Add more participants as needed + ], + positionsConfig: { + fields: { + article: { + type: 'autocomplete', + label: 'Artikel', + apiUrl: '/WarehouseArticle/autoComplete', + customFieldReference: 'WarehouseArticle', + }, + hoursRequired: { type: 'input', label: 'Benötigte Stunden', inputType: 'number' }, + amountRequired: { type: 'input', label: 'Benötigte Menge', inputType: 'number' }, + description: { type: 'textarea', label: 'Beschreibung' } + }, + validateForm(formData) { + const requiredFields = ['article', 'hoursRequired', 'amountRequired']; + for (const field of requiredFields) { + if (!formData[field]) { + window.notify('error', `Bitte füllen Sie ${this.positionsConfig.fields[field].label} aus`); + return false; + } + } + return true; + } + }, + project: { + projectNumber: '', + description: '', + startDate: null, + endDate: null, + participants: [], + additionalParticipants: '', + totalSum: 0, + positions: [], + storageLocation: '', + notes: '' + } + }; + }, + async mounted() { + if (this.id !== 'create') { + const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseProject/getById`, { params: { id: this.id } }); + this.project = response.data; + } else { + this.project.projectNumber = await this.generateProjectNumber(); + } + }, + methods: { + async submit() { + if (!this.project.description) return window.notify('error', 'Bitte geben Sie eine Beschreibung ein.'); + + const url = this.id === 'create' + ? `${window.TT_CONFIG["BASE_PATH"]}/WarehouseProject/create` + : `${window.TT_CONFIG["BASE_PATH"]}/WarehouseProject/update`; + + const response = await axios.post(url, this.project); + + if (response.data.success) { + window.notify('success', response.data.message ?? 'Projekt erfolgreich gespeichert'); + this.$emit('close'); + } else { + window.notify('error', response.data.errors ? Object.values(response.data.errors).join('
') : response.data.message || 'Ein Fehler ist aufgetreten'); + } + }, + async fetchArticleData(article) { + if (typeof article === 'number') { + const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseArticle/getById`, { params: { id: article } }); + this.$refs.positionsManager.updateField('description', response.data.description); + } + }, + async generateProjectNumber() { + const currentCount = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseProject/count`); + return `PRJ-${new Date().getFullYear()}-${String(currentCount.data + 1).padStart(4, '0')}`; + } + } +}); + + +Vue.component('warehouse-project', { + template: ` + + + + + + + + `, + data() { + return { + window: window, + projectModalId: null, + } + }, +}); diff --git a/public/plugins/vue/tt-components/tt-position-manager.js b/public/plugins/vue/tt-components/tt-position-manager.js index 182f5b04e..7d8996bbd 100644 --- a/public/plugins/vue/tt-components/tt-position-manager.js +++ b/public/plugins/vue/tt-components/tt-position-manager.js @@ -33,6 +33,13 @@ Vue.component('tt-positions-manager', { :api-url="window.TT_CONFIG['BASE_PATH'] + field.apiUrl" sm /> + + {{ window.moment(row[key] * 1000).format('DD.MM.YYYY HH:mm:ss') }}