Updated Warehouse

This commit is contained in:
Luca Haid
2025-02-04 19:06:08 +01:00
parent 514d5d5d7e
commit 7c8af7aed4
24 changed files with 1126 additions and 99 deletions

View File

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html>
<head>
<title>Xinon Rechnung</title>
<meta charset="utf-8" />
</head>
<body style="border:0; margin: 0;font-family: sans-serif, Verdana;font-size: 11px;" onload="subst()">
<script>
function subst() {
var vars = {};
var query_strings_from_url = document.location.search.substring(1).split('&');
for (var query_string in query_strings_from_url) {
if (query_strings_from_url.hasOwnProperty(query_string)) {
var temp_var = query_strings_from_url[query_string].split('=', 2);
vars[temp_var[0]] = decodeURI(temp_var[1]);
}
}
var css_selector_classes = ['page', 'frompage', 'topage', 'webpage', 'section', 'subsection', 'date', 'isodate', 'time', 'title', 'doctitle', 'sitepage', 'sitepages'];
for (var css_class in css_selector_classes) {
if (css_selector_classes.hasOwnProperty(css_class)) {
var element = document.getElementsByClassName(css_selector_classes[css_class]);
for (var j = 0; j < element.length; ++j) {
element[j].textContent = vars[css_selector_classes[css_class]];
}
}
}
}
</script>
<div style="margin-bottom: 16px;height: 1px"></div>
<div style="color:grey;text-align: center;margin-bottom: 0">
<span>XINON GmbH | Fladnitz 150 | 8322 Studenzen</span><br>
<span>Tel.: +43 3115 40800 | E-Mail: office@xinon.at</span><br>
<span>UID: ATU68711968 | FN: 416556h | LG: Feldbach</span><br>
<span>IBAN: {{ bank_iban }} | BIC: {{ bank_bic }}</span><br>
</div>
<div style="text-align: right">Seite <span class="page"></span> von <span class="topage"></span></div>
<div style="margin-top: 16px;height: 1px"></div>
</body>
</html>

View File

@@ -0,0 +1,92 @@
<!DOCTYPE html>
<html>
<head>
<title>XINON Shipping Note Header</title>
<meta charset="utf-8" />
<style>
body {
border: 0;
margin: 0;
font-family: sans-serif, Verdana;
font-size: 12px;
}
.info-table {
border-collapse: collapse;
width: 100%;
}
.customer-details {
vertical-align: bottom;
font-size: 14px;
padding-left: 30pt;
width: 35%;
}
.invoice-details {
border: 2px solid #e1e1e1;
padding: 6px;
}
.invoice-details td {
text-align: left;
}
.invoice-details td:first-child {
text-align: right;
}
.separator {
margin-top: 24px;
height: 1px;
}
#topSpacer {
margin-bottom: 32px;
height: 20px;
}
</style>
</head>
<body>
<div id="topSpacer"></div>
<div style="height: 50px; margin-bottom: 48px">
<img alt="Xinon Logo" src="{{ basedir }}/public/assets/images/xinon-full.png" style="text-align:left;height: 85px;">
</div>
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td class="customer-details" style="float: left">
<h3>Lieferant</h3>
<div>{{ addressLine_1 }}</div>
<div>{{ addressLine_2 }}</div>
<div>{{ addressLine_3 }}</div>
<div>{{ addressLine_4 }}</div>
<div style="margin-bottom: 12pt"></div>
<div>{{ externalReference }}</div>
</td>
<td class="customer-details" align="top">
<h3>Rechnungsadresse</h3>
<div>{{ billingAddressLine_1 }}</div>
<div>{{ billingAddressLine_2 }}</div>
<div>{{ billingAddressLine_3 }}</div>
<div>{{ billingAddressLine_4 }}</div>
<div>{{ billingAddressLine_5 }}</div>
<div>{{ billingAddressLine_6 }}</div>
</td>
<td class="customer-details" style="float: right">
<div>{{ shippingAddressLine_1 }}</div>
<div>{{ shippingAddressLine_2 }}</div>
<div>{{ shippingAddressLine_3 }}</div>
<div>{{ shippingAddressLine_4 }}</div>
</td>
</tr>
</table>
<div class="separator"></div>
</body>
</html>

