From 8abf7444f3917859060f8a381e17fbcd6367dc4e Mon Sep 17 00:00:00 2001 From: Luca Haid Date: Tue, 9 Sep 2025 12:40:34 +0200 Subject: [PATCH] added new useredit page --- application/UserEdit/UserEditController.php | 170 +++++++++ .../UserPermissionTemplateModel.php | 20 + ...120000_create_user_permission_template.php | 33 ++ public/js/pages/UserEdit/UserEdit.css | 166 +++++++++ public/js/pages/UserEdit/UserEdit.js | 350 ++++++++++++++++++ .../UserPermissionTemplate.css | 49 +++ .../UserPermissionTemplate.js | 147 ++++++++ .../plugins/vue/tt-components/tt-tooltip.js | 2 +- 8 files changed, 936 insertions(+), 1 deletion(-) create mode 100644 application/UserEdit/UserEditController.php create mode 100644 application/UserPermissionTemplate/UserPermissionTemplateModel.php create mode 100644 db/migrations/20250909120000_create_user_permission_template.php create mode 100644 public/js/pages/UserEdit/UserEdit.css create mode 100644 public/js/pages/UserEdit/UserEdit.js create mode 100644 public/js/pages/UserPermissionTemplate/UserPermissionTemplate.css create mode 100644 public/js/pages/UserPermissionTemplate/UserPermissionTemplate.js diff --git a/application/UserEdit/UserEditController.php b/application/UserEdit/UserEditController.php new file mode 100644 index 000000000..c49d9f1f9 --- /dev/null +++ b/application/UserEdit/UserEditController.php @@ -0,0 +1,170 @@ +needlogin = true; + $this->user = new User(); + $this->user->loadMe(); + $this->layout()->set('me', $this->user); + + if (!$this->user->isAdmin()) { + $this->redirect("Dashboard"); + } + + // if post then set postData + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $this->postData = json_decode(file_get_contents('php://input'), true); + } + } + + protected function indexAction() { + $id = $this->request->id; + if (!is_numeric($id) || $id <= 0) throw new Exception("User ID is required.", 400); + + $user = new User($id); + if (!$user->id) throw new Exception("User not found.", 404); + + $preorderNetworks = $user->getFlag("preorder_networks")->value(); + $consentProjects = $user->getFlag("constructionConsent_projects")->value(); + + $userData = $user->toArray(); + $userData['permissions'] = (array)$user->permissions->data; + $userData['preorder_networks'] = $preorderNetworks ? json_decode($preorderNetworks, true) : []; + $userData['constructionconsent_projects'] = $consentProjects ? json_decode($consentProjects, true) : []; + $userData['employee_number'] = $user->getFlag("employee_number")->value(); + $userData['project_api_key'] = $user->getFlag("project_api_key")->value(); + $userData['vodia_identity_domain'] = $user->getFlag("vodia_identity_domain")->value(); + $userData['vodia_identity_username'] = $user->getFlag("vodia_identity_username")->value(); + $userData['vodia_identity_default'] = $user->getFlag("vodia_identity_default")->value(); + + + $JS_VARIABLES = [ + "USER_DATA" => $userData, + "LOOKUPS" => [ + "addresses" => array_map(fn($addr) => ['value' => $addr->id, 'text' => ($addr->company) ? $addr->company : $addr->getFullName()], AddressModel::getAll()), + "networks" => array_map(fn($net) => ['value' => $net->id, 'text' => $net->name], NetworkModel::getAll()), + "consentProjects" => array_map(fn($proj) => ['value' => $proj->id, 'text' => $proj->name], ConstructionConsentProject::getAll()), + "permissionTemplates" => UserPermissionTemplateModel::getAll([], null, 0, ['key' => 'name', 'order' => 'asc']), + "users" => array_map(fn($u) => ['value' => $u->id, 'text' => $u->name], UserModel::search(['active' => 1])), + ], + "PERMISSIONS_CONFIG" => $this->getPermissionsConfig(), + "SAVE_URL" => self::getUrl("User", "save"), + "API_KEY_URL" => self::getUrl("User", "generateApikey"), + ]; + + Helper::renderVue($this, "UserEdit", "Benutzer bearbeiten: " . $user->name, $JS_VARIABLES); + } + + protected function getUserDataForTemplateAction() { + $id = $this->request->id; + if (!$id) self::sendError("User ID is required."); + $user = new User($id); + if (!$user->id) self::sendError("User not found."); + + $preorderNetworks = $user->getFlag("preorder_networks")->value(); + $consentProjects = $user->getFlag("constructionConsent_projects")->value(); + + self::returnJson([ + 'permissions' => (array)$user->permissions->data, + 'preorder_networks' => $preorderNetworks ? json_decode($preorderNetworks, true) : [], + 'constructionconsent_projects' => $consentProjects ? json_decode($consentProjects, true) : [], + 'vodia_identity_domain' => $user->getFlag("vodia_identity_domain")->value(), + 'vodia_identity_default' => $user->getFlag("vodia_identity_default")->value(), + ]); + } + + protected function managePermissionTemplatesAction() { + Helper::renderVue($this, "UserPermissionTemplate", "Berechtigungsvorlagen", ["PERMISSIONS_CONFIG" => $this->getPermissionsConfig()]); + } + + protected function getPermissionTemplatesAction() { + self::returnJson(array_map( + function ($perm) { + $perm = (array)$perm; + $perm['permissions'] = json_decode($perm['permissions'], true) ?: []; + return $perm; + }, UserPermissionTemplateModel::getAll([], null, 0, ['key' => 'name', 'order' => 'asc']) + )); + } + + protected function savePermissionTemplateAction() { + if (empty($this->postData['name'])) self::sendError("Template name is required."); + + $data = [ + 'name' => $this->postData['name'], + 'permissions' => json_encode($this->postData['permissions'] ?? []), + ]; + + if (empty($this->postData['id'])) { + $data += ['createBy' => $this->user->id, 'create' => time()]; + $id = UserPermissionTemplateModel::create($data); + self::returnJson(['success' => true, 'message' => 'Vorlage erstellt.', 'id' => $id]); + } + + $template = UserPermissionTemplateModel::get($this->postData['id']); + $data += [ + 'id' => $this->postData['id'], + 'create' => $template->create, + 'createBy' => $template->createBy, + ]; + + UserPermissionTemplateModel::update($data); + self::returnJson(['success' => true, 'message' => 'Vorlage gespeichert.']); + } + + protected function deletePermissionTemplateAction() { + $post = json_decode(file_get_contents('php://input'), true); + if (empty($post['id'])) self::sendError("Template ID is required."); + UserPermissionTemplateModel::delete($post['id']); + self::returnJson(['success' => true, 'message' => 'Vorlage gelöscht.']); + } + + private function getPermissionsConfig(): array { + return [ + 'Rollen' => [ + 'admin' => 'Administrator', + 'employee' => TT_SYSOWNER_NAME_HTML . ' Mitarbeiter', + 'technician' => 'Techniker', + ], + 'Preorder' => [ + 'preorderfront' => 'Frontdesk (Semi-Readonly)', + 'preorderlogistics' => 'Logistikpartner', + 'preorderaddressreporting' => 'Address Reporting API User', + 'preorderreadonly' => 'Readonly', + 'canPreorder' => 'Modul: Vorbestellung', + 'canPreorderpricing' => 'Modul: Bepreisung', + 'canPreorderpricingReadonly' => 'Modul: Bepreisung (Readonly)', + 'canPreorderbilling' => 'Modul: Verrechnung', + 'canPreorderbillingReadonly' => 'Modul: Verrechnung (Readonly)', + ], + 'Module' => [ + 'canBuilding' => 'Objekte & Anschlüsse', + 'canPipework' => 'Tiefbau', + 'canLinework' => 'Leitungsbau', + 'canPatching' => 'Patching', + 'canFilestore' => 'Filestore (Netzbau)', + 'canCpeprovisioning' => 'CPE Provisioning', + 'canCpeshipping' => 'CPE Versand', + 'canVoipnumbering' => 'VOIP Nummernverwaltung', + 'canOrder' => 'Bestellung', + 'canBilling' => 'Verrechnung', + ], + 'Lager' => [ + 'canWarehouseAdmin' => 'Lager-Admin', + 'canWarehouseUser' => 'Lager-User', + 'canWarehouseEShop' => 'Energie Steiermark Shop', + ], + 'Zusatzberechtigungen' => [ + 'canFibu' => 'Buchhaltung', + 'canStatistics' => 'Statistiken', + 'canADBExtended' => 'Address-DB erweitert', + 'canAssetAdmin' => 'Anlagen-Admin', + 'canRMLAdmin' => 'RML-Workorder-Admin', + 'canRMLCompany' => 'RML-Workorder-Firma', + ] + ]; + } +} \ No newline at end of file diff --git a/application/UserPermissionTemplate/UserPermissionTemplateModel.php b/application/UserPermissionTemplate/UserPermissionTemplateModel.php new file mode 100644 index 000000000..66c71b318 --- /dev/null +++ b/application/UserPermissionTemplate/UserPermissionTemplateModel.php @@ -0,0 +1,20 @@ +getEnvironment() == "thetool") { + if (!$this->hasTable('UserPermissionTemplate')) { + $table = $this->table('UserPermissionTemplate', ['id' => false, 'primary_key' => ['id']]); + $table->addColumn('id', 'integer', ['identity' => true, 'signed' => false]) + ->addColumn('name', 'string', ['limit' => 255, 'null' => false]) + ->addColumn('permissions', 'text', ['null' => false]) + ->addColumn('create', 'integer', ['null' => false]) + ->addColumn('createBy', 'integer', ['null' => false]) + ->addIndex(['name'], ['unique' => true]) + ->create(); + } + } + } + + public function down(): void + { + if ($this->getEnvironment() == "thetool") { + if ($this->hasTable('UserPermissionTemplate')) { + $this->table('UserPermissionTemplate')->drop()->save(); + } + } + } +} diff --git a/public/js/pages/UserEdit/UserEdit.css b/public/js/pages/UserEdit/UserEdit.css new file mode 100644 index 000000000..c19304306 --- /dev/null +++ b/public/js/pages/UserEdit/UserEdit.css @@ -0,0 +1,166 @@ +.user-edit-container { + padding-bottom: 60px; /* Space for the bottom save button */ +} + +.user-edit-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + margin-top: 1.5rem; + padding: 0.5rem; +} + +.user-edit-header.sticky-header { + position: sticky; + top: 60px; /* Adjust based on your main header height */ + background-color: #f8f9fa; /* Match card background */ + z-index: 10; + padding: 0.75rem 1rem; + border-bottom: 1px solid #dee2e6; + margin: -1.25rem -1.25rem 1rem -1.25rem; /* Make it span the card width */ +} + +.user-edit-header.collapsible { + cursor: pointer; + border-bottom: 1px solid #dee2e6; + padding-bottom: 0.75rem; + transition: background-color 0.2s; +} +.user-edit-header.collapsible:hover { + background-color: #f1f3f5; +} + +.user-edit-header h3 { + margin: 0; +} + +.user-form-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 0.5rem 1.5rem; +} + +.user-form-grid-half { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); + gap: 1rem; +} + +.user-form-grid-toggles { + display: flex; + gap: 2rem; + margin-top: 1rem; +} + +.user-form-grid-toggles .form-group { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 0; +} + +.permission-template-section { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 1rem; + align-items: end; +} + +.permissions-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 1.5rem 2rem; +} + +.permission-group { + border: 1px solid #dee2e6; + border-radius: 0.3rem; + padding: 1rem; + background-color: #fff; +} + +.permission-group h5 { + font-size: 1.1rem; + margin-bottom: 1rem; + color: #0056b3; +} + +.form-check-label { + user-select: none; +} + +.password-generation-grid { + display: grid; + grid-template-columns: 1fr auto auto; + gap: 0.5rem; + align-items: end; +} + +.selected-items-viewer { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 1.5rem; + margin-top: 1.5rem; +} + +.selection-list-container { + border: 1px solid #e9ecef; + padding: 0.75rem; + border-radius: 0.25rem; +} + +.selection-list { + list-style-type: none; + padding-left: 0; + margin-top: 0.5rem; + max-height: 200px; + overflow-y: auto; +} +.selection-list li { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.25rem 0.5rem; + border-radius: 0.2rem; +} +.selection-list li:hover { + background-color: #f8f9fa; +} +.selection-list li .fa-times-circle { + color: #dc3545; + cursor: pointer; + opacity: 0.7; +} +.selection-list li .fa-times-circle:hover { + opacity: 1; +} + +.user-edit-footer { + position: fixed; + bottom: 0; + left: 0; + right: 0; + padding: 0.75rem; + background-color: #ffffff; + border-top: 1px solid #dee2e6; + display: flex; + justify-content: flex-end; + z-index: 1000; +} + +/* Slide-fade transition */ +.slide-fade-enter-active, .slide-fade-leave-active { + transition: all .3s ease; +} +.slide-fade-enter, .slide-fade-leave-to { + transform: translateY(-10px); + opacity: 0; +} +.slide-fade-fast-enter-active, .slide-fade-fast-leave-active { + transition: all .2s ease; +} +.slide-fade-fast-enter, .slide-fade-fast-leave-to { + transform: translateY(-5px); + opacity: 0; +} \ No newline at end of file diff --git a/public/js/pages/UserEdit/UserEdit.js b/public/js/pages/UserEdit/UserEdit.js new file mode 100644 index 000000000..302ac1f41 --- /dev/null +++ b/public/js/pages/UserEdit/UserEdit.js @@ -0,0 +1,350 @@ +Vue.component("UserEdit", { + template: ` + +
+ + +
+ + + + + +
+
+
+ + +
+
+ + +
+
+ +
+

