242 lines
6.9 KiB
TypeScript
242 lines
6.9 KiB
TypeScript
|
|
import {
|
||
|
|
type ColumnDef,
|
||
|
|
flexRender,
|
||
|
|
getCoreRowModel,
|
||
|
|
useReactTable,
|
||
|
|
} from "@tanstack/react-table";
|
||
|
|
import {
|
||
|
|
Table,
|
||
|
|
TableHeader,
|
||
|
|
TableBody,
|
||
|
|
TableRow,
|
||
|
|
TableHead,
|
||
|
|
TableCell,
|
||
|
|
} from "@/components/ui/table";
|
||
|
|
import {
|
||
|
|
Card,
|
||
|
|
CardHeader,
|
||
|
|
CardTitle,
|
||
|
|
CardDescription,
|
||
|
|
CardContent,
|
||
|
|
} from "@/components/ui/card";
|
||
|
|
import { Button } from "@/components/ui/button";
|
||
|
|
import { type Audits } from "@/types/audit";
|
||
|
|
import { AuditFilterBar } from "@/components/filters/audit-filter-bar";
|
||
|
|
import { AuditDetailDialog } from "@/components/dialogs/audit-detail-dialog";
|
||
|
|
|
||
|
|
interface AuditListTemplateProps {
|
||
|
|
// data
|
||
|
|
items: Audits[];
|
||
|
|
total: number;
|
||
|
|
columns: ColumnDef<Audits>[];
|
||
|
|
isLoading: boolean;
|
||
|
|
isFetching: boolean;
|
||
|
|
|
||
|
|
// pagination
|
||
|
|
pageNumber: number;
|
||
|
|
pageSize: number;
|
||
|
|
pageCount: number;
|
||
|
|
onPreviousPage: () => void;
|
||
|
|
onNextPage: () => void;
|
||
|
|
canPreviousPage: boolean;
|
||
|
|
canNextPage: boolean;
|
||
|
|
|
||
|
|
// filter
|
||
|
|
username: string | null;
|
||
|
|
action: string | null;
|
||
|
|
from: string | null;
|
||
|
|
to: string | null;
|
||
|
|
onUsernameChange: (v: string | null) => void;
|
||
|
|
onActionChange: (v: string | null) => void;
|
||
|
|
onFromChange: (v: string | null) => void;
|
||
|
|
onToChange: (v: string | null) => void;
|
||
|
|
onSearch: () => void;
|
||
|
|
onReset: () => void;
|
||
|
|
|
||
|
|
// detail dialog
|
||
|
|
selectedAudit: Audits | null;
|
||
|
|
onRowClick: (audit: Audits) => void;
|
||
|
|
onDialogClose: () => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function AuditListTemplate({
|
||
|
|
items,
|
||
|
|
total,
|
||
|
|
columns,
|
||
|
|
isLoading,
|
||
|
|
isFetching,
|
||
|
|
pageNumber,
|
||
|
|
pageSize,
|
||
|
|
pageCount,
|
||
|
|
onPreviousPage,
|
||
|
|
onNextPage,
|
||
|
|
canPreviousPage,
|
||
|
|
canNextPage,
|
||
|
|
username,
|
||
|
|
action,
|
||
|
|
from,
|
||
|
|
to,
|
||
|
|
onUsernameChange,
|
||
|
|
onActionChange,
|
||
|
|
onFromChange,
|
||
|
|
onToChange,
|
||
|
|
onSearch,
|
||
|
|
onReset,
|
||
|
|
selectedAudit,
|
||
|
|
onRowClick,
|
||
|
|
onDialogClose,
|
||
|
|
}: AuditListTemplateProps) {
|
||
|
|
const table = useReactTable({
|
||
|
|
data: items,
|
||
|
|
columns,
|
||
|
|
state: {
|
||
|
|
pagination: { pageIndex: Math.max(0, pageNumber - 1), pageSize },
|
||
|
|
},
|
||
|
|
pageCount,
|
||
|
|
manualPagination: true,
|
||
|
|
onPaginationChange: (updater) => {
|
||
|
|
const next =
|
||
|
|
typeof updater === "function"
|
||
|
|
? updater({ pageIndex: Math.max(0, pageNumber - 1), pageSize })
|
||
|
|
: updater;
|
||
|
|
const newPage = (next.pageIndex ?? 0) + 1;
|
||
|
|
if (newPage > pageNumber) onNextPage();
|
||
|
|
else if (newPage < pageNumber) onPreviousPage();
|
||
|
|
},
|
||
|
|
getCoreRowModel: getCoreRowModel(),
|
||
|
|
});
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="w-full px-4 md:px-6 space-y-4">
|
||
|
|
<div>
|
||
|
|
<h1 className="text-2xl md:text-3xl font-bold">Nhật ký hoạt động</h1>
|
||
|
|
<p className="text-muted-foreground mt-1 text-sm">
|
||
|
|
Xem nhật ký audit hệ thống
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<Card>
|
||
|
|
<CardHeader className="pb-3">
|
||
|
|
<CardTitle className="text-base">Danh sách audit</CardTitle>
|
||
|
|
<CardDescription className="text-xs">
|
||
|
|
Lọc theo người dùng, loại, hành động và khoảng thời gian. Nhấn vào
|
||
|
|
dòng để xem chi tiết.
|
||
|
|
</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
|
||
|
|
<CardContent>
|
||
|
|
<AuditFilterBar
|
||
|
|
username={username}
|
||
|
|
action={action}
|
||
|
|
from={from}
|
||
|
|
to={to}
|
||
|
|
isLoading={isLoading}
|
||
|
|
isFetching={isFetching}
|
||
|
|
onUsernameChange={onUsernameChange}
|
||
|
|
onActionChange={onActionChange}
|
||
|
|
onFromChange={onFromChange}
|
||
|
|
onToChange={onToChange}
|
||
|
|
onSearch={onSearch}
|
||
|
|
onReset={onReset}
|
||
|
|
/>
|
||
|
|
|
||
|
|
<div className="rounded-md border overflow-x-auto">
|
||
|
|
<Table className="min-w-[640px] w-full">
|
||
|
|
<TableHeader className="sticky top-0 bg-background z-10">
|
||
|
|
{table.getHeaderGroups().map((hg) => (
|
||
|
|
<TableRow key={hg.id}>
|
||
|
|
{hg.headers.map((header) => (
|
||
|
|
<TableHead
|
||
|
|
key={header.id}
|
||
|
|
className="text-xs font-semibold whitespace-nowrap"
|
||
|
|
>
|
||
|
|
{flexRender(
|
||
|
|
header.column.columnDef.header,
|
||
|
|
header.getContext()
|
||
|
|
)}
|
||
|
|
</TableHead>
|
||
|
|
))}
|
||
|
|
</TableRow>
|
||
|
|
))}
|
||
|
|
</TableHeader>
|
||
|
|
|
||
|
|
<TableBody>
|
||
|
|
{isLoading || isFetching ? (
|
||
|
|
<TableRow>
|
||
|
|
<TableCell
|
||
|
|
colSpan={columns.length}
|
||
|
|
className="text-center py-10 text-muted-foreground text-sm"
|
||
|
|
>
|
||
|
|
Đang tải...
|
||
|
|
</TableCell>
|
||
|
|
</TableRow>
|
||
|
|
) : table.getRowModel().rows.length === 0 ? (
|
||
|
|
<TableRow>
|
||
|
|
<TableCell
|
||
|
|
colSpan={columns.length}
|
||
|
|
className="text-center py-10 text-muted-foreground text-sm"
|
||
|
|
>
|
||
|
|
Không có dữ liệu
|
||
|
|
</TableCell>
|
||
|
|
</TableRow>
|
||
|
|
) : (
|
||
|
|
table.getRowModel().rows.map((row) => (
|
||
|
|
<TableRow
|
||
|
|
key={row.id}
|
||
|
|
className="hover:bg-muted/40 cursor-pointer"
|
||
|
|
onClick={() => onRowClick(row.original)}
|
||
|
|
>
|
||
|
|
{row.getVisibleCells().map((cell) => (
|
||
|
|
<TableCell key={cell.id} className="py-2.5 align-middle">
|
||
|
|
{cell.column.columnDef.cell
|
||
|
|
? flexRender(
|
||
|
|
cell.column.columnDef.cell,
|
||
|
|
cell.getContext()
|
||
|
|
)
|
||
|
|
: String(cell.getValue() ?? "")}
|
||
|
|
</TableCell>
|
||
|
|
))}
|
||
|
|
</TableRow>
|
||
|
|
))
|
||
|
|
)}
|
||
|
|
</TableBody>
|
||
|
|
</Table>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex items-center justify-between mt-4">
|
||
|
|
<span className="text-xs text-muted-foreground">
|
||
|
|
Hiển thị {items.length} / {total} mục
|
||
|
|
</span>
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
<Button
|
||
|
|
variant="outline"
|
||
|
|
size="sm"
|
||
|
|
disabled={!canPreviousPage || isFetching}
|
||
|
|
onClick={onPreviousPage}
|
||
|
|
>
|
||
|
|
Trước
|
||
|
|
</Button>
|
||
|
|
<span className="text-sm tabular-nums">
|
||
|
|
{pageNumber} / {pageCount}
|
||
|
|
</span>
|
||
|
|
<Button
|
||
|
|
variant="outline"
|
||
|
|
size="sm"
|
||
|
|
disabled={!canNextPage || isFetching}
|
||
|
|
onClick={onNextPage}
|
||
|
|
>
|
||
|
|
Sau
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
<AuditDetailDialog
|
||
|
|
audit={selectedAudit}
|
||
|
|
open={!!selectedAudit}
|
||
|
|
onClose={onDialogClose}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|