Added charts for diapers

This commit is contained in:
John Overton
2025-12-15 15:29:47 -06:00
parent 03fa3d2840
commit 5202fbaf44
3 changed files with 211 additions and 37 deletions

View File

@@ -0,0 +1,140 @@
'use client';
import React, { useMemo } from 'react';
import { cn } from '@/src/lib/utils';
import { Modal, ModalContent } from '@/src/components/ui/modal';
import { growthChartStyles } from './growth-chart.styles';
import { styles } from './reports.styles';
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip as RechartsTooltip,
ResponsiveContainer,
} from 'recharts';
import { ActivityType, DateRange } from './reports.types';
export type DiaperChartMetric = 'wet' | 'poopy';
interface DiaperChartModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
metric: DiaperChartMetric | null;
activities: ActivityType[];
dateRange: DateRange;
}
/**
* DiaperChartModal Component
*
* Displays a line chart modal showing daily diaper counts.
* Supports wet and poopy diaper metrics.
*/
const DiaperChartModal: React.FC<DiaperChartModalProps> = ({
open,
onOpenChange,
metric,
activities,
dateRange,
}) => {
// Calculate daily diaper counts
const chartData = useMemo(() => {
if (!activities.length || !dateRange.from || !dateRange.to || !metric) {
return [];
}
const startDate = new Date(dateRange.from);
startDate.setHours(0, 0, 0, 0);
const endDate = new Date(dateRange.to);
endDate.setHours(23, 59, 59, 999);
const countsByDay: Record<string, number> = {};
activities.forEach((activity) => {
if ('type' in activity && 'time' in activity && 'condition' in activity) {
const activityType = (activity as any).type;
const diaperActivity = activity as any;
// Check if this matches the metric we're looking for
if (metric === 'wet') {
if (activityType !== 'WET' && activityType !== 'BOTH') return;
} else if (metric === 'poopy') {
if (activityType !== 'DIRTY' && activityType !== 'BOTH') return;
}
const diaperTime = new Date(diaperActivity.time);
const dayKey = diaperTime.toISOString().split('T')[0];
if (diaperTime >= startDate && diaperTime <= endDate) {
countsByDay[dayKey] = (countsByDay[dayKey] || 0) + 1;
}
}
});
// Convert to array and sort by date
return Object.entries(countsByDay)
.map(([date, count]) => ({
date,
label: new Date(date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
value: count,
}))
.sort((a, b) => (a.date < b.date ? -1 : 1));
}, [activities, dateRange, metric]);
const title = metric === 'wet' ? 'Wet Diapers Over Time' : 'Poopy Diapers Over Time';
const description =
dateRange.from && dateRange.to
? `From ${dateRange.from.toLocaleDateString()} to ${dateRange.to.toLocaleDateString()}`
: undefined;
return (
<Modal open={open && !!metric} onOpenChange={onOpenChange} title={title} description={description}>
<ModalContent>
{chartData.length === 0 ? (
<div className={cn(styles.emptyContainer, 'reports-empty-container')}>
<p className={cn(styles.emptyText, 'reports-empty-text')}>
No {metric === 'wet' ? 'wet' : 'poopy'} diaper data available for the selected date range.
</p>
</div>
) : (
<div className={cn(growthChartStyles.chartWrapper, 'growth-chart-wrapper')}>
<ResponsiveContainer width="100%" height={300}>
<LineChart data={chartData} margin={{ top: 20, right: 24, left: 8, bottom: 20 }}>
<CartesianGrid strokeDasharray="3 3" className="growth-chart-grid" />
<XAxis
dataKey="label"
label={{ value: 'Date', position: 'insideBottom', offset: -5 }}
className="growth-chart-axis"
/>
<YAxis
type="number"
domain={[0, 'auto']}
tickFormatter={(value) => value.toFixed(0)}
label={{ value: 'Count', angle: -90, position: 'insideLeft' }}
className="growth-chart-axis"
/>
<RechartsTooltip
formatter={(value: any) => [`${value}`, 'Diapers']}
labelFormatter={(label: any) => `Date: ${label}`}
/>
<Line
type="monotone"
dataKey="value"
stroke="#14b8a6"
strokeWidth={2}
dot={{ r: 4, fill: '#14b8a6' }}
activeDot={{ r: 6, fill: '#0f766e' }}
/>
</LineChart>
</ResponsiveContainer>
</div>
)}
</ModalContent>
</Modal>
);
};
export default DiaperChartModal;

View File

@@ -1,6 +1,6 @@
'use client';
import React from 'react';
import React, { useState } from 'react';
import { Icon } from 'lucide-react';
import { diaper } from '@lucide/lab';
import { cn } from '@/src/lib/utils';
@@ -11,10 +11,13 @@ import {
AccordionContent,
} from '@/src/components/ui/accordion';
import { styles } from './reports.styles';
import { DiaperStats } from './reports.types';
import { DiaperStats, ActivityType, DateRange } from './reports.types';
import DiaperChartModal, { DiaperChartMetric } from './DiaperChartModal';
interface DiaperStatsSectionProps {
stats: DiaperStats;
activities: ActivityType[];
dateRange: DateRange;
}
/**
@@ -22,41 +25,72 @@ interface DiaperStatsSectionProps {
*
* Displays diaper statistics including wet and poopy diaper counts.
*/
const DiaperStatsSection: React.FC<DiaperStatsSectionProps> = ({ stats }) => {
return (
<AccordionItem value="diaper">
<AccordionTrigger className={cn(styles.accordionTrigger, "reports-accordion-trigger")}>
<Icon iconNode={diaper} className={cn(styles.accordionTriggerIcon, "reports-accordion-trigger-icon reports-icon-diaper-wet")} />
<span>Diaper Statistics</span>
</AccordionTrigger>
<AccordionContent className={styles.accordionContent}>
<div className={styles.statsGrid}>
<Card className={cn(styles.statCard, "reports-stat-card")}>
<CardContent className="p-4">
<div className={cn(styles.statCardValue, "reports-stat-card-value")}>
{stats.wetCount}
</div>
<div className={cn(styles.statCardLabel, "reports-stat-card-label")}>Wet Diapers</div>
<div className={cn(styles.statCardSubLabel, "reports-stat-card-sublabel")}>
{stats.avgWetPerDay}/day avg
</div>
</CardContent>
</Card>
const DiaperStatsSection: React.FC<DiaperStatsSectionProps> = ({ stats, activities, dateRange }) => {
const [chartModalOpen, setChartModalOpen] = useState(false);
const [chartMetric, setChartMetric] = useState<DiaperChartMetric | null>(null);
<Card className={cn(styles.statCard, "reports-stat-card")}>
<CardContent className="p-4">
<div className={cn(styles.statCardValue, "reports-stat-card-value")}>
{stats.poopCount}
</div>
<div className={cn(styles.statCardLabel, "reports-stat-card-label")}>Poopy Diapers</div>
<div className={cn(styles.statCardSubLabel, "reports-stat-card-sublabel")}>
{stats.avgPoopPerDay}/day avg
</div>
</CardContent>
</Card>
</div>
</AccordionContent>
</AccordionItem>
return (
<>
<AccordionItem value="diaper">
<AccordionTrigger className={cn(styles.accordionTrigger, "reports-accordion-trigger")}>
<Icon iconNode={diaper} className={cn(styles.accordionTriggerIcon, "reports-accordion-trigger-icon reports-icon-diaper-wet")} />
<span>Diaper Statistics</span>
</AccordionTrigger>
<AccordionContent className={styles.accordionContent}>
<div className={styles.statsGrid}>
<Card
className={cn(styles.statCard, "reports-stat-card cursor-pointer")}
onClick={() => {
setChartMetric('wet');
setChartModalOpen(true);
}}
>
<CardContent className="p-4">
<div className={cn(styles.statCardValue, "reports-stat-card-value")}>
{stats.wetCount}
</div>
<div className={cn(styles.statCardLabel, "reports-stat-card-label")}>Wet Diapers</div>
<div className={cn(styles.statCardSubLabel, "reports-stat-card-sublabel")}>
{stats.avgWetPerDay}/day avg
</div>
</CardContent>
</Card>
<Card
className={cn(styles.statCard, "reports-stat-card cursor-pointer")}
onClick={() => {
setChartMetric('poopy');
setChartModalOpen(true);
}}
>
<CardContent className="p-4">
<div className={cn(styles.statCardValue, "reports-stat-card-value")}>
{stats.poopCount}
</div>
<div className={cn(styles.statCardLabel, "reports-stat-card-label")}>Poopy Diapers</div>
<div className={cn(styles.statCardSubLabel, "reports-stat-card-sublabel")}>
{stats.avgPoopPerDay}/day avg
</div>
</CardContent>
</Card>
</div>
</AccordionContent>
</AccordionItem>
{/* Diaper chart modal */}
<DiaperChartModal
open={chartModalOpen}
onOpenChange={(open) => {
setChartModalOpen(open);
if (!open) {
setChartMetric(null);
}
}}
metric={chartMetric}
activities={activities}
dateRange={dateRange}
/>
</>
);
};

View File

@@ -756,7 +756,7 @@ const StatsTab: React.FC<StatsTabProps> = ({
<FeedingStatsSection stats={stats.feeding} activities={activities} dateRange={dateRange} />
{/* Diaper Section */}
<DiaperStatsSection stats={stats.diaper} />
<DiaperStatsSection stats={stats.diaper} activities={activities} dateRange={dateRange} />
{/* Pumping Section */}
<PumpingStatsSection stats={stats.pump} />