Files
maternal-app/maternal-web/store/slices/childrenSlice.ts
Andrei 52144ca4a9 fix: Add mock children data to Redux store for development
Added Alice and Bob as mock children in development mode to allow
tracking pages and UI to work without requiring authentication.

Changes:
- Updated childrenSlice to use childrenApi for consistent backend calls
- Pre-populated Redux store with 2 mock children (Alice, Bob)
- Set selectedChildId to first child by default
- Added mock token to localStorage for API client

This allows all tracking forms and ChildSelector components to work
in development without needing real login/auth flow.

TODO: Remove mocks and implement real authentication in production.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-05 06:48:05 +00:00

360 lines
11 KiB
TypeScript

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<Child>({
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<Child, 'id' | 'createdAt' | 'updatedAt'>, { 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<Child> }, { 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<string, string>;
lastSyncTime: string | null;
}
// Mock children for development (matches real data from andrei@cloudz.ro)
const MOCK_CHILDREN: Child[] = process.env.NODE_ENV === 'development' ? [
{
id: 'child_alice123',
familyId: 'fam_test123',
name: 'Alice',
birthDate: '2023-03-15',
gender: 'female',
displayColor: '#FF6B9D',
sortOrder: 1,
createdAt: new Date().toISOString(),
},
{
id: 'child_bob456',
familyId: 'fam_test123',
name: 'Bob',
birthDate: '2024-06-20',
gender: 'male',
displayColor: '#4A90E2',
sortOrder: 2,
createdAt: new Date().toISOString(),
},
] : [];
const childrenSlice = createSlice({
name: 'children',
initialState: childrenAdapter.getInitialState<ChildrenState>(
{
loading: false,
error: null,
selectedChildId: MOCK_CHILDREN.length > 0 ? MOCK_CHILDREN[0].id : null,
selectedChildIds: MOCK_CHILDREN.length > 0 ? [MOCK_CHILDREN[0].id] : [],
defaultChildId: MOCK_CHILDREN.length > 0 ? MOCK_CHILDREN[0].id : null,
viewMode: 'auto',
lastSelectedPerScreen: {},
lastSyncTime: null,
},
MOCK_CHILDREN
),
reducers: {
// Optimistic operations
optimisticCreate: (state, action: PayloadAction<Child>) => {
childrenAdapter.addOne(state, {
...action.payload,
_optimistic: true,
_localId: action.payload.id,
_version: 1,
});
},
optimisticUpdate: (state, action: PayloadAction<{ id: string; changes: Partial<Child> }>) => {
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<string>) => {
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<string>) => {
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<string[]>) => {
state.selectedChildIds = action.payload;
state.selectedChildId = action.payload[0] || null;
},
// Toggle child selection (for multi-select)
toggleChildSelection: (state, action: PayloadAction<string>) => {
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<string>) => {
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<RootState>(
(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;