@goplusvn/core 0.1.5 → 0.1.6

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.
@@ -0,0 +1,160 @@
1
+ /* Auto-generated Radix Colors (HSL) */
2
+ :root {
3
+ --slate-1: 240 20% 99%;
4
+ --slate-2: 240 20% 98%;
5
+ --slate-3: 240 11.1% 94.7%;
6
+ --slate-4: 240 9.5% 91.8%;
7
+ --slate-5: 230 10.7% 89%;
8
+ --slate-6: 240 10.1% 86.5%;
9
+ --slate-7: 233 9.9% 82.2%;
10
+ --slate-8: 231 10.2% 75.1%;
11
+ --slate-9: 231 5.9% 57.1%;
12
+ --slate-10: 226 5.4% 52.7%;
13
+ --slate-11: 220 5.9% 40%;
14
+ --slate-12: 210 12.5% 12.5%;
15
+
16
+ --indigo-1: 240 33.3% 99.4%;
17
+ --indigo-2: 225 100% 98.4%;
18
+ --indigo-3: 222 89.5% 96.3%;
19
+ --indigo-4: 224 100% 94.1%;
20
+ --indigo-5: 224 100% 91.2%;
21
+ --indigo-6: 225 100% 87.8%;
22
+ --indigo-7: 226 86.7% 82.4%;
23
+ --indigo-8: 226 75.4% 74.5%;
24
+ --indigo-9: 226 70% 55.5%;
25
+ --indigo-10: 226 65.2% 51.6%;
26
+ --indigo-11: 226 55.7% 50.4%;
27
+ --indigo-12: 226 49.6% 24.1%;
28
+
29
+ --red-1: 0 100% 99.4%;
30
+ --red-2: 0 100% 98.4%;
31
+ --red-3: 357 90.5% 95.9%;
32
+ --red-4: 358 100% 92.9%;
33
+ --red-5: 359 100% 90.2%;
34
+ --red-6: 359 94.1% 86.7%;
35
+ --red-7: 359 77.3% 81%;
36
+ --red-8: 359 69.9% 73.9%;
37
+ --red-9: 358 75.1% 59%;
38
+ --red-10: 358 69.3% 55.3%;
39
+ --red-11: 358 64.8% 49%;
40
+ --red-12: 351 62.6% 24.1%;
41
+
42
+ --green-1: 140 60% 99%;
43
+ --green-2: 137 46.7% 97.1%;
44
+ --green-3: 139 47.1% 93.3%;
45
+ --green-4: 140 49.1% 89.2%;
46
+ --green-5: 142 43.9% 83.9%;
47
+ --green-6: 144 41.4% 77.3%;
48
+ --green-7: 146 39.5% 68.2%;
49
+ --green-8: 151 40.2% 54.1%;
50
+ --green-9: 151 54.7% 41.6%;
51
+ --green-10: 152 56.3% 38.6%;
52
+ --green-11: 154 59.8% 32.2%;
53
+ --green-12: 155 40.5% 16.5%;
54
+
55
+ --orange-1: 20 60% 99%;
56
+ --orange-2: 33 100% 96.5%;
57
+ --orange-3: 37 100% 92%;
58
+ --orange-4: 34 100% 85.5%;
59
+ --orange-5: 33 100% 80.2%;
60
+ --orange-6: 30 100% 75.5%;
61
+ --orange-7: 27 86.7% 70.6%;
62
+ --orange-8: 25 79.9% 62.9%;
63
+ --orange-9: 23 93.4% 52.5%;
64
+ --orange-10: 24 100% 46.9%;
65
+ --orange-11: 23 100% 40%;
66
+ --orange-12: 16 50.4% 22.9%;
67
+
68
+ --blue-1: 210 100% 99.2%;
69
+ --blue-2: 207 100% 97.8%;
70
+ --blue-3: 205 92.3% 94.9%;
71
+ --blue-4: 203 100% 91.8%;
72
+ --blue-5: 206 100% 88%;
73
+ --blue-6: 207 93% 83.1%;
74
+ --blue-7: 207 85.2% 76.1%;
75
+ --blue-8: 206 81.9% 65.3%;
76
+ --blue-9: 206 100% 50%;
77
+ --blue-10: 207 95.9% 48%;
78
+ --blue-11: 208 88.1% 42.9%;
79
+ --blue-12: 216 70.9% 22.9%;
80
+ }
81
+
82
+ .dark {
83
+ --slate-1: 240 5.6% 7.1%;
84
+ --slate-2: 220 5.9% 10%;
85
+ --slate-3: 225 5.7% 13.7%;
86
+ --slate-4: 210 7.1% 16.5%;
87
+ --slate-5: 214 7.1% 19.4%;
88
+ --slate-6: 213 7.7% 22.9%;
89
+ --slate-7: 213 7.6% 28.4%;
90
+ --slate-8: 212 7.7% 38.2%;
91
+ --slate-9: 219 6.3% 43.9%;
92
+ --slate-10: 222 5.2% 49.2%;
93
+ --slate-11: 216 6.8% 71%;
94
+ --slate-12: 220 9.1% 93.5%;
95
+
96
+ --indigo-1: 231 29.2% 9.4%;
97
+ --indigo-2: 230 31% 11.4%;
98
+ --indigo-3: 225 50.5% 19%;
99
+ --indigo-4: 225 54.3% 24.9%;
100
+ --indigo-5: 225 51.6% 30%;
101
+ --indigo-6: 226 46.7% 35.3%;
102
+ --indigo-7: 226 44.5% 41%;
103
+ --indigo-8: 226 45.1% 47.8%;
104
+ --indigo-9: 226 70% 55.5%;
105
+ --indigo-10: 228 72.7% 61.2%;
106
+ --indigo-11: 228 100% 81%;
107
+ --indigo-12: 224 100% 92%;
108
+
109
+ --red-1: 0 19% 8.2%;
110
+ --red-2: 355 25.5% 10%;
111
+ --red-3: 350 53.2% 15.1%;
112
+ --red-4: 348 68.4% 18.6%;
113
+ --red-5: 350 63% 23.3%;
114
+ --red-6: 352 53% 29.2%;
115
+ --red-7: 355 46.6% 37.5%;
116
+ --red-8: 358 44.8% 49%;
117
+ --red-9: 358 75.1% 59%;
118
+ --red-10: 0 79% 64.5%;
119
+ --red-11: 2 100% 78.6%;
120
+ --red-12: 350 100% 91%;
121
+
122
+ --green-1: 154 20% 6.9%;
123
+ --green-2: 153 20% 8.8%;
124
+ --green-3: 152 40.6% 12.5%;
125
+ --green-4: 154 55.3% 14.9%;
126
+ --green-5: 154 52.1% 18.8%;
127
+ --green-6: 153 46.2% 23.3%;
128
+ --green-7: 152 44.4% 28.2%;
129
+ --green-8: 151 45% 33.5%;
130
+ --green-9: 151 54.7% 41.6%;
131
+ --green-10: 151 55.1% 44.5%;
132
+ --green-11: 151 65.1% 53.9%;
133
+ --green-12: 144 69.6% 82%;
134
+
135
+ --orange-1: 27 24.3% 7.3%;
136
+ --orange-2: 28 33.3% 8.8%;
137
+ --orange-3: 29 64.5% 12.2%;
138
+ --orange-4: 28 100% 13.7%;
139
+ --orange-5: 28 100% 16.9%;
140
+ --orange-6: 27 78.9% 22.4%;
141
+ --orange-7: 25 62.6% 30.4%;
142
+ --orange-8: 23 59.8% 40%;
143
+ --orange-9: 23 93.4% 52.5%;
144
+ --orange-10: 26 100% 56.1%;
145
+ --orange-11: 26 100% 67.1%;
146
+ --orange-12: 30 100% 88%;
147
+
148
+ --blue-1: 215 42.2% 8.8%;
149
+ --blue-2: 218 39.3% 11%;
150
+ --blue-3: 212 69% 16.5%;
151
+ --blue-4: 209 100% 19.2%;
152
+ --blue-5: 207 100% 22.7%;
153
+ --blue-6: 209 78.8% 29.6%;
154
+ --blue-7: 211 66.3% 37.3%;
155
+ --blue-8: 211 65.1% 44.9%;
156
+ --blue-9: 206 100% 50%;
157
+ --blue-10: 210 100% 61.6%;
158
+ --blue-11: 210 100% 72%;
159
+ --blue-12: 205 100% 88%;
160
+ }
@@ -0,0 +1,62 @@
1
+ export interface ParsedCCCD {
2
+ idNumber: string
3
+ oldIdNumber: string
4
+ name: string
5
+ birthday: string // yyyy-MM-dd
6
+ gender: string // 'male' | 'female' | 'other'
7
+ address: string
8
+ issueDate: string // yyyy-MM-dd
9
+ raw: string
10
+ }
11
+
12
+ export function parseCCCD(rawData: string): ParsedCCCD | null {
13
+ try {
14
+ if (!rawData || typeof rawData !== "string") return null
15
+
16
+ // Định dạng phổ biến: 12Số|9Số|Họ Tên|ddMMyyyy|Giới tính|Địa chỉ|ddMMyyyy
17
+ const parts = rawData.split("|")
18
+
19
+ // Một vài thẻ có thể thiếu số CMND cũ nên mảng độ dài từ 6 hoặc 7
20
+ if (parts.length < 6) return null
21
+
22
+ // Dữ liệu tuỳ biến theo index có thể lệch nếu không có oldIdNumber (tuy nhiên thẻ chuẩn VN luôn có hoặc bỏ trống field)
23
+ // Format chuẩn nhất (7 parts): [0]số CCCD, [1]số CMND, [2]Họ Tên, [3]Ngày sinh, [4]Giới tính, [5]Địa chỉ, [6]Ngày cấp
24
+ const idNumber = parts[0] || ""
25
+ const oldIdNumber = parts[1] || ""
26
+ const name = parts[2] || ""
27
+
28
+ // Convert ddMMyyyy -> yyyy-MM-dd
29
+ const rawDob = parts[3] || ""
30
+ let birthday = ""
31
+ if (rawDob.length === 8) {
32
+ birthday = `${rawDob.substring(4, 8)}-${rawDob.substring(2, 4)}-${rawDob.substring(0, 2)}`
33
+ }
34
+
35
+ const rawGender = parts[4] || ""
36
+ let gender = "other"
37
+ if (rawGender.toLowerCase() === "nam") gender = "male"
38
+ if (rawGender.toLowerCase() === "nữ" || rawGender.toLowerCase() === "nu") gender = "female"
39
+
40
+ const address = parts[5] || ""
41
+
42
+ const rawIssueDate = parts[6] || ""
43
+ let issueDate = ""
44
+ if (rawIssueDate.length === 8) {
45
+ issueDate = `${rawIssueDate.substring(4, 8)}-${rawIssueDate.substring(2, 4)}-${rawIssueDate.substring(0, 2)}`
46
+ }
47
+
48
+ return {
49
+ idNumber,
50
+ oldIdNumber,
51
+ name,
52
+ birthday,
53
+ gender,
54
+ address,
55
+ issueDate,
56
+ raw: rawData
57
+ }
58
+ } catch (error) {
59
+ console.error("Failed to parse CCCD:", error)
60
+ return null
61
+ }
62
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Date/Time utility functions for Vietnam timezone (GMT+7)
3
+ * Always formats dates/times according to Asia/Ho_Chi_Minh timezone
4
+ * regardless of server timezone
5
+ *
6
+ * Standard formats:
7
+ * - Date only: dd/MM/yyyy (e.g. 03/02/2026)
8
+ * - Date + time: dd/MM/yyyy HH:mm (e.g. 03/02/2026 08:05)
9
+ * - Full datetime: dd/MM/yyyy HH:mm:ss (e.g. 03/02/2026 08:05:09)
10
+ * - Time only: HH:mm (e.g. 08:05)
11
+ */
12
+
13
+ const VIETNAM_TIMEZONE = "Asia/Ho_Chi_Minh"
14
+
15
+ /**
16
+ * Internal helper: extract date/time parts in Vietnam timezone
17
+ */
18
+ function getVietnamParts(date: Date) {
19
+ const parts = new Intl.DateTimeFormat("en-US", {
20
+ timeZone: VIETNAM_TIMEZONE,
21
+ year: "numeric",
22
+ month: "2-digit",
23
+ day: "2-digit",
24
+ hour: "2-digit",
25
+ minute: "2-digit",
26
+ second: "2-digit",
27
+ hour12: false,
28
+ }).formatToParts(date)
29
+
30
+ return {
31
+ year: parts.find((p) => p.type === "year")?.value ?? "0000",
32
+ month: parts.find((p) => p.type === "month")?.value ?? "00",
33
+ day: parts.find((p) => p.type === "day")?.value ?? "00",
34
+ hour: parts.find((p) => p.type === "hour")?.value ?? "00",
35
+ minute: parts.find((p) => p.type === "minute")?.value ?? "00",
36
+ second: parts.find((p) => p.type === "second")?.value ?? "00",
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Safely convert input to Date object
42
+ */
43
+ function toDateObj(date: Date | string): Date {
44
+ return typeof date === "string" ? new Date(date) : date
45
+ }
46
+
47
+ /**
48
+ * Get current date/time in Vietnam timezone
49
+ */
50
+ export function getVietnamNow(): Date {
51
+ const p = getVietnamParts(new Date())
52
+ const dateString = `${p.year}-${p.month}-${p.day}T${p.hour}:${p.minute}:${p.second}+07:00`
53
+ return new Date(dateString)
54
+ }
55
+
56
+ /**
57
+ * Format time in Vietnam timezone
58
+ * Output: HH:mm (e.g. 08:05)
59
+ */
60
+ export function formatVietnamTime(date: Date | string): string {
61
+ const p = getVietnamParts(toDateObj(date))
62
+ return `${p.hour}:${p.minute}`
63
+ }
64
+
65
+ /**
66
+ * Format date in Vietnam timezone
67
+ * Output: dd/MM/yyyy (e.g. 03/02/2026)
68
+ */
69
+ export function formatVietnamDate(date: Date | string): string {
70
+ const p = getVietnamParts(toDateObj(date))
71
+ return `${p.day}/${p.month}/${p.year}`
72
+ }
73
+
74
+ /**
75
+ * Format date and time in Vietnam timezone
76
+ * Output: dd/MM/yyyy HH:mm (e.g. 03/02/2026 08:05)
77
+ */
78
+ export function formatVietnamDateTime(date: Date | string): string {
79
+ const p = getVietnamParts(toDateObj(date))
80
+ return `${p.day}/${p.month}/${p.year} ${p.hour}:${p.minute}`
81
+ }
82
+
83
+ /**
84
+ * Format date and time with seconds in Vietnam timezone
85
+ * Output: dd/MM/yyyy HH:mm:ss (e.g. 03/02/2026 08:05:09)
86
+ */
87
+ export function formatVietnamDateTimeWithSeconds(date: Date | string): string {
88
+ const p = getVietnamParts(toDateObj(date))
89
+ return `${p.day}/${p.month}/${p.year} ${p.hour}:${p.minute}:${p.second}`
90
+ }
91
+
92
+ /**
93
+ * Format time for date-fns with Vietnam timezone
94
+ * Returns date object adjusted to Vietnam timezone for use with date-fns format()
95
+ * @deprecated Use formatVietnamDate/DateTime/Time instead for display
96
+ */
97
+ export function getVietnamDateForFormat(date: Date | string): Date {
98
+ const dateObj = toDateObj(date)
99
+
100
+ // Get timezone offset difference
101
+ const utcTime = dateObj.getTime()
102
+ const vietnamOffset = 7 * 60 * 60 * 1000 // GMT+7 in milliseconds
103
+ const serverOffset = dateObj.getTimezoneOffset() * 60 * 1000
104
+
105
+ // Adjust to Vietnam timezone
106
+ const vietnamTime = utcTime + vietnamOffset - serverOffset
107
+ return new Date(vietnamTime)
108
+ }
109
+
110
+ /**
111
+ * Get date prefix in VN timezone (YYMMDD)
112
+ * @param date optional date, default is now
113
+ */
114
+ export function getVNDatePrefix(date: Date = new Date()): string {
115
+ const p = getVietnamParts(date)
116
+ return `${p.year.slice(-2)}${p.month}${p.day}`
117
+ }
118
+
119
+ /**
120
+ * Get filename-safe timestamp in VN timezone (yyyyMMdd_HHmm)
121
+ * Used for generating export filenames
122
+ * @param date optional date, default is now
123
+ */
124
+ export function getVietnamFilenameTimestamp(date: Date = new Date()): string {
125
+ const p = getVietnamParts(date)
126
+ return `${p.year}${p.month}${p.day}_${p.hour}${p.minute}`
127
+ }
128
+
129
+ /**
130
+ * Lấy mốc giới hạn giờ của Việt Nam (UTC+7) cho một ngày bất kỳ (từ 00:00:00.000 đến 23:59:59.999),
131
+ * đảm bảo Prisma an toàn khỏi việc lùi múi giờ server
132
+ * @param dateStringOrDate chuỗi ngày dạng "yyyy-MM-dd" hoặc Date object
133
+ */
134
+ export function getVietnamDayBounds(dateStringOrDate: string | Date | undefined): { start: Date; end: Date } | undefined {
135
+ if (!dateStringOrDate) return undefined;
136
+
137
+ let year, month, day;
138
+ if (typeof dateStringOrDate === 'string' && dateStringOrDate.includes('-')) {
139
+ const parts = dateStringOrDate.split('T')[0].split('-');
140
+ year = parts[0];
141
+ month = parts[1];
142
+ day = parts[2];
143
+ } else {
144
+ // Nếu truyền vào Date, parse lại mốc "nguyên ngày" ở giờ máy chủ
145
+ const p = getVietnamParts(toDateObj(dateStringOrDate));
146
+ year = p.year;
147
+ month = p.month;
148
+ day = p.day;
149
+ }
150
+
151
+ // Nửa đêm và cuối ngày ở múi giờ Việt Nam (+07:00)
152
+ // Khi server nhận chuỗi này, Date object sẽ map chuẩn xác đến UTC milliseconds cụ thể.
153
+ const start = new Date(`${year}-${month}-${day}T00:00:00.000+07:00`);
154
+ const end = new Date(`${year}-${month}-${day}T23:59:59.999+07:00`);
155
+
156
+ return { start, end };
157
+ }
158
+
159
+ /**
160
+ * Lấy chuỗi định dạng "yyyy-MM-dd" cho ngày đầu tháng và cuối tháng
161
+ * Dựa trên giờ cố định của Việt Nam (tránh lùi/tiến ngày khi server lệch múi giờ)
162
+ */
163
+ export function getVietnamMonthBoundsString(): { from: string; to: string } {
164
+ const p = getVietnamParts(new Date());
165
+ const year = parseInt(p.year, 10);
166
+ const month = parseInt(p.month, 10); // 1-12
167
+
168
+ // Tính ngày cuối tháng bằng cách khai báo mốc ngày "0" của tháng tiếp theo
169
+ const lastDay = new Date(year, month, 0).getDate();
170
+
171
+ const from = `${p.year}-${p.month}-01`;
172
+ const to = `${p.year}-${p.month}-${lastDay.toString().padStart(2, '0')}`;
173
+
174
+ return { from, to };
175
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Fetcher dùng chung cho SWR + helper bóc danh sách từ response API.
3
+ *
4
+ * Trước đây mỗi nơi tự viết `(url) => fetch(url).then(r => r.json())` KHÔNG check
5
+ * `res.ok`, nên khi API lỗi (4xx/5xx) SWR vẫn coi là "thành công" với data rỗng/
6
+ * `{error}` thay vì vào trạng thái error. Fetcher này throw khi `!res.ok` để SWR
7
+ * xử lý đúng, và tự bóc các shape `{items}` / `{data}` / mảng thuần.
8
+ */
9
+
10
+ /** Bóc danh sách từ nhiều shape response: `{items}` | `{data}` | mảng | []. */
11
+ export function unwrapList<T = unknown>(data: unknown): T[] {
12
+ if (Array.isArray(data)) return data as T[]
13
+ if (data && typeof data === "object") {
14
+ const obj = data as Record<string, unknown>
15
+ if (Array.isArray(obj.items)) return obj.items as T[]
16
+ if (Array.isArray(obj.data)) return obj.data as T[]
17
+ }
18
+ return []
19
+ }
20
+
21
+ /**
22
+ * SWR fetcher chuẩn: throw khi response không ok, trả JSON đã parse.
23
+ * Trả `any` để giữ đúng hành vi cũ (code tự bóc `.data`/`.items` sau đó) và để
24
+ * SWR suy luận `data` là `any` thay vì `{}`. Muốn an toàn kiểu thì khai báo ở
25
+ * lời gọi useSWR: `useSWR<MyType>(key, swrFetcher)`.
26
+ */
27
+ export async function swrFetcher(url: string): Promise<any> {
28
+ const res = await fetch(url)
29
+ if (!res.ok) {
30
+ let message = `Request failed: ${res.status}`
31
+ try {
32
+ const body = await res.json()
33
+ if (body?.error || body?.message) message = body.error || body.message
34
+ } catch {
35
+ // body không phải JSON — giữ message mặc định
36
+ }
37
+ throw new Error(message)
38
+ }
39
+ return res.json()
40
+ }
41
+
42
+ /** SWR fetcher trả về MẢNG đã bóc shape (kèm check res.ok). */
43
+ export async function swrListFetcher<T = unknown>(url: string): Promise<T[]> {
44
+ const data = await swrFetcher(url)
45
+ return unwrapList<T>(data)
46
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Recursively serialize Prisma Decimal objects to strings
3
+ * This is needed because Decimal objects cannot be passed to Client Components
4
+ * @param obj - The object to serialize
5
+ * @returns Serialized object with Decimal fields converted to strings
6
+ */
7
+ export function serializeDecimalFields<T>(obj: T): T {
8
+ if (obj === null || obj === undefined) {
9
+ return obj
10
+ }
11
+
12
+ // Use native JSON serialization loop for performance.
13
+ // The recursive JS for-in loop causes blocking on the event loop for large objects (e.g list of orders)
14
+ try {
15
+ const serialized = JSON.stringify(obj, function (key, value) {
16
+ if (value !== null && typeof value === "object") {
17
+ // Detect Prisma Decimal
18
+ const isDecimalInstance =
19
+ value.constructor?.name === "Decimal" ||
20
+ value.constructor?.name === "Decimal2" ||
21
+ typeof value.toNumber === "function"
22
+
23
+ if (isDecimalInstance) {
24
+ return value.toString()
25
+ }
26
+
27
+ // If it's a POJO Decimal (lost prototype), we cannot easily convert to string
28
+ // without the Prisma.Decimal constructor, which rejects POJOs.
29
+ // We leave it as is to avoid returning "[object Object]"
30
+ const isPojoDecimal = value.s !== undefined && value.e !== undefined && Array.isArray(value.d);
31
+ if (isPojoDecimal) {
32
+ // Attempt a basic conversion if it has only one digit block and e is positive (integers)
33
+ if (value.d.length === 1 && value.e >= 0) {
34
+ const str = value.d[0].toString();
35
+ // Assuming basic case for simple numbers, otherwise fallback to returning POJO
36
+ if (value.e === str.length - 1) return (value.s < 0 ? "-" : "") + str;
37
+ }
38
+ }
39
+ }
40
+ return value
41
+ })
42
+ return JSON.parse(serialized)
43
+ } catch (error) {
44
+ console.error("Failed to serialize decimal fields:", error)
45
+ return obj
46
+ }
47
+ }