@goplusvn/core 0.1.47 → 0.1.49

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.
@@ -20,3 +20,8 @@ export type {
20
20
  ActionCatalogPageProps,
21
21
  CatalogMenuSection,
22
22
  } from "./permission-catalog-pages";
23
+
24
+ export {
25
+ registerResourceGroupMeta,
26
+ type ResourceGroupMeta,
27
+ } from "./lib/group-taxonomy";
@@ -0,0 +1,38 @@
1
+ // API contract CRUD /api/actions — DÙNG CHUNG cho action-list-page và
2
+ // Action Manager nhúng trong resource-list-page. Trước 2026-07 khối
3
+ // fetch + error semantics này bị copy nguyên văn ở cả 2 trang ("2 chỗ sửa
4
+ // 1 nghiệp vụ" — audit); giờ endpoint/method/payload/parse-lỗi sống 1 nơi,
5
+ // mỗi trang chỉ còn state + toast riêng của nó.
6
+ import { throwFetchError } from "../../lib/fetch-error";
7
+
8
+ export interface ActionPayload {
9
+ code: string;
10
+ name: string;
11
+ description?: string;
12
+ }
13
+
14
+ /** Tạo (POST) hoặc cập nhật (PUT khi có editingId) một Action. Throw khi lỗi
15
+ * (đã parse message qua throwFetchError) — caller bắt và toast. */
16
+ export async function saveAction<T = unknown>(
17
+ payload: ActionPayload,
18
+ editingId?: string,
19
+ ): Promise<T> {
20
+ const body = { ...payload, status: "active" };
21
+ const res = await fetch("/api/actions", {
22
+ method: editingId ? "PUT" : "POST",
23
+ headers: { "Content-Type": "application/json" },
24
+ body: JSON.stringify(editingId ? { id: editingId, ...body } : body),
25
+ });
26
+ if (!res.ok) {
27
+ await throwFetchError(res, "Operation failed");
28
+ }
29
+ return res.json();
30
+ }
31
+
32
+ /** Xóa một Action theo id. Throw khi lỗi — caller bắt và toast. */
33
+ export async function deleteActionById(id: string): Promise<void> {
34
+ const res = await fetch(`/api/actions?id=${id}`, { method: "DELETE" });
35
+ if (!res.ok) {
36
+ await throwFetchError(res, "Delete failed");
37
+ }
38
+ }
@@ -0,0 +1,110 @@
1
+ // Taxonomy NHÓM resource hiển thị ở các trang RBAC. Default là bộ nhóm GoERP
2
+ // chuẩn; app có nhóm/nhãn riêng thì ĐĂNG KÝ MỘT LẦN ở setup (cùng pattern
3
+ // configureSettingsService) thay vì sửa core + chờ republish — đúng lớp lỗi
4
+ // OCP mà audit 2026-07 chỉ ra.
5
+ import type { ComponentType } from "react";
6
+
7
+ import {
8
+ Box,
9
+ Boxes,
10
+ ClipboardList,
11
+ Settings,
12
+ ShoppingBag,
13
+ Truck,
14
+ Users,
15
+ Wallet,
16
+ } from "lucide-react";
17
+
18
+ export interface ResourceGroupMeta {
19
+ icon: ComponentType<{ className?: string }>;
20
+ label: string;
21
+ iconCls: string;
22
+ }
23
+
24
+ const FALLBACK_ICON_CLS =
25
+ "text-slate-600 bg-slate-200 dark:text-slate-300 dark:bg-slate-500/20";
26
+
27
+ const groupMeta: Record<string, ResourceGroupMeta> = {
28
+ sales: {
29
+ icon: ShoppingBag,
30
+ label: "Bán hàng",
31
+ iconCls: "text-blue-600 bg-blue-100 dark:text-blue-400 dark:bg-blue-500/15",
32
+ },
33
+ purchase: {
34
+ icon: ClipboardList,
35
+ label: "Mua hàng",
36
+ iconCls:
37
+ "text-amber-600 bg-amber-100 dark:text-amber-400 dark:bg-amber-500/15",
38
+ },
39
+ inventory: {
40
+ icon: Truck,
41
+ label: "Kho",
42
+ iconCls:
43
+ "text-emerald-600 bg-emerald-100 dark:text-emerald-400 dark:bg-emerald-500/15",
44
+ },
45
+ finance: {
46
+ icon: Wallet,
47
+ label: "Tài chính",
48
+ iconCls:
49
+ "text-violet-600 bg-violet-100 dark:text-violet-400 dark:bg-violet-500/15",
50
+ },
51
+ hr: {
52
+ icon: Users,
53
+ label: "Nhân sự",
54
+ iconCls: "text-rose-600 bg-rose-100 dark:text-rose-400 dark:bg-rose-500/15",
55
+ },
56
+ master: {
57
+ icon: Boxes,
58
+ label: "Danh mục",
59
+ iconCls: "text-cyan-600 bg-cyan-100 dark:text-cyan-400 dark:bg-cyan-500/15",
60
+ },
61
+ users: {
62
+ icon: Users,
63
+ label: "Người dùng",
64
+ iconCls:
65
+ "text-fuchsia-600 bg-fuchsia-100 dark:text-fuchsia-400 dark:bg-fuchsia-500/15",
66
+ },
67
+ system: {
68
+ icon: Settings,
69
+ label: "Hệ thống",
70
+ iconCls: FALLBACK_ICON_CLS,
71
+ },
72
+ other: {
73
+ icon: Box,
74
+ label: "Khác",
75
+ iconCls: FALLBACK_ICON_CLS,
76
+ },
77
+ };
78
+
79
+ let groupOrder: string[] = [
80
+ "sales",
81
+ "purchase",
82
+ "inventory",
83
+ "finance",
84
+ "hr",
85
+ "master",
86
+ "users",
87
+ "system",
88
+ "other",
89
+ ];
90
+
91
+ /**
92
+ * App đăng ký thêm/ghi đè metadata nhóm (icon/label/màu) và tuỳ chọn thứ tự.
93
+ * Gọi một lần ở composition root của app (trước khi render trang RBAC).
94
+ */
95
+ export function registerResourceGroupMeta(
96
+ meta: Record<string, ResourceGroupMeta>,
97
+ order?: string[],
98
+ ): void {
99
+ Object.assign(groupMeta, meta);
100
+ if (order) groupOrder = order;
101
+ }
102
+
103
+ export const getGroupMeta = (group: string): ResourceGroupMeta =>
104
+ groupMeta[group?.toLowerCase()] || {
105
+ icon: Box,
106
+ label: group || "Khác",
107
+ iconCls: FALLBACK_ICON_CLS,
108
+ };
109
+
110
+ export const getGroupOrder = (): string[] => groupOrder;
@@ -0,0 +1,148 @@
1
+ "use client";
2
+
3
+ // Khung dùng chung cho các trang RBAC (resource-list, action-list, …):
4
+ // bar navy 1 dòng + toolbar sticky (search + đếm + toggle List/Grid).
5
+ // Trước 2026-07 hai khối này bị copy nguyên văn từng class-string giữa các
6
+ // trang (~31% dòng trùng — audit); giờ skeleton sống 1 nơi, mỗi trang chỉ
7
+ // truyền nội dung (icon/title/subtitle/nút riêng).
8
+ import type { ReactNode } from "react";
9
+
10
+ import { LayoutGrid, List, Search, X } from "lucide-react";
11
+
12
+ import { cn } from "../../../utils";
13
+ import { Input } from "../../../ui/primitives/input";
14
+
15
+ /** Bar navy 1 dòng trên cùng — icon + tiêu đề + subtitle (ẩn mobile) + nút phải. */
16
+ export function RbacPageBar({
17
+ icon,
18
+ title,
19
+ subtitle,
20
+ children,
21
+ }: {
22
+ icon: ReactNode;
23
+ title: string;
24
+ /** Dòng số liệu bên phải tiêu đề — ẩn dưới md. */
25
+ subtitle?: ReactNode;
26
+ /** Các nút bên phải (Thêm, Làm mới, Đồng bộ…). */
27
+ children?: ReactNode;
28
+ }) {
29
+ return (
30
+ <div className="-mx-4 -mt-4 flex flex-wrap items-center gap-1.5 bg-sidebar px-3 py-2 text-sidebar-foreground shadow-md md:-mx-8 lg:mx-3 lg:mt-1 lg:rounded-xl lg:px-4">
31
+ <span className="flex h-8 w-8 shrink-0 items-center justify-center">
32
+ {icon}
33
+ </span>
34
+ <h1 className="min-w-0 flex-1 truncate px-1 text-base font-bold text-primary-foreground sm:text-lg">
35
+ {title}
36
+ </h1>
37
+ {subtitle ? (
38
+ <span className="hidden shrink-0 text-xs tabular-nums text-primary-foreground/70 md:inline">
39
+ {subtitle}
40
+ </span>
41
+ ) : null}
42
+ {children}
43
+ </div>
44
+ );
45
+ }
46
+
47
+ export type RbacViewMode = "list" | "grid";
48
+
49
+ /** Toolbar sticky: ô search có nút xóa + slot nút phụ + bộ đếm + toggle dòng/thẻ. */
50
+ export function RbacStickyToolbar({
51
+ searchTerm,
52
+ onSearchChange,
53
+ searchPlaceholder,
54
+ searchMaxWidthClass = "sm:max-w-72",
55
+ count,
56
+ countLabel,
57
+ matchedCount,
58
+ view,
59
+ onViewChange,
60
+ children,
61
+ }: {
62
+ searchTerm: string;
63
+ onSearchChange: (value: string) => void;
64
+ searchPlaceholder: string;
65
+ /** Giới hạn bề ngang ô search (mặc định sm:max-w-72). */
66
+ searchMaxWidthClass?: string;
67
+ /** Tổng số mục khi KHÔNG search, hiển thị "<count> <countLabel>". */
68
+ count: number;
69
+ countLabel: string;
70
+ /** Số khớp khi ĐANG search — truyền undefined nếu muốn luôn hiện count. */
71
+ matchedCount?: number;
72
+ view: RbacViewMode;
73
+ onViewChange: (view: RbacViewMode) => void;
74
+ /** Nút phụ chen giữa search và bộ đếm (Làm mới…). */
75
+ children?: ReactNode;
76
+ }) {
77
+ const searching = searchTerm.trim().length > 0;
78
+ return (
79
+ <div className="sticky top-0 z-20 flex flex-wrap items-center gap-2 border-b border-border bg-card/95 px-3 py-2 backdrop-blur supports-[backdrop-filter]:bg-card/85 sm:px-4">
80
+ <div className={cn("relative min-w-[140px] flex-1", searchMaxWidthClass)}>
81
+ <Search className="pointer-events-none absolute left-2.5 top-2 h-3.5 w-3.5 text-muted-foreground" />
82
+ <Input
83
+ placeholder={searchPlaceholder}
84
+ className="h-8 w-full rounded-md border border-border bg-card pl-8 pr-8 text-sm"
85
+ value={searchTerm}
86
+ onChange={(e) => onSearchChange(e.target.value)}
87
+ />
88
+ {searchTerm && (
89
+ <button
90
+ type="button"
91
+ onClick={() => onSearchChange("")}
92
+ className="absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
93
+ title="Xóa tìm kiếm"
94
+ >
95
+ <X className="h-3.5 w-3.5" />
96
+ </button>
97
+ )}
98
+ </div>
99
+ {children}
100
+ <p className="ml-auto whitespace-nowrap text-xs text-muted-foreground">
101
+ {searching && matchedCount !== undefined ? (
102
+ <>
103
+ <span className="font-semibold tabular-nums text-foreground">
104
+ {matchedCount}
105
+ </span>{" "}
106
+ khớp
107
+ </>
108
+ ) : (
109
+ <>
110
+ <span className="font-semibold tabular-nums text-foreground">
111
+ {count}
112
+ </span>{" "}
113
+ {countLabel}
114
+ </>
115
+ )}
116
+ </p>
117
+ {/* Chuyển dạng hiển thị: dòng / thẻ */}
118
+ <div className="flex shrink-0 overflow-hidden rounded-md border border-border">
119
+ <button
120
+ type="button"
121
+ onClick={() => onViewChange("list")}
122
+ title="Dạng dòng"
123
+ className={cn(
124
+ "px-2 py-1.5 transition-colors",
125
+ view === "list"
126
+ ? "bg-muted text-foreground"
127
+ : "bg-card text-muted-foreground hover:text-foreground",
128
+ )}
129
+ >
130
+ <List className="h-3.5 w-3.5" />
131
+ </button>
132
+ <button
133
+ type="button"
134
+ onClick={() => onViewChange("grid")}
135
+ title="Dạng thẻ"
136
+ className={cn(
137
+ "border-l border-border px-2 py-1.5 transition-colors",
138
+ view === "grid"
139
+ ? "bg-muted text-foreground"
140
+ : "bg-card text-muted-foreground hover:text-foreground",
141
+ )}
142
+ >
143
+ <LayoutGrid className="h-3.5 w-3.5" />
144
+ </button>
145
+ </div>
146
+ </div>
147
+ );
148
+ }