update for warehouse

This commit is contained in:
2024-10-10 08:49:50 +02:00
parent 8a2b8c0b20
commit c57eef6e8d
56 changed files with 2250 additions and 451 deletions

View File

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

View File

@@ -0,0 +1,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>

View 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>

View File

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

View File

@@ -534,6 +534,7 @@ class AddressController extends mfBaseController {
];
$results[] = $result;
$this->returnJson($results);
die();
}
}

View File

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

View File

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

View File

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

View File

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

View File

View 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() {

View File

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

View File

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

View File

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

View File

@@ -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'],

View File

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

View File

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

View File

@@ -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']],
];

View File

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

View File

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

View 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));
}
}

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

View File

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

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

View File

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

View File

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

View File

@@ -1,7 +0,0 @@
<?php
class WarehouseRevenueAccountModel extends TTCrudBaseModel {
public int $id;
public int $revenueAccountNumber;
public string $title;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,10 @@
<?php
class WarehouseShippingNoteTextElementModel extends TTCrudBaseModel {
public int $id;
public string $title;
public string $content;
public int $create;
public int $createBy;
}

View 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();
}
}
}

View File

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

View File

@@ -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 && \

View File

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

View File

@@ -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) . "'";
}

View File

@@ -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.") ";
}

View File

@@ -169,6 +169,7 @@ class mfRouter {
}
$baseurl = $baseurl ?? "";
$baseurl = preg_replace('@/$@', '', $baseurl);
define("MFFANCYBASEURL",$baseurl);
}

View File

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

View File

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

View File

@@ -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,
}
},
})

View File

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

View File

@@ -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: `

View File

@@ -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() {

View File

@@ -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
},
},
})

View File

@@ -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() {

View File

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

View 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();
}
})

View 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;
}
}
})

View File

@@ -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', '');
}

View File

@@ -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"

View File

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

View File

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

View File

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