frontend_nyla/src/hooks/useInvitations.ts
2026-01-26 01:29:24 +01:00

258 lines
7.1 KiB
TypeScript

/**
* useInvitations Hook
*
* Hook for managing invitations (creating, listing, validating, accepting).
* Uses the /api/invitations endpoints.
*/
import { useState, useCallback, useRef } from 'react';
import api from '../api';
// Types
export interface PaginationParams {
page?: number;
pageSize?: number;
search?: string;
filters?: Record<string, any>;
sort?: Array<{ field: string; direction: 'asc' | 'desc' }>;
}
export interface PaginationMetadata {
currentPage: number;
pageSize: number;
totalItems: number;
totalPages: number;
}
export interface Invitation {
id: string;
token: string;
mandateId: string;
featureInstanceId?: string;
roleIds: string[];
targetUsername: string;
email?: string;
createdBy: string;
createdAt: number;
expiresAt: number;
usedBy?: string;
usedAt?: number;
revokedAt?: number;
maxUses: number;
currentUses: number;
inviteUrl: string;
emailSent?: boolean;
isExpired?: boolean;
isUsedUp?: boolean;
}
export interface InvitationCreate {
targetUsername: string;
email?: string;
roleIds: string[];
featureInstanceId?: string;
expiresInHours?: number;
maxUses?: number;
}
export interface InvitationValidation {
valid: boolean;
reason?: string;
mandateId?: string;
mandateName?: string;
featureInstanceId?: string;
roleIds: string[];
roleLabels?: string[];
targetUsername?: string;
}
/**
* Hook for managing invitations
*/
export function useInvitations() {
const [invitations, setInvitations] = useState<Invitation[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [pagination, setPagination] = useState<PaginationMetadata | null>(null);
// Store current context for refetch
const currentMandateIdRef = useRef<string>('');
const currentOptionsRef = useRef<{ includeUsed?: boolean; includeExpired?: boolean }>({});
/**
* Fetch all invitations for a mandate with optional pagination
*/
const fetchInvitations = useCallback(async (
mandateIdOrPagination?: string | PaginationParams,
options?: { includeUsed?: boolean; includeExpired?: boolean }
): Promise<Invitation[]> => {
setLoading(true);
setError(null);
let mandateId: string;
let paginationParams: PaginationParams = {};
// Handle backward compatibility
if (typeof mandateIdOrPagination === 'string') {
mandateId = mandateIdOrPagination;
currentMandateIdRef.current = mandateId;
if (options) {
currentOptionsRef.current = options;
}
} else if (mandateIdOrPagination && typeof mandateIdOrPagination === 'object') {
// Called with pagination params only (refetch from FormGeneratorTable)
paginationParams = mandateIdOrPagination;
mandateId = currentMandateIdRef.current;
} else {
mandateId = currentMandateIdRef.current;
}
if (!mandateId) {
setLoading(false);
return [];
}
const fetchOptions = options || currentOptionsRef.current;
try {
const params = new URLSearchParams();
if (fetchOptions?.includeUsed) params.append('includeUsed', 'true');
if (fetchOptions?.includeExpired) params.append('includeExpired', 'true');
if (Object.keys(paginationParams).length > 0) {
params.append('pagination', JSON.stringify(paginationParams));
}
const response = await api.get(`/api/invitations/?${params.toString()}`, {
headers: { 'X-Mandate-Id': mandateId }
});
let data: Invitation[] = [];
if (response.data?.items && Array.isArray(response.data.items)) {
data = response.data.items;
if (response.data.pagination) {
setPagination(response.data.pagination);
}
} else {
data = Array.isArray(response.data) ? response.data : [];
}
setInvitations(data);
return data;
} catch (err: any) {
const errorMessage = err.response?.data?.detail || err.message || 'Failed to fetch invitations';
setError(errorMessage);
setInvitations([]);
setPagination(null);
return [];
} finally {
setLoading(false);
}
}, []);
/**
* Create a new invitation
*/
const createInvitation = useCallback(async (
mandateId: string,
data: InvitationCreate
): Promise<{ success: boolean; data?: Invitation; error?: string }> => {
setLoading(true);
setError(null);
try {
const response = await api.post('/api/invitations/', data, {
headers: { 'X-Mandate-Id': mandateId }
});
return { success: true, data: response.data };
} catch (err: any) {
const errorMessage = err.response?.data?.detail || err.message || 'Failed to create invitation';
setError(errorMessage);
return { success: false, error: errorMessage };
} finally {
setLoading(false);
}
}, []);
/**
* Revoke an invitation
*/
const revokeInvitation = useCallback(async (
mandateId: string,
invitationId: string
): Promise<{ success: boolean; error?: string }> => {
setLoading(true);
setError(null);
try {
await api.delete(`/api/invitations/${invitationId}`, {
headers: { 'X-Mandate-Id': mandateId }
});
// Optimistically update local state
setInvitations(prev => prev.filter(inv => inv.id !== invitationId));
return { success: true };
} catch (err: any) {
const errorMessage = err.response?.data?.detail || err.message || 'Failed to revoke invitation';
setError(errorMessage);
return { success: false, error: errorMessage };
} finally {
setLoading(false);
}
}, []);
/**
* Validate an invitation token (public - no auth required)
*/
const validateInvitation = useCallback(async (
token: string
): Promise<InvitationValidation> => {
setLoading(true);
setError(null);
try {
const response = await api.get(`/api/invitations/validate/${token}`);
return response.data;
} catch (err: any) {
const errorMessage = err.response?.data?.detail || err.message || 'Failed to validate invitation';
setError(errorMessage);
return {
valid: false,
reason: errorMessage,
roleIds: []
};
} finally {
setLoading(false);
}
}, []);
/**
* Accept an invitation (requires authentication)
*/
const acceptInvitation = useCallback(async (
token: string
): Promise<{ success: boolean; data?: any; error?: string }> => {
setLoading(true);
setError(null);
try {
const response = await api.post(`/api/invitations/accept/${token}`);
return { success: true, data: response.data };
} catch (err: any) {
const errorMessage = err.response?.data?.detail || err.message || 'Failed to accept invitation';
setError(errorMessage);
return { success: false, error: errorMessage };
} finally {
setLoading(false);
}
}, []);
return {
invitations,
loading,
error,
pagination,
fetchInvitations,
createInvitation,
revokeInvitation,
validateInvitation,
acceptInvitation,
};
}
export default useInvitations;