/** * Recipients Store - Platform-aware recipient management * Uses Firebase abstraction layer for cross-platform support */ import { create } from 'zustand'; import { doc } from '../firebase'; import { RecipientService, Recipient } from '../services/recipientService'; import { withGlobalLoading } from './uiStore'; export interface RecipientDetailCacheEntry { recipient: Recipient | null; loading: boolean; error: string | null; lastFetched?: number; } const buildDetailEntry = ( existing: RecipientDetailCacheEntry | undefined, overrides: Partial ): RecipientDetailCacheEntry => ({ recipient: existing?.recipient ?? null, loading: existing?.loading ?? false, error: existing?.error ?? null, lastFetched: existing?.lastFetched, ...overrides, }); const snapshotExists = (docSnap: any): boolean => { const existsValue = docSnap?.exists; if (typeof existsValue === 'function') { try { return !!existsValue.call(docSnap); } catch { return false; } } return !!existsValue; }; const normalizeRecipient = (data: Recipient | (Recipient & { createdAt?: any; updatedAt?: any }) | null | undefined): Recipient | null => { if (!data) { return null; } const createdAtValue = (data.createdAt as any)?.toDate?.() ?? data.createdAt ?? new Date(); const updatedAtValue = (data.updatedAt as any)?.toDate?.() ?? data.updatedAt ?? new Date(); return { ...data, createdAt: createdAtValue, updatedAt: updatedAtValue, }; }; interface RecipientsState { // State recipients: Recipient[]; recipientDetails: Record; loading: boolean; error: string | null; addError: string | null; updateError: string | null; deleteError: string | null; isInitialized: boolean; currentUser: any | null; // Actions setRecipients: (recipients: Recipient[]) => void; setLoading: (loading: boolean) => void; setError: (error: string | null) => void; setAddError: (error: string | null) => void; setUpdateError: (error: string | null) => void; setDeleteError: (error: string | null) => void; setIsInitialized: (isInitialized: boolean) => void; setCurrentUser: (user: any | null) => void; // Data actions fetchRecipients: () => Promise; addRecipient: (recipientData: { fullName: string; phoneNumber: string }) => Promise; updateRecipient: (recipientId: string, updates: { fullName?: string; phoneNumber?: string }) => Promise; deleteRecipient: (recipientId: string) => Promise; refreshRecipients: () => Promise; ensureRecipientDetail: (recipientId: string) => void; refreshRecipientDetail: (recipientId: string) => Promise; invalidateRecipientDetail: (recipientId: string) => Promise; removeRecipientDetail: (recipientId: string) => void; clearRecipientDetails: () => void; // Error clearing clearErrors: () => void; clearAddError: () => void; clearUpdateError: () => void; clearDeleteError: () => void; // Initialize initialize: (user: any | null) => Promise; } export const useRecipientsStore = create((set, get) => ({ // Initial state recipients: [], recipientDetails: {}, loading: false, error: null, addError: null, updateError: null, deleteError: null, isInitialized: false, currentUser: null, // Setters setRecipients: (recipients) => set({ recipients }), setLoading: (loading) => set({ loading }), setError: (error) => set({ error }), setAddError: (addError) => set({ addError }), setUpdateError: (updateError) => set({ updateError }), setDeleteError: (deleteError) => set({ deleteError }), setIsInitialized: (isInitialized) => set({ isInitialized }), setCurrentUser: (currentUser) => set({ currentUser }), // Fetch recipients fetchRecipients: async () => { const { currentUser } = get(); if (!currentUser?.uid) { set({ recipients: [], recipientDetails: {}, error: null, isInitialized: true }); return; } set({ loading: true, error: null }); try { const userRecipients = await RecipientService.getUserRecipients(currentUser.uid); const normalizedRecipients = userRecipients .map((recipient) => normalizeRecipient(recipient)) .filter((recipient): recipient is Recipient => recipient !== null); set((state) => { const nextDetails = { ...state.recipientDetails }; normalizedRecipients.forEach((recipient) => { const detailEntry = nextDetails[recipient.id]; if (detailEntry) { nextDetails[recipient.id] = buildDetailEntry(detailEntry, { recipient, loading: false, error: null, lastFetched: Date.now(), }); } }); return { recipients: normalizedRecipients, recipientDetails: nextDetails, }; }); } catch (err) { console.error('Error fetching recipients:', err); set({ error: err instanceof Error ? err.message : 'Failed to fetch recipients', recipients: [], recipientDetails: {}, }); } finally { set({ loading: false, isInitialized: true }); } }, // Add recipient addRecipient: async (recipientData: { fullName: string; phoneNumber: string }) => { const { currentUser } = get(); if (!currentUser?.uid) { set({ addError: 'User not found' }); return; } set({ loading: true, addError: null }); try { const result = await withGlobalLoading(() => RecipientService.addRecipient(currentUser.uid, recipientData)); if (!result.success) { set({ addError: result.error || 'Failed to add recipient' }); return; } await get().fetchRecipients(); if (result.recipientId) { await get().invalidateRecipientDetail(result.recipientId); } } catch (err) { set({ addError: err instanceof Error ? err.message : 'Failed to add recipient' }); } finally { set({ loading: false }); } }, // Update recipient updateRecipient: async (recipientId: string, updates: { fullName?: string; phoneNumber?: string }) => { set({ loading: true, updateError: null }); try { const result = await withGlobalLoading(() => RecipientService.updateRecipient(recipientId, updates)); if (!result.success) { set({ updateError: result.error || 'Failed to update recipient' }); return; } await get().fetchRecipients(); await get().invalidateRecipientDetail(recipientId); } catch (err) { set({ updateError: err instanceof Error ? err.message : 'Failed to update recipient' }); } finally { set({ loading: false }); } }, // Delete recipient deleteRecipient: async (recipientId: string) => { set({ loading: true, deleteError: null }); try { const result = await withGlobalLoading(() => RecipientService.deleteRecipient(recipientId)); if (!result.success) { set({ deleteError: result.error || 'Failed to delete recipient' }); return; } await get().fetchRecipients(); get().removeRecipientDetail(recipientId); } catch (err) { set({ deleteError: err instanceof Error ? err.message : 'Failed to delete recipient' }); } finally { set({ loading: false }); } }, // Refresh recipients refreshRecipients: async () => { await get().fetchRecipients(); }, ensureRecipientDetail: (recipientId: string) => { if (!recipientId) { return; } const existing = get().recipientDetails[recipientId]; if (existing?.loading) { return; } if (existing?.lastFetched) { return; } void get().refreshRecipientDetail(recipientId).catch((error) => { set((state) => ({ recipientDetails: { ...state.recipientDetails, [recipientId]: buildDetailEntry(state.recipientDetails[recipientId], { loading: false, error: error instanceof Error ? error.message : 'Failed to fetch recipient', }), }, })); }); }, refreshRecipientDetail: async (recipientId: string) => { if (!recipientId) { return null; } set((state) => ({ recipientDetails: { ...state.recipientDetails, [recipientId]: buildDetailEntry(state.recipientDetails[recipientId], { loading: true, error: null, }), }, })); try { const docRef = doc('recipients', recipientId); const docSnap = await docRef.get(); const recipient = normalizeRecipient(snapshotExists(docSnap) ? (docSnap.data() as Recipient) : null); set((state) => ({ recipientDetails: { ...state.recipientDetails, [recipientId]: buildDetailEntry(state.recipientDetails[recipientId], { recipient, loading: false, error: recipient ? null : 'Recipient not found', lastFetched: Date.now(), }), }, })); return recipient; } catch (error) { set((state) => ({ recipientDetails: { ...state.recipientDetails, [recipientId]: buildDetailEntry(state.recipientDetails[recipientId], { loading: false, error: error instanceof Error ? error.message : 'Failed to fetch recipient', }), }, })); throw error; } }, invalidateRecipientDetail: async (recipientId: string) => { if (!recipientId) { return null; } set((state) => ({ recipientDetails: { ...state.recipientDetails, [recipientId]: buildDetailEntry(state.recipientDetails[recipientId], { loading: true, error: null, lastFetched: undefined, }), }, })); return get().refreshRecipientDetail(recipientId); }, removeRecipientDetail: (recipientId: string) => { set((state) => { const { [recipientId]: _removed, ...rest } = state.recipientDetails; return { recipientDetails: rest }; }); }, clearRecipientDetails: () => { set({ recipientDetails: {} }); }, // Error clearing clearErrors: () => set({ error: null, addError: null, updateError: null, deleteError: null, }), clearAddError: () => set({ addError: null }), clearUpdateError: () => set({ updateError: null }), clearDeleteError: () => set({ deleteError: null }), // Initialize initialize: async (user: any | null) => { const previousUser = get().currentUser; if (previousUser?.uid && previousUser.uid !== user?.uid) { get().clearRecipientDetails(); set({ recipients: [] }); } set({ currentUser: user }); if (user?.uid) { await get().fetchRecipients(); } else { get().clearRecipientDetails(); set({ recipients: [], error: null, isInitialized: true }); } }, })); export const getRecipientDetailFromCache = (recipientId: string): Recipient | null => { if (!recipientId) { return null; } return useRecipientsStore.getState().recipientDetails[recipientId]?.recipient ?? null; };