mirror of
https://github.com/Oak-and-Sprout/sprout-track.git
synced 2026-02-09 00:59:01 -06:00
Added charts for diapers
This commit is contained in:
140
src/components/Reports/DiaperChartModal.tsx
Normal file
140
src/components/Reports/DiaperChartModal.tsx
Normal 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;
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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} />
|
||||
|
||||
Reference in New Issue
Block a user