@goplusvn/core 0.1.61 → 0.1.63
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.
package/package.json
CHANGED
|
@@ -137,8 +137,8 @@ export function CrudExportButton({
|
|
|
137
137
|
className="rounded-r-none border-r-0"
|
|
138
138
|
onClick={() => handleExport("current")}
|
|
139
139
|
>
|
|
140
|
-
<Download className="
|
|
141
|
-
{loading ? "Exporting..." : "Export"}
|
|
140
|
+
<Download className="h-4 w-4 sm:mr-2" />
|
|
141
|
+
<span className="hidden sm:inline">{loading ? "Exporting..." : "Export"}</span>
|
|
142
142
|
</Button>
|
|
143
143
|
<DropdownMenu>
|
|
144
144
|
<DropdownMenuTrigger asChild>
|
|
@@ -368,8 +368,8 @@ export function ImportDialog({
|
|
|
368
368
|
<DialogTrigger asChild>
|
|
369
369
|
{trigger ?? (
|
|
370
370
|
<Button variant="outline" size="sm">
|
|
371
|
-
<Upload className="
|
|
372
|
-
Import
|
|
371
|
+
<Upload className="h-4 w-4 sm:mr-2" />
|
|
372
|
+
<span className="hidden sm:inline">Import</span>
|
|
373
373
|
</Button>
|
|
374
374
|
)}
|
|
375
375
|
</DialogTrigger>
|
|
@@ -48,89 +48,99 @@ export function UserToolbar({
|
|
|
48
48
|
}, [localSearch, onSearchChange]);
|
|
49
49
|
|
|
50
50
|
return (
|
|
51
|
-
<div className="flex items-center gap-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
<SelectTrigger className="h-9 w-[150px] text-xs bg-background border hover:bg-muted/50 transition-colors shadow-sm rounded-md focus-visible:ring-primary">
|
|
74
|
-
<SelectValue placeholder="Loại tài khoản" />
|
|
75
|
-
</SelectTrigger>
|
|
76
|
-
<SelectContent className="rounded-lg">
|
|
77
|
-
<SelectItem value="all">Tất cả loại</SelectItem>
|
|
78
|
-
<SelectItem value="employee">Nhân viên</SelectItem>
|
|
79
|
-
<SelectItem value="customer">Khách hàng</SelectItem>
|
|
80
|
-
<SelectItem value="supplier">Nhà cung cấp</SelectItem>
|
|
81
|
-
</SelectContent>
|
|
82
|
-
</Select>
|
|
51
|
+
<div className="flex flex-col xl:flex-row items-start xl:items-center justify-between w-full gap-3 xl:gap-4">
|
|
52
|
+
{/* Tìm kiếm + bộ lọc chính */}
|
|
53
|
+
<div className="flex w-full min-w-0 xl:w-auto flex-1 items-center gap-2 xl:gap-3">
|
|
54
|
+
<div className="relative w-full xl:flex-[2] xl:min-w-[220px] max-w-none">
|
|
55
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
56
|
+
<Input
|
|
57
|
+
placeholder="Tìm người dùng..."
|
|
58
|
+
value={localSearch}
|
|
59
|
+
onChange={(e) => setLocalSearch(e.target.value)}
|
|
60
|
+
className="pl-9 h-9 w-full text-xs bg-secondary/30 border hover:bg-muted/50 transition-colors shadow-sm rounded-md focus-visible:ring-1 focus-visible:ring-primary"
|
|
61
|
+
/>
|
|
62
|
+
{localSearch && (
|
|
63
|
+
<Button
|
|
64
|
+
variant="ghost"
|
|
65
|
+
size="sm"
|
|
66
|
+
className="absolute right-1 top-1/2 -translate-y-1/2 h-7 w-7 p-0 rounded-md hover:bg-muted"
|
|
67
|
+
onClick={() => setLocalSearch("")}
|
|
68
|
+
>
|
|
69
|
+
<X className="h-3.5 w-3.5" />
|
|
70
|
+
</Button>
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
83
73
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
74
|
+
<div className="w-full xl:w-auto flex-1 min-w-0">
|
|
75
|
+
<Select value={userType} onValueChange={onUserTypeChange}>
|
|
76
|
+
<SelectTrigger className="h-9 w-full xl:w-[150px] text-xs bg-background border hover:bg-muted/50 transition-colors shadow-sm rounded-md focus-visible:ring-primary min-w-0">
|
|
77
|
+
<SelectValue placeholder="Loại tài khoản" className="truncate" />
|
|
78
|
+
</SelectTrigger>
|
|
79
|
+
<SelectContent className="rounded-lg">
|
|
80
|
+
<SelectItem value="all">Tất cả loại</SelectItem>
|
|
81
|
+
<SelectItem value="employee">Nhân viên</SelectItem>
|
|
82
|
+
<SelectItem value="customer">Khách hàng</SelectItem>
|
|
83
|
+
<SelectItem value="supplier">Nhà cung cấp</SelectItem>
|
|
84
|
+
</SelectContent>
|
|
85
|
+
</Select>
|
|
86
|
+
</div>
|
|
97
87
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
className="
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
onClick={() => onViewModeChange("grid")}
|
|
113
|
-
>
|
|
114
|
-
<LayoutGrid className="h-4 w-4" />
|
|
115
|
-
</Button>
|
|
88
|
+
<div className="w-full xl:w-auto flex-1 min-w-0">
|
|
89
|
+
<Select onValueChange={onRoleChange}>
|
|
90
|
+
<SelectTrigger className="h-9 w-full xl:w-[170px] text-xs bg-background border hover:bg-muted/50 transition-colors shadow-sm rounded-md focus-visible:ring-primary min-w-0">
|
|
91
|
+
<SelectValue placeholder="Lọc theo vai trò" className="truncate" />
|
|
92
|
+
</SelectTrigger>
|
|
93
|
+
<SelectContent className="rounded-lg">
|
|
94
|
+
<SelectItem value="all">Tất cả vai trò</SelectItem>
|
|
95
|
+
{roles?.map((role) => (
|
|
96
|
+
<SelectItem key={role.code} value={role.code}>
|
|
97
|
+
{role.name}
|
|
98
|
+
</SelectItem>
|
|
99
|
+
))}
|
|
100
|
+
</SelectContent>
|
|
101
|
+
</Select>
|
|
116
102
|
</div>
|
|
103
|
+
</div>
|
|
117
104
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
105
|
+
{/* Hành động */}
|
|
106
|
+
<div className="flex items-center justify-between sm:justify-end gap-2 w-full xl:w-auto shrink-0 overflow-x-auto pb-0.5 sm:pb-0">
|
|
107
|
+
<div className="flex items-center gap-2">
|
|
108
|
+
<div className="flex items-center bg-[#f1f5f9] dark:bg-[#181d26] rounded-[6px] p-0.5 border border-[#dddddd] dark:border-[#333840] shrink-0">
|
|
109
|
+
<Button
|
|
110
|
+
variant={viewMode === "table" ? "secondary" : "ghost"}
|
|
111
|
+
size="sm"
|
|
112
|
+
className="h-8 w-8 p-0 rounded-md"
|
|
113
|
+
onClick={() => onViewModeChange("table")}
|
|
114
|
+
>
|
|
115
|
+
<List className="h-4 w-4" />
|
|
116
|
+
</Button>
|
|
117
|
+
<Button
|
|
118
|
+
variant={viewMode === "grid" ? "secondary" : "ghost"}
|
|
119
|
+
size="sm"
|
|
120
|
+
className="h-8 w-8 p-0 rounded-md"
|
|
121
|
+
onClick={() => onViewModeChange("grid")}
|
|
122
|
+
>
|
|
123
|
+
<LayoutGrid className="h-4 w-4" />
|
|
124
|
+
</Button>
|
|
125
125
|
</div>
|
|
126
|
-
)}
|
|
127
126
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
127
|
+
{permissions.export && (
|
|
128
|
+
<div className="[&>button]:h-9 [&>button]:rounded-md [&>button]:shadow-sm [&>button]:bg-background [&>button]:border [&>button]:text-foreground [&>button]:hover:bg-muted/50 shrink-0">
|
|
129
|
+
<CrudExportButton
|
|
130
|
+
endpoint="/api/crud/users/export"
|
|
131
|
+
search={localSearch}
|
|
132
|
+
canExport={true}
|
|
133
|
+
/>
|
|
134
|
+
</div>
|
|
135
|
+
)}
|
|
136
|
+
|
|
137
|
+
{permissions.create && (
|
|
138
|
+
<Button size="sm" className="h-9 px-4 text-xs bg-primary hover:bg-primary/90 text-primary-foreground shadow-sm rounded-md font-medium border-0 shrink-0" onClick={onCreateNew}>
|
|
139
|
+
<Plus className="h-4 w-4 sm:mr-1.5" />
|
|
140
|
+
<span className="hidden sm:inline">Thêm mới</span>
|
|
141
|
+
</Button>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
134
144
|
</div>
|
|
135
145
|
</div>
|
|
136
146
|
);
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
Card,
|
|
5
|
-
CardContent,
|
|
6
|
-
CardHeader,
|
|
7
5
|
Badge,
|
|
8
6
|
Button,
|
|
9
7
|
DropdownMenu,
|
|
@@ -13,9 +11,11 @@ import {
|
|
|
13
11
|
Avatar,
|
|
14
12
|
AvatarFallback,
|
|
15
13
|
AvatarImage,
|
|
14
|
+
Separator,
|
|
16
15
|
} from "../../ui";
|
|
17
16
|
import { cn } from "../../utils";
|
|
18
|
-
import {
|
|
17
|
+
import { Phone, MoreHorizontal, Eye, Edit, User, Briefcase, Building2, KeyRound, Mail, ShieldCheck, Copy } from "lucide-react";
|
|
18
|
+
import { toast } from "sonner";
|
|
19
19
|
|
|
20
20
|
interface UsersCardViewProps {
|
|
21
21
|
data: any[];
|
|
@@ -24,31 +24,16 @@ interface UsersCardViewProps {
|
|
|
24
24
|
onViewPermissions?: (user: any) => void;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
// Professional Corporate Palette
|
|
27
|
+
// Professional Corporate Palette for Avatars
|
|
28
28
|
const AVATAR_PALETTE = [
|
|
29
|
-
{ bg: "bg-
|
|
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" },
|
|
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" },
|
|
34
35
|
];
|
|
35
36
|
|
|
36
|
-
const ROLE_PALETTE = [
|
|
37
|
-
"bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-400 dark:border-blue-800",
|
|
38
|
-
"bg-emerald-50 text-emerald-700 border-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400 dark:border-emerald-800",
|
|
39
|
-
"bg-amber-50 text-amber-700 border-amber-200 dark:bg-amber-900/30 dark:text-amber-400 dark:border-amber-800",
|
|
40
|
-
"bg-purple-50 text-purple-700 border-purple-200 dark:bg-purple-900/30 dark:text-purple-400 dark:border-purple-800",
|
|
41
|
-
"bg-rose-50 text-rose-700 border-rose-200 dark:bg-rose-900/30 dark:text-rose-400 dark:border-rose-800",
|
|
42
|
-
];
|
|
43
|
-
|
|
44
|
-
const getRoleStyle = (role: string) => {
|
|
45
|
-
let hash = 0;
|
|
46
|
-
for (let i = 0; i < role.length; i++) {
|
|
47
|
-
hash = role.charCodeAt(i) + ((hash << 5) - hash);
|
|
48
|
-
}
|
|
49
|
-
return ROLE_PALETTE[Math.abs(hash) % ROLE_PALETTE.length];
|
|
50
|
-
};
|
|
51
|
-
|
|
52
37
|
const getAvatarColor = (name: string | null) => {
|
|
53
38
|
if (!name) return AVATAR_PALETTE[0];
|
|
54
39
|
const charCode = name.charCodeAt(0) + (name.charCodeAt(name.length - 1) || 0);
|
|
@@ -62,7 +47,7 @@ export function UsersCardView({
|
|
|
62
47
|
}: UsersCardViewProps) {
|
|
63
48
|
if (!data.length) {
|
|
64
49
|
return (
|
|
65
|
-
<div className="flex flex-col items-center justify-center min-h-[400px] border border-
|
|
50
|
+
<div className="flex flex-col items-center justify-center min-h-[400px] border border-dashed rounded-xl bg-card/50 p-8 text-center">
|
|
66
51
|
<div className="size-16 rounded-full bg-primary/5 flex items-center justify-center mb-4">
|
|
67
52
|
<User className="size-8 text-primary" strokeWidth={1.5} />
|
|
68
53
|
</div>
|
|
@@ -83,189 +68,198 @@ export function UsersCardView({
|
|
|
83
68
|
.map((n) => n[0])
|
|
84
69
|
.join("")
|
|
85
70
|
.toUpperCase()
|
|
86
|
-
.slice(0,
|
|
71
|
+
.slice(0, 2);
|
|
87
72
|
};
|
|
88
73
|
|
|
89
74
|
return (
|
|
90
|
-
<div className="
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
<
|
|
104
|
-
<
|
|
105
|
-
<
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
)}
|
|
75
|
+
<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
|
+
{data.map((user) => {
|
|
77
|
+
const isActive = user.isActive || user.status === "active";
|
|
78
|
+
const avatarColors = getAvatarColor(user.name);
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<Card
|
|
82
|
+
key={user.id}
|
|
83
|
+
className="group relative flex flex-col bg-card border-border/60 hover:border-primary/40 hover:shadow-md transition-all duration-300 cursor-pointer overflow-hidden rounded-xl shadow-sm"
|
|
84
|
+
onClick={() => onSelect && onSelect(user)}
|
|
85
|
+
>
|
|
86
|
+
{/* Action Menu (Absolute on Card) */}
|
|
87
|
+
<div className="absolute top-3 right-2 z-10">
|
|
88
|
+
<DropdownMenu modal={false}>
|
|
89
|
+
<DropdownMenuTrigger asChild>
|
|
90
|
+
<Button
|
|
91
|
+
variant="ghost"
|
|
92
|
+
size="icon"
|
|
93
|
+
className="h-7 w-7 rounded-full bg-background hover:bg-muted text-muted-foreground hover:text-foreground opacity-0 group-hover:opacity-100 transition-all duration-200"
|
|
94
|
+
onClick={(e) => e.stopPropagation()}
|
|
110
95
|
>
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
96
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
97
|
+
</Button>
|
|
98
|
+
</DropdownMenuTrigger>
|
|
99
|
+
<DropdownMenuContent align="end" className="w-[160px] rounded-xl shadow-lg">
|
|
100
|
+
<DropdownMenuItem
|
|
101
|
+
onClick={(e) => {
|
|
102
|
+
e.stopPropagation();
|
|
103
|
+
setTimeout(() => onSelect && onSelect(user), 0);
|
|
104
|
+
}}
|
|
105
|
+
className="text-xs cursor-pointer py-2 rounded-md"
|
|
106
|
+
>
|
|
107
|
+
<Eye className="mr-2 h-4 w-4 text-muted-foreground" />
|
|
108
|
+
Xem chi tiết
|
|
109
|
+
</DropdownMenuItem>
|
|
110
|
+
<DropdownMenuItem
|
|
111
|
+
onClick={(e) => {
|
|
112
|
+
e.stopPropagation();
|
|
113
|
+
setTimeout(() => onSelect && onSelect(user), 0);
|
|
114
|
+
}}
|
|
115
|
+
className="text-xs cursor-pointer py-2 rounded-md"
|
|
116
|
+
>
|
|
117
|
+
<Edit className="mr-2 h-4 w-4 text-muted-foreground" />
|
|
118
|
+
Chỉnh sửa
|
|
119
|
+
</DropdownMenuItem>
|
|
120
|
+
{onViewPermissions && (
|
|
121
|
+
<DropdownMenuItem
|
|
122
|
+
onClick={(e) => {
|
|
123
|
+
e.stopPropagation();
|
|
124
|
+
setTimeout(() => onViewPermissions(user), 0);
|
|
125
|
+
}}
|
|
126
|
+
className="text-xs cursor-pointer py-2 rounded-md"
|
|
127
|
+
>
|
|
128
|
+
<KeyRound className="mr-2 h-4 w-4 text-muted-foreground" />
|
|
129
|
+
Quyền hiệu lực
|
|
130
|
+
</DropdownMenuItem>
|
|
131
|
+
)}
|
|
132
|
+
</DropdownMenuContent>
|
|
133
|
+
</DropdownMenu>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<div className="flex flex-col flex-1">
|
|
137
|
+
{/* TOP SECTION: Clean Minimalist Header */}
|
|
138
|
+
<div className="p-4 pt-5 flex items-start gap-3 bg-muted/20 border-b border-border/40">
|
|
139
|
+
|
|
140
|
+
<div className="relative shrink-0 z-10">
|
|
141
|
+
<Avatar className="h-12 w-12 rounded-full border border-border/50 shadow-sm">
|
|
142
|
+
<AvatarImage src={user.image || user.avatar || ""} alt={user.name || ""} className="object-cover rounded-full" />
|
|
143
|
+
<AvatarFallback className={cn("text-xs font-bold rounded-full", avatarColors.bg, avatarColors.text)}>
|
|
144
|
+
{getInitials(user.name || user.email)}
|
|
145
|
+
</AvatarFallback>
|
|
146
|
+
</Avatar>
|
|
147
|
+
<span
|
|
123
148
|
className={cn(
|
|
124
|
-
"absolute -bottom-0.5 -right-0.5
|
|
125
|
-
isActive ? "bg-
|
|
126
|
-
)}
|
|
127
|
-
title={isActive ? "
|
|
149
|
+
"absolute -bottom-0.5 -right-0.5 h-3.5 w-3.5 rounded-full border-2 border-card shadow-sm",
|
|
150
|
+
isActive ? "bg-emerald-500" : "bg-muted-foreground/50"
|
|
151
|
+
)}
|
|
152
|
+
title={isActive ? "Đang hoạt động" : "Đã khóa"}
|
|
128
153
|
/>
|
|
129
154
|
</div>
|
|
130
|
-
|
|
131
|
-
<div className="flex-
|
|
132
|
-
<div className="flex items-center gap-
|
|
133
|
-
<h3
|
|
134
|
-
className="font-semibold text-sm text-foreground truncate leading-tight"
|
|
135
|
-
title={user.name}
|
|
136
|
-
>
|
|
155
|
+
|
|
156
|
+
<div className="flex flex-col min-w-0 flex-1 pt-0.5 z-10">
|
|
157
|
+
<div className="flex items-center gap-1.5 flex-wrap pr-6">
|
|
158
|
+
<h3 className="font-bold text-sm text-foreground leading-tight truncate" title={user.name}>
|
|
137
159
|
{user.name || "Chưa đặt tên"}
|
|
138
160
|
</h3>
|
|
139
|
-
|
|
140
|
-
{/* User Type Badge (Compact) */}
|
|
141
161
|
{user.userType === "customer" && (
|
|
142
|
-
<
|
|
143
|
-
Khách hàng
|
|
144
|
-
</span>
|
|
162
|
+
<Badge className="text-[9px] font-bold bg-emerald-100 text-emerald-700 hover:bg-emerald-200 border-transparent uppercase tracking-widest px-1.5 py-0 h-4 shrink-0 rounded-md">Khách</Badge>
|
|
145
163
|
)}
|
|
146
164
|
{user.userType === "supplier" && (
|
|
147
|
-
<
|
|
148
|
-
Nhà cung cấp
|
|
149
|
-
</span>
|
|
165
|
+
<Badge className="text-[9px] font-bold bg-orange-100 text-orange-700 hover:bg-orange-200 border-transparent uppercase tracking-widest px-1.5 py-0 h-4 shrink-0 rounded-md">NCC</Badge>
|
|
150
166
|
)}
|
|
151
167
|
{(!user.userType || user.userType === "employee") && (
|
|
152
|
-
<
|
|
153
|
-
Nhân viên
|
|
154
|
-
</span>
|
|
168
|
+
<Badge className="text-[9px] font-bold bg-indigo-100 text-indigo-700 hover:bg-indigo-200 border-transparent uppercase tracking-widest px-1.5 py-0 h-4 shrink-0 rounded-md">NV</Badge>
|
|
155
169
|
)}
|
|
156
170
|
</div>
|
|
157
|
-
|
|
158
|
-
<div className="flex items-center gap-1.5
|
|
159
|
-
<span className="truncate
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
variant="ghost"
|
|
170
|
-
className="h-7 w-7 p-0 text-muted-foreground hover:text-primary hover:bg-primary/5 shrink-0 -mt-1 -mr-1 rounded-full"
|
|
171
|
-
onClick={(e) => e.stopPropagation()}
|
|
172
|
-
>
|
|
173
|
-
<MoreHorizontal className="size-4" />
|
|
174
|
-
</Button>
|
|
175
|
-
</DropdownMenuTrigger>
|
|
176
|
-
<DropdownMenuContent align="end" className="w-[140px] rounded-xl border-border p-1.5">
|
|
177
|
-
<DropdownMenuItem
|
|
178
|
-
onSelect={(e) => {
|
|
179
|
-
e.preventDefault();
|
|
180
|
-
setTimeout(() => {
|
|
181
|
-
onSelect && onSelect(user);
|
|
182
|
-
}, 150);
|
|
183
|
-
}}
|
|
184
|
-
className="text-xs font-medium text-foreground cursor-pointer rounded-md py-1.5 focus:bg-primary/5 focus:text-primary"
|
|
185
|
-
>
|
|
186
|
-
<Eye className="mr-2 size-3.5" />
|
|
187
|
-
Xem chi tiết
|
|
188
|
-
</DropdownMenuItem>
|
|
189
|
-
<DropdownMenuItem
|
|
190
|
-
onSelect={(e) => {
|
|
191
|
-
e.preventDefault();
|
|
192
|
-
setTimeout(() => {
|
|
193
|
-
onSelect && onSelect(user);
|
|
194
|
-
}, 150);
|
|
195
|
-
}}
|
|
196
|
-
className="text-xs font-medium text-foreground cursor-pointer rounded-md py-1.5 focus:bg-primary/5 focus:text-primary"
|
|
197
|
-
>
|
|
198
|
-
<Edit className="mr-2 size-3.5" />
|
|
199
|
-
Chỉnh sửa
|
|
200
|
-
</DropdownMenuItem>
|
|
201
|
-
{onViewPermissions && (
|
|
202
|
-
<DropdownMenuItem
|
|
203
|
-
onSelect={(e) => {
|
|
204
|
-
e.preventDefault();
|
|
205
|
-
setTimeout(() => onViewPermissions(user), 0);
|
|
171
|
+
|
|
172
|
+
<div className="flex items-center gap-1.5 mt-1.5 text-muted-foreground w-max max-w-full group/email">
|
|
173
|
+
<span className="text-xs truncate font-medium">{user.email || "—"}</span>
|
|
174
|
+
{user.email && (
|
|
175
|
+
<Button
|
|
176
|
+
variant="ghost"
|
|
177
|
+
size="icon"
|
|
178
|
+
className="h-5 w-5 rounded-md hover:bg-muted shrink-0 text-muted-foreground opacity-60 group-hover/email:opacity-100 transition-opacity"
|
|
179
|
+
onClick={(e) => {
|
|
180
|
+
e.stopPropagation();
|
|
181
|
+
navigator.clipboard.writeText(user.email);
|
|
182
|
+
toast.success("Đã copy Email");
|
|
206
183
|
}}
|
|
207
|
-
|
|
184
|
+
title="Copy email"
|
|
208
185
|
>
|
|
209
|
-
<
|
|
210
|
-
|
|
211
|
-
</DropdownMenuItem>
|
|
186
|
+
<Copy className="w-3 h-3" />
|
|
187
|
+
</Button>
|
|
212
188
|
)}
|
|
213
|
-
</
|
|
214
|
-
</
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
215
191
|
</div>
|
|
216
192
|
|
|
217
|
-
|
|
218
|
-
|
|
193
|
+
<Separator />
|
|
194
|
+
|
|
195
|
+
{/* MIDDLE SECTION: 2-Column Split (Khu vực / Liên hệ) */}
|
|
196
|
+
<div className="px-4 py-3.5 grid grid-cols-2 gap-3 flex-1 items-center bg-card">
|
|
219
197
|
|
|
220
|
-
{/*
|
|
221
|
-
<div className="
|
|
222
|
-
<div className="flex items-center gap-1.5
|
|
223
|
-
<
|
|
224
|
-
<span className="text-
|
|
225
|
-
{user.profiles?.phone || user.phone || "—"}
|
|
226
|
-
</span>
|
|
227
|
-
</div>
|
|
228
|
-
|
|
229
|
-
<div className="flex items-center gap-1.5 min-w-0" title="Trạng thái">
|
|
230
|
-
<ShieldCheck className="size-3 text-muted-foreground shrink-0" strokeWidth={1.5} />
|
|
231
|
-
<span className={cn("text-xs font-medium truncate", isActive ? "text-emerald-600 dark:text-emerald-500" : "text-muted-foreground")}>
|
|
232
|
-
{isActive ? "Đang hoạt động" : "Đã khóa"}
|
|
233
|
-
</span>
|
|
198
|
+
{/* Column 1: Khu vực */}
|
|
199
|
+
<div className="flex flex-col min-w-0 pr-2">
|
|
200
|
+
<div className="flex items-center gap-1.5 mb-1.5">
|
|
201
|
+
<Building2 className="w-3 h-3 text-muted-foreground/70" />
|
|
202
|
+
<span className="text-[9px] text-muted-foreground/80 uppercase font-bold tracking-wider">Khu vực</span>
|
|
234
203
|
</div>
|
|
204
|
+
<span className="text-xs text-foreground/90 font-semibold truncate" title={(user.branchNames && user.branchNames.length > 0) ? user.branchNames.join(", ") : (user.branchName || user.branch?.name || "")}>
|
|
205
|
+
{(user.branchNames && user.branchNames.length > 0)
|
|
206
|
+
? user.branchNames.join(", ")
|
|
207
|
+
: (user.branchName || user.branch?.name || "Chưa có chi nhánh")}
|
|
208
|
+
</span>
|
|
209
|
+
<span className="text-[10px] text-muted-foreground truncate mt-0.5" title={user.departmentName}>
|
|
210
|
+
{user.departmentName || "Chưa phân bổ"}
|
|
211
|
+
</span>
|
|
212
|
+
</div>
|
|
235
213
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
214
|
+
{/* Column 2: Liên hệ */}
|
|
215
|
+
<div className="flex flex-col min-w-0 border-l border-border/50 pl-3">
|
|
216
|
+
<div className="flex items-center gap-1.5 mb-1.5">
|
|
217
|
+
<Phone className="w-3 h-3 text-muted-foreground/70" />
|
|
218
|
+
<span className="text-[9px] text-muted-foreground/80 uppercase font-bold tracking-wider">Liên hệ</span>
|
|
241
219
|
</div>
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
<span className="text-xs text-muted-foreground truncate">
|
|
246
|
-
{(user.branchNames && user.branchNames.length > 0) ? user.branchNames.join(", ") : (user.branchName || user.branch?.name || "—")}
|
|
220
|
+
<div className="flex items-center justify-between gap-1 max-w-full group/phone" onClick={(e) => e.stopPropagation()}>
|
|
221
|
+
<span className="text-xs text-foreground/90 font-semibold truncate">
|
|
222
|
+
{user.profiles?.phone || user.phone || "—"}
|
|
247
223
|
</span>
|
|
224
|
+
{(user.profiles?.phone || user.phone) && (
|
|
225
|
+
<Button
|
|
226
|
+
variant="ghost"
|
|
227
|
+
size="icon"
|
|
228
|
+
className="h-5 w-5 rounded-md hover:bg-muted shrink-0 text-muted-foreground opacity-60 group-hover/phone:opacity-100 transition-opacity"
|
|
229
|
+
onClick={(e) => {
|
|
230
|
+
e.stopPropagation();
|
|
231
|
+
const phone = user.profiles?.phone || user.phone;
|
|
232
|
+
navigator.clipboard.writeText(phone);
|
|
233
|
+
toast.success("Đã copy Số điện thoại");
|
|
234
|
+
}}
|
|
235
|
+
title="Copy số điện thoại"
|
|
236
|
+
>
|
|
237
|
+
<Copy className="w-3 h-3" />
|
|
238
|
+
</Button>
|
|
239
|
+
)}
|
|
248
240
|
</div>
|
|
249
241
|
</div>
|
|
242
|
+
</div>
|
|
250
243
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
244
|
+
<Separator />
|
|
245
|
+
|
|
246
|
+
{/* BOTTOM SECTION: Roles */}
|
|
247
|
+
<div className="px-4 py-3 bg-card border-t border-border/40 shrink-0 h-[44px] flex items-center">
|
|
248
|
+
<div className="flex items-center gap-2 w-full overflow-hidden [mask-image:linear-gradient(to_right,black_85%,transparent_100%)] text-[11px] text-muted-foreground/90 font-medium">
|
|
249
|
+
<ShieldCheck className="w-3.5 h-3.5 shrink-0 text-muted-foreground/50" />
|
|
250
|
+
<span className="truncate">
|
|
251
|
+
{user.roleNames && user.roleNames.length > 0 ? (
|
|
252
|
+
user.roleNames.join(" • ")
|
|
253
|
+
) : (
|
|
254
|
+
<span className="italic opacity-60">Chưa phân vai trò</span>
|
|
255
|
+
)}
|
|
256
|
+
</span>
|
|
257
|
+
</div>
|
|
264
258
|
</div>
|
|
265
|
-
</
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
259
|
+
</div>
|
|
260
|
+
</Card>
|
|
261
|
+
);
|
|
262
|
+
})}
|
|
269
263
|
</div>
|
|
270
264
|
);
|
|
271
265
|
}
|
|
@@ -13,9 +13,13 @@ import {
|
|
|
13
13
|
TableRow,
|
|
14
14
|
Badge,
|
|
15
15
|
Button,
|
|
16
|
+
Avatar,
|
|
17
|
+
AvatarFallback,
|
|
18
|
+
AvatarImage,
|
|
16
19
|
} from "../../ui";
|
|
17
|
-
import { Edit2 } from "lucide-react";
|
|
20
|
+
import { Edit2, Phone, Briefcase, Building2 } from "lucide-react";
|
|
18
21
|
import type { EntityConfig, CrudPermissions } from "../../types";
|
|
22
|
+
import { cn } from "../../utils";
|
|
19
23
|
|
|
20
24
|
import { UserToolbar } from "../components/user-toolbar";
|
|
21
25
|
import { UserStats } from "../components/user-stats";
|
|
@@ -274,136 +278,197 @@ function BasicUserTable({
|
|
|
274
278
|
const [currentPage, setCurrentPage] = useState(1);
|
|
275
279
|
const pageSize = 10;
|
|
276
280
|
|
|
277
|
-
if (!data.length) return
|
|
281
|
+
if (!data.length) return (
|
|
282
|
+
<div className="p-8 text-center bg-card flex flex-col items-center justify-center border-t border-border">
|
|
283
|
+
<p className="text-sm font-medium text-muted-foreground">Không có dữ liệu</p>
|
|
284
|
+
</div>
|
|
285
|
+
);
|
|
278
286
|
|
|
279
287
|
const totalPages = Math.ceil(data.length / pageSize);
|
|
280
288
|
const paginatedData = data.slice((currentPage - 1) * pageSize, currentPage * pageSize);
|
|
281
289
|
|
|
290
|
+
const getInitials = (name: string | null) => {
|
|
291
|
+
if (!name) return "U";
|
|
292
|
+
return name.split(" ").map((n) => n[0]).join("").toUpperCase().slice(0, 2);
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
// Corporate Palette for Avatars
|
|
296
|
+
const AVATAR_PALETTE = [
|
|
297
|
+
{ bg: "bg-blue-600", text: "text-white" },
|
|
298
|
+
{ bg: "bg-emerald-600", text: "text-white" },
|
|
299
|
+
{ bg: "bg-orange-600", text: "text-white" },
|
|
300
|
+
{ bg: "bg-violet-600", text: "text-white" },
|
|
301
|
+
{ bg: "bg-cyan-600", text: "text-white" },
|
|
302
|
+
{ bg: "bg-rose-600", text: "text-white" },
|
|
303
|
+
];
|
|
304
|
+
|
|
305
|
+
const getAvatarColor = (name: string | null) => {
|
|
306
|
+
if (!name) return AVATAR_PALETTE[0];
|
|
307
|
+
const charCode = name.charCodeAt(0) + (name.charCodeAt(name.length - 1) || 0);
|
|
308
|
+
return AVATAR_PALETTE[charCode % AVATAR_PALETTE.length];
|
|
309
|
+
};
|
|
310
|
+
|
|
282
311
|
return (
|
|
283
|
-
<div className="flex flex-col
|
|
284
|
-
<div className="bg-
|
|
285
|
-
<Table>
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
<
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
312
|
+
<div className="flex flex-col border border-border rounded-lg overflow-hidden shadow-sm mt-2">
|
|
313
|
+
<div className="bg-card w-full overflow-x-auto">
|
|
314
|
+
<Table className="w-full">
|
|
315
|
+
<TableHeader>
|
|
316
|
+
<TableRow className="border-b border-border bg-muted/50 hover:bg-muted/50">
|
|
317
|
+
<TableHead className="w-[50px] font-semibold text-muted-foreground text-xs uppercase tracking-wider text-center h-11">STT</TableHead>
|
|
318
|
+
<TableHead className="w-[30%] min-w-[250px] font-semibold text-muted-foreground text-xs uppercase tracking-wider h-11">Người dùng</TableHead>
|
|
319
|
+
<TableHead className="w-[15%] min-w-[150px] font-semibold text-muted-foreground text-xs uppercase tracking-wider h-11">Liên hệ</TableHead>
|
|
320
|
+
<TableHead className="w-[20%] min-w-[180px] font-semibold text-muted-foreground text-xs uppercase tracking-wider h-11">Phòng ban / Chi nhánh</TableHead>
|
|
321
|
+
<TableHead className="w-[25%] min-w-[200px] font-semibold text-muted-foreground text-xs uppercase tracking-wider h-11">Vai trò</TableHead>
|
|
322
|
+
<TableHead className="w-[60px] text-right h-11"></TableHead>
|
|
323
|
+
</TableRow>
|
|
324
|
+
</TableHeader>
|
|
325
|
+
<TableBody>
|
|
326
|
+
{paginatedData.map((user, index) => {
|
|
327
|
+
const isActive = user.isActive || user.status === "active";
|
|
328
|
+
const avatarColors = getAvatarColor(user.name);
|
|
329
|
+
|
|
330
|
+
return (
|
|
331
|
+
<TableRow key={user.id} className="border-b border-border/40 hover:bg-accent/10 transition-colors group">
|
|
332
|
+
{/* STT */}
|
|
333
|
+
<TableCell className="py-3 text-center text-muted-foreground text-xs font-medium align-middle">
|
|
334
|
+
{(currentPage - 1) * pageSize + index + 1}
|
|
335
|
+
</TableCell>
|
|
336
|
+
|
|
337
|
+
{/* User Info (Avatar + Name + Type + Status) */}
|
|
338
|
+
<TableCell className="py-3 align-top">
|
|
339
|
+
<div className="flex items-start gap-3">
|
|
340
|
+
<div className="relative shrink-0 mt-0.5">
|
|
341
|
+
<Avatar className="h-10 w-10 rounded-full border shadow-sm">
|
|
342
|
+
<AvatarImage src={user.image || user.avatar || ""} alt={user.name || ""} className="object-cover rounded-full" />
|
|
343
|
+
<AvatarFallback className={cn("text-xs font-bold rounded-full", avatarColors.bg, avatarColors.text)}>
|
|
344
|
+
{getInitials(user.name || user.email)}
|
|
345
|
+
</AvatarFallback>
|
|
346
|
+
</Avatar>
|
|
347
|
+
{/* Note: Status indicator is now a badge next to the name */}
|
|
348
|
+
</div>
|
|
349
|
+
|
|
350
|
+
<div className="flex flex-col min-w-0">
|
|
351
|
+
<div className="flex items-center gap-1.5 mb-1 flex-wrap">
|
|
352
|
+
<span className="font-bold text-sm text-foreground truncate mr-1">{user.name || "Chưa đặt tên"}</span>
|
|
353
|
+
|
|
354
|
+
{/* Status Badge */}
|
|
355
|
+
{isActive ? (
|
|
356
|
+
<Badge className="text-[9px] h-4 px-1.5 font-bold bg-emerald-100 text-emerald-700 hover:bg-emerald-200 border-transparent uppercase tracking-widest shrink-0 rounded-full">
|
|
357
|
+
<div className="w-1.5 h-1.5 rounded-full bg-emerald-600 mr-1.5" />
|
|
358
|
+
Hoạt động
|
|
359
|
+
</Badge>
|
|
360
|
+
) : (
|
|
361
|
+
<Badge className="text-[9px] h-4 px-1.5 font-bold bg-slate-100 text-slate-600 hover:bg-slate-200 border-transparent uppercase tracking-widest shrink-0 rounded-full">
|
|
362
|
+
<div className="w-1.5 h-1.5 rounded-full bg-slate-400 mr-1.5" />
|
|
363
|
+
Đã khóa
|
|
364
|
+
</Badge>
|
|
365
|
+
)}
|
|
366
|
+
|
|
367
|
+
{/* Type Badge */}
|
|
368
|
+
{user.userType === "customer" && <Badge className="text-[9px] h-4 px-1.5 font-bold bg-emerald-100 text-emerald-700 hover:bg-emerald-200 border-transparent uppercase tracking-widest shrink-0 rounded-full">Khách hàng</Badge>}
|
|
369
|
+
{user.userType === "supplier" && <Badge className="text-[9px] h-4 px-1.5 font-bold bg-orange-100 text-orange-700 hover:bg-orange-200 border-transparent uppercase tracking-widest shrink-0 rounded-full">Nhà cung cấp</Badge>}
|
|
370
|
+
{(!user.userType || user.userType === "employee") && <Badge className="text-[9px] h-4 px-1.5 font-bold bg-indigo-100 text-indigo-700 hover:bg-indigo-200 border-transparent uppercase tracking-widest shrink-0 rounded-full">Nhân viên</Badge>}
|
|
371
|
+
</div>
|
|
372
|
+
<span className="text-xs text-muted-foreground truncate font-medium">{user.email || "—"}</span>
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
</TableCell>
|
|
376
|
+
|
|
377
|
+
{/* Contact */}
|
|
378
|
+
<TableCell className="py-3 align-top">
|
|
379
|
+
<div className="flex items-center gap-1.5 mt-1">
|
|
380
|
+
<Phone className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
|
381
|
+
<span className="text-sm font-medium text-foreground/80">{user.profiles?.phone || user.phone || "—"}</span>
|
|
382
|
+
</div>
|
|
383
|
+
</TableCell>
|
|
384
|
+
|
|
385
|
+
{/* Department & Branch */}
|
|
386
|
+
<TableCell className="py-3 align-top">
|
|
387
|
+
<div className="flex flex-col gap-1.5 mt-1">
|
|
388
|
+
<div className="flex items-center gap-1.5">
|
|
389
|
+
<Briefcase className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
|
390
|
+
<span className="text-sm font-semibold text-foreground/80 truncate max-w-[200px]" title={user.departmentName}>{user.departmentName || "—"}</span>
|
|
391
|
+
</div>
|
|
392
|
+
<div className="flex items-center gap-1.5">
|
|
393
|
+
<Building2 className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
|
394
|
+
<span className="text-xs text-muted-foreground truncate max-w-[200px]" title={(user.branchNames && user.branchNames.length > 0) ? user.branchNames.join(", ") : (user.branchName || user.branch?.name || "")}>
|
|
395
|
+
{(user.branchNames && user.branchNames.length > 0) ? user.branchNames.join(", ") : (user.branchName || user.branch?.name || "—")}
|
|
396
|
+
</span>
|
|
397
|
+
</div>
|
|
398
|
+
</div>
|
|
399
|
+
</TableCell>
|
|
400
|
+
|
|
401
|
+
{/* Roles */}
|
|
402
|
+
<TableCell className="py-3 align-top">
|
|
403
|
+
<div className="flex flex-wrap gap-1 mt-0.5">
|
|
404
|
+
{user.roleNames && user.roleNames.length > 0 ? (
|
|
405
|
+
user.roleNames.map((r: string, i: number) => (
|
|
406
|
+
<Badge
|
|
407
|
+
key={i}
|
|
408
|
+
className="text-[10px] px-2 py-0.5 font-medium bg-slate-100 text-slate-700 border-transparent hover:bg-slate-200 truncate max-w-[140px]"
|
|
409
|
+
title={r}
|
|
410
|
+
>
|
|
411
|
+
{r}
|
|
412
|
+
</Badge>
|
|
413
|
+
))
|
|
414
|
+
) : (
|
|
415
|
+
<span className="text-xs text-muted-foreground italic">—</span>
|
|
416
|
+
)}
|
|
417
|
+
</div>
|
|
418
|
+
</TableCell>
|
|
419
|
+
|
|
420
|
+
{/* Actions */}
|
|
421
|
+
<TableCell className="py-3 text-right align-middle">
|
|
422
|
+
<Button
|
|
423
|
+
variant="ghost"
|
|
424
|
+
size="icon"
|
|
425
|
+
className="h-8 w-8 rounded-full text-muted-foreground hover:text-foreground hover:bg-background shadow-sm opacity-0 group-hover:opacity-100 transition-all focus:opacity-100"
|
|
426
|
+
onClick={(e: React.MouseEvent) => {
|
|
427
|
+
e.stopPropagation();
|
|
428
|
+
onEdit(user);
|
|
429
|
+
}}
|
|
430
|
+
>
|
|
431
|
+
<Edit2 className="h-4 w-4" />
|
|
432
|
+
</Button>
|
|
433
|
+
</TableCell>
|
|
434
|
+
</TableRow>
|
|
435
|
+
);
|
|
436
|
+
})}
|
|
437
|
+
</TableBody>
|
|
438
|
+
</Table>
|
|
439
|
+
</div>
|
|
375
440
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
441
|
+
{/* Pagination */}
|
|
442
|
+
{totalPages > 1 && (
|
|
443
|
+
<div className="flex items-center justify-between px-4 py-3 bg-muted/20 border-t border-border">
|
|
444
|
+
<p className="text-xs text-muted-foreground font-medium">
|
|
445
|
+
Hiển thị từ <span className="font-bold text-foreground">{(currentPage - 1) * pageSize + 1}</span> đến <span className="font-bold text-foreground">{Math.min(currentPage * pageSize, data.length)}</span> / <span className="font-bold text-foreground">{data.length}</span>
|
|
446
|
+
</p>
|
|
447
|
+
<div className="flex items-center gap-1.5">
|
|
448
|
+
<Button
|
|
449
|
+
variant="outline"
|
|
450
|
+
size="sm"
|
|
451
|
+
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
|
|
452
|
+
disabled={currentPage === 1}
|
|
453
|
+
className="h-7 text-xs px-2.5 rounded-full bg-card"
|
|
454
|
+
>
|
|
455
|
+
Trang trước
|
|
456
|
+
</Button>
|
|
457
|
+
<div className="text-xs font-semibold text-foreground px-2">
|
|
458
|
+
{currentPage} / {totalPages}
|
|
459
|
+
</div>
|
|
460
|
+
<Button
|
|
461
|
+
variant="outline"
|
|
462
|
+
size="sm"
|
|
463
|
+
onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
|
|
464
|
+
disabled={currentPage === totalPages}
|
|
465
|
+
className="h-7 text-xs px-2.5 rounded-full bg-card"
|
|
466
|
+
>
|
|
467
|
+
Trang sau
|
|
468
|
+
</Button>
|
|
394
469
|
</div>
|
|
395
|
-
<Button
|
|
396
|
-
variant="outline"
|
|
397
|
-
size="sm"
|
|
398
|
-
onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
|
|
399
|
-
disabled={currentPage === totalPages}
|
|
400
|
-
className="h-8 rounded-full"
|
|
401
|
-
>
|
|
402
|
-
Trang sau
|
|
403
|
-
</Button>
|
|
404
470
|
</div>
|
|
405
|
-
|
|
406
|
-
)}
|
|
471
|
+
)}
|
|
407
472
|
</div>
|
|
408
473
|
);
|
|
409
474
|
}
|