283 lines
12 KiB
JavaScript
283 lines
12 KiB
JavaScript
// 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">●</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();
|
||
}
|
||
}); |