@goplusvn/core 0.1.43 → 0.1.44

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/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.44 — UnifiedProfileDialog: chi nhánh mặc định + ngày sinh DatePicker + fix Select loại người dùng
4
+
5
+ **Ngày sinh dùng `DatePicker` (thay `<input type="date">`):** ô ngày sinh cũ khó
6
+ nhập tay và không đồng bộ style; nay dùng `DatePicker` (gõ được `dd/MM/yyyy` +
7
+ lịch popover). Cầu nối 2 chiều `ymdToDate`/`dateToYmd` giữ nguyên định dạng lưu
8
+ `"yyyy-MM-dd"`, neo giờ 00:00 địa phương để tránh lệch ngày do timezone.
9
+
10
+ **Chi nhánh mặc định (`UserBranch.isDefault`):**
11
+
12
+ - Tab "Công việc" của dialog người dùng: mỗi chi nhánh đã chọn có nút "⭐ Đặt
13
+ mặc định"; chi nhánh mặc định hiện badge "Mặc định". Chọn chi nhánh đầu tiên
14
+ tự động thành mặc định; bỏ chọn chi nhánh đang là mặc định thì tự thăng chi
15
+ nhánh còn lại kế tiếp.
16
+ - Payload gửi thêm `defaultBranchId` (đã lọc để luôn nằm trong `branchIds`, fallback
17
+ `branchIds[0]`). App đọc field này thay cho hard-code `isDefault: index === 0`.
18
+ - Đọc lại `data.defaultBranchId` khi mở dialog ở chế độ sửa (fallback `branchIds[0]`).
19
+
20
+ **Fix Select "Loại người dùng" icon + chữ rớt 2 dòng:** base `SelectTrigger` của
21
+ core có `[&_span]:line-clamp-none` — selector này (`.trigger span`, độ ưu tiên
22
+ `0,1,1`) biên dịch kèm `display:block`, THẮNG class `inline-flex` (`0,1,0`) trên
23
+ span icon+chữ bên trong, nên (cộng với Preflight `svg{display:block}`) icon bị
24
+ đẩy xuống dòng riêng. Nới chiều ngang KHÔNG chữa vì đây là lỗi xếp dọc. Fix:
25
+ trigger dùng `[&_span]:!inline-flex [&_span]:!items-center [&_span]:!gap-2`
26
+ (`!important` thắng tuyệt đối `line-clamp-none` bất kể thứ tự stylesheet) +
27
+ `[&_svg]:shrink-0`, `whitespace-nowrap`, `style={{width:210}}`. Kiểm chứng bằng
28
+ repro Radix+Tailwind: span trong đổi `display` từ `block` → `flex`.
29
+
3
30
  ## 0.1.34 — Tab navigation: gom nhóm module, tiêu đề tab chi tiết, nút làm tươi
4
31
 
5
32
  **PageTabs:**
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.43",
4
+ "version": "0.1.44",
5
5
  "private": false,
