@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.
Files changed (30) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/package.json +1 -1
  3. package/src/audit/__tests__/prisma-audit-extension.test.ts +113 -0
  4. package/src/audit/prisma-audit-extension.ts +41 -13
  5. package/src/configs/status.ts +67 -0
  6. package/src/cron/__tests__/cron-single-runner.test.ts +260 -0
  7. package/src/cron/cron-schedule.ts +113 -20
  8. package/src/cron/db-cron-manager.ts +90 -7
  9. package/src/cron/simple-cron-job.ts +17 -6
  10. package/src/crud/__tests__/status-boolean-display.test.tsx +186 -0
  11. package/src/crud/components/crud-detail-dialog.tsx +21 -8
  12. package/src/crud/components/crud-field-renderer.tsx +20 -13
  13. package/src/crud/components/crud-filter-chips.tsx +14 -39
  14. package/src/crud/components/crud-filters/datetime-filter.tsx +58 -17
  15. package/src/crud/components/crud-provider.tsx +10 -2
  16. package/src/crud/components/crud-table-toolbar.tsx +38 -40
  17. package/src/crud/components/crud-table.tsx +13 -8
  18. package/src/crud/lib/coerce.ts +36 -10
  19. package/src/crud/lib/filter-defaults.test.ts +115 -0
  20. package/src/crud/lib/filter-defaults.ts +56 -0
  21. package/src/crud/lib/filter-display.ts +59 -0
  22. package/src/crud/lib/query-builder.ts +4 -0
  23. package/src/types/index.ts +16 -9
  24. package/src/ui/data-display/data-table/data-table-toolbar.tsx +8 -2
  25. package/src/ui/data-display/data-table/data-table.tsx +32 -4
  26. package/src/ui/layout/user-dropdown.tsx +30 -23
  27. package/src/ui/primitives/status-badge.tsx +14 -10
  28. package/src/ui/shared/status-indicator.tsx +266 -74
  29. package/src/utils/index.ts +87 -38
  30. 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<string, { color: string; dotClass: string; label: string }> = {
20
+ const STATUS_MAP: Record<
21
+ string,
22
+ { color: string; dotClass: string; label: string }
23
+ > = {
21
24
  // Active / Positive
22
- active: { color: "text-success-text", dotClass: "bg-success-semantic", label: "Hoạt động" },
23
- completed: { color: "text-success-text", dotClass: "bg-success-semantic", label: "Hoàn thành" },
24
- delivered: { color: "text-success-text", dotClass: "bg-success-semantic", label: "Đã giao" },
25
- paid: { color: "text-success-text", dotClass: "bg-success-semantic", label: "Đã thanh toán" },
26
- approved: { color: "text-success-text", dotClass: "bg-success-semantic", label: "Đã duyệt" },
27
- director_approved: { color: "text-success-text", dotClass: "bg-success-semantic", label: "Giám đốc đã duyệt" },
28
- confirmed: { color: "text-success-text", dotClass: "bg-success-semantic", label: "Đã xác nhận" },
29
- received: { color: "text-success-text", dotClass: "bg-success-semantic", label: "Đã nhận" },
30
- delivery_completed: { color: "text-success-text", dotClass: "bg-success-semantic", label: "Hoàn thành" },
31
- supplier_confirmed: { color: "text-success-text", dotClass: "bg-success-semantic", label: "NCC xác nhận" },
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: { color: "text-warning-text", dotClass: "bg-warning", label: "Chờ xử lý" },
35
- processing: { color: "text-warning-text", dotClass: "bg-warning", label: "Đang xử lý" },
36
- partial: { color: "text-warning-text", dotClass: "bg-warning", label: "Một phần" },
37
- partially_paid:{ color: "text-warning-text", dotClass: "bg-warning", label: "TT một phần" },
38
- waiting: { color: "text-warning-text", dotClass: "bg-warning", label: "Đang chờ" },
39
- sent: { color: "text-warning-text", dotClass: "bg-warning", label: "Chờ duyệt" },
40
- pending_l1: { color: "text-warning-text", dotClass: "bg-warning", label: "Chờ duyệt L1" },
41
- pending_l2: { color: "text-warning-text", dotClass: "bg-warning", label: "Chờ duyệt L2" },
42
- pending_l3: { color: "text-warning-text", dotClass: "bg-warning", label: "Chờ duyệt L3" },
43
- pending_control: { color: "text-warning-text", dotClass: "bg-warning", label: "Chờ kiểm soát" },
44
- accounting_pending: { color: "text-warning-text", dotClass: "bg-warning", label: "Chờ KT duyệt" },
45
- director_pending: { color: "text-warning-text", dotClass: "bg-warning", label: "Chờ BGĐ duyệt" },
46
- accounting_revision: { color: "text-warning-text", dotClass: "bg-warning", label: "Chỉnh sửa" },
47
- waiting_supplier:{ color: "text-warning-text", dotClass: "bg-warning", label: "Chờ NCC" },
48
- partially_received: { color: "text-warning-text", dotClass: "bg-warning", label: "Nhận một phần" },
77
+ pending: {
78
+ color: "text-warning-text",
79
+ dotClass: "bg-warning",
80
+ label: "Chờ xử ",
81
+ },
82
+ processing: {
83
+ color: "text-warning-text",
84
+ dotClass: "bg-warning",
85
+ label: "Đang xử ",
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: { color: "text-warning-text", dotClass: "bg-warning", label: "Nhận một phần" },
51
- delivery_adjustment: { color: "text-warning-text", dotClass: "bg-warning", label: "Điều chỉnh giao hàng" },
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: { color: "text-info-text", dotClass: "bg-info", label: "Đã đặt" },
55
- shipping: { color: "text-info-text", dotClass: "bg-info", label: "Đang giao" },
56
- in_transit: { color: "text-info-text", dotClass: "bg-info", label: "Đang vận chuyển" },
57
- delivering: { color: "text-info-text", dotClass: "bg-info", label: "Đang giao hàng" },
58
- accounting_approved: { color: "text-info-text", dotClass: "bg-info", label: "Kế toán đã duyệt" },
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: { color: "text-danger-text", dotClass: "bg-danger", label: "Đã hủy" },
62
- overdue: { color: "text-danger-text", dotClass: "bg-danger", label: "Quá hạn" },
63
- rejected: { color: "text-danger-text", dotClass: "bg-danger", label: "Từ chối" },
64
- failed: { color: "text-danger-text", dotClass: "bg-danger", label: "Thất bại" },
65
- inactive: { color: "text-danger-text", dotClass: "bg-danger", label: "Ngừng hoạt động" },
66
- unpaid: { color: "text-danger-text", dotClass: "bg-danger", label: "Chưa thanh toán" },
67
- refunded: { color: "text-danger-text", dotClass: "bg-danger", label: "Đã hoàn tiền" },
68
- returned: { color: "text-danger-text", dotClass: "bg-danger", label: "Hoàn trả" },
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: { color: "text-text-tertiary", dotClass: "bg-text-tertiary", label: "Nháp" },
72
- archived: { color: "text-text-disabled", dotClass: "bg-text-disabled", label: "Lưu trữ" },
73
- unknown: { color: "text-text-tertiary", dotClass: "bg-text-tertiary", label: "Không xác định" },
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: string, labelOverride?: string) {
112
- const key = status?.toLowerCase().replace(/\s+/g, "_") ?? "unknown"
113
- const resolved = STATUS_MAP[key] ?? STATUS_MAP.unknown
114
- const badgeVariant = TONE_TO_BADGE_VARIANT[resolved.color] ?? "secondary"
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: SOLID_STATUS_CLASS[badgeVariant] ?? SOLID_STATUS_CLASS.secondary,
121
- }
297
+ solidClass:
298
+ SOLID_STATUS_CLASS[badgeVariant] ?? SOLID_STATUS_CLASS.secondary,
299
+ };
122
300
  }
