feat(tenants): UI fuer Mandanten-Verwaltung — Modul 4/16 des UI-Backlogs

Backend existierte vollstaendig (list, create, list users, assign user;
tenants:read/write), Frontend hatte 0% Abdeckung.

- api/tenants.ts: TanStack-Hooks (useTenants, useTenantUsers mit
  enabled-Gating, create, assignUser mit Cache-Invalidierung)
- pages/Tenants.tsx: Tenant-Karten (Name, Slug, Standard-Badge),
  expandierbare User-Liste pro Tenant (Rolle, E-Mail), Create-Dialog
  (Name + Slug mit Auto-Normalisierung), Assign-User-Dialog mit
  User-Picker (bereits zugewiesene gefiltert) — Create/Users/Assign
  hinter tenants:write gegated
- Platzierung: Settings-Subpage /settings/tenants (statisch, Core-Route)
  + Settings-Nav-Item
- i18n tenants.* de/en

Verifikation: Vitest 8/8 (Rendering, Standard-Badge, Expand-User-Liste,
Create-Flow, Assign-Flow, Permission-Gating, Empty/Error) · tsc exit 0 ·
production build exit 0.
This commit is contained in:
Agent Zero
2026-09-13 09:29:42 +02:00
parent 8d8beebd38
commit 79ca1cbe6d
7 changed files with 655 additions and 0 deletions
+85
View File
@@ -0,0 +1,85 @@
/**
* Tenants API client — multi-tenant management.
*
* Backend: /api/v1/tenants (list, create, list users, assign user).
* Permissions: tenants:read (list) / tenants:write (create, users, assign).
*/
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiGet, apiPost } from '@/api/client';
export interface Tenant {
id: string;
name: string;
slug: string;
is_default: boolean;
}
export interface TenantListResponse {
items: Tenant[];
}
export interface TenantUser {
id: string;
email: string;
name: string;
role: string;
is_active: boolean;
}
export interface TenantUsersResponse {
items: TenantUser[];
}
export interface TenantCreatePayload {
name: string;
slug: string;
}
// ─── Query hooks ─────────────────────────────────────────────
export function useTenants() {
return useQuery<TenantListResponse>({
queryKey: ['tenants'],
queryFn: () => apiGet<TenantListResponse>('/tenants'),
});
}
export function useTenantUsers(tenantId: string | null) {
return useQuery<TenantUsersResponse>({
queryKey: ['tenants', tenantId, 'users'],
queryFn: () => apiGet<TenantUsersResponse>(`/tenants/${tenantId}/users`),
enabled: !!tenantId,
});
}
// ─── Mutation hooks ──────────────────────────────────────────
export function useCreateTenant() {
const qc = useQueryClient();
return useMutation<
{ id: string; name: string; slug: string },
Error,
TenantCreatePayload
>({
mutationFn: (data) => apiPost('/tenants', data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['tenants'] });
},
});
}
export function useAssignUserToTenant() {
const qc = useQueryClient();
return useMutation<
{ message: string },
Error,
{ tenantId: string; userId: string }
>({
mutationFn: ({ tenantId, userId }) =>
apiPost(`/tenants/${tenantId}/users`, { user_id: userId }),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['tenants'] });
},
});
}