diff --git a/src/app/FamilySwitcher.tsx b/src/app/FamilySwitcher.tsx new file mode 100644 index 0000000..db06cc5 --- /dev/null +++ b/src/app/FamilySwitcher.tsx @@ -0,0 +1,76 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; + +export default function FamilySwitcher() { + const router = useRouter(); + const [families, setFamilies] = useState([]); + const [loading, setLoading] = useState(true); + const [open, setOpen] = useState(false); + const [members, setMembers] = useState([]); + + useEffect(() => { + fetchFamilies(); + }, []); + + const fetchFamilies = async () => { + try { + // TODO: Get families from API with current user session + // For now, show single family + setFamilies([{ id: "default", name: "Our Family" }]); + setLoading(false); + } catch (err) { + console.error("Failed to fetch families:", err); + setLoading(false); + } + }; + + const fetchMembers = async (familyId: string) => { + try { + const res = await fetch(`/api/family/members?familyId=${familyId}`); + const data = await res.json(); + setMembers(data.members || []); + } catch (err) { + console.error("Failed to fetch members:", err); + } + }; + + const handleFamilySelect = (familyId: string) => { + setOpen(false); + // TODO: Switch to selected family + router.refresh(); + }; + + if (loading) return null; + + if (families.length <= 1) return null; // Don't show if only one family + + return ( +
+ + + {open && ( +
+ {families.map((family) => ( + + ))} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/app/api/family/members/route.ts b/src/app/api/family/members/route.ts new file mode 100644 index 0000000..1baa5e8 --- /dev/null +++ b/src/app/api/family/members/route.ts @@ -0,0 +1,64 @@ +import { NextResponse } from "next/server"; +import { sql } from "@/db"; + +// GET - list family members +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const familyId = searchParams.get("familyId") || "default"; + + try { + const members = await sql.unsafe( + `SELECT fm.id, fm.user_id as "userId", fm.role, fm.display_name as "displayName", fm.created_at as "createdAt", + u.name, u.email + FROM family_members fm + LEFT JOIN users u ON u.id = fm.user_id + WHERE fm.family_id = $1 + ORDER BY fm.created_at`, + [familyId] + ); + return NextResponse.json({ members: members || [] }); + } catch (error) { + console.error(error); + return NextResponse.json({ error: String(error) }, { status: 500 }); + } +} + +// DELETE - remove member +export async function DELETE(request: Request) { + const { searchParams } = new URL(request.url); + const memberId = searchParams.get("id"); + + if (!memberId) { + return NextResponse.json({ error: "Member ID required" }, { status: 400 }); + } + + try { + await sql.unsafe(`DELETE FROM family_members WHERE id = $1`, [memberId]); + return NextResponse.json({ success: true }); + } catch (error) { + console.error(error); + return NextResponse.json({ error: String(error) }, { status: 500 }); + } +} + +// PATCH - update member role +export async function PATCH(request: Request) { + try { + const body = await request.json(); + const { memberId, role, displayName } = body; + + if (!memberId) { + return NextResponse.json({ error: "Member ID required" }, { status: 400 }); + } + + await sql.unsafe( + `UPDATE family_members SET role = $1, display_name = $2 WHERE id = $3`, + [role || "caregiver", displayName, memberId] + ); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error(error); + return NextResponse.json({ error: String(error) }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/family/route.ts b/src/app/api/family/route.ts new file mode 100644 index 0000000..aacf145 --- /dev/null +++ b/src/app/api/family/route.ts @@ -0,0 +1,46 @@ +import { NextResponse } from "next/server"; +import { sql } from "@/db"; + +// GET - get family details +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const familyId = searchParams.get("familyId") || "default"; + + try { + const family = await sql.unsafe( + `SELECT id, name, pediatrician_phone, tier, max_children, max_members FROM families WHERE id = $1`, + [familyId] + ); + + if (!family || family.length === 0) { + return NextResponse.json({ error: "Family not found" }, { status: 404 }); + } + + return NextResponse.json({ family: family[0] }); + } catch (error) { + console.error(error); + return NextResponse.json({ error: String(error) }, { status: 500 }); + } +} + +// PATCH - update family +export async function PATCH(request: Request) { + try { + const body = await request.json(); + const { familyId, name, pediatricianPhone, tier } = body; + + if (!familyId) { + return NextResponse.json({ error: "Family ID required" }, { status: 400 }); + } + + await sql.unsafe( + `UPDATE families SET name = COALESCE($1, name), pediatrician_phone = COALESCE($2, pediatrician_phone), tier = COALESCE($3, tier), updated_at = NOW() WHERE id = $4`, + [name, pediatricianPhone, tier, familyId] + ); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error(error); + return NextResponse.json({ error: String(error) }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index 7e2f51f..5f5a7d7 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -6,6 +6,15 @@ import { useRouter } from "next/navigation"; import { useTheme } from "../ThemeProvider"; import { useFamily } from "../FamilyProvider"; +interface Member { + id: string; + userId: string; + role: string; + displayName: string; + name: string; + email: string; +} + interface Invite { id: string; email: string; @@ -17,14 +26,19 @@ interface Invite { export default function SettingsPage() { const router = useRouter(); const { theme, mode, setMode } = useTheme(); - const { tier, memberCount } = useFamily(); + const { tier, memberCount, familyId } = useFamily(); const [themeOpen, setThemeOpen] = useState(false); const [inviteOpen, setInviteOpen] = useState(false); + const [membersOpen, setMembersOpen] = useState(false); + const [familyOpen, setFamilyOpen] = useState(false); + const [members, setMembers] = useState([]); const [invites, setInvites] = useState([]); const [showAddInvite, setShowAddInvite] = useState(false); const [inviteEmail, setInviteEmail] = useState(""); const [inviteRole, setInviteRole] = useState("caregiver"); const [inviteLoading, setInviteLoading] = useState(false); + const [familyName, setFamilyName] = useState(""); + const [pediatricianPhone, setPediatricianPhone] = useState(""); // Check if can invite more members const canInvite = tier === "pro" || memberCount < 2; @@ -38,7 +52,18 @@ export default function SettingsPage() { useEffect(() => { fetchInvites(); - }, []); + fetchMembers(); + }, [familyId]); + + const fetchMembers = async () => { + try { + const res = await fetch(`/api/family/members?familyId=${familyId || "default"}`); + const data = await res.json(); + setMembers(data.members || []); + } catch (err) { + console.error("Failed to fetch members:", err); + } + }; const fetchInvites = async () => { try { @@ -103,7 +128,44 @@ export default function SettingsPage() { - {/* Family */} + {/* Family - Collapsible */} +
+ + + {familyOpen && ( +
+ setFamilyName(e.target.value)} + placeholder="Family name" + className="w-full p-2 border rounded-lg text-sm" + /> + setPediatricianPhone(e.target.value)} + placeholder="Pediatrician phone" + className="w-full p-2 border rounded-lg text-sm" + /> +
+ Plan: {tier} + {tier === "free" && ( + + )} +
+
+ )} +
👨‍👩‍👧 @@ -181,6 +243,43 @@ export default function SettingsPage() { )}
+ {/* Family Members - Collapsible */} +
+ + + {membersOpen && ( +
+ {members.length > 0 ? ( +
+ {members.map((member) => ( +
+
+
{member.displayName || member.name}
+
{member.email}
+
+ + {member.role} + +
+ ))} +
+ ) : ( +

No members yet

+ )} +
+ )} +
+ {/* Theme - Collapsible */}