- Create DynamicChildDashboard component with tab view (1-3 children) and card view (4+) - Integrate with Redux viewMode selector for automatic layout switching - Update GraphQL dashboard query to include displayColor, sortOrder, nickname - Replace static summary with dynamic multi-child dashboard - Add child selection handling with Redux state sync - Implement compact metrics display for card view - Build and test successfully
313 lines
9.1 KiB
TypeScript
313 lines
9.1 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import {
|
|
Box,
|
|
Tabs,
|
|
Tab,
|
|
Card,
|
|
CardContent,
|
|
Avatar,
|
|
Typography,
|
|
Grid,
|
|
Chip,
|
|
Paper,
|
|
} from '@mui/material';
|
|
import { useSelector, useDispatch } from 'react-redux';
|
|
import {
|
|
childrenSelectors,
|
|
selectViewMode,
|
|
selectSelectedChild,
|
|
selectChild,
|
|
Child,
|
|
} from '@/store/slices/childrenSlice';
|
|
import { RootState } from '@/store/store';
|
|
import {
|
|
Restaurant,
|
|
Hotel,
|
|
BabyChangingStation,
|
|
MedicalServices,
|
|
} from '@mui/icons-material';
|
|
|
|
interface DashboardMetrics {
|
|
feedingCount: number;
|
|
sleepDuration: number;
|
|
diaperCount: number;
|
|
medicationCount: number;
|
|
}
|
|
|
|
interface DynamicChildDashboardProps {
|
|
childMetrics: Record<string, DashboardMetrics>;
|
|
onChildSelect: (childId: string) => void;
|
|
}
|
|
|
|
export default function DynamicChildDashboard({
|
|
childMetrics,
|
|
onChildSelect,
|
|
}: DynamicChildDashboardProps) {
|
|
const dispatch = useDispatch();
|
|
const children = useSelector((state: RootState) => childrenSelectors.selectAll(state));
|
|
const viewMode = useSelector(selectViewMode);
|
|
const selectedChild = useSelector(selectSelectedChild);
|
|
const [selectedTab, setSelectedTab] = useState<string>('');
|
|
|
|
// Initialize selected tab
|
|
useEffect(() => {
|
|
if (selectedChild?.id) {
|
|
setSelectedTab(selectedChild.id);
|
|
} else if (children.length > 0) {
|
|
const firstChildId = children[0].id;
|
|
setSelectedTab(firstChildId);
|
|
dispatch(selectChild(firstChildId));
|
|
onChildSelect(firstChildId);
|
|
}
|
|
}, [selectedChild, children, dispatch, onChildSelect]);
|
|
|
|
const handleTabChange = (_event: React.SyntheticEvent, newValue: string) => {
|
|
setSelectedTab(newValue);
|
|
dispatch(selectChild(newValue));
|
|
onChildSelect(newValue);
|
|
};
|
|
|
|
const handleCardClick = (childId: string) => {
|
|
setSelectedTab(childId);
|
|
dispatch(selectChild(childId));
|
|
onChildSelect(childId);
|
|
};
|
|
|
|
const formatSleepHours = (minutes: number) => {
|
|
const hours = Math.floor(minutes / 60);
|
|
const mins = minutes % 60;
|
|
if (hours > 0 && mins > 0) {
|
|
return `${hours}h ${mins}m`;
|
|
} else if (hours > 0) {
|
|
return `${hours}h`;
|
|
} else {
|
|
return `${mins}m`;
|
|
}
|
|
};
|
|
|
|
const renderMetrics = (child: Child, isCompact: boolean = false) => {
|
|
const metrics = childMetrics[child.id] || {
|
|
feedingCount: 0,
|
|
sleepDuration: 0,
|
|
diaperCount: 0,
|
|
medicationCount: 0,
|
|
};
|
|
|
|
if (isCompact) {
|
|
return (
|
|
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', mt: 1 }}>
|
|
<Chip
|
|
icon={<Restaurant sx={{ fontSize: 16 }} />}
|
|
label={`${metrics.feedingCount} feeds`}
|
|
size="small"
|
|
variant="outlined"
|
|
/>
|
|
<Chip
|
|
icon={<Hotel sx={{ fontSize: 16 }} />}
|
|
label={formatSleepHours(metrics.sleepDuration)}
|
|
size="small"
|
|
variant="outlined"
|
|
/>
|
|
<Chip
|
|
icon={<BabyChangingStation sx={{ fontSize: 16 }} />}
|
|
label={`${metrics.diaperCount} diapers`}
|
|
size="small"
|
|
variant="outlined"
|
|
/>
|
|
{metrics.medicationCount > 0 && (
|
|
<Chip
|
|
icon={<MedicalServices sx={{ fontSize: 16 }} />}
|
|
label={`${metrics.medicationCount} meds`}
|
|
size="small"
|
|
variant="outlined"
|
|
/>
|
|
)}
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Grid container spacing={2} sx={{ mt: 1 }}>
|
|
<Grid item xs={6} sm={3}>
|
|
<Box textAlign="center">
|
|
<Restaurant sx={{ fontSize: 32, color: 'primary.main', mb: 1 }} />
|
|
<Typography variant="h4" fontWeight="600">
|
|
{metrics.feedingCount}
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary">
|
|
Feedings
|
|
</Typography>
|
|
</Box>
|
|
</Grid>
|
|
<Grid item xs={6} sm={3}>
|
|
<Box textAlign="center">
|
|
<Hotel sx={{ fontSize: 32, color: 'info.main', mb: 1 }} />
|
|
<Typography variant="h4" fontWeight="600">
|
|
{formatSleepHours(metrics.sleepDuration)}
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary">
|
|
Sleep
|
|
</Typography>
|
|
</Box>
|
|
</Grid>
|
|
<Grid item xs={6} sm={3}>
|
|
<Box textAlign="center">
|
|
<BabyChangingStation sx={{ fontSize: 32, color: 'warning.main', mb: 1 }} />
|
|
<Typography variant="h4" fontWeight="600">
|
|
{metrics.diaperCount}
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary">
|
|
Diapers
|
|
</Typography>
|
|
</Box>
|
|
</Grid>
|
|
<Grid item xs={6} sm={3}>
|
|
<Box textAlign="center">
|
|
<MedicalServices sx={{ fontSize: 32, color: 'error.main', mb: 1 }} />
|
|
<Typography variant="h4" fontWeight="600">
|
|
{metrics.medicationCount}
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary">
|
|
Medications
|
|
</Typography>
|
|
</Box>
|
|
</Grid>
|
|
</Grid>
|
|
);
|
|
};
|
|
|
|
// Tab view for 1-3 children
|
|
if (viewMode === 'tabs') {
|
|
return (
|
|
<Box>
|
|
<Tabs
|
|
value={selectedTab}
|
|
onChange={handleTabChange}
|
|
variant="fullWidth"
|
|
sx={{
|
|
borderBottom: 1,
|
|
borderColor: 'divider',
|
|
mb: 3,
|
|
}}
|
|
>
|
|
{children.map((child) => (
|
|
<Tab
|
|
key={child.id}
|
|
value={child.id}
|
|
label={
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
<Avatar
|
|
sx={{
|
|
bgcolor: child.displayColor,
|
|
width: 32,
|
|
height: 32,
|
|
fontSize: '0.875rem',
|
|
}}
|
|
>
|
|
{child.name[0]}
|
|
</Avatar>
|
|
<Box sx={{ textAlign: 'left' }}>
|
|
<Typography variant="body2" fontWeight="600">
|
|
{child.name}
|
|
</Typography>
|
|
{child.nickname && (
|
|
<Typography variant="caption" color="text.secondary">
|
|
{child.nickname}
|
|
</Typography>
|
|
)}
|
|
</Box>
|
|
</Box>
|
|
}
|
|
sx={{
|
|
textTransform: 'none',
|
|
minHeight: 64,
|
|
}}
|
|
/>
|
|
))}
|
|
</Tabs>
|
|
|
|
{children.map((child) => (
|
|
<Box
|
|
key={child.id}
|
|
role="tabpanel"
|
|
hidden={selectedTab !== child.id}
|
|
id={`child-tabpanel-${child.id}`}
|
|
aria-labelledby={`child-tab-${child.id}`}
|
|
>
|
|
{selectedTab === child.id && (
|
|
<Paper sx={{ p: 3 }}>
|
|
{renderMetrics(child, false)}
|
|
</Paper>
|
|
)}
|
|
</Box>
|
|
))}
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
// Card view for 4+ children
|
|
return (
|
|
<Box>
|
|
<Typography variant="h6" gutterBottom fontWeight="600">
|
|
Select a child to view details
|
|
</Typography>
|
|
<Grid container spacing={2}>
|
|
{children.map((child) => (
|
|
<Grid item xs={12} sm={6} md={4} key={child.id}>
|
|
<Card
|
|
sx={{
|
|
cursor: 'pointer',
|
|
border: selectedTab === child.id ? 2 : 1,
|
|
borderColor: selectedTab === child.id ? child.displayColor : 'divider',
|
|
transition: 'all 0.2s',
|
|
'&:hover': {
|
|
transform: 'translateY(-4px)',
|
|
boxShadow: 4,
|
|
},
|
|
}}
|
|
onClick={() => handleCardClick(child.id)}
|
|
>
|
|
<CardContent>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
|
|
<Avatar
|
|
sx={{
|
|
bgcolor: child.displayColor,
|
|
width: 48,
|
|
height: 48,
|
|
}}
|
|
>
|
|
{child.name[0]}
|
|
</Avatar>
|
|
<Box sx={{ flex: 1 }}>
|
|
<Typography variant="h6" fontWeight="600">
|
|
{child.name}
|
|
</Typography>
|
|
{child.nickname && (
|
|
<Typography variant="body2" color="text.secondary">
|
|
{child.nickname}
|
|
</Typography>
|
|
)}
|
|
</Box>
|
|
</Box>
|
|
{renderMetrics(child, true)}
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
))}
|
|
</Grid>
|
|
|
|
{/* Show detailed metrics for selected child */}
|
|
{selectedTab && (
|
|
<Paper sx={{ p: 3, mt: 3 }}>
|
|
<Typography variant="h6" gutterBottom fontWeight="600">
|
|
{children.find(c => c.id === selectedTab)?.name}'s Today
|
|
</Typography>
|
|
{renderMetrics(children.find(c => c.id === selectedTab)!, false)}
|
|
</Paper>
|
|
)}
|
|
</Box>
|
|
);
|
|
}
|