387 lines
11 KiB
TypeScript
387 lines
11 KiB
TypeScript
/**
|
|
* 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>
|
|
): 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<string, RecipientDetailCacheEntry>;
|
|
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<void>;
|
|
addRecipient: (recipientData: { fullName: string; phoneNumber: string }) => Promise<void>;
|
|
updateRecipient: (recipientId: string, updates: { fullName?: string; phoneNumber?: string }) => Promise<void>;
|
|
deleteRecipient: (recipientId: string) => Promise<void>;
|
|
refreshRecipients: () => Promise<void>;
|
|
ensureRecipientDetail: (recipientId: string) => void;
|
|
refreshRecipientDetail: (recipientId: string) => Promise<Recipient | null>;
|
|
invalidateRecipientDetail: (recipientId: string) => Promise<Recipient | null>;
|
|
removeRecipientDetail: (recipientId: string) => void;
|
|
clearRecipientDetails: () => void;
|
|
|
|
// Error clearing
|
|
clearErrors: () => void;
|
|
clearAddError: () => void;
|
|
clearUpdateError: () => void;
|
|
clearDeleteError: () => void;
|
|
|
|
// Initialize
|
|
initialize: (user: any | null) => Promise<void>;
|
|
}
|
|
|
|
export const useRecipientsStore = create<RecipientsState>((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;
|
|
};
|