Notiz column now has max 5 lines with expand/collapse animation and auto-resizing edit textarea. Moved raw SQL query to WorkorderModel::getPreorderIdsByCampaigns() to fix OOM in archiveWorkorders(). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
268 lines
14 KiB
JavaScript
268 lines
14 KiB
JavaScript
// WorkorderCompany.js
|
||
Vue.component('workorder-company', {
|
||
template: `
|
||
<div>
|
||
<tt-card>
|
||
|
||
<div v-if="/Android/.test(navigator.userAgent) && /Chrome/.test(navigator.userAgent) || /iPhone/.test(navigator.userAgent) && /Safari/.test(navigator.userAgent)" class="mb-3">
|
||
<a :href="window.TT_CONFIG.BASE_PATH + '/WorkorderCompany/Mobile'" class="btn btn-primary">
|
||
<i class="fas fa-phone-alt"></i> Mobile Ansicht
|
||
</a>
|
||
</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>Kontakt:</strong> {{ row.phone }} / {{ row.email }}</div>
|
||
<div><strong>OAID:</strong> <span class="text-pink">{{ row.oaid }}</span></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:additionalinfo="{ row }">
|
||
<div v-if="editingAdditionalInfoId === row.id" class="wo-notiz-edit">
|
||
<tt-textarea v-model="tempAdditionalInfo" @keydown.esc.native="cancelEdit" :rows="editTextareaRows" 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>
|
||
<div :class="['wo-notiz-text', expandedNotes.includes(row.id) ? 'wo-notiz-expanded' : '']">{{ row.additionalInfo || '-' }}</div>
|
||
<div class="wo-notiz-actions">
|
||
<button v-if="isNoteLong(row.additionalInfo)" class="wo-notiz-toggle" @click="toggleNoteExpand(row.id)">{{ expandedNotes.includes(row.id) ? '▴ weniger' : '▾ mehr' }}</button>
|
||
<a v-if="!['completed', 'cancelled', 'documented'].includes(row.status)"
|
||
class="wo-notiz-edit-btn" @click="startAdditionalInfoEdit(row)" title="Zusatz-Info bearbeiten"><i class="fas fa-edit"></i></a>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<template v-slot:deadlinedate="{ row }">
|
||
{{ formatDate(row.deadlineDate) }}
|
||
</template>
|
||
|
||
<template v-slot:appointmentdate="{ row }">
|
||
<div v-if="!row.appointmentDate && ['assigned', 'in_progress', 'correction_requested', 'problem_solved'].includes(row.status)">
|
||
<tt-date-picker placeholder="Termin festlegen..." :date-range="false"
|
||
@input="setAppointment(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-if="row.appointmentDate">
|
||
<span>{{ formatDate(row.appointmentDate, true) }}</span>
|
||
<tt-button v-if="!['completed', 'cancelled', 'documented'].includes(row.status)"
|
||
icon="fas fa-edit" @click="openRescheduleModal(row)"
|
||
additional-class="btn-link btn-sm p-0 ml-2" title="Termin ändern"/>
|
||
<tt-button v-if="!['completed', 'cancelled', 'documented'].includes(row.status)"
|
||
icon="fas fa-times" @click="clearAppointment(row)"
|
||
additional-class="btn-link btn-sm p-0 ml-1 text-danger" title="Termin löschen"/>
|
||
</div>
|
||
<span v-else>–</span>
|
||
</template>
|
||
|
||
<template v-slot:expandedRow="{ row }">
|
||
<civil-engineering-manager
|
||
v-if="row.status === 'civil_engineering_required'"
|
||
@workorder-completed="$refs.table.$refs.table.refreshTable()"
|
||
:is-admin="false"
|
||
:workorder-id="row.id"
|
||
/>
|
||
<workorder-details-manager
|
||
v-else
|
||
@workorder-completed="$refs.table.$refs.table.refreshTable()"
|
||
:workorder-id="row.id"
|
||
:is-admin="false"
|
||
/>
|
||
</template>
|
||
</tt-table-crud>
|
||
</tt-card>
|
||
|
||
<tt-modal v-if="rescheduleModalData" :show.sync="rescheduleModalData" title="Termin verschieben" @submit="rescheduleAppointment">
|
||
<p><strong>Auftrag:</strong> #{{ rescheduleModalData.workorder.id }}</p>
|
||
<tt-date-picker label="Neuer Termin" :date-range="false" v-model="rescheduleModalData.newDate"
|
||
:additional-props="{ timePicker: true, timePicker24Hour: true, locale: { format: 'DD.MM.YYYY HH:mm' }, singleDatePicker: true }" sm row/>
|
||
<tt-textarea label="Grund" v-model="rescheduleModalData.reason" sm row required/>
|
||
</tt-modal>
|
||
</div>
|
||
`,
|
||
data() {
|
||
return {
|
||
window,
|
||
navigator,
|
||
rescheduleModalData: null,
|
||
editingAdditionalInfoId: null,
|
||
tempAdditionalInfo: '',
|
||
expandedNotes: [],
|
||
crudConfig: {
|
||
...window.TT_CONFIG.CRUD_CONFIG,
|
||
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: {
|
||
editTextareaRows() {
|
||
const lines = (this.tempAdditionalInfo || '').split('\n').length;
|
||
return String(Math.max(4, lines + 1));
|
||
},
|
||
},
|
||
methods: {
|
||
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');
|
||
},
|
||
getCalendarType(networkOwnerName) {
|
||
if (!networkOwnerName) return '1';
|
||
const name = networkOwnerName.toLowerCase();
|
||
if (name.includes('xinon')) return '2';
|
||
if (name.includes('sbidi')) return '7';
|
||
if (name.includes('estmk')) return '3';
|
||
return '1';
|
||
},
|
||
getCampaignName(preordercampaignId) {
|
||
const col = this.crudConfig.columns.find(c => c.key === 'preordercampaign_id');
|
||
const opt = col?.table?.filterOptions?.find(o => o.value == preordercampaignId);
|
||
return opt?.text || '';
|
||
},
|
||
openCalendarWithPrefill(workorder, dateUnix, win) {
|
||
const m = window.moment.unix(dateUnix);
|
||
const zeitraum = m.hour() < 12 ? 'VM' : 'NM';
|
||
const campaignName = this.getCampaignName(workorder.preordercampaign_id);
|
||
const locationParts = [workorder.street, workorder.hausnummer];
|
||
if (workorder.stiege) locationParts.push('/' + workorder.stiege);
|
||
const location = locationParts.join(' ') + ', ' + workorder.plz + ' ' + workorder.city;
|
||
const descLines = [];
|
||
if (workorder.oaid || campaignName || workorder.city) {
|
||
descLines.push([workorder.oaid, campaignName, workorder.city].filter(Boolean).join(' - '));
|
||
}
|
||
descLines.push('Zeitraum: ' + zeitraum);
|
||
if (workorder.phone) descLines.push('Tel.: ' + workorder.phone);
|
||
if (workorder.additionalInfo) descLines.push(workorder.additionalInfo);
|
||
const calendarData = {
|
||
type: this.getCalendarType(workorder.networkOwnerName),
|
||
subject: workorder.customerCompany || workorder.customerName || '',
|
||
location: location,
|
||
cstart: m.format('DD.MM.YYYY HH:mm'),
|
||
cend: m.clone().add(90, 'minutes').format('DD.MM.YYYY HH:mm'),
|
||
description: descLines.join('<br>'),
|
||
customer_phone: workorder.phone || null,
|
||
customer_email: workorder.email || null,
|
||
calendar_user_name: 'Pusnik',
|
||
attendee_names: ['Ziga Harc'],
|
||
};
|
||
localStorage.setItem('Calendar_create', JSON.stringify(calendarData));
|
||
win.location.href = '/Calendar/View';
|
||
},
|
||
async setAppointment(workorder, date) {
|
||
if (!date) return;
|
||
if (moment.unix(date).hour() >= 23 || moment.unix(date).hour() < 1) {
|
||
this.$refs.table.$refs.table.refreshTable();
|
||
return window.notify('error', 'Bitte Uhrzeit angeben!');
|
||
}
|
||
const calWin = window.open('about:blank', '_blank');
|
||
const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/scheduleAppointment`, {
|
||
workorderId: workorder.id, appointmentDate: date
|
||
});
|
||
if (data.success) {
|
||
window.notify('success', data.message);
|
||
this.$refs.table.$refs.table.refreshTable();
|
||
this.openCalendarWithPrefill(workorder, date, calWin);
|
||
} else {
|
||
if (calWin) calWin.close();
|
||
window.notify('error', data.message || 'Ein Fehler ist aufgetreten.');
|
||
}
|
||
},
|
||
openRescheduleModal(row) {
|
||
this.rescheduleModalData = { workorder: row, newDate: row.appointmentDate, reason: '' };
|
||
},
|
||
async rescheduleAppointment() {
|
||
const { workorder, newDate, reason } = this.rescheduleModalData;
|
||
if (!newDate || !reason) return window.notify('error', 'Bitte geben Sie ein neues Datum und einen Grund an.');
|
||
if (moment.unix(newDate).hour() >= 23 || moment.unix(newDate).hour() < 1) return window.notify('error', 'Bitte Uhrzeit angeben!');
|
||
const calWin = window.open('about:blank', '_blank');
|
||
const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/rescheduleAppointment`, {
|
||
workorderId: workorder.id, appointmentDate: newDate, reason: reason
|
||
});
|
||
if (data.success) {
|
||
window.notify('success', data.message);
|
||
this.$refs.table.$refs.table.refreshTable();
|
||
this.openCalendarWithPrefill(workorder, newDate, calWin);
|
||
this.rescheduleModalData = null;
|
||
} else {
|
||
if (calWin) calWin.close();
|
||
window.notify('error', data.message || 'Ein Fehler ist aufgetreten.');
|
||
}
|
||
},
|
||
async clearAppointment(workorder) {
|
||
if (!confirm('Möchten Sie den Termin wirklich löschen?')) return;
|
||
const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/clearAppointment`, {
|
||
workorderId: workorder.id
|
||
});
|
||
if (data.success) {
|
||
window.notify('success', data.message);
|
||
this.$refs.table.$refs.table.refreshTable();
|
||
} else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.');
|
||
},
|
||
isNoteLong(text) {
|
||
if (!text) return false;
|
||
return text.split('\n').length > 4 || text.length > 180;
|
||
},
|
||
toggleNoteExpand(rowId) {
|
||
const idx = this.expandedNotes.indexOf(rowId);
|
||
if (idx === -1) this.expandedNotes.push(rowId);
|
||
else this.expandedNotes.splice(idx, 1);
|
||
},
|
||
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;
|
||
}
|
||
const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/updateAdditionalInfo`, {
|
||
workorderId: 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.');
|
||
this.cancelEdit();
|
||
},
|
||
}
|
||
});
|