tia/src/app/admin/families/page.tsx
Mannu 7189fc766c refactor(ui): apply design system components across all pages
Replace raw <input>/<button>/<select> elements with Button, Card, Input,
Select, Modal, Badge, and ConfirmDialog from @/components/ui in all
non-admin and admin pages. Removes ~550 lines of inline Tailwind utility
classes from form elements while keeping all business logic intact.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 10:24:43 +05:30

239 lines
8.2 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { Button, Input, Select, Badge } from "@/components/ui";
interface Member {
id: string;
userId: string;
email: string;
role: string;
displayName: string;
}
interface Family {
id: string;
name: string;
tier: string;
maxChildren: number;
maxMembers: number;
createdAt: string;
userCount: number;
childCount: number;
members: Member[];
}
export default function AdminFamilies() {
const [families, setFamilies] = useState<Family[]>([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState("");
const [tierFilter, setTierFilter] = useState("all");
const [showMembers, setShowMembers] = useState<string | null>(null);
const [addMember, setAddMember] = useState<{familyId: string; email: string; role: string; name: string} | null>(null);
useEffect(() => {
fetchFamilies();
}, []);
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);
}
setFamilies(data.families || []);
} catch (err) {
console.error("Failed to fetch families:", err);
}
setLoading(false);
};
const handleCreateFamily = async () => {
const name = prompt("Family name:");
if (!name) return;
try {
const res = await fetch("/api/admin/families", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ name }),
});
if (res.ok) fetchFamilies();
} catch (err) {
console.error("Failed to create:", err);
}
};
const handleAddMember = async () => {
if (!addMember?.email || !addMember?.familyId) return;
try {
const res = await fetch("/api/admin/families", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify(addMember),
});
if (res.ok) {
fetchFamilies();
setAddMember(null);
}
} catch (err) {
console.error("Failed to add member:", err);
}
};
const handleRemoveMember = async (memberId: string) => {
if (!confirm("Remove this member?")) return;
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 filteredFamilies = families.filter((f) => {
const matchesSearch = f.name.toLowerCase().includes(search.toLowerCase());
const matchesTier = tierFilter === "all" || f.tier === tierFilter;
return matchesSearch && matchesTier;
});
const exportCSV = () => {
const headers = ["Name", "Tier", "Users", "Children", "Max Children", "Max Members", "Created"];
const rows = filteredFamilies.map((f) => [
f.name,
f.tier,
f.userCount,
f.childCount,
f.maxChildren,
f.maxMembers,
f.createdAt,
]);
const csv = [headers, ...rows].map((row) => row.join(",")).join("\n");
const blob = new Blob([csv], { type: "text/csv" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "families.csv";
a.click();
};
if (loading) {
return (
<div className="p-6 text-white">Loading...</div>
);
}
return (
<div className="p-6 space-y-4">
<div className="flex justify-between items-center">
<div>
<h1 className="text-2xl font-bold">Families</h1>
<p className="text-gray-400">{families.length} total families</p>
</div>
<div className="flex gap-2">
<Button onClick={handleCreateFamily}>+ New Family</Button>
<Button variant="secondary" onClick={exportCSV}>Export CSV</Button>
</div>
</div>
{/* Filters */}
<div className="flex gap-4">
<Input
type="text"
placeholder="Search families..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="flex-1"
/>
<Select
value={tierFilter}
onChange={(e) => setTierFilter(e.target.value)}
>
<option value="all">All Tiers</option>
<option value="free">Free</option>
<option value="pro">Pro</option>
</Select>
</div>
{/* Table */}
<div className="bg-gray-800 rounded-xl overflow-hidden">
<table className="w-full">
<thead className="bg-gray-700">
<tr>
<th className="px-4 py-3 text-left text-sm font-medium">Family</th>
<th className="px-4 py-3 text-left text-sm font-medium">Tier</th>
<th className="px-4 py-3 text-left text-sm font-medium">Users</th>
<th className="px-4 py-3 text-left text-sm font-medium">Children</th>
<th className="px-4 py-3 text-left text-sm font-medium">Limits</th>
<th className="px-4 py-3 text-left text-sm font-medium">Created</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-700">
{filteredFamilies.map((family) => (
<>
<tr key={family.id} className="hover:bg-gray-750">
<td className="px-4 py-3">
<div className="font-medium">{family.name}</div>
<div className="text-xs text-gray-500">{family.id}</div>
</td>
<td className="px-4 py-3">
<Badge variant={family.tier === "pro" ? "rose" : "default"}>{family.tier}</Badge>
</td>
<td className="px-4 py-3">
<Button variant="ghost" size="sm" onClick={() => setShowMembers(showMembers === family.id ? null : family.id)}>
{family.userCount} users
</Button>
</td>
<td className="px-4 py-3">{family.childCount}</td>
<td className="px-4 py-3 text-sm text-gray-400">
{family.maxChildren} kids, {family.maxMembers} members
</td>
<td className="px-4 py-3 text-sm text-gray-400">
{family.createdAt?.slice(0, 10)}
</td>
</tr>
{showMembers === family.id && (
<tr>
<td colSpan={6} className="bg-gray-800 p-4">
<div className="space-y-2">
<div className="flex gap-2 items-center">
<Input
type="email"
placeholder="New member email"
onChange={(e) => setAddMember({familyId: family.id, email: e.target.value, role: "caregiver", name: ""})}
className="flex-1"
/>
<Select
onChange={(e) => setAddMember(addMember ? {...addMember, role: e.target.value} : null)}
>
<option value="caregiver">Caregiver</option>
<option value="owner">Owner</option>
</Select>
<Button size="sm" onClick={handleAddMember}>Add</Button>
</div>
{(family.members || []).map((m) => (
<div key={m.id} className="flex justify-between items-center bg-gray-700 p-2 rounded">
<span>{m.email} ({m.role})</span>
<Button variant="danger" size="sm" onClick={() => handleRemoveMember(m.id)}>Remove</Button>
</div>
))}
</div>
</td>
</tr>
)}
</>
))}
</tbody>
</table>
{filteredFamilies.length === 0 && (
<div className="p-8 text-center text-gray-500">No families found</div>
)}
</div>
</div>
);
}