added new rml stuff

This commit is contained in:
Luca Haid
2025-11-03 12:59:00 +01:00
parent 37be99b284
commit 887552a734
7 changed files with 306 additions and 20 deletions

View File

@@ -115,6 +115,7 @@
const isSettingsOpen = ref(false);
const theme = ref('system'); // 'light', 'dark', 'system'
const showThemePicker = ref(false);
const savingData = ref(false); // <-- ADDED
const API_BASE_URL = window.TT_CONFIG.BASE_PATH || '/WorkorderCompany';
@@ -211,9 +212,24 @@
});
});
// MODIFIED
const isChecklistComplete = computed(() => {
if (checklist.value.length === 0) return true;
return checklist.value.every(item => item.completed);
// Check documents
if (checklist.value.length > 0) {
if (!checklist.value.every(item => item.completed)) {
return false;
}
}
// Check new fields
if (tenantConfig.value?.requireCableLength && (!selectedWorkorder.value.cableLength || !selectedWorkorder.value.cableLength.trim())) {
return false;
}
if (tenantConfig.value?.requireCableType && (!selectedWorkorder.value.cableType || !selectedWorkorder.value.cableType.trim())) {
return false;
}
return true; // All checks passed
});
const translatedDocs = computed(() => {
@@ -291,7 +307,7 @@
documentation.docs = docRes.data.docs.map(d => ({...d, isPdf: d.mimetype === 'application/pdf'}));
documentation.journals = docRes.data.journals;
if (configRes.data.success) {
tenantConfig.value = configRes.data;
tenantConfig.value = configRes.data; // <-- MODIFIED: This will now contain all flags
}
} catch (e) { console.error("Could not load details", e); }
finally { isDetailsLoading.value = false; }
@@ -329,6 +345,36 @@
finally { isEditingInfo.value = false; }
};
// START ADDED
const saveWorkorderData = async () => {
savingData.value = true;
try {
const response = await api.post('/updateWorkorderData', {
workorderId: selectedWorkorder.value.id,
cableLength: selectedWorkorder.value.cableLength,
cableType: selectedWorkorder.value.cableType
});
if (response.data.success) {
alert('Daten gespeichert.'); // PWA uses alert()
documentation.journals = response.data.journals; // Update journal
// Also update the main list item
const woInList = workorders.value.find(w => w.id === selectedWorkorder.value.id);
if (woInList) {
woInList.cableLength = selectedWorkorder.value.cableLength;
woInList.cableType = selectedWorkorder.value.cableType;
}
} else {
alert(response.data.message || 'Speichern fehlgeschlagen.');
}
} catch (e) {
console.error("Failed to save data", e);
alert('Ein Netzwerkfehler ist aufgetreten.');
} finally {
savingData.value = false;
}
};
// END ADDED
const addJournalEntry = async () => {
if (!newJournalEntry.value.trim()) return;
try {
@@ -401,13 +447,23 @@
finally { problemModal.show = false; problemModal.selectedInterventions = []; problemModal.details = {}; }
};
// MODIFIED
const handleCompleteClick = () => {
if (isChecklistComplete.value) {
if (confirm("Möchten Sie diesen Auftrag wirklich abschließen?")) {
completeWorkorder();
}
} else {
missingTasksPopover.tasks = checklist.value.filter(t => !t.completed).map(t => t.text);
const missingDocs = checklist.value.filter(t => !t.completed).map(t => t.text);
const missingData = [];
if (tenantConfig.value?.requireCableLength && (!selectedWorkorder.value.cableLength || !selectedWorkorder.value.cableLength.trim())) {
missingData.push("Kabellänge");
}
if (tenantConfig.value?.requireCableType && (!selectedWorkorder.value.cableType || !selectedWorkorder.value.cableType.trim())) {
missingData.push("Kabeltyp");
}
missingTasksPopover.tasks = [...missingDocs, ...missingData];
missingTasksPopover.show = true;
setTimeout(() => missingTasksPopover.show = false, 4000);
}
@@ -415,10 +471,18 @@
const completeWorkorder = async () => {
try {
await api.post('/completeWorkorder', { workorderId: selectedWorkorder.value.id });
await fetchWorkorders();
closeDetails();
} catch(e) { console.error("Failed to complete workorder", e); }
// Server-side validation will catch errors if client-side check fails
const response = await api.post('/completeWorkorder', { workorderId: selectedWorkorder.value.id });
if (response.data.success) {
await fetchWorkorders();
closeDetails();
} else {
alert(response.data.message); // Show validation error from server
}
} catch(e) {
console.error("Failed to complete workorder", e);
alert(e.response?.data?.message || 'Fehler beim Abschließen.');
}
};
const selectFcp = (fcpValue) => {
@@ -456,8 +520,10 @@
checklist, fullscreenViewer, missingTasksPopover, translatedDocs, filteredJournals, installModal, isStandalone,
selectedFcp, isFcpSelectOpen, fcpOptions, selectedFcpText, fcpSearchTerm, filteredFcpOptions, fcpInputRef,
isSettingsOpen, theme, showThemePicker,
savingData, // <-- ADDED
fetchWorkorders, openDetails, closeDetails, getStatusInfo, formatDate, googleMapsLink, startEditInfo, saveAdditionalInfo,
handleFileSelect, executeUpload, addJournalEntry, submitProblem, handleCompleteClick, selectFcp, setTheme
handleFileSelect, executeUpload, addJournalEntry, submitProblem, handleCompleteClick, selectFcp, setTheme,
saveWorkorderData // <-- ADDED
};
},
template: `
@@ -593,6 +659,20 @@
<p v-else class="text-sm whitespace-pre-wrap text-slate-800 dark:text-slate-200">{{ selectedWorkorder.additionalInfo || 'Keine Notiz.' }}</p>
</div>
<div v-if="tenantConfig && (tenantConfig.requireCableLength || tenantConfig.requireCableType)" class="bg-white dark:bg-slate-900 p-4 rounded-lg border border-slate-200 dark:border-slate-800 space-y-3 text-sm">
<h3 class="font-bold text-slate-700 dark:text-secondary">Zusatzdaten</h3>
<div v-if="tenantConfig.requireCableLength">
<label class="text-xs text-slate-500 dark:text-slate-300 font-semibold">Kabellänge (m)</label>
<input v-model="selectedWorkorder.cableLength" type="text" class="mt-1 w-full p-2 border rounded-md dark:bg-slate-800 dark:border-slate-700 dark:text-white" placeholder="z.B. 20m">
</div>
<div v-if="tenantConfig.requireCableType">
<label class="text-xs text-slate-500 dark:text-slate-300 font-semibold">Kabeltyp</label>
<input v-model="selectedWorkorder.cableType" type="text" class="mt-1 w-full p-2 border rounded-md dark:bg-slate-800 dark:border-slate-700 dark:text-white" placeholder="z.B. LWL-Kabel 4F">
</div>
<button @click="saveWorkorderData" :disabled="savingData" class="mt-2 w-full px-4 py-2 bg-secondary text-primary font-bold rounded-md text-sm disabled:bg-slate-300">
{{ savingData ? 'Speichert...' : 'Daten speichern' }}
</button>
</div>
<div class="bg-white dark:bg-slate-900 p-4 rounded-lg border border-slate-200 dark:border-slate-800">
<h3 class="font-bold text-slate-700 dark:text-secondary mb-3">Checkliste</h3>
<div v-if="isDetailsLoading" class="space-y-3 animate-pulse">
@@ -672,7 +752,7 @@
<button @click="handleCompleteClick" class="w-full px-4 py-3 bg-green-600 text-white font-bold rounded-md text-center disabled:bg-slate-300">Abschließen</button>
<transition name="fade">
<div v-if="missingTasksPopover.show" class="absolute bottom-full right-0 mb-2 w-72 bg-red-700 text-white text-sm rounded-lg shadow-lg p-3">
<h4 class="font-bold mb-1">Fehlende Checklisten-Punkte:</h4>
<h4 class="font-bold mb-1">Fehlende Punkte:</h4>
<ul class="list-disc list-inside space-y-1">
<li v-for="task in missingTasksPopover.tasks" :key="task">{{ task }}</li>
</ul>
@@ -870,4 +950,4 @@
</script>
<script src="/js/pages/WorkorderBase/WorkorderServiceWorker.js"></script>
</body>
</html>
</html>

View File

@@ -14,6 +14,8 @@ class WorkorderModel extends TTCrudBaseModel
public ?int $deadlineDate;
public ?int $appointmentDate;
public ?string $additionalInfo;
public ?string $cableLength;
public ?string $cableType;
public int $create;
public int $createBy;

View File

@@ -143,6 +143,18 @@ class WorkorderCompanyController extends WorkorderBaseController {
$workorder = WorkorderModel::get($this->postData['workorderId']);
if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden.");
// START ADDED VALIDATION
$tenantConfig = $this->getTenantConfigFromWorkorder($workorder->id);
if ($tenantConfig) {
if ($tenantConfig->requireCableLength && empty(trim($workorder->cableLength))) {
self::sendError("Bitte geben Sie die Kabellänge an, um den Auftrag abzuschließen.");
}
if ($tenantConfig->requireCableType && empty(trim($workorder->cableType))) {
self::sendError("Bitte geben Sie den Kabeltyp an, um den Auftrag abzuschließen.");
}
}
// END ADDED VALIDATION
$workorder->status = 'documented';
WorkorderModel::update((array)$workorder);
self::returnJson(['success' => true, 'message' => 'Arbeitsauftrag zur Prüfung eingereicht.']);
@@ -155,7 +167,14 @@ class WorkorderCompanyController extends WorkorderBaseController {
self::returnJson(['success' => false, 'message' => 'Keine Mandantenkonfiguration gefunden.']);
return;
}
self::returnJson(['success' => true, 'documentationTypes' => json_decode($tenantConfig->documentationTypes, true), 'civilEngineeringDocsRequired' => $tenantConfig->civilEngineeringDocsRequired, 'interventionTypes' => json_decode($tenantConfig->interventionTypes, true)]);
self::returnJson([
'success' => true,
'documentationTypes' => json_decode($tenantConfig->documentationTypes, true),
'civilEngineeringDocsRequired' => $tenantConfig->civilEngineeringDocsRequired,
'interventionTypes' => json_decode($tenantConfig->interventionTypes, true),
'requireCableLength' => $tenantConfig->requireCableLength,
'requireCableType' => $tenantConfig->requireCableType
]);
}
protected function uploadDocumentationAction() {
@@ -237,5 +256,52 @@ class WorkorderCompanyController extends WorkorderBaseController {
]);
self::returnJson(['success' => true, 'message' => 'Tiefbau erfolgreich abgeschlossen.']);
}
protected function updateWorkorderDataAction() {
if (empty($this->postData['workorderId'])) self::sendError("Arbeitsauftrags-ID fehlt.");
$workorder = WorkorderModel::get($this->postData['workorderId']);
if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden.");
$journalText = "Zusatzdaten aktualisiert:\n";
$changed = false;
if (isset($this->postData['cableLength'])) {
if ($workorder->cableLength != $this.postData['cableLength']) {
$journalText .= "Kabellänge: '{$workorder->cableLength}' -> '{$this->postData['cableLength']}'\n";
$workorder->cableLength = $this.postData['cableLength'];
$changed = true;
}
}
if (isset($this->postData['cableType'])) {
if ($workorder->cableType != $this.postData['cableType']) {
$journalText .= "Kabeltyp: '{$workorder->cableType}' -> '{$this->postData['cableType']}'\n";
$workorder->cableType = $this.postData['cableType'];
$changed = true;
}
}
if (!$changed) {
self::returnJson(['success' => true, 'message' => 'Keine Änderungen vorgenommen.']);
return;
}
WorkorderModel::update((array)$workorder);
WorkorderJournalModel::create([
'workorderId' => $workorder->id,
'text' => $journalText,
'create' => time(),
'createBy' => $this->user->id,
]);
// Re-fetch journals to return
$journals = WorkorderJournalModel::getAll(['workorderId' => intval($workorder->id)], null, 0, ['key' => 'create', 'order' => 'DESC']);
foreach ($journals as $journal) {
$journal->createByName = UserModel::getOne($journal->createBy)->name ?? 'Unbekannt';
}
self::returnJson(['success' => true, 'message' => 'Daten gespeichert.', 'journals' => $journals]);
}
//endregion
}

View File

@@ -10,6 +10,8 @@ class WorkorderTenantConfigModel extends TTCrudBaseModel {
public ?string $workorderActiveFilters; // JSON
public ?string $interventionTypes; // JSON
public int $civilEngineeringDocsRequired;
public int $requireCableLength;
public int $requireCableType;
public int $create;
public int $createBy;
@@ -31,4 +33,5 @@ class WorkorderTenantConfigModel extends TTCrudBaseModel {
$row = $result ? $result->fetch_assoc() : null;
return $row ? new self($row) : null;
}}
}
}

View File

@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class WorkorderAddCableFields extends AbstractMigration
{
public function up(): void
{
if ($this->getEnvironment() == "thetool") {
$tableWorkorder = $this->table("Workorder");
$tableWorkorder
->addColumn("cableLength", "string", [
'null' => true,
'default' => null,
'after' => 'additionalInfo'
])
->addColumn("cableType", "string", [
'null' => true,
'default' => null,
'after' => 'cableLength'
])
->update();
$tableConfig = $this->table("WorkorderTenantConfig");
$tableConfig
->addColumn("requireCableLength", "boolean", [
'null' => false,
'default' => 0,
'after' => 'civilEngineeringDocsRequired'
])
->addColumn("requireCableType", "boolean", [
'null' => false,
'default' => 0,
'after' => 'requireCableLength'
])
->update();
}
}
public function down(): void
{
if ($this->getEnvironment() == "thetool") {
$this->table("Workorder")
->removeColumn("cableLength")
->removeColumn("cableType")
->save();
$this->table("WorkorderTenantConfig")
->removeColumn("requireCableLength")
->removeColumn("requireCableType")
->save();
}
}
}

View File

@@ -183,7 +183,7 @@ Vue.component('workorder-details-manager', {
:disabled="!canComplete || isReadOnly" :loading="completing"
additional-class="btn-success w-100" icon="fas fa-check-double"/>
<small v-if="!canComplete && !isReadOnly" class="form-text text-muted text-center mt-2">
Bitte laden Sie alle benötigten Dokumente hoch, um den Auftrag abzuschließen.
Bitte laden Sie alle benötigten Dokumente hoch und füllen Sie alle Zusatzdaten (z.B. Kabellänge/-typ) aus, um den Auftrag abzuschließen.
</small>
<div v-if="isReadOnly" class="alert alert-secondary text-center mt-2 p-2">
Auftrag bereits abgeschlossen oder storniert. Keine Aktionen mehr möglich.
@@ -245,6 +245,35 @@ Vue.component('workorder-details-manager', {
</div>
</div>
<div v-if="(requireCableLength || requireCableType)" class="card mb-3">
<div class="card-body">
<h5 class="card-title">Zusatzdaten</h5>
<p class="small text-muted">Diese Daten werden für den Abschluss benötigt.</p>
<tt-input
v-if="requireCableLength"
label="Kabellänge (m)"
v-model="workorder.cableLength"
:disabled="isReadOnly"
sm row
/>
<tt-input
v-if="requireCableType"
label="Kabeltyp"
v-model="workorder.cableType"
:disabled="isReadOnly"
sm row
/>
<tt-button
text="Daten speichern"
@click="saveWorkorderData"
:loading="savingData"
:disabled="isReadOnly || savingData"
additional-class="btn-info float-right"
icon="fas fa-save"
/>
</div>
</div>
<div class="card mb-3" v-if="isAdmin && selectedDocs.length > 0">
<div class="card-header bg-warning"><h5><i class="fas fa-exclamation-triangle mr-2"></i>Korrektur anfordern</h5></div>
<div class="card-body">
@@ -296,6 +325,9 @@ Vue.component('workorder-details-manager', {
uploadData: { files: [], documentType: 'photo_hup_mounted', description: '' },
interventionData: null,
interventionTypes: [],
requireCableLength: false,
requireCableType: false,
savingData: false,
// Admin state
selectedDocs: [], correctionText: '', correctionLoading: false, showAcceptModal: false, showRevertModal: false,
}),
@@ -305,7 +337,19 @@ Vue.component('workorder-details-manager', {
return this.tenantDocTypes ?? [];
},
allDocTypes() { return [...this.requiredDocTypes, {value: 'other', text: 'Sonstiges Dokument (optional)'}]; },
canComplete() { return this.requiredDocTypes.every(docType => this.isUploaded(docType.value)); },
canComplete() {
const docsUploaded = this.requiredDocTypes.every(docType => this.isUploaded(docType.value));
if (!docsUploaded) return false;
if (this.requireCableLength && (!this.workorder.cableLength || !this.workorder.cableLength.trim())) {
return false;
}
if (this.requireCableType && (!this.workorder.cableType || !this.workorder.cableType.trim())) {
return false;
}
return true; // All checks passed
},
docsWithStatus() {
if (!this.journals?.length) return this.docs;
const correctionJournal = [...this.journals].sort((a, b) => b.create - a.create).find(j => j.statusChange?.includes('correction_requested'));
@@ -348,6 +392,8 @@ Vue.component('workorder-details-manager', {
if (data.success) {
this.tenantDocTypes = data.documentationTypes;
this.interventionTypes = data.interventionTypes;
this.requireCableLength = data.requireCableLength || false;
this.requireCableType = data.requireCableType || false;
}
} catch (e) { console.error("Mandantenkonfiguration nicht geladen", e); }
finally { this.loadingConfig = false; }
@@ -404,6 +450,28 @@ Vue.component('workorder-details-manager', {
} else window.notify('error', data.message);
} catch (e) { window.notify('error', 'Netzwerkfehler'); }
},
async saveWorkorderData() {
this.savingData = true;
try {
const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/updateWorkorderData`, {
workorderId: this.workorderId,
cableLength: this.workorder.cableLength,
cableType: this.workorder.cableType
});
if (data.success) {
window.notify('success', data.message);
if (data.journals) {
this.journals = data.journals; // Update journal with new entry
}
} else {
window.notify('error', data.message || 'Speichern fehlgeschlagen.');
}
} catch (e) {
window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.');
} finally {
this.savingData = false;
}
},
openInterventionModal() {
this.interventionData = { types: [], details: { stuck: {}, stuck_fcp: {}, stuck_hup: {}, other: {} } };
},
@@ -489,4 +557,4 @@ Vue.component('workorder-details-manager', {
await this.loadTenantConfig();
await this.fetchData();
}
});
});

