feat(delegations): UI fuer Berechtigungs-Delegationen — Modul 2/16 des UI-Backlogs
Backend existierte vollstaendig (5 Endpoints: list/create/update/delete/ active-check, delegations:read/write seit Audit-Fix im Katalog), Frontend hatte 0% Abdeckung. Modul folgt dem Approvals-Muster (Modul 1): - api/delegations.ts: TanStack-Hooks (useDelegations mit direction-Filter, useActiveDelegations, create/update/delete-Mutations mit Cache-Invalidierung) - pages/Delegations.tsx: Richtungstabs (alle/von mir/an mich), Karten mit Phasen-Badges (aktiv/geplant/abgelaufen/inaktiv), Erstellen-Dialog mit Empfaenger-Picker (useUsers, sich selbst ausschliessend), Start/Ende- Datetime, Scope-Toggle (alle Berechtigungen), Aktivieren/Deaktivieren, Loeschen mit Confirm — Aktionen hinter delegations:write gegated - Route /delegations (PermissionRoute delegations:read) — als Core-Route bewusst statisch registriert (Phase-Q-Regel: nur Plugin-Routen laufen ueber Manifeste) Sidebar-Entry order 92 (ArrowRightLeft-Icon) - i18n delegations.* + nav.delegations de/en Verifikation: Vitest 9/9 (Rendering, Tabs, Phasen, Permission-Gating, Create-Flow, Toggle, Delete) · tsc exit 0 · production build exit 0.
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Delegations API client — temporary permission handovers between users.
|
||||
*
|
||||
* Backend: /api/v1/delegations (list, create, update, delete, active-check).
|
||||
* Lifecycle: active while now is within [start_at, end_at] and active=true.
|
||||
* Permissions: delegations:read / delegations:write.
|
||||
*/
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { apiGet, apiPost, apiPut, apiDelete } from '@/api/client';
|
||||
|
||||
export type DelegationDirection = 'from' | 'to' | 'all';
|
||||
|
||||
export interface Delegation {
|
||||
id: string;
|
||||
from_user_id: string;
|
||||
to_user_id: string;
|
||||
start_at: string | null;
|
||||
end_at: string | null;
|
||||
scope: Record<string, unknown> | null;
|
||||
active: boolean;
|
||||
tenant_id: string;
|
||||
created_at: string | null;
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
export interface DelegationListResponse {
|
||||
items: Delegation[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface DelegationCreatePayload {
|
||||
to_user_id: string;
|
||||
start_at: string;
|
||||
end_at: string;
|
||||
scope?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export interface DelegationUpdatePayload {
|
||||
start_at?: string;
|
||||
end_at?: string;
|
||||
scope?: Record<string, unknown> | null;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
export interface ActiveDelegationCheck {
|
||||
is_active: boolean;
|
||||
active_delegations: Delegation[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
// ─── Query hooks ─────────────────────────────────────────────
|
||||
|
||||
export function useDelegations(direction: DelegationDirection = 'all') {
|
||||
return useQuery<DelegationListResponse>({
|
||||
queryKey: ['delegations', direction],
|
||||
queryFn: () =>
|
||||
apiGet<DelegationListResponse>(`/delegations?direction=${direction}`),
|
||||
});
|
||||
}
|
||||
|
||||
export function useActiveDelegations() {
|
||||
return useQuery<ActiveDelegationCheck>({
|
||||
queryKey: ['delegations', 'active-check'],
|
||||
queryFn: () => apiGet<ActiveDelegationCheck>('/delegations/active'),
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Mutation hooks ──────────────────────────────────────────
|
||||
|
||||
function useInvalidateDelegations() {
|
||||
const qc = useQueryClient();
|
||||
return () => {
|
||||
qc.invalidateQueries({ queryKey: ['delegations'] });
|
||||
};
|
||||
}
|
||||
|
||||
export function useCreateDelegation() {
|
||||
const invalidate = useInvalidateDelegations();
|
||||
return useMutation<Delegation, Error, DelegationCreatePayload>({
|
||||
mutationFn: (data) => apiPost<Delegation>('/delegations', data),
|
||||
onSuccess: invalidate,
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateDelegation() {
|
||||
const invalidate = useInvalidateDelegations();
|
||||
return useMutation<
|
||||
Delegation,
|
||||
Error,
|
||||
{ delegationId: string; payload: DelegationUpdatePayload }
|
||||
>({
|
||||
mutationFn: ({ delegationId, payload }) =>
|
||||
apiPut<Delegation>(`/delegations/${delegationId}`, payload),
|
||||
onSuccess: invalidate,
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteDelegation() {
|
||||
const invalidate = useInvalidateDelegations();
|
||||
return useMutation<void, Error, string>({
|
||||
mutationFn: (delegationId) =>
|
||||
apiDelete(`/delegations/${delegationId}`),
|
||||
onSuccess: invalidate,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user