@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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@goplusvn/core",
3
3
  "description": "GoPlusVN Platform Kit - ERP kernel: layout, RBAC, CRUD, multi-tenant, system pages",
4
- "version": "0.1.61",
4
+ "version": "0.1.63",
5
5
  "private": false,
6
6
  "publishConfig": {
7
7
  "registry": "https://registry.npmjs.org",
@@ -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="mr-2 h-4 w-4" />
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="mr-2 h-4 w-4" />
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-2 flex-wrap">
52
- <div className="relative">
53
- <Search className="absolute left-3 top-2.5 h-4 w-4 text-muted-foreground" />
54
- <Input
55
- placeholder="Tìm người dùng..."
56
- value={localSearch}
57
- onChange={(e) => setLocalSearch(e.target.value)}
58
- className="h-9 w-[220px] lg:w-[280px] pl-9 text-xs bg-background border hover:bg-muted/50 transition-colors shadow-sm rounded-md focus-visible:ring-1 focus-visible:ring-primary"
59
- />
60
- {localSearch && (
61
- <Button
62
- variant="ghost"
63
- size="sm"
64
- className="absolute right-1 top-1/2 -translate-y-1/2 h-7 w-7 p-0 rounded-md hover:bg-muted"
65
- onClick={() => setLocalSearch("")}
66
- >
67
- <X className="h-3.5 w-3.5" />
68
- </Button>
69
- )}
70
- </div>
71
-
72
- <Select value={userType} onValueChange={onUserTypeChange}>
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
- <Select onValueChange={onRoleChange}>
85
- <SelectTrigger className="h-9 w-[170px] text-xs bg-background border hover:bg-muted/50 transition-colors shadow-sm rounded-md focus-visible:ring-primary">
86
- <SelectValue placeholder="Lọc theo vai trò" />
87
- </SelectTrigger>
88
- <SelectContent className="rounded-lg">
89
- <SelectItem value="all">Tất cả vai trò</SelectItem>
90
- {roles?.map((role) => (
91
- <SelectItem key={role.code} value={role.code}>
92
- {role.name}
93
- </SelectItem>
94
- ))}
95
- </SelectContent>
96
- </Select>
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
- <div className="ml-auto flex items-center gap-2">
99
- <div className="flex items-center bg-[#f1f5f9] dark:bg-[#181d26] rounded-[6px] p-0.5 border border-[#dddddd] dark:border-[#333840]">
100
- <Button
101
- variant={viewMode === "table" ? "secondary" : "ghost"}
102
- size="sm"
103
- className="h-8 w-8 p-0 rounded-md"
104
- onClick={() => onViewModeChange("table")}
105
- >
106
- <List className="h-4 w-4" />
107
- </Button>
108
- <Button
109
- variant={viewMode === "grid" ? "secondary" : "ghost"}
110
- size="sm"
111
- className="h-8 w-8 p-0 rounded-md"
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
- {permissions.export && (
119
- <div className="[&>button]:h-9 [&>button]:rounded-md [&>button]:shadow-sm [&>button]:bg-background [&>button]:border [&>button]:text-foreground [&>button]:hover:bg-muted/50">
120
- <CrudExportButton
121
- endpoint="/api/crud/users/export"
122
- search={localSearch}
123
- canExport={true}
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
- {permissions.create && (
129
- <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" onClick={onCreateNew}>
130
- <Plus className="h-4 w-4 mr-1.5" />
131
- Thêm mới
132
- </Button>
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 { Mail, Phone, MoreHorizontal, Eye, Edit, User, Briefcase, Store, ShieldCheck, Hash, Building2 , KeyRound} from "lucide-react";
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-primary", text: "text-primary-foreground" }, // Primary
30
- { bg: "bg-emerald-600", text: "text-white" }, // Emerald
31
- { bg: "bg-orange-600", text: "text-white" }, // Orange
32
- { bg: "bg-violet-600", text: "text-white" }, // Violet
33
- { bg: "bg-cyan-600", text: "text-white" }, // Cyan
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-border rounded-xl bg-card p-8 text-center">
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, 1);
71
+ .slice(0, 2);
87
72
  };
88
73
 
89
74
  return (
90
- <div className="space-y-4">
91
- <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 pb-4">
92
- {data.map((user) => {
93
- const isActive = user.isActive || user.status === "active";
94
- const avatarColors = getAvatarColor(user.name);
95
-
96
- return (
97
- <Card
98
- key={user.id}
99
- className="group relative transition-all duration-200 border border-border bg-card cursor-pointer h-full flex flex-col rounded-xl overflow-hidden shadow-sm hover:shadow-md hover:border-primary/30"
100
- onClick={() => onSelect && onSelect(user)}
101
- >
102
- {/* Header */}
103
- <div className="p-3 pb-2 flex gap-2.5 items-start bg-muted border-b border-border">
104
- <div className="relative shrink-0 mt-0.5">
105
- <div
106
- className={cn(
107
- "size-8 rounded-full flex items-center justify-center font-semibold text-xs",
108
- avatarColors.bg, avatarColors.text
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
- {user.image || user.avatar ? (
112
- <img
113
- src={user.image || user.avatar}
114
- alt={user.name}
115
- className="w-full h-full object-cover rounded-full"
116
- />
117
- ) : (
118
- getInitials(user.name || user.email)
119
- )}
120
- </div>
121
- {/* Status Dot */}
122
- <div
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 w-2.5 h-2.5 border-2 border-card rounded-full z-10",
125
- isActive ? "bg-success-semantic" : "bg-text-tertiary"
126
- )}
127
- title={isActive ? "Hoạt động" : "Đã khóa"}
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-1 min-w-0">
132
- <div className="flex items-center gap-2 mb-0.5">
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
- <span className="text-[10px] px-1.5 py-0.5 font-semibold rounded bg-emerald-600 text-white shrink-0">
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
- <span className="text-[10px] px-1.5 py-0.5 font-semibold rounded bg-orange-600 text-white shrink-0">
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
- <span className="text-[10px] px-1.5 py-0.5 font-semibold rounded bg-primary/10 text-primary border border-primary/20 shrink-0">
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 text-xs text-muted-foreground">
159
- <span className="truncate leading-none" title={user.email}>
160
- {user.email || "—"}
161
- </span>
162
- </div>
163
- </div>
164
-
165
- {/* Actions */}
166
- <DropdownMenu modal={false}>
167
- <DropdownMenuTrigger asChild>
168
- <Button
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
- className="rounded-lg text-[13px] cursor-pointer"
184
+ title="Copy email"
208
185
  >
209
- <KeyRound className="size-3.5 mr-2 text-muted-foreground" />
210
- Quyền hiệu lực
211
- </DropdownMenuItem>
186
+ <Copy className="w-3 h-3" />
187
+ </Button>
212
188
  )}
213
- </DropdownMenuContent>
214
- </DropdownMenu>
189
+ </div>
190
+ </div>
215
191
  </div>
216
192
 
217
- {/* Body */}
218
- <div className="p-3 pt-0 flex-1 flex flex-col bg-card">
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
- {/* 2-Column Compact Grid */}
221
- <div className="grid grid-cols-2 gap-x-2 gap-y-1.5 mb-2 mt-2">
222
- <div className="flex items-center gap-1.5 min-w-0" title="Số điện thoại">
223
- <Phone className="size-3 text-muted-foreground shrink-0" strokeWidth={1.5} />
224
- <span className="text-xs text-muted-foreground truncate">
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
- <div className="flex items-center gap-1.5 min-w-0" title="Bộ phận">
237
- <Briefcase className="size-3 text-muted-foreground shrink-0" strokeWidth={1.5} />
238
- <span className="text-xs text-muted-foreground truncate">
239
- {user.departmentName || "—"}
240
- </span>
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
- <div className="flex items-center gap-1.5 min-w-0" title="Chi nhánh">
244
- <Building2 className="size-3 text-muted-foreground shrink-0" strokeWidth={1.5} />
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
- {/* Roles - Compact Format */}
252
- {user.roleNames && user.roleNames.length > 0 && (
253
- <div className="mt-auto pt-2.5 border-t border-border flex flex-wrap gap-1">
254
- {user.roleNames.map((role: string, i: number) => {
255
- const style = getRoleStyle(role);
256
- return (
257
- <span key={i} className={cn("text-[10px] px-1.5 py-0.5 font-medium rounded truncate max-w-[100px] inline-flex items-center border", style)}>
258
- {role}
259
- </span>
260
- );
261
- })}
262
- </div>
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
- </Card>
266
- );
267
- })}
268
- </div>
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 <div className="p-4 text-center">No data</div>;
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 gap-4">
284
- <div className="bg-background">
285
- <Table>
286
- <TableHeader>
287
- <TableRow className="text-sm border-b">
288
- <TableHead className="w-[50px] font-semibold text-foreground text-center h-10">STT</TableHead>
289
- <TableHead className="w-[20%] min-w-[150px] font-semibold text-foreground h-10">Người dùng</TableHead>
290
- <TableHead className="w-[20%] min-w-[150px] font-semibold text-foreground h-10">Liên hệ</TableHead>
291
- <TableHead className="w-[15%] min-w-[120px] font-semibold text-foreground h-10">Trạng thái</TableHead>
292
- <TableHead className="w-[15%] min-w-[120px] font-semibold text-foreground h-10">Phòng ban</TableHead>
293
- <TableHead className="w-[20%] min-w-[150px] font-semibold text-foreground h-10">Vai trò</TableHead>
294
- <TableHead className="w-[50px] text-center h-10"></TableHead>
295
- </TableRow>
296
- </TableHeader>
297
- <TableBody>
298
- {paginatedData.map((user, index) => (
299
- <TableRow key={user.id} className="border-b group">
300
- {/* STT */}
301
- <TableCell className="py-3 text-center text-muted-foreground font-medium text-xs">
302
- {(currentPage - 1) * pageSize + index + 1}
303
- </TableCell>
304
-
305
- {/* User Info */}
306
- <TableCell className="py-3">
307
- <div className="flex flex-col">
308
- <div className="flex items-center gap-2">
309
- <span className="font-semibold text-sm text-foreground">{user.name}</span>
310
- {user.userType === "customer" && <span className="text-xs text-foreground bg-muted border px-2.5 py-0.5 rounded-full font-semibold whitespace-nowrap">Khách hàng</span>}
311
- {user.userType === "supplier" && <span className="text-xs text-background bg-foreground px-2.5 py-0.5 rounded-full font-semibold whitespace-nowrap">Nhà cung cấp</span>}
312
- {(!user.userType || user.userType === "employee") && <span className="text-xs text-foreground bg-muted border px-2.5 py-0.5 rounded-full font-semibold whitespace-nowrap">Nhân viên</span>}
313
- </div>
314
- </div>
315
- </TableCell>
316
-
317
- {/* Contact */}
318
- <TableCell className="py-3">
319
- <div className="flex flex-col">
320
- <span className="text-sm text-foreground">{user.email}</span>
321
- {user.phone && <span className="text-sm text-muted-foreground">{user.phone}</span>}
322
- </div>
323
- </TableCell>
324
-
325
- {/* Status */}
326
- <TableCell className="py-3">
327
- <div className="flex items-center gap-1.5">
328
- <span className={`inline-flex items-center text-xs font-semibold px-2.5 py-1 rounded-full whitespace-nowrap ${
329
- user.isActive || user.status === "active"
330
- ? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400"
331
- : "bg-muted text-muted-foreground"
332
- }`}>
333
- {user.isActive || user.status === "active" ? "Hoạt động" : "Đã khóa"}
334
- </span>
335
- </div>
336
- </TableCell>
337
-
338
- {/* Department */}
339
- <TableCell className="py-3 text-sm text-foreground">
340
- {user.departmentName || ""}
341
- </TableCell>
342
-
343
- {/* Roles */}
344
- <TableCell className="py-3">
345
- <div className="flex flex-wrap gap-1.5">
346
- {user.roleNames?.map((r: string, i: number) => (
347
- <span
348
- key={i}
349
- className="inline-flex items-center bg-muted text-foreground border font-semibold text-xs px-2.5 py-1 rounded-full whitespace-nowrap"
350
- >
351
- {r}
352
- </span>
353
- ))}
354
- </div>
355
- </TableCell>
356
-
357
- {/* Actions */}
358
- <TableCell className="py-2.5 text-right">
359
- <Button
360
- variant="ghost"
361
- size="icon"
362
- onClick={(e: React.MouseEvent) => {
363
- e.stopPropagation();
364
- onEdit(user);
365
- }}
366
- >
367
- <Edit2 className="h-4 w-4" />
368
- </Button>
369
- </TableCell>
370
- </TableRow>
371
- ))}
372
- </TableBody>
373
- </Table>
374
- </div>
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
- {/* Pagination */}
377
- {totalPages > 1 && (
378
- <div className="flex items-center justify-between px-2 pt-2">
379
- <p className="text-sm text-muted-foreground">
380
- Hiển thị từ {(currentPage - 1) * pageSize + 1} đến {Math.min(currentPage * pageSize, data.length)} trong tổng số {data.length}
381
- </p>
382
- <div className="flex items-center gap-2">
383
- <Button
384
- variant="outline"
385
- size="sm"
386
- onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
387
- disabled={currentPage === 1}
388
- className="h-8 rounded-full"
389
- >
390
- Trang trước
391
- </Button>
392
- <div className="text-sm font-semibold text-foreground px-2">
393
- Trang {currentPage} / {totalPages}
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
- </div>
406
- )}
471
+ )}
407
472
  </div>
408
473
  );
409
474
  }