@goplusvn/core 0.1.67 → 0.1.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGELOG.md +85 -1
  2. package/bin/goerp-guardrails.mjs +45 -0
  3. package/eslint/index.mjs +120 -0
  4. package/package.json +10 -3
  5. package/scripts/doctor.ts +99 -0
  6. package/src/guardrails/__tests__/guardrails.test.ts +430 -0
  7. package/src/guardrails/index.ts +57 -0
  8. package/src/guardrails/preset.ts +75 -0
  9. package/src/guardrails/primitives.ts +307 -0
  10. package/src/guardrails/rules/auth.ts +178 -0
  11. package/src/guardrails/rules/debt.ts +71 -0
  12. package/src/guardrails/rules/design.ts +95 -0
  13. package/src/guardrails/rules/layering.ts +160 -0
  14. package/src/guardrails/rules/one-door.ts +115 -0
  15. package/src/guardrails/rules/rbac.ts +282 -0
  16. package/src/guardrails/rules/safety.ts +86 -0
  17. package/src/guardrails/rules/structure.ts +136 -0
  18. package/src/guardrails/run.ts +130 -0
  19. package/src/guardrails/scanner.ts +144 -0
  20. package/src/guardrails/types.ts +181 -0
  21. package/src/types/index.ts +1 -1
  22. package/src/ui/data-display/shallow-pagination.tsx +189 -0
  23. package/src/ui/index.tsx +1 -0
  24. package/src/user/components/index.ts +1 -0
  25. package/src/user/components/user-toolbar.tsx +8 -2
  26. package/src/user/components/user-visuals.tsx +84 -0
  27. package/src/user/components/users-card-view.tsx +1 -26
  28. package/src/user/components/users-table.tsx +215 -0
  29. package/src/user/pages/users-client-page.tsx +84 -259
  30. package/templates/starter-app/AGENTS.md +39 -3
  31. package/templates/starter-app/eslint.config.mjs +85 -0
  32. package/templates/starter-app/package.json +19 -2
  33. package/templates/starter-app/prettier.config.mjs +54 -0
  34. package/templates/starter-app/src/__tests__/architecture.test.ts +35 -143
