import { createSlice, createAsyncThunk, createEntityAdapter, PayloadAction } from '@reduxjs/toolkit'; import { RootState } from '../store'; import { childrenApi } from '@/lib/api/children'; export interface Child { id: string; familyId: string; name: string; birthDate: string; gender: 'male' | 'female' | 'other'; photoUrl?: string; photoAlt?: string; displayColor: string; sortOrder: number; nickname?: string; medicalInfo?: any; createdAt: string; updatedAt?: string; // Offline metadata _optimistic?: boolean; _localId?: string; _version?: number; } // Create entity adapter const childrenAdapter = createEntityAdapter({ selectId: (child) => child.id, // Sort by sortOrder (birth order) instead of createdAt sortComparer: (a, b) => a.sortOrder - b.sortOrder, }); // Async thunks export const fetchChildren = createAsyncThunk( 'children/fetch', async (familyId: string) => { const children = await childrenApi.getChildren(familyId); return children; } ); 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); } } ); interface ChildrenState { loading: boolean; error: string | null; selectedChildId: string | null; selectedChildIds: string[]; defaultChildId: string | null; viewMode: 'auto' | 'tabs' | 'cards'; lastSelectedPerScreen: Record; lastSyncTime: string | null; } const childrenSlice = createSlice({ name: 'children', initialState: childrenAdapter.getInitialState({ loading: false, error: null, selectedChildId: null, selectedChildIds: [], defaultChildId: null, viewMode: 'auto', lastSelectedPerScreen: {}, lastSyncTime: 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 (single) selectChild: (state, action: PayloadAction) => { state.selectedChildId = action.payload; state.selectedChildIds = [action.payload]; // Save to localStorage if (typeof window !== 'undefined') { localStorage.setItem('selectedChildId', action.payload); } }, // Select multiple children selectChildren: (state, action: PayloadAction) => { state.selectedChildIds = action.payload; state.selectedChildId = action.payload[0] || null; }, // Toggle child selection (for multi-select) toggleChildSelection: (state, action: PayloadAction) => { const childId = action.payload; const index = state.selectedChildIds.indexOf(childId); if (index >= 0) { state.selectedChildIds.splice(index, 1); } else { state.selectedChildIds.push(childId); } state.selectedChildId = state.selectedChildIds[0] || null; }, // Set default child for quick actions setDefaultChild: (state, action: PayloadAction) => { state.defaultChildId = action.payload; if (typeof window !== 'undefined') { localStorage.setItem('defaultChildId', action.payload); } }, // Set view mode setViewMode: (state, action: PayloadAction<'auto' | 'tabs' | 'cards'>) => { state.viewMode = action.payload; if (typeof window !== 'undefined') { localStorage.setItem('childViewMode', action.payload); } }, // Remember last selected child per screen setLastSelectedForScreen: (state, action: PayloadAction<{ screen: string; childId: string }>) => { state.lastSelectedPerScreen[action.payload.screen] = action.payload.childId; if (typeof window !== 'undefined') { localStorage.setItem('lastSelectedPerScreen', JSON.stringify(state.lastSelectedPerScreen)); } }, clearChildren: (state) => { childrenAdapter.removeAll(state); state.selectedChildId = null; state.selectedChildIds = []; state.defaultChildId = 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, selectChildren, toggleChildSelection, setDefaultChild, setViewMode, setLastSelectedForScreen, 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 selectSelectedChildren = (state: RootState) => { const ids = state.children.selectedChildIds; return ids .map(id => state.children.entities[id]) .filter((child): child is Child => child !== undefined); }; export const selectDefaultChild = (state: RootState) => { const id = state.children.defaultChildId; 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 const selectChildrenCount = (state: RootState) => childrenSelectors.selectTotal(state); export const selectViewMode = (state: RootState) => { const { viewMode } = state.children; const childrenCount = childrenSelectors.selectTotal(state); if (viewMode === 'auto') { return childrenCount <= 3 ? 'tabs' : 'cards'; } return viewMode; }; export const selectChildColor = (childId: string) => (state: RootState) => { const child = state.children.entities[childId]; return child?.displayColor || '#FF6B9D'; }; export const selectLastSelectedForScreen = (screen: string) => (state: RootState) => { return state.children.lastSelectedPerScreen[screen]; }; export default childrenSlice.reducer;