diff --git a/Layout/default/VueViews/WorkorderCompanyPWA.php b/Layout/default/VueViews/WorkorderCompanyPWA.php index 27ef3c832..37634e7dd 100644 --- a/Layout/default/VueViews/WorkorderCompanyPWA.php +++ b/Layout/default/VueViews/WorkorderCompanyPWA.php @@ -115,6 +115,7 @@ const isSettingsOpen = ref(false); const theme = ref('system'); // 'light', 'dark', 'system' const showThemePicker = ref(false); + const savingData = ref(false); // <-- ADDED const API_BASE_URL = window.TT_CONFIG.BASE_PATH || '/WorkorderCompany'; @@ -211,9 +212,24 @@ }); }); + // MODIFIED const isChecklistComplete = computed(() => { - if (checklist.value.length === 0) return true; - return checklist.value.every(item => item.completed); + // Check documents + if (checklist.value.length > 0) { + if (!checklist.value.every(item => item.completed)) { + return false; + } + } + + // Check new fields + if (tenantConfig.value?.requireCableLength && (!selectedWorkorder.value.cableLength || !selectedWorkorder.value.cableLength.trim())) { + return false; + } + if (tenantConfig.value?.requireCableType && (!selectedWorkorder.value.cableType || !selectedWorkorder.value.cableType.trim())) { + return false; + } + + return true; // All checks passed }); const translatedDocs = computed(() => { @@ -291,7 +307,7 @@ documentation.docs = docRes.data.docs.map(d => ({...d, isPdf: d.mimetype === 'application/pdf'})); documentation.journals = docRes.data.journals; if (configRes.data.success) { - tenantConfig.value = configRes.data; + tenantConfig.value = configRes.data; // <-- MODIFIED: This will now contain all flags } } catch (e) { console.error("Could not load details", e); } finally { isDetailsLoading.value = false; } @@ -329,6 +345,36 @@ finally { isEditingInfo.value = false; } }; + // START ADDED + const saveWorkorderData = async () => { + savingData.value = true; + try { + const response = await api.post('/updateWorkorderData', { + workorderId: selectedWorkorder.value.id, + cableLength: selectedWorkorder.value.cableLength, + cableType: selectedWorkorder.value.cableType + }); + if (response.data.success) { + alert('Daten gespeichert.'); // PWA uses alert() + documentation.journals = response.data.journals; // Update journal + // Also update the main list item + const woInList = workorders.value.find(w => w.id === selectedWorkorder.value.id); + if (woInList) { + woInList.cableLength = selectedWorkorder.value.cableLength; + woInList.cableType = selectedWorkorder.value.cableType; + } + } else { + alert(response.data.message || 'Speichern fehlgeschlagen.'); + } + } catch (e) { + console.error("Failed to save data", e); + alert('Ein Netzwerkfehler ist aufgetreten.'); + } finally { + savingData.value = false; + } + }; + // END ADDED + const addJournalEntry = async () => { if (!newJournalEntry.value.trim()) return; try { @@ -401,13 +447,23 @@ finally { problemModal.show = false; problemModal.selectedInterventions = []; problemModal.details = {}; } }; + // MODIFIED const handleCompleteClick = () => { if (isChecklistComplete.value) { if (confirm("Möchten Sie diesen Auftrag wirklich abschließen?")) { completeWorkorder(); } } else { - missingTasksPopover.tasks = checklist.value.filter(t => !t.completed).map(t => t.text); + const missingDocs = checklist.value.filter(t => !t.completed).map(t => t.text); + const missingData = []; + if (tenantConfig.value?.requireCableLength && (!selectedWorkorder.value.cableLength || !selectedWorkorder.value.cableLength.trim())) { + missingData.push("Kabellänge"); + } + if (tenantConfig.value?.requireCableType && (!selectedWorkorder.value.cableType || !selectedWorkorder.value.cableType.trim())) { + missingData.push("Kabeltyp"); + } + + missingTasksPopover.tasks = [...missingDocs, ...missingData]; missingTasksPopover.show = true; setTimeout(() => missingTasksPopover.show = false, 4000); } @@ -415,10 +471,18 @@ const completeWorkorder = async () => { try { - await api.post('/completeWorkorder', { workorderId: selectedWorkorder.value.id }); - await fetchWorkorders(); - closeDetails(); - } catch(e) { console.error("Failed to complete workorder", e); } + // Server-side validation will catch errors if client-side check fails + const response = await api.post('/completeWorkorder', { workorderId: selectedWorkorder.value.id }); + if (response.data.success) { + await fetchWorkorders(); + closeDetails(); + } else { + alert(response.data.message); // Show validation error from server + } + } catch(e) { + console.error("Failed to complete workorder", e); + alert(e.response?.data?.message || 'Fehler beim Abschließen.'); + } }; const selectFcp = (fcpValue) => { @@ -456,8 +520,10 @@ checklist, fullscreenViewer, missingTasksPopover, translatedDocs, filteredJournals, installModal, isStandalone, selectedFcp, isFcpSelectOpen, fcpOptions, selectedFcpText, fcpSearchTerm, filteredFcpOptions, fcpInputRef, isSettingsOpen, theme, showThemePicker, + savingData, // <-- ADDED fetchWorkorders, openDetails, closeDetails, getStatusInfo, formatDate, googleMapsLink, startEditInfo, saveAdditionalInfo, - handleFileSelect, executeUpload, addJournalEntry, submitProblem, handleCompleteClick, selectFcp, setTheme + handleFileSelect, executeUpload, addJournalEntry, submitProblem, handleCompleteClick, selectFcp, setTheme, + saveWorkorderData // <-- ADDED }; }, template: ` @@ -593,6 +659,20 @@

