improved ipam

This commit is contained in:
2025-08-26 17:18:58 +02:00
parent 68c4e50b20
commit 177a02c8c8
11 changed files with 324 additions and 71 deletions
@@ -7,7 +7,7 @@ Vue.component('r-m-l-workorder-admin', {
<div style="width: 300px;">
<tt-select
class="mb-0"
:options="companies"
:options="companiesForMassAssign"
v-model="massAssignCompanyId"
@input="massAssignCompanies"
placeholder="Firma auswählen..."
@@ -62,32 +62,35 @@ Vue.component('r-m-l-workorder-admin', {
<div class="d-flex justify-content-between align-items-center">
<div class="flex-grow-1">
<div v-if="editingWorkorderId === row.id">
<tt-select
:options="companies"
:value="row.companyId"
@input="assignCompany(row, $event)"
@blur="editingWorkorderId = null"
@keydown.esc.native="editingWorkorderId = null"
placeholder="Firma zuweisen..."
sm
no-form-group
<div v-if="companiesLoading" class="spinner-border spinner-border-sm"></div>
<tt-select v-else
:options="companiesByTenant[row.tenantId] || []"
:value="row.companyId"
@input="assignCompany(row, $event)"
@blur="editingWorkorderId = null"
@keydown.esc.native="editingWorkorderId = null"
placeholder="Firma zuweisen..."
sm
no-form-group
/>
</div>
<div v-else-if="row.status === 'new'">
<tt-select
:options="companies"
:value="row.companyId"
@input="assignCompany(row, $event)"
placeholder="Firma zuweisen..."
sm
no-form-group
<div v-if="companiesLoading" class="spinner-border spinner-border-sm"></div>
<tt-select v-else
:options="companiesByTenant[row.tenantId] || []"
:value="row.companyId"
@input="assignCompany(row, $event)"
@focus="getCompaniesForWorkorder(row)"
placeholder="Firma zuweisen..."
sm
no-form-group
/>
</div>
<div v-else class="d-flex align-items-center">
<span>{{ row.companyName || 'N/A' }}</span>
<tt-button
icon="fas fa-edit"
@click="editingWorkorderId = row.id"
@click="startCompanyEdit(row)"
additional-class="btn-link btn-sm p-0 ml-2"
title="Zuweisung ändern"
/>
@@ -156,9 +159,11 @@ Vue.component('r-m-l-workorder-admin', {
workordersToAssign: [],
editingWorkorderId: null,
editingDeadlineId: null,
companies: [],
companiesByTenant: {},
companiesLoading: false,
massAssignCompanyId: null,
massAssignLoading: false,
companiesForMassAssign: [],
crudConfig: {
...window.TT_CONFIG.CRUD_CONFIG,
selectable: false,
@@ -184,10 +189,6 @@ Vue.component('r-m-l-workorder-admin', {
}
}
},
async mounted() {
const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/getCompanies`);
this.companies = response.data;
},
methods: {
addToAssignList(row) {
if (!this.workordersToAssign.includes(row.id)) {
@@ -205,6 +206,24 @@ Vue.component('r-m-l-workorder-admin', {
if (!timestamp) return '';
return window.moment.unix(timestamp).format('DD.MM.YYYY');
},
async getCompaniesForWorkorder(workorder) {
if (!workorder.tenantId || this.companiesByTenant[workorder.tenantId]) {
return;
}
this.companiesLoading = true;
try {
const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/getCompanies`, { params: { tenantId: workorder.tenantId } });
this.$set(this.companiesByTenant, workorder.tenantId, response.data);
} catch (e) {
window.notify('error', 'Firmenliste konnte nicht geladen werden.');
} finally {
this.companiesLoading = false;
}
},
async startCompanyEdit(row) {
await this.getCompaniesForWorkorder(row);
this.editingWorkorderId = row.id;
},
async assignCompany(workorder, companyId) {
if (!companyId) {
this.editingWorkorderId = null;
@@ -320,6 +339,36 @@ Vue.component('r-m-l-workorder-admin', {
window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.');
}
}
},
watch: {
workordersToAssign: {
async handler(newVal) {
if (newVal.length === 0) {
this.companiesForMassAssign = [];
return;
}
const firstWorkorderId = newVal[0];
const firstWorkorder = this.$refs.table.$refs.table.rows.find(r => r.id === firstWorkorderId);
if (!firstWorkorder) return;
const firstTenantId = firstWorkorder.tenantId;
const allSameTenant = newVal.every(id => {
const wo = this.$refs.table.$refs.table.rows.find(r => r.id === id);
return wo && wo.tenantId === firstTenantId;
});
if (!allSameTenant) {
window.notify('error', 'Massen-Zuweisung nur für Aufträge des gleichen Mandanten möglich.');
this.workordersToAssign.pop(); // remove last added
return;
}
await this.getCompaniesForWorkorder(firstWorkorder);
this.companiesForMassAssign = this.companiesByTenant[firstTenantId] || [];
},
deep: true
}
}
});
@@ -365,7 +414,8 @@ Vue.component('rml-documentation-viewer-admin', {
<div class="card-header"><h5><i class="fas fa-check-circle text-success mr-2"></i>Dokumentation akzeptieren</h5></div>
<div class="card-body">
<p class="small text-muted">Prüfen Sie, ob alle erforderlichen Dokumente vorhanden und korrekt sind.</p>
<ul class="list-unstyled">
<div v-if="loadingConfig" class="text-center"><i class="fas fa-spinner fa-spin"></i></div>
<ul v-else class="list-unstyled">
<li v-for="docType in requiredDocTypes" :key="docType.value" class="mb-2 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>
@@ -409,6 +459,7 @@ Vue.component('rml-documentation-viewer-admin', {
data() {
return {
loading: true,
loadingConfig: true,
correctionLoading: false,
docs: [],
journals: [],
@@ -416,7 +467,16 @@ Vue.component('rml-documentation-viewer-admin', {
correctionText: '',
newJournalMessage: '',
addingJournalEntry: false,
requiredDocTypes: [
tenantDocTypes: null,
}
},
computed: {
requiredDocTypes() {
if (this.tenantDocTypes) {
return this.tenantDocTypes;
}
// Fallback for RML default
return [
{ value: 'photo_hup_mounted', text: 'Foto vom montierten HÜP' },
{ value: 'photo_hup_open', text: 'Foto von dem offenen HÜP' },
{ value: 'photo_splice_cassette_hup', text: 'Foto der Spleißkassette HÜP' },
@@ -426,7 +486,7 @@ Vue.component('rml-documentation-viewer-admin', {
{ value: 'photo_patch_position_osp', text: 'Foto der Patch-Position - OSP-Seite' },
{ value: 'photo_patch_position_anb', text: 'Foto der Patch-Position - ANB-Seite' },
{ value: 'measurement_protocol_otdr', text: 'ODTR Messung (1310nm & 1550nm)' },
]
];
}
},
methods: {
@@ -445,6 +505,21 @@ Vue.component('rml-documentation-viewer-admin', {
this.loading = false;
}
},
async loadTenantConfig() {
this.loadingConfig = true;
try {
const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/getTenantConfig`, {
params: { workorderId: this.workorderId }
});
if (response.data.success) {
this.tenantDocTypes = response.data.documentationTypes;
}
} catch (e) {
console.error("Could not load tenant documentation config", e);
} finally {
this.loadingConfig = false;
}
},
async requestCorrection() {
if (!this.correctionText) return window.notify('error', 'Bitte geben Sie einen Grund für die Korrektur an.');
if (this.selectedDocs.length === 0) return window.notify('error', 'Bitte wählen Sie mindestens ein Dokument für die Korrektur aus.');
@@ -498,7 +573,8 @@ Vue.component('rml-documentation-viewer-admin', {
return window.moment.unix(timestamp).format('DD.MM.YYYY HH:mm');
},
},
mounted() {
this.fetchData();
async mounted() {
await this.loadTenantConfig();
await this.fetchData();
}
});
@@ -22,7 +22,7 @@ Vue.component('r-m-l-workorder-company', {
</template>
<template v-slot:appointmentdate="{ row }">
<div v-if="!row.appointmentDate && ['assigned', 'correction_requested'].includes(row.status)">
<div v-if="!row.appointmentDate && ['assigned', 'correction_requested', 'problem_solved'].includes(row.status)">
<tt-date-picker
placeholder="Termin festlegen..."
:date-range="false"
@@ -193,7 +193,7 @@ Vue.component('documentation-manager', {
props: ['workorderId'],
template: `
<div class="p-3 bg-light" style="width: 100%;">
<div v-if="loadingWorkorder" class="text-center p-5"><i class="fas fa-spinner fa-spin fa-2x"></i></div>
<div v-if="loadingWorkorder || loadingConfig" 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 mb-3 mb-lg-0">
<div>
@@ -327,6 +327,8 @@ Vue.component('documentation-manager', {
data() {
return {
loadingWorkorder: true,
loadingConfig: true,
tenantDocTypes: null,
workorder: null,
uploading: false,
completing: false,
@@ -346,8 +348,16 @@ Vue.component('documentation-manager', {
files: [],
documentType: 'photo_hup_mounted',
description: ''
},
requiredDocTypes: [
}
}
},
computed: {
requiredDocTypes() {
if (this.tenantDocTypes) {
return this.tenantDocTypes;
}
// Fallback for RML default
return [
{ value: 'photo_hup_mounted', text: 'Foto vom montierten HÜP' },
{ value: 'photo_hup_open', text: 'Foto von dem offenen HÜP' },
{ value: 'photo_splice_cassette_hup', text: 'Foto der Spleißkassette HÜP' },
@@ -357,10 +367,8 @@ Vue.component('documentation-manager', {
{ value: 'photo_patch_position_osp', text: 'Foto der Patch-Position - OSP-Seite' },
{ value: 'photo_patch_position_anb', text: 'Foto der Patch-Position - ANB-Seite' },
{ value: 'measurement_protocol_otdr', text: 'ODTR Messung (1310nm & 1550nm)' },
]
}
},
computed: {
];
},
allDocTypes() {
return [
...this.requiredDocTypes,
@@ -403,6 +411,21 @@ Vue.component('documentation-manager', {
if (!timestamp) return '';
return window.moment.unix(timestamp).format('DD.MM.YYYY HH:mm');
},
async loadTenantConfig() {
this.loadingConfig = true;
try {
const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/getTenantConfig`, {
params: { workorderId: this.workorderId }
});
if (response.data.success) {
this.tenantDocTypes = response.data.documentationTypes;
}
} catch (e) {
console.error("Could not load tenant documentation config", e);
} finally {
this.loadingConfig = false;
}
},
async loadWorkorder() {
this.loadingWorkorder = true;
try {
@@ -545,7 +568,6 @@ Vue.component('documentation-manager', {
}
let journalParts = [];
// Sort types to have a consistent order in the journal message
types.sort();
for (const type of types) {
@@ -599,6 +621,7 @@ Vue.component('documentation-manager', {
},
async mounted() {
await this.loadWorkorder();
await this.loadTenantConfig();
await this.fetchDocs();
}
});