685 lines
18 KiB
Vue
685 lines
18 KiB
Vue
<template>
|
|
<div class="role-management">
|
|
<div class="page-header">
|
|
<h2>Rollenverwaltung</h2>
|
|
<Button
|
|
label="Neue Rolle"
|
|
icon="pi pi-plus"
|
|
@click="openCreateDialog"
|
|
/>
|
|
</div>
|
|
|
|
<DataTable
|
|
:value="roles"
|
|
:loading="loading"
|
|
stripedRows
|
|
showGridlines
|
|
>
|
|
<Column field="name" header="Name" sortable style="width: 200px" />
|
|
<Column field="description" header="Beschreibung" sortable />
|
|
<Column field="isSystem" header="System-Rolle" sortable style="width: 150px">
|
|
<template #body="{ data }">
|
|
<Tag :value="data.isSystem ? 'Ja' : 'Nein'" :severity="data.isSystem ? 'warning' : 'secondary'" />
|
|
</template>
|
|
</Column>
|
|
<Column header="Aktionen" style="width: 150px">
|
|
<template #body="{ data }">
|
|
<div class="action-buttons">
|
|
<Button
|
|
icon="pi pi-pencil"
|
|
severity="info"
|
|
text
|
|
rounded
|
|
@click="openEditDialog(data)"
|
|
:disabled="data.isSystem"
|
|
v-tooltip.top="data.isSystem ? 'System-Rollen können nicht bearbeitet werden' : 'Bearbeiten'"
|
|
/>
|
|
<Button
|
|
icon="pi pi-trash"
|
|
severity="danger"
|
|
text
|
|
rounded
|
|
@click="confirmDelete(data)"
|
|
:disabled="data.isSystem"
|
|
v-tooltip.top="data.isSystem ? 'System-Rollen können nicht gelöscht werden' : 'Löschen'"
|
|
/>
|
|
</div>
|
|
</template>
|
|
</Column>
|
|
</DataTable>
|
|
|
|
<!-- Create/Edit Dialog -->
|
|
<Dialog
|
|
v-model:visible="dialogVisible"
|
|
:header="isEditMode ? 'Rolle bearbeiten' : 'Neue Rolle erstellen'"
|
|
modal
|
|
:style="{ width: '90vw', maxWidth: '900px' }"
|
|
class="role-dialog"
|
|
>
|
|
<div class="form-container">
|
|
<div class="form-field">
|
|
<label for="name">Name *</label>
|
|
<InputText
|
|
id="name"
|
|
v-model="formData.name"
|
|
:class="{ 'p-invalid': errors.name }"
|
|
placeholder="z.B. Vertriebsmitarbeiter"
|
|
/>
|
|
<small v-if="errors.name" class="p-error">{{ errors.name }}</small>
|
|
</div>
|
|
|
|
<div class="form-field">
|
|
<label for="description">Beschreibung</label>
|
|
<Textarea
|
|
id="description"
|
|
v-model="formData.description"
|
|
rows="3"
|
|
placeholder="Beschreibung der Rolle und ihrer Aufgaben"
|
|
/>
|
|
</div>
|
|
|
|
<div class="permissions-section">
|
|
<h3>Modulberechtigungen</h3>
|
|
<div class="permissions-matrix">
|
|
<div class="matrix-header">
|
|
<div class="module-cell">Modul</div>
|
|
<div class="permission-cell" v-tooltip.top="'Benutzer kann Daten ansehen'">
|
|
<i class="pi pi-eye"></i>
|
|
<span>Lesen</span>
|
|
</div>
|
|
<div class="permission-cell" v-tooltip.top="'Benutzer kann neue Einträge erstellen'">
|
|
<i class="pi pi-plus"></i>
|
|
<span>Erstellen</span>
|
|
</div>
|
|
<div class="permission-cell" v-tooltip.top="'Benutzer kann bestehende Einträge bearbeiten'">
|
|
<i class="pi pi-pencil"></i>
|
|
<span>Bearbeiten</span>
|
|
</div>
|
|
<div class="permission-cell" v-tooltip.top="'Benutzer kann Einträge löschen'">
|
|
<i class="pi pi-trash"></i>
|
|
<span>Löschen</span>
|
|
</div>
|
|
<div class="permission-cell" v-tooltip.top="'Benutzer kann Daten exportieren'">
|
|
<i class="pi pi-download"></i>
|
|
<span>Export</span>
|
|
</div>
|
|
<div class="permission-cell" v-tooltip.top="'Benutzer kann Modul-Einstellungen verwalten'">
|
|
<i class="pi pi-cog"></i>
|
|
<span>Verwalten</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-for="module in modules"
|
|
:key="module.id"
|
|
class="matrix-row hover-row"
|
|
>
|
|
<div class="module-cell">
|
|
<i :class="module.icon"></i>
|
|
<span>{{ module.name }}</span>
|
|
</div>
|
|
<div class="permission-cell">
|
|
<Checkbox
|
|
v-model="getPermission(module.id).canView"
|
|
:binary="true"
|
|
/>
|
|
</div>
|
|
<div class="permission-cell">
|
|
<Checkbox
|
|
v-model="getPermission(module.id).canCreate"
|
|
:binary="true"
|
|
/>
|
|
</div>
|
|
<div class="permission-cell">
|
|
<Checkbox
|
|
v-model="getPermission(module.id).canEdit"
|
|
:binary="true"
|
|
/>
|
|
</div>
|
|
<div class="permission-cell">
|
|
<Checkbox
|
|
v-model="getPermission(module.id).canDelete"
|
|
:binary="true"
|
|
/>
|
|
</div>
|
|
<div class="permission-cell">
|
|
<Checkbox
|
|
v-model="getPermission(module.id).canExport"
|
|
:binary="true"
|
|
/>
|
|
</div>
|
|
<div class="permission-cell">
|
|
<Checkbox
|
|
v-model="getPermission(module.id).canManage"
|
|
:binary="true"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<template #footer>
|
|
<Button label="Abbrechen" severity="secondary" @click="dialogVisible = false" />
|
|
<Button label="Speichern" @click="saveRole" :loading="saving" />
|
|
</template>
|
|
</Dialog>
|
|
|
|
<!-- Delete Confirmation -->
|
|
<Dialog
|
|
v-model:visible="deleteDialogVisible"
|
|
header="Rolle löschen"
|
|
modal
|
|
:style="{ width: '450px' }"
|
|
>
|
|
<p>Möchten Sie die Rolle <strong>{{ roleToDelete?.name }}</strong> wirklich löschen?</p>
|
|
<p class="text-muted">Diese Aktion kann nicht rückgängig gemacht werden.</p>
|
|
|
|
<template #footer>
|
|
<Button label="Abbrechen" severity="secondary" @click="deleteDialogVisible = false" />
|
|
<Button label="Löschen" severity="danger" @click="deleteRole" :loading="deleting" />
|
|
</template>
|
|
</Dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted } from 'vue';
|
|
import { useAuthStore } from '../stores/auth';
|
|
import DataTable from 'primevue/datatable';
|
|
import Column from 'primevue/column';
|
|
import Button from 'primevue/button';
|
|
import Dialog from 'primevue/dialog';
|
|
import InputText from 'primevue/inputtext';
|
|
import Textarea from 'primevue/textarea';
|
|
import Checkbox from 'primevue/checkbox';
|
|
import Tag from 'primevue/tag';
|
|
import { useToast } from 'primevue/usetoast';
|
|
|
|
const authStore = useAuthStore();
|
|
const toast = useToast();
|
|
|
|
const loading = ref(false);
|
|
const saving = ref(false);
|
|
const deleting = ref(false);
|
|
const roles = ref([]);
|
|
const modules = ref([]);
|
|
const dialogVisible = ref(false);
|
|
const deleteDialogVisible = ref(false);
|
|
const isEditMode = ref(false);
|
|
const roleToDelete = ref(null);
|
|
|
|
const formData = ref({
|
|
name: '',
|
|
description: '',
|
|
isSystem: false,
|
|
permissions: []
|
|
});
|
|
|
|
const errors = ref({});
|
|
|
|
const fetchRoles = async () => {
|
|
loading.value = true;
|
|
try {
|
|
const response = await fetch('/api/roles', {
|
|
credentials: 'same-origin',
|
|
headers: {
|
|
'Accept': 'application/ld+json'
|
|
}
|
|
});
|
|
|
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
|
|
|
const data = await response.json();
|
|
roles.value = data.member || [];
|
|
console.log('Loaded roles:', roles.value);
|
|
} catch (error) {
|
|
console.error('Error fetching roles:', error);
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Fehler',
|
|
detail: 'Rollen konnten nicht geladen werden',
|
|
life: 5000
|
|
});
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
const fetchModules = async () => {
|
|
try {
|
|
const response = await fetch('/api/modules', {
|
|
credentials: 'same-origin',
|
|
headers: {
|
|
'Accept': 'application/ld+json'
|
|
}
|
|
});
|
|
|
|
console.log('Modules API response status:', response.status);
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
console.error('Modules API error:', response.status, errorText);
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
console.log('Modules API raw data:', data);
|
|
console.log('First module:', data.member[0]);
|
|
console.log('First module isActive:', data.member[0]?.isActive);
|
|
|
|
const allModules = data.member || data['hydra:member'] || [];
|
|
console.log('All modules before filter:', allModules);
|
|
|
|
modules.value = allModules
|
|
.filter(m => {
|
|
console.log(`Module ${m.name}: isActive = ${m.isActive}`);
|
|
return m.isActive;
|
|
})
|
|
.sort((a, b) => a.sortOrder - b.sortOrder);
|
|
|
|
console.log('Loaded modules:', modules.value);
|
|
} catch (error) {
|
|
console.error('Error fetching modules:', error);
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Fehler',
|
|
detail: 'Module konnten nicht geladen werden: ' + error.message,
|
|
life: 5000
|
|
});
|
|
}
|
|
};
|
|
|
|
const getPermission = (moduleId) => {
|
|
let permission = formData.value.permissions.find(p => p.moduleId === moduleId);
|
|
if (!permission) {
|
|
permission = {
|
|
moduleId,
|
|
canView: false,
|
|
canCreate: false,
|
|
canEdit: false,
|
|
canDelete: false,
|
|
canExport: false,
|
|
canManage: false
|
|
};
|
|
formData.value.permissions.push(permission);
|
|
}
|
|
console.log(`getPermission(${moduleId}):`, permission);
|
|
return permission;
|
|
};
|
|
|
|
const openCreateDialog = () => {
|
|
isEditMode.value = false;
|
|
formData.value = {
|
|
name: '',
|
|
description: '',
|
|
isSystem: false,
|
|
permissions: []
|
|
};
|
|
errors.value = {};
|
|
dialogVisible.value = true;
|
|
};
|
|
|
|
const openEditDialog = async (role) => {
|
|
isEditMode.value = true;
|
|
|
|
// Load full role details with permissions from API
|
|
try {
|
|
const response = await fetch(role['@id'] || `/api/roles/${role.id}`, {
|
|
credentials: 'same-origin',
|
|
headers: {
|
|
'Accept': 'application/ld+json'
|
|
}
|
|
});
|
|
|
|
if (!response.ok) throw new Error('Failed to load role details');
|
|
|
|
const fullRole = await response.json();
|
|
console.log('Loaded full role for editing:', fullRole);
|
|
|
|
formData.value = {
|
|
id: fullRole.id,
|
|
'@id': fullRole['@id'],
|
|
name: fullRole.name,
|
|
description: fullRole.description || '',
|
|
isSystem: fullRole.isSystem,
|
|
permissions: []
|
|
};
|
|
|
|
// Convert role.permissions to our internal format
|
|
if (fullRole.permissions && fullRole.permissions.length > 0) {
|
|
fullRole.permissions.forEach(perm => {
|
|
console.log('Processing permission:', perm);
|
|
formData.value.permissions.push({
|
|
id: perm.id,
|
|
'@id': perm['@id'],
|
|
moduleId: perm.module?.id || perm.module,
|
|
canView: perm.canView ?? false,
|
|
canCreate: perm.canCreate ?? false,
|
|
canEdit: perm.canEdit ?? false,
|
|
canDelete: perm.canDelete ?? false,
|
|
canExport: perm.canExport ?? false,
|
|
canManage: perm.canManage ?? false
|
|
});
|
|
});
|
|
}
|
|
|
|
console.log('Form data permissions:', formData.value.permissions);
|
|
|
|
errors.value = {};
|
|
dialogVisible.value = true;
|
|
} catch (error) {
|
|
console.error('Error loading role:', error);
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Fehler',
|
|
detail: 'Rolle konnte nicht geladen werden',
|
|
life: 3000
|
|
});
|
|
}
|
|
};
|
|
|
|
const validateForm = () => {
|
|
errors.value = {};
|
|
if (!formData.value.name) errors.value.name = 'Name ist erforderlich';
|
|
return Object.keys(errors.value).length === 0;
|
|
};
|
|
|
|
const saveRole = async () => {
|
|
if (!validateForm()) return;
|
|
|
|
saving.value = true;
|
|
try {
|
|
// Step 1: Save role (without permissions)
|
|
const rolePayload = {
|
|
name: formData.value.name,
|
|
description: formData.value.description,
|
|
isSystem: formData.value.isSystem
|
|
};
|
|
|
|
console.log('Saving role with payload:', rolePayload);
|
|
|
|
const url = isEditMode.value ? `/api/roles/${formData.value.id}` : '/api/roles';
|
|
const method = isEditMode.value ? 'PUT' : 'POST';
|
|
|
|
const response = await fetch(url, {
|
|
method,
|
|
credentials: 'same-origin',
|
|
headers: {
|
|
'Content-Type': 'application/ld+json',
|
|
'Accept': 'application/ld+json'
|
|
},
|
|
body: JSON.stringify(rolePayload)
|
|
});
|
|
|
|
console.log('Save response status:', response.status);
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
console.error('API Error Response:', errorText);
|
|
throw new Error(`Fehler beim Speichern der Rolle: ${response.status}`);
|
|
}
|
|
|
|
const savedRole = await response.json();
|
|
console.log('Saved role:', savedRole);
|
|
|
|
// Step 2: Delete existing permissions for this role (if editing)
|
|
if (isEditMode.value && savedRole.permissions && savedRole.permissions.length > 0) {
|
|
for (const perm of savedRole.permissions) {
|
|
try {
|
|
await fetch(perm['@id'], {
|
|
method: 'DELETE',
|
|
credentials: 'same-origin'
|
|
});
|
|
} catch (e) {
|
|
console.warn('Could not delete permission:', e);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Step 3: Create new permissions
|
|
const permissionsToCreate = formData.value.permissions
|
|
.filter(p => p.canView || p.canCreate || p.canEdit || p.canDelete || p.canExport || p.canManage);
|
|
|
|
console.log('Creating permissions:', permissionsToCreate);
|
|
|
|
for (const perm of permissionsToCreate) {
|
|
const permPayload = {
|
|
role: savedRole['@id'],
|
|
module: `/api/modules/${perm.moduleId}`,
|
|
canView: perm.canView || false,
|
|
canCreate: perm.canCreate || false,
|
|
canEdit: perm.canEdit || false,
|
|
canDelete: perm.canDelete || false,
|
|
canExport: perm.canExport || false,
|
|
canManage: perm.canManage || false
|
|
};
|
|
|
|
const permResponse = await fetch('/api/role_permissions', {
|
|
method: 'POST',
|
|
credentials: 'same-origin',
|
|
headers: {
|
|
'Content-Type': 'application/ld+json',
|
|
'Accept': 'application/ld+json'
|
|
},
|
|
body: JSON.stringify(permPayload)
|
|
});
|
|
|
|
if (!permResponse.ok) {
|
|
const errorText = await permResponse.text();
|
|
console.error('Permission creation error:', errorText);
|
|
}
|
|
}
|
|
|
|
toast.add({
|
|
severity: 'success',
|
|
summary: 'Erfolg',
|
|
detail: `Rolle wurde ${isEditMode.value ? 'aktualisiert' : 'erstellt'}`,
|
|
life: 3000
|
|
});
|
|
|
|
dialogVisible.value = false;
|
|
await fetchRoles();
|
|
} catch (error) {
|
|
console.error('Error saving role:', error);
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Fehler',
|
|
detail: 'Rolle konnte nicht gespeichert werden: ' + error.message,
|
|
life: 3000
|
|
});
|
|
} finally {
|
|
saving.value = false;
|
|
}
|
|
};
|
|
|
|
const confirmDelete = (role) => {
|
|
roleToDelete.value = role;
|
|
deleteDialogVisible.value = true;
|
|
};
|
|
|
|
const deleteRole = async () => {
|
|
deleting.value = true;
|
|
try {
|
|
const response = await fetch(`/api/roles/${roleToDelete.value.id}`, {
|
|
method: 'DELETE',
|
|
credentials: 'same-origin'
|
|
});
|
|
|
|
if (!response.ok) throw new Error('Fehler beim Löschen');
|
|
|
|
toast.add({
|
|
severity: 'success',
|
|
summary: 'Erfolg',
|
|
detail: 'Rolle wurde gelöscht',
|
|
life: 3000
|
|
});
|
|
|
|
deleteDialogVisible.value = false;
|
|
await fetchRoles();
|
|
} catch (error) {
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Fehler',
|
|
detail: 'Rolle konnte nicht gelöscht werden',
|
|
life: 3000
|
|
});
|
|
} finally {
|
|
deleting.value = false;
|
|
}
|
|
};
|
|
|
|
onMounted(() => {
|
|
fetchRoles();
|
|
fetchModules();
|
|
});
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.role-management {
|
|
padding: 1.5rem;
|
|
|
|
@media (max-width: 768px) {
|
|
padding: 1rem;
|
|
}
|
|
}
|
|
|
|
.page-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 1.5rem;
|
|
flex-wrap: wrap;
|
|
gap: 1rem;
|
|
|
|
h2 {
|
|
margin: 0;
|
|
font-size: 1.75rem;
|
|
|
|
@media (max-width: 768px) {
|
|
font-size: 1.5rem;
|
|
}
|
|
}
|
|
}
|
|
|
|
.action-buttons {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
justify-content: center;
|
|
}
|
|
|
|
.role-dialog {
|
|
.form-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1.5rem;
|
|
}
|
|
|
|
.form-field {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
|
|
label {
|
|
font-weight: 600;
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.p-error {
|
|
color: var(--p-red-500);
|
|
font-size: 0.875rem;
|
|
}
|
|
}
|
|
|
|
.permissions-section {
|
|
h3 {
|
|
margin: 0 0 1rem 0;
|
|
font-size: 1.25rem;
|
|
}
|
|
|
|
.permissions-matrix {
|
|
border: 1px solid var(--p-surface-border);
|
|
border-radius: 6px;
|
|
overflow: hidden;
|
|
|
|
.matrix-header,
|
|
.matrix-row {
|
|
display: grid;
|
|
grid-template-columns: 200px repeat(6, 1fr);
|
|
|
|
@media (max-width: 1024px) {
|
|
grid-template-columns: 150px repeat(6, 1fr);
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
grid-template-columns: 120px repeat(6, 60px);
|
|
overflow-x: auto;
|
|
}
|
|
}
|
|
|
|
.matrix-header {
|
|
background: var(--p-content-background);
|
|
font-weight: 600;
|
|
border-bottom: 2px solid var(--p-surface-border);
|
|
|
|
.permission-cell {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
font-size: 0.85rem;
|
|
|
|
i {
|
|
font-size: 1rem;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
span {
|
|
display: none;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.matrix-row {
|
|
&:not(:last-child) {
|
|
border-bottom: 1px solid var(--p-surface-border);
|
|
}
|
|
}
|
|
|
|
.module-cell,
|
|
.permission-cell {
|
|
padding: 0.75rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.module-cell {
|
|
justify-content: flex-start;
|
|
font-weight: 500;
|
|
border-right: 1px solid var(--p-surface-border);
|
|
|
|
i {
|
|
color: var(--p-primary-color);
|
|
}
|
|
}
|
|
|
|
.permission-cell {
|
|
border-right: 1px solid var(--p-surface-border);
|
|
|
|
&:last-child {
|
|
border-right: none;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.text-muted {
|
|
color: var(--p-text-muted-color);
|
|
font-size: 0.875rem;
|
|
}
|
|
</style>
|