Workorder mph/improve

This commit is contained in:
Luca Haid
2025-12-13 21:27:43 +00:00
parent 1435923200
commit 0755df5408
19 changed files with 658 additions and 122 deletions
@@ -4,11 +4,7 @@ Vue.component('workorder-mph-company', {
<tt-card>
<tt-table-crud ref="table" :crud-config="crudConfig">
<template v-slot:hausnummerinfo="{ row }">
<div class="small">
<div><strong>Adresse:</strong> {{ row.street }} {{ row.hausnummer }}<template v-if="row.stiege">/{{ row.stiege }}</template></div>
<div><strong>Ort:</strong> {{ row.plz }} {{ row.city }}</div>
<div><strong>Wohneinheiten:</strong> {{ row.wohneinheitCount }}</div>
</div>
<span class="small">{{ row.street }} {{ row.hausnummer }}<template v-if="row.stiege">/{{ row.stiege }}</template>, {{ row.plz }} {{ row.city }}</span>
</template>
<template v-slot:status="{ row }">
@@ -17,59 +13,71 @@ Vue.component('workorder-mph-company', {
<span class="ml-2">{{ getStatusColumn(row.status).text }}</span>
</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 v-if="!['completed', 'cancelled', 'documented'].includes(row.status)"
icon="fas fa-edit" @click="startAdditionalInfoEdit(row)"
additional-class="btn-link btn-sm p-0 ml-2" title="Notiz bearbeiten"/>
</div>
</template>
<template v-slot:deadlinedate="{ row }">{{ formatDate(row.deadlineDate) }}</template>
<template v-slot:appointmentdate="{ row }">
<div v-if="editingAppointmentId === row.id">
<tt-date-picker :value="row.appointmentDate" :date-range="false" time-picker
@input="scheduleAppointment(row, $event)" @blur="editingAppointmentId = null"
sm no-form-group/>
<div v-if="!row.appointmentDate && canSchedule(row)">
<tt-date-picker placeholder="Termin festlegen..." :date-range="false"
@input="scheduleAppointment(row, $event)" sm no-form-group
:additional-props="{ timePicker: true, timePicker24Hour: true, locale: { format: 'DD.MM.YYYY HH:mm' }, drops: 'up' }"/>
</div>
<div v-else class="d-flex align-items-center">
<div v-else-if="row.appointmentDate" class="d-flex align-items-center">
<span>{{ formatDate(row.appointmentDate, true) }}</span>
<tt-button v-if="canSchedule(row)" icon="fas fa-edit"
@click="editingAppointmentId = row.id"
additional-class="btn-link btn-sm p-0 ml-2" title="Termin planen"/>
<tt-button v-if="canSchedule(row)"
icon="fas fa-edit" @click="openRescheduleModal(row)"
additional-class="btn-link btn-sm p-0 ml-2" title="Termin ändern"/>
</div>
</template>
<template v-slot:additionalinfo="{ row }">
<span style="white-space: pre-wrap; max-width: 250px; display: inline-block;">{{ row.additionalInfo || '-' }}</span>
<span v-else></span>
</template>
<template v-slot:expandedRow="{ row }">
<div class="workorder-mph-expanded-wrapper">
<!-- Action Buttons -->
<div class="mb-3" v-if="!['completed', 'cancelled', 'documented'].includes(row.status)">
<div class="btn-group" role="group">
<tt-button v-if="row.status === 'assigned'" text="Termin planen"
@click="editingAppointmentId = row.id" icon="fas fa-calendar-plus"
additional-class="btn-primary"/>
<tt-button v-if="row.status === 'scheduled'" text="Arbeit beginnen"
@click="startWork(row)" icon="fas fa-play" additional-class="btn-success"/>
<tt-button v-if="row.status === 'scheduled'" text="Termin verschieben"
@click="openRescheduleModal(row)" icon="fas fa-calendar-alt"
additional-class="btn-warning"/>
<tt-button v-if="row.status === 'in_progress'" text="Auftrag abschließen"
@click="openCompleteModal(row)" icon="fas fa-check-double"
additional-class="btn-success"/>
<workorder-mph-data-provider :workorder-mph-id="row.id" v-slot="{ docs, journals, refresh }">
<div class="workorder-mph-expanded-wrapper">
<!-- Action Buttons -->
<div class="mb-3" v-if="!['completed', 'cancelled', 'documented'].includes(row.status)">
<div class="btn-group" role="group">
<tt-button v-if="row.status === 'scheduled'" text="Arbeit beginnen"
@click="startWork(row)" icon="fas fa-play" additional-class="btn-success"/>
<tt-button v-if="row.status === 'in_progress'" text="Auftrag abschließen"
@click="openCompleteModal(row)" icon="fas fa-check-double"
additional-class="btn-success"/>
</div>
</div>
</div>
<div class="row g-3">
<div class="col-xl-4 col-lg-6">
<checkbox-documentation :workorder-mph-id="parseInt(row.id)" :is-admin="false"/>
</div>
<div class="col-xl-8 col-lg-6">
<workorder-mph-details-manager :workorder-mph-id="row.id" :is-admin="false"
@workorder-completed="$refs.table.$refs.table.refreshTable()"/>
</div>
<div class="col-12">
<wohneinheit-status-manager :workorder-mph-id="parseInt(row.id)" :is-admin="false"
@wohneinheit-updated="checkAllWohneinheitenHaveNotes(row.id)"/>
<div class="row g-2">
<!-- Left Column (1/4): Docs Checkbox, Journal -->
<div class="col-xl-3 col-lg-4">
<div class="mph-details-stack">
<checkbox-documentation :workorder-mph-id="parseInt(row.id)" :is-admin="false" @refresh="refresh"/>
<workorder-mph-journal :journals="journals" :workorder-mph-id="row.id" :is-admin="false" @refresh="refresh"/>
</div>
</div>
<!-- Right Column (3/4): Wohneinheiten, Documents -->
<div class="col-xl-9 col-lg-8">
<div class="mph-details-stack">
<wohneinheit-status-manager :workorder-mph-id="parseInt(row.id)" :is-admin="false" @refresh="refresh"/>
<workorder-mph-documents :docs="docs" :workorder-mph-id="row.id" :is-admin="false" @refresh="refresh"/>
</div>
</div>
</div>
</div>
</div>
</workorder-mph-data-provider>
</template>
</tt-table-crud>
@@ -77,7 +85,8 @@ Vue.component('workorder-mph-company', {
title="Termin verschieben" @submit="rescheduleAppointment">
<p>Aktueller Termin: <strong>{{ formatDate(rescheduleModalData.currentDate, true) }}</strong></p>
<tt-date-picker label="Neuer Termin" v-model="rescheduleModalData.newDate"
:date-range="false" time-picker sm row required/>
:date-range="false" sm row required
:additional-props="{ timePicker: true, timePicker24Hour: true, locale: { format: 'DD.MM.YYYY HH:mm' }, singleDatePicker: true }"/>
<tt-textarea label="Grund" v-model="rescheduleModalData.reason" sm row required/>
</tt-modal>
@@ -94,7 +103,8 @@ Vue.component('workorder-mph-company', {
data() {
return {
window,
editingAppointmentId: null,
editingAdditionalInfoId: null,
tempAdditionalInfo: '',
rescheduleModalData: null,
completeModalData: null,
crudConfig: {
@@ -102,7 +112,7 @@ Vue.component('workorder-mph-company', {
selectable: false,
expandable: true,
customRowClass: (row) => {
if (['completed', 'new', 'cancelled'].includes(row.status)) return 'tt-mph-workorder-irrelevant';
if (['completed', 'new', 'cancelled', 'archived'].includes(row.status)) return 'tt-mph-workorder-irrelevant';
const deadlineDate = moment.unix(row.deadlineDate);
if (!deadlineDate.isValid()) return 'tt-mph-workorder-irrelevant';
const daysLeft = deadlineDate.diff(moment(), 'days');
@@ -123,17 +133,46 @@ Vue.component('workorder-mph-company', {
return window.moment.unix(timestamp).format(withTime ? 'DD.MM.YYYY HH:mm' : 'DD.MM.YYYY');
},
canSchedule(row) {
return ['assigned', 'scheduled'].includes(row.status);
return ['assigned', 'scheduled', 'in_progress'].includes(row.status);
},
async scheduleAppointment(row, newDate) {
if (!newDate) {
this.editingAppointmentId = null;
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}/WorkorderMphCompany/updateAdditionalInfo`, {
workorderMphId: row.id,
additionalInfo: this.tempAdditionalInfo
});
if (data.success) {
window.notify('success', data.message);
row.additionalInfo = data.newInfo;
} else {
window.notify('error', data.message || 'Update fehlgeschlagen.');
}
} catch (e) {
window.notify('error', 'Netzwerkfehler.');
} finally {
this.cancelEdit();
}
},
async scheduleAppointment(row, newDate) {
if (!newDate) return;
const hour = parseInt(moment.unix(newDate).format('H'));
if (hour >= 23 || hour < 1) {
window.notify('error', 'Bitte geben Sie eine Uhrzeit zwischen 01:00 und 22:59 an!');
this.$refs.table.$refs.table.refreshTable();
return;
}
@@ -150,8 +189,6 @@ Vue.component('workorder-mph-company', {
}
} catch (e) {
window.notify('error', 'Netzwerkfehler.');
} finally {
this.editingAppointmentId = null;
}
},
openRescheduleModal(row) {
@@ -169,8 +206,7 @@ Vue.component('workorder-mph-company', {
const hour = parseInt(moment.unix(this.rescheduleModalData.newDate).format('H'));
if (hour >= 23 || hour < 1) {
window.notify('error', 'Bitte geben Sie eine Uhrzeit zwischen 01:00 und 22:59 an!');
return;
return window.notify('error', 'Bitte geben Sie eine Uhrzeit zwischen 01:00 und 22:59 an!');
}
try {
@@ -225,10 +261,6 @@ Vue.component('workorder-mph-company', {
} catch (e) {
window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.');
}
},
async checkAllWohneinheitenHaveNotes(workorderId) {
// This is called when a wohneinheit is updated
// Could be used to enable/disable the complete button
}
}
});