Fix admin panels

- Families: add "New Family" button
- Users: add "Add User" form with family selector
- Add delete user option
- Include member_id for proper removal

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Manohar Gupta 2026-05-16 14:19:22 +05:30
parent 8867e66928
commit 2de47056e7
4 changed files with 222 additions and 13 deletions

View file

@ -47,6 +47,9 @@ export default function AdminFamilies() {
headers: { Authorization: `Bearer ${localStorage.getItem("admin_token")}` },
});
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);
@ -54,6 +57,24 @@ export default function AdminFamilies() {
setLoading(false);
};
const handleCreateFamily = async () => {
const name = prompt("Family name:");
if (!name) return;
try {
const res = await fetch("/api/admin/families", {
method: "POST",
headers: {
Authorization: `Bearer ${localStorage.getItem("admin_token")}`,
"Content-Type": "application/json",
},
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 {
@ -126,12 +147,20 @@ export default function AdminFamilies() {
<h1 className="text-2xl font-bold">Families</h1>
<p className="text-gray-400">{families.length} total families</p>
</div>
<button
onClick={exportCSV}
className="px-4 py-2 bg-emerald-600 hover:bg-emerald-700 rounded-lg"
>
Export CSV
</button>
<div className="flex gap-2">
<button
onClick={handleCreateFamily}
className="px-4 py-2 bg-rose-500 hover:bg-rose-600 rounded-lg"
>
+ New Family
</button>
<button
onClick={exportCSV}
className="px-4 py-2 bg-emerald-600 hover:bg-emerald-700 rounded-lg"
>
Export CSV
</button>
</div>
</div>
{/* Filters */}

View file

@ -10,13 +10,22 @@ interface User {
familyId: string;
familyName: string;
createdAt: string;
memberId?: string;
}
interface Family {
id: string;
name: string;
}
export default function AdminUsers() {
const router = useRouter();
const [users, setUsers] = useState<User[]>([]);
const [families, setFamilies] = useState<Family[]>([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState("");
const [showAdd, setShowAdd] = useState(false);
const [newUser, setNewUser] = useState({ email: "", name: "", familyId: "", role: "caregiver" });
useEffect(() => {
const token = localStorage.getItem("admin_token");
@ -25,6 +34,7 @@ export default function AdminUsers() {
return;
}
fetchUsers();
fetchFamilies();
}, []);
const fetchUsers = async () => {
@ -40,9 +50,56 @@ export default function AdminUsers() {
setLoading(false);
};
const fetchFamilies = async () => {
try {
const res = await fetch("/api/admin/families", {
headers: { Authorization: `Bearer ${localStorage.getItem("admin_token")}` },
});
const data = await res.json();
setFamilies(data.families || []);
} catch (err) {
console.error("Failed to fetch families:", err);
}
};
const handleAddUser = async () => {
if (!newUser.email) return;
try {
const res = await fetch("/api/admin/users", {
method: "POST",
headers: {
Authorization: `Bearer ${localStorage.getItem("admin_token")}`,
"Content-Type": "application/json",
},
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 handleRemoveUser = async (userId: string, memberId?: string) => {
if (!confirm("Delete this user?")) return;
try {
const params = memberId ? `memberId=${memberId}` : `userId=${userId}`;
const res = await fetch(`/api/admin/users?${params}`, {
method: "DELETE",
headers: { Authorization: `Bearer ${localStorage.getItem("admin_token")}` },
});
if (res.ok) fetchUsers();
} catch (err) {
console.error("Failed to delete:", err);
}
};
const filteredUsers = users.filter((u) =>
u.email.toLowerCase().includes(search.toLowerCase()) ||
u.name.toLowerCase().includes(search.toLowerCase())
(u.name || "").toLowerCase().includes(search.toLowerCase())
);
const exportCSV = () => {
@ -68,11 +125,41 @@ export default function AdminUsers() {
<h1 className="text-2xl font-bold">Users</h1>
<p className="text-gray-400">{users.length} total users</p>
</div>
<button onClick={exportCSV} className="px-4 py-2 bg-emerald-600 hover:bg-emerald-700 rounded-lg">
Export CSV
<button onClick={() => setShowAdd(!showAdd)} className="px-4 py-2 bg-rose-500 hover:bg-rose-600 rounded-lg">
+ Add User
</button>
</div>
{showAdd && (
<div className="bg-gray-800 p-4 rounded-lg flex gap-2 flex-wrap">
<input
type="email"
placeholder="Email"
value={newUser.email}
onChange={(e) => setNewUser({ ...newUser, email: e.target.value })}
className="flex-1 bg-gray-700 border border-gray-600 rounded px-3 py-2 text-white min-w-[200px]"
/>
<input
type="text"
placeholder="Name (optional)"
value={newUser.name}
onChange={(e) => setNewUser({ ...newUser, name: e.target.value })}
className="bg-gray-700 border border-gray-600 rounded px-3 py-2 text-white"
/>
<select
value={newUser.familyId}
onChange={(e) => setNewUser({ ...newUser, familyId: e.target.value })}
className="bg-gray-700 border border-gray-600 rounded px-3 py-2 text-white"
>
<option value="">Select Family</option>
{families.map((f) => (
<option key={f.id} value={f.id}>{f.name}</option>
))}
</select>
<button onClick={handleAddUser} className="px-4 py-2 bg-rose-500 rounded text-white">Create</button>
</div>
)}
<input
type="text"
placeholder="Search users..."
@ -88,6 +175,7 @@ export default function AdminUsers() {
<th className="px-4 py-3 text-left text-sm font-medium">User</th>
<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">Joined</th>
<th className="px-4 py-3 text-left text-sm font-medium">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-700">
@ -97,10 +185,18 @@ export default function AdminUsers() {
<div className="font-medium">{user.name || user.email}</div>
<div className="text-xs text-gray-500">{user.email}</div>
</td>
<td className="px-4 py-3">{user.familyName}</td>
<td className="px-4 py-3">{user.familyName || "-"}</td>
<td className="px-4 py-3 text-sm text-gray-400">
{user.createdAt?.slice(0, 10)}
</td>
<td className="px-4 py-3">
<button
onClick={() => handleRemoveUser(user.id, user.memberId)}
className="text-red-400 text-sm hover:underline"
>
Delete
</button>
</td>
</tr>
))}
</tbody>

View file

@ -100,7 +100,7 @@ export async function PATCH(request: Request) {
}
}
// Add member to family
// Add member to family or create family
export async function POST(request: Request) {
try {
const authHeader = request.headers.get("authorization");
@ -109,8 +109,19 @@ export async function POST(request: Request) {
}
const body = await request.json();
const { familyId, email, role, displayName } = body;
const { familyId, email, role, displayName, name } = body;
// Create new family
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 });
}
// Add member to existing family
if (!familyId || !email) {
return NextResponse.json({ error: "familyId and email required" }, { status: 400 });
}

View file

@ -1,6 +1,7 @@
import { NextResponse } from "next/server";
import { sql } from "@/db";
// GET all users
export async function GET(request: Request) {
try {
const authHeader = request.headers.get("authorization");
@ -8,13 +9,14 @@ export async function GET(request: Request) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// Get all users via family_members table with family info
// Get all users
const users = await sql`
SELECT
u.id,
u.email,
u.name,
fm.family_id,
fm.id as member_id,
f.name as family_name,
u.created_at
FROM users u
@ -29,6 +31,7 @@ export async function GET(request: Request) {
email: u.email,
name: u.name,
familyId: u.family_id,
memberId: u.member_id,
familyName: u.family_name,
createdAt: u.created_at ? new Date(u.created_at).toISOString() : null,
})),
@ -37,4 +40,74 @@ export async function GET(request: Request) {
console.error("Admin users error:", error);
return NextResponse.json({ error: String(error) }, { status: 500 });
}
}
// Create user or add to family
export async function POST(request: Request) {
try {
const authHeader = request.headers.get("authorization");
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const body = await request.json();
const { email, familyId, role, name } = body;
if (!email) {
return NextResponse.json({ error: "email required" }, { status: 400 });
}
// Check if user exists
let existing = await sql`SELECT id FROM users WHERE email = ${email}`;
let userId = existing?.[0]?.id;
if (!userId) {
userId = crypto.randomUUID();
await sql`
INSERT INTO users (id, email, name, created_at, updated_at)
VALUES (${userId}, ${email}, ${name || email}, NOW(), NOW())
`;
}
// Add to family if familyId provided
if (familyId) {
await sql`
INSERT INTO family_members (id, family_id, user_id, role, display_name, created_at)
VALUES (${crypto.randomUUID()}, ${familyId}, ${userId}, ${role || 'caregiver'}, ${name || email}, NOW())
ON CONFLICT DO NOTHING
`;
}
return NextResponse.json({ success: true, userId });
} catch (error) {
console.error("Admin users error:", error);
return NextResponse.json({ error: String(error) }, { status: 500 });
}
}
// Remove user from family
export async function DELETE(request: Request) {
try {
const authHeader = request.headers.get("authorization");
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { searchParams } = new URL(request.url);
const memberId = searchParams.get("memberId");
const userId = searchParams.get("userId");
if (memberId) {
await sql`DELETE FROM family_members WHERE id = ${memberId}`;
} else if (userId) {
// Delete user entirely (careful!)
await sql`DELETE FROM family_members WHERE user_id = ${userId}`;
await sql`DELETE FROM users WHERE id = ${userId}`;
}
return NextResponse.json({ success: true });
} catch (error) {
console.error("Admin delete error:", error);
return NextResponse.json({ error: String(error) }, { status: 500 });
}
}