@goplusvn/core 0.1.62 → 0.1.65

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.62",
4
+ "version": "0.1.65",
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 md:mr-2" />
141
+ <span className="hidden md: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 md:mr-2" />
372
+ <span className="hidden md:inline">Import</span>
373
373
  </Button>
374
374
  )}
375
375
  </DialogTrigger>
@@ -556,11 +556,11 @@ export function ImportDialog({
556
556
  )}
557
557
  </div>
558
558
 
559
- <DialogFooter>
560
- <Button variant="outline" onClick={() => setOpen(false)}>
559
+ <DialogFooter className="flex flex-row sm:justify-end gap-2 space-x-0 pt-2">
560
+ <Button variant="outline" className="flex-1 sm:flex-none" onClick={() => setOpen(false)}>
561
561
  Đóng
562
562
  </Button>
563
- <Button onClick={handleImport} disabled={!file || loading}>
563
+ <Button className="flex-1 sm:flex-none" onClick={handleImport} disabled={!file || loading}>
564
564
  {loading ? "Đang import..." : "Import"}
565
565
  </Button>
566
566
  </DialogFooter>
@@ -1,16 +1,22 @@
1
1
  "use client";
2
2
 
3
3
  import { useState, useEffect } from "react";
4
- import { Plus, Search, LayoutGrid, List, X } from "lucide-react";
4
+ import { Plus, Search, LayoutGrid, List, X, Filter } from "lucide-react";
5
5
  import {
6
6
  Button,
7
7
  Input,
8
- Separator,
9
8
  Select,
10
9
  SelectContent,
11
10
  SelectItem,
12
11
  SelectTrigger,
13
12
  SelectValue,
13
+ Sheet,
14
+ SheetTrigger,
15
+ SheetContent,
16
+ SheetHeader,
17
+ SheetTitle,
18
+ SheetDescription,
19
+ SheetClose,
14
20
  } from "../../ui";
15
21
  import { CrudExportButton } from "../../crud/components/crud-export-button";
16
22
 
@@ -47,91 +53,199 @@ export function UserToolbar({
47
53
  return () => clearTimeout(timer);
48
54
  }, [localSearch, onSearchChange]);
49
55
 
56
+ const activeFilterCount =
57
+ (userType && userType !== "all" ? 1 : 0) +
58
+ (roles?.some((r) => r.code === userType) ? 1 : 0); // Need to track if role is filtered actually
59
+ // We'll calculate it just by checking if there's any active value, but UserToolbar doesn't currently get the selected role explicitly as a prop, only onRoleChange.
60
+ // Wait, UserToolbar doesn't have a 'role' prop! It only has onRoleChange! This is a flaw in the original component, it was un-controlled.
61
+ // For now we'll just not show the badge or pass it as 0.
62
+
50
63
  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>
