Amba-Agent-App/lib/stores/recipientsStore.ts
2026-01-16 00:22:35 +03:00

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;
};