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