6
6
  "publishConfig": {
7
7
  "registry": "https://registry.npmjs.org",
@@ -21,6 +21,7 @@ import {
21
21
  SelectTrigger,
22
22
  SelectValue,
23
23
  Switch,
24
+ DatePicker,
24
25
  } from "../../ui";
25
26
  import { toast } from "sonner";
26
27
  import { cn } from "../../utils";
@@ -38,6 +39,7 @@ import {
38
39
  Briefcase,
39
40
  ShieldCheck,
40
41
  KeyRound,
42
+ Star,
41
43
  } from "lucide-react";
42
44
 
43
45
  interface UnifiedProfileDialogProps {
@@ -59,6 +61,22 @@ const fetcher = async (url: string) => {
59
61
  return Array.isArray(data) ? data : data.items || data.data || [];
60
62
  };
61
63
 
64
+ // birthday được lưu/gửi dưới dạng chuỗi "yyyy-MM-dd" (như input date cũ).
65
+ // DatePicker làm việc với Date nên cần cầu nối 2 chiều, tránh lệch ngày do TZ
66
+ // bằng cách neo giờ 00:00 địa phương và tự dựng chuỗi (không dùng toISOString).
67
+ const ymdToDate = (s?: string): Date | undefined => {
68
+ if (!s) return undefined;
69
+ const d = new Date(`${s}T00:00:00`);
70
+ return isNaN(d.getTime()) ? undefined : d;
71
+ };
72
+ const dateToYmd = (d?: Date): string => {
73
+ if (!d) return "";
74
+ const y = d.getFullYear();
75
+ const m = String(d.getMonth() + 1).padStart(2, "0");
76
+ const day = String(d.getDate()).padStart(2, "0");
77
+ return `${y}-${m}-${day}`;
78
+ };
79
+
62
80
  export function UnifiedProfileDialog({
63
81
  open,
64
82
  onOpenChange,
@@ -102,6 +120,7 @@ export function UnifiedProfileDialog({
102
120
  const [profileData, setProfileData] = useState<any>({});
103
121
  const [selectedRoles, setSelectedRoles] = useState<string[]>([]);
104
122
  const [selectedBranches, setSelectedBranches] = useState<string[]>([]);
123
+ const [defaultBranchId, setDefaultBranchId] = useState<string | null>(null);
105
124
  const [passwordData, setPasswordData] = useState({
106
125
  password: "",
107
126
  confirm: "",
@@ -173,6 +192,7 @@ export function UnifiedProfileDialog({
173
192
  });
174
193
  setSelectedRoles([]);
175
194
  setSelectedBranches([]);
195
+ setDefaultBranchId(null);
176
196
  setPasswordData({ password: "", confirm: "" });
177
197
  setEnableAccount(false);
178
198
  } else {
@@ -203,6 +223,9 @@ export function UnifiedProfileDialog({
203
223
  const roles = d.roleCodes || [];
204
224
  setSelectedRoles(roles);
205
225
  setSelectedBranches(d.branchIds || []);
226
+ setDefaultBranchId(
227
+ d.defaultBranchId || (d.branchIds && d.branchIds[0]) || null,
228
+ );
206
229
  setPasswordData({ password: "", confirm: "" });
207
230
  // Enable account if user has roles or if it's not a customer (employees always have accounts?)
208
231
  // For now, if roles exist, we assume account is enabled.
@@ -216,11 +239,39 @@ export function UnifiedProfileDialog({
216
239
  if (mode === "create" || (data?.branchIds?.length || 0) === 0) {
217
240
  if (selectedBranches.length === 0) {
218
241
  setSelectedBranches([branches[0].id]);
242
+ setDefaultBranchId(branches[0].id);
219
243
  }
220
244
  }
221
245
  }
222
246
  }, [open, branches, mode, data]);
223
247
 
248
+ // -- Branch selection helpers --
249
+ // Toggle a branch's membership while keeping a valid default branch.
250
+ const toggleBranch = (branchId: string) => {
251
+ if (selectedBranches.includes(branchId)) {
252
+ const next = selectedBranches.filter((id) => id !== branchId);
253
+ setSelectedBranches(next);
254
+ // Removing the current default → promote the first remaining branch.
255
+ if (defaultBranchId === branchId) {
256
+ setDefaultBranchId(next[0] ?? null);
257
+ }
258
+ } else {
259
+ setSelectedBranches([...selectedBranches, branchId]);
260
+ // First branch selected becomes the default automatically.
261
+ if (!defaultBranchId) {
262
+ setDefaultBranchId(branchId);
263
+ }
264
+ }
265
+ };
266
+
267
+ // Mark a (selected) branch as the default working branch.
268
+ const markDefaultBranch = (branchId: string) => {
269
+ if (!selectedBranches.includes(branchId)) {
270
+ setSelectedBranches((prev) => [...prev, branchId]);
271
+ }
272
+ setDefaultBranchId(branchId);
273
+ };
274
+
224
275
  // -- Helpers --
225
276
  const getInitials = (name: string | null) => {
226
277
  if (!name) return "U";
@@ -254,6 +305,10 @@ export function UnifiedProfileDialog({
254
305
  userType, // Add userType to payload
255
306
  roleCodes: selectedRoles,
256
307
  branchIds: selectedBranches,
308
+ defaultBranchId:
309
+ defaultBranchId && selectedBranches.includes(defaultBranchId)
310
+ ? defaultBranchId
311
+ : (selectedBranches[0] ?? null),
257
312
  };
258
313
 
259
314
  // Handle Password for Customer
@@ -394,25 +449,41 @@ export function UnifiedProfileDialog({
394
449
  onValueChange={(val) => setUserType(val)}
395
450
  disabled={mode === "view"}
396
451
  >
397
- <SelectTrigger className="w-[140px] bg-white dark:bg-[#1e293b] border-[#e2e8f0] dark:border-[#334155] focus:ring-[#1641CE] focus:border-[#1641CE] h-9 rounded-md">
452
+ <SelectTrigger
453
+ style={{ width: 210 }}
454
+ // Base SelectTrigger có `[&_span]:line-clamp-none` — selector này biên
455
+ // dịch kèm `display:block` với độ ưu tiên (0,1,1) cao hơn `inline-flex`
456
+ // (0,1,0) của span icon+chữ bên trong, nên icon (svg Preflight display:block)
457
+ // bị đẩy xuống dòng riêng. `!inline-flex` (!important) thắng tuyệt đối.
458
+ className="shrink-0 h-9 min-h-0 whitespace-nowrap [&_span]:!inline-flex [&_span]:!items-center [&_span]:!gap-2 [&_span]:overflow-hidden [&_svg]:shrink-0 bg-white dark:bg-[#1e293b] border-[#e2e8f0] dark:border-[#334155] focus:ring-[#1641CE] focus:border-[#1641CE] rounded-md"
459
+ >
398
460
  <SelectValue placeholder="Chọn loại" />
399
461
  </SelectTrigger>
400
462
  <SelectContent>
401
463
  <SelectItem value="employee">
402
- <span className="flex items-center gap-2">
403
- <UserCog className="h-4 w-4 text-[#1641CE]" />
464
+ <span
465
+ className="inline-flex items-center gap-2"
466
+ style={{ whiteSpace: "nowrap" }}
467
+ >
468
+ <UserCog className="h-4 w-4 text-[#1641CE] shrink-0" />
404
469
  Nhân viên
405
470
  </span>
406
471
  </SelectItem>
407
472
  <SelectItem value="customer">
408
- <span className="flex items-center gap-2">
409
- <User className="h-4 w-4 text-[#059669]" />
473
+ <span
474
+ className="inline-flex items-center gap-2"
475
+ style={{ whiteSpace: "nowrap" }}
476
+ >
477
+ <User className="h-4 w-4 text-[#059669] shrink-0" />
410
478
  Khách hàng
411
479
  </span>
412
480
  </SelectItem>
413
481
  <SelectItem value="supplier">
414
- <span className="flex items-center gap-2">
415
- <Store className="h-4 w-4 text-[#EA580C]" />
482
+ <span
483
+ className="inline-flex items-center gap-2"
484
+ style={{ whiteSpace: "nowrap" }}
485
+ >
486
+ <Store className="h-4 w-4 text-[#EA580C] shrink-0" />
416
487
  Nhà cung cấp
417
488
  </span>
418
489
  </SelectItem>
@@ -587,16 +658,17 @@ export function UnifiedProfileDialog({
587
658
  {/* Additional Info for All */}
588
659
  <div className="space-y-1.5">
589
660
  <Label className="text-[13px] font-medium text-[#334155] dark:text-[#cbd5e1]">Ngày sinh</Label>
590
- <Input
591
- type="date"
592
- value={profileData.birthday}
593
- onChange={(e) =>
661
+ <DatePicker
662
+ value={ymdToDate(profileData.birthday)}
663
+ onValueChange={(d) =>
594
664
  setProfileData({
595
665
  ...profileData,
596
- birthday: e.target.value,
666
+ birthday: dateToYmd(d),
597
667
  })
598
668
  }
599
- className="bg-white dark:bg-[#1e293b] border-[#e2e8f0] dark:border-[#334155] focus:ring-primary focus:border-primary rounded-md h-9 text-[13px]"
669
+ formatStr="dd/MM/yyyy"
670
+ placeholder="dd/mm/yyyy"
671
+ buttonClassName="bg-white dark:bg-[#1e293b] border-[#e2e8f0] dark:border-[#334155] focus:ring-primary focus:border-primary rounded-md h-9 text-[13px]"
600
672
  />
601
673
  </div>
602
674
  <div className="space-y-1.5">
@@ -816,34 +888,56 @@ export function UnifiedProfileDialog({
816
888
  </Select>
817
889
  </div>
818
890
  <div className="col-span-2 space-y-3 pt-2">
819
- <Label className="text-[13px] font-medium text-[#41454d] dark:text-[#9297a0]">Chi nhánh làm việc</Label>
891
+ <div className="flex items-center justify-between gap-3 flex-wrap">
892
+ <Label className="text-[13px] font-medium text-[#41454d] dark:text-[#9297a0]">Chi nhánh làm việc</Label>
893
+ <span className="inline-flex items-center gap-1 text-[12px] text-[#94a3b8]">
894
+ <Star className="h-3 w-3" />
895
+ Đánh dấu một chi nhánh mặc định
896
+ </span>
897
+ </div>
820
898
  <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
821
899
  {branches?.map((branch) => {
822
900
  const isSelected = selectedBranches.includes(branch.id);
901
+ const isDefault =
902
+ isSelected && defaultBranchId === branch.id;
823
903
  return (
824
904
  <div
825
905
  key={branch.id}
826
906
  className={cn(
827
- "flex items-center gap-3 p-3 rounded-md border cursor-pointer hover:bg-slate-50 transition-colors",
907
+ "flex items-center gap-3 p-3 rounded-md border cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-800/40 transition-colors",
828
908
  isSelected
829
- ? "border-blue-500 bg-blue-50/50"
909
+ ? "border-blue-500 bg-blue-50/50 dark:bg-blue-500/10"
830
910
  : "bg-background border-slate-200 dark:border-slate-800",
831
911
  )}
832
- onClick={() => {
833
- setSelectedBranches((prev) =>
834
- prev.includes(branch.id)
835
- ? prev.filter((id) => id !== branch.id)
836
- : [...prev, branch.id],
837
- );
838
- }}
912
+ onClick={() => toggleBranch(branch.id)}
839
913
  >
840
914
  <Checkbox
841
915
  checked={isSelected}
842
916
  className="data-[state=checked]:bg-blue-600 data-[state=checked]:border-blue-600"
843
917
  />
844
- <span className="text-sm font-medium text-slate-900 dark:text-slate-100">
918
+ <span className="flex-1 text-sm font-medium text-slate-900 dark:text-slate-100">
845
919
  {branch.name}
846
920
  </span>
921
+ {isSelected &&
922
+ (isDefault ? (
923
+ <span className="inline-flex items-center gap-1 rounded-full bg-blue-600 px-2 py-0.5 text-[11px] font-semibold text-white shrink-0">
924
+ <Star className="h-3 w-3 fill-current" />
925
+ Mặc định
926
+ </span>
927
+ ) : (
928
+ <button
929
+ type="button"
930
+ onClick={(e) => {
931
+ e.stopPropagation();
932
+ markDefaultBranch(branch.id);
933
+ }}
934
+ title="Đặt làm chi nhánh mặc định"
935
+ className="inline-flex items-center gap-1 rounded-full border border-slate-200 dark:border-slate-700 px-2 py-0.5 text-[11px] font-medium text-slate-500 dark:text-slate-400 hover:border-blue-400 hover:text-blue-600 transition-colors shrink-0"
936
+ >
937
+ <Star className="h-3 w-3" />
938
+ Đặt mặc định
939
+ </button>
940
+ ))}
847
941
  </div>
848
942
  );
849
943
  })}