{{ selectedWorkorder.additionalInfo || 'Keine Notiz.' }}

+
+

Zusatzdaten

+
+ + +
+
+ + +
+ +

Checkliste

@@ -672,7 +752,7 @@
-

Fehlende Checklisten-Punkte:

+

Fehlende Punkte:

  • {{ task }}
@@ -870,4 +950,4 @@ - + \ No newline at end of file diff --git a/application/Workorder/WorkorderModel.php b/application/Workorder/WorkorderModel.php index 6ec80a964..541b86191 100644 --- a/application/Workorder/WorkorderModel.php +++ b/application/Workorder/WorkorderModel.php @@ -14,6 +14,8 @@ class WorkorderModel extends TTCrudBaseModel public ?int $deadlineDate; public ?int $appointmentDate; public ?string $additionalInfo; + public ?string $cableLength; + public ?string $cableType; public int $create; public int $createBy; diff --git a/application/WorkorderCompany/WorkorderCompanyController.php b/application/WorkorderCompany/WorkorderCompanyController.php index 7c501f946..e0ab6266a 100644 --- a/application/WorkorderCompany/WorkorderCompanyController.php +++ b/application/WorkorderCompany/WorkorderCompanyController.php @@ -143,6 +143,18 @@ class WorkorderCompanyController extends WorkorderBaseController { $workorder = WorkorderModel::get($this->postData['workorderId']); if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); + // START ADDED VALIDATION + $tenantConfig = $this->getTenantConfigFromWorkorder($workorder->id); + if ($tenantConfig) { + if ($tenantConfig->requireCableLength && empty(trim($workorder->cableLength))) { + self::sendError("Bitte geben Sie die Kabellänge an, um den Auftrag abzuschließen."); + } + if ($tenantConfig->requireCableType && empty(trim($workorder->cableType))) { + self::sendError("Bitte geben Sie den Kabeltyp an, um den Auftrag abzuschließen."); + } + } + // END ADDED VALIDATION + $workorder->status = 'documented'; WorkorderModel::update((array)$workorder); self::returnJson(['success' => true, 'message' => 'Arbeitsauftrag zur Prüfung eingereicht.']); @@ -155,7 +167,14 @@ class WorkorderCompanyController extends WorkorderBaseController { self::returnJson(['success' => false, 'message' => 'Keine Mandantenkonfiguration gefunden.']); return; } - self::returnJson(['success' => true, 'documentationTypes' => json_decode($tenantConfig->documentationTypes, true), 'civilEngineeringDocsRequired' => $tenantConfig->civilEngineeringDocsRequired, 'interventionTypes' => json_decode($tenantConfig->interventionTypes, true)]); + self::returnJson([ + 'success' => true, + 'documentationTypes' => json_decode($tenantConfig->documentationTypes, true), + 'civilEngineeringDocsRequired' => $tenantConfig->civilEngineeringDocsRequired, + 'interventionTypes' => json_decode($tenantConfig->interventionTypes, true), + 'requireCableLength' => $tenantConfig->requireCableLength, + 'requireCableType' => $tenantConfig->requireCableType + ]); } protected function uploadDocumentationAction() { @@ -237,5 +256,52 @@ class WorkorderCompanyController extends WorkorderBaseController { ]); self::returnJson(['success' => true, 'message' => 'Tiefbau erfolgreich abgeschlossen.']); } + + protected function updateWorkorderDataAction() { + if (empty($this->postData['workorderId'])) self::sendError("Arbeitsauftrags-ID fehlt."); + $workorder = WorkorderModel::get($this->postData['workorderId']); + if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); + + $journalText = "Zusatzdaten aktualisiert:\n"; + $changed = false; + + if (isset($this->postData['cableLength'])) { + if ($workorder->cableLength != $this.postData['cableLength']) { + $journalText .= "Kabellänge: '{$workorder->cableLength}' -> '{$this->postData['cableLength']}'\n"; + $workorder->cableLength = $this.postData['cableLength']; + $changed = true; + } + } + + if (isset($this->postData['cableType'])) { + if ($workorder->cableType != $this.postData['cableType']) { + $journalText .= "Kabeltyp: '{$workorder->cableType}' -> '{$this->postData['cableType']}'\n"; + $workorder->cableType = $this.postData['cableType']; + $changed = true; + } + } + + if (!$changed) { + self::returnJson(['success' => true, 'message' => 'Keine Änderungen vorgenommen.']); + return; + } + + WorkorderModel::update((array)$workorder); + + WorkorderJournalModel::create([ + 'workorderId' => $workorder->id, + 'text' => $journalText, + 'create' => time(), + 'createBy' => $this->user->id, + ]); + + // Re-fetch journals to return + $journals = WorkorderJournalModel::getAll(['workorderId' => intval($workorder->id)], null, 0, ['key' => 'create', 'order' => 'DESC']); + foreach ($journals as $journal) { + $journal->createByName = UserModel::getOne($journal->createBy)->name ?? 'Unbekannt'; + } + + self::returnJson(['success' => true, 'message' => 'Daten gespeichert.', 'journals' => $journals]); + } //endregion } \ No newline at end of file diff --git a/application/WorkorderTenantConfig/WorkorderTenantConfigModel.php b/application/WorkorderTenantConfig/WorkorderTenantConfigModel.php index 862642472..6454d6502 100644 --- a/application/WorkorderTenantConfig/WorkorderTenantConfigModel.php +++ b/application/WorkorderTenantConfig/WorkorderTenantConfigModel.php @@ -10,6 +10,8 @@ class WorkorderTenantConfigModel extends TTCrudBaseModel { public ?string $workorderActiveFilters; // JSON public ?string $interventionTypes; // JSON public int $civilEngineeringDocsRequired; + public int $requireCableLength; + public int $requireCableType; public int $create; public int $createBy; @@ -31,4 +33,5 @@ class WorkorderTenantConfigModel extends TTCrudBaseModel { $row = $result ? $result->fetch_assoc() : null; return $row ? new self($row) : null; - }} \ No newline at end of file + } +} \ No newline at end of file diff --git a/db/migrations/20251103125000_workorder_add_cable_fields.php b/db/migrations/20251103125000_workorder_add_cable_fields.php new file mode 100644 index 000000000..a15ad1206 --- /dev/null +++ b/db/migrations/20251103125000_workorder_add_cable_fields.php @@ -0,0 +1,55 @@ +getEnvironment() == "thetool") { + $tableWorkorder = $this->table("Workorder"); + $tableWorkorder + ->addColumn("cableLength", "string", [ + 'null' => true, + 'default' => null, + 'after' => 'additionalInfo' + ]) + ->addColumn("cableType", "string", [ + 'null' => true, + 'default' => null, + 'after' => 'cableLength' + ]) + ->update(); + + $tableConfig = $this->table("WorkorderTenantConfig"); + $tableConfig + ->addColumn("requireCableLength", "boolean", [ + 'null' => false, + 'default' => 0, + 'after' => 'civilEngineeringDocsRequired' + ]) + ->addColumn("requireCableType", "boolean", [ + 'null' => false, + 'default' => 0, + 'after' => 'requireCableLength' + ]) + ->update(); + } + } + + public function down(): void + { + if ($this->getEnvironment() == "thetool") { + $this->table("Workorder") + ->removeColumn("cableLength") + ->removeColumn("cableType") + ->save(); + + $this->table("WorkorderTenantConfig") + ->removeColumn("requireCableLength") + ->removeColumn("requireCableType") + ->save(); + } + } +} \ No newline at end of file diff --git a/public/js/pages/WorkorderBase/WorkorderBase.js b/public/js/pages/WorkorderBase/WorkorderBase.js index 47770f6f9..3972717c4 100644 --- a/public/js/pages/WorkorderBase/WorkorderBase.js +++ b/public/js/pages/WorkorderBase/WorkorderBase.js @@ -183,7 +183,7 @@ Vue.component('workorder-details-manager', { :disabled="!canComplete || isReadOnly" :loading="completing" additional-class="btn-success w-100" icon="fas fa-check-double"/> - Bitte laden Sie alle benötigten Dokumente hoch, um den Auftrag abzuschließen. + Bitte laden Sie alle benötigten Dokumente hoch und füllen Sie alle Zusatzdaten (z.B. Kabellänge/-typ) aus, um den Auftrag abzuschließen.
Auftrag bereits abgeschlossen oder storniert. Keine Aktionen mehr möglich. @@ -245,6 +245,35 @@ Vue.component('workorder-details-manager', {
+
+
+
Zusatzdaten
+

Diese Daten werden für den Abschluss benötigt.

+ + + +
+
+
Korrektur anfordern
@@ -296,6 +325,9 @@ Vue.component('workorder-details-manager', { uploadData: { files: [], documentType: 'photo_hup_mounted', description: '' }, interventionData: null, interventionTypes: [], + requireCableLength: false, + requireCableType: false, + savingData: false, // Admin state selectedDocs: [], correctionText: '', correctionLoading: false, showAcceptModal: false, showRevertModal: false, }), @@ -305,7 +337,19 @@ Vue.component('workorder-details-manager', { return this.tenantDocTypes ?? []; }, allDocTypes() { return [...this.requiredDocTypes, {value: 'other', text: 'Sonstiges Dokument (optional)'}]; }, - canComplete() { return this.requiredDocTypes.every(docType => this.isUploaded(docType.value)); }, + canComplete() { + const docsUploaded = this.requiredDocTypes.every(docType => this.isUploaded(docType.value)); + if (!docsUploaded) return false; + + if (this.requireCableLength && (!this.workorder.cableLength || !this.workorder.cableLength.trim())) { + return false; + } + if (this.requireCableType && (!this.workorder.cableType || !this.workorder.cableType.trim())) { + return false; + } + + return true; // All checks passed + }, docsWithStatus() { if (!this.journals?.length) return this.docs; const correctionJournal = [...this.journals].sort((a, b) => b.create - a.create).find(j => j.statusChange?.includes('correction_requested')); @@ -348,6 +392,8 @@ Vue.component('workorder-details-manager', { if (data.success) { this.tenantDocTypes = data.documentationTypes; this.interventionTypes = data.interventionTypes; + this.requireCableLength = data.requireCableLength || false; + this.requireCableType = data.requireCableType || false; } } catch (e) { console.error("Mandantenkonfiguration nicht geladen", e); } finally { this.loadingConfig = false; } @@ -404,6 +450,28 @@ Vue.component('workorder-details-manager', { } else window.notify('error', data.message); } catch (e) { window.notify('error', 'Netzwerkfehler'); } }, + async saveWorkorderData() { + this.savingData = true; + try { + const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/updateWorkorderData`, { + workorderId: this.workorderId, + cableLength: this.workorder.cableLength, + cableType: this.workorder.cableType + }); + if (data.success) { + window.notify('success', data.message); + if (data.journals) { + this.journals = data.journals; // Update journal with new entry + } + } else { + window.notify('error', data.message || 'Speichern fehlgeschlagen.'); + } + } catch (e) { + window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); + } finally { + this.savingData = false; + } + }, openInterventionModal() { this.interventionData = { types: [], details: { stuck: {}, stuck_fcp: {}, stuck_hup: {}, other: {} } }; }, @@ -489,4 +557,4 @@ Vue.component('workorder-details-manager', { await this.loadTenantConfig(); await this.fetchData(); } -}); +}); \ No newline at end of file diff --git a/public/js/pages/WorkorderTenantConfig/WorkorderTenantConfig.js b/public/js/pages/WorkorderTenantConfig/WorkorderTenantConfig.js index 2b3e29821..76406c561 100644 --- a/public/js/pages/WorkorderTenantConfig/WorkorderTenantConfig.js +++ b/public/js/pages/WorkorderTenantConfig/WorkorderTenantConfig.js @@ -77,9 +77,19 @@ Vue.component('workorder-tenant-config', {
Optionen
- -

Tiefbau-Doku: {{ config.civilEngineeringDocsRequired ? 'Ja' : 'Nein' }}

+
+ + + +
+
+

Tiefbau-Doku: {{ config.civilEngineeringDocsRequired ? 'Ja' : 'Nein' }}

+

Kabellänge-Doku: {{ config.requireCableLength ? 'Ja' : 'Nein' }}

+

Kabeltyp-Doku: {{ config.requireCableType ? 'Ja' : 'Nein' }}

+
Zugeordnete Firmen
@@ -312,7 +322,9 @@ Vue.component('workorder-tenant-config', { interventionTypes: [], workorderCreationFilters: '{}', workorderActiveFilters: '{}', - civilEngineeringDocsRequired: 0 + civilEngineeringDocsRequired: 0, + requireCableLength: 0, + requireCableType: 0 } : {visibleForAddressId: []}; this.showModal = true;