diff --git a/src/app/activity/page.tsx b/src/app/activity/page.tsx index 22ae5ce..cc50b00 100644 --- a/src/app/activity/page.tsx +++ b/src/app/activity/page.tsx @@ -1,12 +1,12 @@ "use client"; import { useState, useEffect } from "react"; -import Link from "next/link"; +import { useRouter } from "next/navigation"; import { useFamily } from "../FamilyProvider"; import { getGuideline, getAgeInMonths } from "@/lib/guidelines"; import { api } from "@/lib/api"; import { CalendarView } from "@/components/CalendarView"; -import { LogModal, type LogType as ModalLogType } from "@/components/LogModal"; +import { LogModal, type LogType as ModalLogType, type SmartDefault } from "@/components/LogModal"; import type { Log, LogType } from "@/types"; type ViewMode = "timeline" | "calendar"; @@ -23,16 +23,32 @@ function getIcon(type: LogType) { return "๐Ÿ“"; } +function formatDayLabel(dateStr: string): string { + const d = new Date(dateStr); + const today = new Date().toDateString(); + const yesterday = new Date(Date.now() - 86_400_000).toDateString(); + if (d.toDateString() === today) return "Today"; + if (d.toDateString() === yesterday) return "Yesterday"; + return d.toLocaleDateString("en-IN", { weekday: "short", month: "short", day: "numeric" }); +} + export default function ActivityPage() { + const router = useRouter(); const { child, childId: providerChildId } = useFamily(); - const [view, setView] = useState("timeline"); - const [logs, setLogs] = useState([]); - const [loading, setLoading] = useState(true); - const [filter, setFilter] = useState("all"); - const [showSuggested, setShowSuggested] = useState(true); - const [generating, setGenerating] = useState(false); - const [fabOpen, setFabOpen] = useState(false); - const [modalType, setModalType] = useState(null); + const [view, setView] = useState("timeline"); + const [logs, setLogs] = useState([]); + const [loading, setLoading] = useState(true); + const [filter, setFilter] = useState("all"); + const [showSuggested, setShowSuggested] = useState(true); + const [guideExpanded, setGuideExpanded] = useState(false); + const [generating, setGenerating] = useState(false); + const [fabOpen, setFabOpen] = useState(false); + const [menuOpen, setMenuOpen] = useState(false); + const [modalType, setModalType] = useState(null); + const [selectedLog, setSelectedLog] = useState(null); + const [deleteConfirm, setDeleteConfirm] = useState(false); + const [smartDefault, setSmartDefault] = useState(null); + const [pendingDeleteId, setPendingDeleteId] = useState<{ id: string; type: string } | null>(null); const childId = providerChildId ?? ""; useEffect(() => { @@ -52,6 +68,7 @@ export default function ActivityPage() { const generateHistory = async () => { if (!child) return; + setMenuOpen(false); setGenerating(true); try { const data = await api.post<{ success: boolean }>("/api/history", { childId: child.id, birthDate: child.birthDate }); @@ -62,6 +79,47 @@ export default function ActivityPage() { setGenerating(false); }; + const deleteLog = async (log: Log) => { + try { + await fetch(`/api/logs/${log.id}?type=${log.type}`, { method: "DELETE" }); + setSelectedLog(null); + setDeleteConfirm(false); + fetchLogs(); + } catch (err) { + console.error("Failed to delete log:", err); + } + }; + + const handleEdit = (log: Log) => { + // Store the old log to delete after the new one is saved + setPendingDeleteId({ id: log.id, type: log.type }); + setSmartDefault({ subType: log.subType ?? "", amountMl: log.amount ?? undefined } as SmartDefault); + setModalType(log.type as ModalLogType); + setSelectedLog(null); + }; + + // Today's stats (computed from loaded logs โ€” no extra fetch) + const todayStr = new Date().toDateString(); + const todayLogs = logs.filter(l => new Date(l.loggedAt).toDateString() === todayStr); + const todayCounts = { + feed: todayLogs.filter(l => l.type === "feed").length, + sleep: todayLogs.filter(l => l.type === "sleep").length, + diaper: todayLogs.filter(l => l.type === "diaper").length, + }; + + // Last 4 calendar days (computed from loaded logs โ€” no extra fetch) + const last4Days = Array.from({ length: 4 }, (_, i) => { + const d = new Date(Date.now() - i * 86_400_000); + const dateStr = d.toDateString(); + const dayLogs = logs.filter(l => new Date(l.loggedAt).toDateString() === dateStr); + return { + label: i === 0 ? "Today" : i === 1 ? "Yest." : d.toLocaleDateString("en-IN", { weekday: "short" }), + feed: dayLogs.filter(l => l.type === "feed").length, + sleep: dayLogs.filter(l => l.type === "sleep").length, + diaper: dayLogs.filter(l => l.type === "diaper").length, + }; + }); + const filteredLogs = filter === "all" ? logs : logs.filter(l => l.type === filter); const groupedByDay = filteredLogs.reduce((acc, log) => { @@ -72,138 +130,233 @@ export default function ActivityPage() { return acc; }, []); + const guide = child ? getGuideline(child.birthDate) : null; + const ageMonths = child ? getAgeInMonths(child.birthDate) : 0; + + const guideItems = guide ? [ + { icon: "๐Ÿผ", label: "Feeds/day", count: todayCounts.feed, target: guide.feeds.times, barClass: "bg-rose-400", textClass: "text-rose-600 dark:text-rose-400" }, + { icon: "๐Ÿ˜ด", label: "Sleep (hrs)", count: todayCounts.sleep, target: guide.sleep.totalHours, barClass: "bg-amber-400", textClass: "text-amber-600 dark:text-amber-400" }, + { icon: "๐Ÿšผ", label: "Diapers", count: todayCounts.diaper, target: guide.diapers.count, barClass: "bg-blue-400", textClass: "text-blue-600 dark:text-blue-400" }, + ] : []; + return (
{/* Header */}
- โ† +

Activity

- {child && logs.length === 0 && !loading && ( - - )}
- {/* View toggle */} -
- {(["timeline", "calendar"] as const).map(v => ( + +
+ {/* โ‹ฏ overflow menu */} +
- ))} + onClick={() => setMenuOpen(o => !o)} + className="p-2 rounded-xl bg-white dark:bg-gray-800 shadow-sm text-lg leading-none" + >โ‹ฏ + {menuOpen && ( + <> +
setMenuOpen(false)} /> +
+ +
+ + )} +
+ + {/* View toggle */} +
+ {(["timeline", "calendar"] as const).map(v => ( + + ))} +
{/* Filter pills */} -
- {(["all", "feed", "sleep", "diaper"] as const).map(f => ( +
+ {([ + { value: "all", label: "All" }, + { value: "feed", label: "๐Ÿผ Feed" }, + { value: "sleep", label: "๐Ÿ˜ด Sleep" }, + { value: "diaper", label: "๐Ÿšผ Diaper" }, + ] as { value: LogType | "all"; label: string }[]).map(f => ( ))}
- {/* Guidelines card */} - {child && showSuggested && (() => { - const guide = getGuideline(child.birthDate); - const ageMonths = getAgeInMonths(child.birthDate); - return ( -
-
-
-
- {child.name} ยท {ageMonths} months old + {!loading && ( + <> + {/* Daily summary bar */} +
+
+ {[ + { icon: "๐Ÿผ", label: "Feeds", count: todayCounts.feed }, + { icon: "๐Ÿšผ", label: "Diapers", count: todayCounts.diaper }, + { icon: "๐Ÿ˜ด", label: "Sleep", count: todayCounts.sleep }, + ].map(item => ( +
+ {item.icon} + {item.count} + today
- -
-
-
-
{guide.feeds.times}
-
feeds/day
-
-
-
{guide.sleep.totalHours}h
-
sleep/day
-
-
-
{guide.diapers.count}
-
diapers/day
-
-
+ ))}
- ); - })()} + + {/* 4-day overview strip */} +
+ {last4Days.map(d => ( +
+

{d.label}

+

๐Ÿผร—{d.feed}

+

๐Ÿ˜ดร—{d.sleep}

+

๐Ÿšผร—{d.diaper}

+
+ ))} +
+ + {/* Collapsible guidelines card */} + {child && guide && showSuggested && ( +
+ + + {guideExpanded && ( +
+
+ +
+
+ {guideItems.map(item => ( +
+
{item.label}
+
+ {item.count} + /{item.target} +
+
+
0 ? (item.count / item.target) * 100 : 0)}%` }} + /> +
+
+ ))} +
+
+ )} +
+ )} + + )} {/* Content */}
{loading ? ( -
Loadingโ€ฆ
+
+
+ {["๐Ÿผ", "๐Ÿ˜ด", "๐Ÿšผ"].map((e, i) => ( + {e} + ))} +
+

Loading activityโ€ฆ

+
) : view === "calendar" ? ( ) : groupedByDay.length === 0 ? ( -
-
๐Ÿ“Š
-

No activity yet

+
+ ๐Ÿ“‹ +

+ No {filter !== "all" ? filter : ""} logs yet +

+

Tap + to start logging

) : (
- {groupedByDay.map(day => ( -
-
- {new Date(day.date).toLocaleDateString("en-US", { weekday: "long", month: "short", day: "numeric" })} -
-
- {day.logs - .sort((a, b) => new Date(b.loggedAt).getTime() - new Date(a.loggedAt).getTime()) - .map(log => ( -
- {getIcon(log.type)} -
-
{log.type}
-
- {[ - log.subType?.replace(/_/g, " "), - log.amount ? `${log.amount}ml` : null, - log.notes, - ].filter(Boolean).join(" ยท ")} + {groupedByDay.map(day => { + const label = formatDayLabel(day.date); + const isToday = label === "Today"; + return ( +
+
+ {label} +
+
+ {day.logs + .sort((a, b) => new Date(b.loggedAt).getTime() - new Date(a.loggedAt).getTime()) + .map(log => ( +
-
- {new Date(log.loggedAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })} -
-
- ))} +
+ {new Date(log.loggedAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })} +
+ + ))} +
-
- ))} + ); + })}
)}
{/* FAB */} -
+
{fabOpen && (["feed", "sleep", "diaper"] as ModalLogType[]).map(t => ( + + +
+ ) : ( +
+

+ Delete this {selectedLog.type} log? +

+ + +
+ )} +
+ + )} + setModalType(null)} - onSaved={fetchLogs} - smartDefault={modalType ? (() => { - const last = logs.find(l => l.type === modalType); - return last ? { subType: last.subType ?? "", amountMl: last.amount ?? undefined } : null; - })() : null} + onClose={() => { setModalType(null); setSmartDefault(null); setPendingDeleteId(null); }} + onSaved={async () => { + // If editing, delete the old log after the new one is saved + if (pendingDeleteId) { + try { + await fetch(`/api/logs/${pendingDeleteId.id}?type=${pendingDeleteId.type}`, { method: "DELETE" }); + } catch (err) { + console.error("Failed to delete old log during edit:", err); + } + setPendingDeleteId(null); + } + setSmartDefault(null); + fetchLogs(); + }} + smartDefault={smartDefault} />
); diff --git a/src/app/api/logs/[id]/route.ts b/src/app/api/logs/[id]/route.ts new file mode 100644 index 0000000..3c16e17 --- /dev/null +++ b/src/app/api/logs/[id]/route.ts @@ -0,0 +1,116 @@ +import { NextResponse } from "next/server"; +import { sql } from "@/db"; +import { requireFamily } from "@/lib/auth"; + +const TABLE: Record = { + feed: "feeds", + sleep: "sleeps", + diaper: "diapers_logs", +}; + +export async function DELETE( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { + const auth = await requireFamily(); + if (!auth.success) { + return NextResponse.json({ error: auth.error }, { status: auth.status }); + } + + const { searchParams } = new URL(request.url); + const type = searchParams.get("type"); + if (!type || !TABLE[type]) { + return NextResponse.json({ error: "Invalid or missing type" }, { status: 400 }); + } + + const table = TABLE[type]; + const familyId = auth.session!.familyId!; + const { id } = await params; + + try { + // Verify the log belongs to this family via children.family_id + const rows = await sql.unsafe( + `SELECT l.id FROM ${table} l + JOIN children c ON c.id = l.child_id + WHERE l.id = $1 AND c.family_id = $2`, + [id, familyId] + ); + if (!rows || rows.length === 0) { + return NextResponse.json({ error: "Not found" }, { status: 404 }); + } + + await sql.unsafe(`DELETE FROM ${table} WHERE id = $1`, [id]); + return NextResponse.json({ success: true }); + } catch (error) { + console.error(error); + return NextResponse.json({ error: String(error) }, { status: 500 }); + } +} + +export async function PATCH( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { + const auth = await requireFamily(); + if (!auth.success) { + return NextResponse.json({ error: auth.error }, { status: auth.status }); + } + + const { searchParams } = new URL(request.url); + const type = searchParams.get("type"); + if (!type || !TABLE[type]) { + return NextResponse.json({ error: "Invalid or missing type" }, { status: 400 }); + } + + const table = TABLE[type]; + const familyId = auth.session!.familyId!; + const { id } = await params; + + try { + // Ownership check + const rows = await sql.unsafe( + `SELECT l.id FROM ${table} l + JOIN children c ON c.id = l.child_id + WHERE l.id = $1 AND c.family_id = $2`, + [id, familyId] + ); + if (!rows || rows.length === 0) { + return NextResponse.json({ error: "Not found" }, { status: 404 }); + } + + const body = await request.json(); + const { notes, amountMl, subType } = body; + + if (type === "feed") { + await sql.unsafe( + `UPDATE feeds SET + type = COALESCE($2, type), + amount_ml = COALESCE($3, amount_ml), + notes = COALESCE($4, notes) + WHERE id = $1`, + [id, subType ?? null, amountMl ?? null, notes ?? null] + ); + } else if (type === "diaper") { + await sql.unsafe( + `UPDATE diapers_logs SET + type = COALESCE($2, type), + notes = COALESCE($3, notes) + WHERE id = $1`, + [id, subType ?? null, notes ?? null] + ); + } else if (type === "sleep") { + await sql.unsafe( + `UPDATE sleeps SET + type = COALESCE($2, type), + notes = COALESCE($3, notes) + WHERE id = $1`, + [id, subType ?? null, notes ?? null] + ); + } + + return NextResponse.json({ success: true }); + } catch (error) { + console.error(error); + return NextResponse.json({ error: String(error) }, { status: 500 }); + } +}