View File

@@ -0,0 +1,133 @@
<?php
/**
* @var string $ressourcePathPrefix
* @var WarehouseOrderModel $order
* @var Array $positions
* @var Array $textElements
*/
$this->setReturnValue(['filename' => $order["id"] . ".pdf"]);
?>
<!DOCTYPE html>
<html>
<head>
<title>Bestellung</title>
<meta charset="utf-8" />
<style>
body {
margin-top: 0;
/*padding-top: 20pt;*/
font-family: "Open Sans", sans-serif, Verdana;
font-size: 12px;
}
tr {
page-break-inside: avoid;
}
.uneven {
background-color: #ebebeb;
}
table tr td:last-child {
text-align: right;
}
.additionalRow td:first-child {
text-align: left;
padding-left: 20pt;
}
th {
height: 28px;
}
#invoiceTable tr *:nth-child(5),
#invoiceTable tr *:nth-child(4),
#invoiceTable tr *:nth-child(3) {
text-align: right;
}
#invoiceTable tr *:not(:first-child) {
padding: 4px 0;
}
#invoiceTable tr td {
font-size: 11px;
}
tr.position td {
vertical-align: top;
}
tr.position td:first-child {
vertical-align: middle !important;
padding-left: 4pt;
}
#invoiceTable tr td:first-child {
max-width: 200pt;
}
</style>
</head>
<body>
<div>
<!--
TODO: enable option for showing prices
vertauschen
Die gelieferte Ware bleibt bis zur vollständigen Bezahlung in unserem Eigentum.
-->
<h2 style="text-align: center;color: #005384">XINON Lieferantenbestellung vom <?=date("d.m.Y", $order["create"])?></h2>
<table style="border-collapse: collapse; width: 100%;" id="invoiceTable">
<tr style="font-weight: bold; border-bottom: 1px solid black;" class="uneven">
<th style="text-align: center;padding-right: 6pt">Position</th>
<th style="text-align: center;padding-right: 6pt">Artikel</th>
<th style="text-align: center;padding-right: 6pt">Art.-Nr. Lieferant</th>
<th style="text-align: right">Menge</th>
<th style="text-align: right">Einzelpreis</th>
<th style="text-align: right;padding-right: 8pt">Gesamtpreis</th>
</tr>
<?php $i = 0; foreach($order['positions'] as $p):?>
<tr class="position <?=($i%2 == 0) ? "even" : "uneven" ?>">
<td style="text-align: center;"><?= $i + 1 ?></td>
<td style="text-align: left;padding-right: 8pt"><?=$p["articleName"]?></td>
<td style="text-align: center;padding-right: 8pt"><?=$p["distributorArticleNumber"]?></td>
<td style="text-align: right"><?=$p["amount"]?></td>
<td style="text-align: right"><?=number_format($p["buyPrice"], 2, ",", ".")?> €</td>
<td style="text-align: right;padding-right: 8pt"><?=number_format($p["amount"] * $p["buyPrice"], 2, ",", ".")?> €</td>
</tr>
<?php $i++; endforeach;?>
<!-- display a grey like header sum with top border to differentiate 2nd last td = Summe , last td is the calculated value both bold-->
<tr class="uneven">
<?php
$sum = 0;
foreach($order['positions'] as $p){
$sum += $p["amount"] * $p["buyPrice"];
}
?>
<td colspan="5" style="text-align: right;border-top: 1px solid black;font-weight: bold
;border-bottom: 1px solid black;
">Summe</td>
<td style="text-align: right;border-top: 1px solid black;font-weight: bold
;border-bottom: 1px solid black;
"><?=number_format($sum, 2, ",", ".")?> €</td>
</tr>
</table>
<div>
<h3>Anmerkungen</h3>
<p>
<?=$order["note"]?>
</p>
</div>
</body>
</html>

View File