Berechtigungen

+ +
+ + +
+ + +
+
+
+
+
{{ groupName }}
+
+ + +
+
+
+
+
+ +
+
+

Mitarbeiter-spezifische Felder

+ +
+ + +
+ + + + + +
+
+
+
+ +
+

Projekt- & Netzwerkzugriff

+ +
+ + +
+ + +
+
+ + +
+
+
+ +
+

Passwort & API Key

+ +
+ +
+ +
+ + + +
+ +
+ + +
+ +
+ +
+
+
+
+
+ +
+
+ `, + components: { + 'collapsible-selection-list': { + props: ['title', 'items', 'lookup', 'collapsible'], + data: () => ({ collapsed: true }), + computed: { + selectedItems() { + if (!this.items || !this.lookup) return []; + const lookupMap = new Map(this.lookup.map(i => [i.value, i.text])); + return this.items.map(id => ({ id, text: lookupMap.get(id) || `ID: ${id}` })); + } + }, + template: ` +
+ {{ title }} ({{ items.length }}) + +
    +
  • + {{ item.text }} + +
  • +
+
+
` + } + }, + data() { + return { + user: JSON.parse(JSON.stringify(window.TT_CONFIG.USER_DATA)), // Deep copy + initialPermissions: {}, + lookups: window.TT_CONFIG.LOOKUPS, + permissionsConfig: window.TT_CONFIG.PERMISSIONS_CONFIG, + password: { new: '', repeat: '' }, + passwordFieldType: 'password', + selectedTemplate: null, + userToLoad: null, + isSaving: false, + isToggling: false, + collapsedSections: { + permissions: false, + employeeSpecific: true, + projects: true, + security: true, + } + } + }, + computed: { + templateOptions() { + const options = this.lookups.permissionTemplates.map(t => ({ value: t.id, text: t.name })); + options.unshift({ value: null, text: 'Vorlage auswählen...' }); + return options; + }, + permissionChangesTooltip() { + const added = []; + const removed = []; + for (const key in this.user.permissions) { + const initial = !!this.initialPermissions[key]; + const current = !!this.user.permissions[key]; + if (initial !== current) { + const permissionLabel = this.findPermissionLabel(key); + if (!permissionLabel || /^\d+$/.test(permissionLabel)) continue; + if (current) { + added.push(`- ${permissionLabel}`); + } else { + removed.push(`- ${permissionLabel}`); + } + } + } + + let tooltipText = ''; + if (added.length > 0) { + tooltipText += 'Hinzugefügt:\n' + added.join('\n'); + } + if (removed.length > 0) { + if (tooltipText.length > 0) { + tooltipText += '\n\n'; // Two line breaks + } + tooltipText += 'Entfernt:\n' + removed.join('\n'); + } + + return tooltipText || 'Keine Berechtigungsänderungen'; + } + }, + methods: { + toggleSection(section) { + this.collapsedSections[section] = !this.collapsedSections[section]; + }, + getChevronClass(section) { + return this.collapsedSections[section] ? 'fas fa-chevron-down' : 'fas fa-chevron-up'; + }, + findPermissionLabel(key) { + for (const group in this.permissionsConfig) { + if (this.permissionsConfig[group][key]) { + return this.permissionsConfig[group][key]; + } + } + return key; + }, + applyTemplate(templateId) { + if (!templateId) return; + const template = this.lookups.permissionTemplates.find(t => t.id == templateId); + template.permissions = JSON.parse(template.permissions); + if (template) { + const newPermissions = { ...this.user.permissions }; + for (const key in template.permissions) { + newPermissions[key] = template.permissions[key]; + } + this.user.permissions = newPermissions; + window.notify('success', `Vorlage "${template.name}" angewendet.`); + } + this.selectedTemplate = null; + }, + async loadDataFromUser(userId) { + if(!userId) return; + try { + const response = await axios.get(`/UserEdit/getUserDataForTemplate?id=${userId}`); + const dataToApply = response.data; + + // Apply Permissions + const newPermissions = { ...this.user.permissions }; + for (const key in newPermissions) { + newPermissions[key] = dataToApply.permissions[key] === 'true'; + } + this.user.permissions = newPermissions; + + // Apply other fields + this.$set(this.user, 'preorder_networks', dataToApply.preorder_networks || []); + this.$set(this.user, 'constructionconsent_projects', dataToApply.constructionconsent_projects || []); + this.$set(this.user, 'vodia_identity_domain', dataToApply.vodia_identity_domain || ''); + this.$set(this.user, 'vodia_identity_default', dataToApply.vodia_identity_default || ''); + + const selectedUser = this.lookups.users.find(u => u.value === userId); + window.notify('success', `Daten von "${selectedUser.text}" geladen.`); + } catch (error) { + window.notify('error', 'Daten konnten nicht geladen werden.'); + } finally { + this.userToLoad = null; // Reset autocomplete + } + }, + saveUser() { + this.isSaving = true; + if (this.password.new && this.password.new !== this.password.repeat) { + window.notify('error', 'Die Passwörter stimmen nicht überein!'); + this.isSaving = false; + return; + } + + const formData = new FormData(); + // Append standard fields + const fields = ['id', 'username', 'name', 'email', 'mobile', 'address_id', 'employee_number', 'project_api_key', 'vodia_identity_domain', 'vodia_identity_username', 'vodia_identity_default']; + fields.forEach(field => formData.append(field, this.user[field] || '')); + + // Append booleans as 'true'/'false' strings + formData.append('active', this.user.active ? 'true' : 'false'); + formData.append('twofactorrequired', this.user.twofactorrequired ? 'true' : 'false'); + + if (this.password.new) { + formData.append('password', this.password.new); + formData.append('password2', this.password.repeat); + } + + // Append ONLY TRUE permissions to mimic checkbox form behavior + for (const key in this.user.permissions) { + if (this.user.permissions[key]) { + if (key.startsWith('can')) { + formData.append(`can[${key.replace('can', '')}]`, 'true'); + } else { + formData.append(key, 'true'); + } + } + } + + (this.user.preorder_networks || []).forEach(val => formData.append('preorder_networks[]', val)); + (this.user.constructionconsent_projects || []).forEach(val => formData.append('constructionconsent_projects[]', val)); + + axios.post(window.TT_CONFIG.SAVE_URL, formData) + .then(() => { + window.notify('success', 'Benutzer erfolgreich gespeichert.'); + setTimeout(() => window.location.href = '/User', 150); + }) + .catch(error => { + window.notify('error', 'Fehler beim Speichern des Benutzers.'); + console.error(error); + }) + .finally(() => { + this.isSaving = false; + }); + }, + async generateApiKey() { + try { + await axios.post(window.TT_CONFIG.API_KEY_URL, new URLSearchParams({id: this.user.id})); + window.notify('success', 'Neuer API Key wurde generiert. Seite wird neu geladen...'); + setTimeout(() => window.location.reload(), 1500); + } catch (error) { + window.notify('error', 'API Key konnte nicht generiert werden.'); + } + }, + generatePassword() { + const length = 14; + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + let retVal = ""; + for (let i = 0, n = charset.length; i < length; ++i) { + retVal += charset.charAt(Math.floor(Math.random() * n)); + } + this.password.new = retVal; + this.password.repeat = retVal; + window.notify('info', 'Neues Passwort generiert.'); + }, + togglePasswordVisibility() { + this.passwordFieldType = this.passwordFieldType === 'password' ? 'text' : 'password'; + }, + removeItem(arrayName, valueToRemove) { + const index = this.user[arrayName].indexOf(valueToRemove); + if (index > -1) { + this.user[arrayName].splice(index, 1); + } + } + }, + created() { + const permissions = {}; + Object.values(this.permissionsConfig).forEach(group => { + Object.keys(group).forEach(key => { + permissions[key] = this.user.permissions[key] === 'true' || this.user.permissions[key] === true; + }); + }); + this.user.permissions = permissions; + this.initialPermissions = JSON.parse(JSON.stringify(permissions)); // Deep copy for change tracking + this.user.active = this.user.active == 1; + this.user.twofactorrequired = this.user.twofactorrequired == 1; + } +}); \ No newline at end of file diff --git a/public/js/pages/UserPermissionTemplate/UserPermissionTemplate.css b/public/js/pages/UserPermissionTemplate/UserPermissionTemplate.css new file mode 100644 index 000000000..4d9d1c307 --- /dev/null +++ b/public/js/pages/UserPermissionTemplate/UserPermissionTemplate.css @@ -0,0 +1,49 @@ +.template-header { + display: flex; + justify-content: space-between; + align-items: center; + padding-bottom: 1rem; + border-bottom: 1px solid #dee2e6; + margin-bottom: 0.5rem; +} + +.template-header h3 { + margin: 0; +} + +.template-list .list-group-item { + display: flex; + justify-content: space-between; + align-items: center; +} + +.template-list .list-group-item .actions { + display: flex; + gap: 0.5rem; +} + + +/* Modal Styles */ +.permissions-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 1rem 2rem; +} + +.permission-group { + border: 1px solid #dee2e6; + border-radius: 0.3rem; + padding: 1rem; + background-color: #f8f9fa; +} + +.permission-group h5 { + font-size: 1.1rem; + border-bottom: 1px solid #e0e0e0; + padding-bottom: 0.5rem; + margin-bottom: 1rem; +} + +.form-check-label { + user-select: none; +} \ No newline at end of file diff --git a/public/js/pages/UserPermissionTemplate/UserPermissionTemplate.js b/public/js/pages/UserPermissionTemplate/UserPermissionTemplate.js new file mode 100644 index 000000000..bcfca9d7e --- /dev/null +++ b/public/js/pages/UserPermissionTemplate/UserPermissionTemplate.js @@ -0,0 +1,147 @@ +Vue.component('UserPermissionTemplate', { + template: ` + +
+

