"use client"; import { ArrowDown, ArrowUp, ArrowUpDown, Ban, ChevronLeft, ChevronRight, KeyRound, MoreHorizontal, Pencil, Plus, Search, ShieldCheck, ShieldOff, Trash2, UserCheck, } from "lucide-react"; import { createContext, use, useState } from "react"; import { toast } from "sonner"; import { columnFilteringFeature, createColumnHelper, createFilteredRowModel, createPaginatedRowModel, createSortedRowModel, globalFilteringFeature, rowPaginationFeature, rowSortingFeature, tableFeatures, useTable, type SortFn, } from "@tanstack/react-table"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { tr } from "@/i18n/tr"; import { fail, type ActionResult } from "@/lib/action-result"; import { cn } from "@/lib/utils"; import { formatDate } from "@/shared/format"; import type { SystemRole } from "@/shared/schemas/common"; import type { UserCreate, UserUpdate } from "@/shared/schemas/user"; import { compareTr, includesTr } from "@/shared/search"; import { banUserAction, createUserAction, removeUserAction, setPasswordAction, setRoleAction, unbanUserAction, updateUserAction, } from "../actions"; import { checkAdminAction, type AdminAction, type GuardViolation } from "../guards"; import type { AdminUser } from "../types"; import { BanUserDialog, DeleteUserDialog, SetPasswordDialog } from "./user-action-dialogs"; import { UserFormDialog } from "./user-form-dialog"; const t = tr.adminUsers; // --- Tablo yapılandırması (modül seviyesinde sabit: v9 kararlı girdi ister) --- const features = tableFeatures({ rowSortingFeature, sortedRowModel: createSortedRowModel(), columnFilteringFeature, globalFilteringFeature, filteredRowModel: createFilteredRowModel(), rowPaginationFeature, paginatedRowModel: createPaginatedRowModel(), }); const byTr: SortFn = (a, b, id) => compareTr(String(a.getValue(id) ?? ""), String(b.getValue(id) ?? "")); const helper = createColumnHelper(); /** Mobilde gizlenen sütunlar. */ const COLUMN_CLASS: Partial> = { username: "hidden sm:table-cell", email: "hidden lg:table-cell", createdAt: "hidden md:table-cell", actions: "w-12 text-right", }; const PAGE_SIZES = [10, 20, 50] as const; type DialogState = { kind: "create" } | { kind: "edit" | "password" | "ban" | "delete"; user: AdminUser } | null; function SortableHeader({ label, sorted, onToggle, }: { label: string; sorted: false | "asc" | "desc"; onToggle: ((event: unknown) => void) | undefined; }) { const Icon = sorted === "asc" ? ArrowUp : sorted === "desc" ? ArrowDown : ArrowUpDown; return ( ); } type RowActionsApi = { currentUserId: string; guard: (action: AdminAction, target: AdminUser) => GuardViolation | null; guardMessage: (v: GuardViolation) => string; setDialog: (d: DialogState) => void; setRole: (user: AdminUser, role: SystemRole) => void; unbanUser: (user: AdminUser) => void; }; /** Satır aksiyonları güncel durumu context'ten okur; sütunlar modül seviyesinde sabit kalır. */ const RowActionsContext = createContext(null); function useRowActions() { const ctx = use(RowActionsContext); if (!ctx) throw new Error("RowActionsContext eksik"); return ctx; } function NameCell({ user }: { user: AdminUser }) { const { currentUserId } = useRowActions(); return (
{user.name} {user.id === currentUserId ? ( ({t.you}) ) : null} @{user.username}
); } function RowActions({ user }: { user: AdminUser }) { const { guard, guardMessage, setDialog, setRole, unbanUser } = useRowActions(); const del = guard({ type: "delete" }, user); const ban = guard({ type: "ban" }, user); const toggleRole: SystemRole = user.role === "admin" ? "user" : "admin"; const role = guard({ type: "setRole", role: toggleRole }, user); const blocked = del ?? role; return ( // modal={false}: menüden açılan Dialog kapandıktan sonra sayfanın kilitli kalmasını önler. {user.name} setDialog({ kind: "edit", user })}> {t.actions.edit} setRole(user, toggleRole)}> {toggleRole === "admin" ? : } {toggleRole === "admin" ? t.actions.makeAdmin : t.actions.makeUser} setDialog({ kind: "password", user })}> {t.actions.resetPassword} {user.banned ? ( unbanUser(user)}> {t.actions.unban} ) : ( setDialog({ kind: "ban", user })} > {t.actions.ban} )} setDialog({ kind: "delete", user })} > {t.actions.delete} {blocked ? (

{guardMessage(blocked)}

) : null}
); } const columns = helper.columns([ helper.accessor("name", { header: ({ column }) => ( ), sortFn: byTr, cell: ({ row }) => , }), helper.accessor("username", { header: ({ column }) => ( ), sortFn: byTr, cell: ({ getValue }) => @{getValue()}, }), helper.accessor((u) => u.email ?? "", { id: "email", header: ({ column }) => ( ), sortFn: byTr, cell: ({ getValue }) => getValue() || {t.noEmail}, }), helper.accessor("role", { header: ({ column }) => ( ), cell: ({ getValue }) => ( {tr.roles[getValue()]} ), }), helper.accessor("banned", { header: ({ column }) => ( ), cell: ({ row }) => row.original.banned ? ( {t.status.banned} ) : ( {t.status.active} ), }), helper.accessor("createdAt", { header: ({ column }) => ( ), cell: ({ getValue }) => {formatDate(getValue())}, }), helper.display({ id: "actions", header: () => {tr.common.actions}, cell: ({ row }) => , }), ]); export function UsersTable({ users, currentUserId, }: { users: AdminUser[]; currentUserId: string; }) { const [dialog, setDialog] = useState(null); // Arayüzdeki kural kontrolü yalnızca erken geri bildirim içindir; asıl kontrol sunucuda. const adminCount = users.filter((u) => u.role === "admin").length; const guard = (action: AdminAction, target: AdminUser): GuardViolation | null => checkAdminAction(action, { actorId: currentUserId, target, adminCount }); const guardMessage = (v: GuardViolation) => (v === "self" ? t.guards.self : t.guards.lastAdmin); /** Server Action'ı çağırır; başarıda bildirim gösterir. Sayfa revalidatePath ile tazelenir. */ async function run(promise: Promise, success: string): Promise { try { const result = await promise; if (result.ok) toast.success(success); return result; } catch { return fail(tr.common.genericError); } } const createUser = (values: UserCreate) => run(createUserAction(values), t.toast.created(values.name)); const updateUser = (id: string, values: UserUpdate) => run(updateUserAction(id, values), t.toast.updated(values.name)); async function setRole(user: AdminUser, role: SystemRole) { const result = await run(setRoleAction(user.id, role), t.toast.roleChanged(user.name)); if (!result.ok) toast.error(result.error); } const setPassword = (user: AdminUser, values: { password: string }) => run(setPasswordAction(user.id, values), t.toast.passwordSet(user.name)); const banUser = (user: AdminUser, values: { reason?: string }) => run(banUserAction(user.id, values), t.toast.banned(user.name)); async function unbanUser(user: AdminUser) { const result = await run(unbanUserAction(user.id), t.toast.unbanned(user.name)); if (!result.ok) toast.error(result.error); } const removeUser = (user: AdminUser) => run(removeUserAction(user.id), t.toast.removed(user.name)); const table = useTable({ features, columns, data: users, // Türkçe, büyük/küçük harf ve aksan duyarsız arama (ad, kullanıcı adı, e-posta). globalFilterFn: (row, _columnId, filterValue) => { const q = String(filterValue ?? ""); const u = row.original; return includesTr(u.name, q) || includesTr(u.username, q) || includesTr(u.email, q); }, getColumnCanGlobalFilter: (column) => column.id !== "actions", initialState: { sorting: [{ id: "createdAt", desc: true }], pagination: { pageIndex: 0, pageSize: 10 }, }, }); const globalFilter = String(table.state.globalFilter ?? ""); const pageCount = Math.max(table.getPageCount(), 1); const filteredCount = table.getFilteredRowModel().rows.length; const selected = dialog && dialog.kind !== "create" ? dialog.user : undefined; const close = (open: boolean) => !open && setDialog(null); const rowActions: RowActionsApi = { currentUserId, guard, guardMessage, setDialog, setRole, unbanUser, }; return (
table.setGlobalFilter(e.target.value)} placeholder={t.searchPlaceholder} aria-label={tr.common.search} />
{table.getHeaderGroups().map((group) => ( {group.headers.map((header) => ( {header.isPlaceholder ? null : } ))} ))} {table.getRowModel().rows.length ? ( table.getRowModel().rows.map((row) => ( {row.getAllCells().map((cell) => ( ))} )) ) : ( {tr.common.noResults} )}
{tr.common.totalRows(filteredCount)}
{tr.common.rowsPerPage} {tr.common.page(table.state.pagination.pageIndex + 1, pageCount)}
); }