added new rml features
This commit is contained in:
@@ -18,6 +18,7 @@ class RMLWorkorderAdminController extends TTCrud
|
||||
['value' => 'assigned', 'text' => 'Zugewiesen', 'icon' => 'fas fa-user-check text-info'],
|
||||
['value' => 'scheduled', 'text' => 'Terminiert', 'icon' => 'fas fa-calendar-check text-warning'],
|
||||
['value' => 'correction_requested', 'text' => 'Korrektur angefordert', 'icon' => 'fas fa-exclamation-triangle text-danger'],
|
||||
['value' => 'intervention_required', 'text' => 'Eingriff benötigt', 'icon' => 'fas fa-times-circle text-danger'],
|
||||
['value' => 'documented', 'text' => 'Dokumentiert', 'icon' => 'fas fa-file-alt text-success'],
|
||||
['value' => 'completed', 'text' => 'Abgeschlossen', 'icon' => 'fas fa-check-double text-secondary'],
|
||||
]]],
|
||||
@@ -214,4 +215,49 @@ class RMLWorkorderAdminController extends TTCrud
|
||||
]);
|
||||
}
|
||||
|
||||
protected function updateDeadlineAction() {
|
||||
$post = json_decode(file_get_contents('php://input'), true);
|
||||
if (empty($post['workorderId']) || empty($post['deadlineDate'])) self::sendError("Required fields are missing.");
|
||||
|
||||
$workorder = RMLWorkorderModel::get($post['workorderId']);
|
||||
if (!$workorder) self::sendError("Workorder not found.");
|
||||
|
||||
$workorder->deadlineDate = $post['deadlineDate'];
|
||||
RMLWorkorderModel::update((array)$workorder);
|
||||
|
||||
RMLWorkorderJournalModel::create([
|
||||
'workorderId' => $workorder->id,
|
||||
'text' => 'Deadline wurde auf ' . date('d.m.Y', $post['deadlineDate']) . ' geändert.',
|
||||
'create' => time(),
|
||||
'createBy' => $this->user->id,
|
||||
]);
|
||||
|
||||
self::returnJson(['success' => true, 'message' => 'Deadline erfolgreich aktualisiert.']);
|
||||
}
|
||||
|
||||
protected function acceptDocumentationAction() {
|
||||
$post = json_decode(file_get_contents('php://input'), true);
|
||||
if (empty($post['workorderId'])) self::sendError("Workorder ID is missing.");
|
||||
|
||||
$workorder = RMLWorkorderModel::get($post['workorderId']);
|
||||
if (!$workorder) self::sendError("Workorder not found.");
|
||||
|
||||
if ($workorder->status !== 'documented') {
|
||||
self::sendError("Die Dokumentation muss zuerst von der Firma als fertig markiert werden.");
|
||||
}
|
||||
|
||||
$oldStatus = $workorder->status;
|
||||
$workorder->status = 'completed';
|
||||
RMLWorkorderModel::update((array)$workorder);
|
||||
|
||||
RMLWorkorderJournalModel::create([
|
||||
'workorderId' => $workorder->id,
|
||||
'text' => 'Dokumentation wurde akzeptiert und der Auftrag abgeschlossen.',
|
||||
'statusChange' => "$oldStatus -> completed",
|
||||
'create' => time(),
|
||||
'createBy' => $this->user->id,
|
||||
]);
|
||||
|
||||
self::returnJson(['success' => true, 'message' => 'Dokumentation akzeptiert und Auftrag abgeschlossen.']);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ class RMLWorkorderCompanyController extends TTCrud
|
||||
['value' => 'assigned', 'text' => 'Zugewiesen', 'icon' => 'fas fa-user-check text-info'],
|
||||
['value' => 'scheduled', 'text' => 'Terminiert', 'icon' => 'fas fa-calendar-check text-warning'],
|
||||
['value' => 'correction_requested', 'text' => 'Korrektur angefordert', 'icon' => 'fas fa-exclamation-triangle text-danger'],
|
||||
['value' => 'intervention_required', 'text' => 'Eingriff benötigt', 'icon' => 'fas fa-times-circle text-danger'],
|
||||
['value' => 'documented', 'text' => 'Dokumentiert', 'icon' => 'fas fa-file-alt text-success'],
|
||||
['value' => 'completed', 'text' => 'Abgeschlossen', 'icon' => 'fas fa-check-double text-secondary'],
|
||||
]]],
|
||||
@@ -29,7 +30,8 @@ class RMLWorkorderCompanyController extends TTCrud
|
||||
if ($company) {
|
||||
$this->additionalJSVariables['COMPANY_ID'] = $company->id;
|
||||
} else {
|
||||
$this->sendError('Access Denied. You are not associated with a registered RML company.', 403);
|
||||
// Allow access but show no data if not associated
|
||||
$this->additionalJSVariables['COMPANY_ID'] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,34 +51,19 @@ class RMLWorkorderCompanyController extends TTCrud
|
||||
$filters = $json['filters'] ?? [];
|
||||
$order = $json['order'] ?? ['key' => 'deadlineDate', 'order' => 'ASC'];
|
||||
|
||||
$company = RMLWorkorderCompanyModel::getFirst(['addressId' => $this->user->address_id]);
|
||||
if (!$company) {
|
||||
self::sendError("Company not found for user.", 403);
|
||||
$companyId = $this->user->address_id;
|
||||
if ($companyId === 0) {
|
||||
self::returnJson(['rows' => [], 'pagination' => array_merge($pagination, ['total_rows' => 0, 'total_pages' => 0, 'filtered_available' => 0])]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get paginated workorders and total count from the new model methods
|
||||
$workorders = RMLWorkorderModel::getCompanyWorkorders($filters, $pagination['per_page'], ($pagination['page'] - 1) * $pagination['per_page'], $order, $company->id);
|
||||
$totalCount = RMLWorkorderModel::countCompanyWorkorders($filters, $company->id);
|
||||
$workorders = RMLWorkorderModel::getCompanyWorkorders($filters, $pagination['per_page'], ($pagination['page'] - 1) * $pagination['per_page'], $order, $companyId);
|
||||
$totalCount = RMLWorkorderModel::countCompanyWorkorders($filters, $companyId);
|
||||
|
||||
// Format rows for the frontend
|
||||
$rows = array_map(function($workorder) {
|
||||
$row = (array)$workorder;
|
||||
|
||||
$anschlussadresse = "{$row['street']} {$row['hausnummer']}";
|
||||
if ($row['stiege']) $anschlussadresse .= "/{$row['stiege']}";
|
||||
if ($row['apartment']) $anschlussadresse .= " / WE: {$row['apartment']}";
|
||||
$anschlussadresse .= ", {$row['plz']} {$row['city']}";
|
||||
|
||||
$kunde = $row['customerCompany'] ?: $row['customerName'];
|
||||
|
||||
$row['preorderInfo'] = "<strong>Kunde:</strong> {$kunde}<br>" .
|
||||
"<strong>Anschluss:</strong> {$anschlussadresse}<br>" .
|
||||
"<strong>Kontakt:</strong> {$row['phone']} / {$row['email']}<br>" .
|
||||
"<strong>OAID:</strong> <span class='text-pink'>{$row['oaid']}</span>";
|
||||
|
||||
// Clean up unnecessary fields before sending
|
||||
$row['preorderInfo'] = $this->getPreorderInfoTextByData($row);
|
||||
unset($row['customerName'], $row['customerCompany'], $row['street'], $row['hausnummer'], $row['stiege'], $row['oaid'], $row['apartment'], $row['plz'], $row['city'], $row['phone'], $row['email']);
|
||||
|
||||
return $row;
|
||||
}, $workorders);
|
||||
|
||||
@@ -91,6 +78,21 @@ class RMLWorkorderCompanyController extends TTCrud
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
private function getPreorderInfoTextByData($data) {
|
||||
$anschlussadresse = "{$data['street']} {$data['hausnummer']}";
|
||||
if ($data['stiege']) $anschlussadresse .= "/{$data['stiege']}";
|
||||
if ($data['apartment']) $anschlussadresse .= " / WE: {$data['apartment']}";
|
||||
$anschlussadresse .= ", {$data['plz']} {$data['city']}";
|
||||
|
||||
$kunde = $data['customerCompany'] ?: $data['customerName'];
|
||||
|
||||
return "<strong>Kunde:</strong> {$kunde}<br>" .
|
||||
"<strong>Anschluss:</strong> {$anschlussadresse}<br>" .
|
||||
"<strong>Kontakt:</strong> {$data['phone']} / {$data['email']}<br>" .
|
||||
"<strong>OAID:</strong> <span class='text-pink'>{$data['oaid']}</span>";
|
||||
}
|
||||
|
||||
public function getWorkorderByIdAction() {
|
||||
$id = $this->request->id;
|
||||
if(!$id) self::sendError("ID missing");
|
||||
@@ -129,13 +131,80 @@ class RMLWorkorderCompanyController extends TTCrud
|
||||
$workorder = RMLWorkorderModel::get($post['workorderId']);
|
||||
if(!$workorder) self::sendError("Workorder not found");
|
||||
|
||||
$hour = (int)date('H', $post['appointmentDate']);
|
||||
if ($hour >= 23 || $hour < 1) {
|
||||
self::sendError("Bitte Uhrzeit angeben!");
|
||||
}
|
||||
|
||||
$workorder->appointmentDate = $post['appointmentDate'];
|
||||
$workorder->status = 'scheduled';
|
||||
RMLWorkorderModel::update((array)$workorder);
|
||||
|
||||
RMLWorkorderJournalModel::create([
|
||||
'workorderId' => $workorder->id,
|
||||
'text' => 'Termin festgelegt auf: ' . date('d.m.Y H:i', $post['appointmentDate']),
|
||||
'create' => time(),
|
||||
'createBy' => $this->user->id,
|
||||
]);
|
||||
|
||||
self::returnJson(['success' => true, 'message' => 'Termin erfolgreich gespeichert.']);
|
||||
}
|
||||
|
||||
protected function rescheduleAppointmentAction() {
|
||||
$post = json_decode(file_get_contents('php://input'), true);
|
||||
if (empty($post['workorderId']) || empty($post['appointmentDate']) || empty($post['reason'])) {
|
||||
self::sendError("Required fields are missing.");
|
||||
}
|
||||
|
||||
$workorder = RMLWorkorderModel::get($post['workorderId']);
|
||||
if(!$workorder) self::sendError("Workorder not found.");
|
||||
|
||||
$hour = (int)date('H', $post['appointmentDate']);
|
||||
if ($hour >= 23 || $hour < 1) {
|
||||
self::sendError("Bitte Uhrzeit angeben!");
|
||||
}
|
||||
|
||||
$oldDateFormatted = $workorder->appointmentDate ? date('d.m.Y H:i', $workorder->appointmentDate) : 'N/A';
|
||||
$newDateFormatted = date('d.m.Y H:i', $post['appointmentDate']);
|
||||
|
||||
$workorder->appointmentDate = $post['appointmentDate'];
|
||||
RMLWorkorderModel::update((array)$workorder);
|
||||
|
||||
RMLWorkorderJournalModel::create([
|
||||
'workorderId' => $workorder->id,
|
||||
'text' => "Termin verschoben von {$oldDateFormatted} auf {$newDateFormatted}. Grund: " . $post['reason'],
|
||||
'create' => time(),
|
||||
'createBy' => $this->user->id,
|
||||
]);
|
||||
|
||||
self::returnJson(['success' => true, 'message' => 'Termin erfolgreich verschoben.']);
|
||||
}
|
||||
|
||||
protected function requestInterventionAction() {
|
||||
$post = json_decode(file_get_contents('php://input'), true);
|
||||
if (empty($post['workorderId']) || empty($post['journalText'])) {
|
||||
self::sendError("Required fields are missing.");
|
||||
}
|
||||
|
||||
$workorder = RMLWorkorderModel::get($post['workorderId']);
|
||||
if(!$workorder) self::sendError("Workorder not found.");
|
||||
|
||||
$oldStatus = $workorder->status;
|
||||
$workorder->status = 'intervention_required';
|
||||
RMLWorkorderModel::update((array)$workorder);
|
||||
|
||||
RMLWorkorderJournalModel::create([
|
||||
'workorderId' => $workorder->id,
|
||||
'text' => "Eingriff benötigt: " . $post['journalText'],
|
||||
'statusChange' => "$oldStatus -> intervention_required",
|
||||
'create' => time(),
|
||||
'createBy' => $this->user->id,
|
||||
]);
|
||||
|
||||
self::returnJson(['success' => true, 'message' => 'Eingriff wurde angefordert.']);
|
||||
}
|
||||
|
||||
|
||||
protected function uploadDocumentationAction()
|
||||
{
|
||||
if (empty($_FILES['files']) || empty($_POST['workorderId'])) {
|
||||
@@ -199,11 +268,12 @@ class RMLWorkorderCompanyController extends TTCrud
|
||||
$typeCounts = [];
|
||||
|
||||
$translationMap = [
|
||||
'photo_before' => 'Foto vorher',
|
||||
'photo_during' => 'Foto währenddessen',
|
||||
'photo_after' => 'Foto nachher',
|
||||
'measurement_protocol' => 'Messprotokoll',
|
||||
'customer_signature' => 'Kundenunterschrift',
|
||||
'photo_hup_mounted' => 'Foto_montierter_HÜP',
|
||||
'photo_hup_open' => 'Foto_offener_HÜP',
|
||||
'photo_splice_cassette' => 'Foto_Spleißkassette',
|
||||
'photo_hup_closed_stickers' => 'Foto_geschlossener_HÜP_mit_Aufklebern',
|
||||
'photo_fcp_labeled' => 'Foto_FCP_beschriftet',
|
||||
'measurement_protocol_otdr' => 'ODTR_Messung',
|
||||
];
|
||||
|
||||
foreach($docs as $doc) {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class RmlworkorderUpdateStatusEnum extends AbstractMigration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if ($this->getEnvironment() == 'thetool') {
|
||||
$this->execute("ALTER TABLE `RMLWorkorder` MODIFY `status` enum('new','assigned','scheduled','correction_requested','documented','completed','intervention_required') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT 'new'");
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if ($this->getEnvironment() == 'thetool') {
|
||||
$this->execute("ALTER TABLE `RMLWorkorder` MODIFY `status` enum('new','assigned','scheduled','documented','completed') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT 'new'");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,10 +103,28 @@ Vue.component('r-m-l-workorder-admin', {
|
||||
</template>
|
||||
|
||||
<template v-slot:deadlinedate="{ row }">
|
||||
{{ formatDate(row.deadlineDate) }}
|
||||
<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">
|
||||
bis zum Termin: {{ row.daysUntilDeadline }} Tag{{ row.daysUntilDeadline !== 1 ? 'e' : '' }}
|
||||
ü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 }">
|
||||
@@ -117,6 +135,7 @@ Vue.component('r-m-l-workorder-admin', {
|
||||
<rml-documentation-viewer-admin
|
||||
:workorder-id="row.id"
|
||||
@workorder-updated="$refs.table.$refs.table.refreshTable()"
|
||||
@accept-documentation="acceptDocumentation"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -128,6 +147,7 @@ Vue.component('r-m-l-workorder-admin', {
|
||||
window,
|
||||
workordersToAssign: [],
|
||||
editingWorkorderId: null,
|
||||
editingDeadlineId: null,
|
||||
companies: [],
|
||||
massAssignCompanyId: null,
|
||||
massAssignLoading: false,
|
||||
@@ -141,8 +161,7 @@ Vue.component('r-m-l-workorder-admin', {
|
||||
if (['completed', 'new'].includes(row.status) || !deadlineDate.isValid()) {
|
||||
return 'tt-rml-workorder-irrelevant';
|
||||
}
|
||||
// if status is correction_requested, return tt-rml-workorder-high
|
||||
if (row.status === 'correction_requested') {
|
||||
if (['correction_requested', 'intervention_required'].includes(row.status)) {
|
||||
return 'tt-rml-workorder-high';
|
||||
}
|
||||
|
||||
@@ -231,6 +250,43 @@ Vue.component('r-m-l-workorder-admin', {
|
||||
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.');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -273,7 +329,6 @@ Vue.component('rml-documentation-viewer-admin', {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- add a new card with a green button Dokumentation akzeptieren-->
|
||||
<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">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// RMLWorkorderCompany.js
|
||||
Vue.component('r-m-l-workorder-company', {
|
||||
template: `
|
||||
<div>
|
||||
<tt-card>
|
||||
<tt-table-crud
|
||||
ref="table"
|
||||
@@ -28,9 +29,19 @@ Vue.component('r-m-l-workorder-company', {
|
||||
@input="setAppointment(row, $event)"
|
||||
sm
|
||||
no-form-group
|
||||
:additional-props="{ timePicker: true, timePicker24Hour: true, locale: { format: 'DD.MM.YYYY HH:mm' }, drops: 'up' }"
|
||||
/>
|
||||
</div>
|
||||
<span v-else>{{ formatDate(row.appointmentDate) }}</span>
|
||||
<div v-else-if="row.appointmentDate">
|
||||
<span>{{ formatDate(row.appointmentDate, true) }}</span>
|
||||
<tt-button
|
||||
icon="fas fa-edit"
|
||||
@click="openRescheduleModal(row)"
|
||||
additional-class="btn-link btn-sm p-0 ml-2"
|
||||
title="Termin ändern"
|
||||
/>
|
||||
</div>
|
||||
<span v-else>–</span>
|
||||
</template>
|
||||
|
||||
<template v-slot:expandedRow="{ row }">
|
||||
@@ -42,9 +53,24 @@ Vue.component('r-m-l-workorder-company', {
|
||||
|
||||
</tt-table-crud>
|
||||
</tt-card>
|
||||
|
||||
<tt-modal v-if="rescheduleData" :show="true" title="Termin verschieben" @close="closeRescheduleModal" @submit="rescheduleAppointment">
|
||||
<p><strong>Auftrag:</strong> #{{ rescheduleData.workorder.id }}</p>
|
||||
<tt-date-picker
|
||||
label="Neuer Termin"
|
||||
:date-range="false"
|
||||
v-model="rescheduleData.newDate"
|
||||
sm
|
||||
row
|
||||
:additional-props="{ timePicker: true, timePicker24Hour: true, locale: { format: 'DD.MM.YYYY HH:mm' }, singleDatePicker: true }"
|
||||
/>
|
||||
<tt-textarea label="Grund" v-model="rescheduleData.reason" sm row required/>
|
||||
</tt-modal>
|
||||
</div>
|
||||
`,
|
||||
data() {
|
||||
return {
|
||||
rescheduleData: null,
|
||||
crudConfig: {
|
||||
...window.TT_CONFIG.CRUD_CONFIG,
|
||||
expandable: true,
|
||||
@@ -55,6 +81,10 @@ Vue.component('r-m-l-workorder-company', {
|
||||
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';
|
||||
@@ -71,12 +101,20 @@ Vue.component('r-m-l-workorder-company', {
|
||||
const column = this.crudConfig.columns.find(c => c.key === 'status');
|
||||
return column.table.filterOptions.find(opt => opt.value === status) || {};
|
||||
},
|
||||
formatDate(timestamp) {
|
||||
formatDate(timestamp, withTime = false) {
|
||||
if (!timestamp) return '–';
|
||||
return window.moment.unix(timestamp).format('DD.MM.YYYY');
|
||||
const format = withTime ? 'DD.MM.YYYY HH:mm' : 'DD.MM.YYYY';
|
||||
return window.moment.unix(timestamp).format(format);
|
||||
},
|
||||
async setAppointment(workorder, date) {
|
||||
if (!date) return;
|
||||
|
||||
const hour = moment.unix(date).hour();
|
||||
if (hour >= 23 || hour < 1) {
|
||||
this.$refs.table.$refs.table.refreshTable(); // Re-render to clear invalid date from picker
|
||||
return window.notify('error', 'Bitte Uhrzeit angeben!');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/scheduleAppointment`, {
|
||||
workorderId: workorder.id,
|
||||
@@ -89,7 +127,44 @@ Vue.component('r-m-l-workorder-company', {
|
||||
window.notify('error', response.data.message || 'Ein Fehler ist aufgetreten.');
|
||||
}
|
||||
} catch (e) {
|
||||
window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.');
|
||||
window.notify('error', e.response?.data?.message || 'Ein Netzwerkfehler ist aufgetreten.');
|
||||
}
|
||||
},
|
||||
openRescheduleModal(row) {
|
||||
this.rescheduleData = {
|
||||
workorder: row,
|
||||
newDate: row.appointmentDate,
|
||||
reason: ''
|
||||
};
|
||||
},
|
||||
closeRescheduleModal() {
|
||||
this.rescheduleData = null;
|
||||
},
|
||||
async rescheduleAppointment() {
|
||||
const { workorder, newDate, reason } = this.rescheduleData;
|
||||
if (!newDate || !reason) {
|
||||
return window.notify('error', 'Bitte geben Sie ein neues Datum und einen Grund an.');
|
||||
}
|
||||
const hour = moment.unix(newDate).hour();
|
||||
if (hour >= 23 || hour < 1) {
|
||||
return window.notify('error', 'Bitte Uhrzeit angeben!');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/rescheduleAppointment`, {
|
||||
workorderId: workorder.id,
|
||||
appointmentDate: newDate,
|
||||
reason: reason
|
||||
});
|
||||
if (response.data.success) {
|
||||
window.notify('success', response.data.message);
|
||||
this.$refs.table.$refs.table.refreshTable();
|
||||
this.closeRescheduleModal();
|
||||
} else {
|
||||
window.notify('error', response.data.message || 'Ein Fehler ist aufgetreten.');
|
||||
}
|
||||
} catch (e) {
|
||||
window.notify('error', e.response?.data?.message || 'Ein Netzwerkfehler ist aufgetreten.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,31 +210,45 @@ Vue.component('documentation-manager', {
|
||||
<tt-button
|
||||
text="Auftrag abschließen"
|
||||
@click="completeWorkorder"
|
||||
:disabled="!canComplete || workorder.status === 'completed'"
|
||||
:disabled="!canComplete || workorder.status === 'documented' || workorder.status === 'completed'"
|
||||
:loading="completing"
|
||||
additional-class="btn-success w-100"
|
||||
icon="fas fa-check-double"
|
||||
/>
|
||||
<small v-if="!canComplete" class="form-text text-muted text-center mt-2">
|
||||
<small v-if="!canComplete && workorder.status !== 'documented' && workorder.status !== 'completed'" class="form-text text-muted text-center mt-2">
|
||||
Bitte laden Sie alle benötigten Dokumente hoch, um den Auftrag abzuschließen.
|
||||
</small>
|
||||
<div v-if="workorder.status === 'completed'" class="alert alert-secondary text-center mt-2 p-2">
|
||||
Auftrag bereits abgeschlossen.
|
||||
<div v-if="workorder.status === 'documented' || workorder.status === 'completed'" class="alert alert-secondary text-center mt-2 p-2">
|
||||
Auftrag zur Prüfung eingereicht.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3" v-if="['assigned', 'scheduled', 'correction_requested'].includes(workorder.status)">
|
||||
<div class="card-header bg-danger text-white"><h5><i class="fas fa-hard-hat mr-2"></i>Eingriff benötigt</h5></div>
|
||||
<div class="card-body">
|
||||
<p class="small text-muted">Falls ein Problem auftritt, das ein Eingreifen erfordert, melden Sie es hier.</p>
|
||||
<tt-button
|
||||
text="Problem melden"
|
||||
@click="openInterventionModal"
|
||||
additional-class="btn-danger w-100"
|
||||
icon="fas fa-exclamation-triangle"
|
||||
/>
|
||||
</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 class="list-group list-group-flush">
|
||||
<li v-if="journals.length === 0" class="list-group-item text-center text-muted">Keine Einträge vorhanden.</li>
|
||||
<li v-for="log in journals" :key="log.id" class="list-group-item small" :class="{'list-group-item-danger': log.statusChange && log.statusChange.includes('correction_requested')}">
|
||||
<li v-for="log in journals" :key="log.id" class="list-group-item small" :class="{'list-group-item-danger': log.statusChange && (log.statusChange.includes('correction_requested') || log.statusChange.includes('intervention_required'))}">
|
||||
<strong>{{ formatDate(log.create) }} ({{ log.createByName }}):</strong>
|
||||
<div class="text-muted" style="white-space: pre-wrap;">{{ log.text }}</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card-footer" v-if="workorder.status !== 'completed'">
|
||||
<div class="card-footer" v-if="workorder.status !== 'completed' && workorder.status !== 'documented'">
|
||||
<tt-textarea v-model="newJournalMessage" placeholder="Ihre Nachricht oder Anmerkung..." rows="2" />
|
||||
<tt-button
|
||||
text="Eintrag speichern"
|
||||
@@ -174,7 +263,7 @@ Vue.component('documentation-manager', {
|
||||
</div>
|
||||
|
||||
<div class="col-lg-8">
|
||||
<div class="card mb-3" v-if="workorder.status !== 'completed'">
|
||||
<div class="card mb-3" v-if="workorder.status !== 'documented' && workorder.status !== 'completed'">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Neues Dokument hochladen</h5>
|
||||
<tt-select label="Dokumententyp" :options="requiredDocTypes" v-model="uploadData.documentType" sm row />
|
||||
@@ -191,8 +280,8 @@ Vue.component('documentation-manager', {
|
||||
|
||||
<tt-file-gallery
|
||||
:files="filesWithStatus"
|
||||
:edit-mode="workorder.status !== 'completed'"
|
||||
:delete-mode="workorder.status !== 'completed'"
|
||||
:edit-mode="workorder.status !== 'completed' && workorder.status !== 'documented'"
|
||||
:delete-mode="workorder.status !== 'completed' && workorder.status !== 'documented'"
|
||||
@delete-file="deleteDocumentation"
|
||||
@update-file="updateDocumentation"
|
||||
>
|
||||
@@ -207,6 +296,18 @@ Vue.component('documentation-manager', {
|
||||
</tt-file-gallery>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<tt-modal v-if="interventionData" :show="true" title="Eingriff anfordern" @close="interventionData = null" @submit="requestIntervention">
|
||||
<tt-select
|
||||
label="Art des Problems"
|
||||
:options="[{value: 'stuck', text: 'Ab X Laufmeter stecken geblieben'}, {value: 'no_air', text: 'Keine Luftverbindung'}, {value: 'other', text: 'Sonstiges'}]"
|
||||
v-model="interventionData.type"
|
||||
sm
|
||||
row
|
||||
/>
|
||||
<tt-input v-if="interventionData.type === 'stuck'" label="Distanz (Meter)" type="number" v-model="interventionData.distance" sm row required />
|
||||
<tt-textarea v-if="interventionData.type === 'other'" label="Grund" v-model="interventionData.otherReason" sm row required />
|
||||
</tt-modal>
|
||||
</div>
|
||||
`,
|
||||
data() {
|
||||
@@ -219,17 +320,19 @@ Vue.component('documentation-manager', {
|
||||
journals: [],
|
||||
newJournalMessage: '',
|
||||
addingJournalEntry: false,
|
||||
interventionData: null,
|
||||
uploadData: {
|
||||
files: [],
|
||||
documentType: 'photo_before',
|
||||
documentType: 'photo_hup_mounted',
|
||||
description: ''
|
||||
},
|
||||
requiredDocTypes: [
|
||||
{ value: 'photo_before', text: 'Foto: Zustand vorher' },
|
||||
{ value: 'photo_during', text: 'Foto: Während der Arbeit' },
|
||||
{ value: 'photo_after', text: 'Foto: Zustand nachher' },
|
||||
{ value: 'measurement_protocol', text: 'Messprotokoll (z.B. OTDR)' },
|
||||
{ value: 'customer_signature', text: 'Unterschriebenes Arbeitsprotokoll' },
|
||||
{ value: 'photo_hup_mounted', text: 'Foto vom montierten HÜP' },
|
||||
{ value: 'photo_hup_open', text: 'Foto von dem offen HÜP' },
|
||||
{ value: 'photo_splice_cassette', text: 'Foto der Spleißkassette' },
|
||||
{ value: 'photo_hup_closed_stickers', text: 'Foto vom geschlossenen HÜP mit allen Aufklebern' },
|
||||
{ value: 'photo_fcp_labeled', text: 'Foto vom FCP beschriftet' },
|
||||
{ value: 'measurement_protocol_otdr', text: 'ODTR – Messung (1310nm & 1510nm)' },
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -326,7 +429,7 @@ Vue.component('documentation-manager', {
|
||||
this.uploading = false;
|
||||
},
|
||||
async completeWorkorder() {
|
||||
if(!confirm('Möchten Sie diesen Auftrag wirklich abschließen? Diese Aktion kann nicht rückgängig gemacht werden.')) return;
|
||||
if(!confirm('Möchten Sie diesen Auftrag wirklich abschließen und zur Prüfung einreichen?')) return;
|
||||
this.completing = true;
|
||||
try {
|
||||
const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/completeWorkorder`, { workorderId: this.workorder.id });
|
||||
@@ -390,6 +493,41 @@ Vue.component('documentation-manager', {
|
||||
} finally {
|
||||
this.addingJournalEntry = false;
|
||||
}
|
||||
},
|
||||
openInterventionModal() {
|
||||
this.interventionData = { type: 'stuck', distance: '', otherReason: '' };
|
||||
},
|
||||
async requestIntervention() {
|
||||
let journalText = '';
|
||||
const { type, distance, otherReason } = this.interventionData;
|
||||
|
||||
if (type === 'stuck') {
|
||||
if (!distance || isNaN(distance)) return window.notify('error', 'Bitte eine gültige Distanz eingeben.');
|
||||
journalText = `Ab ${distance} Laufmeter stecken geblieben.`;
|
||||
} else if (type === 'no_air') {
|
||||
journalText = 'Keine Luftverbindung.';
|
||||
} else if (type === 'other') {
|
||||
if (!otherReason.trim()) return window.notify('error', 'Bitte geben Sie einen Grund an.');
|
||||
journalText = otherReason.trim();
|
||||
} else {
|
||||
return window.notify('error', 'Ungültiger Problemtyp.');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/requestIntervention`, {
|
||||
workorderId: this.workorderId,
|
||||
journalText: journalText
|
||||
});
|
||||
if (response.data.success) {
|
||||
window.notify('success', response.data.message);
|
||||
this.interventionData = null;
|
||||
this.$emit('workorder-completed'); // This just refreshes the table
|
||||
} else {
|
||||
window.notify('error', response.data.message || 'Ein Fehler ist aufgetreten.');
|
||||
}
|
||||
} catch (e) {
|
||||
window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.');
|
||||
}
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
|
||||
Reference in New Issue
Block a user