@@ -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]] ['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 = [ protected array $additionalActions = [
['key' => 'openHistory','title' => 'Historie','class' => 'fas fa-history text-secondary'], ['key' => 'openHistory','title' => 'Historie','class' => 'fas fa-history text-secondary'],
['key' => 'editDistributorEntries','title' => 'Lieferanten','class' => 'fas fa-truck text-cyan'], ['key' => 'editDistributorEntries','title' => 'Lieferanten','class' => 'fas fa-truck text-cyan'],

View File

@@ -0,0 +1,9 @@
<?php
/**
* @property mixed|null $name
*/
class WarehouseOffer extends mfBaseModel
{
}

View File

@@ -0,0 +1,56 @@
<?php
class WarehouseOfferController extends TTCrud {
protected string $headerTitle = 'Angebote';
protected bool $createText = false;
protected array $columns = [
['key' => '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));
}
}

View File

@@ -0,0 +1,52 @@
<?php
/**
* Class WarehouseOfferModel
*
* Represents a warehouse offer with customer details and related metadata.
*
* @property int $id Unique identifier for the warehouse offer
* @property string $offerNumber Unique offer number
* @property string $customerNumber Customer number
* @property string $customerName Name of the customer
* @property string $customerStreet Street address of the customer
* @property string $customerCity City of the customer
* @property string $customerZip Postal code of the customer
* @property string $customerVAT VAT number of the customer
* @property int $editor ID of the editor who last modified the offer
* @property string $purpose Purpose of the offer
* @property string $positions Details about positions in the offer
* @property string $alternativePositions Details about alternative positions in the offer
* @property float $totalDiscount Total discount applied to the offer
* @property string $paymentTerms Payment terms for the offer
* @property string $deliveryTerms Delivery terms for the offer
* @property string $closingText Closing text for the offer
* @property string $notes Additional notes for the offer
* @property string $status Current status of the offer
* @property float $totalAmount Total amount of the offer
* @property int $create Timestamp of the offer creation
* @property int $createBy ID of the user who created the offer
*/
class WarehouseOfferModel extends TTCrudBaseModel {
public int $id;
public string $offerNumber;
public string $customerNumber;
public string $customerName;
public string $customerStreet;
public string $customerCity;
public string $customerZip;
public string $customerVAT;
public int $editor;
public string $purpose;
public string $positions;
public string $alternativePositions;
public float $totalDiscount;
public string $paymentTerms;
public string $deliveryTerms;
public string $closingText;
public string $notes;
public string $status;
public float $totalAmount;
public int $create;
public int $createBy;
}

View File

@@ -1,77 +1,159 @@
<?php <?php
//TODO: enable switching distributors in the order preview
class WarehouseOrderController extends TTCrud { class WarehouseOrderController extends TTCrud {
protected string $headerTitle = 'Lieferantenbestellungen'; protected string $headerTitle = 'Lieferantenbestellungen';
protected bool $createText = false; protected bool $createText = false;
protected array $columns = [
['key' => '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 $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.', protected array $infoMessages = ['create' => 'Bestellung wurde erfolgreich erstellt.',
'update' => 'Bestellung wurde aktualisiert.', 'update' => 'Bestellung wurde aktualisiert.',
'delete' => 'Bestellung wurde gelöscht', 'delete' => 'Bestellung wurde gelöscht',
'noChanges' => 'Keine Änderungen',]; '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 { protected function beforeCreate(): bool {
$currentCount = WarehouseOrderModel::count(['create' => ['from' => strtotime(date('Y-01-01'))]]); $this->postData['orderNumber'] = 'PO' . date('Y') . '-' . str_pad(WarehouseOrderModel::count(['create' => ['from' => strtotime(date('Y-01-01'))]]) + 1, 4, '0', STR_PAD_LEFT);
$this->postData['orderNumber'] = 'PO' . date('Y') . '-' . str_pad($currentCount + 1, 4, '0', STR_PAD_LEFT);
return true; return true;
} }
protected function getArticleDistributorDataAction() { protected function getArticleDistributorDataAction() {
$data = []; $articleId = $this->request->articleId;
$article = $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') { protected function getByIdParse(array $order): array {
foreach (WarehouseDistributorModel::getAll() as $distributor) { $order['positions'] = json_decode($order['positions'], true);
$data[] = [
'id' => $distributor->id, foreach ($order['positions'] as &$position) {
'name' => $distributor->name, $position['distributorName'] = WarehouseDistributorModel::get($position['distributorId'])->name;
]; $position['articleName'] = WarehouseArticleModel::get($position['article'])->title;
}
} 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,
];
}
} }
self::returnJson($data); return $order;
} }
protected function beforeUpdate($postData): bool { protected function createPDFAction() {
(new WarehouseHistoryController)->create($postData, $this->mod); $order = (array) WarehouseOrderModel::get($this->request->id);
return true; $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 }}","<strong>Ihre Referenz:</strong> ". $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 }}", "<strong>Referenz: ". $order["orderNumber"] . "</strong>", $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));
}
} }

