// 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: `
Keine Wohneinheiten gefunden.
OAID: {{ we.oaid }}
{{ we.contact }}
{{ we.preorderContact }}
{{ getStatusText(we.status) }}
`,
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: `
`,
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: `
{{ 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: `
`,
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: `
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'); }
}
}
});