diff --git a/application/Radius/RadiusController.php b/application/Radius/RadiusController.php index 95a5f4f63..12b12071a 100644 --- a/application/Radius/RadiusController.php +++ b/application/Radius/RadiusController.php @@ -16,7 +16,16 @@ class RadiusController extends mfBaseController { protected function indexAction() { $this->layout()->set('additionalJS', ["plugins/chart.js/chart.4.4.6.js", "plugins/chart.js/chartjs-adapter-moment.min.js"]); - Helper::renderVue($this, $this->mod, "Radius", ['CAN_BILLING' => $this->me->can("Billing"), 'HIDE_PAGE_TITLE' => true]); + + $allowedAcsUserIds = [9, 13, 25, 65, 135, 145, 178]; + $acsEnabled = in_array($this->me->id, $allowedAcsUserIds); + + Helper::renderVue($this, $this->mod, "Radius", [ + 'CAN_BILLING' => $this->me->can("Billing"), + 'HIDE_PAGE_TITLE' => true, + 'USER_ID' => $this->me->id, + 'ACS_ENABLED' => $acsEnabled + ]); } protected function proxyUnsecureHTTPRequestToRadiusAction() { @@ -35,6 +44,49 @@ class RadiusController extends mfBaseController { die(); } + /** + * Run speedtest via ACS proxy + */ + protected function genieacsRunSpeedtestAction() { + try { + $input = json_decode(file_get_contents('php://input'), true); + $ip = $input['ip'] ?? null; + + if (!$ip) { + self::sendError("IP address is required"); + } + + $url = "http://acs.xinon.at:5000/run-speedtest"; + $apiKey = "2H9zWrgxPEJL9MZ1yTGtWh16cPCu0AsQ"; + + $data = json_encode(['ip' => $ip]); + + $opts = [ + "http" => [ + "method" => "POST", + "header" => "Content-Type: application/json\r\n" . + "X-API-Key: " . $apiKey . "\r\n" . + "Content-Length: " . strlen($data) . "\r\n", + "content" => $data + ] + ]; + + $context = stream_context_create($opts); + $response = file_get_contents($url, false, $context); + + if ($response === false) { + self::sendError("Failed to connect to speedtest server"); + } + + header("Content-Type: application/json"); + echo $response; + die(); + } catch (Exception $e) { + error_log("GenieACS runSpeedtest error: " . $e->getMessage()); + self::sendError("Error running speedtest: " . $e->getMessage()); + } + } + protected function sendCustomerEmailAction() { $input = json_decode(file_get_contents('php://input'), true); if (!$input || !isset($input['username'], $input['year'], $input['month'], $input['monthlySummary'], $input['monthlyDetails'], $input['recipient'])) diff --git a/application/WorkorderMphBase/WorkorderMphBaseController.php b/application/WorkorderMphBase/WorkorderMphBaseController.php index 00943af3a..b78a02b08 100644 --- a/application/WorkorderMphBase/WorkorderMphBaseController.php +++ b/application/WorkorderMphBase/WorkorderMphBaseController.php @@ -65,6 +65,81 @@ class WorkorderMphBaseController extends TTCrud 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. */ @@ -139,14 +214,15 @@ class WorkorderMphBaseController extends TTCrud $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.zusatz"; + 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')) { - $preorderList = PreorderModel::search(['adb_hausnummer_id' => $workorder->hausnummerId, 'deleted' => 0]); + // 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; @@ -161,21 +237,30 @@ class WorkorderMphBaseController extends TTCrud $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'], @@ -187,6 +272,7 @@ class WorkorderMphBaseController extends TTCrud 'status' => intval($we['status_id']), 'spliceCompleted' => intval($we['splice_hak_completed'] ?? 0), 'note' => $we['note'], + 'documentCount' => $documentCount, ]; } @@ -214,6 +300,11 @@ class WorkorderMphBaseController extends TTCrud $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); @@ -315,6 +406,93 @@ class WorkorderMphBaseController extends TTCrud } } + /** + * 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 */ @@ -327,17 +505,25 @@ class WorkorderMphBaseController extends TTCrud if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); $changes = []; - $checkboxFields = ['easement', 'btb', 'fttxLocationSupplied', 'conduitToHuepLaid', 'huepMounted', 'dropCableAvailable', 'spliceCompleted']; + $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) { + 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[] = "$field: " . ($newValue ? 'ja' : 'nein'); + $changes[] = "$fieldLabel: " . ($newValue ? 'ja' : 'nein'); // Check for FTTx Location mit Leerrohr versorgt if ($field === 'fttxLocationSupplied' && $newValue === 1) { @@ -374,4 +560,4 @@ class WorkorderMphBaseController extends TTCrud self::returnJson(['success' => true, 'message' => 'Dokumentation aktualisiert.']); } //endregion -} +} \ No newline at end of file diff --git a/db/migrations/20251203080600_add_wohneinheit_documentation.php b/db/migrations/20251203080600_add_wohneinheit_documentation.php new file mode 100644 index 000000000..cfe5c637d --- /dev/null +++ b/db/migrations/20251203080600_add_wohneinheit_documentation.php @@ -0,0 +1,38 @@ +getEnvironment() == "addressdb") { + $table = $this->table('WohneinheitDocumentation', ['id' => 'id', 'primary_key' => 'id']); + if (!$table->exists()) { + $table + ->addColumn('wohneinheit_id', 'integer', ['null' => false]) + ->addColumn('fileId', 'integer', ['null' => false]) + ->addColumn('description', 'text', ['null' => true]) + ->addColumn('documentType', 'string', ['limit' => 100, 'null' => false, 'default' => 'photo']) + ->addColumn('create', 'integer', ['null' => false]) + ->addColumn('createBy', 'integer', ['null' => false]) + ->addIndex(['wohneinheit_id'], ['name' => 'wohneinheit_id_idx']) + ->addIndex(['fileId'], ['name' => 'fileId_idx']) + ->create(); + } + } + } + + public function down(): void + { + // AddressDB modifications + if ($this->getEnvironment() == "addressdb") { + $table = $this->table('WohneinheitDocumentation'); + if ($table->exists()) { + $table->drop()->save(); + } + } + } +} diff --git a/public/js/pages/Radius/Radius.css b/public/js/pages/Radius/Radius.css index 93776214e..5fc4970fa 100644 --- a/public/js/pages/Radius/Radius.css +++ b/public/js/pages/Radius/Radius.css @@ -16,10 +16,12 @@ .radius-scope .inline-copy { display:flex; align-items:center; gap:8px; justify-content: flex-end; } .radius-scope .grid { display:grid; } .radius-scope .g-2 { gap: 8px; } +.radius-scope .g-3 { gap: 12px; } .radius-scope .g-4 { gap: 16px; } .radius-scope .g-6 { gap: 24px; } .radius-scope .cols-1 { grid-template-columns: 1fr; } .radius-scope .cols-2 { grid-template-columns: repeat(2, minmax(0,1fr)); } +.radius-scope .cols-3 { grid-template-columns: repeat(3, minmax(0,1fr)); } .radius-scope .cols-4 { grid-template-columns: repeat(4, minmax(0,1fr)); } @media (max-width: 900px){ .radius-scope .cols-4 { grid-template-columns: repeat(2, minmax(0,1fr)); } } @media (max-width: 600px){ .radius-scope .cols-4 { grid-template-columns: 1fr; } } diff --git a/public/js/pages/Radius/RadiusUsers.js b/public/js/pages/Radius/RadiusUsers.js index 0f995ff90..5058a1deb 100644 --- a/public/js/pages/Radius/RadiusUsers.js +++ b/public/js/pages/Radius/RadiusUsers.js @@ -57,7 +57,7 @@ Vue.component('radius-users', { Username Info Status - Aktionen + Aktionen @@ -105,8 +105,8 @@ Vue.component('radius-users', { class="fa-duotone fa-circle-info"> - - + @@ -76,18 +75,27 @@ Vue.component('workorder-mph-admin', { @@ -234,4 +242,4 @@ Vue.component('workorder-mph-admin', { } } } -}); +}); \ No newline at end of file diff --git a/public/js/pages/WorkorderMphBase/WorkorderMphBase.css b/public/js/pages/WorkorderMphBase/WorkorderMphBase.css index ea0d8fdc8..d6ab6ec99 100644 --- a/public/js/pages/WorkorderMphBase/WorkorderMphBase.css +++ b/public/js/pages/WorkorderMphBase/WorkorderMphBase.css @@ -34,10 +34,47 @@ /* * Wohneinheit Manager - Dense Table Layout */ +.wohneinheit-manager-container { + min-height: 500px; + max-height: 800px; + overflow-y: auto; + border-bottom: 1px solid #dee2e6; +} + +.wohneinheit-manager .we-splice .we-splice-checkbox .checkmark:after { + left: 5px; + top: 1px; +} + .wohneinheit-manager .we-table { display: table; width: 100%; border-collapse: collapse; + table-layout: fixed; +} + +.wohneinheit-manager .we-header { + display: table-header-group; +} + +.wohneinheit-manager .we-header-row { + display: table-row; + background-color: #f8f9fa; + position: sticky; + top: 0; + z-index: 10; + box-shadow: 0 1px 2px rgba(0,0,0,0.05); +} + +.wohneinheit-manager .we-header .we-cell { + display: table-cell; + padding: 8px 6px; + color: #495057; + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.3px; + border-bottom: 2px solid #dee2e6; + background-color: #f8f9fa; /* Needed for sticky */ } .wohneinheit-manager .we-row { @@ -50,60 +87,63 @@ background-color: #f8f9fa; } -.wohneinheit-manager .we-cell { - display: table-cell; - padding: 8px 12px; - vertical-align: middle; +.wohneinheit-manager .we-row.locked-row { + background-color: #e7f5ff; } -.wohneinheit-manager .we-bezeichner { - width: 20%; - font-size: 0.9rem; +.wohneinheit-manager .we-row.locked-row:hover { + background-color: #d0ebff; +} + +.wohneinheit-manager .we-cell { + display: table-cell; + padding: 6px 6px; + vertical-align: top; + font-size: 0.85rem; +} + +.wohneinheit-manager .we-zusatz { + width: 30%; +} + +.wohneinheit-manager .we-tuer { + width: 15%; } .wohneinheit-manager .we-status { - width: 18%; + width: 20%; } .wohneinheit-manager .we-splice { width: 12%; - padding: 4px 8px; -} - -.wohneinheit-manager .we-note { - width: 35%; -} - -.wohneinheit-manager .we-actions { - width: 15%; - text-align: right; -} - -.wohneinheit-manager .we-splice .custom-checkbox-item { - padding: 4px; - justify-content: center; - flex-direction: column; - height: auto; text-align: center; } -.wohneinheit-manager .we-splice .custom-checkbox-item .checkmark { - margin-right: 0; - margin-bottom: 2px; +.wohneinheit-manager .we-documents { + width: 12%; + text-align: center; +} + +.wohneinheit-manager .we-actions { + width: 8%; + text-align: center; +} + +/* Input Sizing for Dense View */ +.wohneinheit-manager .form-control-sm { + height: calc(1.4em + 0.5rem + 2px); + padding: 0.25rem 0.5rem; + font-size: 0.8rem; +} + +.wohneinheit-manager .we-splice .we-splice-checkbox .checkmark { width: 18px; height: 18px; } -.wohneinheit-manager .we-splice .custom-checkbox-item .checkbox-label { - font-size: 0.7rem; - line-height: 1; -} - -.contact-info { - font-size: 0.85rem; - margin-top: 4px; - padding-left: 4px; - border-left: 2px solid #007bff; +.wohneinheit-manager .badge { + font-size: 0.65rem; + padding: 2px 4px; } .workorder-mph-button { @@ -111,39 +151,15 @@ } /* - * Custom Checkboxes - Compact & Beautiful + * Custom Checkboxes - Compact */ -.custom-checkboxes-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 8px 16px; -} - .custom-checkbox-item { display: flex; align-items: center; - padding: 8px 12px; - background: #f8f9fa; - border: 1px solid #dee2e6; - border-radius: 6px; cursor: pointer; - transition: all 0.2s ease; - margin: 0; user-select: none; } -.custom-checkbox-item:hover:not(.disabled) { - background: #e9ecef; - border-color: #007bff; - transform: translateY(-1px); - box-shadow: 0 2px 4px rgba(0,0,0,0.1); -} - -.custom-checkbox-item.disabled { - opacity: 0.6; - cursor: not-allowed; -} - .custom-checkbox-item input[type="checkbox"] { position: absolute; opacity: 0; @@ -157,8 +173,7 @@ background-color: #fff; border: 2px solid #adb5bd; border-radius: 4px; - margin-right: 10px; - flex-shrink: 0; + margin-right: 0; transition: all 0.2s ease; } @@ -184,50 +199,255 @@ display: block; } -.custom-checkbox-item .checkbox-label { - font-size: 0.9rem; - font-weight: 500; - color: #495057; -} - -.custom-checkbox-item input[type="checkbox"]:checked ~ .checkbox-label { - color: #28a745; -} - /* - * Required Documents Checklist + * Expanded View Layout */ -.required-docs-checklist { - background: #f8f9fa; - border-radius: 6px; - padding: 8px; +.workorder-mph-expanded-wrapper { + background: #f1f3f5; + padding: 16px; + border-radius: 8px; + margin: -8px; } -.doc-check-item { - display: flex; - align-items: center; - padding: 6px 10px; - margin-bottom: 4px; +.workorder-mph-expanded-wrapper .row.g-2 { + margin-right: -0.5rem; + margin-left: -0.5rem; +} + +.workorder-mph-expanded-wrapper .row.g-2 > .col, +.workorder-mph-expanded-wrapper .row.g-2 > [class*="col-"] { + padding-right: 0.5rem; + padding-left: 0.5rem; +} + +/* Card Styling */ +.workorder-mph-card { background: white; - border-radius: 4px; - font-size: 0.9rem; - gap: 10px; + border: 1px solid #dee2e6; + border-radius: 6px; + box-shadow: 0 1px 2px rgba(0,0,0,0.05); + margin-bottom: 16px; + display: flex; + flex-direction: column; } -.doc-check-item:last-child { +.workorder-mph-card:last-child { margin-bottom: 0; } -.doc-check-item i:first-child { - width: 20px; - text-align: center; +.workorder-mph-card-header { + padding: 8px 12px; + background: linear-gradient(to bottom, #ffffff, #f8f9fa); + border-bottom: 1px solid #e9ecef; + font-weight: 600; + font-size: 0.85rem; + color: #495057; + display: flex; + align-items: center; + justify-content: flex-start; /* Changed from space-between */ } -.doc-check-item span { - flex: 1; - font-weight: 500; -} - -.doc-check-item .ml-auto { +.workorder-mph-card-header > div:last-child, +.workorder-mph-card-header > span:last-child:not(:first-child) { margin-left: auto; } + +/* Drag and Drop Zone */ +.mph-drop-zone { + border: 2px dashed #ced4da; + border-radius: 6px; + padding: 16px; + text-align: center; + background: #f8f9fa; + transition: all 0.2s ease; + cursor: pointer; + position: relative; + min-height: 100px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.mph-drop-zone:hover, +.mph-drop-zone.dragging { + background: #e9ecef; + border-color: #007bff; +} + +.mph-drop-zone input[type="file"] { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0; + cursor: pointer; + z-index: 10; +} + +.mph-drop-zone-icon { + font-size: 1.8rem; + color: #adb5bd; + margin-bottom: 8px; + transition: color 0.2s; +} + +.mph-drop-zone:hover .mph-drop-zone-icon { + color: #007bff; +} + +.mph-drop-zone-text { + color: #495057; + font-weight: 500; + font-size: 0.85rem; +} + +.mph-drop-zone-hint { + color: #6c757d; + font-size: 0.75rem; +} + +.mph-file-list { + margin-top: 10px; + text-align: left; + width: 100%; +} + +.mph-file-item { + display: flex; + align-items: center; + padding: 6px 10px; + background: white; + border: 1px solid #dee2e6; + border-radius: 4px; + margin-bottom: 4px; + font-size: 0.85rem; +} + +.mph-file-item:last-child { + margin-bottom: 0; +} + +.mph-file-item .remove-file { + margin-left: auto; + color: #dc3545; + cursor: pointer; + padding: 2px 6px; +} + +.mph-file-item .remove-file:hover { + background: #ffe3e3; + border-radius: 4px; +} + +.workorder-mph-card-header i { + margin-right: 8px; + color: #6c757d; +} + +.workorder-mph-card-body { + padding: 12px; + flex: 1; +} + +.workorder-mph-card-body-compact { + padding: 8px; +} + +/* Documentation Checkboxes - Vertical List for Side Panel */ +.mph-checkbox-vertical { + display: flex; + flex-direction: column; + gap: 4px; +} + +.mph-checkbox-item { + display: flex; + align-items: center; + padding: 6px 8px; + background: #f8f9fa; + border: 1px solid #e9ecef; + border-radius: 4px; + cursor: pointer; + transition: all 0.15s ease; +} + +.mph-checkbox-item:hover:not(.disabled) { + background: #e9ecef; + border-color: #ced4da; +} + +.mph-checkbox-item .checkmark { + width: 16px; + height: 16px; + border-radius: 3px; + margin-right: 8px; +} + +.mph-checkbox-item .checkmark:after { + left: 4px; + top: 0px; + width: 4px; + height: 8px; +} + +.mph-checkbox-item .label { + font-size: 0.8rem; + line-height: 1.2; +} + +/* Details Stack */ +.mph-details-stack { + display: flex; + flex-direction: column; + gap: 12px; +} + +/* Admin Review Checklist */ +.mph-doc-checklist { + display: flex; + flex-direction: column; + gap: 4px; +} + +.mph-doc-check-item { + display: flex; + align-items: center; + padding: 4px 8px; + background: #f8f9fa; + border-radius: 4px; + font-size: 0.8rem; + border: 1px solid transparent; +} + +.mph-doc-check-item.has-doc { + background-color: #f0fff4; + border-color: #c3e6cb; +} + +/* Journal List */ +.mph-journal-list { + max-height: 200px; + overflow-y: auto; + margin: -12px; +} + +.mph-journal-item { + padding: 8px 12px; + border-bottom: 1px solid #f1f3f5; + font-size: 0.8rem; +} + +.mph-journal-item-header { + font-size: 0.75rem; + color: #868e96; + margin-bottom: 2px; +} + +/* File Gallery Adjustments */ +.mph-docs-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + gap: 8px; +} \ No newline at end of file diff --git a/public/js/pages/WorkorderMphBase/WorkorderMphBase.js b/public/js/pages/WorkorderMphBase/WorkorderMphBase.js index 162b97403..b5855b620 100644 --- a/public/js/pages/WorkorderMphBase/WorkorderMphBase.js +++ b/public/js/pages/WorkorderMphBase/WorkorderMphBase.js @@ -1,6 +1,6 @@ // WorkorderMphBase.js - Shared components for WorkorderMph module -// Traffic light component (reused from WorkorderBase) +// Traffic light component Vue.component('traffic-light-mph', { props: ['deadline', 'status'], computed: { @@ -19,6 +19,49 @@ Vue.component('traffic-light-mph', { template: `` }); +// Data Provider Component +Vue.component('workorder-mph-data-provider', { + props: { + workorderMphId: { type: [Number, String], required: true } + }, + data: () => ({ + loading: true, + docs: [], + journals: [] + }), + methods: { + async fetchData() { + this.loading = true; + try { + // Try to detect context (Admin or Company) based on URL or guess + const isCompany = window.location.pathname.includes('Company'); + const basePath = isCompany ? '/WorkorderMphCompany' : '/WorkorderMphAdmin'; + + const { data } = await axios.get(`${window.TT_CONFIG.BASE_PATH}${basePath}/getDocumentation`, { + params: { workorderMphId: this.workorderMphId } + }); + this.docs = data.docs || []; + this.journals = data.journals || []; + } catch (e) { + console.error("Failed to fetch data", e); + } finally { + this.loading = false; + } + } + }, + mounted() { + this.fetchData(); + }, + render() { + return this.$scopedSlots.default({ + loading: this.loading, + docs: this.docs, + journals: this.journals, + refresh: this.fetchData + }); + } +}); + // Wohneinheit Status Manager Component Vue.component('wohneinheit-status-manager', { props: { @@ -26,73 +69,128 @@ Vue.component('wohneinheit-status-manager', { isAdmin: { type: Boolean, default: false } }, template: ` -
-
-
Wohneinheiten Status
- - AddressDB #{{ hausnummerId }} - -
-
-
-
- Keine Wohneinheiten gefunden. +
+
+ Wohneinheiten + -
-
-
-
- Zusatz - -
-
- OAID: {{ we.oaid }} -
-
- {{ we.contact }} -
-
- {{ we.preorderContact }} -
-
- Keine Kontaktinfo +
+ +
+
+ Keine Wohneinheiten gefunden. +
+
+ +
+
+
+
+
+
Zusatz / Kontakt
+
Tür
+
Status
+
Spleiß
+
Docs
+
+
- -
- -
-
- Tür - +
+
+
+ +
+ +
+ OAID: {{ we.oaid }} +
+
+ {{ we.contact }} +
+
+ {{ we.preorderContact }} +
+
+ +
+ +
+ +
+
+ {{ getStatusText(we.status) }} +
+ +
+ +
+ +
+ +
+
-
-
- Status -
- {{ getStatusText(we.status) }} +
+ +
- -
Speichert...
-
- -
-
-
+
+ + + +
+
+
+
+
+ +
+ + +
Dateien auswählen
+
+
+
+ {{ file.name }} + +
+
+
+
+
+ + +
+ +
+
+
+ +
+
`, data: () => ({ @@ -100,149 +198,149 @@ Vue.component('wohneinheit-status-manager', { wohneinheiten: [], statusOptions: [], hausnummerId: null, - debounceTimers: {} + debounceTimers: {}, + documentsModal: { + show: false, + loading: false, + uploading: false, + isDragging: false, + wohneinheitId: null, + zusatz: '', + docs: [], + files: [], + uploadDescription: '' + } }), computed: { - filteredStatusOptions() { - // Filter out status with code 300 - return this.statusOptions.filter(opt => opt.code !== 300); - } + filteredStatusOptions() { return this.statusOptions.filter(opt => opt.code !== 300); } }, methods: { async fetchWohneinheiten() { this.loading = true; try { const basePath = this.isAdmin ? '/WorkorderMphAdmin' : '/WorkorderMphCompany'; - const { data } = await axios.get(`${window.TT_CONFIG.BASE_PATH}${basePath}/getWohneinheiten`, { - params: { workorderMphId: this.workorderMphId } - }); - this.wohneinheiten = data.wohneinheiten.map(we => ({ - ...we, - spliceCompleted: !!we.spliceCompleted, - saving: false - })); + const { data } = await axios.get(`${window.TT_CONFIG.BASE_PATH}${basePath}/getWohneinheiten`, { params: { workorderMphId: this.workorderMphId } }); + this.wohneinheiten = data.wohneinheiten.map(we => ({ ...we, spliceCompleted: !!we.spliceCompleted, saving: false })); this.statusOptions = data.statusOptions || []; this.hausnummerId = data.hausnummerId; - } catch (e) { - window.notify('error', 'Wohneinheiten konnten nicht geladen werden.'); - console.error(e); - } finally { - this.loading = false; - } - }, - isLocked(we) { - const status = this.statusOptions.find(s => s.value === we.status); - return status && status.code === 300; + } catch (e) { console.error(e); } finally { this.loading = false; } }, + isLocked(we) { const status = this.statusOptions.find(s => s.value === we.status); return status && status.code === 300; }, debouncedSave(we) { we.saving = true; - if (this.debounceTimers[we.wohneinheitId]) { - clearTimeout(this.debounceTimers[we.wohneinheitId]); - } - this.debounceTimers[we.wohneinheitId] = setTimeout(() => { - this.saveWohneinheit(we); - }, 1000); + if (this.debounceTimers[we.wohneinheitId]) clearTimeout(this.debounceTimers[we.wohneinheitId]); + this.debounceTimers[we.wohneinheitId] = setTimeout(() => this.saveWohneinheit(we), 1000); }, async saveWohneinheit(we) { - we.saving = true; try { const basePath = this.isAdmin ? '/WorkorderMphAdmin' : '/WorkorderMphCompany'; - const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}${basePath}/updateWohneinheit`, { - workorderMphId: this.workorderMphId, - wohneinheitId: we.wohneinheitId, - status: we.status, - spliceCompleted: we.spliceCompleted ? 1 : 0, - tuer: we.tuer, - zusatz: we.zusatz + await axios.post(`${window.TT_CONFIG.BASE_PATH}${basePath}/updateWohneinheit`, { + workorderMphId: this.workorderMphId, wohneinheitId: we.wohneinheitId, status: we.status, spliceCompleted: we.spliceCompleted ? 1 : 0, tuer: we.tuer, zusatz: we.zusatz }); - if (data.success) { - // Silent success or small indicator - this.$emit('wohneinheit-updated'); - } else { - window.notify('error', data.message || 'Speichern fehlgeschlagen.'); - } - } catch (e) { - window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); - } finally { - we.saving = false; - } + this.$emit('wohneinheit-updated'); + } catch (e) { window.notify('error', 'Fehler beim Speichern'); } finally { we.saving = false; } }, - getStatusText(statusValue) { - const option = this.statusOptions.find(opt => opt.value === statusValue); - return option ? option.text : ''; + getStatusText(val) { const o = this.statusOptions.find(opt => opt.value === val); return o ? o.text : ''; }, + async openDocumentsModal(we) { + this.documentsModal.show = true; + this.documentsModal.wohneinheitId = we.wohneinheitId; + this.documentsModal.zusatz = we.zusatz || we.oaid; + this.documentsModal.loading = true; + this.documentsModal.files = []; + this.documentsModal.uploadDescription = ''; + + try { + const basePath = this.isAdmin ? '/WorkorderMphAdmin' : '/WorkorderMphCompany'; + const { data } = await axios.get(`${window.TT_CONFIG.BASE_PATH}${basePath}/getWohneinheitDocuments`, { params: { wohneinheitId: we.wohneinheitId } }); + this.documentsModal.docs = data.docs || []; we.documentCount = this.documentsModal.docs.length; + } catch (e) { console.error(e); } finally { this.documentsModal.loading = false; } + }, + handleFileSelect(event) { + this.addFiles(event.target.files); + }, + handleDrop(event) { + this.documentsModal.isDragging = false; + this.addFiles(event.dataTransfer.files); + }, + addFiles(fileList) { + if (!fileList.length) return; + this.documentsModal.files = [...this.documentsModal.files, ...Array.from(fileList)]; + }, + removeFile(index) { + this.documentsModal.files.splice(index, 1); + }, + async uploadWohneinheitDocument() { + if (!this.documentsModal.files.length) return; + this.documentsModal.uploading = true; + + const basePath = this.isAdmin ? '/WorkorderMphAdmin' : '/WorkorderMphCompany'; + let successCount = 0; + + for (const file of this.documentsModal.files) { + const formData = new FormData(); + formData.append('wohneinheitId', this.documentsModal.wohneinheitId); + formData.append('description', this.documentsModal.uploadDescription); + formData.append('file', file); + + try { + const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}${basePath}/uploadWohneinheitDocument`, formData); + if (data.success) successCount++; + } catch (e) { console.error(e); } + } + + if (successCount > 0) { + window.notify('success', `${successCount} Datei(en) hochgeladen`); + this.documentsModal.files = []; + this.documentsModal.uploadDescription = ''; + this.$refs.weFileInput.value = ''; + const { data } = await axios.get(`${window.TT_CONFIG.BASE_PATH}${basePath}/getWohneinheitDocuments`, { params: { wohneinheitId: this.documentsModal.wohneinheitId } }); + this.documentsModal.docs = data.docs || []; + } else { + window.notify('error', 'Upload fehlgeschlagen'); + } + + this.documentsModal.uploading = false; + }, + async deleteWohneinheitDocument(file) { + try { + const basePath = this.isAdmin ? '/WorkorderMphAdmin' : '/WorkorderMphCompany'; + await axios.post(`${window.TT_CONFIG.BASE_PATH}${basePath}/deleteWohneinheitDocument`, { documentationId: file.id }); + window.notify('success', 'Gelöscht'); + const { data } = await axios.get(`${window.TT_CONFIG.BASE_PATH}${basePath}/getWohneinheitDocuments`, { params: { wohneinheitId: this.documentsModal.wohneinheitId } }); + this.documentsModal.docs = data.docs || []; + } catch (e) { window.notify('error', 'Fehler beim Löschen'); } } }, - async mounted() { - await this.fetchWohneinheiten(); - } + mounted() { this.fetchWohneinheiten(); } }); // Checkbox Documentation Component Vue.component('checkbox-documentation', { - props: { - workorderMphId: { type: Number, required: true }, - isAdmin: { type: Boolean, default: false } - }, + props: { workorderMphId: { type: Number, required: true }, isAdmin: { type: Boolean, default: false } }, template: ` -
-
-
Dokumentation Checkboxen
-
-
-
- - - - - - - - - - - -
-
- Speichert... -
+
+
+ Dokumentation + +
+
+
+
+
`, data: () => ({ - loading: true, - saving: false, - checkboxes: { - easement: false, - btb: false, - fttxLocationSupplied: false, - conduitToHuepLaid: false, - huepMounted: false, - dropCableAvailable: false + loading: true, saving: false, + checkboxes: { easement: false, btb: false, fttxLocationSupplied: false, conduitToHuepLaid: false, huepMounted: false, dropCableAvailable: false }, + labels: { + easement: 'Leitungsrecht', btb: 'Bautechnische Begehung', fttxLocationSupplied: 'FTTx Location versorgt', + conduitToHuepLaid: 'Leerrohr bis HAK', huepMounted: 'HAK montiert', dropCableAvailable: 'Dropkabel vorhanden' } }), methods: { @@ -250,337 +348,272 @@ Vue.component('checkbox-documentation', { this.loading = true; try { const basePath = this.isAdmin ? '/WorkorderMphAdmin' : '/WorkorderMphCompany'; - const { data } = await axios.get(`${window.TT_CONFIG.BASE_PATH}${basePath}/getWorkorderById`, { - params: { id: this.workorderMphId } - }); - this.checkboxes = { - easement: !!data.easement, - btb: !!data.btb, - fttxLocationSupplied: !!data.fttxLocationSupplied, - conduitToHuepLaid: !!data.conduitToHuepLaid, - huepMounted: !!data.huepMounted, - dropCableAvailable: !!data.dropCableAvailable - }; - } catch (e) { - window.notify('error', 'Checkboxen konnten nicht geladen werden.'); - console.error(e); - } finally { - this.loading = false; - } + const { data } = await axios.get(`${window.TT_CONFIG.BASE_PATH}${basePath}/getWorkorderById`, { params: { id: this.workorderMphId } }); + for (let key in this.checkboxes) this.checkboxes[key] = !!data[key]; + } catch (e) { console.error(e); } finally { this.loading = false; } }, async saveCheckboxes() { this.saving = true; try { const basePath = this.isAdmin ? '/WorkorderMphAdmin' : '/WorkorderMphCompany'; - const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}${basePath}/updateCheckboxes`, { - workorderMphId: this.workorderMphId, - ...this.checkboxes - }); - if (data.success) { - // Silent success - this.$emit('checkboxes-updated'); - } else { - window.notify('error', data.message || 'Speichern fehlgeschlagen.'); - } - } catch (e) { - window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); - } finally { - this.saving = false; - } + await axios.post(`${window.TT_CONFIG.BASE_PATH}${basePath}/updateCheckboxes`, { workorderMphId: this.workorderMphId, ...this.checkboxes }); + } catch (e) { window.notify('error', 'Speichern fehlgeschlagen'); } finally { this.saving = false; } } }, - async mounted() { - await this.fetchCheckboxes(); + mounted() { this.fetchCheckboxes(); } +}); + +// Journal Component +Vue.component('workorder-mph-journal', { + props: { journals: { type: Array, default: () => [] }, workorderMphId: { type: [Number, String], required: true }, isAdmin: { type: Boolean, default: false } }, + data: () => ({ newMessage: '', adding: false }), + computed: { + canSend() { return this.newMessage.trim().length >= 3; }, + sendButtonClass() { return this.canSend ? 'btn-primary' : 'btn-secondary'; } + }, + template: ` +
+
Journal
+
+
+
+
{{ formatDate(log.create) }} - {{ log.createByName }}
+
{{ log.text }}
+
{{ log.statusChange }}
+
+
+
Keine Einträge
+
+
+ + +
+
+ `, + methods: { + formatDate(ts) { return window.moment.unix(ts).format('DD.MM HH:mm'); }, + async addEntry() { + if (!this.canSend) return; + this.adding = true; + try { + const basePath = this.isAdmin ? '/WorkorderMphAdmin' : '/WorkorderMphCompany'; + const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}${basePath}/addJournal`, { workorderMphId: this.workorderMphId, text: this.newMessage }); + if (data.success) { this.newMessage = ''; this.$emit('refresh'); } + } catch (e) { window.notify('error', 'Fehler'); } finally { this.adding = false; } + } } }); -// WorkorderMph Details Manager -Vue.component('workorder-mph-details-manager', { - props: { - workorderMphId: { type: String, required: true }, - isAdmin: { type: Boolean, default: false } - }, +// Documents Component (Upload + List) +Vue.component('workorder-mph-documents', { + props: { docs: { type: Array, default: () => [] }, workorderMphId: { type: [Number, String], required: true }, isAdmin: { type: Boolean, default: false } }, data: () => ({ - loading: true, - docs: [], - journals: [], - newJournalMessage: '', - addingJournalEntry: false, uploading: false, - completing: false, - showCompleteModal: false, - showAcceptModal: false, + isDragging: false, uploadData: { files: [], documentType: '', description: '' }, - wohneinheitenWithNotes: true, requiredDocs: [ - { key: 'huep_photo', label: 'HÜP/HAK Foto', icon: 'fas fa-camera', example: 'Foto der installierten HÜP/HAK' }, - { key: 'bep_md_photo', label: 'BEP MD Foto', icon: 'fas fa-camera', example: 'Foto der BEP (MD) Installation' }, - { key: 'ont_photo', label: 'ONT Foto', icon: 'fas fa-camera', example: 'Foto der ONT Installation' }, - { key: 'cable_routing', label: 'Kabelverlegung', icon: 'fas fa-route', example: 'Fotos der Kabelverlegung im Treppenhaus' }, - { key: 'fttx_location', label: 'FTTx Location', icon: 'fas fa-map-marker-alt', example: 'Foto/Dokument der FTTx Location' }, - { key: 'signature', label: 'Unterschrift', icon: 'fas fa-signature', example: 'Unterschriebenes Übergabeprotokoll' }, - { key: 'other', label: 'Sonstige Dokumentation', icon: 'fas fa-file', example: 'Weitere relevante Dokumente' } + { key: 'photo_hak_mounted', label: 'Foto vom montierten HAK' }, + { key: 'photo_hak_open', label: 'Foto von dem offenen HAK' }, + { key: 'photo_splice_cassette_hak', label: 'Foto der Spleißkasette - HAK' }, + { key: 'photo_splice_cassette_fcp', label: 'Foto der Spleißkasette - FCP' }, + { key: 'photo_fcp_labeled', label: 'Foto vom FCP beschriftet' }, + { key: 'photo_patch_pos_osp', label: 'Foto der Patch-Position - OSP-Seite' }, + { key: 'photo_patch_pos_anb', label: 'Foto der Patch-Position - ANB-Seite' }, + { key: 'otdr_measurement', label: 'ODTR - Messung (1310nm & 1550nm)' }, + { key: 'other', label: 'Sonstige' } ] }), - template: ` -
-
-
-
-
-
-
Auftrag abschließen
-

