Files
thetool/public/js/pages/RMLWorkorder/RMLWorkorder.js
2025-06-29 20:32:28 +02:00

423 lines
18 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.
// RMLWorkorder.js
// =================================================================================
// Main Component - Switches between Admin and Company View
// =================================================================================
Vue.component('r-m-l-workorder', {
template: `
<div>
<rml-workorder-admin-view v-if="window.TT_CONFIG.RML_ADMIN === '1'"></rml-workorder-admin-view>
<rml-workorder-company-view v-else></rml-workorder-company-view>
</div>
`,
data() { return { window: window } }
});
// =================================================================================
// RML Admin View
// =================================================================================
Vue.component('rml-workorder-admin-view', {
template: `
<tt-card>
<assign-company-modal
v-if="assignModalWorkorderId"
:workorder-id="assignModalWorkorderId"
@close="assignModalWorkorderId = null; $refs.table.$refs.table.refreshTable()"
/>
<documentation-viewer-modal
v-if="docsModalWorkorderId"
:workorder-id="docsModalWorkorderId"
@close="docsModalWorkorderId = null"
/>
<tt-table-crud
ref="table"
@assign="assignModalWorkorderId = $event.id"
@view_docs="docsModalWorkorderId = $event.id"
:crud-config="crudConfig"
>
<!-- <template v-slot:preorderinfo="{ row }">-->
<!-- <div v-html="row.preorderInfo"></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>-->
</tt-table-crud>
</tt-card>
`,
data() {
return {
assignModalWorkorderId: null,
docsModalWorkorderId: null,
crudConfig: {
...window.TT_CONFIG.CRUD_CONFIG,
additionalActions: [
{
"key": "assign",
"title": "Firma zuweisen",
"class": "fas fa-user-plus text-primary",
"condition": (row) => row.status === 'new',
},
{
"key": "view_docs",
"title": "Dokumentation ansehen",
"class": "fas fa-folder-open text-info",
"condition": (row) => ['documented', 'completed'].includes(row.status),
},
]
}
}
},
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');
}
}
});
// =================================================================================
// RML Company View
// =================================================================================
Vue.component('rml-workorder-company-view', {
template: `
<tt-card>
<schedule-appointment-modal
v-if="scheduleModalWorkorderId"
:workorder-id="scheduleModalWorkorderId"
@close="scheduleModalWorkorderId = null; $refs.table.$refs.table.refreshTable()"
/>
<documentation-modal
v-if="documentModalWorkorder"
:workorder="documentModalWorkorder"
@close="documentModalWorkorder = null; $refs.table.$refs.table.refreshTable()"
/>
<tt-table-crud
ref="table"
@schedule="scheduleModalWorkorderId = $event.id"
@document="documentModalWorkorder = $event"
:crud-config="crudConfig"
>
<template v-slot:preorderinfo="{ row }">
<div v-html="row.preorderInfo"></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>
</tt-table-crud>
</tt-card>
`,
data() {
return {
scheduleModalWorkorderId: null,
documentModalWorkorder: null,
crudConfig: {
...window.TT_CONFIG.CRUD_CONFIG,
additionalActions: [
{
"key": "schedule",
"title": "Termin festlegen",
"class": "fas fa-calendar-plus text-primary",
"condition": (row) => row.status === 'assigned',
},
{
"key": "document",
"title": "Dokumentieren & Abschließen",
"class": "fas fa-camera text-success",
"condition": (row) => ['assigned', 'scheduled', 'documented'].includes(row.status),
},
]
}
}
},
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');
}
}
});
// =================================================================================
// Modals and Helper Components
// =================================================================================
// Traffic Light Component
Vue.component('traffic-light', {
props: ['deadline', 'status'],
computed: {
lightColor() {
if (this.status === 'completed') return '#cccccc'; // Grey for completed
const now = moment();
const deadlineDate = moment.unix(this.deadline);
if (!deadlineDate.isValid()) return '#cccccc'; // Grey for invalid date
if (deadlineDate.isBefore(now)) return '#dc3545'; // Red for overdue
if (deadlineDate.isBefore(now.clone().add(1, 'weeks'))) return '#dc3545'; // Red
if (deadlineDate.isBefore(now.clone().add(3, 'weeks'))) return '#ffc107'; // Yellow
return '#28a745'; // Green
}
},
template: `<span :style="{ color: lightColor, fontSize: '1.2em' }" class="mr-2" title="Dringlichkeit">&#9679;</span>`
});
// Modal for RML Admin to assign a company
Vue.component('assign-company-modal', {
props: ['workorderId'],
template: `
<tt-modal :show="true" title="Firma zuweisen" @submit="submit" @update:show="$emit('close')">
<tt-select label="Firma" :options="companies" v-model="selectedCompanyId" sm row required />
</tt-modal>
`,
data() { return { companies: [], selectedCompanyId: null } },
async mounted() {
const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorder/getCompanies`);
this.companies = response.data;
},
methods: {
async submit() {
if (!this.selectedCompanyId) return window.notify('error', 'Bitte eine Firma auswählen.');
const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorder/assignWorkorder`, {
workorderId: this.workorderId,
companyId: this.selectedCompanyId
});
if(response.data.success) {
window.notify('success', response.data.message);
this.$emit('close');
} else {
window.notify('error', response.data.message || 'Ein Fehler ist aufgetreten.');
}
}
}
});
// Modal for Company to schedule an appointment
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.');
const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorder/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.');
}
}
}
});
// Documentation Upload Modal for Companies
Vue.component('documentation-modal', {
props: ['workorder'],
template: `
<tt-modal :show="true" :title="'Dokumentation für Auftrag #' + workorder.id" :save="false" :delete="false" @update:show="$emit('close')">
<div class="mb-3">
<h5>Benötigte Dokumente</h5>
<ul>
<li v-for="docType in requiredDocTypes" :key="docType.value">
{{ docType.text }}
<i v-if="isUploaded(docType.value)" class="fas fa-check-circle text-success"></i>
</li>
</ul>
</div>
<div class="card mb-3">
<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 (optional)" v-model="uploadData.description" sm row />
<div class="form-group row">
<label class="col-form-label col-sm-4 col-form-label-sm">Datei</label>
<div class="col-sm-8">
<input type="file" class="form-control-file" @change="handleFileUpload" ref="fileInput" />
</div>
</div>
<tt-button text="Hochladen" @click="uploadFile" :loading="uploading" additional-class="btn-primary float-right" />
</div>
</div>
<documentation-viewer-modal :workorder-id="workorder.id" :key="viewerKey" />
<template v-slot:footer>
<tt-button text="Auftrag abschließen" @click="completeWorkorder" :disabled="!canComplete" additional-class="btn-success" />
<button class="btn btn-secondary" @click="$emit('close')">Schließen</button>
</template>
</tt-modal>
`,
data() {
return {
uploading: false,
viewerKey: 0,
uploadedFiles: [],
uploadData: {
file: null,
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() {
// Check if at least one of each required document type is uploaded.
return this.requiredDocTypes.every(docType => this.isUploaded(docType.value));
}
},
methods: {
isUploaded(docType) {
return this.uploadedFiles.some(file => file.documentType === docType);
},
handleFileUpload(event) {
this.uploadData.file = event.target.files[0];
},
async uploadFile() {
if(!this.uploadData.file) return window.notify('error', 'Bitte eine Datei auswählen.');
this.uploading = true;
const formData = new FormData();
formData.append('file', this.uploadData.file);
formData.append('workorderId', this.workorder.id);
formData.append('documentType', this.uploadData.documentType);
formData.append('description', this.uploadData.description);
const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorder/uploadDocumentation`, formData);
if(response.data.success) {
window.notify('success', `Datei "${response.data.fileName}" wurde hochgeladen.`);
this.$refs.fileInput.value = ''; // Clear file input
this.uploadData.file = null;
this.uploadData.description = '';
this.viewerKey++; // Refresh the viewer
} else {
window.notify('error', response.data.error || 'Upload fehlgeschlagen.');
}
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;
const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorder/completeWorkorder`, { workorderId: this.workorder.id });
if(response.data.success) {
window.notify('success', response.data.message);
this.$emit('close');
} else {
window.notify('error', response.data.message || 'Ein Fehler ist aufgetreten.');
}
}
},
async mounted() {
const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorder/getDocumentation`, { params: { workorderId: this.workorder.id }});
this.uploadedFiles = response.data;
}
});
// Read-only viewer for documentation, used by both Admins and Companies
Vue.component('documentation-viewer-modal', {
props: ['workorderId'],
template: `
<div class="card">
<div class="card-header">
<h5>Hochgeladene Dokumente</h5>
</div>
<div v-if="loading" class="card-body text-center">
<i class="fas fa-spinner fa-spin"></i>
</div>
<div v-else-if="!docs.length" class="card-body text-center text-muted">
Keine Dokumente vorhanden.
</div>
<ul v-else class="list-group list-group-flush">
<li v-for="doc in docs" :key="doc.id" class="list-group-item">
<a :href="'/File/download?id=' + doc.fileId" target="_blank">
<i class="fas fa-file-download"></i> {{ doc.fileName }}
</a>
<div class="text-muted small">
<strong>Typ:</strong> {{ getDocTypeText(doc.documentType) }} <br/>
<strong>Beschreibung:</strong> {{ doc.description || '-' }} <br/>
<strong>Hochgeladen von:</strong> {{ doc.createBy }} am {{ formatDate(doc.create) }}
</div>
</li>
</ul>
</div>
`,
data() {
return { loading: false, docs: [] }
},
methods: {
async fetchDocs() {
this.loading = true;
const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorder/getDocumentation`, { params: { workorderId: this.workorderId }});
this.docs = response.data;
this.loading = false;
},
formatDate(timestamp) {
return window.moment.unix(timestamp).format('DD.MM.YYYY HH:mm');
},
getDocTypeText(type) {
const types = [
{ 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' },
];
return types.find(t => t.value === type)?.text || type;
}
},
mounted() {
this.fetchDocs();
}
});