import { createSlice, createAsyncThunk, createEntityAdapter, PayloadAction } from '@reduxjs/toolkit'; import { RootState } from '../store'; export interface Child { id: string; familyId: string; name: string; birthDate: string; gender: 'male' | 'female' | 'other'; profilePhoto?: string; metadata?: Record; createdAt: string; updatedAt: string; // Offline metadata _optimistic?: boolean; _localId?: string; _version?: number; } // Create entity adapter const childrenAdapter = createEntityAdapter({ selectId: (child) => child.id, sortComparer: (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), }); // Async thunks export const fetchChildren = createAsyncThunk( 'children/fetch', async (familyId: string) => { const response = await fetch(`/api/v1/children?familyId=${familyId}`); if (!response.ok) { throw new Error('Failed to fetch children'); } const data = await response.json(); return data.data; } ); export const createChild = createAsyncThunk( 'children/create', async (child: Omit, { rejectWithValue }) => { try { const response = await fetch('/api/v1/children', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(child), }); if (!response.ok) { throw new Error('Failed to create child'); } return await response.json(); } catch (error: any) { return rejectWithValue(error.message); } } ); export const updateChild = createAsyncThunk( 'children/update', async ({ id, updates }: { id: string; updates: Partial }, { rejectWithValue }) => { try { const response = await fetch(`/api/v1/children/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updates), }); if (!response.ok) { throw new Error('Failed to update child'); } return await response.json(); } catch (error: any) { return rejectWithValue(error.message); } } ); const childrenSlice = createSlice({ name: 'children', initialState: childrenAdapter.getInitialState({ loading: false, error: null as string | null, selectedChildId: null as string | null, lastSyncTime: null as string | null, }), reducers: { // Optimistic operations optimisticCreate: (state, action: PayloadAction) => { childrenAdapter.addOne(state, { ...action.payload, _optimistic: true, _localId: action.payload.id, _version: 1, }); }, optimisticUpdate: (state, action: PayloadAction<{ id: string; changes: Partial }>) => { const { id, changes } = action.payload; const existing = state.entities[id]; if (existing) { childrenAdapter.updateOne(state, { id, changes: { ...changes, _optimistic: true, _version: (existing._version || 0) + 1, }, }); } }, rollbackOptimistic: (state, action: PayloadAction) => { const child = state.entities[action.payload]; if (child?._optimistic) { childrenAdapter.removeOne(state, action.payload); } }, markSynced: (state, action: PayloadAction<{ localId: string; serverId: string; serverData: Child }>) => { const { localId, serverId, serverData } = action.payload; childrenAdapter.removeOne(state, localId); childrenAdapter.addOne(state, { ...serverData, _optimistic: false, }); }, // Select child selectChild: (state, action: PayloadAction) => { state.selectedChildId = action.payload; }, clearChildren: (state) => { childrenAdapter.removeAll(state); state.selectedChildId = null; state.error = null; }, }, extraReducers: (builder) => { builder // Fetch children .addCase(fetchChildren.pending, (state) => { state.loading = true; state.error = null; }) .addCase(fetchChildren.fulfilled, (state, action) => { state.loading = false; state.lastSyncTime = new Date().toISOString(); const serverChildren = action.payload.map((c: Child) => ({ ...c, _optimistic: false, })); childrenAdapter.upsertMany(state, serverChildren); }) .addCase(fetchChildren.rejected, (state, action) => { state.loading = false; state.error = action.error.message || 'Failed to fetch children'; }) // Create child .addCase(createChild.pending, (state) => { state.loading = true; state.error = null; }) .addCase(createChild.fulfilled, (state, action) => { state.loading = false; const serverChild = action.payload.data; const optimisticId = action.meta.arg.id || action.meta.requestId; if (state.entities[optimisticId]?._optimistic) { childrenAdapter.removeOne(state, optimisticId); } childrenAdapter.addOne(state, { ...serverChild, _optimistic: false, }); }) .addCase(createChild.rejected, (state, action) => { state.loading = false; state.error = action.payload as string; const optimisticId = action.meta.arg.id || action.meta.requestId; if (state.entities[optimisticId]?._optimistic) { childrenAdapter.removeOne(state, optimisticId); } }) // Update child .addCase(updateChild.fulfilled, (state, action) => { const serverChild = action.payload.data; childrenAdapter.updateOne(state, { id: serverChild.id, changes: { ...serverChild, _optimistic: false, }, }); }) .addCase(updateChild.rejected, (state, action) => { state.error = action.payload as string; }); }, }); export const { optimisticCreate, optimisticUpdate, rollbackOptimistic, markSynced, selectChild, clearChildren, } = childrenSlice.actions; // Export selectors export const childrenSelectors = childrenAdapter.getSelectors( (state) => state.children ); // Custom selectors export const selectSelectedChild = (state: RootState) => { const id = state.children.selectedChildId; return id ? state.children.entities[id] : null; }; export const selectChildrenByFamily = (state: RootState, familyId: string) => childrenSelectors .selectAll(state) .filter((child) => child.familyId === familyId); export const selectPendingChildren = (state: RootState) => childrenSelectors .selectAll(state) .filter((child) => child._optimistic); export default childrenSlice.reducer;