'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]; protected array $additionalJS = ['js/pages/WarehouseArticle/WarehouseArticleModal.js']; protected array $additionalHead = ['']; 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; } 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'] .= " (ESTMK)"; } $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'])); } 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(is_array($data) ? $data : ['data' => $data]); } //TODO: export this to an api class for openstreetmap protected function getDistanceAction() { $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 * 2; } $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 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; } }