Merge branch 'ShippingNote/rework' into 'master'
Shipping note/rework See merge request fronk/thetool!1124
This commit is contained in:
@@ -67,7 +67,7 @@
|
||||
<div>{{ addressLine_5 }}</div>
|
||||
</td>
|
||||
<td class="customer-details">
|
||||
<div>Rechnungsadresse</div>
|
||||
<div>{{ billingAddressHeader }}</div>
|
||||
<div>{{ billingAddressLine_1 }}</div>
|
||||
<div>{{ billingAddressLine_2 }}</div>
|
||||
<div>{{ billingAddressLine_3 }}</div>
|
||||
|
||||
@@ -96,6 +96,10 @@ TODO: enable option for showing prices
|
||||
<?= $shippingNote->note ?>
|
||||
</p>
|
||||
|
||||
<?php if($shippingNote->status === 'cancelled'): ?>
|
||||
<div style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(255, 0, 0, 0.5); text-align: center; font-size: 40pt; color: white; line-height: 60pt;">STORNIERT</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<table style="border-collapse: collapse; width: 100%;" id="invoiceTable">
|
||||
<tr style="font-weight: bold; border-bottom: 1px solid black;" class="uneven">
|
||||
<th style="text-align: center;padding-right: 6pt">Position</th>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
25
db/migrations/20250320130000_warehouse_modify_15.php
Normal file
25
db/migrations/20250320130000_warehouse_modify_15.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class WarehouseModify15 extends AbstractMigration {
|
||||
public function up(): void {
|
||||
if ($this->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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) . "'";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -118,6 +118,7 @@ class mfUpload_TmpFile {
|
||||
}
|
||||
|
||||
public function pdftotext() {
|
||||
if (!isset($cmd)) $cmd = "";
|
||||
$cmd .= PDFTOTEXT_BIN_PATH . " " . $this->tmp_name . " -";
|
||||
|
||||
$lines = [];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
@@ -1,43 +1,49 @@
|
||||
// here we need to change window.TT_CONFIG["CRUD_CONFIG"]["additionalActions"] to add actions and this conditions like this:
|
||||
// <button v-if="window.TT_CONFIG['WAREHOUSE_ADMIN'] == true && status === 'new'" class="btn btn-warning" @click="changeStatus('in_progress')">In
|
||||
// Bearbeitung
|
||||
// </button>
|
||||
// <button v-if="window.TT_CONFIG['WAREHOUSE_ADMIN'] == true && (status === 'new' || status === 'in_progress')" class="btn btn-success"
|
||||
// @click="changeStatus('accepted')">Akzeptieren
|
||||
// </button>
|
||||
// <button v-if="window.TT_CONFIG['WAREHOUSE_ADMIN'] == true && status === 'accepted'" class="btn btn-info" @click="changeStatus('invoiced')">
|
||||
// Verrechnet
|
||||
// </button>
|
||||
// <button class="btn btn-info" @click="$emit('open-signing-modal', id)">Unterschreiben</button>
|
||||
|
||||
// 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('<br>') : response.data.message || 'Ein Fehler ist aufgetreten');
|
||||
}
|
||||
this.submitLoading = false;
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<tt-modal :show="true" @submit="submit" :delete="false" @update:show="$emit('close')" title="Logeintrag hinzufügen" :save-loading="submitLoading">
|
||||
<template>
|
||||
|
||||
<div class="form-group" style="margin: 10px 0">
|
||||
<label>Dateiupload (Mehrere)</label>
|
||||
<input type="file" class="form-control" @change="handleFileUpload" multiple/>
|
||||
</div>
|
||||
|
||||
<div v-if="uploadedFiles.length" class="upload-success-alert">
|
||||
<div class="alert-header">
|
||||
<i class="fa fa-check-circle" aria-hidden="true"></i>
|
||||
<span v-if="uploadedFiles.length === 1">Datei erfolgreich hochgeladen</span>
|
||||
<span v-else>Dateien erfolgreich hochgeladen</span>
|
||||
</div>
|
||||
<ul class="file-list">
|
||||
<li v-for="(file, index) in uploadedFiles" :key="file.id" class="file-item">
|
||||
<i class="fa fa-file" aria-hidden="true"></i>
|
||||
<span class="file-name">{{ file.name }}</span>
|
||||
<button type="button" class="remove-btn" @click="removeFile(index)">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
<tt-textarea label="Bemerkung" v-model="note" sm/>
|
||||
</template>
|
||||
</tt-modal>
|
||||
`
|
||||
})
|
||||
|
||||
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: `
|
||||
<div>
|
||||
<a :href="'/File/download?id=' + id" target="_blank" v-if="file">{{ file.filename }}</a>
|
||||
<template v-else>
|
||||
<div class="spinner-border spinner-border-sm text-primary" role="status"><span class="sr-only">Loading...</span></div>
|
||||
</template>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
|
||||
|
||||
Vue.component('warehouse-shipping-note-logs', {
|
||||
props: {
|
||||
shippingNoteId: {type: Number, required: true},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
logs: [], loading: false,
|
||||
};
|
||||
},
|
||||
//language=Vue
|
||||
template: `
|
||||
<div>
|
||||
<div v-if="loading" class="text-center">
|
||||
<i class="fa fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
|
||||
<div v-if="!loading">
|
||||
<div v-for="log in logs">
|
||||
{{ formatDate(log.create) }} ({{ getUserName(log.createBy) }}) | {{ log.message }}
|
||||
<template v-if="log.fileIds">
|
||||
<div v-for="file in JSON.parse(log.fileIds)">
|
||||
<tt-file :id="file"/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
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', {
|
||||
<warehouse-history-modal :show.sync="historyModal" :id="historyModalId"/>
|
||||
<warehouse-shipping-note-signature-pad v-if="signingShippingNoteId" :shipping-note-id="signingShippingNoteId"
|
||||
@close="signingShippingNoteId = null"/>
|
||||
<add-log-modal-sn v-if="addLogModalId" :shipping-note-id="addLogModalId" @close="addLogModalId = null"/>
|
||||
|
||||
<button @click="shippingNoteModalId = 'create'" class="btn btn-primary">Lieferschein erstellen</button>
|
||||
|
||||
<tt-table-crud emit-edit
|
||||
@openHistory="historyModal = true; historyModalId = $event.id"
|
||||
@print="window.open(window.TT_CONFIG['BASE_PATH'] + '/WarehouseShippingNote/createPDF?id=' + $event.id)"
|
||||
@printWithPrice="window.open(window.TT_CONFIG['BASE_PATH'] + '/WarehouseShippingNote/createPDF?id=' + $event.id + '&price=true')"
|
||||
@status_to_progress="changeStatus($event.id, 'in_progress')"
|
||||
@status_to_accepted="changeStatus($event.id, 'accepted')"
|
||||
@status_to_invoiced="changeStatus($event.id, 'invoiced')"
|
||||
@status_to_on_hold="changeStatus($event.id, 'on_hold')"
|
||||
@status_to_cancelled="changeStatus($event.id, 'cancelled')"
|
||||
@add_log="addLogModalId = $event.id"
|
||||
@edit="shippingNoteModalId = $event.id"
|
||||
ref="table">
|
||||
|
||||
<template v-slot:expandedRow="{ row }">
|
||||
<warehouse-shipping-note-positions :positions="JSON.parse(row.positions)" :hours-entries="JSON.parse(row.hoursEntries)"/>
|
||||
<warehouse-shipping-note-logs :shipping-note-id="row.id"/>
|
||||
</template>
|
||||
|
||||
</tt-table-crud>
|
||||
@@ -123,7 +292,8 @@ Vue.component('warehouse-shipping-note', {
|
||||
historyModal: false,
|
||||
historyModalId: null,
|
||||
shippingNoteModalId: null,
|
||||
signingShippingNoteId: null
|
||||
signingShippingNoteId: null,
|
||||
addLogModalId: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
||||
@@ -1,790 +1,184 @@
|
||||
Vue.component('warehouse-shipping-note-modal-text-elements', {
|
||||
props: {
|
||||
textElements: Array
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
window: window,
|
||||
textElementsData: [],
|
||||
}
|
||||
},
|
||||
//language=Vue
|
||||
template: `
|
||||
<div style="display: flex; align-items: center; justify-content: center;">
|
||||
<template v-if="textElementsData.length > 0">
|
||||
<div v-for="textElement in textElementsData" style="display: inline-block; margin-right: 10px;">
|
||||
<input type="checkbox" v-model="textElements[textElement.id]" :id="'textElement' + textElement.id">
|
||||
<label :for="'textElement' + textElement.id" :title="textElement.content">{{ textElement.title }}</label>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="text-center">
|
||||
<i class="fa fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
`,
|
||||
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: `
|
||||
<div class="warehouse-shipping-note-modal-hours-entry-container" v-bind:class="{ 'hideHourlyPrice': !showHourlyPrice, 'hideKilometer': false }">
|
||||
<tt-autocomplete v-model="userId" :api-url="userApiUrl" label="Mitarbeiter" sm/>
|
||||
<tt-input v-model="date" label="Datum" type="date" sm/>
|
||||
<tt-input v-model="hourCount" label="Stunden" sm/>
|
||||
<tt-select v-model="priceType" label="Stundenart" sm :options="[{text: 'Normal', value: 'normal'}, {text: '+50%', value: '50'}, {text: '+100%', value: '100'}]"/>
|
||||
<tt-autocomplete v-model="carId" :api-url="carApiUrl" label="Fahrzeug" sm/>
|
||||
<tt-input v-model="hourlyPrice" label="Stundenlohn" type="number" sm v-if="showHourlyPrice"/>
|
||||
<tt-input :disabled="carId === ''" v-model="kilometerCount" label="Kilometer" sm/>
|
||||
<div class="warehouse-shipping-note-modal-hours-entry-actions">
|
||||
<button @click="createOrUpdate" class="btn btn-sm btn-primary">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
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: `
|
||||
<div style="display: flex; align-items: center; justify-content: center;">
|
||||
<table class="table table-striped table-sm" style="width: max-content">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Mitarbeiter</th>
|
||||
<th>Datum</th>
|
||||
<th>ST</th>
|
||||
<th>KM</th>
|
||||
<th v-if="showHourlyPrice">Stundenlohn</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="hoursEntries.length === 0">
|
||||
<td colspan="6" class="text-center">Keine Einträge</td>
|
||||
</tr>
|
||||
<tr v-for="entry in hoursEntries">
|
||||
<td>{{ userNames[entry.userId] }}</td>
|
||||
<td>{{ window.moment(entry.date).format('DD.MM.YYYY') }}</td>
|
||||
<td>{{ entry.hourCount }}</td>
|
||||
<td>{{ entry.kilometerCount }}</td>
|
||||
<td v-if="showHourlyPrice">{{ entry.hourlyPrice }}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-danger" @click="$emit('delete', entry)">Löschen</button>
|
||||
<button class="btn btn-sm btn-primary" @click="$emit('edit', entry)">Bearbeiten</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`,
|
||||
// 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: `
|
||||
<div>
|
||||
<warehouse-shipping-note-modal-hours-entry @create="create" @update="update" :index.sync="selectedUpdateIndex"
|
||||
:show-hourly-price="showHourlyPrice" ref="entry"/>
|
||||
<warehouse-shipping-note-modal-hours-view @delete="deleteEntry" @edit="editEntry" :hours-entries="hoursEntries"
|
||||
:show-hourly-price="showHourlyPrice"/>
|
||||
</div>
|
||||
`,
|
||||
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: `
|
||||
<div class="warehouse-shipping-note-modal-positions-entry-container" :class="{ 'hidePrice': !isAdmin }">
|
||||
<tt-autocomplete v-model="articleId" :api-url="articleApiUrl" label="Artikel" sm ref="article"/>
|
||||
<!-- <tt-autocomplete v-model="articlePacketId" :api-url="articlePacketApiUrl" label="Artikel Packet" sm/>-->
|
||||
<tt-input v-model="amount" label="Menge" sm/>
|
||||
<tt-checkbox v-model="isEnergieMaterial" label="Energie Material" sm/>
|
||||
<tt-input v-show="isAdmin" v-model="price" label="Preis" type="number" sm/>
|
||||
<div class="warehouse-shipping-note-modal-positions-entry-actions">
|
||||
<button @click="createOrUpdate" class="btn btn-sm btn-primary">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
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: `
|
||||
<div style="display: flex; align-items: center; justify-content: center;">
|
||||
<table class="table table-striped table-sm" style="width: max-content">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Artikel</th>
|
||||
<th>Menge</th>
|
||||
<th>Energie Material</th>
|
||||
<th v-if="isAdmin">Preis</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="positions.length === 0">
|
||||
<td colspan="4" class="text-center">Keine Einträge</td>
|
||||
</tr>
|
||||
<tr v-for="position in positions">
|
||||
<td>{{ position.article ? articleNames[position.article] : position.articlePacket ? articlePacketNames[position.articlePacket] :
|
||||
position.articleText }}
|
||||
</td>
|
||||
<td>{{ position.amount }}</td>
|
||||
<td>{{ position?.isEnergieMaterial ? 'Ja' : 'Nein' }}</td>
|
||||
<td v-if="isAdmin">{{ (position.price?.toFixed(2)) }} €</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-danger" @click="$emit('delete', position)">Löschen</button>
|
||||
<button class="btn btn-sm btn-primary" @click="$emit('edit', position)">Bearbeiten</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`,
|
||||
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: `
|
||||
<div>
|
||||
<warehouse-shipping-note-modal-positions-entry @create="create" @update="update" :index.sync="selectedUpdateIndex" :bill-addr-id="billAddrId"
|
||||
ref="entry"/>
|
||||
<warehouse-shipping-note-modal-positions-view @delete="deleteEntry" @edit="editEntry" :positions="positions"/>
|
||||
</div>
|
||||
`,
|
||||
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: `
|
||||
<tt-modal :show="true" @submit="submit" @delete="reqDelete" :delete="id !== 'create'" :title="title" @update:show="$emit('close')">
|
||||
<tt-modal :show="true"
|
||||
:delete="false"
|
||||
:title="id === 'create' ? 'Lieferschein erstellen' : 'Lieferschein bearbeiten'"
|
||||
@update:show="$emit('close')"
|
||||
@submit="submit"
|
||||
:save-loading="loading"
|
||||
>
|
||||
<div style="width: 99%">
|
||||
<h4 class="text-center">Liefer- und Rechnungsadresse</h4>
|
||||
<tt-autocomplete v-model="billAddrId" :api-url="billAddrAutoCompleteUrl" label="Rechnungsadresse" sm row/>
|
||||
<warehouse-shipping-note-modal-address :billAddrId="billAddrId" :del-addr-name.sync="delAddrName" :del-addr-line.sync="delAddrLine"
|
||||
:del-addr-p-l-z.sync="delAddrPLZ" :del-addr-city.sync="delAddrCity"
|
||||
:del-addr-e-mail.sync="delAddrEMail"/>
|
||||
|
||||
|
||||
<div v-show="delAddrFilled === true">
|
||||
<template v-if="window.TT_CONFIG['WAREHOUSE_ADMIN'] == true && 1 < 0">
|
||||
<hr>
|
||||
<h4 class="text-center">Textelemente</h4>
|
||||
<warehouse-shipping-note-modal-text-elements :text-elements="textElements"/>
|
||||
</template>
|
||||
|
||||
|
||||
<hr>
|
||||
<tt-textarea label="Art der Arbeit" v-model="note" sm row/>
|
||||
|
||||
|
||||
<hr>
|
||||
<h4 class="text-center">Stunden</h4>
|
||||
<warehouse-shipping-note-modal-hours :hours-entries.sync="hoursEntries" :show-hourly-price="false"/>
|
||||
|
||||
|
||||
<hr>
|
||||
<h4 class="text-center">Positionen</h4>
|
||||
<warehouse-shipping-note-modal-positions :positions.sync="positions" :bill-addr-id="billAddrId"/>
|
||||
</div>
|
||||
|
||||
<div v-show="delAddrFilled === false" class="text-center">Bitte füllen Sie die Rechnungs- und Lieferadresse aus</div>
|
||||
|
||||
<h4 class="text-center">Adresse</h4>
|
||||
|
||||
<tt-input v-model="shippingNote.deliveryAddressName" label="Name" sm row/>
|
||||
<tt-input v-model="shippingNote.deliveryAddressEMail" label="E-Mail" sm row/>
|
||||
<tt-autocomplete :api-url="geoAutocompleteUrl" label="Adresse*" sm row v-model="geoAddr"/>
|
||||
<div class="mb-1" v-if="shippingNote.deliveryAddressLine">{{ shippingNote.deliveryAddressLine }}<br>{{ shippingNote.deliveryAddressPLZ }} {{ shippingNote.deliveryAddressCity }}</div>
|
||||
|
||||
<template v-if="window.TT_CONFIG['WAREHOUSE_ADMIN'] == true">
|
||||
<tt-autocomplete v-model="shippingNote.billingAddressId" :api-url="billAddrAutoCompleteUrl" label="Rechnungsadresse" sm row/>
|
||||
</template>
|
||||
|
||||
<tt-textarea v-model="shippingNote.note" label="Art der Arbeit" sm row/>
|
||||
|
||||
<template v-if="shippingNote.deliveryAddressLine">
|
||||
<hr>
|
||||
<h4 class="text-center">Stunden</h4>
|
||||
<tt-positions-manager
|
||||
ref="hoursManager"
|
||||
@updateField-userId="updateCarId"
|
||||
@updateField-carId="updateKilometer"
|
||||
:config="hoursConfig" v-model="shippingNote.hoursEntries" sm row/>
|
||||
|
||||
<hr>
|
||||
<h4 class="text-center">Positionen</h4>
|
||||
<tt-positions-manager :config="positionsConfig" v-model="shippingNote.positions" sm row/>
|
||||
</template>
|
||||
<span v-else class="text-danger">Bitte Lieferadresse eingeben</span>
|
||||
|
||||
</div>
|
||||
<!-- TODO: fix these buttons-->
|
||||
<template v-slot:footer-prepend v-if="id !== 'create'">
|
||||
<button v-if="window.TT_CONFIG['WAREHOUSE_ADMIN'] == true && status === 'new'" class="btn btn-warning" @click="changeStatus('in_progress')">In
|
||||
Bearbeitung
|
||||
</button>
|
||||
<button v-if="window.TT_CONFIG['WAREHOUSE_ADMIN'] == true && (status === 'new' || status === 'in_progress')" class="btn btn-success"
|
||||
@click="changeStatus('accepted')">Akzeptieren
|
||||
</button>
|
||||
<button v-if="window.TT_CONFIG['WAREHOUSE_ADMIN'] == true && status === 'accepted'" class="btn btn-info" @click="changeStatus('invoiced')">
|
||||
Verrechnet
|
||||
</button>
|
||||
<button class="btn btn-info" @click="$emit('open-signing-modal', id)">Unterschreiben</button>
|
||||
</template>
|
||||
|
||||
</tt-modal>
|
||||
`,
|
||||
|
||||
// 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('<br>') : 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('<br>') : 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: `
|
||||
<div>
|
||||
<tt-select v-model="addressMode" :options="addressModes" label="Lieferadresse Art" sm row :disabled="billAddrId === ''"/>
|
||||
|
||||
<template v-if="addressMode === 'existing'">
|
||||
<tt-select v-model="selectedAddr" :options="addresses" label="Lieferadresse" sm row/>
|
||||
</template>
|
||||
|
||||
<template v-else-if="addressMode === 'new'">
|
||||
<tt-input :value="delAddrName" @input="$emit('update:delAddrName', $event)" label="Lieferadresse Name*" sm row/>
|
||||
<tt-input :value="delAddrEMail" @input="$emit('update:delAddrEMail', $event)" label="Lieferadresse E-Mail" sm row/>
|
||||
<tt-autocomplete :api-url="window.TT_CONFIG['BASE_PATH'] + '/WarehouseShippingNote/geoAutocomplete'" @input="newAddrGeoLatLon = $event"
|
||||
label="Adresse*" sm row/>
|
||||
|
||||
<span v-if="delAddrLine && delAddrPLZ && delAddrCity">Adresse: {{ delAddrLine }}, {{ delAddrPLZ }} {{ delAddrCity }}</span>
|
||||
<!-- <tt-input :value="delAddrLine" @input="$emit('update:delAddrLine', $event)" label="Lieferadresse" sm row/>-->
|
||||
<!-- <tt-input :value="delAddrPLZ" @input="$emit('update:delAddrPLZ', $event)" label="Lieferadresse PLZ" sm row/>-->
|
||||
<!-- <tt-input :value="delAddrCity" @input="$emit('update:delAddrCity', $event)" label="Lieferadresse Ort" sm row/>-->
|
||||
</template>
|
||||
</div>
|
||||
`,
|
||||
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}
|
||||
|
||||
@@ -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 */
|
||||
}
|
||||
|
||||
@@ -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: `
|
||||
<div v-if="loading" class="d-flex justify-content-center align-items-center">
|
||||
<div class="spinner-border spinner-border-sm text-primary" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else>{{ text }}</span>
|
||||
`,
|
||||
<div v-if="loading" class="d-flex justify-content-center align-items-center">
|
||||
<div class="spinner-border spinner-border-sm text-primary" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else>{{ text }}</span>
|
||||
`,
|
||||
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',
|
||||
</template>
|
||||
<div class="form-container">
|
||||
<template v-for="(field, key) in config.fields">
|
||||
<template v-if="typeof field.showCondition === 'function' ? field.showCondition(formData) : true">
|
||||
<slot :name="key" v-bind:field="field" v-bind:value="formData[key]">
|
||||
<tt-input
|
||||
v-if="field.type === 'input'"
|
||||
@@ -90,6 +95,7 @@ Vue.component('tt-positions-manager',
|
||||
:options="field.options"
|
||||
/>
|
||||
</slot>
|
||||
</template>
|
||||
</template>
|
||||
<div class="button-wrapper">
|
||||
<tt-button @click="saveEntry" sm :additional-class="selectedIndex === null ? 'btn-primary' : 'btn-success'"
|
||||
@@ -121,10 +127,12 @@ Vue.component('tt-positions-manager',
|
||||
<tr v-for="(position, index) in group" :key="groupMode ? groupName + index : index">
|
||||
<td v-for="(field, key) in config.fields">
|
||||
<tt-resolver
|
||||
v-if="field.customFieldReference && position[key]"
|
||||
:reference="field.customFieldReference"
|
||||
v-if="position[key] && (field.customFieldReference || field.type === 'autocomplete')"
|
||||
:autocomplete="!field.customFieldReference && field.type === 'autocomplete'"
|
||||
:reference="field.customFieldReference || field.apiUrl"
|
||||
:value="position[key]"
|
||||
/>
|
||||
<span v-else-if="field.type === 'checkbox'">{{ position[key] ? 'Ja' : 'Nein' }}</span>
|
||||
<span v-else>{{ formatFieldValue(position[key] ?? position[key + '_text'], field) }}</span>
|
||||
</td>
|
||||
<td class="d-flex justify-content-end">
|
||||
@@ -147,6 +155,8 @@ Vue.component('tt-positions-manager',
|
||||
this.$set(this.formData, key, value);
|
||||
},
|
||||
defaultValidateForm(formData) {
|
||||
console.log(formData);
|
||||
console.log(this.config["validateFormOptions"]);
|
||||
for (const field of this.config["validateFormOptions"]) {
|
||||
if (!(formData[field.key] || formData[field.key + '_text'])) {
|
||||
window.notify('error', field.message);
|
||||
@@ -157,7 +167,7 @@ Vue.component('tt-positions-manager',
|
||||
},
|
||||
checkEmitDisplayValueAutocomplete() {
|
||||
for (const [key, field] of Object.entries(this.config.fields)) {
|
||||
if (field.type === 'autocomplete' && field.emitDisplayValue && isNaN(this.formData[key])) {
|
||||
if (field.type === 'autocomplete' && field.emitDisplayValue && (isNaN(this.formData[key]) || !this.formData[key]) && this.$refs['autocomplete-' + key][0]) {
|
||||
this.$set(this.formData, key + '_text', this.$refs['autocomplete-' + key][0].displayValue);
|
||||
this.$delete(this.formData, key);
|
||||
}
|
||||
@@ -192,10 +202,19 @@ Vue.component('tt-positions-manager',
|
||||
},
|
||||
resetForm() {
|
||||
this.formData = {};
|
||||
for (const [key, field] of Object.entries(this.config.fields)) {
|
||||
if (field.type === 'autocomplete' && field.emitDisplayValue && this.$refs['autocomplete-' + key][0]) {
|
||||
this.$refs['autocomplete-' + key][0].displayValue = '';
|
||||
}
|
||||
if (field.inputType === 'date') {
|
||||
this.$set(this.formData, key, moment().format('YYYY-MM-DD'));
|
||||
}
|
||||
}
|
||||
this.selectedIndex = null;
|
||||
},
|
||||
formatFieldValue(value, field) {
|
||||
if (field.formatter) return field.formatter(value);
|
||||
if (field.inputType === 'date') return moment(value).format('DD.MM.YYYY');
|
||||
return value;
|
||||
},
|
||||
},
|
||||
@@ -212,6 +231,9 @@ Vue.component('tt-positions-manager',
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.resetForm();
|
||||
},
|
||||
computed: {
|
||||
groupedPositions() {
|
||||
const groups = {};
|
||||
|
||||
Reference in New Issue
Block a user