myCRM/assets/js/views/RoleManagement.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>