- Orphaned families (0 members) now visible with amber badge in /admin/families with filter tab and explicit "Delete Family + Data" cascade delete button - Delete confirmation shows exact counts: children, logs, memories - Delete child button added to /admin/children with count confirmation - New DELETE /api/admin/families/[id] — full cascade delete (children, feeds, diapers_logs, sleeps, vaccinations, growth, memories, chat, etc.) - New GET/DELETE /api/admin/children/[id] — cascade child delete with counts - Extended families GET to include logCount + memoryCount per family - New /api/admin/engagement — feature adoption %, per-family engagement table, AI usage stats (30d), daily activity chart using correct table names - /admin/analytics fully redesigned: adoption funnel bars, per-family engagement table (sortable, filterable by activity), AI cost tab with INR breakdown - Fixes wrong table names in old analytics (activity_logs, growth_records → real tables) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
169 lines
No EOL
5.5 KiB
TypeScript
169 lines
No EOL
5.5 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { sql } from "@/db";
|
|
import { requireAdmin } from "@/lib/admin-auth";
|
|
|
|
// GET all families with members
|
|
export async function GET(request: Request) {
|
|
const auth = await requireAdmin(request);
|
|
if (!auth.success) return NextResponse.json({ error: auth.error }, { status: auth.status });
|
|
|
|
try {
|
|
const families = await sql`
|
|
SELECT
|
|
f.id,
|
|
f.name,
|
|
f.tier,
|
|
f.max_children,
|
|
f.max_members,
|
|
f.created_at,
|
|
COUNT(DISTINCT fm.user_id) as user_count,
|
|
COUNT(DISTINCT c.id) as child_count,
|
|
COUNT(DISTINCT fd.id) + COUNT(DISTINCT dl.id) + COUNT(DISTINCT sl.id) as log_count,
|
|
COUNT(DISTINCT mem.id) as memory_count
|
|
FROM families f
|
|
LEFT JOIN family_members fm ON fm.family_id = f.id
|
|
LEFT JOIN children c ON c.family_id = f.id
|
|
LEFT JOIN feeds fd ON fd.child_id = c.id
|
|
LEFT JOIN diapers_logs dl ON dl.child_id = c.id
|
|
LEFT JOIN sleeps sl ON sl.child_id = c.id
|
|
LEFT JOIN memories mem ON mem.family_id = f.id
|
|
GROUP BY f.id
|
|
ORDER BY f.created_at DESC
|
|
`;
|
|
|
|
const familyIds = families.map((f: any) => f.id);
|
|
let members: any[] = [];
|
|
if (familyIds.length > 0) {
|
|
members = await sql`
|
|
SELECT fm.id, fm.family_id, fm.user_id, fm.role, u.email
|
|
FROM family_members fm
|
|
JOIN users u ON u.id = fm.user_id
|
|
WHERE fm.family_id = ANY(${familyIds})
|
|
`;
|
|
}
|
|
|
|
const memberMap = new Map();
|
|
(members || []).forEach((m: any) => {
|
|
if (!memberMap.has(m.family_id)) memberMap.set(m.family_id, []);
|
|
memberMap.get(m.family_id).push({
|
|
id: m.id,
|
|
userId: m.user_id,
|
|
email: m.email,
|
|
role: m.role,
|
|
displayName: m.email,
|
|
});
|
|
});
|
|
|
|
return NextResponse.json({
|
|
families: families.map((f: any) => ({
|
|
id: f.id,
|
|
name: f.name,
|
|
tier: f.tier || "free",
|
|
maxChildren: f.max_children || 1,
|
|
maxMembers: f.max_members || 2,
|
|
createdAt: f.created_at ? new Date(f.created_at).toISOString() : null,
|
|
userCount: Number(f.user_count) || 0,
|
|
childCount: Number(f.child_count) || 0,
|
|
logCount: Number(f.log_count) || 0,
|
|
memoryCount: Number(f.memory_count) || 0,
|
|
members: memberMap.get(f.id) || [],
|
|
})),
|
|
});
|
|
} catch (error) {
|
|
console.error("Admin families error:", error);
|
|
return NextResponse.json({ error: String(error) }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
// Update family tier
|
|
export async function PATCH(request: Request) {
|
|
const auth = await requireAdmin(request);
|
|
if (!auth.success) return NextResponse.json({ error: auth.error }, { status: auth.status });
|
|
|
|
try {
|
|
const body = await request.json();
|
|
const { familyId, tier, maxChildren, maxMembers } = body;
|
|
|
|
if (!familyId) {
|
|
return NextResponse.json({ error: "familyId required" }, { status: 400 });
|
|
}
|
|
|
|
await sql`
|
|
UPDATE families
|
|
SET tier = COALESCE(${tier}, tier),
|
|
max_children = COALESCE(${maxChildren}, max_children),
|
|
max_members = COALESCE(${maxMembers}, max_members)
|
|
WHERE id = ${familyId}
|
|
`;
|
|
|
|
return NextResponse.json({ success: true });
|
|
} catch (error) {
|
|
console.error("Admin families error:", error);
|
|
return NextResponse.json({ error: String(error) }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
// Add member to family or create family
|
|
export async function POST(request: Request) {
|
|
const auth = await requireAdmin(request);
|
|
if (!auth.success) return NextResponse.json({ error: auth.error }, { status: auth.status });
|
|
|
|
try {
|
|
const body = await request.json();
|
|
const { familyId, email, role, displayName, name } = body;
|
|
|
|
if (name && !familyId) {
|
|
const newFamilyId = crypto.randomUUID();
|
|
await sql`
|
|
INSERT INTO families (id, name, tier, max_children, max_members, created_at, updated_at)
|
|
VALUES (${newFamilyId}, ${name}, 'free', 1, 2, NOW(), NOW())
|
|
`;
|
|
return NextResponse.json({ success: true, familyId: newFamilyId });
|
|
}
|
|
|
|
if (!familyId || !email) {
|
|
return NextResponse.json({ error: "familyId and email required" }, { status: 400 });
|
|
}
|
|
|
|
let users = await sql`SELECT id FROM users WHERE email = ${email}`;
|
|
let userId = users?.[0]?.id;
|
|
|
|
if (!userId) {
|
|
userId = crypto.randomUUID();
|
|
await sql`INSERT INTO users (id, email, created_at, updated_at) VALUES (${userId}, ${email}, NOW(), NOW())`;
|
|
}
|
|
|
|
await sql`
|
|
INSERT INTO family_members (id, family_id, user_id, role, display_name, created_at)
|
|
VALUES (${crypto.randomUUID()}, ${familyId}, ${userId}, ${role || 'caregiver'}, ${displayName || email}, NOW())
|
|
ON CONFLICT DO NOTHING
|
|
`;
|
|
|
|
return NextResponse.json({ success: true });
|
|
} catch (error) {
|
|
console.error("Admin add member error:", error);
|
|
return NextResponse.json({ error: String(error) }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
// Remove member from family
|
|
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");
|
|
|
|
if (!memberId) {
|
|
return NextResponse.json({ error: "memberId required" }, { status: 400 });
|
|
}
|
|
|
|
await sql`DELETE FROM family_members WHERE id = ${memberId}`;
|
|
|
|
return NextResponse.json({ success: true });
|
|
} catch (error) {
|
|
console.error("Admin remove member error:", error);
|
|
return NextResponse.json({ error: String(error) }, { status: 500 });
|
|
}
|
|
} |