/** * 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; 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; frontendUrl?: string; expiresInHours?: number; maxUses?: number; } export interface InvitationValidation { valid: boolean; reason?: string; mandateId?: string; mandateName?: string; featureInstanceId?: string; roleIds: string[]; roleLabels?: string[]; targetUsername?: string; email?: string; } /** * Hook for managing invitations */ export function useInvitations() { const [invitations, setInvitations] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [pagination, setPagination] = useState(null); // Store current context for refetch const currentMandateIdRef = useRef(''); 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 => { 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(); params.append('frontendUrl', window.location.origin); 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 payload = { ...data, frontendUrl: data.frontendUrl || window.location.origin, }; const response = await api.post('/api/invitations/', payload, { 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 => { 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;