query( 'SELECT r.*, (SELECT COUNT(*) FROM staff s WHERE s.role_id=r.id) + (SELECT COUNT(*) FROM staff_department sd WHERE sd.role_id=r.id) AS staff_count FROM roles r ORDER BY r.name' )->fetchAll(); api_json([ 'permissionKeys' => $rolePermissionKeys, 'permissionLabels' => permission_labels(), 'permissionGroups' => array_values(array_map(fn($g) => ['title' => $g['title'], 'keys' => array_keys($g['items'])], permission_catalog())), 'defaultAgentPermissions' => default_permissions('agent'), 'items' => array_map(fn(array $r) => [ 'id' => (int) $r['id'], 'name' => $r['name'], 'permissions' => json_decode($r['permissions'] ?? '{}', true) ?? [], 'effectivePermissions' => permission_effective(json_decode($r['permissions'] ?? '{}', true) ?? []), 'staffCount' => (int) $r['staff_count'], 'isActive' => (bool) ($r['is_active'] ?? 1), 'notes' => $r['notes'] ?? null, 'createdAt' => $r['created_at'] ?? null, 'updatedAt' => $r['updated_at'] ?? null, ], $roles), ]); } $body = api_request_body(); $action = $body['action'] ?? ''; if ($action === 'save') { $id = (int) ($body['id'] ?? 0); $name = trim((string) ($body['name'] ?? '')); if ($name === '') api_error(422, 'NAME_REQUIRED', 'Bitte einen Namen angeben.'); $incoming = (array) ($body['permissions'] ?? []); // Rechte, die eine ältere App-Version nicht kennt (und deshalb nicht mitschickt), bleiben unverändert bzw. bekommen den Standardwert $existingPerms = default_permissions('agent'); if ($id) { $ep = $pdo->prepare('SELECT permissions FROM roles WHERE id = ?'); $ep->execute([$id]); $existingPerms = array_merge($existingPerms, json_decode((string) $ep->fetchColumn(), true) ?: []); } $perms = []; foreach ($rolePermissionKeys as $p) { $want = array_key_exists($p, $incoming) ? !empty($incoming[$p]) : !empty($existingPerms[$p]); // Wer kein echter Administrator ist, kann keine Rechte vergeben, die er selbst nicht hat if ($staff['role'] !== 'admin' && $want && !has_permission($staff, $p)) $want = !empty($existingPerms[$p]) && $id > 0; $perms[$p] = $want; } $json = json_encode($perms); $dup = $pdo->prepare('SELECT COUNT(*) FROM roles WHERE LOWER(name) = LOWER(?) AND id <> ?'); $dup->execute([$name, $id]); if ((int) $dup->fetchColumn() > 0) api_error(422, 'NAME_TAKEN', 'Eine Rolle mit diesem Namen existiert bereits.'); // Fehlende Schlüssel (ältere App-Version) lassen Status/Notizen unverändert $cur = null; if ($id) { $c = $pdo->prepare('SELECT is_active, notes FROM roles WHERE id = ?'); $c->execute([$id]); $cur = $c->fetch() ?: null; } $isActive = array_key_exists('isActive', $body) ? (!empty($body['isActive']) ? 1 : 0) : (int) ($cur['is_active'] ?? 1); $notes = array_key_exists('notes', $body) ? (trim((string) $body['notes']) ?: null) : ($cur['notes'] ?? null); if ($id === 0) { $pdo->prepare('INSERT INTO roles (name, permissions, is_active, notes, created_at, updated_at) VALUES (?,?,?,?,?,?)')->execute([$name, $json, $isActive, $notes, db_now(), db_now()]); $newId = (int) $pdo->lastInsertId(); audit_log($staff['id'], "Rolle erstellt: {$name} (Mobile App)"); } else { $pdo->prepare('UPDATE roles SET name=?, permissions=?, is_active=?, notes=?, updated_at=? WHERE id=?')->execute([$name, $json, $isActive, $notes, db_now(), $id]); $newId = $id; audit_log($staff['id'], "Rolle aktualisiert: {$name} (Mobile App)"); } api_json(['id' => $newId]); } if ($action === 'delete') { $id = (int) ($body['id'] ?? 0); $pdo->prepare('UPDATE staff SET role_id = NULL WHERE role_id = ?')->execute([$id]); $pdo->prepare('UPDATE staff_department SET role_id = NULL WHERE role_id = ?')->execute([$id]); $pdo->prepare('DELETE FROM roles WHERE id = ?')->execute([$id]); audit_log($staff['id'], "Rolle gelöscht: #{$id} (Mobile App)"); api_json(['ok' => true]); } api_error(422, 'INVALID_ACTION', 'Unbekannte Aktion.');