Feature/warehouse
This commit is contained in:
@@ -12,9 +12,9 @@ $additionalCSS = $additionalCSS ?? [];
|
||||
$additionalJS = $additionalJS ?? [];
|
||||
|
||||
$additionalJS = [
|
||||
...$additionalJS,
|
||||
"bundler.php",
|
||||
"js/pages/" . $vueViewName . "/" . $vueViewName . ".js",
|
||||
...$additionalJS,
|
||||
];
|
||||
|
||||
$additionalCSS = [
|
||||
|
||||
@@ -130,6 +130,27 @@
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if($me->is(["Admin"])&& isset($_GET['warehouse'])): ?>
|
||||
<li class="has-submenu">
|
||||
<a href="#">
|
||||
<i class="fa-solid fa-warehouse"></i>Lager <div class="arrow-down"></div>
|
||||
</a>
|
||||
<ul class="submenu">
|
||||
<!-- create links for WarehouseArticle, WarehouseDistributor, WarehouseLocation, WarehouseItem, WarehouseOrderRecommendation -->
|
||||
<?php if($me->isAdmin() || $me->can("WarehouseArticle")): ?><li><a href="<?=self::getUrl("WarehouseArticle")?>?warehouse"><i class="far fa-fw fa-box text-info"></i> Artikel</a></li><?php endif; ?>
|
||||
<?php if($me->isAdmin() || $me->can("WarehouseDistributor")): ?><li><a href="<?=self::getUrl("WarehouseDistributor")?>?warehouse"><i class="far fa-fw fa-truck text-info"></i> Lieferanten</a></li><?php endif; ?>
|
||||
<?php if($me->isAdmin() || $me->can("WarehouseArticlePriceType")): ?><li><a href="<?=self::getUrl("WarehouseArticlePriceType")?>?warehouse"><i class="far fa-fw fa-money-bill-wave text-info"></i> Preis Typen</a></li><?php endif; ?>
|
||||
<?php if($me->isAdmin() || $me->can("WarehouseLocation")): ?><li><a href="<?=self::getUrl("WarehouseLocation")?>?warehouse"><i class="far fa-fw fa-map-marker-alt text-info"></i> Lagerorte</a></li><?php endif; ?>
|
||||
<?php if($me->isAdmin() || $me->can("WarehouseItem")): ?><li><a href="<?=self::getUrl("WarehouseItem")?>?warehouse"><i class="far fa-fw fa-boxes text-info"></i> Lagerbestand</a></li><?php endif; ?>
|
||||
<?php if($me->isAdmin() || $me->can("WarehouseOrderRecommendation")): ?><li><a href="<?=self::getUrl("WarehouseOrderRecommendation")?>?warehouse"><i class="far fa-fw fa-box-full text-info"></i> Bestellvorschläge</a></li><?php endif; ?>
|
||||
<?php if($me->isAdmin() || $me->can("WarehouseEShop")): ?><li><a href="<?=self::getUrl("WarehouseEShop")?>?warehouse"><i class="far fa-fw fa-shopping-cart text-info"></i> E-Shop</a></li><?php endif; ?>
|
||||
<?php if($me->isAdmin() || $me->can("WarehouseEShopOrder")): ?><li><a href="<?=self::getUrl("WarehouseEShopOrder")?>?warehouse"><i class="far fa-fw fa-shopping-basket text-info"></i> E-Shop Bestellungen</a></li><?php endif; ?>
|
||||
<?php if($me->isAdmin() || $me->can("WarehouseOrder")): ?><li><a href="<?=self::getUrl("WarehouseOrder")?>?warehouse"><i class="far fa-fw fa-shopping-bag text-info"></i> Bestellungen</a></li><?php endif; ?>
|
||||
<?php if($me->isAdmin() || $me->can("WarehouseShippingNote")): ?><li><a href="<?=self::getUrl("WarehouseShippingNote")?>?warehouse"><i class="far fa-fw fa-shipping-fast text-info"></i> Lieferscheine</a></li><?php endif; ?>
|
||||
</ul>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if($me->is(["Admin"]) || $me->can("Voipnumbering")): ?>
|
||||
<li class="has-submenu mobile-hide">
|
||||
<a href="#">
|
||||
|
||||
@@ -24,20 +24,7 @@ class VoiceCallHistoryController extends mfBaseController {
|
||||
}
|
||||
|
||||
protected function indexAction(): void {
|
||||
$JSGlobals = ["BASE_URL" => self::getUrl("Domain"),
|
||||
"DASHBOARD_URL" => self::getUrl("Dashboard"),
|
||||
"MFAPPNAME" => MFAPPNAME_SLUG,
|
||||
"PAGE_TITLE" => "Voice Call History",
|
||||
"PATH" => [
|
||||
["text" => MFAPPNAME_SLUG, "href" => self::getUrl("Dashboard")],
|
||||
["text" => "Voice Call History", "href" => self::getUrl("VoiceCallHistory")]
|
||||
],
|
||||
"VOICE_CALL_HISTORY_API_URL" => self::getUrl("VoiceCallHistory/api"),
|
||||
];
|
||||
|
||||
$this->layout()->set("vueViewName", "VoiceCallHistory");
|
||||
$this->layout()->set("JSGlobals", $JSGlobals);
|
||||
$this->layout()->setTemplate("VueViews/Vue");
|
||||
Helper::renderVue($this, "Voice Call History", $this->mod, ["VOICE_CALL_HISTORY_API_URL" => $this->getUrl("VoiceCallHistory", "api")]);
|
||||
}
|
||||
|
||||
protected function apiAction() {
|
||||
|
||||
9
application/WarehouseArticle/WarehouseArticle.php
Normal file
9
application/WarehouseArticle/WarehouseArticle.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @property mixed|null $name
|
||||
*/
|
||||
class WarehouseArticle extends mfBaseModel
|
||||
{
|
||||
|
||||
}
|
||||
188
application/WarehouseArticle/WarehouseArticleController.php
Normal file
188
application/WarehouseArticle/WarehouseArticleController.php
Normal file
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
class WarehouseArticleController extends TTCrud {
|
||||
protected string $headerTitle = 'Artikel';
|
||||
protected string $createText = 'Artikel erstellen';
|
||||
|
||||
// @formatter:off
|
||||
protected array $columns = [
|
||||
['key' => 'title', 'text' => 'Titel', 'required' => true, 'table' => ['priority' => 9]],
|
||||
['key' => 'description', 'text' => 'Beschreibung', 'required' => true, 'table' => false],
|
||||
['key' => 'category', 'text' => 'Kategorie', 'required' => true],
|
||||
['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', 'modal' => ['type' => 'number'], 'table' => ['class' => 'text-center']], // Stock/inventory related
|
||||
['key' => 'criticalAmount', 'text' => 'Kritische Menge', 'modal' => ['type' => 'number'], 'table' => ['class' => 'text-center']], // Stock/inventory related
|
||||
['key' => 'isEShop', 'text' => 'Ist E-Shop', 'modal' => ['type' => 'checkbox'], 'table' => false], // Boolean value
|
||||
['key' => 'actions', 'text' => 'Aktionen', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center', 'priority' => 8]]
|
||||
];
|
||||
|
||||
protected array $additionalActions = [
|
||||
['key' => 'openHistory','title' => 'Historie','class' => 'fas fa-history text-secondary'],
|
||||
['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'],
|
||||
];
|
||||
// @formatter:on
|
||||
|
||||
protected array $infoMessages = ['create' => 'Artikel wurde erstellt',
|
||||
'update' => 'Artikel wurde aktualisiert',
|
||||
'delete' => 'Artikel wurde gelöscht',
|
||||
'noChanges' => 'Keine Änderungen',];
|
||||
|
||||
protected function beforeUpdate($postData): bool {
|
||||
(new WarehouseHistoryController)->create($postData, $this->mod);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function afterUpdate($postData) {
|
||||
self::updateCheapestPurchasePrice($postData['id']);
|
||||
}
|
||||
|
||||
public static function updateCheapestPurchasePrice($articleId) {
|
||||
$cheapestPurchasePrice = WarehouseArticleDistributorModel::getAll(['articleId' => $articleId], 1, 0,
|
||||
['key' => 'purchasePrice', 'order' => 'ASC'])[0]->purchasePrice;
|
||||
|
||||
$article = WarehouseArticleModel::get($articleId);
|
||||
|
||||
if (!$article instanceof WarehouseArticleModel) {
|
||||
throw new Exception("Article is not an instance of WarehouseArticleModel");
|
||||
}
|
||||
|
||||
//TODO: think of a new way as we have multiple sell prices now and article sellPriceOverride and sellPriceMultiplier do not exist anymore
|
||||
// $cheapestSellPrice = $article->sellPriceOverride ?? $article->sellPriceMultiplier * $cheapestPurchasePrice;
|
||||
|
||||
if ($article->cheapestPurchasePrice != $cheapestPurchasePrice) {
|
||||
WarehouseArticleModel::update([...get_object_vars($article), // Unpack properties into an array
|
||||
'cheapestPurchasePrice' => $cheapestPurchasePrice]);
|
||||
}
|
||||
}
|
||||
|
||||
protected function afterCreate($postData) {
|
||||
self::updateCheapestPurchasePrice($postData['id']);
|
||||
}
|
||||
|
||||
protected function updateAllCheapestPurchasePricesAction() {
|
||||
$articles = WarehouseArticleModel::getAll();
|
||||
foreach ($articles as $article) {
|
||||
self::updateCheapestPurchasePrice($article->id);
|
||||
}
|
||||
}
|
||||
|
||||
protected function getHistoryAction() {
|
||||
self::returnJson((new WarehouseHistoryController)->getHistory($this->request->id, $this->mod, $this->columns));
|
||||
}
|
||||
|
||||
protected function importAction() {
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
// read import.json from directory of this file
|
||||
$json = fopen(dirname(__FILE__) . '/import.json', 'r') or die('Unable to open file!');
|
||||
|
||||
// read file content
|
||||
$data = fread($json, filesize(dirname(__FILE__) . '/import.json'));
|
||||
|
||||
// decode json
|
||||
$data = json_decode($data, true);
|
||||
|
||||
// close file
|
||||
fclose($json);
|
||||
|
||||
// die with data length
|
||||
// loop through data
|
||||
echo 'Importing ' . count($data) . ' items' . PHP_EOL;
|
||||
$count = 0;
|
||||
foreach ($data as $item) {
|
||||
// echo count + 1
|
||||
echo ++$count . PHP_EOL;
|
||||
// Check if Distributor exists
|
||||
$distributor = WarehouseDistributorModel::getAll(['name' => $item['Lieferant']]);
|
||||
if (empty($distributor)) {
|
||||
$distributorId = WarehouseDistributorModel::create(['name' => $item['Lieferant'],
|
||||
'address' => 'Missing',
|
||||
'plz' => 'Missing',
|
||||
'city' => 'Missing',
|
||||
'countryId' => 1,
|
||||
'email' => 'Missing',
|
||||
'phone' => 'Missing',
|
||||
'contactPerson' => 'Missing',]);
|
||||
} else {
|
||||
$distributorId = $distributor[0]->id;
|
||||
}
|
||||
|
||||
// only continue if PRODUKT 1.ZEILE and PRODUKT 2.ZEILE and ARTIKEL GRUPPE and VK and EK and Lieferant/ Hersteller Artikelnr: are set
|
||||
if (!isset($item['PRODUKT 1.ZEILE'])) {
|
||||
echo 'Missing data for ' . $item['PRODUKT 1.ZEILE'] . PHP_EOL;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (empty($item['VK'])) {
|
||||
$item['VK'] = 0;
|
||||
$calcSellPriceMultiplier = 0;
|
||||
} else {
|
||||
$item['VK'] = floatval(str_replace(',', '', $item['VK']));
|
||||
}
|
||||
|
||||
if (empty($item['EK'])) {
|
||||
$item['EK'] = 0;
|
||||
$calcSellPriceMultiplier = 0;
|
||||
$purchasePrice = 0;
|
||||
} else {
|
||||
$item['EK'] = floatval(str_replace(',', '', $item['EK']));
|
||||
}
|
||||
|
||||
if (!empty($item['VK']) && !empty($item['EK'])) {
|
||||
$calcSellPriceMultiplier = $item['VK'] / $item['EK'];
|
||||
|
||||
// if calcSellPriceMultiplier has more than 2 decimal places assign $calcSellPriceOverride
|
||||
echo strlen(substr(strrchr($calcSellPriceMultiplier, "."), 1)) . PHP_EOL;
|
||||
if (strlen(substr(strrchr($calcSellPriceMultiplier, "."), 1)) > 2) {
|
||||
$calcSellPriceOverride = $item['VK'];
|
||||
}
|
||||
|
||||
$purchasePrice = str_replace(',', '', $item['EK']);
|
||||
}
|
||||
|
||||
if (!isset($calcSellPriceMultiplier) && !isset($calcSellPriceOverride) || !isset($purchasePrice)) {
|
||||
echo 'Missing data for ' . $item['PRODUKT 1.ZEILE'] . PHP_EOL;
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// Check if Article exists
|
||||
$article = WarehouseArticleModel::getAll(['title' => ['exact' => $item['PRODUKT 1.ZEILE']]]);
|
||||
if (empty($article)) {
|
||||
$articleCreateData = ['title' => $item['PRODUKT 1.ZEILE'],
|
||||
'description' => $item['PRODUKT 2. ZEILE'],
|
||||
'category' => $item['ARTIKEL GRUPPE'],
|
||||
'cheapestPurchasePrice' => 0,
|
||||
'warningAmount' => 25,
|
||||
'criticalAmount' => 10,
|
||||
'isEShop' => 0,];
|
||||
|
||||
// if calcSellPriceOverride is set, add it to the $articleCreateData array
|
||||
if (isset($calcSellPriceOverride)) {
|
||||
$articleCreateData['sellPriceOverride'] = $calcSellPriceOverride;
|
||||
} else if (isset($calcSellPriceMultiplier)) {
|
||||
$articleCreateData['sellPriceMultiplier'] = $calcSellPriceMultiplier;
|
||||
}
|
||||
|
||||
$articleId = WarehouseArticleModel::create($articleCreateData);
|
||||
} else {
|
||||
echo 'Article already exists with title ' . $item['PRODUKT 1.ZEILE'] . PHP_EOL;
|
||||
$articleId = $article[0]->id;
|
||||
}
|
||||
|
||||
// Check if ArticleDistributor exists
|
||||
$articleDistributor = WarehouseArticleDistributorModel::getAll(['articleId' => $articleId,
|
||||
'distributorId' => $distributorId]);
|
||||
if (empty($articleDistributor)) {
|
||||
WarehouseArticleDistributorModel::create(['articleId' => $articleId,
|
||||
'distributorId' => $distributorId,
|
||||
'purchasePrice' => $purchasePrice,
|
||||
'externalArticleNumber' => $item['Lieferant/ Hersteller Artikelnr:'],]);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
15
application/WarehouseArticle/WarehouseArticleModel.php
Normal file
15
application/WarehouseArticle/WarehouseArticleModel.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
class WarehouseArticleModel extends TTCrudBaseModel {
|
||||
public int $id;
|
||||
public string $title;
|
||||
public string $description;
|
||||
public string $category;
|
||||
public ?float $cheapestPurchasePrice;
|
||||
public ?float $cheapestSellPrice;
|
||||
public int $warningAmount;
|
||||
public int $criticalAmount;
|
||||
public int $isEShop;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @property mixed|null $name
|
||||
*/
|
||||
class WarehouseArticleDistributor extends mfBaseModel
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
class WarehouseArticleDistributorController extends TTCrud {
|
||||
protected string $headerTitle = 'Lieferanteintrag';
|
||||
protected string $createText = 'Lieferanteintrag erstellen';
|
||||
|
||||
// @formatter:off
|
||||
protected array $columns = [
|
||||
['key' => 'purchasePrice', 'text' => 'Einkaufspreis', 'required' => true, 'modal' => ['type' => 'number']],
|
||||
['key' => 'articleId', 'text' => 'Artikel', 'required' => true, 'modal' => ['type' => 'select', 'data' => 'WarehouseArticle']],
|
||||
['key' => 'distributorId', 'text' => 'Lieferant', 'required' => true, 'modal' => ['type' => 'select', 'data' => 'WarehouseDistributor']],
|
||||
['key' => 'externalArticleNumber', 'text' => 'Externe Artikelnummer', 'required' => true],
|
||||
['key' => 'note', 'text' => 'Notiz', 'required' => false, 'modal' => ['type' => 'textarea']],
|
||||
['key' => 'actions', 'text' => 'Aktionen', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center']]
|
||||
];
|
||||
// @formatter:on
|
||||
|
||||
protected array $infoMessages = ['create' => 'Lieferanteintrag wurde erstellt',
|
||||
'update' => 'Lieferanteintrag wurde aktualisiert',
|
||||
'delete' => 'Lieferanteintrag wurde gelöscht',
|
||||
'noChanges' => 'Keine Änderungen',];
|
||||
|
||||
protected function checkExistingThresholdEntry($postData): bool {
|
||||
$count = WarehouseLocationThresholdOverrideModel::count(['articleId' => $postData['articleId'],
|
||||
'distributorId' => $postData['distributorId']]);
|
||||
|
||||
if ($count > 0) {
|
||||
self::returnJson(['success' => false,
|
||||
'message' => 'Es existiert bereits ein Eintrag mit dieser Artikelnummer und diesem Lieferanten.']);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function beforeCreate($postData): bool {
|
||||
return $this->checkExistingThresholdEntry($postData);
|
||||
}
|
||||
|
||||
protected function afterCreate($postData) {
|
||||
WarehouseArticleController::updateCheapestPurchasePrice($postData['articleId']);
|
||||
}
|
||||
|
||||
protected function beforeUpdate($postData): bool {
|
||||
$existing = $this->checkExistingThresholdEntry($postData);
|
||||
|
||||
if (!$existing) {
|
||||
return false;
|
||||
}
|
||||
|
||||
(new WarehouseHistoryController)->create($postData, $this->mod);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function afterUpdate($postData) {
|
||||
WarehouseArticleController::updateCheapestPurchasePrice($postData['articleId']);
|
||||
}
|
||||
|
||||
protected function getHistoryAction() {
|
||||
$history = WarehouseHistoryModel::getByRowId($this->request->id, $this->mod);
|
||||
|
||||
$history = array_map(function ($item) {
|
||||
$item = (array) $item;
|
||||
|
||||
$item['columnHeader'] = $this->columns[array_search($item['key'], array_column($this->columns, 'key'))]['text'];
|
||||
return $item;
|
||||
}, $history);
|
||||
|
||||
self::returnJson($history);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
class WarehouseArticleDistributorModel extends TTCrudBaseModel {
|
||||
public int $id;
|
||||
public int $articleId;
|
||||
public int $distributorId;
|
||||
public float $purchasePrice;
|
||||
public string $externalArticleNumber;
|
||||
public ?string $note;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @property mixed|null $name
|
||||
*/
|
||||
class WarehouseArticlePrice extends mfBaseModel
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
class WarehouseArticlePriceController extends TTCrud {
|
||||
protected string $headerTitle = 'Artikel Verkaufspreise';
|
||||
protected string $createText = 'Artikel Verkaufspreis erstellen';
|
||||
|
||||
// @formatter:off
|
||||
protected array $columns = [
|
||||
['key' => 'articleId', 'text' => 'Artikel', 'required' => true],
|
||||
['key' => 'articlePriceTypeId', 'text' => 'Preistyp', 'required' => true],
|
||||
['key' => 'priceMultiplier', 'text' => 'Preismultiplikator', 'required' => false],
|
||||
['key' => 'priceOverride', 'text' => 'Preisüberschreibung', 'required' => false]
|
||||
];
|
||||
// @formatter:on
|
||||
|
||||
protected array $infoMessages = ['create' => 'Artikel Verkaufspreis wurde erstellt',
|
||||
'update' => 'Artikel Verkaufspreis wurde aktualisiert',
|
||||
'delete' => 'Artikel Verkaufspreis wurde gelöscht',
|
||||
'noChanges' => 'Keine Änderungen'];
|
||||
|
||||
protected function beforeCreate($postData): bool {
|
||||
return $this->validate($postData);
|
||||
}
|
||||
|
||||
protected function validate($postData): bool {
|
||||
// check if postData priceOverride or priceMultiplier is set but only one of them
|
||||
if (isset($postData['priceOverride']) && isset($postData['priceMultiplier'])) {
|
||||
self::returnJson(['success' => false,
|
||||
'message' => 'Entweder Preismultiplikator oder Preisüberschreibung kann gesetzt werden.']);
|
||||
return false;
|
||||
} else if (!isset($postData['priceOverride']) && !isset($postData['priceMultiplier'])) {
|
||||
self::returnJson(['success' => false,
|
||||
'message' => 'Entweder Preismultiplikator oder Preisüberschreibung muss gesetzt werden.']);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (isset($postData['id'])) {
|
||||
$count = WarehouseArticlePriceModel::count(['articleId' => $postData['articleId'],
|
||||
'articlePriceTypeId' => $postData['articlePriceTypeId'],
|
||||
'id' => $postData['id']]);
|
||||
if ($count > 0) return true;
|
||||
} else {
|
||||
$count = WarehouseArticlePriceModel::count(['articleId' => $postData['articleId'],
|
||||
'articlePriceTypeId' => $postData['articlePriceTypeId']]);
|
||||
if ($count > 0) {
|
||||
self::returnJson(['success' => false,
|
||||
'message' => 'Es existiert bereits ein Preis für diesen Artikel und diesen Preistyp.']);
|
||||
return false;
|
||||
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function beforeUpdate($postData): bool {
|
||||
$existing = $this->validate($postData);
|
||||
|
||||
if (!$existing) {
|
||||
return false;
|
||||
}
|
||||
|
||||
(new WarehouseHistoryController)->create($postData, $this->mod);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function getHistoryAction() {
|
||||
$history = WarehouseHistoryModel::getByRowId($this->request->id, 'WarehouseArticleDistributor');
|
||||
|
||||
$history = array_map(function ($item) {
|
||||
$item = (array) $item;
|
||||
|
||||
$item['columnHeader'] = $this->columns[array_search($item['key'], array_column($this->columns, 'key'))]['text'];
|
||||
return $item;
|
||||
}, $history);
|
||||
|
||||
self::returnJson($history);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
class WarehouseArticlePriceModel extends TTCrudBaseModel {
|
||||
public int $id;
|
||||
public int $articleId;
|
||||
public int $articlePriceTypeId;
|
||||
public ?float $priceMultiplier;
|
||||
public ?float $priceOverride;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @property mixed|null $name
|
||||
*/
|
||||
class WarehouseArticlePriceType extends mfBaseModel
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
class WarehouseArticlePriceTypeController extends TTCrud {
|
||||
protected string $headerTitle = 'Artikel Verkaufspreise';
|
||||
protected string $createText = 'Artikel Verkaufspreis erstellen';
|
||||
|
||||
// @formatter:off
|
||||
protected array $columns = [
|
||||
['key' => 'title', 'text' => 'Titel', 'required' => true],
|
||||
['key' => 'actions', 'text' => 'Aktionen', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center', 'priority' => 10]],
|
||||
];
|
||||
// @formatter:on
|
||||
|
||||
protected array $infoMessages = ['create' => 'Artikel Verkaufspreis wurde erstellt',
|
||||
'update' => 'Artikel Verkaufspreis wurde aktualisiert',
|
||||
'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 beforeUpdate($postData): bool {
|
||||
$existing = $this->checkExistingDistributorEntry($postData);
|
||||
|
||||
if (!$existing) {
|
||||
return false;
|
||||
}
|
||||
|
||||
(new WarehouseHistoryController)->create($postData, $this->mod);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function getHistoryAction() {
|
||||
$history = WarehouseHistoryModel::getByRowId($this->request->id, $this->mod);
|
||||
|
||||
$history = array_map(function ($item) {
|
||||
$item = (array) $item;
|
||||
|
||||
$item['columnHeader'] = $this->columns[array_search($item['key'], array_column($this->columns, 'key'))]['text'];
|
||||
return $item;
|
||||
}, $history);
|
||||
|
||||
self::returnJson($history);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
|
||||
class WarehouseArticlePriceTypeModel extends TTCrudBaseModel {
|
||||
public int $id;
|
||||
public string $title;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @property mixed|null $name
|
||||
*/
|
||||
class WarehouseDistributor extends mfBaseModel
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
class WarehouseDistributorController extends TTCrud {
|
||||
protected string $headerTitle = 'Lieferant';
|
||||
protected string $createText = 'Lieferant erstellen';
|
||||
|
||||
// @formatter:off
|
||||
protected array $columns = [
|
||||
['key' => 'name', 'text' => 'Name', 'required' => true, 'table' => ['priority' => 11]],
|
||||
['key' => 'address', 'text' => 'Adresse', 'required' => true],
|
||||
['key' => 'plz', 'text' => 'PLZ', 'required' => true],
|
||||
['key' => 'city', 'text' => 'Stadt', 'required' => true],
|
||||
['key' => 'countryId', 'text' => 'Land', 'required' => true, 'modal' => ['type' => 'select', 'items' => []]],
|
||||
['key' => 'email', 'text' => 'E-Mail', 'required' => true],
|
||||
['key' => 'phone', 'text' => 'Telefon', 'required' => true],
|
||||
['key' => 'contactPerson', 'text' => 'Kontaktperson', 'required' => true],
|
||||
['key' => 'actions', 'text' => 'Aktionen', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center', 'priority' => 10]],
|
||||
];
|
||||
// @formatter:on
|
||||
|
||||
protected array $additionalActions = [['key' => 'openHistory', 'title' => 'Historie', 'class' => 'fas fa-history text-primary']];
|
||||
|
||||
protected array $infoMessages = ['create' => 'Lieferant wurde erstellt',
|
||||
'update' => 'Lieferant wurde aktualisiert',
|
||||
'delete' => 'Lieferant wurde gelöscht',
|
||||
'noChanges' => 'Keine Änderungen',];
|
||||
|
||||
public function prepareCrudConfig() {
|
||||
$countries = array_map(function ($country) {
|
||||
return ['value' => $country->id, 'text' => $country->name];
|
||||
}, CountryModel::getAll());
|
||||
|
||||
$this->columns[4]['modal']['items'] = $countries;
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
class WarehouseDistributorModel extends TTCrudBaseModel {
|
||||
public int $id;
|
||||
public string $name;
|
||||
public string $address;
|
||||
public string $plz;
|
||||
public string $city;
|
||||
public int $countryId;
|
||||
public string $email;
|
||||
public string $phone;
|
||||
public string $contactPerson;
|
||||
}
|
||||
9
application/WarehouseEShop/WarehouseEShop.php
Normal file
9
application/WarehouseEShop/WarehouseEShop.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @property mixed|null $name
|
||||
*/
|
||||
class WarehouseEShop extends mfBaseModel
|
||||
{
|
||||
|
||||
}
|
||||
41
application/WarehouseEShop/WarehouseEShopController.php
Normal file
41
application/WarehouseEShop/WarehouseEShopController.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
class WarehouseEShopController extends TTCrud {
|
||||
protected string $headerTitle = 'Energie Steiermark Shop';
|
||||
protected bool $createText = false;
|
||||
|
||||
protected array $columns = [
|
||||
['key' => 'title', 'text' => 'Titel'],
|
||||
['key' => 'category', 'text' => 'Kategorie'],
|
||||
['key' => 'amount', 'text' => 'Menge', 'table' => ['filter' => false,'sortable' => false,'class' => 'p-0 width-80']],
|
||||
['key' => 'add', 'text' => 'Hinzufügen', 'table' => ['filter' => false,'sortable' => false, 'class' => 'width-120 text-center']]
|
||||
];
|
||||
|
||||
protected array $infoMessages = [
|
||||
'create' => 'Not possible',
|
||||
'update' => 'Not possible',
|
||||
'delete' => 'Not possible',
|
||||
'noChanges' => 'Keine Änderungen',
|
||||
];
|
||||
|
||||
public function getAction() {
|
||||
$filter = $this->postData['filters'] ?? [];
|
||||
$order = $this->postData['order'] ?? ['key' => null, 'order' => 'ASC'];
|
||||
$page = $this->postData['pagination']['page'] ?? 1;
|
||||
$perPage = $this->postData['pagination']['per_page'] ?? 10;
|
||||
|
||||
$filter['isEShop'] = 1;
|
||||
|
||||
$rows = WarehouseArticleModel::getAll($filter, $perPage, ($page - 1) * $perPage, $order);
|
||||
$filteredAvailable = WarehouseArticleModel::count($filter);
|
||||
$totalRows = WarehouseArticleModel::count(['isEShop' => 1]);
|
||||
|
||||
self::returnJson(["rows" => $rows,
|
||||
"pagination" => ["page" => $page,
|
||||
"total_pages" => ceil($filteredAvailable / $perPage),
|
||||
"per_page" => $perPage,
|
||||
"filtered_available" => intval($filteredAvailable),
|
||||
"total_rows" => intval($totalRows)]]); }
|
||||
|
||||
|
||||
}
|
||||
7
application/WarehouseEShop/WarehouseEShopModel.php
Normal file
7
application/WarehouseEShop/WarehouseEShopModel.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
class WarehouseEShopModel extends TTCrudBaseModel {
|
||||
public int $id;
|
||||
public string $title;
|
||||
public int $assignedTo;
|
||||
}
|
||||
9
application/WarehouseEShopOrder/WarehouseEShopOrder.php
Normal file
9
application/WarehouseEShopOrder/WarehouseEShopOrder.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @property mixed|null $name
|
||||
*/
|
||||
class WarehouseEShopOrder extends mfBaseModel
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
class WarehouseEShopOrderController extends TTCrud {
|
||||
protected string $headerTitle = 'Energie Steiermark Bestellungen';
|
||||
protected bool $createText = false;
|
||||
|
||||
protected array $columns = [
|
||||
['key' => 'id', 'text' => 'ID', 'modal' => false],
|
||||
['key' => 'status', 'text' => 'Status', 'required' => true],
|
||||
['key' => 'deliveryMode', 'text' => 'Liefermodus', 'required' => true, 'modal' => ['type' => 'select', 'items' => [
|
||||
['value' => 'singleAddress', 'text' => 'Einzelne Adresse'],
|
||||
['value' => 'multipleAddresses', 'text' => 'Mehrere Adressen'],
|
||||
]]],
|
||||
['key' => 'deliveryAddressName', 'text' => 'Lieferadresse Name', 'required' => true, 'table' => false],
|
||||
['key' => 'deliveryAddressLine', 'text' => 'Lieferadresse', 'required' => true, 'required_length' => 4],
|
||||
['key' => 'deliveryAddressPLZ', 'text' => 'Lieferadresse PLZ', 'required' => true, 'regex' => '/^\d{4}$/', 'table' => false],
|
||||
['key' => 'deliveryAddressCity', 'text' => 'Lieferadresse Stadt', 'required' => true, 'required_length' => 3, 'table' => false],
|
||||
['key' => 'create', 'text' => 'Erstellt', 'required' => true, 'modal' => false, 'filter' => 'datetime'],
|
||||
['key' => 'createBy', 'text' => 'Erstellt von', 'required' => true,'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, sie erhalten in Kürze eine Bestätigungsmail',
|
||||
'update' => 'Lagerort wurde aktualisiert',
|
||||
'delete' => 'Lagerort wurde gelöscht',
|
||||
'noChanges' => 'Keine Änderungen',
|
||||
];
|
||||
|
||||
protected function prepareCrudConfig() {
|
||||
$users = array_map(function($user) {
|
||||
return ['value' => intval($user->id), 'text' => $user->name];
|
||||
}, UserModel::search(['employee' => true]));
|
||||
|
||||
$this->columns[8]['modal']['items'] = $users;
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected function createOrderAction() {
|
||||
//TODO: change this to beforeCreate and afterCreate
|
||||
$json = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$shoppingCart = $json['shoppingCart'];
|
||||
unset($json['shoppingCart']);
|
||||
|
||||
$json['status'] = 'new';
|
||||
$json['create'] = time();
|
||||
$json['createBy'] = $this->user->id;
|
||||
|
||||
Helper::validateArray($json, $this->getCheckArray());
|
||||
|
||||
$id = WarehouseEShopOrderModel::create([
|
||||
'status' => 'new',
|
||||
'deliveryMode' => $json['deliveryMode'],
|
||||
'deliveryAddressName' => $json['deliveryAddressName'],
|
||||
'deliveryAddressLine' => $json['deliveryAddressLine'],
|
||||
'deliveryAddressPLZ' => $json['deliveryAddressPLZ'],
|
||||
'deliveryAddressCity' => $json['deliveryAddressCity'],
|
||||
'create' => $json['create'],
|
||||
'createBy' => $json['createBy'],
|
||||
]);
|
||||
|
||||
// now create WarehouseEShopOrderItems for each item in the shopping cart
|
||||
foreach ($shoppingCart as $item) {
|
||||
WarehouseEShopOrderItemModel::create([
|
||||
'orderId' => $id,
|
||||
'articleId' => $item['itemId'],
|
||||
'quantity' => intval($item['amount']),
|
||||
]);
|
||||
}
|
||||
|
||||
self::returnJson(['success' => true,
|
||||
'message' => $this->infoMessages['create'],
|
||||
'id' => $id]);
|
||||
|
||||
$json['id'] = $id;
|
||||
die(json_encode($json));
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
}
|
||||
24
application/WarehouseEShopOrder/WarehouseEShopOrderModel.php
Normal file
24
application/WarehouseEShopOrder/WarehouseEShopOrderModel.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
class WarehouseEShopOrderModel extends TTCrudBaseModel {
|
||||
public int $id;
|
||||
public string $status;
|
||||
public string $deliveryMode;
|
||||
public string $deliveryAddressName;
|
||||
public string $deliveryAddressLine;
|
||||
public string $deliveryAddressPLZ;
|
||||
public string $deliveryAddressCity;
|
||||
public int $create;
|
||||
public int $createBy;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @property mixed|null $name
|
||||
*/
|
||||
class WarehouseEShopOrderItem extends mfBaseModel
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $orderId
|
||||
* @property int $articleId
|
||||
* @property int $quantity
|
||||
*/
|
||||
|
||||
class WarehouseEShopOrderItemModel extends TTCrudBaseModel {
|
||||
public int $id;
|
||||
public int $orderId;
|
||||
public int $articleId;
|
||||
public int $quantity;
|
||||
}
|
||||
9
application/WarehouseHistory/WarehouseHistory.php
Normal file
9
application/WarehouseHistory/WarehouseHistory.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @property mixed|null $name
|
||||
*/
|
||||
class WarehouseHistory extends mfBaseModel
|
||||
{
|
||||
|
||||
}
|
||||
52
application/WarehouseHistory/WarehouseHistoryController.php
Normal file
52
application/WarehouseHistory/WarehouseHistoryController.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
class WarehouseHistoryController {
|
||||
|
||||
public function create($postData, $mod) {
|
||||
$modelClass = $mod . 'Model';
|
||||
$modelClass = new $modelClass();
|
||||
$currentData = $modelClass::get($postData['id']);
|
||||
$me = new User();
|
||||
$me->loadMe();
|
||||
|
||||
foreach (array_diff_assoc($postData, (array) $currentData) as $key => $value) {
|
||||
WarehouseHistoryModel::create(['table' => $mod,
|
||||
'row_id' => $postData['id'],
|
||||
'key' => $key,
|
||||
'old_value' => $currentData->$key,
|
||||
'new_value' => $value,
|
||||
'note' => '',
|
||||
'user_id' => $me->id,
|
||||
'create' => date('U')]);
|
||||
}
|
||||
}
|
||||
|
||||
public function getHistory($id, string $mod, $columns): array {
|
||||
$history = WarehouseHistoryModel::getByRowId($id, $mod);
|
||||
|
||||
return array_map(function ($item) use ($columns) {
|
||||
$item = (array) $item;
|
||||
|
||||
if (isset($this->columns[array_search($item['key'], array_column($columns, 'key'))]['modal']['type'])) {
|
||||
|
||||
if($this->columns[array_search($item['key'], array_column($columns, 'key'))]['modal']['type'] === 'checkbox') {
|
||||
$item['old_value'] = $item['old_value'] === '1' ? 'Ja' : 'Nein';
|
||||
$item['new_value'] = $item['new_value'] === '1' ? 'Ja' : 'Nein';
|
||||
}
|
||||
|
||||
if($this->columns[array_search($item['key'], array_column($columns, 'key'))]['modal']['type'] === 'select') {
|
||||
$column = $this->columns[array_search($item['key'], array_column($columns, 'key'))];
|
||||
$item['old_value'] = $column['modal']['items'][array_search($item['old_value'], array_column($column['modal']['items'], 'value'))]['text'];
|
||||
$item['new_value'] = $column['modal']['items'][array_search($item['new_value'], array_column($column['modal']['items'], 'value'))]['text'];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
$item['columnHeader'] = $columns[array_search($item['key'], array_column($columns, 'key'))]['text'];
|
||||
return $item;
|
||||
}, $history);
|
||||
}
|
||||
|
||||
}
|
||||
65
application/WarehouseHistory/WarehouseHistoryModel.php
Normal file
65
application/WarehouseHistory/WarehouseHistoryModel.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
//TODO: maybe convert this to use with TTCrudBaseModel
|
||||
class WarehouseHistoryModel {
|
||||
public int $id;
|
||||
public string $table;
|
||||
public int $row_id;
|
||||
public string $key;
|
||||
public string $old_value;
|
||||
public string $new_value;
|
||||
public string $note;
|
||||
public int $user_id;
|
||||
public string $user_name;
|
||||
public int $create;
|
||||
|
||||
public function __construct($data = []) {
|
||||
foreach ($data as $field => $value) {
|
||||
if (property_exists(get_called_class(), $field)) {
|
||||
$this->$field = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function create($data) {
|
||||
$FronkDB = FronkDB::singleton();
|
||||
$db = $FronkDB->link;
|
||||
$sql = /** @lang text */ "INSERT INTO `WarehouseHistory` (`table`, `row_id`, `key`, `old_value`, `new_value`, `note`, `user_id`, `create`) VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->execute([
|
||||
$data["table"],
|
||||
$data["row_id"],
|
||||
$data["key"],
|
||||
$data["old_value"],
|
||||
$data["new_value"],
|
||||
$data["note"],
|
||||
$data["user_id"],
|
||||
$data["create"]
|
||||
]);
|
||||
|
||||
return $stmt->insert_id;
|
||||
}
|
||||
/**
|
||||
* Retrieves an array of WarehouseHistoryModel objects by row ID.
|
||||
*
|
||||
* @param int $id The row ID.
|
||||
* @param string $table The table name.
|
||||
* @return WarehouseHistoryModel[] Array of WarehouseHistoryModel objects.
|
||||
*/
|
||||
public static function getByRowId(int $id, string $table): array {
|
||||
$db = FronkDB::singleton();
|
||||
$id = $db->escape($id);
|
||||
$sql = /** @lang text */ "SELECT WH.*, W.name as user_name FROM `WarehouseHistory` WH
|
||||
LEFT JOIN `Worker` W ON WH.user_id = W.id
|
||||
WHERE `row_id` = $id AND `table` = '$table' ORDER BY `create` DESC";
|
||||
$result = $db->query($sql);
|
||||
|
||||
$rows = [];
|
||||
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$rows[] = new WarehouseHistoryModel($row);
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
}
|
||||
9
application/WarehouseItem/WarehouseItem.php
Normal file
9
application/WarehouseItem/WarehouseItem.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @property mixed|null $name
|
||||
*/
|
||||
class WarehouseItem extends mfBaseModel
|
||||
{
|
||||
|
||||
}
|
||||
77
application/WarehouseItem/WarehouseItemController.php
Normal file
77
application/WarehouseItem/WarehouseItemController.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
class WarehouseItemController extends TTCrud {
|
||||
protected string $headerTitle = 'Eintrag';
|
||||
protected string $createText = 'Eintrag erstellen';
|
||||
|
||||
// TODO: change articleId and warehouseLocationId to autocomplete
|
||||
|
||||
// @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' => 'note', 'text' => 'Notiz', 'required' => false],
|
||||
['key' => 'actions', 'text' => 'Aktionen', 'table' => ['filter' => false], 'required' => false, 'modal' => false]
|
||||
];
|
||||
// @formatter:on
|
||||
|
||||
protected array $additionalActions = [
|
||||
['key' => 'openHistory', 'title' => 'Historie', 'class' => 'fas fa-history text-primary'],
|
||||
];
|
||||
|
||||
protected array $infoMessages = [
|
||||
'create' => 'Eintrag wurde erstellt',
|
||||
'update' => 'Eintrag wurde aktualisiert',
|
||||
'delete' => 'Eintrag wurde gelöscht',
|
||||
'noChanges' => 'Keine Änderungen',
|
||||
'alreadyExists' => 'Eintrag existiert bereits an diesem Lagerort.'
|
||||
];
|
||||
|
||||
protected function checkExistingItemOnLocation($postData): bool {
|
||||
$count = WarehouseItemModel::count(['articleId' => $postData['articleId'],
|
||||
'warehouseLocationId' => $postData['warehouseLocationId'],
|
||||
'serialNumber' => null]);
|
||||
|
||||
if ($count > 0) {
|
||||
self::returnJson(['success' => false, 'message' => $this->infoMessages['alreadyExists']]);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function beforeCreate($postData): bool {
|
||||
return $this->checkExistingItemOnLocation($postData);
|
||||
}
|
||||
|
||||
protected function beforeUpdate($postData): bool {
|
||||
$existing = $this->checkExistingItemOnLocation($postData);
|
||||
|
||||
if (!$existing) {
|
||||
return false;
|
||||
}
|
||||
|
||||
(new WarehouseHistoryController)->create($postData, $this->mod);
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
}
|
||||
10
application/WarehouseItem/WarehouseItemModel.php
Normal file
10
application/WarehouseItem/WarehouseItemModel.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
class WarehouseItemModel extends TTCrudBaseModel {
|
||||
public int $id;
|
||||
public int $articleId;
|
||||
public int $warehouseLocationId;
|
||||
public int $quantity;
|
||||
public ?string $serialNumber;
|
||||
public ?string $note;
|
||||
}
|
||||
9
application/WarehouseLocation/WarehouseLocation.php
Normal file
9
application/WarehouseLocation/WarehouseLocation.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @property mixed|null $name
|
||||
*/
|
||||
class WarehouseLocation extends mfBaseModel
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
class WarehouseLocationController extends TTCrud {
|
||||
protected string $headerTitle = 'Lagerorte';
|
||||
protected string $createText = 'Lagerort erstellen';
|
||||
|
||||
protected array $columns = [
|
||||
['key' => 'title', 'text' => 'Titel', 'required' => true],
|
||||
['key' => 'assignedTo', 'text' => 'Zugewiesen an', 'required' => true, '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' => 'Lagerort wurde erstellt',
|
||||
'update' => 'Lagerort wurde aktualisiert',
|
||||
'delete' => 'Lagerort wurde gelöscht',
|
||||
'noChanges' => 'Keine Änderungen',
|
||||
];
|
||||
|
||||
public function prepareCrudConfig() {
|
||||
$users = array_map(function($user) {
|
||||
return ['value' => $user->id, 'text' => $user->name];
|
||||
}, UserModel::search(['employee' => true]));
|
||||
|
||||
$this->columns[1]['modal']['items'] = $users;
|
||||
}
|
||||
|
||||
protected function beforeUpdate($postData): bool {
|
||||
(new WarehouseHistoryController)->create($postData, $this->mod);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function getHistoryAction() {
|
||||
$this->prepareCrudConfig();
|
||||
self::returnJson((new WarehouseHistoryController)->getHistory($this->request->id, $this->mod, $this->columns));
|
||||
}
|
||||
|
||||
}
|
||||
7
application/WarehouseLocation/WarehouseLocationModel.php
Normal file
7
application/WarehouseLocation/WarehouseLocationModel.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
class WarehouseLocationModel extends TTCrudBaseModel {
|
||||
public int $id;
|
||||
public string $title;
|
||||
public int $assignedTo;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @property mixed|null $name
|
||||
*/
|
||||
class WarehouseLocationThresholdOverride extends mfBaseModel
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
class WarehouseLocationThresholdOverrideController extends TTCrud {
|
||||
protected string $headerTitle = 'Lagerort Schwellenwert überschreiben';
|
||||
protected string $createText = 'Lieferanteintrag erstellen';
|
||||
|
||||
// @formatter:off
|
||||
protected array $columns = [
|
||||
['key' => 'locationId', 'text' => 'Lagerort', 'required' => true, 'modal' => ['type' => 'select', 'data' => 'WarehouseLocation']],
|
||||
['key' => 'articleId', 'text' => 'Artikel', 'required' => true, 'modal' => ['type' => 'select', 'data' => 'WarehouseArticle']],
|
||||
['key' => 'warningAmount', 'text' => 'Warnmenge', 'required' => true, 'modal' => ['type' => 'number']],
|
||||
['key' => 'criticalAmount', 'text' => 'Kritische Menge', 'required' => true, 'modal' => ['type' => 'number']],
|
||||
['key' => 'actions', 'text' => 'Aktionen', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center']]
|
||||
];
|
||||
// @formatter:on
|
||||
|
||||
protected array $infoMessages = ['create' => 'Lagerort Schwellenwert wurde erstellt',
|
||||
'update' => 'Lagerort Schwellenwert wurde aktualisiert',
|
||||
'delete' => 'Lagerort Schwellenwert 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 = WarehouseLocationThresholdOverrideModel::count(['locationId' => $postData['locationId'],
|
||||
'articleId' => $postData['articleId'],
|
||||
'id' => $postData['id']]);
|
||||
if ($count > 0) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
$count = WarehouseLocationThresholdOverrideModel::count(['locationId' => $postData['locationId'],
|
||||
'articleId' => $postData['articleId']]);
|
||||
if ($count > 0) {
|
||||
self::returnJson(['success' => false,
|
||||
'message' => 'Es existiert bereits ein Eintrag mit dieser Artikelnummer und diesem Lagerort.']);
|
||||
return false;
|
||||
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function beforeCreate($postData): bool {
|
||||
return $this->checkExistingDistributorEntry($postData);
|
||||
}
|
||||
|
||||
protected function beforeUpdate($postData): bool {
|
||||
$existing = $this->checkExistingDistributorEntry($postData);
|
||||
|
||||
if (!$existing) {
|
||||
return false;
|
||||
}
|
||||
|
||||
(new WarehouseHistoryController)->create($postData, $this->mod);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function getHistoryAction() {
|
||||
$history = WarehouseHistoryModel::getByRowId($this->request->id, 'WarehouseArticleDistributor');
|
||||
|
||||
$history = array_map(function ($item) {
|
||||
$item = (array) $item;
|
||||
|
||||
$item['columnHeader'] = $this->columns[array_search($item['key'], array_column($this->columns, 'key'))]['text'];
|
||||
return $item;
|
||||
}, $history);
|
||||
|
||||
self::returnJson($history);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
class WarehouseLocationThresholdOverrideModel extends TTCrudBaseModel {
|
||||
public int $id;
|
||||
public int $locationId;
|
||||
public int $articleId;
|
||||
public int $warningAmount;
|
||||
public int $criticalAmount;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @property mixed|null $name
|
||||
*/
|
||||
class WarehouseOrderRecommendation extends mfBaseModel
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
class WarehouseOrderRecommendationController extends TTCrud {
|
||||
protected string $headerTitle = 'Lagerbestellvorschläge';
|
||||
protected bool $createText = false;
|
||||
protected bool $onlyView = true;
|
||||
|
||||
protected array $columns = [
|
||||
['key' => 'articleTitle', 'text' => 'Artikel', 'required' => true],
|
||||
['key' => 'locationTitle', 'text' => 'Lagerort', 'required' => true, 'table' => ['filter' => false, 'sortable' => false]],
|
||||
['key' => 'count', 'text' => 'Anzahl', 'required' => true, 'table' => ['filter' => false, 'sortable' => false]],
|
||||
['key' => 'status', 'text' => 'Status', 'required' => true, 'table' => ['filter' => false, 'sortable' => false]],
|
||||
['key' => 'recommendation', 'text' => 'Empfehlung', 'required' => true, 'table' => ['filter' => false, 'sortable' => false]],
|
||||
// ['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' => 'Lagerort wurde erstellt',
|
||||
'update' => 'Lagerort wurde aktualisiert',
|
||||
'delete' => 'Lagerort wurde gelöscht',
|
||||
'noChanges' => 'Keine Änderungen',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class WarehouseOrderRecommendationModel
|
||||
*
|
||||
* This class represents a recommendation model for warehouse orders based on the critical and warning amounts of articles in the warehouse.
|
||||
*
|
||||
* @property int $articleId The ID of the article.
|
||||
* @property string $articleTitle The title of the article.
|
||||
* @property int $count The current count of items for the article.
|
||||
* @property string $status The status of the article's inventory ("Kritisch" or "Warnung").
|
||||
* @property string $recommendation The recommendation message for ordering.
|
||||
*/
|
||||
class WarehouseOrderRecommendationModel {
|
||||
public int $articleId;
|
||||
public ?int $locationId;
|
||||
public string $locationTitle;
|
||||
public string $articleTitle;
|
||||
public int $count;
|
||||
public string $status;
|
||||
public string $recommendation;
|
||||
|
||||
/**
|
||||
* WarehouseOrderRecommendationModel constructor.
|
||||
*
|
||||
* Initializes a new instance of the WarehouseOrderRecommendationModel class.
|
||||
*
|
||||
* @param array $data An associative array of property values to initialize the model.
|
||||
*/
|
||||
public function __construct(array $data = []) {
|
||||
foreach ($data as $field => $value) {
|
||||
if (property_exists(get_called_class(), $field)) {
|
||||
$this->$field = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves recommendations based on the provided filter.
|
||||
*
|
||||
* @param array $filter An associative array of filter options.
|
||||
* @return array An array of WarehouseOrderRecommendationModel instances.
|
||||
*/
|
||||
private static function customGet(array $filter = []): array {
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
$filterOptions = [];
|
||||
|
||||
// Convert filter to filterOptions because the filter is not directly usable
|
||||
if (isset($filter['articleTitle'])) {
|
||||
$filterOptions['title'] = $filter['articleTitle'];
|
||||
}
|
||||
|
||||
$articles = [];
|
||||
foreach (WarehouseArticleModel::getAll($filterOptions) as $article) {
|
||||
$articles[$article->id] = $article;
|
||||
}
|
||||
|
||||
$items = WarehouseItemModel::getAll();
|
||||
|
||||
$recommendations = [];
|
||||
// Check if the amount of items is below the critical or warning amount
|
||||
foreach ($articles as $article) {
|
||||
$items = array_filter($items, function ($item) use ($article) {
|
||||
return $item->articleId === $article->id;
|
||||
});
|
||||
|
||||
$count = array_reduce($items, function ($carry, $item) {
|
||||
return $carry + $item->quantity;
|
||||
}, 0);
|
||||
|
||||
if ($count <= $article->criticalAmount) {
|
||||
$minOrderAmount = $article->criticalAmount - $count;
|
||||
$recommendedOrderAmount = $article->warningAmount - $count;
|
||||
$recommendations[] = new WarehouseOrderRecommendationModel(["articleId" => $article->id,
|
||||
"articleTitle" => $article->title,
|
||||
"count" => $count,
|
||||
"status" => "Kritisch",
|
||||
"recommendation" => "Bestellen, min. $minOrderAmount, empfohlen: $recommendedOrderAmount"]);
|
||||
} else if ($count <= $article->warningAmount) {
|
||||
$minOrderAmount = $article->warningAmount - $count;
|
||||
$recommendations[] = new WarehouseOrderRecommendationModel(["articleId" => $article->id,
|
||||
"articleTitle" => $article->title,
|
||||
"count" => $count,
|
||||
"status" => "Warnung",
|
||||
"recommendation" => "Bestellen, min. $minOrderAmount"]);
|
||||
}
|
||||
}
|
||||
|
||||
$locations = [];
|
||||
foreach (WarehouseLocationModel::getAll() as $location) {
|
||||
$locations[$location->id] = $location;
|
||||
}
|
||||
|
||||
$locationThresholdOverrides = WarehouseLocationThresholdOverrideModel::getAll();
|
||||
|
||||
foreach ($locationThresholdOverrides as $locationThresholdOverride) {
|
||||
if (!isset($articles[$locationThresholdOverride->articleId])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$items = array_filter($items, function ($item) use ($locationThresholdOverride) {
|
||||
return $item->articleId === $locationThresholdOverride->articleId;
|
||||
});
|
||||
|
||||
$count = array_reduce($items, function ($carry, $item) {
|
||||
return $carry + $item->quantity;
|
||||
}, 0);
|
||||
|
||||
if ($count <= $locationThresholdOverride->criticalAmount) {
|
||||
$minOrderAmount = $locationThresholdOverride->criticalAmount - $count;
|
||||
$recommendedOrderAmount = $locationThresholdOverride->warningAmount - $count;
|
||||
$recommendations[] = new WarehouseOrderRecommendationModel(["articleId" => $locationThresholdOverride->articleId,
|
||||
"articleTitle" => $articles[$locationThresholdOverride->articleId]->title,
|
||||
"locationId" => $locationThresholdOverride->locationId,
|
||||
"locationTitle" => $locations[$locationThresholdOverride->locationId]->title,
|
||||
"count" => $count,
|
||||
"status" => "Kritisch",
|
||||
"recommendation" => "Bestellen, min. $minOrderAmount, empfohlen: $recommendedOrderAmount"]);
|
||||
} else if ($count <= $locationThresholdOverride->warningAmount) {
|
||||
$minOrderAmount = $locationThresholdOverride->warningAmount - $count;
|
||||
$recommendations[] = new WarehouseOrderRecommendationModel(["articleId" => $locationThresholdOverride->articleId,
|
||||
"articleTitle" => $articles[$locationThresholdOverride->articleId]->title,
|
||||
"locationId" => $locationThresholdOverride->locationId,
|
||||
"locationTitle" => $locations[$locationThresholdOverride->locationId]->title,
|
||||
"count" => $count,
|
||||
"status" => "Warnung",
|
||||
"recommendation" => "Bestellen, min. $minOrderAmount"]);
|
||||
}
|
||||
}
|
||||
|
||||
return $recommendations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the number of recommendations based on the provided filter.
|
||||
*
|
||||
* @param array $filter An associative array of filter options.
|
||||
* @return int The number of recommendations.
|
||||
*/
|
||||
public static function count(array $filter = []): int {
|
||||
return count(self::customGet($filter));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all recommendations based on the provided filter, limit, offset, and order.
|
||||
*
|
||||
* @param array $filter An associative array of filter options.
|
||||
* @param int|null $limit The maximum number of recommendations to return.
|
||||
* @param int $offset The number of recommendations to skip.
|
||||
* @param array $order An associative array containing the key to sort by and the order direction ('ASC' or 'DESC').
|
||||
* @return array An array of WarehouseOrderRecommendationModel instances.
|
||||
*/
|
||||
public static function getAll(array $filter = [], int $limit = null, int $offset = 0, array $order = ["key" => null]): array {
|
||||
$recommendations = self::customGet($filter);
|
||||
|
||||
// apply limit,offset and order
|
||||
if ($order['key'] !== null) {
|
||||
usort($recommendations, function ($a, $b) use ($order) {
|
||||
return $order['order'] === 'ASC' ? $a->{$order['key']} <=> $b->{$order['key']} : $b->{$order['key']} <=> $a->{$order['key']};
|
||||
});
|
||||
}
|
||||
|
||||
if ($limit !== null) {
|
||||
return array_slice($recommendations, $offset, $limit);
|
||||
}
|
||||
return $recommendations;
|
||||
}
|
||||
|
||||
}
|
||||
161
db/migrations/20240715161500_add_warehouse_tables.php
Normal file
161
db/migrations/20240715161500_add_warehouse_tables.php
Normal file
@@ -0,0 +1,161 @@
|
||||
<?php /** @noinspection ALL */
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class AddWarehouseTables extends AbstractMigration {
|
||||
public function up(): void {
|
||||
if ($this->getEnvironment() == "thetool") {
|
||||
|
||||
// WarehouseArticle Table
|
||||
$warehouseArticleTable = $this->table("WarehouseArticle", ["signed" => true]);
|
||||
$warehouseArticleTable->addColumn("title", "string", ["null" => false, "limit" => 255]);
|
||||
$warehouseArticleTable->addColumn("description", "text", ["null" => false]);
|
||||
$warehouseArticleTable->addColumn("category", "string", ["null" => false, "limit" => 255]);
|
||||
$warehouseArticleTable->addColumn("cheapestPurchasePrice", "float", ["null" => true]);
|
||||
$warehouseArticleTable->addColumn("cheapestSellPrice", "float", ["null" => true]);
|
||||
$warehouseArticleTable->addColumn("warningAmount", "integer", ["null" => false]);
|
||||
$warehouseArticleTable->addColumn("criticalAmount", "integer", ["null" => false]);
|
||||
$warehouseArticleTable->addColumn("isEShop", "integer", ["null" => false, "limit" => \Phinx\Db\Adapter\MysqlAdapter::INT_TINY]);
|
||||
$warehouseArticleTable->addIndex(["category"]);
|
||||
$warehouseArticleTable->addIndex(["isEShop"]);
|
||||
$warehouseArticleTable->save();
|
||||
// WarehouseArticle Table End
|
||||
|
||||
|
||||
// WarehouseArticleDistributor Table
|
||||
$warehouseArticleDistributorTable = $this->table("WarehouseArticleDistributor", ["signed" => true]);
|
||||
$warehouseArticleDistributorTable->addColumn("articleId", "integer", ["null" => false]);
|
||||
$warehouseArticleDistributorTable->addColumn("distributorId", "integer", ["null" => false]);
|
||||
$warehouseArticleDistributorTable->addColumn("purchasePrice", "float", ["null" => false]);
|
||||
$warehouseArticleDistributorTable->addColumn("externalArticleNumber", "string", ["null" => false, "limit" => 255]);
|
||||
$warehouseArticleDistributorTable->addColumn("note", "string", ["null" => true, "limit" => 255]);
|
||||
$warehouseArticleDistributorTable->addIndex(["articleId"]);
|
||||
$warehouseArticleDistributorTable->addIndex(["distributorId"]);
|
||||
$warehouseArticleDistributorTable->save();
|
||||
// WarehouseArticleDistributor Table End
|
||||
|
||||
|
||||
// WarehouseArticlePrice Table
|
||||
$warehouseArticlePriceTable = $this->table("WarehouseArticlePrice", ["signed" => true]);
|
||||
$warehouseArticlePriceTable->addColumn("articleId", "integer", ["null" => false]);
|
||||
$warehouseArticlePriceTable->addColumn("articlePriceTypeId", "integer", ["null" => false]);
|
||||
$warehouseArticlePriceTable->addColumn("priceMultiplier", "float", ["null" => true]);
|
||||
$warehouseArticlePriceTable->addColumn("priceOverride", "float", ["null" => true]);
|
||||
$warehouseArticlePriceTable->addIndex(["articleId"]);
|
||||
$warehouseArticlePriceTable->addIndex(["articlePriceTypeId"]);
|
||||
$warehouseArticlePriceTable->save();
|
||||
// WarehouseArticlePrice Table End
|
||||
|
||||
|
||||
// WarehouseArticlePriceType Table
|
||||
$warehouseArticlePriceTypeTable = $this->table("WarehouseArticlePriceType", ["signed" => true]);
|
||||
$warehouseArticlePriceTypeTable->addColumn("title", "string", ["null" => false, "limit" => 255]);
|
||||
$warehouseArticlePriceTypeTable->save();
|
||||
// WarehouseArticlePriceType Table End
|
||||
|
||||
|
||||
// WarehouseDistributor Table
|
||||
$warehouseDistributorTable = $this->table("WarehouseDistributor", ["signed" => true]);
|
||||
$warehouseDistributorTable->addColumn("name", "string", ["null" => false, "limit" => 255]);
|
||||
$warehouseDistributorTable->addColumn("address", "string", ["null" => false, "limit" => 255]);
|
||||
$warehouseDistributorTable->addColumn("plz", "string", ["null" => false, "limit" => 16]);
|
||||
$warehouseDistributorTable->addColumn("city", "string", ["null" => false, "limit" => 255]);
|
||||
$warehouseDistributorTable->addColumn("countryId", "integer", ["null" => false]);
|
||||
$warehouseDistributorTable->addColumn("email", "string", ["null" => false, "limit" => 255]);
|
||||
$warehouseDistributorTable->addColumn("phone", "string", ["null" => false, "limit" => 255]);
|
||||
$warehouseDistributorTable->addColumn("contactPerson", "string", ["null" => false, "limit" => 255]);
|
||||
$warehouseDistributorTable->save();
|
||||
// WarehouseDistributor Table End
|
||||
|
||||
|
||||
// WarehouseEShopOrder Table
|
||||
$warehouseEShopOrderTable = $this->table("WarehouseEShopOrder", ["signed" => true]);
|
||||
$warehouseEShopOrderTable->addColumn("status", "enum", ["values" => ["new", "accepted", "sent", "done"], "null" => false]);
|
||||
$warehouseEShopOrderTable->addColumn("deliveryMode", "enum", ["values" => ["singleAddress", "multipleAddresses"], "null" => false]);
|
||||
$warehouseEShopOrderTable->addColumn("deliveryAddressName", "string", ["null" => false, "limit" => 255]);
|
||||
$warehouseEShopOrderTable->addColumn("deliveryAddressLine", "string", ["null" => false, "limit" => 255]);
|
||||
$warehouseEShopOrderTable->addColumn("deliveryAddressPLZ", "string", ["null" => false, "limit" => 255]);
|
||||
$warehouseEShopOrderTable->addColumn("deliveryAddressCity", "string", ["null" => false, "limit" => 255]);
|
||||
$warehouseEShopOrderTable->addColumn("create", "integer", ["null" => false]);
|
||||
$warehouseEShopOrderTable->addColumn("createBy", "integer", ["null" => false]);
|
||||
$warehouseEShopOrderTable->save();
|
||||
// WarehouseEShopOrder Table End
|
||||
|
||||
|
||||
// WarehouseEShopOrderItem Table
|
||||
$warehouseEShopOrderItemTable = $this->table("WarehouseEShopOrderItem", ["signed" => true]);
|
||||
$warehouseEShopOrderItemTable->addColumn("orderId", "integer", ["null" => false]);
|
||||
$warehouseEShopOrderItemTable->addColumn("articleId", "integer", ["null" => false]);
|
||||
$warehouseEShopOrderItemTable->addColumn("quantity", "integer", ["null" => false]);
|
||||
$warehouseEShopOrderItemTable->addIndex(["orderId"]);
|
||||
$warehouseEShopOrderItemTable->save();
|
||||
// WarehouseEShopOrderItem Table End
|
||||
|
||||
|
||||
// WarehouseHistory Table
|
||||
$warehouseHistoryTable = $this->table("WarehouseHistory", ["signed" => true]);
|
||||
$warehouseHistoryTable->addColumn("table", "string", ["null" => false, "limit" => 255]);
|
||||
$warehouseHistoryTable->addColumn("row_id", "integer", ["null" => false]);
|
||||
$warehouseHistoryTable->addColumn("key", "string", ["null" => false, "limit" => 255]);
|
||||
$warehouseHistoryTable->addColumn("old_value", "string", ["null" => true, "limit" => 255]);
|
||||
$warehouseHistoryTable->addColumn("new_value", "string", ["null" => true, "limit" => 255]);
|
||||
$warehouseHistoryTable->addColumn("note", "string", ["null" => true, "limit" => 255]);
|
||||
$warehouseHistoryTable->addColumn("user_id", "integer", ["null" => false]);
|
||||
$warehouseHistoryTable->addColumn("create", "integer", ["null" => false]);
|
||||
$warehouseHistoryTable->addIndex(["table"]);
|
||||
$warehouseHistoryTable->addIndex(["row_id"]);
|
||||
$warehouseHistoryTable->save();
|
||||
// WarehouseHistory Table End
|
||||
|
||||
|
||||
// WarehouseItem Table
|
||||
$warehouseItemTable = $this->table("WarehouseItem", ["signed" => true]);
|
||||
$warehouseItemTable->addColumn("articleId", "integer", ["null" => false]);
|
||||
$warehouseItemTable->addColumn("warehouseLocationId", "integer", ["null" => false]);
|
||||
$warehouseItemTable->addColumn("quantity", "integer", ["null" => false]);
|
||||
$warehouseItemTable->addColumn("serialNumber", "string", ["null" => true, "limit" => 255]);
|
||||
$warehouseItemTable->addColumn("note", "string", ["null" => true, "limit" => 255]);
|
||||
$warehouseItemTable->addIndex(["articleId"]);
|
||||
$warehouseItemTable->addIndex(["warehouseLocationId"]);
|
||||
$warehouseItemTable->save();
|
||||
// WarehouseItem Table End
|
||||
|
||||
// WarehouseLocation Table
|
||||
$warehouseLocationTable = $this->table("WarehouseLocation", ["signed" => true]);
|
||||
$warehouseLocationTable->addColumn("title", "string", ["null" => false, "limit" => 255]);
|
||||
$warehouseLocationTable->addColumn("assignedTo", "integer", ["null" => false]);
|
||||
$warehouseLocationTable->addIndex(["assignedTo"]);
|
||||
$warehouseLocationTable->save();
|
||||
// WarehouseLocation Table End
|
||||
|
||||
|
||||
// WarehouseLocationThresholdOverride Table
|
||||
$warehouseLocationThresholdOverrideTable = $this->table("WarehouseLocationThresholdOverride", ["signed" => true]);
|
||||
$warehouseLocationThresholdOverrideTable->addColumn("locationId", "integer", ["null" => false]);
|
||||
$warehouseLocationThresholdOverrideTable->addColumn("articleId", "integer", ["null" => false]);
|
||||
$warehouseLocationThresholdOverrideTable->addColumn("warningAmount", "integer", ["null" => false]);
|
||||
$warehouseLocationThresholdOverrideTable->addColumn("criticalAmount", "integer", ["null" => false]);
|
||||
$warehouseLocationThresholdOverrideTable->addIndex(["locationId"]);
|
||||
$warehouseLocationThresholdOverrideTable->addIndex(["articleId"]);
|
||||
$warehouseLocationThresholdOverrideTable->save();
|
||||
// WarehouseLocationThresholdOverride Table End
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void {
|
||||
if ($this->getEnvironment() == "thetool") {
|
||||
$this->table("WarehouseArticle")->drop()->save();
|
||||
$this->table("WarehouseArticleDistributor")->drop()->save();
|
||||
$this->table("WarehouseArticlePrice")->drop()->save();
|
||||
$this->table("WarehouseArticlePriceType")->drop()->save();
|
||||
$this->table("WarehouseDistributor")->drop()->save();
|
||||
$this->table("WarehouseEShopOrder")->drop()->save();
|
||||
$this->table("WarehouseEShopOrderItem")->drop()->save();
|
||||
$this->table("WarehouseHistory")->drop()->save();
|
||||
$this->table("WarehouseItem")->drop()->save();
|
||||
$this->table("WarehouseLocation")->drop()->save();
|
||||
$this->table("WarehouseLocationThresholdOverride")->drop()->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,9 @@ class Helper {
|
||||
/**
|
||||
* Generate SQL Filter condition (space separated) for a given column.
|
||||
*
|
||||
* @param string|null $filterValue The filter value to match against.
|
||||
* @param string|null|array $filterValue The filter value to match against.
|
||||
* @param string $columnName The name of the column in the database table.
|
||||
* @return string The SQL condition generated based on the filter value and column name.
|
||||
* @noinspection PhpMissingParamTypeInspection
|
||||
*/
|
||||
public static function generateFilterCondition($filterValue, string $columnName, bool $exactMatch = false): string {
|
||||
$sql = "";
|
||||
@@ -19,6 +18,8 @@ class Helper {
|
||||
$sql = " AND `$columnName` >= " . $filterValue['from'];
|
||||
} elseif (isset($filterValue['to'])) {
|
||||
$sql = " AND `$columnName` <= " . $filterValue['to'];
|
||||
} else if (isset($filterValue['exact'])) {
|
||||
$sql = " AND `$columnName` = " . "'{$filterValue['exact']}'";
|
||||
}
|
||||
} else if ($filterValue === "0" || $filterValue === "1") {
|
||||
$sql .= " AND `$columnName` = " . $filterValue;
|
||||
@@ -37,4 +38,95 @@ class Helper {
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates an array of data based on a set of predefined rules.
|
||||
*
|
||||
* @param array $data The data to validate. Keys represent field names, and values are the corresponding data.
|
||||
* @param array $checkArray An associative array defining validation rules for each field:
|
||||
* - key: The field name to validate.
|
||||
* - value: An associative array of validation rules for that field:
|
||||
* - required (bool, optional): Whether the field is required. Default: false.
|
||||
* - title (string, optional): The human-readable name of the field to use in error messages.
|
||||
* - required_length (int, optional): The minimum required length of the value. Default: 1.
|
||||
* - regex (string, optional): A regular expression pattern the value must match.
|
||||
*
|
||||
* @return array|true Returns `true` if validation passes for all fields. Otherwise, returns an associative array of errors
|
||||
* where keys are field names, and values are error messages.
|
||||
*/
|
||||
public static function validateArray(array $data, array $checkArray, bool $printErrors = true) {
|
||||
$errors = [];
|
||||
|
||||
foreach ($checkArray as $key => $rules) {
|
||||
$value = $data[$key] ?? null;
|
||||
$title = $rules['title'] ?? $key;
|
||||
|
||||
// Apply default values for missing rules
|
||||
|
||||
$rules = array_merge([
|
||||
'required' => false,
|
||||
'required_length' => 1,
|
||||
'regex' => false,
|
||||
], $rules);
|
||||
|
||||
// Required Check
|
||||
if ($rules['required'] && (is_null($value) || $value === '')) {
|
||||
$errors[$key] = "$title wird benötigt.";
|
||||
}
|
||||
|
||||
// Length Check (only if value exists)
|
||||
if (!is_null($value) && strlen($value) < $rules['required_length']) {
|
||||
$errors[$key] = "$title muss mindestens $rules[required_length] Zeichen lang sein.";
|
||||
}
|
||||
|
||||
// Regex Check (only if value exists and regex is provided)
|
||||
if (!is_null($value) && $rules['regex'] && !preg_match($rules['regex'], $value)) {
|
||||
$errors[$key] = "$title hat ein ungültiges Format.";
|
||||
}
|
||||
}
|
||||
|
||||
if ($printErrors) {
|
||||
if (!empty($errors)) {
|
||||
header('Content-Type: application/json');
|
||||
die(json_encode(
|
||||
[
|
||||
'success' => false,
|
||||
'errors' => $errors
|
||||
]
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return empty($errors) ? true : $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays Vue component with the given header title.
|
||||
*
|
||||
* @param mfBaseController $controller The controller instance to generate $JSGlobals for.
|
||||
* @param string $pageName The name of the Vue component to render.
|
||||
* @param string $headerTitle The title to display in the header.
|
||||
* @param array $additionalGlobals Additional global variables to pass to the Vue component.
|
||||
*/
|
||||
public static function renderVue(mfBaseController $controller, string $pageName, string $headerTitle, array $additionalGlobals = []) {
|
||||
$JSGlobals = ["BASE_URL" => $controller::getUrl($pageName),
|
||||
"MF_URL" => $controller::getUrl(""),
|
||||
"DASHBOARD_URL" => $controller::getUrl("Dashboard"),
|
||||
"MF_APP_NAME" => MFAPPNAME_SLUG,
|
||||
"BASE_PATH" => $controller::getUrl(""),
|
||||
"PAGE_TITLE" => $headerTitle,
|
||||
"PATH" => [
|
||||
["text" => MFAPPNAME_SLUG, "href" => $controller::getUrl("Dashboard")],
|
||||
["text" => $headerTitle, "href" => $controller::getUrl($pageName)]
|
||||
],
|
||||
];
|
||||
|
||||
$JSGlobals = array_merge($JSGlobals, $additionalGlobals);
|
||||
|
||||
$controller->layout()->set("vueViewName", $pageName);
|
||||
$controller->layout()->set("JSGlobals", $JSGlobals);
|
||||
$controller->layout()->setTemplate("VueViews/Vue");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
178
lib/TTCrud/TTCrud.php
Normal file
178
lib/TTCrud/TTCrud.php
Normal file
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class TTCrud
|
||||
* @property string $headerTitle
|
||||
* @property string $createText
|
||||
* @property array $columns
|
||||
* @property array $additionalActions
|
||||
* @property array $infoMessages
|
||||
* @property bool $onlyView
|
||||
*/
|
||||
class TTCrud extends mfBaseController {
|
||||
public User $user;
|
||||
private array $checkArray;
|
||||
public ?array $postData;
|
||||
/** @noinspection PhpMissingFieldTypeInspection */
|
||||
public $model;
|
||||
|
||||
protected function init() {
|
||||
$this->needlogin = true;
|
||||
$me = new User();
|
||||
$me->loadMe();
|
||||
$this->user = $me;
|
||||
$this->layout()->set('me', $me);
|
||||
|
||||
if (!$me->is(["Admin"])) {
|
||||
$this->redirect("Dashboard");
|
||||
}
|
||||
|
||||
$modelName = $this->mod . 'Model';
|
||||
$this->model = new $modelName();
|
||||
$this->postData = json_decode(file_get_contents('php://input'), true);
|
||||
$this->checkArray = $this->getCheckArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the checkArray for the CRUD component.
|
||||
* @return array
|
||||
*/
|
||||
protected function getCheckArray(): array {
|
||||
$checkArray = [];
|
||||
|
||||
foreach ($this->columns as $column) {
|
||||
$checkArray[$column['key']] = ['required' => $column['required'] ?? false,
|
||||
'required_length' => $column['required_length'] ?? 0,
|
||||
'title' => $column['text'] ?? $column['key'],
|
||||
'regex' => $column['regex'] ?? false];
|
||||
}
|
||||
|
||||
return $checkArray;
|
||||
}
|
||||
|
||||
protected function indexAction() {
|
||||
$this->layout()->set('additionalJS', ['js/pages/WarehouseHistory/WarehouseHistoryModal.js']);
|
||||
$customJsFile = defined('BASEDIR') ? BASEDIR . "/public/js/pages/{$this->mod}/{$this->mod}.js" : null;
|
||||
|
||||
if ($customJsFile && file_exists($customJsFile)) {
|
||||
$pageName = $this->mod;
|
||||
} else {
|
||||
$pageName = "DefaultCrudView";
|
||||
}
|
||||
|
||||
Helper::renderVue($this, $pageName, $this->headerTitle, ["CRUD_CONFIG" => $this->getCrudConfig(),
|
||||
"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"),]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the configuration for the CRUD component for the Vue component.
|
||||
* @return array
|
||||
*/
|
||||
protected function getCrudConfig(): array {
|
||||
if (method_exists($this, 'prepareCrudConfig')) {
|
||||
$this->prepareCrudConfig();
|
||||
}
|
||||
|
||||
|
||||
$columns = array_map(function ($column) {
|
||||
$newColumn = $column;
|
||||
unset($newColumn['required'], $newColumn['required_length'], $newColumn['regex']);
|
||||
return $newColumn;
|
||||
}, $this->columns);
|
||||
|
||||
return ['key' => $this->mod,
|
||||
'tableHeader' => $this->headerTitle,
|
||||
'createText' => $this->createText,
|
||||
'columns' => $columns,
|
||||
'additionalActions' => $this->additionalActions];
|
||||
}
|
||||
|
||||
protected function getAction() {
|
||||
$filter = $this->postData['filters'] ?? [];
|
||||
$order = $this->postData['order'] ?? ['key' => null, 'order' => 'ASC'];
|
||||
$page = $this->postData['pagination']['page'] ?? 1;
|
||||
$perPage = $this->postData['pagination']['per_page'] ?? 10;
|
||||
|
||||
$rows = $this->model::getAll($filter, $perPage, ($page - 1) * $perPage, $order);
|
||||
$filteredAvailable = $this->model::count($filter);
|
||||
$totalRows = $this->model::count();
|
||||
|
||||
self::returnJson(["rows" => $rows,
|
||||
"pagination" => ["page" => $page,
|
||||
"total_pages" => ceil($filteredAvailable / $perPage),
|
||||
"per_page" => $perPage,
|
||||
"filtered_available" => intval($filteredAvailable),
|
||||
"total_rows" => intval($totalRows)]]);
|
||||
}
|
||||
|
||||
protected function createAction() {
|
||||
Helper::validateArray($this->postData, $this->checkArray);
|
||||
|
||||
if (method_exists($this, 'beforeCreate') && !$this->beforeCreate($this->postData)) {
|
||||
self::returnJson(['success' => false, 'message' => 'Ein Fehler ist aufgetreten.']);
|
||||
}
|
||||
|
||||
$id = $this->model::create($this->postData);
|
||||
|
||||
if (method_exists($this, 'afterCreate')) {
|
||||
$this->afterCreate($this->postData);
|
||||
}
|
||||
|
||||
self::returnJson(['success' => true,
|
||||
'message' => $this->infoMessages['create'],
|
||||
'id' => $id]);
|
||||
}
|
||||
|
||||
protected function updateAction() {
|
||||
Helper::validateArray($this->postData, array_merge($this->checkArray, ['id' => ['required' => true]]));
|
||||
|
||||
if (method_exists($this, 'beforeUpdate') && !$this->beforeUpdate($this->postData)) {
|
||||
self::returnJson(['success' => false, 'message' => 'Ein Fehler ist aufgetreten.']);
|
||||
}
|
||||
|
||||
$affectedRows = $this->model::update($this->postData);
|
||||
|
||||
if (method_exists($this, 'afterUpdate')) {
|
||||
$this->afterUpdate($this->postData);
|
||||
}
|
||||
|
||||
self::returnJson(['success' => $affectedRows > 0,
|
||||
'message' => $affectedRows > 0 ? $this->infoMessages['update'] : $this->infoMessages['noChanges']]);
|
||||
}
|
||||
|
||||
protected function deleteAction() {
|
||||
Helper::validateArray($this->postData, ['id' => ['required' => true]]);
|
||||
|
||||
if (method_exists($this, 'beforeDelete') && !$this->beforeDelete($this->postData)) {
|
||||
self::returnJson(['success' => false, 'message' => 'Ein Fehler ist aufgetreten.']);
|
||||
}
|
||||
|
||||
$affectedRows = $this->model::delete($this->postData['id']);
|
||||
|
||||
self::returnJson(['success' => $affectedRows > 0,
|
||||
'message' => $affectedRows > 0 ? $this->infoMessages['delete'] : $this->infoMessages['noChanges']]);
|
||||
}
|
||||
|
||||
protected function autocompleteAction() {
|
||||
$searchedID = $this->request->searchedID;
|
||||
|
||||
$textKey = property_exists($this->model, 'name') ? 'name' : 'title';
|
||||
|
||||
if (strlen($searchedID) > 0) {
|
||||
$filter = ['id' => $searchedID];
|
||||
} else {
|
||||
$filter = [$textKey => $this->request->q];
|
||||
}
|
||||
|
||||
$data = $this->model::getAll($filter, 10);
|
||||
|
||||
self::returnJson(array_map(function ($item) use ($textKey) {
|
||||
return ['value' => $item->id, 'text' => $item->$textKey];
|
||||
}, $data));
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
170
lib/TTCrudBaseModel/TTCrudBaseModel.php
Normal file
170
lib/TTCrudBaseModel/TTCrudBaseModel.php
Normal file
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
class TTCrudBaseModel {
|
||||
public function __construct($data = []) {
|
||||
foreach ($data as $field => $value) {
|
||||
if (property_exists(get_called_class(), $field)) {
|
||||
$this->$field = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function create($data) {
|
||||
$FronkDB = FronkDB::singleton();
|
||||
$db = $FronkDB->link;
|
||||
$table = self::getTable();
|
||||
self::checkAllFields($data, ['id']);
|
||||
|
||||
$sqlColumns = [];
|
||||
$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());
|
||||
}
|
||||
|
||||
$sqlValues[] = $value === null ? 'NULL' : "'" . $db->real_escape_string($value) . "'";
|
||||
$sqlColumns[] = "`$field`";
|
||||
}
|
||||
|
||||
$sql = "INSERT INTO `$table` (" . implode(", ", $sqlColumns) . ") VALUES (" . implode(", ", $sqlValues) . ")";
|
||||
$db->query($sql) or die($db->error);
|
||||
|
||||
return $db->insert_id;
|
||||
}
|
||||
|
||||
public static function getTable(): string {
|
||||
return str_replace('Model', '', get_called_class());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if all required fields of the current class are present in a given data array.
|
||||
*
|
||||
* This method uses reflection to determine which fields are required based on their type declarations.
|
||||
* It then iterates over these required fields and throws an exception if any of them are missing
|
||||
* from the provided data array.
|
||||
*
|
||||
* @param array $data The data array to check.
|
||||
* @param array $skip An optional array of field names to skip during the check.
|
||||
*
|
||||
* @return void
|
||||
* @throws Exception If any required field is missing in the `$data` array.
|
||||
*
|
||||
*/
|
||||
public static function checkAllFields(array $data, array $skip = []) {
|
||||
$requiredVars = array_filter(get_class_vars(get_called_class()), function ($value, $key) {
|
||||
$reflectionProperty = new ReflectionProperty(get_called_class(), $key);
|
||||
return !$reflectionProperty->hasType() || !$reflectionProperty->getType()->allowsNull();
|
||||
}, ARRAY_FILTER_USE_BOTH);
|
||||
|
||||
foreach ($requiredVars as $field => $value) {
|
||||
if (in_array($field, $skip)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($data[$field])) {
|
||||
throw new Exception("Required field $field is missing in data array");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static function get($id): TTCrudBaseModel {
|
||||
$FronkDB = FronkDB::singleton();
|
||||
$db = $FronkDB->link;
|
||||
$id = $db->real_escape_string($id);
|
||||
$table = self::getTable();
|
||||
$sql = "SELECT * FROM `$table` WHERE `id` = $id";
|
||||
$result = $db->query($sql);
|
||||
// as TTCRudBaseModel is abstract, we need to get the class name of the child class
|
||||
$class = get_called_class();
|
||||
return new $class($result->fetch_assoc());
|
||||
}
|
||||
|
||||
public static function count($filter = []): int {
|
||||
$FronkDB = FronkDB::singleton();
|
||||
$db = $FronkDB->link;
|
||||
$table = self::getTable();
|
||||
$filter = self::getSQLFilter($filter);
|
||||
$sql = "SELECT COUNT(*) as count FROM `$table` $filter";
|
||||
$result = $db->query($sql);
|
||||
|
||||
return $result->fetch_assoc()['count'];
|
||||
}
|
||||
|
||||
public static function getSQLFilter($filter): string {
|
||||
if (empty($filter)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
$sql = "WHERE 1=1";
|
||||
foreach ($filter as $key => $value) {
|
||||
if (!property_exists(get_called_class(), $key)) {
|
||||
throw new Exception("Field $key does not exist in " . get_called_class());
|
||||
}
|
||||
$sql .= Helper::generateFilterCondition($value, $key, gettype($value) === "integer");
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
public static function getAll($filter = [], $limit = null, $offset = 0, $order = ["key" => null]): array {
|
||||
$FronkDB = FronkDB::singleton();
|
||||
$db = $FronkDB->link;
|
||||
$table = self::getTable();
|
||||
$filter = self::getSQLFilter($filter);
|
||||
$sql = "SELECT * FROM `$table` $filter";
|
||||
|
||||
$sql .= $order['key'] === null ? " ORDER BY `id` ASC" : " ORDER BY `" . $order['key'] . "` " . $order['order'];
|
||||
$sql .= $limit === null ? "" : " LIMIT " . $limit . " OFFSET " . $offset;
|
||||
try {
|
||||
$result = $db->query($sql);
|
||||
} catch (Exception $e) {
|
||||
echo $sql;
|
||||
die($e->getMessage());
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
$class = get_called_class();
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$rows[] = new $class($row);
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
public static function update($data) {
|
||||
$FronkDB = FronkDB::singleton();
|
||||
$db = $FronkDB->link;
|
||||
$table = self::getTable();
|
||||
|
||||
// Check if all fields are set
|
||||
self::checkAllFields($data);
|
||||
|
||||
$values = [];
|
||||
foreach ($data as $field => $value) {
|
||||
if (!property_exists(get_called_class(), $field)) {
|
||||
throw new Exception("Field $field does not exist in " . get_called_class());
|
||||
}
|
||||
if ($field === "id") {
|
||||
continue;
|
||||
}
|
||||
|
||||
$values[] = $value === null ? "`$field` = NULL" : "`$field` = '" . $db->real_escape_string($value) . "'";
|
||||
}
|
||||
|
||||
$sql = "UPDATE `$table` SET " . implode(", ", $values) . " WHERE `id` = " . $db->real_escape_string($data['id']);
|
||||
$db->query($sql);
|
||||
return $db->affected_rows;
|
||||
}
|
||||
|
||||
public static function delete($id) {
|
||||
$FronkDB = FronkDB::singleton();
|
||||
$db = $FronkDB->link;
|
||||
$table = self::getTable();
|
||||
$id = $db->real_escape_string($id);
|
||||
$sql = "DELETE FROM `$table` WHERE `id` = $id";
|
||||
$db->query($sql);
|
||||
return $db->affected_rows;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -27,19 +27,23 @@ function combineAndMinifyJS($files) {
|
||||
|
||||
$jsFiles = [
|
||||
"plugins/axios/axios.min.js",
|
||||
"plugins/axios/axios.inject.js",
|
||||
"plugins/moment/moment.min.js",
|
||||
"plugins/daterangepicker/daterangepicker.js",
|
||||
"plugins/vue/" . (isset($_GET['VUE_DEBUG']) || $_SERVER['HTTP_HOST'] === "localhost" ? "vue.js" : "vue.min.js"),
|
||||
"plugins/vue/tt-components/tt-card.js",
|
||||
"plugins/vue/tt-components/tt-table.js",
|
||||
"plugins/vue/tt-components/tt-table-crud.js",
|
||||
"plugins/vue/tt-components/tt-page-title.js",
|
||||
"plugins/vue/tt-components/tt-loader.js",
|
||||
"plugins/vue/tt-components/tt-select.js",
|
||||
"plugins/vue/tt-components/tt-datepicker.js",
|
||||
"plugins/vue/tt-components/tt-input.js",
|
||||
"plugins/vue/tt-components/tt-modal.js",
|
||||
"plugins/vue/tt-components/tt-autocomplete.js",
|
||||
"plugins/vue/tt-components/tt-icon-select.js",
|
||||
"plugins/vue/tt-components/tt-number-range.js",
|
||||
"plugins/vue/tt-components/tt-checkbox.js",
|
||||
];
|
||||
|
||||
|
||||
|
||||
13
public/js/pages/DefaultCrudView/DefaultCrudView.js
Normal file
13
public/js/pages/DefaultCrudView/DefaultCrudView.js
Normal file
@@ -0,0 +1,13 @@
|
||||
Vue.component('default-crud-view', {
|
||||
//language=Vue
|
||||
template: `
|
||||
<tt-card>
|
||||
<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,
|
||||
}
|
||||
},
|
||||
})
|
||||
287
public/js/pages/WarehouseArticle/WarehouseArticle.js
Normal file
287
public/js/pages/WarehouseArticle/WarehouseArticle.js
Normal file
@@ -0,0 +1,287 @@
|
||||
Vue.component('warehouse-distributor-modal', {
|
||||
//language=Vue
|
||||
template: `
|
||||
<tt-modal :show.sync="showModal" title="Lieferanten" :delete="false" :save="false"
|
||||
@close="$emit('update:show', false)">
|
||||
<div v-for="(row, index) in rows" :key="index" style="display:grid;grid-template-columns: auto auto">
|
||||
<div style="display:grid; grid-template-columns: auto auto auto">
|
||||
<tt-autocomplete v-model="row.distributorId"
|
||||
:api-url="window['TT_CONFIG']['BASE_PATH'] + '/WarehouseDistributor/autocomplete'"></tt-autocomplete>
|
||||
<tt-input v-model="row.purchasePrice" placeholder="Price" type="number" sm></tt-input>
|
||||
<tt-input v-model="row.externalArticleNumber" placeholder="External Article Number" sm></tt-input>
|
||||
</div>
|
||||
<div class="btn-group mb-4">
|
||||
<a class="btn btn-sm btn-primary" href="#" @click="saveRow(index)" title="Save Row">
|
||||
<i class="fas fa-check"></i>
|
||||
</a>
|
||||
<a class="btn btn-sm btn-danger" href="#" @click="deleteRow(index)" title="Delete Row">
|
||||
<i class="fas fa-trash"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="#" @click="addRow"><i class="fas fa-plus"></i>Lieferant hinzufügen</a>
|
||||
|
||||
</tt-modal>
|
||||
`, props: {
|
||||
show: {type: Boolean, default: false}, id: {type: Number, default: null},
|
||||
}, data() {
|
||||
return {
|
||||
window: window, showModal: false, rows: []
|
||||
};
|
||||
}, async mounted() {
|
||||
|
||||
}, methods: {
|
||||
async fetchArticleDistributor() {
|
||||
const response = await axios.post(window['TT_CONFIG']['BASE_PATH'] + '/WarehouseArticleDistributor/get', {filters: {articleId: this.id}});
|
||||
|
||||
this.rows = response.data.rows
|
||||
}, addRow() {
|
||||
this.rows.push({distributor: null, price: null, externalArticleNumber: null});
|
||||
}, async saveRow(index) {
|
||||
// post to /WarehouseArticleDistributor/save with rows data and articleId
|
||||
const row = this.rows[index];
|
||||
|
||||
const data = {
|
||||
articleId: this.id, distributorId: row.distributorId, purchasePrice: row.purchasePrice, externalArticleNumber: row.externalArticleNumber
|
||||
}
|
||||
|
||||
if (row.id) {
|
||||
data.id = row.id;
|
||||
}
|
||||
|
||||
const response = await axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticleDistributor/${row.id ? 'update' : 'create'}`, data);
|
||||
this.$emit('doUpdate');
|
||||
this.window.notify(response.data.success ? 'success' : 'error',
|
||||
response.data.errors ? Object.values(response.data.errors).join('<br>') : response.data.message);
|
||||
await this.fetchArticleDistributor();
|
||||
}, async deleteRow(index) {
|
||||
const row = this.rows[index];
|
||||
|
||||
if (!row.id) {
|
||||
this.window.notify('error', 'Eintrag hat keine ID');
|
||||
}
|
||||
|
||||
const response = axios.post(window['TT_CONFIG']['BASE_PATH'] + '/WarehouseArticleDistributor/delete', {id: row.id});
|
||||
this.$emit('doUpdate');
|
||||
this.window.notify(response.data.success ? 'success' : 'error',
|
||||
response.data.errors ? Object.values(response.data.errors).join('<br>') : response.data.message);
|
||||
await this.fetchArticleDistributor();
|
||||
}
|
||||
}, watch: {
|
||||
show(newVal) {
|
||||
this.showModal = newVal;
|
||||
if (!newVal) {
|
||||
this.rows = [];
|
||||
return;
|
||||
}
|
||||
this.rows = [];
|
||||
this.fetchArticleDistributor().then();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Vue.component('warehouse-threshold-modal', {
|
||||
//language=Vue
|
||||
template: `
|
||||
<tt-modal :show.sync="showModal" title="Schwellenwerte" :delete="false" :save="false" @close="$emit('update:show', false)">
|
||||
<div v-for="(row, index) in rows" :key="index" style="display:grid;grid-template-columns: auto auto">
|
||||
<div style="display:grid; grid-template-columns: auto auto auto">
|
||||
<tt-autocomplete v-model="row.locationId"
|
||||
:api-url="window['TT_CONFIG']['BASE_PATH'] + '/WarehouseLocation/autocomplete'"></tt-autocomplete>
|
||||
<tt-input v-model="row.warningAmount" placeholder="Warnmenge" type="number" sm></tt-input>
|
||||
<tt-input v-model="row.criticalAmount" placeholder="Kritische Menge" type="number" sm></tt-input>
|
||||
</div>
|
||||
<div class="btn-group mb-4">
|
||||
<a class="btn btn-sm btn-primary" href="#" @click="saveRow(index)" title="Save Row">
|
||||
<i class="fas fa-check"></i>
|
||||
</a>
|
||||
<a class="btn btn-sm btn-danger" href="#" @click="deleteRow(index)" title="Delete Row">
|
||||
<i class="fas fa-trash"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="#" @click="addRow"><i class="fas fa-plus"></i>Schwellenwert hinzufügen</a>
|
||||
|
||||
</tt-modal>
|
||||
`, props: {
|
||||
show: {type: Boolean, default: false}, id: {type: Number, default: null},
|
||||
}, data() {
|
||||
return {
|
||||
window: window, showModal: false, rows: []
|
||||
};
|
||||
}, async mounted() {
|
||||
|
||||
}, methods: {
|
||||
async fetchLocationThreshold() {
|
||||
const response = await axios.post(window['TT_CONFIG']['BASE_PATH'] + '/WarehouseLocationThresholdOverride/get', {filters: {articleId: this.id}});
|
||||
|
||||
this.rows = response.data.rows
|
||||
}, addRow() {
|
||||
this.rows.push({distributor: null, price: null, externalArticleNumber: null});
|
||||
}, async saveRow(index) {
|
||||
// post to /WarehouseArticleDistributor/save with rows data and articleId
|
||||
const row = this.rows[index];
|
||||
|
||||
const data = {
|
||||
articleId: this.id, locationId: row.locationId, warningAmount: row.warningAmount, criticalAmount: row.criticalAmount
|
||||
}
|
||||
|
||||
if (row.id) {
|
||||
data.id = row.id;
|
||||
}
|
||||
|
||||
const response = await axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseLocationThresholdOverride/${row.id ? 'update' : 'create'}`, data);
|
||||
this.$emit('doUpdate');
|
||||
this.window.notify(response.data.success ? 'success' : 'error',
|
||||
response.data.errors ? Object.values(response.data.errors).join('<br>') : response.data.message);
|
||||
await this.fetchLocationThreshold()
|
||||
}, async deleteRow(index) {
|
||||
const row = this.rows[index];
|
||||
|
||||
if (!row.id) {
|
||||
this.window.notify('error', 'Eintrag hat keine ID');
|
||||
}
|
||||
|
||||
const response = axios.post(window['TT_CONFIG']['BASE_PATH'] + '/WarehouseLocationThresholdOverride/delete', {id: row.id});
|
||||
this.$emit('doUpdate');
|
||||
this.window.notify(response.data.success ? 'success' : 'error',
|
||||
response.data.errors ? Object.values(response.data.errors).join('<br>') : response.data.message);
|
||||
await this.fetchLocationThreshold();
|
||||
}
|
||||
}, watch: {
|
||||
show(newVal) {
|
||||
this.showModal = newVal;
|
||||
if (!newVal) {
|
||||
this.rows = [];
|
||||
return;
|
||||
}
|
||||
this.rows = [];
|
||||
this.fetchLocationThreshold().then();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Vue.component('warehouse-article-price-modal', {
|
||||
//language=Vue
|
||||
template: `
|
||||
<tt-modal :show.sync="showModal" title="Artikel-Preise" :delete="false" :save="false" @close="$emit('update:show', false)">
|
||||
<div v-for="(row, index) in rows" :key="index" style="display:grid;grid-template-columns: auto auto">
|
||||
<div style="display:grid; grid-template-columns: auto auto auto">
|
||||
<tt-autocomplete v-model="row.articlePriceTypeId"
|
||||
:api-url="window['TT_CONFIG']['BASE_PATH'] + '/WarehouseArticlePriceType/autocomplete'"></tt-autocomplete>
|
||||
<tt-input v-model="row.priceMultiplier" placeholder="Preis Multiplikator" type="number" sm></tt-input>
|
||||
<tt-input v-model="row.priceOverride" placeholder="Preis" type="number" sm></tt-input>
|
||||
</div>
|
||||
<div class="btn-group mb-4">
|
||||
<a class="btn btn-sm btn-primary" href="#" @click="saveRow(index)" title="Save Row">
|
||||
<i class="fas fa-check"></i>
|
||||
</a>
|
||||
<a class="btn btn-sm btn-danger" href="#" @click="deleteRow(index)" title="Delete Row">
|
||||
<i class="fas fa-trash"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="#" @click="addRow"><i class="fas fa-plus"></i>Preis hinzufügen</a>
|
||||
|
||||
</tt-modal>
|
||||
`, props: {
|
||||
show: {type: Boolean, default: false}, id: {type: Number, default: null},
|
||||
}, data() {
|
||||
return {
|
||||
window: window, showModal: false, rows: []
|
||||
};
|
||||
}, async mounted() {
|
||||
|
||||
}, methods: {
|
||||
async fetchLocationThreshold() {
|
||||
const response = await axios.post(window['TT_CONFIG']['BASE_PATH'] + '/WarehouseArticlePrice/get', {filters: {articleId: this.id}});
|
||||
|
||||
this.rows = response.data.rows
|
||||
}, addRow() {
|
||||
this.rows.push({articlePriceTypeId: null, priceMultiplier: null, priceOverride: null});
|
||||
}, async saveRow(index) {
|
||||
// post to /WarehouseArticleDistributor/save with rows data and articleId
|
||||
const row = this.rows[index];
|
||||
|
||||
const data = {
|
||||
articleId: this.id, articlePriceTypeId: row.articlePriceTypeId, priceMultiplier: row.priceMultiplier, priceOverride: row.priceOverride
|
||||
}
|
||||
|
||||
if (row.id) {
|
||||
data.id = row.id;
|
||||
}
|
||||
|
||||
const response = await axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticlePrice/${row.id ? 'update' : 'create'}`, data);
|
||||
this.$emit('doUpdate');
|
||||
this.window.notify(response.data.success ? 'success' : 'error',
|
||||
response.data.errors ? Object.values(response.data.errors).join('<br>') : response.data.message);
|
||||
await this.fetchLocationThreshold()
|
||||
}, async deleteRow(index) {
|
||||
const row = this.rows[index];
|
||||
|
||||
if (!row.id) {
|
||||
this.window.notify('error', 'Eintrag hat keine ID');
|
||||
}
|
||||
|
||||
const response = axios.post(window['TT_CONFIG']['BASE_PATH'] + '/WarehouseArticlePrice/delete', {id: row.id});
|
||||
this.$emit('doUpdate');
|
||||
this.window.notify(response.data.success ? 'success' : 'error',
|
||||
response.data.errors ? Object.values(response.data.errors).join('<br>') : response.data.message);
|
||||
await this.fetchLocationThreshold();
|
||||
}
|
||||
}, watch: {
|
||||
show(newVal) {
|
||||
this.showModal = newVal;
|
||||
if (!newVal) {
|
||||
this.rows = [];
|
||||
return;
|
||||
}
|
||||
this.rows = [];
|
||||
this.fetchLocationThreshold().then();
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
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">
|
||||
|
||||
<template v-slot:cheapestPurchasePrice="{ row }">
|
||||
<span>{{(row.cheapestPurchasePrice * row.sellPriceMultiplier).toFixed(2)}} €</span>
|
||||
</template>
|
||||
|
||||
</tt-table-crud>
|
||||
|
||||
<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"/>
|
||||
<warehouse-history-modal :show.sync="historyModal" :id="historyModalId"/>
|
||||
</tt-card>
|
||||
`, data() {
|
||||
return {
|
||||
window: window,
|
||||
historyModal: false,
|
||||
historyModalId: null,
|
||||
distributorModal: false,
|
||||
distributorModalId: null,
|
||||
thresholdModal: false,
|
||||
thresholdModalId: null,
|
||||
priceModal: false,
|
||||
priceModalId: null
|
||||
}
|
||||
}, methods: {
|
||||
refreshTable() {
|
||||
this.$refs.table.$refs.table.refreshTable();
|
||||
}
|
||||
}
|
||||
})
|
||||
13
public/js/pages/WarehouseDistributor/WarehouseDistributor.js
Normal file
13
public/js/pages/WarehouseDistributor/WarehouseDistributor.js
Normal file
@@ -0,0 +1,13 @@
|
||||
Vue.component('warehouse-distributor', {
|
||||
//language=Vue
|
||||
template: `
|
||||
<tt-card>
|
||||
<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,
|
||||
}
|
||||
},
|
||||
})
|
||||
130
public/js/pages/WarehouseEShop/WarehouseEShop.js
Normal file
130
public/js/pages/WarehouseEShop/WarehouseEShop.js
Normal file
@@ -0,0 +1,130 @@
|
||||
|
||||
|
||||
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 d-flex justify-content-between align-items-center">
|
||||
{{ item.title }}
|
||||
<span class="badge badge-primary badge-pill">{{ item.amount }}</span>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
<p v-else>Der Einkaufswagen ist leer.</p>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
});
|
||||
|
||||
|
||||
Vue.component('warehouse-e-shop', {
|
||||
//language=Vue
|
||||
template: `
|
||||
<tt-card>
|
||||
<tt-expandable-shopping-cart :cart-items="shoppingCart" @submitOrder="openOrderDialog"/>
|
||||
|
||||
<tt-modal :show.sync="createOrderDialog"
|
||||
:delete="false"
|
||||
title="Bestellung abschicken"
|
||||
@submit="submitOrder">
|
||||
|
||||
<tt-select v-model="createOrderDialogData.deliveryMode" label="Adresse" :options="[
|
||||
{text: 'Einzelne Adresse', value: 'singleAddress'},
|
||||
{text: 'Mehrere Adressen', value: 'multipleAddresses'},
|
||||
]" sm row/>
|
||||
<tt-input v-model="createOrderDialogData.deliveryAddressName" label="Name" sm row/>
|
||||
<tt-input v-model="createOrderDialogData.deliveryAddressLine" label="Straße" sm row/>
|
||||
<tt-input v-model="createOrderDialogData.deliveryAddressPLZ" label="PLZ" sm row/>
|
||||
<tt-input v-model="createOrderDialogData.deliveryAddressCity" label="Stadt" sm row/>
|
||||
|
||||
</tt-modal>
|
||||
|
||||
<tt-table-crud>
|
||||
|
||||
<template v-slot:amount="{ row }">
|
||||
<!-- this has no padding - add a full width full height tt-input with -->
|
||||
<tt-input type="number" style="width: 100%; height: 100%;margin:0 !important" v-model="itemAmounts[row.id]"/>
|
||||
</template>
|
||||
|
||||
<template v-slot:add="{ row }">
|
||||
<a style="cursor: pointer;" @click="addToCart(row)">
|
||||
<i class="fas fa-shopping-cart text-primary"></i>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
</tt-table-crud>
|
||||
</tt-card>
|
||||
`, data() {
|
||||
return {
|
||||
window: window, itemAmounts: {}, shoppingCart: [], createOrderDialog: false, createOrderDialogData: {
|
||||
deliveryMode: 'singleAddress',
|
||||
deliveryAddressName: '',
|
||||
deliveryAddressLine: '',
|
||||
deliveryAddressPLZ: '',
|
||||
deliveryAddressCity: '',
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async openOrderDialog() {
|
||||
this.createOrderDialog = true;
|
||||
},
|
||||
async submitOrder() {
|
||||
const response = await axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseEShopOrder/createOrder`, {
|
||||
shoppingCart: this.shoppingCart,
|
||||
deliveryMode: this.createOrderDialogData.deliveryMode,
|
||||
deliveryAddressName: this.createOrderDialogData.deliveryAddressName,
|
||||
deliveryAddressLine: this.createOrderDialogData.deliveryAddressLine,
|
||||
deliveryAddressPLZ: this.createOrderDialogData.deliveryAddressPLZ,
|
||||
deliveryAddressCity: this.createOrderDialogData.deliveryAddressCity,
|
||||
});
|
||||
if (response.data.success) {
|
||||
this.window.notify('success', response.data.message || 'Erfolgreich gespeichert');
|
||||
this.shoppingCart = [];
|
||||
this.createOrderDialog = false;
|
||||
} else {
|
||||
this.window.notify('error',
|
||||
response.data.errors ? Object.values(response.data.errors).join('<br>') : response.data.message ||
|
||||
'Ein Fehler ist aufgetreten');
|
||||
}
|
||||
},
|
||||
addToCart(row) {
|
||||
if (!this.itemAmounts[row.id] || this.itemAmounts[row.id] === 0) {
|
||||
window.notify('error', 'Bitte geben Sie eine Menge ein.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if Article is already in the shopping cart
|
||||
if (this.shoppingCart.some(item => item.itemId === row.id)) {
|
||||
window.notify('warning', `${row.title} ist bereits im Warenkorb.`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.shoppingCart.push({itemId: row.id, title: row.title, amount: this.itemAmounts[row.id]});
|
||||
window.notify('success', `${this.itemAmounts[row.id]}x ${row.title} wurde dem Warenkorb hinzugefügt.`);
|
||||
|
||||
}
|
||||
},
|
||||
})
|
||||
19
public/js/pages/WarehouseEShopOrder/WarehouseEShopOrder.js
Normal file
19
public/js/pages/WarehouseEShopOrder/WarehouseEShopOrder.js
Normal file
@@ -0,0 +1,19 @@
|
||||
Vue.component('warehouse-e-shop-order', {
|
||||
//language=Vue
|
||||
template: `
|
||||
<tt-card>
|
||||
<tt-table-crud @openHistory="historyModal = true; historyModalId = $event.id">
|
||||
|
||||
<template v-slot:create="{ row }">
|
||||
{{ new Date(row.create * 1000).toLocaleDateString() }}
|
||||
</template>
|
||||
|
||||
</tt-table-crud>
|
||||
<warehouse-history-modal :show.sync="historyModal" :id="historyModalId"/>
|
||||
</tt-card>
|
||||
`, data() {
|
||||
return {
|
||||
window: window, historyModal: false, historyModalId: null,
|
||||
}
|
||||
},
|
||||
})
|
||||
45
public/js/pages/WarehouseHistory/WarehouseHistoryModal.js
Normal file
45
public/js/pages/WarehouseHistory/WarehouseHistoryModal.js
Normal file
@@ -0,0 +1,45 @@
|
||||
Vue.component('warehouse-history-modal', {
|
||||
//language=Vue
|
||||
template: `
|
||||
<tt-modal :show.sync="showModal" title="History" :delete="false" :save="false"
|
||||
@close="$emit('update:show', false)"
|
||||
>
|
||||
<tt-table
|
||||
v-if="showModal"
|
||||
:fetch-url="window['TT_CONFIG']['BASE_URL'] + '/getHistory?id=' + id"
|
||||
:config="WarehouseHistoryTableConfig" small>
|
||||
|
||||
<template v-slot:create="{ row }">
|
||||
{{ window.moment(row.create * 1000).format('DD.MM.YYYY HH:mm:ss') }}
|
||||
</template>
|
||||
|
||||
</tt-table>
|
||||
</tt-modal>
|
||||
`,
|
||||
props: {
|
||||
show: { type: Boolean, default: false },
|
||||
id: { type: Number, default: null },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
window: window,
|
||||
showModal: false,
|
||||
WarehouseHistoryTableConfig: {
|
||||
key: 'WarehouseHistory',
|
||||
tableHeader: 'Lager Historie',
|
||||
headers: [
|
||||
{text: "Zeit", key: "create", filter: false, sortable: false},
|
||||
{text: "Feld", key: "columnHeader", filter: false, sortable: false},
|
||||
{text: "Alter Wert", key: "old_value", filter: false, sortable: false},
|
||||
{text: "Neuer Wert", key: "new_value", filter: false, sortable: false},
|
||||
{text: "Geändert von", key: "user_name", filter: false, sortable: false},
|
||||
],
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
show(newVal) {
|
||||
this.showModal = newVal
|
||||
}
|
||||
}
|
||||
})
|
||||
13
public/js/pages/WarehouseItem/WarehouseItem.js
Normal file
13
public/js/pages/WarehouseItem/WarehouseItem.js
Normal file
@@ -0,0 +1,13 @@
|
||||
Vue.component('warehouse-item', {
|
||||
//language=Vue
|
||||
template: `
|
||||
<tt-card>
|
||||
<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,
|
||||
}
|
||||
},
|
||||
})
|
||||
13
public/js/pages/WarehouseLocation/WarehouseLocation.js
Normal file
13
public/js/pages/WarehouseLocation/WarehouseLocation.js
Normal file
@@ -0,0 +1,13 @@
|
||||
Vue.component('warehouse-location', {
|
||||
//language=Vue
|
||||
template: `
|
||||
<tt-card>
|
||||
<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,
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,12 @@
|
||||
Vue.component('warehouse-order-recommendation', {
|
||||
//language=Vue
|
||||
template: `
|
||||
<tt-card>
|
||||
<tt-table-crud @openHistory="historyModal = true; historyModalId = $event.id"/>
|
||||
</tt-card>
|
||||
`, data() {
|
||||
return {
|
||||
window: window, historyModal: false, historyModalId: null,
|
||||
}
|
||||
},
|
||||
})
|
||||
6
public/plugins/axios/axios.inject.js
Normal file
6
public/plugins/axios/axios.inject.js
Normal file
@@ -0,0 +1,6 @@
|
||||
axios.interceptors.response.use(function (response) {
|
||||
return response;
|
||||
}, function (error) {
|
||||
window.notify('error', error.response.data.message || 'Ein Fehler ist aufgetreten')
|
||||
return Promise.reject(error);
|
||||
});
|
||||
@@ -2,6 +2,10 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tt-table {
|
||||
/*width: unset !important;*/
|
||||
}
|
||||
|
||||
.tt-table.loading tbody:after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -151,4 +155,175 @@ input[type=number]::-webkit-outer-spin-button {
|
||||
|
||||
td {
|
||||
vertical-align: middle !important;
|
||||
}
|
||||
}
|
||||
|
||||
/*TODO: export this classes somewhere else*/
|
||||
|
||||
.text-cyan {
|
||||
color: var(--cyan);
|
||||
}
|
||||
|
||||
.text-green {
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
.text-red {
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
.text-yellow {
|
||||
color: var(--yellow);
|
||||
}
|
||||
|
||||
.text-blue {
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
.text-purple {
|
||||
color: var(--purple);
|
||||
}
|
||||
|
||||
.text-orange {
|
||||
color: var(--orange);
|
||||
}
|
||||
|
||||
.text-pink {
|
||||
color: var(--pink);
|
||||
}
|
||||
|
||||
.text-gray {
|
||||
color: var(--gray);
|
||||
}
|
||||
|
||||
.text-white {
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.text-light {
|
||||
color: var(--light);
|
||||
}
|
||||
|
||||
.text-dark {
|
||||
color: var(--dark);
|
||||
}
|
||||
|
||||
.width-40 {
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.width-80 {
|
||||
width: 80px;
|
||||
}
|
||||
.width-120 {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.width-160 {
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
.width-200 {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.width-240 {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.width-280 {
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.width-320 {
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
.width-360 {
|
||||
width: 360px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*TODO: remove this*/
|
||||
/* Styles for the component */
|
||||
.tt-expandable-shopping-cart {
|
||||
position: fixed;
|
||||
bottom: 65px;
|
||||
right: 20px;
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 5px;
|
||||
width: 50px; /* Initial width */
|
||||
height: 50px;
|
||||
transition: width 0.3s ease, height 0.3s ease;
|
||||
}
|
||||
|
||||
.expanded {
|
||||
width: 500px; /* Expanded width */
|
||||
height: 600px; /* Expanded height */
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.toggle-button {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 24px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.cart-content {
|
||||
padding: 15px;
|
||||
display: none; /* Initially hidden */
|
||||
}
|
||||
|
||||
.expanded .cart-content {
|
||||
display: block; /* Show when expanded */
|
||||
}
|
||||
|
||||
.cart-count {
|
||||
background-color: #dc3545; /* Red background */
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
padding: 0 6px;
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
}
|
||||
|
||||
.tt-expandable-shopping-cart.expanded {
|
||||
border-top-left-radius: 0.25rem;
|
||||
border-top-right-radius: 0.25rem;
|
||||
|
||||
}
|
||||
|
||||
.tt-expandable-shopping-cart.expanded .cart-content {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.tt-expandable-shopping-cart.expanded .cart-content h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
|
||||
.tt-expandable-shopping-cart.expanded .toggle-button {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 3fr auto auto;
|
||||
grid-gap: 10px;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: var(--gray)
|
||||
}
|
||||
|
||||
.tt-expandable-shopping-cart.expanded .toggle-button i {
|
||||
justify-self: left;
|
||||
}
|
||||
|
||||
.tt-expandable-shopping-cart.expanded .cart-count {
|
||||
position: unset !important;
|
||||
width: 21px;
|
||||
height: 21px;
|
||||
grid-column-start: 3;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
Vue.component('tt-autocomplete', {
|
||||
template: `
|
||||
<div class="form-group">
|
||||
<div class="form-group" :class="{'row': row}">
|
||||
<slot name="prepend"></slot>
|
||||
<label v-if="label" :for="label">{{ label }}</label>
|
||||
<div class="autocomplete position-relative">
|
||||
<label :class="{'col-form-label': row, 'col-sm-4': row, 'col-form-label-sm': sm && row}"
|
||||
v-if="label" :for="label">{{ label }}</label>
|
||||
<div class="autocomplete position-relative" :class="{'col-sm-8 p-0': row}">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control form-control-sm"
|
||||
:id="label"
|
||||
class="form-control"
|
||||
:class="{'form-control-sm': sm}"
|
||||
v-model="displayValue"
|
||||
@input="onInput"
|
||||
@focus="onFocus"
|
||||
@@ -50,18 +53,12 @@ Vue.component('tt-autocomplete', {
|
||||
// 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,
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
}
|
||||
items: { type: Array, default: () => [] },
|
||||
sm: { type: Boolean, default: true },
|
||||
row: { type: Boolean, default: false },
|
||||
},
|
||||
async mounted() {
|
||||
if (this.value && this.apiUrl) {
|
||||
@@ -70,6 +67,8 @@ Vue.component('tt-autocomplete', {
|
||||
} else if (this.value) {
|
||||
const selectedItem = this.items.find(item => item.value === this.value);
|
||||
this.displayValue = selectedItem ? selectedItem.text : '';
|
||||
} else {
|
||||
this.displayValue = '';
|
||||
}
|
||||
|
||||
},
|
||||
@@ -86,6 +85,7 @@ Vue.component('tt-autocomplete', {
|
||||
watch: {
|
||||
value(newValue) {
|
||||
const selectedItem = this.displayingItems.find(item => item.value === newValue);
|
||||
console.log(selectedItem);
|
||||
this.displayValue = selectedItem ? selectedItem.text : '';
|
||||
},
|
||||
apiUrl() {
|
||||
@@ -95,7 +95,7 @@ Vue.component('tt-autocomplete', {
|
||||
methods: {
|
||||
onInput(event) {
|
||||
this.displayValue = event.target.value;
|
||||
this.$emit('input', undefined);
|
||||
this.$emit('input', '');
|
||||
this.fetchSuggestions();
|
||||
},
|
||||
onFocus() {
|
||||
|
||||
42
public/plugins/vue/tt-components/tt-checkbox.js
Normal file
42
public/plugins/vue/tt-components/tt-checkbox.js
Normal file
@@ -0,0 +1,42 @@
|
||||
Vue.component('tt-checkbox', {
|
||||
props: {
|
||||
label: String,
|
||||
required: Boolean,
|
||||
row: Boolean,
|
||||
value: [String, Number],
|
||||
hint: String,
|
||||
additionalProps: Object,
|
||||
sm: { type: Boolean, default: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
checkedValue: this.value,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
value(val) {
|
||||
this.checkedValue = val;
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="form-group" :class="{'row': row}">
|
||||
<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>
|
||||
|
||||
<input type="checkbox"
|
||||
:id="label"
|
||||
:name="label"
|
||||
class="form-control float-left"
|
||||
:class="{'form-control-sm': sm, 'col-sm-8': row}"
|
||||
:required="required"
|
||||
v-bind="additionalProps"
|
||||
v-model="checkedValue"
|
||||
@input="$emit('input', $event.target.checked ? 1 : 0)"
|
||||
>
|
||||
<small v-if="hint" class="form-text text-muted">{{ hint }}</small>
|
||||
</div>
|
||||
`
|
||||
});
|
||||
@@ -4,6 +4,7 @@ Vue.component('tt-input', {
|
||||
type: String,
|
||||
placeholder: String,
|
||||
required: Boolean,
|
||||
row: Boolean,
|
||||
value: [String, Number],
|
||||
hint: String,
|
||||
additionalProps: Object,
|
||||
@@ -20,15 +21,17 @@ Vue.component('tt-input', {
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="form-group">
|
||||
<div class="form-group" :class="{'row': row}">
|
||||
<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>
|
||||
<input :type="type"
|
||||
:id="label"
|
||||
:name="label"
|
||||
class="form-control"
|
||||
:class="{'form-control-sm': sm}"
|
||||
:class="{'form-control-sm': sm, 'col-sm-8': row}"
|
||||
:placeholder="placeholder"
|
||||
:required="required"
|
||||
v-bind="additionalProps"
|
||||
|
||||
47
public/plugins/vue/tt-components/tt-modal.js
Normal file
47
public/plugins/vue/tt-components/tt-modal.js
Normal file
@@ -0,0 +1,47 @@
|
||||
Vue.component('tt-modal', {
|
||||
props: {
|
||||
show: { type: Boolean, default: false },
|
||||
title: { type: String, default: 'Modal Title' },
|
||||
delete: { type: Boolean, default: true },
|
||||
save: { type: Boolean, default: true },
|
||||
},
|
||||
watch: {
|
||||
show(newVal) {
|
||||
if (!newVal) {
|
||||
this.$emit('close')
|
||||
}
|
||||
},
|
||||
},
|
||||
//language=Vue
|
||||
template: `
|
||||
<div class="modal show d-block"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
style="background: rgba(0, 0, 0, 0.5);"
|
||||
ref="modal"
|
||||
@mousedown="$emit('update:show', false)"
|
||||
@keydown.esc="$emit('update:show', false)"
|
||||
v-if="show">
|
||||
<div class="modal-dialog modal-lg" role="document" @mousedown.stop>
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{{title}}</h5>
|
||||
<button type="button" class="close" @click="$emit('update:show', false)">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<slot name="footer">
|
||||
<button v-if="save" class="btn btn-primary" @click="$emit('submit')">Save</button>
|
||||
<button v-if="$props.delete" class="btn btn-danger" @click="$emit('delete')">Delete</button>
|
||||
<button class="btn btn-secondary" @click="$emit('update:show', false)">Close</button>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
@@ -1,5 +1,13 @@
|
||||
Vue.component('tt-select', {
|
||||
props: ['options', 'label', 'required', 'value', 'suffix'],
|
||||
props: {
|
||||
options: {type: Array, required: true},
|
||||
label: {type: String, required: false},
|
||||
required: {type: Boolean, default: false},
|
||||
value: {type: [String, Number], required: false},
|
||||
suffix: {type: String, required: false},
|
||||
sm: {type: Boolean, default: false},
|
||||
row: {type: Boolean, default: false},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedOption: undefined,
|
||||
@@ -14,9 +22,13 @@ Vue.component('tt-select', {
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<div class="form-group">
|
||||
<label v-if="label" :for="label">{{ label }}</label>
|
||||
<select class="form-control form-control-sm" :required="required" v-model="selectedOption"
|
||||
<div class="form-group" :class="{'row': row}">
|
||||
<label v-if="label"
|
||||
:class="{'col-form-label': row, 'col-sm-4': row, 'col-form-label-sm': sm && row}"
|
||||
|
||||
:for="label">{{ label }}</label>
|
||||
<select class="form-control" :class="{'form-control-sm': sm, 'col-sm-8': row}"
|
||||
: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 }}
|
||||
|
||||
122
public/plugins/vue/tt-components/tt-table-crud.js
Normal file
122
public/plugins/vue/tt-components/tt-table-crud.js
Normal file
@@ -0,0 +1,122 @@
|
||||
Vue.component('tt-table-crud', {
|
||||
//language=Vue
|
||||
template: `
|
||||
<div>
|
||||
<tt-table :fetch-url="window['TT_CONFIG']['TABLE_URL']" :config="tableConfig"
|
||||
small ssr ref="table">
|
||||
<template v-slot:top-buttons>
|
||||
<button type="button" class="btn btn-primary" @click="openCrudModal" v-if="crudConfig.createText !== false">
|
||||
<i class="fas fa-plus"></i>
|
||||
{{crudConfig.createText || 'Erstellen'}}
|
||||
</button>
|
||||
|
||||
<slot name="table-top-buttons"></slot>
|
||||
</template>
|
||||
|
||||
<template v-for="column in crudConfig.columns"
|
||||
:slot="column.key.toLowerCase()"
|
||||
slot-scope="{row}">
|
||||
<slot v-if="$scopedSlots[column.key.toLowerCase()] && column.modal?.type !== 'select'"
|
||||
:name="column.key.toLowerCase()" :row="row">
|
||||
|
||||
</slot>
|
||||
<span v-else-if="column.modal?.type === 'select' && column.modal.items.find(item => item.value + '' === row[column.key] + '')">{{column.modal.items.find(item => item.value == row[column.key]).text}}</span>
|
||||
<span v-else-if="column.modal?.type === 'select'"
|
||||
:data-all-items="JSON.stringify(column.modal.items)"
|
||||
>Select not found for column {{column.key}} and value {{row[column.key]}}</span>
|
||||
</template>
|
||||
|
||||
<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'}">
|
||||
<a style="cursor: pointer;" @click="openCrudModal(row)"><i class="far fa-edit text-primary"
|
||||
title="Editieren"></i></a>
|
||||
|
||||
<!-- v-for action crudConfig.additionalActions -> v-if action.condition(row) || true , action.class for icon class, action.title for title, action.key for emitting event -->
|
||||
<a v-for="action in crudConfig.additionalActions"
|
||||
v-if="!action.condition || action.condition(row)"
|
||||
style="cursor: pointer;" @click="$emit(action.key, row)">
|
||||
<i :class="action.class" :title="action.title"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
</tt-table>
|
||||
|
||||
<tt-modal :show.sync="crudModal"
|
||||
:title="crudConfig.createText || 'Erstellen'"
|
||||
@submit="submitCrudModal"
|
||||
@delete="deleteCrudModal"
|
||||
@close="resetCrudModalData">
|
||||
|
||||
<template v-for="column in modalConfig.headers">
|
||||
<!-- @formatter:off -->
|
||||
<tt-input v-if="column.type === 'text'" v-model="crudModalData[column.key]" :label="column.text" sm row/>
|
||||
<tt-input v-if="column.type === 'number'" v-model="crudModalData[column.key]" :label="column.text" type="number" sm row/>
|
||||
<tt-textarea v-if="column.type === 'textarea'" v-model="crudModalData[column.key]" :label="column.text" sm row/>
|
||||
<tt-select v-if="column.type === 'select'" v-model="crudModalData[column.key]" :label="column.text" :options="column.items" sm row/>
|
||||
<tt-autocomplete v-if="column.type === 'autocomplete'" v-model="crudModalData[column.key]" :label="column.text" :items="column.items" sm row/>
|
||||
<tt-date-picker v-if="column.type === 'datepicker'" v-model="crudModalData[column.key]" :label="column.text" sm row/>
|
||||
<tt-icon-select v-if="column.type === 'icon-select'" v-model="crudModalData[column.key]" :label="column.text" sm row/>
|
||||
<tt-checkbox v-if="column.type === 'checkbox'" v-model="crudModalData[column.key]" :label="column.text" sm row/>
|
||||
<!-- @formatter:on -->
|
||||
</template>
|
||||
|
||||
</tt-modal>
|
||||
</div>
|
||||
`, props: {
|
||||
crudConfig: {type: Object, required: false, default: () => (window['TT_CONFIG']['CRUD_CONFIG'])}
|
||||
}, data() {
|
||||
return {
|
||||
crudModal: false, crudModalData: {}, window: window
|
||||
|
||||
}
|
||||
}, methods: {
|
||||
openCrudModal(row = null) {
|
||||
this.crudModalData = row ? JSON.parse(JSON.stringify(row)) : {};
|
||||
this.crudModal = true;
|
||||
}, resetCrudModalData() {
|
||||
this.crudModalData = {};
|
||||
this.crudModal = false;
|
||||
}, async submitCrudModal() {
|
||||
delete this.crudModalData.isTrusted;
|
||||
const response = await axios.post(this.crudModalData.id ? window['TT_CONFIG']['UPDATE_URL'] : window['TT_CONFIG']['CREATE_URL'],
|
||||
this.crudModalData);
|
||||
if (response.data.success) {
|
||||
this.$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 deleteCrudModal() {
|
||||
|
||||
const response = await axios.post(window['TT_CONFIG']['DELETE_URL'], {id: this.crudModalData.id});
|
||||
if (response.data.success) {
|
||||
this.$refs.table.refreshTable();
|
||||
this.resetCrudModalData();
|
||||
}
|
||||
this.window.notify(response.data.success ? 'success' : 'error', response.data.message);
|
||||
}
|
||||
}, computed: {
|
||||
tableConfig() {
|
||||
return {
|
||||
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}
|
||||
})
|
||||
}
|
||||
}, modalConfig() {
|
||||
return {
|
||||
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}
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -32,11 +32,11 @@
|
||||
|
||||
|
||||
Vue.component('tt-table-pagination', {
|
||||
props: {
|
||||
props: {
|
||||
pagination: {
|
||||
type: Object,
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({page: 1, per_page: 10, total_rows: 0, filtered_available: 0, total_pages: 1})
|
||||
default: () => ({page: 1, per_page: 10, total_rows: 0, filtered_available: 0, total_pages: 1})
|
||||
}, reverse: {type: Boolean, default: false}
|
||||
}, computed: {
|
||||
pagesToDisplay() {
|
||||
@@ -49,233 +49,247 @@ 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);
|
||||
const total = this.pagination.total_rows === this.pagination.filtered_available ? this.pagination.total_rows : `${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}`;
|
||||
}
|
||||
}, methods: {
|
||||
}, methods: {
|
||||
fetchRows(page) {
|
||||
this.$emit('fetch-rows', page);
|
||||
}
|
||||
}, template: `
|
||||
<div class="tt-table-pagination-container">
|
||||
<div v-if="pagination && typeof pagination.total_rows === 'number'" class="tt-table-pagination-wrapper">
|
||||
<div class="tt-table-pagination-container">
|
||||
<div v-if="pagination && typeof pagination.total_rows === 'number'"
|
||||
class="tt-table-pagination-wrapper">
|
||||
<span class="tt-table-text-center" v-text="pageInfoText"
|
||||
:style="{ 'grid-row': reverse ? 2 : 1, 'grid-column': 1 }"></span>
|
||||
|
||||
|
||||
<ul class="pagination tt-table-pagination" :style="{ 'grid-row': reverse ? 1 : 2, 'grid-column': 1 }">
|
||||
<li class="page-item tt-table-page-item" v-bind:class="{ disabled: pagination.page === 1 }">
|
||||
<a class="page-link" href="#" v-on:click.prevent="fetchRows(1)" aria-label="First">
|
||||
<span aria-hidden="true">«</span>
|
||||
<span class="sr-only">First</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item tt-table-page-item" v-for="pageNumber in pagesToDisplay"
|
||||
v-bind:class="{ 'active': pageNumber === pagination.page, 'disabled': pageNumber === pagination.page }">
|
||||
<a class="page-link"
|
||||
v-bind:class="{ 'active': pageNumber === pagination.page, 'disabled': pageNumber === pagination.page }"
|
||||
href="#"
|
||||
v-on:click.prevent="fetchRows(pageNumber)">{{ pageNumber }}</a>
|
||||
</li>
|
||||
<li class="page-item tt-table-page-item"
|
||||
v-bind:class="{ disabled: pagination.page === pagination.total_pages }">
|
||||
<a class="page-link" href="#" v-on:click.prevent="fetchRows(pagination.total_pages)"
|
||||
aria-label="Last">
|
||||
<span aria-hidden="true">»</span>
|
||||
<span class="sr-only">Last</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="pagination tt-table-pagination"
|
||||
:style="{ 'grid-row': reverse ? 1 : 2, 'grid-column': 1 }">
|
||||
<li class="page-item tt-table-page-item" v-bind:class="{ disabled: pagination.page === 1 }">
|
||||
<a class="page-link" href="#" v-on:click.prevent="fetchRows(1)" aria-label="First">
|
||||
<span aria-hidden="true">«</span>
|
||||
<span class="sr-only">First</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item tt-table-page-item" v-for="pageNumber in pagesToDisplay"
|
||||
v-bind:class="{ 'active': pageNumber === pagination.page, 'disabled': pageNumber === pagination.page }">
|
||||
<a class="page-link"
|
||||
v-bind:class="{ 'active': pageNumber === pagination.page, 'disabled': pageNumber === pagination.page }"
|
||||
href="#"
|
||||
v-on:click.prevent="fetchRows(pageNumber)">{{ pageNumber }}</a>
|
||||
</li>
|
||||
<li class="page-item tt-table-page-item"
|
||||
v-bind:class="{ disabled: pagination.page === pagination.total_pages }">
|
||||
<a class="page-link" href="#" v-on:click.prevent="fetchRows(pagination.total_pages)"
|
||||
aria-label="Last">
|
||||
<span aria-hidden="true">»</span>
|
||||
<span class="sr-only">Last</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
<span class="tt-table-text-center"
|
||||
:style="{ 'grid-row': reverse ? 2 : 1, 'grid-column': 2 }">Einträge pro Seite</span>
|
||||
<span class="tt-table-text-center"
|
||||
:style="{ 'grid-row': reverse ? 2 : 1, 'grid-column': 2 }">Einträge pro Seite</span>
|
||||
|
||||
|
||||
<select v-model="pagination.per_page" v-on:change="fetchRows(1)"
|
||||
class="form-control form-control-sm tt-table-select"
|
||||
:style="{ 'grid-row': reverse ? 1 : 2, 'grid-column': 2 }">
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
<select v-model="pagination.per_page" v-on:change="fetchRows(1)"
|
||||
class="form-control form-control-sm tt-table-select"
|
||||
:style="{ 'grid-row': reverse ? 1 : 2, 'grid-column': 2 }">
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
|
||||
|
||||
Vue.component('tt-table', {
|
||||
template: `
|
||||
<div class="tt-table-container" ref="tableContainer">
|
||||
<!-- Top Buttons -->
|
||||
<div class="tt-table-top-buttons-container">
|
||||
<slot name="top-buttons"></slot>
|
||||
</div>
|
||||
<!-- Pagination Controls -->
|
||||
<nav aria-label="Page navigation">
|
||||
<div class="tt-table-top-pagination-container">
|
||||
<!-- if excelExport is true, show the export button fontawesome icon excel -->
|
||||
<div style="display:flex;align-items: center;">
|
||||
<i v-if="!Object.values(columns).every(column => column.filter === false)" title="Filter zurücksetzen"
|
||||
@click="resetTable" class="fas fa-times cursor-pointer text-danger"
|
||||
style="font-size: 24px;margin-right: 6px;cursor: pointer; color: var(--orange)"></i>
|
||||
<i v-if="excelExport" title="EXCEL Export" @click="exportToExcel" class="fa fa-file-excel"
|
||||
style="font-size: 24px;margin-right: 6px;cursor: pointer; color: var(--success)"></i>
|
||||
<h4 style="margin: 0">{{ config.tableHeader }}</h4>
|
||||
</div>
|
||||
<div class="tt-table-container" ref="tableContainer">
|
||||
<!-- Top Buttons -->
|
||||
<div class="tt-table-top-buttons-container">
|
||||
<slot name="top-buttons"></slot>
|
||||
</div>
|
||||
<!-- Pagination Controls -->
|
||||
<nav aria-label="Page navigation">
|
||||
<div class="tt-table-top-pagination-container">
|
||||
<!-- if excelExport is true, show the export button fontawesome icon excel -->
|
||||
<div style="display:flex;align-items: center;">
|
||||
<i v-if="!Object.values(columns).every(column => column.filter === false)"
|
||||
title="Filter zurücksetzen"
|
||||
@click="resetTable" class="fas fa-times cursor-pointer text-danger"
|
||||
style="font-size: 24px;margin-right: 6px;cursor: pointer; color: var(--orange)"></i>
|
||||
<i v-if="excelExport" title="EXCEL Export" @click="exportToExcel" class="fa fa-file-excel"
|
||||
style="font-size: 24px;margin-right: 6px;cursor: pointer; color: var(--success)"></i>
|
||||
<h4 style="margin: 0">{{ config.tableHeader }}</h4>
|
||||
</div>
|
||||
|
||||
<tt-table-pagination :pagination="pagination" @fetch-rows="fetchRows"
|
||||
v-if="pagination"></tt-table-pagination>
|
||||
</div>
|
||||
</nav>
|
||||
<!-- Table -->
|
||||
<table
|
||||
ref="table"
|
||||
:class="['table','tt-table','table-condensed',{ 'loading': loading },{ 'table-striped': striped },{ 'table-bordered': bordered },{ 'table-hover': hover },{ 'table-sm': small }]">
|
||||
<thead style="border-width: 2px">
|
||||
<tr>
|
||||
<th scope="col" v-for="column in columns"
|
||||
:ref="'table_header_'+column.key"
|
||||
v-if="!hiddenColumns.includes(column.key)"
|
||||
:style="'vertical-align: top; text-align: center;' +
|
||||
<tt-table-pagination :pagination="pagination" @fetch-rows="fetchRows"
|
||||
v-if="pagination"></tt-table-pagination>
|
||||
</div>
|
||||
</nav>
|
||||
<!-- Table -->
|
||||
<table
|
||||
ref="table"
|
||||
:class="['table','tt-table','table-condensed',{ 'loading': loading },{ 'table-striped': striped },{ 'table-bordered': bordered },{ 'table-hover': hover },{ 'table-sm': small }]">
|
||||
<thead style="border-width: 2px">
|
||||
<tr>
|
||||
<th scope="col" v-for="column in columns"
|
||||
:ref="'table_header_'+column.key"
|
||||
v-if="!hiddenColumns.includes(column.key)"
|
||||
:style="'vertical-align: top; text-align: center;' +
|
||||
(column.filter === 'dateRange' ? 'min-width: 260px;' : '') +
|
||||
(originalColumnWidths[column.key] ? 'width: ' + originalColumnWidths[column.key] + 'px;' : '')"
|
||||
>
|
||||
<div style="text-align:center; white-space: nowrap;word-break: keep-all;user-select: none;"
|
||||
:style="{ 'cursor': column.sortable ? 'pointer' : 'default' }"
|
||||
@click="column.sortable ? setOrder(column.key) : undefined">
|
||||
{{ column.text }}
|
||||
<i
|
||||
v-if="column.sortable"
|
||||
:class="getSortIconClass(column.key)"></i>
|
||||
</div>
|
||||
<tt-input v-if="column.filter === 'search'" sm v-model="filters[column.key]"></tt-input>
|
||||
<tt-icon-select v-else-if="column.filter === 'iconSelect'" :options="column.filterOptions"
|
||||
v-model="filters[column.key]"></tt-icon-select>
|
||||
<tt-number-range v-else-if="column.filter === 'numberRange'" :returnText="!ssr"
|
||||
v-model="filters[column.key]"></tt-number-range>
|
||||
<tt-select v-else-if="column.filter === 'select'"
|
||||
:options="[{text: 'Alle', value: undefined}, ...column.filterOptions]"
|
||||
v-model="filters[column.key]"></tt-select>
|
||||
|
||||
<tt-autocomplete v-else-if="column.filter === 'autocomplete'"
|
||||
:items="[{text: 'Alle', value: undefined}, ...column.filterOptions]"
|
||||
v-model="filters[column.key]"></tt-autocomplete>
|
||||
<tt-date-picker v-else-if="column.filter === 'date'" v-model="filters[column.key]"></tt-date-picker>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="pagination?.filtered_available === 0" style="height: 150px">
|
||||
<td :colspan="Object.keys(columns).length" :rowspan="5" class="text-center">Keine Ergebnisse!</td>
|
||||
</tr>
|
||||
<tr v-else-if="(pagination === null && ssr === true) || rows === null"
|
||||
style="height: 150px">
|
||||
<td :colspan="Object.keys(columns).length" class="text-center">Laden...</td>
|
||||
</tr>
|
||||
<template v-for="(row, index) in (ssr === false ? computedRows : rows)">
|
||||
<tr :class="typeof config.customRowClass === 'function' ? config.customRowClass(row) : ''"
|
||||
@click="$emit('row-click', row)"
|
||||
>
|
||||
<template v-for="(column, key) in columns" v-if="!hiddenColumns.includes(column.key)">
|
||||
<td :class="{ 'text-center': column.filter === 'iconSelect',
|
||||
>
|
||||
<div style="text-align:center; white-space: nowrap;word-break: keep-all;user-select: none;"
|
||||
:style="{ 'cursor': column.sortable ? 'pointer' : 'default' }"
|
||||
@click="column.sortable ? setOrder(column.key) : undefined">
|
||||
{{ column.text }}
|
||||
<i
|
||||
v-if="column.sortable"
|
||||
:class="getSortIconClass(column.key)"></i>
|
||||
</div>
|
||||
<!-- @formatter:off -->
|
||||
<tt-input v-if="column.filter === 'search'" v-model="filters[column.key]" sm/>
|
||||
<tt-icon-select v-else-if="column.filter === 'iconSelect'" :options="column.filterOptions" v-model="filters[column.key]" sm/>
|
||||
<tt-number-range v-else-if="column.filter === 'numberRange'" :returnText="!ssr" v-model="filters[column.key]" sm/>
|
||||
<tt-select v-else-if="column.filter === 'select'" :options="[{text: 'Alle', value: undefined}, ...column.filterOptions]" v-model="filters[column.key]" sm/>
|
||||
<tt-autocomplete v-else-if="column.filter === 'autocomplete'" :items="[{text: 'Alle', value: undefined}, ...column.filterOptions]" v-model="filters[column.key]" sm/>
|
||||
<tt-date-picker v-else-if="column.filter === 'date'" v-model="filters[column.key]"/>
|
||||
<!-- @formatter:on -->
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="pagination?.filtered_available === 0" style="height: 150px">
|
||||
<td :colspan="Object.keys(columns).length" :rowspan="5" class="text-center">Keine Ergebnisse!</td>
|
||||
</tr>
|
||||
<tr v-else-if="(pagination === null && ssr === true) || rows === null"
|
||||
style="height: 150px">
|
||||
<td :colspan="Object.keys(columns).length" class="text-center">Laden...</td>
|
||||
</tr>
|
||||
<template v-for="(row, index) in (ssr === false ? computedRows : rows)">
|
||||
<tr :class="typeof config.customRowClass === 'function' ? config.customRowClass(row) : ''"
|
||||
@click="$emit('row-click', row)"
|
||||
>
|
||||
<template v-for="(column, key) in columns" v-if="!hiddenColumns.includes(column.key)">
|
||||
<td :class="{ 'text-center': column.filter === 'iconSelect',
|
||||
[columns[key].class]: true,
|
||||
//'text-nowrap': !originalColumnWidths[column.key]
|
||||
}">
|
||||
<!-- If td is first of row then check isExpanded and display fas.fa-chevron-right or fas.fa-chevron-down with cursor pointer -->
|
||||
<i v-if="key === Object.keys(columns)[0] &&
|
||||
}"
|
||||
>
|
||||
<!-- If td is first of row then check isExpanded and display fas.fa-chevron-right or fas.fa-chevron-down with cursor pointer -->
|
||||
<i v-if="key === Object.keys(columns)[0] &&
|
||||
($scopedSlots.expandedRow && (typeof config.expandCondition !== 'function' || config.expandCondition(row)) || hiddenColumns.length > 0)"
|
||||
|
||||
@click.stop="toggleExpand(index)"
|
||||
:class="isExpanded(index) ? 'fas fa-chevron-down' : 'fas fa-chevron-right'"
|
||||
style="cursor: pointer;font-size: 14px;padding-right: 8px;user-select: none"></i>
|
||||
<slot :name="key.toLowerCase()" :value="row[key]" :row="row">
|
||||
@click.stop="toggleExpand(index)"
|
||||
:class="isExpanded(index) ? 'fas fa-chevron-down' : 'fas fa-chevron-right'"
|
||||
style="cursor: pointer;font-size: 14px;padding-right: 8px;user-select: none"></i>
|
||||
<slot :name="key.toLowerCase()" :value="row[key]" :row="row">
|
||||
<span
|
||||
v-if="column.filter === 'date'">{{ row[key] ? (moment.unix(row[key]).isValid() ? moment.unix(row[key]).format('DD.MM.YYYY HH:mm') : moment(row[key]).format('DD.MM.YYYY HH:mm')) : '' }}</span>
|
||||
<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
|
||||
v-html="(column.prefix) + (row[key] === null || typeof row[key] === 'undefined' ? '' : row[key]?.toString()?.replace('\\\\n', '<br>')) + (column.suffix )"></span>
|
||||
v-if="column.filter === 'date'">{{
|
||||
row[key] ? (moment.unix(row[key])
|
||||
.isValid() ? moment.unix(row[key]).format('DD.MM.YYYY HH:mm') : moment(row[key])
|
||||
.format('DD.MM.YYYY HH:mm')) : ''
|
||||
}}</span>
|
||||
<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="row[key] !== null"
|
||||
v-html="(column.prefix) + (row[key] === null || typeof row[key] === 'undefined' ? '' : row[key]?.toString()?.replace('\\\\n', '<br>')) + (column.suffix )"></span>
|
||||
|
||||
</slot>
|
||||
</td>
|
||||
</template>
|
||||
</tr>
|
||||
<tr v-if="isExpanded(index) && ($scopedSlots.expandedRow|| hiddenColumns.length > 0)">
|
||||
<td :colspan="Object.keys(columns).length">
|
||||
</slot>
|
||||
</td>
|
||||
</template>
|
||||
</tr>
|
||||
<tr v-if="isExpanded(index) && ($scopedSlots.expandedRow|| hiddenColumns.length > 0)">
|
||||
<td :colspan="Object.keys(columns).length">
|
||||
|
||||
<!-- display ul with li for each column in hiddenColumns with slot name key.toLowerCase() or span with value of row[key] -->
|
||||
<ul v-if="hiddenColumns.length > 0" style="list-style-type: none;padding: 0;margin: 0;">
|
||||
<li v-for="(column, key) in columns" :key="'hiddenColumn-'+key">
|
||||
<template v-if="hiddenColumns.includes(key)">
|
||||
<strong>{{ column.text }}:</strong>
|
||||
<slot :name="key.toLowerCase()" :value="row[key]" :row="row">
|
||||
<!-- display ul with li for each column in hiddenColumns with slot name key.toLowerCase() or span with value of row[key] -->
|
||||
<ul v-if="hiddenColumns.length > 0" style="list-style-type: none;padding: 0;margin: 0;">
|
||||
<li v-for="(column, key) in columns" :key="'hiddenColumn-'+key">
|
||||
<template v-if="hiddenColumns.includes(key)">
|
||||
<strong>{{ column.text }}:</strong>
|
||||
<slot :name="key.toLowerCase()" :value="row[key]" :row="row">
|
||||
|
||||
<span
|
||||
v-if="column.filter === 'date'">{{ row[key] ? (moment.unix(row[key]).isValid() ? moment.unix(row[key]).format('DD.MM.YYYY HH:mm') : moment(row[key]).format('DD.MM.YYYY HH:mm')) : '' }}</span>
|
||||
<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
|
||||
v-html="(column.prefix) + (row[key] === null || typeof row[key] === 'undefined' ? '' : row[key]?.toString()?.replace('\\\\n', '<br>')) + (column.suffix )">
|
||||
v-if="column.filter === 'date'">{{
|
||||
row[key] ? (moment.unix(row[key])
|
||||
.isValid() ? moment.unix(row[key]).format('DD.MM.YYYY HH:mm') : moment(row[key])
|
||||
.format('DD.MM.YYYY HH:mm')) : ''
|
||||
}}</span>
|
||||
<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="row[key] !== null"
|
||||
v-html="(column.prefix) + (row[key] === null || typeof row[key] === 'undefined' ? '' : row[key]?.toString()?.replace('\\\\n', '<br>')) + (column.suffix )">
|
||||
</span>
|
||||
</slot>
|
||||
</template>
|
||||
</li>
|
||||
</ul>
|
||||
</slot>
|
||||
</template>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<slot name="expandedRow" :row="row"></slot>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- Pagination Controls -->
|
||||
<nav aria-label="Page navigation">
|
||||
<tt-table-pagination :pagination="pagination" @fetch-rows="fetchRows"
|
||||
v-if="pagination"></tt-table-pagination>
|
||||
</nav>
|
||||
</div>
|
||||
`, props: {
|
||||
fetchUrl: String,
|
||||
data: Array,
|
||||
striped: {type: Boolean, default: true},
|
||||
bordered: {type: Boolean, default: true},
|
||||
hover: {type: Boolean, default: true},
|
||||
sticky: {type: Boolean, default: true},
|
||||
small: {type: Boolean, default: true},
|
||||
excelExport: {type: Boolean, default: false},
|
||||
config: {type: Object, default: () => ({}), required: true},
|
||||
ssr: {type: Boolean, default: false},
|
||||
<slot name="expandedRow" :row="row"></slot>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- Pagination Controls -->
|
||||
<nav aria-label="Page navigation">
|
||||
<tt-table-pagination :pagination="pagination" @fetch-rows="fetchRows"
|
||||
v-if="pagination"></tt-table-pagination>
|
||||
</nav>
|
||||
</div>
|
||||
`, props: {
|
||||
fetchUrl: String,
|
||||
data: Array,
|
||||
striped: {type: Boolean, default: true},
|
||||
bordered: {type: Boolean, default: true},
|
||||
hover: {type: Boolean, default: true},
|
||||
sticky: {type: Boolean, default: true},
|
||||
small: {type: Boolean, default: true},
|
||||
excelExport: {type: Boolean, default: false},
|
||||
config: {type: Object, default: () => ({}), required: true},
|
||||
ssr: {type: Boolean, default: false},
|
||||
disableInitialFetch: {type: Boolean, default: false}
|
||||
}, data() {
|
||||
return {
|
||||
window: window,
|
||||
moment: window.moment,
|
||||
loading: false,
|
||||
rows: null,
|
||||
rawRows: null,
|
||||
pagination: {},
|
||||
filters: {},
|
||||
debounceTimeout: null,
|
||||
disableDebounce: false,
|
||||
window: window,
|
||||
moment: window.moment,
|
||||
loading: false,
|
||||
rows: null,
|
||||
rawRows: null,
|
||||
pagination: {},
|
||||
filters: {},
|
||||
debounceTimeout: null,
|
||||
disableDebounce: false,
|
||||
latestFetchTimestamp: null,
|
||||
order: {
|
||||
order: {
|
||||
key: null, order: 'asc' // default sort order
|
||||
},
|
||||
expandedRows: {},
|
||||
isInitialised: false,
|
||||
hiddenColumns: [],
|
||||
expandedRows: {},
|
||||
isInitialised: false,
|
||||
hiddenColumns: [],
|
||||
originalColumnWidths: {},
|
||||
originalTableWidth: null
|
||||
originalTableWidth: null,
|
||||
debouncedHandleResize: null
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
methods: {
|
||||
/**
|
||||
* Creates a debounced function that delays invoking `fn` until after `wait` milliseconds
|
||||
* have elapsed since the last time the debounced function was invoked.
|
||||
@@ -290,7 +304,12 @@ Vue.component('tt-table', {
|
||||
if (this.debounceTimeout) clearTimeout(this.debounceTimeout);
|
||||
this.debounceTimeout = setTimeout(() => fn.apply(context, args), wait);
|
||||
}
|
||||
}, /**
|
||||
},
|
||||
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.
|
||||
@@ -310,10 +329,10 @@ Vue.component('tt-table', {
|
||||
}
|
||||
|
||||
this.pagination = {
|
||||
page: page++,
|
||||
per_page: this.pagination?.per_page ? parseInt(this.pagination.per_page) : 10,
|
||||
total_rows: this.rawRows.length || 0,
|
||||
total_pages: this.rawRows.length / this.pagination?.per_page,
|
||||
page: page++,
|
||||
per_page: this.pagination?.per_page ? parseInt(this.pagination.per_page) : 10,
|
||||
total_rows: this.rawRows.length || 0,
|
||||
total_pages: this.rawRows.length / this.pagination?.per_page,
|
||||
filtered_available: this.rawRows.length
|
||||
};
|
||||
this.loading = false;
|
||||
@@ -380,7 +399,7 @@ Vue.component('tt-table', {
|
||||
// filter filters with empty values or empty objects
|
||||
filters,
|
||||
paginationPerPage: this.pagination.per_page,
|
||||
order: this.order.key ? this.order : undefined,
|
||||
order: this.order.key ? this.order : undefined,
|
||||
}));
|
||||
}, parseSettingsFromLocalStorage() {
|
||||
const settings = JSON.parse(localStorage.getItem(`tt-table-${this.config.key}`) || '{}');
|
||||
@@ -428,7 +447,9 @@ Vue.component('tt-table', {
|
||||
if (!this.columns[key]) continue;
|
||||
|
||||
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;
|
||||
parsedRow[this.columns[key].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 {
|
||||
@@ -472,15 +493,19 @@ 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() {
|
||||
const tableContainerComputedStyle = window.getComputedStyle(this.$refs.tableContainer);
|
||||
let viewportWidth = this.$refs.tableContainer.offsetWidth -
|
||||
parseInt(tableContainerComputedStyle.paddingLeft) -
|
||||
parseInt(tableContainerComputedStyle.paddingRight);
|
||||
if (!this.$refs.tableContainer) return;
|
||||
const tableContainer = this.$refs.tableContainer;
|
||||
const {paddingLeft, paddingRight} = window.getComputedStyle(tableContainer);
|
||||
|
||||
let viewportWidth = tableContainer.offsetWidth - parseInt(paddingLeft) - parseInt(paddingRight);
|
||||
this.originalColumnWidths = {}
|
||||
this.hiddenColumns = []
|
||||
await this.$nextTick()
|
||||
|
||||
@@ -504,8 +529,8 @@ Vue.component('tt-table', {
|
||||
}
|
||||
}
|
||||
|
||||
}, watch: {
|
||||
filters: {
|
||||
}, watch: {
|
||||
filters: {
|
||||
handler: function () {
|
||||
if (!this.isInitialised) return;
|
||||
|
||||
@@ -521,7 +546,7 @@ Vue.component('tt-table', {
|
||||
|
||||
this.saveSettingsToLocalStorage();
|
||||
}, deep: true
|
||||
}, order: {
|
||||
}, order: {
|
||||
handler: function () {
|
||||
if (!this.isInitialised) return;
|
||||
|
||||
@@ -530,21 +555,21 @@ Vue.component('tt-table', {
|
||||
}
|
||||
this.saveSettingsToLocalStorage();
|
||||
}, deep: true
|
||||
}, expandedRows: {
|
||||
}, expandedRows: {
|
||||
handler: function () {
|
||||
if (!this.isInitialised) return;
|
||||
|
||||
this.saveSettingsToLocalStorage();
|
||||
}, deep: true
|
||||
},
|
||||
rows: {
|
||||
rows: {
|
||||
handler: async function () {
|
||||
this.$nextTick(() => {
|
||||
this.handleResponsiveColumns()
|
||||
})
|
||||
}, deep: true
|
||||
},
|
||||
computedRows: {
|
||||
computedRows: {
|
||||
handler: async function () {
|
||||
this.$nextTick(() => {
|
||||
this.handleResponsiveColumns()
|
||||
@@ -565,15 +590,15 @@ Vue.component('tt-table', {
|
||||
}
|
||||
|
||||
columns[column.key] = {
|
||||
text: column.text,
|
||||
key: column.key,
|
||||
filter: column.filter !== undefined ? column.filter : 'search',
|
||||
text: column.text,
|
||||
key: column.key,
|
||||
filter: column.filter !== undefined ? column.filter : 'search',
|
||||
filterOptions: column.filterOptions || undefined,
|
||||
sortable: column.sortable !== undefined ? column.sortable : true,
|
||||
class: column.class !== undefined ? column.class : '',
|
||||
prefix: column.prefix || '',
|
||||
suffix: column.suffix || '',
|
||||
priority: column.priority + 100 || i--
|
||||
sortable: column.sortable !== undefined ? column.sortable : true,
|
||||
class: column.class !== undefined ? column.class : '',
|
||||
prefix: column.prefix || '',
|
||||
suffix: column.suffix || '',
|
||||
priority: column.priority + 100 || i--
|
||||
};
|
||||
return columns;
|
||||
}, {});
|
||||
@@ -581,7 +606,9 @@ 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
|
||||
@@ -647,10 +674,13 @@ Vue.component('tt-table', {
|
||||
if (header.filter === 'search') {
|
||||
|
||||
const isNegated = filterValue.startsWith('!');
|
||||
const substrings = (isNegated ? filterValue.slice(1) : filterValue).split(' ').map(s => s.toLowerCase());
|
||||
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]).join(' ').toLowerCase() : row[header.key].toString().toLowerCase();
|
||||
typeof row[header.key] === 'object' ? Object.values(row[header.key])
|
||||
.join(' ')
|
||||
.toLowerCase() : row[header.key].toString().toLowerCase();
|
||||
|
||||
let substringMatch = true;
|
||||
for (var k = 0, klen = substrings.length; k < klen; ++k) {
|
||||
@@ -673,7 +703,12 @@ 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;
|
||||
@@ -682,7 +717,8 @@ 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) {
|
||||
@@ -708,10 +744,12 @@ Vue.component('tt-table', {
|
||||
|
||||
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() :
|
||||
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() :
|
||||
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;
|
||||
@@ -757,26 +795,28 @@ 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);
|
||||
}
|
||||
|
||||
|
||||
window.addEventListener('resize', this.debounce(this.handleResponsiveColumns, 100));
|
||||
this.debouncedHandleResize = this.debounce(this.handleResponsiveColumns, 100);
|
||||
window.addEventListener('resize', this.debouncedHandleResize);
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.debounce(this.handleResponsiveColumns, 100));
|
||||
window.removeEventListener('resize', this.debouncedHandleResize);
|
||||
}
|
||||
})
|
||||
|
||||
Vue.config.errorHandler = function(err, vm, info) {
|
||||
Vue.config.errorHandler = function (err, vm, info) {
|
||||
// still log errors to the console
|
||||
console.error(info, err, vm);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user