Files
thetool/public/js/pages/WorkorderTenantConfig/WorkorderTenantConfig.js
2025-12-13 21:27:43 +00:00

373 lines
19 KiB
JavaScript

// WorkorderTenantConfig.js
Vue.component('workorder-tenant-config', {
template: `
<tt-card>
<div class="d-flex justify-content-between align-items-center mb-3">
<ul class="nav nav-pills">
<li class="nav-item"><a class="nav-link" :class="{active: activeTab === 'configs'}" href="#"
@click.prevent="activeTab = 'configs'"><i class="fas fa-cogs mr-1"></i>
Mandanten-Konfigurationen</a></li>
<li class="nav-item"><a class="nav-link" :class="{active: activeTab === 'companies'}" href="#"
@click.prevent="activeTab = 'companies'"><i class="fas fa-building mr-1"></i>
Firmenverwaltung</a></li>
</ul>
<tt-button :text="activeTab === 'configs' ? 'Neue Konfiguration' : 'Neue Firma'" @click="openCreateModal"
icon="fas fa-plus" additional-class="btn-primary"/>
</div>
<div v-if="loading" class="text-center p-5"><i class="fas fa-spinner fa-spin fa-2x"></i></div>
<div v-show="!loading && activeTab === 'configs'">
<div v-for="config in configs" :key="config.id" class="card mb-3 config-card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="fas fa-file-alt mr-2"></i>
<tt-autocomplete v-if="editingId === config.id" :api-url="addressApiUrl"
v-model="editableItem.addressId" @input="editableItem.name = $event.text" no-form-group
sm/>
<strong v-else>{{ config.name }}</strong>
</h5>
<div>
<tt-button v-if="editingId !== config.id" @click="startEdit(config)" icon="fas fa-edit"
additional-class="btn-sm btn-outline-primary mr-2"/>
<tt-button v-if="editingId === config.id" @click="saveItem" icon="fas fa-save"
additional-class="btn-sm btn-success mr-2"/>
<tt-button v-if="editingId === config.id" @click="cancelEdit" icon="fas fa-times"
additional-class="btn-sm btn-secondary mr-2"/>
<tt-button @click="deleteItem(config, 'configs')" icon="fas fa-trash"
additional-class="btn-sm btn-outline-danger"/>
</div>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6>Dokumentationstypen</h6>
<tt-positions-manager :config="docTypesConfig" v-model="editableItem.documentationTypes"
v-if="editingId === config.id"/>
<ul v-else class="list-group list-group-flush type-list">
<li v-for="doc in JSON.parse(config.documentationTypes || '[]')" class="list-group-item">
<span>{{ doc.text }}</span><small class="code-font">{{ doc.value }}</small></li>
</ul>
</div>
<div class="col-md-6">
<h6>Interventionstypen</h6>
<tt-positions-manager :config="interventionTypesConfig" v-model="editableItem.interventionTypes"
v-if="editingId === config.id"/>
<ul v-else class="list-group list-group-flush type-list">
<li v-for="intervention in JSON.parse(config.interventionTypes || '[]')" class="list-group-item">
<span>{{ intervention.text }}</span><small class="code-font">{{ intervention.value }}</small></li>
</ul>
</div>
</div>
<hr>
<div class="row mt-3">
<div class="col-md-6">
<h6>Filter für Auftragserstellung</h6>
<pre v-if="editingId !== config.id"
class="code-block">{{ JSON.stringify(JSON.parse(config.workorderCreationFilters || '{}'), null, 2) }}</pre>
<tt-textarea v-else v-model="editableJsonFilter" rows="6"/>
</div>
<div class="col-md-6">
<h6>Filter für aktive Arbeitsaufträge</h6>
<pre v-if="editingId !== config.id"
class="code-block">{{ JSON.stringify(JSON.parse(config.workorderActiveFilters || '{}'), null, 2) }}</pre>
<tt-textarea v-else v-model="editableJsonActiveFilter" rows="6"/>
</div>
</div>
<div class="row mt-3">
<div class="col-md-6">
<h6 class="mb-3">Optionen</h6>
<div v-if="editingId === config.id">
<tt-checkbox label="Workorder aktivieren"
v-model="editableItem.enableWorkorder" sm/>
<tt-checkbox label="WorkorderMPH aktivieren"
v-model="editableItem.enableWorkorderMph" sm/>
<hr>
<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>Workorder: <strong>{{ config.enableWorkorder ? 'Aktiviert' : 'Deaktiviert' }}</strong></p>
<p>WorkorderMPH: <strong>{{ config.enableWorkorderMph ? 'Aktiviert' : 'Deaktiviert' }}</strong></p>
<hr>
<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>
<ul v-if="companiesByTenantMap[config.addressId] && companiesByTenantMap[config.addressId].length"
class="list-group">
<li v-for="company in companiesByTenantMap[config.addressId]" class="list-group-item py-1"><i
class="fas fa-building mr-2 text-muted"></i>{{ company.name }}
</li>
</ul>
<div v-else class="text-muted p-3">Keine Firmen zugeordnet.</div>
</div>
</div>
</div>
</div>
</div>
<div v-show="!loading && activeTab === 'companies'">
<div v-for="company in companies" :key="company.id" class="card mb-3 config-card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="fas fa-building mr-2"></i><strong>{{ company.name }}</strong></h5>
<div>
<tt-button v-if="editingId !== company.id" @click="startEdit(company)" icon="fas fa-edit"
additional-class="btn-sm btn-outline-primary mr-2"/>
<tt-button v-if="editingId === company.id" @click="saveItem" icon="fas fa-save"
additional-class="btn-sm btn-success mr-2"/>
<tt-button v-if="editingId === company.id" @click="cancelEdit" icon="fas fa-times"
additional-class="btn-sm btn-secondary mr-2"/>
<tt-button @click="deleteItem(company, 'companies')" icon="fas fa-trash"
additional-class="btn-sm btn-outline-danger"/>
</div>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6>Sichtbar für Mandanten</h6>
<div v-if="editingId === company.id">
<ul class="list-group tenant-edit-list mb-2">
<li v-for="(tenantId, index) in editableItem.visibleForAddressId" :key="tenantId"
class="list-group-item d-flex justify-content-between align-items-center">
<tt-resolver :value="tenantId" reference="/WorkorderTenantConfig/addressAutocomplete"
:autocomplete="true"/>
<tt-button icon="fas fa-trash" @click="removeTenant(index)"
additional-class="btn-sm btn-link text-danger"/>
</li>
</ul>
<tt-autocomplete :api-url="addressApiUrl" v-model="tenantToAdd" @input="addTenant"
placeholder="Mandant hinzufügen..." sm no-form-group/>
</div>
<ul v-else class="list-group list-group-flush">
<li v-for="tenantId in JSON.parse(company.visibleForAddressId || '[]')"
class="list-group-item py-1">
<tt-resolver :value="tenantId" reference="/WorkorderTenantConfig/addressAutocomplete"
:autocomplete="true"/>
</li>
</ul>
</div>
<div class="col-md-6">
<h6>Zugehörige Mitarbeiter <small>(mit RMLCompany-Recht)</small></h6>
<ul v-if="company.workers && company.workers.length" class="list-group list-group-flush">
<li v-for="worker in company.workers" class="list-group-item py-1">{{ worker.name }} (
{{ worker.email }})
</li>
</ul>
<div v-else class="text-muted p-3">Keine Mitarbeiter für diese Firma gefunden.</div>
</div>
</div>
</div>
</div>
</div>
<tt-modal v-if="showModal" :show.sync="showModal" :title="modalTitle" @submit="saveNewItem" :delete="false">
<div v-if="activeTab === 'configs'">
<tt-autocomplete label="Mandant" :api-url="addressApiUrl" v-model="newItem.addressId"
@input="newItem.name = $event.text" sm row required/>
</div>
<div v-if="activeTab === 'companies'">
<tt-autocomplete label="Firma" :api-url="addressApiUrl" v-model="newItem.addressId"
@input="newItem.name = $event.text" sm row required/>
<div class="form-group row">
<label class="col-form-label col-sm-4 col-form-label-sm">Sichtbar für Mandanten</label>
<div class="col-sm-8">
<div class="tenant-tags-container mb-2">
<span v-for="(tenantId, index) in newItem.visibleForAddressId" :key="tenantId"
class="badge badge-primary tenant-tag">
<tt-resolver :value="tenantId" reference="/WorkorderTenantConfig/addressAutocomplete"
:autocomplete="true"/>
<i class="fas fa-times-circle" @click="removeTenant(index, true)"></i>
</span>
</div>
<tt-autocomplete :api-url="addressApiUrl" v-model="tenantToAdd" @input="addTenant($event, true)"
placeholder="Mandant hinzufügen..." sm no-form-group/>
</div>
</div>
</div>
</tt-modal>
</tt-card>
`,
data() {
return {
loading: true, activeTab: 'configs', configs: [], companies: [], editingId: null, editableItem: {},
editableJsonFilter: '', editableJsonActiveFilter: '', showModal: false, newItem: {}, tenantToAdd: null,
addressApiUrl: `${window.TT_CONFIG.BASE_PATH}/WorkorderTenantConfig/addressAutocomplete`,
docTypesConfig: {
fields: {
text: {type: 'input', label: 'Anzeigename'},
value: {type: 'input', label: 'Schlüssel'}
}
},
interventionTypesConfig: {
fields: {
text: {type: 'input', label: 'Anzeigename'},
value: {type: 'input', label: 'Schlüssel'}
}
},
};
},
computed: {
modalTitle() {
return this.activeTab === 'configs' ? 'Neue Konfiguration erstellen' : 'Neue Firma anlegen';
},
companiesByTenantMap() {
const map = {};
this.configs.forEach(config => {
map[config.addressId] = [];
});
this.companies.forEach(company => {
try {
const visibleFor = JSON.parse(company.visibleForAddressId || '[]');
if (Array.isArray(visibleFor)) {
visibleFor.forEach(tenantId => {
if (map[tenantId]) map[tenantId].push(company);
});
}
} catch (e) {
}
});
return map;
}
},
methods: {
async fetchData() {
this.loading = true;
try {
const [configsRes, companiesRes] = await Promise.all([
axios.get(`${window.TT_CONFIG.BASE_PATH}/WorkorderTenantConfig/getTenantConfigs`),
axios.get(`${window.TT_CONFIG.BASE_PATH}/WorkorderTenantConfig/getCompanies`),
]);
this.configs = configsRes.data;
this.companies = companiesRes.data;
} catch (e) {
window.notify('error', 'Daten konnten nicht geladen werden.');
} finally {
this.loading = false;
}
},
startEdit(item) {
this.editingId = item.id;
this.editableItem = JSON.parse(JSON.stringify(item));
if (this.activeTab === 'configs') {
this.editableItem.documentationTypes = JSON.parse(this.editableItem.documentationTypes || '[]');
this.editableItem.interventionTypes = JSON.parse(this.editableItem.interventionTypes || '[]');
this.editableJsonFilter = JSON.stringify(JSON.parse(this.editableItem.workorderCreationFilters || '{}'), null, 2);
this.editableItem.workorderActiveFilters = this.editableItem.workorderActiveFilters || '{}';
this.editableJsonActiveFilter = JSON.stringify(JSON.parse(this.editableItem.workorderActiveFilters), null, 2);
} else {
try {
this.editableItem.visibleForAddressId = JSON.parse(this.editableItem.visibleForAddressId || '[]');
} catch (e) {
this.editableItem.visibleForAddressId = [];
}
}
},
cancelEdit() {
this.editingId = null;
this.editableItem = {};
this.editableJsonFilter = '';
this.editableJsonActiveFilter = '';
},
async saveItem() {
const endpoint = this.activeTab === 'configs' ? 'saveTenantConfig' : 'saveCompany';
let payload = {...this.editableItem};
if (this.activeTab === 'configs') {
try {
JSON.parse(this.editableJsonFilter);
payload.workorderCreationFilters = this.editableJsonFilter;
} catch (e) {
return window.notify('error', 'Filter für Auftragserstellung ist kein valides JSON.');
}
try {
JSON.parse(this.editableJsonActiveFilter);
payload.workorderActiveFilters = this.editableJsonActiveFilter;
} catch (e) {
return window.notify('error', 'Filter für aktive Arbeitsaufträge ist kein valides JSON.');
}
}
try {
const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderTenantConfig/${endpoint}`, payload);
if (data.success) {
window.notify('success', data.message);
this.cancelEdit();
await this.fetchData();
} else {
window.notify('error', data.message || 'Speichern fehlgeschlagen.');
}
} catch (e) {
window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.');
}
},
async deleteItem(item, type) {
if (!confirm(`Soll der Eintrag "${item.name}" wirklich gelöscht werden?`)) return;
const endpoint = type === 'configs' ? 'deleteTenantConfig' : 'deleteCompany';
try {
const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderTenantConfig/${endpoint}`, {id: item.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', 'Ein Netzwerkfehler ist aufgetreten.');
}
},
openCreateModal() {
this.newItem = this.activeTab === 'configs'
? {
documentationTypes: [],
interventionTypes: [],
workorderCreationFilters: '{}',
workorderActiveFilters: '{}',
civilEngineeringDocsRequired: 0,
requireCableLength: 0,
requireCableType: 0,
enableWorkorder: 1,
enableWorkorderMph: 1
}
: {visibleForAddressId: []};
this.showModal = true;
},
async saveNewItem() {
const endpoint = this.activeTab === 'configs' ? 'saveTenantConfig' : 'saveCompany';
try {
const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderTenantConfig/${endpoint}`, this.newItem);
if (data.success) {
window.notify('success', data.message);
this.showModal = false;
await this.fetchData();
} else {
window.notify('error', data.message || 'Speichern fehlgeschlagen.');
}
} catch (e) {
window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.');
}
},
addTenant(tenant, isNewItem = false) {
const tenantId = typeof tenant === 'object' && tenant !== null ? tenant.value : tenant;
const targetArray = isNewItem ? this.newItem.visibleForAddressId : this.editableItem.visibleForAddressId;
if (tenantId && !targetArray.includes(tenantId)) targetArray.push(tenantId);
this.$nextTick(() => {
this.tenantToAdd = null;
});
},
removeTenant(index, isNewItem = false) {
const targetArray = isNewItem ? this.newItem.visibleForAddressId : this.editableItem.visibleForAddressId;
targetArray.splice(index, 1);
},
},
async mounted() {
await this.fetchData();
}
});