820 lines
37 KiB
PHP
820 lines
37 KiB
PHP
<?php
|
|
|
|
class WarehouseShippingNoteController extends TTCrud {
|
|
protected string $headerTitle = 'Lieferscheine';
|
|
protected bool $createText = false;
|
|
|
|
//@formatter:off
|
|
protected array $columns = [
|
|
['key' => 'id', 'text' => 'LS-Nr.', 'required' => false, 'modal' => false, 'table' => ['class' => 'text-nowrap']],
|
|
['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' => 'type', 'text' => 'Typ', 'required' => false],
|
|
['key' => 'deliveryAddressName', 'text' => 'L.-Adr. Name', 'required' => true],
|
|
['key' => 'deliveryAddressLine', 'text' => 'L.-Adr.', 'required' => true],
|
|
['key' => 'deliveryAddressPLZ', 'text' => 'L.-Adr. PLZ', 'required' => true],
|
|
['key' => 'deliveryAddressEMail', 'text' => 'L.-Adr. EMail', 'required' => false, 'table' => false],
|
|
['key' => 'note', 'text' => 'Art der Arbeit', 'required' => true, 'table' => false],
|
|
['key' => 'positions', 'text' => 'Positionen', 'required' => true, 'table' => false, 'modal' => false],
|
|
['key' => 'create', 'text' => 'Erstellt', 'required' => false, 'modal' => ['visible' => false], 'table' => ['filter' => 'date']],
|
|
['key' => 'createBy', 'text' => 'Erstellt von', 'required' => false, 'type' => 'autocomplete', 'table' => ['class' => 'text-nowrap', 'filter' => 'select'], 'modal' => ['items' => [], 'type' => 'select',]],
|
|
['key' => 'actions', 'text' => 'Aktionen', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center']],];
|
|
|
|
protected array $defaultOrder = ['key' => 'create', 'order' => 'DESC'];
|
|
protected array $additionalJSVariables = ['WAREHOUSE_ADMIN' => true, 'HIDE_MENU' => false];
|
|
protected array $infoMessages = ['create' => 'Lieferschein wurde erstellt.',
|
|
'update' => 'Lieferschein wurde aktualisiert',
|
|
'delete' => 'Lieferschein wurde gelöscht',
|
|
'noChanges' => 'Keine Änderungen vorgenommen'];
|
|
//@formatter:on
|
|
|
|
protected function prepareCrudConfig() {
|
|
if (!$this->user->can('WarehouseAdmin')) $this->additionalJSVariables['WAREHOUSE_ADMIN'] = false;
|
|
if (isset($_SESSION[MFAPPNAME . '_warehouse_login_override']) && is_numeric($_SESSION[MFAPPNAME . '_warehouse_login_override'])) $this->additionalJSVariables['HIDE_MENU'] = true;
|
|
}
|
|
|
|
protected function beforeCreate($postData): bool {
|
|
$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 beforeUpdate($postData): bool {
|
|
if (!$this->user->can('WarehouseAdmin')) {
|
|
$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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
private function validate($data, array $rules): void {
|
|
foreach ($rules as $rule) {
|
|
if (($result = $rule($data)) !== true) {
|
|
$this->sendError(is_string($result) ? $result : 'Validation failed');
|
|
}
|
|
}
|
|
}
|
|
|
|
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() {
|
|
empty($this->request->articleId) && $this->sendError('Keine Artikel ID gefunden');
|
|
empty($this->request->addressId) && $this->sendError('Keine Adress 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;
|
|
|
|
$article = WarehouseArticleModel::get($this->request->articleId);
|
|
$prices = json_decode($article->cheapestSellPrice, true);
|
|
|
|
foreach ($prices as $price)
|
|
if ($price['title'] === $priceType)
|
|
self::returnJson(['success' => true, 'price' => $price['price']]);
|
|
|
|
|
|
$this->sendError('Kein Preis gefunden');
|
|
}
|
|
|
|
protected function signAction() {
|
|
$id = $this->request->id;
|
|
|
|
if (empty($id) || !$shippingNote = (array) WarehouseShippingNoteModel::get($id)) $this->sendError('Lieferschein nicht gefunden');
|
|
if ($shippingNote["signature"] || $shippingNote["signatureName"]) $this->sendError('Bereits unterschrieben');
|
|
|
|
$post = json_decode(file_get_contents('php://input'), true);
|
|
|
|
if (empty($post['signature']) || empty($post['signatureName'])) {
|
|
$this->sendError('Unterschrift/Name fehlt');
|
|
}
|
|
|
|
try {
|
|
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) {
|
|
$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) {
|
|
http_response_code(500);
|
|
self::returnJson(['success' => false, 'message' => 'Lieferschein wurde nicht gefunden']);
|
|
}
|
|
|
|
$shippingNote = WarehouseShippingNoteModel::get($id);
|
|
if (!empty($shippingNote->billingAddressId)) $address = AddressModel::getOne($shippingNote->billingAddressId);
|
|
$positions = [];
|
|
|
|
// loop through all positions and add articleTitle and articleDescription to each position entry
|
|
foreach (json_decode($shippingNote->positions, true) as $position) {
|
|
if (isset($position['article'])) {
|
|
$article = WarehouseArticleModel::get($position['article']);
|
|
if (isset($article->articleNumber)) {
|
|
$position['articleTitle'] = $article->articleNumber . " | " . $article->title;
|
|
} else {
|
|
$position['articleTitle'] = $article->title;
|
|
}
|
|
|
|
if (isset($position['isEnergieMaterial']) && $position['isEnergieMaterial'] == 1) {
|
|
$position['articleTitle'] .= " (beigestelltes Material)";
|
|
}
|
|
|
|
$position['articleDescription'] = $article->description === $article->title ? "" : $article->description;
|
|
$position['articleUnit'] = $article->unit;
|
|
$positions[] = $position;
|
|
} elseif (isset($position['articlePacket'])) {
|
|
$articlePacket = WarehouseArticlePacketModel::get($position['articlePacket']);
|
|
$position['articleTitle'] = $articlePacket->title;
|
|
$position['articleDescription'] = $articlePacket->description === $articlePacket->title ? "" : $articlePacket->description;
|
|
$position['articleUnit'] = 'Stk.';
|
|
$positions[] = $position;
|
|
} elseif (isset($position['articleText'])) {
|
|
$position['articleTitle'] = $position['articleText'];
|
|
$position['articleDescription'] = "";
|
|
$position['articleUnit'] = 'Stk.';
|
|
$positions[] = $position;
|
|
} elseif (isset($position['article_text'])) {
|
|
$position['articleTitle'] = $position['article_text'];
|
|
$position['articleDescription'] = "";
|
|
$position['articleUnit'] = 'Stk.';
|
|
$positions[] = $position;
|
|
}
|
|
}
|
|
|
|
|
|
// json decode hoursEntries and add to positions
|
|
$hoursEntries = json_decode($shippingNote->hoursEntries, true);
|
|
foreach ($hoursEntries as $hoursEntry) {
|
|
// die(json_encode($hoursEntry));
|
|
$articleTitle = "Arbeitsstunden";
|
|
if (isset($hoursEntry['priceType']) && $hoursEntry['priceType'] == 50) {
|
|
$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' => $articleDescription,
|
|
'articleUnit' => 'Std.',
|
|
'amount' => $hoursEntry['hourCount'],
|
|
];
|
|
}
|
|
|
|
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'],
|
|
'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',
|
|
'amount' => $hoursEntry['kilometerCount'],
|
|
'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'],
|
|
'price' => 2 * $hoursEntry['kilometerCount'] ?? 0,
|
|
];
|
|
}
|
|
}
|
|
|
|
usort($positions, function ($a, $b) {
|
|
$aHasMitarbeiter = str_contains($a['articleDescription'], 'Mitarbeiter');
|
|
$bHasMitarbeiter = str_contains($b['articleDescription'], 'Mitarbeiter');
|
|
$aHasFahrzeug = str_contains($a['articleDescription'], 'Fahrzeug');
|
|
$bHasFahrzeug = str_contains($b['articleDescription'], 'Fahrzeug');
|
|
|
|
if ($aHasMitarbeiter && !$bHasMitarbeiter) return -1;
|
|
if (!$aHasMitarbeiter && $bHasMitarbeiter) return 1;
|
|
if ($aHasFahrzeug && !$bHasFahrzeug) return -1;
|
|
if (!$aHasFahrzeug && $bHasFahrzeug) return 1;
|
|
return 0;
|
|
});
|
|
|
|
|
|
|
|
$textElements = [];
|
|
// parse shippingNote.textElements ({"1":true,"2":true}) to array, fetch each text element and put content into array
|
|
$shippingNoteTextElements = json_decode($shippingNote->textElements, true);
|
|
foreach ($shippingNoteTextElements as $key => $value) {
|
|
if ($value) {
|
|
$textElement = WarehouseShippingNoteTextElementModel::get($key);
|
|
$textElements[] = $textElement->content;
|
|
}
|
|
}
|
|
if (empty($textElements)) {
|
|
$textElements = null;
|
|
}
|
|
|
|
$pdf_vars = ["shippingNote" => $shippingNote,
|
|
"positions" => $positions,
|
|
"textElements" => $textElements,
|
|
"showPrices" => isset($_GET['price']) && $_GET['price'] == "true",
|
|
"bank_iban" => TT_INVOICE_BANK_IBAN,
|
|
"bank_bic" => TT_INVOICE_BANK_BIC,
|
|
"bank_bank" => TT_INVOICE_BANK_BANK,
|
|
"bank_owner" => TT_INVOICE_BANK_OWNER];
|
|
|
|
// Replace placeholders in header
|
|
// create shipping note in this format LS2024-X0001
|
|
// pad number on the left side with zeros
|
|
$shippingNoteNumber = "LS" . date("Y", $shippingNote->create) . "-" . str_pad($shippingNote->id, 4, "0", STR_PAD_LEFT);
|
|
$headerHtml = file_get_contents(BASEDIR . "/Layout/default/WarehouseShippingNote/PDF_HEADER.html");
|
|
$headerHtml = str_replace("{{ basedir }}", BASEDIR, $headerHtml);
|
|
$headerHtml = str_replace("{{ addressLine_1 }}", $shippingNote->deliveryAddressName, $headerHtml);
|
|
$headerHtml = str_replace("{{ addressLine_2 }}", $shippingNote->deliveryAddressLine, $headerHtml);
|
|
$headerHtml = str_replace("{{ addressLine_3 }}", $shippingNote->deliveryAddressPLZ . " " . $shippingNote->deliveryAddressCity, $headerHtml);
|
|
$headerHtml = str_replace("{{ addressLine_4 }}", "", $headerHtml);
|
|
$headerHtml = str_replace("{{ addressLine_5 }}", "", $headerHtml);
|
|
$headerHtml = str_replace("{{ 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 }}", !isset($address) ? '' : $address->customer_number, $headerHtml);
|
|
$headerHtml = str_replace("{{ shippingNoteNumber }}", $shippingNoteNumber, $headerHtml);
|
|
$headerHtml = str_replace("{{ shippingNoteDate }}", date("d.m.Y", $shippingNote->create), $headerHtml);
|
|
|
|
$headerFile = BASEDIR . "/var/temp/shipping-note_header-" . date("U") . "-" . rand(1000, 9999) . ".html";
|
|
file_put_contents($headerFile, $headerHtml);
|
|
|
|
|
|
// Replace placeholders in header
|
|
$footerHtml = file_get_contents(BASEDIR . "/Layout/default/WarehouseShippingNote/PDF_FOOTER.html");
|
|
$footerHtml = str_replace("{{ bank_iban }}", TT_INVOICE_BANK_IBAN_FORMATTED, $footerHtml);
|
|
$footerHtml = str_replace("{{ bank_bic }}", TT_INVOICE_BANK_BIC, $footerHtml);
|
|
$footerHtml = str_replace("{{ bank_bank }}", TT_INVOICE_BANK_BANK, $footerHtml);
|
|
$footerHtml = str_replace("{{ bank_owner }}", TT_INVOICE_BANK_OWNER, $footerHtml);
|
|
|
|
|
|
$footerFile = BASEDIR . "/var/temp/shipping-note_header-" . date("U") . "-" . rand(1000, 9999) . ".html";
|
|
file_put_contents($footerFile, $footerHtml);
|
|
|
|
|
|
$pdf = new PdfForm("WarehouseShippingNote/PDF_MAIN", $pdf_vars);
|
|
$wkhtmltopdfArgs = "--header-html $headerFile --footer-html $footerFile";
|
|
$filename = $pdf->render($wkhtmltopdfArgs);
|
|
|
|
if ($returnFilename === true) return $filename;
|
|
|
|
// return the pdf and die so the client sees the pdf not the filename
|
|
header('Content-Type: application/pdf');
|
|
header('Content-Disposition: inline; filename="' . $filename . '"');
|
|
readfile($filename);
|
|
die();
|
|
}
|
|
|
|
// TODO: either move this to UserController or make it better
|
|
protected function userAutoCompleteAction() {
|
|
$users = array_map(function ($user) {
|
|
return ['value' => $user->id, 'text' => $user->name];
|
|
}, UserModel::search(['employee' => true]));
|
|
|
|
$out = null;
|
|
$searchedID = $this->request->searchedID;
|
|
if (strlen($searchedID) > 0) {
|
|
// find user with value searchedID
|
|
$out = array_filter($users, function ($user) use ($searchedID) {
|
|
return $user['value'] == $searchedID;
|
|
});
|
|
} else {
|
|
$out = array_filter($users, function ($user) {
|
|
;
|
|
return strpos(strtolower($user['text']), strtolower($this->request->q)) !== false;
|
|
});
|
|
|
|
$out = array_slice($out, 0, 10);
|
|
}
|
|
|
|
self::returnJson(array_values($out));
|
|
}
|
|
|
|
protected function changeStatusAction() {
|
|
$json = json_decode(file_get_contents('php://input'), true);
|
|
if (empty($json['id'])) self::sendError('Lieferschein wurde nicht gefunden');
|
|
|
|
$shippingNote = (array) WarehouseShippingNoteModel::get($json['id']);
|
|
if ($shippingNote['status'] === 'invoiced') self::sendError('Status kann nicht geändert werden');
|
|
|
|
$shippingNote['status'] = $json['status'];
|
|
WarehouseShippingNoteModel::update($shippingNote);
|
|
$statusNiceText = [
|
|
'new' => 'Neu',
|
|
'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[$json['status']] . ' geändert']);
|
|
}
|
|
|
|
//TODO: either move this to TimerecordingCarController or make it better
|
|
protected function timerecordingCarAutoCompleteAction() {
|
|
$timerecordingCars = array_map(function ($timerecordingCar) {
|
|
return ['value' => $timerecordingCar->id,
|
|
'text' => $timerecordingCar->number_plate . " " . $timerecordingCar->brand . " " . $timerecordingCar->model];
|
|
}, TimerecordingCarModel::getAll());
|
|
|
|
$out = null;
|
|
$searchedID = $this->request->searchedID;
|
|
if (strlen($searchedID) > 0) {
|
|
// find user with value searchedID
|
|
$out = array_filter($timerecordingCars, function ($timerecordingCar) use ($searchedID) {
|
|
return $timerecordingCar['value'] == $searchedID;
|
|
});
|
|
} else {
|
|
$out = array_filter($timerecordingCars, function ($timerecordingCar) {
|
|
return strpos(strtolower($timerecordingCar['text']), strtolower($this->request->q)) !== false;
|
|
});
|
|
|
|
$out = array_slice($out, 0, 10);
|
|
}
|
|
|
|
self::returnJson(array_values($out));
|
|
}
|
|
|
|
protected function timerecordingCarForUserAction() {
|
|
$timerecordingCars = TimerecordingCarModel::getAll();
|
|
foreach ($timerecordingCars as $timerecordingCar) {
|
|
if ($timerecordingCar->user_id == $this->request->userId) {
|
|
header('Content-Type: application/json');
|
|
die(json_encode(['success' => true, 'id' => $timerecordingCar->id]));
|
|
}
|
|
}
|
|
die(json_encode(['success' => true, 'status' => 'USER_NO_CAR']));
|
|
}
|
|
|
|
/**
|
|
* Helper function to parse Google Geocoding API response into the format expected by the frontend.
|
|
* @param array $components The address_components array from Google's response.
|
|
* @return array The formatted address array.
|
|
*/
|
|
private function formatGoogleAddress(array $components): array {
|
|
$address = [
|
|
'house_number' => null,
|
|
'road' => null,
|
|
'postcode' => null,
|
|
'city' => null,
|
|
'town' => null,
|
|
'village' => null,
|
|
'residential' => null,
|
|
'hamlet' => null,
|
|
];
|
|
|
|
foreach ($components as $component) {
|
|
$types = $component['types'];
|
|
if (in_array('street_number', $types)) {
|
|
$address['house_number'] = $component['long_name'];
|
|
}
|
|
if (in_array('route', $types)) {
|
|
$address['road'] = $component['long_name'];
|
|
}
|
|
if (in_array('postal_code', $types)) {
|
|
$address['postcode'] = $component['long_name'];
|
|
}
|
|
if (in_array('locality', $types)) {
|
|
$address['city'] = $component['long_name'];
|
|
$address['town'] = $component['long_name']; // Town is often the same as city
|
|
}
|
|
// A village in Google can be sublocality or sublocality_level_1
|
|
if (in_array('sublocality', $types) || in_array('sublocality_level_1', $types)) {
|
|
$address['village'] = $component['long_name'];
|
|
}
|
|
if (in_array('neighborhood', $types)) {
|
|
$address['residential'] = $component['long_name'];
|
|
if (!$address['village']) { // Fallback for village if not already set
|
|
$address['village'] = $component['long_name'];
|
|
}
|
|
}
|
|
}
|
|
|
|
// If city is null, try to find it in administrative areas
|
|
if (!$address['city']) {
|
|
foreach ($components as $component) {
|
|
if (in_array('administrative_area_level_3', $component['types'])) {
|
|
$address['city'] = $component['long_name'];
|
|
$address['town'] = $component['long_name'];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $address;
|
|
}
|
|
|
|
protected function geoAutocompleteAction() {
|
|
$search = urlencode($this->request->q);
|
|
$apiKey = TT_GEOCODING_API_SECRET;
|
|
// Bias search results for Austria (AT) in German (de)
|
|
$url = TT_GEOCODING_API_URL . "?address=$search&key=$apiKey®ion=at&language=de&components=country:AT";
|
|
|
|
$response = @file_get_contents($url);
|
|
if ($response === false) {
|
|
self::returnJson([]);
|
|
return;
|
|
}
|
|
|
|
$data = json_decode($response, true);
|
|
$out = [];
|
|
|
|
if ($data['status'] === 'OK') {
|
|
foreach ($data['results'] as $entry) {
|
|
$lat = $entry['geometry']['location']['lat'];
|
|
$lng = $entry['geometry']['location']['lng'];
|
|
$text = $entry['formatted_address'];
|
|
|
|
$hasHouseNumber = false;
|
|
foreach ($entry['address_components'] as $component) {
|
|
if (in_array('street_number', $component['types'])) {
|
|
$hasHouseNumber = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
$value = $lat . "," . $lng . (!$hasHouseNumber ? ", area" : "");
|
|
|
|
// Deduplication logic from original code
|
|
$isDuplicate = false;
|
|
foreach ($out as $existing) {
|
|
if ($existing['text'] === $text) {
|
|
$isDuplicate = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!$isDuplicate) {
|
|
$out[] = ['value' => $value, 'text' => $text];
|
|
}
|
|
}
|
|
}
|
|
self::returnJson($out);
|
|
}
|
|
|
|
protected function geoReverseAction() {
|
|
$lat = $this->request->lat;
|
|
$lon = $this->request->lon;
|
|
$apiKey = TT_GEOCODING_API_SECRET;
|
|
$url = TT_GEOCODING_API_URL . "?latlng=$lat,$lon&key=$apiKey&language=de";
|
|
|
|
$response = @file_get_contents($url);
|
|
if ($response === false) {
|
|
self::returnJson(['address' => [], 'error' => 'REQUEST_FAILED', 'error_message' => 'Could not connect to Google Maps API.']);
|
|
return;
|
|
}
|
|
|
|
$data = json_decode($response, true);
|
|
|
|
if ($data['status'] === 'OK' && !empty($data['results'])) {
|
|
$addressComponents = $data['results'][0]['address_components'];
|
|
$formattedAddress = $this->formatGoogleAddress($addressComponents);
|
|
self::returnJson(['address' => $formattedAddress]);
|
|
} else {
|
|
self::returnJson(['address' => [], 'error' => $data['status'], 'error_message' => $data['error_message'] ?? 'Reverse geocoding failed.']);
|
|
}
|
|
}
|
|
|
|
//TODO: export this to an api class for openstreetmap
|
|
protected function getDistanceAction() {
|
|
$this->request->to = str_replace("Gebiet ", "", $this->request->to);
|
|
$filename = TEMP_DIR . "/OpenStreetMap/" . urlencode($this->request->from) . "-" . urlencode($this->request->to) . ".json";
|
|
|
|
if (file_exists($filename)) {
|
|
$data = file_get_contents($filename);
|
|
self::returnJson(json_decode($data, true));
|
|
}
|
|
|
|
$from = $this->request->from;
|
|
$to = $this->request->to;
|
|
$from = urlencode($from);
|
|
$to = urlencode($to);
|
|
|
|
function geocode($address) {
|
|
if ($address === 'Xinon GmbH') {
|
|
return [['lat' => 46.99555015, 'lon' => 15.77507876755547]];
|
|
}
|
|
|
|
$address = urlencode($address);
|
|
$apiKey = TT_GEOCODING_API_SECRET;
|
|
// Bias search results for Austria
|
|
$url = TT_GEOCODING_API_URL . "?address=$address&key=$apiKey®ion=at&components=country:AT";
|
|
|
|
$response = @file_get_contents($url);
|
|
if ($response === false) {
|
|
die(json_encode(['success' => false, 'message' => 'Error while geocoding: API connection failed.']));
|
|
}
|
|
|
|
$data = json_decode($response, true);
|
|
|
|
if ($data['status'] === 'OK' && !empty($data['results'])) {
|
|
$location = $data['results'][0]['geometry']['location'];
|
|
// The OSRM routing engine expects 'lon', so we map Google's 'lng' to 'lon'.
|
|
return [['lat' => $location['lat'], 'lon' => $location['lng']]];
|
|
} else {
|
|
die(json_encode(['success' => false, 'message' => 'Error while geocoding: ' . ($data['error_message'] ?? $data['status'])]));
|
|
}
|
|
}
|
|
|
|
function route($from, $to) {
|
|
// The geocode() function (which you've already updated to use Google)
|
|
// will provide the necessary coordinates. It will `die()` on error, so we can
|
|
// assume we have valid data if the script continues.
|
|
$fromData = geocode($from);
|
|
$toData = geocode($to);
|
|
|
|
$fromLat = $fromData[0]['lat'];
|
|
$fromLon = $fromData[0]['lon'];
|
|
$toLat = $toData[0]['lat'];
|
|
$toLon = $toData[0]['lon'];
|
|
|
|
$apiKey = TT_GEOCODING_API_SECRET;
|
|
$url = "https://maps.googleapis.com/maps/api/directions/json" .
|
|
"?origin={$fromLat},{$fromLon}" .
|
|
"&destination={$toLat},{$toLon}" .
|
|
"&key={$apiKey}" .
|
|
"&mode=driving" .
|
|
"®ion=at"; // Bias results for Austria
|
|
|
|
$response = @file_get_contents($url);
|
|
if ($response === false) {
|
|
die(json_encode(['success' => false, 'message' => 'Could not connect to Google Directions API.']));
|
|
}
|
|
|
|
$data = json_decode($response, true);
|
|
|
|
// Check for a successful response and if a route was found
|
|
if ($data['status'] === 'OK' && !empty($data['routes'])) {
|
|
// The distance is provided in meters in the first "leg" of the first "route"
|
|
$distance = $data['routes'][0]['legs'][0]['distance']['value'];
|
|
|
|
// Return the round-trip distance
|
|
return $distance * 2;
|
|
} else {
|
|
// Handle cases where no route is found or another API error occurred
|
|
$errorMessage = $data['error_message'] ?? 'No route could be found between the locations.';
|
|
die(json_encode(['success' => false, 'message' => "Routing Error: " . $errorMessage]));
|
|
}
|
|
}
|
|
|
|
$distance = route($from, $to);
|
|
|
|
$roundedDistanceKm = round($distance / 1000, 0);
|
|
|
|
if (!file_exists(dirname($filename))) {
|
|
mkdir(dirname($filename), 0777, true);
|
|
}
|
|
file_put_contents($filename, json_encode(['success' => true, 'distance' => $roundedDistanceKm]));
|
|
|
|
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 getRecentCalendarEventsAction()
|
|
{
|
|
$worker = UserModel::getOne($this->user->id);
|
|
if (!$worker || !empty($worker->name)) {
|
|
die($worker->name);
|
|
self::returnJson([]);
|
|
return;
|
|
}
|
|
|
|
if (strpos($_SERVER['REMOTE_ADDR'], '172.18') === 0) {
|
|
self::returnJson([]);
|
|
return;
|
|
}
|
|
|
|
$calendars = CalendarModel::getAll();
|
|
|
|
// loop through all calendars and find the one with user_id as the worker's id
|
|
$calendarId = null;
|
|
foreach ($calendars as $calendar) {
|
|
if ($calendar->user_id == $worker->id) {
|
|
$calendarId = $calendar->go_calendar_id;
|
|
break;
|
|
}
|
|
}
|
|
|
|
$eventsJson = CalendarModel::getCalendarEvents($this->user, 0, 0, true);
|
|
|
|
if (isset($_GET['die_calendar'])) {
|
|
die($eventsJson);
|
|
}
|
|
|
|
$allEvents = json_decode($eventsJson, true)['data'] ?? [];
|
|
|
|
if (!is_array($allEvents)) {
|
|
self::returnJson([]);
|
|
return;
|
|
}
|
|
|
|
|
|
$endDate = new DateTime();
|
|
$startDate = (new DateTime())->modify('-3 days');
|
|
$startTimestamp = $startDate->setTime(0, 0, 0)->getTimestamp();
|
|
$endTimestamp = $endDate->setTime(23, 59, 59)->getTimestamp();
|
|
|
|
$filteredEvents = array_filter($allEvents, function ($event) use ($startTimestamp, $endTimestamp, $calendarId) {
|
|
if (!isset($event['cstart']) && !isset($event['category']) || (intval($event['calendar_id']['calendar_id']) != $calendarId)) {
|
|
return false;
|
|
}
|
|
$eventStartTimestamp = strtotime($event['cstart']['cstart'] ?? $event['cstart']);
|
|
if (empty($event['location']['location']) || empty($event['category']['category'])) {
|
|
return false;
|
|
}
|
|
if (date('H', $eventStartTimestamp) < 5 || date('H', $eventStartTimestamp) > 21) {
|
|
return false;
|
|
}
|
|
|
|
return $eventStartTimestamp >= $startTimestamp && $eventStartTimestamp <= $endTimestamp;
|
|
});
|
|
|
|
usort($filteredEvents, function ($a, $b) {
|
|
$timeA = strtotime($a['cstart']['cstart'] ?? $a['cstart']);
|
|
$timeB = strtotime($b['cstart']['cstart'] ?? $b['cstart']);
|
|
return $timeB <=> $timeA;
|
|
});
|
|
|
|
$limitedEvents = array_slice($filteredEvents, 0, 20);
|
|
|
|
$finalResponse = array_map(function ($event) {
|
|
$eventStart = $event['cstart']['cstart'] ?? $event['cstart'];
|
|
return [
|
|
'date' => date('Y-m-d H:i:s', strtotime($eventStart)),
|
|
'location' => $event['location']['location'] ?? '',
|
|
'category' => $event['category']['category'] ?? ''
|
|
];
|
|
}, $limitedEvents);
|
|
|
|
self::returnJson($finalResponse);
|
|
}
|
|
|
|
protected function warehouseLogoutAction() {
|
|
if (isset($_SESSION[MFAPPNAME . '_warehouse_login_override'])) {
|
|
unset($_SESSION[MFAPPNAME . '_warehouse_login_override']);
|
|
self::returnJson(['success' => true, 'message' => 'Logout erfolgreich']);
|
|
} else {
|
|
self::returnJson(['success' => false, 'message' => 'Kein aktiver Login gefunden']);
|
|
}
|
|
}
|
|
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);
|
|
}
|
|
|
|
protected function swAction() {
|
|
$javascript = "self.addEventListener('install', event => {
|
|
console.log('Patching PWA Service Worker: Installing...');
|
|
});
|
|
|
|
self.addEventListener('activate', event => {
|
|
console.log('Patching PWA Service Worker: Activating...');
|
|
});
|
|
|
|
self.addEventListener('fetch', event => {
|
|
event.respondWith(fetch(event.request));
|
|
});
|
|
|
|
console.log('Patching PWA Service Worker: Script loaded.');";
|
|
|
|
header("Content-Type: application/javascript");
|
|
header("Service-Worker-Allowed: /");
|
|
header("Cache-Control: no-cache");
|
|
header("Pragma: no-cache");
|
|
header("Expires: 0");
|
|
|
|
echo $javascript;
|
|
exit;
|
|
|
|
}
|
|
|
|
}
|