310 lines
18 KiB
JavaScript
310 lines
18 KiB
JavaScript
// 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" />
|
||
</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"/>
|
||
<workorder-details-manager v-else :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'].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: []
|
||
}
|
||
}
|
||
},
|
||
computed: {
|
||
companiesForMassAssign() {
|
||
if (this.workordersToAssign.length === 0) return [];
|
||
return Object.values(this.companiesByTenant)[0] || [];
|
||
}
|
||
},
|
||
methods: {
|
||
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
|
||
}
|
||
}
|
||
});
|