644 lines
36 KiB
JavaScript
644 lines
36 KiB
JavaScript
// 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: `<span :style="{ color: lightInfo.color, fontSize: '1.2em' }" class="mr-2" :title="lightInfo.title">●</span>`
|
||
});
|
||
|
||
// A manager for civil engineering tasks, used when a workorder requires it.
|
||
Vue.component('civil-engineering-manager', {
|
||
props: ['workorderId', 'isAdmin'],
|
||
template: `
|
||
<div class="p-3 bg-light" style="width: 100%;">
|
||
<div v-if="loading" class="text-center p-5"><i class="fas fa-spinner fa-spin fa-2x"></i></div>
|
||
<div v-else class="row">
|
||
<div class="col-lg-4">
|
||
<div class="card h-100">
|
||
<div class="card-body">
|
||
<h5 class="card-title">Tiefbau-Arbeiten</h5>
|
||
<p class="small text-muted">Schließen Sie den Tiefbau-Auftrag ab. Laden Sie Dokumente hoch, falls erforderlich.</p>
|
||
<hr>
|
||
<tt-button text="Tiefbau abschließen" @click="showCompleteModal = true"
|
||
:disabled="!canComplete || completing"
|
||
:loading="completing" additional-class="btn-success w-100" icon="fas fa-check-double"
|
||
/>
|
||
<small v-if="docsRequired && !canComplete" class="form-text text-muted text-center mt-2">
|
||
Bitte laden Sie mindestens ein Dokument hoch, um den Auftrag abzuschließen.
|
||
</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-lg-8">
|
||
<div class="card mb-3" v-if="docsRequired">
|
||
<div class="card-body">
|
||
<h5 class="card-title">Dokument hochladen</h5>
|
||
<tt-input label="Beschreibung" v-model="uploadData.description" sm row placeholder="Optional"/>
|
||
<div class="form-group row">
|
||
<label class="col-form-label col-sm-4 col-form-label-sm">Dateien</label>
|
||
<div class="col-sm-8 p-0">
|
||
<input type="file" class="form-control-file form-control-sm p-0" @change="handleFileUpload" ref="fileInput" multiple accept="image/*,.pdf"/>
|
||
</div>
|
||
</div>
|
||
<tt-button text="Hochladen" @click="uploadFiles" :loading="uploading" additional-class="btn-primary float-right" icon="fas fa-upload"/>
|
||
</div>
|
||
</div>
|
||
<tt-file-gallery
|
||
v-if="docsRequired"
|
||
:files="uploadedFiles"
|
||
:edit-mode="false"
|
||
:delete-mode="true"
|
||
@delete-file="deleteDocumentation">
|
||
</tt-file-gallery>
|
||
<div v-else class="alert alert-info">Für diesen Auftraggeber ist keine Dokumentation für Tiefbau-Arbeiten erforderlich.</div>
|
||
</div>
|
||
</div>
|
||
<tt-modal :show.sync="showCompleteModal" title="Tiefbau abschließen" @submit="completeTask" @close="showCompleteModal = false" :delete="false">
|
||
Möchten Sie diese Tiefbau-Arbeiten wirklich als abgeschlossen markieren?
|
||
</tt-modal>
|
||
</div>
|
||
`,
|
||
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: `
|
||
<div class="p-3 bg-light">
|
||
<div v-if="loading" class="text-center p-5"><i class="fas fa-spinner fa-spin fa-2x"></i></div>
|
||
<div v-else class="row">
|
||
<div class="col-lg-5 mb-3 mb-lg-0">
|
||
<div v-if="!isAdmin" class="card mb-3">
|
||
<div class="card-body">
|
||
<h5 class="card-title">Benötigte Dokumente</h5>
|
||
<ul class="list-unstyled">
|
||
<li v-for="docType in requiredDocTypes" :key="docType.value" class="mb-2 d-flex align-items-center small">
|
||
<i :class="isUploaded(docType.value) ? 'fas fa-check-circle text-success' : 'far fa-circle text-muted'" class="fa-fw mr-2"></i>
|
||
<span>{{ docType.text }}</span>
|
||
</li>
|
||
</ul>
|
||
<hr>
|
||
<tt-button text="Auftrag zur Prüfung einreichen" @click="showCompleteModal = true"
|
||
:disabled="!canComplete || isReadOnly" :loading="completing"
|
||
additional-class="btn-success w-100" icon="fas fa-check-double"/>
|
||
<small v-if="!canComplete && !isReadOnly" class="form-text text-muted text-center mt-2">
|
||
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.
|
||
</small>
|
||
<div v-if="isReadOnly" class="alert alert-secondary text-center mt-2 p-2">
|
||
Auftrag bereits abgeschlossen oder storniert. Keine Aktionen mehr möglich.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="isAdmin" class="card mb-3">
|
||
<div class="card-body">
|
||
<h5 class="card-title">Prüfung & Freigabe</h5>
|
||
<p class="small text-muted">Prüfen Sie, ob alle erforderlichen Dokumente vorhanden und korrekt sind.</p>
|
||
<ul v-if="!loadingConfig" class="list-unstyled">
|
||
<li v-for="docType in requiredDocTypes" :key="docType.value" class="mb-2 d-flex align-items-center small">
|
||
<i :class="isUploaded(docType.value) ? 'fas fa-check-circle text-success' : 'far fa-circle text-muted'" class="fa-fw mr-2"></i>
|
||
<span>{{ docType.text }}</span>
|
||
</li>
|
||
</ul>
|
||
<div v-else class="text-center"><i class="fas fa-spinner fa-spin"></i></div>
|
||
<hr>
|
||
<tt-button text="Dokumentation akzeptieren" @click="showAcceptModal = true"
|
||
additional-class="btn-success w-100" icon="fas fa-check"/>
|
||
<tt-button v-if="workorder.status === 'documented'"
|
||
text="Status zurücksetzen (auf Zugewiesen)"
|
||
@click="showRevertModal = true"
|
||
additional-class="btn-warning w-100 mt-1" icon="fas fa-undo"/>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card mt-3">
|
||
<div class="card-header"><h5><i class="fas fa-history mr-2"></i>Journal</h5></div>
|
||
<div class="card-body p-0" style="max-height: 250px; overflow-y: auto;">
|
||
<ul v-if="journals.length" class="list-group list-group-flush">
|
||
<li v-for="log in isAdmin ? journals : journals.filter(j => !j.text.toLowerCase().includes('wurde zugewiesen.'))" :key="log.id" class="list-group-item small"
|
||
:class="{'list-group-item-danger': log.statusChange && (log.statusChange.includes('correction_requested') || log.statusChange.includes('intervention_required'))}">
|
||
<strong>{{ formatDate(log.create) }} ({{ log.createByName }}):</strong>
|
||
<div class="text-muted" style="white-space: pre-wrap;">{{ log.text }}</div>
|
||
</li>
|
||
</ul>
|
||
<div v-else class="card-body text-muted text-center">Keine Journaleinträge.</div>
|
||
</div>
|
||
<div class="card-footer" v-if="!isReadOnly">
|
||
<tt-textarea v-model="newJournalMessage" placeholder="Nachricht oder Anmerkung..." rows="2"/>
|
||
<tt-button text="Eintrag speichern" @click="addJournalEntry" :loading="addingJournalEntry"
|
||
additional-class="btn-info btn-sm w-100 mt-2" icon="fas fa-paper-plane"/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="col-lg-7">
|
||
<div class="card mb-3" v-if="isAdmin || !isReadOnly">
|
||
<div class="card-body">
|
||
<h5 class="card-title">Neues Dokument hochladen</h5>
|
||
<tt-select label="Dokumententyp" :options="allDocTypes" v-model="uploadData.documentType" sm row/>
|
||
<tt-input label="Beschreibung" v-model="uploadData.description" sm row placeholder="Optional"/>
|
||
<div class="form-group row"><label class="col-form-label col-sm-4 col-form-label-sm">Dateien</label>
|
||
<div class="col-sm-8 p-0"><input type="file" class="form-control-file form-control-sm p-0" @change="handleFileUpload" ref="fileInput" multiple accept="image/*,.pdf"/></div>
|
||
</div>
|
||
<tt-button text="Hochladen" @click="uploadFiles" :loading="uploading" additional-class="btn-primary float-right" icon="fas fa-upload"/>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="(requireCableLength || requireCableType)" class="card mb-3">
|
||
<div class="card-body">
|
||
<h5 class="card-title">Zusatzdaten</h5>
|
||
<p class="small text-muted">Diese Daten werden für den Abschluss benötigt.</p>
|
||
<tt-input
|
||
v-if="requireCableLength"
|
||
label="Kabellänge (m)"
|
||
v-model="workorder.cableLength"
|
||
:disabled="isReadOnly"
|
||
sm row
|
||
/>
|
||
<tt-input
|
||
v-if="requireCableType"
|
||
label="Kabeltyp"
|
||
v-model="workorder.cableType"
|
||
:disabled="isReadOnly"
|
||
sm row
|
||
/>
|
||
<tt-button
|
||
text="Daten speichern"
|
||
@click="saveWorkorderData"
|
||
:loading="savingData"
|
||
:disabled="isReadOnly || savingData"
|
||
additional-class="btn-info float-right"
|
||
icon="fas fa-save"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="showTechnicalData && technicalData && (technicalData.patchposition?.equipmentName || technicalData.rimoWorkorders?.length)" class="card mb-3">
|
||
<div class="card-header bg-purple text-white d-flex justify-content-between align-items-center py-2">
|
||
<span><i class="fas fa-microchip mr-2"></i>Technische Daten</span>
|
||
<small v-if="technicalData.dropcable?.parsed_at" class="opacity-75">
|
||
<i class="fas fa-check mr-1"></i>{{ formatDate(technicalData.dropcable.parsed_at) }}
|
||
</small>
|
||
</div>
|
||
<div class="card-body py-2">
|
||
<div class="row small">
|
||
<div class="col-md-6 mb-2" v-if="technicalData.patchposition?.equipmentName">
|
||
<div class="d-flex">
|
||
<span class="text-muted mr-2"><i class="fas fa-ethernet"></i></span>
|
||
<div>
|
||
<span class="text-monospace font-weight-bold">{{ technicalData.patchposition.equipmentName }}</span>
|
||
<span v-if="technicalData.patchposition.equipmentPort" class="text-muted ml-1">Port {{ technicalData.patchposition.equipmentPort }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-6 mb-2" v-if="technicalData.rimoWorkorders?.length">
|
||
<div v-for="wo in technicalData.rimoWorkorders" :key="wo.id" class="d-flex align-items-center justify-content-between">
|
||
<div class="d-flex align-items-center">
|
||
<span class="text-muted mr-2"><i class="fas fa-file-alt"></i></span>
|
||
<span class="font-weight-bold">{{ wo.rimoName }}</span>
|
||
<span class="badge badge-light ml-2">{{ wo.rimoStatus }}</span>
|
||
</div>
|
||
<div class="btn-group btn-group-sm">
|
||
<a :href="wo.downloadUrl" target="_blank" class="btn btn-outline-secondary" title="AHA PDF"><i class="fas fa-file-pdf"></i></a>
|
||
<button @click="parseAha(wo)" class="btn btn-outline-secondary" :disabled="parsingAhaId === wo.id" :title="technicalData.dropcable?.parsed_at ? 'Aktualisieren' : 'Daten laden'">
|
||
<i :class="parsingAhaId === wo.id ? 'fas fa-spinner fa-spin' : 'fas fa-sync-alt'"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-if="technicalData.dropcable?.entries?.length" class="mt-2">
|
||
<table class="table table-sm table-bordered mb-0 small">
|
||
<thead class="thead-light">
|
||
<tr><th>Kabel-ID</th><th>Typ</th><th class="text-center">PLAN</th><th class="text-center">IST</th><th>Status</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="(dk, idx) in technicalData.dropcable.entries" :key="idx">
|
||
<td class="text-monospace">{{ dk.cable_id }}</td>
|
||
<td class="text-truncate" style="max-width:200px" :title="dk.type">{{ dk.type }}</td>
|
||
<td class="text-center">{{ dk.laenge_plan || '-' }}</td>
|
||
<td class="text-center">{{ dk.laenge_ist || '-' }}</td>
|
||
<td><span class="badge" :class="dk.status === 'Planfreigabe' ? 'badge-success' : 'badge-secondary'">{{ dk.status || '-' }}</span></td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div v-if="technicalData.dropcable?.map_file" class="mt-2 text-center">
|
||
<a :href="technicalData.dropcable.map_file.download_url" target="_blank" class="d-block">
|
||
<img :src="technicalData.dropcable.map_file.download_url" class="img-fluid border rounded shadow-sm" style="max-height:300px;cursor:zoom-in" :alt="'Lageplan'" @error="$event.target.parentElement.style.display='none'">
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card mb-3" v-if="isAdmin && selectedDocs.length > 0">
|
||
<div class="card-header bg-warning"><h5><i class="fas fa-exclamation-triangle mr-2"></i>Korrektur anfordern</h5></div>
|
||
<div class="card-body">
|
||
<p class="small text-muted">Die ausgewählten Dokumente werden als fehlerhaft markiert. Bitte geben Sie einen Grund an.</p>
|
||
<tt-textarea v-model="correctionText" label="Grund" sm row required/>
|
||
<tt-button text="Korrektur anfordern" @click="requestCorrection" :loading="correctionLoading" additional-class="btn-danger float-right"/>
|
||
</div>
|
||
</div>
|
||
|
||
<tt-file-gallery :files="docsWithStatus" :edit-mode="!isAdmin && !isReadOnly" :delete-mode="!isReadOnly" :selectable="isAdmin"
|
||
@delete-file="deleteDocumentation" @update-file="updateDocumentation" @selection-changed="selectedDocs = $event">
|
||
<template v-slot:file-edit="{ file }">
|
||
<tt-select label="Dokumententyp" :options="allDocTypes" v-model="file.documentType" sm/>
|
||
</template>
|
||
</tt-file-gallery>
|
||
|
||
<div class="card mt-3" v-if="!isAdmin && !isReadOnly">
|
||
<div class="card-header bg-danger text-white"><h5><i class="fas fa-hard-hat mr-2"></i>Eingriff benötigt</h5></div>
|
||
<div class="card-body">
|
||
<p class="small text-muted">Falls ein Problem auftritt, das ein Eingreifen erfordert, melden Sie es hier.</p>
|
||
<tt-button text="Problem melden" @click="openInterventionModal" additional-class="btn-danger w-100" icon="fas fa-exclamation-triangle"/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<tt-modal v-if="interventionData" :show.sync="interventionData" title="Eingriff anfordern" @submit="requestIntervention">
|
||
<tt-select label="Art des Problems" :options="interventionTypes" v-model="interventionData.types" sm row multiple/>
|
||
<div v-for="type in interventionData.types" :key="type">
|
||
<tt-input v-if="['stuck', 'stuck_fcp', 'stuck_hup'].includes(type)" :label="'Distanz (m) für \\'' + getInterventionLabel(type) + '\\''" type="number" v-model="interventionData.details[type].distance" sm row required/>
|
||
<tt-textarea v-if="type === 'other'" label="Grund für 'Sonstiges'" v-model="interventionData.details.other.reason" sm row required/>
|
||
</div>
|
||
</tt-modal>
|
||
<tt-modal :show.sync="showCompleteModal" title="Auftrag abschließen" @submit="completeWorkorder" :delete="false">
|
||
Möchten Sie diesen Auftrag wirklich abschließen und zur Prüfung einreichen?
|
||
</tt-modal>
|
||
<tt-modal :show.sync="showAcceptModal" title="Dokumentation akzeptieren" @submit="acceptDocumentation" :delete="false">
|
||
Soll die Dokumentation für diesen Arbeitsauftrag wirklich akzeptiert und der Auftrag abgeschlossen werden?
|
||
</tt-modal>
|
||
<tt-modal :show.sync="showRevertModal" title="Status zurücksetzen" @submit="revertDocumentedStatus" :delete="false">
|
||
Möchten Sie den Status dieses Auftrags wirklich von 'Dokumentiert' auf 'Zugewiesen' zurücksetzen? Die Firma muss den Auftrag dann erneut einreichen.
|
||
</tt-modal>
|
||
</div>`,
|
||
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();
|
||
}
|
||
}); |