feat: pump tracker, photo log, milk stock, medication profiles, calendar, reminders, audit log, edit events
New pages: - /calendar — monthly grid with event dots, day detail on tap - /medications — CRUD for medication profiles (presets + custom, dose/unit/intervals/crisis mode) - /milk — milk stock management with location-based expiry, alerts, mark-as-used - /reminders — recurring medication reminder CRUD with Pushover notifications New APIs: - /api/medication-profiles — profiles CRUD + last-intakes endpoint (auto-seeds 10 presets per family) - /api/milk — milk stock CRUD with auto-calculated expiry - /api/reminders + /api/reminders/check — reminder CRUD + cron-callable check endpoint - /api/upload — multipart photo upload to public/uploads/ - /api/invite/send-email — email invitation with family invite link - /api/admin/audit — last 100 audit log entries (superadmin only) Event modal improvements: - PUMP event type (side selector + volume + timer) - MEDICATION: profile chip picker, next-dose timing status, crisis mode toggle - Photo attachment (upload + thumbnail preview) - Datetime inputs split into date + time (iOS Safari datetime-local fix) - Edit mode via initialEvent prop (pre-fills all fields, saves via PATCH) - Timer now shows h:mm:ss when >= 1 hour Timeline: - Modify button opens pre-filled edit modal per event - Photo thumbnail in expanded view Dashboard: - PUMP in quick-log - Medication status card (too-soon / available with next-dose time) Schema additions: MedicationProfile, MedcationReminder, MilkStock, AuditLog models Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,10 +9,12 @@ import { useBaby } from "@/contexts/baby-context";
|
||||
import { checkFeedAlert, getThresholds } from "@/lib/notifications";
|
||||
import { format, isToday } from "date-fns";
|
||||
import { fr } from "date-fns/locale";
|
||||
import { AlertTriangle, Clock, CheckCircle2 } from "lucide-react";
|
||||
|
||||
const QUICK_LOG_TYPES: EventType[] = [
|
||||
"BREASTFEED",
|
||||
"BOTTLE",
|
||||
"PUMP",
|
||||
"DIAPER_WET",
|
||||
"DIAPER_STOOL",
|
||||
"NAP",
|
||||
@@ -93,6 +95,19 @@ export default function DashboardPage() {
|
||||
const todayCounts: Partial<Record<EventType, number>> = {};
|
||||
for (const ev of todayEvents) todayCounts[ev.type] = (todayCounts[ev.type] ?? 0) + 1;
|
||||
|
||||
const { data: medProfiles = [] } = useQuery<{ id: string; name: string; molecule: string | null; intervalHours: number; minIntervalHours: number | null }[]>({
|
||||
queryKey: ["med-profiles"],
|
||||
queryFn: () => fetch("/api/medication-profiles").then((r) => r.json()),
|
||||
enabled: !!selectedBaby?.id,
|
||||
});
|
||||
|
||||
const { data: lastIntakes = {} } = useQuery<Record<string, { startedAt: string }>>({
|
||||
queryKey: ["last-intakes", selectedBaby?.id],
|
||||
queryFn: () => fetch(`/api/medication-profiles/last-intakes?babyId=${selectedBaby!.id}`).then((r) => r.json()),
|
||||
enabled: !!selectedBaby?.id,
|
||||
refetchInterval: 30_000,
|
||||
});
|
||||
|
||||
if (familyLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
@@ -195,6 +210,52 @@ export default function DashboardPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Medication status */}
|
||||
{medProfiles.length > 0 && (() => {
|
||||
const activeMeds = medProfiles.filter((p) => lastIntakes[p.name]);
|
||||
if (activeMeds.length === 0) return null;
|
||||
return (
|
||||
<div className="mt-6">
|
||||
<h2 className="text-xs font-semibold text-slate-400 dark:text-slate-500 uppercase tracking-wider mb-3">Médicaments</h2>
|
||||
<div className="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl overflow-hidden divide-y divide-slate-100 dark:divide-slate-700">
|
||||
{activeMeds.map((p) => {
|
||||
const intake = lastIntakes[p.name];
|
||||
const nextAt = new Date(new Date(intake.startedAt).getTime() + p.intervalHours * 3600000);
|
||||
const waitMs = nextAt.getTime() - Date.now();
|
||||
const tooSoon = waitMs > 0;
|
||||
const h = Math.floor(Math.abs(waitMs) / 3600000);
|
||||
const m = Math.floor((Math.abs(waitMs) % 3600000) / 60000);
|
||||
const timeLabel = h > 0 ? `${h}h${m.toString().padStart(2, "0")}` : `${m}min`;
|
||||
return (
|
||||
<div key={p.id} className="flex items-center gap-3 px-4 py-3">
|
||||
<div className={`w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 ${tooSoon ? "bg-red-50 dark:bg-red-950" : "bg-green-50 dark:bg-green-950"}`}>
|
||||
{tooSoon
|
||||
? <AlertTriangle className="w-4 h-4 text-red-500" />
|
||||
: <CheckCircle2 className="w-4 h-4 text-green-500" />
|
||||
}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-slate-700 dark:text-slate-200">{p.name}</p>
|
||||
{p.molecule && <p className="text-xs text-slate-400 dark:text-slate-500">{p.molecule}</p>}
|
||||
</div>
|
||||
<div className="text-right flex-shrink-0">
|
||||
{tooSoon ? (
|
||||
<p className="text-xs font-semibold text-red-600 dark:text-red-400">Dans {timeLabel}</p>
|
||||
) : (
|
||||
<p className="text-xs font-semibold text-green-600 dark:text-green-400">Disponible</p>
|
||||
)}
|
||||
<p className="text-[10px] text-slate-400 dark:text-slate-500 mt-0.5 flex items-center gap-1 justify-end">
|
||||
<Clock className="w-2.5 h-2.5" />{format(nextAt, "HH:mm")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Today's timeline */}
|
||||
{todayEvents.length > 0 && (
|
||||
<div className="mt-6">
|
||||
|
||||
Reference in New Issue
Block a user