View File

@@ -1,5 +1,5 @@
<?php <?php
//TODO: migration for extReference
/** /**
* Class WarehouseOrderModel * Class WarehouseOrderModel
* *
@@ -7,6 +7,7 @@
* *
* @property int $id Unique identifier for the warehouse order * @property int $id Unique identifier for the warehouse order
* @property string $orderNumber Unique order number * @property string $orderNumber Unique order number
* @property string $extReference External reference number
* @property int $distributorId ID of the distributor associated with the order * @property int $distributorId ID of the distributor associated with the order
* @property string $delAddrCity City of the delivery address * @property string $delAddrCity City of the delivery address
* @property string $delAddrEMail Email associated with the delivery address * @property string $delAddrEMail Email associated with the delivery address
@@ -22,6 +23,7 @@
class WarehouseOrderModel extends TTCrudBaseModel { class WarehouseOrderModel extends TTCrudBaseModel {
public int $id; public int $id;
public string $orderNumber; public string $orderNumber;
public string $extReference;
public int $distributorId; public int $distributorId;
public string $delAddrCity; public string $delAddrCity;
public string $delAddrEMail; public string $delAddrEMail;

View File

@@ -0,0 +1,4 @@
<?php
class WarehouseProject extends mfBaseModel {
}

View File

@@ -0,0 +1,36 @@
<?php
class WarehouseProjectController extends TTCrud {
protected string $headerTitle = 'Projekte';
protected string $createText = 'Neues Projekt erstellen';
//@formatter:off
protected array $columns = [
['key' => '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;
}
}

View File

@@ -0,0 +1,15 @@
<?php
class WarehouseProjectModel extends TTCrudBaseModel {
public int $id;
public string $title;
public string $description;
public string $startDate;
public string $endDate;
public string $status;
public string $priority;
public int $assignedTo;
public int $createBy;
public int $create;
}

View File

@@ -0,0 +1,21 @@
<?php declare(strict_types = 1);
use Phinx\Migration\AbstractMigration;
final class WarehouseModify12 extends AbstractMigration {
public function up(): void {
if ($this->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();;
}
}
}

View File

@@ -8,7 +8,7 @@ class Helper {
* @param string $columnName The name of the column in the database table. * @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. * @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 = ""; $sql = "";
if (is_array($filterValue)) { if (is_array($filterValue)) {
@@ -30,6 +30,9 @@ class Helper {
} else if (!empty($filterValue)) { } else if (!empty($filterValue)) {
if ($exactMatch) { if ($exactMatch) {
$sql .= " AND `$columnName` = '" . $filterValue . "'"; $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] === "%") { } else if ($filterValue[0] === "%") {
$sql .= " AND `$columnName` LIKE '" . $filterValue . "'"; $sql .= " AND `$columnName` LIKE '" . $filterValue . "'";
} else if ($filterValue[strlen($filterValue) - 1] === "%") { } else if ($filterValue[strlen($filterValue) - 1] === "%") {

View File

@@ -10,6 +10,8 @@
* @property array $additionalJSVariables * @property array $additionalJSVariables
* @property array $infoMessages * @property array $infoMessages
* @property bool $onlyView * @property bool $onlyView
* @property array $defaultOrder
* @property array $autocompleteColumns
*/ */
class TTCrud extends mfBaseController { class TTCrud extends mfBaseController {
public User $user; public User $user;
@@ -251,18 +253,20 @@ class TTCrud extends mfBaseController {
} }
protected function autocompleteAction() { protected function autocompleteAction() {
$searchedID = $this->request->searchedID;
$textKey = property_exists($this->model, 'name') ? 'name' : 'title'; $textKey = property_exists($this->model, 'name') ? 'name' : 'title';
if (strlen($this->request->searchedID) > 0) {
if (strlen($searchedID) > 0) { $filter = ['id' => $this->request->searchedID];
$filter = ['id' => $searchedID];
$data = $this->model::getAll($filter, 10); $data = $this->model::getAll($filter, 10);
} else { } else {
$filter = [$textKey => $this->request->q . '%']; if (isset($this->autocompleteColumns) && is_array($this->autocompleteColumns)) {
$data = $this->model::getAll($filter, 10); $filterKey = join('|', $this->autocompleteColumns);
} else {
$filterKey = $textKey;
}
$data = [];
if (count($data) < 11) { if (count($data) < 11) {
$filter = [$textKey => $this->request->q]; $filter = [$filterKey => '%' . $this->request->q . '%'];
$lazyData = $this->model::getAll($filter, 10); $lazyData = $this->model::getAll($filter, 10);
$data = array_merge($data, $lazyData); $data = array_merge($data, $lazyData);
$data = array_unique($data, SORT_REGULAR); $data = array_unique($data, SORT_REGULAR);
@@ -270,7 +274,6 @@ class TTCrud extends mfBaseController {
} }
} }
self::returnJson(array_map(function ($item) use ($textKey) { self::returnJson(array_map(function ($item) use ($textKey) {
return ['value' => $item->id, 'text' => $item->$textKey]; return ['value' => $item->id, 'text' => $item->$textKey];
}, $data)); }, $data));
@@ -285,6 +288,7 @@ class TTCrud extends mfBaseController {
} }
$data = (array) $this->model::get($id); $data = (array) $this->model::get($id);
if (method_exists($this, 'getByIdParse') && !isset($_GET['disableParse'])) $data = $this->getByIdParse($data);
self::returnJson($data); self::returnJson($data);
} }
} }

