'id', 'text' => 'LS-Nr.', 'required' => false, 'modal' => false, 'table' => ['class' => 'text-nowrap']], ['key' => 'billingAddressId', 'text' => 'Rechnungsadresse', 'required' => true, 'type' => 'autocomplete', 'table' => ['class' => 'text-nowrap', 'filter' => 'autocomplete'], 'modal' => ['apiUrl' => 'Address/api?do=findAddress', 'items' => '/Address/Api?do=findAddress', 'type' => 'autocomplete']], ['key' => 'deliveryAddressName', 'text' => 'L.-Adr. Name', 'required' => true], ['key' => 'deliveryAddressLine', 'text' => 'L.-Adr.', 'required' => true], ['key' => 'deliveryAddressPLZ', 'text' => 'L.-Adr. PLZ', 'required' => true], ['key' => 'deliveryAddressEMail', 'text' => 'L.-Adr. EMail', 'required' => true, 'table' => false], ['key' => 'note', 'text' => 'Art der Arbeit', 'required' => true, 'table' => false], ['key' => 'status', 'text' => 'Status', 'required' => true, 'table' => ['filter' => 'select'], 'modal' => ['type' => 'select', 'items' => [['value' => 'new', 'text' => 'Neu'], ['value' => 'accepted', 'text' => 'Akzeptiert'], ['value' => 'invoiced', 'text' => 'In Rechnung gestellt'],]]], ['key' => 'positions', 'text' => 'Positionen', 'required' => true, 'table' => false, 'modal' => false], ['key' => 'create', 'text' => 'Erstellt', 'required' => false, 'modal' => false, 'table' => ['filter' => 'date']], ['key' => 'createBy', 'text' => 'Erstellt von', 'required' => true, 'type' => 'autocomplete', 'table' => ['class' => 'text-nowrap', 'filter' => 'select'], 'modal' => ['items' => [], 'type' => 'select',]], ['key' => 'actions', 'text' => 'Aktionen', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false, 'class' => 'text-center']],]; protected array $additionalActions = [['key' => 'openHistory', 'title' => 'Historie', 'class' => 'fas fa-history text-primary'], ['key' => 'print', 'title' => 'Drucken', 'class' => 'fas fa-print text-primary'], ['key' => 'printWithPrice', 'title' => 'Drucken mit Preis', 'class' => 'fas fa-print text-success'], ]; protected array $infoMessages = ['create' => 'Lieferschein wurde erstellt.', 'update' => 'Lieferschein wurde aktualisiert', 'delete' => 'Lieferschein wurde gelöscht', 'noChanges' => 'Keine Änderungen vorgenommen']; protected function prepareCrudConfig() { $users = array_map(function ($user) { return ['value' => intval($user->id), 'text' => $user->name]; }, UserModel::search()); $this->columns[array_search('createBy', array_column($this->columns, 'key'))]['modal']['items'] = $users; } protected function beforeCreate($postData): bool { // if postdata status is not new we return an error if ($postData['status'] !== 'new') { http_response_code(500); self::returnJson(['success' => false, 'message' => 'Status muss "Neu" sein']); die(); } $postData['positions'] = json_encode($postData['positions']); return true; } protected function customAutoCompleteBillingAddressId($id) { $address = new Address($id); if ($address->id) { $result = ['id' => $address->id, 'title' => str_replace("'", "\\'", str_replace(["\n", "\r"], " ", $address->getCompanyOrName())) . " (" . $address->zip . " " . $address->city . ", " . $address->street . ")" . (($address->customer_number) ? " [" . $address->customer_number . "]" : "")]; return $result; } return null; } protected function beforeUpdate($postData): bool { $postData['positions'] = json_encode($postData['positions']); (new WarehouseHistoryController)->create($postData, $this->mod); return true; } protected function getHistoryAction() { $historyEntries = []; // remove all history elements where key is positions foreach ((new WarehouseHistoryController)->getHistory($this->request->id, $this->mod, $this->columns) as $entry) { if ($entry['key'] !== 'positions') { $historyEntries[] = $entry; } } // $historyEntries = array_filter($historyEntries, function ($entry) { // return $entry['key'] !== 'positions'; // }); self::returnJson($historyEntries); } protected function getArticleAddressPriceAction() { $articleId = $this->request->articleId; $addressId = $this->request->addressId; if (strlen($articleId) < 1) { http_response_code(500); self::returnJson(['success' => false, 'message' => 'Keine Artikel ID gefunden']); } if (strlen($addressId) < 1) { http_response_code(500); self::returnJson(['success' => false, 'message' => 'Keine Adress ID gefunden']); } //TODO: implement a select to select price category for each address // for now we default with price with name "Verkauf" $prices = WarehouseArticlePriceTypeModel::getAll(['title' => 'Verkauf']); // if array is empty we return an error if (empty($prices)) { http_response_code(500); self::returnJson(['success' => false, 'message' => 'Keine Preiskategorie gefunden']); } $priceType = $prices[0]->title; $article = WarehouseArticleModel::get($articleId); $sellPrices = json_decode($article->cheapestSellPrice, true); $sellPrice = array_search($priceType, array_column($sellPrices, 'title')); if (empty($sellPrice)) { http_response_code(500); self::returnJson(['success' => false, 'message' => 'Kein Preis gefunden']); } self::returnJson(['success' => true, 'price' => $sellPrices[$sellPrice]['price']]); } protected function getDeliveryAddressesAction() { $billingAddressId = $this->request->billingAddressId; if (strlen($billingAddressId) < 1) { http_response_code(500); self::returnJson(['success' => false, 'message' => 'Keine Rechnungsadresse gefunden']); } $deliveryAddresses = WarehouseShippingNoteModel::getAll(['billingAddressId' => $billingAddressId]); // TODO: maybe this should be improved as it is kinda hacky $result = []; foreach ($deliveryAddresses as $deliveryAddress) { $found = false; foreach ($result as $r) { if ($r->deliveryAddressName == $deliveryAddress->deliveryAddressName && $r->deliveryAddressLine == $deliveryAddress->deliveryAddressLine) { $found = true; break; } } if ($found) { continue; } $result[] = $deliveryAddress; } self::returnJson($result); } protected function getAllTextElementsAction() { $textElements = WarehouseShippingNoteTextElementModel::getAll(); self::returnJson($textElements); } protected function 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 ($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']); } try { $shippingNote['signatureDate'] = date("Y-m-d"); WarehouseShippingNoteModel::update($shippingNote); self::returnJson(['success' => true, 'message' => 'Unterschrift wurde gespeichert']); } catch (Exception $e) { http_response_code(500); self::returnJson(['success' => false, 'message' => 'Unterschrift konnte nicht gespeichert werden']); } } protected function createPDFAction() { $id = $this->request->id; if (strlen($id) < 1) { http_response_code(500); self::returnJson(['success' => false, 'message' => 'Lieferschein wurde nicht gefunden']); } $shippingNote = WarehouseShippingNoteModel::get($id); $address = AddressModel::getOne($shippingNote->billingAddressId); $positions = []; // loop through all positions and add articleTitle and articleDescription to each position entry foreach (json_decode($shippingNote->positions, true) as $position) { if (isset($position['article'])) { $article = WarehouseArticleModel::get($position['article']); $position['articleTitle'] = $article->title; $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; } } // json decode hoursEntries and add to positions $hoursEntries = json_decode($shippingNote->hoursEntries, true); foreach ($hoursEntries as $hoursEntry) { $positions[] = [ 'articleTitle' => "Arbeitsstunden", 'articleDescription' => "Mitarbeiter: " . UserModel::getOne($hoursEntry['userId'])->name, 'articleUnit' => 'Std.', 'amount' => $hoursEntry['hourCount'], 'price' => $hoursEntry['hourlyPrice'] * $hoursEntry['hourCount'] ?? 0, ]; if ($hoursEntry['carId']) { $positions[] = [ 'articleTitle' => "Fahrkostenpauschale", 'articleDescription' => "Fahrzeug: " . TimerecordingCarModel::getOne($hoursEntry['carId'])->number_plate, 'articleUnit' => 'Km', 'amount' => $hoursEntry['kilometerCount'], 'price' => 1 * $hoursEntry['kilometerCount'] ?? 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("{{ customerNumber }}", $address->customer_number, $headerHtml); $headerHtml = str_replace("{{ shippingNoteNumber }}", $shippingNoteNumber, $headerHtml); $headerHtml = str_replace("{{ shippingNoteDate }}", date("d.m.Y", $shippingNote->create), $headerHtml); $headerFile = BASEDIR . "/var/temp/shipping-note_header-" . date("U") . "-" . rand(1000, 9999) . ".html"; file_put_contents($headerFile, $headerHtml); // Replace placeholders in header $footerHtml = file_get_contents(BASEDIR . "/Layout/default/WarehouseShippingNote/PDF_FOOTER.html"); $footerHtml = str_replace("{{ bank_iban }}", TT_INVOICE_BANK_IBAN_FORMATTED, $footerHtml); $footerHtml = str_replace("{{ bank_bic }}", TT_INVOICE_BANK_BIC, $footerHtml); $footerHtml = str_replace("{{ bank_bank }}", TT_INVOICE_BANK_BANK, $footerHtml); $footerHtml = str_replace("{{ bank_owner }}", TT_INVOICE_BANK_OWNER, $footerHtml); $footerFile = BASEDIR . "/var/temp/shipping-note_header-" . date("U") . "-" . rand(1000, 9999) . ".html"; file_put_contents($footerFile, $footerHtml); $pdf = new PdfForm("WarehouseShippingNote/PDF_MAIN", $pdf_vars); $wkhtmltopdfArgs = "--header-html $headerFile --footer-html $footerFile"; $filename = $pdf->render($wkhtmltopdfArgs); // return the pdf and die so the client sees the pdf not the filename header('Content-Type: application/pdf'); header('Content-Disposition: inline; filename="' . $filename . '"'); readfile($filename); } // 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)); } //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(); $out = null; foreach ($timerecordingCars as $timerecordingCar) { if ($timerecordingCar->user_id == $this->user->id) { header('Content-Type: application/json'); die(json_encode(['success' => true, 'id' => $timerecordingCar->id])); } } die(json_encode(['success' => true, 'status' => 'USER_NO_CAR'])); } protected function geoAutocompleteAction() { $search = $this->request->q; $search = urlencode($search); $url = "https://nominatim.haid.in/search?q=$search&format=json"; $data = json_decode(file_get_contents($url), true); $out = []; foreach ($data as $entry) { $parsedDisplayNameParts = []; foreach(explode(',', $entry['display_name']) as $part) { // if str_includes Bezirk remove it if (strpos($part, 'Bezirk') !== false) { continue; } $parsedDisplayNameParts[] = $part; } $out[] = ['value' => $entry['lat'] . "," . $entry['lon'], 'text' => implode(',', $parsedDisplayNameParts)]; } self::returnJson($out); } protected function geoReverseAction() { $lat = $this->request->lat; $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); } //TODO: export this to an api class for openstreetmap protected function getDistanceAction() { // $filename = TEMP_DIR . "/DeviceMonitoring/interfacesWithCongestion.json"; // use dir TEMP_DIR /OpenStreetMap/from-to.json to cache the results $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]]; } $curl = curl_init(); curl_setopt_array($curl, [ CURLOPT_URL => "https://nominatim.haid.in/search?q=$address&format=json", CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_ENCODING => "", CURLOPT_MAXREDIRS => 10, CURLOPT_TIMEOUT => 30, CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, CURLOPT_CUSTOMREQUEST => "GET", CURLOPT_HTTPHEADER => [ "accept: application/json", "accept-language: de-AT,de;q=0.9,en;q=0.8", "origin: https://routing.openstreetmap.de", "referer: https://routing.openstreetmap.de/", "user-agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36" ], ]); $response = curl_exec($curl); $err = curl_error($curl); if ($err) { die(json_encode(['success' => false, 'message' => 'Error while geocoding'])); } curl_close($curl); return json_decode($response, true); } function route($from, $to) { $fromData = geocode($from); $toData = geocode($to); $fromLat = $fromData[0]['lat']; $fromLon = $fromData[0]['lon']; $toLat = $toData[0]['lat']; $toLon = $toData[0]['lon']; $url = "https://router.project-osrm.org/route/v1/driving/$fromLon,$fromLat;$toLon,$toLat?overview=false"; $data = json_decode(file_get_contents($url), true); $distance = $data['routes'][0]['distance']; return $distance; } $fromData = geocode($from); $toData = geocode($to); $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]); } }