@goplusvn/core 0.1.63 → 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.63",
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="h-4 w-4 sm:mr-2" />
141
- <span className="hidden sm:inline">{loading ? "Exporting..." : "Export"}</span>
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="h-4 w-4 sm:mr-2" />
372
- <span className="hidden sm:inline">Import</span>
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,33 +53,44 @@ 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 flex-col xl:flex-row items-start xl:items-center justify-between w-full gap-3 xl:gap-4">
64
+ <div className="flex flex-col xl:flex-row items-start xl:items-center justify-between w-full gap-3 xl:gap-3">
52
65
  {/* 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">
66
+ <div className="flex w-full min-w-0 xl:w-auto flex-1 items-center gap-2">
54
67
  <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" />
68
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none" />
56
69
  <Input
57
70
  placeholder="Tìm người dùng..."
58
71
  value={localSearch}
59
72
  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"
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"
61
74
  />
62
75
  {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>
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>
71
87
  )}
72
88
  </div>
73
89
 
74
- <div className="w-full xl:w-auto flex-1 min-w-0">
90
+ {/* Desktop Filters */}
91
+ <div className="hidden min-w-0 grow shrink basis-[150px] xl:block">
75
92
  <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">
93
+ <SelectTrigger className="h-9 w-full min-w-0 shadow-sm">
77
94
  <SelectValue placeholder="Loại tài khoản" className="truncate" />
78
95
  </SelectTrigger>
79
96
  <SelectContent className="rounded-lg">
@@ -85,9 +102,9 @@ export function UserToolbar({
85
102
  </Select>
86
103
  </div>
87
104
 
88
- <div className="w-full xl:w-auto flex-1 min-w-0">
105
+ <div className="hidden min-w-0 grow shrink basis-[150px] xl:block">
89
106
  <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">
107
+ <SelectTrigger className="h-9 w-full min-w-0 shadow-sm">
91
108
  <SelectValue placeholder="Lọc theo vai trò" className="truncate" />
92
109
  </SelectTrigger>
93
110
  <SelectContent className="rounded-lg">
@@ -103,21 +120,103 @@ export function UserToolbar({
103
120
  </div>
104
121
 
105
122
  {/* 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">
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>
203
+ </div>
204
+
107
205
  <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">
206
+ {/* View Toggle */}
207
+ <div className="flex items-center rounded-md border p-0.5 bg-muted/30 shrink-0">
109
208
  <Button
110
209
  variant={viewMode === "table" ? "secondary" : "ghost"}
111
- size="sm"
112
- className="h-8 w-8 p-0 rounded-md"
210
+ size="icon"
211
+ className="h-7 w-7 shadow-none"
113
212
  onClick={() => onViewModeChange("table")}
114
213
  >
115
214
  <List className="h-4 w-4" />
116
215
  </Button>
117
216
  <Button
118
217
  variant={viewMode === "grid" ? "secondary" : "ghost"}
119
- size="sm"
120
- className="h-8 w-8 p-0 rounded-md"
218
+ size="icon"
219
+ className="h-7 w-7 shadow-none"
121
220
  onClick={() => onViewModeChange("grid")}
122
221
  >
123
222
  <LayoutGrid className="h-4 w-4" />
@@ -125,7 +224,7 @@ export function UserToolbar({
125
224
  </div>
126
225
 
127
226
  {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">
227
+ <div className="shrink-0">
129
228
  <CrudExportButton
130
229
  endpoint="/api/crud/users/export"
131
230
  search={localSearch}
@@ -135,8 +234,12 @@ export function UserToolbar({
135
234
  )}
136
235
 
137
236
  {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" />
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" />
140
243
  <span className="hidden sm:inline">Thêm mới</span>
141
244
  </Button>
142
245
  )}
@@ -145,3 +248,4 @@ export function UserToolbar({
145
248
  </div>
146
249
  );
147
250
  }
251
+