@goplusvn/core 0.1.12 → 0.1.14

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,173 @@
1
+ "use client"
2
+
3
+ /**
4
+ * StatusIndicator — Semantic status dot + text component
5
+ * Inspired by Plane's priority-icon pattern
6
+ *
7
+ * Maps ERP status strings to semantic colors automatically.
8
+ * Supports custom color overrides for non-standard statuses.
9
+ *
10
+ * @example
11
+ * <StatusIndicator status="active" />
12
+ * <StatusIndicator status="pending" />
13
+ * <StatusIndicator status="cancelled" label="Đã hủy" />
14
+ */
15
+
16
+ import React, { memo, useMemo } from "react"
17
+ import { cn } from "../../utils"
18
+
19
+ // ── Status → Semantic Color Map ──────────────────────────────
20
+ const STATUS_MAP: Record<string, { color: string; dotClass: string; label: string }> = {
21
+ // 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" },
32
+
33
+ // 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" },
49
+ delivery_adjustment: { color: "text-warning-text", dotClass: "bg-warning", label: "Điều chỉnh giao hàng" },
50
+
51
+ // Info / In-progress
52
+ ordered: { color: "text-info-text", dotClass: "bg-info", label: "Đã đặt" },
53
+ shipping: { color: "text-info-text", dotClass: "bg-info", label: "Đang giao" },
54
+ in_transit: { color: "text-info-text", dotClass: "bg-info", label: "Đang vận chuyển" },
55
+ delivering: { color: "text-info-text", dotClass: "bg-info", label: "Đang giao hàng" },
56
+ accounting_approved: { color: "text-info-text", dotClass: "bg-info", label: "Kế toán đã duyệt" },
57
+
58
+ // Danger / Negative
59
+ cancelled: { color: "text-danger-text", dotClass: "bg-danger", label: "Đã hủy" },
60
+ overdue: { color: "text-danger-text", dotClass: "bg-danger", label: "Quá hạn" },
61
+ rejected: { color: "text-danger-text", dotClass: "bg-danger", label: "Từ chối" },
62
+ failed: { color: "text-danger-text", dotClass: "bg-danger", label: "Thất bại" },
63
+ inactive: { color: "text-danger-text", dotClass: "bg-danger", label: "Ngừng hoạt động" },
64
+ unpaid: { color: "text-danger-text", dotClass: "bg-danger", label: "Chưa thanh toán" },
65
+ refunded: { color: "text-danger-text", dotClass: "bg-danger", label: "Đã hoàn tiền" },
66
+ returned: { color: "text-danger-text", dotClass: "bg-danger", label: "Hoàn trả" },
67
+
68
+ // Neutral
69
+ draft: { color: "text-text-tertiary", dotClass: "bg-text-tertiary", label: "Nháp" },
70
+ archived: { color: "text-text-disabled", dotClass: "bg-text-disabled", label: "Lưu trữ" },
71
+ unknown: { color: "text-text-tertiary", dotClass: "bg-text-tertiary", label: "Không xác định" },
72
+ }
73
+
74
+ // ── Tone (text color) → Badge variant ───────────────────────
75
+ // Lets callers render the SAME status taxonomy as a filled colored Badge
76
+ // (bg + text) instead of a bare dot. Single source of truth = STATUS_MAP.
77
+ const TONE_TO_BADGE_VARIANT: Record<
78
+ string,
79
+ "success" | "warning" | "danger" | "info" | "secondary"
80
+ > = {
81
+ "text-success-text": "success",
82
+ "text-warning-text": "warning",
83
+ "text-danger-text": "danger",
84
+ "text-info-text": "info",
85
+ "text-text-tertiary": "secondary",
86
+ "text-text-disabled": "secondary",
87
+ }
88
+
89
+ // ── "Solid / Trầm sang" (direction D) chip styles ────────────
90
+ // Deep step-700 fills + white text. The subtle Badge variants (green-3 etc.)
91
+ // wash out on dark surfaces like the navy order header; these read crisply on
92
+ // both white and navy. Keyed by Badge variant so it tracks the same taxonomy.
93
+ const SOLID_STATUS_CLASS: Record<string, string> = {
94
+ success: "bg-emerald-700 text-white",
95
+ warning: "bg-amber-700 text-white",
96
+ danger: "bg-red-700 text-white",
97
+ info: "bg-blue-700 text-white",
98
+ secondary: "bg-slate-600 text-white",
99
+ }
100
+
101
+ /**
102
+ * Resolve a status string to its canonical label + colors + matching Badge
103
+ * variant. Use when you need a filled colored chip (e.g. the order header)
104
+ * rather than the dot-only `StatusIndicator`.
105
+ *
106
+ * - `badgeVariant`: the subtle semantic Badge variant (good on white surfaces).
107
+ * - `solidClass`: deep "Trầm sang" fill + white text (good on navy/dark bars).
108
+ */
109
+ export function getStatusMeta(status: string, labelOverride?: string) {
110
+ const key = status?.toLowerCase().replace(/\s+/g, "_") ?? "unknown"
111
+ const resolved = STATUS_MAP[key] ?? STATUS_MAP.unknown
112
+ const badgeVariant = TONE_TO_BADGE_VARIANT[resolved.color] ?? "secondary"
113
+ return {
114
+ label: labelOverride ?? resolved.label,
115
+ color: resolved.color,
116
+ dotClass: resolved.dotClass,
117
+ badgeVariant,
118
+ solidClass: SOLID_STATUS_CLASS[badgeVariant] ?? SOLID_STATUS_CLASS.secondary,
119
+ }
120
+ }
121
+
122
+ export interface StatusIndicatorProps {
123
+ /** Status key (e.g., "active", "pending", "cancelled") */
124
+ status: string
125
+ /** Override the default Vietnamese label */
126
+ label?: string
127
+ /** Size variant */
128
+ size?: "sm" | "md" | "lg"
129
+ /** Show dot only (no text) */
130
+ dotOnly?: boolean
131
+ /** Custom dot color class override */
132
+ dotColor?: string
133
+ /** Additional CSS classes */
134
+ className?: string
135
+ }
136
+
137
+ export const StatusIndicator = memo(function StatusIndicator({
138
+ status,
139
+ label: labelOverride,
140
+ size = "md",
141
+ dotOnly = false,
142
+ dotColor,
143
+ className,
144
+ }: StatusIndicatorProps) {
145
+ const resolved = useMemo(() => {
146
+ const key = status?.toLowerCase().replace(/\s+/g, "_") ?? "unknown"
147
+ return STATUS_MAP[key] ?? STATUS_MAP.unknown
148
+ }, [status])
149
+
150
+ const dotSizeClass = size === "sm" ? "size-1.5" : size === "lg" ? "size-3" : "size-2"
151
+ const textSizeClass = size === "sm" ? "text-[11px]" : size === "lg" ? "text-sm" : "text-xs"
152
+ const displayLabel = labelOverride ?? resolved.label
153
+
154
+ if (dotOnly) {
155
+ return (
156
+ <span
157
+ className={cn("inline-block rounded-full", dotColor ?? resolved.dotClass, dotSizeClass, className)}
158
+ title={displayLabel}
159
+ />
160
+ )
161
+ }
162
+
163
+ return (
164
+ <span className={cn("inline-flex items-center gap-1.5", className)}>
165
+ <span
166
+ className={cn("inline-block rounded-full flex-shrink-0", dotColor ?? resolved.dotClass, dotSizeClass)}
167
+ />
168
+ <span className={cn("font-medium", resolved.color, textSizeClass)}>
169
+ {displayLabel}
170
+ </span>
171
+ </span>
172
+ )
173
+ })
@@ -0,0 +1,47 @@
1
+ // Hợp đồng style bảng CHUẨN của app — trích từ bảng "Đơn bán hàng" của vinhhoa
2
+ // để MỌI trang có bảng (trừ trang CRUD generic) dùng lại đồng nhất: header dính,
3
+ // hàng zebra + viền trái primary khi hover, số tiền vàng, footer dính.
4
+ //
5
+ // Cách dùng: import các hằng này thay vì tự viết class rời rạc.
6
+ // <div className={tableShell}><div className={tableScroll}>
7
+ // <Table className={cn(tableBase, "min-w-[1200px]")}>
8
+ // <TableHeader className={theadSticky}><TableRow className={headerRow}>
9
+ // <TableHead className={thClass}>…</TableHead>
10
+ // …
11
+ // <TableRow className={bodyRow} onClick={…}>
12
+ // <TableCell className={sttCell}>{i+1}</TableCell>
13
+ // <TableCell className={moneyCell} style={{ color: MONEY_GOLD }}>…</TableCell>
14
+
15
+ export const tableShell =
16
+ "flex h-full flex-col overflow-hidden rounded-md border border-border bg-card shadow-sm";
17
+
18
+ export const tableScroll = "relative min-h-0 flex-1 overflow-auto show-scrollbar";
19
+
20
+ // w-full ép bảng GIÃN HẾT chiều rộng khung (core mặc định w-max → co cụm theo nội
21
+ // dung trên màn rộng; w-full đè lại). Kèm min-w-[Xpx] ở từng bảng để cuộn ngang
22
+ // khi màn hẹp. border-separate để nền header/footer dính không bị viền cell xuyên qua.
23
+ export const tableBase = "w-full border-separate border-spacing-0";
24
+
25
+ export const theadSticky = "sticky top-0 z-20 bg-card";
26
+
27
+ export const headerRow = "border-b-2 border-border hover:bg-transparent";
28
+
29
+ export const thClass = "whitespace-nowrap bg-card py-2.5 text-xs font-semibold text-foreground";
30
+
31
+ // Hàng dữ liệu: con trỏ tay, zebra, hover đổi nền + viền trái primary (không xê dịch).
32
+ export const bodyRow =
33
+ "group relative cursor-pointer border-b border-l-[3px] border-border/40 border-l-transparent transition-colors even:bg-muted/30 hover:border-l-primary hover:bg-accent/50 dark:hover:bg-slate-800/40";
34
+
35
+ // Hàng không bấm được (chỉ hiển thị) — bỏ con trỏ tay.
36
+ export const bodyRowStatic =
37
+ "group relative border-b border-l-[3px] border-border/40 border-l-transparent transition-colors even:bg-muted/30 hover:bg-accent/40";
38
+
39
+ export const sttCell = "text-center text-xs tabular-nums text-muted-foreground";
40
+
41
+ export const moneyCell = "whitespace-nowrap text-right font-bold tabular-nums";
42
+
43
+ // Vàng tiền tệ dùng thống nhất toàn app (giống ô "Tổng tiền" đơn bán hàng).
44
+ export const MONEY_GOLD = "#c8860b";
45
+
46
+ export const tfootSticky =
47
+ "sticky bottom-0 z-20 border-t-2 border-border bg-card [&>tr]:hover:bg-transparent";
@@ -0,0 +1,41 @@
1
+ "use client";
2
+
3
+ import { TableCell } from "../primitives";
4
+
5
+ import { cn } from "../../utils";
6
+
7
+ /**
8
+ * Ô tổng dùng chung cho dòng <TableFooter> của các bảng danh sách — bám mẫu
9
+ * đơn bán hàng vinhhoa. `value` là tổng TOÀN BỘ kết quả sau lọc (mọi trang),
10
+ * không phải tổng của riêng trang hiển thị.
11
+ */
12
+ export function SumFooterCell({
13
+ value,
14
+ note,
15
+ align = "right",
16
+ colSpan,
17
+ className,
18
+ }: {
19
+ value: string;
20
+ note?: string;
21
+ align?: "left" | "center" | "right";
22
+ colSpan?: number;
23
+ className?: string;
24
+ }) {
25
+ const alignText = align === "left" ? "text-left" : align === "center" ? "text-center" : "text-right";
26
+ const alignItems = align === "left" ? "items-start" : align === "center" ? "items-center" : "items-end";
27
+
28
+ return (
29
+ <TableCell colSpan={colSpan} className={cn("whitespace-nowrap bg-card py-2 tabular-nums", alignText, className)}>
30
+ <div className={cn("flex flex-col leading-tight", alignItems)}>
31
+ <span className="font-bold text-foreground">{value}</span>
32
+ {note != null && <span className="text-[10px] font-medium text-muted-foreground">{note}</span>}
33
+ </div>
34
+ </TableCell>
35
+ );
36
+ }
37
+
38
+ /** Cộng nhanh một trường số trên mảng dòng (tổng-trang phía client). */
39
+ export function sumBy<T>(rows: T[], pick: (row: T) => number | null | undefined) {
40
+ return rows.reduce((acc, row) => acc + (Number(pick(row)) || 0), 0);
41
+ }