View File

@@ -81,17 +81,13 @@ class TTCrudBaseModel {
} }
public static function get($id, $die= false): TTCrudBaseModel { public static function get($id): TTCrudBaseModel {
$FronkDB = FronkDB::singleton(); $FronkDB = FronkDB::singleton();
$db = $FronkDB->link; $db = $FronkDB->link;
$id = $db->real_escape_string($id); $id = $db->real_escape_string($id);
$table = self::getTable(); $table = self::getTable();
$sql = "SELECT * FROM `$table` WHERE `id` = $id"; $sql = "SELECT * FROM `$table` WHERE `id` = $id";
if($die) {
die($sql);
}
$result = $db->query($sql); $result = $db->query($sql);
// as TTCRudBaseModel is abstract, we need to get the class name of the child class // as TTCRudBaseModel is abstract, we need to get the class name of the child class
$class = get_called_class(); $class = get_called_class();
@@ -109,16 +105,16 @@ class TTCrudBaseModel {
return $result->fetch_assoc()['count']; return $result->fetch_assoc()['count'];
} }
public static function getSQLFilter($filter): string { public static function getSQLFilter(array $filter): string {
if (empty($filter)) { if (empty($filter)) return '';
return ""; $sql = 'WHERE 1=1';
} $calledClass = get_called_class();
$sql = "WHERE 1=1";
foreach ($filter as $key => $value) { foreach ($filter as $key => $value) {
if (!property_exists(get_called_class(), $key)) { foreach (explode('|', $key) as $column) {
http_response_code(500); if (!property_exists($calledClass, $column)) {
throw new Exception("Field $key does not exist in " . get_called_class()); throw new InvalidArgumentException("Field $column does not exist in $calledClass");
}
} }
$sql .= Helper::generateFilterCondition($value, $key, gettype($value) === "integer"); $sql .= Helper::generateFilterCondition($value, $key, gettype($value) === "integer");
} }

View File

@@ -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;
}
}

View File

@@ -0,0 +1,178 @@
Vue.component('warehouse-offer-modal', {
props: {
id: {type: [String, Number], required: true},
mode: {type: String, default: 'edit'}
},
template: `
<tt-modal :show="true"
@submit="submit"
:delete="id !== 'create'"
:title="id === 'create' ? 'Angebot erstellen' : \`Angebot #\${id} bearbeiten\`"
@update:show="$emit('close')">
<div style="width: 99%"><h4 class="text-center">Angebotdetails</h4>
<tt-select label="Sachbearbeiter"
:options="window.TT_CONFIG.CRUD_CONFIG.columns.find(column => column.key === 'createBy')?.modal.items"
sm
row
v-model="offer.editor"/>
<tt-input label="Kundennummer" v-model="offer.customerNumber" sm row/>
<tt-input label="Kundenreferenz" v-model="offer.reference" sm row/>
<tt-textarea label="Angebotszweck" v-model="offer.purpose" sm row/>
<hr>
<h4 class="text-center">Kundenadresse</h4>
<div style="display: grid; grid-gap: 10px; grid-template-columns: 2fr 2fr 1fr 1fr 2fr;">
<tt-input label="Name" v-model="offer.customerName" sm/>
<tt-input label="Straße" v-model="offer.customerStreet" sm/>
<tt-input label="PLZ" v-model="offer.customerZip" sm/>
<tt-input label="Ort" v-model="offer.customerCity" sm/>
<tt-input label="UID" v-model="offer.customerVAT" sm/>
</div>
<hr>
<h4 class="text-center">Positionen</h4>
<tt-positions-manager ref="positionsManager" v-model="offer.positions" :config="positionsConfig" @updateField-article="fetchArticleData"/>
<hr>
<h4 class="text-center">Alternative Artikel</h4>
<tt-positions-manager ref="alternativePositionsManager" v-model="offer.alternativePositions" :config="alternativePositionsConfig"/>
<hr>
<tt-input label="Gesamtrabatt (%)" v-model="offer.totalDiscount" sm row type="number"/>
<tt-select label="Zahlungskonditionen" :options="paymentTerms" sm row v-model="offer.paymentTerms"/>
<tt-select label="Lieferkonditionen" :options="deliveryTerms" sm row v-model="offer.deliveryTerms"/>
<tt-select label="Schlusstext" :options="closingTexts" sm row v-model="offer.closingText"/>
<hr>
<tt-textarea label="Notizen" v-model="offer.notes" sm row/>
</div>
</tt-modal>
`,
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('<br>') : 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: `
<tt-card>
<warehouse-offer-modal v-if="offerModalId" :id="offerModalId" @close="offerModalId = null;$refs.table.$refs.table.refreshTable()"/>
<button @click="offerModalId = 'create'" class="btn btn-primary">Angebot erstellen</button>
<tt-table-crud emit-edit @edit="offerModalId = $event.id" ref="table">
<template v-slot:expandedRow="{ row }">
<div>
<h5>Notizen</h5>
<p>{{ row.notes }}</p>
<h5>Verlauf</h5>
<ul>
<li v-for="entry in row.journal">{{ entry.date }} - {{ entry.description }}</li>
</ul>
</div>
</template>
</tt-table-crud>
</tt-card>
`,
data() {
return {
window: window,
offerModalId: null,
}
},
});

View File

@@ -24,4 +24,29 @@
grid-template-columns: 1fr 1fr !important; grid-template-columns: 1fr 1fr !important;
grid-gap: 10px; grid-gap: 10px;
} }
} }
/* 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;
}

View File

@@ -16,6 +16,7 @@ Vue.component('warehouse-order-modal', {
sm sm
row row
v-model="order.editor"/> v-model="order.editor"/>
<tt-input label="Externe Referenz" v-model="order.extReference" sm row/>
<hr> <hr>
<h4 class="text-center">Positionen</h4> <h4 class="text-center">Positionen</h4>
@@ -80,6 +81,7 @@ Vue.component('warehouse-order-modal', {
}, },
}, },
order: { order: {
extReference: '',
delAddrName: 'XINON GmbH', delAddrName: 'XINON GmbH',
delAddrLine: 'Fladnitz im Raabtal 150', delAddrLine: 'Fladnitz im Raabtal 150',
delAddrPLZ: '8322', delAddrPLZ: '8322',
@@ -92,11 +94,13 @@ Vue.component('warehouse-order-modal', {
} }
}, },
async mounted() { async mounted() {
if (this.id !== 'create') { if (this.id === 'create') return;
const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/getById`, {params: {id: this.id}});
response.data.positions = JSON.parse(response.data.positions); console.log(this.id);
this.order = response.data;
} 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: { methods: {
async submit() { async submit() {
@@ -112,18 +116,20 @@ Vue.component('warehouse-order-modal', {
positions: this.order.positions.filter(position => position.distributorId === distributorId) positions: this.order.positions.filter(position => position.distributorId === distributorId)
} }
); );
if (response.data.success) window.notify('success', response.data.message ?? 'Bestellung erfolgreich erstellt'); if (response.data.success) {
else window.notify('error', 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('<br>') : response.data.message || 'Ein Fehler ist aufgetreten'); response.data.errors ? Object.values(response.data.errors).join('<br>') : response.data.message || 'Ein Fehler ist aufgetreten');
} }
} else { } else {
const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/update`, this.order); 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'); if (response.data.success) {
else window.notify('error', 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('<br>') : response.data.message || 'Ein Fehler ist aufgetreten'); response.data.errors ? Object.values(response.data.errors).join('<br>') : response.data.message || 'Ein Fehler ist aufgetreten');
} }
this.$emit('close');
}, },
async fetchDistributors(article) { async fetchDistributors(article) {
const url = `${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/getArticleDistributorData`; const url = `${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/getArticleDistributorData`;
@@ -148,22 +154,93 @@ Vue.component('warehouse-order-modal', {
}, },
}); });
Vue.component('warehouse-order-detail', {
Vue.component('warehouse-order', {
//language=Vue //language=Vue
template: ` template: `
<tt-card> <tt-card>
<warehouse-order-modal v-if="orderModalId" :id="orderModalId" @close="orderModalId = null;$refs.table.$refs.table.refreshTable()"/> <template v-slot:header><h4>Bestellungsdetails für #{{ loading ? 'Laden...' : order.orderNumber }}</h4></template>
<button @click="orderModalId = 'create'" class="btn btn-primary">Bestellung erstellen</button>
<tt-table-crud emit-edit @edit="orderModalId = $event.id" ref="table">
<template v-slot:expandedRow="{ row }"><span>Work in Progress</span></template>
</tt-table-crud>
</tt-card>
`, data() {
return {
window: window,
orderModalId: null,
<template v-if="loading">
<div class="d-flex justify-content-center align-items-center">
<div class="spinner-border spinner-border-sm text-primary" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
</template>
<template v-else>
<h3>Lieferadresse</h3>
<div>{{order.delAddrName}}</div>
<div>{{order.delAddrEMail}}</div>
<div>{{order.delAddrLine}}</div>
<div>{{order.delAddrPLZ}} {{order.delAddrCity}}</div>
<div style="display: grid; grid-gap: 10px; grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr;margin-top: 24px">
<div><strong>Artikel</strong></div>
<div><strong>Menge</strong></div>
<div><strong>Preis</strong></div>
<div><strong>Lieferant</strong></div>
<div><strong>Verwendung</strong></div>
<div><strong>Summe</strong></div>
</div>
<div style="display: grid; grid-gap: 10px; grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr;" v-for="position in order.positions">
<div>{{ position.articleName }}</div>
<div>{{ position.amount }}</div>
<div>{{ position.buyPrice }}</div>
<div>{{ position.distributorName }}</div>
<div>{{ position.verwendung }}</div>
<div>{{ position.amount * position.buyPrice }}</div>
</div>
</template>
</tt-card>
`,
props: {
id: {type: [String, Number], required: true}
},
data() {
return {
window: window,
order: {},
loading: true
} }
}, },
}) 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: `
<tt-card>
<warehouse-order-modal v-if="orderModalId" :id="orderModalId" @close="closeOrderModal"/>
<button @click="orderModalId = 'create'" class="btn btn-primary">Bestellung erstellen</button>
<tt-table-crud emit-edit @edit="orderModalId = $event.id" ref="table">
<template v-slot:expandedRow="{ row }">
<warehouse-order-detail :id="row['id']"/>
</template>
<template v-slot:sum="{ row }">{{ calculateSum(JSON.parse(row["positions"])).toFixed(2)}} €</template>
<!-- TODO: think of a way here prob we add it to the database as field-->
<template v-slot:distributor="{ row }">{{ row.id % 2 == 0 ? 'Triotronik' : 'Discomp' }}</template>
</tt-table-crud>
</tt-card>
`,
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);
}
}
});

View File

@@ -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;
}
}

View File

@@ -0,0 +1,161 @@
Vue.component('warehouse-project-modal', {
props: {
id: { type: [String, Number], required: true },
mode: { type: String, default: 'edit' }
},
template: `
<tt-modal :show="true"
@submit="submit"
:delete="id !== 'create'"
:title="id === 'create' ? 'Projekt erstellen' : \`Projekt #\${id} bearbeiten\`"
@update:show="$emit('close')">
<div style="width: 99%">
<h4 class="text-center">Projektübersicht</h4>
<tt-input label="Projektnummer" v-model="project.projectNumber" sm row disabled />
<tt-textarea label="Um was handelt es sich?" v-model="project.description" sm row/>
<hr>
<h4 class="text-center">Zeitraum</h4>
<div style="display: grid; grid-gap: 10px; grid-template-columns: 1fr 1fr;">
<tt-date-picker label="Startdatum" v-model="project.startDate" sm/>
<tt-date-picker label="Enddatum" v-model="project.endDate" sm/>
</div>
<hr>
<h4 class="text-center">Beteiligte Personen</h4>
<tt-select label="Personen (XINON MT)"
:options="participantsOptions"
v-model="project.participants"
sm row />
<tt-textarea label="Freitext für weitere Personen" v-model="project.additionalParticipants" sm row/>
<hr>
<h4 class="text-center">Projektübersicht</h4>
<tt-input label="Gesamtsumme des Projekts (€)" v-model.number="project.totalSum" sm row type="number"/>
<tt-positions-manager
ref="positionsManager"
v-model="project.positions"
:config="positionsConfig"
@updateField-article="fetchArticleData"
/>
<hr>
<h4 class="text-center">Lagerort</h4>
<tt-input label="Lagerort für dieses Projekt" v-model="project.storageLocation" sm row/>
<hr>
<tt-textarea label="Notizen" v-model="project.notes" sm row/>
</div>
</tt-modal>
`,
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('<br>') : 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: `
<tt-card>
<warehouse-project-modal v-if="projectModalId" :id="projectModalId" @close="projectModalId = null;$refs.table.$refs.table.refreshTable()"/>
<button @click="projectModalId = 'create'" class="btn btn-primary">Angebot erstellen</button>
<tt-table-crud emit-edit @edit="projectModalId = $event.id" ref="table">
<template v-slot:expandedRow="{ row }">
<div>
<h5>Notizen</h5>
<p>{{ row.notes }}</p>
<h5>Verlauf</h5>
<ul>
<li v-for="entry in row.journal">{{ entry.date }} - {{ entry.description }}</li>
</ul>
</div>
</template>
</tt-table-crud>
</tt-card>
`,
data() {
return {
window: window,
projectModalId: null,
}
},
});

View File

@@ -33,6 +33,13 @@ Vue.component('tt-positions-manager', {
:api-url="window.TT_CONFIG['BASE_PATH'] + field.apiUrl" :api-url="window.TT_CONFIG['BASE_PATH'] + field.apiUrl"
sm sm
/> />
<tt-textarea
v-else-if="field.type === 'textarea'"
:label="field.label"
v-model="formData[key]"
@input="$emit('updateField-' + key, $event)"
sm
/>
<tt-checkbox <tt-checkbox
v-else-if="field.type === 'checkbox'" v-else-if="field.type === 'checkbox'"
:label="field.label" :label="field.label"

View File

@@ -229,6 +229,7 @@ Vue.component('tt-table', {
.isValid() ? moment.unix(row[key]).format('DD.MM.YYYY HH:mm') : moment(row[key]) .isValid() ? moment.unix(row[key]).format('DD.MM.YYYY HH:mm') : moment(row[key])
.format('DD.MM.YYYY HH:mm')) : '' .format('DD.MM.YYYY HH:mm')) : ''
}}</span> }}</span>
<span v-else-if="key === 'create'">{{ window.moment(row[key] * 1000).format('DD.MM.YYYY HH:mm:ss') }}</span>
<i v-else-if="column.filter === 'iconSelect'" <i v-else-if="column.filter === 'iconSelect'"
:title="columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.text" :title="columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.text"
:class="columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.icon"></i> :class="columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.icon"></i>