fixed workorder civil engineering

This commit is contained in:
Luca Haid
2026-01-27 16:09:10 +01:00
parent dc388b07ab
commit 4bfd0eddce
3 changed files with 99 additions and 20 deletions

View File

@@ -79,7 +79,7 @@ class RimoWorkorder extends mfBaseModel {
$fn = 'aha_lageplan_' . $this->id . '_' . time() . '.png';
$file = FileModel::create(['name' => 'AHA Lageplan ' . $this->rimo_name, 'filename' => $fn,
'store_filename' => $fn, 'orig_filename' => 'AHA_Lageplan_' . $this->rimo_name . '.png',
'mimetype' => 'image/png', 'subfolder' => 'aha_maps']);
'mimetype' => 'image/png', 'subfolder' => 'aha_maps', 'create_by' => 1]);
if ($file->save()) {
$dir = MFUPLOAD_FILE_SAVE_PATH . '/aha_maps';
if (!is_dir($dir)) mkdir($dir, 0755, true);

View File

@@ -90,8 +90,8 @@ Vue.component('workorder-admin', {
<template v-slot:appointmentdate="{ row }">{{ formatDate(row.appointmentDate, true) }}</template>
<template v-slot:expandedRow="{ row }">
<civil-engineering-manager v-if="row.status === 'civil_engineering_required'" :workorder-id="row.id" :is-admin="true"/>
<workorder-details-manager v-else :workorder-id="row.id" :is-admin="true"
<civil-engineering-manager v-if="row.status === 'civil_engineering_required'" :workorder-id="row.id" :is-admin="true" class="mb-3"/>
<workorder-details-manager :workorder-id="row.id" :is-admin="true"
@workorder-completed="$refs.table.$refs.table.refreshTable()"
@accept-documentation="acceptDocumentation(row.id)"/>
</template>

View File

@@ -27,25 +27,56 @@ Vue.component('civil-engineering-manager', {
<div v-if="loading" class="text-center p-5"><i class="fas fa-spinner fa-spin fa-2x"></i></div>
<div v-else class="row">
<div class="col-lg-4">
<div class="card h-100">
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title">Tiefbau-Arbeiten</h5>
<h5 class="card-title"><i class="fas fa-hard-hat text-orange mr-2"></i>Tiefbau-Arbeiten</h5>
<p class="small text-muted">Schließen Sie den Tiefbau-Auftrag ab. Laden Sie Dokumente hoch, falls erforderlich.</p>
<div v-if="tiefbauSeesNormalDocs && documentationTypes.length" class="mb-3">
<h6 class="small font-weight-bold">Checkliste</h6>
<ul class="list-unstyled mb-0">
<li v-for="docType in documentationTypes" :key="docType.value" class="mb-1 d-flex align-items-center small">
<i :class="isUploaded(docType.value) ? 'fas fa-check-circle text-success' : 'far fa-circle text-muted'" class="fa-fw mr-2"></i>
<span>{{ docType.text }}</span>
</li>
</ul>
</div>
<hr>
<tt-button text="Tiefbau abschließen" @click="showCompleteModal = true"
:disabled="!canComplete || completing"
:loading="completing" additional-class="btn-success w-100" icon="fas fa-check-double"
/>
<small v-if="docsRequired && !canComplete" class="form-text text-muted text-center mt-2">
Bitte laden Sie mindestens ein Dokument hoch, um den Auftrag abzuschließen.
<small v-if="docsRequired && !canComplete && !tiefbauSeesNormalDocs" class="form-text text-muted text-center mt-2">
Bitte laden Sie mindestens ein Tiefbau-Dokument hoch, um den Auftrag abzuschließen.
</small>
</div>
</div>
<div class="card">
<div class="card-header py-2"><h6 class="mb-0"><i class="fas fa-history mr-2"></i>Journal</h6></div>
<div class="card-body p-0" style="max-height: 200px; overflow-y: auto;">
<ul v-if="journals.length" class="list-group list-group-flush">
<li v-for="log in journals" :key="log.id" class="list-group-item small py-2">
<strong>{{ formatDate(log.create) }} ({{ log.createByName }}):</strong>
<div class="text-muted" style="white-space: pre-wrap;">{{ log.text }}</div>
</li>
</ul>
<div v-else class="p-3 text-muted text-center small">Keine Journaleinträge.</div>
</div>
<div class="card-footer py-2">
<tt-textarea v-model="newJournalMessage" placeholder="Nachricht..." rows="2" sm no-form-group/>
<tt-button text="Eintrag speichern" @click="addJournalEntry" :loading="addingJournalEntry"
additional-class="btn-info btn-sm w-100 mt-2" icon="fas fa-paper-plane"/>
</div>
</div>
</div>
<div class="col-lg-8">
<div class="card mb-3" v-if="docsRequired">
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title">Dokument hochladen</h5>
<tt-select v-if="tiefbauSeesNormalDocs && allDocTypes.length" label="Dokumententyp" :options="allDocTypes" v-model="uploadData.documentType" sm row/>
<tt-input label="Beschreibung" v-model="uploadData.description" sm row placeholder="Optional"/>
<div class="form-group row">
<label class="col-form-label col-sm-4 col-form-label-sm">Dateien</label>
@@ -56,14 +87,21 @@ Vue.component('civil-engineering-manager', {
<tt-button text="Hochladen" @click="uploadFiles" :loading="uploading" additional-class="btn-primary float-right" icon="fas fa-upload"/>
</div>
</div>
<tt-file-gallery
v-if="docsRequired"
:files="uploadedFiles"
:edit-mode="false"
:edit-mode="tiefbauSeesNormalDocs"
:delete-mode="true"
@delete-file="deleteDocumentation">
@delete-file="deleteDocumentation"
@update-file="updateDocumentation">
<template v-if="tiefbauSeesNormalDocs" v-slot:file-edit="{ file }">
<tt-select label="Dokumententyp" :options="allDocTypes" v-model="file.documentType" sm/>
</template>
</tt-file-gallery>
<div v-else class="alert alert-info">Für diesen Auftraggeber ist keine Dokumentation für Tiefbau-Arbeiten erforderlich.</div>
<div v-if="!docsRequired && !tiefbauSeesNormalDocs" class="alert alert-info">
Für diesen Auftraggeber ist keine Dokumentation für Tiefbau-Arbeiten erforderlich.
</div>
</div>
</div>
<tt-modal :show.sync="showCompleteModal" title="Tiefbau abschließen" @submit="completeTask" @close="showCompleteModal = false" :delete="false">
@@ -72,25 +110,43 @@ Vue.component('civil-engineering-manager', {
</div>
`,
data: () => ({
loading: true, uploading: false, completing: false, docsRequired: false,
uploadedFiles: [], uploadData: { files: [], description: '' }, showCompleteModal: false
loading: true, uploading: false, completing: false,
docsRequired: false, tiefbauSeesNormalDocs: false, documentationTypes: [],
uploadedFiles: [], journals: [],
uploadData: { files: [], description: '', documentType: 'civil_engineering_photo' },
showCompleteModal: false,
newJournalMessage: '', addingJournalEntry: false
}),
computed: {
canComplete() {
if (this.tiefbauSeesNormalDocs) {
// When tiefbauSeesNormalDocs is enabled, can always complete (no strict requirements)
return true;
}
// Original logic: require at least one civil engineering doc if docsRequired
return !this.docsRequired || this.uploadedFiles.length > 0;
},
allDocTypes() {
return [...this.documentationTypes, { value: 'civil_engineering_photo', text: 'Tiefbau Foto' }, { value: 'other', text: 'Sonstiges' }];
}
},
methods: {
formatDate(timestamp) { return timestamp ? window.moment.unix(timestamp).format('DD.MM.YYYY HH:mm') : ''; },
isUploaded(docType) {
return Array.isArray(this.uploadedFiles) && this.uploadedFiles.some(doc => doc.documentType === docType);
},
async fetchInitialData() {
this.loading = true;
try {
const configRes = await axios.get(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/getTenantConfig`, { params: { workorderId: this.workorderId } });
this.docsRequired = configRes.data.civilEngineeringDocsRequired || false;
this.tiefbauSeesNormalDocs = configRes.data.tiefbauSeesNormalDocs || false;
this.documentationTypes = configRes.data.documentationTypes || [];
if(this.docsRequired) {
// Always load docs and journals
const docRes = await axios.get(`${window.TT_CONFIG.BASE_PATH}/${this.isAdmin ? 'WorkorderAdmin' : 'WorkorderCompany'}/getDocumentation`, { params: { workorderId: this.workorderId } });
this.uploadedFiles = docRes.data.docs || [];
}
this.journals = docRes.data.journals || [];
} catch (e) {
window.notify('error', 'Konfiguration konnte nicht geladen werden.');
console.error(e);
@@ -104,7 +160,7 @@ Vue.component('civil-engineering-manager', {
this.uploading = true;
const formData = new FormData();
formData.append('workorderId', this.workorderId);
formData.append('documentType', 'civil_engineering_photo');
formData.append('documentType', this.uploadData.documentType || 'civil_engineering_photo');
formData.append('description', this.uploadData.description);
for (const file of this.uploadData.files) formData.append('files[]', file);
try {
@@ -112,7 +168,7 @@ Vue.component('civil-engineering-manager', {
if (data.success) {
window.notify('success', data.message);
this.$refs.fileInput.value = '';
this.uploadData = { files: [], description: '' };
this.uploadData = { files: [], description: '', documentType: this.tiefbauSeesNormalDocs ? this.documentationTypes[0]?.value : 'civil_engineering_photo' };
await this.fetchInitialData();
} else window.notify('error', data.error || 'Upload fehlgeschlagen.');
} catch (e) {
@@ -131,6 +187,29 @@ Vue.component('civil-engineering-manager', {
window.notify('error', 'Netzwerkfehler beim Löschen.');
}
},
async updateDocumentation(file) {
try {
const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/updateDocumentation`, { id: file.id, documentType: file.documentType });
if (data.success) {
window.notify('success', data.message);
await this.fetchInitialData();
} else window.notify('error', data.message || 'Update fehlgeschlagen.');
} catch (e) {
window.notify('error', 'Netzwerkfehler beim Update.');
}
},
async addJournalEntry() {
if (!this.newJournalMessage.trim()) return;
this.addingJournalEntry = true;
try {
const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/${this.isAdmin ? 'WorkorderAdmin' : 'WorkorderCompany'}/addJournal`, { workorderId: this.workorderId, text: this.newJournalMessage });
if (data.success) {
this.newJournalMessage = '';
this.journals = data.journals || [];
} else window.notify('error', data.message);
} catch (e) { window.notify('error', 'Netzwerkfehler'); }
finally { this.addingJournalEntry = false; }
},
async completeTask() {
this.completing = true;
try {