Files
thetool/public/js/pages/WorkorderMphBase/WorkorderMphBase.js
2025-11-16 19:19:17 +01:00

544 lines
25 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// WorkorderMphBase.js - Shared components for WorkorderMph module
// Traffic light component (reused from WorkorderBase)
Vue.component('traffic-light-mph', {
props: ['deadline', 'status'],
computed: {
lightInfo() {
const deadlineDate = moment.unix(this.deadline);
const daysLeft = deadlineDate.diff(moment(), 'days');
if (['completed', 'new', 'cancelled'].includes(this.status)) return { color: '#cccccc', title: 'Status irrelevant für Dringlichkeit' };
if (!deadlineDate.isValid()) return { color: '#cccccc', title: 'Keine Deadline gesetzt' };
if (deadlineDate.isBefore(moment())) return { color: '#dc3545', title: 'Deadline überschritten' };
if (daysLeft <= 7) return { color: '#dc3545', title: 'Dringend: Weniger als 1 Woche' };
if (daysLeft <= 21) return { color: '#ffc107', title: 'Mittel: Weniger als 3 Wochen' };
return { color: '#28a745', title: 'Im Plan: Mehr als 3 Wochen' };
}
},
template: `<span :style="{ color: lightInfo.color, fontSize: '1.2em' }" class="mr-2" :title="lightInfo.title">&#9679;</span>`
});
// Wohneinheit Status Manager Component
Vue.component('wohneinheit-status-manager', {
props: {
workorderMphId: { type: Number, required: true },
isAdmin: { type: Boolean, default: false }
},
template: `
<div class="card wohneinheit-manager">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="fas fa-building mr-2"></i>Wohneinheiten Status</h5>
</div>
<div v-if="loading" class="text-center p-5"><i class="fas fa-spinner fa-spin fa-2x"></i></div>
<div v-else class="card-body p-0">
<div v-if="wohneinheiten.length === 0" class="alert alert-info m-3">
Keine Wohneinheiten gefunden.
</div>
<div v-else class="we-table">
<div v-for="we in wohneinheiten" :key="we.wohneinheitId" class="we-row">
<div class="we-cell we-bezeichner">
<strong>{{ we.bezeichner }}</strong>
<div v-if="we.contact" class="contact-info text-muted">
<i class="fas fa-user mr-1"></i>{{ we.contact }}
</div>
<div v-else class="text-muted small fst-italic">
<i class="fas fa-user-slash mr-1"></i>Keine Kontaktinfo
</div>
</div>
<div class="we-cell we-status">
<tt-select v-model="we.status" :options="statusOptions" sm no-form-group
:disabled="isAdmin" @input="markAsChanged(we)"/>
</div>
<div class="we-cell we-note">
<tt-textarea v-model="we.note" sm rows="1" no-form-group
:disabled="isAdmin"
@input="markAsChanged(we)"
:placeholder="isAdmin ? 'Keine Notiz' : 'Pflichtfeld'"/>
</div>
<div class="we-cell we-actions">
<span v-if="we.changed" class="text-warning small">
<i class="fas fa-exclamation-triangle"></i>
</span>
<tt-button v-if="!isAdmin" @click="saveWohneinheit(we)" text="Speichern" sm
:loading="we.saving" :disabled="!we.changed"/>
</div>
</div>
</div>
</div>
</div>
`,
data: () => ({
loading: true,
wohneinheiten: [],
statusOptions: []
}),
methods: {
async fetchWohneinheiten() {
this.loading = true;
try {
const basePath = this.isAdmin ? '/WorkorderMphAdmin' : '/WorkorderMphCompany';
const { data } = await axios.get(`${window.TT_CONFIG.BASE_PATH}${basePath}/getWohneinheiten`, {
params: { workorderMphId: this.workorderMphId }
});
this.wohneinheiten = data.wohneinheiten.map(we => ({ ...we, changed: false, saving: false }));
this.statusOptions = data.statusOptions || [];
} catch (e) {
window.notify('error', 'Wohneinheiten konnten nicht geladen werden.');
console.error(e);
} finally {
this.loading = false;
}
},
markAsChanged(we) {
we.changed = true;
},
async saveWohneinheit(we) {
if (!we.note || !we.note.trim()) {
return window.notify('error', 'Bitte eine Notiz eingeben.');
}
we.saving = true;
try {
const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderMphCompany/updateWohneinheit`, {
workorderMphId: this.workorderMphId,
wohneinheitId: we.wohneinheitId,
status: we.status,
note: we.note
});
if (data.success) {
window.notify('success', data.message);
we.changed = false;
this.$emit('wohneinheit-updated');
} else {
window.notify('error', data.message || 'Speichern fehlgeschlagen.');
}
} catch (e) {
window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.');
} finally {
we.saving = false;
}
},
getStatusText(statusValue) {
const option = this.statusOptions.find(opt => opt.value === statusValue);
return option ? option.text : '';
}
},
async mounted() {
await this.fetchWohneinheiten();
}
});
// Checkbox Documentation Component
Vue.component('checkbox-documentation', {
props: {
workorderMphId: { type: Number, required: true },
isAdmin: { type: Boolean, default: false }
},
template: `
<div class="checkbox-docs card mb-3">
<div class="card-body p-3">
<h5 class="card-title mb-3"><i class="fas fa-clipboard-check mr-2"></i>Dokumentation Checkboxen</h5>
<div v-if="loading" class="text-center p-3"><i class="fas fa-spinner fa-spin"></i></div>
<div v-else>
<div class="custom-checkboxes-grid">
<label class="custom-checkbox-item" :class="{ disabled: isAdmin }">
<input type="checkbox" v-model="checkboxes.easement" :disabled="isAdmin">
<span class="checkmark"></span>
<span class="checkbox-label">Leitungsrecht</span>
</label>
<label class="custom-checkbox-item" :class="{ disabled: isAdmin }">
<input type="checkbox" v-model="checkboxes.btb" :disabled="isAdmin">
<span class="checkmark"></span>
<span class="checkbox-label">BTB</span>
</label>
<label class="custom-checkbox-item" :class="{ disabled: isAdmin }">
<input type="checkbox" v-model="checkboxes.fttxLocationSupplied" :disabled="isAdmin">
<span class="checkmark"></span>
<span class="checkbox-label">FTTx Location mit Leerrohr versorgt</span>
</label>
<label class="custom-checkbox-item" :class="{ disabled: isAdmin }">
<input type="checkbox" v-model="checkboxes.conduitToHuepLaid" :disabled="isAdmin">
<span class="checkmark"></span>
<span class="checkbox-label">Leerrohr bis HÜP/HAK verlegt</span>
</label>
<label class="custom-checkbox-item" :class="{ disabled: isAdmin }">
<input type="checkbox" v-model="checkboxes.huepMounted" :disabled="isAdmin">
<span class="checkmark"></span>
<span class="checkbox-label">HÜP/HAK montiert</span>
</label>
<label class="custom-checkbox-item" :class="{ disabled: isAdmin }">
<input type="checkbox" v-model="checkboxes.dropCableAvailable" :disabled="isAdmin">
<span class="checkmark"></span>
<span class="checkbox-label">Dropkabel vorhanden</span>
</label>
</div>
<div class="mt-3 text-right" v-if="!isAdmin">
<tt-button @click="saveCheckboxes" text="Speichern"
:loading="saving" additional-class="btn-primary btn-sm"/>
</div>
</div>
</div>
</div>
`,
data: () => ({
loading: true,
saving: false,
checkboxes: {
easement: false,
btb: false,
fttxLocationSupplied: false,
conduitToHuepLaid: false,
huepMounted: false,
dropCableAvailable: false
}
}),
methods: {
async fetchCheckboxes() {
this.loading = true;
try {
const { data } = await axios.get(`${window.TT_CONFIG.BASE_PATH}/WorkorderMphCompany/getWorkorderById`, {
params: { id: this.workorderMphId }
});
this.checkboxes = {
easement: !!data.easement,
btb: !!data.btb,
fttxLocationSupplied: !!data.fttxLocationSupplied,
conduitToHuepLaid: !!data.conduitToHuepLaid,
huepMounted: !!data.huepMounted,
dropCableAvailable: !!data.dropCableAvailable
};
} catch (e) {
window.notify('error', 'Checkboxen konnten nicht geladen werden.');
console.error(e);
} finally {
this.loading = false;
}
},
async saveCheckboxes() {
this.saving = true;
try {
const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderMphCompany/updateCheckboxes`, {
workorderMphId: this.workorderMphId,
...this.checkboxes
});
if (data.success) {
window.notify('success', data.message);
this.$emit('checkboxes-updated');
} else {
window.notify('error', data.message || 'Speichern fehlgeschlagen.');
}
} catch (e) {
window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.');
} finally {
this.saving = false;
}
}
},
async mounted() {
await this.fetchCheckboxes();
}
});
// WorkorderMph Details Manager
Vue.component('workorder-mph-details-manager', {
props: {
workorderMphId: { type: String, required: true },
isAdmin: { type: Boolean, default: false }
},
data: () => ({
loading: true,
docs: [],
journals: [],
newJournalMessage: '',
addingJournalEntry: false,
uploading: false,
completing: false,
showCompleteModal: false,
showAcceptModal: false,
uploadData: { files: [], documentType: '', description: '' },
wohneinheitenWithNotes: true,
requiredDocs: [
{ key: 'huep_photo', label: 'HÜP/HAK Foto', icon: 'fas fa-camera', example: 'Foto der installierten HÜP/HAK' },
{ key: 'bep_md_photo', label: 'BEP MD Foto', icon: 'fas fa-camera', example: 'Foto der BEP (MD) Installation' },
{ key: 'ont_photo', label: 'ONT Foto', icon: 'fas fa-camera', example: 'Foto der ONT Installation' },
{ key: 'cable_routing', label: 'Kabelverlegung', icon: 'fas fa-route', example: 'Fotos der Kabelverlegung im Treppenhaus' },
{ key: 'fttx_location', label: 'FTTx Location', icon: 'fas fa-map-marker-alt', example: 'Foto/Dokument der FTTx Location' },
{ key: 'signature', label: 'Unterschrift', icon: 'fas fa-signature', example: 'Unterschriebenes Übergabeprotokoll' },
{ key: 'other', label: 'Sonstige Dokumentation', icon: 'fas fa-file', example: 'Weitere relevante Dokumente' }
]
}),
template: `
<div class="p-3 bg-light">
<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-5 mb-3 mb-lg-0">
<div v-if="!isAdmin" class="card mb-3">
<div class="card-body">
<h5 class="card-title">Auftrag abschließen</h5>
<p class="small text-muted">Dokumentieren Sie alle Wohneinheiten und laden Sie die erforderlichen Dokumente hoch.</p>
<hr>
<tt-button text="Auftrag zur Prüfung einreichen" @click="showCompleteModal = true"
: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 fügen Sie für jede Wohneinheit eine Notiz hinzu und laden Sie Dokumente hoch.
</small>
<div v-if="isReadOnly" class="alert alert-secondary text-center mt-2 p-2">
Auftrag bereits abgeschlossen oder storniert.
</div>
</div>
</div>
<div v-if="isAdmin" class="card mb-3">
<div class="card-body p-3">
<h5 class="card-title mb-3">Prüfung & Freigabe</h5>
<p class="small text-muted mb-3">Prüfen Sie die hochgeladenen Dokumente:</p>
<div class="required-docs-checklist mb-3">
<div v-for="doc in requiredDocs" :key="doc.key" class="doc-check-item">
<i :class="[doc.icon, hasDocType(doc.key) ? 'text-success' : 'text-muted']"></i>
<span :class="{ 'text-success': hasDocType(doc.key) }">{{ doc.label }}</span>
<i v-if="hasDocType(doc.key)" class="fas fa-check-circle text-success ml-auto"></i>
<i v-else class="fas fa-circle text-muted ml-auto" style="font-size: 0.8em;"></i>
</div>
</div>
<small class="text-muted d-block mb-3">
<i class="fas fa-info-circle"></i> Stellen Sie sicher, dass alle relevanten Dokumente vorhanden sind.
</small>
<tt-button text="Dokumentation akzeptieren" @click="showAcceptModal = true"
additional-class="btn-success w-100" icon="fas fa-check"/>
</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: 250px; 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">
<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="card-body text-muted text-center">Keine Journaleinträge.</div>
</div>
<div class="card-footer" v-if="!isReadOnly">
<tt-textarea v-model="newJournalMessage" placeholder="Nachricht oder Anmerkung..." rows="2"/>
<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-7">
<div class="card mb-3" v-if="!isReadOnly">
<div class="card-body p-3">
<h5 class="card-title mb-3">Neues Dokument hochladen</h5>
<div class="form-group row mb-2">
<label class="col-form-label col-sm-4 col-form-label-sm">Dokumententyp*</label>
<div class="col-sm-8">
<select v-model="uploadData.documentType" class="form-control form-control-sm" required>
<option value="">-- Bitte wählen --</option>
<option v-for="doc in requiredDocs" :key="doc.key" :value="doc.key">
{{ doc.label }}
</option>
</select>
<small v-if="uploadData.documentType" class="form-text text-muted">
<i class="fas fa-info-circle"></i> {{ getDocExample(uploadData.documentType) }}
</small>
</div>
</div>
<div class="form-group row mb-2">
<label class="col-form-label col-sm-4 col-form-label-sm">Beschreibung</label>
<div class="col-sm-8">
<input type="text" v-model="uploadData.description" class="form-control form-control-sm"
placeholder="Optional: Zusätzliche Beschreibung"/>
</div>
</div>
<div class="form-group row mb-3">
<label class="col-form-label col-sm-4 col-form-label-sm">Dateien*</label>
<div class="col-sm-8">
<input type="file" class="form-control-file form-control-sm"
@change="handleFileUpload" ref="fileInput" multiple accept="image/*,.pdf"/>
<small class="form-text text-muted">Erlaubt: Bilder (JPG, PNG) und PDF</small>
</div>
</div>
<div class="text-right">
<tt-button text="Hochladen" @click="uploadFiles" :loading="uploading"
:disabled="!uploadData.documentType || !uploadData.files.length"
additional-class="btn-primary btn-sm" icon="fas fa-upload"/>
</div>
</div>
</div>
<tt-file-gallery :files="docs" :edit-mode="false" :delete-mode="!isReadOnly && !isAdmin"
@delete-file="deleteDocumentation">
</tt-file-gallery>
</div>
</div>
<tt-modal :show.sync="showCompleteModal" title="Auftrag abschließen" @submit="completeWorkorder" :delete="false">
Möchten Sie diesen Auftrag wirklich abschließen und zur Prüfung einreichen?
</tt-modal>
<tt-modal :show.sync="showAcceptModal" title="Dokumentation akzeptieren" @submit="acceptDocumentation" :delete="false">
Soll die Dokumentation für diesen Arbeitsauftrag wirklich akzeptiert und der Auftrag abgeschlossen werden?
</tt-modal>
</div>
`,
computed: {
isReadOnly() {
return ['completed', 'cancelled'].includes(this.workorder?.status);
},
canComplete() {
return this.wohneinheitenWithNotes && this.docs.length > 0;
}
},
methods: {
formatDate(timestamp) {
return timestamp ? window.moment.unix(timestamp).format('DD.MM.YYYY HH:mm') : '';
},
hasDocType(docType) {
return this.docs.some(doc => doc.documentType === docType);
},
getDocExample(docType) {
const doc = this.requiredDocs.find(d => d.key === docType);
return doc ? doc.example : '';
},
async fetchData() {
this.loading = true;
try {
const basePath = this.isAdmin ? '/WorkorderMphAdmin' : '/WorkorderMphCompany';
const { data } = await axios.get(`${window.TT_CONFIG.BASE_PATH}${basePath}/getDocumentation`, {
params: { workorderMphId: this.workorderMphId }
});
this.docs = data.docs || [];
this.journals = data.journals || [];
} catch (e) {
window.notify('error', 'Details konnten nicht geladen werden.');
this.docs = [];
this.journals = [];
} finally {
this.loading = false;
}
},
async addJournalEntry() {
if (!this.newJournalMessage.trim()) return window.notify('error', 'Bitte eine Nachricht eingeben.');
this.addingJournalEntry = true;
try {
const basePath = this.isAdmin ? '/WorkorderMphAdmin' : '/WorkorderMphCompany';
const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}${basePath}/addJournal`, {
workorderMphId: this.workorderMphId,
text: this.newJournalMessage
});
if (data.success) {
window.notify('success', data.message);
this.journals = data.journals || [];
this.newJournalMessage = '';
} else {
window.notify('error', data.message || 'Eintrag fehlgeschlagen.');
}
} catch (e) {
window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.');
} finally {
this.addingJournalEntry = false;
}
},
handleFileUpload(event) {
this.uploadData.files = event.target.files;
},
async uploadFiles() {
if (!this.uploadData.files?.length) return window.notify('error', 'Bitte eine oder mehrere Dateien auswählen.');
this.uploading = true;
const formData = new FormData();
formData.append('workorderMphId', this.workorderMphId);
formData.append('documentType', this.uploadData.documentType);
formData.append('description', this.uploadData.description);
for (const file of this.uploadData.files) {
formData.append('file', file);
}
try {
const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderMphCompany/uploadDocumentation`, formData);
if (data.success) {
window.notify('success', data.message);
this.$refs.fileInput.value = '';
this.uploadData = { files: [], documentType: 'photo', description: '' };
await this.fetchData();
} else {
window.notify('error', data.error || 'Upload fehlgeschlagen.');
}
} catch (e) {
window.notify('error', 'Ein Netzwerkfehler ist beim Upload aufgetreten.');
} finally {
this.uploading = false;
}
},
async deleteDocumentation(file) {
try {
const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderMphCompany/deleteDocumentation`, {
documentationId: file.id
});
if (data.success) {
window.notify('success', data.message);
await this.fetchData();
} else {
window.notify('error', data.message || 'Löschen fehlgeschlagen.');
}
} catch (e) {
window.notify('error', 'Netzwerkfehler beim Löschen.');
}
},
async completeWorkorder() {
this.completing = true;
try {
const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderMphCompany/completeWorkorder`, {
workorderId: this.workorderMphId
});
if (data.success) {
window.notify('success', data.message);
this.$emit('workorder-completed');
this.showCompleteModal = false;
} else {
window.notify('error', data.message || 'Abschluss fehlgeschlagen.');
}
} catch (e) {
window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.');
} finally {
this.completing = false;
}
},
async acceptDocumentation() {
try {
const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderMphAdmin/acceptDocumentation`, {
workorderId: this.workorderMphId
});
if (data.success) {
window.notify('success', data.message);
this.$emit('documentation-accepted');
this.showAcceptModal = false;
} else {
window.notify('error', data.message || 'Akzeptieren fehlgeschlagen.');
}
} catch (e) {
window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.');
}
}
},
async mounted() {
await this.fetchData();
}
});