@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 +27 -0
- package/package.json +1 -1
- package/src/user/components/unified-profile-dialog.tsx +118 -24
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
|
@@ -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
|
|
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
|
|
403
|
-
|
|
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
|
|
409
|
-
|
|
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
|
|
415
|
-
|
|
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
|
-
<
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
onChange={(e) =>
|
|
661
|
+
<DatePicker
|
|
662
|
+
value={ymdToDate(profileData.birthday)}
|
|
663
|
+
onValueChange={(d) =>
|
|
594
664
|
setProfileData({
|
|
595
665
|
...profileData,
|
|
596
|
-
birthday:
|
|
666
|
+
birthday: dateToYmd(d),
|
|
597
667
|
})
|
|
598
668
|
}
|
|
599
|
-
|
|
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
|
-
<
|
|
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
|
})}
|