Files
thetool/public/js/pages/WorkorderMphBase/WorkorderMphBase.js
2025-12-03 14:08:44 +00:00

619 lines
32 KiB
JavaScript

// 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: `<span :style="{ color: lightInfo.color, fontSize: '1.2em' }" class="mr-2" :title="lightInfo.title">&#9679;</span>`
});
// 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: `
<div class="workorder-mph-card mph-wohneinheit-card">
<div class="workorder-mph-card-header">
<span><i class="fas fa-building"></i> Wohneinheiten</span>
<div>
<span v-if="loading" class="mr-2"><i class="fas fa-spinner fa-spin text-muted"></i></span>
<a v-if="hausnummerId" :href="'/AddressDB/view/?id=' + hausnummerId" target="_blank" class="small text-muted">
<i class="fas fa-external-link-alt mr-1"></i>AddressDB #{{ hausnummerId }}
</a>
</div>
</div>
<div v-if="wohneinheiten.length === 0 && !loading" class="workorder-mph-card-body">
<div class="alert alert-info mb-0 py-2 small">
<i class="fas fa-info-circle mr-1"></i> Keine Wohneinheiten gefunden.
</div>
</div>
<div v-else class="wohneinheit-manager-container">
<div class="wohneinheit-manager">
<div class="we-table">
<div class="we-header">
<div class="we-header-row">
<div class="we-cell we-zusatz">Zusatz / Kontakt</div>
<div class="we-cell we-tuer">Tür</div>
<div class="we-cell we-status">Status</div>
<div class="we-cell we-splice">Spleiß</div>
<div class="we-cell we-documents">Docs</div>
<div class="we-cell we-actions"></div>
</div>
</div>
<div v-for="we in wohneinheiten" :key="we.wohneinheitId" class="we-row" :class="{ 'locked-row': isLocked(we) }">
<div class="we-cell we-zusatz">
<div class="d-flex align-items-center mb-1">
<input type="text" class="form-control form-control-sm mr-1" v-model="we.zusatz"
:disabled="isLocked(we)" placeholder="Zusatz">
</div>
<!-- OAID: light red, vertical -->
<div v-if="we.oaid" class="text-danger small mb-1" style="font-weight: 500;">
<i class="fas fa-fingerprint mr-1"></i>OAID: {{ we.oaid }}
</div>
<div v-if="we.contact" class="text-muted small text-truncate" :title="we.contact">
<i class="fas fa-user mr-1"></i>{{ we.contact }}
</div>
<div v-else-if="we.preorderContact" class="text-info small text-truncate" :title="'VB: ' + we.preorderContact">
<i class="fas fa-user-check mr-1"></i>{{ we.preorderContact }}
</div>
</div>
<div class="we-cell we-tuer">
<input type="text" class="form-control form-control-sm" v-model="we.tuer"
:disabled="isLocked(we)" placeholder="Tür">
</div>
<div class="we-cell we-status">
<div v-if="isLocked(we)" class="locked-status small">
<i class="fas fa-lock mr-1"></i>{{ getStatusText(we.status) }}
</div>
<tt-select v-else v-model="we.status" :options="filteredStatusOptions" sm no-form-group/>
</div>
<div class="we-cell we-splice">
<label class="custom-checkbox-item we-splice-checkbox justify-content-center" title="Spleiß im HÜP/HAK erledigt">
<input type="checkbox" v-model="we.spliceCompleted" :disabled="isLocked(we)">
<span class="checkmark"></span>
</label>
</div>
<div class="we-cell we-documents">
<button class="btn btn-sm btn-outline-secondary position-relative workorder-mph-button" @click="openDocumentsModal(we)" title="Dokumente">
<i class="fas fa-camera"></i>
<span v-if="we.documentCount" class="badge badge-pill badge-danger position-absolute" style="top: -5px; right: -5px;">{{ we.documentCount }}</span>
</button>
</div>
<div class="we-cell we-actions">
<button class="btn btn-sm btn-primary workorder-mph-button" @click="saveWohneinheit(we)" :disabled="isLocked(we) || we.saving" title="Speichern">
<i v-if="we.saving" class="fas fa-spinner fa-spin"></i>
<i v-else class="fas fa-save"></i>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Documents Modal -->
<tt-modal :show.sync="documentsModal.show" :title="'Dokumente für WE ' + documentsModal.zusatz" size="lg" :delete="false" submit-text="Schließen" @submit="documentsModal.show = false">
<div v-if="documentsModal.loading" class="text-center"><i class="fas fa-spinner fa-spin"></i></div>
<div v-else>
<div class="bg-light p-3 mb-3 border rounded">
<div class="row">
<div class="col-md-5">
<label class="small font-weight-bold mb-1">Datei(en)</label>
<div class="mph-drop-zone"
:class="{ 'dragging': documentsModal.isDragging }"
@dragover.prevent="documentsModal.isDragging = true"
@dragleave.prevent="documentsModal.isDragging = false"
@drop.prevent="handleDrop">
<input type="file" ref="weFileInput" @change="handleFileSelect" multiple accept="image/*,.pdf">
<i class="fas fa-cloud-upload-alt mph-drop-zone-icon"></i>
<div class="mph-drop-zone-text">Dateien auswählen</div>
</div>
<div v-if="documentsModal.files.length" class="mph-file-list">
<div v-for="(file, index) in documentsModal.files" :key="index" class="mph-file-item">
<span class="text-truncate small flex-grow-1">{{ file.name }}</span>
<span class="remove-file" @click="removeFile(index)"><i class="fas fa-times"></i></span>
</div>
</div>
</div>
<div class="col-md-7 d-flex flex-column justify-content-end">
<div class="form-group mb-2">
<label class="small font-weight-bold mb-1">Beschreibung</label>
<input type="text" class="form-control form-control-sm" v-model="documentsModal.uploadDescription" placeholder="Optional">
</div>
<tt-button text="Upload starten" @click="uploadWohneinheitDocument" :loading="documentsModal.uploading" additional-class="btn-primary btn-sm w-100" icon="fas fa-upload"/>
</div>
</div>
</div>
<tt-file-gallery :files="documentsModal.docs" :edit-mode="false" :delete-mode="!isAdmin" @delete-file="deleteWohneinheitDocument"/>
</div>
</tt-modal>
</div>
`,
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');
} 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 || [];
} 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'); }
}
},
mounted() { this.fetchWohneinheiten(); }
});
// Checkbox Documentation Component
Vue.component('checkbox-documentation', {
props: { workorderMphId: { type: Number, required: true }, isAdmin: { type: Boolean, default: false } },
template: `
<div class="workorder-mph-card">
<div class="workorder-mph-card-header">
<span><i class="fas fa-tasks"></i> Dokumentation</span>
<span v-if="saving" class="text-muted small"><i class="fas fa-sync fa-spin"></i></span>
</div>
<div class="workorder-mph-card-body-compact">
<div v-if="loading" class="text-center py-2"><i class="fas fa-spinner fa-spin text-muted"></i></div>
<div v-else class="mph-checkbox-vertical">
<label class="mph-checkbox-item" v-for="(label, key) in labels" :key="key">
<input type="checkbox" v-model="checkboxes[key]" @change="saveCheckboxes">
<span class="checkmark"></span>
<span class="label">{{ label }}</span>
</label>
</div>
</div>
</div>
`,
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 });
} 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: `
<div class="workorder-mph-card">
<div class="workorder-mph-card-header"><i class="fas fa-history"></i> Journal</div>
<div class="workorder-mph-card-body-compact">
<div class="mph-journal-list" v-if="journals.length">
<div v-for="log in journals" :key="log.id" class="mph-journal-item">
<div class="mph-journal-item-header">{{ formatDate(log.create) }} - {{ log.createByName }}</div>
<div style="white-space: pre-wrap; color: #495057;">{{ log.text }}</div>
<div v-if="log.statusChange" class="badge badge-light border mt-1">{{ log.statusChange }}</div>
</div>
</div>
<div v-else class="text-center text-muted small py-2">Keine Einträge</div>
</div>
<div class="p-2 border-top bg-light">
<tt-textarea v-model="newMessage" placeholder="Notiz..." rows="2" no-form-group sm/>
<tt-button text="Senden" @click="addEntry" :loading="adding" :disabled="!canSend" :additional-class="sendButtonClass + ' btn-sm btn-block mt-1'"/>
</div>
</div>
`,
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: `
<div>
<div class="workorder-mph-card mb-3">
<div class="workorder-mph-card-header"><i class="fas fa-upload"></i> Dokument hochladen</div>
<div class="workorder-mph-card-body">
<div class="row">
<div class="col-md-6 mb-2">
<tt-select label="Typ *" :options="docOptions" v-model="uploadData.documentType" sm no-form-group class="mb-2"/>
<label class="small font-weight-bold mb-1">Beschreibung</label>
<input type="text" class="form-control form-control-sm mb-2" v-model="uploadData.description" placeholder="Optional">
</div>
<div class="col-md-6 mb-2">
<label class="small font-weight-bold mb-1">Datei(en)</label>
<div class="mph-drop-zone"
:class="{ 'dragging': isDragging }"
@dragover.prevent="isDragging = true"
@dragleave.prevent="isDragging = false"
@drop.prevent="handleDrop">
<input type="file" ref="fileInput" @change="handleFileSelect" multiple accept="image/*,.pdf">
<i class="fas fa-cloud-upload-alt mph-drop-zone-icon"></i>
<div class="mph-drop-zone-text">Dateien auswählen</div>
<div class="mph-drop-zone-hint">oder hierher ziehen</div>
</div>
<div v-if="uploadData.files.length" class="mph-file-list">
<div v-for="(file, index) in uploadData.files" :key="index" class="mph-file-item">
<i class="fas fa-file mr-2 text-muted"></i>
<span class="text-truncate" style="max-width: 180px;">{{ file.name }}</span>
<span class="remove-file" @click="removeFile(index)"><i class="fas fa-times"></i></span>
</div>
</div>
</div>
</div>
<div class="text-right mt-2">
<tt-button icon="fas fa-upload" text="Hochladen" @click="uploadFiles" :loading="uploading" additional-class="btn-primary btn-sm"/>
</div>
</div>
</div>
<div class="workorder-mph-card">
<div class="workorder-mph-card-header">
<span><i class="fas fa-folder-open"></i> Dokumente</span>
<span class="badge badge-secondary">{{ docs.length }}</span>
</div>
<div class="workorder-mph-card-body">
<div v-if="docs.length === 0" class="text-muted small text-center">Keine Dokumente</div>
<tt-file-gallery v-else :files="docs" :edit-mode="false" :delete-mode="!isAdmin" @delete-file="deleteDoc"/>
</div>
</div>
</div>
`,
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: `
<div class="workorder-mph-card">
<div class="workorder-mph-card-header"><i class="fas fa-check-circle"></i> Prüfung & Freigabe</div>
<div class="workorder-mph-card-body-compact">
<div class="mph-doc-checklist mb-2">
<div v-for="item in checklist" :key="item.key" class="mph-doc-check-item" :class="{ 'has-doc': hasDoc(item.key) }">
<i :class="item.icon" class="mr-2" style="width: 16px; text-align: center;"></i>
<span class="flex-grow-1">{{ item.label }}</span>
<i v-if="hasDoc(item.key)" class="fas fa-check text-success"></i>
<i v-else class="fas fa-times text-muted" style="opacity: 0.3"></i>
</div>
</div>
<tt-button text="Akzeptieren & Abschluss" @click="acceptModal = true" :disabled="!canAccept" additional-class="btn-success btn-sm btn-block" icon="fas fa-check-double"/>
<tt-modal :show.sync="acceptModal" title="Freigabe" @submit="accept" :delete="false">
Dokumentation akzeptieren und Auftrag abschließen?
</tt-modal>
</div>
</div>
`
});
// Legacy Wrapper for compatibility
Vue.component('workorder-mph-details-manager', {
props: { workorderMphId: { type: [Number, String], required: true }, isAdmin: { type: Boolean, default: false } },
template: `
<workorder-mph-data-provider :workorder-mph-id="workorderMphId" v-slot="{ docs, journals, refresh, loading }">
<div class="mph-details-stack">
<div v-if="loading && !docs.length" class="text-center"><i class="fas fa-spinner fa-spin"></i></div>
<template v-else>
<workorder-mph-admin-review v-if="isAdmin" :docs="docs" :workorder-mph-id="workorderMphId" @refresh="refresh" />
<!-- Company Complete Logic (Inline simplified) -->
<div v-if="!isAdmin" class="workorder-mph-card mb-3">
<div class="workorder-mph-card-header"><i class="fas fa-check-double"></i> Abschluss</div>
<div class="workorder-mph-card-body-compact">
<tt-button text="Fertigmelden" @click="completeWorkorder" additional-class="btn-success btn-block" icon="fas fa-flag-checkered"/>
</div>
</div>
<workorder-mph-documents :docs="docs" :workorder-mph-id="workorderMphId" :is-admin="isAdmin" @refresh="refresh" />
<workorder-mph-journal :journals="journals" :workorder-mph-id="workorderMphId" :is-admin="isAdmin" @refresh="refresh" />
</template>
</div>
</workorder-mph-data-provider>
`,
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'); }
}
}
});