// WorkorderMphBase.js - Shared components for WorkorderMph module // Traffic light component Vue.component('traffic-light-mph', { 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: `` }); // 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: { workorderMphId: { type: Number, required: true }, isAdmin: { type: Boolean, default: false } }, template: `
Wohneinheiten
AddressDB #{{ hausnummerId }}
Keine Wohneinheiten gefunden.
Zusatz / Kontakt
Tür
Status
Spleiß
Docs
OAID: {{ we.oaid }}
{{ we.contact }}
{{ we.preorderContact }}
{{ getStatusText(we.status) }}
Dateien auswählen
{{ file.name }}
`, data: () => ({ loading: true, wohneinheiten: [], statusOptions: [], hausnummerId: null, debounceTimers: {}, documentsModal: { show: false, loading: false, uploading: false, isDragging: false, wohneinheitId: null, zusatz: '', docs: [], files: [], uploadDescription: '' } }), computed: { 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 })); this.statusOptions = data.statusOptions || []; this.hausnummerId = data.hausnummerId; } 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); }, async saveWohneinheit(we) { try { const basePath = this.isAdmin ? '/WorkorderMphAdmin' : '/WorkorderMphCompany'; 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 }); this.$emit('wohneinheit-updated'); this.$emit('refresh'); } catch (e) { window.notify('error', 'Fehler beim Speichern'); } finally { we.saving = false; } }, 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 || []; // Update document count in wohneinheit list const we = this.wohneinheiten.find(w => w.wohneinheitId === this.documentsModal.wohneinheitId); if (we) we.documentCount = this.documentsModal.docs.length; } 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 || []; // Update document count in wohneinheit list const we = this.wohneinheiten.find(w => w.wohneinheitId === this.documentsModal.wohneinheitId); if (we) we.documentCount = this.documentsModal.docs.length; } catch (e) { window.notify('error', 'Fehler beim Löschen'); } } }, mounted() { this.fetchWohneinheiten(); } }); // Checkbox Documentation Component Vue.component('checkbox-documentation', { props: { workorderMphId: { type: Number, required: true }, isAdmin: { type: Boolean, default: false } }, template: `
Dokumentation
`, data: () => ({ 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: { async fetchCheckboxes() { 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 } }); 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'; await axios.post(`${window.TT_CONFIG.BASE_PATH}${basePath}/updateCheckboxes`, { workorderMphId: this.workorderMphId, ...this.checkboxes }); this.$emit('refresh'); } catch (e) { window.notify('error', 'Speichern fehlgeschlagen'); } finally { this.saving = false; } } }, 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; } } } }); // 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: () => ({ uploading: false, isDragging: false, uploadData: { files: [], documentType: '', description: '' }, requiredDocs: [ { 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' } ] }), computed: { docOptions() { return [ { value: '', text: '-- Bitte wählen --' }, ...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: { handleFileSelect(event) { this.addFiles(event.target.files); }, handleDrop(event) { this.isDragging = false; this.addFiles(event.dataTransfer.files); }, addFiles(fileList) { if (!fileList.length) return; // Convert FileList to Array and append this.uploadData.files = [...this.uploadData.files, ...Array.from(fileList)]; }, removeFile(index) { this.uploadData.files.splice(index, 1); }, async uploadFiles() { 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); // Handle multiple files for (let i = 0; i < this.uploadData.files.length; i++) { formData.append('files[]', this.uploadData.files[i]); } try { 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.message || 'Upload fehlgeschlagen'); } } catch (e) { window.notify('error', 'Upload Fehler'); } finally { this.uploading = false; } }, async deleteDoc(file) { try { 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'); } } } }); // 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'); } } } });