@goplusvn/core 0.1.87 → 0.1.89
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 +45 -0
- package/package.json +1 -1
- package/src/audit/__tests__/prisma-audit-extension.test.ts +113 -0
- package/src/audit/prisma-audit-extension.ts +41 -13
- package/src/configs/status.ts +67 -0
- package/src/cron/__tests__/cron-single-runner.test.ts +260 -0
- package/src/cron/cron-schedule.ts +113 -20
- package/src/cron/db-cron-manager.ts +90 -7
- package/src/cron/simple-cron-job.ts +17 -6
- package/src/crud/__tests__/status-boolean-display.test.tsx +186 -0
- package/src/crud/components/crud-detail-dialog.tsx +21 -8
- package/src/crud/components/crud-field-renderer.tsx +20 -13
- package/src/crud/components/crud-filter-chips.tsx +14 -39
- package/src/crud/components/crud-filters/datetime-filter.tsx +58 -17
- package/src/crud/components/crud-provider.tsx +10 -2
- package/src/crud/components/crud-table-toolbar.tsx +38 -40
- package/src/crud/components/crud-table.tsx +13 -8
- package/src/crud/lib/coerce.ts +36 -10
- package/src/crud/lib/filter-defaults.test.ts +115 -0
- package/src/crud/lib/filter-defaults.ts +56 -0
- package/src/crud/lib/filter-display.ts +59 -0
- package/src/crud/lib/query-builder.ts +4 -0
- package/src/types/index.ts +16 -9
- package/src/ui/data-display/data-table/data-table-toolbar.tsx +8 -2
- package/src/ui/data-display/data-table/data-table.tsx +32 -4
- package/src/ui/layout/user-dropdown.tsx +30 -23
- package/src/ui/primitives/status-badge.tsx +14 -10
- package/src/ui/shared/status-indicator.tsx +266 -74
- package/src/utils/index.ts +87 -38
- package/src/workspace/__tests__/workspace-delegation.test.ts +29 -11
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use client"
|
|
1
|
+
"use client";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* StatusIndicator — Semantic status dot + text component
|
|
@@ -13,65 +13,236 @@
|
|
|
13
13
|
* <StatusIndicator status="cancelled" label="Đã hủy" />
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import React, { memo, useMemo } from "react"
|
|
17
|
-
import { cn } from "../../utils"
|
|
16
|
+
import React, { memo, useMemo } from "react";
|
|
17
|
+
import { cn, normalizeStatusValue } from "../../utils";
|
|
18
18
|
|
|
19
19
|
// ── Status → Semantic Color Map ──────────────────────────────
|
|
20
|
-
const STATUS_MAP: Record<
|
|
20
|
+
const STATUS_MAP: Record<
|
|
21
|
+
string,
|
|
22
|
+
{ color: string; dotClass: string; label: string }
|
|
23
|
+
> = {
|
|
21
24
|
// Active / Positive
|
|
22
|
-
active:
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
active: {
|
|
26
|
+
color: "text-success-text",
|
|
27
|
+
dotClass: "bg-success-semantic",
|
|
28
|
+
label: "Hoạt động",
|
|
29
|
+
},
|
|
30
|
+
completed: {
|
|
31
|
+
color: "text-success-text",
|
|
32
|
+
dotClass: "bg-success-semantic",
|
|
33
|
+
label: "Hoàn thành",
|
|
34
|
+
},
|
|
35
|
+
delivered: {
|
|
36
|
+
color: "text-success-text",
|
|
37
|
+
dotClass: "bg-success-semantic",
|
|
38
|
+
label: "Đã giao",
|
|
39
|
+
},
|
|
40
|
+
paid: {
|
|
41
|
+
color: "text-success-text",
|
|
42
|
+
dotClass: "bg-success-semantic",
|
|
43
|
+
label: "Đã thanh toán",
|
|
44
|
+
},
|
|
45
|
+
approved: {
|
|
46
|
+
color: "text-success-text",
|
|
47
|
+
dotClass: "bg-success-semantic",
|
|
48
|
+
label: "Đã duyệt",
|
|
49
|
+
},
|
|
50
|
+
director_approved: {
|
|
51
|
+
color: "text-success-text",
|
|
52
|
+
dotClass: "bg-success-semantic",
|
|
53
|
+
label: "Giám đốc đã duyệt",
|
|
54
|
+
},
|
|
55
|
+
confirmed: {
|
|
56
|
+
color: "text-success-text",
|
|
57
|
+
dotClass: "bg-success-semantic",
|
|
58
|
+
label: "Đã xác nhận",
|
|
59
|
+
},
|
|
60
|
+
received: {
|
|
61
|
+
color: "text-success-text",
|
|
62
|
+
dotClass: "bg-success-semantic",
|
|
63
|
+
label: "Đã nhận",
|
|
64
|
+
},
|
|
65
|
+
delivery_completed: {
|
|
66
|
+
color: "text-success-text",
|
|
67
|
+
dotClass: "bg-success-semantic",
|
|
68
|
+
label: "Hoàn thành",
|
|
69
|
+
},
|
|
70
|
+
supplier_confirmed: {
|
|
71
|
+
color: "text-success-text",
|
|
72
|
+
dotClass: "bg-success-semantic",
|
|
73
|
+
label: "NCC xác nhận",
|
|
74
|
+
},
|
|
32
75
|
|
|
33
76
|
// Pending / Warning
|
|
34
|
-
pending:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
77
|
+
pending: {
|
|
78
|
+
color: "text-warning-text",
|
|
79
|
+
dotClass: "bg-warning",
|
|
80
|
+
label: "Chờ xử lý",
|
|
81
|
+
},
|
|
82
|
+
processing: {
|
|
83
|
+
color: "text-warning-text",
|
|
84
|
+
dotClass: "bg-warning",
|
|
85
|
+
label: "Đang xử lý",
|
|
86
|
+
},
|
|
87
|
+
partial: {
|
|
88
|
+
color: "text-warning-text",
|
|
89
|
+
dotClass: "bg-warning",
|
|
90
|
+
label: "Một phần",
|
|
91
|
+
},
|
|
92
|
+
partially_paid: {
|
|
93
|
+
color: "text-warning-text",
|
|
94
|
+
dotClass: "bg-warning",
|
|
95
|
+
label: "TT một phần",
|
|
96
|
+
},
|
|
97
|
+
waiting: {
|
|
98
|
+
color: "text-warning-text",
|
|
99
|
+
dotClass: "bg-warning",
|
|
100
|
+
label: "Đang chờ",
|
|
101
|
+
},
|
|
102
|
+
sent: {
|
|
103
|
+
color: "text-warning-text",
|
|
104
|
+
dotClass: "bg-warning",
|
|
105
|
+
label: "Chờ duyệt",
|
|
106
|
+
},
|
|
107
|
+
pending_l1: {
|
|
108
|
+
color: "text-warning-text",
|
|
109
|
+
dotClass: "bg-warning",
|
|
110
|
+
label: "Chờ duyệt L1",
|
|
111
|
+
},
|
|
112
|
+
pending_l2: {
|
|
113
|
+
color: "text-warning-text",
|
|
114
|
+
dotClass: "bg-warning",
|
|
115
|
+
label: "Chờ duyệt L2",
|
|
116
|
+
},
|
|
117
|
+
pending_l3: {
|
|
118
|
+
color: "text-warning-text",
|
|
119
|
+
dotClass: "bg-warning",
|
|
120
|
+
label: "Chờ duyệt L3",
|
|
121
|
+
},
|
|
122
|
+
pending_control: {
|
|
123
|
+
color: "text-warning-text",
|
|
124
|
+
dotClass: "bg-warning",
|
|
125
|
+
label: "Chờ kiểm soát",
|
|
126
|
+
},
|
|
127
|
+
accounting_pending: {
|
|
128
|
+
color: "text-warning-text",
|
|
129
|
+
dotClass: "bg-warning",
|
|
130
|
+
label: "Chờ KT duyệt",
|
|
131
|
+
},
|
|
132
|
+
director_pending: {
|
|
133
|
+
color: "text-warning-text",
|
|
134
|
+
dotClass: "bg-warning",
|
|
135
|
+
label: "Chờ BGĐ duyệt",
|
|
136
|
+
},
|
|
137
|
+
accounting_revision: {
|
|
138
|
+
color: "text-warning-text",
|
|
139
|
+
dotClass: "bg-warning",
|
|
140
|
+
label: "Chỉnh sửa",
|
|
141
|
+
},
|
|
142
|
+
waiting_supplier: {
|
|
143
|
+
color: "text-warning-text",
|
|
144
|
+
dotClass: "bg-warning",
|
|
145
|
+
label: "Chờ NCC",
|
|
146
|
+
},
|
|
147
|
+
partially_received: {
|
|
148
|
+
color: "text-warning-text",
|
|
149
|
+
dotClass: "bg-warning",
|
|
150
|
+
label: "Nhận một phần",
|
|
151
|
+
},
|
|
49
152
|
// Key dữ liệu THẬT của PurchaseOrder vinhhoa (bản local từng drift vì core thiếu key này — 2026-07 hợp nhất về core).
|
|
50
|
-
partial_received: {
|
|
51
|
-
|
|
153
|
+
partial_received: {
|
|
154
|
+
color: "text-warning-text",
|
|
155
|
+
dotClass: "bg-warning",
|
|
156
|
+
label: "Nhận một phần",
|
|
157
|
+
},
|
|
158
|
+
delivery_adjustment: {
|
|
159
|
+
color: "text-warning-text",
|
|
160
|
+
dotClass: "bg-warning",
|
|
161
|
+
label: "Điều chỉnh giao hàng",
|
|
162
|
+
},
|
|
52
163
|
|
|
53
164
|
// Info / In-progress
|
|
54
|
-
ordered:
|
|
55
|
-
shipping:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
165
|
+
ordered: { color: "text-info-text", dotClass: "bg-info", label: "Đã đặt" },
|
|
166
|
+
shipping: {
|
|
167
|
+
color: "text-info-text",
|
|
168
|
+
dotClass: "bg-info",
|
|
169
|
+
label: "Đang giao",
|
|
170
|
+
},
|
|
171
|
+
in_transit: {
|
|
172
|
+
color: "text-info-text",
|
|
173
|
+
dotClass: "bg-info",
|
|
174
|
+
label: "Đang vận chuyển",
|
|
175
|
+
},
|
|
176
|
+
delivering: {
|
|
177
|
+
color: "text-info-text",
|
|
178
|
+
dotClass: "bg-info",
|
|
179
|
+
label: "Đang giao hàng",
|
|
180
|
+
},
|
|
181
|
+
accounting_approved: {
|
|
182
|
+
color: "text-info-text",
|
|
183
|
+
dotClass: "bg-info",
|
|
184
|
+
label: "Kế toán đã duyệt",
|
|
185
|
+
},
|
|
59
186
|
|
|
60
187
|
// Danger / Negative
|
|
61
|
-
cancelled:
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
188
|
+
cancelled: {
|
|
189
|
+
color: "text-danger-text",
|
|
190
|
+
dotClass: "bg-danger",
|
|
191
|
+
label: "Đã hủy",
|
|
192
|
+
},
|
|
193
|
+
overdue: {
|
|
194
|
+
color: "text-danger-text",
|
|
195
|
+
dotClass: "bg-danger",
|
|
196
|
+
label: "Quá hạn",
|
|
197
|
+
},
|
|
198
|
+
rejected: {
|
|
199
|
+
color: "text-danger-text",
|
|
200
|
+
dotClass: "bg-danger",
|
|
201
|
+
label: "Từ chối",
|
|
202
|
+
},
|
|
203
|
+
failed: {
|
|
204
|
+
color: "text-danger-text",
|
|
205
|
+
dotClass: "bg-danger",
|
|
206
|
+
label: "Thất bại",
|
|
207
|
+
},
|
|
208
|
+
inactive: {
|
|
209
|
+
color: "text-danger-text",
|
|
210
|
+
dotClass: "bg-danger",
|
|
211
|
+
label: "Ngừng hoạt động",
|
|
212
|
+
},
|
|
213
|
+
unpaid: {
|
|
214
|
+
color: "text-danger-text",
|
|
215
|
+
dotClass: "bg-danger",
|
|
216
|
+
label: "Chưa thanh toán",
|
|
217
|
+
},
|
|
218
|
+
refunded: {
|
|
219
|
+
color: "text-danger-text",
|
|
220
|
+
dotClass: "bg-danger",
|
|
221
|
+
label: "Đã hoàn tiền",
|
|
222
|
+
},
|
|
223
|
+
returned: {
|
|
224
|
+
color: "text-danger-text",
|
|
225
|
+
dotClass: "bg-danger",
|
|
226
|
+
label: "Hoàn trả",
|
|
227
|
+
},
|
|
69
228
|
|
|
70
229
|
// Neutral
|
|
71
|
-
draft:
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
230
|
+
draft: {
|
|
231
|
+
color: "text-text-tertiary",
|
|
232
|
+
dotClass: "bg-text-tertiary",
|
|
233
|
+
label: "Nháp",
|
|
234
|
+
},
|
|
235
|
+
archived: {
|
|
236
|
+
color: "text-text-disabled",
|
|
237
|
+
dotClass: "bg-text-disabled",
|
|
238
|
+
label: "Lưu trữ",
|
|
239
|
+
},
|
|
240
|
+
unknown: {
|
|
241
|
+
color: "text-text-tertiary",
|
|
242
|
+
dotClass: "bg-text-tertiary",
|
|
243
|
+
label: "Không xác định",
|
|
244
|
+
},
|
|
245
|
+
};
|
|
75
246
|
|
|
76
247
|
// ── Tone (text color) → Badge variant ───────────────────────
|
|
77
248
|
// Lets callers render the SAME status taxonomy as a filled colored Badge
|
|
@@ -86,7 +257,7 @@ const TONE_TO_BADGE_VARIANT: Record<
|
|
|
86
257
|
"text-info-text": "info",
|
|
87
258
|
"text-text-tertiary": "secondary",
|
|
88
259
|
"text-text-disabled": "secondary",
|
|
89
|
-
}
|
|
260
|
+
};
|
|
90
261
|
|
|
91
262
|
// ── "Solid / Trầm sang" (direction D) chip styles ────────────
|
|
92
263
|
// Deep step-700 fills + white text. The subtle Badge variants (green-3 etc.)
|
|
@@ -98,7 +269,7 @@ const SOLID_STATUS_CLASS: Record<string, string> = {
|
|
|
98
269
|
danger: "bg-red-700 text-white",
|
|
99
270
|
info: "bg-blue-700 text-white",
|
|
100
271
|
secondary: "bg-slate-600 text-white",
|
|
101
|
-
}
|
|
272
|
+
};
|
|
102
273
|
|
|
103
274
|
/**
|
|
104
275
|
* Resolve a status string to its canonical label + colors + matching Badge
|
|
@@ -108,32 +279,42 @@ const SOLID_STATUS_CLASS: Record<string, string> = {
|
|
|
108
279
|
* - `badgeVariant`: the subtle semantic Badge variant (good on white surfaces).
|
|
109
280
|
* - `solidClass`: deep "Trầm sang" fill + white text (good on navy/dark bars).
|
|
110
281
|
*/
|
|
111
|
-
export function getStatusMeta(status:
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
282
|
+
export function getStatusMeta(status: unknown, labelOverride?: string) {
|
|
283
|
+
// Chuẩn hoá TRƯỚC khi tra bảng: cột `is_active` kiểu Boolean gửi xuống
|
|
284
|
+
// `true`/`false`, gọi thẳng `.toLowerCase()` là ném TypeError và cả bảng
|
|
285
|
+
// trắng màn. `normalizeStatusValue` gom boolean/số/chuỗi về cùng một key.
|
|
286
|
+
const key = normalizeStatusValue(status) ?? "unknown";
|
|
287
|
+
const known = STATUS_MAP[key];
|
|
288
|
+
const resolved = known ?? STATUS_MAP.unknown;
|
|
289
|
+
const badgeVariant = TONE_TO_BADGE_VARIANT[resolved.color] ?? "secondary";
|
|
115
290
|
return {
|
|
291
|
+
/** false = key lạ, đang dùng nhãn "Không xác định" — nơi gọi tự quyết. */
|
|
292
|
+
matched: Boolean(known),
|
|
116
293
|
label: labelOverride ?? resolved.label,
|
|
117
294
|
color: resolved.color,
|
|
118
295
|
dotClass: resolved.dotClass,
|
|
119
296
|
badgeVariant,
|
|
120
|
-
solidClass:
|
|
121
|
-
|
|
297
|
+
solidClass:
|
|
298
|
+
SOLID_STATUS_CLASS[badgeVariant] ?? SOLID_STATUS_CLASS.secondary,
|
|
299
|
+
};
|
|
122
300
|
}
|
|
123
301
|
|
|
124
302
|
export interface StatusIndicatorProps {
|
|
125
|
-
/**
|
|
126
|
-
|
|
303
|
+
/**
|
|
304
|
+
* Giá trị trạng thái: chuỗi ("active", "pending"…), boolean của cột
|
|
305
|
+
* `is_active`, hay số 1/0 — đều tra về cùng một key.
|
|
306
|
+
*/
|
|
307
|
+
status: unknown;
|
|
127
308
|
/** Override the default Vietnamese label */
|
|
128
|
-
label?: string
|
|
309
|
+
label?: string;
|
|
129
310
|
/** Size variant */
|
|
130
|
-
size?: "sm" | "md" | "lg"
|
|
311
|
+
size?: "sm" | "md" | "lg";
|
|
131
312
|
/** Show dot only (no text) */
|
|
132
|
-
dotOnly?: boolean
|
|
313
|
+
dotOnly?: boolean;
|
|
133
314
|
/** Custom dot color class override */
|
|
134
|
-
dotColor?: string
|
|
315
|
+
dotColor?: string;
|
|
135
316
|
/** Additional CSS classes */
|
|
136
|
-
className?: string
|
|
317
|
+
className?: string;
|
|
137
318
|
}
|
|
138
319
|
|
|
139
320
|
export const StatusIndicator = memo(function StatusIndicator({
|
|
@@ -145,31 +326,42 @@ export const StatusIndicator = memo(function StatusIndicator({
|
|
|
145
326
|
className,
|
|
146
327
|
}: StatusIndicatorProps) {
|
|
147
328
|
const resolved = useMemo(() => {
|
|
148
|
-
const key = status
|
|
149
|
-
return STATUS_MAP[key] ?? STATUS_MAP.unknown
|
|
150
|
-
}, [status])
|
|
329
|
+
const key = normalizeStatusValue(status) ?? "unknown";
|
|
330
|
+
return STATUS_MAP[key] ?? STATUS_MAP.unknown;
|
|
331
|
+
}, [status]);
|
|
151
332
|
|
|
152
|
-
const dotSizeClass =
|
|
153
|
-
|
|
154
|
-
const
|
|
333
|
+
const dotSizeClass =
|
|
334
|
+
size === "sm" ? "size-1.5" : size === "lg" ? "size-3" : "size-2";
|
|
335
|
+
const textSizeClass =
|
|
336
|
+
size === "sm" ? "text-[11px]" : size === "lg" ? "text-sm" : "text-xs";
|
|
337
|
+
const displayLabel = labelOverride ?? resolved.label;
|
|
155
338
|
|
|
156
339
|
if (dotOnly) {
|
|
157
340
|
return (
|
|
158
341
|
<span
|
|
159
|
-
className={cn(
|
|
342
|
+
className={cn(
|
|
343
|
+
"inline-block rounded-full",
|
|
344
|
+
dotColor ?? resolved.dotClass,
|
|
345
|
+
dotSizeClass,
|
|
346
|
+
className,
|
|
347
|
+
)}
|
|
160
348
|
title={displayLabel}
|
|
161
349
|
/>
|
|
162
|
-
)
|
|
350
|
+
);
|
|
163
351
|
}
|
|
164
352
|
|
|
165
353
|
return (
|
|
166
354
|
<span className={cn("inline-flex items-center gap-1.5", className)}>
|
|
167
355
|
<span
|
|
168
|
-
className={cn(
|
|
356
|
+
className={cn(
|
|
357
|
+
"inline-block rounded-full flex-shrink-0",
|
|
358
|
+
dotColor ?? resolved.dotClass,
|
|
359
|
+
dotSizeClass,
|
|
360
|
+
)}
|
|
169
361
|
/>
|
|
170
362
|
<span className={cn("font-medium", resolved.color, textSizeClass)}>
|
|
171
363
|
{displayLabel}
|
|
172
364
|
</span>
|
|
173
365
|
</span>
|
|
174
|
-
)
|
|
175
|
-
})
|
|
366
|
+
);
|
|
367
|
+
});
|
package/src/utils/index.ts
CHANGED
|
@@ -78,16 +78,16 @@ export const isNonNegative = (num: number) => num >= 0;
|
|
|
78
78
|
* @returns Chuỗi đọc bằng chữ, VD: "Bảy mươi hai triệu một trăm năm mươi chín nghìn tám trăm hai mươi đồng"
|
|
79
79
|
*/
|
|
80
80
|
export function readMoney(amount: number): string {
|
|
81
|
-
if (!Number.isFinite(amount)) return "Không đồng"
|
|
81
|
+
if (!Number.isFinite(amount)) return "Không đồng";
|
|
82
82
|
|
|
83
|
-
const isNegative = amount < 0
|
|
83
|
+
const isNegative = amount < 0;
|
|
84
84
|
|
|
85
85
|
// Bỏ phần thập phân - VND không có đơn vị lẻ
|
|
86
|
-
amount = Math.floor(Math.abs(amount))
|
|
86
|
+
amount = Math.floor(Math.abs(amount));
|
|
87
87
|
|
|
88
|
-
if (amount === 0) return "Không đồng"
|
|
88
|
+
if (amount === 0) return "Không đồng";
|
|
89
89
|
|
|
90
|
-
const unit = ["", "nghìn", "triệu", "tỷ", "nghìn tỷ", "triệu tỷ"]
|
|
90
|
+
const unit = ["", "nghìn", "triệu", "tỷ", "nghìn tỷ", "triệu tỷ"];
|
|
91
91
|
const digit = [
|
|
92
92
|
"không",
|
|
93
93
|
"một",
|
|
@@ -99,62 +99,62 @@ export function readMoney(amount: number): string {
|
|
|
99
99
|
"bảy",
|
|
100
100
|
"tám",
|
|
101
101
|
"chín",
|
|
102
|
-
]
|
|
102
|
+
];
|
|
103
103
|
|
|
104
|
-
let str = amount.toString()
|
|
105
|
-
const groups: string[] = []
|
|
104
|
+
let str = amount.toString();
|
|
105
|
+
const groups: string[] = [];
|
|
106
106
|
while (str.length > 0) {
|
|
107
|
-
groups.push(str.slice(-3))
|
|
108
|
-
str = str.slice(0, -3)
|
|
107
|
+
groups.push(str.slice(-3));
|
|
108
|
+
str = str.slice(0, -3);
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
let result = ""
|
|
111
|
+
let result = "";
|
|
112
112
|
for (let i = 0; i < groups.length; i++) {
|
|
113
|
-
const group = groups[i]
|
|
114
|
-
if (group === "000") continue
|
|
113
|
+
const group = groups[i];
|
|
114
|
+
if (group === "000") continue;
|
|
115
115
|
|
|
116
|
-
const [a, b, c] = group.padStart(3, "0").split("").map(Number)
|
|
117
|
-
let groupResult = ""
|
|
116
|
+
const [a, b, c] = group.padStart(3, "0").split("").map(Number);
|
|
117
|
+
let groupResult = "";
|
|
118
118
|
|
|
119
|
-
const hasHundreds = a !== 0 || groups.length > 1
|
|
119
|
+
const hasHundreds = a !== 0 || groups.length > 1;
|
|
120
120
|
|
|
121
121
|
if (hasHundreds) {
|
|
122
|
-
groupResult += `${digit[a]} trăm
|
|
122
|
+
groupResult += `${digit[a]} trăm `;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
if (b === 0 && c !== 0) {
|
|
126
|
-
if (hasHundreds) groupResult += "lẻ "
|
|
126
|
+
if (hasHundreds) groupResult += "lẻ ";
|
|
127
127
|
} else if (b === 1) {
|
|
128
|
-
groupResult += "mười "
|
|
128
|
+
groupResult += "mười ";
|
|
129
129
|
} else if (b > 1) {
|
|
130
|
-
groupResult += `${digit[b]} mươi
|
|
130
|
+
groupResult += `${digit[b]} mươi `;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
if (c === 1 && b > 1) {
|
|
134
|
-
groupResult += "mốt "
|
|
134
|
+
groupResult += "mốt ";
|
|
135
135
|
} else if (c === 5 && b > 0) {
|
|
136
|
-
groupResult += "lăm "
|
|
136
|
+
groupResult += "lăm ";
|
|
137
137
|
} else if (c !== 0) {
|
|
138
|
-
groupResult += `${digit[c]}
|
|
138
|
+
groupResult += `${digit[c]} `;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
if (i === groups.length - 1 && a === 0) {
|
|
142
|
-
groupResult = groupResult.replace("không trăm ", "")
|
|
143
|
-
if (b === 0) groupResult = groupResult.replace("lẻ ", "")
|
|
142
|
+
groupResult = groupResult.replace("không trăm ", "");
|
|
143
|
+
if (b === 0) groupResult = groupResult.replace("lẻ ", "");
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
if (groupResult.trim() !== "") {
|
|
147
|
-
result = `${groupResult.trim()} ${unit[i]} ${result}
|
|
147
|
+
result = `${groupResult.trim()} ${unit[i]} ${result}`;
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
result = result.trim() + " đồng"
|
|
151
|
+
result = result.trim() + " đồng";
|
|
152
152
|
|
|
153
153
|
if (isNegative) {
|
|
154
|
-
return "Âm " + result
|
|
154
|
+
return "Âm " + result;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
return result.charAt(0).toUpperCase() + result.slice(1)
|
|
157
|
+
return result.charAt(0).toUpperCase() + result.slice(1);
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
/**
|
|
@@ -260,10 +260,31 @@ export function formatDate(
|
|
|
260
260
|
const dateObj = typeof date === "string" ? new Date(date) : date;
|
|
261
261
|
|
|
262
262
|
const formatOptions: Record<string, Intl.DateTimeFormatOptions> = {
|
|
263
|
-
short: {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
263
|
+
short: {
|
|
264
|
+
day: "2-digit",
|
|
265
|
+
month: "2-digit",
|
|
266
|
+
year: "numeric",
|
|
267
|
+
timeZone: "Asia/Ho_Chi_Minh",
|
|
268
|
+
},
|
|
269
|
+
medium: {
|
|
270
|
+
day: "2-digit",
|
|
271
|
+
month: "short",
|
|
272
|
+
year: "numeric",
|
|
273
|
+
timeZone: "Asia/Ho_Chi_Minh",
|
|
274
|
+
},
|
|
275
|
+
long: {
|
|
276
|
+
day: "numeric",
|
|
277
|
+
month: "long",
|
|
278
|
+
year: "numeric",
|
|
279
|
+
timeZone: "Asia/Ho_Chi_Minh",
|
|
280
|
+
},
|
|
281
|
+
full: {
|
|
282
|
+
weekday: "long",
|
|
283
|
+
day: "numeric",
|
|
284
|
+
month: "long",
|
|
285
|
+
year: "numeric",
|
|
286
|
+
timeZone: "Asia/Ho_Chi_Minh",
|
|
287
|
+
},
|
|
267
288
|
};
|
|
268
289
|
|
|
269
290
|
return new Intl.DateTimeFormat(locale, formatOptions[format]).format(dateObj);
|
|
@@ -303,9 +324,24 @@ export function formatRelativeDate(value?: string | number | Date): string {
|
|
|
303
324
|
yesterday.setDate(today.getDate() - 1);
|
|
304
325
|
|
|
305
326
|
// Compare dates in Vietnam timezone
|
|
306
|
-
const vnDateStr = new Intl.DateTimeFormat("en-CA", {
|
|
307
|
-
|
|
308
|
-
|
|
327
|
+
const vnDateStr = new Intl.DateTimeFormat("en-CA", {
|
|
328
|
+
timeZone: "Asia/Ho_Chi_Minh",
|
|
329
|
+
year: "numeric",
|
|
330
|
+
month: "2-digit",
|
|
331
|
+
day: "2-digit",
|
|
332
|
+
}).format(date);
|
|
333
|
+
const vnTodayStr = new Intl.DateTimeFormat("en-CA", {
|
|
334
|
+
timeZone: "Asia/Ho_Chi_Minh",
|
|
335
|
+
year: "numeric",
|
|
336
|
+
month: "2-digit",
|
|
337
|
+
day: "2-digit",
|
|
338
|
+
}).format(today);
|
|
339
|
+
const vnYesterdayStr = new Intl.DateTimeFormat("en-CA", {
|
|
340
|
+
timeZone: "Asia/Ho_Chi_Minh",
|
|
341
|
+
year: "numeric",
|
|
342
|
+
month: "2-digit",
|
|
343
|
+
day: "2-digit",
|
|
344
|
+
}).format(yesterday);
|
|
309
345
|
|
|
310
346
|
if (vnDateStr === vnTodayStr) return "Today";
|
|
311
347
|
if (vnDateStr === vnYesterdayStr) return "Yesterday";
|
|
@@ -317,8 +353,18 @@ export function formatRelativeDate(value?: string | number | Date): string {
|
|
|
317
353
|
* Check if date is before today (Asia/Ho_Chi_Minh timezone)
|
|
318
354
|
*/
|
|
319
355
|
export function isBeforeToday(date: Date): boolean {
|
|
320
|
-
const vnTodayStr = new Intl.DateTimeFormat("en-CA", {
|
|
321
|
-
|
|
356
|
+
const vnTodayStr = new Intl.DateTimeFormat("en-CA", {
|
|
357
|
+
timeZone: "Asia/Ho_Chi_Minh",
|
|
358
|
+
year: "numeric",
|
|
359
|
+
month: "2-digit",
|
|
360
|
+
day: "2-digit",
|
|
361
|
+
}).format(new Date());
|
|
362
|
+
const vnDateStr = new Intl.DateTimeFormat("en-CA", {
|
|
363
|
+
timeZone: "Asia/Ho_Chi_Minh",
|
|
364
|
+
year: "numeric",
|
|
365
|
+
month: "2-digit",
|
|
366
|
+
day: "2-digit",
|
|
367
|
+
}).format(date);
|
|
322
368
|
return vnDateStr < vnTodayStr;
|
|
323
369
|
}
|
|
324
370
|
|
|
@@ -683,6 +729,9 @@ export {
|
|
|
683
729
|
STATUS_VALUES,
|
|
684
730
|
booleanToStatus,
|
|
685
731
|
statusToBoolean,
|
|
732
|
+
normalizeStatusValue,
|
|
733
|
+
isTruthyStatus,
|
|
734
|
+
matchesOptionValue,
|
|
686
735
|
} from "../configs/status";
|
|
687
736
|
|
|
688
737
|
// ============================================================================
|