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:
parent
8867e66928
commit
2de47056e7
4 changed files with 222 additions and 13 deletions
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue