619 lines
32 KiB
JavaScript
619 lines
32 KiB
JavaScript
// WorkorderMphBase.js - Shared components for WorkorderMph module
|
|
|
|
// Traffic light component
|
|
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">●</span>`
|
|
});
|
|
|
|
// Data Provider Component
|
|
Vue.component('workorder-mph-data-provider', {
|
|
props: {
|
|
workorderMphId: { type: [Number, String], required: true }
|
|
},
|
|
data: () => ({
|
|
loading: true,
|
|
docs: [],
|
|
journals: []
|
|
}),
|
|
methods: {
|
|
async fetchData() {
|
|
this.loading = true;
|
|
try {
|
|
// Try to detect context (Admin or Company) based on URL or guess
|
|
const isCompany = window.location.pathname.includes('Company');
|
|
const basePath = isCompany ? '/WorkorderMphCompany' : '/WorkorderMphAdmin';
|
|
|
|
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) {
|
|
console.error("Failed to fetch data", e);
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
}
|
|
},
|
|
mounted() {
|
|
this.fetchData();
|
|
},
|
|
render() {
|
|
return this.$scopedSlots.default({
|
|
loading: this.loading,
|
|
docs: this.docs,
|
|
journals: this.journals,
|
|
refresh: this.fetchData
|
|
});
|
|
}
|
|
});
|
|
|
|
// Wohneinheit Status Manager Component
|
|
Vue.component('wohneinheit-status-manager', {
|
|
props: {
|
|
workorderMphId: { type: Number, required: true },
|
|
isAdmin: { type: Boolean, default: false }
|
|
},
|
|
template: `
|
|
<div class="workorder-mph-card mph-wohneinheit-card">
|
|
<div class="workorder-mph-card-header">
|
|
<span><i class="fas fa-building"></i> Wohneinheiten</span>
|
|
<div>
|
|
<span v-if="loading" class="mr-2"><i class="fas fa-spinner fa-spin text-muted"></i></span>
|
|
<a v-if="hausnummerId" :href="'/AddressDB/view/?id=' + hausnummerId" target="_blank" class="small text-muted">
|
|
<i class="fas fa-external-link-alt mr-1"></i>AddressDB #{{ hausnummerId }}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="wohneinheiten.length === 0 && !loading" class="workorder-mph-card-body">
|
|
<div class="alert alert-info mb-0 py-2 small">
|
|
<i class="fas fa-info-circle mr-1"></i> Keine Wohneinheiten gefunden.
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else class="wohneinheit-manager-container">
|
|
<div class="wohneinheit-manager">
|
|
<div class="we-table">
|
|
<div class="we-header">
|
|
<div class="we-header-row">
|
|
<div class="we-cell we-zusatz">Zusatz / Kontakt</div>
|
|
<div class="we-cell we-tuer">Tür</div>
|
|
<div class="we-cell we-status">Status</div>
|
|
<div class="we-cell we-splice">Spleiß</div>
|
|
<div class="we-cell we-documents">Docs</div>
|
|
<div class="we-cell we-actions"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-for="we in wohneinheiten" :key="we.wohneinheitId" class="we-row" :class="{ 'locked-row': isLocked(we) }">
|
|
<div class="we-cell we-zusatz">
|
|
<div class="d-flex align-items-center mb-1">
|
|
<input type="text" class="form-control form-control-sm mr-1" v-model="we.zusatz"
|
|
:disabled="isLocked(we)" placeholder="Zusatz">
|
|
</div>
|
|
<!-- OAID: light red, vertical -->
|
|
<div v-if="we.oaid" class="text-danger small mb-1" style="font-weight: 500;">
|
|
<i class="fas fa-fingerprint mr-1"></i>OAID: {{ we.oaid }}
|
|
</div>
|
|
<div v-if="we.contact" class="text-muted small text-truncate" :title="we.contact">
|
|
<i class="fas fa-user mr-1"></i>{{ we.contact }}
|
|
</div>
|
|
<div v-else-if="we.preorderContact" class="text-info small text-truncate" :title="'VB: ' + we.preorderContact">
|
|
<i class="fas fa-user-check mr-1"></i>{{ we.preorderContact }}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="we-cell we-tuer">
|
|
<input type="text" class="form-control form-control-sm" v-model="we.tuer"
|
|
:disabled="isLocked(we)" placeholder="Tür">
|
|
</div>
|
|
|
|
<div class="we-cell we-status">
|
|
<div v-if="isLocked(we)" class="locked-status small">
|
|
<i class="fas fa-lock mr-1"></i>{{ getStatusText(we.status) }}
|
|
</div>
|
|
<tt-select v-else v-model="we.status" :options="filteredStatusOptions" sm no-form-group/>
|
|
</div>
|
|
|
|
<div class="we-cell we-splice">
|
|
<label class="custom-checkbox-item we-splice-checkbox justify-content-center" title="Spleiß im HÜP/HAK erledigt">
|
|
<input type="checkbox" v-model="we.spliceCompleted" :disabled="isLocked(we)">
|
|
<span class="checkmark"></span>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="we-cell we-documents">
|
|
<button class="btn btn-sm btn-outline-secondary position-relative workorder-mph-button" @click="openDocumentsModal(we)" title="Dokumente">
|
|
<i class="fas fa-camera"></i>
|
|
<span v-if="we.documentCount" class="badge badge-pill badge-danger position-absolute" style="top: -5px; right: -5px;">{{ we.documentCount }}</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="we-cell we-actions">
|
|
<button class="btn btn-sm btn-primary workorder-mph-button" @click="saveWohneinheit(we)" :disabled="isLocked(we) || we.saving" title="Speichern">
|
|
<i v-if="we.saving" class="fas fa-spinner fa-spin"></i>
|
|
<i v-else class="fas fa-save"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Documents Modal -->
|
|
<tt-modal :show.sync="documentsModal.show" :title="'Dokumente für WE ' + documentsModal.zusatz" size="lg" :delete="false" submit-text="Schließen" @submit="documentsModal.show = false">
|
|
<div v-if="documentsModal.loading" class="text-center"><i class="fas fa-spinner fa-spin"></i></div>
|
|
<div v-else>
|
|
<div class="bg-light p-3 mb-3 border rounded">
|
|
<div class="row">
|
|
<div class="col-md-5">
|
|
<label class="small font-weight-bold mb-1">Datei(en)</label>
|
|
<div class="mph-drop-zone"
|
|
:class="{ 'dragging': documentsModal.isDragging }"
|
|
@dragover.prevent="documentsModal.isDragging = true"
|
|
@dragleave.prevent="documentsModal.isDragging = false"
|
|
@drop.prevent="handleDrop">
|
|
<input type="file" ref="weFileInput" @change="handleFileSelect" multiple accept="image/*,.pdf">
|
|
<i class="fas fa-cloud-upload-alt mph-drop-zone-icon"></i>
|
|
<div class="mph-drop-zone-text">Dateien auswählen</div>
|
|
</div>
|
|
<div v-if="documentsModal.files.length" class="mph-file-list">
|
|
<div v-for="(file, index) in documentsModal.files" :key="index" class="mph-file-item">
|
|
<span class="text-truncate small flex-grow-1">{{ file.name }}</span>
|
|
<span class="remove-file" @click="removeFile(index)"><i class="fas fa-times"></i></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-7 d-flex flex-column justify-content-end">
|
|
<div class="form-group mb-2">
|
|
<label class="small font-weight-bold mb-1">Beschreibung</label>
|
|
<input type="text" class="form-control form-control-sm" v-model="documentsModal.uploadDescription" placeholder="Optional">
|
|
</div>
|
|
<tt-button text="Upload starten" @click="uploadWohneinheitDocument" :loading="documentsModal.uploading" additional-class="btn-primary btn-sm w-100" icon="fas fa-upload"/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<tt-file-gallery :files="documentsModal.docs" :edit-mode="false" :delete-mode="!isAdmin" @delete-file="deleteWohneinheitDocument"/>
|
|
</div>
|
|
</tt-modal>
|
|
</div>
|
|
`,
|
|
data: () => ({
|
|
loading: true,
|
|
wohneinheiten: [],
|
|
statusOptions: [],
|
|
hausnummerId: null,
|
|
debounceTimers: {},
|
|
documentsModal: {
|
|
show: false,
|
|
loading: false,
|
|
uploading: false,
|
|
isDragging: false,
|
|
wohneinheitId: null,
|
|
zusatz: '',
|
|
docs: [],
|
|
files: [],
|
|
uploadDescription: ''
|
|
}
|
|
}),
|
|
computed: {
|
|
filteredStatusOptions() { return this.statusOptions.filter(opt => opt.code !== 300); }
|
|
},
|
|
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, spliceCompleted: !!we.spliceCompleted, saving: false }));
|
|
this.statusOptions = data.statusOptions || [];
|
|
this.hausnummerId = data.hausnummerId;
|
|
} catch (e) { console.error(e); } finally { this.loading = false; }
|
|
},
|
|
isLocked(we) { const status = this.statusOptions.find(s => s.value === we.status); return status && status.code === 300; },
|
|
debouncedSave(we) {
|
|
we.saving = true;
|
|
if (this.debounceTimers[we.wohneinheitId]) clearTimeout(this.debounceTimers[we.wohneinheitId]);
|
|
this.debounceTimers[we.wohneinheitId] = setTimeout(() => this.saveWohneinheit(we), 1000);
|
|
},
|
|
async saveWohneinheit(we) {
|
|
try {
|
|
const basePath = this.isAdmin ? '/WorkorderMphAdmin' : '/WorkorderMphCompany';
|
|
await axios.post(`${window.TT_CONFIG.BASE_PATH}${basePath}/updateWohneinheit`, {
|
|
workorderMphId: this.workorderMphId, wohneinheitId: we.wohneinheitId, status: we.status, spliceCompleted: we.spliceCompleted ? 1 : 0, tuer: we.tuer, zusatz: we.zusatz
|
|
});
|
|
this.$emit('wohneinheit-updated');
|
|
} catch (e) { window.notify('error', 'Fehler beim Speichern'); } finally { we.saving = false; }
|
|
},
|
|
getStatusText(val) { const o = this.statusOptions.find(opt => opt.value === val); return o ? o.text : ''; },
|
|
async openDocumentsModal(we) {
|
|
this.documentsModal.show = true;
|
|
this.documentsModal.wohneinheitId = we.wohneinheitId;
|
|
this.documentsModal.zusatz = we.zusatz || we.oaid;
|
|
this.documentsModal.loading = true;
|
|
this.documentsModal.files = [];
|
|
this.documentsModal.uploadDescription = '';
|
|
|
|
try {
|
|
const basePath = this.isAdmin ? '/WorkorderMphAdmin' : '/WorkorderMphCompany';
|
|
const { data } = await axios.get(`${window.TT_CONFIG.BASE_PATH}${basePath}/getWohneinheitDocuments`, { params: { wohneinheitId: we.wohneinheitId } });
|
|
this.documentsModal.docs = data.docs || []; we.documentCount = this.documentsModal.docs.length;
|
|
} catch (e) { console.error(e); } finally { this.documentsModal.loading = false; }
|
|
},
|
|
handleFileSelect(event) {
|
|
this.addFiles(event.target.files);
|
|
},
|
|
handleDrop(event) {
|
|
this.documentsModal.isDragging = false;
|
|
this.addFiles(event.dataTransfer.files);
|
|
},
|
|
addFiles(fileList) {
|
|
if (!fileList.length) return;
|
|
this.documentsModal.files = [...this.documentsModal.files, ...Array.from(fileList)];
|
|
},
|
|
removeFile(index) {
|
|
this.documentsModal.files.splice(index, 1);
|
|
},
|
|
async uploadWohneinheitDocument() {
|
|
if (!this.documentsModal.files.length) return;
|
|
this.documentsModal.uploading = true;
|
|
|
|
const basePath = this.isAdmin ? '/WorkorderMphAdmin' : '/WorkorderMphCompany';
|
|
let successCount = 0;
|
|
|
|
for (const file of this.documentsModal.files) {
|
|
const formData = new FormData();
|
|
formData.append('wohneinheitId', this.documentsModal.wohneinheitId);
|
|
formData.append('description', this.documentsModal.uploadDescription);
|
|
formData.append('file', file);
|
|
|
|
try {
|
|
const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}${basePath}/uploadWohneinheitDocument`, formData);
|
|
if (data.success) successCount++;
|
|
} catch (e) { console.error(e); }
|
|
}
|
|
|
|
if (successCount > 0) {
|
|
window.notify('success', `${successCount} Datei(en) hochgeladen`);
|
|
this.documentsModal.files = [];
|
|
this.documentsModal.uploadDescription = '';
|
|
this.$refs.weFileInput.value = '';
|
|
const { data } = await axios.get(`${window.TT_CONFIG.BASE_PATH}${basePath}/getWohneinheitDocuments`, { params: { wohneinheitId: this.documentsModal.wohneinheitId } });
|
|
this.documentsModal.docs = data.docs || [];
|
|
} else {
|
|
window.notify('error', 'Upload fehlgeschlagen');
|
|
}
|
|
|
|
this.documentsModal.uploading = false;
|
|
},
|
|
async deleteWohneinheitDocument(file) {
|
|
try {
|
|
const basePath = this.isAdmin ? '/WorkorderMphAdmin' : '/WorkorderMphCompany';
|
|
await axios.post(`${window.TT_CONFIG.BASE_PATH}${basePath}/deleteWohneinheitDocument`, { documentationId: file.id });
|
|
window.notify('success', 'Gelöscht');
|
|
const { data } = await axios.get(`${window.TT_CONFIG.BASE_PATH}${basePath}/getWohneinheitDocuments`, { params: { wohneinheitId: this.documentsModal.wohneinheitId } });
|
|
this.documentsModal.docs = data.docs || [];
|
|
} catch (e) { window.notify('error', 'Fehler beim Löschen'); }
|
|
}
|
|
},
|
|
mounted() { this.fetchWohneinheiten(); }
|
|
});
|
|
|
|
// Checkbox Documentation Component
|
|
Vue.component('checkbox-documentation', {
|
|
props: { workorderMphId: { type: Number, required: true }, isAdmin: { type: Boolean, default: false } },
|
|
template: `
|
|
<div class="workorder-mph-card">
|
|
<div class="workorder-mph-card-header">
|
|
<span><i class="fas fa-tasks"></i> Dokumentation</span>
|
|
<span v-if="saving" class="text-muted small"><i class="fas fa-sync fa-spin"></i></span>
|
|
</div>
|
|
<div class="workorder-mph-card-body-compact">
|
|
<div v-if="loading" class="text-center py-2"><i class="fas fa-spinner fa-spin text-muted"></i></div>
|
|
<div v-else class="mph-checkbox-vertical">
|
|
<label class="mph-checkbox-item" v-for="(label, key) in labels" :key="key">
|
|
<input type="checkbox" v-model="checkboxes[key]" @change="saveCheckboxes">
|
|
<span class="checkmark"></span>
|
|
<span class="label">{{ label }}</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`,
|
|
data: () => ({
|
|
loading: true, saving: false,
|
|
checkboxes: { easement: false, btb: false, fttxLocationSupplied: false, conduitToHuepLaid: false, huepMounted: false, dropCableAvailable: false },
|
|
labels: {
|
|
easement: 'Leitungsrecht', btb: 'Bautechnische Begehung', fttxLocationSupplied: 'FTTx Location versorgt',
|
|
conduitToHuepLaid: 'Leerrohr bis HAK', huepMounted: 'HAK montiert', dropCableAvailable: 'Dropkabel vorhanden'
|
|
}
|
|
}),
|
|
methods: {
|
|
async fetchCheckboxes() {
|
|
this.loading = true;
|
|
try {
|
|
const basePath = this.isAdmin ? '/WorkorderMphAdmin' : '/WorkorderMphCompany';
|
|
const { data } = await axios.get(`${window.TT_CONFIG.BASE_PATH}${basePath}/getWorkorderById`, { params: { id: this.workorderMphId } });
|
|
for (let key in this.checkboxes) this.checkboxes[key] = !!data[key];
|
|
} catch (e) { console.error(e); } finally { this.loading = false; }
|
|
},
|
|
async saveCheckboxes() {
|
|
this.saving = true;
|
|
try {
|
|
const basePath = this.isAdmin ? '/WorkorderMphAdmin' : '/WorkorderMphCompany';
|
|
await axios.post(`${window.TT_CONFIG.BASE_PATH}${basePath}/updateCheckboxes`, { workorderMphId: this.workorderMphId, ...this.checkboxes });
|
|
} catch (e) { window.notify('error', 'Speichern fehlgeschlagen'); } finally { this.saving = false; }
|
|
}
|
|
},
|
|
mounted() { this.fetchCheckboxes(); }
|
|
});
|
|
|
|
// Journal Component
|
|
Vue.component('workorder-mph-journal', {
|
|
props: { journals: { type: Array, default: () => [] }, workorderMphId: { type: [Number, String], required: true }, isAdmin: { type: Boolean, default: false } },
|
|
data: () => ({ newMessage: '', adding: false }),
|
|
computed: {
|
|
canSend() { return this.newMessage.trim().length >= 3; },
|
|
sendButtonClass() { return this.canSend ? 'btn-primary' : 'btn-secondary'; }
|
|
},
|
|
template: `
|
|
<div class="workorder-mph-card">
|
|
<div class="workorder-mph-card-header"><i class="fas fa-history"></i> Journal</div>
|
|
<div class="workorder-mph-card-body-compact">
|
|
<div class="mph-journal-list" v-if="journals.length">
|
|
<div v-for="log in journals" :key="log.id" class="mph-journal-item">
|
|
<div class="mph-journal-item-header">{{ formatDate(log.create) }} - {{ log.createByName }}</div>
|
|
<div style="white-space: pre-wrap; color: #495057;">{{ log.text }}</div>
|
|
<div v-if="log.statusChange" class="badge badge-light border mt-1">{{ log.statusChange }}</div>
|
|
</div>
|
|
</div>
|
|
<div v-else class="text-center text-muted small py-2">Keine Einträge</div>
|
|
</div>
|
|
<div class="p-2 border-top bg-light">
|
|
<tt-textarea v-model="newMessage" placeholder="Notiz..." rows="2" no-form-group sm/>
|
|
<tt-button text="Senden" @click="addEntry" :loading="adding" :disabled="!canSend" :additional-class="sendButtonClass + ' btn-sm btn-block mt-1'"/>
|
|
</div>
|
|
</div>
|
|
`,
|
|
methods: {
|
|
formatDate(ts) { return window.moment.unix(ts).format('DD.MM HH:mm'); },
|
|
async addEntry() {
|
|
if (!this.canSend) return;
|
|
this.adding = 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.newMessage });
|
|
if (data.success) { this.newMessage = ''; this.$emit('refresh'); }
|
|
} catch (e) { window.notify('error', 'Fehler'); } finally { this.adding = false; }
|
|
}
|
|
}
|
|
});
|
|
|
|
// Documents Component (Upload + List)
|
|
Vue.component('workorder-mph-documents', {
|
|
props: { docs: { type: Array, default: () => [] }, workorderMphId: { type: [Number, String], required: true }, isAdmin: { type: Boolean, default: false } },
|
|
data: () => ({
|
|
uploading: false,
|
|
isDragging: false,
|
|
uploadData: { files: [], documentType: '', description: '' },
|
|
requiredDocs: [
|
|
{ key: 'photo_hak_mounted', label: 'Foto vom montierten HAK' },
|
|
{ key: 'photo_hak_open', label: 'Foto von dem offenen HAK' },
|
|
{ key: 'photo_splice_cassette_hak', label: 'Foto der Spleißkasette - HAK' },
|
|
{ key: 'photo_splice_cassette_fcp', label: 'Foto der Spleißkasette - FCP' },
|
|
{ key: 'photo_fcp_labeled', label: 'Foto vom FCP beschriftet' },
|
|
{ key: 'photo_patch_pos_osp', label: 'Foto der Patch-Position - OSP-Seite' },
|
|
{ key: 'photo_patch_pos_anb', label: 'Foto der Patch-Position - ANB-Seite' },
|
|
{ key: 'otdr_measurement', label: 'ODTR - Messung (1310nm & 1550nm)' },
|
|
{ key: 'other', label: 'Sonstige' }
|
|
]
|
|
}),
|
|
computed: {
|
|
docOptions() {
|
|
return [
|
|
{ value: '', text: '-- Bitte wählen --' },
|
|
...this.requiredDocs.map(d => ({ value: d.key, text: d.label }))
|
|
];
|
|
}
|
|
},
|
|
template: `
|
|
<div>
|
|
<div class="workorder-mph-card mb-3">
|
|
<div class="workorder-mph-card-header"><i class="fas fa-upload"></i> Dokument hochladen</div>
|
|
<div class="workorder-mph-card-body">
|
|
<div class="row">
|
|
<div class="col-md-6 mb-2">
|
|
<tt-select label="Typ *" :options="docOptions" v-model="uploadData.documentType" sm no-form-group class="mb-2"/>
|
|
<label class="small font-weight-bold mb-1">Beschreibung</label>
|
|
<input type="text" class="form-control form-control-sm mb-2" v-model="uploadData.description" placeholder="Optional">
|
|
</div>
|
|
<div class="col-md-6 mb-2">
|
|
<label class="small font-weight-bold mb-1">Datei(en)</label>
|
|
<div class="mph-drop-zone"
|
|
:class="{ 'dragging': isDragging }"
|
|
@dragover.prevent="isDragging = true"
|
|
@dragleave.prevent="isDragging = false"
|
|
@drop.prevent="handleDrop">
|
|
<input type="file" ref="fileInput" @change="handleFileSelect" multiple accept="image/*,.pdf">
|
|
<i class="fas fa-cloud-upload-alt mph-drop-zone-icon"></i>
|
|
<div class="mph-drop-zone-text">Dateien auswählen</div>
|
|
<div class="mph-drop-zone-hint">oder hierher ziehen</div>
|
|
</div>
|
|
|
|
<div v-if="uploadData.files.length" class="mph-file-list">
|
|
<div v-for="(file, index) in uploadData.files" :key="index" class="mph-file-item">
|
|
<i class="fas fa-file mr-2 text-muted"></i>
|
|
<span class="text-truncate" style="max-width: 180px;">{{ file.name }}</span>
|
|
<span class="remove-file" @click="removeFile(index)"><i class="fas fa-times"></i></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="text-right mt-2">
|
|
<tt-button icon="fas fa-upload" text="Hochladen" @click="uploadFiles" :loading="uploading" additional-class="btn-primary btn-sm"/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="workorder-mph-card">
|
|
<div class="workorder-mph-card-header">
|
|
<span><i class="fas fa-folder-open"></i> Dokumente</span>
|
|
<span class="badge badge-secondary">{{ docs.length }}</span>
|
|
</div>
|
|
<div class="workorder-mph-card-body">
|
|
<div v-if="docs.length === 0" class="text-muted small text-center">Keine Dokumente</div>
|
|
<tt-file-gallery v-else :files="docs" :edit-mode="false" :delete-mode="!isAdmin" @delete-file="deleteDoc"/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`,
|
|
methods: {
|
|
handleFileSelect(event) {
|
|
this.addFiles(event.target.files);
|
|
},
|
|
handleDrop(event) {
|
|
this.isDragging = false;
|
|
this.addFiles(event.dataTransfer.files);
|
|
},
|
|
addFiles(fileList) {
|
|
if (!fileList.length) return;
|
|
// Convert FileList to Array and append
|
|
this.uploadData.files = [...this.uploadData.files, ...Array.from(fileList)];
|
|
},
|
|
removeFile(index) {
|
|
this.uploadData.files.splice(index, 1);
|
|
},
|
|
async uploadFiles() {
|
|
if (!this.uploadData.files.length || !this.uploadData.documentType) return window.notify('error', 'Bitte Typ und Datei wä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);
|
|
// Handle multiple files
|
|
for (let i = 0; i < this.uploadData.files.length; i++) {
|
|
formData.append('files[]', this.uploadData.files[i]);
|
|
}
|
|
|
|
try {
|
|
const basePath = this.isAdmin ? '/WorkorderMphAdmin' : '/WorkorderMphCompany';
|
|
const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}${basePath}/uploadDocumentation`, formData);
|
|
if (data.success) {
|
|
window.notify('success', 'OK');
|
|
this.uploadData = { files: [], documentType: '', description: '' };
|
|
this.$refs.fileInput.value='';
|
|
this.$emit('refresh');
|
|
} else {
|
|
window.notify('error', data.message || 'Upload fehlgeschlagen');
|
|
}
|
|
} catch (e) { window.notify('error', 'Upload Fehler'); } finally { this.uploading = false; }
|
|
},
|
|
async deleteDoc(file) {
|
|
try {
|
|
const basePath = this.isAdmin ? '/WorkorderMphAdmin' : '/WorkorderMphCompany';
|
|
await axios.post(`${window.TT_CONFIG.BASE_PATH}${basePath}/deleteDocumentation`, { documentationId: file.id });
|
|
window.notify('success', 'Gelöscht'); this.$emit('refresh');
|
|
} catch (e) { window.notify('error', 'Fehler'); }
|
|
}
|
|
}
|
|
});
|
|
|
|
// Admin Review Component
|
|
Vue.component('workorder-mph-admin-review', {
|
|
props: { docs: { type: Array, default: () => [] }, workorderMphId: { type: [Number, String], required: true } },
|
|
data: () => ({
|
|
checklist: [
|
|
{ key: 'photo_hak_mounted', label: 'Foto vom montierten HAK', icon: 'fas fa-camera' },
|
|
{ key: 'photo_hak_open', label: 'Foto von dem offenen HAK', icon: 'fas fa-camera' },
|
|
{ key: 'photo_splice_cassette_hak', label: 'Foto der Spleißkasette - HAK', icon: 'fas fa-camera' },
|
|
{ key: 'photo_splice_cassette_fcp', label: 'Foto der Spleißkasette - FCP', icon: 'fas fa-camera' },
|
|
{ key: 'photo_fcp_labeled', label: 'Foto vom FCP beschriftet', icon: 'fas fa-tag' },
|
|
{ key: 'photo_patch_pos_osp', label: 'Foto der Patch-Position - OSP-Seite', icon: 'fas fa-ethernet' },
|
|
{ key: 'photo_patch_pos_anb', label: 'Foto der Patch-Position - ANB-Seite', icon: 'fas fa-ethernet' },
|
|
{ key: 'otdr_measurement', label: 'ODTR - Messung (1310nm & 1550nm)', icon: 'fas fa-chart-line' }
|
|
],
|
|
acceptModal: false
|
|
}),
|
|
computed: {
|
|
canAccept() {
|
|
return this.checklist.every(item => this.hasDoc(item.key));
|
|
}
|
|
},
|
|
methods: {
|
|
hasDoc(key) { return this.docs.some(d => d.documentType === key); },
|
|
async accept() {
|
|
try {
|
|
const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderMphAdmin/acceptDocumentation`, { workorderId: this.workorderMphId });
|
|
if (data.success) { window.notify('success', 'Akzeptiert'); this.acceptModal = false; this.$emit('refresh'); }
|
|
} catch (e) { window.notify('error', 'Fehler'); }
|
|
}
|
|
},
|
|
template: `
|
|
<div class="workorder-mph-card">
|
|
<div class="workorder-mph-card-header"><i class="fas fa-check-circle"></i> Prüfung & Freigabe</div>
|
|
<div class="workorder-mph-card-body-compact">
|
|
<div class="mph-doc-checklist mb-2">
|
|
<div v-for="item in checklist" :key="item.key" class="mph-doc-check-item" :class="{ 'has-doc': hasDoc(item.key) }">
|
|
<i :class="item.icon" class="mr-2" style="width: 16px; text-align: center;"></i>
|
|
<span class="flex-grow-1">{{ item.label }}</span>
|
|
<i v-if="hasDoc(item.key)" class="fas fa-check text-success"></i>
|
|
<i v-else class="fas fa-times text-muted" style="opacity: 0.3"></i>
|
|
</div>
|
|
</div>
|
|
<tt-button text="Akzeptieren & Abschluss" @click="acceptModal = true" :disabled="!canAccept" additional-class="btn-success btn-sm btn-block" icon="fas fa-check-double"/>
|
|
|
|
<tt-modal :show.sync="acceptModal" title="Freigabe" @submit="accept" :delete="false">
|
|
Dokumentation akzeptieren und Auftrag abschließen?
|
|
</tt-modal>
|
|
</div>
|
|
</div>
|
|
`
|
|
});
|
|
|
|
// Legacy Wrapper for compatibility
|
|
Vue.component('workorder-mph-details-manager', {
|
|
props: { workorderMphId: { type: [Number, String], required: true }, isAdmin: { type: Boolean, default: false } },
|
|
template: `
|
|
<workorder-mph-data-provider :workorder-mph-id="workorderMphId" v-slot="{ docs, journals, refresh, loading }">
|
|
<div class="mph-details-stack">
|
|
<div v-if="loading && !docs.length" class="text-center"><i class="fas fa-spinner fa-spin"></i></div>
|
|
<template v-else>
|
|
<workorder-mph-admin-review v-if="isAdmin" :docs="docs" :workorder-mph-id="workorderMphId" @refresh="refresh" />
|
|
<!-- Company Complete Logic (Inline simplified) -->
|
|
<div v-if="!isAdmin" class="workorder-mph-card mb-3">
|
|
<div class="workorder-mph-card-header"><i class="fas fa-check-double"></i> Abschluss</div>
|
|
<div class="workorder-mph-card-body-compact">
|
|
<tt-button text="Fertigmelden" @click="completeWorkorder" additional-class="btn-success btn-block" icon="fas fa-flag-checkered"/>
|
|
</div>
|
|
</div>
|
|
<workorder-mph-documents :docs="docs" :workorder-mph-id="workorderMphId" :is-admin="isAdmin" @refresh="refresh" />
|
|
<workorder-mph-journal :journals="journals" :workorder-mph-id="workorderMphId" :is-admin="isAdmin" @refresh="refresh" />
|
|
</template>
|
|
</div>
|
|
</workorder-mph-data-provider>
|
|
`,
|
|
methods: {
|
|
async completeWorkorder() {
|
|
if (!confirm('Wirklich abschließen?')) return;
|
|
try {
|
|
const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderMphCompany/completeWorkorder`, { workorderId: this.workorderMphId });
|
|
if (data.success) { window.notify('success', 'Erledigt'); this.$emit('workorder-completed'); }
|
|
} catch (e) { window.notify('error', 'Fehler'); }
|
|
}
|
|
}
|
|
}); |