- 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>
208 lines
5.6 KiB
TypeScript
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 };
|