Files
thetool/public/js/pages/WorkorderAdmin/WorkorderAdmin.js
2026-01-27 16:09:10 +01:00

336 lines
20 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.
// WorkorderAdmin.js
Vue.component('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="companiesForMassAssign" v-model="massAssignCompanyId"
@input="openMassAssignModal" 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">
<div v-if="companiesLoading" class="spinner-border spinner-border-sm"></div>
<tt-select v-else :options="companiesByTenant[row.tenantId] || []" :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="companiesByTenant[row.tenantId] || []" :value="row.companyId"
@input="assignCompany(row, $event)" @focus="getCompaniesForWorkorder(row)"
placeholder="Firma zuweisen..." sm no-form-group/>
</div>
<div v-else><span>{{ row.companyName || 'N/A' }}</span></div>
</div>
<div style="display: grid; grid-template-columns: repeat(2, auto); gap: 0px; padding-left: 8px;">
<tt-button v-if="!['completed', 'new'].includes(row.status)" icon="fas fa-edit" @click="startCompanyEdit(row)"
additional-class="btn-link workorder-button" title="Zuweisung ändern" />
<tt-button v-if="!workordersToAssign.includes(row.id)" icon="fas fa-plus-circle text-success" @click="addToAssignList(row)"
additional-class="btn-link workorder-button" 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 workorder-button" title="Von Zuweisungsliste entfernen" />
<tt-button v-if="row.status === 'intervention_required'" icon="fas fa-check-circle text-success"
@click="problemSolvedModalData = row" additional-class="btn-link workorder-button" title="Auftrag auf Problem behoben setzen" />
<tt-button v-if="['intervention_required', 'assigned'].includes(row.status)" icon="fas fa-hard-hat text-orange" @click="openCivilEngineeringModal(row)"
additional-class="btn-link workorder-button" title="Tiefbau benötigt" />
<tt-button v-if="!['completed', 'cancelled'].includes(row.status)" icon="fas fa-ban text-danger" @click="cancelWorkorderModalData = row"
additional-class="btn-link workorder-button" title="Auftrag stornieren" />
<tt-button v-if="row.status === 'completed'" icon="fas fa-euro-sign text-purple" @click="chargeWorkorder(row)"
additional-class="btn-link workorder-button" title="Auftrag verrechnen" />
</div>
</div>
</template>
<template v-slot:additionalinfo="{ row }">
<div v-if="editingAdditionalInfoId === row.id">
<tt-textarea v-model="tempAdditionalInfo" @keydown.esc.native="cancelEdit" rows="3" no-form-group sm ref="editTextarea"/>
<div class="mt-2 d-flex justify-content-end">
<tt-button text="Abbrechen" @click="cancelEdit" sm additional-class="btn-secondary mr-2"/>
<tt-button text="Speichern" @click="updateAdditionalInfo(row)" sm additional-class="btn-success"/>
</div>
</div>
<div v-else class="d-flex align-items-start">
<span style="white-space: pre-wrap; max-width: 250px; display: inline-block;">{{ row.additionalInfo || '-' }}</span>
<tt-button icon="fas fa-edit" @click="startAdditionalInfoEdit(row)" additional-class="btn-link btn-sm p-0 ml-2" title="Zusatz-Info bearbeiten"/>
</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>
<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, true) }}</template>
<template v-slot:expandedRow="{ row }">
<civil-engineering-manager v-if="row.status === 'civil_engineering_required'" :workorder-id="row.id" :is-admin="true" class="mb-3"/>
<workorder-details-manager :workorder-id="row.id" :is-admin="true"
@workorder-completed="$refs.table.$refs.table.refreshTable()"
@accept-documentation="acceptDocumentation(row.id)"/>
</template>
</tt-table-crud>
<tt-modal v-if="civilEngineeringData" :show.sync="civilEngineeringData" title="Tiefbau zuweisen" @submit="assignCivilEngineering">
<p><strong>Auftrag:</strong> #{{ civilEngineeringData.workorder.id }}</p>
<tt-select label="Tiefbau-Firma" :options="companiesByTenant[civilEngineeringData.workorder.tenantId] || []" v-model="civilEngineeringData.companyId" sm row required/>
</tt-modal>
<tt-modal v-if="cancelWorkorderModalData" :show.sync="cancelWorkorderModalData" title="Auftrag stornieren" @submit="cancelWorkorder">
<p>Soll der Auftrag <strong>#{{ cancelWorkorderModalData.id }}</strong> wirklich storniert werden?</p>
<tt-textarea label="Grund (optional)" v-model="cancelWorkorderModalData.reason" sm row/>
</tt-modal>
<tt-modal v-if="problemSolvedModalData" :show.sync="problemSolvedModalData" title="Problem als gelöst markieren" @submit="setToProblemSolved">
<p>Soll das Problem bei Auftrag <strong>#{{ problemSolvedModalData.id }}</strong> als gelöst markiert werden?</p>
<tt-textarea label="Journaleintrag" v-model="problemSolvedModalData.text" sm row required/>
</tt-modal>
<tt-modal v-if="massAssignModalData" :show.sync="massAssignModalData" :title="workordersToAssign.length + ' Aufträge zuweisen'" @submit="massAssignCompanies">
<p>Sollen <strong>{{ workordersToAssign.length }}</strong> Workorder(s) der Firma <strong>{{ massAssignModalData.companyName }}</strong> zugewiesen werden?</p>
<tt-date-picker label="Deadline" v-model="massAssignModalData.deadline" :date-range="false" sm row/>
</tt-modal>
</tt-card>
`,
data() {
return {
window, workordersToAssign: [], editingWorkorderId: null, editingDeadlineId: null, editingAdditionalInfoId: null,
civilEngineeringData: null, tempAdditionalInfo: '', companiesByTenant: {}, companiesLoading: false, massAssignCompanyId: null,
cancelWorkorderModalData: null, problemSolvedModalData: null, massAssignModalData: null,
crudConfig: {
...window.TT_CONFIG.CRUD_CONFIG, selectable: false, expandable: true,
customRowClass: (row) => {
if (['completed', 'new', 'cancelled', 'charged', 'archived'].includes(row.status)) return 'tt-rml-workorder-irrelevant';
if (['correction_requested', 'intervention_required'].includes(row.status)) return 'tt-rml-workorder-high';
const deadlineDate = moment.unix(row.deadlineDate);
if (!deadlineDate.isValid()) return 'tt-rml-workorder-irrelevant';
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: [
{
key: 'charge_workorder',
title: 'Verrechnen',
class: 'fas fa-euro-sign text-purple',
condition: (row) => row.status === 'completed'
}
]
}
}
},
computed: {
companiesForMassAssign() {
if (this.workordersToAssign.length === 0) return [];
const rows = this.$refs.table?.$refs.table.rows;
if (!rows) return[];
const firstWorkorder = rows.find(r => r.id === this.workordersToAssign[0]);
if (!firstWorkorder) return [];
return this.companiesByTenant[firstWorkorder.tenantId] || [];
}
},
methods: {
async chargeWorkorder(row) {
if (!confirm(`Soll der Auftrag #${row.id} wirklich als "verrechnet" markiert werden?`)) return;
try {
const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderAdmin/chargeWorkorder`, { workorderId: row.id });
if (data.success) {
window.notify('success', data.message);
this.$refs.table.$refs.table.refreshTable();
} else {
window.notify('error', data.message || 'Aktion fehlgeschlagen.');
}
} catch (e) {
window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.');
}
},
async acceptDocumentation(workorderId) {
const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderAdmin/acceptDocumentation`, { workorderId });
if (data.success) {
window.notify('success', data.message);
this.$refs.table.$refs.table.refreshTable();
} else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.');
},
openCivilEngineeringModal(row) {
this.getCompaniesForWorkorder(row);
this.civilEngineeringData = { workorder: row, companyId: null };
},
async assignCivilEngineering() {
if (!this.civilEngineeringData.companyId) return window.notify('error', 'Bitte eine Firma auswählen.');
const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderAdmin/setCivilEngineeringRequired`, {
workorderId: this.civilEngineeringData.workorder.id, companyId: this.civilEngineeringData.companyId
});
if (data.success) {
window.notify('success', data.message);
this.$refs.table.$refs.table.refreshTable();
this.civilEngineeringData = null;
} else window.notify('error', data.message || 'Zuweisung fehlgeschlagen.');
},
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, withTime = false) {
if (!timestamp) return '';
return window.moment.unix(timestamp).format(withTime ? 'DD.MM.YYYY HH:mm' : 'DD.MM.YYYY');
},
async getCompaniesForWorkorder(workorder) {
if (!workorder.tenantId || this.companiesByTenant[workorder.tenantId]) return;
this.companiesLoading = true;
try {
const {data} = await axios.get(`${window.TT_CONFIG.BASE_PATH}/WorkorderAdmin/getCompanies`, {params: {tenantId: workorder.tenantId}});
this.$set(this.companiesByTenant, workorder.tenantId, data);
} catch (e) {
window.notify('error', 'Firmenliste konnte nicht geladen werden.');
} finally {
this.companiesLoading = false;
}
},
async startCompanyEdit(row) {
await this.getCompaniesForWorkorder(row);
this.editingWorkorderId = row.id;
},
async assignCompany(workorder, companyId) {
if (!companyId) { this.editingWorkorderId = null; return; }
const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderAdmin/assignWorkorder`, { workorderId: workorder.id, companyId: companyId });
if (data.success) {
window.notify('success', data.message);
this.$refs.table.$refs.table.refreshTable();
} else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.');
this.editingWorkorderId = null;
},
openMassAssignModal(companyId) {
if (!companyId) return;
const companyName = this.companiesForMassAssign.find(c => c.value === companyId)?.text;
this.massAssignModalData = { companyId, companyName, deadline: null };
},
async massAssignCompanies() {
try {
const { companyId, deadline } = this.massAssignModalData;
const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderAdmin/massAssignWorkorders`, {
companyId: companyId, workorderIds: this.workordersToAssign, deadlineDate: deadline
});
if (data.success) {
window.notify('success', data.message);
this.workordersToAssign = [];
this.$refs.table.$refs.table.refreshTable();
} else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.');
} catch (e) {} finally {
this.massAssignCompanyId = null;
this.massAssignModalData = null;
}
},
async updateDeadline(workorder, newDate) {
if (!newDate) { this.editingDeadlineId = null; return; }
try {
const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderAdmin/updateDeadline`, { workorderId: workorder.id, deadlineDate: newDate });
if (data.success) {
window.notify('success', data.message);
this.$refs.table.$refs.table.refreshTable();
} else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.');
} catch (e) {} finally {
this.editingDeadlineId = null;
}
},
async setToProblemSolved() {
const { id, text } = this.problemSolvedModalData;
if (!text || !text.trim()) return window.notify('error', 'Bitte geben Sie einen Text ein.');
const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderAdmin/setToProblemSolved`, { workorderId: id, text: text });
if (data.success) {
window.notify('success', data.message);
this.$refs.table.$refs.table.refreshTable();
this.problemSolvedModalData = null;
} else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.');
},
startAdditionalInfoEdit(row) {
this.editingAdditionalInfoId = row.id;
this.tempAdditionalInfo = row.additionalInfo || '';
this.$nextTick(() => this.$refs.editTextarea?.$el.querySelector('textarea').focus());
},
cancelEdit() {
this.editingAdditionalInfoId = null;
this.tempAdditionalInfo = '';
},
async updateAdditionalInfo(row) {
if (row.additionalInfo === this.tempAdditionalInfo) { this.cancelEdit(); return; }
try {
const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderAdmin/updateAdditionalInfo`, {
workorderId: row.id, additionalInfo: this.tempAdditionalInfo
});
if (data.success) {
window.notify('success', data.message);
row.additionalInfo = data.newInfo; // Update local data
} else window.notify('error', data.message || 'Update fehlgeschlagen.');
} catch (e) {
} finally {
this.cancelEdit();
}
},
async cancelWorkorder() {
const { id, reason } = this.cancelWorkorderModalData;
const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderAdmin/cancelWorkorder`, { workorderId: id, reason: reason });
if (data.success) {
window.notify('success', data.message);
this.$refs.table.$refs.table.refreshTable();
this.cancelWorkorderModalData = null;
} else window.notify('error', data.message || 'Stornierung fehlgeschlagen.');
}
},
watch: {
workordersToAssign: {
async handler(newVal) {
if (newVal.length === 0) return;
const rows = this.$refs.table?.$refs.table.rows;
if (!rows) return;
const firstWorkorder = rows.find(r => r.id === newVal[0]);
if (!firstWorkorder) return;
const firstTenantId = firstWorkorder.tenantId;
const allSameTenant = newVal.every(id => {
const wo = rows.find(r => r.id === id);
return wo && wo.tenantId === firstTenantId;
});
if (!allSameTenant) {
window.notify('error', 'Massen-Zuweisung nur für Aufträge des gleichen Mandanten möglich.');
this.workordersToAssign.pop();
return;
}
await this.getCompaniesForWorkorder(firstWorkorder);
},
deep: true
}
}
});