Dokumentieren Sie alle Wohneinheiten und laden Sie die erforderlichen Dokumente hoch.

-
- - - Bitte fügen Sie für jede Wohneinheit eine Notiz hinzu und laden Sie Dokumente hoch. - -
- Auftrag bereits abgeschlossen oder storniert. -
-
-
- -
-
-
Prüfung & Freigabe
-

Prüfen Sie die hochgeladenen Dokumente:

- -
-
- - {{ doc.label }} - - -
-
- - - Stellen Sie sicher, dass alle relevanten Dokumente vorhanden sind. - - - -
-
- -
-
Journal
-
-
    -
  • - {{ formatDate(log.create) }} ({{ log.createByName }}): -
    {{ log.text }}
    -
  • -
-
Keine Journaleinträge.
-
- -
-
- -
-
-
-
Neues Dokument hochladen
- - -
-
- - {{ getDocExample(uploadData.documentType) }} - -
-
- - - -
- -
- - Erlaubt: Bilder (JPG, PNG) und PDF -
-
- -
- -
-
-
- - - -
-
- - - Möchten Sie diesen Auftrag wirklich abschließen und zur Prüfung einreichen? - - - Soll die Dokumentation für diesen Arbeitsauftrag wirklich akzeptiert und der Auftrag abgeschlossen werden? - -
- `, computed: { - isReadOnly() { - return ['completed', 'cancelled'].includes(this.workorder?.status); - }, - canComplete() { - return this.wohneinheitenWithNotes && this.docs.length > 0; - }, - docTypeOptions() { + docOptions() { return [ { value: '', text: '-- Bitte wählen --' }, - ...this.requiredDocs.map(doc => ({ value: doc.key, text: doc.label })) + ...this.requiredDocs.map(d => ({ value: d.key, text: d.label })) ]; } }, + template: ` +
+
+
Dokument hochladen
+
+
+
+ + + +
+
+ +
+ + +
Dateien auswählen
+
oder hierher ziehen
+
+ +
+
+ + {{ file.name }} + +
+
+
+
+
+ +
+
+
+ +
+
+ Dokumente + {{ docs.length }} +
+
+
Keine Dokumente
+ +
+
+
+ `, methods: { - formatDate(timestamp) { - return timestamp ? window.moment.unix(timestamp).format('DD.MM.YYYY HH:mm') : '–'; + handleFileSelect(event) { + this.addFiles(event.target.files); }, - hasDocType(docType) { - return this.docs.some(doc => doc.documentType === docType); + handleDrop(event) { + this.isDragging = false; + this.addFiles(event.dataTransfer.files); }, - getDocExample(docType) { - const doc = this.requiredDocs.find(d => d.key === docType); - return doc ? doc.example : ''; + addFiles(fileList) { + if (!fileList.length) return; + // Convert FileList to Array and append + this.uploadData.files = [...this.uploadData.files, ...Array.from(fileList)]; }, - async fetchData() { - this.loading = true; - try { - const basePath = this.isAdmin ? '/WorkorderMphAdmin' : '/WorkorderMphCompany'; - const { data } = await axios.get(`${window.TT_CONFIG.BASE_PATH}${basePath}/getDocumentation`, { - params: { workorderMphId: this.workorderMphId } - }); - this.docs = data.docs || []; - this.journals = data.journals || []; - } catch (e) { - window.notify('error', 'Details konnten nicht geladen werden.'); - this.docs = []; - this.journals = []; - } finally { - this.loading = false; - } - }, - async addJournalEntry() { - if (!this.newJournalMessage.trim()) return window.notify('error', 'Bitte eine Nachricht eingeben.'); - - this.addingJournalEntry = true; - try { - const basePath = this.isAdmin ? '/WorkorderMphAdmin' : '/WorkorderMphCompany'; - const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}${basePath}/addJournal`, { - workorderMphId: this.workorderMphId, - text: this.newJournalMessage - }); - if (data.success) { - window.notify('success', data.message); - this.journals = data.journals || []; - this.newJournalMessage = ''; - } else { - window.notify('error', data.message || 'Eintrag fehlgeschlagen.'); - } - } catch (e) { - window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); - } finally { - this.addingJournalEntry = false; - } - }, - handleFileUpload(event) { - this.uploadData.files = event.target.files; + removeFile(index) { + this.uploadData.files.splice(index, 1); }, async uploadFiles() { - if (!this.uploadData.files?.length) return window.notify('error', 'Bitte eine oder mehrere Dateien auswählen.'); - + if (!this.uploadData.files.length || !this.uploadData.documentType) return window.notify('error', 'Bitte Typ und Datei wählen'); this.uploading = true; const formData = new FormData(); formData.append('workorderMphId', this.workorderMphId); formData.append('documentType', this.uploadData.documentType); formData.append('description', this.uploadData.description); - for (const file of this.uploadData.files) { - formData.append('file', file); + // Handle multiple files + for (let i = 0; i < this.uploadData.files.length; i++) { + formData.append('files[]', this.uploadData.files[i]); } try { - const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderMphCompany/uploadDocumentation`, formData); - if (data.success) { - window.notify('success', data.message); - this.$refs.fileInput.value = ''; - this.uploadData = { files: [], documentType: '', description: '' }; - await this.fetchData(); + const basePath = this.isAdmin ? '/WorkorderMphAdmin' : '/WorkorderMphCompany'; + const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}${basePath}/uploadDocumentation`, formData); + if (data.success) { + window.notify('success', 'OK'); + this.uploadData = { files: [], documentType: '', description: '' }; + this.$refs.fileInput.value=''; + this.$emit('refresh'); } else { - window.notify('error', data.error || 'Upload fehlgeschlagen.'); + window.notify('error', data.message || 'Upload fehlgeschlagen'); } - } catch (e) { - window.notify('error', 'Ein Netzwerkfehler ist beim Upload aufgetreten.'); - } finally { - this.uploading = false; - } + } catch (e) { window.notify('error', 'Upload Fehler'); } finally { this.uploading = false; } }, - async deleteDocumentation(file) { + async deleteDoc(file) { try { - const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderMphCompany/deleteDocumentation`, { - documentationId: file.id - }); - if (data.success) { - window.notify('success', data.message); - await this.fetchData(); - } else { - window.notify('error', data.message || 'Löschen fehlgeschlagen.'); - } - } catch (e) { - window.notify('error', 'Netzwerkfehler beim Löschen.'); - } - }, - async completeWorkorder() { - this.completing = true; - try { - const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderMphCompany/completeWorkorder`, { - workorderId: this.workorderMphId - }); - if (data.success) { - window.notify('success', data.message); - this.$emit('workorder-completed'); - this.showCompleteModal = false; - } else { - window.notify('error', data.message || 'Abschluss fehlgeschlagen.'); - } - } catch (e) { - window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); - } finally { - this.completing = false; - } - }, - async acceptDocumentation() { - try { - const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderMphAdmin/acceptDocumentation`, { - workorderId: this.workorderMphId - }); - if (data.success) { - window.notify('success', data.message); - this.$emit('documentation-accepted'); - this.showAcceptModal = false; - } else { - window.notify('error', data.message || 'Akzeptieren fehlgeschlagen.'); - } - } catch (e) { - window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); - } + const basePath = this.isAdmin ? '/WorkorderMphAdmin' : '/WorkorderMphCompany'; + await axios.post(`${window.TT_CONFIG.BASE_PATH}${basePath}/deleteDocumentation`, { documentationId: file.id }); + window.notify('success', 'Gelöscht'); this.$emit('refresh'); + } catch (e) { window.notify('error', 'Fehler'); } } - }, - async mounted() { - await this.fetchData(); } }); + +// Admin Review Component +Vue.component('workorder-mph-admin-review', { + props: { docs: { type: Array, default: () => [] }, workorderMphId: { type: [Number, String], required: true } }, + data: () => ({ + checklist: [ + { key: 'photo_hak_mounted', label: 'Foto vom montierten HAK', icon: 'fas fa-camera' }, + { key: 'photo_hak_open', label: 'Foto von dem offenen HAK', icon: 'fas fa-camera' }, + { key: 'photo_splice_cassette_hak', label: 'Foto der Spleißkasette - HAK', icon: 'fas fa-camera' }, + { key: 'photo_splice_cassette_fcp', label: 'Foto der Spleißkasette - FCP', icon: 'fas fa-camera' }, + { key: 'photo_fcp_labeled', label: 'Foto vom FCP beschriftet', icon: 'fas fa-tag' }, + { key: 'photo_patch_pos_osp', label: 'Foto der Patch-Position - OSP-Seite', icon: 'fas fa-ethernet' }, + { key: 'photo_patch_pos_anb', label: 'Foto der Patch-Position - ANB-Seite', icon: 'fas fa-ethernet' }, + { key: 'otdr_measurement', label: 'ODTR - Messung (1310nm & 1550nm)', icon: 'fas fa-chart-line' } + ], + acceptModal: false + }), + computed: { + canAccept() { + return this.checklist.every(item => this.hasDoc(item.key)); + } + }, + methods: { + hasDoc(key) { return this.docs.some(d => d.documentType === key); }, + async accept() { + try { + const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderMphAdmin/acceptDocumentation`, { workorderId: this.workorderMphId }); + if (data.success) { window.notify('success', 'Akzeptiert'); this.acceptModal = false; this.$emit('refresh'); } + } catch (e) { window.notify('error', 'Fehler'); } + } + }, + template: ` +
+
Prüfung & Freigabe
+
+
+
+ + {{ item.label }} + + +
+
+ + + + Dokumentation akzeptieren und Auftrag abschließen? + +
+
+ ` +}); + +// Legacy Wrapper for compatibility +Vue.component('workorder-mph-details-manager', { + props: { workorderMphId: { type: [Number, String], required: true }, isAdmin: { type: Boolean, default: false } }, + template: ` + +
+
+ +
+
+ `, + methods: { + async completeWorkorder() { + if (!confirm('Wirklich abschließen?')) return; + try { + const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderMphCompany/completeWorkorder`, { workorderId: this.workorderMphId }); + if (data.success) { window.notify('success', 'Erledigt'); this.$emit('workorder-completed'); } + } catch (e) { window.notify('error', 'Fehler'); } + } + } +}); \ No newline at end of file diff --git a/public/js/pages/WorkorderMphCompany/WorkorderMphCompany.js b/public/js/pages/WorkorderMphCompany/WorkorderMphCompany.js index 039d9a8e6..f4e198720 100644 --- a/public/js/pages/WorkorderMphCompany/WorkorderMphCompany.js +++ b/public/js/pages/WorkorderMphCompany/WorkorderMphCompany.js @@ -38,9 +38,9 @@ Vue.component('workorder-mph-company', { diff --git a/public/plugins/vue/tt-components/tt-file-gallery.js b/public/plugins/vue/tt-components/tt-file-gallery.js index ea61b0ed4..50bb6d69a 100644 --- a/public/plugins/vue/tt-components/tt-file-gallery.js +++ b/public/plugins/vue/tt-components/tt-file-gallery.js @@ -50,7 +50,7 @@ Vue.component('tt-fullscreen-viewer', { } }, methods: { - isImage: file => file?.mimetype?.startsWith('image/'), + isImage: file => file?.mimetype?.startsWith('image/') || file?.mimetype === 'application/octet-stream', isPdf: file => file?.mimetype === 'application/pdf', onContentLoad() { this.isLoading = false; @@ -249,7 +249,7 @@ Vue.component('tt-file-gallery', { }, }, methods: { - isImage: file => file.mimetype?.startsWith('image/'), + isImage: file => file.mimetype?.startsWith('image/') || file.mimetype === 'application/octet-stream', isPdf: file => file.mimetype === 'application/pdf', getFileIcon(file) {