149 lines
5.5 KiB
TypeScript
149 lines
5.5 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { toast } from "sonner";
|
|
import { KeyRound, Mail, MoreVertical, Trash2 } from "lucide-react";
|
|
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu";
|
|
import { ConfirmDeleteDialog } from "@/components/confirm-delete-dialog";
|
|
import { UserRoleMenu } from "@/components/admin/user-role-menu";
|
|
import { EditUserEmailDialog } from "@/components/admin/edit-user-email-dialog";
|
|
import { ChangePasswordDialog } from "@/components/admin/change-password-dialog";
|
|
import { deleteUser } from "@/lib/actions/admin";
|
|
import { Role } from "@/lib/generated/prisma/enums";
|
|
|
|
export interface AdminUserRow {
|
|
id: string;
|
|
name: string | null;
|
|
email: string;
|
|
role: Role;
|
|
createdAtLabel: string;
|
|
}
|
|
|
|
export function AdminUserTable({
|
|
initialUsers,
|
|
currentUserId,
|
|
}: {
|
|
initialUsers: AdminUserRow[];
|
|
currentUserId: string;
|
|
}) {
|
|
const [users, setUsers] = useState(initialUsers);
|
|
const [emailDialogFor, setEmailDialogFor] = useState<AdminUserRow | null>(null);
|
|
const [passwordDialogFor, setPasswordDialogFor] = useState<AdminUserRow | null>(null);
|
|
const [deleteDialogFor, setDeleteDialogFor] = useState<AdminUserRow | null>(null);
|
|
|
|
async function handleDelete(userId: string) {
|
|
const result = await deleteUser(userId);
|
|
if (result?.error) {
|
|
toast.error(result.error);
|
|
return;
|
|
}
|
|
setUsers((prev) => prev.filter((u) => u.id !== userId));
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<div className="max-w-3xl overflow-hidden rounded-xl border">
|
|
<table className="w-full text-sm">
|
|
<thead className="bg-muted/40 text-left text-muted-foreground">
|
|
<tr>
|
|
<th className="px-4 py-2 font-medium">Name</th>
|
|
<th className="px-4 py-2 font-medium">Email</th>
|
|
<th className="px-4 py-2 font-medium">Role</th>
|
|
<th className="px-4 py-2 font-medium">Joined</th>
|
|
<th className="px-4 py-2 font-medium">
|
|
<span className="sr-only">Actions</span>
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y">
|
|
{users.map((user) => {
|
|
const isSelf = user.id === currentUserId;
|
|
return (
|
|
<tr key={user.id}>
|
|
<td className="px-4 py-2">{user.name || "—"}</td>
|
|
<td className="px-4 py-2 text-muted-foreground">{user.email}</td>
|
|
<td className="px-4 py-2">
|
|
<UserRoleMenu userId={user.id} role={user.role} />
|
|
</td>
|
|
<td className="px-4 py-2 text-muted-foreground">{user.createdAtLabel}</td>
|
|
<td className="px-4 py-2 text-right">
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger
|
|
render={
|
|
<button
|
|
className="flex size-7 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
|
aria-label={`More options for ${user.email}`}
|
|
>
|
|
<MoreVertical className="size-4" />
|
|
</button>
|
|
}
|
|
/>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem onClick={() => setEmailDialogFor(user)}>
|
|
<Mail className="size-4" />
|
|
Edit email
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => setPasswordDialogFor(user)}>
|
|
<KeyRound className="size-4" />
|
|
Change password
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem
|
|
variant="destructive"
|
|
disabled={isSelf}
|
|
onClick={() => setDeleteDialogFor(user)}
|
|
>
|
|
<Trash2 className="size-4" />
|
|
Delete account
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{emailDialogFor && (
|
|
<EditUserEmailDialog
|
|
userId={emailDialogFor.id}
|
|
currentEmail={emailDialogFor.email}
|
|
open={!!emailDialogFor}
|
|
onOpenChange={(open) => !open && setEmailDialogFor(null)}
|
|
onSaved={(email) =>
|
|
setUsers((prev) => prev.map((u) => (u.id === emailDialogFor.id ? { ...u, email } : u)))
|
|
}
|
|
/>
|
|
)}
|
|
|
|
{passwordDialogFor && (
|
|
<ChangePasswordDialog
|
|
userId={passwordDialogFor.id}
|
|
userLabel={passwordDialogFor.email}
|
|
open={!!passwordDialogFor}
|
|
onOpenChange={(open) => !open && setPasswordDialogFor(null)}
|
|
/>
|
|
)}
|
|
|
|
{deleteDialogFor && (
|
|
<ConfirmDeleteDialog
|
|
open={!!deleteDialogFor}
|
|
onOpenChange={(open) => !open && setDeleteDialogFor(null)}
|
|
title="Delete account?"
|
|
description={`Delete ${deleteDialogFor.email}? This permanently deletes their account and all of their categories, groups, and to-dos. This can't be undone.`}
|
|
onConfirm={() => handleDelete(deleteDialogFor.id)}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
}
|