Files
leocrm/frontend/src/api/permissionTemplates.ts
T
Agent Zero 33b4b52206 feat(templates): UI fuer Berechtigungs-Vorlagen — Modul 6/16 des UI-Backlogs
Backend existierte vollstaendig (list mit entity_type-Filter, create, update,
delete, apply — templates:read/write, seit Audit-Fix im Katalog), Frontend
hatte 0% Abdeckung.

- api/permissionTemplates.ts: TanStack-Hooks (usePermissionTemplates,
  create/update/delete/apply mit Cache-Invalidierung, TemplateLevel-Typ)
- pages/PermissionTemplates.tsx: Template-Karten (Name, Level-Badge,
  Entity-Type, Auto-Share-Zusammenfassung), Create/Edit-Dialog mit
  Level-Select und JSON-Textarea inkl. Array-Validierung mit Fehlertext,
  Apply-Dialog (Entity-Type vorbelegt, Entity-ID), Ergebnis-Banner mit
  Anzahl erstellter Berechtigungen, Delete mit Confirm — alle Aktionen
  hinter templates:write gegated
- Platzierung: Settings-Subpage /settings/permission-templates (statisch,
  Core-Route) + Settings-Nav-Item
- i18n permissionTemplates.* de/en

Verifikation: Vitest 10/10 (Rendering, Level-Badges, Create mit validem +
invalidem JSON, Edit prefilled, Apply mit Ergebnis, Delete-Confirm,
Permission-Gating, Empty/Error) · tsc exit 0 · production build exit 0.
2026-09-13 10:27:10 +02:00

112 lines
3.4 KiB
TypeScript

/**
* Permission Templates API client — reusable permission presets.
*
* Backend: /api/v1/permission-templates (list, create, update, delete,
* apply). Templates define default sharing rules; applying them creates
* entity_permissions entries automatically.
* Permissions: templates:read (list) / templates:write (everything else).
*/
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiGet, apiPost, apiPut, apiDelete } from '@/api/client';
export type TemplateLevel = 'read' | 'write' | 'admin' | 'delete';
export interface ShareEntry {
principal_type?: string;
principal_id?: string;
level?: string;
}
export interface PermissionTemplate {
id: string;
name: string;
entity_type: string;
trigger_condition: Record<string, unknown> | null;
auto_share_with: ShareEntry[] | null;
level: TemplateLevel;
tenant_id: string;
created_at: string | null;
updated_at: string | null;
}
export interface TemplateListResponse {
items: PermissionTemplate[];
total: number;
}
export interface TemplateCreatePayload {
name: string;
entity_type: string;
level?: TemplateLevel;
trigger_condition?: Record<string, unknown> | null;
auto_share_with?: ShareEntry[] | null;
}
export type TemplateUpdatePayload = Partial<TemplateCreatePayload>;
export interface TemplateApplyPayload {
entity_type: string;
entity_id: string;
template_id?: string | null;
}
export interface TemplateApplyResponse {
applied: Array<Record<string, unknown>>;
count: number;
}
// ─── Query hooks ─────────────────────────────────────────────
export function usePermissionTemplates(entityType?: string) {
const qs = entityType ? `?entity_type=${encodeURIComponent(entityType)}` : '';
return useQuery<TemplateListResponse>({
queryKey: ['permission-templates', entityType ?? null],
queryFn: () => apiGet<TemplateListResponse>(`/permission-templates${qs}`),
});
}
// ─── Mutation hooks ──────────────────────────────────────────
function useInvalidateTemplates() {
const qc = useQueryClient();
return () => {
qc.invalidateQueries({ queryKey: ['permission-templates'] });
};
}
export function useCreatePermissionTemplate() {
const invalidate = useInvalidateTemplates();
return useMutation<PermissionTemplate, Error, TemplateCreatePayload>({
mutationFn: (data) => apiPost<PermissionTemplate>('/permission-templates', data),
onSuccess: invalidate,
});
}
export function useUpdatePermissionTemplate() {
const invalidate = useInvalidateTemplates();
return useMutation<
PermissionTemplate,
Error,
{ templateId: string; payload: TemplateUpdatePayload }
>({
mutationFn: ({ templateId, payload }) =>
apiPut<PermissionTemplate>(`/permission-templates/${templateId}`, payload),
onSuccess: invalidate,
});
}
export function useDeletePermissionTemplate() {
const invalidate = useInvalidateTemplates();
return useMutation<void, Error, string>({
mutationFn: (templateId) => apiDelete(`/permission-templates/${templateId}`),
onSuccess: invalidate,
});
}
export function useApplyPermissionTemplate() {
return useMutation<TemplateApplyResponse, Error, TemplateApplyPayload>({
mutationFn: (data) => apiPost<TemplateApplyResponse>('/permission-templates/apply', data),
});
}