added new rml features

This commit is contained in:
2025-08-12 16:26:37 +02:00
parent 8045a80b50
commit ffed33c4b2
5 changed files with 428 additions and 98 deletions

View File

@@ -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.']);
}
}

View File

@@ -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) {

View File

@@ -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'");
}
}
}

View File

@@ -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">

View File

@@ -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() {