// WorkorderCommon.js // A simple component to display a status light based on a deadline. Vue.component('traffic-light', { props: ['deadline', 'status'], computed: { lightInfo() { const deadlineDate = moment.unix(this.deadline); const daysLeft = deadlineDate.diff(moment(), 'days'); if (['completed', 'new', 'cancelled'].includes(this.status)) return { color: '#cccccc', title: 'Status irrelevant für Dringlichkeit' }; if (!deadlineDate.isValid()) return { color: '#cccccc', title: 'Keine Deadline gesetzt' }; if (deadlineDate.isBefore(moment())) return { color: '#dc3545', title: 'Deadline überschritten' }; if (daysLeft <= 7) return { color: '#dc3545', title: 'Dringend: Weniger als 1 Woche' }; if (daysLeft <= 21) return { color: '#ffc107', title: 'Mittel: Weniger als 3 Wochen' }; return { color: '#28a745', title: 'Im Plan: Mehr als 3 Wochen' }; } }, template: `` }); // A manager for civil engineering tasks, used when a workorder requires it. Vue.component('civil-engineering-manager', { props: ['workorderId', 'isAdmin'], template: `
Tiefbau-Arbeiten

Schließen Sie den Tiefbau-Auftrag ab. Laden Sie Dokumente hoch, falls erforderlich.


