Files
thetool/public/js/pages/RMLWorkorderCompany/RMLWorkorderCompany.js
2025-07-23 20:44:25 +02:00

283 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// RMLWorkorderCompany.js
Vue.component('r-m-l-workorder-company', {
template: `
<tt-card>
<schedule-appointment-modal
v-if="scheduleModalWorkorderId"
:workorder-id="scheduleModalWorkorderId"
@close="scheduleModalWorkorderId = null; $refs.table.$refs.table.refreshTable()"
/>
<tt-table-crud
ref="table"
@schedule="scheduleModalWorkorderId = $event.id"
:crud-config="crudConfig"
>
<template v-slot:preorderinfo="{ row }">
<div v-html="row.preorderInfo" class="small"></div>
</template>
<template v-slot:status="{ row }">
<traffic-light :deadline="row.deadlineDate" :status="row.status" />
<i :class="getStatusColumn(row.status).icon" :title="getStatusColumn(row.status).text"></i>
<span class="ml-2">{{ getStatusColumn(row.status).text }}</span>
</template>
<template v-slot:deadlinedate="{ row }">
{{ formatDate(row.deadlineDate) }}
</template>
<template v-slot:appointmentdate="{ row }">
{{ formatDate(row.appointmentDate) }}
</template>
<template v-slot:expandedRow="{ row }">
<documentation-manager
:workorder-id="row.id"
@workorder-completed="$refs.table.$refs.table.refreshTable()"
/>
</template>
</tt-table-crud>
</tt-card>
`,
data() {
return {
scheduleModalWorkorderId: null,
crudConfig: {
...window.TT_CONFIG.CRUD_CONFIG,
expandable: true,
additionalActions: [
{
"key": "schedule",
"title": "Termin festlegen",
"class": "fas fa-calendar-plus text-primary",
"condition": (row) => row.status === 'assigned',
}
]
}
}
},
methods: {
getStatusColumn(status) {
const column = this.crudConfig.columns.find(c => c.key === 'status');
return column.table.filterOptions.find(opt => opt.value === status) || {};
},
formatDate(timestamp) {
if (!timestamp) return '';
return window.moment.unix(timestamp).format('DD.MM.YYYY');
}
}
});
Vue.component('traffic-light', {
props: ['deadline', 'status'],
computed: {
lightInfo() {
if (['completed', 'new'].includes(this.status)) return { color: '#cccccc', title: 'Status irrelevant für Dringlichkeit' };
const now = moment();
const deadlineDate = moment.unix(this.deadline);
if (!deadlineDate.isValid()) return { color: '#cccccc', title: 'Keine Deadline gesetzt' };
if (deadlineDate.isBefore(now)) return { color: '#dc3545', title: 'Deadline überschritten' };
const daysLeft = deadlineDate.diff(now, 'days');
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>`
});
Vue.component('schedule-appointment-modal', {
props: ['workorderId'],
template: `
<tt-modal :show="true" title="Termin festlegen" @submit="submit" @update:show="$emit('close')">
<tt-date-picker label="Termindatum" :date-range="false" v-model="appointmentDate" sm row required />
</tt-modal>
`,
data() { return { appointmentDate: null } },
methods: {
async submit() {
if (!this.appointmentDate) return window.notify('error', 'Bitte ein Datum auswählen.');
try {
const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/scheduleAppointment`, {
workorderId: this.workorderId,
appointmentDate: this.appointmentDate
});
if(response.data.success) {
window.notify('success', response.data.message);
this.$emit('close');
} else {
window.notify('error', response.data.message || 'Ein Fehler ist aufgetreten.');
}
} catch (e) {
window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.');
}
}
}
});
Vue.component('documentation-manager', {
props: ['workorderId'],
template: `
<div class="p-3 bg-light" style="width: 100%;">
<div v-if="loadingWorkorder" 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 mb-3 mb-lg-0">
<div class="card h-100">
<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">
<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 abschließen"
@click="completeWorkorder"
:disabled="!canComplete || workorder.status === 'completed'"
:loading="completing"
additional-class="btn-success w-100"
icon="fas fa-check-double"
/>
<small v-if="!canComplete" class="form-text text-muted text-center mt-2">
Bitte laden Sie alle benötigten Dokumente hoch, um den Auftrag abzuschließen.
</small>
<div v-if="workorder.status === 'completed'" class="alert alert-secondary text-center mt-2 p-2">
Auftrag bereits abgeschlossen.
</div>
</div>
</div>
</div>
<div class="col-lg-8">
<div class="card mb-3" v-if="workorder.status !== 'completed'">
<div class="card-body">
<h5 class="card-title">Neues Dokument hochladen</h5>
<tt-select label="Dokumententyp" :options="requiredDocTypes" 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">
<input type="file" class="form-control-file form-control-sm" @change="handleFileUpload" ref="fileInput" multiple />
</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 :files="uploadedFiles" />
</div>
</div>
</div>
`,
data() {
return {
loadingWorkorder: true,
workorder: null,
uploading: false,
completing: false,
uploadedFiles: [],
uploadData: {
files: [],
documentType: 'photo_before',
description: ''
},
requiredDocTypes: [
{ value: 'photo_before', text: 'Foto: Zustand vorher' },
{ value: 'photo_during', text: 'Foto: Während der Arbeit' },
{ value: 'photo_after', text: 'Foto: Zustand nachher' },
{ value: 'measurement_protocol', text: 'Messprotokoll (z.B. OTDR)' },
{ value: 'customer_signature', text: 'Unterschriebenes Arbeitsprotokoll' },
]
}
},
computed: {
canComplete() {
return this.requiredDocTypes.every(docType => this.isUploaded(docType.value));
}
},
methods: {
async loadWorkorder() {
this.loadingWorkorder = true;
try {
const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/getWorkorderById`, { params: { id: this.workorderId }});
this.workorder = response.data;
} catch(e) {
window.notify('error', 'Arbeitsauftragsdetails konnten nicht geladen werden.');
}
this.loadingWorkorder = false;
},
isUploaded(docType) {
return this.uploadedFiles.some(file => file.documentType === docType);
},
async fetchDocs() {
try {
const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/getDocumentation`, { params: { workorderId: this.workorderId }});
this.uploadedFiles = response.data;
} catch(e) {
window.notify('error', 'Dokumente konnten nicht geladen werden.');
}
},
handleFileUpload(event) {
this.uploadData.files = event.target.files;
},
async uploadFiles() {
if(!this.uploadData.files || this.uploadData.files.length === 0) return window.notify('error', 'Bitte eine oder mehrere Dateien auswählen.');
this.uploading = true;
const formData = new FormData();
formData.append('workorderId', this.workorder.id);
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 response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/uploadDocumentation`, formData);
if(response.data.success) {
window.notify('success', response.data.message);
this.$refs.fileInput.value = '';
this.uploadData.files = [];
this.uploadData.description = '';
await this.fetchDocs();
await this.loadWorkorder(); // Reload to get updated status
} else {
window.notify('error', response.data.error || 'Upload fehlgeschlagen.');
}
} catch(e) {
window.notify('error', 'Ein Netzwerkfehler ist beim Upload aufgetreten.');
}
this.uploading = false;
},
async completeWorkorder() {
if(!confirm('Möchten Sie diesen Auftrag wirklich abschließen? Diese Aktion kann nicht rückgängig gemacht werden.')) return;
this.completing = true;
try {
const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/completeWorkorder`, { workorderId: this.workorder.id });
if(response.data.success) {
window.notify('success', response.data.message);
this.$emit('workorder-completed');
} else {
window.notify('error', response.data.message || 'Ein Fehler ist aufgetreten.');
}
} catch(e) {
window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.');
}
this.completing = false;
},
getDocTypeText(type) {
const found = this.requiredDocTypes.find(t => t.value === type);
return found ? found.text : type;
}
},
async mounted() {
await this.loadWorkorder();
await this.fetchDocs();
}
});