Berechtigungsvorlagen

+ +
+ +
+ + +
+ Keine Vorlagen gefunden. +
+ + + +
+
+
+
{{ groupName }}
+
+ + +
+
+
+
+
+ `, + data() { + // Helper function to get a fresh permissions object. It's defined here to avoid relying on `this`. + const getFreshPermissions = (config) => { + const permissions = {}; + if (!config) return permissions; + Object.values(config).forEach(group => { + Object.keys(group).forEach(key => { + permissions[key] = false; + }); + }); + return permissions; + }; + + return { + loading: true, + templates: [], + showModal: false, + isSaving: false, + editableTemplate: { + id: null, + name: '', + permissions: getFreshPermissions(window.TT_CONFIG.PERMISSIONS_CONFIG) // Initialize correctly + }, + modalKey: Date.now(), + } + }, + computed: { + permissionsConfig() { + return window.TT_CONFIG.PERMISSIONS_CONFIG; + }, + modalTitle() { + return this.editableTemplate && this.editableTemplate.id ? 'Vorlage bearbeiten' : 'Neue Vorlage erstellen'; + } + }, + methods: { + fetchTemplates() { + this.loading = true; + axios.get('/UserEdit/getPermissionTemplates') + .then(response => { + this.templates = response.data; + }) + .catch(e => window.notify('error', 'Vorlagen konnten nicht geladen werden.')) + .finally(() => this.loading = false); + }, + openModal(template = null) { + this.modalKey = Date.now(); // Ensure unique IDs for checkboxes in modal + if (template) { + this.editableTemplate = JSON.parse(JSON.stringify(template)); // Deep copy + const permissions = this.freshPermissions(); + // Ensure all keys from config exist on the loaded template's permissions + for (const key in permissions) { + if (this.editableTemplate.permissions.hasOwnProperty(key)) { + permissions[key] = this.editableTemplate.permissions[key]; + } + } + this.editableTemplate.permissions = permissions; + } else { + this.editableTemplate = { id: null, name: '', permissions: this.freshPermissions() }; + } + this.showModal = true; + }, + freshPermissions() { + const permissions = {}; + Object.values(this.permissionsConfig).forEach(group => { + Object.keys(group).forEach(key => { + permissions[key] = false; + }); + }); + return permissions; + }, + async saveTemplate() { + if (!this.editableTemplate.name) { + return window.notify('error', 'Bitte einen Namen für die Vorlage eingeben.'); + } + this.isSaving = true; + try { + const response = await axios.post('/UserEdit/savePermissionTemplate', this.editableTemplate); + if(response.data.success) { + window.notify('success', response.data.message); + this.showModal = false; + this.fetchTemplates(); // Refresh list + } else { + window.notify('error', response.data.message || 'Fehler beim Speichern.'); + } + } catch (e) { + window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); + } finally { + this.isSaving = false; + } + }, + async deleteTemplate(template) { + if (!confirm(`Soll die Vorlage "${template.name}" wirklich gelöscht werden?`)) return; + try { + const response = await axios.post('/UserEdit/deletePermissionTemplate', { id: template.id }); + if(response.data.success) { + window.notify('success', response.data.message); + this.fetchTemplates(); // Refresh list + } else { + window.notify('error', response.data.message || 'Fehler beim Löschen.'); + } + } catch (e) { + window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); + } + } + }, + mounted() { + this.fetchTemplates(); + } +}); \ No newline at end of file diff --git a/public/plugins/vue/tt-components/tt-tooltip.js b/public/plugins/vue/tt-components/tt-tooltip.js index e4b64644f..58224c226 100644 --- a/public/plugins/vue/tt-components/tt-tooltip.js +++ b/public/plugins/vue/tt-components/tt-tooltip.js @@ -30,7 +30,7 @@ Vue.component('tt-tooltip', {
- {{ text }} +
`