Bitte laden Sie mindestens ein Dokument hoch, um den Auftrag abzuschließen.
Dokument hochladen
Für diesen Auftraggeber ist keine Dokumentation für Tiefbau-Arbeiten erforderlich.
Möchten Sie diese Tiefbau-Arbeiten wirklich als abgeschlossen markieren?
`, data: () => ({ loading: true, uploading: false, completing: false, docsRequired: false, uploadedFiles: [], uploadData: { files: [], description: '' }, showCompleteModal: false }), computed: { canComplete() { return !this.docsRequired || this.uploadedFiles.length > 0; } }, methods: { async fetchInitialData() { this.loading = true; try { const configRes = await axios.get(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/getTenantConfig`, { params: { workorderId: this.workorderId } }); this.docsRequired = configRes.data.civilEngineeringDocsRequired || false; if(this.docsRequired) { const docRes = await axios.get(`${window.TT_CONFIG.BASE_PATH}/${this.isAdmin ? 'WorkorderAdmin' : 'WorkorderCompany'}/getDocumentation`, { params: { workorderId: this.workorderId } }); this.uploadedFiles = docRes.data.docs || []; } } catch (e) { window.notify('error', 'Konfiguration konnte nicht geladen werden.'); console.error(e); } finally { this.loading = false; } }, handleFileUpload(event) { this.uploadData.files = event.target.files; }, async uploadFiles() { if (!this.uploadData.files?.length) return window.notify('error', 'Bitte eine oder mehrere Dateien auswählen.'); this.uploading = true; const formData = new FormData(); formData.append('workorderId', this.workorderId); formData.append('documentType', 'civil_engineering_photo'); formData.append('description', this.uploadData.description); for (const file of this.uploadData.files) formData.append('files[]', file); try { const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/uploadDocumentation`, formData); if (data.success) { window.notify('success', data.message); this.$refs.fileInput.value = ''; this.uploadData = { files: [], description: '' }; await this.fetchInitialData(); } else window.notify('error', data.error || 'Upload fehlgeschlagen.'); } catch (e) { window.notify('error', 'Ein Netzwerkfehler ist beim Upload aufgetreten.'); } this.uploading = false; }, async deleteDocumentation(file) { try { const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/deleteDocumentation`, {id: file.id}); if (data.success) { window.notify('success', data.message); await this.fetchInitialData(); } else window.notify('error', data.message || 'Löschen fehlgeschlagen.'); } catch (e) { window.notify('error', 'Netzwerkfehler beim Löschen.'); } }, async completeTask() { this.completing = true; try { const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/completeCivilEngineering`, { workorderId: this.workorderId }); 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 mounted() { await this.fetchInitialData(); } }); /** * Unified component for viewing and managing Workorder Details, Documentation, and Journals. * Adapts its UI and functionality based on the isAdmin prop. */ Vue.component('workorder-details-manager', { props: { workorderId: { type: String, required: true }, isAdmin: { type: Boolean, default: false } }, template: `
Benötigte Dokumente
  • {{ docType.text }}

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.
Prüfung & Freigabe

Prüfen Sie, ob alle erforderlichen Dokumente vorhanden und korrekt sind.

  • {{ docType.text }}

Journal
  • {{ formatDate(log.create) }} ({{ log.createByName }}):
    {{ log.text }}
Keine Journaleinträge.
Neues Dokument hochladen
Zusatzdaten

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

Technische Daten {{ formatDate(technicalData.dropcable.parsed_at) }}
{{ technicalData.patchposition.equipmentName }} Port {{ technicalData.patchposition.equipmentPort }}
{{ wo.rimoName }} {{ wo.rimoStatus }}
Kabel-IDTypPLANISTStatus
{{ dk.cable_id }} {{ dk.type }} {{ dk.laenge_plan || '-' }} {{ dk.laenge_ist || '-' }} {{ dk.status || '-' }}
Korrektur anfordern

Die ausgewählten Dokumente werden als fehlerhaft markiert. Bitte geben Sie einen Grund an.

Eingriff benötigt

Falls ein Problem auftritt, das ein Eingreifen erfordert, melden Sie es hier.

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? Möchten Sie den Status dieses Auftrags wirklich von 'Dokumentiert' auf 'Zugewiesen' zurücksetzen? Die Firma muss den Auftrag dann erneut einreichen.
`, data: () => ({ loading: true, loadingConfig: true, workorder: null, docs: [], journals: [], tenantDocTypes: null, newJournalMessage: '', addingJournalEntry: false, // Company state uploading: false, completing: false, showCompleteModal: false, uploadData: { files: [], documentType: 'photo_hup_mounted', description: '' }, interventionData: null, interventionTypes: [], requireCableLength: false, requireCableType: false, savingData: false, // Technical data showTechnicalData: false, technicalData: null, parsingAhaId: null, // Admin state selectedDocs: [], correctionText: '', correctionLoading: false, showAcceptModal: false, showRevertModal: false, }), computed: { isReadOnly() { return ['completed', 'cancelled'].includes(this.workorder?.status); }, requiredDocTypes() { return this.tenantDocTypes ?? []; }, allDocTypes() { return [...this.requiredDocTypes, {value: 'other', text: 'Sonstiges Dokument (optional)'}]; }, 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')); if (!correctionJournal?.fileIds) return this.docs; try { const incorrectFileIds = JSON.parse(correctionJournal.fileIds); if (!Array.isArray(incorrectFileIds)) return this.docs; return this.docs.map(doc => incorrectFileIds.includes(doc.id) ? { ...doc, class: 'border border-danger' } : doc); } catch (e) { return this.docs; } } }, methods: { formatDate(timestamp) { return timestamp ? window.moment.unix(timestamp).format('DD.MM.YYYY HH:mm') : '–'; }, isUploaded(docType) { return Array.isArray(this.docs) && this.docs.some(doc => doc.documentType === docType); }, async fetchData() { this.loading = true; try { const [workorderRes, docsJournalsRes] = await Promise.all([ axios.get(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/getWorkorderById`, {params: {id: this.workorderId}}), axios.get(`${window.TT_CONFIG.BASE_PATH}/${this.isAdmin ? 'WorkorderAdmin' : 'WorkorderCompany'}/getDocumentation`, {params: {workorderId: this.workorderId}}) ]); this.workorder = workorderRes.data; // FIX: Ensure docs and journals are always arrays this.docs = docsJournalsRes.data.docs || []; this.journals = docsJournalsRes.data.journals || []; // Reload tenant config to get updated technical data (AHA may have been auto-parsed) if (this.showTechnicalData) this.loadTenantConfig(); } catch (e) { window.notify('error', 'Details konnten nicht geladen werden.'); this.docs = []; // Ensure it's an array on error this.journals = []; } finally { this.loading = false; } }, async loadTenantConfig() { this.loadingConfig = true; try { const {data} = await axios.get(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/getTenantConfig`, {params: {workorderId: this.workorderId}}); if (data.success) { this.tenantDocTypes = data.documentationTypes; this.interventionTypes = data.interventionTypes; this.requireCableLength = data.requireCableLength || false; this.requireCableType = data.requireCableType || false; this.showTechnicalData = data.showTechnicalData || false; this.technicalData = data.technicalData || null; } } catch (e) { console.error("Mandantenkonfiguration nicht geladen", e); } finally { this.loadingConfig = false; } }, async addJournalEntry() { if (!this.newJournalMessage.trim()) return; this.addingJournalEntry = true; try { const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/${this.isAdmin ? 'WorkorderAdmin' : 'WorkorderCompany'}/addJournal`, { workorderId: this.workorderId, text: this.newJournalMessage }); if (data.success) { this.newJournalMessage = ''; this.journals = data.journals; } else window.notify('error', data.message); } catch (e) { window.notify('error', 'Netzwerkfehler'); } finally { this.addingJournalEntry = false; } }, // Company Methods handleFileUpload(event) { this.uploadData.files = event.target.files; }, async uploadFiles() { if (!this.uploadData.files?.length) return window.notify('error', 'Bitte Dateien auswählen.'); this.uploading = true; const formData = new FormData(); formData.append('workorderId', this.workorderId); formData.append('documentType', this.uploadData.documentType); formData.append('description', this.uploadData.description); for (const file of this.uploadData.files) formData.append('files[]', file); try { const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/uploadDocumentation`, formData); if (data.success) { window.notify('success', data.message); this.$refs.fileInput.value = ''; this.uploadData.files = []; await this.fetchData(); this.$emit('workorder-updated'); } else window.notify('error', data.error); } catch (e) { window.notify('error', 'Upload-Fehler'); } this.uploading = false; }, async deleteDocumentation(file) { try { const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/deleteDocumentation`, {id: file.id}); if (data.success) { window.notify('success', data.message); await this.fetchData(); } else window.notify('error', data.message); } catch (e) { window.notify('error', 'Netzwerkfehler'); } }, async updateDocumentation(file) { try { const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/updateDocumentation`, { id: file.id, documentType: file.documentType }); if (data.success) { window.notify('success', data.message); await this.fetchData(); } 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: {} } }; }, async requestIntervention() { const { types, details } = this.interventionData; if (types.length === 0) return window.notify('error', 'Bitte Problem auswählen.'); let journalParts = []; for (const type of types.sort()) { const problemText = this.interventionTypes.find(o => o.value === type)?.text || type; if (['stuck', 'stuck_fcp', 'stuck_hup'].includes(type)) { if (!details[type]?.distance > 0) return window.notify('error', `Bitte DisWtanz für "${problemText}" eingeben.`); journalParts.push(problemText.replace('X', details[type].distance)); } else if (type === 'other') { if (!details.other?.reason?.trim()) return window.notify('error', `Bitte Grund für "Sonstiges" angeben.`); journalParts.push(`Sonstiges: ${details.other.reason.trim()}`); } else journalParts.push(problemText); } try { const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/requestIntervention`, { workorderId: this.workorderId, journalText: journalParts.join('\\n') }); if (data.success) { window.notify('success', data.message); this.interventionData = null; this.$emit('workorder-completed'); } else window.notify('error', data.message); } catch (e) { window.notify('error', 'Netzwerkfehler'); } }, async completeWorkorder() { this.completing = true; try { const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/completeWorkorder`, {workorderId: this.workorderId}); if (data.success) { window.notify('success', data.message); this.$emit('workorder-completed'); this.showCompleteModal = false; } else window.notify('error', data.message); } catch (e) { window.notify('error', 'Netzwerkfehler'); } this.completing = false; }, // Admin Methods async requestCorrection() { if (!this.correctionText) return window.notify('error', 'Bitte geben Sie einen Grund an.'); if (this.selectedDocs.length === 0) return window.notify('error', 'Bitte Dokumente für die Korrektur auswählen.'); this.correctionLoading = true; try { const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderAdmin/requestCorrection`, { workorderId: this.workorderId, text: this.correctionText, fileIds: this.selectedDocs }); if (data.success) { window.notify('success', data.message); this.correctionText = ''; this.selectedDocs = []; await this.fetchData(); this.$emit('workorder-completed'); } else window.notify('error', data.message); } catch (e) { window.notify('error', 'Netzwerkfehler'); } this.correctionLoading = false; }, acceptDocumentation() { this.$emit('accept-documentation', this.workorderId); this.showAcceptModal = false; }, getInterventionLabel(type) { return this.interventionTypes.find(t => t.value === type)?.text || type; }, async parseAha(wo) { this.parsingAhaId = wo.id; try { const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RimoWorkorder/parseAha`, { id: wo.id }); if (data.success) { window.notify('success', `AHA-Daten geladen: ${data.dropkabel_count} Dropkabel${data.has_map ? ', Lageplan vorhanden' : ''}`); // Reload technical data to show the parsed data await this.loadTenantConfig(); } else { window.notify('error', data.message || 'Fehler beim Laden der AHA-Daten'); } } catch (e) { window.notify('error', 'Netzwerkfehler beim Laden der AHA-Daten'); console.error(e); } finally { this.parsingAhaId = null; } }, async revertDocumentedStatus() { // Optional: Add loading state if needed try { const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderAdmin/revertDocumentedStatus`, { workorderId: this.workorderId }); if (data.success) { window.notify('success', data.message); this.showRevertModal = false; await this.fetchData(); // Refresh data to show new status this.$emit('workorder-updated'); // Or a more specific event if needed } else { window.notify('error', data.message || 'Status konnte nicht zurückgesetzt werden.'); } } catch (e) { window.notify('error', 'Netzwerkfehler beim Zurücksetzen des Status.'); } }, }, async mounted() { await this.loadTenantConfig(); await this.fetchData(); } });