diff --git a/src/app/admin/families/page.tsx b/src/app/admin/families/page.tsx index f04fd77..29e501c 100644 --- a/src/app/admin/families/page.tsx +++ b/src/app/admin/families/page.tsx @@ -29,77 +29,104 @@ export default function AdminFamilies() { const [search, setSearch] = useState(""); const [tierFilter, setTierFilter] = useState("all"); const [showMembers, setShowMembers] = useState(null); - const [addMember, setAddMember] = useState<{ familyId: string; email: string; role: string; name: string } | null>(null); const [tierChanging, setTierChanging] = useState(null); + const [removingMemberId, setRemovingMemberId] = useState(null); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(null); + + // New family form + const [showNewFamily, setShowNewFamily] = useState(false); + const [newFamilyName, setNewFamilyName] = useState(""); + const [creatingFamily, setCreatingFamily] = useState(false); + + // Add member form — separate email/role state to avoid lost-role bug + const [memberForms, setMemberForms] = useState>({}); useEffect(() => { fetchFamilies(); }, []); + const showMessage = (msg: string, type: "success" | "error") => { + if (type === "success") { setSuccess(msg); setTimeout(() => setSuccess(null), 3000); } + else { setError(msg); setTimeout(() => setError(null), 5000); } + }; + const fetchFamilies = async () => { try { const res = await fetch("/api/admin/families", { credentials: "include" }); const data = await res.json(); - if (data.error) console.error("API error:", data.error); + if (data.error) throw new Error(data.error); setFamilies(data.families || []); - } catch (err) { - console.error("Failed to fetch families:", err); + } catch (err: any) { + showMessage(err.message || "Failed to load families", "error"); } setLoading(false); }; const handleCreateFamily = async () => { - const name = prompt("Family name:"); - if (!name) return; + if (!newFamilyName.trim()) return; + setCreatingFamily(true); try { const res = await fetch("/api/admin/families", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", - body: JSON.stringify({ name }), + body: JSON.stringify({ name: newFamilyName.trim() }), }); - if (res.ok) fetchFamilies(); - } catch (err) { - console.error("Failed to create:", err); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || "Failed to create family"); + showMessage(`Family "${newFamilyName.trim()}" created`, "success"); + setNewFamilyName(""); + setShowNewFamily(false); + fetchFamilies(); + } catch (err: any) { + showMessage(err.message, "error"); } + setCreatingFamily(false); }; - const handleAddMember = async () => { - if (!addMember?.email || !addMember?.familyId) return; + const handleAddMember = async (familyId: string) => { + const form = memberForms[familyId]; + if (!form?.email) return; try { const res = await fetch("/api/admin/families", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", - body: JSON.stringify(addMember), + body: JSON.stringify({ familyId, email: form.email, role: form.role || "caregiver", displayName: form.email }), }); - if (res.ok) { - fetchFamilies(); - setAddMember(null); - } - } catch (err) { - console.error("Failed to add member:", err); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || "Failed to add member"); + showMessage("Member added", "success"); + setMemberForms(prev => ({ ...prev, [familyId]: { email: "", role: "caregiver" } })); + fetchFamilies(); + } catch (err: any) { + showMessage(err.message, "error"); } }; const handleRemoveMember = async (memberId: string) => { - if (!confirm("Remove this member?")) return; + if (!window.confirm("Remove this member from the family?")) return; + setRemovingMemberId(memberId); try { const res = await fetch(`/api/admin/families?memberId=${memberId}`, { method: "DELETE", credentials: "include", }); - if (res.ok) fetchFamilies(); - } catch (err) { - console.error("Failed to remove:", err); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || "Failed to remove member"); + showMessage("Member removed", "success"); + fetchFamilies(); + } catch (err: any) { + showMessage(err.message, "error"); } + setRemovingMemberId(null); }; - const handleTierChange = async (familyId: string, newTier: string) => { - const family = families.find(f => f.id === familyId); - if (!family) return; + const handleTierChange = async (familyId: string, currentTier: string) => { + const newTier = currentTier === "pro" ? "free" : "pro"; const label = newTier === "pro" ? "Upgrade to Pro?" : "Downgrade to Free?"; - if (!confirm(label)) return; + if (!window.confirm(label)) return; setTierChanging(familyId); try { const res = await fetch("/api/admin/families", { @@ -113,13 +140,23 @@ export default function AdminFamilies() { maxMembers: newTier === "pro" ? 10 : 2, }), }); - if (res.ok) fetchFamilies(); - } catch (err) { - console.error("Failed to update tier:", err); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || "Failed to update tier"); + showMessage(`Family moved to ${newTier}`, "success"); + fetchFamilies(); + } catch (err: any) { + showMessage(err.message, "error"); } setTierChanging(null); }; + const updateMemberForm = (familyId: string, field: "email" | "role", value: string) => { + setMemberForms(prev => { + const current = prev[familyId] || { email: "", role: "caregiver" }; + return { ...prev, [familyId]: { ...current, [field]: value } }; + }); + }; + const filteredFamilies = families.filter((f) => { const matchesSearch = f.name.toLowerCase().includes(search.toLowerCase()); const matchesTier = tierFilter === "all" || f.tier === tierFilter; @@ -143,10 +180,22 @@ export default function AdminFamilies() { if (loading) return
Loading...
; const proCount = families.filter(f => f.tier === "pro").length; - const freeCount = families.filter(f => f.tier === "free").length; + const freeCount = families.filter(f => f.tier !== "pro").length; return (
+ {error && ( +
+ ⚠ {error} + +
+ )} + {success && ( +
+ ✓ {success} +
+ )} +

Families

@@ -155,16 +204,39 @@ export default function AdminFamilies() {

- +
+ {/* New family inline form */} + {showNewFamily && ( +
+
+ + setNewFamilyName(e.target.value)} + onKeyDown={(e) => { if (e.key === "Enter") handleCreateFamily(); }} + autoFocus + /> +
+ + +
+ )} + {/* Filters */}
setSearch(e.target.value)} className="flex-1" @@ -191,95 +263,109 @@ export default function AdminFamilies() { - {filteredFamilies.map((family) => ( - <> - - -
{family.name}
-
{family.id.slice(0, 8)}…
- - -
+ {filteredFamilies.map((family) => { + const memberForm = memberForms[family.id] || { email: "", role: "caregiver" }; + return ( + <> + + +
{family.name}
+
{family.id.slice(0, 8)}…
+ + {family.tier} -
- - - - - {family.childCount} - - {family.maxChildren} kids · {family.maxMembers} members - - - {family.createdAt?.slice(0, 10)} - - - - - - {showMembers === family.id && ( - - -
-
Members
-
- - setAddMember({ familyId: family.id, email: e.target.value, role: "caregiver", name: "" }) - } - className="flex-1 max-w-xs" - /> - - -
-
- {(family.members || []).map((m) => ( -
-
- {m.email} - {m.role} -
- -
- ))} - {(family.members || []).length === 0 && ( -
No members yet
- )} -
-
+ + + + + {family.childCount} + + {family.maxChildren} kids · {family.maxMembers} members + + + {family.createdAt?.slice(0, 10)} + + + - )} - - ))} + + {showMembers === family.id && ( + + +
+
Members
+ + {/* Add member form */} +
+ updateMemberForm(family.id, "email", e.target.value)} + className="flex-1 max-w-xs" + onKeyDown={(e) => { if (e.key === "Enter") handleAddMember(family.id); }} + /> + + +
+ + {/* Member list */} +
+ {(family.members || []).map((m) => ( +
+
+ {m.email} + {m.role} +
+ +
+ ))} + {(family.members || []).length === 0 && ( +
No members yet
+ )} +
+
+ + + )} + + ); + })} {filteredFamilies.length === 0 && ( diff --git a/src/app/admin/users/page.tsx b/src/app/admin/users/page.tsx index 237c6f9..332703d 100644 --- a/src/app/admin/users/page.tsx +++ b/src/app/admin/users/page.tsx @@ -28,19 +28,28 @@ export default function AdminUsers() { const [showPassword, setShowPassword] = useState(null); const [passwordValue, setPasswordValue] = useState(""); const [newUser, setNewUser] = useState({ email: "", name: "", familyId: "", role: "caregiver" }); + const [deletingId, setDeletingId] = useState(null); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(null); useEffect(() => { fetchUsers(); fetchFamilies(); }, []); + const showMessage = (msg: string, type: "success" | "error") => { + if (type === "success") { setSuccess(msg); setTimeout(() => setSuccess(null), 3000); } + else { setError(msg); setTimeout(() => setError(null), 5000); } + }; + const fetchUsers = async () => { try { const res = await fetch("/api/admin/users", { credentials: "include" }); const data = await res.json(); + if (data.error) throw new Error(data.error); setUsers(data.users || []); - } catch (err) { - console.error("Failed to fetch users:", err); + } catch (err: any) { + showMessage(err.message || "Failed to load users", "error"); } setLoading(false); }; @@ -50,9 +59,7 @@ export default function AdminUsers() { const res = await fetch("/api/admin/families", { credentials: "include" }); const data = await res.json(); setFamilies(data.families || []); - } catch (err) { - console.error("Failed to fetch families:", err); - } + } catch {} }; const handleAddUser = async () => { @@ -64,28 +71,33 @@ export default function AdminUsers() { credentials: "include", body: JSON.stringify(newUser), }); - if (res.ok) { - fetchUsers(); - setShowAdd(false); - setNewUser({ email: "", name: "", familyId: "", role: "caregiver" }); - } - } catch (err) { - console.error("Failed to create user:", err); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || "Failed to create user"); + showMessage("User created", "success"); + setShowAdd(false); + setNewUser({ email: "", name: "", familyId: "", role: "caregiver" }); + fetchUsers(); + } catch (err: any) { + showMessage(err.message, "error"); } }; - const handleRemoveUser = async (userId: string, memberId?: string) => { - if (!confirm("Delete this user?")) return; + const handleDeleteUser = async (userId: string) => { + if (!window.confirm("Delete this user permanently? This cannot be undone.")) return; + setDeletingId(userId); try { - const params = memberId ? `memberId=${memberId}` : `userId=${userId}`; - const res = await fetch(`/api/admin/users?${params}`, { + const res = await fetch(`/api/admin/users?userId=${userId}`, { method: "DELETE", credentials: "include", }); - if (res.ok) fetchUsers(); - } catch (err) { - console.error("Failed to delete:", err); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || "Delete failed"); + showMessage("User deleted", "success"); + setUsers(prev => prev.filter(u => u.id !== userId)); + } catch (err: any) { + showMessage(err.message, "error"); } + setDeletingId(null); }; const handleSetPassword = async (userId: string, password: string) => { @@ -97,13 +109,14 @@ export default function AdminUsers() { credentials: "include", body: JSON.stringify({ userId, password }), }); - if (res.ok) { - fetchUsers(); - setShowPassword(null); - setPasswordValue(""); - } - } catch (err) { - console.error("Failed to set password:", err); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || "Failed to set password"); + showMessage("Password updated", "success"); + setShowPassword(null); + setPasswordValue(""); + fetchUsers(); + } catch (err: any) { + showMessage(err.message, "error"); } }; @@ -124,12 +137,23 @@ export default function AdminUsers() { a.click(); }; - if (loading) { - return
Loading...
; - } + if (loading) return
Loading...
; return (
+ {/* Toast notifications */} + {error && ( +
+ ⚠ {error} + +
+ )} + {success && ( +
+ ✓ {success} +
+ )} +

Users

@@ -142,36 +166,47 @@ export default function AdminUsers() {
{showAdd && ( -
- setNewUser({ ...newUser, email: e.target.value })} - className="flex-1 min-w-[200px]" - /> - setNewUser({ ...newUser, name: e.target.value })} - /> - - +
+
+ + setNewUser({ ...newUser, email: e.target.value })} + /> +
+
+ + setNewUser({ ...newUser, name: e.target.value })} + /> +
+
+ + +
+
+ + +
)} setSearch(e.target.value)} /> @@ -194,7 +229,7 @@ export default function AdminUsers() {
{user.name || user.email}
{user.email}
- {user.familyName || "-"} + {user.familyName || } @@ -228,22 +269,18 @@ export default function AdminUsers() {
setPasswordValue(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter" && showPassword) handleSetPassword(showPassword, passwordValue); + }} />
- -
diff --git a/src/app/api/admin/users/route.ts b/src/app/api/admin/users/route.ts index 8612c4f..fdd46ba 100644 --- a/src/app/api/admin/users/route.ts +++ b/src/app/api/admin/users/route.ts @@ -116,13 +116,12 @@ export async function PATCH(request: Request) { } } -// Remove user from family +// Remove user from family or delete entirely export async function DELETE(request: Request) { const auth = await requireAdmin(request); if (!auth.success) return NextResponse.json({ error: auth.error }, { status: auth.status }); try { - const { searchParams } = new URL(request.url); const memberId = searchParams.get("memberId"); const userId = searchParams.get("userId"); @@ -130,9 +129,15 @@ export async function DELETE(request: Request) { if (memberId) { await sql`DELETE FROM family_members WHERE id = ${memberId}`; } else if (userId) { - // Delete user entirely (careful!) + // Must clean up FK-constrained tables before deleting user + // (all have ON DELETE no action constraints) + await sql`DELETE FROM sessions WHERE user_id = ${userId}`; + await sql`DELETE FROM accounts WHERE user_id = ${userId}`; + await sql`UPDATE support_tickets SET user_id = NULL WHERE user_id = ${userId}`; await sql`DELETE FROM family_members WHERE user_id = ${userId}`; await sql`DELETE FROM users WHERE id = ${userId}`; + } else { + return NextResponse.json({ error: "memberId or userId required" }, { status: 400 }); } return NextResponse.json({ success: true });