feat: per-baby breastfeeding and pump feature toggles
Add isBreastfed and hasPump flags to Baby model. When disabled, hide breastfeeding/pump options from quick-add FAB, dashboard counters and quick-log groups, and the milk stock nav link. Settings page gains toggle UI to configure per baby. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Baby" ADD COLUMN "hasPump" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
ADD COLUMN "isBreastfed" BOOLEAN NOT NULL DEFAULT true;
|
||||||
@@ -150,6 +150,8 @@ model Baby {
|
|||||||
name String
|
name String
|
||||||
birthDate DateTime
|
birthDate DateTime
|
||||||
gender String?
|
gender String?
|
||||||
|
isBreastfed Boolean @default(true)
|
||||||
|
hasPump Boolean @default(true)
|
||||||
familyId String
|
familyId String
|
||||||
family Family @relation(fields: [familyId], references: [id])
|
family Family @relation(fields: [familyId], references: [id])
|
||||||
events Event[]
|
events Event[]
|
||||||
|
|||||||
@@ -12,24 +12,21 @@ import { fr } from "date-fns/locale";
|
|||||||
import { AlertTriangle, Clock, CheckCircle2, Pin, PinOff, Pencil, Check, X, Plus, FileText, ImageIcon } from "lucide-react";
|
import { AlertTriangle, Clock, CheckCircle2, Pin, PinOff, Pencil, Check, X, Plus, FileText, ImageIcon } from "lucide-react";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
|
|
||||||
const QUICK_LOG_GROUPS: { label: string; types: EventType[] }[] = [
|
function buildQuickLogGroups(isBreastfed: boolean, hasPump: boolean): { label: string; types: EventType[] }[] {
|
||||||
{
|
const feedTypes: EventType[] = [
|
||||||
label: "Alimentation & Soins",
|
...(isBreastfed ? (["BREASTFEED"] as EventType[]) : []),
|
||||||
types: ["BREASTFEED", "BOTTLE", "PUMP", "DIAPER_WET", "DIAPER_STOOL"],
|
"BOTTLE",
|
||||||
},
|
...(hasPump ? (["PUMP"] as EventType[]) : []),
|
||||||
{
|
"DIAPER_WET",
|
||||||
label: "Sommeil & Activités",
|
"DIAPER_STOOL",
|
||||||
types: ["NAP", "NIGHT_SLEEP", "BATH", "WALK", "TUMMY_TIME"],
|
];
|
||||||
},
|
return [
|
||||||
{
|
{ label: "Alimentation & Soins", types: feedTypes },
|
||||||
label: "Santé",
|
{ label: "Sommeil & Activités", types: ["NAP", "NIGHT_SLEEP", "BATH", "WALK", "TUMMY_TIME"] },
|
||||||
types: ["TEMPERATURE", "MEDICATION", "SYMPTOM"],
|
{ label: "Santé", types: ["TEMPERATURE", "MEDICATION", "SYMPTOM"] },
|
||||||
},
|
{ label: "Autre", types: ["OTHER"] },
|
||||||
{
|
];
|
||||||
label: "Autre",
|
}
|
||||||
types: ["OTHER"],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
interface Event {
|
interface Event {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -434,7 +431,7 @@ export default function DashboardPage() {
|
|||||||
queryKey: ["events-feeds", selectedBaby?.id],
|
queryKey: ["events-feeds", selectedBaby?.id],
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
fetch(`/api/events?babyId=${selectedBaby!.id}&limit=100&type=BREASTFEED`).then((r) => r.json()),
|
fetch(`/api/events?babyId=${selectedBaby!.id}&limit=100&type=BREASTFEED`).then((r) => r.json()),
|
||||||
enabled: !!selectedBaby?.id,
|
enabled: !!selectedBaby?.id && (selectedBaby?.isBreastfed ?? true),
|
||||||
staleTime: 60_000,
|
staleTime: 60_000,
|
||||||
});
|
});
|
||||||
const { data: bottleData } = useQuery({
|
const { data: bottleData } = useQuery({
|
||||||
@@ -577,8 +574,11 @@ export default function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Today's summary counters */}
|
{/* Today's summary counters */}
|
||||||
<div className="grid grid-cols-4 gap-3 mb-6">
|
<div className={`grid gap-3 mb-6 ${selectedBaby.isBreastfed ? "grid-cols-4" : "grid-cols-3"}`}>
|
||||||
{(["BREASTFEED", "BOTTLE", "DIAPER_WET", "DIAPER_STOOL"] as EventType[]).map((t) => {
|
{(["BREASTFEED", "BOTTLE", "DIAPER_WET", "DIAPER_STOOL"] as EventType[]).filter((t) => {
|
||||||
|
if (t === "BREASTFEED" && !selectedBaby.isBreastfed) return false;
|
||||||
|
return true;
|
||||||
|
}).map((t) => {
|
||||||
const cfg = EVENT_CONFIG[t];
|
const cfg = EVENT_CONFIG[t];
|
||||||
return (
|
return (
|
||||||
<div key={t} className="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl p-3 text-center">
|
<div key={t} className="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl p-3 text-center">
|
||||||
@@ -673,7 +673,7 @@ export default function DashboardPage() {
|
|||||||
|
|
||||||
{/* Quick log */}
|
{/* Quick log */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{QUICK_LOG_GROUPS.map((group) => (
|
{buildQuickLogGroups(selectedBaby.isBreastfed, selectedBaby.hasPump).map((group) => (
|
||||||
<div key={group.label}>
|
<div key={group.label}>
|
||||||
<h2 className="text-xs font-semibold text-slate-400 dark:text-slate-500 uppercase tracking-wider mb-2">{group.label}</h2>
|
<h2 className="text-xs font-semibold text-slate-400 dark:text-slate-500 uppercase tracking-wider mb-2">{group.label}</h2>
|
||||||
<div className="grid grid-cols-4 sm:grid-cols-6 gap-2">
|
<div className="grid grid-cols-4 sm:grid-cols-6 gap-2">
|
||||||
|
|||||||
@@ -438,7 +438,7 @@ export default function SettingsPage() {
|
|||||||
const [dateTo, setDateTo] = useState("");
|
const [dateTo, setDateTo] = useState("");
|
||||||
|
|
||||||
const [editingBaby, setEditingBaby] = useState(false);
|
const [editingBaby, setEditingBaby] = useState(false);
|
||||||
const [babyForm, setBabyForm] = useState({ name: "", birthDate: "", gender: "" });
|
const [babyForm, setBabyForm] = useState({ name: "", birthDate: "", gender: "", isBreastfed: true, hasPump: true });
|
||||||
const [savingBaby, setSavingBaby] = useState(false);
|
const [savingBaby, setSavingBaby] = useState(false);
|
||||||
|
|
||||||
const [thresholds, setThresholdsState] = useState(getThresholds);
|
const [thresholds, setThresholdsState] = useState(getThresholds);
|
||||||
@@ -563,13 +563,15 @@ export default function SettingsPage() {
|
|||||||
setExportLoading(false);
|
setExportLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function startEditBaby(b?: { name: string; birthDate: string; gender?: string }) {
|
function startEditBaby(b?: { name: string; birthDate: string; gender?: string; isBreastfed?: boolean; hasPump?: boolean }) {
|
||||||
const src = b ?? selectedBaby;
|
const src = b ?? selectedBaby;
|
||||||
if (!src) return;
|
if (!src) return;
|
||||||
setBabyForm({
|
setBabyForm({
|
||||||
name: src.name,
|
name: src.name,
|
||||||
birthDate: src.birthDate.slice(0, 10),
|
birthDate: src.birthDate.slice(0, 10),
|
||||||
gender: src.gender ?? "",
|
gender: src.gender ?? "",
|
||||||
|
isBreastfed: src.isBreastfed !== false,
|
||||||
|
hasPump: src.hasPump !== false,
|
||||||
});
|
});
|
||||||
setEditingBaby(true);
|
setEditingBaby(true);
|
||||||
}
|
}
|
||||||
@@ -696,6 +698,25 @@ export default function SettingsPage() {
|
|||||||
<option value="girl">Fille</option>
|
<option value="girl">Fille</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400">Alimentation</label>
|
||||||
|
{[
|
||||||
|
{ key: "isBreastfed" as const, label: "Allaitement maternel" },
|
||||||
|
{ key: "hasPump" as const, label: "Tire-lait & stock lait" },
|
||||||
|
].map(({ key, label }) => (
|
||||||
|
<button
|
||||||
|
key={key}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setBabyForm((f) => ({ ...f, [key]: !f[key] }))}
|
||||||
|
className="w-full flex items-center justify-between px-3 py-2 rounded-lg border border-slate-200 dark:border-slate-600 text-sm text-slate-700 dark:text-slate-200 hover:bg-slate-50 dark:hover:bg-slate-700/50 transition"
|
||||||
|
>
|
||||||
|
<span>{label}</span>
|
||||||
|
<div className={`w-9 h-5 rounded-full transition-colors ${babyForm[key] ? "bg-indigo-600" : "bg-slate-200 dark:bg-slate-600"} relative flex-shrink-0`}>
|
||||||
|
<div className={`absolute top-0.5 w-4 h-4 rounded-full bg-white shadow transition-transform ${babyForm[key] ? "translate-x-4" : "translate-x-0.5"}`} />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button type="button" onClick={() => setEditingBaby(false)} className="flex-1 py-2 border border-slate-200 dark:border-slate-700 rounded-lg text-sm text-slate-600 dark:text-slate-400 bg-white dark:bg-slate-700">Annuler</button>
|
<button type="button" onClick={() => setEditingBaby(false)} className="flex-1 py-2 border border-slate-200 dark:border-slate-700 rounded-lg text-sm text-slate-600 dark:text-slate-400 bg-white dark:bg-slate-700">Annuler</button>
|
||||||
<button type="submit" disabled={savingBaby} className="flex-1 py-2 bg-indigo-600 text-white rounded-lg text-sm font-medium">
|
<button type="submit" disabled={savingBaby} className="flex-1 py-2 bg-indigo-600 text-white rounded-lg text-sm font-medium">
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export async function PATCH(
|
|||||||
if (!session?.user?.id) return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
if (!session?.user?.id) return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||||
|
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
const { name, birthDate, gender } = await req.json();
|
const { name, birthDate, gender, isBreastfed, hasPump } = await req.json();
|
||||||
|
|
||||||
const familyId = (session.user as { familyId?: string }).familyId;
|
const familyId = (session.user as { familyId?: string }).familyId;
|
||||||
if (!familyId) return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
if (!familyId) return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||||
@@ -25,6 +25,8 @@ export async function PATCH(
|
|||||||
...(name ? { name } : {}),
|
...(name ? { name } : {}),
|
||||||
...(birthDate ? { birthDate: new Date(birthDate) } : {}),
|
...(birthDate ? { birthDate: new Date(birthDate) } : {}),
|
||||||
...(gender !== undefined ? { gender } : {}),
|
...(gender !== undefined ? { gender } : {}),
|
||||||
|
...(isBreastfed !== undefined ? { isBreastfed } : {}),
|
||||||
|
...(hasPump !== undefined ? { hasPump } : {}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -186,6 +186,7 @@ export function SidebarNav() {
|
|||||||
{/* Grouped links */}
|
{/* Grouped links */}
|
||||||
{DRAWER_GROUPS.map((group) => {
|
{DRAWER_GROUPS.map((group) => {
|
||||||
const groupLinks = group.hrefs
|
const groupLinks = group.hrefs
|
||||||
|
.filter((href) => !(href === "/milk" && !selectedBaby?.hasPump))
|
||||||
.map((href) => LINKS.find((l) => l.href === href))
|
.map((href) => LINKS.find((l) => l.href === href))
|
||||||
.filter(Boolean) as typeof LINKS;
|
.filter(Boolean) as typeof LINKS;
|
||||||
if (groupLinks.length === 0) return null;
|
if (groupLinks.length === 0) return null;
|
||||||
@@ -265,6 +266,7 @@ export function BottomNav() {
|
|||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const isSuperAdmin = (session?.user as { isSuperAdmin?: boolean })?.isSuperAdmin;
|
const isSuperAdmin = (session?.user as { isSuperAdmin?: boolean })?.isSuperAdmin;
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
|
const { selectedBaby } = useBaby();
|
||||||
|
|
||||||
const secondaryLinks = isSuperAdmin
|
const secondaryLinks = isSuperAdmin
|
||||||
? [...SECONDARY_LINKS, { href: "/admin", label: "Admin", Icon: ShieldCheck }]
|
? [...SECONDARY_LINKS, { href: "/admin", label: "Admin", Icon: ShieldCheck }]
|
||||||
@@ -327,6 +329,7 @@ export function BottomNav() {
|
|||||||
<div className="px-4 pb-4 space-y-4">
|
<div className="px-4 pb-4 space-y-4">
|
||||||
{DRAWER_GROUPS.map((group) => {
|
{DRAWER_GROUPS.map((group) => {
|
||||||
const groupLinks = group.hrefs
|
const groupLinks = group.hrefs
|
||||||
|
.filter((href) => !(href === "/milk" && !selectedBaby?.hasPump))
|
||||||
.map((href) => secondaryLinks.find((l) => l.href === href))
|
.map((href) => secondaryLinks.find((l) => l.href === href))
|
||||||
.filter(Boolean) as typeof secondaryLinks;
|
.filter(Boolean) as typeof secondaryLinks;
|
||||||
if (groupLinks.length === 0) return null;
|
if (groupLinks.length === 0) return null;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { Plus, X } from "lucide-react";
|
|||||||
|
|
||||||
const FAB_HIDDEN_PATHS = ["/settings", "/admin"];
|
const FAB_HIDDEN_PATHS = ["/settings", "/admin"];
|
||||||
|
|
||||||
const QUICK_TYPES: EventType[] = [
|
const ALL_QUICK_TYPES: EventType[] = [
|
||||||
"BREASTFEED",
|
"BREASTFEED",
|
||||||
"BOTTLE",
|
"BOTTLE",
|
||||||
"PUMP",
|
"PUMP",
|
||||||
@@ -32,6 +32,12 @@ export function QuickAddFab() {
|
|||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
const { selectedBaby } = useBaby();
|
const { selectedBaby } = useBaby();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
const quickTypes = ALL_QUICK_TYPES.filter((t) => {
|
||||||
|
if (t === "BREASTFEED" && !selectedBaby?.isBreastfed) return false;
|
||||||
|
if (t === "PUMP" && !selectedBaby?.hasPump) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
const [sheetOpen, setSheetOpen] = useState(false);
|
const [sheetOpen, setSheetOpen] = useState(false);
|
||||||
const [activeModal, setActiveModal] = useState<EventType | null>(null);
|
const [activeModal, setActiveModal] = useState<EventType | null>(null);
|
||||||
|
|
||||||
@@ -75,7 +81,7 @@ export function QuickAddFab() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-5 gap-2 px-4 pb-2">
|
<div className="grid grid-cols-5 gap-2 px-4 pb-2">
|
||||||
{QUICK_TYPES.map((type) => {
|
{quickTypes.map((type) => {
|
||||||
const cfg = EVENT_CONFIG[type];
|
const cfg = EVENT_CONFIG[type];
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ interface Baby {
|
|||||||
name: string;
|
name: string;
|
||||||
birthDate: string;
|
birthDate: string;
|
||||||
gender?: string;
|
gender?: string;
|
||||||
|
isBreastfed: boolean;
|
||||||
|
hasPump: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Family {
|
interface Family {
|
||||||
|
|||||||
Reference in New Issue
Block a user