UMBRA/frontend/src/hooks/useAdmin.ts
Kyle Pope 14fc085009 Phase 5: Shared calendar polish — scoped polling, admin stats, dual panel fix, edge case handling
- Scope shared calendar polling to CalendarPage only (other consumers no longer poll)
- Add admin sharing stats card (owned/member/invites sent/received) in UserDetailSection
- Fix dual EventDetailPanel mount via JS media query breakpoint (replaces CSS hidden)
- Auto-close panel + toast when shared calendar is removed while viewing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 15:38:52 +08:00

208 lines
5.6 KiB
TypeScript

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import api, { getErrorMessage } from '@/lib/api';
import type {
AdminUserDetail,
AdminDashboardData,
SystemConfig,
AuditLogEntry,
UserRole,
} from '@/types';
interface UserListResponse {
users: AdminUserDetail[];
total: number;
}
interface AuditLogResponse {
entries: AuditLogEntry[];
total: number;
}
interface CreateUserPayload {
username: string;
password: string;
role: UserRole;
email?: string;
first_name?: string;
last_name?: string;
preferred_name?: string;
}
interface UpdateRolePayload {
userId: number;
role: UserRole;
}
interface ResetPasswordResult {
message: string;
temporary_password: string;
}
// ── Queries ──────────────────────────────────────────────────────────────────
export function useAdminUsers() {
return useQuery<AdminUserDetail[]>({
queryKey: ['admin', 'users'],
queryFn: async () => {
const { data } = await api.get<UserListResponse>('/admin/users');
return data.users;
},
});
}
export function useAdminUserDetail(userId: number | null) {
return useQuery<AdminUserDetail>({
queryKey: ['admin', 'users', userId],
queryFn: async () => {
const { data } = await api.get<AdminUserDetail>(`/admin/users/${userId}`);
return data;
},
enabled: userId !== null,
});
}
interface SharingStats {
shared_calendars_owned: number;
calendars_member_of: number;
pending_invites_sent: number;
pending_invites_received: number;
}
export function useAdminSharingStats(userId: number | null) {
return useQuery<SharingStats>({
queryKey: ['admin', 'users', userId, 'sharing-stats'],
queryFn: async () => {
const { data } = await api.get<SharingStats>(`/admin/users/${userId}/sharing-stats`);
return data;
},
enabled: userId !== null,
});
}
export function useAdminDashboard() {
return useQuery<AdminDashboardData>({
queryKey: ['admin', 'dashboard'],
queryFn: async () => {
const { data } = await api.get<AdminDashboardData>('/admin/dashboard');
return data;
},
});
}
export function useAdminConfig() {
return useQuery<SystemConfig>({
queryKey: ['admin', 'config'],
queryFn: async () => {
const { data } = await api.get<SystemConfig>('/admin/config');
return data;
},
});
}
export function useAuditLog(
page: number,
perPage: number,
action?: string,
targetUserId?: number
) {
return useQuery<AuditLogResponse>({
queryKey: ['admin', 'audit-log', page, perPage, action, targetUserId],
queryFn: async () => {
const params: Record<string, unknown> = { page, per_page: perPage };
if (action) params.action = action;
if (targetUserId) params.target_user_id = targetUserId;
const { data } = await api.get<AuditLogResponse>('/admin/audit-log', { params });
return data;
},
});
}
// ── Mutations ─────────────────────────────────────────────────────────────────
function useAdminMutation<TVariables, TData = unknown>(
mutationFn: (vars: TVariables) => Promise<TData>,
onSuccess?: () => void
) {
const queryClient = useQueryClient();
return useMutation<TData, Error, TVariables>({
mutationFn,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin'] });
onSuccess?.();
},
});
}
export function useCreateUser() {
return useAdminMutation(async (payload: CreateUserPayload) => {
const { data } = await api.post('/admin/users', payload);
return data;
});
}
export function useUpdateRole() {
return useAdminMutation(async ({ userId, role }: UpdateRolePayload) => {
const { data } = await api.put(`/admin/users/${userId}/role`, { role });
return data;
});
}
export function useResetPassword() {
return useAdminMutation(async (userId: number) => {
const { data } = await api.post<ResetPasswordResult>(`/admin/users/${userId}/reset-password`);
return data;
});
}
export function useDisableMfa() {
return useAdminMutation(async (userId: number) => {
const { data } = await api.post(`/admin/users/${userId}/disable-mfa`);
return data;
});
}
export function useEnforceMfa() {
return useAdminMutation(async (userId: number) => {
const { data } = await api.put(`/admin/users/${userId}/enforce-mfa`, { enforce: true });
return data;
});
}
export function useRemoveMfaEnforcement() {
return useAdminMutation(async (userId: number) => {
const { data } = await api.put(`/admin/users/${userId}/enforce-mfa`, { enforce: false });
return data;
});
}
export function useToggleUserActive() {
return useAdminMutation(async ({ userId, active }: { userId: number; active: boolean }) => {
const { data } = await api.put(`/admin/users/${userId}/active`, { is_active: active });
return data;
});
}
export function useRevokeSessions() {
return useAdminMutation(async (userId: number) => {
const { data } = await api.delete(`/admin/users/${userId}/sessions`);
return data;
});
}
export function useDeleteUser() {
return useAdminMutation(async (userId: number) => {
const { data } = await api.delete(`/admin/users/${userId}`);
return data;
});
}
export function useUpdateConfig() {
return useAdminMutation(async (config: Partial<SystemConfig>) => {
const { data } = await api.put('/admin/config', config);
return data;
});
}
// Re-export getErrorMessage for convenience in admin components
export { getErrorMessage };