diff --git a/maternal-web/app/insights/page.tsx b/maternal-web/app/insights/page.tsx
index 888ea9a..f33a4ad 100644
--- a/maternal-web/app/insights/page.tsx
+++ b/maternal-web/app/insights/page.tsx
@@ -1,78 +1,656 @@
'use client';
-import { Box, Typography, Grid, Card, CardContent } from '@mui/material';
-import { TrendingUp, Insights as InsightsIcon, Timeline } from '@mui/icons-material';
+import { useState, useEffect } from 'react';
+import {
+ Box,
+ Typography,
+ Grid,
+ Card,
+ CardContent,
+ Select,
+ MenuItem,
+ FormControl,
+ InputLabel,
+ CircularProgress,
+ Alert,
+ Paper,
+ Divider,
+ List,
+ ListItem,
+ ListItemAvatar,
+ ListItemText,
+ Avatar,
+ Chip,
+ ToggleButtonGroup,
+ ToggleButton,
+} from '@mui/material';
+import {
+ Restaurant,
+ Hotel,
+ BabyChangingStation,
+ TrendingUp,
+ Timeline,
+ CalendarToday,
+ Assessment,
+} from '@mui/icons-material';
import { AppShell } from '@/components/layouts/AppShell/AppShell';
import { ProtectedRoute } from '@/components/common/ProtectedRoute';
+import { motion } from 'framer-motion';
+import { trackingApi, Activity, ActivityType } from '@/lib/api/tracking';
+import { childrenApi, Child } from '@/lib/api/children';
+import { format, subDays, startOfDay, endOfDay, parseISO, differenceInMinutes, formatDistanceToNow } from 'date-fns';
+import { BarChart, Bar, LineChart, Line, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
+
+type DateRange = '7days' | '30days' | '3months';
+
+interface DayData {
+ date: string;
+ feedings: number;
+ sleepHours: number;
+ diapers: number;
+ activities: number;
+}
+
+interface DiaperTypeData {
+ name: string;
+ value: number;
+ color: string;
+}
+
+interface ActivityTypeData {
+ name: string;
+ count: number;
+ color: string;
+}
+
+const COLORS = {
+ feeding: '#FFB6C1',
+ sleep: '#B6D7FF',
+ diaper: '#FFE4B5',
+ medication: '#D4B5FF',
+ milestone: '#B5FFD4',
+ note: '#FFD3B6',
+ wet: '#87CEEB',
+ dirty: '#D2691E',
+ both: '#FF8C00',
+ dry: '#90EE90',
+};
+
+const getActivityIcon = (type: ActivityType) => {
+ switch (type) {
+ case 'feeding':
+ return ;
+ case 'sleep':
+ return ;
+ case 'diaper':
+ return ;
+ default:
+ return ;
+ }
+};
+
+const getActivityColor = (type: ActivityType) => {
+ return COLORS[type as keyof typeof COLORS] || '#CCCCCC';
+};
export default function InsightsPage() {
+ const [children, setChildren] = useState([]);
+ const [selectedChild, setSelectedChild] = useState('');
+ const [dateRange, setDateRange] = useState('7days');
+ const [activities, setActivities] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ // Fetch children on mount
+ useEffect(() => {
+ const fetchChildren = async () => {
+ try {
+ const childrenData = await childrenApi.getChildren();
+ setChildren(childrenData);
+ if (childrenData.length > 0) {
+ setSelectedChild(childrenData[0].id);
+ }
+ } catch (err: any) {
+ setError(err.response?.data?.message || 'Failed to load children');
+ }
+ };
+ fetchChildren();
+ }, []);
+
+ // Fetch activities when child or date range changes
+ useEffect(() => {
+ if (!selectedChild) return;
+
+ const fetchActivities = async () => {
+ setLoading(true);
+ setError(null);
+ try {
+ const days = dateRange === '7days' ? 7 : dateRange === '30days' ? 30 : 90;
+ const endDate = endOfDay(new Date());
+ const startDate = startOfDay(subDays(new Date(), days - 1));
+
+ const activitiesData = await trackingApi.getActivities(
+ selectedChild,
+ undefined,
+ startDate.toISOString(),
+ endDate.toISOString()
+ );
+ setActivities(activitiesData);
+ } catch (err: any) {
+ setError(err.response?.data?.message || 'Failed to load activities');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchActivities();
+ }, [selectedChild, dateRange]);
+
+ // Calculate statistics
+ const calculateStats = () => {
+ const totalFeedings = activities.filter((a) => a.type === 'feeding').length;
+ const totalDiapers = activities.filter((a) => a.type === 'diaper').length;
+
+ // Calculate sleep hours
+ const sleepActivities = activities.filter((a) => a.type === 'sleep');
+ const totalSleepMinutes = sleepActivities.reduce((acc, activity) => {
+ if (activity.data?.endTime && activity.data?.startTime) {
+ const start = parseISO(activity.data.startTime);
+ const end = parseISO(activity.data.endTime);
+ return acc + differenceInMinutes(end, start);
+ }
+ return acc;
+ }, 0);
+ const days = dateRange === '7days' ? 7 : dateRange === '30days' ? 30 : 90;
+ const avgSleepHours = days > 0 ? (totalSleepMinutes / 60 / days).toFixed(1) : '0.0';
+
+ // Most common activity type
+ const typeCounts: Record = {};
+ activities.forEach((a) => {
+ typeCounts[a.type] = (typeCounts[a.type] || 0) + 1;
+ });
+ const mostCommonType = Object.entries(typeCounts).sort((a, b) => b[1] - a[1])[0]?.[0] || 'None';
+
+ return {
+ totalFeedings,
+ avgSleepHours,
+ totalDiapers,
+ mostCommonType,
+ };
+ };
+
+ // Prepare chart data - daily breakdown
+ const prepareDailyData = (): DayData[] => {
+ const days = dateRange === '7days' ? 7 : dateRange === '30days' ? 30 : 90;
+ const dailyMap = new Map();
+
+ // Initialize all days
+ for (let i = days - 1; i >= 0; i--) {
+ const date = format(subDays(new Date(), i), 'yyyy-MM-dd');
+ dailyMap.set(date, {
+ date: format(subDays(new Date(), i), 'MMM dd'),
+ feedings: 0,
+ sleepHours: 0,
+ diapers: 0,
+ activities: 0,
+ });
+ }
+
+ // Populate with actual data
+ activities.forEach((activity) => {
+ const dateKey = format(parseISO(activity.timestamp), 'yyyy-MM-dd');
+ const data = dailyMap.get(dateKey);
+ if (data) {
+ data.activities += 1;
+ if (activity.type === 'feeding') data.feedings += 1;
+ if (activity.type === 'diaper') data.diapers += 1;
+ if (activity.type === 'sleep' && activity.data?.endTime && activity.data?.startTime) {
+ const start = parseISO(activity.data.startTime);
+ const end = parseISO(activity.data.endTime);
+ const hours = differenceInMinutes(end, start) / 60;
+ data.sleepHours += hours;
+ }
+ }
+ });
+
+ return Array.from(dailyMap.values()).map((d) => ({
+ ...d,
+ sleepHours: Number(d.sleepHours.toFixed(1)),
+ }));
+ };
+
+ // Prepare diaper type data
+ const prepareDiaperData = (): DiaperTypeData[] => {
+ const diaperActivities = activities.filter((a) => a.type === 'diaper');
+ const typeCount: Record = {};
+
+ diaperActivities.forEach((activity) => {
+ const type = activity.data?.type || 'unknown';
+ typeCount[type] = (typeCount[type] || 0) + 1;
+ });
+
+ return Object.entries(typeCount).map(([name, value]) => ({
+ name: name.charAt(0).toUpperCase() + name.slice(1),
+ value,
+ color: COLORS[name as keyof typeof COLORS] || '#CCCCCC',
+ }));
+ };
+
+ // Prepare activity type data
+ const prepareActivityTypeData = (): ActivityTypeData[] => {
+ const typeCount: Record = {};
+
+ activities.forEach((activity) => {
+ typeCount[activity.type] = (typeCount[activity.type] || 0) + 1;
+ });
+
+ return Object.entries(typeCount).map(([name, count]) => ({
+ name: name.charAt(0).toUpperCase() + name.slice(1),
+ count,
+ color: COLORS[name as keyof typeof COLORS] || '#CCCCCC',
+ }));
+ };
+
+ const stats = calculateStats();
+ const dailyData = prepareDailyData();
+ const diaperData = prepareDiaperData();
+ const activityTypeData = prepareActivityTypeData();
+ const recentActivities = [...activities]
+ .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
+ .slice(0, 20);
+
+ // Empty state check
+ const noChildren = children.length === 0;
+ const noActivities = activities.length === 0 && !loading;
+
return (
-
-
- Insights & Analytics
-
-
- Track patterns and get insights about your child's activities
-
+
+
+
+ Insights & Analytics
+
+
+ Track patterns and get insights about your child's activities
+
-
-
-
-
-
-
-
- Sleep Patterns
-
-
-
- Average sleep duration: Coming soon
-
-
- Sleep quality: Coming soon
-
-
-
-
+ {/* Filters */}
+
+
+ {children.length > 1 && (
+
+
+ Child
+
+
+
+ )}
+
+ newValue && setDateRange(newValue)}
+ fullWidth
+ size="large"
+ >
+ 7 Days
+ 30 Days
+ 3 Months
+
+
+
+
-
-
-
-
-
-
- Feeding Patterns
-
-
-
- Average feeding frequency: Coming soon
-
-
- Total daily intake: Coming soon
-
-
-
-
+ {/* Error State */}
+ {error && (
+
+ {error}
+
+ )}
-
-
-
-
-
-
- Activity Timeline
-
+ {/* No Children State */}
+ {noChildren && !loading && (
+
+ No children found. Please add a child to view insights.
+
+ )}
+
+ {/* Loading State */}
+ {loading && (
+
+
-
- Detailed analytics and trends will be displayed here
-
-
-
-
-
-
+ )}
+
+ {/* No Activities State */}
+ {noActivities && !noChildren && (
+
+ No activities found for the selected date range. Start tracking activities to see insights!
+
+ )}
+
+ {/* Content */}
+ {!loading && !noChildren && !noActivities && (
+ <>
+ {/* Summary Statistics */}
+
+
+
+
+
+
+
+
+ Feedings
+
+
+
+ {stats.totalFeedings}
+
+
+ Total count
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Sleep
+
+
+
+ {stats.avgSleepHours}h
+
+
+ Average per day
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Diapers
+
+
+
+ {stats.totalDiapers}
+
+
+ Total changes
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Top Activity
+
+
+
+ {stats.mostCommonType}
+
+
+ Most frequent
+
+
+
+
+
+
+
+ {/* Charts */}
+
+ {/* Feeding Frequency Chart */}
+
+
+
+
+
+
+ Feeding Frequency
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Sleep Duration Chart */}
+
+
+
+
+
+
+ Sleep Duration (Hours)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Diaper Changes by Type */}
+ {diaperData.length > 0 && (
+
+
+
+
+
+
+ Diaper Changes by Type
+
+
+
+
+ `${name} ${(percent * 100).toFixed(0)}%`}
+ outerRadius={80}
+ fill="#8884d8"
+ dataKey="value"
+ >
+ {diaperData.map((entry, index) => (
+ |
+ ))}
+
+
+
+
+
+
+
+ )}
+
+ {/* Activity Timeline */}
+ 0 ? 6 : 12}>
+
+
+
+
+
+ Activity Timeline
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Activity Type Distribution */}
+ {activityTypeData.length > 0 && (
+
+
+
+ Activity Distribution
+
+
+ {activityTypeData.map((activity) => (
+
+ ))}
+
+
+
+ )}
+
+ {/* Recent Activities */}
+
+
+
+ Recent Activities (Last 20)
+
+
+
+ {recentActivities.map((activity, index) => (
+
+
+
+
+ {getActivityIcon(activity.type)}
+
+
+
+
+ {activity.type}
+
+
+
+ }
+ secondary={
+
+ {activity.notes || format(parseISO(activity.timestamp), 'MMM dd, yyyy HH:mm')}
+
+ }
+ />
+
+
+ ))}
+
+
+
+ >
+ )}
+
+
);
diff --git a/maternal-web/app/settings/page.tsx b/maternal-web/app/settings/page.tsx
index 388e620..b05477c 100644
--- a/maternal-web/app/settings/page.tsx
+++ b/maternal-web/app/settings/page.tsx
@@ -1,23 +1,48 @@
'use client';
-import { Box, Typography, Card, CardContent, TextField, Button, Divider, Switch, FormControlLabel } from '@mui/material';
+import { Box, Typography, Card, CardContent, TextField, Button, Divider, Switch, FormControlLabel, Alert, CircularProgress, Snackbar } from '@mui/material';
import { Save, Logout } from '@mui/icons-material';
import { useAuth } from '@/lib/auth/AuthContext';
import { useState } from 'react';
import { AppShell } from '@/components/layouts/AppShell/AppShell';
import { ProtectedRoute } from '@/components/common/ProtectedRoute';
+import { usersApi } from '@/lib/api/users';
+import { motion } from 'framer-motion';
export default function SettingsPage() {
- const { user, logout } = useAuth();
+ const { user, logout, refreshUser } = useAuth();
+ const [name, setName] = useState(user?.name || '');
const [settings, setSettings] = useState({
notifications: true,
emailUpdates: false,
darkMode: false,
});
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [successMessage, setSuccessMessage] = useState(null);
+ const [nameError, setNameError] = useState(null);
- const handleSave = () => {
- // Save settings functionality to be implemented
- alert('Settings saved successfully!');
+ const handleSave = async () => {
+ // Validate name
+ if (!name || name.trim() === '') {
+ setNameError('Name cannot be empty');
+ return;
+ }
+
+ setIsLoading(true);
+ setError(null);
+ setNameError(null);
+
+ try {
+ await usersApi.updateProfile({ name: name.trim() });
+ await refreshUser();
+ setSuccessMessage('Profile updated successfully!');
+ } catch (err: any) {
+ console.error('Failed to update profile:', err);
+ setError(err.response?.data?.message || 'Failed to update profile. Please try again.');
+ } finally {
+ setIsLoading(false);
+ }
};
const handleLogout = async () => {
@@ -35,104 +60,165 @@ export default function SettingsPage() {
Manage your account settings and preferences
+ {/* Error Alert */}
+ {error && (
+
+ setError(null)}>
+ {error}
+
+
+ )}
+
{/* Profile Settings */}
-
-
-
- Profile Information
-
-
-
-
- }
- onClick={handleSave}
- sx={{ alignSelf: 'flex-start' }}
- >
- Save Changes
-
-
-
-
+
+
+
+
+ Profile Information
+
+
+ {
+ setName(e.target.value);
+ if (nameError) setNameError(null);
+ }}
+ fullWidth
+ error={!!nameError}
+ helperText={nameError}
+ disabled={isLoading}
+ />
+
+ : }
+ onClick={handleSave}
+ disabled={isLoading}
+ sx={{ alignSelf: 'flex-start' }}
+ >
+ {isLoading ? 'Saving...' : 'Save Changes'}
+
+
+
+
+
{/* Notification Settings */}
-
-
-
- Notifications
-
-
- setSettings({ ...settings, notifications: e.target.checked })}
- />
- }
- label="Push Notifications"
- />
- setSettings({ ...settings, emailUpdates: e.target.checked })}
- />
- }
- label="Email Updates"
- />
-
-
-
+
+
+
+
+ Notifications
+
+
+ Settings are stored locally (backend integration coming soon)
+
+
+ setSettings({ ...settings, notifications: e.target.checked })}
+ />
+ }
+ label="Push Notifications"
+ />
+ setSettings({ ...settings, emailUpdates: e.target.checked })}
+ />
+ }
+ label="Email Updates"
+ />
+
+
+
+
{/* Appearance Settings */}
-
-
-
- Appearance
-
-
- setSettings({ ...settings, darkMode: e.target.checked })}
- />
- }
- label="Dark Mode (Coming Soon)"
- disabled
- />
-
-
-
+
+
+
+
+ Appearance
+
+
+ setSettings({ ...settings, darkMode: e.target.checked })}
+ />
+ }
+ label="Dark Mode (Coming Soon)"
+ disabled
+ />
+
+
+
+
{/* Account Actions */}
-
-
-
- Account Actions
-
-
- }
- onClick={handleLogout}
- fullWidth
- >
- Logout
-
-
-
+
+
+
+
+ Account Actions
+
+
+ }
+ onClick={handleLogout}
+ fullWidth
+ >
+ Logout
+
+
+
+
+
+ {/* Success Snackbar */}
+ setSuccessMessage(null)}
+ anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
+ >
+ setSuccessMessage(null)} severity="success" sx={{ width: '100%' }}>
+ {successMessage}
+
+
diff --git a/maternal-web/app/track/diaper/page.tsx b/maternal-web/app/track/diaper/page.tsx
index 3d45b7e..becac09 100644
--- a/maternal-web/app/track/diaper/page.tsx
+++ b/maternal-web/app/track/diaper/page.tsx
@@ -1,89 +1,337 @@
'use client';
-import { useState } from 'react';
+import { useState, useEffect } from 'react';
import {
Box,
Typography,
Button,
Paper,
TextField,
+ FormControl,
+ InputLabel,
+ Select,
+ MenuItem,
IconButton,
Alert,
+ CircularProgress,
+ Card,
+ CardContent,
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogContentText,
+ DialogActions,
Chip,
- FormControl,
- FormLabel,
- RadioGroup,
- FormControlLabel,
- Radio,
+ Snackbar,
ToggleButtonGroup,
ToggleButton,
+ FormLabel,
} from '@mui/material';
import {
ArrowBack,
+ Refresh,
Save,
- Mic,
+ Delete,
BabyChangingStation,
+ Warning,
+ CheckCircle,
} from '@mui/icons-material';
import { useRouter } from 'next/navigation';
import { AppShell } from '@/components/layouts/AppShell/AppShell';
import { ProtectedRoute } from '@/components/common/ProtectedRoute';
+import { useAuth } from '@/lib/auth/AuthContext';
+import { trackingApi, Activity } from '@/lib/api/tracking';
+import { childrenApi, Child } from '@/lib/api/children';
import { motion } from 'framer-motion';
-import { useForm, Controller } from 'react-hook-form';
-import { zodResolver } from '@hookform/resolvers/zod';
-import * as z from 'zod';
-import { format } from 'date-fns';
+import { formatDistanceToNow, format } from 'date-fns';
-const diaperSchema = z.object({
- type: z.enum(['wet', 'dirty', 'both', 'clean']),
- timestamp: z.string(),
- rash: z.boolean(),
- notes: z.string().optional(),
-});
-
-type DiaperFormData = z.infer;
+interface DiaperData {
+ diaperType: 'wet' | 'dirty' | 'both' | 'dry';
+ conditions: string[];
+ hasRash: boolean;
+ rashSeverity?: 'mild' | 'moderate' | 'severe';
+}
export default function DiaperTrackPage() {
const router = useRouter();
+ const { user } = useAuth();
+ const [children, setChildren] = useState([]);
+ const [selectedChild, setSelectedChild] = useState('');
+
+ // Diaper state
+ const [timestamp, setTimestamp] = useState(
+ format(new Date(), "yyyy-MM-dd'T'HH:mm")
+ );
+ const [diaperType, setDiaperType] = useState<'wet' | 'dirty' | 'both' | 'dry'>('wet');
+ const [conditions, setConditions] = useState(['normal']);
+ const [hasRash, setHasRash] = useState(false);
+ const [rashSeverity, setRashSeverity] = useState<'mild' | 'moderate' | 'severe'>('mild');
+
+ // Common state
+ const [notes, setNotes] = useState('');
+ const [recentDiapers, setRecentDiapers] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [childrenLoading, setChildrenLoading] = useState(true);
+ const [diapersLoading, setDiapersLoading] = useState(false);
const [error, setError] = useState(null);
- const [success, setSuccess] = useState(false);
+ const [successMessage, setSuccessMessage] = useState(null);
- const {
- register,
- handleSubmit,
- setValue,
- watch,
- control,
- formState: { errors },
- } = useForm({
- resolver: zodResolver(diaperSchema),
- defaultValues: {
- type: 'wet',
- rash: false,
- timestamp: format(new Date(), "yyyy-MM-dd'T'HH:mm"),
- },
- });
+ // Delete confirmation dialog
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
+ const [activityToDelete, setActivityToDelete] = useState(null);
- const diaperType = watch('type');
- const rash = watch('rash');
+ const familyId = user?.families?.[0]?.familyId;
- const setTimeNow = () => {
- const now = format(new Date(), "yyyy-MM-dd'T'HH:mm");
- setValue('timestamp', now);
- };
+ const availableConditions = [
+ 'normal',
+ 'soft',
+ 'hard',
+ 'watery',
+ 'mucus',
+ 'blood',
+ ];
- const onSubmit = async (data: DiaperFormData) => {
- setError(null);
+ // Load children
+ useEffect(() => {
+ if (familyId) {
+ loadChildren();
+ }
+ }, [familyId]);
+
+ // Load recent diapers when child is selected
+ useEffect(() => {
+ if (selectedChild) {
+ loadRecentDiapers();
+ }
+ }, [selectedChild]);
+
+ const loadChildren = async () => {
+ if (!familyId) return;
try {
- // TODO: Call API to save diaper data
- console.log('Diaper data:', data);
- setSuccess(true);
- setTimeout(() => router.push('/'), 2000);
+ setChildrenLoading(true);
+ const childrenData = await childrenApi.getChildren(familyId);
+ setChildren(childrenData);
+ if (childrenData.length > 0) {
+ setSelectedChild(childrenData[0].id);
+ }
} catch (err: any) {
- setError(err.message || 'Failed to log diaper change');
+ console.error('Failed to load children:', err);
+ setError(err.response?.data?.message || 'Failed to load children');
+ } finally {
+ setChildrenLoading(false);
}
};
+ const loadRecentDiapers = async () => {
+ if (!selectedChild) return;
+
+ try {
+ setDiapersLoading(true);
+ const activities = await trackingApi.getActivities(selectedChild, 'diaper');
+ // Sort by timestamp descending and take last 10
+ const sorted = activities.sort((a, b) =>
+ new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
+ ).slice(0, 10);
+ setRecentDiapers(sorted);
+ } catch (err: any) {
+ console.error('Failed to load recent diapers:', err);
+ } finally {
+ setDiapersLoading(false);
+ }
+ };
+
+ const setTimeNow = () => {
+ setTimestamp(format(new Date(), "yyyy-MM-dd'T'HH:mm"));
+ };
+
+ const handleConditionToggle = (condition: string) => {
+ setConditions((prev) => {
+ if (prev.includes(condition)) {
+ // Remove condition, but ensure at least one remains
+ if (prev.length === 1) return prev;
+ return prev.filter((c) => c !== condition);
+ } else {
+ return [...prev, condition];
+ }
+ });
+ };
+
+ const handleSubmit = async () => {
+ if (!selectedChild) {
+ setError('Please select a child');
+ return;
+ }
+
+ // Validation
+ if (!timestamp) {
+ setError('Please enter timestamp');
+ return;
+ }
+
+ if (conditions.length === 0) {
+ setError('Please select at least one condition');
+ return;
+ }
+
+ try {
+ setLoading(true);
+ setError(null);
+
+ const data: DiaperData = {
+ diaperType,
+ conditions,
+ hasRash,
+ };
+
+ if (hasRash) {
+ data.rashSeverity = rashSeverity;
+ }
+
+ await trackingApi.createActivity(selectedChild, {
+ type: 'diaper',
+ timestamp,
+ data,
+ notes: notes || undefined,
+ });
+
+ setSuccessMessage('Diaper change logged successfully!');
+
+ // Reset form
+ resetForm();
+
+ // Reload recent diapers
+ await loadRecentDiapers();
+ } catch (err: any) {
+ console.error('Failed to save diaper:', err);
+ setError(err.response?.data?.message || 'Failed to save diaper change');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const resetForm = () => {
+ setTimestamp(format(new Date(), "yyyy-MM-dd'T'HH:mm"));
+ setDiaperType('wet');
+ setConditions(['normal']);
+ setHasRash(false);
+ setRashSeverity('mild');
+ setNotes('');
+ };
+
+ const handleDeleteClick = (activityId: string) => {
+ setActivityToDelete(activityId);
+ setDeleteDialogOpen(true);
+ };
+
+ const handleDeleteConfirm = async () => {
+ if (!activityToDelete) return;
+
+ try {
+ setLoading(true);
+ await trackingApi.deleteActivity(activityToDelete);
+ setSuccessMessage('Diaper change deleted successfully');
+ setDeleteDialogOpen(false);
+ setActivityToDelete(null);
+ await loadRecentDiapers();
+ } catch (err: any) {
+ console.error('Failed to delete diaper:', err);
+ setError(err.response?.data?.message || 'Failed to delete diaper change');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const getDiaperTypeColor = (type: string) => {
+ switch (type) {
+ case 'wet':
+ return '#2196f3'; // blue
+ case 'dirty':
+ return '#795548'; // brown
+ case 'both':
+ return '#ff9800'; // orange
+ case 'dry':
+ return '#4caf50'; // green
+ default:
+ return '#757575'; // grey
+ }
+ };
+
+ const getDiaperTypeIcon = (type: string) => {
+ switch (type) {
+ case 'wet':
+ return '💧';
+ case 'dirty':
+ return '💩';
+ case 'both':
+ return '💧💩';
+ case 'dry':
+ return '✨';
+ default:
+ return '🍼';
+ }
+ };
+
+ const getDiaperDetails = (activity: Activity) => {
+ const data = activity.data as DiaperData;
+ const typeLabel = data.diaperType.charAt(0).toUpperCase() + data.diaperType.slice(1);
+ const conditionsLabel = data.conditions.join(', ');
+
+ let details = `${typeLabel} - ${conditionsLabel}`;
+
+ if (data.hasRash) {
+ details += ` - Rash (${data.rashSeverity})`;
+ }
+
+ return details;
+ };
+
+ const getRashSeverityColor = (severity: string) => {
+ switch (severity) {
+ case 'mild':
+ return 'warning';
+ case 'moderate':
+ return 'error';
+ case 'severe':
+ return 'error';
+ default:
+ return 'default';
+ }
+ };
+
+ if (childrenLoading) {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+
+ if (!familyId || children.length === 0) {
+ return (
+
+
+
+
+ Please add a child first before tracking diaper changes.
+
+
+
+
+
+ );
+ }
+
return (
@@ -97,14 +345,8 @@ export default function DiaperTrackPage() {
- {success && (
-
- Diaper change logged successfully!
-
- )}
-
{error && (
-
+ setError(null)}>
{error}
)}
@@ -114,150 +356,299 @@ export default function DiaperTrackPage() {
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
+ {/* Child Selector */}
+ {children.length > 1 && (
+
+
+ Select Child
+
+
+
+ )}
+
+ {/* Main Form */}
-
- {/* Icon Header */}
-
-
-
-
- {/* Time */}
-
- Time
-
-
-
-
-
-
- {/* Diaper Type */}
-
-
- Diaper Type
-
- (
- {
- if (value !== null) {
- field.onChange(value);
- }
- }}
- >
-
-
- 💧
- Wet
-
-
-
-
- 💩
- Dirty
-
-
-
-
- 💧💩
- Both
-
-
-
-
- ✨
- Clean
-
-
-
- )}
- />
-
-
- {/* Rash Indicator */}
-
-
- Diaper Rash?
-
-
- setValue('rash', false)}
- />
- }
- label="No"
- />
- setValue('rash', true)}
- />
- }
- label="Yes"
- />
-
-
-
- {/* Rash Warning */}
- {rash && (
-
- Consider applying diaper rash cream and consulting your pediatrician if it persists.
-
- )}
-
- {/* Notes */}
-
-
- {/* Voice Input Button */}
-
- }
- label="Use Voice Input"
- onClick={() => {/* TODO: Implement voice input */}}
- sx={{ cursor: 'pointer' }}
- />
-
-
- {/* Submit Button */}
- }
- >
- Save Diaper Change
-
+ {/* Icon Header */}
+
+
+
+ {/* Timestamp */}
+
+
+ Time
+
+
+ setTimestamp(e.target.value)}
+ InputLabelProps={{ shrink: true }}
+ />
+
+
+
+
+ {/* Diaper Type */}
+
+
+ Diaper Type
+
+ {
+ if (value !== null) {
+ setDiaperType(value);
+ }
+ }}
+ fullWidth
+ >
+
+
+ 💧
+ Wet
+
+
+
+
+ 💩
+ Dirty
+
+
+
+
+ 💧💩
+ Both
+
+
+
+
+ ✨
+ Dry
+
+
+
+
+
+ {/* Condition Selector */}
+
+
+ Condition (select all that apply)
+
+
+ {availableConditions.map((condition) => (
+ handleConditionToggle(condition)}
+ color={conditions.includes(condition) ? 'primary' : 'default'}
+ variant={conditions.includes(condition) ? 'filled' : 'outlined'}
+ sx={{ cursor: 'pointer' }}
+ />
+ ))}
+
+
+
+ {/* Rash Indicator */}
+
+ Diaper Rash?
+
+
+
+ {/* Rash Severity */}
+ {hasRash && (
+
+
+
+ Diaper rash detected. Consider applying diaper rash cream and consulting your pediatrician if it persists.
+
+
+
+ Rash Severity
+
+
+
+ )}
+
+ {/* Notes Field */}
+ setNotes(e.target.value)}
+ sx={{ mb: 3 }}
+ placeholder="Color, consistency, or any concerns..."
+ />
+
+ {/* Submit Button */}
+ }
+ onClick={handleSubmit}
+ disabled={loading}
+ >
+ {loading ? 'Saving...' : 'Save Diaper Change'}
+
+
+
+ {/* Recent Diapers */}
+
+
+
+ Recent Diaper Changes
+
+
+
+
+
+
+ {diapersLoading ? (
+
+
+
+ ) : recentDiapers.length === 0 ? (
+
+
+ No diaper changes yet
+
+
+ ) : (
+
+ {recentDiapers.map((activity, index) => {
+ const data = activity.data as DiaperData;
+ return (
+
+
+
+
+
+ {getDiaperTypeIcon(data.diaperType)}
+
+
+
+
+ Diaper Change
+
+
+ {data.hasRash && (
+ }
+ label={`Rash: ${data.rashSeverity}`}
+ size="small"
+ color={getRashSeverityColor(data.rashSeverity || 'mild') as any}
+ />
+ )}
+
+
+
+ {getDiaperDetails(activity)}
+
+ {activity.notes && (
+
+ {activity.notes}
+
+ )}
+
+
+ handleDeleteClick(activity.id)}
+ disabled={loading}
+ >
+
+
+
+
+
+
+
+ );
+ })}
+
+ )}
+
+ {/* Delete Confirmation Dialog */}
+
+
+ {/* Success Snackbar */}
+ setSuccessMessage(null)}
+ message={successMessage}
+ />
);
diff --git a/maternal-web/app/track/feeding/page.tsx b/maternal-web/app/track/feeding/page.tsx
index 31cea66..453dfaf 100644
--- a/maternal-web/app/track/feeding/page.tsx
+++ b/maternal-web/app/track/feeding/page.tsx
@@ -8,89 +8,331 @@ import {
Paper,
TextField,
FormControl,
- FormLabel,
- RadioGroup,
- FormControlLabel,
- Radio,
+ InputLabel,
+ Select,
+ MenuItem,
IconButton,
Alert,
+ Tabs,
+ Tab,
+ CircularProgress,
+ Card,
+ CardContent,
+ Divider,
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogContentText,
+ DialogActions,
Chip,
+ Snackbar,
} from '@mui/material';
import {
ArrowBack,
PlayArrow,
Stop,
+ Refresh,
Save,
- Mic,
+ Restaurant,
+ LocalCafe,
+ Fastfood,
+ Delete,
+ Edit,
} from '@mui/icons-material';
import { useRouter } from 'next/navigation';
import { AppShell } from '@/components/layouts/AppShell/AppShell';
import { ProtectedRoute } from '@/components/common/ProtectedRoute';
+import { useAuth } from '@/lib/auth/AuthContext';
+import { trackingApi, Activity } from '@/lib/api/tracking';
+import { childrenApi, Child } from '@/lib/api/children';
import { motion } from 'framer-motion';
-import { useForm } from 'react-hook-form';
-import { zodResolver } from '@hookform/resolvers/zod';
-import * as z from 'zod';
+import { formatDistanceToNow } from 'date-fns';
-const feedingSchema = z.object({
- type: z.enum(['breast_left', 'breast_right', 'breast_both', 'bottle', 'solid']),
- amount: z.number().min(0).optional(),
- unit: z.enum(['ml', 'oz']).optional(),
- notes: z.string().optional(),
-});
-
-type FeedingFormData = z.infer;
+interface FeedingData {
+ feedingType: 'breast' | 'bottle' | 'solid';
+ side?: 'left' | 'right' | 'both';
+ duration?: number;
+ amount?: number;
+ bottleType?: 'formula' | 'breastmilk' | 'other';
+ foodDescription?: string;
+ amountDescription?: string;
+}
export default function FeedingTrackPage() {
const router = useRouter();
+ const { user } = useAuth();
+ const [children, setChildren] = useState([]);
+ const [selectedChild, setSelectedChild] = useState('');
+ const [feedingType, setFeedingType] = useState<'breast' | 'bottle' | 'solid'>('breast');
+
+ // Breastfeeding state
+ const [side, setSide] = useState<'left' | 'right' | 'both'>('left');
+ const [duration, setDuration] = useState(0);
const [isTimerRunning, setIsTimerRunning] = useState(false);
- const [duration, setDuration] = useState(0);
+ const [timerSeconds, setTimerSeconds] = useState(0);
+
+ // Bottle feeding state
+ const [amount, setAmount] = useState('');
+ const [bottleType, setBottleType] = useState<'formula' | 'breastmilk' | 'other'>('formula');
+
+ // Solid food state
+ const [foodDescription, setFoodDescription] = useState('');
+ const [amountDescription, setAmountDescription] = useState('');
+
+ // Common state
+ const [notes, setNotes] = useState('');
+ const [recentFeedings, setRecentFeedings] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [childrenLoading, setChildrenLoading] = useState(true);
+ const [feedingsLoading, setFeedingsLoading] = useState(false);
const [error, setError] = useState(null);
- const [success, setSuccess] = useState(false);
+ const [successMessage, setSuccessMessage] = useState(null);
- const {
- register,
- handleSubmit,
- watch,
- formState: { errors },
- } = useForm({
- resolver: zodResolver(feedingSchema),
- defaultValues: {
- type: 'breast_left',
- unit: 'ml',
- },
- });
+ // Delete confirmation dialog
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
+ const [activityToDelete, setActivityToDelete] = useState(null);
- const feedingType = watch('type');
+ const familyId = user?.families?.[0]?.familyId;
+ // Load children
+ useEffect(() => {
+ if (familyId) {
+ loadChildren();
+ }
+ }, [familyId]);
+
+ // Load recent feedings when child is selected
+ useEffect(() => {
+ if (selectedChild) {
+ loadRecentFeedings();
+ }
+ }, [selectedChild]);
+
+ // Timer effect
useEffect(() => {
let interval: NodeJS.Timeout;
if (isTimerRunning) {
interval = setInterval(() => {
- setDuration((prev) => prev + 1);
+ setTimerSeconds((prev) => prev + 1);
}, 1000);
}
return () => clearInterval(interval);
}, [isTimerRunning]);
+ const loadChildren = async () => {
+ if (!familyId) return;
+
+ try {
+ setChildrenLoading(true);
+ const childrenData = await childrenApi.getChildren(familyId);
+ setChildren(childrenData);
+ if (childrenData.length > 0) {
+ setSelectedChild(childrenData[0].id);
+ }
+ } catch (err: any) {
+ console.error('Failed to load children:', err);
+ setError(err.response?.data?.message || 'Failed to load children');
+ } finally {
+ setChildrenLoading(false);
+ }
+ };
+
+ const loadRecentFeedings = async () => {
+ if (!selectedChild) return;
+
+ try {
+ setFeedingsLoading(true);
+ const activities = await trackingApi.getActivities(selectedChild, 'feeding');
+ // Sort by timestamp descending and take last 10
+ const sorted = activities.sort((a, b) =>
+ new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
+ ).slice(0, 10);
+ setRecentFeedings(sorted);
+ } catch (err: any) {
+ console.error('Failed to load recent feedings:', err);
+ } finally {
+ setFeedingsLoading(false);
+ }
+ };
+
const formatDuration = (seconds: number) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
};
- const onSubmit = async (data: FeedingFormData) => {
- setError(null);
+ const startTimer = () => {
+ setIsTimerRunning(true);
+ };
+
+ const stopTimer = () => {
+ setIsTimerRunning(false);
+ setDuration(Math.floor(timerSeconds / 60));
+ };
+
+ const resetTimer = () => {
+ setIsTimerRunning(false);
+ setTimerSeconds(0);
+ setDuration(0);
+ };
+
+ const handleSubmit = async () => {
+ if (!selectedChild) {
+ setError('Please select a child');
+ return;
+ }
+
+ // Validation
+ if (feedingType === 'breast' && duration === 0 && timerSeconds === 0) {
+ setError('Please enter duration or use the timer');
+ return;
+ }
+
+ if (feedingType === 'bottle' && !amount) {
+ setError('Please enter amount');
+ return;
+ }
+
+ if (feedingType === 'solid' && !foodDescription) {
+ setError('Please enter food description');
+ return;
+ }
try {
- // TODO: Call API to save feeding data
- console.log('Feeding data:', { ...data, duration });
- setSuccess(true);
- setTimeout(() => router.push('/'), 2000);
+ setLoading(true);
+ setError(null);
+
+ const data: FeedingData = {
+ feedingType,
+ };
+
+ if (feedingType === 'breast') {
+ data.side = side;
+ data.duration = duration || Math.floor(timerSeconds / 60);
+ } else if (feedingType === 'bottle') {
+ data.amount = parseFloat(amount);
+ data.bottleType = bottleType;
+ } else if (feedingType === 'solid') {
+ data.foodDescription = foodDescription;
+ data.amountDescription = amountDescription;
+ }
+
+ await trackingApi.createActivity(selectedChild, {
+ type: 'feeding',
+ timestamp: new Date().toISOString(),
+ data,
+ notes: notes || undefined,
+ });
+
+ setSuccessMessage('Feeding logged successfully!');
+
+ // Reset form
+ resetForm();
+
+ // Reload recent feedings
+ await loadRecentFeedings();
} catch (err: any) {
- setError(err.message || 'Failed to log feeding');
+ console.error('Failed to save feeding:', err);
+ setError(err.response?.data?.message || 'Failed to save feeding');
+ } finally {
+ setLoading(false);
}
};
+ const resetForm = () => {
+ setSide('left');
+ setDuration(0);
+ setTimerSeconds(0);
+ setIsTimerRunning(false);
+ setAmount('');
+ setBottleType('formula');
+ setFoodDescription('');
+ setAmountDescription('');
+ setNotes('');
+ };
+
+ const handleDeleteClick = (activityId: string) => {
+ setActivityToDelete(activityId);
+ setDeleteDialogOpen(true);
+ };
+
+ const handleDeleteConfirm = async () => {
+ if (!activityToDelete) return;
+
+ try {
+ setLoading(true);
+ await trackingApi.deleteActivity(activityToDelete);
+ setSuccessMessage('Feeding deleted successfully');
+ setDeleteDialogOpen(false);
+ setActivityToDelete(null);
+ await loadRecentFeedings();
+ } catch (err: any) {
+ console.error('Failed to delete feeding:', err);
+ setError(err.response?.data?.message || 'Failed to delete feeding');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const getFeedingTypeIcon = (type: string) => {
+ switch (type) {
+ case 'breast':
+ return ;
+ case 'bottle':
+ return ;
+ case 'solid':
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ const getFeedingDetails = (activity: Activity) => {
+ const data = activity.data as FeedingData;
+
+ if (data.feedingType === 'breast') {
+ return `${data.side?.toUpperCase()} - ${data.duration || 0} min`;
+ } else if (data.feedingType === 'bottle') {
+ return `${data.amount || 0} ml - ${data.bottleType}`;
+ } else if (data.feedingType === 'solid') {
+ return `${data.foodDescription}${data.amountDescription ? ` - ${data.amountDescription}` : ''}`;
+ }
+ return '';
+ };
+
+ if (childrenLoading) {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+
+ if (!familyId || children.length === 0) {
+ return (
+
+
+
+
+ Please add a child first before tracking feeding activities.
+
+
+
+
+
+ );
+ }
+
return (
@@ -104,14 +346,8 @@ export default function FeedingTrackPage() {
- {success && (
-
- Feeding logged successfully!
-
- )}
-
{error && (
-
+ setError(null)}>
{error}
)}
@@ -121,133 +357,291 @@ export default function FeedingTrackPage() {
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
-
- {/* Timer Section */}
-
-
- {formatDuration(duration)}
-
-
- {!isTimerRunning ? (
- }
- onClick={() => setIsTimerRunning(true)}
- >
- Start Timer
-
- ) : (
- }
- onClick={() => setIsTimerRunning(false)}
- >
- Stop Timer
-
- )}
-
-
-
-
- {/* Feeding Type */}
-
-
- Feeding Type
-
-
- }
- label="Left Breast"
- />
- }
- label="Right Breast"
- />
- }
- label="Both"
- />
- }
- label="Bottle"
- />
- }
- label="Solid Food"
- />
-
+ {/* Child Selector */}
+ {children.length > 1 && (
+
+
+ Select Child
+
+
+ )}
- {/* Amount (for bottle/solid) */}
- {(feedingType === 'bottle' || feedingType === 'solid') && (
-
-
-
-
- }
- label="ml"
- />
- }
- label="oz"
- />
-
-
+ {/* Main Form */}
+
+ {/* Feeding Type Tabs */}
+ setFeedingType(newValue)}
+ sx={{ mb: 3 }}
+ variant="fullWidth"
+ >
+ } iconPosition="start" />
+ } iconPosition="start" />
+ } iconPosition="start" />
+
+
+ {/* Breastfeeding Form */}
+ {feedingType === 'breast' && (
+
+ {/* Timer Display */}
+
+
+ {formatDuration(timerSeconds)}
+
+
+ {!isTimerRunning ? (
+ }
+ onClick={startTimer}
+ >
+ Start Timer
+
+ ) : (
+ }
+ onClick={stopTimer}
+ >
+ Stop Timer
+
+ )}
+ }
+ onClick={resetTimer}
+ >
+ Reset
+
+
- )}
- {/* Notes */}
-
+ {/* Side Selector */}
+
+ Side
+
+
- {/* Voice Input Button */}
-
- }
- label="Use Voice Input"
- onClick={() => {/* TODO: Implement voice input */}}
- sx={{ cursor: 'pointer' }}
+ {/* Manual Duration Input */}
+ setDuration(parseInt(e.target.value) || 0)}
+ sx={{ mb: 3 }}
+ helperText="Or use the timer above"
/>
+ )}
- {/* Submit Button */}
- }
- >
- Save Feeding
-
+ {/* Bottle Form */}
+ {feedingType === 'bottle' && (
+
+ setAmount(e.target.value)}
+ sx={{ mb: 3 }}
+ />
+
+
+ Type
+
+
+
+ )}
+
+ {/* Solid Food Form */}
+ {feedingType === 'solid' && (
+
+ setFoodDescription(e.target.value)}
+ sx={{ mb: 3 }}
+ placeholder="e.g., Mashed banana, Rice cereal"
+ />
+
+ setAmountDescription(e.target.value)}
+ sx={{ mb: 3 }}
+ placeholder="e.g., 2 tablespoons, Half bowl"
+ />
+
+ )}
+
+ {/* Common Notes Field */}
+ setNotes(e.target.value)}
+ sx={{ mb: 3 }}
+ placeholder="Any additional notes..."
+ />
+
+ {/* Submit Button */}
+ }
+ onClick={handleSubmit}
+ disabled={loading}
+ >
+ {loading ? 'Saving...' : 'Save Feeding'}
+
+
+
+ {/* Recent Feedings */}
+
+
+
+ Recent Feedings
+
+
+
+
+
+ {feedingsLoading ? (
+
+
+
+ ) : recentFeedings.length === 0 ? (
+
+
+ No feeding activities yet
+
+
+ ) : (
+
+ {recentFeedings.map((activity, index) => {
+ const data = activity.data as FeedingData;
+ return (
+
+
+
+
+
+ {getFeedingTypeIcon(data.feedingType)}
+
+
+
+
+ {data.feedingType.charAt(0).toUpperCase() + data.feedingType.slice(1)}
+
+
+
+
+ {getFeedingDetails(activity)}
+
+ {activity.notes && (
+
+ {activity.notes}
+
+ )}
+
+
+ handleDeleteClick(activity.id)}
+ disabled={loading}
+ >
+
+
+
+
+
+
+
+ );
+ })}
+
+ )}
+
+ {/* Delete Confirmation Dialog */}
+
+
+ {/* Success Snackbar */}
+ setSuccessMessage(null)}
+ message={successMessage}
+ />
);
diff --git a/maternal-web/app/track/sleep/page.tsx b/maternal-web/app/track/sleep/page.tsx
index a837125..210bb96 100644
--- a/maternal-web/app/track/sleep/page.tsx
+++ b/maternal-web/app/track/sleep/page.tsx
@@ -1,105 +1,352 @@
'use client';
-import { useState } from 'react';
+import { useState, useEffect } from 'react';
import {
Box,
Typography,
Button,
Paper,
TextField,
+ FormControl,
+ InputLabel,
+ Select,
+ MenuItem,
IconButton,
Alert,
+ CircularProgress,
+ Card,
+ CardContent,
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogContentText,
+ DialogActions,
Chip,
- FormControl,
- FormLabel,
- RadioGroup,
- FormControlLabel,
- Radio,
+ Snackbar,
} from '@mui/material';
import {
ArrowBack,
- Bedtime,
- WbSunny,
+ Refresh,
Save,
- Mic,
+ Delete,
+ Bedtime,
+ Hotel,
+ DirectionsCar,
+ Chair,
+ Home,
} from '@mui/icons-material';
import { useRouter } from 'next/navigation';
import { AppShell } from '@/components/layouts/AppShell/AppShell';
import { ProtectedRoute } from '@/components/common/ProtectedRoute';
+import { useAuth } from '@/lib/auth/AuthContext';
+import { trackingApi, Activity } from '@/lib/api/tracking';
+import { childrenApi, Child } from '@/lib/api/children';
import { motion } from 'framer-motion';
-import { useForm } from 'react-hook-form';
-import { zodResolver } from '@hookform/resolvers/zod';
-import * as z from 'zod';
-import { format } from 'date-fns';
+import { formatDistanceToNow, format } from 'date-fns';
-const sleepSchema = z.object({
- startTime: z.string(),
- endTime: z.string(),
- quality: z.enum(['excellent', 'good', 'fair', 'poor']),
- notes: z.string().optional(),
-}).refine((data) => new Date(data.endTime) > new Date(data.startTime), {
- message: 'End time must be after start time',
- path: ['endTime'],
-});
-
-type SleepFormData = z.infer;
+interface SleepData {
+ startTime: string;
+ endTime?: string;
+ quality: 'excellent' | 'good' | 'fair' | 'poor';
+ location: string;
+ isOngoing?: boolean;
+}
export default function SleepTrackPage() {
const router = useRouter();
+ const { user } = useAuth();
+ const [children, setChildren] = useState([]);
+ const [selectedChild, setSelectedChild] = useState('');
+
+ // Sleep state
+ const [startTime, setStartTime] = useState(
+ format(new Date(), "yyyy-MM-dd'T'HH:mm")
+ );
+ const [endTime, setEndTime] = useState(
+ format(new Date(), "yyyy-MM-dd'T'HH:mm")
+ );
+ const [quality, setQuality] = useState<'excellent' | 'good' | 'fair' | 'poor'>('good');
+ const [location, setLocation] = useState('crib');
+ const [isOngoing, setIsOngoing] = useState(false);
+
+ // Common state
+ const [notes, setNotes] = useState('');
+ const [recentSleeps, setRecentSleeps] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [childrenLoading, setChildrenLoading] = useState(true);
+ const [sleepsLoading, setSleepsLoading] = useState(false);
const [error, setError] = useState(null);
- const [success, setSuccess] = useState(false);
+ const [successMessage, setSuccessMessage] = useState(null);
- const {
- register,
- handleSubmit,
- setValue,
- watch,
- formState: { errors },
- } = useForm({
- resolver: zodResolver(sleepSchema),
- defaultValues: {
- quality: 'good',
- },
- });
+ // Delete confirmation dialog
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
+ const [activityToDelete, setActivityToDelete] = useState(null);
- const startTime = watch('startTime');
- const endTime = watch('endTime');
+ const familyId = user?.families?.[0]?.familyId;
+
+ // Load children
+ useEffect(() => {
+ if (familyId) {
+ loadChildren();
+ }
+ }, [familyId]);
+
+ // Load recent sleeps when child is selected
+ useEffect(() => {
+ if (selectedChild) {
+ loadRecentSleeps();
+ }
+ }, [selectedChild]);
+
+ const loadChildren = async () => {
+ if (!familyId) return;
+
+ try {
+ setChildrenLoading(true);
+ const childrenData = await childrenApi.getChildren(familyId);
+ setChildren(childrenData);
+ if (childrenData.length > 0) {
+ setSelectedChild(childrenData[0].id);
+ }
+ } catch (err: any) {
+ console.error('Failed to load children:', err);
+ setError(err.response?.data?.message || 'Failed to load children');
+ } finally {
+ setChildrenLoading(false);
+ }
+ };
+
+ const loadRecentSleeps = async () => {
+ if (!selectedChild) return;
+
+ try {
+ setSleepsLoading(true);
+ const activities = await trackingApi.getActivities(selectedChild, 'sleep');
+ // Sort by timestamp descending and take last 10
+ const sorted = activities.sort((a, b) =>
+ new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
+ ).slice(0, 10);
+ setRecentSleeps(sorted);
+ } catch (err: any) {
+ console.error('Failed to load recent sleeps:', err);
+ } finally {
+ setSleepsLoading(false);
+ }
+ };
+
+ const formatDuration = (start: string, end?: string) => {
+ const startDate = new Date(start);
+ const endDate = end ? new Date(end) : new Date();
+ const diffMs = endDate.getTime() - startDate.getTime();
+
+ if (diffMs < 0) return 'Invalid duration';
+
+ const hours = Math.floor(diffMs / (1000 * 60 * 60));
+ const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
+
+ if (hours === 0) {
+ return `${minutes} minute${minutes !== 1 ? 's' : ''}`;
+ } else if (minutes === 0) {
+ return `${hours} hour${hours !== 1 ? 's' : ''}`;
+ } else {
+ return `${hours} hour${hours !== 1 ? 's' : ''} ${minutes} minute${minutes !== 1 ? 's' : ''}`;
+ }
+ };
const calculateDuration = () => {
- if (!startTime || !endTime) return null;
+ if (!startTime) return null;
+ if (isOngoing) {
+ return formatDuration(startTime);
+ }
+ if (!endTime) return null;
+
const start = new Date(startTime);
const end = new Date(endTime);
- const diff = end.getTime() - start.getTime();
- if (diff < 0) return null;
- const hours = Math.floor(diff / (1000 * 60 * 60));
- const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
- return `${hours}h ${minutes}m`;
+ if (end <= start) return null;
+
+ return formatDuration(startTime, endTime);
};
const setStartNow = () => {
- const now = format(new Date(), "yyyy-MM-dd'T'HH:mm");
- setValue('startTime', now);
+ setStartTime(format(new Date(), "yyyy-MM-dd'T'HH:mm"));
};
const setEndNow = () => {
- const now = format(new Date(), "yyyy-MM-dd'T'HH:mm");
- setValue('endTime', now);
+ setEndTime(format(new Date(), "yyyy-MM-dd'T'HH:mm"));
};
- const onSubmit = async (data: SleepFormData) => {
- setError(null);
+ const handleSubmit = async () => {
+ if (!selectedChild) {
+ setError('Please select a child');
+ return;
+ }
+
+ // Validation
+ if (!startTime) {
+ setError('Please enter start time');
+ return;
+ }
+
+ if (!isOngoing && !endTime) {
+ setError('Please enter end time or mark as ongoing');
+ return;
+ }
+
+ if (!isOngoing && endTime) {
+ const start = new Date(startTime);
+ const end = new Date(endTime);
+ if (end <= start) {
+ setError('End time must be after start time');
+ return;
+ }
+ }
try {
- // TODO: Call API to save sleep data
- console.log('Sleep data:', data);
- setSuccess(true);
- setTimeout(() => router.push('/'), 2000);
+ setLoading(true);
+ setError(null);
+
+ const data: SleepData = {
+ startTime,
+ quality,
+ location,
+ isOngoing,
+ };
+
+ if (!isOngoing && endTime) {
+ data.endTime = endTime;
+ }
+
+ await trackingApi.createActivity(selectedChild, {
+ type: 'sleep',
+ timestamp: startTime,
+ data,
+ notes: notes || undefined,
+ });
+
+ setSuccessMessage('Sleep logged successfully!');
+
+ // Reset form
+ resetForm();
+
+ // Reload recent sleeps
+ await loadRecentSleeps();
} catch (err: any) {
- setError(err.message || 'Failed to log sleep');
+ console.error('Failed to save sleep:', err);
+ setError(err.response?.data?.message || 'Failed to save sleep');
+ } finally {
+ setLoading(false);
}
};
+ const resetForm = () => {
+ setStartTime(format(new Date(), "yyyy-MM-dd'T'HH:mm"));
+ setEndTime(format(new Date(), "yyyy-MM-dd'T'HH:mm"));
+ setQuality('good');
+ setLocation('crib');
+ setIsOngoing(false);
+ setNotes('');
+ };
+
+ const handleDeleteClick = (activityId: string) => {
+ setActivityToDelete(activityId);
+ setDeleteDialogOpen(true);
+ };
+
+ const handleDeleteConfirm = async () => {
+ if (!activityToDelete) return;
+
+ try {
+ setLoading(true);
+ await trackingApi.deleteActivity(activityToDelete);
+ setSuccessMessage('Sleep deleted successfully');
+ setDeleteDialogOpen(false);
+ setActivityToDelete(null);
+ await loadRecentSleeps();
+ } catch (err: any) {
+ console.error('Failed to delete sleep:', err);
+ setError(err.response?.data?.message || 'Failed to delete sleep');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const getLocationIcon = (loc: string) => {
+ switch (loc) {
+ case 'crib':
+ return ;
+ case 'bed':
+ return ;
+ case 'stroller':
+ return ;
+ case 'carrier':
+ return ;
+ case 'other':
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ const getQualityColor = (qual: string) => {
+ switch (qual) {
+ case 'excellent':
+ return 'success';
+ case 'good':
+ return 'primary';
+ case 'fair':
+ return 'warning';
+ case 'poor':
+ return 'error';
+ default:
+ return 'default';
+ }
+ };
+
+ const getSleepDetails = (activity: Activity) => {
+ const data = activity.data as SleepData;
+ const duration = data.endTime
+ ? formatDuration(data.startTime, data.endTime)
+ : data.isOngoing
+ ? `Ongoing - ${formatDuration(data.startTime)}`
+ : 'No end time';
+
+ return `${duration} - ${data.location.charAt(0).toUpperCase() + data.location.slice(1)}`;
+ };
+
+ if (childrenLoading) {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+
+ if (!familyId || children.length === 0) {
+ return (
+
+
+
+
+ Please add a child first before tracking sleep activities.
+
+
+
+
+
+ );
+ }
+
return (
@@ -113,14 +360,8 @@ export default function SleepTrackPage() {
- {success && (
-
- Sleep logged successfully!
-
- )}
-
{error && (
-
+ setError(null)}>
{error}
)}
@@ -130,46 +371,74 @@ export default function SleepTrackPage() {
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
-
-
- {/* Start Time */}
-
-
-
-
- Sleep Start
-
-
-
-
-
-
-
+ {/* Child Selector */}
+ {children.length > 1 && (
+
+
+ Select Child
+
+
+
+ )}
- {/* End Time */}
+ {/* Main Form */}
+
+ {/* Start Time */}
+
+
+ Sleep Start Time
+
+
+ setStartTime(e.target.value)}
+ InputLabelProps={{ shrink: true }}
+ />
+
+
+
+
+ {/* Ongoing Checkbox */}
+
+
+ Sleep Status
+
+
+
+
+ {/* End Time */}
+ {!isOngoing && (
-
-
-
- Wake Up
-
-
+
+ Wake Up Time
+
setEndTime(e.target.value)}
InputLabelProps={{ shrink: true }}
/>
+ )}
- {/* Duration Display */}
- {calculateDuration() && (
-
-
-
- )}
-
- {/* Sleep Quality */}
-
-
- Sleep Quality
-
-
- }
- label="Excellent"
- />
- }
- label="Good"
- />
- }
- label="Fair"
- />
- }
- label="Poor"
- />
-
-
-
- {/* Notes */}
-
-
- {/* Voice Input Button */}
-
+ {/* Duration Display */}
+ {calculateDuration() && (
+
}
- label="Use Voice Input"
- onClick={() => {/* TODO: Implement voice input */}}
- sx={{ cursor: 'pointer' }}
+ label={`Duration: ${calculateDuration()}`}
+ color="primary"
+ sx={{ fontSize: '1rem', py: 3 }}
/>
+ )}
- {/* Submit Button */}
- }
+ {/* Sleep Quality */}
+
+ Sleep Quality
+
+
+
+ {/* Location */}
+
+ Location
+
+
+
+ {/* Common Notes Field */}
+ setNotes(e.target.value)}
+ sx={{ mb: 3 }}
+ placeholder="Any disruptions, dreams, or observations..."
+ />
+
+ {/* Submit Button */}
+ }
+ onClick={handleSubmit}
+ disabled={loading}
+ >
+ {loading ? 'Saving...' : 'Save Sleep'}
+
+
+
+ {/* Recent Sleeps */}
+
+
+
+ Recent Sleep Activities
+
+
+
+
+
+ {sleepsLoading ? (
+
+
+
+ ) : recentSleeps.length === 0 ? (
+
+
+ No sleep activities yet
+
+
+ ) : (
+
+ {recentSleeps.map((activity, index) => {
+ const data = activity.data as SleepData;
+ return (
+
+
+
+
+
+ {getLocationIcon(data.location)}
+
+
+
+
+ Sleep
+
+
+
+
+
+ {getSleepDetails(activity)}
+
+ {activity.notes && (
+
+ {activity.notes}
+
+ )}
+
+
+ handleDeleteClick(activity.id)}
+ disabled={loading}
+ >
+
+
+
+
+
+
+
+ );
+ })}
+
+ )}
+
+ {/* Delete Confirmation Dialog */}
+
+
+ {/* Success Snackbar */}
+ setSuccessMessage(null)}
+ message={successMessage}
+ />
);
diff --git a/maternal-web/lib/api/users.ts b/maternal-web/lib/api/users.ts
new file mode 100644
index 0000000..a73eae8
--- /dev/null
+++ b/maternal-web/lib/api/users.ts
@@ -0,0 +1,23 @@
+import apiClient from './client';
+
+export interface UpdateProfileData {
+ name?: string;
+}
+
+export interface UserProfile {
+ id: string;
+ email: string;
+ name: string;
+ role: string;
+ locale: string;
+ emailVerified: boolean;
+ families?: string[];
+}
+
+export const usersApi = {
+ // Update user profile
+ updateProfile: async (data: UpdateProfileData): Promise => {
+ const response = await apiClient.patch('/api/v1/auth/profile', data);
+ return response.data.data;
+ },
+};