350 lines
17 KiB
JavaScript
350 lines
17 KiB
JavaScript
Vue.component("UserEdit", {
|
|
template: `
|
|
<tt-card>
|
|
<div class="user-edit-container">
|
|
<tt-loader v-if="isSaving"/>
|
|
<div class="user-edit-header sticky-header">
|
|
<h3>Allgemeine Informationen</h3>
|
|
<tt-tooltip :text="permissionChangesTooltip" position="left">
|
|
<tt-button text="Speichern" icon="fas fa-save" additional-class="btn-primary" @click="saveUser" :loading="isSaving"/>
|
|
</tt-tooltip>
|
|
</div>
|
|
<div class="user-form-grid">
|
|
<tt-input label="Username" v-model="user.username" sm/>
|
|
<tt-input label="Name" v-model="user.name" sm/>
|
|
<tt-input label="Email" v-model="user.email" type="email" sm/>
|
|
<tt-input label="Handy Nr." v-model="user.mobile" placeholder="+43..." sm/>
|
|
<tt-select label="Firma/Person" :options="lookups.addresses" v-model.number="user.address_id" sm/>
|
|
</div>
|
|
<div class="user-form-grid-toggles">
|
|
<div class="form-group">
|
|
<label>Aktiv</label>
|
|
<tt-switch v-model="user.active" :loading="isToggling"/>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>2FA erzwingen</label>
|
|
<tt-switch v-model="user.twofactorrequired" :loading="isToggling"/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="user-edit-header collapsible" @click="toggleSection('permissions')">
|
|
<h3>Berechtigungen</h3>
|
|
<i :class="getChevronClass('permissions')"></i>
|
|
</div>
|
|
<transition name="slide-fade">
|
|
<tt-card v-show="!collapsedSections.permissions">
|
|
<div class="permission-template-section">
|
|
<tt-select label="Vorlage anwenden" :options="templateOptions" v-model="selectedTemplate" @input="applyTemplate" sm/>
|
|
<tt-autocomplete label="Aus bestehenden User laden" :items="lookups.users" v-model="userToLoad" @input="loadDataFromUser" sm/>
|
|
</div>
|
|
<hr>
|
|
<div class="permissions-grid">
|
|
<div v-for="(group, groupName) in permissionsConfig" :key="groupName" class="permission-group">
|
|
<h5>{{ groupName }}</h5>
|
|
<div v-for="(label, key) in group" :key="key" class="form-group form-check">
|
|
<input type="checkbox" class="form-check-input" :id="'perm-' + key" v-model="user.permissions[key]">
|
|
<label class="form-check-label" :for="'perm-' + key" v-html="label"></label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</tt-card>
|
|
</transition>
|
|
|
|
<div v-if="user.permissions.employee">
|
|
<div class="user-edit-header collapsible" @click="toggleSection('employeeSpecific')">
|
|
<h3>Mitarbeiter-spezifische Felder</h3>
|
|
<i :class="getChevronClass('employeeSpecific')"></i>
|
|
</div>
|
|
<transition name="slide-fade">
|
|
<tt-card v-show="!collapsedSections.employeeSpecific">
|
|
<div class="user-form-grid">
|
|
<tt-input label="Mitarbeiternummer" v-model="user.employee_number" sm/>
|
|
<tt-input label="OpenProject API Key" v-model="user.project_api_key" sm/>
|
|
<tt-input label="Vodia Domain" v-model="user.vodia_identity_domain" sm/>
|
|
<tt-input label="Vodia Username (Extension)" v-model="user.vodia_identity_username" sm/>
|
|
<tt-input label="Vodia Standard-Identität" v-model="user.vodia_identity_default" sm hint="+43720123456"/>
|
|
</div>
|
|
</tt-card>
|
|
</transition>
|
|
</div>
|
|
|
|
<div class="user-edit-header collapsible" @click="toggleSection('projects')">
|
|
<h3>Projekt- & Netzwerkzugriff</h3>
|
|
<i :class="getChevronClass('projects')"></i>
|
|
</div>
|
|
<transition name="slide-fade">
|
|
<tt-card v-show="!collapsedSections.projects">
|
|
<div class="user-form-grid">
|
|
<tt-select label="Preorder Netzgebiete" :options="lookups.networks" v-model="user.preorder_networks" multiple sm searchable/>
|
|
<tt-select label="Zustimmungserklärungsprojekte" :options="lookups.consentProjects" v-model="user.constructionconsent_projects" multiple sm searchable/>
|
|
</div>
|
|
<div class="selected-items-viewer">
|
|
<collapsible-selection-list title="Ausgewählte Netzgebiete" :items="user.preorder_networks" :lookup="lookups.networks" @remove="removeItem('preorder_networks', $event)"/>
|
|
<collapsible-selection-list title="Ausgewählte Projekte" :items="user.constructionconsent_projects" :lookup="lookups.consentProjects" @remove="removeItem('constructionconsent_projects', $event)"/>
|
|
</div>
|
|
</tt-card>
|
|
</transition>
|
|
|
|
<div class="user-edit-header collapsible" @click="toggleSection('security')">
|
|
<h3>Passwort & API Key</h3>
|
|
<i :class="getChevronClass('security')"></i>
|
|
</div>
|
|
<transition name="slide-fade">
|
|
<div v-show="!collapsedSections.security" class="user-form-grid-half">
|
|
<tt-card>
|
|
<div class="password-generation-grid">
|
|
<tt-input label="Neues Passwort" v-model="password.new" :type="passwordFieldType" sm/>
|
|
<tt-button icon="fas fa-sync-alt" @click="generatePassword" additional-class="btn-outline-secondary" sm title="Passwort generieren"/>
|
|
<tt-button :icon="passwordFieldType === 'password' ? 'fas fa-eye' : 'fas fa-eye-slash'" @click="togglePasswordVisibility" additional-class="btn-outline-secondary" sm title="Passwort anzeigen"/>
|
|
</div>
|
|
<tt-input label="Passwort wiederholen" v-model="password.repeat" type="password" sm/>
|
|
</tt-card>
|
|
<tt-card>
|
|
<label>API Key</label>
|
|
<div class="input-group">
|
|
<input type="text" class="form-control form-control-sm" :value="user.apikey" readonly>
|
|
<div class="input-group-append">
|
|
<tt-button text="Neu generieren" @click="generateApiKey" additional-class="btn-outline-primary" sm confirm-text="Soll wirklich ein neuer API Key generiert werden? Der alte wird dadurch ungültig."/>
|
|
</div>
|
|
</div>
|
|
</tt-card>
|
|
</div>
|
|
</transition>
|
|
<div class="user-edit-footer">
|
|
<tt-tooltip :text="permissionChangesTooltip" position="top">
|
|
<tt-button text="Speichern" icon="fas fa-save" additional-class="btn-primary" @click="saveUser" :loading="isSaving"/>
|
|
</tt-tooltip>
|
|
</div>
|
|
</div>
|
|
</tt-card>
|
|
`,
|
|
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: `
|
|
<div v-if="items && items.length" class="selection-list-container">
|
|
<strong @click="collapsed = !collapsed" style="cursor: pointer;">{{ title }} ({{ items.length }}) <i :class="collapsed ? 'fas fa-chevron-down' : 'fas fa-chevron-up'"></i></strong>
|
|
<transition name="slide-fade-fast">
|
|
<ul v-show="!collapsed" class="selection-list">
|
|
<li v-for="item in selectedItems" :key="item.id">
|
|
{{ item.text }}
|
|
<i class="fas fa-times-circle" @click="$emit('remove', item.id)"></i>
|
|
</li>
|
|
</ul>
|
|
</transition>
|
|
</div>`
|
|
}
|
|
},
|
|
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;
|
|
}
|
|
}); |