'status', 'text' => 'Status', 'modal' => false, 'table' => ['filter' => 'iconSelect', 'filterOptions' => [ ['value' => 'new', 'text' => 'Neu', 'icon' => 'fas fa-star text-primary'], ['value' => 'assigned', 'text' => 'Zugewiesen', 'icon' => 'fas fa-user-check text-info'], ['value' => 'scheduled', 'text' => 'Geplant', 'icon' => 'fas fa-calendar-check text-warning'], ['value' => 'in_progress', 'text' => 'In Bearbeitung', 'icon' => 'fas fa-cog text-warning'], ['value' => 'documented', 'text' => 'Dokumentiert', 'icon' => 'fas fa-file-alt text-success'], ['value' => 'completed', 'text' => 'Abgeschlossen', 'icon' => 'fas fa-check-double text-secondary'], ['value' => 'cancelled', 'text' => 'Abgebrochen', 'icon' => 'fas fa-ban text-danger'], ['value' => 'archived', 'text' => 'Archiviert', 'icon' => 'fas fa-archive text-muted'], ]] ]; protected array $additionalJS = ["js/pages/WorkorderMphBase/WorkorderMphBase.js"]; protected array $additionalHead = [""]; protected function getStatusText(string $statusKey): string { $statusMap = array_column($this->statusColumn['table']['filterOptions'] ?? [], 'text', 'value'); return $statusMap[$statusKey] ?? ucfirst(str_replace('_', ' ', $statusKey)); } //region SHARED ACTIONS /** * Fetches documentation and journal entries for a given workorder. */ protected function getDocumentationAction() { if (empty($this->request->workorderMphId)) self::sendError("Arbeitsauftrags-ID fehlt."); $docs = WorkorderMphDocumentationModel::getAll(['workorderMphId' => intval($this->request->workorderMphId)], null, 0, ['key' => 'create', 'order' => 'ASC']); $journals = WorkorderMphJournalModel::getAll(['workorderMphId' => intval($this->request->workorderMphId)], null, 0, ['key' => 'create', 'order' => 'DESC']); $responseDocs = []; $typeCounts = []; foreach ($docs as $doc) { $file = new File($doc->fileId); $documentTypeKey = $doc->documentType; $typeCounts[$documentTypeKey] = ($typeCounts[$documentTypeKey] ?? 0) + 1; $originalFilename = $file->orig_filename ?? $file->filename; $extension = pathinfo($originalFilename, PATHINFO_EXTENSION); $newFilename = "{$documentTypeKey}_{$typeCounts[$documentTypeKey]}." . strtolower($extension); $responseDocs[] = [ 'id' => $doc->id, 'fileId' => $doc->fileId, 'fileName' => $newFilename, 'description' => $doc->description, 'documentType' => $documentTypeKey, 'userName' => UserModel::getOne($doc->createBy)->name ?? 'Unbekannt', 'mimetype' => $file->mimetype ?? 'application/octet-stream', 'create' => $doc->create ]; } foreach ($journals as $journal) { $journal->createByName = UserModel::getOne($journal->createBy)->name ?? 'Unbekannt'; } self::returnJson(['docs' => $responseDocs, 'journals' => $journals]); } /** * Upload documentation for the Workorder itself (not Wohneinheit). */ protected function uploadDocumentationAction() { if (empty($_FILES['files']) && empty($_FILES['file'])) self::sendError('Erforderliche Daten fehlen.'); if (empty($_POST['workorderMphId'])) self::sendError('Workorder ID fehlt.'); $workorderMphId = intval($_POST['workorderMphId']); $uploadedCount = 0; // Handle multiple files (files[]) if (!empty($_FILES['files'])) { foreach ($_FILES['files']['name'] as $index => $name) { if ($_FILES['files']['error'][$index] === UPLOAD_ERR_OK) { // Mock the $_FILES entry for handleFormUpload $_FILES['single_upload_file'] = [ 'name' => $name, 'type' => $_FILES['files']['type'][$index], 'tmp_name' => $_FILES['files']['tmp_name'][$index], 'error' => $_FILES['files']['error'][$index], 'size' => $_FILES['files']['size'][$index] ]; try { $uploaded = mfUpload::handleFormUpload("single_upload_file", false, "/WorkorderMph"); WorkorderMphDocumentationModel::create([ 'workorderMphId' => $workorderMphId, 'fileId' => $uploaded->id, 'description' => $_POST['description'] ?? '', 'documentType' => $_POST['documentType'] ?? 'other', 'create' => time(), 'createBy' => $this->user->id ]); $uploadedCount++; } catch (Exception $e) { // Log error } } } } // Handle single file (file) - fallback or primary if JS sends single elseif (!empty($_FILES['file'])) { try { $uploaded = mfUpload::handleFormUpload("file", false, "/WorkorderMph"); WorkorderMphDocumentationModel::create([ 'workorderMphId' => $workorderMphId, 'fileId' => $uploaded->id, 'description' => $_POST['description'] ?? '', 'documentType' => $_POST['documentType'] ?? 'other', 'create' => time(), 'createBy' => $this->user->id ]); $uploadedCount++; } catch (Exception $e) { self::sendError("Upload fehlgeschlagen: " . $e->getMessage()); } } if ($uploadedCount > 0) { self::returnJson(['success' => true, 'message' => "$uploadedCount Datei(en) erfolgreich hochgeladen."]); } else { self::sendError("Keine Dateien wurden hochgeladen."); } } /** * Delete Workorder documentation */ protected function deleteDocumentationAction() { if (empty($this->postData['documentationId'])) self::sendError("Dokumentations-ID fehlt."); WorkorderMphDocumentationModel::delete($this->postData['documentationId']); self::returnJson(['success' => true, 'message' => 'Dokument gelöscht.']); } /** * Adds a new entry to a workorder's journal. */ protected function addJournalAction() { $post = json_decode(file_get_contents('php://input'), true); if (empty($post['workorderMphId']) || empty(trim($post['text']))) self::sendError("Arbeitsauftrags-ID und Text sind erforderlich."); WorkorderMphJournalModel::create([ 'workorderMphId' => $post['workorderMphId'], 'text' => $post['text'], 'createBy' => $this->user->id, 'create' => time() ]); $journals = WorkorderMphJournalModel::getAll(['workorderMphId' => intval($post['workorderMphId'])], null, 0, ['key' => 'create', 'order' => 'DESC']); foreach ($journals as $journal) { $journal->createByName = UserModel::getOne($journal->createBy)->name ?? 'Unbekannt'; } self::returnJson(['success' => true, 'message' => 'Journaleintrag hinzugefügt.', 'journals' => $journals]); } /** * Updates the additional info field for a workorder. */ protected function updateAdditionalInfoAction() { $post = json_decode(file_get_contents('php://input'), true); if (empty($post['workorderMphId'])) self::sendError("Arbeitsauftrags-ID fehlt."); $workorder = WorkorderMphModel::get($post['workorderMphId']); if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); $oldInfo = $workorder->additionalInfo; $newInfo = $post['additionalInfo'] ?? null; $workorder->additionalInfo = $newInfo; WorkorderMphModel::update((array)$workorder); WorkorderMphJournalModel::create([ 'workorderMphId' => $workorder->id, 'text' => "Zusatzinfo geändert.\nAlt: '{$oldInfo}'\nNeu: '{$newInfo}'", 'create' => time(), 'createBy' => $this->user->id, ]); self::returnJson(['success' => true, 'message' => 'Zusatzinfo aktualisiert.', 'newInfo' => $newInfo]); } /** * Get all Wohneinheiten for a specific workorder with their statuses and notes */ protected function getWohneinheitenAction() { if (empty($this->request->workorderMphId)) self::sendError("Arbeitsauftrags-ID fehlt."); $workorderMphId = intval($this->request->workorderMphId); $workorder = WorkorderMphModel::get($workorderMphId); if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); // Get all Wohneinheiten for this Hausnummer from addressdb $db = FronkDB::singleton(ADDRESSDB_DBHOST, ADDRESSDB_DBUSER, ADDRESSDB_DBPASS, ADDRESSDB_DBNAME); $hausnummerId = $db->escape($workorder->hausnummerId); // Fetch statuses from addressdb $statusSql = "SELECT id, code, name FROM Status WHERE type = 'wohneinheit' ORDER BY code ASC"; $statusResult = $db->query($statusSql); $statuses = $statusResult ? $statusResult->fetch_all(MYSQLI_ASSOC) : []; $statusOptions = array_map(function($s) { return ['value' => intval($s['id']), 'text' => $s['code'] . ' - ' . $s['name'], 'code' => intval($s['code'])]; }, $statuses); // Fetch Wohneinheiten directly $sql = "SELECT w.id, w.zusatz, w.tuer, w.contact, w.oaid, w.note, w.status_id, w.splice_hak_completed FROM Wohneinheit w WHERE w.hausnummer_id = $hausnummerId ORDER BY w.oaid ASC"; $result = $db->query($sql); $wohneinheiten = $result ? $result->fetch_all(MYSQLI_ASSOC) : []; // Get Preorders for this Hausnummer to fallback contact info $preorders = []; if (class_exists('PreorderModel')) { // Use searchActive to filter out canceled preorders (status_code = 20) $preorderList = PreorderModel::searchActive(['adb_hausnummer_id' => $workorder->hausnummerId]); foreach ($preorderList as $preorder) { if ($preorder->adb_wohneinheit_id) { $preorders[$preorder->adb_wohneinheit_id] = $preorder; } } } // Merge data $response = []; foreach ($wohneinheiten as $we) { // Contact info logic $contact = $we['contact']; $preorderContact = null; $preorderUcode = null; if (isset($preorders[$we['id']])) { $p = $preorders[$we['id']]; $preorderUcode = $p->ucode; $pContact = trim($p->firstname . ' ' . $p->lastname); if ($p->phone) $pContact .= ' (' . $p->phone . ')'; $preorderContact = $pContact; // If address contact is empty, use preorder contact if (empty($contact)) { $contact = $pContact; } } // Get document count for this Wohneinheit $docCountSql = "SELECT COUNT(*) as cnt FROM WohneinheitDocumentation WHERE wohneinheit_id = " . $db->escape($we['id']); $docCountResult = $db->query($docCountSql); $documentCount = 0; if ($docCountResult) { $docCountRow = $docCountResult->fetch_assoc(); $documentCount = intval($docCountRow['cnt']); } $response[] = [ 'wohneinheitId' => intval($we['id']), 'zusatz' => $we['zusatz'], 'tuer' => $we['tuer'], 'contact' => $contact, 'preorderContact' => $preorderContact, 'preorderUcode' => $preorderUcode, 'oaid' => $we['oaid'], 'status' => intval($we['status_id']), 'spliceCompleted' => intval($we['splice_hak_completed'] ?? 0), 'note' => $we['note'], 'documentCount' => $documentCount, ]; } self::returnJson([ 'wohneinheiten' => $response, 'statusOptions' => $statusOptions, 'hausnummerId' => $workorder->hausnummerId ]); } /** * Update status and note for a specific Wohneinheit */ protected function updateWohneinheitAction() { $post = json_decode(file_get_contents('php://input'), true); if (empty($post['workorderMphId']) || empty($post['wohneinheitId'])) { self::sendError("Arbeitsauftrags-ID und Wohneinheit-ID sind erforderlich."); } $workorderMphId = intval($post['workorderMphId']); $wohneinheitId = intval($post['wohneinheitId']); $newStatusId = intval($post['status'] ?? 1); $spliceCompleted = isset($post['spliceCompleted']) ? intval($post['spliceCompleted']) : 0; $tuer = $post['tuer'] ?? null; $zusatz = $post['zusatz'] ?? null; // Validate that "Tür" field is not empty if it's being set if ($tuer !== null && trim($tuer) === '') { self::sendError("Das Feld 'Tür' darf nicht leer sein."); } $db = FronkDB::singleton(ADDRESSDB_DBHOST, ADDRESSDB_DBUSER, ADDRESSDB_DBPASS, ADDRESSDB_DBNAME); $escapedWohneinheitId = $db->escape($wohneinheitId); // Fetch current state $currentSql = "SELECT status_id, tuer, zusatz, splice_hak_completed FROM Wohneinheit WHERE id = $escapedWohneinheitId"; $result = $db->query($currentSql); $current = $result ? $result->fetch_assoc() : null; if (!$current) self::sendError("Wohneinheit nicht gefunden."); $oldStatusId = intval($current['status_id']); $oldTuer = $current['tuer']; $oldZusatz = $current['zusatz']; $oldSplice = intval($current['splice_hak_completed'] ?? 0); // Update Wohneinheit $escapedTuer = $tuer !== null ? "'" . $db->escape($tuer) . "'" : "NULL"; $escapedZusatz = $zusatz !== null ? "'" . $db->escape($zusatz) . "'" : "NULL"; $escapedStatusId = $db->escape($newStatusId); $escapedSplice = $db->escape($spliceCompleted); $updateSql = "UPDATE Wohneinheit SET status_id = $escapedStatusId, tuer = $escapedTuer, zusatz = $escapedZusatz, splice_hak_completed = $escapedSplice WHERE id = $escapedWohneinheitId"; $db->query($updateSql); // Journaling $changes = []; if ($oldStatusId !== $newStatusId) { // Fetch status names for better logging $statusNamesSql = "SELECT id, code, name FROM Status WHERE id IN ($oldStatusId, $newStatusId)"; $statusRes = $db->query($statusNamesSql); $statusMap = []; if ($statusRes) { while($row = $statusRes->fetch_assoc()) { $statusMap[$row['id']] = $row['code'] . ' - ' . $row['name']; } } $oldText = $statusMap[$oldStatusId] ?? "ID $oldStatusId"; $newText = $statusMap[$newStatusId] ?? "ID $newStatusId"; $changes[] = "Status: $oldText → $newText"; } if ($oldSplice !== $spliceCompleted) { $changes[] = "Spleiß: " . ($spliceCompleted ? 'Erledigt' : 'Nicht erledigt'); } if ($oldTuer !== $tuer) { $changes[] = "Tür aktualisiert: '$oldTuer' -> '$tuer'"; } if ($oldZusatz !== $zusatz) { $changes[] = "Zusatz aktualisiert: '$oldZusatz' -> '$zusatz'"; } if (!empty($changes)) { WorkorderMphJournalModel::create([ 'workorderMphId' => $workorderMphId, 'text' => "Wohneinheit $wohneinheitId: " . implode(', ', $changes), 'create' => time(), 'createBy' => $this->user->id, ]); } // Status flag logic for BEP MD (241) and ONT (300). Need to check codes for these IDs. // Since we only have IDs, we need to check the code of the newStatusId. $newStatusCodeSql = "SELECT code FROM Status WHERE id = $escapedStatusId"; $resCode = $db->query($newStatusCodeSql); $newStatusCode = $resCode ? intval($resCode->fetch_assoc()['code']) : 0; if (in_array($newStatusCode, [241, 300])) { // 241=BEP MD, 300=ONT $this->setWohneinheitStatusflag($wohneinheitId, 200); } self::returnJson(['success' => true, 'message' => 'Wohneinheit aktualisiert.']); } /** * Set statusflag on Wohneinheit in addressdb */ private function setWohneinheitStatusflag(int $wohneinheitId, int $statusflagId) { $db = FronkDB::singleton(ADDRESSDB_DBHOST, ADDRESSDB_DBUSER, ADDRESSDB_DBPASS, ADDRESSDB_DBNAME); $weId = $db->escape($wohneinheitId); $sfId = $db->escape($statusflagId); // Check if statusflag already exists $checkSql = "SELECT COUNT(*) as count FROM WohneinheitStatusflagValue WHERE wohneinheit_id = $weId AND statusflag_id = $sfId"; $result = $db->query($checkSql); $exists = $result->fetch_assoc()['count'] > 0; if (!$exists) { $insertSql = "INSERT INTO WohneinheitStatusflagValue (wohneinheit_id, statusflag_id, create, createBy) VALUES ($weId, $sfId, " . time() . ", " . $this->user->id . ")"; $db->query($insertSql); } } /** * Get documents for a specific Wohneinheit */ protected function getWohneinheitDocumentsAction() { if (empty($this->request->wohneinheitId)) self::sendError("Wohneinheit-ID fehlt."); $wohneinheitId = intval($this->request->wohneinheitId); $db = FronkDB::singleton(ADDRESSDB_DBHOST, ADDRESSDB_DBUSER, ADDRESSDB_DBPASS, ADDRESSDB_DBNAME); $sql = "SELECT * FROM WohneinheitDocumentation WHERE wohneinheit_id = " . $db->escape($wohneinheitId) . " ORDER BY `create` ASC"; $result = $db->query($sql); $docs = $result ? $result->fetch_all(MYSQLI_ASSOC) : []; $responseDocs = []; foreach ($docs as $doc) { $file = new File($doc['fileId']); $responseDocs[] = [ 'id' => $doc['id'], 'fileId' => $doc['fileId'], 'fileName' => $file->orig_filename ?? $file->filename, 'description' => $doc['description'], 'documentType' => $doc['documentType'], 'userName' => UserModel::getOne($doc['createBy'])->name ?? 'Unbekannt', 'mimetype' => $file->mimetype ?? 'application/octet-stream', 'create' => $doc['create'] ]; } self::returnJson(['docs' => $responseDocs]); } /** * Upload document for a specific Wohneinheit */ protected function uploadWohneinheitDocumentAction() { if (empty($_FILES['file']) || empty($_POST['wohneinheitId'])) { self::sendError("Datei und Wohneinheit-ID sind erforderlich."); } $wohneinheitId = intval($_POST['wohneinheitId']); $documentType = $_POST['documentType'] ?? 'photo'; $description = $_POST['description'] ?? null; // Upload file using mfUpload handleFormUpload for proper handling try { $upload = mfUpload::handleFormUpload("file", false, "/WorkorderMph/Wohneinheit"); $file = $upload; // handleFormUpload returns the File object } catch (Exception $e) { self::sendError("Datei-Upload fehlgeschlagen: " . $e->getMessage()); return; } // Insert into WohneinheitDocumentation table in addressdb $db = FronkDB::singleton(ADDRESSDB_DBHOST, ADDRESSDB_DBUSER, ADDRESSDB_DBPASS, ADDRESSDB_DBNAME); $escapedWohneinheitId = $db->escape($wohneinheitId); $escapedFileId = $db->escape($file->id); $escapedDescription = $description ? "'" . $db->escape($description) . "'" : "NULL"; $escapedDocumentType = "'" . $db->escape($documentType) . "'"; $escapedCreateBy = $db->escape($this->user->id); $escapedCreate = time(); $sql = "INSERT INTO WohneinheitDocumentation (wohneinheit_id, fileId, description, documentType, `create`, createBy) VALUES ($escapedWohneinheitId, $escapedFileId, $escapedDescription, $escapedDocumentType, $escapedCreate, $escapedCreateBy)"; $db->query($sql); self::returnJson(['success' => true, 'message' => 'Dokument erfolgreich hochgeladen.', 'fileId' => $file->id]); } /** * Delete document for a specific Wohneinheit */ protected function deleteWohneinheitDocumentAction() { if (empty($this->postData['documentationId'])) self::sendError("Dokumentations-ID fehlt."); $documentationId = intval($this->postData['documentationId']); $db = FronkDB::singleton(ADDRESSDB_DBHOST, ADDRESSDB_DBUSER, ADDRESSDB_DBPASS, ADDRESSDB_DBNAME); $escapedId = $db->escape($documentationId); $sql = "DELETE FROM WohneinheitDocumentation WHERE id = $escapedId"; $db->query($sql); self::returnJson(['success' => true, 'message' => 'Dokument gelöscht.']); } /** * Update checkbox documentation fields */ protected function updateCheckboxesAction() { $post = json_decode(file_get_contents('php://input'), true); if (empty($post['workorderMphId'])) self::sendError("Arbeitsauftrags-ID fehlt."); $workorder = WorkorderMphModel::get($post['workorderMphId']); if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); $changes = []; $checkboxFields = [ 'easement' => 'Leitungsrecht', 'btb' => 'Bautechnische Begehung', 'fttxLocationSupplied' => 'FTTx Location mit Leerrohr versorgt', 'conduitToHuepLaid' => 'Leerrohr bis HÜP/HAK verlegt', 'huepMounted' => 'HÜP/HAK montiert', 'dropCableAvailable' => 'Dropkabel vorhanden', 'spliceCompleted' => 'Spleiß abgeschlossen' ]; $updateHausnummerStatus = false; foreach ($checkboxFields as $field => $fieldLabel) { if (array_key_exists($field, $post)) { $oldValue = $workorder->$field; $newValue = $post[$field] ? 1 : 0; if ($oldValue !== $newValue) { $workorder->$field = $newValue; $changes[] = "$fieldLabel: " . ($newValue ? 'ja' : 'nein'); // Check for FTTx Location mit Leerrohr versorgt if ($field === 'fttxLocationSupplied' && $newValue === 1) { $updateHausnummerStatus = true; } } } } if (!empty($changes)) { WorkorderMphModel::update((array)$workorder); if ($updateHausnummerStatus) { $db = FronkDB::singleton(ADDRESSDB_DBHOST, ADDRESSDB_DBUSER, ADDRESSDB_DBPASS, ADDRESSDB_DBNAME); // Find status ID for code 200 $statusSql = "SELECT id FROM Status WHERE code = 200 AND type = 'hausnummer' LIMIT 1"; $statusResult = $db->query($statusSql); if ($statusResult && $row = $statusResult->fetch_assoc()) { $statusId = $row['id']; $hnId = $db->escape($workorder->hausnummerId); $updateHnSql = "UPDATE Hausnummer SET status_id = $statusId WHERE id = $hnId"; $db->query($updateHnSql); } } WorkorderMphJournalModel::create([ 'workorderMphId' => $workorder->id, 'text' => "Dokumentation aktualisiert:\n" . implode("\n", $changes), 'create' => time(), 'createBy' => $this->user->id, ]); } self::returnJson(['success' => true, 'message' => 'Dokumentation aktualisiert.']); } //endregion }