@@ -0,0 +1,189 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useState } from "react";
4
+ import type { KeyboardEvent } from "react";
5
+ import { usePathname, useSearchParams } from "next/navigation";
6
+ import {
7
+ ChevronLeft,
8
+ ChevronRight,
9
+ ChevronsLeft,
10
+ ChevronsRight,
11
+ } from "lucide-react";
12
+
13
+ import { Button } from "../primitives";
14
+ import {
15
+ Select,
16
+ SelectContent,
17
+ SelectItem,
18
+ SelectTrigger,
19
+ SelectValue,
20
+ } from "../primitives/client";
21
+
22
+ /**
23
+ * Biến thể NÔNG của `UrlPagination` cho trang mà CLIENT làm chủ dữ liệu (SWR
24
+ * hoặc danh sách đã nạp đủ về client).
25
+ *
26
+ * `UrlPagination` dùng `router.push`, nên đổi trang là một lượt điều hướng RSC:
27
+ * chạy lại page.tsx, `loading.tsx` nháy skeleton TOÀN TRANG, rồi trả về đúng
28
+ * một trang danh sách mà client đã tự cắt/fetch được. Bản này ghi URL bằng
29
+ * `history.replaceState` (Next 14.1+ vẫn đồng bộ `useSearchParams`) nên chỉ
30
+ * client re-render — và đổi trang không tạo mục lịch sử mới, giống đổi bộ lọc.
31
+ *
32
+ * CHỈ dùng cho trang có client đọc `page`/`pageSize` từ `useSearchParams`.
33
+ * Trang SSR thuần vẫn phải dùng `UrlPagination` vì chúng cần lượt RSC mới có
34
+ * dữ liệu. Giao diện giữ giống `DataTablePagination` 1:1 để hai loại trang
35
+ * nhìn như nhau.
36
+ */
37
+ export function ShallowPagination({
38
+ totalItems,
39
+ currentPage,
40
+ pageSize,
41
+ disabled,
42
+ }: {
43
+ totalItems: number;
44
+ currentPage: number;
45
+ pageSize: number;
46
+ disabled?: boolean;
47
+ }) {
48
+ const pathname = usePathname();
49
+ const searchParams = useSearchParams();
50
+
51
+ const pageCount = Math.ceil(totalItems / pageSize);
52
+ const startItem = (currentPage - 1) * pageSize + 1;
53
+ const endItem = Math.min(currentPage * pageSize, totalItems);
54
+
55
+ const applyParams = useCallback(
56
+ (params: Record<string, string | number | null>) => {
57
+ const next = new URLSearchParams(searchParams?.toString());
58
+ for (const [key, value] of Object.entries(params)) {
59
+ if (value === null) next.delete(key);
60
+ else next.set(key, String(value));
61
+ }
62
+ const qs = next.toString();
63
+ window.history.replaceState(
64
+ null,
65
+ "",
66
+ qs ? `${pathname}?${qs}` : pathname,
67
+ );
68
+ },
69
+ [pathname, searchParams],
70
+ );
71
+
72
+ const handlePageChange = (page: number) => {
73
+ if (page < 1 || page > Math.max(1, pageCount)) return;
74
+ applyParams({ page });
75
+ };
76
+
77
+ const handlePageSizeChange = (value: string) => {
78
+ // Đổi số dòng thì về trang 1, tránh rơi ra ngoài phạm vi
79
+ applyParams({ pageSize: Number(value), page: 1 });
80
+ };
81
+
82
+ const [inputPage, setInputPage] = useState(String(currentPage));
83
+
84
+ useEffect(() => {
85
+ setInputPage(String(currentPage));
86
+ }, [currentPage]);
87
+
88
+ const handlePageInputBlur = () => {
89
+ const pageNum = Number(inputPage);
90
+ if (isNaN(pageNum) || pageNum < 1 || pageNum > pageCount) {
91
+ setInputPage(String(currentPage));
92
+ } else if (pageNum !== currentPage) {
93
+ handlePageChange(pageNum);
94
+ }
95
+ };
96
+
97
+ const handlePageInputKeyDown = (e: KeyboardEvent) => {
98
+ if (e.key === "Enter") handlePageInputBlur();
99
+ };
100
+
101
+ return (
102
+ <div className="flex flex-col sm:flex-row items-center justify-between gap-1 sm:gap-2 px-1 sm:px-2">
103
+ <div className="hidden sm:block flex-1 text-[11px] text-left text-muted-foreground w-full">
104
+ Hiển thị {totalItems > 0 ? startItem : 0} -{" "}
105
+ {totalItems > 0 ? endItem : 0} trong tổng số {totalItems} mục
106
+ </div>
107
+
108
+ <div className="flex items-center justify-center gap-2 w-full sm:w-auto overflow-x-auto">
109
+ <div className="flex items-center gap-1 px-1 border-r pr-1 sm:pr-1.5">
110
+ <p className="hidden sm:block text-[11px] font-medium whitespace-nowrap">
111
+ Số dòng
112
+ </p>
113
+ <Select
114
+ value={`${pageSize}`}
115
+ onValueChange={handlePageSizeChange}
116
+ disabled={disabled}
117
+ >
118
+ <SelectTrigger className="!h-6 !min-h-0 py-0 w-[60px] sm:w-[65px] text-[11px] px-1.5 focus:ring-inset focus:ring-offset-0">
119
+ <SelectValue placeholder={pageSize} />
120
+ </SelectTrigger>
121
+ <SelectContent side="top">
122
+ {[10, 20, 50, 100, 500, 1000].map((size) => (
123
+ <SelectItem key={size} value={`${size}`}>
124
+ {size}
125
+ </SelectItem>
126
+ ))}
127
+ </SelectContent>
128
+ </Select>
129
+ </div>
130
+ <div className="flex items-center gap-1 pl-1">
131
+ <Button
132
+ variant="outline"
133
+ className="hidden h-6 w-6 p-0 lg:flex"
134
+ onClick={() => handlePageChange(1)}
135
+ disabled={currentPage <= 1 || disabled || totalItems === 0}
136
+ >
137
+ <span className="sr-only">Trang đầu</span>
138
+ <ChevronsLeft className="h-3 w-3" />
139
+ </Button>
140
+ <Button
141
+ variant="outline"
142
+ className="h-6 w-6 p-0"
143
+ onClick={() => handlePageChange(currentPage - 1)}
144
+ disabled={currentPage <= 1 || disabled || totalItems === 0}
145
+ >
146
+ <span className="sr-only">Trang trước</span>
147
+ <ChevronLeft className="h-3 w-3" />
148
+ </Button>
149
+
150
+ <div className="flex items-center gap-1 sm:gap-2 px-1">
151
+ <span className="hidden sm:inline text-[11px] font-medium whitespace-nowrap">
152
+ Trang
153
+ </span>
154
+ <input
155
+ className="h-6 w-10 sm:w-12 rounded-md border border-input bg-background px-1 text-[11px] text-center focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
156
+ value={inputPage}
157
+ onChange={(e) => setInputPage(e.target.value)}
158
+ onBlur={handlePageInputBlur}
159
+ onKeyDown={handlePageInputKeyDown}
160
+ disabled={disabled || totalItems === 0}
161
+ />
162
+ <span className="text-[11px] font-medium text-muted-foreground whitespace-nowrap">
163
+ / {Math.max(1, pageCount)}
164
+ </span>
165
+ </div>
166
+
167
+ <Button
168
+ variant="outline"
169
+ className="h-6 w-6 p-0"
170
+ onClick={() => handlePageChange(currentPage + 1)}
171
+ disabled={currentPage >= pageCount || disabled || totalItems === 0}
172
+ >
173
+ <span className="sr-only">Trang sau</span>
174
+ <ChevronRight className="h-3 w-3" />
175
+ </Button>
176
+ <Button
177
+ variant="outline"
178
+ className="hidden h-6 w-6 p-0 lg:flex"
179
+ onClick={() => handlePageChange(pageCount)}
180
+ disabled={currentPage >= pageCount || disabled || totalItems === 0}
181
+ >
182
+ <span className="sr-only">Trang cuối</span>
183
+ <ChevronsRight className="h-3 w-3" />
184
+ </Button>
185
+ </div>
186
+ </div>
187
+ </div>
188
+ );
189
+ }
package/src/ui/index.tsx CHANGED
@@ -19,3 +19,4 @@ export * from "./pages/not-found";
19
19
  export * from "./shared";