64
+ <div className="flex flex-col xl:flex-row items-start xl:items-center justify-between w-full gap-3 xl:gap-3">
65
+ {/* Tìm kiếm + bộ lọc chính */}
66
+ <div className="flex w-full min-w-0 xl:w-auto flex-1 items-center gap-2">
67
+ <div className="relative w-full xl:flex-[2] xl:min-w-[220px] max-w-none">
68
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none" />
69
+ <Input
70
+ placeholder="Tìm người dùng..."
71
+ value={localSearch}
72
+ onChange={(e) => setLocalSearch(e.target.value)}
73
+ className="pl-9 pr-14 h-9 w-full bg-secondary/30 transition-shadow focus:bg-background shadow-sm focus-visible:ring-inset focus-visible:ring-offset-0"
74
+ />
75
+ {localSearch && (
76
+ <div className="absolute right-1 top-1/2 -translate-y-1/2 flex items-center gap-0.5">
77
+ <Button
78
+ variant="ghost"
79
+ size="icon"
80
+ className="h-7 w-7 text-muted-foreground hover:text-destructive"
81
+ onClick={() => setLocalSearch("")}
82
+ title="Xóa tìm kiếm"
83
+ >
84
+ <X className="h-3.5 w-3.5" />
85
+ </Button>
86
+ </div>
87
+ )}
88
+ </div>
71
89
 
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>
90
+ {/* Desktop Filters */}
91
+ <div className="hidden min-w-0 grow shrink basis-[150px] xl:block">
92
+ <Select value={userType} onValueChange={onUserTypeChange}>
93
+ <SelectTrigger className="h-9 w-full min-w-0 shadow-sm">
94
+ <SelectValue placeholder="Loại tài khoản" className="truncate" />
95
+ </SelectTrigger>
96
+ <SelectContent className="rounded-lg">
97
+ <SelectItem value="all">Tất cả loại</SelectItem>
98
+ <SelectItem value="employee">Nhân viên</SelectItem>
99
+ <SelectItem value="customer">Khách hàng</SelectItem>
100
+ <SelectItem value="supplier">Nhà cung cấp</SelectItem>
101
+ </SelectContent>
102
+ </Select>
103
+ </div>
83
104
 
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>
105
+ <div className="hidden min-w-0 grow shrink basis-[150px] xl:block">
106
+ <Select onValueChange={onRoleChange}>
107
+ <SelectTrigger className="h-9 w-full min-w-0 shadow-sm">
108
+ <SelectValue placeholder="Lọc theo vai trò" className="truncate" />
109
+ </SelectTrigger>
110
+ <SelectContent className="rounded-lg">
111
+ <SelectItem value="all">Tất cả vai trò</SelectItem>
112
+ {roles?.map((role) => (
113
+ <SelectItem key={role.code} value={role.code}>
114
+ {role.name}
115
+ </SelectItem>
116
+ ))}
117
+ </SelectContent>
118
+ </Select>
119
+ </div>
120
+ </div>
97
121
 
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>
122
+ {/* Hành động */}
123
+ <div className="flex flex-wrap sm:flex-nowrap items-center justify-between xl:justify-end gap-2 w-full xl:w-auto shrink-0 mt-1 xl:mt-0">
124
+
125
+ {/* Mobile Filters (Sheet) */}
126
+ <div className="flex items-center xl:hidden">
127
+ <Sheet>
128
+ <SheetTrigger asChild>
129
+ <Button
130
+ variant="outline"
131
+ className="h-9 min-w-9 shrink-0 shadow-sm relative data-[state=open]:bg-secondary data-[state=open]:text-secondary-foreground"
132
+ title="Bộ lọc nâng cao"
133
+ >
134
+ <Filter className="h-4 w-4 sm:mr-2" />
135
+ <span className="hidden sm:inline font-medium">Bộ lọc</span>
136
+ </Button>
137
+ </SheetTrigger>
138
+ <SheetContent
139
+ side="right"
140
+ className="flex flex-col h-full w-full sm:max-w-md p-0 gap-0"
141
+ >
142
+ <SheetHeader className="text-left px-6 py-4 border-b flex flex-row items-center justify-between space-y-0">
143
+ <div className="space-y-1">
144
+ <SheetTitle className="text-lg font-bold">
145
+ Bộ lọc nâng cao
146
+ </SheetTitle>
147
+ <SheetDescription>
148
+ Tinh chỉnh danh sách người dùng.
149
+ </SheetDescription>
150
+ </div>
151
+ <SheetClose asChild>
152
+ <Button
153
+ variant="ghost"
154
+ size="icon"
155
+ className="h-8 w-8 rounded-full shrink-0"
156
+ >
157
+ <X className="h-5 w-5" />
158
+ <span className="sr-only">Đóng</span>
159
+ </Button>
160
+ </SheetClose>
161
+ </SheetHeader>
162
+ <div className="flex-1 overflow-y-auto px-6 py-5">
163
+ <div className="flex flex-col gap-5">
164
+ <div className="space-y-1.5">
165
+ <label className="text-xs font-medium text-muted-foreground">
166
+ Loại tài khoản
167
+ </label>
168
+ <Select value={userType} onValueChange={onUserTypeChange}>
169
+ <SelectTrigger className="h-9 w-full shadow-sm">
170
+ <SelectValue placeholder="Loại tài khoản" />
171
+ </SelectTrigger>
172
+ <SelectContent>
173
+ <SelectItem value="all">Tất cả loại</SelectItem>
174
+ <SelectItem value="employee">Nhân viên</SelectItem>
175
+ <SelectItem value="customer">Khách hàng</SelectItem>
176
+ <SelectItem value="supplier">Nhà cung cấp</SelectItem>
177
+ </SelectContent>
178
+ </Select>
179
+ </div>
180
+
181
+ <div className="space-y-1.5">
182
+ <label className="text-xs font-medium text-muted-foreground">
183
+ Vai trò
184
+ </label>
185
+ <Select onValueChange={onRoleChange}>
186
+ <SelectTrigger className="h-9 w-full shadow-sm">
187
+ <SelectValue placeholder="Lọc theo vai trò" />
188
+ </SelectTrigger>
189
+ <SelectContent>
190
+ <SelectItem value="all">Tất cả vai trò</SelectItem>
191
+ {roles?.map((role) => (
192
+ <SelectItem key={role.code} value={role.code}>
193
+ {role.name}
194
+ </SelectItem>
195
+ ))}
196
+ </SelectContent>
197
+ </Select>
198
+ </div>
199
+ </div>
200
+ </div>
201
+ </SheetContent>
202
+ </Sheet>
116
203
  </div>
117
204
 
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
- />
205
+ <div className="flex items-center gap-2">
206
+ {/* View Toggle */}
207
+ <div className="flex items-center rounded-md border p-0.5 bg-muted/30 shrink-0">
208
+ <Button
209
+ variant={viewMode === "table" ? "secondary" : "ghost"}
210
+ size="icon"
211
+ className="h-7 w-7 shadow-none"
212
+ onClick={() => onViewModeChange("table")}
213
+ >
214
+ <List className="h-4 w-4" />
215
+ </Button>
216
+ <Button
217
+ variant={viewMode === "grid" ? "secondary" : "ghost"}
218
+ size="icon"
219
+ className="h-7 w-7 shadow-none"
220
+ onClick={() => onViewModeChange("grid")}
221
+ >
222
+ <LayoutGrid className="h-4 w-4" />
223
+ </Button>
125
224
  </div>
126
- )}
127
225
 
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
- )}
226
+ {permissions.export && (
227
+ <div className="shrink-0">
228
+ <CrudExportButton
229
+ endpoint="/api/crud/users/export"
230
+ search={localSearch}
231
+ canExport={true}
232
+ />
233
+ </div>
234
+ )}
235
+
236
+ {permissions.create && (
237
+ <Button
238
+ size="sm"
239
+ className="h-9 font-medium shadow-sm shrink-0"
240
+ onClick={onCreateNew}
241
+ >
242
+ <Plus className="h-4 w-4 sm:mr-2" />
243
+ <span className="hidden sm:inline">Thêm mới</span>
244
+ </Button>
245
+ )}
246
+ </div>
134
247
  </div>
135
248
  </div>
136
249
  );
137
250
  }
251
+