From 6894fa8edf61c7de70812ba5704e39d28f0a3aaa Mon Sep 17 00:00:00 2001 From: andupetcu <47487320+andupetcu@users.noreply.github.com> Date: Tue, 30 Sep 2025 22:10:00 +0300 Subject: [PATCH] Add backend API integration for Children, Family, and Tracking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit API Services Created: - lib/api/children.ts: Full CRUD operations for children management - lib/api/families.ts: Family member management and invitations - lib/api/tracking.ts: Activity tracking (feeding, sleep, diaper, etc.) Children Page Implementation: - Fetch and display children from backend API - Add/Edit child with modal dialog (ChildDialog component) - Delete child with confirmation (DeleteConfirmDialog component) - Age calculation from birthDate - Loading states and error handling - Responsive card grid layout - Gender-based avatar colors - Empty state for no children AuthContext Updates: - Added families array to User interface - Includes familyId for API calls Components: - components/children/ChildDialog.tsx: Form for add/edit child - components/children/DeleteConfirmDialog.tsx: Delete confirmation All components use Material-UI theme and include proper TypeScript types 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- maternal-web/app/children/page.tsx | 332 +++++++++++++++--- .../components/children/ChildDialog.tsx | 157 +++++++++ .../children/DeleteConfirmDialog.tsx | 52 +++ maternal-web/lib/api/children.ts | 60 ++++ maternal-web/lib/api/families.ts | 69 ++++ maternal-web/lib/api/tracking.ts | 80 +++++ maternal-web/lib/auth/AuthContext.tsx | 5 + 7 files changed, 712 insertions(+), 43 deletions(-) create mode 100644 maternal-web/components/children/ChildDialog.tsx create mode 100644 maternal-web/components/children/DeleteConfirmDialog.tsx create mode 100644 maternal-web/lib/api/children.ts create mode 100644 maternal-web/lib/api/families.ts create mode 100644 maternal-web/lib/api/tracking.ts diff --git a/maternal-web/app/children/page.tsx b/maternal-web/app/children/page.tsx index 79d24a3..2d103b2 100644 --- a/maternal-web/app/children/page.tsx +++ b/maternal-web/app/children/page.tsx @@ -1,59 +1,305 @@ 'use client'; -import { Box, Typography, Grid, Card, CardContent, Button } from '@mui/material'; -import { Add, ChildCare } from '@mui/icons-material'; -import { useRouter } from 'next/navigation'; +import { useState, useEffect } from 'react'; +import { + Box, + Typography, + Grid, + Card, + CardContent, + Button, + Avatar, + IconButton, + CircularProgress, + Alert, + Chip, + CardActions, +} from '@mui/material'; +import { Add, ChildCare, Edit, Delete, Cake } from '@mui/icons-material'; import { AppShell } from '@/components/layouts/AppShell/AppShell'; import { ProtectedRoute } from '@/components/common/ProtectedRoute'; +import { useAuth } from '@/lib/auth/AuthContext'; +import { childrenApi, Child, CreateChildData } from '@/lib/api/children'; +import { ChildDialog } from '@/components/children/ChildDialog'; +import { DeleteConfirmDialog } from '@/components/children/DeleteConfirmDialog'; +import { motion } from 'framer-motion'; export default function ChildrenPage() { - const router = useRouter(); + const { user } = useAuth(); + const [children, setChildren] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + const [dialogOpen, setDialogOpen] = useState(false); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [selectedChild, setSelectedChild] = useState(null); + const [childToDelete, setChildToDelete] = useState(null); + const [actionLoading, setActionLoading] = useState(false); + + // Get familyId from user + const familyId = user?.families?.[0]?.familyId; + + useEffect(() => { + if (familyId) { + fetchChildren(); + } else { + setLoading(false); + setError('No family found. Please complete onboarding first.'); + } + }, [familyId]); + + const fetchChildren = async () => { + if (!familyId) return; + + try { + setLoading(true); + setError(''); + const data = await childrenApi.getChildren(familyId); + setChildren(data); + } catch (err: any) { + console.error('Failed to fetch children:', err); + setError(err.response?.data?.message || 'Failed to load children'); + } finally { + setLoading(false); + } + }; + + const handleAddChild = () => { + setSelectedChild(null); + setDialogOpen(true); + }; + + const handleEditChild = (child: Child) => { + setSelectedChild(child); + setDialogOpen(true); + }; + + const handleDeleteClick = (child: Child) => { + setChildToDelete(child); + setDeleteDialogOpen(true); + }; + + const handleSubmit = async (data: CreateChildData) => { + if (!familyId) { + throw new Error('No family ID found'); + } + + try { + setActionLoading(true); + if (selectedChild) { + await childrenApi.updateChild(selectedChild.id, data); + } else { + await childrenApi.createChild(familyId, data); + } + await fetchChildren(); + setDialogOpen(false); + } catch (err: any) { + console.error('Failed to save child:', err); + throw new Error(err.response?.data?.message || 'Failed to save child'); + } finally { + setActionLoading(false); + } + }; + + const handleDeleteConfirm = async () => { + if (!childToDelete) return; + + try { + setActionLoading(true); + await childrenApi.deleteChild(childToDelete.id); + await fetchChildren(); + setDeleteDialogOpen(false); + setChildToDelete(null); + } catch (err: any) { + console.error('Failed to delete child:', err); + setError(err.response?.data?.message || 'Failed to delete child'); + } finally { + setActionLoading(false); + } + }; + + const calculateAge = (birthDate: string): string => { + const birth = new Date(birthDate); + const today = new Date(); + + let years = today.getFullYear() - birth.getFullYear(); + let months = today.getMonth() - birth.getMonth(); + + if (months < 0) { + years--; + months += 12; + } + + if (today.getDate() < birth.getDate()) { + months--; + if (months < 0) { + years--; + months += 12; + } + } + + if (years === 0) { + return `${months} month${months !== 1 ? 's' : ''}`; + } else if (months === 0) { + return `${years} year${years !== 1 ? 's' : ''}`; + } else { + return `${years} year${years !== 1 ? 's' : ''}, ${months} month${months !== 1 ? 's' : ''}`; + } + }; return ( - - - - Children - - - Manage your family's children profiles - - - - + + + + Children + + + Manage your family's children profiles + + + + - - - - - - - No children added yet - - - Add your first child to start tracking their activities - - - - - - + {error && ( + setError('')}> + {error} + + )} + + {loading ? ( + + + + ) : children.length === 0 ? ( + + + + + + + No children added yet + + + Add your first child to start tracking their activities + + + + + + + ) : ( + + {children.map((child, index) => ( + + + + + + + + + + + {child.name} + + + + + + + + + {new Date(child.birthDate).toLocaleDateString()} + + + + + Age: {calculateAge(child.birthDate)} + + + + + handleEditChild(child)} + color="primary" + > + + + handleDeleteClick(child)} + color="error" + > + + + + + + + ))} + + )} + + setDialogOpen(false)} + onSubmit={handleSubmit} + child={selectedChild} + isLoading={actionLoading} + /> + + setDeleteDialogOpen(false)} + onConfirm={handleDeleteConfirm} + childName={childToDelete?.name || ''} + isLoading={actionLoading} + /> ); diff --git a/maternal-web/components/children/ChildDialog.tsx b/maternal-web/components/children/ChildDialog.tsx new file mode 100644 index 0000000..db77ffa --- /dev/null +++ b/maternal-web/components/children/ChildDialog.tsx @@ -0,0 +1,157 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + TextField, + MenuItem, + Box, + Alert, +} from '@mui/material'; +import { Child, CreateChildData } from '@/lib/api/children'; + +interface ChildDialogProps { + open: boolean; + onClose: () => void; + onSubmit: (data: CreateChildData) => Promise; + child?: Child | null; + isLoading?: boolean; +} + +export function ChildDialog({ open, onClose, onSubmit, child, isLoading = false }: ChildDialogProps) { + const [formData, setFormData] = useState({ + name: '', + birthDate: '', + gender: 'male', + photoUrl: '', + }); + const [error, setError] = useState(''); + + useEffect(() => { + if (child) { + setFormData({ + name: child.name, + birthDate: child.birthDate.split('T')[0], // Convert to YYYY-MM-DD format + gender: child.gender, + photoUrl: child.photoUrl || '', + }); + } else { + setFormData({ + name: '', + birthDate: '', + gender: 'male', + photoUrl: '', + }); + } + setError(''); + }, [child, open]); + + const handleChange = (field: keyof CreateChildData) => ( + e: React.ChangeEvent + ) => { + setFormData({ ...formData, [field]: e.target.value }); + }; + + const handleSubmit = async () => { + setError(''); + + // Validation + if (!formData.name.trim()) { + setError('Please enter a name'); + return; + } + if (!formData.birthDate) { + setError('Please select a birth date'); + return; + } + + // Check if birth date is in the future + const selectedDate = new Date(formData.birthDate); + const today = new Date(); + today.setHours(0, 0, 0, 0); + if (selectedDate > today) { + setError('Birth date cannot be in the future'); + return; + } + + try { + await onSubmit(formData); + onClose(); + } catch (err: any) { + setError(err.message || 'Failed to save child'); + } + }; + + return ( + + {child ? 'Edit Child' : 'Add Child'} + + + {error && ( + setError('')}> + {error} + + )} + + + + + + + Male + Female + Other + + + + + + + + + + + ); +} diff --git a/maternal-web/components/children/DeleteConfirmDialog.tsx b/maternal-web/components/children/DeleteConfirmDialog.tsx new file mode 100644 index 0000000..abd2de4 --- /dev/null +++ b/maternal-web/components/children/DeleteConfirmDialog.tsx @@ -0,0 +1,52 @@ +'use client'; + +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + Typography, +} from '@mui/material'; +import { Warning } from '@mui/icons-material'; + +interface DeleteConfirmDialogProps { + open: boolean; + onClose: () => void; + onConfirm: () => void; + childName: string; + isLoading?: boolean; +} + +export function DeleteConfirmDialog({ + open, + onClose, + onConfirm, + childName, + isLoading = false, +}: DeleteConfirmDialogProps) { + return ( + + + + Confirm Delete + + + + Are you sure you want to delete {childName}? + + + This action cannot be undone. All associated data will be permanently removed. + + + + + + + + ); +} diff --git a/maternal-web/lib/api/children.ts b/maternal-web/lib/api/children.ts new file mode 100644 index 0000000..a251e8e --- /dev/null +++ b/maternal-web/lib/api/children.ts @@ -0,0 +1,60 @@ +import apiClient from './client'; + +export interface Child { + id: string; + familyId: string; + name: string; + birthDate: string; + gender: 'male' | 'female' | 'other'; + photoUrl?: string; + medicalInfo?: any; + createdAt: string; +} + +export interface CreateChildData { + name: string; + birthDate: string; + gender: 'male' | 'female' | 'other'; + photoUrl?: string; + medicalInfo?: any; +} + +export interface UpdateChildData extends Partial {} + +export const childrenApi = { + // Get all children for the authenticated user + getChildren: async (familyId?: string): Promise => { + const params = familyId ? { familyId } : {}; + const response = await apiClient.get('/api/v1/children', { params }); + return response.data.data.children; + }, + + // Get a specific child + getChild: async (id: string): Promise => { + const response = await apiClient.get(`/api/v1/children/${id}`); + return response.data.data.child; + }, + + // Create a new child + createChild: async (familyId: string, data: CreateChildData): Promise => { + const response = await apiClient.post(`/api/v1/children?familyId=${familyId}`, data); + return response.data.data.child; + }, + + // Update a child + updateChild: async (id: string, data: UpdateChildData): Promise => { + const response = await apiClient.patch(`/api/v1/children/${id}`, data); + return response.data.data.child; + }, + + // Delete a child + deleteChild: async (id: string): Promise => { + await apiClient.delete(`/api/v1/children/${id}`); + }, + + // Get child's age + getChildAge: async (id: string): Promise<{ ageInMonths: number; ageInYears: number; remainingMonths: number }> => { + const response = await apiClient.get(`/api/v1/children/${id}/age`); + return response.data.data; + }, +}; diff --git a/maternal-web/lib/api/families.ts b/maternal-web/lib/api/families.ts new file mode 100644 index 0000000..7fe7a78 --- /dev/null +++ b/maternal-web/lib/api/families.ts @@ -0,0 +1,69 @@ +import apiClient from './client'; + +export interface Family { + id: string; + name: string; + shareCode: string; + createdBy: string; + subscriptionTier: string; + members?: FamilyMember[]; +} + +export interface FamilyMember { + id: string; + userId: string; + familyId: string; + role: 'parent' | 'caregiver' | 'viewer'; + permissions: any; + user?: { + id: string; + name: string; + email: string; + }; +} + +export interface InviteMemberData { + email: string; + role: 'parent' | 'caregiver' | 'viewer'; +} + +export interface JoinFamilyData { + shareCode: string; +} + +export const familiesApi = { + // Get a specific family + getFamily: async (familyId: string): Promise => { + const response = await apiClient.get(`/api/v1/families/${familyId}`); + return response.data.data.family; + }, + + // Get family members + getFamilyMembers: async (familyId: string): Promise => { + const response = await apiClient.get(`/api/v1/families/${familyId}/members`); + return response.data.data.members; + }, + + // Invite a family member + inviteMember: async (familyId: string, data: InviteMemberData): Promise => { + const response = await apiClient.post(`/api/v1/families/invite?familyId=${familyId}`, data); + return response.data.data.invitation; + }, + + // Join a family using share code + joinFamily: async (data: JoinFamilyData): Promise => { + const response = await apiClient.post('/api/v1/families/join', data); + return response.data.data.member; + }, + + // Update member role + updateMemberRole: async (familyId: string, userId: string, role: string): Promise => { + const response = await apiClient.patch(`/api/v1/families/${familyId}/members/${userId}/role`, { role }); + return response.data.data.member; + }, + + // Remove a family member + removeMember: async (familyId: string, userId: string): Promise => { + await apiClient.delete(`/api/v1/families/${familyId}/members/${userId}`); + }, +}; diff --git a/maternal-web/lib/api/tracking.ts b/maternal-web/lib/api/tracking.ts new file mode 100644 index 0000000..99dfce7 --- /dev/null +++ b/maternal-web/lib/api/tracking.ts @@ -0,0 +1,80 @@ +import apiClient from './client'; + +export type ActivityType = 'feeding' | 'sleep' | 'diaper' | 'medication' | 'milestone' | 'note'; + +export interface Activity { + id: string; + childId: string; + type: ActivityType; + timestamp: string; + data: any; + notes?: string; + loggedBy: string; + createdAt: string; +} + +export interface CreateActivityData { + type: ActivityType; + timestamp: string; + data: any; + notes?: string; +} + +export interface UpdateActivityData extends Partial {} + +export interface DailySummary { + date: string; + feedingCount: number; + sleepTotalMinutes: number; + diaperCount: number; + activities: Activity[]; +} + +export const trackingApi = { + // Get all activities for a child + getActivities: async ( + childId: string, + type?: ActivityType, + startDate?: string, + endDate?: string + ): Promise => { + const params: any = { childId }; + if (type) params.type = type; + if (startDate) params.startDate = startDate; + if (endDate) params.endDate = endDate; + + const response = await apiClient.get('/api/v1/activities', { params }); + return response.data.data.activities; + }, + + // Get a specific activity + getActivity: async (id: string): Promise => { + const response = await apiClient.get(`/api/v1/activities/${id}`); + return response.data.data.activity; + }, + + // Create a new activity + createActivity: async (childId: string, data: CreateActivityData): Promise => { + const response = await apiClient.post(`/api/v1/activities?childId=${childId}`, data); + return response.data.data.activity; + }, + + // Update an activity + updateActivity: async (id: string, data: UpdateActivityData): Promise => { + const response = await apiClient.patch(`/api/v1/activities/${id}`, data); + return response.data.data.activity; + }, + + // Delete an activity + deleteActivity: async (id: string): Promise => { + await apiClient.delete(`/api/v1/activities/${id}`); + }, + + // Get daily summary + getDailySummary: async (childId: string, date: string): Promise => { + const response = await apiClient.get('/api/v1/activities/daily-summary', { + params: { childId, date }, + }); + return response.data.data; + }, +}; diff --git a/maternal-web/lib/auth/AuthContext.tsx b/maternal-web/lib/auth/AuthContext.tsx index 1b597b4..246926e 100644 --- a/maternal-web/lib/auth/AuthContext.tsx +++ b/maternal-web/lib/auth/AuthContext.tsx @@ -10,6 +10,11 @@ export interface User { email: string; name: string; role: string; + families?: Array<{ + id: string; + familyId: string; + role: string; + }>; } export interface LoginCredentials {