diff --git a/Layout/default/WarehouseShippingNote/PDF_HEADER.html b/Layout/default/WarehouseShippingNote/PDF_HEADER.html
index f1ba3ce5e..6c6bd5781 100644
--- a/Layout/default/WarehouseShippingNote/PDF_HEADER.html
+++ b/Layout/default/WarehouseShippingNote/PDF_HEADER.html
@@ -67,7 +67,7 @@
{{ addressLine_5 }}
- Rechnungsadresse
+ {{ billingAddressHeader }}
{{ billingAddressLine_1 }}
{{ billingAddressLine_2 }}
{{ billingAddressLine_3 }}
diff --git a/Layout/default/WarehouseShippingNote/PDF_MAIN.php b/Layout/default/WarehouseShippingNote/PDF_MAIN.php
index 3c2806f8f..70a178950 100644
--- a/Layout/default/WarehouseShippingNote/PDF_MAIN.php
+++ b/Layout/default/WarehouseShippingNote/PDF_MAIN.php
@@ -96,6 +96,10 @@ TODO: enable option for showing prices
= $shippingNote->note ?>
+ status === 'cancelled'): ?>
+ STORNIERT
+
+
| Position |
diff --git a/application/WarehouseShippingNote/WarehouseShippingNoteController.php b/application/WarehouseShippingNote/WarehouseShippingNoteController.php
index 38c6f7123..fa2aa67b5 100644
--- a/application/WarehouseShippingNote/WarehouseShippingNoteController.php
+++ b/application/WarehouseShippingNote/WarehouseShippingNoteController.php
@@ -7,12 +7,14 @@ class WarehouseShippingNoteController extends TTCrud {
//@formatter:off
protected array $columns = [
['key' => 'id', 'text' => 'LS-Nr.', 'required' => false, 'modal' => false, 'table' => ['class' => 'text-nowrap']],
- ['key' => 'billingAddressId', 'text' => 'Rechnungsadresse', 'required' => true, 'type' => 'autocomplete', 'table' => ['class' => 'text-nowrap', 'filter' => 'autocomplete'], 'modal' => ['apiUrl' => 'Address/api?do=findAddress', 'items' => '/Address/Api?do=findAddress', 'type' => 'autocomplete']],
+ ['key' => 'billingAddressId', 'text' => 'Rechnungsadresse', 'type' => 'autocomplete', 'table' => ['class' => 'text-nowrap', 'filter' => 'autocomplete'], 'modal' => ['apiUrl' => 'Address/api?do=findAddress', 'items' => '/Address/Api?do=findAddress', 'type' => 'autocomplete']],
['key' => 'status', 'text' => 'Status', 'required' => true, 'table' => ['filter' => 'iconSelect'], 'modal' => ['type' => 'iconSelect', 'items' => [
['value' => 'new', 'text' => 'Neu', 'icon' => 'fas fa-star text-primary'],
['value' => 'in_progress', 'text' => 'In Bearbeitung', 'icon' => 'fas fa-cog text-warning'],
['value' => 'accepted', 'text' => 'Akzeptiert', 'icon' => 'fas fa-check text-success'],
['value' => 'invoiced', 'text' => 'In Rechnung gestellt', 'icon' => 'fas fa-file-invoice-dollar text-info'],
+ ['value' => 'cancelled', 'text' => 'Storniert', 'icon' => 'fas fa-ban text-danger'],
+ ['value' => 'on_hold', 'text' => 'In Wartestellung', 'icon' => 'fas fa-pause text-warning'],
]]],
['key' => 'deliveryAddressName', 'text' => 'L.-Adr. Name', 'required' => true],
['key' => 'deliveryAddressLine', 'text' => 'L.-Adr.', 'required' => true],
@@ -26,11 +28,6 @@ class WarehouseShippingNoteController extends TTCrud {
protected array $defaultOrder = ['key' => 'create', 'order' => 'DESC'];
- 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 $additionalJSVariables = ['WAREHOUSE_ADMIN' => true];
protected array $infoMessages = ['create' => 'Lieferschein wurde erstellt.',
@@ -40,189 +37,117 @@ class WarehouseShippingNoteController extends TTCrud {
//@formatter:on
protected function prepareCrudConfig() {
- $users = array_map(function ($user) {
- return ['value' => intval($user->id), 'text' => $user->name];
- }, UserModel::search(['employee' => true]));
-
- $this->columns[array_search('createBy', array_column($this->columns, 'key'))]['modal']['items'] = $users;
-
- if (!$this->user->can('WarehouseAdmin')) {
- $this->additionalJSVariables['WAREHOUSE_ADMIN'] = false;
- }
+ if (!$this->user->can('WarehouseAdmin')) $this->additionalJSVariables['WAREHOUSE_ADMIN'] = false;
}
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();
- }
-
- foreach ($postData['hoursEntries'] as $hoursEntry) {
- if (!preg_match('/^[0-9,.]*$/', $hoursEntry['hourCount'])) {
- http_response_code(500);
- self::returnJson(['success' => false, 'message' => 'Stundenanzahl darf nur Zahlen, Komma oder Punkt enthalten']);
- die();
- }
- }
-
+ $this->validate($postData, [
+ fn($p) => $p['status'] === 'new' ?: 'Status muss "Neu" sein',
+ fn($p) => $this->validateHours($p['hoursEntries'])
+ ]);
$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;
- }
- return null;
- }
-
protected function beforeUpdate($postData): bool {
- $shippingNote = WarehouseShippingNoteModel::get($postData['id']);
- if ($shippingNote->status === 'accepted' || $shippingNote->status === 'invoiced') {
- http_response_code(500);
- self::returnJson(['success' => false, 'message' => 'Änderungen nicht mehr möglich']);
- die();
- }
-
- foreach ($postData['hoursEntries'] as $hoursEntry) {
- if (!preg_match('/^[0-9,.]*$/', $hoursEntry['hourCount'])) {
- http_response_code(500);
- self::returnJson(['success' => false, 'message' => 'Stundenanzahl darf nur Zahlen, Komma oder Punkt enthalten']);
- die();
- }
- }
-
+ $this->validate($postData, [
+ fn($p) => !in_array(WarehouseShippingNoteModel::get($p['id'])->status,
+ ['accepted', 'invoiced']) ?: 'Änderungen nicht mehr möglich',
+ fn($p) => $this->validateHours($p['hoursEntries'])
+ ]);
$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;
+ private function validateHours($entries): bool {
+ foreach ($entries as $e) {
+ if (!preg_match('/^[\d,.]*$/', $e['hourCount'])) {
+ $this->sendError('Stundenanzahl darf nur Zahlen, Komma oder Punkt enthalten');
}
}
+ return true;
+ }
-// $historyEntries = array_filter($historyEntries, function ($entry) {
-// return $entry['key'] !== 'positions';
-// });
+ private function validate($data, array $rules): void {
+ foreach ($rules as $rule) {
+ if (($result = $rule($data)) !== true) {
+ $this->sendError(is_string($result) ? $result : 'Validation failed');
+ }
+ }
+ }
- self::returnJson($historyEntries);
+ protected function customAutoCompleteBillingAddressId($id) {
+ $address = new Address($id);
+
+ if (!$address->id) return null;
+
+ return [
+ 'id' => $address->id,
+ 'title' => sprintf(
+ "%s (%s %s, %s)%s",
+ str_replace(["'", "\n", "\r"], ["\\'", ' ', ' '], $address->getCompanyOrName()),
+ $address->zip,
+ $address->city,
+ $address->street,
+ $address->customer_number ? " [{$address->customer_number}]" : ''
+ )
+ ];
+ }
+
+ protected function getHistoryAction() {
+ self::returnJson(array_values(
+ array_filter(
+ (new WarehouseHistoryController)->getHistory($this->request->id, $this->mod, $this->columns),
+ fn($e) => $e['key'] !== 'positions'
+ )
+ ));
}
protected function getArticleAddressPriceAction() {
- $articleId = $this->request->articleId;
- $addressId = $this->request->addressId;
+ empty($this->request->articleId) && $this->sendError('Keine Artikel ID gefunden');
+ empty($this->request->addressId) && $this->sendError('Keine Adress ID gefunden');
- if (strlen($articleId) < 1) {
- http_response_code(500);
- self::returnJson(['success' => false, 'message' => 'Keine Artikel ID gefunden']);
- }
+ // TODO: we use verkauf as default now, later we will need to use from address
+ $priceTypes = WarehouseArticlePriceTypeModel::getAll(['title' => 'Verkauf']);
+ if (empty($priceTypes)) $this->sendError('Keine Preiskategorie gefunden');
+ $priceType = $priceTypes[0]->title;
- if (strlen($addressId) < 1) {
- http_response_code(500);
- self::returnJson(['success' => false, 'message' => 'Keine Adress ID gefunden']);
- }
+ $article = WarehouseArticleModel::get($this->request->articleId);
+ $prices = json_decode($article->cheapestSellPrice, true);
- //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;
+ foreach ($prices as $price)
+ if ($price['title'] === $priceType)
+ self::returnJson(['success' => true, 'price' => $price['price']]);
- $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);
+ $this->sendError('Kein Preis gefunden');
}
protected function signAction() {
$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);
+ if (empty($id) || !$shippingNote = (array) WarehouseShippingNoteModel::get($id)) $this->sendError('Lieferschein nicht gefunden');
+ if ($shippingNote["signature"] || $shippingNote["signatureName"]) $this->sendError('Bereits unterschrieben');
- if ($shippingNote->signature || $shippingNote->signatureName) {
- http_response_code(500);
- self::returnJson(['success' => false, 'message' => 'Lieferschein wurde bereits unterschrieben']);
- }
$post = json_decode(file_get_contents('php://input'), true);
- $shippingNote = (array) $shippingNote;
- $shippingNote['signature'] = $post['signature'];
- $shippingNote['signatureName'] = $post['signatureName'];
-
- if (strlen($shippingNote['signature']) < 1 || strlen($shippingNote['signatureName']) < 1) {
- http_response_code(500);
- self::returnJson(['success' => false, 'message' => 'Unterschrift oder Name fehlt']);
+ if (empty($post['signature']) || empty($post['signatureName'])) {
+ $this->sendError('Unterschrift/Name fehlt');
}
try {
- $shippingNote['signatureDate'] = date("Y-m-d");
- WarehouseShippingNoteModel::update($shippingNote);
- self::returnJson(['success' => true, 'message' => 'Unterschrift wurde gespeichert']);
+ WarehouseShippingNoteModel::update(array_merge($shippingNote, ['signature' => $post['signature'], 'signatureName' => $post['signatureName'], 'signatureDate' => date('Y-m-d')]));
+ self::returnJson(['success' => true, 'message' => 'Unterschrift gespeichert']);
} catch (Exception $e) {
- http_response_code(500);
- self::returnJson(['success' => false, 'message' => 'Unterschrift konnte nicht gespeichert werden']);
+ $this->sendError('Speichern fehlgeschlagen');
}
}
+ private function sendSuccess(string $message) {
+ self::returnJson(['success' => true, 'message' => $message]);
+ }
+
public function createPDFAction($returnFilename = false, $idOverride = null) {
$id = $idOverride ?? $this->request->id;
if (strlen($id) < 1) {
@@ -231,7 +156,7 @@ class WarehouseShippingNoteController extends TTCrud {
}
$shippingNote = WarehouseShippingNoteModel::get($id);
- $address = AddressModel::getOne($shippingNote->billingAddressId);
+ if (!empty($shippingNote->billingAddressId)) $address = AddressModel::getOne($shippingNote->billingAddressId);
$positions = [];
// loop through all positions and add articleTitle and articleDescription to each position entry
@@ -265,6 +190,7 @@ class WarehouseShippingNoteController extends TTCrud {
}
}
+
// json decode hoursEntries and add to positions
$hoursEntries = json_decode($shippingNote->hoursEntries, true);
foreach ($hoursEntries as $hoursEntry) {
@@ -274,31 +200,64 @@ class WarehouseShippingNoteController extends TTCrud {
$articleTitle = "Arbeitsstunden (50% Zuschlag)";
} elseif (isset($hoursEntry['priceType']) && $hoursEntry['priceType'] == 100) {
$articleTitle = "Arbeitsstunden (100% Zuschlag)";
+ } elseif (isset($hoursEntry['hourType']) && $hoursEntry['hourType'] == '50') {
+ $articleTitle = "Arbeitsstunden (+50% Zuschlag)";
+ } elseif (isset($hoursEntry['hourType']) && $hoursEntry['hourType'] == '100') {
+ $articleTitle = "Arbeitsstunden (+100% Zuschlag)";
+ } elseif (isset($hoursEntry['hourType']) && $hoursEntry['hourType'] == 'regie') {
+ $articleTitle = "Arbeitsstunden (Regie)";
}
if (floatval(str_replace(",", ".", $hoursEntry['hourCount'])) > 0) {
+ $userText = '';
+ if (!empty($hoursEntry['userId'])) {
+ $user = UserModel::getOne($hoursEntry['userId']);
+ $userText = $user->name;
+ } else {
+ $userText = $hoursEntry['userId_text'];
+ }
+
+ $articleDescription = "Datum: ". date("d.m.Y", strtotime($hoursEntry['date'])) . " | Mitarbeiter: " . $userText;
+ if (isset($hoursEntry['comment'])) {
+ $articleDescription .= " | Zusatz: " . $hoursEntry['comment'];
+ }
+
$positions[] = [
'articleTitle' => $articleTitle,
- 'articleDescription' => "Datum: ". date("d.m.Y", strtotime($hoursEntry['date'])) . " | Mitarbeiter: " . UserModel::getOne($hoursEntry['userId'])->name,
+ 'articleDescription' => $articleDescription,
'articleUnit' => 'Std.',
'amount' => $hoursEntry['hourCount'],
- 'price' => $hoursEntry['hourlyPrice'] * $hoursEntry['hourCount'] ?? 0,
];
}
- if ($hoursEntry['carId']) {
+ if (!empty($hoursEntry['externalCar']) && !empty($hoursEntry['kilometerCount']) && $hoursEntry['kilometerCount'] > 0 && $hoursEntry['externalCar'] == 1) {
+ $positions[] = [
+ 'articleTitle' => "Fahrkostenpauschale (hin und retour)",
+ 'articleDescription' => "Datum: ". date("d.m.Y", strtotime($hoursEntry['date'])) . " | Externes Fahrzeug",
+ 'articleUnit' => 'km',
+ 'amount' => $hoursEntry['kilometerCount'] * 2,
+ 'price' => 2 * $hoursEntry['kilometerCount'] ?? 0,
+ ];
+ } else if (!empty($hoursEntry['carId']) && $hoursEntry['kilometerCount'] > 0) {
+ $hoursEntry['carId'] = intval($hoursEntry['carId']);
$positions[] = [
'articleTitle' => "Fahrkostenpauschale (hin und retour)",
'articleDescription' => "Datum: ". date("d.m.Y", strtotime($hoursEntry['date'])) . " | Fahrzeug: " . TimerecordingCarModel::getOne($hoursEntry['carId'])->number_plate,
- 'articleUnit' => 'Km',
+ 'articleUnit' => 'km',
+ 'amount' => $hoursEntry['kilometerCount'] * 2,
+ 'price' => 2 * $hoursEntry['kilometerCount'] ?? 0,
+ ];
+ } else if (!empty($hoursEntry['carId_text']) && $hoursEntry['kilometerCount'] > 0) {
+ $positions[] = [
+ 'articleTitle' => "Fahrkostenpauschale (hin und retour)",
+ 'articleDescription' => "Datum: ". date("d.m.Y", strtotime($hoursEntry['date'])) . " | Fahrzeug: " . $hoursEntry['carId_text'],
+ 'articleUnit' => 'km',
'amount' => $hoursEntry['kilometerCount'] * 2,
'price' => 2 * $hoursEntry['kilometerCount'] ?? 0,
];
}
-
}
-
usort($positions, function ($a, $b) {
$aHasMitarbeiter = str_contains($a['articleDescription'], 'Mitarbeiter');
$bHasMitarbeiter = str_contains($b['articleDescription'], 'Mitarbeiter');
@@ -347,12 +306,13 @@ class WarehouseShippingNoteController extends TTCrud {
$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("{{ billingAddressLine_1 }}", $address->getCompanyOrName(), $headerHtml);
- $headerHtml = str_replace("{{ billingAddressLine_2 }}", $address->street, $headerHtml);
- $headerHtml = str_replace("{{ billingAddressLine_3 }}", $address->zip . " " . $address->city, $headerHtml);
+ $headerHtml = str_replace("{{ billingAddressHeader }}", !isset($address) ? '' : "Rechnungsadresse", $headerHtml);
+ $headerHtml = str_replace("{{ billingAddressLine_1 }}", !isset($address) ? '' : $address->getCompanyOrName(), $headerHtml);
+ $headerHtml = str_replace("{{ billingAddressLine_2 }}", !isset($address) ? '' : $address->street, $headerHtml);
+ $headerHtml = str_replace("{{ billingAddressLine_3 }}", !isset($address) ? '' : $address->zip . " " . $address->city, $headerHtml);
$headerHtml = str_replace("{{ billingAddressLine_4 }}", "", $headerHtml);
$headerHtml = str_replace("{{ billingAddressLine_5 }}", "", $headerHtml);
- $headerHtml = str_replace("{{ customerNumber }}", $address->customer_number, $headerHtml);
+ $headerHtml = str_replace("{{ customerNumber }}", !isset($address) ? '' : $address->customer_number, $headerHtml);
$headerHtml = str_replace("{{ shippingNoteNumber }}", $shippingNoteNumber, $headerHtml);
$headerHtml = str_replace("{{ shippingNoteDate }}", date("d.m.Y", $shippingNote->create), $headerHtml);
@@ -434,6 +394,8 @@ class WarehouseShippingNoteController extends TTCrud {
'in_progress' => 'In Bearbeitung',
'accepted' => 'Akzeptiert',
'invoiced' => 'In Rechnung gestellt',
+ 'cancelled' => 'Storniert',
+ 'on_hold' => 'In Wartestellung',
];
self::returnJson(['success' => true, 'message' => 'Status wurde auf ' . $statusNiceText[$status] . ' geändert']);
}
@@ -465,9 +427,8 @@ class WarehouseShippingNoteController extends TTCrud {
protected function timerecordingCarForUserAction() {
$timerecordingCars = TimerecordingCarModel::getAll();
- $out = null;
foreach ($timerecordingCars as $timerecordingCar) {
- if ($timerecordingCar->user_id == $this->user->id) {
+ if ($timerecordingCar->user_id == $this->request->userId) {
header('Content-Type: application/json');
die(json_encode(['success' => true, 'id' => $timerecordingCar->id]));
}
@@ -502,7 +463,7 @@ class WarehouseShippingNoteController extends TTCrud {
$lon = $this->request->lon;
$url = "https://nominatim.haid.in/reverse?lat=$lat&lon=$lon&format=json";
$data = json_decode(file_get_contents($url), true);
- self::returnJson($data);
+ self::returnJson(is_array($data) ? $data : ['data' => $data]);
}
@@ -587,4 +548,51 @@ class WarehouseShippingNoteController extends TTCrud {
self::returnJson(['success' => true, 'distance' => $roundedDistanceKm]);
}
+ protected function uploadFileAction() {
+ $file = $_FILES['file'] ?? null;
+
+ if (!$file || $file['error'] !== UPLOAD_ERR_OK) {
+ self::returnJson(['error' => 'File upload failed']);
+ exit;
+ }
+
+ try {
+ $uploaded = mfUpload::handleFormUpload("file", false, "/WarehouseShippingNote");
+ self::returnJson(['success' => true, 'fileId' => $uploaded->id]);
+ } catch (Exception $e) {
+ self::returnJson(['error' => 'Upload error: ' . $e->getMessage()]);
+ }
+ }
+
+ protected function createNewLogAction() {
+ $postData = json_decode(file_get_contents('php://input'), true);
+
+ if (empty($postData['shippingNoteId'])) {
+ self::returnJson(['success' => false, 'message' => 'ShippingNoteId fehlt']);
+ return;
+ }
+
+ $log = [
+ "table" => "WarehouseShippingNote",
+ "rowId" => intval($postData['shippingNoteId']),
+ "type" => 'noChanges',
+ "fileIds" => $postData['fileIds'] ?? null,
+ "message" => $postData['note'] ?? null,
+ "createBy" => intval($this->user->id),
+ "create" => time()
+ ];
+ WarehouseLogModel::create($log);
+ self::returnJson(['success' => true, 'message' => 'Logeintrag erfolgreich erstellt']);
+ }
+
+ protected function getLogAction() {
+ $shippingNoteId = $this->request->shippingNoteId;
+ if (empty($shippingNoteId)) {
+ self::returnJson(['success' => false, 'message' => 'ShippingNoteId fehlt']);
+ return;
+ }
+
+ $logs = WarehouseLogModel::getAll(['table' => 'WarehouseShippingNote','rowId' => $shippingNoteId], null, 0, ['order' => 'DESC', 'key' => 'create']);
+ self::returnJson($logs);
+ }
}
diff --git a/application/WarehouseShippingNote/WarehouseShippingNoteModel.php b/application/WarehouseShippingNote/WarehouseShippingNoteModel.php
index 59aed4ea2..5817569c2 100644
--- a/application/WarehouseShippingNote/WarehouseShippingNoteModel.php
+++ b/application/WarehouseShippingNote/WarehouseShippingNoteModel.php
@@ -2,7 +2,7 @@
class WarehouseShippingNoteModel extends TTCrudBaseModel {
public int $id;
- public int $billingAddressId;
+ public ?int $billingAddressId;
public string $deliveryAddressName;
public string $deliveryAddressLine;
public string $deliveryAddressPLZ;
diff --git a/db/migrations/20250320130000_warehouse_modify_15.php b/db/migrations/20250320130000_warehouse_modify_15.php
new file mode 100644
index 000000000..e656b9ff2
--- /dev/null
+++ b/db/migrations/20250320130000_warehouse_modify_15.php
@@ -0,0 +1,25 @@
+getEnvironment() == "thetool") {
+ $WarehouseShippingNoteTable = $this->table("WarehouseShippingNote");
+ $WarehouseShippingNoteTable
+ ->changeColumn("billingAddressId", "integer", ["null" => true])
+ ->changeColumn("status", "enum", ["values" => ['new', 'in_progress', 'accepted', 'invoiced', 'cancelled', 'on_hold']])
+ ->save();
+ }
+ }
+
+ public function down(): void {
+ if ($this->getEnvironment() == "thetool") {
+ $WarehouseShippingNoteTable = $this->table("WarehouseShippingNote");
+ $WarehouseShippingNoteTable
+ ->changeColumn("billingAddressId", "integer", ["null" => false])
+ ->changeColumn("status", "enum", ["values" => ['new', 'in_progress', 'accepted', 'invoiced']])
+ ->save();
+ }
+ }
+}
diff --git a/lib/TTCrudBaseModel/TTCrudBaseModel.php b/lib/TTCrudBaseModel/TTCrudBaseModel.php
index 72647faf7..d336a25d6 100644
--- a/lib/TTCrudBaseModel/TTCrudBaseModel.php
+++ b/lib/TTCrudBaseModel/TTCrudBaseModel.php
@@ -29,6 +29,10 @@ class TTCrudBaseModel {
if (is_array($value)) {
$value = json_encode($value);
+ } else if ($value === "" && (new ReflectionProperty(get_called_class(), $field))->getType()->getName() === "float") {
+ $value = null;
+ } else if ($value === "" && (new ReflectionProperty(get_called_class(), $field))->getType()->getName() === "int") {
+ $value = null;
}
$sqlValues[] = $value === null ? 'NULL' : "'" . $db->real_escape_string($value) . "'";
diff --git a/lib/mvcfronk/mfBase/mfBaseController.php b/lib/mvcfronk/mfBase/mfBaseController.php
index 775bfb910..05c8322dd 100644
--- a/lib/mvcfronk/mfBase/mfBaseController.php
+++ b/lib/mvcfronk/mfBase/mfBaseController.php
@@ -370,4 +370,10 @@ class mfBaseController
return $dbdate;
}
+ public static function sendError(string $message): void {
+ http_response_code(422); // More appropriate status for validation errors
+ self::returnJson(['success' => false, 'message' => $message]);
+ exit;
+ }
+
}
diff --git a/lib/mvcfronk/mfUpload/mfUpload_TmpFile.php b/lib/mvcfronk/mfUpload/mfUpload_TmpFile.php
index 9fb29c407..f2cdfca90 100644
--- a/lib/mvcfronk/mfUpload/mfUpload_TmpFile.php
+++ b/lib/mvcfronk/mfUpload/mfUpload_TmpFile.php
@@ -118,6 +118,7 @@ class mfUpload_TmpFile {
}
public function pdftotext() {
+ if (!isset($cmd)) $cmd = "";
$cmd .= PDFTOTEXT_BIN_PATH . " " . $this->tmp_name . " -";
$lines = [];
diff --git a/public/js/pages/WarehouseShippingNote/WarehouseShippingNote.css b/public/js/pages/WarehouseShippingNote/WarehouseShippingNote.css
index be92707e7..3c020b08c 100644
--- a/public/js/pages/WarehouseShippingNote/WarehouseShippingNote.css
+++ b/public/js/pages/WarehouseShippingNote/WarehouseShippingNote.css
@@ -1,62 +1,11 @@
-.warehouse-shipping-note-modal-positions-entry-container {
- display: grid;
- grid-template-columns: 2fr 1fr 0.5fr 1fr 1fr;
- grid-gap: 10px;
-}
-
-.warehouse-shipping-note-modal-positions-entry-container.hidePrice {
- grid-template-columns: 2fr 1fr 0.5fr 1fr;
-}
-
-.warehouse-shipping-note-modal-positions-entry-actions, .warehouse-shipping-note-modal-hours-entry-actions {
- display: flex;
- flex-direction: column;
- justify-content: center;
- padding-top: 13px;
-}
-
-.warehouse-shipping-note-modal-hours-entry-container {
- display: grid;
- grid-template-columns: 2fr 1fr 1fr 1fr 2fr 1fr 1fr 1fr;
- grid-gap: 10px;
-}
-
-.warehouse-shipping-note-modal-hours-entry-container.hideHourlyPrice {
- grid-template-columns: 2fr 1fr 1fr 1fr 2fr 1fr 1fr;
-}
-
-.warehouse-shipping-note-modal-hours-entry-container.hideHourlyPrice.hideKilometer {
- grid-template-columns: 2fr 1fr 1fr 1fr 2fr 1fr;
-}
-
@media (min-width: 992px) {
.modal-lg, .modal-xl {
/*max width either 90% or 1120px*/
- max-width: min(90vw, 1120px) !important;
+ max-width: min(90vw, 1400px) !important;
}
}
@media (max-width: 992px) {
- .warehouse-shipping-note-modal-positions-entry-container,
- .warehouse-shipping-note-modal-hours-entry-container{
- display: grid;
- grid-template-columns: 1fr 1fr !important;
- grid-gap: 10px;
- }
-
- .warehouse-shipping-note-modal-positions-entry-actions {
- grid-column: 2;
- }
-
- .warehouse-shipping-note-modal-hours-entry-actions {
- grid-column: 1 / span 2;
- margin-bottom: 8px;
- }
-
- .modal .table.table-striped.table-sm button{
- margin-bottom: 4px;
- }
-
.signModal > div {
margin: 0;
width: 100vw;
@@ -79,4 +28,159 @@
display: none;
}
-}
\ No newline at end of file
+}
+
+.grid-container {
+ display: grid;
+ grid-template-columns: 2fr 0.5fr 0.5fr 1fr 2fr 0.5fr;
+ grid-gap: 10px;
+}
+.grid-container.header {
+ margin-top: 24px;
+}
+
+.upload-success-alert {
+ background-color: #d4edda;
+ border: 1px solid #c3e6cb;
+ border-radius: 8px;
+ padding: 15px;
+ margin-bottom: 20px;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
+}
+
+.alert-header {
+ display: flex;
+ align-items: center;
+ margin-bottom: 10px;
+ font-size: 18px;
+ color: #155724;
+}
+
+.alert-header i {
+ margin-right: 10px;
+ font-size: 24px;
+}
+
+.file-list {
+ list-style-type: none;
+ padding: 0;
+ margin: 0;
+}
+
+.file-item {
+ display: flex;
+ align-items: center;
+ background-color: #ffffff;
+ border-radius: 4px;
+ padding: 10px;
+ margin-bottom: 8px;
+ transition: background-color 0.3s ease;
+}
+
+.file-item:hover {
+ background-color: #f8f9fa;
+}
+
+.file-item i {
+ color: #000000;
+}
+
+.file-name {
+ margin-left: 10px;
+ flex-grow: 1;
+ font-size: 14px;
+}
+
+.remove-btn {
+ background-color: #dc3545;
+ color: #ffffff;
+ border: none;
+ border-radius: 4px;
+ padding: 5px 10px;
+ cursor: pointer;
+ transition: background-color 0.3s ease;
+}
+
+.remove-btn:hover {
+ background-color: #c82333;
+}
+
+
+
+
+/* Expanded Row Styling */
+.order-summary {
+ padding: 1rem;
+}
+.position-item {
+ margin-bottom: 1rem;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+}
+.position-header {
+ background-color: #f0f0f0;
+ padding: 0.5rem;
+ font-weight: bold;
+}
+.position-details {
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
+ padding: 0.5rem;
+}
+.field-item {
+ margin-bottom: 0.5rem;
+}
+
+
+.ios-switch-wrapper {
+ position: relative;
+ display: inline-block;
+ width: 50px;
+ height: 28px;
+}
+
+.ios-switch-wrapper.disabled {
+ opacity: 0.6;
+}
+
+.ios-switch-wrapper input {
+ opacity: 0;
+ width: 0;
+ height: 0;
+}
+
+.ios-switch-slider {
+ position: absolute;
+ cursor: pointer;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: #ccc;
+ transition: .4s;
+ border-radius: 34px;
+}
+
+.ios-switch-slider:before {
+ position: absolute;
+ content: "";
+ height: 20px;
+ width: 20px;
+ left: 4px;
+ bottom: 4px;
+ background-color: white;
+ transition: .4s;
+ border-radius: 50%;
+}
+
+input:checked + .ios-switch-slider {
+ background-color: #4cd964;
+}
+
+input:checked + .ios-switch-slider:before {
+ transform: translateX(22px);
+}
+
+input:disabled + .ios-switch-slider {
+ cursor: not-allowed;
+}
diff --git a/public/js/pages/WarehouseShippingNote/WarehouseShippingNote.js b/public/js/pages/WarehouseShippingNote/WarehouseShippingNote.js
index e4e7c81b4..dcdd9fa3e 100644
--- a/public/js/pages/WarehouseShippingNote/WarehouseShippingNote.js
+++ b/public/js/pages/WarehouseShippingNote/WarehouseShippingNote.js
@@ -1,43 +1,49 @@
-// here we need to change window.TT_CONFIG["CRUD_CONFIG"]["additionalActions"] to add actions and this conditions like this:
-//
-//
-//
-//
-
-// inside additionalActions each entry looks like {
-// "key": "openHistory",
-// "title": "Historie",
-// "class": "fas fa-history text-primary"
-// } but this is without the condition function
-
window.TT_CONFIG["CRUD_CONFIG"]["additionalActions"] = [
- ...window.TT_CONFIG["CRUD_CONFIG"]["additionalActions"],
{
"key": "status_to_progress",
"title": "In Bearbeitung",
"class": "fas fa-cog text-warning",
- "condition": (row) => window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1' && row.status === 'new',
+ "condition": (row) => window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1' && ['new', 'on_hold'].includes(row.status),
},
{
"key": "status_to_accepted",
"title": "Akzeptieren",
"class": "fas fa-check text-success",
- "condition": (row) => window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1' && (row.status === 'new' || row.status === 'in_progress'),
+ "condition": (row) => window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1' && ['new', 'in_progress', 'on_hold'].includes(row.status),
},
{
"key": "status_to_invoiced",
"title": "Verrechnet",
"class": "fas fa-file-invoice text-info",
- "condition": (row) => window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1' && row.status === 'accepted',
+ "condition": (row) => window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1' && ['in_progress', 'accepted', 'on_hold'].includes(row.status),
},
+ {
+ "key": "status_to_on_hold",
+ "title": "On Hold",
+ "class": "fas fa-pause text-warning",
+ "condition": (row) => window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1' && ['accepted', 'new', 'in_progress'].includes(row.status),
+ },
+ {
+ "key": "status_to_cancelled",
+ "title": "Storniert",
+ "class": "fas fa-ban text-danger",
+ "condition": (row) => (window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1' && ['new', 'in_progress', 'accepted'].includes(row.status)) || (row.status === 'new' && row.signature === null),
+ },
+ {
+ "key": "add_log",
+ "title": "Log Eintrag hinzufügen",
+ "class": "fas fa-plus text-primary",
+ },
+ {
+ "key": "print",
+ "title": "Drucken",
+ "class": "fas fa-print text-primary",
+ }
]
+// normal regie 50% 100%
+
+window.TT_CONFIG["CRUD_CONFIG"]["editCondition"] = (row) => row.status === 'new' && row.signature === null || window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1';
Vue.component('warehouse-shipping-note-positions', {
//language=Vue
@@ -87,6 +93,165 @@ Vue.component('warehouse-shipping-note-positions', {
}
})
+Vue.component('add-log-modal-sn', {
+ props: {
+ shippingNoteId: {type: Number, required: true},
+ },
+ data() {
+ return {
+ window: window,
+ note: '',
+ file: null,
+ uploadedFiles: [],
+ submitLoading: false
+ };
+ },
+ methods: {
+ async handleFileUpload(event) {
+ const files = event.target.files;
+ if (!files.length) return;
+
+ for (let i = 0; i < files.length; i++) {
+ const file = files[i];
+ const formData = new FormData();
+ formData.append('file', file);
+
+ try {
+ const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseShippingNote/uploadFile`, formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data'
+ }
+ });
+
+ if (response.data.success) {
+ this.uploadedFiles.push({
+ id: response.data.fileId,
+ name: file.name
+ });
+ window.notify('success', `File "${file.name}" uploaded successfully`);
+ } else {
+ window.notify('error', `File "${file.name}" upload failed: ${response.data.error || 'Unknown error'}`);
+ }
+ } catch (error) {
+ window.notify('error', `Error uploading file "${file.name}"`);
+ }
+ }
+
+ // Clear the file input
+ event.target.value = '';
+ },
+ removeFile: index => this.uploadedFiles.splice(index, 1),
+ async submit() {
+ this.submitLoading = true;
+
+ const fileIds = this.uploadedFiles.map(file => file.id);
+ const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseShippingNote/createNewLogAction`, {
+ shippingNoteId: this.shippingNoteId,
+ note: this.note,
+ fileIds: JSON.stringify(fileIds),
+ });
+
+ if (response.data.success) {
+ this.$emit('close');
+ window.notify('success', response.data.message ?? 'Log Eintrag erfolgreich erstellt');
+ } else {
+ window.notify('error',
+ response.data.errors ? Object.values(response.data.errors).join(' ') : response.data.message || 'Ein Fehler ist aufgetreten');
+ }
+ this.submitLoading = false;
+ },
+ },
+ template: `
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+ {{ file.name }}
+
+
+
+
+
+
+
+
+
+ `
+})
+
+Vue.component('tt-file', {
+ props: ['id'],
+ data: () => ({file: null}),
+ async mounted() {
+ const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/File/getById`, {params: {id: this.id}});
+ this.file = response.data;
+ },
+ template: `
+
+ `
+})
+
+
+Vue.component('warehouse-shipping-note-logs', {
+ props: {
+ shippingNoteId: {type: Number, required: true},
+ },
+ data() {
+ return {
+ logs: [], loading: false,
+ };
+ },
+ //language=Vue
+ template: `
+
+
+
+
+
+
+
+ {{ formatDate(log.create) }} ({{ getUserName(log.createBy) }}) | {{ log.message }}
+
+
+
+
+
+
+
+
+ `,
+ async mounted() {
+ this.loading = true;
+ const response = await axios.get(window.TT_CONFIG.BASE_PATH + '/WarehouseShippingNote/getLog', {params: {shippingNoteId: this.shippingNoteId}});
+ this.logs = response.data;
+ this.loading = false;
+ },
+ methods: {
+ formatDate: date => window.moment(date * 1000).format('DD.MM.YYYY HH:mm'),
+ getUserName: id => window.TT_CONFIG.CRUD_CONFIG.columns.find(col => col.key === 'createBy')?.modal.items.find(u => u.value === id)?.text
+ }
+})
+
// noinspection JSUnusedLocalSymbols
Vue.component('warehouse-shipping-note', {
//language=Vue
@@ -98,21 +263,25 @@ Vue.component('warehouse-shipping-note', {
+
+
@@ -123,7 +292,8 @@ Vue.component('warehouse-shipping-note', {
historyModal: false,
historyModalId: null,
shippingNoteModalId: null,
- signingShippingNoteId: null
+ signingShippingNoteId: null,
+ addLogModalId: null
}
},
mounted() {
diff --git a/public/js/pages/WarehouseShippingNote/WarehouseShippingNoteModal.js b/public/js/pages/WarehouseShippingNote/WarehouseShippingNoteModal.js
index 42d264e95..f96ab9554 100644
--- a/public/js/pages/WarehouseShippingNote/WarehouseShippingNoteModal.js
+++ b/public/js/pages/WarehouseShippingNote/WarehouseShippingNoteModal.js
@@ -1,790 +1,184 @@
-Vue.component('warehouse-shipping-note-modal-text-elements', {
- props: {
- textElements: Array
- },
- data() {
- return {
- window: window,
- textElementsData: [],
- }
- },
- //language=Vue
- template: `
-
-
-
-
-
-
-
-
-
-
-
-
-
- `,
- async mounted() {
- const response = await axios.get(window.TT_CONFIG["BASE_PATH"] + '/WarehouseShippingNote/getAllTextElements');
- this.textElementsData = response.data;
- }
-})
-
-// TODO: maybe also think about creating a component for simple forms like this
-Vue.component('warehouse-shipping-note-modal-hours-entry', {
- props: {
- index: {type: [Number], required: false, default: null},
- showHourlyPrice: {type: Boolean, default: false},
- },
- data() {
- return {
- window: window,
- userApiUrl: window.TT_CONFIG['BASE_PATH'] + '/WarehouseShippingNote/userAutoComplete',
- carApiUrl: window.TT_CONFIG['BASE_PATH'] + '/WarehouseShippingNote/timerecordingCarAutoComplete',
- userId: '',
- carId: '',
- date: '',
- hourCount: '',
- kilometerCount: '',
- hourlyPrice: '',
- priceType: 'normal',
- }
- },
- //language=Vue
- template: `
-
-
-
-
-
-
-
-
-
-
-
-
- `,
- methods: {
- async createOrUpdate() {
- if (!this.userId || !this.date || !this.hourCount) {
- this.window.notify('error', 'Bitte füllen Sie alle Felder aus');
- return;
- }
-
- this.$emit(this.index === null ? 'create' : 'update', {
- userId: this.userId,
- date: this.date,
- hourCount: this.hourCount,
- priceType: this.priceType,
- hourlyPrice: this.hourlyPrice || null,
- carId: this.carId ? this.carId : null,
- kilometerCount: this.carId ? this.kilometerCount : null
- });
- // TODO: maybe make this cleaner
- Object.assign(this.$data, this.$options.data.apply(this))
- await this.$nextTick();
- this.userId = this.window.TT_CONFIG['USER_ID']
- this.updateDate();
- this.updateKilometerCount().then();
- this.updateCarId().then();
- },
- async updateKilometerCount() {
- if (!this.carId) {
- this.kilometerCount = '';
- return;
- }
- const delAddr = this.$parent.$parent.$parent.delAddrLine +
- ' ' +
- this.$parent.$parent.$parent.delAddrCity +
- ' ' +
- this.$parent.$parent.$parent.delAddrPLZ;
- const response = await axios.get(window.TT_CONFIG["BASE_PATH"] + '/WarehouseShippingNote/getDistance?from=Xinon%20GmbH&to=' + delAddr);
- this.kilometerCount = response.data.distance
- },
- async updateCarId() {
- if (!this.userId || this.carId) return;
- const response = await axios.get(window.TT_CONFIG["BASE_PATH"] + '/WarehouseShippingNote/timerecordingCarForUser?userId=' + this.userId);
- if (response.data.status === 'USER_NO_CAR') {
- // this.window.notify('info', 'Kein zugewiesenes Fahrzeug gefunden');
- this.carId = '';
- return;
- }
- this.carId = response.data.id;
- },
- updateDate() {
- if (!this.date) {
- const today = new Date();
- const dd = String(today.getDate()).padStart(2, '0');
- const mm = String(today.getMonth() + 1).padStart(2, '0');
- const yyyy = today.getFullYear();
- this.date = `${yyyy}-${mm}-${dd}`;
- }
- }
- },
- async mounted() {
- if (!this.userId) this.userId = this.window.TT_CONFIG['USER_ID'];
- if (!this.carId) this.updateCarId().then();
- if (!this.date) this.updateDate();
- if (!this.kilometerCount) this.updateKilometerCount().then();
-
- this.$parent.$parent.$parent.$watch('delAddrLine', this.updateKilometerCount);
- this.$watch('carId', this.updateKilometerCount);
-
- }
-})
-
-// TODO: we should create this to a tt-simple-table component
-Vue.component('warehouse-shipping-note-modal-hours-view', {
- props: {
- hoursEntries: {type: Array, required: true},
- showHourlyPrice: {type: Boolean, default: false},
- },
- data() {
- return {
- window: window,
- userNames: {}
- }
- },
- //language=Vue
- template: `
-
-
-
-
- | Mitarbeiter |
- Datum |
- ST |
- KM |
- Stundenlohn |
- Aktionen |
-
-
-
-
- | Keine Einträge |
-
-
- | {{ userNames[entry.userId] }} |
- {{ window.moment(entry.date).format('DD.MM.YYYY') }} |
- {{ entry.hourCount }} |
- {{ entry.kilometerCount }} |
- {{ entry.hourlyPrice }} |
-
-
-
- |
-
-
-
-
- `,
- // add a method and a watcher to fetch the user names
- methods: {
- async fetchUserNames() {
- for (const entry of this.hoursEntries) {
- if (!entry.userId) continue;
- if (entry.userId in this.userNames) continue;
-
- const response = await axios.get(window.TT_CONFIG["BASE_PATH"] + '/WarehouseShippingNote/userAutoComplete?searchedID=' + entry.userId);
- this.$set(this.userNames, entry.userId, response.data[0].text);
- }
- }
- },
- watch: {
- hoursEntries: {
- handler: 'fetchUserNames', immediate: true
- }
- },
-})
-
-// this component will combine the above 2 components and show the entries and the input fields
-Vue.component('warehouse-shipping-note-modal-hours', {
- props: {
- hoursEntries: {type: Array, required: true},
- showHourlyPrice: {type: Boolean, default: false},
- },
- data() {
- return {
- window: window,
- selectedUpdateIndex: null,
- }
- },
- //language=Vue
- template: `
-
-
-
-
- `,
- methods: {
- create(entry) {
- this.$emit('update:hoursEntries', [...this.hoursEntries, entry]);
- this.window.notify('success', 'Eintrag erstellt');
- },
- update(entry) {
- this.$emit('update:hoursEntries', this.hoursEntries.map((oldEntry, index) => index === this.selectedUpdateIndex ? entry : oldEntry));
- this.window.notify('success', 'Eintrag aktualisiert');
- this.selectedUpdateIndex = null;
- },
- deleteEntry(entry) {
- this.$emit('update:hoursEntries', this.hoursEntries.filter(oldEntry => oldEntry !== entry));
- this.window.notify('success', 'Eintrag gelöscht');
- },
- editEntry(entry) {
- this.selectedUpdateIndex = this.hoursEntries.indexOf(entry);
- this.$refs.entry.userId = entry.userId;
- this.$refs.entry.date = entry.date;
- this.$refs.entry.hourCount = entry.hourCount;
- this.$refs.entry.note = entry.note;
- this.$refs.entry.hourlyPrice = entry.hourlyPrice;
- }
- }
-})
-
-// now we need the same as above for positions
-// so we need warehouse-shipping-note-modal-positions-entry, warehouse-shipping-note-modal-positions-view and warehouse-shipping-note-modal-positions
-// positions have a article or article packet, amount and price
-// when a article or article packet is selected we should fetch the name and description
-// then fetch the default price for the address
-Vue.component('warehouse-shipping-note-modal-positions-entry', {
- props: {
- index: {type: [Number], required: false, default: null},
- billAddrId: {type: [String, Number], required: true},
- },
- data() {
- return {
- window: window,
- isAdmin: false,
- articleApiUrl: window.TT_CONFIG['BASE_PATH'] + '/WarehouseArticle/autoComplete',
- articlePacketApiUrl: window.TT_CONFIG['BASE_PATH'] + '/WarehouseArticlePacket/autoComplete',
- articleId: '',
- articlePacketId: '',
- amount: '',
- price: '',
- isEnergieMaterial: false,
- }
- },
- //language=Vue
- template: `
-
-
-
-
-
-
-
-
-
-
- `,
- methods: {
- // TODO: if articlePacket is needed we need to implement this
- async createOrUpdate() {
- if (!this.amount) return this.window.notify('error', 'Bitte füllen sie die Menge aus');
- const data = {
- amount: this.amount,
- isEnergieMaterial: this.isEnergieMaterial,
- price: parseFloat(this.price) ?? ''
- }
- if (isNaN(data.price)) data.price = '';
- if (!this.articleId && this.$refs.article.displayValue) {
- data.articleText = this.$refs.article.displayValue;
- } else if (this.articleId) {
- data.article = this.articleId;
- } else {
- return this.window.notify('error', 'Bitte wählen Sie einen Artikel aus');
- }
-
- this.$emit(this.index === null ? 'create' : 'update', data);
- Object.assign(this.$data, this.$options.data.apply(this))
- },
- async fetchPrice() {
- if (!this.articleId && !this.articlePacketId || !this.billAddrId) return;
-
- const url = `${window.TT_CONFIG["BASE_PATH"]}/WarehouseShippingNote/getArticleAddressPrice?articleId=${this.articleId ||
- this.articlePacketId}&addressId=${this.billAddrId}`;
- const response = await axios.get(url);
- this.price = response.data.price;
- }
- },
- watch: {
- articleId: {handler: 'fetchPrice', immediate: false},
- articlePacketId: {handler: 'fetchPrice', immediate: false},
- billAddrId: {handler: 'fetchPrice', immediate: false},
- },
-})
-
-// here will warehouse-shipping-note-modal-positions-view show the positions in a table
-Vue.component('warehouse-shipping-note-modal-positions-view', {
- props: {
- positions: {type: Array, required: true},
- },
- data() {
- return {
- window: window,
- isAdmin: false,
- articleNames: {},
- articlePacketNames: {},
- }
- },
- //language=Vue
- template: `
-
-
-
-
- | Artikel |
- Menge |
- Energie Material |
- Preis |
- Aktionen |
-
-
-
-
- | Keine Einträge |
-
-
- | {{ position.article ? articleNames[position.article] : position.articlePacket ? articlePacketNames[position.articlePacket] :
- position.articleText }}
- |
- {{ position.amount }} |
- {{ position?.isEnergieMaterial ? 'Ja' : 'Nein' }} |
- {{ (position.price?.toFixed(2)) }} € |
-
-
-
- |
-
-
-
-
- `,
- methods: {
- async fetchNames() {
- // TODO: there must be a better way to do this
- for (const position of this.positions) {
- if (position.article) this.$set(this.articleNames, position.article, 'Loading...');
- if (position.articlePacket) this.$set(this.articlePacketNames, position.articlePacket, 'Loading...');
- }
-
- const articlePromises = this.positions.filter(position => position.article)
- .map(position => axios.get(window.TT_CONFIG["BASE_PATH"] + '/WarehouseArticle/autoComplete?searchedID=' + position.article));
- const articlePacketPromises = this.positions.filter(position => position.articlePacket)
- .map(position => axios.get(window.TT_CONFIG["BASE_PATH"] + '/WarehouseArticlePacket/autoComplete?searchedID=' + position.articlePacket));
-
- const articleResponses = await Promise.all(articlePromises);
- const articlePacketResponses = await Promise.all(articlePacketPromises);
-
- for (const response of articleResponses) {
- this.$set(this.articleNames, response.data[0].value, response.data[0].text);
- }
-
- for (const response of articlePacketResponses) {
- this.$set(this.articlePacketNames, response.data[0].value, response.data[0].text);
- }
-
- }
- },
- // watch positions and fetch article / article packet names - and initially fill them with Loading...
- watch: {
- positions: {
- handler: 'fetchNames', immediate: true
- }
- }
-})
-
-// and here we combine the above 2 components
-Vue.component('warehouse-shipping-note-modal-positions', {
- props: {
- positions: {type: Array, required: true},
- billAddrId: {type: [String, Number], required: true},
- },
- data() {
- return {
- window: window,
- articleNames: {},
- articlePacketNames: {},
- selectedUpdateIndex: null,
- }
- },
- //language=Vue
- template: `
-
-
-
-
- `,
- methods: {
- create(entry) {
- this.$emit('update:positions', [...this.positions, entry]);
- this.window.notify('success', 'Eintrag erstellt');
- },
- update(entry) {
- this.$emit('update:positions', this.positions.map((oldEntry, index) => index === this.selectedUpdateIndex ? entry : oldEntry));
- this.window.notify('success', 'Eintrag aktualisiert');
- this.selectedUpdateIndex = null;
- },
- deleteEntry(entry) {
- this.$emit('update:positions', this.positions.filter(oldEntry => oldEntry !== entry));
- this.window.notify('success', 'Eintrag gelöscht');
- },
- editEntry(entry) {
- this.selectedUpdateIndex = this.positions.indexOf(entry);
- if (entry.article) this.$refs.entry.articleId = entry.article;
- if (entry.articlePacket) this.$refs.entry.articlePacketId = entry.articlePacket;
- if (entry.articleText) this.$refs.entry.$refs.article.displayValue = entry.articleText;
- this.$refs.entry.amount = entry.amount;
- this.$refs.entry.price = entry.price;
- },
- },
-})
-
-
// noinspection EqualityComparisonWithCoercionJS
Vue.component('warehouse-shipping-note-modal', {
props: {
id: {type: [String, Number], required: true},
- // available modes are ['sign', 'edit', 'accept', 'create']
- mode: {type: String, default: 'sign'}
},
data() {
return {
window: window,
+ loading: false,
billAddrAutoCompleteUrl: window.TT_CONFIG['BASE_PATH'] + '/Address/Api?do=findAddress&fibu_primary_account=1',
- billAddrId: '',
- delAddrName: '',
- delAddrLine: '',
- delAddrPLZ: '',
- delAddrCity: '',
- delAddrEMail: '',
- status: '',
- note: '',
- textElements: [],
- hoursEntries: [],
- positions: [],
+ geoAutocompleteUrl: window.TT_CONFIG['BASE_PATH'] + '/WarehouseShippingNote/geoAutocomplete',
+ shippingNote: {
+ billingAddressId: null,
+ deliveryAddressName: '',
+ deliveryAddressEMail: '',
+ deliveryAddressLine: '',
+ deliveryAddressPLZ: '',
+ deliveryAddressCity: '',
+ status: 'new',
+ positions: [],
+ textElements: [],
+ hoursEntries: [],
+ },
+ geoAddr: '',
+ positionsConfig: {
+ customOrdering: 'article',
+ fields: {
+ article: {
+ type: 'autocomplete',
+ label: 'Artikel',
+ apiUrl: '/WarehouseArticle/autoComplete',
+ customFieldReference: 'WarehouseArticle',
+ emitDisplayValue: true,
+ },
+ amount: {type: 'input', label: 'Menge', inputType: 'number'},
+ isEnergieMaterial: {type: 'checkbox', label: 'Energie Material'},
+ },
+ validateFormOptions: [
+ {key: 'article', message: 'Bitte füllen Sie den Artikel aus'},
+ {key: 'amount', message: 'Bitte füllen Sie die Menge aus'},
+ ]
+ },
+ hoursConfig: {
+ fields: {
+ userId: {
+ type: 'autocomplete',
+ label: 'Mitarbeiter',
+ apiUrl: '/WarehouseShippingNote/userAutoComplete',
+ emitDisplayValue: true,
+ },
+ date: {type: 'input', label: 'Datum', inputType: 'date'},
+ hourCount: {type: 'input', label: 'Stunden', inputType: 'number'},
+ carId: {
+ type: 'autocomplete',
+ label: 'Auto',
+ apiUrl: '/WarehouseShippingNote/timerecordingCarAutoComplete',
+ emitDisplayValue: true,
+ showCondition: (formData) => {
+ return !isNaN(parseInt(formData.userId))
+ },
+ },
+ externalCar: {
+ type: 'checkbox',
+ label: 'Externes Auto',
+ showCondition: (formData) => {
+ return isNaN(parseInt(formData.userId)) || !formData.userId
+ },
+ },
+ kilometerCount: {type: 'input', label: 'Kilometer', inputType: 'number'},
+ hourType: {type: 'select', label: 'Stundenart', options: [
+ {text: 'Normal', value: undefined},
+ {text: '+50%', value: '50'},
+ {text: '+100%', value: '100'},
+ {text: 'Regie', value: 'regie'},
+ ]},
+ comment: {type: 'input', label: 'Kommentar'},
+ },
+ validateFormOptions: [
+ {key: 'userId', message: 'Bitte füllen Sie den Mitarbeiter aus'},
+ {key: 'hourCount', message: 'Bitte füllen Sie die Stunden aus'},
+ ]
+ }
+
+
}
},
-
//language=Vue
template: `
-
+
- Liefer- und Rechnungsadresse
-
-
-
-
-
-
-
- Textelemente
-
-
-
-
-
-
-
-
-
- Stunden
-
-
-
-
- Positionen
-
-
-
- Bitte füllen Sie die Rechnungs- und Lieferadresse aus
-
+ Adresse
+
+
+
+
+ {{ shippingNote.deliveryAddressLine }} {{ shippingNote.deliveryAddressPLZ }} {{ shippingNote.deliveryAddressCity }}
+
+
+
+
+
+
+
+
+
+ Stunden
+
+
+
+ Positionen
+
+
+ Bitte Lieferadresse eingeben
+
-
-
-
-
+
`,
-
- // now we need methods for fetching the shipping note, submiting the shipping note and translate the keys as they are different in the backend
async mounted() {
- // fetch by /getById?id=ID
- if (this.id !== 'create') {
- const response = await axios.get(window.TT_CONFIG["BASE_PATH"] + '/WarehouseShippingNote/getById?id=' + this.id);
- this.billAddrId = response.data.billingAddressId;
- this.delAddrName = response.data.deliveryAddressName;
- this.delAddrLine = response.data.deliveryAddressLine;
- this.delAddrPLZ = response.data.deliveryAddressPLZ;
- this.delAddrCity = response.data.deliveryAddressCity;
- this.delAddrEMail = response.data.deliveryAddressEMail;
- this.note = response.data.note;
- this.status = response.data.status;
+ if (this.id === 'create') return;
- for (const key of ['textElements', 'hoursEntries', 'positions']) {
- try {
- this[key] = JSON.parse(response.data[key]);
- } catch {
- this.textElements = [];
- }
- }
- } else {
- const knr = new URLSearchParams(window.location.search).get('knr');
- if (knr) {
- const response = await axios.get(window.TT_CONFIG["BASE_PATH"] + '/Address/Api?do=findAddress&fibu_primary_account=1&autocomplete=1&q=' + knr);
- for (const address of response.data) {
- if (address.text.endsWith(`[${knr}]`)) {
- this.billAddrId = address.value;
- break;
- }
- }
- }
+ const {data} = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseShippingNote/getById`, {params: {id: this.id}});
+ this.shippingNote = {...data, positions: JSON.parse(data.positions), hoursEntries: JSON.parse(data.hoursEntries)};
+ },
+ watch: {
+ geoAddr: async function() {
+ const [lat, lon] = this.geoAddr.split(',');
+ const { address } = (await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseShippingNote/geoReverse?lat=${lat}&lon=${lon}`)).data;
+
+ const addrKey = ['road', 'village', 'hamlet', 'residential', 'city'].find(k => address[k]);
+ this.shippingNote.deliveryAddressLine = addrKey ? `${address[addrKey]}${address["house_number"] ? ` ${address["house_number"]}` : ''}` : '';
+
+ this.shippingNote.deliveryAddressPLZ = address["postcode"];
+ this.shippingNote.deliveryAddressCity = address["village"] ?? address.city ?? address["town"];
}
},
- methods: {
- async reqDelete() {
- const response = await axios.post(window.TT_CONFIG['DELETE_URL'], {id: this.id});
- if (response.data.success) {
- this.window.notify('success', response.data.message || 'Erfolgreich gelöscht');
- this.$emit('close');
- } else {
- this.window.notify('error', response.data.message || 'Ein Fehler ist aufgetreten');
- }
- },
- async changeStatus(newStatus) {
- const response = await axios.post(window.TT_CONFIG.BASE_PATH + '/WarehouseShippingNote/changeStatus', {id: this.id, status: newStatus});
- if (response.data.success) {
- this.window.notify('success', response.data.message || 'Erfolgreich aktualisiert');
- this.status = newStatus;
- this.$emit('close');
- } else {
- this.window.notify('error', response.data.message || 'Ein Fehler ist aufgetreten');
- }
- },
+ methods: {
async submit() {
- const data = {
- billingAddressId: this.billAddrId,
- deliveryAddressName: this.delAddrName,
- deliveryAddressLine: this.delAddrLine,
- deliveryAddressPLZ: this.delAddrPLZ,
- deliveryAddressCity: this.delAddrCity,
- deliveryAddressEMail: this.delAddrEMail,
- textElements: this.textElements,
- hoursEntries: this.hoursEntries,
- positions: this.positions,
- note: this.note,
- status: this.status ? this.status : 'new'
- }
+ this.loading = true;
+ if (!this.shippingNote.positions.length && !this.shippingNote.hoursEntries.length)
+ return window.notify('error', 'Mindestens eine Position oder eine Stundenbuchung sind erforderlich');
- if (this.id !== 'create') data.id = this.id;
+ const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseShippingNote/${this.id === 'create' ? 'create' : 'update'}`, this.shippingNote);
- const url = this.id === 'create' ? window.TT_CONFIG['CREATE_URL'] : window.TT_CONFIG['UPDATE_URL'];
- const response = await axios.post(url, data);
+ if ((this.id !== 'create' && response.data.success === true) || response.data.success === true) this.$emit('close');
- if (response.data.success) {
- this.window.notify('success', response.data.message || 'Erfolgreich gespeichert');
- this.$emit('close');
- } else {
- this.window.notify('error',
- response.data.errors ? Object.values(response.data.errors).join(' ') : response.data.message || 'Ein Fehler ist aufgetreten');
- }
-
- }
-
- },
- computed: {
- title() {
- return this.id === 'create' ? 'Lieferschein erstellen' : `Lieferschein #${this.id} bearbeiten`;
+ window.notify(response.data.success ? 'success' : 'error',
+ response.data.success
+ ? this.id === 'create' ? response.data.message : (response.data.message ?? 'Bestellung erfolgreich aktualisiert')
+ : (response.data.errors ? Object.values(response.data.errors).join(' ') : response.data.message || 'Ein Fehler ist aufgetreten')
+ );
+ this.loading = false;
},
- delAddrFilled() {
- if (this.id !== 'create') return true;
- return !!this.delAddrName && !!this.delAddrLine && !!this.delAddrPLZ && !!this.delAddrCity;
- }
- }
-
-})
-
-Vue.component('warehouse-shipping-note-modal-address', {
- props: {
- billAddrId: {type: [String, Number], required: true},
- delAddrName: {type: String, required: true},
- delAddrLine: {type: String, required: true},
- delAddrPLZ: {type: String, required: true},
- delAddrCity: {type: String, required: true},
- delAddrEMail: {type: String, required: true},
- },
- data() {
- return {
- window: window,
- addressModes: [{text: 'Wie Rechnungsadresse', value: 'billing'},
- {text: 'Bestehende Lieferadresse', value: 'existing'},
- {text: 'Andere Lieferadresse', value: 'new'}],
- addressMode: 'existing',
- addresses: [],
- fetchedBillAddr: null,
- selectedAddr: '',
- newAddrGeoLatLon: '',
- }
- },
- //language=Vue
- template: `
-
-
-
-
-
-
-
-
-
-
-
-
- Adresse: {{ delAddrLine }}, {{ delAddrPLZ }} {{ delAddrCity }}
-
-
-
-
-
- `,
- watch: {
- billAddrId: {handler: 'updateBillingMode', immediate: false},
- addressMode: {handler: 'fetchDeliveryAddresses', immediate: false},
- selectedAddr: {handler: 'setSelectedAddrValues', immediate: false},
- newAddrGeoLatLon: {handler: 'fetchGeoAddress', immediate: false},
- },
- methods: {
- async fetchGeoAddress() {
- if (!this.newAddrGeoLatLon) {
- this.$emit('update:delAddrLine', '');
- this.$emit('update:delAddrPLZ', '');
- this.$emit('update:delAddrCity', '');
- return;
- }
- const [lat, lon] = this.newAddrGeoLatLon.split(',');
- const response = await axios.get(window.TT_CONFIG["BASE_PATH"] + '/WarehouseShippingNote/geoReverse?lat=' + lat + '&lon=' + lon);
-
- if (response.data.address.road) {
- this.$emit('update:delAddrLine',
- `${response.data.address.road}${response.data.address.house_number ? ' ' + response.data.address.house_number : ''}`);
- } else if(response.data.address.village) {
- this.$emit('update:delAddrLine',
- `${response.data.address.village}${response.data.address.house_number ? ' ' + response.data.address.house_number : ''}`);
- } else if(response.data.address.hamlet) {
- this.$emit('update:delAddrLine',
- `${response.data.address.hamlet}${response.data.address.house_number ? ' ' + response.data.address.house_number : ''}`);
- } else if(response.data.address.residential) {
- this.$emit('update:delAddrLine',
- `${response.data.address.residential}${response.data.address.house_number ? ' ' + response.data.address.house_number : ''}`);
- } else if(response.data.address.city) {
- this.$emit('update:delAddrLine',
- `${response.data.address.city}${response.data.address.house_number ? ' ' + response.data.address.house_number : ''}`);
- }
-
- this.$emit('update:delAddrPLZ', response.data.address.postcode);
- this.$emit('update:delAddrCity', response.data.address.village || response.data.address.city || response.data.address.town);
+ async updateCarId(userId) {
+ if (!userId) return this.$refs.hoursManager.updateField('carId', null);
+ const {data} = await axios.get(window.TT_CONFIG["BASE_PATH"] + '/WarehouseShippingNote/timerecordingCarForUser?userId=' + userId);
+ if (data.status === 'USER_NO_CAR') this.$refs.hoursManager.updateField('carId', null);
+ else this.$refs.hoursManager.updateField('carId', data.id);
},
- async updateBillingMode() {
- await this.fetchDeliveryAddresses();
-
- // Here we check if the address is already in the list of addresses, if not we will set the addressMode to billing and fetch the billing address
- if (this.delAddrName && this.delAddrLine && this.delAddrPLZ && this.delAddrCity) {
- const foundAddress = this.addresses.find(address =>
- address.deliveryAddressName === this.delAddrName &&
- address.deliveryAddressLine === this.delAddrLine &&
- address.deliveryAddressPLZ === this.delAddrPLZ &&
- address.deliveryAddressCity === this.delAddrCity);
- if (foundAddress) {
- this.addressMode = 'existing';
- this.selectedAddr = foundAddress.id;
- } else {
- this.addressMode = 'new';
- }
- } else {
- this.addressMode = 'billing';
- await this.fetchBillingAddress();
- }
- },
- async fetchDeliveryAddresses(newVal, oldVal) {
- if ((oldVal === 'billing' || oldVal === 'existing') && newVal === 'new') {
- this.$emit('update:delAddrName', '');
- this.$emit('update:delAddrLine', '');
- this.$emit('update:delAddrPLZ', '');
- this.$emit('update:delAddrCity', '');
- this.$emit('update:delAddrEMail', '');
- return;
- }
-
- if (this.addressMode === 'billing' && this.billAddrId) {
- await this.fetchBillingAddress();
- return;
- }
-
- if (!this.billAddrId || this.addressMode !== 'existing' || this.fetchedBillAddr === this.billAddrId) return;
-
- const response = await axios.get(window.TT_CONFIG["BASE_PATH"] + '/WarehouseShippingNote/getDeliveryAddresses?billingAddressId=' + this.billAddrId);
-
- this.fetchedBillAddr = this.billAddrId;
- this.addresses = response.data.map(address => {
- address.value = address.id;
- address.text = `${address.deliveryAddressName} - ${address.deliveryAddressLine}, ${address.deliveryAddressPLZ} ${address.deliveryAddressCity}`;
- return address;
- });
- },
- setSelectedAddrValues() {
- if (!this.selectedAddr) return;
-
- const selectedAddress = this.addresses.find(address => address.id === parseInt(this.selectedAddr));
- if (!selectedAddress) {
- this.window.notify('error', 'Lieferadresse konnte nicht gefunden werden');
- return;
- }
-
- this.$emit('update:delAddrName', selectedAddress.deliveryAddressName);
- this.$emit('update:delAddrLine', selectedAddress.deliveryAddressLine);
- this.$emit('update:delAddrPLZ', selectedAddress.deliveryAddressPLZ);
- this.$emit('update:delAddrCity', selectedAddress.deliveryAddressCity);
- },
- async fetchBillingAddress() {
- const response = await axios.get(window.TT_CONFIG["BASE_PATH"] + '/Address/api?do=getAddress&id=' + this.billAddrId);
- if (response.data.status !== 'OK' || !response.data.result.address) {
- this.window.notify('error', 'Rechnungsadresse konnte nicht gefunden werden');
- return;
- }
- // TODO: here is still a bug that we fetch the billing address twice
- // this.window.notify('success', 'Rechnungsadresse gefunden');
-
- this.$emit('update:delAddrName',
- response.data.result.address.company || response.data.result.address.firstname + ' ' + response.data.result.address.lastname);
- this.$emit('update:delAddrLine', response.data.result.address.street);
- this.$emit('update:delAddrPLZ', response.data.result.address.zip);
- this.$emit('update:delAddrCity', response.data.result.address.city);
- this.$emit('update:delAddrEMail', response.data.result.address.email);
+ async updateKilometer(carId) {
+ if (!carId) return this.$refs.hoursManager.updateField('kilometerCount', null);
+ const delAddr = this.shippingNote.deliveryAddressLine + ' ' + this.shippingNote.deliveryAddressPLZ + ' ' + this.shippingNote.deliveryAddressCity;
+ const {data} = await axios.get(window.TT_CONFIG["BASE_PATH"] + '/WarehouseShippingNote/getDistance?from=Xinon%20GmbH&to=' + delAddr);
+ this.$refs.hoursManager.updateField('kilometerCount', data.distance);
}
}
})
-// now we need a signature pad component which will fire a close or a signed event and takes shipping note as a prop
-// when mounted it will load https://cdn.jsdelivr.net/npm/signature_pad@4.1.7/dist/signature_pad.umd.min.js
-// and display using a tt-modal
-// and when save/submit is clicked we will send it to /WarehouseShippingNote/sign?id=ID POST with the signature as a base64 encoded image string
-
Vue.component('warehouse-shipping-note-signature-pad', {
props: {
shippingNoteId: {type: Number, required: true}
diff --git a/public/plugins/vue/tt-components/css/tt-position-manager.css b/public/plugins/vue/tt-components/css/tt-position-manager.css
index ed770af35..679e04983 100644
--- a/public/plugins/vue/tt-components/css/tt-position-manager.css
+++ b/public/plugins/vue/tt-components/css/tt-position-manager.css
@@ -8,14 +8,20 @@
}
.positions-manager .form-container {
- display: flex;
- align-items: center; /* Vertically center */
- justify-content: flex-start;
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 1rem;
+ align-items: center;
padding-bottom: 1rem;
border-bottom: 1px solid #ddd;
}
+@media (max-width: 768px) {
+ .positions-manager .form-container {
+ grid-template-columns: minmax(100%, 1fr);
+ }
+}
+
.positions-manager .form-container .button-wrapper {
align-self: flex-end; /* Align button container at the bottom */
}
diff --git a/public/plugins/vue/tt-components/tt-position-manager.js b/public/plugins/vue/tt-components/tt-position-manager.js
index d66e9a34e..7fddfa35f 100644
--- a/public/plugins/vue/tt-components/tt-position-manager.js
+++ b/public/plugins/vue/tt-components/tt-position-manager.js
@@ -1,29 +1,33 @@
Vue.component('tt-resolver', {
props: {
- value: {type: Number, required: true},
- reference: {type: String, required: true},
- },
- data() {
- return {
- window: window,
- loading: true,
- text: '',
- }
+ value: { type: [Number, String], required: true },
+ reference: { type: String, required: true },
+ autocomplete: { type: Boolean, default: false }
},
+ data: () => ({
+ loading: true,
+ text: ''
+ }),
template: `
-
- {{ text }}
- `,
+
+ {{ text }}
+ `,
async created() {
- const entry = await axios.get(window.TT_CONFIG['BASE_PATH'] + '/' + this.reference + '/getById?id=' + this.value);
- this.text = entry.data.name ?? entry.data.title ?? entry.data.text ?? '[E] Key not found';
+ const endpoint = this.autocomplete
+ ? `${window.TT_CONFIG["BASE_PATH"]}${this.reference}?searchedID=${this.value}`
+ : `${window.TT_CONFIG["BASE_PATH"]}/${this.reference}/getById?id=${this.value}`;
+
+ const { data } = await axios.get(endpoint);
+
+ this.text = this.autocomplete ? data[0]?.text : data.name ?? data.title ?? data.text ?? '[E] Key not found';
this.loading = false;
}
-})
+});
+
Vue.component('tt-positions-manager',
{
@@ -48,6 +52,7 @@ Vue.component('tt-positions-manager',
|