diff --git a/Layout/default/WarehouseShippingNote/PDF_FOOTER.html b/Layout/default/WarehouseShippingNote/PDF_FOOTER.html
new file mode 100644
index 000000000..0a984b7d6
--- /dev/null
+++ b/Layout/default/WarehouseShippingNote/PDF_FOOTER.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
Ihr XINON Lieferschein vom =date("d.m.Y", $shippingNote->create)?>
+
+
+
+ Position
+ Menge
+ EH
+ Artikel
+
+ Preis
+
+
+
+
+ ">
+ = $i + 1 ?>
+ =$p["amount"]?>
+ =$p["articleUnit"]?>
+ =$p["articleTitle"]?>
+
+ =number_format(
+ $p["price"] * $p["amount"], 2, ",", ".")?> €
+
+
+ ">
+
+
+
+
+ = $p["articleDescription"] ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Layout/default/menu.php b/Layout/default/menu.php
index f4d22bff1..edcb0c6d3 100644
--- a/Layout/default/menu.php
+++ b/Layout/default/menu.php
@@ -142,18 +142,19 @@
diff --git a/application/Address/AddressController.php b/application/Address/AddressController.php
index 2c5c3e647..60b475168 100644
--- a/application/Address/AddressController.php
+++ b/application/Address/AddressController.php
@@ -534,6 +534,7 @@ class AddressController extends mfBaseController {
];
$results[] = $result;
$this->returnJson($results);
+ die();
}
}
diff --git a/application/WarehouseAdministration/WarehouseAdministration.php b/application/WarehouseAdministration/WarehouseAdministration.php
new file mode 100644
index 000000000..292496634
--- /dev/null
+++ b/application/WarehouseAdministration/WarehouseAdministration.php
@@ -0,0 +1,9 @@
+loadMe();
+ $this->layout()->set("me", $me);
+ $this->me = $me;
+
+ if (!$this->me->isAdmin()) {
+ $this->redirect("dashboard");
+ }
+ }
+
+ protected function indexAction(): void {
+ $this->layout()->set('additionalJS', ['js/pages/WarehouseHistory/WarehouseHistoryModal.js']);
+
+ Helper::renderVue($this, 'WarehouseAdministration', 'Administration-Tools', ["CREATE_URL" => $this::getUrl($this->mod . "/create"),
+ "TABLE_URL" => $this::getUrl($this->mod . "/get"),
+ "UPDATE_URL" => $this::getUrl($this->mod . "/update"),
+ "DELETE_URL" => $this::getUrl($this->mod . "/delete"),]);
+ }
+
+ //TODO: this needs improvement as it is inefficient but it doesnt matter as it doesnt get called very often
+ // and also maybe we should move it to WarehouseLocationController
+ protected function createLocationsAction(): void {
+ $existingLocations = WarehouseLocationModel::getAll();
+ $companyCars = TimerecordingCarModel::getAll();
+
+
+ $wantedCarLocations = [];
+ foreach ($companyCars as $car) {
+ // check if $car->brand includes "Anhänger" or "Anhaenger", if yes then continue
+ if (strpos($car->brand, "Anhänger") !== false || strpos($car->brand, "Anhaenger") !== false) {
+ continue;
+ }
+
+ $carModelParts = explode(" ", $car->model);
+ if (count($carModelParts) > 1) {
+ $wantedCarLocations[] = "{$car->number_plate} {$car->brand} {$carModelParts[0]} {$carModelParts[1]}";
+ } else {
+ $wantedCarLocations[] = "{$car->number_plate} {$car->brand} {$carModelParts[0]}";
+ }
+ }
+
+
+ // create a warehouse location for each wantedcar but check if $existingLocations[]->title already exists with the same title
+ foreach ($wantedCarLocations as $wantedCarLocation) {
+ $locationExists = false;
+ foreach ($existingLocations as $existingLocation) {
+ if ($existingLocation->title === $wantedCarLocation) {
+ $locationExists = true;
+ break;
+ }
+ }
+
+ if (!$locationExists) {
+ $numberPlate = explode(" ", $wantedCarLocation)[0];
+ $assignedTo = 1;
+ foreach ($companyCars as $car) {
+ if ($car->number_plate === $numberPlate) {
+ $assignedTo = $car->user_id;
+ break;
+ }
+ }
+ if ($assignedTo === null) {
+ $assignedTo = 6;
+ }
+
+ WarehouseLocationModel::create([
+ "title" => $wantedCarLocation,
+ "description" => "Automatisch erstellt",
+ "assignedTo" => $assignedTo,
+ "createdBy" => $this->me->id,
+ "create" => time()
+ ]);
+ }
+ }
+
+ $existingLocations = WarehouseLocationModel::getAll();
+ $users = UserModel::search(['employee' => true]);
+
+ // now create a warehouse location for each user only if they dont already have one (for example if they have a company car)
+ foreach ($users as $user) {
+ $locationExists = false;
+ foreach ($existingLocations as $existingLocation) {
+ if (intval($existingLocation->assignedTo) === intval($user->id)) {
+ $locationExists = true;
+ break;
+ }
+ }
+
+ if (!$locationExists) {
+ WarehouseLocationModel::create([
+ "title" => $user->name . "'s Lagerort",
+ "description" => "Automatisch erstellt",
+ "assignedTo" => $user->id,
+ "createdBy" => $this->me->id,
+ "create" => time()
+ ]);
+ }
+ }
+
+ var_dump($existingLocations);
+ die();
+
+ }
+}
\ No newline at end of file
diff --git a/application/WarehouseArticle/WarehouseArticleController.php b/application/WarehouseArticle/WarehouseArticleController.php
index 643fe6f8b..65826d967 100644
--- a/application/WarehouseArticle/WarehouseArticleController.php
+++ b/application/WarehouseArticle/WarehouseArticleController.php
@@ -7,11 +7,12 @@ class WarehouseArticleController extends TTCrud {
// @formatter:off
protected array $columns = [
['key' => 'title', 'text' => 'Titel', 'required' => true, 'table' => ['priority' => 9]],
- ['key' => 'description', 'text' => 'Beschreibung', 'required' => true, 'table' => false],
+ ['key' => 'description', 'text' => 'Beschreibung', 'required' => true],
['key' => 'category', 'text' => 'Kategorie', 'required' => true],
['key' => 'unit', 'text' => 'Einheit', 'required' => true,'table' => false], // Boolean value
- ['key' => 'defaultSellMultiplier', 'text' => 'Standard Multiplikator','regex' => '/^[0-9]*$/' , 'required' => true,'modal' => ['type' => 'number'], 'table' => false], // Boolean value
- ['key' => 'revenueAccount', 'text' => 'Erlöskonto', 'required' => true,'modal' => ['type' => 'select'], 'table' => false], // Boolean value
+ ['key' => 'revenueAccount', 'text' => 'Erlöskonto', 'required' => true,'modal' =>
+ ['type' => 'select', 'items' => [['value' => 0, 'text' => 'Dienstleistungen'], ['value' => 1, 'text' => 'Handelswaren']]
+ ], 'table' => false], // Boolean value
['key' => 'cheapestPurchasePrice', 'text' => 'Einkauf', 'modal' => false, 'table' => ['class' => 'text-center', 'suffix' => ' €']],
['key' => 'cheapestSellPrice', 'text' => 'Verkauf', 'modal' => false, 'table' => ['class' => 'text-center', 'suffix' => ' €']],
['key' => 'warningAmount', 'text' => 'Warnmenge', 'required' => true,'modal' => ['type' => 'number'], 'table' => ['class' => 'text-center']], // Stock/inventory related
@@ -27,6 +28,7 @@ class WarehouseArticleController extends TTCrud {
['key' => 'editDistributorEntries','title' => 'Lieferanten','class' => 'fas fa-truck text-cyan'],
['key' => 'editThresholdEntries','title' => 'Schwellenwerte','class' => 'far fa-fw fa-box-full text-orange'],
['key' => 'editPricesEntries','title' => 'Preise','class' => 'fas fa-euro-sign text-green'],
+ ['key' => 'addToCart','title' => 'Zur Bestellung hinzufügen','class' => 'fas fa-shopping-cart text-primary'],
];
// @formatter:on
@@ -35,15 +37,6 @@ class WarehouseArticleController extends TTCrud {
'delete' => 'Artikel wurde gelöscht',
'noChanges' => 'Keine Änderungen',];
- public function prepareCrudConfig() {
- $revenueAccounts = WarehouseRevenueAccountModel::getAll();
- $revenueAccounts = array_map(function ($revenueAccount) {
- return ['value' => $revenueAccount->id, 'text' => $revenueAccount->title];
- }, $revenueAccounts);
-
- $this->columns[5]['modal']['items'] = $revenueAccounts;
- }
-
protected function beforeUpdate($postData): bool {
(new WarehouseHistoryController)->create($postData, $this->mod);
return true;
@@ -77,6 +70,11 @@ class WarehouseArticleController extends TTCrud {
WarehouseArticleModel::update(array_merge(get_object_vars($article), ['cheapestPurchasePrice' => $cheapestPurchasePrice]));
}
+ protected function afterCreate($postData) {
+ self::updateCheapestPurchasePrice($postData['id']);
+ self::updateSellPrices($postData['id']);
+ }
+
/**
* Updates the sell prices for a given article.
*
@@ -96,33 +94,31 @@ class WarehouseArticleController extends TTCrud {
$cheapestSellPrices = [];
// Calculate sell prices for each price type, use default sell multiplier if no specific price is set
foreach ($priceTypes as $priceType) {
- $articlePriceType = array_filter($articlePriceTypes, function ($apt) use ($priceType) {
- return $apt->articlePriceTypeId == $priceType->id;
- });
+ $articlePriceType = null;
+ foreach ($articlePriceTypes as $apt) {
+ if ($apt->articlePriceTypeId == $priceType->id) {
+ $articlePriceType = $apt;
+ break;
+ }
+ }
- $sellPrice = $article->defaultSellMultiplier * $article->cheapestPurchasePrice;
- if (!empty($articlePriceType)) {
- $articlePriceType = $articlePriceType[0];
+ $sellPrice = $priceType->defaultPriceFactor * $article->cheapestPurchasePrice;
+ if ($articlePriceType !== null) {
$sellPrice = $articlePriceType->priceOverride ?: $articlePriceType->priceMultiplier * $article->cheapestPurchasePrice;
}
- $cheapestSellPrices[$priceType->id] = ['title' => $priceType->title, 'price' => $sellPrice];
+ $cheapestSellPrices[$priceType->id] = ['title' => $priceType->title, 'price' => round($sellPrice, 2)];
}
$article->cheapestSellPrice = json_encode($cheapestSellPrices);
WarehouseArticleModel::update(get_object_vars($article));
}
-
- protected function afterCreate($postData) {
- self::updateCheapestPurchasePrice($postData['id']);
- self::updateSellPrices($postData['id']);
- }
-
- protected function updatePricesAction() {
+ public function updatePricesAction() {
foreach (WarehouseArticleModel::getAll() as $article) {
self::updateCheapestPurchasePrice($article->id);
self::updateSellPrices($article->id);
}
+ self::returnJson(['success' => true, 'message' => 'Preise wurden aktualisiert']);
}
protected function getHistoryAction() {
@@ -241,4 +237,52 @@ class WarehouseArticleController extends TTCrud {
}
}
+
+ protected function prepareOrderAction() {
+ // inside post json it will look like
+ // [
+ // {
+ // "amount": "5",
+ // "itemId": 441,
+ // "title": "RT-FB-7590AX"
+ // },
+ // {
+ // "amount": "5",
+ // "itemId": 421,
+ // "title": "RT-FB-7590"
+ // }
+ //]
+ // get the json from the post request
+ // then create a array containing each order we need to make, so search through WarehouseArticleDistributorModel to get the distributorId and purchasePrice (use lowest purchasePrice)
+ // then get the WarehouseDistributorModel and then create a summary of the orders we need to make for each distributor
+
+ $postData = json_decode(file_get_contents('php://input'), true);
+ $orders = [];
+ foreach ($postData as $order) {
+ $articleDistributors = WarehouseArticleDistributorModel::getAll(['articleId' => $order['itemId']]);
+ $cheapestArticleDistributor = $articleDistributors[0];
+ foreach ($articleDistributors as $articleDistributor) {
+ if ($articleDistributor->purchasePrice < $cheapestArticleDistributor->purchasePrice) {
+ $cheapestArticleDistributor = $articleDistributor;
+ }
+ }
+ $distributor = WarehouseDistributorModel::get($cheapestArticleDistributor->distributorId);
+
+ if (!isset($orders[$distributor->id])) {
+ $orders[$distributor->id] = ['distributor' => array($distributor),
+ 'orderAmount' => 0,
+ 'orders' => []];
+ }
+
+ $orders[$distributor->id]['orders'][] = ['articleId' => $order['itemId'],
+ 'amount' => $order['amount'],
+ 'sum' => $order['amount'] * $cheapestArticleDistributor->purchasePrice,
+ 'purchasePrice' => $cheapestArticleDistributor->purchasePrice,
+ 'externalArticleNumber' => $cheapestArticleDistributor->externalArticleNumber,
+ 'title' => $order['title'],];
+ $orders[$distributor->id]['orderAmount'] += $order['amount'] * $cheapestArticleDistributor->purchasePrice;
+ }
+
+ self::returnJson($orders);
+ }
}
\ No newline at end of file
diff --git a/application/WarehouseArticle/WarehouseArticleModel.php b/application/WarehouseArticle/WarehouseArticleModel.php
index 926920fac..2a6a14cf3 100644
--- a/application/WarehouseArticle/WarehouseArticleModel.php
+++ b/application/WarehouseArticle/WarehouseArticleModel.php
@@ -11,7 +11,6 @@ class WarehouseArticleModel extends TTCrudBaseModel {
public int $criticalAmount;
public int $isEShop;
public int $isEShopHide;
- public float $defaultSellMultiplier;
public string $unit;
public int $isSerialDocumentation;
public int $revenueAccount;
diff --git a/application/WarehouseArticle/import.json b/application/WarehouseArticle/import.json
new file mode 100644
index 000000000..e69de29bb
diff --git a/application/WarehouseArticlePriceType/WarehouseArticlePriceTypeController.php b/application/WarehouseArticlePriceType/WarehouseArticlePriceTypeController.php
index e9cc254fb..2591c8b1b 100644
--- a/application/WarehouseArticlePriceType/WarehouseArticlePriceTypeController.php
+++ b/application/WarehouseArticlePriceType/WarehouseArticlePriceTypeController.php
@@ -7,6 +7,11 @@ class WarehouseArticlePriceTypeController extends TTCrud {
// @formatter:off
protected array $columns = [
['key' => 'title', 'text' => 'Titel', 'required' => true],
+ ['key' => 'description', 'text' => 'Beschreibung', 'required' => false],
+ ['key' => 'defaultPriceFactor', 'text' => 'Standard Preisfaktor', 'required' => true, 'modal' => ['type' => 'number']],
+ ['key' => 'create', 'text' => 'Erstellt', 'required' => false, 'modal' => false, 'table' => ['filter' => 'datetime', 'class' => 'text-nowrap']],
+ ['key' => 'createBy', 'text' => 'Erstellt von', 'required' => false, 'modal' => [
+ 'type' => 'select', 'items' => [], 'table' => ['class' => 'text-nowrap']], 'visible' => false],
['key' => 'actions', 'text' => 'Aktionen', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center', 'priority' => 10]],
];
// @formatter:on
@@ -16,47 +21,30 @@ class WarehouseArticlePriceTypeController extends TTCrud {
'delete' => 'Artikel Verkaufspreis wurde gelöscht',
'noChanges' => 'Keine Änderungen'];
- protected function checkExistingDistributorEntry($postData): bool {
- // if postData id exists check if there is already an entry with the same articleId and locationId if postdata id and WarehouseLocationThresholdOverrideModel id are different return false
- if (isset($postData['id'])) {
- $count = WarehouseArticlePriceTypeModel::count(['title' => $postData['title'], 'id' => $postData['id']]);
- if ($count > 0) {
- return true;
- }
- } else {
- $count = WarehouseArticlePriceTypeModel::count(['title' => $postData['title']]);
- if ($count > 0) {
- self::returnJson(['success' => false,
- 'message' => 'Es existiert bereits ein Preis Typ mit diesem Titel.']);
- return false;
-
- }
- }
- return true;
- }
-
- protected function beforeCreate($postData): bool {
- return $this->checkExistingDistributorEntry($postData);
+ protected function prepareCrudConfig() {
+ // add all users to createBy column
+ $this->columns[array_search('createBy', array_column($this->columns, 'key'))]['modal']['items'] = array_map(function ($user) {
+ return ['value' => $user->id, 'text' => $user->name];
+ }, UserModel::getAll());
}
protected function beforeUpdate($postData): bool {
- $existing = $this->checkExistingDistributorEntry($postData);
-
- if (!$existing) {
- return false;
- }
-
(new WarehouseHistoryController)->create($postData, $this->mod);
-
return true;
}
- public function afterCreate($postData) {
- WarehouseArticleController::updateSellPrices($postData['articleId']);
+ protected function afterUpdate($postData) {
+ $WarehouseArticleController = new WarehouseArticleController;
+ // set mod of WarehouseArticleController to WarehouseArticle
+ $WarehouseArticleController->mod = 'WarehouseArticle';
+ $WarehouseArticleController->updatePricesAction();
}
- public function afterUpdate($postData) {
- WarehouseArticleController::updateSellPrices($postData['articleId']);
+ protected function afterCreate($postData) {
+ $WarehouseArticleController = new WarehouseArticleController;
+ // set mod of WarehouseArticleController to WarehouseArticle
+ $WarehouseArticleController->mod = 'WarehouseArticle';
+ $WarehouseArticleController->updatePricesAction();
}
protected function getHistoryAction() {
diff --git a/application/WarehouseArticlePriceType/WarehouseArticlePriceTypeModel.php b/application/WarehouseArticlePriceType/WarehouseArticlePriceTypeModel.php
index 7a87f539e..c58f39d23 100644
--- a/application/WarehouseArticlePriceType/WarehouseArticlePriceTypeModel.php
+++ b/application/WarehouseArticlePriceType/WarehouseArticlePriceTypeModel.php
@@ -3,4 +3,8 @@
class WarehouseArticlePriceTypeModel extends TTCrudBaseModel {
public int $id;
public string $title;
+ public ?string $description;
+ public float $defaultPriceFactor;
+ public int $create;
+ public int $createBy;
}
\ No newline at end of file
diff --git a/application/WarehouseEShopOrder/WarehouseEShopOrderController.php b/application/WarehouseEShopOrder/WarehouseEShopOrderController.php
index a138645f9..f0f21df52 100644
--- a/application/WarehouseEShopOrder/WarehouseEShopOrderController.php
+++ b/application/WarehouseEShopOrder/WarehouseEShopOrderController.php
@@ -153,6 +153,7 @@ class WarehouseEShopOrderController extends TTCrud {
}
// if it is still null, die with order id:
if ($realOrderItems === null) {
+ continue;
self::returnJson(['success' => false, 'message' => 'Bestellung mit ID ' . $order['id'] . ' hat keine Artikel. Bitte überprüfen.']);
die();
}
diff --git a/application/WarehouseEShopOrderItem/WarehouseEShopOrderItemModel.php b/application/WarehouseEShopOrderItem/WarehouseEShopOrderItemModel.php
index 6bbf0cd56..f1bf47acc 100644
--- a/application/WarehouseEShopOrderItem/WarehouseEShopOrderItemModel.php
+++ b/application/WarehouseEShopOrderItem/WarehouseEShopOrderItemModel.php
@@ -4,12 +4,13 @@
* @property int $orderId
* @property int $articleId
* @property int $quantity
+ * @property int $price
*/
class WarehouseEShopOrderItemModel extends TTCrudBaseModel {
public int $id;
public int $orderId;
public ?int $articleId;
- public ?int $articlePacketId;
public int $quantity;
+ public ?int $articlePacketId;
}
\ No newline at end of file
diff --git a/application/WarehouseHistory/WarehouseHistoryController.php b/application/WarehouseHistory/WarehouseHistoryController.php
index 493daf507..f5e88b80d 100644
--- a/application/WarehouseHistory/WarehouseHistoryController.php
+++ b/application/WarehouseHistory/WarehouseHistoryController.php
@@ -9,6 +9,12 @@ class WarehouseHistoryController {
$me = new User();
$me->loadMe();
+ foreach ($postData as $key => $value) {
+ if (is_array($value)) {
+ $postData[$key] = json_encode($value);
+ }
+ }
+
foreach (array_diff_assoc($postData, (array) $currentData) as $key => $value) {
WarehouseHistoryModel::create(['table' => $mod,
'row_id' => $postData['id'],
diff --git a/application/WarehouseItem/WarehouseItemController.php b/application/WarehouseItem/WarehouseItemController.php
index cf1c220ce..7e5d80532 100644
--- a/application/WarehouseItem/WarehouseItemController.php
+++ b/application/WarehouseItem/WarehouseItemController.php
@@ -4,14 +4,25 @@ class WarehouseItemController extends TTCrud {
protected string $headerTitle = 'Eintrag';
protected string $createText = 'Eintrag erstellen';
- // TODO: change articleId and warehouseLocationId to autocomplete
+ // TODO: check if historyController is needed
+ // TODO: check if apiUrl uses self::getUrl to get the correct URL
// @formatter:off
protected array $columns = [
- ['key' => 'articleId', 'text' => 'Artikel', 'required' => true, 'type' => 'select','table' => ['class' => 'text-nowrap'], 'modal' => ['items' => [], 'type' => 'select']],
- ['key' => 'warehouseLocationId', 'text' => 'Lagerort', 'required' => true, 'type' => 'select', 'modal' => ['items' => [], 'type' => 'select']],
- ['key' => 'quantity', 'text' => 'Menge', 'required' => true, 'type' => 'number'],
- ['key' => 'serialNumber', 'text' => 'Seriennummer', 'required' => false],
+ ['key' => 'articleId', 'text' => 'Artikel', 'required' => true, 'type' => 'autocomplete','table' => ['class' => 'text-nowrap', 'filter' => 'autocomplete'],'modal' => [
+ 'apiUrl' => 'WarehouseArticle/autocomplete','items' => 'WarehouseArticle/autocomplete', 'type' => 'autocomplete']],
+ ['key' => 'warehouseLocationId', 'text' => 'Lagerort', 'required' => true, 'type' => 'autocomplete', 'table' => ['filter' => 'autocomplete'], 'modal' => [
+ 'items' => 'WarehouseLocation/autocomplete',
+ 'apiUrl' => 'WarehouseLocation/autocomplete', 'type' => 'autocomplete']],
+ ['key' => 'quantity', 'text' => 'Menge', 'required' => false, 'type' => 'number'
+ // quantity is only visible in modal when warehouseArticle(articleId).serial is false, add modal config here to reference warehouseArticle
+ , 'modal' => ['type' => 'number',
+ 'visible' => ['reference' => 'WarehouseArticle', 'use' => 'articleId=id', 'key' => 'isSerialDocumentation', 'value' => false]
+ ]
+ ],
+ ['key' => 'rack', 'text' => 'Regal', 'required' => false, 'modal' => ['type' => 'text']],
+ ['key' => 'shelf', 'text' => 'Fach', 'required' => false, 'modal' => ['type' => 'text'], 'table' => false],
+ ['key' => 'serialNumber', 'text' => 'Seriennummer', 'required' => false, 'modal' => ['type' => 'text', 'visible' => ['reference' => 'WarehouseArticle', 'use' => 'articleId=id', 'key' => 'isSerialDocumentation', 'value' => true]]],
['key' => 'note', 'text' => 'Notiz', 'required' => false],
['key' => 'actions', 'text' => 'Aktionen', 'table' => ['filter' => false], 'required' => false, 'modal' => false]
];
@@ -58,18 +69,6 @@ class WarehouseItemController extends TTCrud {
return true;
}
- public function prepareCrudConfig() {
- $articles = array_map(function($article) {
- return ['value' => $article->id, 'text' => $article->title];
- }, WarehouseArticleModel::getAll());
- $this->columns[0]['modal']['items'] = $articles;
-
- $locations = array_map(function($location) {
- return ['value' => $location->id, 'text' => $location->title];
- }, WarehouseLocationModel::getAll());
- $this->columns[1]['modal']['items'] = $locations;
- }
-
protected function getHistoryAction() {
self::returnJson((new WarehouseHistoryController)->getHistory($this->request->id, $this->mod, $this->columns));
}
diff --git a/application/WarehouseItem/WarehouseItemModel.php b/application/WarehouseItem/WarehouseItemModel.php
index 56eb73af7..5bff64e28 100644
--- a/application/WarehouseItem/WarehouseItemModel.php
+++ b/application/WarehouseItem/WarehouseItemModel.php
@@ -4,7 +4,9 @@ class WarehouseItemModel extends TTCrudBaseModel {
public int $id;
public int $articleId;
public int $warehouseLocationId;
- public int $quantity;
+ public ?string $rack;
+ public ?string $shelf;
+ public ?int $quantity;
public ?string $serialNumber;
public ?string $note;
}
\ No newline at end of file
diff --git a/application/WarehouseLocation/WarehouseLocationController.php b/application/WarehouseLocation/WarehouseLocationController.php
index af7851546..e732b3454 100644
--- a/application/WarehouseLocation/WarehouseLocationController.php
+++ b/application/WarehouseLocation/WarehouseLocationController.php
@@ -6,7 +6,9 @@ class WarehouseLocationController extends TTCrud {
protected array $columns = [
['key' => 'title', 'text' => 'Titel', 'required' => true],
- ['key' => 'assignedTo', 'text' => 'Zugewiesen an', 'required' => true, 'modal' => ['type' => 'select', 'items' => []]],
+ ['key' => 'assignedTo', 'text' => 'Zugewiesen an', 'required' => true,
+ 'table' => ['filter' => 'select', 'items' => []],
+ 'modal' => ['type' => 'select', 'items' => []]],
['key' => 'actions', 'text' => 'Aktionen', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center']],
];
diff --git a/application/WarehouseLocation/WarehouseLocationModel.php b/application/WarehouseLocation/WarehouseLocationModel.php
index 90705745d..698780440 100644
--- a/application/WarehouseLocation/WarehouseLocationModel.php
+++ b/application/WarehouseLocation/WarehouseLocationModel.php
@@ -3,5 +3,8 @@
class WarehouseLocationModel extends TTCrudBaseModel {
public int $id;
public string $title;
+ public string $description;
public int $assignedTo;
+ public int $createdBy;
+ public int $create;
}
\ No newline at end of file
diff --git a/application/WarehouseOrder/WarehouseOrder.php b/application/WarehouseOrder/WarehouseOrder.php
new file mode 100644
index 000000000..035cdc93b
--- /dev/null
+++ b/application/WarehouseOrder/WarehouseOrder.php
@@ -0,0 +1,9 @@
+ 'id', 'text' => 'ID', 'modal' => false],
+ ['key' => 'distributorId', 'text' => 'Lieferant', 'required' => true, 'type' => 'autocomplete','table' => ['class' => 'text-nowrap', 'filter' => 'autocomplete'],'modal' => [
+ 'apiUrl' => 'WarehouseDistributor/autocomplete','items' => 'WarehouseDistributor/autocomplete', 'type' => 'autocomplete']],
+ ['key' => 'extRef', 'text' => 'Externe Referenz', 'required' => false],
+ ['key' => 'intRef', 'text' => 'Interne Referenz', 'required' => false],
+ ['key' => 'status', 'text' => 'Status', 'required' => true, 'modal' => ['type' => 'select', 'items' => [
+ ['value' => 'new', 'text' => 'Neu'],
+ ['value' => 'accepted', 'text' => 'An Lieferant übergeben'],
+ ['value' => 'sent', 'text' => 'Gesendet'],
+ ['value' => 'done', 'text' => 'Erledigt'],
+ ]]],
+ ['key' => 'trackingNumber', 'text' => 'Trackingnummer', 'required' => false],
+ ['key' => 'sum', 'text' => 'Summe', 'required' => true, 'modal' => false, 'table' => ['filter' => 'numberRange']],
+ ['key' => 'create', 'text' => 'Erstellt', 'required' => true, 'modal' => false, 'filter' => 'datetime'],
+ ['key' => 'createBy', 'text' => 'Erstellt von', 'required' => true, 'table' => ['filter' => 'select'], 'modal' => ['type' => 'select', 'items' => []]],
+ ['key' => 'actions', 'text' => 'Aktionen', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center']],
+ ];
+
+ protected array $additionalActions = [['key' => 'openHistory', 'title' => 'Historie', 'class' => 'fas fa-history text-primary']];
+
+ protected array $infoMessages = ['create' => 'Bestellung wurde erfolgreich erstellt.',
+ 'update' => 'Bestellung wurde aktualisiert.',
+ 'delete' => 'Bestellung wurde gelöscht',
+ 'noChanges' => 'Keine Änderungen',];
+
+ public function permissionCheck(): bool {
+ return $this->user->can(["WarehouseEShop"]);
+ }
+
+ protected function prepareCrudConfig() {
+ // Fill Users in createBy column
+ $column = array_search('createBy', array_column($this->columns, 'key'));
+ $this->columns[$column]['modal']['items'] = array_map(function ($user) {
+ return ['value' => intval($user->id), 'text' => $user->name];
+ }, UserModel::search());
+
+ }
+
+ protected function createOrderAction() {
+ ini_set('display_errors', 1);
+ ini_set('display_startup_errors', 1);
+ error_reporting(E_ALL);
+
+ $json = json_decode(file_get_contents('php://input'), true);
+ $orders = $json;
+ $orderIds = [];
+
+ foreach ($orders as $order) {
+ $distributor = $order['distributor'][0];
+ $orderAmount = $order['orderAmount'];
+ $orders = $order['orders'];
+
+ $order = [
+ 'distributorId' => $distributor['id'],
+ 'extRef' => null,
+ 'status' => 'new',
+ 'trackingNumber' => null,
+ 'sum' => $orderAmount,
+ 'create' => time(),
+ 'createBy' => $this->user->id,
+ ];
+
+ $orderId = WarehouseOrderModel::create($order);
+ $orderIds[] = $orderId;
+
+ foreach ($orders as $orderItem) {
+ $article = WarehouseArticleModel::get($orderItem['articleId']);
+
+ WarehouseEShopOrderItemModel::create([
+ 'orderId' => $orderId,
+ 'articleId' => $orderItem['articleId'],
+ 'quantity' => $orderItem['amount'],
+ 'price' => $article->cheapestPurchasePrice,
+ ]);
+ }
+ }
+
+ self::returnJson(['success' => true, 'message' => $this->infoMessages['create'], 'ids' => $orderIds]);
+ }
+
+ protected function getOrderItemsAction() {
+ $orderItems = WarehouseEShopOrderItemModel::getAll(['orderId' => $this->request->id]);
+
+ // also get the article name of the order items
+
+ foreach ($orderItems as $key => $orderItem) {
+ $article = WarehouseArticleModel::get($orderItem->articleId);
+ $orderItem->articleName = $article->title;
+ }
+
+ self::returnJson($orderItems);
+ }
+
+ protected function beforeUpdate($postData): bool {
+ (new WarehouseHistoryController)->create($postData, $this->mod);
+ return true;
+ }
+
+ protected function getHistoryAction() {
+ self::returnJson((new WarehouseHistoryController)->getHistory($this->request->id, $this->mod, $this->columns));
+ }
+}
\ No newline at end of file
diff --git a/application/WarehouseOrder/WarehouseOrderModel.php b/application/WarehouseOrder/WarehouseOrderModel.php
new file mode 100644
index 000000000..65f5d80dc
--- /dev/null
+++ b/application/WarehouseOrder/WarehouseOrderModel.php
@@ -0,0 +1,28 @@
+ 'title', 'text' => 'Titel', 'required' => true],
- ['key' => 'revenueAccountNumber', 'text' => 'Erlöskonto Nummer', 'required' => true, 'modal' => ['type' => 'number']],
- ['key' => 'actions', 'text' => 'Aktionen', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center', 'priority' => 10]],
- ];
- // @formatter:on
-
- protected array $infoMessages = ['create' => 'Erlöskonto wurde erstellt',
- 'update' => 'Erlöskonto wurde aktualisiert',
- 'delete' => 'Erlöskonto wurde gelöscht',
- 'noChanges' => 'Keine Änderungen'];
-
- protected function beforeUpdate($postData): bool {
- (new WarehouseHistoryController)->create($postData, $this->mod);
-
- return true;
- }
-
- protected function getHistoryAction() {
- $history = WarehouseHistoryModel::getByRowId($this->request->id, $this->mod);
-
- $history = array_map(function ($item) {
- $item = (array) $item;
-
- $item['columnHeader'] = $this->columns[array_search($item['key'], array_column($this->columns, 'key'))]['text'];
- return $item;
- }, $history);
-
- self::returnJson($history);
- }
-
-}
\ No newline at end of file
diff --git a/application/WarehouseRevenueAccount/WarehouseRevenueAccountModel.php b/application/WarehouseRevenueAccount/WarehouseRevenueAccountModel.php
deleted file mode 100644
index 2113d4b9f..000000000
--- a/application/WarehouseRevenueAccount/WarehouseRevenueAccountModel.php
+++ /dev/null
@@ -1,7 +0,0 @@
- 'id', 'text' => 'LS-Nr.', 'required' => false, 'modal' => false, 'table' => ['class' => 'text-nowrap']],
+ ['key' => 'billingAddressId',
+ 'text' => 'Rechnungsadresse',
+ 'required' => true,
+ 'type' => 'autocomplete',
+ 'table' => ['class' => 'text-nowrap', 'filter' => 'autocomplete'],
+ 'modal' => ['apiUrl' => 'Address/api?do=findAddress', 'items' => '/Address/Api?do=findAddress', 'type' => 'autocomplete']],
+ ['key' => 'deliveryAddressName', 'text' => 'L.-Adr. Name', 'required' => true],
+ ['key' => 'deliveryAddressLine', 'text' => 'L.-Adr.', 'required' => true],
+ ['key' => 'deliveryAddressPLZ', 'text' => 'L.-Adr. PLZ', 'required' => true],
+ ['key' => 'deliveryAddressCity', 'text' => 'L.-Adr. Ort', 'required' => true],
+ ['key' => 'status',
+ 'text' => 'Status',
+ 'required' => true,
+ 'table' => ['filter' => 'select'],
+ 'modal' => ['type' => 'select',
+ 'items' => [['value' => 'new', 'text' => 'Neu'],
+ ['value' => 'accepted', 'text' => 'Akzeptiert'],
+ ['value' => 'invoiced', 'text' => 'In Rechnung gestellt'],]]],
+ ['key' => 'positions', 'text' => 'Positionen', 'required' => true, 'table' => false, 'modal' => false],
+ ['key' => 'create', 'text' => 'Erstellt', 'required' => false, 'modal' => false, 'table' => ['filter' => 'date']],
+ ['key' => 'createBy',
+ 'text' => 'Erstellt von',
+ 'required' => true,
+ 'type' => 'autocomplete',
+ 'table' => ['class' => 'text-nowrap', 'filter' => 'select'],
+ 'modal' => ['items' => [], 'type' => 'select',]],
+
+ ['key' => 'actions',
+ 'text' => 'Aktionen',
+ 'required' => false,
+ 'modal' => false,
+ 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center']],];
+
+ protected array $additionalActions = [['key' => 'openHistory', 'title' => 'Historie', 'class' => 'fas fa-history text-primary'],
+ ['key' => 'print', 'title' => 'Drucken', 'class' => 'fas fa-print text-primary'],
+ ['key' => 'printWithPrice', 'title' => 'Drucken mit Preis', 'class' => 'fas fa-print text-success'],
+ ];
+
+ protected array $infoMessages = ['create' => 'Lieferschein wurde erstellt.',
+ 'update' => 'Lieferschein wurde aktualisiert',
+ 'delete' => 'Lieferschein wurde gelöscht',
+ 'noChanges' => 'Keine Änderungen vorgenommen'];
+
+ protected function prepareCrudConfig() {
+ $users = array_map(function ($user) {
+ return ['value' => intval($user->id), 'text' => $user->name];
+ }, UserModel::search());
+
+ $this->columns[array_search('createBy', array_column($this->columns, 'key'))]['modal']['items'] = $users;
+ }
+
+ protected function beforeCreate($postData): bool {
+ // if postdata status is not new we return an error
+ if ($postData['status'] !== 'new') {
+ http_response_code(500);
+ self::returnJson(['success' => false, 'message' => 'Status muss "Neu" sein']);
+ die();
+ }
+ $postData['positions'] = json_encode($postData['positions']);
+ return true;
+ }
+
+ protected function customAutoCompleteBillingAddressId($id) {
+ $address = new Address($id);
+ if ($address->id) {
+ $result = ['id' => $address->id,
+ 'title' => str_replace("'", "\\'", str_replace(["\n",
+ "\r"], " ", $address->getCompanyOrName())) . " (" . $address->zip . " " . $address->city . ", " . $address->street . ")" . (($address->customer_number) ? " [" . $address->customer_number . "]" : "")];
+ return $result;
+ }
+ }
+
+ protected function beforeUpdate($postData): bool {
+ $postData['positions'] = json_encode($postData['positions']);
+ (new WarehouseHistoryController)->create($postData, $this->mod);
+ return true;
+ }
+
+ protected function getHistoryAction() {
+ $historyEntries = [];
+
+ // remove all history elements where key is positions
+
+ foreach ((new WarehouseHistoryController)->getHistory($this->request->id, $this->mod, $this->columns) as $entry) {
+ if ($entry['key'] !== 'positions') {
+ $historyEntries[] = $entry;
+ }
+ }
+
+// $historyEntries = array_filter($historyEntries, function ($entry) {
+// return $entry['key'] !== 'positions';
+// });
+
+ self::returnJson($historyEntries);
+ }
+
+ protected function getArticleAddressPriceAction() {
+ $articleId = $this->request->articleId;
+ $addressId = $this->request->addressId;
+
+ if (strlen($articleId) < 1) {
+ http_response_code(500);
+ self::returnJson(['success' => false, 'message' => 'Keine Artikel ID gefunden']);
+ }
+
+ if (strlen($addressId) < 1) {
+ http_response_code(500);
+ self::returnJson(['success' => false, 'message' => 'Keine Adress ID gefunden']);
+ }
+
+ //TODO: implement a select to select price category for each address
+ // for now we default with price with name "Verkauf"
+ $prices = WarehouseArticlePriceTypeModel::getAll(['title' => 'Verkauf']);
+ // if array is empty we return an error
+ if (empty($prices)) {
+ http_response_code(500);
+ self::returnJson(['success' => false, 'message' => 'Keine Preiskategorie gefunden']);
+ }
+ $priceType = $prices[0]->title;
+
+ $article = WarehouseArticleModel::get($articleId);
+ $sellPrices = json_decode($article->cheapestSellPrice, true);
+ $sellPrice = array_search($priceType, array_column($sellPrices, 'title'));
+
+ if (empty($sellPrice)) {
+ http_response_code(500);
+ self::returnJson(['success' => false, 'message' => 'Kein Preis gefunden']);
+ }
+
+ self::returnJson(['success' => true, 'price' => $sellPrices[$sellPrice]['price']]);
+ }
+
+ protected function getDeliveryAddressesAction() {
+ $billingAddressId = $this->request->billingAddressId;
+ if (strlen($billingAddressId) < 1) {
+ http_response_code(500);
+ self::returnJson(['success' => false, 'message' => 'Keine Rechnungsadresse gefunden']);
+ }
+
+ $deliveryAddresses = WarehouseShippingNoteModel::getAll(['billingAddressId' => $billingAddressId]);
+ // TODO: maybe this should be improved as it is kinda hacky
+ $result = [];
+ foreach ($deliveryAddresses as $deliveryAddress) {
+ $found = false;
+ foreach ($result as $r) {
+ if ($r->deliveryAddressName == $deliveryAddress->deliveryAddressName && $r->deliveryAddressLine == $deliveryAddress->deliveryAddressLine) {
+ $found = true;
+ break;
+ }
+ }
+ if ($found) {
+ continue;
+ }
+ $result[] = $deliveryAddress;
+ }
+
+ self::returnJson($result);
+ }
+
+ protected function getAllTextElementsAction() {
+ $textElements = WarehouseShippingNoteTextElementModel::getAll();
+ self::returnJson($textElements);
+ }
+
+ protected function createPDFAction() {
+ $id = $this->request->id;
+ if (strlen($id) < 1) {
+ http_response_code(500);
+ self::returnJson(['success' => false, 'message' => 'Lieferschein wurde nicht gefunden']);
+ }
+
+ $shippingNote = WarehouseShippingNoteModel::get($id);
+ $address = AddressModel::getOne($shippingNote->billingAddressId);
+ $positions = [];
+
+ // loop through all positions and add articleTitle and articleDescription to each position entry
+ foreach (json_decode($shippingNote->positions, true) as $position) {
+ $article = WarehouseArticleModel::get($position['article']);
+ $position['articleTitle'] = $article->title;
+ $position['articleDescription'] = $article->description;
+ $position['articleUnit'] = $article->unit;
+ $positions[] = $position;
+ }
+
+ $textElements = [];
+ // parse shippingNote.textElements ({"1":true,"2":true}) to array, fetch each text element and put content into array
+ $shippingNoteTextElements = json_decode($shippingNote->textElements, true);
+ foreach ($shippingNoteTextElements as $key => $value) {
+ if ($value) {
+ $textElement = WarehouseShippingNoteTextElementModel::get($key);
+ $textElements[] = $textElement->content;
+ }
+ }
+ if (empty($textElements)) {
+ $textElements = null;
+ }
+
+ $pdf_vars = ["shippingNote" => $shippingNote,
+ "positions" => $positions,
+ "textElements" => $textElements,
+ "showPrices" => isset($_GET['price']) && $_GET['price'] == "true",
+ "bank_iban" => TT_INVOICE_BANK_IBAN,
+ "bank_bic" => TT_INVOICE_BANK_BIC,
+ "bank_bank" => TT_INVOICE_BANK_BANK,
+ "bank_owner" => TT_INVOICE_BANK_OWNER];
+
+ // Replace placeholders in header
+ // create shipping note in this format LS2024-X0001
+ // pad number on the left side with zeros
+ $shippingNoteNumber = "LS" . date("Y", $shippingNote->create) . "-" . str_pad($shippingNote->id, 4, "0", STR_PAD_LEFT);
+ $headerHtml = file_get_contents(BASEDIR . "/Layout/default/WarehouseShippingNote/PDF_HEADER.html");
+ $headerHtml = str_replace("{{ basedir }}", BASEDIR, $headerHtml);
+ $headerHtml = str_replace("{{ addressLine_1 }}", $shippingNote->deliveryAddressName, $headerHtml);
+ $headerHtml = str_replace("{{ addressLine_2 }}", $shippingNote->deliveryAddressLine, $headerHtml);
+ $headerHtml = str_replace("{{ addressLine_3 }}", $shippingNote->deliveryAddressPLZ . " " . $shippingNote->deliveryAddressCity, $headerHtml);
+ $headerHtml = str_replace("{{ addressLine_4 }}", "", $headerHtml);
+ $headerHtml = str_replace("{{ addressLine_5 }}", "", $headerHtml);
+ $headerHtml = str_replace("{{ customerNumber }}", $address->customer_number, $headerHtml);
+ $headerHtml = str_replace("{{ shippingNoteNumber }}", $shippingNoteNumber, $headerHtml);
+ $headerHtml = str_replace("{{ shippingNoteDate }}", date("d.m.Y", $shippingNote->create), $headerHtml);
+
+ $headerFile = BASEDIR . "/var/temp/shipping-note_header-" . date("U") . "-" . rand(1000, 9999) . ".html";
+ file_put_contents($headerFile, $headerHtml);
+
+
+ // Replace placeholders in header
+ $footerHtml = file_get_contents(BASEDIR . "/Layout/default/WarehouseShippingNote/PDF_FOOTER.html");
+ $footerHtml = str_replace("{{ bank_iban }}", TT_INVOICE_BANK_IBAN_FORMATTED, $footerHtml);
+ $footerHtml = str_replace("{{ bank_bic }}", TT_INVOICE_BANK_BIC, $footerHtml);
+ $footerHtml = str_replace("{{ bank_bank }}", TT_INVOICE_BANK_BANK, $footerHtml);
+ $footerHtml = str_replace("{{ bank_owner }}", TT_INVOICE_BANK_OWNER, $footerHtml);
+
+
+ $footerFile = BASEDIR . "/var/temp/shipping-note_header-" . date("U") . "-" . rand(1000, 9999) . ".html";
+ file_put_contents($footerFile, $footerHtml);
+
+
+ $pdf = new PdfForm("WarehouseShippingNote/PDF_MAIN", $pdf_vars);
+ $wkhtmltopdfArgs = "--header-html $headerFile --footer-html $footerFile";
+ $filename = $pdf->render($wkhtmltopdfArgs);
+
+ // return the pdf and die so the client sees the pdf not the filename
+ header('Content-Type: application/pdf');
+ header('Content-Disposition: inline; filename="' . $filename . '"');
+ readfile($filename);
+ }
+}
\ No newline at end of file
diff --git a/application/WarehouseShippingNote/WarehouseShippingNoteModel.php b/application/WarehouseShippingNote/WarehouseShippingNoteModel.php
new file mode 100644
index 000000000..8e284d797
--- /dev/null
+++ b/application/WarehouseShippingNote/WarehouseShippingNoteModel.php
@@ -0,0 +1,17 @@
+ 'title', 'text' => 'Titel', 'required' => true],
+ ['key' => 'content', 'text' => 'Text', 'required' => true, 'modal' => []],
+ ['key' => 'create', 'text' => 'Erstellt', 'required' => false, 'modal' => false, 'table' => ['filter' => 'datetime', 'class' => 'text-nowrap']],
+ ['key' => 'createBy', 'text' => 'Erstellt von', 'required' => false, 'modal' => [
+ 'type' => 'select', 'items' => [],'visible' => false], ],
+ ['key' => 'actions', 'text' => 'Aktionen', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center', 'priority' => 10]],
+ ];
+ // @formatter:on
+
+ protected array $infoMessages = ['create' => 'Lieferschein Textelement wurde erstellt',
+ 'update' => 'Lieferschein Textelement wurde aktualisiert',
+ 'delete' => 'Lieferschein Textelement wurde gelöscht',
+ 'noChanges' => 'Keine Änderungen'];
+
+ protected function prepareCrudConfig() {
+ // add all users to createBy column
+ $this->columns[array_search('createBy', array_column($this->columns, 'key'))]['modal']['items'] = array_map(function ($user) {
+ return ['value' => $user->id, 'text' => $user->name];
+ }, UserModel::getAll());
+ }
+
+ protected function beforeUpdate($postData): bool {
+ (new WarehouseHistoryController)->create($postData, $this->mod);
+ return true;
+ }
+
+ protected function getHistoryAction() {
+ $history = WarehouseHistoryModel::getByRowId($this->request->id, $this->mod);
+
+ $history = array_map(function ($item) {
+ $item = (array) $item;
+
+ $item['columnHeader'] = $this->columns[array_search($item['key'], array_column($this->columns, 'key'))]['text'];
+ return $item;
+ }, $history);
+
+ self::returnJson($history);
+ }
+
+}
\ No newline at end of file
diff --git a/application/WarehouseShippingNoteTextElement/WarehouseShippingNoteTextElementModel.php b/application/WarehouseShippingNoteTextElement/WarehouseShippingNoteTextElementModel.php
new file mode 100644
index 000000000..c95c53237
--- /dev/null
+++ b/application/WarehouseShippingNoteTextElement/WarehouseShippingNoteTextElementModel.php
@@ -0,0 +1,10 @@
+getEnvironment() == "thetool") {
+ // Create Tables
+ $WarehouseShippingNoteTextElement = $this->table("WarehouseShippingNoteTextElement", ["signed" => true]);
+ $WarehouseShippingNoteTextElement
+ ->addColumn('title', 'string', ['null' => false])
+ ->addColumn('content', 'text', ['null' => false])
+ ->addColumn('create', 'integer', ['null' => false, 'default' => 1728541890])
+ ->addColumn('createBy', 'integer', ['null' => false, 'default' => 1])
+ ->create();
+
+ $WarehouseShippingNote = $this->table("WarehouseShippingNote", ["signed" => true]);
+ $WarehouseShippingNote
+ ->addColumn('billingAddressId', 'integer', ['null' => false])
+ ->addColumn('deliveryAddressName', 'string', ['null' => false])
+ ->addColumn('deliveryAddressLine', 'string', ['null' => false])
+ ->addColumn('deliveryAddressPLZ', 'string', ['null' => false])
+ ->addColumn('deliveryAddressCity', 'string', ['null' => false])
+ ->addColumn('status', 'enum', ['values' => ['new', 'accepted', 'invoiced'], 'null' => false])
+ ->addColumn('positions', 'text', ['null' => false])
+ ->addColumn('textElements', 'text', ['null' => false])
+ ->addColumn('create', 'integer', ['null' => false, 'default' => 1728541890])
+ ->addColumn('createBy', 'integer', ['null' => false, 'default' => 1])
+ ->create();
+
+ $WarehouseOrder = $this->table("WarehouseOrder", ["signed" => true]);
+ $WarehouseOrder
+ ->addColumn('distributorId', 'integer', ['null' => false])
+ ->addColumn('intRef', 'string', ['null' => true])
+ ->addColumn('extRef', 'string', ['null' => true])
+ ->addColumn('status', 'enum', ['values' => ['new', 'accepted', 'sent', 'done'], 'null' => false, 'default' => 'new'])
+ ->addColumn('sum', 'float', ['null' => true])
+ ->addColumn('trackingNumber', 'string', ['null' => true])
+ ->addColumn('create', 'integer', ['null' => false, 'default' => 1728541890])
+ ->addColumn('createBy', 'integer', ['null' => false, 'default' => 1])
+ ->create();
+
+ $WarehouseOrderItem = $this->table("WarehouseOrderItem", ["signed" => true]);
+ $WarehouseOrderItem
+ ->addColumn('orderId', 'integer', ['null' => false])
+ ->addColumn('articleId', 'integer', ['null' => false])
+ ->addColumn('quantity', 'integer', ['null' => false])
+ ->addColumn('price', 'float', ['null' => false])
+ ->addForeignKey('orderId', 'WarehouseOrder', 'id')
+ ->addForeignKey('articleId', 'WarehouseArticle', 'id')
+ ->create();
+
+ // Delete Table
+ $WarehouseRevenueAccount = $this->table("WarehouseRevenueAccount");
+ $WarehouseRevenueAccount->drop()->save();
+
+ // Modify Tables
+ $WarehouseLocation = $this->table("WarehouseLocation");
+ $WarehouseLocation
+ ->addColumn('description', 'text', ['null' => true])
+ ->addColumn('createBy', 'integer', ['null' => false, 'default' => 1728541890])
+ ->addColumn('create', 'integer', ['null' => false, 'default' => 1])
+ ->update();
+
+ $WarehouseItem = $this->table("WarehouseItem");
+ $WarehouseItem
+ ->changeColumn('quantity', 'integer', ['null' => true])
+ ->addColumn('rack', 'string', ['null' => true])
+ ->addColumn('shelf', 'string', ['null' => true])
+ ->addColumn('createBy', 'integer', ['null' => false, 'default' => 1728541890])
+ ->addColumn('create', 'integer', ['null' => false, 'default' => 1])
+ ->update();
+
+ $WarehouseArticlePriceType = $this->table("WarehouseArticlePriceType");
+ $WarehouseArticlePriceType
+ ->addColumn('description', 'string', ['null' => true])
+ ->addColumn('defaultPriceFactor', 'float', ['null' => true])
+ ->addColumn('createBy', 'integer', ['null' => false, 'default' => 1728541890])
+ ->addColumn('create', 'integer', ['null' => false, 'default' => 1])
+ ->update();
+
+ // Set nullable createBy and create columns
+ $WarehouseLocation
+ ->changeColumn('createBy', 'integer', ['null' => true])
+ ->changeColumn('create', 'integer', ['null' => true])
+ ->save();
+
+ $WarehouseItem
+ ->changeColumn('createBy', 'integer', ['null' => true])
+ ->changeColumn('create', 'integer', ['null' => true])
+ ->save();
+
+ $WarehouseArticlePriceType
+ ->changeColumn('createBy', 'integer', ['null' => true])
+ ->changeColumn('create', 'integer', ['null' => true])
+ ->save();
+ }
+ }
+
+ public function down(): void
+ {
+ if ($this->getEnvironment() == "thetool") {
+ // Drop created tables
+ $this->table('WarehouseShippingNoteTextElement')->drop()->save();
+ $this->table('WarehouseShippingNote')->drop()->save();
+ $this->table('WarehouseOrder')->drop()->save();
+ $this->table('WarehouseOrderItem')->drop()->save();
+
+ // Recreate deleted table
+ $WarehouseRevenueAccount = $this->table("WarehouseRevenueAccount");
+ $WarehouseRevenueAccount
+ ->addColumn('revenueAccountNumber', 'integer', ['null' => false])
+ ->addColumn('title', 'string', ['null' => false])
+ ->create();
+
+ // Revert modifications
+ $WarehouseLocation = $this->table("WarehouseLocation");
+ $WarehouseLocation
+ ->removeColumn('description')
+ ->removeColumn('createBy')
+ ->removeColumn('create')
+ ->update();
+
+ $WarehouseItem = $this->table("WarehouseItem");
+ $WarehouseItem
+ ->changeColumn('quantity', 'integer', ['null' => false])
+ ->removeColumn('rack')
+ ->removeColumn('shelf')
+ ->removeColumn('createBy')
+ ->removeColumn('create')
+ ->update();
+
+ $WarehouseArticlePriceType = $this->table("WarehouseArticlePriceType");
+ $WarehouseArticlePriceType
+ ->removeColumn('description')
+ ->removeColumn('defaultPriceFactor')
+ ->removeColumn('createBy')
+ ->removeColumn('create')
+ ->update();
+ }
+ }
+}
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
index af580e971..f2ae0ab7a 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -29,7 +29,7 @@ services:
adminer:
image: adminer
ports:
- - "8080:8080"
+ - "8088:8080"
volumes:
- ./docker/adminer/php.ini:/etc/php/7.4/cli/conf.d/php.local.ini
diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile
index 73c122e5a..e97ec1a02 100644
--- a/docker/php/Dockerfile
+++ b/docker/php/Dockerfile
@@ -1,6 +1,13 @@
# Use Debian Bookworm as base image
FROM debian:bookworm
+# Install wkhtmltopdf
+RUN apt update
+RUN apt install wget libfontenc1 xfonts-75dpi xfonts-base xfonts-encodings xfonts-utils openssl build-essential libssl-dev libxrender-dev git-core libx11-dev libxext-dev libfontconfig1-dev libfreetype6-dev fontconfig -y
+# wget https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6-1/wkhtmltox_0.12.6-1.stretch_amd64.deb
+# dpkg ignore
+
+
# Install apache2 and PHP and PHP modules
RUN apt update && \
apt install -y apache2 curl cron unzip php8.2 php8.2-curl php8.2-cli php8.2-mysqli php8.2-gd php8.2-zip php8.2-dom php8.2-mbstring && \
diff --git a/lib/TTCrud/TTCrud.php b/lib/TTCrud/TTCrud.php
index fde700436..d6f17358c 100644
--- a/lib/TTCrud/TTCrud.php
+++ b/lib/TTCrud/TTCrud.php
@@ -4,6 +4,7 @@
* Class TTCrud
* @property string $headerTitle
* @property string $createText
+ * @property string|null $historyController
* @property array $columns
* @property array $additionalActions
* @property array $infoMessages
@@ -32,7 +33,7 @@ class TTCrud extends mfBaseController {
$this->redirect("Dashboard");
}
- $modelName = $this->mod . 'Model';
+ $modelName = str_replace('Controller', 'Model', get_class($this));
$this->model = new $modelName();
$this->postData = json_decode(file_get_contents('php://input'), true);
$this->checkArray = $this->getCheckArray();
@@ -88,6 +89,20 @@ class TTCrud extends mfBaseController {
return $newColumn;
}, $this->columns);
+ // check if in columns array there is a column with key "actions" and if so, we set the priority of the first column to 20 and actions to 19
+ $actionsColumn = array_filter($columns, function ($column) {
+ return $column['key'] === 'actions';
+ });
+ if (count($actionsColumn) > 0) {
+ $columns = array_map(function ($column) {
+ if ($column['key'] === 'actions') {
+ $column['priority'] = 119;
+ }
+ return $column;
+ }, $columns);
+ $columns[0]['priority'] = 120;
+ }
+
return ['key' => $this->mod,
'tableHeader' => $this->headerTitle,
'createText' => $this->createText,
@@ -105,7 +120,41 @@ class TTCrud extends mfBaseController {
$filteredAvailable = $this->model::count($filter);
$totalRows = $this->model::count();
+ // check if any column is a autocomplete to add the text to the row
+ $autoCompleteColumns = array_filter($this->columns, function ($column) {
+ return isset($column['type']) && isset($column['modal']) && isset($column['modal']['type']) && $column['type'] === 'autocomplete' && $column['modal']['type'] === 'autocomplete';
+
+ });
+ $autocompleteData = [];
+ foreach ($rows as $row) {
+ $row = (array) $row;
+ foreach ($autoCompleteColumns as $column) {
+ if (isset($autocompleteData[$column['key']][$row[$column['key']]])) {
+ continue;
+ }
+
+ // if function customAutoComplete"COLUMN_KEY" is defined, we call it instead of the default
+ $data = null;
+ if (method_exists($this, 'customAutoComplete' . ucfirst($column['key']))) {
+ $data = $this->{'customAutoComplete' . ucfirst($column['key'])}($row[$column['key']]);
+ } else {
+
+ $autoCompleteModelName = explode('/', $column['modal']['apiUrl'])[0] . 'Model';
+ $autoCompleteModel = new $autoCompleteModelName();
+ $data = $autoCompleteModel::get($row[$column['key']]);
+
+ // TODO: fix that keys can be anything
+ if (isset($data->name) && !isset($data->title)) {
+ $data->title = $data->name;
+ }
+ }
+
+ $autocompleteData[$column['key']][$row[$column['key']]] = $data;
+ }
+ }
+
self::returnJson(["rows" => $rows,
+ "autoCompleteData" => $autocompleteData,
"pagination" => ["page" => $page,
"total_pages" => ceil($filteredAvailable / $perPage),
"per_page" => $perPage,
@@ -114,6 +163,14 @@ class TTCrud extends mfBaseController {
}
protected function createAction() {
+ // if this->model has property createBy, set it to the current user id and create to current epoch time
+ if (property_exists($this->model, 'createBy')) {
+ $this->postData['createBy'] = $this->user->id;
+ }
+ if (property_exists($this->model, 'create')) {
+ $this->postData['create'] = time();
+ }
+
Helper::validateArray($this->postData, $this->checkArray);
if (method_exists($this, 'beforeCreate') && !$this->beforeCreate($this->postData)) {
@@ -178,6 +235,18 @@ class TTCrud extends mfBaseController {
return ['value' => $item->id, 'text' => $item->$textKey];
}, $data));
}
+
+ protected function getByIdAction() {
+ $id = $_GET['id'] ?? null;
+ if (!$id || !is_numeric($id)) {
+ http_response_code(500);
+ self::returnJson(['success' => false, 'message' => 'No ID provided.']);
+ die();
+ }
+
+ $data = (array) $this->model::get($id);
+ self::returnJson($data);
+ }
}
?>
diff --git a/lib/TTCrudBaseModel/TTCrudBaseModel.php b/lib/TTCrudBaseModel/TTCrudBaseModel.php
index c296fd258..3184ffa10 100644
--- a/lib/TTCrudBaseModel/TTCrudBaseModel.php
+++ b/lib/TTCrudBaseModel/TTCrudBaseModel.php
@@ -19,7 +19,15 @@ class TTCrudBaseModel {
$sqlValues = [];
foreach ($data as $field => $value) {
if (!property_exists(get_called_class(), $field)) {
- throw new Exception("Field $field does not exist in " . get_called_class());
+ die(json_encode([
+ "status" => "error",
+ "error" => "Field $field does not exist in " . get_called_class(),
+ "data" => $data
+ ]));
+ }
+
+ if (is_array($value)) {
+ $value = json_encode($value);
}
$sqlValues[] = $value === null ? 'NULL' : "'" . $db->real_escape_string($value) . "'";
@@ -62,7 +70,12 @@ class TTCrudBaseModel {
}
if (!isset($data[$field])) {
- throw new Exception("Required field $field is missing in data array");
+ http_response_code(500);
+ die(json_encode([
+ "status" => "error",
+ "error" => "Required field $field is missing in data array",
+ "data" => $data
+ ]));
}
}
}
@@ -104,6 +117,7 @@ class TTCrudBaseModel {
$sql = "WHERE 1=1";
foreach ($filter as $key => $value) {
if (!property_exists(get_called_class(), $key)) {
+ http_response_code(500);
throw new Exception("Field $key does not exist in " . get_called_class());
}
$sql .= Helper::generateFilterCondition($value, $key, gettype($value) === "integer");
@@ -161,6 +175,10 @@ class TTCrudBaseModel {
$value = null;
}
+ if (is_array($value)) {
+ $value = json_encode($value);
+ }
+
$values[] = $value === null ? "`$field` = NULL" : "`$field` = '" . $db->real_escape_string($value) . "'";
}
diff --git a/lib/mvcfronk/mfExceptionhandler/mfExceptionhandler.php b/lib/mvcfronk/mfExceptionhandler/mfExceptionhandler.php
index a457dc26c..0e83edf95 100644
--- a/lib/mvcfronk/mfExceptionhandler/mfExceptionhandler.php
+++ b/lib/mvcfronk/mfExceptionhandler/mfExceptionhandler.php
@@ -29,6 +29,9 @@ class mfExceptionhandler {
public function __toString() {
$str="[".$this->Time."] ";
if(is_numeric($this->Code) && $this->Code > 0) {
+ if($this->Code == 404 || $this->Code == 500) {
+ http_response_code($this->Code);
+ }
$str.="(Error code ".$this->Code.") ";
}
diff --git a/lib/mvcfronk/mfRouter/mfRouter.php b/lib/mvcfronk/mfRouter/mfRouter.php
index e6752dfdf..01c6f6193 100644
--- a/lib/mvcfronk/mfRouter/mfRouter.php
+++ b/lib/mvcfronk/mfRouter/mfRouter.php
@@ -168,7 +168,8 @@ class mfRouter {
}
}
-
+
+ $baseurl = $baseurl ?? "";
$baseurl = preg_replace('@/$@', '', $baseurl);
define("MFFANCYBASEURL",$baseurl);
}
diff --git a/public/js/pages/WarehouseAdministration/WarehouseAdministration.js b/public/js/pages/WarehouseAdministration/WarehouseAdministration.js
new file mode 100644
index 000000000..5ebc226cf
--- /dev/null
+++ b/public/js/pages/WarehouseAdministration/WarehouseAdministration.js
@@ -0,0 +1,56 @@
+Vue.component('warehouse-administration', {
+ //language=Vue
+ template: `
+
+
+
+
+
+ Lagerorte für alle Mitarbeiter erstellen
+
+
+ Alle Verkaufspreise updaten
+
+
+
+ `,
+ data() {
+ return {
+ isLoading: false,
+ window: window,
+ };
+ },
+ methods: {
+ async createLocationsForAllEmployees() {
+ this.isLoading = true;
+ const response = await axios.get(window.TT_CONFIG.BASE_URL + '/createLocations');
+
+ if (response.data.success) {
+ this.window.notify('success', response.data.message || 'Lagerorte wurden erfolgreich erstellt.');
+ } else {
+ this.window.notify('error', response.data.message || 'Fehler beim Erstellen der Lagerorte.');
+ }
+ this.isLoading = false;
+ },
+ async updateAllSalesPrices() {
+ this.isLoading = true;
+ const response = await axios.get(window.TT_CONFIG.BASE_PATH + '/WarehouseArticle/updatePrices');
+ if (response.data.success) {
+ this.window.notify('success', 'Verkaufspreise wurden erfolgreich aktualisiert.');
+ } else {
+ this.window.notify('error', 'Fehler beim Aktualisieren der Verkaufspreise.');
+ }
+ this.isLoading = false;
+ },
+ sleep(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+ }
+});
\ No newline at end of file
diff --git a/public/js/pages/WarehouseArticle/WarehouseArticle.js b/public/js/pages/WarehouseArticle/WarehouseArticle.js
index 0d9b6d676..fa6c05c06 100644
--- a/public/js/pages/WarehouseArticle/WarehouseArticle.js
+++ b/public/js/pages/WarehouseArticle/WarehouseArticle.js
@@ -250,11 +250,14 @@ Vue.component('warehouse-article', {
//language=Vue
template: `
+
+ @editThresholdEntries="thresholdModal = true; thresholdModalId = $event.id"
+ @addToCart="addShoppingCartModal = true; addShoppingCartModalId = $event.id"
+ >
@@ -268,6 +271,38 @@ Vue.component('warehouse-article', {
+
+
+
+
+
+
+
+ Es werden Bestellungen an folgende Lieferanten gesendet:
+
+
+
{{order.distributor[0].name}} - {{order.orderAmount}} €
+
+
+
+ Artikel
+ Menge
+ Preis
+ Summe
+
+
+ {{item.title}}
+ {{item.amount}}
+ {{item.purchasePrice}} €
+ {{item.sum.toFixed(2)}} €
+
+
+
+
+
@@ -275,19 +310,56 @@ Vue.component('warehouse-article', {
`, data() {
return {
- window: window,
- historyModal: false,
- historyModalId: null,
- distributorModal: false,
- distributorModalId: null,
- thresholdModal: false,
- thresholdModalId: null,
- priceModal: false,
- priceModalId: null
+ window: window,
+ historyModal: false,
+ historyModalId: null,
+ distributorModal: false,
+ distributorModalId: null,
+ thresholdModal: false,
+ thresholdModalId: null,
+ priceModal: false,
+ priceModalId: null,
+ shoppingCart: [],
+ addShoppingCartModal: false,
+ addShoppingCartModalId: null,
+ addShoppingCartModalCount: '',
+ confirmOrderModal: false,
+ confirmOrderModalData: null,
+
}
}, methods: {
refreshTable() {
this.$refs.table.$refs.table.refreshTable();
+ }, async addToShoppingCart() {
+ if (this.addShoppingCartModalCount < 1) { // Check if amount is set
+ window.notify('error', 'Bitte geben Sie eine Menge ein.');
+ return;
+ }
+ if (this.shoppingCart.some(item => item.itemId === this.addShoppingCartModalId)) { // Check if same article is already in cart
+ window.notify('error', 'Artikel bereits im Warenkorb.');
+ return;
+ }
+
+ const response = await axios.get(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticle/getById?id=${this.addShoppingCartModalId}`);
+ this.shoppingCart.push({amount: parseInt(this.addShoppingCartModalCount), itemId: this.addShoppingCartModalId, title: response.data.title});
+ this.addShoppingCartModal = false;
+ this.addShoppingCartModalId = null;
+ this.addShoppingCartModalCount = '';
+ window.notify('success', 'Artikel erfolgreich hinzugefügt.');
+ }, async prepareOrder() {
+ const response = await axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticle/prepareOrder`, this.shoppingCart);
+ this.confirmOrderModal = true;
+ this.confirmOrderModalData = response.data;
+ },
+ async createOrder() {
+ const response = await axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseOrder/createOrder`, this.confirmOrderModalData);
+ this.confirmOrderModal = false;
+ this.confirmOrderModalData = null;
+ this.shoppingCart = [];
+ window.notify(response.data.success ? 'success' : 'error', response.data.message);
+ setTimeout(() => {
+ window.location.href = `${window['TT_CONFIG']['BASE_PATH']}/WarehouseOrder`;
+ }, 2000);
}
}
})
diff --git a/public/js/pages/WarehouseArticlePriceType/WarehouseArticlePriceType.js b/public/js/pages/WarehouseArticlePriceType/WarehouseArticlePriceType.js
new file mode 100644
index 000000000..d56c62c9f
--- /dev/null
+++ b/public/js/pages/WarehouseArticlePriceType/WarehouseArticlePriceType.js
@@ -0,0 +1,14 @@
+Vue.component('warehouse-article-price-type', {
+ //language=Vue
+ template: `
+
+
+
+
+
+ `, data() {
+ return {
+ window: window, historyModal: false, historyModalId: null,
+ }
+ },
+})
diff --git a/public/js/pages/WarehouseDistributor/WarehouseDistributor.js b/public/js/pages/WarehouseDistributor/WarehouseDistributor.js
index 2763aec39..37ad1bc69 100644
--- a/public/js/pages/WarehouseDistributor/WarehouseDistributor.js
+++ b/public/js/pages/WarehouseDistributor/WarehouseDistributor.js
@@ -2,6 +2,7 @@ Vue.component('warehouse-distributor', {
//language=Vue
template: `
+
diff --git a/public/js/pages/WarehouseEShop/WarehouseEShop.js b/public/js/pages/WarehouseEShop/WarehouseEShop.js
index b8a9c74cc..089c253aa 100644
--- a/public/js/pages/WarehouseEShop/WarehouseEShop.js
+++ b/public/js/pages/WarehouseEShop/WarehouseEShop.js
@@ -1,47 +1,3 @@
-
-
-Vue.component('tt-expandable-shopping-cart', {
- props: {
- cartItems: Array,
- },
- data() {
- return {
- isExpanded: false,
- };
- },
- methods: {
- },
- template: `
-
- `
-});
-
-
Vue.component('warehouse-e-shop', {
//language=Vue
template: `
diff --git a/public/js/pages/WarehouseEShopOrder/WarehouseEShopOrder.js b/public/js/pages/WarehouseEShopOrder/WarehouseEShopOrder.js
index b7a535d8a..65a63fba1 100644
--- a/public/js/pages/WarehouseEShopOrder/WarehouseEShopOrder.js
+++ b/public/js/pages/WarehouseEShopOrder/WarehouseEShopOrder.js
@@ -82,7 +82,6 @@ Vue.component('warehouse-e-shop-order', {
}
}, async mounted() {
const response = await axios.get(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseEShopOrder/getAllItemsPerOrder`);
- console.log(response.data);
this.articleItems = response.data;
}, methods: {
async sendSingleOrderEmail() {
diff --git a/public/js/pages/WarehouseHistory/WarehouseHistoryModal.js b/public/js/pages/WarehouseHistory/WarehouseHistoryModal.js
index 47df603f7..3c2ced5f2 100644
--- a/public/js/pages/WarehouseHistory/WarehouseHistoryModal.js
+++ b/public/js/pages/WarehouseHistory/WarehouseHistoryModal.js
@@ -43,3 +43,97 @@ Vue.component('warehouse-history-modal', {
}
}
})
+
+Vue.component('tt-expandable-shopping-cart', {
+ props: {
+ cartItems: Array,
+ },
+ data() {
+ return {
+ isExpanded: false,
+ };
+ },
+ methods: {
+ },
+ template: `
+
+ `
+});
+
+
+//TODO: put this in its own file
+//TODO: also for tt-crud or vuehelper create a check for utility folder and include all js files in there to allow adding custom components that are not part of the core
+//TODO: also add a component for a switch like this as we will need it more often either with value or doing redirect on click
+
+Vue.component('warehouse-administration-switch', {
+ //language=Vue
+ template: `
+
+
+ Lieferanten
+ Lagerorte
+ Preistypen
+ LS Texte
+ Admin-Tools
+
+
+
+ `,
+ props: ['value'],
+ data() {
+ return {
+ isOverflowing: false,
+ showDropdown: false,
+ window: window,
+ };
+ },
+ mounted() {
+ this.checkOverflow();
+ window.addEventListener('resize', this.checkOverflow);
+ },
+ beforeDestroy() {
+ window.removeEventListener('resize', this.checkOverflow);
+ },
+ methods: {
+ checkOverflow() {
+ this.isOverflowing = window.innerWidth < 650
+ },
+ },
+})
diff --git a/public/js/pages/WarehouseItem/WarehouseItem.js b/public/js/pages/WarehouseItem/WarehouseItem.js
index 7c678c55f..9e85f4ff9 100644
--- a/public/js/pages/WarehouseItem/WarehouseItem.js
+++ b/public/js/pages/WarehouseItem/WarehouseItem.js
@@ -2,7 +2,13 @@ Vue.component('warehouse-item', {
//language=Vue
template: `
-
+
+
+ {{ row.rack }} | {{ row.shelf }}
+ {{ row.rack }}
+ -
+
+
`, data() {
diff --git a/public/js/pages/WarehouseLocation/WarehouseLocation.js b/public/js/pages/WarehouseLocation/WarehouseLocation.js
index db6c4f032..429bf8e83 100644
--- a/public/js/pages/WarehouseLocation/WarehouseLocation.js
+++ b/public/js/pages/WarehouseLocation/WarehouseLocation.js
@@ -2,6 +2,7 @@ Vue.component('warehouse-location', {
//language=Vue
template: `
+
diff --git a/public/js/pages/WarehouseOrder/WarehouseOrder.js b/public/js/pages/WarehouseOrder/WarehouseOrder.js
new file mode 100644
index 000000000..ab63807d5
--- /dev/null
+++ b/public/js/pages/WarehouseOrder/WarehouseOrder.js
@@ -0,0 +1,70 @@
+// noinspection JSUnusedLocalSymbols
+Vue.component('warehouse-order', {
+ //language=Vue
+ template: `
+
+
+
+
+ {{ window.moment(row.create * 1000).format('DD.MM.YYYY HH:mm:ss') }}
+
+
+
+ {{ row.sum.toFixed(2) }} €
+
+
+
+
+
+
+
+
+ {{ item.quantity }}x {{ item.articleName }} - {{ item.price.toFixed(2) }} €
+
+
+
+
+
+
+
+
+
+ `, data() {
+ return {
+ window: window, historyModal: false, historyModalId: null, observer: null, orderLazyLoad: {},
+ }
+ }, mounted() {
+ this.observer = new MutationObserver((mutations) => {
+ const lazyLoadingElements = document.querySelectorAll('.lazy-loading');
+ console.log(lazyLoadingElements);
+
+ // check row id and check if it is already defined in orderLazyLoad else alert('loading')
+ // if it is defined do nothing
+
+ for (const element of lazyLoadingElements) {
+ if (element.dataset.rowId in this.orderLazyLoad) {
+ continue;
+ }
+ this.loadOrder(element.dataset.rowId);
+ }
+
+ })
+ this.observer.observe(document.querySelector('.tt-table-container'), {childList: true, subtree: true,});
+ }, methods: {
+ async loadOrder(rowId) {
+ this.orderLazyLoad[rowId] = true;
+ // use BASE_PATH . /WarehouseOrder/getOrderItems?id= + rowId
+ const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/getOrderItems?id=${rowId}`);
+ console.log(response.data);
+ this.orderLazyLoad[rowId] = response.data;
+
+ // force re-render of the table
+ this.$refs.table.$forceUpdate();
+
+
+ }
+ }, beforeDestroy() {
+ this.observer.disconnect();
+ }
+})
diff --git a/public/js/pages/WarehouseShippingNote/WarehouseShippingNote.js b/public/js/pages/WarehouseShippingNote/WarehouseShippingNote.js
new file mode 100644
index 000000000..9d36a2e2f
--- /dev/null
+++ b/public/js/pages/WarehouseShippingNote/WarehouseShippingNote.js
@@ -0,0 +1,321 @@
+const defaultCrudModalData = {
+ billingAddressId: '',
+ deliveryAddressName: '',
+ deliveryAddressLine: '',
+ deliveryAddressPLZ: '',
+ deliveryAddressCity: '',
+ status: 'new',
+ positions: [],
+ textElements: {}
+}
+
+window.crudModalStatusOptions =
+ [{value: 'new', text: 'Neu'}, {value: 'accepted', text: 'Akzeptiert'}, {value: 'invoiced', text: 'In Rechnung gestellt', disabled: true}]
+
+// create a additional vue component for showing positions in the table with lazy loading for article titles and description
+Vue.component('warehouse-shipping-note-positions', {
+ //language=Vue
+ props: {
+ positions: Array
+ }, data() {
+ return {
+ articleData: {}, loading: false
+ }
+ }, template: `
+
+
+
+
+
+
+
+
+ {{ position.amount }}x {{ articleData[position.article]?.text }}
+
+
+
+ `, async mounted() {
+ this.loading = true;
+ for (const position of this.positions) {
+ const response = await axios.get(window.TT_CONFIG["BASE_PATH"] + '/WarehouseArticle/autoComplete?searchedID=' + position.article);
+ this.$set(this.articleData, position.article, response.data[0]);
+ }
+ this.loading = false;
+ }
+})
+
+
+// noinspection JSUnusedLocalSymbols
+Vue.component('warehouse-shipping-note', {
+ //language=Vue
+ template: `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Texte
+
+
+ {{ textElement.title }}
+
+
+
+
+ Positionen
+
+
+
+
+
+
+
+ Hinzufügen
+
+
+
+
+
+ Position
+ Artikel
+ Menge
+ Preis
+
+
+
+
+
+ {{ index + 1 }}
+ {{ articleNames[position.article] }}
+ {{ position.amount }}
+ {{ (position.price?.toFixed(2)) }} €
+
+ Löschen
+
+
+
+
+
+
+
+ Rechnungsadresse auswählen um Positionen hinzuzufügen
+
+
+
+
+
+
+
+ Lieferschein erstellen
+ Lieferscheine Freigeben
+
+
+
+
+
+
+
+
+
+ `, data() {
+ return {
+ window: window,
+ historyModal: false,
+ historyModalId: null,
+ crudModal: false,
+ crudModalSelectDeliveryAddressModeItems: [{text: 'Wie Rechnungsadresse', value: 'billing'},
+ {text: 'Bestehende Lieferadresse', value: 'existing'},
+ {text: 'Neue Lieferadresse', value: 'new'}],
+ crudModalSelectDeliveryAddressMode: 'billing',
+ crudModalDataDeliveryAddressOptions: [],
+ crudModalDataDeliveryAddressSelected: '',
+ crudModalVerifyMode: false,
+ crudModalId: null,
+ crudModalData: defaultCrudModalData,
+ crudModalAddPositionArticle: '',
+ crudModalAddPositionAmount: '',
+ crudModalAddPositionPrice: '',
+ articleNames: {},
+ textElements: [],
+ }
+ }, async mounted() {
+ const response = await axios.get(window.TT_CONFIG["BASE_PATH"] + '/WarehouseShippingNote/getAllTextElements');
+ this.textElements = response.data;
+ },
+
+ methods: {
+ async openVerifyModal() {
+ const unverifiedShippingNotes = await axios.post(window.TT_CONFIG['BASE_PATH'] + '/WarehouseShippingNote/get', {
+ "pagination": {"page": 1, "per_page": 1}, "filters": {
+ "status": "new"
+ }, "order": {"key": null, "order": "asc"}
+ });
+
+ if (unverifiedShippingNotes.data.rows.length === 0) {
+ this.window.notify('warning', 'Keine Lieferscheine zum Freigeben gefunden');
+ return;
+ }
+
+ await this.openCrudModal(unverifiedShippingNotes.data.rows[0]);
+ this.crudModalVerifyMode = true;
+
+ }, resetCrudModalData() {
+ this.crudModalData.billingAddressId = '';
+ this.crudModalData.deliveryAddressName = '';
+ this.crudModalData.deliveryAddressLine = '';
+ this.crudModalData.deliveryAddressPLZ = '';
+ this.crudModalData.deliveryAddressCity = '';
+ this.crudModalAddPositionArticle = '';
+ this.crudModalAddPositionAmount = '';
+ this.crudModalAddPositionPrice = '';
+ this.crudModalSelectDeliveryAddressMode = 'billing';
+ this.crudModalDataDeliveryAddressSelected = '';
+ this.crudModal = false;
+ }, async openCrudModal(data) {
+ this.resetCrudModalData();
+ this.crudModalVerifyMode = false;
+ if (data === 'create') {
+ this.crudModalId = 'create'
+ this.crudModalData = defaultCrudModalData
+ this.crudModal = true
+ } else {
+ const disconnectedData = JSON.parse(JSON.stringify(data));
+
+ if (disconnectedData.status !== 'new') {
+ this.window.notify('warning', 'Lieferschein kann nicht bearbeitet werden, da er bereits genehmigt wurde');
+ return;
+ }
+
+ disconnectedData.textElements = JSON.parse(disconnectedData.textElements);
+ disconnectedData.positions = JSON.parse(disconnectedData.positions);
+ for (const position of disconnectedData.positions) {
+ await this.fetchArticleNames(position.article);
+ }
+ this.crudModalId = 'update'
+ this.crudModalData = disconnectedData
+ this.crudModal = true
+ }
+ }, async addPosition() {
+ const missingFields = [];
+
+ // ---------- Check Required Fields ----------
+
+ if (!this.crudModalAddPositionArticle) missingFields.push('Artikel');
+ if (!this.crudModalAddPositionAmount) missingFields.push('Menge');
+ if (!this.crudModalAddPositionPrice) missingFields.push('Preis-Überschreibung');
+ if (missingFields.length > 0) {
+ window.notify('error', 'Bitte füllen Sie die folgenden Felder aus: ' + missingFields.join(', '));
+ return;
+ }
+
+ // ---------- Check if same article is already in positions ----------
+
+ const articleAlreadyInPositions = this.crudModalData.positions.find(position => position.article === this.crudModalAddPositionArticle);
+ if (articleAlreadyInPositions) {
+ window.notify('error', 'Artikel ist bereits in den Positionen enthalten');
+ return;
+ }
+
+ await this.fetchArticleNames(this.crudModalAddPositionArticle);
+
+ this.crudModalData.positions.push({
+ article: this.crudModalAddPositionArticle, amount: this.crudModalAddPositionAmount, price: parseFloat(this.crudModalAddPositionPrice)
+ });
+
+ //TODO: post to server
+ }, async fetchArticleNames(articleId) {
+ const response = await axios.get(window.TT_CONFIG["BASE_PATH"] + '/WarehouseArticle/autoComplete?searchedID=' + articleId);
+ this.$set(this.articleNames, articleId, response.data[0].text);
+ }, async createOrUpdate() {
+ const response = await axios.post(this.crudModalId === 'create' ? window['TT_CONFIG']['CREATE_URL'] : window['TT_CONFIG']['UPDATE_URL'],
+ this.crudModalData);
+ if (response.data.success) {
+ this.$refs.table.$refs.table.refreshTable();
+ this.resetCrudModalData();
+ this.window.notify('success', response.data.message || 'Erfolgreich gespeichert');
+ } else {
+ this.window.notify('error',
+ response.data.errors ? Object.values(response.data.errors).join('
') : response.data.message || 'Ein Fehler ist aufgetreten');
+ }
+ }, async fetchDeliveryAddresses() {
+ if (!this.crudModalData.billingAddressId || this.crudModalSelectDeliveryAddressMode !== 'existing') return;
+
+ if (this.crudModalSelectDeliveryAddressMode === 'billing') {
+ const response = await axios.get(window.TT_CONFIG["BASE_PATH"] + '/Address/api?do=getAddress&id=' + this.crudModalData.billingAddressId);
+ if (response.data.status !== 'OK' || !response.data.result.address) {
+ window.notify('error', 'Rechnungsadresse konnte nicht gefunden werden');
+ return;
+ }
+
+ this.crudModalData.deliveryAddressName =
+ response.data.result.address.company || response.data.result.address.firstname + ' ' + response.data.result.address.lastname;
+ this.crudModalData.deliveryAddressLine = response.data.result.address.street;
+ this.crudModalData.deliveryAddressPLZ = response.data.result.address.zip;
+ this.crudModalData.deliveryAddressCity = response.data.result.address.city;
+ }
+ if (!this.crudModalData.billingAddressId || this.crudModalSelectDeliveryAddressMode !== 'existing') return;
+
+ const response = await axios.get(window.TT_CONFIG["BASE_PATH"] +
+ '/WarehouseShippingNote/getDeliveryAddresses?billingAddressId=' +
+ this.crudModalData.billingAddressId);
+
+ this.crudModalDataDeliveryAddressOptions = response.data.map(address => {
+ address.value = address.id;
+ address.text = `${address.deliveryAddressName} - ${address.deliveryAddressLine}, ${address.deliveryAddressPLZ} ${address.deliveryAddressCity}`;
+ return address;
+ });
+ }
+ }, watch: {
+ crudModalAddPositionArticle: async function (newValue) {
+ if (!newValue) return;
+
+ const url = `${window.TT_CONFIG["BASE_PATH"]}/WarehouseShippingNote/getArticleAddressPrice?articleId=${newValue}&addressId=${this.crudModalData.billingAddressId}`;
+ const response = await axios.get(url);
+ this.crudModalAddPositionPrice = response.data.price;
+ },
+ crudModalData: {handler: 'fetchDeliveryAddresses', deep: true},
+ crudModalSelectDeliveryAddressMode: {handler: 'fetchDeliveryAddresses', deep: true},
+ crudModalDataDeliveryAddressSelected: function (newValue) {
+ if (!newValue) return;
+
+ const selectedAddress = this.crudModalDataDeliveryAddressOptions.find(address => address.id === parseInt(newValue));
+ if (!selectedAddress) {
+ window.notify('error', 'Lieferadresse konnte nicht gefunden werden');
+ return;
+ }
+
+ this.crudModalData.deliveryAddressName = selectedAddress.deliveryAddressName;
+ this.crudModalData.deliveryAddressLine = selectedAddress.deliveryAddressLine;
+ this.crudModalData.deliveryAddressPLZ = selectedAddress.deliveryAddressPLZ;
+ this.crudModalData.deliveryAddressCity = selectedAddress.deliveryAddressCity;
+ }
+ }
+})
\ No newline at end of file
diff --git a/public/plugins/vue/tt-components/tt-autocomplete.js b/public/plugins/vue/tt-components/tt-autocomplete.js
index 9cd962e4f..7fbc64bd5 100644
--- a/public/plugins/vue/tt-components/tt-autocomplete.js
+++ b/public/plugins/vue/tt-components/tt-autocomplete.js
@@ -1,76 +1,81 @@
+//TODO: if autocomplete is focus and the input has not a single character still show "Bitte mindestens 3 Zeichen eingeben"
+//TODO: fix the fetchSuggestions function to not show a loading spinner when the input gets cleared
+//TODO: fix so we can use arrow keys to navigate the suggestions
+
Vue.component('tt-autocomplete', {
template: `
-
+ `, // 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 },
- apiUrl: String,
- placeholder: { type: String, default: '' },
- items: { type: Array, default: () => [] },
- sm: { type: Boolean, default: true },
- row: { type: Boolean, default: false },
- },
- async mounted() {
+ value: {type: [String, Number]},
+ label: {type: String, required: false},
+ apiUrl: String,
+ placeholder: {type: String, default: ''},
+ items: {type: Array, default: () => []},
+ sm: {type: Boolean, default: true},
+ row: {type: Boolean, default: false},
+ }, async mounted() {
if (this.value && this.apiUrl) {
const response = await axios.get(`${this.apiUrl}&autocomplete=1&searchedID=${this.value}`);
this.displayValue = response.data[0].text;
@@ -82,45 +87,29 @@ Vue.component('tt-autocomplete', {
this.displayValue = '';
}
- },
- data() {
+ }, data() {
return {
- displayingItems: [],
- displayValue: '',
- isLoading: false,
- showSuggestions: false,
- cursor: -1,
- fetchSuggestionsDebounceTimer: null,
+ displayingItems: [], displayValue: '', isLoading: false, showSuggestions: false, cursor: -1, fetchSuggestionsDebounceTimer: null,
};
- },
- watch: {
+ }, watch: {
value(newValue) {
const selectedItem = this.displayingItems.find(item => item.value === newValue);
this.displayValue = selectedItem ? selectedItem.text : '';
- },
- apiUrl() {
+ }, apiUrl() {
this.fetchSuggestions();
},
- },
- methods: {
+ }, methods: {
onInput(event) {
- console.log('input', event.target.value);
this.displayValue = event.target.value;
- console.log('displayValue', this.displayValue);
this.$emit('input', '');
- console.log('value', this.value);
- console.log('displayValue', this.displayValue);
this.fetchSuggestions();
- },
- onFocus() {
+ }, onFocus() {
this.showSuggestions = true;
- },
- onBlur() {
+ }, onBlur() {
setTimeout(() => {
this.showSuggestions = false;
}, 200);
- },
- fetchSuggestions() {
+ }, fetchSuggestions() {
if (this.displayValue.length < 3) {
this.$set(this, 'displayingItems', []);
return this.displayingItems = [];
@@ -146,10 +135,16 @@ Vue.component('tt-autocomplete', {
this.isLoading = true;
clearTimeout(this.fetchSuggestionsDebounceTimer);
+ console.log(this.displayValue);
this.fetchSuggestionsDebounceTimer = setTimeout(() => {
- // Simulate the API call
setTimeout(async () => {
+ if (this.displayValue.length < 3) {
+ this.displayingItems = [];
+ this.isLoading = false;
+ return;
+ }
+
const response = await axios.get(`${this.apiUrl}&autocomplete=1&q=${encodeURIComponent(this.displayValue)}`);
if (response.data?.status === 'error') {
this.displayingItems = [];
@@ -161,14 +156,11 @@ Vue.component('tt-autocomplete', {
this.fetchSuggestionsDebounceTimer = null;
}, 100);
}, 300); // Adjust the 300ms debounce time as needed
- }
- ,
- selectSuggestion(item) {
+ }, selectSuggestion(item) {
this.$emit('input', item.value);
this.displayValue = item.text;
this.showSuggestions = false;
- },
- clear() {
+ }, clear() {
this.displayValue = '';
this.$emit('input', '');
}
diff --git a/public/plugins/vue/tt-components/tt-modal.js b/public/plugins/vue/tt-components/tt-modal.js
index adf453294..c4ad02e9a 100644
--- a/public/plugins/vue/tt-components/tt-modal.js
+++ b/public/plugins/vue/tt-components/tt-modal.js
@@ -1,49 +1,77 @@
Vue.component('tt-modal', {
- props: {
- show: { type: Boolean, default: false },
- title: { type: String, default: 'Modal Title' },
- delete: { type: Boolean, default: true },
- deleteText: { type: String, default: 'Delete' },
- save: { type: Boolean, default: true },
- saveText: { type: String, default: 'Save' },
- },
- watch: {
+ props: {
+ show: {type: Boolean, default: false},
+ title: {type: String, default: 'Überschrift'},
+ delete: {type: Boolean, default: true},
+ deleteText: {type: String, default: 'Löschen'},
+ save: {type: Boolean, default: true},
+ saveText: {type: String, default: 'Speichern'},
+ }, watch: {
show(newVal) {
if (!newVal) {
this.$emit('close')
}
+ // if show now is true then focus the first input element
+ if (newVal) {
+ this.$nextTick(() => {
+ const input = this.$refs.modal.querySelector('input')
+ if (input) {
+ if (input.classList.contains('tt-autocomplete')) {
+ return
+ }
+ input.focus()
+ }
+ })
+ }
},
- },
- //language=Vue
- template: `
-
-
-
-
-
-
-
-
- `
+ `
})
\ No newline at end of file
diff --git a/public/plugins/vue/tt-components/tt-select.js b/public/plugins/vue/tt-components/tt-select.js
index a08b4e652..d8ff377f8 100644
--- a/public/plugins/vue/tt-components/tt-select.js
+++ b/public/plugins/vue/tt-components/tt-select.js
@@ -31,10 +31,10 @@ Vue.component('tt-select', {
:required="required" v-model="selectedOption"
@change="$emit('input', $event.target.value ? $event.target.value : undefined)">
- {{ option }}
+ {{ option }}
{{ suffix }}
- {{ option.text }}
+ {{ option.text }}
diff --git a/public/plugins/vue/tt-components/tt-table-crud.js b/public/plugins/vue/tt-components/tt-table-crud.js
index ea8c23aef..f3a7d8586 100644
--- a/public/plugins/vue/tt-components/tt-table-crud.js
+++ b/public/plugins/vue/tt-components/tt-table-crud.js
@@ -21,14 +21,15 @@ Vue.component('tt-table-crud', {
{{column.modal.items.find(item => item.value == row[column.key]).text}}
-
Select not found for column {{column.key}} and value {{row[column.key]}}
+
Select not found for column {{column.key}} and value {{row[column.key]}}
-
+
@@ -41,12 +42,12 @@ Vue.component('tt-table-crud', {
-
+
-
-
-
-
+
+
+
+
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
@@ -74,14 +75,18 @@ Vue.component('tt-table-crud', {
`, props: {
- crudConfig: {type: Object, required: false, default: () => (window['TT_CONFIG']['CRUD_CONFIG'])}
+ crudConfig: {type: Object, required: false, default: () => (window['TT_CONFIG']['CRUD_CONFIG'])},
+ emitEdit: {type: Boolean, required: false, default: false}
}, data() {
return {
- crudModal: false, crudModalData: {}, window: window
-
+ crudModal: false, crudModalData: {}, crudModalColumnVisibility: {}, crudModalColumnVisibilityCheck: {}, window: window
}
}, methods: {
openCrudModal(row = null) {
+ if (this.emitEdit) {
+ this.$emit('edit', row)
+ return
+ }
this.crudModalData = row ? JSON.parse(JSON.stringify(row)) : {};
this.crudModal = true;
}, resetCrudModalData() {
@@ -97,8 +102,7 @@ Vue.component('tt-table-crud', {
this.window.notify('success', response.data.message || 'Erfolgreich gespeichert');
} else {
this.window.notify('error',
- response.data.errors ? Object.values(response.data.errors).join(' ') : response.data.message ||
- 'Ein Fehler ist aufgetreten');
+ response.data.errors ? Object.values(response.data.errors).join(' ') : response.data.message || 'Ein Fehler ist aufgetreten');
}
}, async deleteCrudModal() {
@@ -108,6 +112,42 @@ Vue.component('tt-table-crud', {
this.resetCrudModalData();
}
this.window.notify(response.data.success ? 'success' : 'error', response.data.message);
+ },
+ async checkCrudModalColumnVisibility(val, oldVal) {
+ const crudModalColumnVisibility = {}
+ for (const column of this.modalConfig.headers) {
+ if (!column?.visible?.reference || typeof column.visible.reference !== 'string') {
+ crudModalColumnVisibility[column.key] = true
+ continue;
+ }
+
+ const localId = this.crudModalData[column.visible.use.split('=')[0]]
+
+ if (this.crudModalColumnVisibilityCheck[column.key] &&
+ this.crudModalColumnVisibilityCheck[column.key][column.visible.reference] === localId) {
+ crudModalColumnVisibility[column.key] = this.crudModalColumnVisibilityCheck[column.key]['visibility']
+ continue;
+ }
+
+ if (!localId) {
+ crudModalColumnVisibility[column.key] = false
+ continue;
+ }
+
+ const reference = column.visible.reference
+
+ let referenceData = await axios.get(window['TT_CONFIG']['BASE_PATH'] + `/${reference}/getById?id=${localId}`)
+ referenceData = referenceData.data
+
+ // noinspection EqualityComparisonWithCoercionJS
+ crudModalColumnVisibility[column.key] = referenceData[column.visible.key] == column.visible.value
+ this.crudModalColumnVisibilityCheck[column.key] = {
+ [column.visible.reference]: localId,
+ visibility: crudModalColumnVisibility[column.key]
+ }
+ }
+
+ this.$set(this, 'crudModalColumnVisibility', crudModalColumnVisibility)
}
}, computed: {
tableConfig() {
@@ -115,17 +155,23 @@ Vue.component('tt-table-crud', {
key: this.crudConfig.key,
tableHeader: this.crudConfig.tableHeader,
headers: this.crudConfig.columns.filter(column => column.table !== false).map(column => {
- return {text: column.text, key: column.key, ...column.table, filterOptions: column?.modal?.items}
+ return {text: column.text, key: column.key, ...column.table, filterOptions: column?.modal?.items, priority: column.priority}
})
}
}, modalConfig() {
return {
- key: this.crudConfig.key,
- headers: this.crudConfig.columns.filter(column => column.modal !== false).map(column => {
+ key: this.crudConfig.key, headers: this.crudConfig.columns.filter(column => column.modal !== false).map(column => {
const type = column.modal?.type || "text"
return {text: column.text, key: column.key, type, ...column.modal}
}),
}
}
+ }, watch: {
+ crudModalData: {
+ handler: async function (val, oldVal) {
+ if (!val) return
+ await this.checkCrudModalColumnVisibility(val, oldVal)
+ }, deep: true
+ }
}
})
\ No newline at end of file
diff --git a/public/plugins/vue/tt-components/tt-table.js b/public/plugins/vue/tt-components/tt-table.js
index 31002b575..75197842c 100644
--- a/public/plugins/vue/tt-components/tt-table.js
+++ b/public/plugins/vue/tt-components/tt-table.js
@@ -35,9 +35,7 @@
Vue.component('tt-table-pagination', {
props: {
pagination: {
- type: Object,
- required: true,
- default: () => ({page: 1, per_page: 10, total_rows: 0, filtered_available: 0, total_pages: 1})
+ type: Object, required: true, default: () => ({page: 1, per_page: 10, total_rows: 0, filtered_available: 0, total_pages: 1})
}, reverse: {type: Boolean, default: false}
}, computed: {
pagesToDisplay() {
@@ -50,9 +48,11 @@ Vue.component('tt-table-pagination', {
}
return pages.length === 0 ? [1] : pages;
}, pageInfoText() {
- const start = Math.min(this.pagination.page * this.pagination.per_page - this.pagination.per_page + 1,
- this.pagination.total_rows);
+ const start = Math.min(this.pagination.page * this.pagination.per_page - this.pagination.per_page + 1, this.pagination.total_rows);
const end = Math.min(this.pagination.page * this.pagination.per_page, this.pagination.total_rows);
+
+ if (!this.pagination.filtered_available) this.pagination.filtered_available = this.pagination.total_rows;
+
const total = this.pagination.total_rows ===
this.pagination.filtered_available ? this.pagination.total_rows : `${this.pagination.filtered_available} (${this.pagination.total_rows})`;
return `${start} bis ${end} von ${total}`;
@@ -145,20 +145,20 @@ Vue.component('tt-table', {
:style="(column.filter === 'dateRange' ? 'min-width: 260px;' : '') +
(originalColumnWidths[column.key] ? 'width: ' + originalColumnWidths[column.key] + 'px;' : '')"
>
-
+
{{ column.text }}
+ :class="getSortIconClass(column.key)">
-
+
@@ -199,10 +199,12 @@ Vue.component('tt-table', {
-
{{
- columns[key].filterOptions.find(option => option.value.toString() ===
- row[key].toString())?.text
- }}
+
+ {{ autoCompleteData[key] && autoCompleteData[key][row[key]] ? autoCompleteData[key][row[key]]['title'] : row[key] }}
+
+
+
+
{{ window.moment(row[key] * 1000).format('DD.MM.YYYY HH:mm:ss') }}
@@ -229,6 +231,9 @@ Vue.component('tt-table', {
+
+ {{ autoCompleteData[key] && autoCompleteData[key][row[key]] ? autoCompleteData[key][row[key]]['title'] : row[key] }}
+
@@ -258,7 +263,7 @@ Vue.component('tt-table', {
Excel Export
-
+
@@ -279,25 +284,26 @@ Vue.component('tt-table', {
disableFiltering: {type: Boolean, default: false}
}, data() {
return {
- window: window,
- moment: window.moment,
- loading: false,
- rows: null,
- rawRows: null,
- pagination: {},
- filters: {},
- debounceTimeout: null,
- disableDebounce: false,
- latestFetchTimestamp: null,
- order: {
+ window: window,
+ moment: window.moment,
+ loading: false,
+ rows: null,
+ rawRows: null,
+ pagination: {},
+ filters: {},
+ debounceTimeout: null,
+ disableDebounce: false,
+ latestFetchTimestamp: null,
+ order: {
key: null, order: 'asc' // default sort order
},
- expandedRows: {},
- isInitialised: false,
- hiddenColumns: [],
- originalColumnWidths: {},
- originalTableWidth: null,
- debouncedHandleResize: null
+ expandedRows: {},
+ isInitialised: false,
+ hiddenColumns: [],
+ originalColumnWidths: {},
+ originalTableWidth: null,
+ debouncedHandleResize: null,
+ autoCompleteData: {}
};
},
@@ -316,12 +322,10 @@ Vue.component('tt-table', {
if (this.debounceTimeout) clearTimeout(this.debounceTimeout);
this.debounceTimeout = setTimeout(() => fn.apply(context, args), wait);
}
- },
- refreshTable() {
+ }, refreshTable() {
this.loading = true;
this.fetchData(this.pagination.page).then()
- },
- /**
+ }, /**
* Fetches and updates data for a specified page.
*
* @param {number} page The page number to fetch data for.
@@ -365,6 +369,7 @@ Vue.component('tt-table', {
this.pagination = {page: 1, per_page: 10, total_rows: 0, total_pages: 1};
} else {
this.rows = response.data.rows;
+ this.autoCompleteData = response.data.autoCompleteData;
this.pagination = response.data.pagination;
}
this.loading = false;
@@ -409,9 +414,7 @@ Vue.component('tt-table', {
localStorage.setItem(`tt-table-${this.config.key}`, JSON.stringify({
// filter filters with empty values or empty objects
- filters,
- paginationPerPage: this.pagination.per_page,
- order: this.order.key ? this.order : undefined,
+ filters, paginationPerPage: this.pagination.per_page, order: this.order.key ? this.order : undefined,
}));
}, parseSettingsFromLocalStorage() {
if (this.disableFiltering) return false;
@@ -462,8 +465,7 @@ Vue.component('tt-table', {
if (this.columns[key] && this.columns[key].filter === 'iconSelect') {
parsedRow[this.columns[key].text] =
- this.columns[key].filterOptions.find(option => option.value.toString() ===
- row[key].toString())?.text;
+ this.columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.text;
} else if (this.columns[key] && this.columns[key].filter === 'date') {
parsedRow[this.columns[key].text] = this.moment(row[key]).format('DD.MM.YYYY HH:mm');
} else {
@@ -478,9 +480,9 @@ Vue.component('tt-table', {
const wb = this.window.XLSX.utils.book_new();
- let data = typeof this.config.customExcelProcessor === 'function' ?
- this.config.customExcelProcessor(JSON.parse(JSON.stringify(this.computedRows))) :
- defaultExcelProcessor.call(this, JSON.parse(JSON.stringify(this.computedRows)));
+ let data = typeof this.config.customExcelProcessor ===
+ 'function' ? this.config.customExcelProcessor(JSON.parse(JSON.stringify(this.computedRows))) : defaultExcelProcessor.call(this,
+ JSON.parse(JSON.stringify(this.computedRows)));
const ws = this.window.XLSX.utils.json_to_sheet(data);
@@ -506,13 +508,10 @@ Vue.component('tt-table', {
this.disableDebounce = true;
window.notify('success', 'Filter zurückgesetzt');
}, toggleExpand(index) {
- this.expandedRows[index] ? this.$delete(this.expandedRows, index) : this.$set(this.expandedRows,
- index,
- true);
+ this.expandedRows[index] ? this.$delete(this.expandedRows, index) : this.$set(this.expandedRows, index, true);
}, isExpanded(index) {
return !!this.expandedRows[index];
- },
- async handleResponsiveColumns() {
+ }, async handleResponsiveColumns() {
if (!this.$refs.tableContainer) return;
const tableContainer = this.$refs.tableContainer;
const {paddingLeft, paddingRight} = window.getComputedStyle(tableContainer);
@@ -536,7 +535,7 @@ Vue.component('tt-table', {
if (i === 0) continue;
- else if (viewportWidth +10 < 0) {
+ else if (viewportWidth + 10 < 0) {
this.hiddenColumns.push(columns[i].key)
}
}
@@ -547,6 +546,16 @@ Vue.component('tt-table', {
handler: function () {
if (!this.isInitialised) return;
+ // go through filters and if there is a set value in filters and the filter of the column is select or autocomplete then parse the value to int
+ for (const key in this.filters) {
+ if (this.filters[key] && (this.columns[key].filter === 'select' || this.columns[key].filter === 'autocomplete')) {
+ // only if first character is a number
+ if (!isNaN(this.filters[key][0])) {
+ this.filters[key] = parseInt(this.filters[key]);
+ }
+ }
+ }
+
if (this.ssr) {
this.fetchRows(this.pagination?.page || 1, true).then();
}
@@ -574,15 +583,13 @@ Vue.component('tt-table', {
this.saveSettingsToLocalStorage();
}, deep: true
- },
- rows: {
+ }, rows: {
handler: async function () {
this.$nextTick(() => {
this.handleResponsiveColumns()
})
}, deep: true
- },
- computedRows: {
+ }, computedRows: {
handler: async function () {
this.$nextTick(() => {
this.handleResponsiveColumns()
@@ -620,9 +627,7 @@ Vue.component('tt-table', {
}, pagesToDisplay() {
let range = 2; // Number of pages before and after the current page
let start = (this.pagination.page < 4 ? 1 : this.pagination.page - range);
- let end = (this.pagination.page +
- range >
- this.pagination.total_pages ? this.pagination.total_pages : this.pagination.page + range);
+ let end = (this.pagination.page + range > this.pagination.total_pages ? this.pagination.total_pages : this.pagination.page + range);
if (end < 5) end = 5;
// Adjust start and end if they are out of bounds
@@ -691,10 +696,9 @@ Vue.component('tt-table', {
const substrings = (isNegated ? filterValue.slice(1) : filterValue).split(' ')
.map(s => s.toLowerCase());
- const targetValue = !row[header.key] ? '' :
- typeof row[header.key] === 'object' ? Object.values(row[header.key])
- .join(' ')
- .toLowerCase() : row[header.key].toString().toLowerCase();
+ const targetValue = !row[header.key] ? '' : 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) {
@@ -717,12 +721,7 @@ Vue.component('tt-table', {
match = false;
break;
}
- } else if (header.filter ===
- 'select' ||
- header.filter ===
- 'iconSelect' ||
- header.filter ===
- 'autocomplete') {
+ } else if (header.filter === 'select' || header.filter === 'iconSelect' || header.filter === 'autocomplete') {
if (filterValue === '') continue;
if (filterValue !== row[header.key]?.toString()) {
match = false;
@@ -731,8 +730,7 @@ Vue.component('tt-table', {
} else if (header.filter === 'date') {
if (!filterValue.from || !filterValue.to) continue;
- const dateInt = row[header.key].length === 10 ? parseInt(row[header.key]) *
- 1000 : parseInt(row[header.key]);
+ const dateInt = row[header.key].length === 10 ? parseInt(row[header.key]) * 1000 : parseInt(row[header.key]);
let rowDate = new Date(dateInt).getTime() / 1000;
if (rowDate < filterValue.from || rowDate > filterValue.to) {
@@ -757,14 +755,10 @@ Vue.component('tt-table', {
const isDateColumn = this.columns[this.order.key].filter === 'date';
output.sort((a, b) => {
- let valueA = isDateColumn ?
- new Date(a[this.order.key].length === 10 ? parseInt(a[this.order.key]) *
- 1000 : parseInt(a[this.order.key])).getTime() :
- a[this.order.key] || ''
- let valueB = isDateColumn ?
- new Date(b[this.order.key].length === 10 ? parseInt(b[this.order.key]) *
- 1000 : parseInt(b[this.order.key])).getTime() :
- b[this.order.key] || ''
+ let valueA = isDateColumn ? new Date(a[this.order.key].length === 10 ? parseInt(a[this.order.key]) *
+ 1000 : parseInt(a[this.order.key])).getTime() : a[this.order.key] || ''
+ let valueB = isDateColumn ? new Date(b[this.order.key].length === 10 ? parseInt(b[this.order.key]) *
+ 1000 : parseInt(b[this.order.key])).getTime() : b[this.order.key] || ''
if (valueA === valueB) return 0;
@@ -778,8 +772,7 @@ Vue.component('tt-table', {
}
// console.timeEnd('Filtering and pagination');
return output;
- },
- visibleRows() {
+ }, visibleRows() {
if (!this.rawRows || this.ssr === true) return null;
// Pagination and slice logic
this.pagination.total_pages = Math.ceil(this.computedRows.length / this.pagination.per_page);
@@ -812,14 +805,12 @@ Vue.component('tt-table', {
// use header#topnav as top to stick to but if window is resized then check if header#topnav height is changed
const headerHeight = document.querySelector('header#topnav')?.offsetHeight || 0;
- style.innerHTML =
- `table thead th { position: sticky; top: ${headerHeight}px; z-index: 1; background-color: white; }`;
+ style.innerHTML = `table thead th { position: sticky; top: ${headerHeight}px; z-index: 1; background-color: white; }`;
style.id = 'tt-table-sticky-header';
window.addEventListener('resize', () => {
const headerHeight = document.querySelector('header#topnav')?.offsetHeight || 0;
- style.innerHTML =
- `table thead th { position: sticky; top: ${headerHeight}px; z-index: 1; background-color: white; }`;
+ style.innerHTML = `table thead th { position: sticky; top: ${headerHeight}px; z-index: 1; background-color: white; }`;
})
document.head.appendChild(style);
@@ -827,8 +818,7 @@ Vue.component('tt-table', {
this.debouncedHandleResize = this.debounce(this.handleResponsiveColumns, 100);
window.addEventListener('resize', this.debouncedHandleResize);
- },
- beforeDestroy() {
+ }, beforeDestroy() {
window.removeEventListener('resize', this.debouncedHandleResize);
}
})