20
20
 
21
21
  export { UrlPagination } from "./data-display/url-pagination";
22
+ export { ShallowPagination } from "./data-display/shallow-pagination";
@@ -1,4 +1,5 @@
1
1
  export { UserStats } from "./user-stats";
2
2
  export { UserToolbar } from "./user-toolbar";
3
3
  export { UsersCardView } from "./users-card-view";
4
+ export { UsersTable } from "./users-table";
4
5
  export { UnifiedProfileDialog } from "./unified-profile-dialog";
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { useState, useEffect } from "react";
3
+ import { useState, useEffect, useRef } from "react";
4
4
  import { Plus, Search, LayoutGrid, List, X, Filter } from "lucide-react";
5
5
  import {
6
6
  Button,
@@ -45,9 +45,15 @@ export function UserToolbar({
45
45
  }: UserToolbarProps) {
46
46
  const [localSearch, setLocalSearch] = useState("");
47
47
 
48
- // Debounce search
48
+ // Debounce search. CHỈ phát khi giá trị thật sự đổi so với lần phát trước:
49
+ // effect này còn chạy lại khi identity `onSearchChange` đổi (client page tạo
50
+ // lại callback lúc URL đổi), mà phát lại "" lúc đó là một cú router.push xoá
51
+ // sạch query — từng nuốt mất ?page khi bấm phân trang.
52
+ const lastEmittedSearch = useRef(localSearch);
49
53
  useEffect(() => {
54
+ if (localSearch === lastEmittedSearch.current) return;
50
55
  const timer = setTimeout(() => {
56
+ lastEmittedSearch.current = localSearch;
51
57
  onSearchChange(localSearch);
52
58
  }, 300);
53
59
  return () => clearTimeout(timer);
@@ -0,0 +1,84 @@
1
+ "use client";
2
+
3
+ import { Badge } from "../../ui";
4
+
5
+ // Palette avatar dùng chung cho card view + table view — đổi một chỗ đổi cả hai.
6
+ export const AVATAR_PALETTE = [
7
+ { bg: "bg-blue-600", text: "text-white" },
8
+ { bg: "bg-emerald-600", text: "text-white" },
9
+ { bg: "bg-orange-600", text: "text-white" },
10
+ { bg: "bg-violet-600", text: "text-white" },
11
+ { bg: "bg-cyan-600", text: "text-white" },
12
+ { bg: "bg-rose-600", text: "text-white" },
13
+ ];
14
+
15
+ export const getAvatarColor = (name: string | null) => {
16
+ if (!name) return AVATAR_PALETTE[0];
17
+ const charCode = name.charCodeAt(0) + (name.charCodeAt(name.length - 1) || 0);
18
+ return AVATAR_PALETTE[charCode % AVATAR_PALETTE.length];
19
+ };
20
+
21
+ export const getInitials = (name: string | null) => {
22
+ if (!name) return "U";
23
+ return name
24
+ .split(" ")
25
+ .map((n) => n[0])
26
+ .join("")
27
+ .toUpperCase()
28
+ .slice(0, 2);
29
+ };
30
+
31
+ const BADGE_BASE =
32
+ "text-[9px] h-4 px-1.5 font-bold border-transparent uppercase tracking-widest shrink-0 rounded-full";
33
+
34
+ export function UserStatusBadge({ active }: { active: boolean }) {
35
+ return active ? (
36
+ <Badge
37
+ className={`${BADGE_BASE} bg-emerald-100 text-emerald-700 hover:bg-emerald-200`}
38
+ >
39
+ <div className="w-1.5 h-1.5 rounded-full bg-emerald-600 mr-1.5" />
40
+ Hoạt động
41
+ </Badge>
42
+ ) : (
43
+ <Badge
44
+ className={`${BADGE_BASE} bg-slate-100 text-slate-600 hover:bg-slate-200`}
45
+ >
46
+ <div className="w-1.5 h-1.5 rounded-full bg-slate-400 mr-1.5" />
47
+ Đã khóa
48
+ </Badge>
49
+ );
50
+ }
51
+
52
+ export function UserTypeBadge({
53
+ userType,
54
+ short,
55
+ }: {
56
+ userType?: string | null;
57
+ short?: boolean;
58
+ }) {
59
+ if (userType === "customer") {
60
+ return (
61
+ <Badge
62
+ className={`${BADGE_BASE} bg-emerald-100 text-emerald-700 hover:bg-emerald-200`}
63
+ >
64
+ {short ? "Khách" : "Khách hàng"}
65
+ </Badge>
66
+ );
67
+ }
68
+ if (userType === "supplier") {
69
+ return (
70
+ <Badge
71
+ className={`${BADGE_BASE} bg-orange-100 text-orange-700 hover:bg-orange-200`}
72
+ >
73
+ {short ? "NCC" : "Nhà cung cấp"}
74
+ </Badge>
75
+ );
76
+ }
77
+ return (
78
+ <Badge
79
+ className={`${BADGE_BASE} bg-indigo-100 text-indigo-700 hover:bg-indigo-200`}
80
+ >
81
+ {short ? "NV" : "Nhân viên"}
82
+ </Badge>
83
+ );
84
+ }
@@ -16,6 +16,7 @@ import {
16
16
  import { cn } from "../../utils";
17
17
  import { Phone, MoreHorizontal, Eye, Edit, User, Briefcase, Building2, KeyRound, Mail, ShieldCheck, Copy } from "lucide-react";
18
18
  import { toast } from "sonner";
19
+ import { getAvatarColor, getInitials } from "./user-visuals";
19
20
 
20
21
  interface UsersCardViewProps {
21
22
  data: any[];
@@ -24,22 +25,6 @@ interface UsersCardViewProps {
24
25
  onViewPermissions?: (user: any) => void;
25
26
  }
26
27
 
27
- // Professional Corporate Palette for Avatars
28
- const AVATAR_PALETTE = [
29
- { bg: "bg-blue-600", text: "text-white" },
30
- { bg: "bg-emerald-600", text: "text-white" },
31
- { bg: "bg-orange-600", text: "text-white" },
32
- { bg: "bg-violet-600", text: "text-white" },
33
- { bg: "bg-cyan-600", text: "text-white" },
34
- { bg: "bg-rose-600", text: "text-white" },
35
- ];
36
-
37
- const getAvatarColor = (name: string | null) => {
38
- if (!name) return AVATAR_PALETTE[0];
39
- const charCode = name.charCodeAt(0) + (name.charCodeAt(name.length - 1) || 0);
40
- return AVATAR_PALETTE[charCode % AVATAR_PALETTE.length];
41
- };
42
-
43
28
  export function UsersCardView({
44
29
  data,
45
30
  onSelect,
@@ -61,16 +46,6 @@ export function UsersCardView({
61
46
  );
62
47
  }
63
48
 
64
- const getInitials = (name: string | null) => {
65
- if (!name) return "U";
66
- return name
67
- .split(" ")
68
- .map((n) => n[0])
69
- .join("")
70
- .toUpperCase()
71
- .slice(0, 2);
72
- };
73
-
74
49
  return (
75
50
  <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-5 pb-4 mt-2">
76
51
  {data.map((user) => {
@@ -0,0 +1,215 @@
1
+ "use client";
2
+
3
+ import { useMemo } from "react";
4
+ import { Briefcase, Building2, Edit2, Phone } from "lucide-react";
5
+
6
+ import type { ColumnDef } from "@tanstack/react-table";
7
+
8
+ import {
9
+ Avatar,
10
+ AvatarFallback,
11
+ AvatarImage,
12
+ Badge,
13
+ Button,
14
+ DataTable,
15
+ } from "../../ui";
16
+ import { cn } from "../../utils";
17
+ import {
18
+ UserStatusBadge,
19
+ UserTypeBadge,
20
+ getAvatarColor,
21
+ getInitials,
22
+ } from "./user-visuals";
23
+
24
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
+ type UserRow = Record<string, any>;
26
+
27
+ interface UsersTableProps {
28
+ data: UserRow[];
29
+ /** Phân trang server-style qua URL — client page cắt trang và cầm URL. */
30
+ page: number;
31
+ pageSize: number;
32
+ total: number;
33
+ onPageChange: (page: number, pageSize: number) => void;
34
+ onRowClick?: (user: UserRow) => void;
35
+ onEdit?: (user: UserRow) => void;
36
+ }
37
+
38
+ /**
39
+ * Bảng danh sách người dùng theo chuẩn chung (DataTable tanstack + thanh phân
40
+ * trang "Hiển thị x - y trong tổng số n mục"). Thay cho BasicUserTable cũ vốn
41
+ * tự chế phân trang bằng state cục bộ — số trang không vào URL, không đồng bộ
42
+ * với card view.
43
+ */
44
+ export function UsersTable({
45
+ data,
46
+ page,
47
+ pageSize,
48
+ total,
49
+ onPageChange,
50
+ onRowClick,
51
+ onEdit,
52
+ }: UsersTableProps) {
53
+ const columns = useMemo<ColumnDef<UserRow>[]>(
54
+ () => [
55
+ {
56
+ id: "user",
57
+ header: "Người dùng",
58
+ size: 300,
59
+ minSize: 260,
60
+ cell: ({ row }) => {
61
+ const user = row.original;
62
+ const isActive = user.isActive || user.status === "active";
63
+ const avatarColors = getAvatarColor(user.name);
64
+ return (
65
+ <div className="flex items-start gap-3 py-1">
66
+ <Avatar className="h-9 w-9 shrink-0 rounded-full border shadow-sm">
67
+ <AvatarImage
68
+ src={user.image || user.avatar || ""}
69
+ alt={user.name || ""}
70
+ className="object-cover rounded-full"
71
+ />
72
+ <AvatarFallback
73
+ className={cn(
74
+ "text-xs font-bold rounded-full",
75
+ avatarColors.bg,
76
+ avatarColors.text,
77
+ )}
78
+ >
79
+ {getInitials(user.name || user.email)}
80
+ </AvatarFallback>
81
+ </Avatar>
82
+ <div className="flex flex-col min-w-0">
83
+ <div className="flex items-center gap-1.5 mb-0.5 flex-wrap">
84
+ <span className="font-semibold text-sm text-foreground truncate mr-1">
85
+ {user.name || "Chưa đặt tên"}
86
+ </span>
87
+ <UserStatusBadge active={isActive} />
88
+ <UserTypeBadge userType={user.userType} />
89
+ </div>
90
+ <span className="text-xs text-muted-foreground truncate">
91
+ {user.email || "—"}
92
+ </span>
93
+ </div>
94
+ </div>
95
+ );
96
+ },
97
+ },
98
+ {
99
+ id: "contact",
100
+ header: "Liên hệ",
101
+ size: 150,
102
+ minSize: 130,
103
+ cell: ({ row }) => (
104
+ <div className="flex items-center gap-1.5">
105
+ <Phone className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
106
+ <span className="text-sm text-foreground/80">
107
+ {row.original.profiles?.phone || row.original.phone || "—"}
108
+ </span>
109
+ </div>
110
+ ),
111
+ },
112
+ {
113
+ id: "department",
114
+ header: "Phòng ban / Chi nhánh",
115
+ size: 200,
116
+ minSize: 170,
117
+ cell: ({ row }) => {
118
+ const user = row.original;
119
+ const branches =
120
+ user.branchNames && user.branchNames.length > 0
121
+ ? user.branchNames.join(", ")
122
+ : user.branchName || user.branch?.name || "—";
123
+ return (
124
+ <div className="flex flex-col gap-1">
125
+ <div className="flex items-center gap-1.5">
126
+ <Briefcase className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
127
+ <span
128
+ className="text-sm font-medium text-foreground/80 truncate max-w-[200px]"
129
+ title={user.departmentName}
130
+ >
131
+ {user.departmentName || "—"}
132
+ </span>
133
+ </div>
134
+ <div className="flex items-center gap-1.5">
135
+ <Building2 className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
136
+ <span
137
+ className="text-xs text-muted-foreground truncate max-w-[200px]"
138
+ title={branches}
139
+ >
140
+ {branches}
141
+ </span>
142
+ </div>
143
+ </div>
144
+ );
145
+ },
146
+ },
147
+ {
148
+ id: "roles",
149
+ header: "Vai trò",
150
+ size: 240,
151
+ minSize: 180,
152
+ cell: ({ row }) => {
153
+ const roleNames: string[] = row.original.roleNames || [];
154
+ return (
155
+ <div className="flex flex-wrap gap-1">
156
+ {roleNames.length > 0 ? (
157
+ roleNames.map((r, i) => (
158
+ <Badge
159
+ key={i}
160
+ variant="secondary"
161
+ className="text-[10px] px-2 py-0.5 font-medium truncate max-w-[140px]"
162
+ title={r}
163
+ >
164
+ {r}
165
+ </Badge>
166
+ ))
167
+ ) : (
168
+ <span className="text-xs text-muted-foreground italic">—</span>
169
+ )}
170
+ </div>
171
+ );
172
+ },
173
+ },
174
+ ...(onEdit
175
+ ? [
176
+ {
177
+ id: "actions",
178
+ header: "",
179
+ size: 60,
180
+ maxSize: 60,
181
+ cell: ({ row }: { row: { original: UserRow } }) => (
182
+ <div className="text-right">
183
+ <Button
184
+ variant="ghost"
185
+ size="icon"
186
+ className="h-8 w-8 rounded-full text-muted-foreground hover:text-foreground"
187
+ onClick={(e) => {
188
+ e.stopPropagation();
189
+ onEdit(row.original);
190
+ }}
191
+ >
192
+ <Edit2 className="h-4 w-4" />
193
+ </Button>
194
+ </div>
195
+ ),
196
+ } satisfies ColumnDef<UserRow>,
197
+ ]
198
+ : []),
199
+ ],
200
+ [onEdit],
201
+ );
202
+
203
+ return (
204
+ <DataTable<UserRow>
205
+ data={data}
206
+ columns={columns}
207
+ enableRowNumber
208
+ pagination={{ page, pageSize, total }}
209
+ onPaginationChange={(p) => onPageChange(p.page, p.pageSize)}
210
+ onRowClick={onRowClick}
211
+ tableClassName="text-sm"
212
+ cellClassName="py-2"
213
+ />
214
+ );
215
+ }