Files
thetool/public/js/pages/RMLWorkorderAdmin/RMLWorkorderAdmin.js
2025-08-12 16:26:37 +02:00

451 lines
19 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.
// RMLWorkorderAdmin.js
Vue.component('r-m-l-workorder-admin', {
template: `
<tt-card>
<div class="mb-2 d-flex align-items-center" v-if="workordersToAssign.length > 0">
<span class="mr-3 font-weight-bold">{{ workordersToAssign.length }} Workorder(s) zuweisen:</span>
<div style="width: 300px;">
<tt-select
class="mb-0"
:options="companies"
v-model="massAssignCompanyId"
@input="massAssignCompanies"
placeholder="Firma auswählen..."
sm
no-form-group
/>
</div>
</div>
<tt-table-crud
ref="table"
:crud-config="crudConfig"
>
<template v-slot:preorderinfo="{ row }">
<div class="small">
<div>
<strong>Kunde:</strong> {{ row.customerCompany || row.customerName }}
</div>
<div>
<strong>Anschluss:</strong>
{{ row.street }} {{ row.hausnummer }}<template v-if="row.stiege">/{{ row.stiege }}</template><template v-if="row.apartment"> / WE: {{ row.apartment }}</template>, {{ row.plz }} {{ row.city }}
</div>
<div>
<strong>OAID:</strong> <span class="text-pink">{{ row.oaid }}</span>
<tt-button
icon="fas fa-external-link-alt"
@click="window.open(window.TT_CONFIG.BASE_PATH + '/Preorder/Index?filter[ucode]=' + row.ucode, '_blank');"
additional-class="btn-link btn-sm p-0 m-0"
title="Zur Bestellung"
/>
</div>
</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:companyname="{ row }">
<div class="d-flex justify-content-between align-items-center">
<div class="flex-grow-1">
<div v-if="editingWorkorderId === row.id">
<tt-select
:options="companies"
:value="row.companyId"
@input="assignCompany(row, $event)"
@blur="editingWorkorderId = null"
@keydown.esc.native="editingWorkorderId = null"
placeholder="Firma zuweisen..."
sm
no-form-group
/>
</div>
<div v-else-if="row.status === 'new'">
<tt-select
:options="companies"
:value="row.companyId"
@input="assignCompany(row, $event)"
placeholder="Firma zuweisen..."
sm
no-form-group
/>
</div>
<div v-else class="d-flex align-items-center">
<span>{{ row.companyName || 'N/A' }}</span>
<tt-button
icon="fas fa-edit"
@click="editingWorkorderId = row.id"
additional-class="btn-link btn-sm p-0 ml-2"
title="Zuweisung ändern"
/>
</div>
</div>
<div class="ml-3">
<tt-button v-if="!workordersToAssign.includes(row.id)"
icon="fas fa-plus-circle text-success"
@click="addToAssignList(row)"
additional-class="btn-link btn-sm p-0"
title="Zur Zuweisungsliste hinzufügen"
/>
<tt-button v-if="workordersToAssign.includes(row.id)"
icon="fas fa-minus-circle text-danger"
@click="removeFromAssignList(row)"
additional-class="btn-link btn-sm p-0"
title="Von Zuweisungsliste entfernen"
/>
</div>
</div>
</template>
<template v-slot:deadlinedate="{ row }">
<div v-if="editingDeadlineId === row.id">
<tt-date-picker
:value="row.deadlineDate"
:date-range="false"
@input="updateDeadline(row, $event)"
@blur="editingDeadlineId = null"
sm
no-form-group
/>
</div>
<div v-else class="d-flex align-items-center">
<span>{{ formatDate(row.deadlineDate) }}</span>
<span v-if="row.daysUntilDeadline !== null && row.daysUntilDeadline >= 0" class="ml-2 text-muted small">
übrig: {{ row.daysUntilDeadline }} Tag{{ row.daysUntilDeadline !== 1 ? 'e' : '' }}
</span>
<tt-button
icon="fas fa-edit"
@click="editingDeadlineId = row.id"
additional-class="btn-link btn-sm p-0 ml-2"
title="Deadline ändern"
/>
</div>
</template>
<template v-slot:appointmentdate="{ row }">
{{ formatDate(row.appointmentDate) }}
</template>
<template v-slot:expandedRow="{ row }">
<rml-documentation-viewer-admin
:workorder-id="row.id"
@workorder-updated="$refs.table.$refs.table.refreshTable()"
@accept-documentation="acceptDocumentation"
/>
</template>
</tt-table-crud>
</tt-card>
`,
data() {
return {
window,
workordersToAssign: [],
editingWorkorderId: null,
editingDeadlineId: null,
companies: [],
massAssignCompanyId: null,
massAssignLoading: false,
crudConfig: {
...window.TT_CONFIG.CRUD_CONFIG,
selectable: false,
expandable: true,
customRowClass: (row) => {
const deadlineDate = moment.unix(row.deadlineDate);
if (['completed', 'new'].includes(row.status) || !deadlineDate.isValid()) {
return 'tt-rml-workorder-irrelevant';
}
if (['correction_requested', 'intervention_required'].includes(row.status)) {
return 'tt-rml-workorder-high';
}
const daysLeft = deadlineDate.diff(moment(), 'days');
if (daysLeft <= 7) return 'tt-rml-workorder-urgent';
if (daysLeft <= 21) return 'tt-rml-workorder-medium';
return 'tt-rml-workorder-ontrack';
},
additionalActions: []
}
}
},
async mounted() {
const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/getCompanies`);
this.companies = response.data;
},
methods: {
addToAssignList(row) {
if (!this.workordersToAssign.includes(row.id)) {
this.workordersToAssign.push(row.id);
}
},
removeFromAssignList(row) {
this.workordersToAssign = this.workordersToAssign.filter(id => id !== row.id);
},
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');
},
async assignCompany(workorder, companyId) {
if (!companyId) {
this.editingWorkorderId = null;
return;
}
const payload = {
workorderId: workorder.id,
companyId: companyId
};
try {
const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/assignWorkorder`, payload);
if (response.data.success) {
window.notify('success', response.data.message);
this.$refs.table.$refs.table.refreshTable();
} else {
window.notify('error', response.data.message || 'Ein Fehler ist aufgetreten.');
}
} catch (e) {
window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.');
} finally {
this.editingWorkorderId = null;
}
},
async massAssignCompanies(companyId) {
if (!companyId) return;
if (!confirm(`${this.workordersToAssign.length} Workorder(s) der ausgewählten Firma zuweisen?`)) {
this.massAssignCompanyId = null; // Reset select on cancel
return;
}
this.massAssignLoading = true;
const payload = {
companyId: companyId,
workorderIds: this.workordersToAssign
};
try {
const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/massAssignWorkorders`, payload);
if (response.data.success) {
window.notify('success', response.data.message);
this.workordersToAssign = [];
this.$refs.table.$refs.table.refreshTable();
} else {
window.notify('error', response.data.message || 'Ein Fehler ist aufgetreten.');
}
} catch (e) {
window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.');
} finally {
this.massAssignLoading = false;
this.massAssignCompanyId = null;
}
},
async updateDeadline(workorder, newDate) {
if (!newDate) {
this.editingDeadlineId = null;
return;
}
try {
const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/updateDeadline`, {
workorderId: workorder.id,
deadlineDate: newDate
});
if (response.data.success) {
window.notify('success', response.data.message);
this.$refs.table.$refs.table.refreshTable();
} else {
window.notify('error', response.data.message || 'Ein Fehler ist aufgetreten.');
}
} catch (e) {
window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.');
} finally {
this.editingDeadlineId = null;
}
},
async acceptDocumentation(workorderId) {
if (!confirm('Soll die Dokumentation für diesen Arbeitsauftrag wirklich akzeptiert und der Auftrag abgeschlossen werden?')) return;
try {
const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/acceptDocumentation`, { workorderId });
if (response.data.success) {
window.notify('success', response.data.message);
this.$refs.table.$refs.table.refreshTable();
} else {
window.notify('error', response.data.message || 'Ein Fehler ist aufgetreten.');
}
} catch (e) {
window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.');
}
}
}
});
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('rml-documentation-viewer-admin', {
props: ['workorderId'],
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-6">
<tt-file-gallery :files="docs" @selection-changed="selectedDocs = $event" selectable />
</div>
<div class="col-lg-6">
<div class="card mb-3">
<div class="card-header"><h5><i class="fas fa-exclamation-triangle text-danger mr-2"></i>Korrektur anfordern</h5></div>
<div class="card-body">
<p class="small text-muted">Wählen Sie die zu korrigierenden Dokumente aus der Galerie aus und geben Sie einen Grund an.</p>
<tt-textarea v-model="correctionText" label="Grund für die Korrektur" sm row />
<tt-button text="Korrektur anfordern" @click="requestCorrection" :loading="correctionLoading" additional-class="btn-danger float-right"/>
</div>
</div>
<div class="card mb-3">
<div class="card-header"><h5><i class="fas fa-check-circle text-success mr-2"></i>Dokumentation akzeptieren</h5></div>
<div class="card-body">
<p class="small text-muted">Wenn die Dokumentation korrekt ist, können Sie sie hier akzeptieren.</p>
<tt-button text="Dokumentation akzeptieren"
@click="$emit('accept-documentation', workorderId)"
additional-class="btn-success float-right"
icon="fas fa-check"
/>
</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: 200px; overflow-y: auto;">
<ul v-if="journals.length" class="list-group list-group-flush">
<li v-for="log in journals" :key="log.id" class="list-group-item small">
<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">
<tt-textarea v-model="newJournalMessage" placeholder="Ihre 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>
</div>
`,
data() {
return {
loading: true,
correctionLoading: false,
docs: [],
journals: [],
selectedDocs: [],
correctionText: '',
newJournalMessage: '',
addingJournalEntry: false,
}
},
methods: {
async fetchData() {
this.loading = true;
try {
const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/getDocumentation`, { params: { workorderId: this.workorderId }});
this.docs = response.data.docs;
this.journals = response.data.journals;
} catch(e) {
window.notify('error', 'Dokumentation konnte nicht geladen werden.');
} finally {
this.loading = false;
}
},
async requestCorrection() {
if (!this.correctionText) return window.notify('error', 'Bitte geben Sie einen Grund für die Korrektur an.');
if (this.selectedDocs.length === 0) return window.notify('error', 'Bitte wählen Sie mindestens ein Dokument für die Korrektur aus.');
this.correctionLoading = true;
try {
const payload = {
workorderId: this.workorderId,
text: this.correctionText,
fileIds: this.selectedDocs
};
const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/requestCorrection`, payload);
if (response.data.success) {
window.notify('success', response.data.message);
this.correctionText = '';
this.selectedDocs = [];
await this.fetchData();
this.$emit('workorder-updated');
} else {
window.notify('error', response.data.message);
}
} catch (e) {
window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.');
}
this.correctionLoading = false;
},
async addJournalEntry() {
if (!this.newJournalMessage.trim()) {
return window.notify('error', 'Bitte geben Sie eine Nachricht ein.');
}
this.addingJournalEntry = true;
try {
const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/addJournal`, {
workorderId: this.workorderId,
text: this.newJournalMessage
});
if (response.data.success) {
window.notify('success', response.data.message || 'Journal-Eintrag hinzugefügt.');
this.newJournalMessage = '';
this.journals = response.data.journals;
} else {
window.notify('error', response.data.message || 'Eintrag konnte nicht gespeichert werden.');
}
} catch (e) {
window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.');
} finally {
this.addingJournalEntry = false;
}
},
formatDate(timestamp) {
return window.moment.unix(timestamp).format('DD.MM.YYYY HH:mm');
},
},
mounted() {
this.fetchData();
}
});