View File

@@ -77,9 +77,19 @@ Vue.component('workorder-tenant-config', {
<div class="row mt-3">
<div class="col-md-6">
<h6 class="mb-3">Optionen</h6>
<tt-checkbox label="Dokumentation für Tiefbau erforderlich"
v-model="editableItem.civilEngineeringDocsRequired" sm v-if="editingId === config.id"/>
<p v-else>Tiefbau-Doku: {{ config.civilEngineeringDocsRequired ? 'Ja' : 'Nein' }}</p>
<div v-if="editingId === config.id">
<tt-checkbox label="Dokumentation für Tiefbau erforderlich"
v-model="editableItem.civilEngineeringDocsRequired" sm/>
<tt-checkbox label="Kabellänge erforderlich"
v-model="editableItem.requireCableLength" sm/>
<tt-checkbox label="Kabeltyp erforderlich"
v-model="editableItem.requireCableType" sm/>
</div>
<div v-else>
<p>Tiefbau-Doku: <strong>{{ config.civilEngineeringDocsRequired ? 'Ja' : 'Nein' }}</strong></p>
<p>Kabellänge-Doku: <strong>{{ config.requireCableLength ? 'Ja' : 'Nein' }}</strong></p>
<p>Kabeltyp-Doku: <strong>{{ config.requireCableType ? 'Ja' : 'Nein' }}</strong></p>
</div>
</div>
<div class="col-md-6">
<h6>Zugeordnete Firmen</h6>
@@ -312,7 +322,9 @@ Vue.component('workorder-tenant-config', {
interventionTypes: [],
workorderCreationFilters: '{}',
workorderActiveFilters: '{}',
civilEngineeringDocsRequired: 0
civilEngineeringDocsRequired: 0,
requireCableLength: 0,
requireCableType: 0
}
: {visibleForAddressId: []};
this.showModal = true;