From 97830c5905de6d69e25459ac95b3c3cb42101136 Mon Sep 17 00:00:00 2001 From: Andrei Date: Sat, 4 Oct 2025 21:28:15 +0000 Subject: [PATCH] feat: Add ChildSelector component and update Child types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TypeScript Types: - Updated Child interface with displayColor, sortOrder, nickname - Added FamilyStatistics interface for UI view mode decisions - Updated CreateChildData to support custom colors and nicknames API Client: - Added getFamilyStatistics() method - Returns totalChildren, viewMode (tabs/cards), ageRange, genderDistribution ChildSelector Component: - Supports 3 modes: single, multiple, all - Shows child avatars with color-coded borders - Displays child name and optional nickname - "All Children" option for bulk operations - Chip-based multi-select display - Compact mode for inline usage - Sorted by sortOrder (birth order) - Disabled state when no children available - Simplified UI for single-child families Features: - Color-coded child indicators using displayColor - Avatar fallback with child's first initial - Checkbox selection for multiple mode - Indeterminate checkbox for partial selection - Required field validation support - Accessible with labels and ARIA Props: - children: Child[] - List of children to display - selectedChildIds: string[] - Currently selected child IDs - onChange: (childIds: string[]) => void - Selection change handler - mode: 'single' | 'multiple' | 'all' - Selection behavior - showAllOption: boolean - Show "All Children" option - label: string - Form label - compact: boolean - Compact display mode Use Cases: - Activity tracking forms - Analytics filtering - Bulk operations - Dashboard child switching - Comparison views 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../components/common/ChildSelector.tsx | 267 ++++++++++++++++++ maternal-web/lib/api/children.ts | 18 ++ 2 files changed, 285 insertions(+) create mode 100644 maternal-web/components/common/ChildSelector.tsx diff --git a/maternal-web/components/common/ChildSelector.tsx b/maternal-web/components/common/ChildSelector.tsx new file mode 100644 index 0000000..19a89f4 --- /dev/null +++ b/maternal-web/components/common/ChildSelector.tsx @@ -0,0 +1,267 @@ +'use client'; + +import React from 'react'; +import { + FormControl, + InputLabel, + Select, + MenuItem, + Checkbox, + ListItemText, + Avatar, + Box, + Chip, + SelectChangeEvent, +} from '@mui/material'; +import { Child } from '@/lib/api/children'; +import { GroupAdd, Person } from '@mui/icons-material'; + +export type ChildSelectorMode = 'single' | 'multiple' | 'all'; + +interface ChildSelectorProps { + children: Child[]; + selectedChildIds: string[]; + onChange: (childIds: string[]) => void; + mode?: ChildSelectorMode; + showAllOption?: boolean; + label?: string; + disabled?: boolean; + compact?: boolean; + required?: boolean; +} + +export default function ChildSelector({ + children, + selectedChildIds, + onChange, + mode = 'single', + showAllOption = false, + label = 'Select Child', + disabled = false, + compact = false, + required = false, +}: ChildSelectorProps) { + const handleChange = (event: SelectChangeEvent) => { + const value = event.target.value; + + if (mode === 'single') { + // Single selection + onChange(typeof value === 'string' ? [value] : value); + } else { + // Multiple selection + const selectedIds = typeof value === 'string' ? value.split(',') : value; + + // Handle "All" option + if (selectedIds.includes('all')) { + if (selectedChildIds.length === children.length) { + // Deselect all + onChange([]); + } else { + // Select all + onChange(children.map((c) => c.id)); + } + } else { + onChange(selectedIds); + } + } + }; + + const getDisplayValue = () => { + if (selectedChildIds.length === 0) { + return ''; + } + + if (mode === 'single') { + return selectedChildIds[0] || ''; + } + + // Multiple mode + if (showAllOption && selectedChildIds.length === children.length) { + return 'all'; + } + + return selectedChildIds; + }; + + const renderValue = (selected: string | string[]) => { + if (!selected || (Array.isArray(selected) && selected.length === 0)) { + return None selected; + } + + if (mode === 'single') { + const child = children.find((c) => c.id === selected); + if (!child) return None selected; + + return ( + + + {child.name[0]} + + {child.name} + + ); + } + + // Multiple mode + if (showAllOption && Array.isArray(selected) && selected.includes('all')) { + return ( + + } + label="All Children" + size="small" + sx={{ bgcolor: 'primary.light' }} + /> + + ); + } + + const selectedIds = Array.isArray(selected) ? selected : [selected]; + return ( + + {selectedIds.map((id) => { + const child = children.find((c) => c.id === id); + if (!child) return null; + + return ( + + {child.name[0]} + + } + label={child.nickname || child.name} + size="small" + sx={{ + bgcolor: `${child.displayColor}20`, + borderColor: child.displayColor, + borderWidth: 1, + borderStyle: 'solid', + }} + /> + ); + })} + + ); + }; + + if (children.length === 0) { + return ( + + No children available + + + ); + } + + // Single child - show simplified selector + if (children.length === 1 && mode === 'single') { + const child = children[0]; + return ( + + {label} + + + ); + } + + return ( + + {label} + + + ); +} diff --git a/maternal-web/lib/api/children.ts b/maternal-web/lib/api/children.ts index 48d4de0..1ee63e1 100644 --- a/maternal-web/lib/api/children.ts +++ b/maternal-web/lib/api/children.ts @@ -8,6 +8,9 @@ export interface Child { gender: 'male' | 'female' | 'other'; photoUrl?: string; photoAlt?: string; + displayColor: string; + sortOrder: number; + nickname?: string; medicalInfo?: any; createdAt: string; } @@ -18,9 +21,18 @@ export interface CreateChildData { gender: 'male' | 'female' | 'other'; photoUrl?: string; photoAlt?: string; + displayColor?: string; + nickname?: string; medicalInfo?: any; } +export interface FamilyStatistics { + totalChildren: number; + viewMode: 'tabs' | 'cards'; + ageRange: { youngest: number; oldest: number } | null; + genderDistribution: { male: number; female: number; other: number }; +} + export interface UpdateChildData extends Partial {} export const childrenApi = { @@ -59,4 +71,10 @@ export const childrenApi = { const response = await apiClient.get(`/api/v1/children/${id}/age`); return response.data.data; }, + + // Get family statistics for multi-child UI + getFamilyStatistics: async (familyId: string): Promise => { + const response = await apiClient.get(`/api/v1/children/family/${familyId}/statistics`); + return response.data.data; + }, };