123
301
 
124
302
  export interface StatusIndicatorProps {
125
- /** Status key (e.g., "active", "pending", "cancelled") */
126
- status: string
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?.toLowerCase().replace(/\s+/g, "_") ?? "unknown"
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 = size === "sm" ? "size-1.5" : size === "lg" ? "size-3" : "size-2"
153
- const textSizeClass = size === "sm" ? "text-[11px]" : size === "lg" ? "text-sm" : "text-xs"
154
- const displayLabel = labelOverride ?? resolved.label
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("inline-block rounded-full", dotColor ?? resolved.dotClass, dotSizeClass, className)}
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("inline-block rounded-full flex-shrink-0", dotColor ?? resolved.dotClass, dotSizeClass)}
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
+ });
@@ -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: { day: "2-digit", month: "2-digit", year: "numeric", timeZone: "Asia/Ho_Chi_Minh" },
264
- medium: { day: "2-digit", month: "short", year: "numeric", timeZone: "Asia/Ho_Chi_Minh" },
265
- long: { day: "numeric", month: "long", year: "numeric", timeZone: "Asia/Ho_Chi_Minh" },
266
- full: { weekday: "long", day: "numeric", month: "long", year: "numeric", timeZone: "Asia/Ho_Chi_Minh" },
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", { timeZone: "Asia/Ho_Chi_Minh", year: "numeric", month: "2-digit", day: "2-digit" }).format(date);
307
- const vnTodayStr = new Intl.DateTimeFormat("en-CA", { timeZone: "Asia/Ho_Chi_Minh", year: "numeric", month: "2-digit", day: "2-digit" }).format(today);
308
- const vnYesterdayStr = new Intl.DateTimeFormat("en-CA", { timeZone: "Asia/Ho_Chi_Minh", year: "numeric", month: "2-digit", day: "2-digit" }).format(yesterday);
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", { timeZone: "Asia/Ho_Chi_Minh", year: "numeric", month: "2-digit", day: "2-digit" }).format(new Date());
321
- const vnDateStr = new Intl.DateTimeFormat("en-CA", { timeZone: "Asia/Ho_Chi_Minh", year: "numeric", month: "2-digit", day: "2-digit" }).format(date);
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
  // ============================================================================