@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.
- package/CHANGELOG.md +83 -0
- package/package.json +3 -1
- package/src/auth/proxy-gate.ts +80 -0
- package/src/crud/crud-route-handlers.ts +157 -0
- package/src/crud/server-service.ts +312 -0
- package/src/crud/server.ts +18 -0
- package/src/providers/index.tsx +19 -0
- package/src/rbac/role-service.ts +40 -33
- package/src/styles/base.css +41 -0
- package/src/ui/index.tsx +1 -0
- package/src/ui/layout/customizer.tsx +12 -42
- package/src/ui/layout/page-tabs.tsx +8 -42
- package/src/ui/layout/sidebar.tsx +7 -5
- package/src/ui/primitives/index.tsx +1 -0
- package/src/ui/primitives/sidebar.tsx +25 -4
- package/src/ui/shared/index.ts +6 -0
- package/src/ui/shared/page-header.tsx +57 -0
- package/src/ui/shared/status-indicator.tsx +173 -0
- package/src/ui/shared/table-styles.ts +47 -0
- package/src/ui/shared/table-sum-footer.tsx +41 -0
|
@@ -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
|
+
}
|