diff --git a/application/WorkorderCompany/WorkorderCompanyModel.php b/application/WorkorderCompany/WorkorderCompanyModel.php index b24193820..b60f07165 100644 --- a/application/WorkorderCompany/WorkorderCompanyModel.php +++ b/application/WorkorderCompany/WorkorderCompanyModel.php @@ -8,4 +8,23 @@ class WorkorderCompanyModel extends TTCrudBaseModel { public ?string $visibleForAddressId; public int $create; public int $createBy; + + public static function getCompanyWorkers(int $companyId): array { + if (!$company = self::get($companyId)) { + return []; + } + + $db = self::getDB(); + $addressId = $db->real_escape_string($company->addressId); + + $sql = "SELECT w.id, w.name, w.email + FROM `" . FRONKDB_DBNAME . "`.`Worker` w + JOIN `" . FRONKDB_DBNAME . "`.`WorkerPermission` wp ON w.id = wp.worker_id + WHERE w.address_id = '$addressId' AND wp.canRMLCompany = 'true' AND w.active = 1 + ORDER BY w.name ASC"; + + $result = $db->query($sql); + + return $result ? $result->fetch_all(MYSQLI_ASSOC) : []; + } } \ No newline at end of file diff --git a/application/WorkorderTenantConfig/WorkorderTenantConfigController.php b/application/WorkorderTenantConfig/WorkorderTenantConfigController.php new file mode 100644 index 000000000..50027294f --- /dev/null +++ b/application/WorkorderTenantConfig/WorkorderTenantConfigController.php @@ -0,0 +1,93 @@ +headerTitle, []); + } + + protected function getTenantConfigsAction() { + $configs = WorkorderTenantConfigModel::getAll([], null, 0, ['key' => 'name', 'order' => 'ASC']); + self::returnJson($configs); + } + + protected function saveTenantConfigAction() { + $data = $this->postData; + $data['documentationTypes'] = json_encode($data['documentationTypes'] ?? []); + $data['interventionTypes'] = json_encode($data['interventionTypes'] ?? []); + $data['workorderCreationFilters'] ??= '{}'; + + if (empty($data['id'])) { + $data['create'] = time(); + $data['createBy'] = $this->user->id; + WorkorderTenantConfigModel::create($data); + } else { + WorkorderTenantConfigModel::update($data); + } + self::returnJson(['success' => true, 'message' => 'Mandanten-Konfiguration gespeichert.']); + } + + protected function deleteTenantConfigAction() { + if (empty($this->postData['id'])) self::sendError("ID fehlt."); + + WorkorderTenantConfigModel::delete($this->postData['id']); + self::returnJson(['success' => true, 'message' => 'Mandanten-Konfiguration gelöscht.']); + } + + protected function getCompaniesAction() { + $companies = WorkorderCompanyModel::getAll([], null, 0, ['key' => 'name', 'order' => 'ASC']); + foreach ($companies as $company) { + $company->workers = WorkorderCompanyModel::getCompanyWorkers($company->id); + } + self::returnJson($companies); + } + + protected function saveCompanyAction() { + $data = $this->postData; + if (empty($data['name']) || empty($data['addressId'])) self::sendError("Name und Adresse sind erforderlich."); + + unset($data['workers']); + $data['visibleForAddressId'] = json_encode($data['visibleForAddressId'] ?? []); + + if (empty($data['id'])) { + $data['create'] = time(); + $data['createBy'] = $this->user->id; + WorkorderCompanyModel::create($data); + } else { + WorkorderCompanyModel::update($data); + } + self::returnJson(['success' => true, 'message' => 'Firma gespeichert.']); + } + + protected function deleteCompanyAction() { + if (empty($this->postData['id'])) self::sendError("ID fehlt."); + + WorkorderCompanyModel::delete($this->postData['id']); + self::returnJson(['success' => true, 'message' => 'Firma gelöscht.']); + } + + protected function addressAutocompleteAction() { + $search = trim($this->request->q ?? ''); + $searchedID = $this->request->searchedID ?? null; + $addresses = []; + + if ($searchedID) { + $ids = array_filter(explode(',', $searchedID)); + if ($ids) $addresses = AddressModel::search(['id' => $ids]); + } elseif (strlen($search) >= 2) { + $addresses = array_slice(AddressModel::search(["company" => $search]), 0, 15); + } + + $results = array_map(function($address) { + return [ + 'value' => $address->id, + 'text' => "{$address->getCompanyOrName()} ({$address->zip} {$address->city})" + ]; + }, $addresses); + + self::returnJson($results); + } +} \ No newline at end of file diff --git a/public/js/pages/WorkorderTenantConfig/WorkorderTenantConfig.css b/public/js/pages/WorkorderTenantConfig/WorkorderTenantConfig.css new file mode 100644 index 000000000..1f9dee0d0 --- /dev/null +++ b/public/js/pages/WorkorderTenantConfig/WorkorderTenantConfig.css @@ -0,0 +1,88 @@ +/* WorkorderTenantConfig.css */ + +.config-card .card-header { + background-color: #f8f9fa; + border-bottom: 1px solid #dee2e6; +} + +.config-card .card-body h6 { + font-weight: 600; + color: #495057; + margin-bottom: 0.75rem; +} + +.config-card .list-group-item { + font-size: 0.9rem; + border-color: #f1f1f1; + padding-left: 0; + padding-right: 0; +} + +.code-block { + background-color: #e9ecef; + padding: 10px; + border-radius: 4px; + font-family: monospace; + font-size: 0.85rem; + white-space: pre-wrap; + max-height: 200px; + overflow-y: auto; +} + +.type-list .list-group-item { + display: flex; + flex-direction: column; + align-items: flex-start; + padding-top: .4rem; + padding-bottom: .4rem; +} + +.type-list .list-group-item span { + font-size: 0.95rem; + font-weight: 500; +} + +.type-list .list-group-item small.code-font { + font-family: monospace; + color: #6c757d; + background-color: #e9ecef; + padding: 0.1rem 0.4rem; + border-radius: 3px; + margin-top: 0.25rem; +} + +.tenant-tags-container { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + padding: 0.5rem; + background-color: #f8f9fa; + border-radius: 0.25rem; + min-height: 38px; +} + +.tenant-tag { + display: flex; + align-items: center; + padding: 0.3em 0.75em; + font-size: 0.9em; +} + +.tenant-tag i.fa-times-circle { + margin-left: 0.5em; + cursor: pointer; + opacity: 0.7; + transition: opacity 0.2s; +} + +.tenant-tag i.fa-times-circle:hover { + opacity: 1; +} + +.tenant-edit-list .list-group-item { + padding: 0.5rem 0.75rem; +} + +.tenant-edit-list .btn-link { + padding: 0; +} \ No newline at end of file diff --git a/public/js/pages/WorkorderTenantConfig/WorkorderTenantConfig.js b/public/js/pages/WorkorderTenantConfig/WorkorderTenantConfig.js new file mode 100644 index 000000000..cf23f8eb6 --- /dev/null +++ b/public/js/pages/WorkorderTenantConfig/WorkorderTenantConfig.js @@ -0,0 +1,333 @@ +// WorkorderTenantConfig.js +Vue.component('workorder-tenant-config', { + template: ` + +
+ + +
+ +
+ +
+
+
+
+ + + {{ config.name }} +
+
+ + + + +
+
+
+
+
+
Dokumentationstypen
+ +
    +
  • + {{ doc.text }}{{ doc.value }}
  • +
+
+
+
Interventionstypen
+ +
    +
  • + {{ intervention.text }}{{ intervention.value }}
  • +
+
+
+
+
+
+
Filter für Auftragserstellung
+
{{ JSON.stringify(JSON.parse(config.workorderCreationFilters || '{}'), null, 2) }}
+ +
+
+
Optionen
+ +

Tiefbau-Doku: {{ config.civilEngineeringDocsRequired ? 'Ja' : 'Nein' }}

+
+
+
Zugeordnete Firmen
+
    +
  • {{ company.name }} +
  • +
+
Keine Firmen zugeordnet.
+
+
+
+
+
+ +
+
+
+
{{ company.name }}
+
+ + + + +
+
+
+
+
+
Sichtbar für Mandanten
+
+
    +
  • + + +
  • +
+ +
+
    +
  • + +
  • +
+
+
+
Zugehörige Mitarbeiter (mit RMLCompany-Recht)
+
    +
  • {{ worker.name }} ( + {{ worker.email }}) +
  • +
+
Keine Mitarbeiter für diese Firma gefunden.
+
+
+
+
+
+ + +
+ +
+
+ +
+ +
+
+ + + + +
+ +
+
+
+
+
+ `, + data() { + return { + loading: true, activeTab: 'configs', configs: [], companies: [], editingId: null, editableItem: {}, + editableJsonFilter: '', 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); + } else { + try { + this.editableItem.visibleForAddressId = JSON.parse(this.editableItem.visibleForAddressId || '[]'); + } catch (e) { + this.editableItem.visibleForAddressId = []; + } + } + }, + cancelEdit() { + this.editingId = null; + this.editableItem = {}; + this.editableJsonFilter = ''; + }, + 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 { + 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: '{}', + civilEngineeringDocsRequired: 0 + } + : {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(); + } +}); \ No newline at end of file