update for warehouse
This commit is contained in:
43
Layout/default/WarehouseShippingNote/PDF_FOOTER.html
Normal file
43
Layout/default/WarehouseShippingNote/PDF_FOOTER.html
Normal 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>
|
||||
98
Layout/default/WarehouseShippingNote/PDF_HEADER.html
Normal file
98
Layout/default/WarehouseShippingNote/PDF_HEADER.html
Normal file
@@ -0,0 +1,98 @@
|
||||
<!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: 8px">
|
||||
<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">
|
||||
<div>{{ addressLine_1 }}</div>
|
||||
<div>{{ addressLine_2 }}</div>
|
||||
<div>{{ addressLine_3 }}</div>
|
||||
<div>{{ addressLine_4 }}</div>
|
||||
<div>{{ addressLine_5 }}</div>
|
||||
</td>
|
||||
<td style="float: right">
|
||||
<table class="info-table">
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<table class="invoice-details">
|
||||
<tr>
|
||||
<td>Kundennummer:</td>
|
||||
<td>{{ customerNumber }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>LS-Nummer:</td>
|
||||
<td>{{ shippingNoteNumber }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>LS-Datum:</td>
|
||||
<td>{{ shippingNoteDate }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<div class="separator"></div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
134
Layout/default/WarehouseShippingNote/PDF_MAIN.php
Normal file
134
Layout/default/WarehouseShippingNote/PDF_MAIN.php
Normal file
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
/**
|
||||
* @var string $ressourcePathPrefix
|
||||
* @var WarehouseShippingNoteModel $shippingNote
|
||||
* @var Array $positions
|
||||
* @var Array $textElements
|
||||
*/
|
||||
|
||||
$this->setReturnValue(['filename' => $shippingNote->id . ".pdf"]);
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Lieferschein</title>
|
||||
<meta charset="utf-8" />
|
||||
<!-- <link href="-->
|
||||
<?php //= self::getResourcePath() ?><!--assets/css/bootstrap.min.css" rel="stylesheet" type="text/css" />-->
|
||||
|
||||
<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">Ihr XINON Lieferschein vom <?=date("d.m.Y", $shippingNote->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">Position</th>
|
||||
<th style="text-align: right;padding-right: 4pt">Menge</th>
|
||||
<th style="text-align: right;padding-right: 4pt">EH</th>
|
||||
<th style="text-align: center">Artikel</th>
|
||||
<?php if($showPrices): ?>
|
||||
<th style="text-align: right;padding-right: 4pt">Preis</th>
|
||||
<?php endif; ?>
|
||||
</tr>
|
||||
<?php $i = 0; foreach($positions as $p):?>
|
||||
|
||||
<tr class="position <?=($i%2 == 0) ? "even" : "uneven" ?>">
|
||||
<td style="text-align: center;"><?= $i + 1 ?></td>
|
||||
<td style="text-align: right;padding-right: 8pt"><?=$p["amount"]?> </td>
|
||||
<td style="text-align: right;padding-right: 8pt"><?=$p["articleUnit"]?> </td>
|
||||
<td style="text-align: center;"><b><?=$p["articleTitle"]?></b></td>
|
||||
<?php if($showPrices): ?>
|
||||
<td style="text-align: right;padding-right: 8pt"><?=number_format(
|
||||
$p["price"] * $p["amount"], 2, ",", ".")?> €</td>
|
||||
<?php endif; ?>
|
||||
</tr>
|
||||
<tr class="<?=($i%2 == 0) ? "even" : "uneven" ?>">
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
|
||||
<td style="text-align: center;"><?= $p["articleDescription"] ?></td>
|
||||
<?php if($showPrices): ?>
|
||||
<td></td>
|
||||
<?php endif; ?>
|
||||
</tr>
|
||||
<?php $i++; endforeach;?>
|
||||
</table>
|
||||
|
||||
<?php if($textElements !== null): ?>
|
||||
<div style="margin-top: 20pt;">
|
||||
<?php foreach($textElements as $textElement): ?>
|
||||
<p><?=$textElement?></p>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -142,18 +142,19 @@
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
<ul class="submenu">
|
||||
<li class="has-sub-submenu font-weight-bold"><a>XINON</a></li>
|
||||
<?php if($me->can("WarehouseAdmin")): ?><li><a href="<?=self::getUrl("WarehouseArticle")?>"><i class="far fa-fw fa-box text-info"></i> Artikel</a></li><?php endif; ?>
|
||||
<?php if($me->can("WarehouseAdmin")): ?><li><a href="<?=self::getUrl("WarehouseArticlePacket")?>"><i class="far fa-fw fa-box text-info"></i> Artikel-Pakete (EStmk)</a></li><?php endif; ?>
|
||||
<?php if($me->can("WarehouseAdmin")): ?><li><a href="<?=self::getUrl("WarehouseDistributor")?>"><i class="far fa-fw fa-truck text-info"></i> Lieferanten</a></li><?php endif; ?>
|
||||
<?php if($me->can("WarehouseAdmin")): ?><li><a href="<?=self::getUrl("WarehouseArticlePriceType")?>"><i class="far fa-fw fa-money-bill-wave text-info"></i> Preis Typen</a></li><?php endif; ?>
|
||||
<?php if($me->can("WarehouseAdmin")): ?><li><a href="<?=self::getUrl("WarehouseLocation")?>"><i class="far fa-fw fa-map-marker-alt text-info"></i> Lagerorte</a></li><?php endif; ?>
|
||||
<?php if($me->can("WarehouseAdmin")): ?><li><a href="<?=self::getUrl("WarehouseItem")?>"><i class="far fa-fw fa-boxes text-info"></i> Lagerbestand</a></li><?php endif; ?>
|
||||
<?php if($me->can("WarehouseAdmin")): ?><li><a href="<?=self::getUrl("WarehouseOrderRecommendation")?>"><i class="far fa-fw fa-box-full text-info"></i> Bestellvorschläge</a></li><?php endif; ?>
|
||||
<?php if($me->can("WarehouseAdmin")): ?><li><a href="<?=self::getUrl("WarehouseOrder")?>"><i class="far fa-fw fa-shopping-bag text-info"></i> Bestellungen</a></li><?php endif; ?>
|
||||
<?php if($me->can("WarehouseAdmin")): ?><li><a href="<?=self::getUrl("WarehouseShippingNote")?>"><i class="far fa-fw fa-shipping-fast text-info"></i> Lieferscheine</a></li><?php endif; ?>
|
||||
|
||||
<?php if($me->can("WarehouseAdmin")): ?><li><a href="<?=self::getUrl("WarehouseDistributor")?>"><i class="far fa-fw fa-cogs text-info"></i> Administration</a></li><?php endif; ?>
|
||||
|
||||
<li class="has-sub-submenu font-weight-bold"><a>E-Stmk Shop</a></li>
|
||||
<?php if($me->can("WarehouseEShop")): ?><li><a href="<?=self::getUrl("WarehouseEShop")?>"><i class="far fa-fw fa-shopping-cart text-info"></i> E-Shop</a></li><?php endif; ?>
|
||||
<?php if($me->can("WarehouseAdmin")): ?><li><a href="<?=self::getUrl("WarehouseEShopOrder")?>"><i class="far fa-fw fa-shopping-basket text-info"></i> E-Shop Bestellungen</a></li><?php endif; ?>
|
||||
<?php if($me->can("WarehouseAdmins")): ?><li><a href="<?=self::getUrl("WarehouseOrder")?>"><i class="far fa-fw fa-shopping-bag text-info"></i> Bestellungen</a></li><?php endif; ?>
|
||||
<?php if($me->can("WarehouseAdmins")): ?><li><a href="<?=self::getUrl("WarehouseShippingNote")?>"><i class="far fa-fw fa-shipping-fast text-info"></i> Lieferscheine</a></li><?php endif; ?>
|
||||
<?php if($me->can("WarehouseAdmin")): ?><li><a href="<?=self::getUrl("WarehouseRevenueAccount")?>"><i class="far fa-fw fa-money-bill-wave text-info"></i> Erlöskontos</a></li><?php endif; ?>
|
||||
<?php if($me->can("WarehouseAdmin")): ?><li><a href="<?=self::getUrl("WarehouseArticlePacket")?>"><i class="far fa-fw fa-box text-info"></i> Artikel-Pakete</a></li><?php endif; ?>
|
||||
</ul>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -534,6 +534,7 @@ class AddressController extends mfBaseController {
|
||||
];
|
||||
$results[] = $result;
|
||||
$this->returnJson($results);
|
||||
die();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @property mixed|null $name
|
||||
*/
|
||||
class WarehouseAdministration extends mfBaseModel
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
class WarehouseAdministrationController extends mfBaseController {
|
||||
private User $me;
|
||||
|
||||
protected function init(): void {
|
||||
$me = new User();
|
||||
$me->loadMe();
|
||||
$this->layout()->set("me", $me);
|
||||
$this->me = $me;
|
||||
|
||||
if (!$this->me->isAdmin()) {
|
||||
$this->redirect("dashboard");
|
||||
}
|
||||
}
|
||||
|
||||
protected function indexAction(): void {
|
||||
$this->layout()->set('additionalJS', ['js/pages/WarehouseHistory/WarehouseHistoryModal.js']);
|
||||
|
||||
Helper::renderVue($this, 'WarehouseAdministration', 'Administration-Tools', ["CREATE_URL" => $this::getUrl($this->mod . "/create"),
|
||||
"TABLE_URL" => $this::getUrl($this->mod . "/get"),
|
||||
"UPDATE_URL" => $this::getUrl($this->mod . "/update"),
|
||||
"DELETE_URL" => $this::getUrl($this->mod . "/delete"),]);
|
||||
}
|
||||
|
||||
//TODO: this needs improvement as it is inefficient but it doesnt matter as it doesnt get called very often
|
||||
// and also maybe we should move it to WarehouseLocationController
|
||||
protected function createLocationsAction(): void {
|
||||
$existingLocations = WarehouseLocationModel::getAll();
|
||||
$companyCars = TimerecordingCarModel::getAll();
|
||||
|
||||
|
||||
$wantedCarLocations = [];
|
||||
foreach ($companyCars as $car) {
|
||||
// check if $car->brand includes "Anhänger" or "Anhaenger", if yes then continue
|
||||
if (strpos($car->brand, "Anhänger") !== false || strpos($car->brand, "Anhaenger") !== false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$carModelParts = explode(" ", $car->model);
|
||||
if (count($carModelParts) > 1) {
|
||||
$wantedCarLocations[] = "{$car->number_plate} {$car->brand} {$carModelParts[0]} {$carModelParts[1]}";
|
||||
} else {
|
||||
$wantedCarLocations[] = "{$car->number_plate} {$car->brand} {$carModelParts[0]}";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// create a warehouse location for each wantedcar but check if $existingLocations[]->title already exists with the same title
|
||||
foreach ($wantedCarLocations as $wantedCarLocation) {
|
||||
$locationExists = false;
|
||||
foreach ($existingLocations as $existingLocation) {
|
||||
if ($existingLocation->title === $wantedCarLocation) {
|
||||
$locationExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$locationExists) {
|
||||
$numberPlate = explode(" ", $wantedCarLocation)[0];
|
||||
$assignedTo = 1;
|
||||
foreach ($companyCars as $car) {
|
||||
if ($car->number_plate === $numberPlate) {
|
||||
$assignedTo = $car->user_id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($assignedTo === null) {
|
||||
$assignedTo = 6;
|
||||
}
|
||||
|
||||
WarehouseLocationModel::create([
|
||||
"title" => $wantedCarLocation,
|
||||
"description" => "Automatisch erstellt",
|
||||
"assignedTo" => $assignedTo,
|
||||
"createdBy" => $this->me->id,
|
||||
"create" => time()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$existingLocations = WarehouseLocationModel::getAll();
|
||||
$users = UserModel::search(['employee' => true]);
|
||||
|
||||
// now create a warehouse location for each user only if they dont already have one (for example if they have a company car)
|
||||
foreach ($users as $user) {
|
||||
$locationExists = false;
|
||||
foreach ($existingLocations as $existingLocation) {
|
||||
if (intval($existingLocation->assignedTo) === intval($user->id)) {
|
||||
$locationExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$locationExists) {
|
||||
WarehouseLocationModel::create([
|
||||
"title" => $user->name . "'s Lagerort",
|
||||
"description" => "Automatisch erstellt",
|
||||
"assignedTo" => $user->id,
|
||||
"createdBy" => $this->me->id,
|
||||
"create" => time()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
var_dump($existingLocations);
|
||||
die();
|
||||
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,12 @@ class WarehouseArticleController extends TTCrud {
|
||||
// @formatter:off
|
||||
protected array $columns = [
|
||||
['key' => 'title', 'text' => 'Titel', 'required' => true, 'table' => ['priority' => 9]],
|
||||
['key' => 'description', 'text' => 'Beschreibung', 'required' => true, 'table' => false],
|
||||
['key' => 'description', 'text' => 'Beschreibung', 'required' => true],
|
||||
['key' => 'category', 'text' => 'Kategorie', 'required' => true],
|
||||
['key' => 'unit', 'text' => 'Einheit', 'required' => true,'table' => false], // Boolean value
|
||||
['key' => 'defaultSellMultiplier', 'text' => 'Standard Multiplikator','regex' => '/^[0-9]*$/' , 'required' => true,'modal' => ['type' => 'number'], 'table' => false], // Boolean value
|
||||
['key' => 'revenueAccount', 'text' => 'Erlöskonto', 'required' => true,'modal' => ['type' => 'select'], 'table' => false], // Boolean value
|
||||
['key' => 'revenueAccount', 'text' => 'Erlöskonto', 'required' => true,'modal' =>
|
||||
['type' => 'select', 'items' => [['value' => 0, 'text' => 'Dienstleistungen'], ['value' => 1, 'text' => 'Handelswaren']]
|
||||
], 'table' => false], // Boolean value
|
||||
['key' => 'cheapestPurchasePrice', 'text' => 'Einkauf', 'modal' => false, 'table' => ['class' => 'text-center', 'suffix' => ' €']],
|
||||
['key' => 'cheapestSellPrice', 'text' => 'Verkauf', 'modal' => false, 'table' => ['class' => 'text-center', 'suffix' => ' €']],
|
||||
['key' => 'warningAmount', 'text' => 'Warnmenge', 'required' => true,'modal' => ['type' => 'number'], 'table' => ['class' => 'text-center']], // Stock/inventory related
|
||||
@@ -27,6 +28,7 @@ class WarehouseArticleController extends TTCrud {
|
||||
['key' => 'editDistributorEntries','title' => 'Lieferanten','class' => 'fas fa-truck text-cyan'],
|
||||
['key' => 'editThresholdEntries','title' => 'Schwellenwerte','class' => 'far fa-fw fa-box-full text-orange'],
|
||||
['key' => 'editPricesEntries','title' => 'Preise','class' => 'fas fa-euro-sign text-green'],
|
||||
['key' => 'addToCart','title' => 'Zur Bestellung hinzufügen','class' => 'fas fa-shopping-cart text-primary'],
|
||||
];
|
||||
// @formatter:on
|
||||
|
||||
@@ -35,15 +37,6 @@ class WarehouseArticleController extends TTCrud {
|
||||
'delete' => 'Artikel wurde gelöscht',
|
||||
'noChanges' => 'Keine Änderungen',];
|
||||
|
||||
public function prepareCrudConfig() {
|
||||
$revenueAccounts = WarehouseRevenueAccountModel::getAll();
|
||||
$revenueAccounts = array_map(function ($revenueAccount) {
|
||||
return ['value' => $revenueAccount->id, 'text' => $revenueAccount->title];
|
||||
}, $revenueAccounts);
|
||||
|
||||
$this->columns[5]['modal']['items'] = $revenueAccounts;
|
||||
}
|
||||
|
||||
protected function beforeUpdate($postData): bool {
|
||||
(new WarehouseHistoryController)->create($postData, $this->mod);
|
||||
return true;
|
||||
@@ -77,6 +70,11 @@ class WarehouseArticleController extends TTCrud {
|
||||
WarehouseArticleModel::update(array_merge(get_object_vars($article), ['cheapestPurchasePrice' => $cheapestPurchasePrice]));
|
||||
}
|
||||
|
||||
protected function afterCreate($postData) {
|
||||
self::updateCheapestPurchasePrice($postData['id']);
|
||||
self::updateSellPrices($postData['id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the sell prices for a given article.
|
||||
*
|
||||
@@ -96,33 +94,31 @@ class WarehouseArticleController extends TTCrud {
|
||||
$cheapestSellPrices = [];
|
||||
// Calculate sell prices for each price type, use default sell multiplier if no specific price is set
|
||||
foreach ($priceTypes as $priceType) {
|
||||
$articlePriceType = array_filter($articlePriceTypes, function ($apt) use ($priceType) {
|
||||
return $apt->articlePriceTypeId == $priceType->id;
|
||||
});
|
||||
$articlePriceType = null;
|
||||
foreach ($articlePriceTypes as $apt) {
|
||||
if ($apt->articlePriceTypeId == $priceType->id) {
|
||||
$articlePriceType = $apt;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$sellPrice = $article->defaultSellMultiplier * $article->cheapestPurchasePrice;
|
||||
if (!empty($articlePriceType)) {
|
||||
$articlePriceType = $articlePriceType[0];
|
||||
$sellPrice = $priceType->defaultPriceFactor * $article->cheapestPurchasePrice;
|
||||
if ($articlePriceType !== null) {
|
||||
$sellPrice = $articlePriceType->priceOverride ?: $articlePriceType->priceMultiplier * $article->cheapestPurchasePrice;
|
||||
}
|
||||
$cheapestSellPrices[$priceType->id] = ['title' => $priceType->title, 'price' => $sellPrice];
|
||||
$cheapestSellPrices[$priceType->id] = ['title' => $priceType->title, 'price' => round($sellPrice, 2)];
|
||||
}
|
||||
|
||||
$article->cheapestSellPrice = json_encode($cheapestSellPrices);
|
||||
WarehouseArticleModel::update(get_object_vars($article));
|
||||
}
|
||||
|
||||
|
||||
protected function afterCreate($postData) {
|
||||
self::updateCheapestPurchasePrice($postData['id']);
|
||||
self::updateSellPrices($postData['id']);
|
||||
}
|
||||
|
||||
protected function updatePricesAction() {
|
||||
public function updatePricesAction() {
|
||||
foreach (WarehouseArticleModel::getAll() as $article) {
|
||||
self::updateCheapestPurchasePrice($article->id);
|
||||
self::updateSellPrices($article->id);
|
||||
}
|
||||
self::returnJson(['success' => true, 'message' => 'Preise wurden aktualisiert']);
|
||||
}
|
||||
|
||||
protected function getHistoryAction() {
|
||||
@@ -241,4 +237,52 @@ class WarehouseArticleController extends TTCrud {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
protected function prepareOrderAction() {
|
||||
// inside post json it will look like
|
||||
// [
|
||||
// {
|
||||
// "amount": "5",
|
||||
// "itemId": 441,
|
||||
// "title": "RT-FB-7590AX"
|
||||
// },
|
||||
// {
|
||||
// "amount": "5",
|
||||
// "itemId": 421,
|
||||
// "title": "RT-FB-7590"
|
||||
// }
|
||||
//]
|
||||
// get the json from the post request
|
||||
// then create a array containing each order we need to make, so search through WarehouseArticleDistributorModel to get the distributorId and purchasePrice (use lowest purchasePrice)
|
||||
// then get the WarehouseDistributorModel and then create a summary of the orders we need to make for each distributor
|
||||
|
||||
$postData = json_decode(file_get_contents('php://input'), true);
|
||||
$orders = [];
|
||||
foreach ($postData as $order) {
|
||||
$articleDistributors = WarehouseArticleDistributorModel::getAll(['articleId' => $order['itemId']]);
|
||||
$cheapestArticleDistributor = $articleDistributors[0];
|
||||
foreach ($articleDistributors as $articleDistributor) {
|
||||
if ($articleDistributor->purchasePrice < $cheapestArticleDistributor->purchasePrice) {
|
||||
$cheapestArticleDistributor = $articleDistributor;
|
||||
}
|
||||
}
|
||||
$distributor = WarehouseDistributorModel::get($cheapestArticleDistributor->distributorId);
|
||||
|
||||
if (!isset($orders[$distributor->id])) {
|
||||
$orders[$distributor->id] = ['distributor' => array($distributor),
|
||||
'orderAmount' => 0,
|
||||
'orders' => []];
|
||||
}
|
||||
|
||||
$orders[$distributor->id]['orders'][] = ['articleId' => $order['itemId'],
|
||||
'amount' => $order['amount'],
|
||||
'sum' => $order['amount'] * $cheapestArticleDistributor->purchasePrice,
|
||||
'purchasePrice' => $cheapestArticleDistributor->purchasePrice,
|
||||
'externalArticleNumber' => $cheapestArticleDistributor->externalArticleNumber,
|
||||
'title' => $order['title'],];
|
||||
$orders[$distributor->id]['orderAmount'] += $order['amount'] * $cheapestArticleDistributor->purchasePrice;
|
||||
}
|
||||
|
||||
self::returnJson($orders);
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@ class WarehouseArticleModel extends TTCrudBaseModel {
|
||||
public int $criticalAmount;
|
||||
public int $isEShop;
|
||||
public int $isEShopHide;
|
||||
public float $defaultSellMultiplier;
|
||||
public string $unit;
|
||||
public int $isSerialDocumentation;
|
||||
public int $revenueAccount;
|
||||
|
||||
0
application/WarehouseArticle/import.json
Normal file
0
application/WarehouseArticle/import.json
Normal file
@@ -7,6 +7,11 @@ class WarehouseArticlePriceTypeController extends TTCrud {
|
||||
// @formatter:off
|
||||
protected array $columns = [
|
||||
['key' => 'title', 'text' => 'Titel', 'required' => true],
|
||||
['key' => 'description', 'text' => 'Beschreibung', 'required' => false],
|
||||
['key' => 'defaultPriceFactor', 'text' => 'Standard Preisfaktor', 'required' => true, 'modal' => ['type' => 'number']],
|
||||
['key' => 'create', 'text' => 'Erstellt', 'required' => false, 'modal' => false, 'table' => ['filter' => 'datetime', 'class' => 'text-nowrap']],
|
||||
['key' => 'createBy', 'text' => 'Erstellt von', 'required' => false, 'modal' => [
|
||||
'type' => 'select', 'items' => [], 'table' => ['class' => 'text-nowrap']], 'visible' => false],
|
||||
['key' => 'actions', 'text' => 'Aktionen', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center', 'priority' => 10]],
|
||||
];
|
||||
// @formatter:on
|
||||
@@ -16,47 +21,30 @@ class WarehouseArticlePriceTypeController extends TTCrud {
|
||||
'delete' => 'Artikel Verkaufspreis wurde gelöscht',
|
||||
'noChanges' => 'Keine Änderungen'];
|
||||
|
||||
protected function checkExistingDistributorEntry($postData): bool {
|
||||
// if postData id exists check if there is already an entry with the same articleId and locationId if postdata id and WarehouseLocationThresholdOverrideModel id are different return false
|
||||
if (isset($postData['id'])) {
|
||||
$count = WarehouseArticlePriceTypeModel::count(['title' => $postData['title'], 'id' => $postData['id']]);
|
||||
if ($count > 0) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
$count = WarehouseArticlePriceTypeModel::count(['title' => $postData['title']]);
|
||||
if ($count > 0) {
|
||||
self::returnJson(['success' => false,
|
||||
'message' => 'Es existiert bereits ein Preis Typ mit diesem Titel.']);
|
||||
return false;
|
||||
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function beforeCreate($postData): bool {
|
||||
return $this->checkExistingDistributorEntry($postData);
|
||||
protected function prepareCrudConfig() {
|
||||
// add all users to createBy column
|
||||
$this->columns[array_search('createBy', array_column($this->columns, 'key'))]['modal']['items'] = array_map(function ($user) {
|
||||
return ['value' => $user->id, 'text' => $user->name];
|
||||
}, UserModel::getAll());
|
||||
}
|
||||
|
||||
protected function beforeUpdate($postData): bool {
|
||||
$existing = $this->checkExistingDistributorEntry($postData);
|
||||
|
||||
if (!$existing) {
|
||||
return false;
|
||||
}
|
||||
|
||||
(new WarehouseHistoryController)->create($postData, $this->mod);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function afterCreate($postData) {
|
||||
WarehouseArticleController::updateSellPrices($postData['articleId']);
|
||||
protected function afterUpdate($postData) {
|
||||
$WarehouseArticleController = new WarehouseArticleController;
|
||||
// set mod of WarehouseArticleController to WarehouseArticle
|
||||
$WarehouseArticleController->mod = 'WarehouseArticle';
|
||||
$WarehouseArticleController->updatePricesAction();
|
||||
}
|
||||
|
||||
public function afterUpdate($postData) {
|
||||
WarehouseArticleController::updateSellPrices($postData['articleId']);
|
||||
protected function afterCreate($postData) {
|
||||
$WarehouseArticleController = new WarehouseArticleController;
|
||||
// set mod of WarehouseArticleController to WarehouseArticle
|
||||
$WarehouseArticleController->mod = 'WarehouseArticle';
|
||||
$WarehouseArticleController->updatePricesAction();
|
||||
}
|
||||
|
||||
protected function getHistoryAction() {
|
||||
|
||||
@@ -3,4 +3,8 @@
|
||||
class WarehouseArticlePriceTypeModel extends TTCrudBaseModel {
|
||||
public int $id;
|
||||
public string $title;
|
||||
public ?string $description;
|
||||
public float $defaultPriceFactor;
|
||||
public int $create;
|
||||
public int $createBy;
|
||||
}
|
||||
@@ -153,6 +153,7 @@ class WarehouseEShopOrderController extends TTCrud {
|
||||
}
|
||||
// if it is still null, die with order id:
|
||||
if ($realOrderItems === null) {
|
||||
continue;
|
||||
self::returnJson(['success' => false, 'message' => 'Bestellung mit ID ' . $order['id'] . ' hat keine Artikel. Bitte überprüfen.']);
|
||||
die();
|
||||
}
|
||||
|
||||
@@ -4,12 +4,13 @@
|
||||
* @property int $orderId
|
||||
* @property int $articleId
|
||||
* @property int $quantity
|
||||
* @property int $price
|
||||
*/
|
||||
|
||||
class WarehouseEShopOrderItemModel extends TTCrudBaseModel {
|
||||
public int $id;
|
||||
public int $orderId;
|
||||
public ?int $articleId;
|
||||
public ?int $articlePacketId;
|
||||
public int $quantity;
|
||||
public ?int $articlePacketId;
|
||||
}
|
||||
@@ -9,6 +9,12 @@ class WarehouseHistoryController {
|
||||
$me = new User();
|
||||
$me->loadMe();
|
||||
|
||||
foreach ($postData as $key => $value) {
|
||||
if (is_array($value)) {
|
||||
$postData[$key] = json_encode($value);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (array_diff_assoc($postData, (array) $currentData) as $key => $value) {
|
||||
WarehouseHistoryModel::create(['table' => $mod,
|
||||
'row_id' => $postData['id'],
|
||||
|
||||
@@ -4,14 +4,25 @@ class WarehouseItemController extends TTCrud {
|
||||
protected string $headerTitle = 'Eintrag';
|
||||
protected string $createText = 'Eintrag erstellen';
|
||||
|
||||
// TODO: change articleId and warehouseLocationId to autocomplete
|
||||
// TODO: check if historyController is needed
|
||||
// TODO: check if apiUrl uses self::getUrl to get the correct URL
|
||||
|
||||
// @formatter:off
|
||||
protected array $columns = [
|
||||
['key' => 'articleId', 'text' => 'Artikel', 'required' => true, 'type' => 'select','table' => ['class' => 'text-nowrap'], 'modal' => ['items' => [], 'type' => 'select']],
|
||||
['key' => 'warehouseLocationId', 'text' => 'Lagerort', 'required' => true, 'type' => 'select', 'modal' => ['items' => [], 'type' => 'select']],
|
||||
['key' => 'quantity', 'text' => 'Menge', 'required' => true, 'type' => 'number'],
|
||||
['key' => 'serialNumber', 'text' => 'Seriennummer', 'required' => false],
|
||||
['key' => 'articleId', 'text' => 'Artikel', 'required' => true, 'type' => 'autocomplete','table' => ['class' => 'text-nowrap', 'filter' => 'autocomplete'],'modal' => [
|
||||
'apiUrl' => 'WarehouseArticle/autocomplete','items' => 'WarehouseArticle/autocomplete', 'type' => 'autocomplete']],
|
||||
['key' => 'warehouseLocationId', 'text' => 'Lagerort', 'required' => true, 'type' => 'autocomplete', 'table' => ['filter' => 'autocomplete'], 'modal' => [
|
||||
'items' => 'WarehouseLocation/autocomplete',
|
||||
'apiUrl' => 'WarehouseLocation/autocomplete', 'type' => 'autocomplete']],
|
||||
['key' => 'quantity', 'text' => 'Menge', 'required' => false, 'type' => 'number'
|
||||
// quantity is only visible in modal when warehouseArticle(articleId).serial is false, add modal config here to reference warehouseArticle
|
||||
, 'modal' => ['type' => 'number',
|
||||
'visible' => ['reference' => 'WarehouseArticle', 'use' => 'articleId=id', 'key' => 'isSerialDocumentation', 'value' => false]
|
||||
]
|
||||
],
|
||||
['key' => 'rack', 'text' => 'Regal', 'required' => false, 'modal' => ['type' => 'text']],
|
||||
['key' => 'shelf', 'text' => 'Fach', 'required' => false, 'modal' => ['type' => 'text'], 'table' => false],
|
||||
['key' => 'serialNumber', 'text' => 'Seriennummer', 'required' => false, 'modal' => ['type' => 'text', 'visible' => ['reference' => 'WarehouseArticle', 'use' => 'articleId=id', 'key' => 'isSerialDocumentation', 'value' => true]]],
|
||||
['key' => 'note', 'text' => 'Notiz', 'required' => false],
|
||||
['key' => 'actions', 'text' => 'Aktionen', 'table' => ['filter' => false], 'required' => false, 'modal' => false]
|
||||
];
|
||||
@@ -58,18 +69,6 @@ class WarehouseItemController extends TTCrud {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function prepareCrudConfig() {
|
||||
$articles = array_map(function($article) {
|
||||
return ['value' => $article->id, 'text' => $article->title];
|
||||
}, WarehouseArticleModel::getAll());
|
||||
$this->columns[0]['modal']['items'] = $articles;
|
||||
|
||||
$locations = array_map(function($location) {
|
||||
return ['value' => $location->id, 'text' => $location->title];
|
||||
}, WarehouseLocationModel::getAll());
|
||||
$this->columns[1]['modal']['items'] = $locations;
|
||||
}
|
||||
|
||||
protected function getHistoryAction() {
|
||||
self::returnJson((new WarehouseHistoryController)->getHistory($this->request->id, $this->mod, $this->columns));
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ class WarehouseItemModel extends TTCrudBaseModel {
|
||||
public int $id;
|
||||
public int $articleId;
|
||||
public int $warehouseLocationId;
|
||||
public int $quantity;
|
||||
public ?string $rack;
|
||||
public ?string $shelf;
|
||||
public ?int $quantity;
|
||||
public ?string $serialNumber;
|
||||
public ?string $note;
|
||||
}
|
||||
@@ -6,7 +6,9 @@ class WarehouseLocationController extends TTCrud {
|
||||
|
||||
protected array $columns = [
|
||||
['key' => 'title', 'text' => 'Titel', 'required' => true],
|
||||
['key' => 'assignedTo', 'text' => 'Zugewiesen an', 'required' => true, 'modal' => ['type' => 'select', 'items' => []]],
|
||||
['key' => 'assignedTo', 'text' => 'Zugewiesen an', 'required' => true,
|
||||
'table' => ['filter' => 'select', 'items' => []],
|
||||
'modal' => ['type' => 'select', 'items' => []]],
|
||||
['key' => 'actions', 'text' => 'Aktionen', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center']],
|
||||
];
|
||||
|
||||
|
||||
@@ -3,5 +3,8 @@
|
||||
class WarehouseLocationModel extends TTCrudBaseModel {
|
||||
public int $id;
|
||||
public string $title;
|
||||
public string $description;
|
||||
public int $assignedTo;
|
||||
public int $createdBy;
|
||||
public int $create;
|
||||
}
|
||||
9
application/WarehouseOrder/WarehouseOrder.php
Normal file
9
application/WarehouseOrder/WarehouseOrder.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @property mixed|null $name
|
||||
*/
|
||||
class WarehouseOrder extends mfBaseModel
|
||||
{
|
||||
|
||||
}
|
||||
110
application/WarehouseOrder/WarehouseOrderController.php
Normal file
110
application/WarehouseOrder/WarehouseOrderController.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
//TODO: enable switching distributors in the order preview
|
||||
|
||||
class WarehouseOrderController extends TTCrud {
|
||||
protected string $headerTitle = 'Lieferantenbestellungen';
|
||||
protected bool $createText = false;
|
||||
|
||||
protected array $columns = [
|
||||
['key' => 'id', 'text' => 'ID', 'modal' => false],
|
||||
['key' => 'distributorId', 'text' => 'Lieferant', 'required' => true, 'type' => 'autocomplete','table' => ['class' => 'text-nowrap', 'filter' => 'autocomplete'],'modal' => [
|
||||
'apiUrl' => 'WarehouseDistributor/autocomplete','items' => 'WarehouseDistributor/autocomplete', 'type' => 'autocomplete']],
|
||||
['key' => 'extRef', 'text' => 'Externe Referenz', 'required' => false],
|
||||
['key' => 'intRef', 'text' => 'Interne Referenz', 'required' => false],
|
||||
['key' => 'status', 'text' => 'Status', 'required' => true, 'modal' => ['type' => 'select', 'items' => [
|
||||
['value' => 'new', 'text' => 'Neu'],
|
||||
['value' => 'accepted', 'text' => 'An Lieferant übergeben'],
|
||||
['value' => 'sent', 'text' => 'Gesendet'],
|
||||
['value' => 'done', 'text' => 'Erledigt'],
|
||||
]]],
|
||||
['key' => 'trackingNumber', 'text' => 'Trackingnummer', 'required' => false],
|
||||
['key' => 'sum', 'text' => 'Summe', 'required' => true, 'modal' => false, 'table' => ['filter' => 'numberRange']],
|
||||
['key' => 'create', 'text' => 'Erstellt', 'required' => true, 'modal' => false, 'filter' => 'datetime'],
|
||||
['key' => 'createBy', 'text' => 'Erstellt von', 'required' => true, 'table' => ['filter' => 'select'], 'modal' => ['type' => 'select', 'items' => []]],
|
||||
['key' => 'actions', 'text' => 'Aktionen', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center']],
|
||||
];
|
||||
|
||||
protected array $additionalActions = [['key' => 'openHistory', 'title' => 'Historie', 'class' => 'fas fa-history text-primary']];
|
||||
|
||||
protected array $infoMessages = ['create' => 'Bestellung wurde erfolgreich erstellt.',
|
||||
'update' => 'Bestellung wurde aktualisiert.',
|
||||
'delete' => 'Bestellung wurde gelöscht',
|
||||
'noChanges' => 'Keine Änderungen',];
|
||||
|
||||
public function permissionCheck(): bool {
|
||||
return $this->user->can(["WarehouseEShop"]);
|
||||
}
|
||||
|
||||
protected function prepareCrudConfig() {
|
||||
// Fill Users in createBy column
|
||||
$column = array_search('createBy', array_column($this->columns, 'key'));
|
||||
$this->columns[$column]['modal']['items'] = array_map(function ($user) {
|
||||
return ['value' => intval($user->id), 'text' => $user->name];
|
||||
}, UserModel::search());
|
||||
|
||||
}
|
||||
|
||||
protected function createOrderAction() {
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
$json = json_decode(file_get_contents('php://input'), true);
|
||||
$orders = $json;
|
||||
$orderIds = [];
|
||||
|
||||
foreach ($orders as $order) {
|
||||
$distributor = $order['distributor'][0];
|
||||
$orderAmount = $order['orderAmount'];
|
||||
$orders = $order['orders'];
|
||||
|
||||
$order = [
|
||||
'distributorId' => $distributor['id'],
|
||||
'extRef' => null,
|
||||
'status' => 'new',
|
||||
'trackingNumber' => null,
|
||||
'sum' => $orderAmount,
|
||||
'create' => time(),
|
||||
'createBy' => $this->user->id,
|
||||
];
|
||||
|
||||
$orderId = WarehouseOrderModel::create($order);
|
||||
$orderIds[] = $orderId;
|
||||
|
||||
foreach ($orders as $orderItem) {
|
||||
$article = WarehouseArticleModel::get($orderItem['articleId']);
|
||||
|
||||
WarehouseEShopOrderItemModel::create([
|
||||
'orderId' => $orderId,
|
||||
'articleId' => $orderItem['articleId'],
|
||||
'quantity' => $orderItem['amount'],
|
||||
'price' => $article->cheapestPurchasePrice,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
self::returnJson(['success' => true, 'message' => $this->infoMessages['create'], 'ids' => $orderIds]);
|
||||
}
|
||||
|
||||
protected function getOrderItemsAction() {
|
||||
$orderItems = WarehouseEShopOrderItemModel::getAll(['orderId' => $this->request->id]);
|
||||
|
||||
// also get the article name of the order items
|
||||
|
||||
foreach ($orderItems as $key => $orderItem) {
|
||||
$article = WarehouseArticleModel::get($orderItem->articleId);
|
||||
$orderItem->articleName = $article->title;
|
||||
}
|
||||
|
||||
self::returnJson($orderItems);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
28
application/WarehouseOrder/WarehouseOrderModel.php
Normal file
28
application/WarehouseOrder/WarehouseOrderModel.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
//TODO: fix phpdoc
|
||||
/**
|
||||
* @property int $id
|
||||
* @property 'new'|'accepted'|'sent'|'done' $status
|
||||
* @property 'singleAddress'|'multipleAddresses' $deliveryMode
|
||||
* @property string $deliveryAddressName
|
||||
* @property string $deliveryAddressLine
|
||||
* @property string $deliveryAddressPLZ
|
||||
* @property string $deliveryAddressCity
|
||||
* @property int $create
|
||||
* @property int $createBy
|
||||
*/
|
||||
|
||||
// id, distributorId, intRef, extRef, status, trackingNumber, create, createBy
|
||||
class WarehouseOrderModel extends TTCrudBaseModel {
|
||||
public int $id;
|
||||
public int $distributorId;
|
||||
public ?string $intRef;
|
||||
public ?string $extRef;
|
||||
public float $sum;
|
||||
public string $status;
|
||||
public ?string $trackingNumber;
|
||||
public int $create;
|
||||
public int $createBy;
|
||||
}
|
||||
|
||||
|
||||
9
application/WarehouseOrderItem/WarehouseOrderItem.php
Normal file
9
application/WarehouseOrderItem/WarehouseOrderItem.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @property mixed|null $name
|
||||
*/
|
||||
class WarehouseOrderItem extends mfBaseModel
|
||||
{
|
||||
|
||||
}
|
||||
16
application/WarehouseOrderItem/WarehouseOrderItemModel.php
Normal file
16
application/WarehouseOrderItem/WarehouseOrderItemModel.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $orderId
|
||||
* @property int $articleId
|
||||
* @property int $quantity
|
||||
* @property int $price
|
||||
*/
|
||||
|
||||
class WarehouseOrderItemModel extends TTCrudBaseModel {
|
||||
public int $id;
|
||||
public int $orderId;
|
||||
public int $articleId;
|
||||
public int $quantity;
|
||||
public float $price;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @property mixed|null $name
|
||||
*/
|
||||
class WarehouseRevenueAccount extends mfBaseModel
|
||||
{
|
||||
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
<?php
|
||||
|
||||
class WarehouseRevenueAccountController extends TTCrud {
|
||||
protected string $headerTitle = 'Erlöskontos';
|
||||
protected string $createText = 'Erlöskonto erstellen';
|
||||
|
||||
// @formatter:off
|
||||
protected array $columns = [
|
||||
['key' => 'title', 'text' => 'Titel', 'required' => true],
|
||||
['key' => 'revenueAccountNumber', 'text' => 'Erlöskonto Nummer', 'required' => true, 'modal' => ['type' => 'number']],
|
||||
['key' => 'actions', 'text' => 'Aktionen', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center', 'priority' => 10]],
|
||||
];
|
||||
// @formatter:on
|
||||
|
||||
protected array $infoMessages = ['create' => 'Erlöskonto wurde erstellt',
|
||||
'update' => 'Erlöskonto wurde aktualisiert',
|
||||
'delete' => 'Erlöskonto wurde gelöscht',
|
||||
'noChanges' => 'Keine Änderungen'];
|
||||
|
||||
protected function beforeUpdate($postData): bool {
|
||||
(new WarehouseHistoryController)->create($postData, $this->mod);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function getHistoryAction() {
|
||||
$history = WarehouseHistoryModel::getByRowId($this->request->id, $this->mod);
|
||||
|
||||
$history = array_map(function ($item) {
|
||||
$item = (array) $item;
|
||||
|
||||
$item['columnHeader'] = $this->columns[array_search($item['key'], array_column($this->columns, 'key'))]['text'];
|
||||
return $item;
|
||||
}, $history);
|
||||
|
||||
self::returnJson($history);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
<?php
|
||||
|
||||
class WarehouseRevenueAccountModel extends TTCrudBaseModel {
|
||||
public int $id;
|
||||
public int $revenueAccountNumber;
|
||||
public string $title;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @property mixed|null $name
|
||||
*/
|
||||
class WarehouseShippingNote extends mfBaseModel
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
<?php
|
||||
|
||||
class WarehouseShippingNoteController extends TTCrud {
|
||||
protected string $headerTitle = 'Lieferscheine';
|
||||
protected bool $createText = false;
|
||||
|
||||
protected array $columns = [['key' => 'id', 'text' => 'LS-Nr.', 'required' => false, 'modal' => false, 'table' => ['class' => 'text-nowrap']],
|
||||
['key' => 'billingAddressId',
|
||||
'text' => 'Rechnungsadresse',
|
||||
'required' => true,
|
||||
'type' => 'autocomplete',
|
||||
'table' => ['class' => 'text-nowrap', 'filter' => 'autocomplete'],
|
||||
'modal' => ['apiUrl' => 'Address/api?do=findAddress', 'items' => '/Address/Api?do=findAddress', 'type' => 'autocomplete']],
|
||||
['key' => 'deliveryAddressName', 'text' => 'L.-Adr. Name', 'required' => true],
|
||||
['key' => 'deliveryAddressLine', 'text' => 'L.-Adr.', 'required' => true],
|
||||
['key' => 'deliveryAddressPLZ', 'text' => 'L.-Adr. PLZ', 'required' => true],
|
||||
['key' => 'deliveryAddressCity', 'text' => 'L.-Adr. Ort', 'required' => true],
|
||||
['key' => 'status',
|
||||
'text' => 'Status',
|
||||
'required' => true,
|
||||
'table' => ['filter' => 'select'],
|
||||
'modal' => ['type' => 'select',
|
||||
'items' => [['value' => 'new', 'text' => 'Neu'],
|
||||
['value' => 'accepted', 'text' => 'Akzeptiert'],
|
||||
['value' => 'invoiced', 'text' => 'In Rechnung gestellt'],]]],
|
||||
['key' => 'positions', 'text' => 'Positionen', 'required' => true, 'table' => false, 'modal' => false],
|
||||
['key' => 'create', 'text' => 'Erstellt', 'required' => false, 'modal' => false, 'table' => ['filter' => 'date']],
|
||||
['key' => 'createBy',
|
||||
'text' => 'Erstellt von',
|
||||
'required' => true,
|
||||
'type' => 'autocomplete',
|
||||
'table' => ['class' => 'text-nowrap', 'filter' => 'select'],
|
||||
'modal' => ['items' => [], 'type' => 'select',]],
|
||||
|
||||
['key' => 'actions',
|
||||
'text' => 'Aktionen',
|
||||
'required' => false,
|
||||
'modal' => false,
|
||||
'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center']],];
|
||||
|
||||
protected array $additionalActions = [['key' => 'openHistory', 'title' => 'Historie', 'class' => 'fas fa-history text-primary'],
|
||||
['key' => 'print', 'title' => 'Drucken', 'class' => 'fas fa-print text-primary'],
|
||||
['key' => 'printWithPrice', 'title' => 'Drucken mit Preis', 'class' => 'fas fa-print text-success'],
|
||||
];
|
||||
|
||||
protected array $infoMessages = ['create' => 'Lieferschein wurde erstellt.',
|
||||
'update' => 'Lieferschein wurde aktualisiert',
|
||||
'delete' => 'Lieferschein wurde gelöscht',
|
||||
'noChanges' => 'Keine Änderungen vorgenommen'];
|
||||
|
||||
protected function prepareCrudConfig() {
|
||||
$users = array_map(function ($user) {
|
||||
return ['value' => intval($user->id), 'text' => $user->name];
|
||||
}, UserModel::search());
|
||||
|
||||
$this->columns[array_search('createBy', array_column($this->columns, 'key'))]['modal']['items'] = $users;
|
||||
}
|
||||
|
||||
protected function beforeCreate($postData): bool {
|
||||
// if postdata status is not new we return an error
|
||||
if ($postData['status'] !== 'new') {
|
||||
http_response_code(500);
|
||||
self::returnJson(['success' => false, 'message' => 'Status muss "Neu" sein']);
|
||||
die();
|
||||
}
|
||||
$postData['positions'] = json_encode($postData['positions']);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function customAutoCompleteBillingAddressId($id) {
|
||||
$address = new Address($id);
|
||||
if ($address->id) {
|
||||
$result = ['id' => $address->id,
|
||||
'title' => str_replace("'", "\\'", str_replace(["\n",
|
||||
"\r"], " ", $address->getCompanyOrName())) . " (" . $address->zip . " " . $address->city . ", " . $address->street . ")" . (($address->customer_number) ? " [" . $address->customer_number . "]" : "")];
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
protected function beforeUpdate($postData): bool {
|
||||
$postData['positions'] = json_encode($postData['positions']);
|
||||
(new WarehouseHistoryController)->create($postData, $this->mod);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function getHistoryAction() {
|
||||
$historyEntries = [];
|
||||
|
||||
// remove all history elements where key is positions
|
||||
|
||||
foreach ((new WarehouseHistoryController)->getHistory($this->request->id, $this->mod, $this->columns) as $entry) {
|
||||
if ($entry['key'] !== 'positions') {
|
||||
$historyEntries[] = $entry;
|
||||
}
|
||||
}
|
||||
|
||||
// $historyEntries = array_filter($historyEntries, function ($entry) {
|
||||
// return $entry['key'] !== 'positions';
|
||||
// });
|
||||
|
||||
self::returnJson($historyEntries);
|
||||
}
|
||||
|
||||
protected function getArticleAddressPriceAction() {
|
||||
$articleId = $this->request->articleId;
|
||||
$addressId = $this->request->addressId;
|
||||
|
||||
if (strlen($articleId) < 1) {
|
||||
http_response_code(500);
|
||||
self::returnJson(['success' => false, 'message' => 'Keine Artikel ID gefunden']);
|
||||
}
|
||||
|
||||
if (strlen($addressId) < 1) {
|
||||
http_response_code(500);
|
||||
self::returnJson(['success' => false, 'message' => 'Keine Adress ID gefunden']);
|
||||
}
|
||||
|
||||
//TODO: implement a select to select price category for each address
|
||||
// for now we default with price with name "Verkauf"
|
||||
$prices = WarehouseArticlePriceTypeModel::getAll(['title' => 'Verkauf']);
|
||||
// if array is empty we return an error
|
||||
if (empty($prices)) {
|
||||
http_response_code(500);
|
||||
self::returnJson(['success' => false, 'message' => 'Keine Preiskategorie gefunden']);
|
||||
}
|
||||
$priceType = $prices[0]->title;
|
||||
|
||||
$article = WarehouseArticleModel::get($articleId);
|
||||
$sellPrices = json_decode($article->cheapestSellPrice, true);
|
||||
$sellPrice = array_search($priceType, array_column($sellPrices, 'title'));
|
||||
|
||||
if (empty($sellPrice)) {
|
||||
http_response_code(500);
|
||||
self::returnJson(['success' => false, 'message' => 'Kein Preis gefunden']);
|
||||
}
|
||||
|
||||
self::returnJson(['success' => true, 'price' => $sellPrices[$sellPrice]['price']]);
|
||||
}
|
||||
|
||||
protected function getDeliveryAddressesAction() {
|
||||
$billingAddressId = $this->request->billingAddressId;
|
||||
if (strlen($billingAddressId) < 1) {
|
||||
http_response_code(500);
|
||||
self::returnJson(['success' => false, 'message' => 'Keine Rechnungsadresse gefunden']);
|
||||
}
|
||||
|
||||
$deliveryAddresses = WarehouseShippingNoteModel::getAll(['billingAddressId' => $billingAddressId]);
|
||||
// TODO: maybe this should be improved as it is kinda hacky
|
||||
$result = [];
|
||||
foreach ($deliveryAddresses as $deliveryAddress) {
|
||||
$found = false;
|
||||
foreach ($result as $r) {
|
||||
if ($r->deliveryAddressName == $deliveryAddress->deliveryAddressName && $r->deliveryAddressLine == $deliveryAddress->deliveryAddressLine) {
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($found) {
|
||||
continue;
|
||||
}
|
||||
$result[] = $deliveryAddress;
|
||||
}
|
||||
|
||||
self::returnJson($result);
|
||||
}
|
||||
|
||||
protected function getAllTextElementsAction() {
|
||||
$textElements = WarehouseShippingNoteTextElementModel::getAll();
|
||||
self::returnJson($textElements);
|
||||
}
|
||||
|
||||
protected function createPDFAction() {
|
||||
$id = $this->request->id;
|
||||
if (strlen($id) < 1) {
|
||||
http_response_code(500);
|
||||
self::returnJson(['success' => false, 'message' => 'Lieferschein wurde nicht gefunden']);
|
||||
}
|
||||
|
||||
$shippingNote = WarehouseShippingNoteModel::get($id);
|
||||
$address = AddressModel::getOne($shippingNote->billingAddressId);
|
||||
$positions = [];
|
||||
|
||||
// loop through all positions and add articleTitle and articleDescription to each position entry
|
||||
foreach (json_decode($shippingNote->positions, true) as $position) {
|
||||
$article = WarehouseArticleModel::get($position['article']);
|
||||
$position['articleTitle'] = $article->title;
|
||||
$position['articleDescription'] = $article->description;
|
||||
$position['articleUnit'] = $article->unit;
|
||||
$positions[] = $position;
|
||||
}
|
||||
|
||||
$textElements = [];
|
||||
// parse shippingNote.textElements ({"1":true,"2":true}) to array, fetch each text element and put content into array
|
||||
$shippingNoteTextElements = json_decode($shippingNote->textElements, true);
|
||||
foreach ($shippingNoteTextElements as $key => $value) {
|
||||
if ($value) {
|
||||
$textElement = WarehouseShippingNoteTextElementModel::get($key);
|
||||
$textElements[] = $textElement->content;
|
||||
}
|
||||
}
|
||||
if (empty($textElements)) {
|
||||
$textElements = null;
|
||||
}
|
||||
|
||||
$pdf_vars = ["shippingNote" => $shippingNote,
|
||||
"positions" => $positions,
|
||||
"textElements" => $textElements,
|
||||
"showPrices" => isset($_GET['price']) && $_GET['price'] == "true",
|
||||
"bank_iban" => TT_INVOICE_BANK_IBAN,
|
||||
"bank_bic" => TT_INVOICE_BANK_BIC,
|
||||
"bank_bank" => TT_INVOICE_BANK_BANK,
|
||||
"bank_owner" => TT_INVOICE_BANK_OWNER];
|
||||
|
||||
// Replace placeholders in header
|
||||
// create shipping note in this format LS2024-X0001
|
||||
// pad number on the left side with zeros
|
||||
$shippingNoteNumber = "LS" . date("Y", $shippingNote->create) . "-" . str_pad($shippingNote->id, 4, "0", STR_PAD_LEFT);
|
||||
$headerHtml = file_get_contents(BASEDIR . "/Layout/default/WarehouseShippingNote/PDF_HEADER.html");
|
||||
$headerHtml = str_replace("{{ basedir }}", BASEDIR, $headerHtml);
|
||||
$headerHtml = str_replace("{{ addressLine_1 }}", $shippingNote->deliveryAddressName, $headerHtml);
|
||||
$headerHtml = str_replace("{{ addressLine_2 }}", $shippingNote->deliveryAddressLine, $headerHtml);
|
||||
$headerHtml = str_replace("{{ addressLine_3 }}", $shippingNote->deliveryAddressPLZ . " " . $shippingNote->deliveryAddressCity, $headerHtml);
|
||||
$headerHtml = str_replace("{{ addressLine_4 }}", "", $headerHtml);
|
||||
$headerHtml = str_replace("{{ addressLine_5 }}", "", $headerHtml);
|
||||
$headerHtml = str_replace("{{ customerNumber }}", $address->customer_number, $headerHtml);
|
||||
$headerHtml = str_replace("{{ shippingNoteNumber }}", $shippingNoteNumber, $headerHtml);
|
||||
$headerHtml = str_replace("{{ shippingNoteDate }}", date("d.m.Y", $shippingNote->create), $headerHtml);
|
||||
|
||||
$headerFile = BASEDIR . "/var/temp/shipping-note_header-" . date("U") . "-" . rand(1000, 9999) . ".html";
|
||||
file_put_contents($headerFile, $headerHtml);
|
||||
|
||||
|
||||
// Replace placeholders in header
|
||||
$footerHtml = file_get_contents(BASEDIR . "/Layout/default/WarehouseShippingNote/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/shipping-note_header-" . date("U") . "-" . rand(1000, 9999) . ".html";
|
||||
file_put_contents($footerFile, $footerHtml);
|
||||
|
||||
|
||||
$pdf = new PdfForm("WarehouseShippingNote/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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
class WarehouseShippingNoteModel extends TTCrudBaseModel {
|
||||
public int $id;
|
||||
public int $billingAddressId;
|
||||
public string $deliveryAddressName;
|
||||
public string $deliveryAddressLine;
|
||||
public string $deliveryAddressPLZ;
|
||||
public string $deliveryAddressCity;
|
||||
public string $status; // 'new'|'accepted'|'invoiced'
|
||||
public string $positions;
|
||||
public string $textElements;
|
||||
public int $create;
|
||||
public int $createBy;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @property mixed|null $name
|
||||
*/
|
||||
class WarehouseShippingNoteTextElement extends mfBaseModel
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
class WarehouseShippingNoteTextElementController extends TTCrud {
|
||||
protected string $headerTitle = 'Lieferschein Textelemente';
|
||||
protected string $createText = 'Lieferschein Textelement erstellen';
|
||||
|
||||
// @formatter:off
|
||||
protected array $columns = [
|
||||
['key' => 'title', 'text' => 'Titel', 'required' => true],
|
||||
['key' => 'content', 'text' => 'Text', 'required' => true, 'modal' => []],
|
||||
['key' => 'create', 'text' => 'Erstellt', 'required' => false, 'modal' => false, 'table' => ['filter' => 'datetime', 'class' => 'text-nowrap']],
|
||||
['key' => 'createBy', 'text' => 'Erstellt von', 'required' => false, 'modal' => [
|
||||
'type' => 'select', 'items' => [],'visible' => false], ],
|
||||
['key' => 'actions', 'text' => 'Aktionen', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center', 'priority' => 10]],
|
||||
];
|
||||
// @formatter:on
|
||||
|
||||
protected array $infoMessages = ['create' => 'Lieferschein Textelement wurde erstellt',
|
||||
'update' => 'Lieferschein Textelement wurde aktualisiert',
|
||||
'delete' => 'Lieferschein Textelement wurde gelöscht',
|
||||
'noChanges' => 'Keine Änderungen'];
|
||||
|
||||
protected function prepareCrudConfig() {
|
||||
// add all users to createBy column
|
||||
$this->columns[array_search('createBy', array_column($this->columns, 'key'))]['modal']['items'] = array_map(function ($user) {
|
||||
return ['value' => $user->id, 'text' => $user->name];
|
||||
}, UserModel::getAll());
|
||||
}
|
||||
|
||||
protected function beforeUpdate($postData): bool {
|
||||
(new WarehouseHistoryController)->create($postData, $this->mod);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function getHistoryAction() {
|
||||
$history = WarehouseHistoryModel::getByRowId($this->request->id, $this->mod);
|
||||
|
||||
$history = array_map(function ($item) {
|
||||
$item = (array) $item;
|
||||
|
||||
$item['columnHeader'] = $this->columns[array_search($item['key'], array_column($this->columns, 'key'))]['text'];
|
||||
return $item;
|
||||
}, $history);
|
||||
|
||||
self::returnJson($history);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
class WarehouseShippingNoteTextElementModel extends TTCrudBaseModel {
|
||||
public int $id;
|
||||
public string $title;
|
||||
public string $content;
|
||||
public int $create;
|
||||
public int $createBy;
|
||||
}
|
||||
|
||||
146
db/migrations/20241010070000_warehouse_modify.php
Normal file
146
db/migrations/20241010070000_warehouse_modify.php
Normal file
@@ -0,0 +1,146 @@
|
||||
<?php /** @noinspection ALL */
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class WarehouseModify extends AbstractMigration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if ($this->getEnvironment() == "thetool") {
|
||||
// Create Tables
|
||||
$WarehouseShippingNoteTextElement = $this->table("WarehouseShippingNoteTextElement", ["signed" => true]);
|
||||
$WarehouseShippingNoteTextElement
|
||||
->addColumn('title', 'string', ['null' => false])
|
||||
->addColumn('content', 'text', ['null' => false])
|
||||
->addColumn('create', 'integer', ['null' => false, 'default' => 1728541890])
|
||||
->addColumn('createBy', 'integer', ['null' => false, 'default' => 1])
|
||||
->create();
|
||||
|
||||
$WarehouseShippingNote = $this->table("WarehouseShippingNote", ["signed" => true]);
|
||||
$WarehouseShippingNote
|
||||
->addColumn('billingAddressId', 'integer', ['null' => false])
|
||||
->addColumn('deliveryAddressName', 'string', ['null' => false])
|
||||
->addColumn('deliveryAddressLine', 'string', ['null' => false])
|
||||
->addColumn('deliveryAddressPLZ', 'string', ['null' => false])
|
||||
->addColumn('deliveryAddressCity', 'string', ['null' => false])
|
||||
->addColumn('status', 'enum', ['values' => ['new', 'accepted', 'invoiced'], 'null' => false])
|
||||
->addColumn('positions', 'text', ['null' => false])
|
||||
->addColumn('textElements', 'text', ['null' => false])
|
||||
->addColumn('create', 'integer', ['null' => false, 'default' => 1728541890])
|
||||
->addColumn('createBy', 'integer', ['null' => false, 'default' => 1])
|
||||
->create();
|
||||
|
||||
$WarehouseOrder = $this->table("WarehouseOrder", ["signed" => true]);
|
||||
$WarehouseOrder
|
||||
->addColumn('distributorId', 'integer', ['null' => false])
|
||||
->addColumn('intRef', 'string', ['null' => true])
|
||||
->addColumn('extRef', 'string', ['null' => true])
|
||||
->addColumn('status', 'enum', ['values' => ['new', 'accepted', 'sent', 'done'], 'null' => false, 'default' => 'new'])
|
||||
->addColumn('sum', 'float', ['null' => true])
|
||||
->addColumn('trackingNumber', 'string', ['null' => true])
|
||||
->addColumn('create', 'integer', ['null' => false, 'default' => 1728541890])
|
||||
->addColumn('createBy', 'integer', ['null' => false, 'default' => 1])
|
||||
->create();
|
||||
|
||||
$WarehouseOrderItem = $this->table("WarehouseOrderItem", ["signed" => true]);
|
||||
$WarehouseOrderItem
|
||||
->addColumn('orderId', 'integer', ['null' => false])
|
||||
->addColumn('articleId', 'integer', ['null' => false])
|
||||
->addColumn('quantity', 'integer', ['null' => false])
|
||||
->addColumn('price', 'float', ['null' => false])
|
||||
->addForeignKey('orderId', 'WarehouseOrder', 'id')
|
||||
->addForeignKey('articleId', 'WarehouseArticle', 'id')
|
||||
->create();
|
||||
|
||||
// Delete Table
|
||||
$WarehouseRevenueAccount = $this->table("WarehouseRevenueAccount");
|
||||
$WarehouseRevenueAccount->drop()->save();
|
||||
|
||||
// Modify Tables
|
||||
$WarehouseLocation = $this->table("WarehouseLocation");
|
||||
$WarehouseLocation
|
||||
->addColumn('description', 'text', ['null' => true])
|
||||
->addColumn('createBy', 'integer', ['null' => false, 'default' => 1728541890])
|
||||
->addColumn('create', 'integer', ['null' => false, 'default' => 1])
|
||||
->update();
|
||||
|
||||
$WarehouseItem = $this->table("WarehouseItem");
|
||||
$WarehouseItem
|
||||
->changeColumn('quantity', 'integer', ['null' => true])
|
||||
->addColumn('rack', 'string', ['null' => true])
|
||||
->addColumn('shelf', 'string', ['null' => true])
|
||||
->addColumn('createBy', 'integer', ['null' => false, 'default' => 1728541890])
|
||||
->addColumn('create', 'integer', ['null' => false, 'default' => 1])
|
||||
->update();
|
||||
|
||||
$WarehouseArticlePriceType = $this->table("WarehouseArticlePriceType");
|
||||
$WarehouseArticlePriceType
|
||||
->addColumn('description', 'string', ['null' => true])
|
||||
->addColumn('defaultPriceFactor', 'float', ['null' => true])
|
||||
->addColumn('createBy', 'integer', ['null' => false, 'default' => 1728541890])
|
||||
->addColumn('create', 'integer', ['null' => false, 'default' => 1])
|
||||
->update();
|
||||
|
||||
// Set nullable createBy and create columns
|
||||
$WarehouseLocation
|
||||
->changeColumn('createBy', 'integer', ['null' => true])
|
||||
->changeColumn('create', 'integer', ['null' => true])
|
||||
->save();
|
||||
|
||||
$WarehouseItem
|
||||
->changeColumn('createBy', 'integer', ['null' => true])
|
||||
->changeColumn('create', 'integer', ['null' => true])
|
||||
->save();
|
||||
|
||||
$WarehouseArticlePriceType
|
||||
->changeColumn('createBy', 'integer', ['null' => true])
|
||||
->changeColumn('create', 'integer', ['null' => true])
|
||||
->save();
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if ($this->getEnvironment() == "thetool") {
|
||||
// Drop created tables
|
||||
$this->table('WarehouseShippingNoteTextElement')->drop()->save();
|
||||
$this->table('WarehouseShippingNote')->drop()->save();
|
||||
$this->table('WarehouseOrder')->drop()->save();
|
||||
$this->table('WarehouseOrderItem')->drop()->save();
|
||||
|
||||
// Recreate deleted table
|
||||
$WarehouseRevenueAccount = $this->table("WarehouseRevenueAccount");
|
||||
$WarehouseRevenueAccount
|
||||
->addColumn('revenueAccountNumber', 'integer', ['null' => false])
|
||||
->addColumn('title', 'string', ['null' => false])
|
||||
->create();
|
||||
|
||||
// Revert modifications
|
||||
$WarehouseLocation = $this->table("WarehouseLocation");
|
||||
$WarehouseLocation
|
||||
->removeColumn('description')
|
||||
->removeColumn('createBy')
|
||||
->removeColumn('create')
|
||||
->update();
|
||||
|
||||
$WarehouseItem = $this->table("WarehouseItem");
|
||||
$WarehouseItem
|
||||
->changeColumn('quantity', 'integer', ['null' => false])
|
||||
->removeColumn('rack')
|
||||
->removeColumn('shelf')
|
||||
->removeColumn('createBy')
|
||||
->removeColumn('create')
|
||||
->update();
|
||||
|
||||
$WarehouseArticlePriceType = $this->table("WarehouseArticlePriceType");
|
||||
$WarehouseArticlePriceType
|
||||
->removeColumn('description')
|
||||
->removeColumn('defaultPriceFactor')
|
||||
->removeColumn('createBy')
|
||||
->removeColumn('create')
|
||||
->update();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ services:
|
||||
adminer:
|
||||
image: adminer
|
||||
ports:
|
||||
- "8080:8080"
|
||||
- "8088:8080"
|
||||
volumes:
|
||||
- ./docker/adminer/php.ini:/etc/php/7.4/cli/conf.d/php.local.ini
|
||||
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
# Use Debian Bookworm as base image
|
||||
FROM debian:bookworm
|
||||
|
||||
# Install wkhtmltopdf
|
||||
RUN apt update
|
||||
RUN apt install wget libfontenc1 xfonts-75dpi xfonts-base xfonts-encodings xfonts-utils openssl build-essential libssl-dev libxrender-dev git-core libx11-dev libxext-dev libfontconfig1-dev libfreetype6-dev fontconfig -y
|
||||
# wget https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6-1/wkhtmltox_0.12.6-1.stretch_amd64.deb
|
||||
# dpkg ignore
|
||||
|
||||
|
||||
# Install apache2 and PHP and PHP modules
|
||||
RUN apt update && \
|
||||
apt install -y apache2 curl cron unzip php8.2 php8.2-curl php8.2-cli php8.2-mysqli php8.2-gd php8.2-zip php8.2-dom php8.2-mbstring && \
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* Class TTCrud
|
||||
* @property string $headerTitle
|
||||
* @property string $createText
|
||||
* @property string|null $historyController
|
||||
* @property array $columns
|
||||
* @property array $additionalActions
|
||||
* @property array $infoMessages
|
||||
@@ -32,7 +33,7 @@ class TTCrud extends mfBaseController {
|
||||
$this->redirect("Dashboard");
|
||||
}
|
||||
|
||||
$modelName = $this->mod . 'Model';
|
||||
$modelName = str_replace('Controller', 'Model', get_class($this));
|
||||
$this->model = new $modelName();
|
||||
$this->postData = json_decode(file_get_contents('php://input'), true);
|
||||
$this->checkArray = $this->getCheckArray();
|
||||
@@ -88,6 +89,20 @@ class TTCrud extends mfBaseController {
|
||||
return $newColumn;
|
||||
}, $this->columns);
|
||||
|
||||
// check if in columns array there is a column with key "actions" and if so, we set the priority of the first column to 20 and actions to 19
|
||||
$actionsColumn = array_filter($columns, function ($column) {
|
||||
return $column['key'] === 'actions';
|
||||
});
|
||||
if (count($actionsColumn) > 0) {
|
||||
$columns = array_map(function ($column) {
|
||||
if ($column['key'] === 'actions') {
|
||||
$column['priority'] = 119;
|
||||
}
|
||||
return $column;
|
||||
}, $columns);
|
||||
$columns[0]['priority'] = 120;
|
||||
}
|
||||
|
||||
return ['key' => $this->mod,
|
||||
'tableHeader' => $this->headerTitle,
|
||||
'createText' => $this->createText,
|
||||
@@ -105,7 +120,41 @@ class TTCrud extends mfBaseController {
|
||||
$filteredAvailable = $this->model::count($filter);
|
||||
$totalRows = $this->model::count();
|
||||
|
||||
// check if any column is a autocomplete to add the text to the row
|
||||
$autoCompleteColumns = array_filter($this->columns, function ($column) {
|
||||
return isset($column['type']) && isset($column['modal']) && isset($column['modal']['type']) && $column['type'] === 'autocomplete' && $column['modal']['type'] === 'autocomplete';
|
||||
|
||||
});
|
||||
$autocompleteData = [];
|
||||
foreach ($rows as $row) {
|
||||
$row = (array) $row;
|
||||
foreach ($autoCompleteColumns as $column) {
|
||||
if (isset($autocompleteData[$column['key']][$row[$column['key']]])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// if function customAutoComplete"COLUMN_KEY" is defined, we call it instead of the default
|
||||
$data = null;
|
||||
if (method_exists($this, 'customAutoComplete' . ucfirst($column['key']))) {
|
||||
$data = $this->{'customAutoComplete' . ucfirst($column['key'])}($row[$column['key']]);
|
||||
} else {
|
||||
|
||||
$autoCompleteModelName = explode('/', $column['modal']['apiUrl'])[0] . 'Model';
|
||||
$autoCompleteModel = new $autoCompleteModelName();
|
||||
$data = $autoCompleteModel::get($row[$column['key']]);
|
||||
|
||||
// TODO: fix that keys can be anything
|
||||
if (isset($data->name) && !isset($data->title)) {
|
||||
$data->title = $data->name;
|
||||
}
|
||||
}
|
||||
|
||||
$autocompleteData[$column['key']][$row[$column['key']]] = $data;
|
||||
}
|
||||
}
|
||||
|
||||
self::returnJson(["rows" => $rows,
|
||||
"autoCompleteData" => $autocompleteData,
|
||||
"pagination" => ["page" => $page,
|
||||
"total_pages" => ceil($filteredAvailable / $perPage),
|
||||
"per_page" => $perPage,
|
||||
@@ -114,6 +163,14 @@ class TTCrud extends mfBaseController {
|
||||
}
|
||||
|
||||
protected function createAction() {
|
||||
// if this->model has property createBy, set it to the current user id and create to current epoch time
|
||||
if (property_exists($this->model, 'createBy')) {
|
||||
$this->postData['createBy'] = $this->user->id;
|
||||
}
|
||||
if (property_exists($this->model, 'create')) {
|
||||
$this->postData['create'] = time();
|
||||
}
|
||||
|
||||
Helper::validateArray($this->postData, $this->checkArray);
|
||||
|
||||
if (method_exists($this, 'beforeCreate') && !$this->beforeCreate($this->postData)) {
|
||||
@@ -178,6 +235,18 @@ class TTCrud extends mfBaseController {
|
||||
return ['value' => $item->id, 'text' => $item->$textKey];
|
||||
}, $data));
|
||||
}
|
||||
|
||||
protected function getByIdAction() {
|
||||
$id = $_GET['id'] ?? null;
|
||||
if (!$id || !is_numeric($id)) {
|
||||
http_response_code(500);
|
||||
self::returnJson(['success' => false, 'message' => 'No ID provided.']);
|
||||
die();
|
||||
}
|
||||
|
||||
$data = (array) $this->model::get($id);
|
||||
self::returnJson($data);
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
@@ -19,7 +19,15 @@ class TTCrudBaseModel {
|
||||
$sqlValues = [];
|
||||
foreach ($data as $field => $value) {
|
||||
if (!property_exists(get_called_class(), $field)) {
|
||||
throw new Exception("Field $field does not exist in " . get_called_class());
|
||||
die(json_encode([
|
||||
"status" => "error",
|
||||
"error" => "Field $field does not exist in " . get_called_class(),
|
||||
"data" => $data
|
||||
]));
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
$value = json_encode($value);
|
||||
}
|
||||
|
||||
$sqlValues[] = $value === null ? 'NULL' : "'" . $db->real_escape_string($value) . "'";
|
||||
@@ -62,7 +70,12 @@ class TTCrudBaseModel {
|
||||
}
|
||||
|
||||
if (!isset($data[$field])) {
|
||||
throw new Exception("Required field $field is missing in data array");
|
||||
http_response_code(500);
|
||||
die(json_encode([
|
||||
"status" => "error",
|
||||
"error" => "Required field $field is missing in data array",
|
||||
"data" => $data
|
||||
]));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,6 +117,7 @@ class TTCrudBaseModel {
|
||||
$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());
|
||||
}
|
||||
$sql .= Helper::generateFilterCondition($value, $key, gettype($value) === "integer");
|
||||
@@ -161,6 +175,10 @@ class TTCrudBaseModel {
|
||||
$value = null;
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
$value = json_encode($value);
|
||||
}
|
||||
|
||||
$values[] = $value === null ? "`$field` = NULL" : "`$field` = '" . $db->real_escape_string($value) . "'";
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,9 @@ class mfExceptionhandler {
|
||||
public function __toString() {
|
||||
$str="[".$this->Time."] ";
|
||||
if(is_numeric($this->Code) && $this->Code > 0) {
|
||||
if($this->Code == 404 || $this->Code == 500) {
|
||||
http_response_code($this->Code);
|
||||
}
|
||||
$str.="(Error code ".$this->Code.") ";
|
||||
}
|
||||
|
||||
|
||||
@@ -169,6 +169,7 @@ class mfRouter {
|
||||
|
||||
}
|
||||
|
||||
$baseurl = $baseurl ?? "";
|
||||
$baseurl = preg_replace('@/$@', '', $baseurl);
|
||||
define("MFFANCYBASEURL",$baseurl);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
Vue.component('warehouse-administration', {
|
||||
//language=Vue
|
||||
template: `
|
||||
<tt-card title="Warehouse Administration">
|
||||
<warehouse-administration-switch/>
|
||||
|
||||
<div class="button-group" style="max-width: 300px;display: flex; flex-direction: column; gap: 10px;">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
:disabled="isLoading"
|
||||
@click="createLocationsForAllEmployees"
|
||||
title="Automatische Lagerorterstellung für Mitarbeiter. Mitarbeiter mit Firmenwagen erhalten einen zugehörigen Fahrzeuglagerort.">
|
||||
Lagerorte für alle Mitarbeiter erstellen
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
:disabled="isLoading"
|
||||
@click="updateAllSalesPrices">
|
||||
Alle Verkaufspreise updaten
|
||||
</button>
|
||||
</div>
|
||||
</tt-card>
|
||||
`,
|
||||
data() {
|
||||
return {
|
||||
isLoading: false,
|
||||
window: window,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async createLocationsForAllEmployees() {
|
||||
this.isLoading = true;
|
||||
const response = await axios.get(window.TT_CONFIG.BASE_URL + '/createLocations');
|
||||
|
||||
if (response.data.success) {
|
||||
this.window.notify('success', response.data.message || 'Lagerorte wurden erfolgreich erstellt.');
|
||||
} else {
|
||||
this.window.notify('error', response.data.message || 'Fehler beim Erstellen der Lagerorte.');
|
||||
}
|
||||
this.isLoading = false;
|
||||
},
|
||||
async updateAllSalesPrices() {
|
||||
this.isLoading = true;
|
||||
const response = await axios.get(window.TT_CONFIG.BASE_PATH + '/WarehouseArticle/updatePrices');
|
||||
if (response.data.success) {
|
||||
this.window.notify('success', 'Verkaufspreise wurden erfolgreich aktualisiert.');
|
||||
} else {
|
||||
this.window.notify('error', 'Fehler beim Aktualisieren der Verkaufspreise.');
|
||||
}
|
||||
this.isLoading = false;
|
||||
},
|
||||
sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -250,11 +250,14 @@ Vue.component('warehouse-article', {
|
||||
//language=Vue
|
||||
template: `
|
||||
<tt-card>
|
||||
|
||||
<tt-table-crud ref="table"
|
||||
@openHistory="historyModal = true; historyModalId = $event.id"
|
||||
@editDistributorEntries="distributorModal = true; distributorModalId = $event.id"
|
||||
@editPricesEntries="priceModal = true; priceModalId = $event.id"
|
||||
@editThresholdEntries="thresholdModal = true; thresholdModalId = $event.id">
|
||||
@editThresholdEntries="thresholdModal = true; thresholdModalId = $event.id"
|
||||
@addToCart="addShoppingCartModal = true; addShoppingCartModalId = $event.id"
|
||||
>
|
||||
|
||||
<template v-slot:cheapestsellprice="{ row }">
|
||||
<template v-for="price in JSON.parse(row.cheapestSellPrice)">
|
||||
@@ -268,6 +271,38 @@ Vue.component('warehouse-article', {
|
||||
|
||||
</tt-table-crud>
|
||||
|
||||
<tt-expandable-shopping-cart :cart-items="shoppingCart" @submitOrder="prepareOrder"/>
|
||||
<tt-modal :show.sync="addShoppingCartModal" title="Artikel zur Bestellung hinzufügen" :delete="false" @submit="addToShoppingCart"
|
||||
@close="addShoppingCartModal = false">
|
||||
<tt-input v-model="addShoppingCartModalCount" placeholder="Menge" type="number" sm></tt-input>
|
||||
</tt-modal>
|
||||
|
||||
<tt-modal :show.sync="confirmOrderModal" title="Bestellung bestätigen" :delete="false"
|
||||
save-text="Bestätigen" @submit="createOrder"
|
||||
@close="confirmOrderModal = false; confirmOrderModalData = null">
|
||||
<span>
|
||||
Es werden Bestellungen an folgende Lieferanten gesendet:
|
||||
</span>
|
||||
<div v-for="(order, index) in confirmOrderModalData" :key="index">
|
||||
<h4>{{order.distributor[0].name}} - {{order.orderAmount}} €
|
||||
</h4>
|
||||
<table class="table table-bordered">
|
||||
<tr>
|
||||
<th>Artikel</th>
|
||||
<th>Menge</th>
|
||||
<th>Preis</th>
|
||||
<th>Summe</th>
|
||||
</tr>
|
||||
<tr v-for="(item, index) in order.orders" :key="index">
|
||||
<td>{{item.title}}</td>
|
||||
<td>{{item.amount}}</td>
|
||||
<td>{{item.purchasePrice}} €</td>
|
||||
<td>{{item.sum.toFixed(2)}} €</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</tt-modal>
|
||||
|
||||
<warehouse-distributor-modal :show.sync="distributorModal" :id="distributorModalId" @doUpdate="refreshTable"/>
|
||||
<warehouse-threshold-modal :show.sync="thresholdModal" :id="thresholdModalId" @doUpdate="refreshTable"/>
|
||||
<warehouse-article-price-modal :show.sync="priceModal" :id="priceModalId" @doUpdate="refreshTable"/>
|
||||
@@ -283,11 +318,48 @@ Vue.component('warehouse-article', {
|
||||
thresholdModal: false,
|
||||
thresholdModalId: null,
|
||||
priceModal: false,
|
||||
priceModalId: null
|
||||
priceModalId: null,
|
||||
shoppingCart: [],
|
||||
addShoppingCartModal: false,
|
||||
addShoppingCartModalId: null,
|
||||
addShoppingCartModalCount: '',
|
||||
confirmOrderModal: false,
|
||||
confirmOrderModalData: null,
|
||||
|
||||
}
|
||||
}, methods: {
|
||||
refreshTable() {
|
||||
this.$refs.table.$refs.table.refreshTable();
|
||||
}, async addToShoppingCart() {
|
||||
if (this.addShoppingCartModalCount < 1) { // Check if amount is set
|
||||
window.notify('error', 'Bitte geben Sie eine Menge ein.');
|
||||
return;
|
||||
}
|
||||
if (this.shoppingCart.some(item => item.itemId === this.addShoppingCartModalId)) { // Check if same article is already in cart
|
||||
window.notify('error', 'Artikel bereits im Warenkorb.');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await axios.get(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticle/getById?id=${this.addShoppingCartModalId}`);
|
||||
this.shoppingCart.push({amount: parseInt(this.addShoppingCartModalCount), itemId: this.addShoppingCartModalId, title: response.data.title});
|
||||
this.addShoppingCartModal = false;
|
||||
this.addShoppingCartModalId = null;
|
||||
this.addShoppingCartModalCount = '';
|
||||
window.notify('success', 'Artikel erfolgreich hinzugefügt.');
|
||||
}, async prepareOrder() {
|
||||
const response = await axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticle/prepareOrder`, this.shoppingCart);
|
||||
this.confirmOrderModal = true;
|
||||
this.confirmOrderModalData = response.data;
|
||||
},
|
||||
async createOrder() {
|
||||
const response = await axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseOrder/createOrder`, this.confirmOrderModalData);
|
||||
this.confirmOrderModal = false;
|
||||
this.confirmOrderModalData = null;
|
||||
this.shoppingCart = [];
|
||||
window.notify(response.data.success ? 'success' : 'error', response.data.message);
|
||||
setTimeout(() => {
|
||||
window.location.href = `${window['TT_CONFIG']['BASE_PATH']}/WarehouseOrder`;
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
Vue.component('warehouse-article-price-type', {
|
||||
//language=Vue
|
||||
template: `
|
||||
<tt-card>
|
||||
<warehouse-administration-switch/>
|
||||
<tt-table-crud @openHistory="historyModal = true; historyModalId = $event.id"/>
|
||||
<warehouse-history-modal :show.sync="historyModal" :id="historyModalId"/>
|
||||
</tt-card>
|
||||
`, data() {
|
||||
return {
|
||||
window: window, historyModal: false, historyModalId: null,
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -2,6 +2,7 @@ Vue.component('warehouse-distributor', {
|
||||
//language=Vue
|
||||
template: `
|
||||
<tt-card>
|
||||
<warehouse-administration-switch/>
|
||||
<tt-table-crud @openHistory="historyModal = true; historyModalId = $event.id"/>
|
||||
<warehouse-history-modal :show.sync="historyModal" :id="historyModalId"/>
|
||||
</tt-card>
|
||||
|
||||
@@ -1,47 +1,3 @@
|
||||
|
||||
|
||||
Vue.component('tt-expandable-shopping-cart', {
|
||||
props: {
|
||||
cartItems: Array,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isExpanded: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
},
|
||||
template: `
|
||||
<div class="tt-expandable-shopping-cart" :class="{ expanded: isExpanded }">
|
||||
<button class="toggle-button" @click="isExpanded = !isExpanded">
|
||||
<i class="fas fa-shopping-cart text-primary"></i>
|
||||
<span v-if="cartItems.length > 0 && isExpanded" class="btn btn-primary" @click.prevent="$emit('submitOrder')">Bestellen</span>
|
||||
<span class="cart-count" v-if="cartItems.length > 0">{{ cartItems.length }}</span>
|
||||
<!-- add arrow down icon when cart is expanded additionally with v-if -->
|
||||
<i v-if="isExpanded" class="fas fa-arrow-down text-danger" style="font-size:21px;grid-column: 4;"></i>
|
||||
</button>
|
||||
<div class="cart-content" v-if="isExpanded">
|
||||
<div v-if="cartItems.length > 0">
|
||||
<h3>Einkaufswagen</h3>
|
||||
<ul class="list-group">
|
||||
<template v-for="item in cartItems">
|
||||
<li class="list-group-item" style="display:grid;grid-template-columns: 1fr auto;gap: 10px;">
|
||||
{{ item.title }}
|
||||
<div style="display:grid;grid-template-columns: 1fr 1fr;gap: 10px;">
|
||||
<span class="badge badge-primary badge-pill" style="height: 16px">{{ item.amount }}</span>
|
||||
<i style="cursor: pointer" class="fas fa-trash-alt text-danger" @click="cartItems.splice(cartItems.indexOf(item), 1)"></i>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
<p v-else>Der Einkaufswagen ist leer.</p>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
});
|
||||
|
||||
|
||||
Vue.component('warehouse-e-shop', {
|
||||
//language=Vue
|
||||
template: `
|
||||
|
||||
@@ -82,7 +82,6 @@ Vue.component('warehouse-e-shop-order', {
|
||||
}
|
||||
}, async mounted() {
|
||||
const response = await axios.get(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseEShopOrder/getAllItemsPerOrder`);
|
||||
console.log(response.data);
|
||||
this.articleItems = response.data;
|
||||
}, methods: {
|
||||
async sendSingleOrderEmail() {
|
||||
|
||||
@@ -43,3 +43,97 @@ Vue.component('warehouse-history-modal', {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Vue.component('tt-expandable-shopping-cart', {
|
||||
props: {
|
||||
cartItems: Array,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isExpanded: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
},
|
||||
template: `
|
||||
<div class="tt-expandable-shopping-cart" :class="{ expanded: isExpanded }">
|
||||
<button class="toggle-button" @click="isExpanded = !isExpanded">
|
||||
<i class="fas fa-shopping-cart text-primary"></i>
|
||||
<span v-if="cartItems.length > 0 && isExpanded" class="btn btn-primary" @click.prevent="$emit('submitOrder')">Bestellen</span>
|
||||
<span class="cart-count" v-if="cartItems.length > 0">{{ cartItems.length }}</span>
|
||||
<!-- add arrow down icon when cart is expanded additionally with v-if -->
|
||||
<i v-if="isExpanded" class="fas fa-arrow-down text-danger" style="font-size:21px;grid-column: 4;"></i>
|
||||
</button>
|
||||
<div class="cart-content" v-if="isExpanded">
|
||||
<div v-if="cartItems.length > 0">
|
||||
<h3>Einkaufswagen</h3>
|
||||
<ul class="list-group">
|
||||
<template v-for="item in cartItems">
|
||||
<li class="list-group-item" style="display:grid;grid-template-columns: 1fr auto;gap: 10px;">
|
||||
{{ item.title }}
|
||||
<div style="display:grid;grid-template-columns: 1fr 1fr;gap: 10px;">
|
||||
<span class="badge badge-primary badge-pill" style="height: 16px">{{ item.amount }}</span>
|
||||
<i style="cursor: pointer" class="fas fa-trash-alt text-danger" @click="cartItems.splice(cartItems.indexOf(item), 1)"></i>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
<p v-else>Der Einkaufswagen ist leer.</p>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
});
|
||||
|
||||
|
||||
//TODO: put this in its own file
|
||||
//TODO: also for tt-crud or vuehelper create a check for utility folder and include all js files in there to allow adding custom components that are not part of the core
|
||||
//TODO: also add a component for a switch like this as we will need it more often either with value or doing redirect on click
|
||||
|
||||
Vue.component('warehouse-administration-switch', {
|
||||
//language=Vue
|
||||
template: `
|
||||
<div class="device-view-switch" style="margin-bottom: 10px">
|
||||
<div v-if="!isOverflowing" class="button-group" style="display:grid; grid-template-columns: repeat(5, 1fr); gap: 10px; justify-content: center; align-items: center; text-align: center; width: 100%;">
|
||||
<button @click="window.location.href = window.TT_CONFIG['BASE_PATH'] + '/WarehouseDistributor';" :class="{ 'active': window.location.href.includes('WarehouseDistributor') }" class="btn btn-primary">Lieferanten</button>
|
||||
<button @click="window.location.href = window.TT_CONFIG['BASE_PATH'] + '/WarehouseLocation';" :class="{ 'active': window.location.href.includes('WarehouseLocation') }" class="btn btn-primary">Lagerorte</button>
|
||||
<button @click="window.location.href = window.TT_CONFIG['BASE_PATH'] + '/WarehouseArticlePriceType';" :class="{ 'active': window.location.href.includes('WarehouseArticlePriceType') }" class="btn btn-primary">Preistypen</button>
|
||||
<button @click="window.location.href = window.TT_CONFIG['BASE_PATH'] + '/WarehouseShippingNoteTextElement';" :class="{ 'active': window.location.href.includes('Device') }" class="btn btn-primary">LS Texte</button>
|
||||
<button @click="window.location.href = window.TT_CONFIG['BASE_PATH'] + '/WarehouseAdministration';" :class="{ 'active': window.location.href.includes('Device') }" class="btn btn-primary">Admin-Tools</button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="dropdown">
|
||||
<button @click="showDropdown = !showDropdown"
|
||||
class="btn btn-primary dropdown-toggle">Ansicht</button>
|
||||
<div v-show="showDropdown" class="dropdown-menu show">
|
||||
<a href="#" @click="window.location.href = window.TT_CONFIG['BASE_PATH'] + '/WarehouseDistributor'; showDropdown = false" class="dropdown-item">Lieferanten</a>
|
||||
<a href="#" @click="window.location.href = window.TT_CONFIG['BASE_PATH'] + '/WarehouseArticlePriceType'; showDropdown = false" class="dropdown-item">Lagerorte</a>
|
||||
<a href="#" @click="window.location.href = window.TT_CONFIG['BASE_PATH'] + '/WarehouseLocation'; showDropdown = false" class="dropdown-item">Preistypen</a>
|
||||
<a href="#" @click="window.location.href = window.TT_CONFIG['BASE_PATH'] + '/WarehouseShippingNoteTextElement'; showDropdown = false" class="dropdown-item">LS Texte</a>
|
||||
<a href="#" @click="window.location.href = window.TT_CONFIG['BASE_PATH'] + '/WarehouseAdministration'; showDropdown = false" class="dropdown-item">Admin-Tools</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
props: ['value'],
|
||||
data() {
|
||||
return {
|
||||
isOverflowing: false,
|
||||
showDropdown: false,
|
||||
window: window,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.checkOverflow();
|
||||
window.addEventListener('resize', this.checkOverflow);
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.checkOverflow);
|
||||
},
|
||||
methods: {
|
||||
checkOverflow() {
|
||||
this.isOverflowing = window.innerWidth < 650
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -2,7 +2,13 @@ Vue.component('warehouse-item', {
|
||||
//language=Vue
|
||||
template: `
|
||||
<tt-card>
|
||||
<tt-table-crud @openHistory="historyModal = true; historyModalId = $event.id"/>
|
||||
<tt-table-crud @openHistory="historyModal = true; historyModalId = $event.id">
|
||||
<template v-slot:rack="{ row }">
|
||||
<span v-if="row.rack && row.shelf">{{ row.rack }} | {{ row.shelf }}</span>
|
||||
<span v-else-if="row.rack">{{ row.rack }}</span>
|
||||
<span v-else> - </span>
|
||||
</template>
|
||||
</tt-table-crud>
|
||||
<warehouse-history-modal :show.sync="historyModal" :id="historyModalId"/>
|
||||
</tt-card>
|
||||
`, data() {
|
||||
|
||||
@@ -2,6 +2,7 @@ Vue.component('warehouse-location', {
|
||||
//language=Vue
|
||||
template: `
|
||||
<tt-card>
|
||||
<warehouse-administration-switch/>
|
||||
<tt-table-crud @openHistory="historyModal = true; historyModalId = $event.id"/>
|
||||
<warehouse-history-modal :show.sync="historyModal" :id="historyModalId"/>
|
||||
</tt-card>
|
||||
|
||||
70
public/js/pages/WarehouseOrder/WarehouseOrder.js
Normal file
70
public/js/pages/WarehouseOrder/WarehouseOrder.js
Normal file
@@ -0,0 +1,70 @@
|
||||
// noinspection JSUnusedLocalSymbols
|
||||
Vue.component('warehouse-order', {
|
||||
//language=Vue
|
||||
template: `
|
||||
<tt-card>
|
||||
<tt-table-crud @openHistory="historyModal = true; historyModalId = $event.id"
|
||||
ref="table">
|
||||
|
||||
<template v-slot:create="{ row }">
|
||||
{{ window.moment(row.create * 1000).format('DD.MM.YYYY HH:mm:ss') }}
|
||||
</template>
|
||||
|
||||
<template v-slot:sum="{ row }">
|
||||
<div style="text-align: right">{{ row.sum.toFixed(2) }} €</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:expandedRow="{ row }">
|
||||
<div class="lazy-loading" :data-row-id="row.id">
|
||||
<tt-loader v-if="orderLazyLoad[row.id] === true"/>
|
||||
<div v-else>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item" v-for="item in orderLazyLoad[row.id]">
|
||||
{{ item.quantity }}x {{ item.articleName }} - {{ item.price.toFixed(2) }} €
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</tt-table-crud>
|
||||
<warehouse-history-modal :show.sync="historyModal" :id="historyModalId"/>
|
||||
</tt-card>
|
||||
`, data() {
|
||||
return {
|
||||
window: window, historyModal: false, historyModalId: null, observer: null, orderLazyLoad: {},
|
||||
}
|
||||
}, mounted() {
|
||||
this.observer = new MutationObserver((mutations) => {
|
||||
const lazyLoadingElements = document.querySelectorAll('.lazy-loading');
|
||||
console.log(lazyLoadingElements);
|
||||
|
||||
// check row id and check if it is already defined in orderLazyLoad else alert('loading')
|
||||
// if it is defined do nothing
|
||||
|
||||
for (const element of lazyLoadingElements) {
|
||||
if (element.dataset.rowId in this.orderLazyLoad) {
|
||||
continue;
|
||||
}
|
||||
this.loadOrder(element.dataset.rowId);
|
||||
}
|
||||
|
||||
})
|
||||
this.observer.observe(document.querySelector('.tt-table-container'), {childList: true, subtree: true,});
|
||||
}, methods: {
|
||||
async loadOrder(rowId) {
|
||||
this.orderLazyLoad[rowId] = true;
|
||||
// use BASE_PATH . /WarehouseOrder/getOrderItems?id= + rowId
|
||||
const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/getOrderItems?id=${rowId}`);
|
||||
console.log(response.data);
|
||||
this.orderLazyLoad[rowId] = response.data;
|
||||
|
||||
// force re-render of the table
|
||||
this.$refs.table.$forceUpdate();
|
||||
|
||||
|
||||
}
|
||||
}, beforeDestroy() {
|
||||
this.observer.disconnect();
|
||||
}
|
||||
})
|
||||
321
public/js/pages/WarehouseShippingNote/WarehouseShippingNote.js
Normal file
321
public/js/pages/WarehouseShippingNote/WarehouseShippingNote.js
Normal file
@@ -0,0 +1,321 @@
|
||||
const defaultCrudModalData = {
|
||||
billingAddressId: '',
|
||||
deliveryAddressName: '',
|
||||
deliveryAddressLine: '',
|
||||
deliveryAddressPLZ: '',
|
||||
deliveryAddressCity: '',
|
||||
status: 'new',
|
||||
positions: [],
|
||||
textElements: {}
|
||||
}
|
||||
|
||||
window.crudModalStatusOptions =
|
||||
[{value: 'new', text: 'Neu'}, {value: 'accepted', text: 'Akzeptiert'}, {value: 'invoiced', text: 'In Rechnung gestellt', disabled: true}]
|
||||
|
||||
// create a additional vue component for showing positions in the table with lazy loading for article titles and description
|
||||
Vue.component('warehouse-shipping-note-positions', {
|
||||
//language=Vue
|
||||
props: {
|
||||
positions: Array
|
||||
}, data() {
|
||||
return {
|
||||
articleData: {}, loading: false
|
||||
}
|
||||
}, template: `
|
||||
<div>
|
||||
<div v-if="loading" class="text-center">
|
||||
<i class="fa fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
|
||||
|
||||
<ul v-if="!loading">
|
||||
<li v-for="position in positions">
|
||||
<span>{{ position.amount }}x {{ articleData[position.article]?.text }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`, async mounted() {
|
||||
this.loading = true;
|
||||
for (const position of this.positions) {
|
||||
const response = await axios.get(window.TT_CONFIG["BASE_PATH"] + '/WarehouseArticle/autoComplete?searchedID=' + position.article);
|
||||
this.$set(this.articleData, position.article, response.data[0]);
|
||||
}
|
||||
this.loading = false;
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// noinspection JSUnusedLocalSymbols
|
||||
Vue.component('warehouse-shipping-note', {
|
||||
//language=Vue
|
||||
template: `
|
||||
<tt-card>
|
||||
<tt-modal :show.sync="crudModal" :id="crudModalId"
|
||||
:delete="false"
|
||||
@submit="createOrUpdate()"
|
||||
:title="crudModalId === 'create' ? 'Lieferschein erstellen' : 'Lieferschein bearbeiten'">
|
||||
<tt-autocomplete v-model="crudModalData.billingAddressId"
|
||||
:api-url="window.TT_CONFIG['BASE_PATH'] + '/Address/Api?do=findAddress'"
|
||||
label="Rechnungsadresse" sm row/>
|
||||
|
||||
<tt-select v-model="crudModalSelectDeliveryAddressMode" :options="crudModalSelectDeliveryAddressModeItems" label="Lieferadresse Art" sm
|
||||
row/>
|
||||
|
||||
<template v-if="crudModalSelectDeliveryAddressMode === 'existing'">
|
||||
<tt-select v-model="crudModalDataDeliveryAddressSelected" :options="crudModalDataDeliveryAddressOptions" label="Lieferadresse" sm row/>
|
||||
</template>
|
||||
<template v-else-if="crudModalSelectDeliveryAddressMode === 'new'">
|
||||
<tt-input v-model="crudModalData.deliveryAddressName" label="Lieferadresse Name" sm row/>
|
||||
<tt-input v-model="crudModalData.deliveryAddressLine" label="Lieferadresse" sm row/>
|
||||
<tt-input v-model="crudModalData.deliveryAddressPLZ" label="Lieferadresse PLZ" sm row/>
|
||||
<tt-input v-model="crudModalData.deliveryAddressCity" label="Lieferadresse Ort" sm row/>
|
||||
</template>
|
||||
<tt-select v-if="crudModalVerifyMode === true" v-model="crudModalData.status" :options="window.crudModalStatusOptions" label="Status" sm
|
||||
row/>
|
||||
|
||||
<!-- show a checkbox for each textElement and if selected set it to selected [{"id":1,"title":"Zahlhinweis","content":"Bezahlung in 14 tagen","create":1728456765,"createBy":145}]-->
|
||||
<template>
|
||||
<hr>
|
||||
<h4 class="text-center">Texte</h4>
|
||||
<div v-for="textElement in textElements" style="display: inline-block; margin-right: 10px;">
|
||||
<input type="checkbox" v-model="crudModalData.textElements[textElement.id]" :id="'textElement' + textElement.id">
|
||||
<label :for="'textElement' + textElement.id">{{ textElement.title }}</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<hr>
|
||||
<h4 class="text-center">Positionen</h4>
|
||||
|
||||
|
||||
<template v-if="crudModalData.billingAddressId">
|
||||
<div style="display: flex; justify-content: space-around;padding: 10px;">
|
||||
<tt-autocomplete v-model="crudModalAddPositionArticle" :api-url="window.TT_CONFIG['BASE_PATH'] + '/WarehouseArticle/autoComplete'"
|
||||
placeholder="Artikel" sm row/>
|
||||
<tt-input v-model="crudModalAddPositionAmount" placeholder="Menge" sm row/>
|
||||
<tt-input v-model="crudModalAddPositionPrice" placeholder="Preis" type="number" sm row/>
|
||||
<button style="max-height: 29px" class="btn btn-sm btn-primary" @click="addPosition">Hinzufügen</button>
|
||||
</div>
|
||||
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Position</th>
|
||||
<th>Artikel</th>
|
||||
<th>Menge</th>
|
||||
<th>Preis</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(position, index) in crudModalData.positions">
|
||||
<td>{{ index + 1 }}</td>
|
||||
<td>{{ articleNames[position.article] }}</td>
|
||||
<td>{{ position.amount }}</td>
|
||||
<td>{{ (position.price?.toFixed(2)) }} €</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-danger" @click="crudModalData.positions.splice(index, 1)">Löschen</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</template>
|
||||
<template v-else>
|
||||
<h5 class="text-center">Rechnungsadresse auswählen um Positionen hinzuzufügen</h5>
|
||||
</template>
|
||||
|
||||
|
||||
</tt-modal>
|
||||
|
||||
<warehouse-history-modal :show.sync="historyModal" :id="historyModalId"/>
|
||||
|
||||
<button @click="openCrudModal('create')" class="btn btn-primary">Lieferschein erstellen</button>
|
||||
<button @click="openVerifyModal" class="btn btn-primary">Lieferscheine Freigeben</button>
|
||||
|
||||
<tt-table-crud emit-edit
|
||||
@openHistory="historyModal = true; historyModalId = $event.id"
|
||||
@print="window.open(window.TT_CONFIG['BASE_PATH'] + '/WarehouseShippingNote/createPDF?id=' + $event.id)"
|
||||
@printWithPrice="window.open(window.TT_CONFIG['BASE_PATH'] + '/WarehouseShippingNote/createPDF?id=' + $event.id + '&price=true')"
|
||||
@edit="openCrudModal($event)"
|
||||
ref="table">
|
||||
|
||||
<template v-slot:expandedRow="{ row }">
|
||||
<warehouse-shipping-note-positions :positions="JSON.parse(row.positions)"/>
|
||||
</template>
|
||||
|
||||
</tt-table-crud>
|
||||
</tt-card>
|
||||
`, data() {
|
||||
return {
|
||||
window: window,
|
||||
historyModal: false,
|
||||
historyModalId: null,
|
||||
crudModal: false,
|
||||
crudModalSelectDeliveryAddressModeItems: [{text: 'Wie Rechnungsadresse', value: 'billing'},
|
||||
{text: 'Bestehende Lieferadresse', value: 'existing'},
|
||||
{text: 'Neue Lieferadresse', value: 'new'}],
|
||||
crudModalSelectDeliveryAddressMode: 'billing',
|
||||
crudModalDataDeliveryAddressOptions: [],
|
||||
crudModalDataDeliveryAddressSelected: '',
|
||||
crudModalVerifyMode: false,
|
||||
crudModalId: null,
|
||||
crudModalData: defaultCrudModalData,
|
||||
crudModalAddPositionArticle: '',
|
||||
crudModalAddPositionAmount: '',
|
||||
crudModalAddPositionPrice: '',
|
||||
articleNames: {},
|
||||
textElements: [],
|
||||
}
|
||||
}, async mounted() {
|
||||
const response = await axios.get(window.TT_CONFIG["BASE_PATH"] + '/WarehouseShippingNote/getAllTextElements');
|
||||
this.textElements = response.data;
|
||||
},
|
||||
|
||||
methods: {
|
||||
async openVerifyModal() {
|
||||
const unverifiedShippingNotes = await axios.post(window.TT_CONFIG['BASE_PATH'] + '/WarehouseShippingNote/get', {
|
||||
"pagination": {"page": 1, "per_page": 1}, "filters": {
|
||||
"status": "new"
|
||||
}, "order": {"key": null, "order": "asc"}
|
||||
});
|
||||
|
||||
if (unverifiedShippingNotes.data.rows.length === 0) {
|
||||
this.window.notify('warning', 'Keine Lieferscheine zum Freigeben gefunden');
|
||||
return;
|
||||
}
|
||||
|
||||
await this.openCrudModal(unverifiedShippingNotes.data.rows[0]);
|
||||
this.crudModalVerifyMode = true;
|
||||
|
||||
}, resetCrudModalData() {
|
||||
this.crudModalData.billingAddressId = '';
|
||||
this.crudModalData.deliveryAddressName = '';
|
||||
this.crudModalData.deliveryAddressLine = '';
|
||||
this.crudModalData.deliveryAddressPLZ = '';
|
||||
this.crudModalData.deliveryAddressCity = '';
|
||||
this.crudModalAddPositionArticle = '';
|
||||
this.crudModalAddPositionAmount = '';
|
||||
this.crudModalAddPositionPrice = '';
|
||||
this.crudModalSelectDeliveryAddressMode = 'billing';
|
||||
this.crudModalDataDeliveryAddressSelected = '';
|
||||
this.crudModal = false;
|
||||
}, async openCrudModal(data) {
|
||||
this.resetCrudModalData();
|
||||
this.crudModalVerifyMode = false;
|
||||
if (data === 'create') {
|
||||
this.crudModalId = 'create'
|
||||
this.crudModalData = defaultCrudModalData
|
||||
this.crudModal = true
|
||||
} else {
|
||||
const disconnectedData = JSON.parse(JSON.stringify(data));
|
||||
|
||||
if (disconnectedData.status !== 'new') {
|
||||
this.window.notify('warning', 'Lieferschein kann nicht bearbeitet werden, da er bereits genehmigt wurde');
|
||||
return;
|
||||
}
|
||||
|
||||
disconnectedData.textElements = JSON.parse(disconnectedData.textElements);
|
||||
disconnectedData.positions = JSON.parse(disconnectedData.positions);
|
||||
for (const position of disconnectedData.positions) {
|
||||
await this.fetchArticleNames(position.article);
|
||||
}
|
||||
this.crudModalId = 'update'
|
||||
this.crudModalData = disconnectedData
|
||||
this.crudModal = true
|
||||
}
|
||||
}, async addPosition() {
|
||||
const missingFields = [];
|
||||
|
||||
// ---------- Check Required Fields ----------
|
||||
|
||||
if (!this.crudModalAddPositionArticle) missingFields.push('Artikel');
|
||||
if (!this.crudModalAddPositionAmount) missingFields.push('Menge');
|
||||
if (!this.crudModalAddPositionPrice) missingFields.push('Preis-Überschreibung');
|
||||
if (missingFields.length > 0) {
|
||||
window.notify('error', 'Bitte füllen Sie die folgenden Felder aus: ' + missingFields.join(', '));
|
||||
return;
|
||||
}
|
||||
|
||||
// ---------- Check if same article is already in positions ----------
|
||||
|
||||
const articleAlreadyInPositions = this.crudModalData.positions.find(position => position.article === this.crudModalAddPositionArticle);
|
||||
if (articleAlreadyInPositions) {
|
||||
window.notify('error', 'Artikel ist bereits in den Positionen enthalten');
|
||||
return;
|
||||
}
|
||||
|
||||
await this.fetchArticleNames(this.crudModalAddPositionArticle);
|
||||
|
||||
this.crudModalData.positions.push({
|
||||
article: this.crudModalAddPositionArticle, amount: this.crudModalAddPositionAmount, price: parseFloat(this.crudModalAddPositionPrice)
|
||||
});
|
||||
|
||||
//TODO: post to server
|
||||
}, async fetchArticleNames(articleId) {
|
||||
const response = await axios.get(window.TT_CONFIG["BASE_PATH"] + '/WarehouseArticle/autoComplete?searchedID=' + articleId);
|
||||
this.$set(this.articleNames, articleId, response.data[0].text);
|
||||
}, async createOrUpdate() {
|
||||
const response = await axios.post(this.crudModalId === 'create' ? window['TT_CONFIG']['CREATE_URL'] : window['TT_CONFIG']['UPDATE_URL'],
|
||||
this.crudModalData);
|
||||
if (response.data.success) {
|
||||
this.$refs.table.$refs.table.refreshTable();
|
||||
this.resetCrudModalData();
|
||||
this.window.notify('success', response.data.message || 'Erfolgreich gespeichert');
|
||||
} else {
|
||||
this.window.notify('error',
|
||||
response.data.errors ? Object.values(response.data.errors).join('<br>') : response.data.message || 'Ein Fehler ist aufgetreten');
|
||||
}
|
||||
}, async fetchDeliveryAddresses() {
|
||||
if (!this.crudModalData.billingAddressId || this.crudModalSelectDeliveryAddressMode !== 'existing') return;
|
||||
|
||||
if (this.crudModalSelectDeliveryAddressMode === 'billing') {
|
||||
const response = await axios.get(window.TT_CONFIG["BASE_PATH"] + '/Address/api?do=getAddress&id=' + this.crudModalData.billingAddressId);
|
||||
if (response.data.status !== 'OK' || !response.data.result.address) {
|
||||
window.notify('error', 'Rechnungsadresse konnte nicht gefunden werden');
|
||||
return;
|
||||
}
|
||||
|
||||
this.crudModalData.deliveryAddressName =
|
||||
response.data.result.address.company || response.data.result.address.firstname + ' ' + response.data.result.address.lastname;
|
||||
this.crudModalData.deliveryAddressLine = response.data.result.address.street;
|
||||
this.crudModalData.deliveryAddressPLZ = response.data.result.address.zip;
|
||||
this.crudModalData.deliveryAddressCity = response.data.result.address.city;
|
||||
}
|
||||
if (!this.crudModalData.billingAddressId || this.crudModalSelectDeliveryAddressMode !== 'existing') return;
|
||||
|
||||
const response = await axios.get(window.TT_CONFIG["BASE_PATH"] +
|
||||
'/WarehouseShippingNote/getDeliveryAddresses?billingAddressId=' +
|
||||
this.crudModalData.billingAddressId);
|
||||
|
||||
this.crudModalDataDeliveryAddressOptions = response.data.map(address => {
|
||||
address.value = address.id;
|
||||
address.text = `${address.deliveryAddressName} - ${address.deliveryAddressLine}, ${address.deliveryAddressPLZ} ${address.deliveryAddressCity}`;
|
||||
return address;
|
||||
});
|
||||
}
|
||||
}, watch: {
|
||||
crudModalAddPositionArticle: async function (newValue) {
|
||||
if (!newValue) return;
|
||||
|
||||
const url = `${window.TT_CONFIG["BASE_PATH"]}/WarehouseShippingNote/getArticleAddressPrice?articleId=${newValue}&addressId=${this.crudModalData.billingAddressId}`;
|
||||
const response = await axios.get(url);
|
||||
this.crudModalAddPositionPrice = response.data.price;
|
||||
},
|
||||
crudModalData: {handler: 'fetchDeliveryAddresses', deep: true},
|
||||
crudModalSelectDeliveryAddressMode: {handler: 'fetchDeliveryAddresses', deep: true},
|
||||
crudModalDataDeliveryAddressSelected: function (newValue) {
|
||||
if (!newValue) return;
|
||||
|
||||
const selectedAddress = this.crudModalDataDeliveryAddressOptions.find(address => address.id === parseInt(newValue));
|
||||
if (!selectedAddress) {
|
||||
window.notify('error', 'Lieferadresse konnte nicht gefunden werden');
|
||||
return;
|
||||
}
|
||||
|
||||
this.crudModalData.deliveryAddressName = selectedAddress.deliveryAddressName;
|
||||
this.crudModalData.deliveryAddressLine = selectedAddress.deliveryAddressLine;
|
||||
this.crudModalData.deliveryAddressPLZ = selectedAddress.deliveryAddressPLZ;
|
||||
this.crudModalData.deliveryAddressCity = selectedAddress.deliveryAddressCity;
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,6 +1,12 @@
|
||||
//TODO: if autocomplete is focus and the input has not a single character still show "Bitte mindestens 3 Zeichen eingeben"
|
||||
//TODO: fix the fetchSuggestions function to not show a loading spinner when the input gets cleared
|
||||
//TODO: fix so we can use arrow keys to navigate the suggestions
|
||||
|
||||
Vue.component('tt-autocomplete', {
|
||||
template: `
|
||||
<div class="form-group" :class="{'row': row}">
|
||||
<div class="form-group" :class="{'row': row}"
|
||||
:data-api-url="apiUrl"
|
||||
>
|
||||
<slot name="prepend"></slot>
|
||||
<label :class="{'col-form-label': row, 'col-sm-4': row, 'col-form-label-sm': sm && row}"
|
||||
v-if="label" :for="label">{{ label }}</label>
|
||||
@@ -8,7 +14,7 @@ Vue.component('tt-autocomplete', {
|
||||
<input
|
||||
type="text"
|
||||
:id="label"
|
||||
class="form-control"
|
||||
class="form-control tt-autocomplete"
|
||||
:class="{'form-control-sm': sm}"
|
||||
v-model="displayValue"
|
||||
:placeholder="placeholder"
|
||||
@@ -18,7 +24,8 @@ Vue.component('tt-autocomplete', {
|
||||
:style="{'padding-right': $slots.append ? '30px' : '0'}"
|
||||
/>
|
||||
<slot name="append"></slot>
|
||||
<button v-show="displayValue.length > 0" @click="displayValue = ''" type="button" class="btn btn-link position-absolute" style="right: -5px; top: 50%; transform: translateY(-50%);">
|
||||
<button v-show="displayValue.length > 0" @click="displayValue = ''; $emit('input', '');" tabindex="-1" type="button" class="btn btn-link position-absolute"
|
||||
style="right: -5px; top: 50%; transform: translateY(-50%);">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
|
||||
@@ -48,7 +55,7 @@ Vue.component('tt-autocomplete', {
|
||||
Mehr Suchergebnisse vorhanden. Bitte genauer eingeben
|
||||
</li>
|
||||
</template>
|
||||
<li v-show="displayingItems.length === 0 && isLoading === false && displayValue.length > 3"
|
||||
<li v-show="displayingItems.length === 0 && isLoading === false && displayValue.length >= 3"
|
||||
class="dropdown-item disabled">
|
||||
Keine Suchergebnisse vorhanden.
|
||||
</li>
|
||||
@@ -58,19 +65,17 @@ Vue.component('tt-autocomplete', {
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
// TODO: Implement giving the option without the need of an API || need to use computed property to filter the items
|
||||
`, // TODO: Implement giving the option without the need of an API || need to use computed property to filter the items
|
||||
// TODO: Fix the weirdness with timeout and selecting the suggestion
|
||||
props: {
|
||||
value: { type: [String, Number] },
|
||||
label: { type: String, required: false },
|
||||
value: {type: [String, Number]},
|
||||
label: {type: String, required: false},
|
||||
apiUrl: String,
|
||||
placeholder: { type: String, default: '' },
|
||||
items: { type: Array, default: () => [] },
|
||||
sm: { type: Boolean, default: true },
|
||||
row: { type: Boolean, default: false },
|
||||
},
|
||||
async mounted() {
|
||||
placeholder: {type: String, default: ''},
|
||||
items: {type: Array, default: () => []},
|
||||
sm: {type: Boolean, default: true},
|
||||
row: {type: Boolean, default: false},
|
||||
}, async mounted() {
|
||||
if (this.value && this.apiUrl) {
|
||||
const response = await axios.get(`${this.apiUrl}&autocomplete=1&searchedID=${this.value}`);
|
||||
this.displayValue = response.data[0].text;
|
||||
@@ -82,45 +87,29 @@ Vue.component('tt-autocomplete', {
|
||||
this.displayValue = '';
|
||||
}
|
||||
|
||||
},
|
||||
data() {
|
||||
}, data() {
|
||||
return {
|
||||
displayingItems: [],
|
||||
displayValue: '',
|
||||
isLoading: false,
|
||||
showSuggestions: false,
|
||||
cursor: -1,
|
||||
fetchSuggestionsDebounceTimer: null,
|
||||
displayingItems: [], displayValue: '', isLoading: false, showSuggestions: false, cursor: -1, fetchSuggestionsDebounceTimer: null,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
}, watch: {
|
||||
value(newValue) {
|
||||
const selectedItem = this.displayingItems.find(item => item.value === newValue);
|
||||
this.displayValue = selectedItem ? selectedItem.text : '';
|
||||
},
|
||||
apiUrl() {
|
||||
}, apiUrl() {
|
||||
this.fetchSuggestions();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
}, methods: {
|
||||
onInput(event) {
|
||||
console.log('input', event.target.value);
|
||||
this.displayValue = event.target.value;
|
||||
console.log('displayValue', this.displayValue);
|
||||
this.$emit('input', '');
|
||||
console.log('value', this.value);
|
||||
console.log('displayValue', this.displayValue);
|
||||
this.fetchSuggestions();
|
||||
},
|
||||
onFocus() {
|
||||
}, onFocus() {
|
||||
this.showSuggestions = true;
|
||||
},
|
||||
onBlur() {
|
||||
}, onBlur() {
|
||||
setTimeout(() => {
|
||||
this.showSuggestions = false;
|
||||
}, 200);
|
||||
},
|
||||
fetchSuggestions() {
|
||||
}, fetchSuggestions() {
|
||||
if (this.displayValue.length < 3) {
|
||||
this.$set(this, 'displayingItems', []);
|
||||
return this.displayingItems = [];
|
||||
@@ -146,10 +135,16 @@ Vue.component('tt-autocomplete', {
|
||||
|
||||
this.isLoading = true;
|
||||
clearTimeout(this.fetchSuggestionsDebounceTimer);
|
||||
console.log(this.displayValue);
|
||||
|
||||
this.fetchSuggestionsDebounceTimer = setTimeout(() => {
|
||||
// Simulate the API call
|
||||
setTimeout(async () => {
|
||||
if (this.displayValue.length < 3) {
|
||||
this.displayingItems = [];
|
||||
this.isLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await axios.get(`${this.apiUrl}&autocomplete=1&q=${encodeURIComponent(this.displayValue)}`);
|
||||
if (response.data?.status === 'error') {
|
||||
this.displayingItems = [];
|
||||
@@ -161,14 +156,11 @@ Vue.component('tt-autocomplete', {
|
||||
this.fetchSuggestionsDebounceTimer = null;
|
||||
}, 100);
|
||||
}, 300); // Adjust the 300ms debounce time as needed
|
||||
}
|
||||
,
|
||||
selectSuggestion(item) {
|
||||
}, selectSuggestion(item) {
|
||||
this.$emit('input', item.value);
|
||||
this.displayValue = item.text;
|
||||
this.showSuggestions = false;
|
||||
},
|
||||
clear() {
|
||||
}, clear() {
|
||||
this.displayValue = '';
|
||||
this.$emit('input', '');
|
||||
}
|
||||
|
||||
@@ -1,20 +1,48 @@
|
||||
Vue.component('tt-modal', {
|
||||
props: {
|
||||
show: { type: Boolean, default: false },
|
||||
title: { type: String, default: 'Modal Title' },
|
||||
delete: { type: Boolean, default: true },
|
||||
deleteText: { type: String, default: 'Delete' },
|
||||
save: { type: Boolean, default: true },
|
||||
saveText: { type: String, default: 'Save' },
|
||||
},
|
||||
watch: {
|
||||
show: {type: Boolean, default: false},
|
||||
title: {type: String, default: 'Überschrift'},
|
||||
delete: {type: Boolean, default: true},
|
||||
deleteText: {type: String, default: 'Löschen'},
|
||||
save: {type: Boolean, default: true},
|
||||
saveText: {type: String, default: 'Speichern'},
|
||||
}, watch: {
|
||||
show(newVal) {
|
||||
if (!newVal) {
|
||||
this.$emit('close')
|
||||
}
|
||||
// if show now is true then focus the first input element
|
||||
if (newVal) {
|
||||
this.$nextTick(() => {
|
||||
const input = this.$refs.modal.querySelector('input')
|
||||
if (input) {
|
||||
if (input.classList.contains('tt-autocomplete')) {
|
||||
return
|
||||
}
|
||||
input.focus()
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
//language=Vue
|
||||
}, // create global listener for esc + return key (if save is enabled)
|
||||
created() {
|
||||
document.addEventListener('keydown', this.keydownHandler)
|
||||
}, destroyed() {
|
||||
document.removeEventListener('keydown', this.keydownHandler)
|
||||
}, methods: {
|
||||
keydownHandler(event) {
|
||||
if (!this.show) {
|
||||
return
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
this.$emit('update:show', false)
|
||||
}
|
||||
if (event.key === 'Enter' && this.save) {
|
||||
// only submit
|
||||
this.$emit('submit')
|
||||
}
|
||||
}
|
||||
}, //language=Vue
|
||||
template: `
|
||||
<div class="modal show d-block"
|
||||
role="dialog"
|
||||
|
||||
@@ -31,10 +31,10 @@ Vue.component('tt-select', {
|
||||
:required="required" v-model="selectedOption"
|
||||
@change="$emit('input', $event.target.value ? $event.target.value : undefined)">
|
||||
<template v-for="option of options">
|
||||
<option v-if="['string','number'].includes(typeof option)" :value="option">{{ option }}
|
||||
<option v-if="['string','number'].includes(typeof option)" :value="option" :disabled="option.disabled === true">{{ option }}
|
||||
<template v-if="suffix"> {{ suffix }}</template>
|
||||
</option>
|
||||
<option v-else :value="option.value">{{ option.text }}</option>
|
||||
<option v-else :value="option.value" :disabled="option.disabled === true">{{ option.text }}</option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -28,7 +28,8 @@ Vue.component('tt-table-crud', {
|
||||
|
||||
<template v-slot:actions="{ row }">
|
||||
<!-- calculate min width 1 + number of actions * 19 -->
|
||||
<div style="display: flex; justify-content: space-around; align-items: center;" :style="{minWidth: (1 + crudConfig?.additionalActions?.length || 0) * 19 + 'px'}">
|
||||
<div style="display: flex; justify-content: space-around; align-items: center;"
|
||||
:style="{minWidth: (1 + crudConfig?.additionalActions?.length || 0) * 19 + 'px'}">
|
||||
<a style="cursor: pointer;" @click="openCrudModal(row)"><i class="far fa-edit text-primary"
|
||||
title="Editieren"></i></a>
|
||||
|
||||
@@ -55,18 +56,18 @@ Vue.component('tt-table-crud', {
|
||||
@delete="deleteCrudModal"
|
||||
@close="resetCrudModalData">
|
||||
|
||||
<template v-for="column in modalConfig.headers">
|
||||
<template v-for="column in modalConfig.headers.filter(column => column.visible !== false)">
|
||||
<!-- @formatter:off -->
|
||||
<!-- <slot v-if="$scopedSlots[column.key.toLowerCase() + '-modal'] && column.type !== false && 1 < 0" :name="column.key.toLowerCase() + '-modal'" slot-scope="{crudModalData}"></slot>-->
|
||||
<slot :name="column.key.toLowerCase() + '-modal'" :crudModalData="crudModalData">
|
||||
<tt-input v-if="column.type === 'text'" v-model="crudModalData[column.key]" :label="column.text" sm row/>
|
||||
<tt-input v-else-if="column.type === 'number'" v-model="crudModalData[column.key]" :label="column.text" type="number" sm row/>
|
||||
<tt-textarea v-else-if="column.type === 'textarea'" v-model="crudModalData[column.key]" :label="column.text" sm row/>
|
||||
<tt-select v-else-if="column.type === 'select'" v-model="crudModalData[column.key]" :label="column.text" :options="column.items" sm row/>
|
||||
<tt-autocomplete v-else-if="column.type === 'autocomplete'" v-model="crudModalData[column.key]" :label="column.text" :items="column.items" sm row/>
|
||||
<tt-date-picker v-else-if="column.type === 'datepicker'" v-model="crudModalData[column.key]" :label="column.text" sm row/>
|
||||
<tt-icon-select v-else-if="column.type === 'icon-select'" v-model="crudModalData[column.key]" :label="column.text" sm row/>
|
||||
<tt-checkbox v-else-if="column.type === 'checkbox'" v-model="crudModalData[column.key]" :label="column.text" sm row/>
|
||||
<tt-input v-show="crudModalColumnVisibility[column.key]" v-if="column.type === 'text'" v-model="crudModalData[column.key]" :label="column.text" sm row/>
|
||||
<tt-input v-show="crudModalColumnVisibility[column.key]" v-else-if="column.type === 'number'" v-model="crudModalData[column.key]" :label="column.text" type="number" sm row/>
|
||||
<tt-textarea v-show="crudModalColumnVisibility[column.key]" v-else-if="column.type === 'textarea'" v-model="crudModalData[column.key]" :label="column.text" sm row/>
|
||||
<tt-select v-show="crudModalColumnVisibility[column.key]" v-else-if="column.type === 'select'" v-model="crudModalData[column.key]" :label="column.text" :options="column.items" sm row/>
|
||||
<tt-autocomplete v-show="crudModalColumnVisibility[column.key]" v-else-if="column.type === 'autocomplete'" v-model="crudModalData[column.key]" :label="column.text" :api-url="column.apiUrl" :items="column.items" sm row/>
|
||||
<tt-date-picker v-show="crudModalColumnVisibility[column.key]" v-else-if="column.type === 'datepicker'" v-model="crudModalData[column.key]" :label="column.text" sm row/>
|
||||
<tt-icon-select v-show="crudModalColumnVisibility[column.key]" v-else-if="column.type === 'icon-select'" v-model="crudModalData[column.key]" :label="column.text" sm row/>
|
||||
<tt-checkbox v-show="crudModalColumnVisibility[column.key]" v-else-if="column.type === 'checkbox'" v-model="crudModalData[column.key]" :label="column.text" sm row/>
|
||||
</slot>
|
||||
<!-- @formatter:on -->
|
||||
</template>
|
||||
@@ -74,14 +75,18 @@ Vue.component('tt-table-crud', {
|
||||
</tt-modal>
|
||||
</div>
|
||||
`, props: {
|
||||
crudConfig: {type: Object, required: false, default: () => (window['TT_CONFIG']['CRUD_CONFIG'])}
|
||||
crudConfig: {type: Object, required: false, default: () => (window['TT_CONFIG']['CRUD_CONFIG'])},
|
||||
emitEdit: {type: Boolean, required: false, default: false}
|
||||
}, data() {
|
||||
return {
|
||||
crudModal: false, crudModalData: {}, window: window
|
||||
|
||||
crudModal: false, crudModalData: {}, crudModalColumnVisibility: {}, crudModalColumnVisibilityCheck: {}, window: window
|
||||
}
|
||||
}, methods: {
|
||||
openCrudModal(row = null) {
|
||||
if (this.emitEdit) {
|
||||
this.$emit('edit', row)
|
||||
return
|
||||
}
|
||||
this.crudModalData = row ? JSON.parse(JSON.stringify(row)) : {};
|
||||
this.crudModal = true;
|
||||
}, resetCrudModalData() {
|
||||
@@ -97,8 +102,7 @@ Vue.component('tt-table-crud', {
|
||||
this.window.notify('success', response.data.message || 'Erfolgreich gespeichert');
|
||||
} else {
|
||||
this.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');
|
||||
}
|
||||
}, async deleteCrudModal() {
|
||||
|
||||
@@ -108,6 +112,42 @@ Vue.component('tt-table-crud', {
|
||||
this.resetCrudModalData();
|
||||
}
|
||||
this.window.notify(response.data.success ? 'success' : 'error', response.data.message);
|
||||
},
|
||||
async checkCrudModalColumnVisibility(val, oldVal) {
|
||||
const crudModalColumnVisibility = {}
|
||||
for (const column of this.modalConfig.headers) {
|
||||
if (!column?.visible?.reference || typeof column.visible.reference !== 'string') {
|
||||
crudModalColumnVisibility[column.key] = true
|
||||
continue;
|
||||
}
|
||||
|
||||
const localId = this.crudModalData[column.visible.use.split('=')[0]]
|
||||
|
||||
if (this.crudModalColumnVisibilityCheck[column.key] &&
|
||||
this.crudModalColumnVisibilityCheck[column.key][column.visible.reference] === localId) {
|
||||
crudModalColumnVisibility[column.key] = this.crudModalColumnVisibilityCheck[column.key]['visibility']
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!localId) {
|
||||
crudModalColumnVisibility[column.key] = false
|
||||
continue;
|
||||
}
|
||||
|
||||
const reference = column.visible.reference
|
||||
|
||||
let referenceData = await axios.get(window['TT_CONFIG']['BASE_PATH'] + `/${reference}/getById?id=${localId}`)
|
||||
referenceData = referenceData.data
|
||||
|
||||
// noinspection EqualityComparisonWithCoercionJS
|
||||
crudModalColumnVisibility[column.key] = referenceData[column.visible.key] == column.visible.value
|
||||
this.crudModalColumnVisibilityCheck[column.key] = {
|
||||
[column.visible.reference]: localId,
|
||||
visibility: crudModalColumnVisibility[column.key]
|
||||
}
|
||||
}
|
||||
|
||||
this.$set(this, 'crudModalColumnVisibility', crudModalColumnVisibility)
|
||||
}
|
||||
}, computed: {
|
||||
tableConfig() {
|
||||
@@ -115,17 +155,23 @@ Vue.component('tt-table-crud', {
|
||||
key: this.crudConfig.key,
|
||||
tableHeader: this.crudConfig.tableHeader,
|
||||
headers: this.crudConfig.columns.filter(column => column.table !== false).map(column => {
|
||||
return {text: column.text, key: column.key, ...column.table, filterOptions: column?.modal?.items}
|
||||
return {text: column.text, key: column.key, ...column.table, filterOptions: column?.modal?.items, priority: column.priority}
|
||||
})
|
||||
}
|
||||
}, modalConfig() {
|
||||
return {
|
||||
key: this.crudConfig.key,
|
||||
headers: this.crudConfig.columns.filter(column => column.modal !== false).map(column => {
|
||||
key: this.crudConfig.key, headers: this.crudConfig.columns.filter(column => column.modal !== false).map(column => {
|
||||
const type = column.modal?.type || "text"
|
||||
return {text: column.text, key: column.key, type, ...column.modal}
|
||||
}),
|
||||
}
|
||||
}
|
||||
}, watch: {
|
||||
crudModalData: {
|
||||
handler: async function (val, oldVal) {
|
||||
if (!val) return
|
||||
await this.checkCrudModalColumnVisibility(val, oldVal)
|
||||
}, deep: true
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -35,9 +35,7 @@
|
||||
Vue.component('tt-table-pagination', {
|
||||
props: {
|
||||
pagination: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({page: 1, per_page: 10, total_rows: 0, filtered_available: 0, total_pages: 1})
|
||||
type: Object, required: true, default: () => ({page: 1, per_page: 10, total_rows: 0, filtered_available: 0, total_pages: 1})
|
||||
}, reverse: {type: Boolean, default: false}
|
||||
}, computed: {
|
||||
pagesToDisplay() {
|
||||
@@ -50,9 +48,11 @@ Vue.component('tt-table-pagination', {
|
||||
}
|
||||
return pages.length === 0 ? [1] : pages;
|
||||
}, pageInfoText() {
|
||||
const start = Math.min(this.pagination.page * this.pagination.per_page - this.pagination.per_page + 1,
|
||||
this.pagination.total_rows);
|
||||
const start = Math.min(this.pagination.page * this.pagination.per_page - this.pagination.per_page + 1, this.pagination.total_rows);
|
||||
const end = Math.min(this.pagination.page * this.pagination.per_page, this.pagination.total_rows);
|
||||
|
||||
if (!this.pagination.filtered_available) this.pagination.filtered_available = this.pagination.total_rows;
|
||||
|
||||
const total = this.pagination.total_rows ===
|
||||
this.pagination.filtered_available ? this.pagination.total_rows : `${this.pagination.filtered_available} (${this.pagination.total_rows})`;
|
||||
return `${start} bis ${end} von ${total}`;
|
||||
@@ -158,7 +158,7 @@ Vue.component('tt-table', {
|
||||
<tt-icon-select v-else-if="column.filter === 'iconSelect' && !disableFiltering" :options="column.filterOptions" v-model="filters[column.key]" sm/>
|
||||
<tt-number-range v-else-if="column.filter === 'numberRange' && !disableFiltering" :returnText="!ssr" v-model="filters[column.key]" sm/>
|
||||
<tt-select v-else-if="column.filter === 'select' && !disableFiltering" :options="[{text: 'Alle', value: undefined}, ...column.filterOptions]" v-model="filters[column.key]" sm/>
|
||||
<tt-autocomplete v-else-if="column.filter === 'autocomplete' && !disableFiltering" :items="[{text: 'Alle', value: undefined}, ...column.filterOptions]" v-model="filters[column.key]" sm/>
|
||||
<tt-autocomplete v-else-if="column.filter === 'autocomplete' && !disableFiltering" :api-url="column.filterOptions" v-model="filters[column.key]" sm/>
|
||||
<tt-date-picker v-else-if="column.filter === 'date' && !disableFiltering" v-model="filters[column.key]"/>
|
||||
<!-- @formatter:on -->
|
||||
</th>
|
||||
@@ -199,10 +199,12 @@ Vue.component('tt-table', {
|
||||
<i v-else-if="column.filter === 'iconSelect'"
|
||||
: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>
|
||||
<span v-else-if="column.filter === 'autocomplete'">{{
|
||||
columns[key].filterOptions.find(option => option.value.toString() ===
|
||||
row[key].toString())?.text
|
||||
}}</span>
|
||||
<span v-else-if="column.filter === 'autocomplete'">
|
||||
{{ autoCompleteData[key] && autoCompleteData[key][row[key]] ? autoCompleteData[key][row[key]]['title'] : row[key] }}
|
||||
|
||||
|
||||
</span>
|
||||
<span v-else-if="key === 'create'">{{ window.moment(row[key] * 1000).format('DD.MM.YYYY HH:mm:ss') }}</span>
|
||||
<span v-else-if="row[key] !== null"
|
||||
v-html="(column.prefix) + (row[key] === null || typeof row[key] === 'undefined' ? '' : row[key]?.toString()?.replace('\\\\n', '<br>')) + (column.suffix )"></span>
|
||||
|
||||
@@ -229,6 +231,9 @@ Vue.component('tt-table', {
|
||||
<i v-else-if="column.filter === 'iconSelect'"
|
||||
: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>
|
||||
<span v-else-if="column.filter === 'autocomplete'">
|
||||
{{ autoCompleteData[key] && autoCompleteData[key][row[key]] ? autoCompleteData[key][row[key]]['title'] : row[key] }}</span>
|
||||
|
||||
<span v-else-if="row[key] !== null"
|
||||
v-html="(column.prefix) + (row[key] === null || typeof row[key] === 'undefined' ? '' : row[key]?.toString()?.replace('\\\\n', '<br>')) + (column.suffix )">
|
||||
</span>
|
||||
@@ -297,7 +302,8 @@ Vue.component('tt-table', {
|
||||
hiddenColumns: [],
|
||||
originalColumnWidths: {},
|
||||
originalTableWidth: null,
|
||||
debouncedHandleResize: null
|
||||
debouncedHandleResize: null,
|
||||
autoCompleteData: {}
|
||||
};
|
||||
},
|
||||
|
||||
@@ -316,12 +322,10 @@ Vue.component('tt-table', {
|
||||
if (this.debounceTimeout) clearTimeout(this.debounceTimeout);
|
||||
this.debounceTimeout = setTimeout(() => fn.apply(context, args), wait);
|
||||
}
|
||||
},
|
||||
refreshTable() {
|
||||
}, refreshTable() {
|
||||
this.loading = true;
|
||||
this.fetchData(this.pagination.page).then()
|
||||
},
|
||||
/**
|
||||
}, /**
|
||||
* Fetches and updates data for a specified page.
|
||||
*
|
||||
* @param {number} page The page number to fetch data for.
|
||||
@@ -365,6 +369,7 @@ Vue.component('tt-table', {
|
||||
this.pagination = {page: 1, per_page: 10, total_rows: 0, total_pages: 1};
|
||||
} else {
|
||||
this.rows = response.data.rows;
|
||||
this.autoCompleteData = response.data.autoCompleteData;
|
||||
this.pagination = response.data.pagination;
|
||||
}
|
||||
this.loading = false;
|
||||
@@ -409,9 +414,7 @@ Vue.component('tt-table', {
|
||||
|
||||
localStorage.setItem(`tt-table-${this.config.key}`, JSON.stringify({
|
||||
// filter filters with empty values or empty objects
|
||||
filters,
|
||||
paginationPerPage: this.pagination.per_page,
|
||||
order: this.order.key ? this.order : undefined,
|
||||
filters, paginationPerPage: this.pagination.per_page, order: this.order.key ? this.order : undefined,
|
||||
}));
|
||||
}, parseSettingsFromLocalStorage() {
|
||||
if (this.disableFiltering) return false;
|
||||
@@ -462,8 +465,7 @@ Vue.component('tt-table', {
|
||||
|
||||
if (this.columns[key] && this.columns[key].filter === 'iconSelect') {
|
||||
parsedRow[this.columns[key].text] =
|
||||
this.columns[key].filterOptions.find(option => option.value.toString() ===
|
||||
row[key].toString())?.text;
|
||||
this.columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.text;
|
||||
} else if (this.columns[key] && this.columns[key].filter === 'date') {
|
||||
parsedRow[this.columns[key].text] = this.moment(row[key]).format('DD.MM.YYYY HH:mm');
|
||||
} else {
|
||||
@@ -478,9 +480,9 @@ Vue.component('tt-table', {
|
||||
const wb = this.window.XLSX.utils.book_new();
|
||||
|
||||
|
||||
let data = typeof this.config.customExcelProcessor === 'function' ?
|
||||
this.config.customExcelProcessor(JSON.parse(JSON.stringify(this.computedRows))) :
|
||||
defaultExcelProcessor.call(this, JSON.parse(JSON.stringify(this.computedRows)));
|
||||
let data = typeof this.config.customExcelProcessor ===
|
||||
'function' ? this.config.customExcelProcessor(JSON.parse(JSON.stringify(this.computedRows))) : defaultExcelProcessor.call(this,
|
||||
JSON.parse(JSON.stringify(this.computedRows)));
|
||||
|
||||
const ws = this.window.XLSX.utils.json_to_sheet(data);
|
||||
|
||||
@@ -506,13 +508,10 @@ Vue.component('tt-table', {
|
||||
this.disableDebounce = true;
|
||||
window.notify('success', 'Filter zurückgesetzt');
|
||||
}, toggleExpand(index) {
|
||||
this.expandedRows[index] ? this.$delete(this.expandedRows, index) : this.$set(this.expandedRows,
|
||||
index,
|
||||
true);
|
||||
this.expandedRows[index] ? this.$delete(this.expandedRows, index) : this.$set(this.expandedRows, index, true);
|
||||
}, isExpanded(index) {
|
||||
return !!this.expandedRows[index];
|
||||
},
|
||||
async handleResponsiveColumns() {
|
||||
}, async handleResponsiveColumns() {
|
||||
if (!this.$refs.tableContainer) return;
|
||||
const tableContainer = this.$refs.tableContainer;
|
||||
const {paddingLeft, paddingRight} = window.getComputedStyle(tableContainer);
|
||||
@@ -536,7 +535,7 @@ Vue.component('tt-table', {
|
||||
|
||||
if (i === 0) continue;
|
||||
|
||||
else if (viewportWidth +10 < 0) {
|
||||
else if (viewportWidth + 10 < 0) {
|
||||
this.hiddenColumns.push(columns[i].key)
|
||||
}
|
||||
}
|
||||
@@ -547,6 +546,16 @@ Vue.component('tt-table', {
|
||||
handler: function () {
|
||||
if (!this.isInitialised) return;
|
||||
|
||||
// go through filters and if there is a set value in filters and the filter of the column is select or autocomplete then parse the value to int
|
||||
for (const key in this.filters) {
|
||||
if (this.filters[key] && (this.columns[key].filter === 'select' || this.columns[key].filter === 'autocomplete')) {
|
||||
// only if first character is a number
|
||||
if (!isNaN(this.filters[key][0])) {
|
||||
this.filters[key] = parseInt(this.filters[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.ssr) {
|
||||
this.fetchRows(this.pagination?.page || 1, true).then();
|
||||
}
|
||||
@@ -574,15 +583,13 @@ Vue.component('tt-table', {
|
||||
|
||||
this.saveSettingsToLocalStorage();
|
||||
}, deep: true
|
||||
},
|
||||
rows: {
|
||||
}, rows: {
|
||||
handler: async function () {
|
||||
this.$nextTick(() => {
|
||||
this.handleResponsiveColumns()
|
||||
})
|
||||
}, deep: true
|
||||
},
|
||||
computedRows: {
|
||||
}, computedRows: {
|
||||
handler: async function () {
|
||||
this.$nextTick(() => {
|
||||
this.handleResponsiveColumns()
|
||||
@@ -620,9 +627,7 @@ Vue.component('tt-table', {
|
||||
}, pagesToDisplay() {
|
||||
let range = 2; // Number of pages before and after the current page
|
||||
let start = (this.pagination.page < 4 ? 1 : this.pagination.page - range);
|
||||
let end = (this.pagination.page +
|
||||
range >
|
||||
this.pagination.total_pages ? this.pagination.total_pages : this.pagination.page + range);
|
||||
let end = (this.pagination.page + range > this.pagination.total_pages ? this.pagination.total_pages : this.pagination.page + range);
|
||||
if (end < 5) end = 5;
|
||||
|
||||
// Adjust start and end if they are out of bounds
|
||||
@@ -691,8 +696,7 @@ Vue.component('tt-table', {
|
||||
const substrings = (isNegated ? filterValue.slice(1) : filterValue).split(' ')
|
||||
.map(s => s.toLowerCase());
|
||||
|
||||
const targetValue = !row[header.key] ? '' :
|
||||
typeof row[header.key] === 'object' ? Object.values(row[header.key])
|
||||
const targetValue = !row[header.key] ? '' : typeof row[header.key] === 'object' ? Object.values(row[header.key])
|
||||
.join(' ')
|
||||
.toLowerCase() : row[header.key].toString().toLowerCase();
|
||||
|
||||
@@ -717,12 +721,7 @@ Vue.component('tt-table', {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
} else if (header.filter ===
|
||||
'select' ||
|
||||
header.filter ===
|
||||
'iconSelect' ||
|
||||
header.filter ===
|
||||
'autocomplete') {
|
||||
} else if (header.filter === 'select' || header.filter === 'iconSelect' || header.filter === 'autocomplete') {
|
||||
if (filterValue === '') continue;
|
||||
if (filterValue !== row[header.key]?.toString()) {
|
||||
match = false;
|
||||
@@ -731,8 +730,7 @@ Vue.component('tt-table', {
|
||||
} else if (header.filter === 'date') {
|
||||
if (!filterValue.from || !filterValue.to) continue;
|
||||
|
||||
const dateInt = row[header.key].length === 10 ? parseInt(row[header.key]) *
|
||||
1000 : parseInt(row[header.key]);
|
||||
const dateInt = row[header.key].length === 10 ? parseInt(row[header.key]) * 1000 : parseInt(row[header.key]);
|
||||
let rowDate = new Date(dateInt).getTime() / 1000;
|
||||
|
||||
if (rowDate < filterValue.from || rowDate > filterValue.to) {
|
||||
@@ -757,14 +755,10 @@ Vue.component('tt-table', {
|
||||
const isDateColumn = this.columns[this.order.key].filter === 'date';
|
||||
|
||||
output.sort((a, b) => {
|
||||
let valueA = isDateColumn ?
|
||||
new Date(a[this.order.key].length === 10 ? parseInt(a[this.order.key]) *
|
||||
1000 : parseInt(a[this.order.key])).getTime() :
|
||||
a[this.order.key] || ''
|
||||
let valueB = isDateColumn ?
|
||||
new Date(b[this.order.key].length === 10 ? parseInt(b[this.order.key]) *
|
||||
1000 : parseInt(b[this.order.key])).getTime() :
|
||||
b[this.order.key] || ''
|
||||
let valueA = isDateColumn ? new Date(a[this.order.key].length === 10 ? parseInt(a[this.order.key]) *
|
||||
1000 : parseInt(a[this.order.key])).getTime() : a[this.order.key] || ''
|
||||
let valueB = isDateColumn ? new Date(b[this.order.key].length === 10 ? parseInt(b[this.order.key]) *
|
||||
1000 : parseInt(b[this.order.key])).getTime() : b[this.order.key] || ''
|
||||
|
||||
if (valueA === valueB) return 0;
|
||||
|
||||
@@ -778,8 +772,7 @@ Vue.component('tt-table', {
|
||||
}
|
||||
// console.timeEnd('Filtering and pagination');
|
||||
return output;
|
||||
},
|
||||
visibleRows() {
|
||||
}, visibleRows() {
|
||||
if (!this.rawRows || this.ssr === true) return null;
|
||||
// Pagination and slice logic
|
||||
this.pagination.total_pages = Math.ceil(this.computedRows.length / this.pagination.per_page);
|
||||
@@ -812,14 +805,12 @@ Vue.component('tt-table', {
|
||||
// use header#topnav as top to stick to but if window is resized then check if header#topnav height is changed
|
||||
|
||||
const headerHeight = document.querySelector('header#topnav')?.offsetHeight || 0;
|
||||
style.innerHTML =
|
||||
`table thead th { position: sticky; top: ${headerHeight}px; z-index: 1; background-color: white; }`;
|
||||
style.innerHTML = `table thead th { position: sticky; top: ${headerHeight}px; z-index: 1; background-color: white; }`;
|
||||
style.id = 'tt-table-sticky-header';
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
const headerHeight = document.querySelector('header#topnav')?.offsetHeight || 0;
|
||||
style.innerHTML =
|
||||
`table thead th { position: sticky; top: ${headerHeight}px; z-index: 1; background-color: white; }`;
|
||||
style.innerHTML = `table thead th { position: sticky; top: ${headerHeight}px; z-index: 1; background-color: white; }`;
|
||||
})
|
||||
|
||||
document.head.appendChild(style);
|
||||
@@ -827,8 +818,7 @@ Vue.component('tt-table', {
|
||||
|
||||
this.debouncedHandleResize = this.debounce(this.handleResponsiveColumns, 100);
|
||||
window.addEventListener('resize', this.debouncedHandleResize);
|
||||
},
|
||||
beforeDestroy() {
|
||||
}, beforeDestroy() {
|
||||
window.removeEventListener('resize', this.debouncedHandleResize);
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user