@goplusvn/core 0.1.71 → 0.1.72

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 (124) hide show
  1. package/package.json +2 -1
  2. package/src/auth/index.ts +5 -1
  3. package/src/configs/entities/departments.config.ts +1 -0
  4. package/src/configs/entities/material-categories.config.ts +1 -0
  5. package/src/crud/components/crud-export-button.tsx +3 -2
  6. package/src/crud/components/crud-page.tsx +56 -25
  7. package/src/crud/components/crud-row-actions.tsx +16 -4
  8. package/src/crud/crud-route-handlers.test.ts +352 -0
  9. package/src/crud/crud-route-handlers.ts +260 -13
  10. package/src/crud/index.ts +3 -0
  11. package/src/crud/lib/coerce.test.ts +118 -0
  12. package/src/crud/lib/coerce.ts +95 -0
  13. package/src/crud/lib/crud-utils.ts +4 -0
  14. package/src/crud/lib/entity-endpoints.test.ts +41 -0
  15. package/src/crud/lib/entity-endpoints.ts +23 -0
  16. package/src/crud/lib/errors.ts +14 -0
  17. package/src/crud/lib/import-request.ts +99 -0
  18. package/src/crud/lib/mutation-builder.test.ts +114 -0
  19. package/src/crud/lib/mutation-builder.ts +27 -34
  20. package/src/crud/lib/permissions.test.ts +57 -0
  21. package/src/crud/lib/permissions.ts +25 -13
  22. package/src/crud/lib/query-builder.ts +13 -6
  23. package/src/crud/pages/entity-crud-page.tsx +11 -8
  24. package/src/crud/server-service.test.ts +112 -0
  25. package/src/crud/server-service.ts +52 -27
  26. package/src/crud/server.ts +11 -1
  27. package/src/guardrails/rules/design.ts +3 -2
  28. package/src/types/index.ts +75 -10
  29. package/src/ui/data-display/__tests__/use-client-pagination.test.ts +59 -0
  30. package/src/ui/data-display/data-table/__tests__/data-table-row-memo.test.tsx +96 -0
  31. package/src/ui/data-display/data-table/data-table-context.tsx +5 -0
  32. package/src/ui/data-display/data-table/data-table.tsx +68 -23
  33. package/src/ui/data-display/data-table-pagination.tsx +25 -8
  34. package/src/ui/data-display/index.tsx +1 -0
  35. package/src/ui/data-display/use-client-pagination.ts +48 -0
  36. package/templates/starter-app/AGENTS.md +1 -1
  37. package/templates/starter-app/prisma/migrations/20260807115347_platform_pages/migration.sql +87 -0
  38. package/templates/starter-app/prisma/migrations/20260808003811_company_profile/migration.sql +25 -0
  39. package/templates/starter-app/scripts/rbac-sync.ts +49 -21
  40. package/templates/starter-app/src/__tests__/architecture.test.ts +3 -2
  41. package/templates/starter-app/src/app/[lang]/(main)/actions/page.tsx +4 -4
  42. package/templates/starter-app/src/app/[lang]/(main)/admin/system/cache/page.tsx +1 -2
  43. package/templates/starter-app/src/app/[lang]/(main)/admin/system/company/actions.ts +7 -5
  44. package/templates/starter-app/src/app/[lang]/(main)/admin/system/company/company-profile-form.tsx +20 -9
  45. package/templates/starter-app/src/app/[lang]/(main)/admin/system/company/page.tsx +2 -1
  46. package/templates/starter-app/src/app/[lang]/(main)/crud/[entity]/page.tsx +4 -2
  47. package/templates/starter-app/src/app/[lang]/(main)/layout.tsx +2 -1
  48. package/templates/starter-app/src/app/[lang]/(main)/page.tsx +4 -4
  49. package/templates/starter-app/src/app/[lang]/(main)/resources/page.tsx +5 -5
  50. package/templates/starter-app/src/app/[lang]/(main)/roles/[id]/edit/page.tsx +5 -2
  51. package/templates/starter-app/src/app/[lang]/(main)/roles/new/page.tsx +1 -0
  52. package/templates/starter-app/src/app/[lang]/(main)/roles/page.tsx +1 -0
  53. package/templates/starter-app/src/app/[lang]/(main)/user/profile/actions.ts +4 -2
  54. package/templates/starter-app/src/app/[lang]/(main)/user/profile/page.tsx +3 -1
  55. package/templates/starter-app/src/app/[lang]/(main)/user/profile/profile-client-page.tsx +13 -5
  56. package/templates/starter-app/src/app/[lang]/(main)/users/page.tsx +3 -2
  57. package/templates/starter-app/src/app/[lang]/layout.tsx +2 -2
  58. package/templates/starter-app/src/app/api/admin/system/audit/route.ts +4 -3
  59. package/templates/starter-app/src/app/api/admin/system/jobs/[name]/history/route.ts +1 -1
  60. package/templates/starter-app/src/app/api/admin/system/jobs/route.ts +31 -12
  61. package/templates/starter-app/src/app/api/admin/system/settings/all/route.ts +2 -2
  62. package/templates/starter-app/src/app/api/admin/system/settings/create/route.ts +10 -3
  63. package/templates/starter-app/src/app/api/admin/system/settings/delete/route.ts +10 -3
  64. package/templates/starter-app/src/app/api/admin/system/settings/route.ts +14 -5
  65. package/templates/starter-app/src/app/api/admin/system/settings/toggle-status/route.ts +16 -7
  66. package/templates/starter-app/src/app/api/admin/system/settings/update/route.ts +12 -6
  67. package/templates/starter-app/src/app/api/admin/system/settings/update-full/route.ts +12 -6
  68. package/templates/starter-app/src/app/api/better-auth/[...all]/route.ts +1 -1
  69. package/templates/starter-app/src/app/api/crud/[entity]/[id]/route.ts +2 -2
  70. package/templates/starter-app/src/app/api/crud/[entity]/export/route.ts +16 -0
  71. package/templates/starter-app/src/app/api/crud/[entity]/import/route.ts +17 -0
  72. package/templates/starter-app/src/app/api/crud/[entity]/route.ts +2 -2
  73. package/templates/starter-app/src/app/api/error-logs/[id]/route.ts +4 -2
  74. package/templates/starter-app/src/app/api/error-logs/route.ts +26 -19
  75. package/templates/starter-app/src/app/api/files/[...key]/route.ts +1 -2
  76. package/templates/starter-app/src/app/api/job-titles/route.ts +1 -1
  77. package/templates/starter-app/src/app/api/notifications/read/route.ts +1 -1
  78. package/templates/starter-app/src/app/api/notifications/route.ts +1 -1
  79. package/templates/starter-app/src/app/api/notifications/unread-count/route.ts +1 -1
  80. package/templates/starter-app/src/app/api/rbac/permissions-version/route.ts +4 -1
  81. package/templates/starter-app/src/app/api/roles/[id]/route.ts +2 -1
  82. package/templates/starter-app/src/app/api/roles/route.ts +2 -1
  83. package/templates/starter-app/src/app/api/security/sessions/[id]/route.ts +5 -5
  84. package/templates/starter-app/src/app/api/security/sessions/revoke-user/route.ts +1 -1
  85. package/templates/starter-app/src/app/api/security/sessions/route.ts +2 -2
  86. package/templates/starter-app/src/app/api/system-categories/route.ts +39 -14
  87. package/templates/starter-app/src/app/api/system-category-groups/route.ts +32 -14
  88. package/templates/starter-app/src/app/api/tasks/[id]/download/route.ts +7 -3
  89. package/templates/starter-app/src/app/api/tasks/route.ts +5 -2
  90. package/templates/starter-app/src/app/api/upload/route.ts +1 -2
  91. package/templates/starter-app/src/app/api/users/[id]/route.ts +20 -12
  92. package/templates/starter-app/src/app/api/users/route.ts +11 -9
  93. package/templates/starter-app/src/app/globals.css +0 -1
  94. package/templates/starter-app/src/components/layout/main-layout-wrapper.tsx +5 -4
  95. package/templates/starter-app/src/configs/entities/department.config.ts +7 -4
  96. package/templates/starter-app/src/configs/entities/index.ts +1 -1
  97. package/templates/starter-app/src/configs/entities/job-titles.config.ts +2 -0
  98. package/templates/starter-app/src/configs/entities/system-alerts.config.ts +2 -0
  99. package/templates/starter-app/src/configs/entities/users.config.ts +15 -2
  100. package/templates/starter-app/src/configs/i18n.ts +6 -6
  101. package/templates/starter-app/src/configs/permissions/menu-tree.ts +1 -1
  102. package/templates/starter-app/src/data/dictionary.ts +2 -1
  103. package/templates/starter-app/src/data/navigations.ts +12 -2
  104. package/templates/starter-app/src/lib/action-guard.ts +3 -4
  105. package/templates/starter-app/src/lib/api-handler.ts +27 -19
  106. package/templates/starter-app/src/lib/auth.ts +3 -1
  107. package/templates/starter-app/src/lib/branch-scope.ts +1 -1
  108. package/templates/starter-app/src/lib/crud/index.ts +5 -12
  109. package/templates/starter-app/src/lib/errors/log-server-error.ts +5 -4
  110. package/templates/starter-app/src/lib/logger.ts +11 -4
  111. package/templates/starter-app/src/lib/page-guard.ts +2 -3
  112. package/templates/starter-app/src/lib/prisma.ts +13 -5
  113. package/templates/starter-app/src/lib/rbac/access.ts +2 -2
  114. package/templates/starter-app/src/lib/storage.ts +3 -1
  115. package/templates/starter-app/src/providers/index.tsx +2 -1
  116. package/templates/starter-app/src/providers/theme-provider.tsx +4 -3
  117. package/templates/starter-app/src/server/services/company-service.ts +4 -2
  118. package/templates/starter-app/src/server/services/notification-service.ts +6 -6
  119. package/templates/starter-app/src/server/services/system-config-service.ts +21 -9
  120. package/templates/starter-app/src/server/services/user-service.ts +2 -2
  121. package/templates/starter-app/src/server/tasks/handlers/export-departments.ts +15 -9
  122. package/templates/starter-app/src/server/tasks/task-runner.ts +6 -7
  123. package/templates/starter-app/tsconfig.json +8 -0
  124. package/templates/starter-app/vitest.config.ts +17 -1
@@ -25,22 +25,34 @@ import {
25
25
  } from "../primitives/client";
26
26
 
27
27
  interface DataTablePaginationProps<TData> {
28
- table: Table<TData>;
28
+ /**
29
+ * Instance tanstack. BỎ TRỐNG khi dùng thuần state: truyền đủ
30
+ * totalItems/currentPage/pageSize + onPageChange/onPageSizeChange
31
+ * (ghép với hook `useClientPagination`).
32
+ */
33
+ table?: Table<TData>;
29
34
  totalItems?: number;
35
+ /** Trang hiện tại, 1-BASED (KHÁC pageIndex 0-based của tanstack). */
30
36
  currentPage?: number;
31
37
  pageSize?: number;
38
+ /** Nhận trang 1-based. Có mặt thì THẮNG table.setPageIndex. */
39
+ onPageChange?: (page: number) => void;
40
+ /** Có mặt thì THẮNG table.setPageSize. Reset về trang 1 là việc của caller (useClientPagination đã lo). */
41
+ onPageSizeChange?: (pageSize: number) => void;
32
42
  }
33
43
 
34
- export function DataTablePagination<TData>({
44
+ export function DataTablePagination<TData = unknown>({
35
45
  table,
36
46
  totalItems,
37
47
  currentPage,
38
48
  pageSize,
49
+ onPageChange,
50
+ onPageSizeChange,
39
51
  }: DataTablePaginationProps<TData>) {
40
- const state = table.getState().pagination;
41
- const effPageSize = pageSize ?? state.pageSize;
42
- const effCurrentPage = currentPage ?? state.pageIndex + 1;
43
- const total = totalItems ?? table.getFilteredRowModel().rows.length;
52
+ const state = table?.getState().pagination;
53
+ const effPageSize = pageSize ?? state?.pageSize ?? 10;
54
+ const effCurrentPage = currentPage ?? (state ? state.pageIndex + 1 : 1);
55
+ const total = totalItems ?? table?.getFilteredRowModel().rows.length ?? 0;
44
56
  const pageCount = Math.max(1, Math.ceil(total / effPageSize));
45
57
  const startItem = total > 0 ? (effCurrentPage - 1) * effPageSize + 1 : 0;
46
58
  const endItem = total > 0 ? Math.min(effCurrentPage * effPageSize, total) : 0;
@@ -52,7 +64,8 @@ export function DataTablePagination<TData>({
52
64
 
53
65
  const goToPage = (page: number) => {
54
66
  if (page < 1 || page > pageCount) return;
55
- table.setPageIndex(page - 1);
67
+ if (onPageChange) onPageChange(page);
68
+ else table?.setPageIndex(page - 1);
56
69
  };
57
70
 
58
71
  const handlePageInputBlur = () => {
@@ -77,7 +90,11 @@ export function DataTablePagination<TData>({
77
90
  </p>
78
91
  <Select
79
92
  value={`${effPageSize}`}
80
- onValueChange={(value) => table.setPageSize(Number(value))}
93
+ onValueChange={(value) => {
94
+ const size = Number(value);
95
+ if (onPageSizeChange) onPageSizeChange(size);
96
+ else table?.setPageSize(size);
97
+ }}
81
98
  >
82
99
  <SelectTrigger className="!h-6 !min-h-0 py-0 w-[60px] sm:w-[65px] text-[11px] px-1.5 focus:ring-inset focus:ring-offset-0">
83
100
  <SelectValue placeholder={effPageSize} />
@@ -14,6 +14,7 @@ export { DataTableViewOptions } from "./data-table-view-options";
14
14
 
15
15
  export * from "./formatted-number-input";
16
16
  export * from "./data-table-pagination";
17
+ export * from "./use-client-pagination";
17
18
  export * from "./avatar";
18
19
  export * from "./kpi-card";
19
20
  export * from "./compact-stat-bar";
@@ -0,0 +1,48 @@
1
+ // @goerp/core/ui/data-display
2
+ // Hook phân trang CLIENT-STATE THUẦN cho bảng client-side (dialog/tab không
3
+ // muốn gắn URL). Ghép với <DataTablePagination totalItems currentPage pageSize
4
+ // onPageChange onPageSizeChange> (không cần table tanstack). Trang 1-based.
5
+ // Cần gắn URL thì dùng UrlPagination/ShallowPagination, KHÔNG dùng hook này.
6
+
7
+ "use client";
8
+
9
+ import { useCallback, useMemo, useState } from "react";
10
+
11
+ export interface ClientPagination<T> {
12
+ /** Trang hiện tại, 1-based — đã clamp vào [1, pageCount] khi items co lại. */
13
+ page: number;
14
+ setPage: (page: number) => void;
15
+ pageSize: number;
16
+ /** Đổi cỡ trang thì TỰ về trang 1 (tránh rơi ngoài phạm vi). */
17
+ setPageSize: (pageSize: number) => void;
18
+ /** Lát cắt items của trang hiện tại. */
19
+ pageItems: T[];
20
+ total: number;
21
+ pageCount: number;
22
+ }
23
+
24
+ export function useClientPagination<T>(
25
+ items: T[],
26
+ initialPageSize = 10,
27
+ ): ClientPagination<T> {
28
+ const [rawPage, setPage] = useState(1);
29
+ const [pageSize, setPageSizeState] = useState(initialPageSize);
30
+
31
+ const total = items.length;
32
+ const pageCount = Math.max(1, Math.ceil(total / pageSize));
33
+ // Clamp thay vì setState trong effect: items co lại (xoá/lọc) không tạo
34
+ // render trung gian với trang rỗng.
35
+ const page = Math.min(Math.max(1, rawPage), pageCount);
36
+
37
+ const pageItems = useMemo(
38
+ () => items.slice((page - 1) * pageSize, page * pageSize),
39
+ [items, page, pageSize],
40
+ );
41
+
42
+ const setPageSize = useCallback((size: number) => {
43
+ setPageSizeState(size);
44
+ setPage(1);
45
+ }, []);
46
+
47
+ return { page, setPage, pageSize, setPageSize, pageItems, total, pageCount };
48
+ }
@@ -58,7 +58,7 @@ ESLint, nên khi thấy id thì tra thẳng được:
58
58
  | `layering/*` | ngoài module chỉ import qua public API; `lib` không gọi ngược lên `modules`; domain không dính `next/*` |
59
59
  | `one-door/*` | một Prisma client, một cửa kho tập tin / phạm vi chi nhánh; export qua `useExport`; API 500 phải ghi `error_logs` |
60
60
  | `structure/*` | đặt tên & vị trí file trong `app/`; `.dockerignore` không nuốt mã nguồn |
61
- | `design/*` | không `tailwind.config.ts`; token lấy từ core; không `flex {bp}:hidden`; bảng dùng `DataTableWrapper` |
61
+ | `design/*` | không `tailwind.config.ts`; token lấy từ core; không `flex {bp}:hidden`; bảng dùng `DataTable` từ `@goerp/core/ui` |
62
62
  | `safety/*` | không `FOR UPDATE` thô; không gọi mạng trong `$transaction`; `findMany` trong page phải có `take` |
63
63
  | `debt/*` | trần `any` theo thư mục — chỉ được hạ, và **không được dư** |
64
64
 
@@ -0,0 +1,87 @@
1
+ -- CreateTable
2
+ CREATE TABLE "job_titles" (
3
+ "id" TEXT NOT NULL,
4
+ "code" TEXT NOT NULL,
5
+ "name" TEXT NOT NULL,
6
+ "description" TEXT,
7
+ "level" INTEGER NOT NULL DEFAULT 0,
8
+ "status" TEXT NOT NULL DEFAULT 'active',
9
+ "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
10
+ "updated_at" TIMESTAMP(3) NOT NULL,
11
+ "created_by" TEXT,
12
+ "updated_by" TEXT,
13
+
14
+ CONSTRAINT "job_titles_pkey" PRIMARY KEY ("id")
15
+ );
16
+
17
+ -- CreateTable
18
+ CREATE TABLE "system_categories" (
19
+ "id" TEXT NOT NULL,
20
+ "code" TEXT NOT NULL,
21
+ "name" TEXT NOT NULL,
22
+ "group_code" TEXT NOT NULL,
23
+ "description" TEXT NOT NULL,
24
+ "status" TEXT NOT NULL DEFAULT 'active',
25
+ "order" INTEGER,
26
+ "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
27
+ "created_by" TEXT NOT NULL,
28
+ "updated_at" TIMESTAMP(3) NOT NULL,
29
+ "updated_by" TEXT NOT NULL,
30
+
31
+ CONSTRAINT "system_categories_pkey" PRIMARY KEY ("id")
32
+ );
33
+
34
+ -- CreateTable
35
+ CREATE TABLE "system_category_groups" (
36
+ "id" TEXT NOT NULL,
37
+ "code" TEXT NOT NULL,
38
+ "name" TEXT NOT NULL,
39
+ "description" TEXT NOT NULL,
40
+ "status" TEXT NOT NULL DEFAULT 'active',
41
+ "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
42
+ "created_by" TEXT NOT NULL,
43
+ "updated_at" TIMESTAMP(3) NOT NULL,
44
+ "updated_by" TEXT NOT NULL,
45
+
46
+ CONSTRAINT "system_category_groups_pkey" PRIMARY KEY ("id")
47
+ );
48
+
49
+ -- CreateTable
50
+ CREATE TABLE "system_alerts" (
51
+ "id" TEXT NOT NULL,
52
+ "title" TEXT NOT NULL,
53
+ "message" TEXT NOT NULL,
54
+ "type" TEXT NOT NULL DEFAULT 'info',
55
+ "status" TEXT NOT NULL DEFAULT 'pending',
56
+ "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
57
+ "created_by" TEXT,
58
+ "updated_at" TIMESTAMP(3) NOT NULL,
59
+ "updated_by" TEXT,
60
+
61
+ CONSTRAINT "system_alerts_pkey" PRIMARY KEY ("id")
62
+ );
63
+
64
+ -- CreateIndex
65
+ CREATE UNIQUE INDEX "job_titles_code_key" ON "job_titles"("code");
66
+
67
+ -- CreateIndex
68
+ CREATE INDEX "job_titles_code_idx" ON "job_titles"("code");
69
+
70
+ -- CreateIndex
71
+ CREATE INDEX "job_titles_status_idx" ON "job_titles"("status");
72
+
73
+ -- CreateIndex
74
+ CREATE INDEX "job_titles_level_idx" ON "job_titles"("level");
75
+
76
+ -- CreateIndex
77
+ CREATE UNIQUE INDEX "system_categories_code_key" ON "system_categories"("code");
78
+
79
+ -- CreateIndex
80
+ CREATE UNIQUE INDEX "system_category_groups_code_key" ON "system_category_groups"("code");
81
+
82
+ -- CreateIndex
83
+ CREATE INDEX "system_alerts_status_idx" ON "system_alerts"("status");
84
+
85
+ -- CreateIndex
86
+ CREATE INDEX "system_alerts_type_idx" ON "system_alerts"("type");
87
+
@@ -0,0 +1,25 @@
1
+ -- CreateTable
2
+ CREATE TABLE "companies" (
3
+ "id" TEXT NOT NULL,
4
+ "name" TEXT NOT NULL,
5
+ "tax_code" TEXT NOT NULL,
6
+ "address" TEXT NOT NULL,
7
+ "phone" TEXT NOT NULL,
8
+ "email" TEXT NOT NULL,
9
+ "logo_url" TEXT,
10
+ "system_name" TEXT,
11
+ "status" TEXT NOT NULL DEFAULT 'active',
12
+ "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
13
+ "created_by" TEXT NOT NULL DEFAULT 'system',
14
+ "updated_at" TIMESTAMP(3) NOT NULL,
15
+ "updated_by" TEXT NOT NULL DEFAULT 'system',
16
+
17
+ CONSTRAINT "companies_pkey" PRIMARY KEY ("id")
18
+ );
19
+
20
+ -- CreateIndex
21
+ CREATE UNIQUE INDEX "companies_tax_code_key" ON "companies"("tax_code");
22
+
23
+ -- CreateIndex
24
+ CREATE INDEX "companies_status_idx" ON "companies"("status");
25
+
@@ -27,6 +27,7 @@ import { randomUUID } from "node:crypto"
27
27
 
28
28
  import { PrismaPg } from "@prisma/adapter-pg"
29
29
  import { PrismaClient } from "@prisma/client"
30
+
30
31
  import "dotenv/config"
31
32
 
32
33
  import { permissionRegistry } from "../src/configs/permissions"
@@ -46,29 +47,39 @@ const STANDARD_ACTIONS: Record<string, string> = {
46
47
  }
47
48
 
48
49
  const connectionString = process.env.DATABASE_URL
49
- if (!connectionString) throw new Error("DATABASE_URL chưa được set (xem .env.example)")
50
+ if (!connectionString)
51
+ throw new Error("DATABASE_URL chưa được set (xem .env.example)")
50
52
  const prisma = new PrismaClient({ adapter: new PrismaPg({ connectionString }) })
51
53
 
52
54
  async function main() {
53
- const now = new Date()
54
- const resourceCount = permissionRegistry.reduce((n, f) => n + f.resources.length, 0)
55
+ const resourceCount = permissionRegistry.reduce(
56
+ (n, f) => n + f.resources.length,
57
+ 0
58
+ )
55
59
  console.log(
56
- `→ rbac-sync: ${permissionRegistry.length} phân hệ, ${resourceCount} resource${DRY_RUN ? " [DRY RUN]" : ""}`,
60
+ `→ rbac-sync: ${permissionRegistry.length} phân hệ, ${resourceCount} resource${DRY_RUN ? " [DRY RUN]" : ""}`
57
61
  )
58
62
 
59
63
  // ── 1. Action: tạo nếu thiếu ───────────────────────────────────────────
60
- const declaredActions = new Map<string, { name: string; description?: string }>()
64
+ const declaredActions = new Map<
65
+ string,
66
+ { name: string; description?: string }
67
+ >()
61
68
  for (const feature of permissionRegistry) {
62
69
  for (const a of feature.customActions ?? []) {
63
70
  declaredActions.set(a.code, { name: a.name, description: a.description })
64
71
  }
65
72
  }
66
73
  const usedActionCodes = new Set(
67
- permissionRegistry.flatMap((f) => f.resources.flatMap((r) => [...r.actions])),
74
+ permissionRegistry.flatMap((f) =>
75
+ f.resources.flatMap((r) => [...r.actions])
76
+ )
68
77
  )
69
78
 
70
79
  const existingActionCodes = new Set(
71
- (await prisma.action.findMany({ select: { code: true } })).map((a) => a.code),
80
+ (await prisma.action.findMany({ select: { code: true } })).map(
81
+ (a) => a.code
82
+ )
72
83
  )
73
84
 
74
85
  for (const code of usedActionCodes) {
@@ -81,7 +92,7 @@ async function main() {
81
92
  // Dừng hẳn thay vì tạo action rác: sai chính tả một action code sẽ khiến
82
93
  // quyền không bao giờ khớp và nút bấm ẩn vĩnh viễn.
83
94
  throw new Error(
84
- `Action "${code}" được dùng nhưng không nằm trong bộ chuẩn và chưa khai ở customActions của phân hệ nào.`,
95
+ `Action "${code}" được dùng nhưng không nằm trong bộ chuẩn và chưa khai ở customActions của phân hệ nào.`
85
96
  )
86
97
  }
87
98
  console.log(` + action: ${code} (${decl.name})`)
@@ -113,12 +124,14 @@ async function main() {
113
124
  }
114
125
  // Lớp tùy chỉnh của admin (thêm/tắt action trên UI) được BẢO TOÀN:
115
126
  // hiệu lực = registry ∪ customActions − disabledActions
116
- const custom = Array.isArray(config.customActions) ? (config.customActions as string[]) : []
127
+ const custom = Array.isArray(config.customActions)
128
+ ? (config.customActions as string[])
129
+ : []
117
130
  const disabled = Array.isArray(config.disabledActions)
118
131
  ? (config.disabledActions as string[])
119
132
  : []
120
133
  config.actions = [...new Set([...r.actions, ...custom])].filter(
121
- (a) => !disabled.includes(a),
134
+ (a) => !disabled.includes(a)
122
135
  )
123
136
 
124
137
  if (!existing) {
@@ -147,14 +160,16 @@ async function main() {
147
160
  }
148
161
 
149
162
  // ── 3. Grant mặc định — CHỈ cho resource vừa tạo (trừ khi --force-grants) ──
150
- const wantsGrants = (code: string) => FORCE_GRANTS || createdResourceCodes.has(code)
163
+ const wantsGrants = (code: string) =>
164
+ FORCE_GRANTS || createdResourceCodes.has(code)
151
165
  let skippedGrants = 0
152
166
 
153
167
  const neededRoleCodes = new Set<string>()
154
168
  for (const feature of permissionRegistry) {
155
169
  for (const r of feature.resources) {
156
170
  if (!wantsGrants(r.code)) continue
157
- for (const roleCode of Object.keys(r.defaultGrants ?? {})) neededRoleCodes.add(roleCode)
171
+ for (const roleCode of Object.keys(r.defaultGrants ?? {}))
172
+ neededRoleCodes.add(roleCode)
158
173
  }
159
174
  }
160
175
  const presentRoles = new Set(
@@ -165,7 +180,7 @@ async function main() {
165
180
  select: { code: true },
166
181
  })
167
182
  ).map((r) => r.code)
168
- : [],
183
+ : []
169
184
  )
170
185
 
171
186
  for (const feature of permissionRegistry) {
@@ -174,11 +189,14 @@ async function main() {
174
189
  for (const [roleCode, grant] of Object.entries(r.defaultGrants ?? {})) {
175
190
  if (!presentRoles.has(roleCode)) {
176
191
  skippedGrants++
177
- console.warn(` ! vai trò "${roleCode}" chưa tồn tại — bỏ qua grant cho ${r.code}`)
192
+ console.warn(
193
+ ` ! vai trò "${roleCode}" chưa tồn tại — bỏ qua grant cho ${r.code}`
194
+ )
178
195
  continue
179
196
  }
180
197
  for (const actionCode of grant === "*" ? r.actions : grant) {
181
- if (VERBOSE || !FORCE_GRANTS) console.log(` ✓ grant: ${roleCode} → ${r.code}:${actionCode}`)
198
+ if (VERBOSE || !FORCE_GRANTS)
199
+ console.log(` ✓ grant: ${roleCode} → ${r.code}:${actionCode}`)
182
200
  if (!DRY_RUN) {
183
201
  await prisma.rolePermission.upsert({
184
202
  where: {
@@ -188,7 +206,12 @@ async function main() {
188
206
  actionCode,
189
207
  },
190
208
  },
191
- create: { id: randomUUID(), roleCode, resourceCode: r.code, actionCode },
209
+ create: {
210
+ id: randomUUID(),
211
+ roleCode,
212
+ resourceCode: r.code,
213
+ actionCode,
214
+ },
192
215
  update: {},
193
216
  })
194
217
  }
@@ -199,22 +222,27 @@ async function main() {
199
222
 
200
223
  // ── 4. Drift (chỉ cảnh báo) ────────────────────────────────────────────
201
224
  const registryCodes = new Set(
202
- permissionRegistry.flatMap((f) => f.resources.map((r) => r.code)),
225
+ permissionRegistry.flatMap((f) => f.resources.map((r) => r.code))
203
226
  )
204
227
  const legacy = (
205
- await prisma.resource.findMany({ where: { status: "active" }, select: { code: true } })
228
+ await prisma.resource.findMany({
229
+ where: { status: "active" },
230
+ select: { code: true },
231
+ })
206
232
  )
207
233
  .map((r) => r.code)
208
234
  .filter((code) => !registryCodes.has(code))
209
235
  .sort()
210
236
 
211
237
  if (legacy.length) {
212
- console.log(`→ Drift: ${legacy.length} resource trong DB chưa khai registry`)
238
+ console.log(
239
+ `→ Drift: ${legacy.length} resource trong DB chưa khai registry`
240
+ )
213
241
  if (VERBOSE) console.log(` ${legacy.join(", ")}`)
214
242
  }
215
243
 
216
244
  console.log(
217
- `→ Xong: ${createdResourceCodes.size} resource mới, ${registryCodes.size - createdResourceCodes.size} resource cập nhật.`,
245
+ `→ Xong: ${createdResourceCodes.size} resource mới, ${registryCodes.size - createdResourceCodes.size} resource cập nhật.`
218
246
  )
219
247
 
220
248
  // Bỏ qua grant = resource đã tạo nhưng KHÔNG ai có quyền, và lần chạy sau
@@ -222,7 +250,7 @@ async function main() {
222
250
  if (skippedGrants) {
223
251
  console.warn(
224
252
  `\n⚠ ${skippedGrants} grant bị bỏ vì vai trò chưa tồn tại. Chạy \`pnpm seed\` để tạo vai trò,` +
225
- ` rồi \`pnpm rbac-sync --force-grants\` để áp lại — nếu không sẽ không ai vào được màn hình nào.`,
253
+ ` rồi \`pnpm rbac-sync --force-grants\` để áp lại — nếu không sẽ không ai vào được màn hình nào.`
226
254
  )
227
255
  }
228
256
  }
@@ -2,9 +2,10 @@ import { join } from "node:path"
2
2
 
3
3
  import { runCoreGuardrails } from "@goerp/core/guardrails"
4
4
 
5
- import { permissionRegistry } from "@/configs/permissions"
6
5
  import { navigations } from "@/data/navigations"
7
6
 
7
+ import { permissionRegistry } from "@/configs/permissions"
8
+
8
9
  /**
9
10
  * HÀNG RÀO KIẾN TRÚC — mỗi thước ở đây ứng với một lỗi im lặng đã từng làm mất
10
11
  * thời gian trong app thật: quyền khai sai thì nút biến mất mà không báo lỗi,
@@ -44,7 +45,7 @@ runCoreGuardrails({
44
45
  // tương ứng trong CÙNG commit — trần dư cũng bị báo đỏ.
45
46
  "debt/any": {
46
47
  app: 2, // Record<string, any> cho where của Prisma (audit, error-logs)
47
- lib: 2, // session as any api-handler / page-guard
48
+ lib: 0, // Session của app gán thẳng vào checkPermission — không cần ép kiểu
48
49
  server: 1, // mask(config: any) trong system-config-service
49
50
  },
50
51
  },
@@ -1,14 +1,14 @@
1
- import { requirePageAccess } from "@/lib/page-guard"
1
+ // Trang hành động = catalog TRA CỨU. Action mới khai trong customActions của
2
+ // Permission Registry rồi chạy rbac-sync.
3
+ import { ActionCatalogPage } from "@goerp/core/rbac/pages"
2
4
  import {
3
5
  getActionsForPermissionMatrix,
4
6
  getResourcesForPermissionMatrix,
5
7
  } from "@goerp/core/rbac/resource-service"
6
- // Trang hành động = catalog TRA CỨU. Action mới khai trong customActions của
7
- // Permission Registry rồi chạy rbac-sync.
8
- import { ActionCatalogPage } from "@goerp/core/rbac/pages"
9
8
 
10
9
  import { getActionMetaMap } from "@/configs/permissions"
11
10
  import { RESOURCE_PAGES } from "@/configs/permissions/menu-tree"
11
+ import { requirePageAccess } from "@/lib/page-guard"
12
12
  import { db } from "@/lib/prisma"
13
13
 
14
14
  export const metadata = { title: "Hành động" }
@@ -13,8 +13,7 @@ export const metadata = { title: "Bộ nhớ đệm" }
13
13
  // serialize closure → "Functions cannot be passed directly to Client Components".
14
14
  async function assertCacheAccess(action: "view" | "update") {
15
15
  const session = await getSession()
16
- const s = session as Parameters<typeof checkPermission>[0]
17
- if (!checkPermission(s, "system-cache", action)) {
16
+ if (!checkPermission(session, "system-cache", action)) {
18
17
  throw new Error("Forbidden")
19
18
  }
20
19
  }
@@ -1,12 +1,11 @@
1
1
  "use server"
2
2
 
3
3
  import { revalidatePath } from "next/cache"
4
+ import { updateSystemCompany } from "@/server/services/company-service"
5
+
6
+ import type { CompanyProfileInput } from "@/server/services/company-service"
4
7
 
5
8
  import { requirePermission } from "@/lib/action-guard"
6
- import {
7
- updateSystemCompany,
8
- type CompanyProfileInput,
9
- } from "@/server/services/company-service"
10
9
 
11
10
  /** Hồ sơ công ty dùng chung quyền với Cấu hình hệ thống (system-setting). */
12
11
  export async function submitCompanyProfile(data: CompanyProfileInput) {
@@ -15,7 +14,10 @@ export async function submitCompanyProfile(data: CompanyProfileInput) {
15
14
  const session = await requirePermission("system-setting", "update")
16
15
  userId = session.user?.id ?? "system"
17
16
  } catch {
18
- return { success: false, error: "Bạn không có quyền cập nhật hồ sơ công ty" }
17
+ return {
18
+ success: false,
19
+ error: "Bạn không có quyền cập nhật hồ sơ công ty",
20
+ }
19
21
  }
20
22
 
21
23
  try {
@@ -1,11 +1,6 @@
1
1
  "use client"
2
2
 
3
3
  import * as React from "react"
4
- import { useForm } from "react-hook-form"
5
- import { zodResolver } from "@hookform/resolvers/zod"
6
- import { toast } from "sonner"
7
- import { z } from "zod"
8
-
9
4
  import {
10
5
  Card,
11
6
  CardContent,
@@ -21,6 +16,10 @@ import {
21
16
  } from "@goerp/core/ui"
22
17
  import { Button } from "@goerp/core/ui/primitives/button"
23
18
  import { Input } from "@goerp/core/ui/primitives/input"
19
+ import { zodResolver } from "@hookform/resolvers/zod"
20
+ import { useForm } from "react-hook-form"
21
+ import { toast } from "sonner"
22
+ import { z } from "zod"
24
23
 
25
24
  import { submitCompanyProfile } from "./actions"
26
25
 
@@ -58,7 +57,11 @@ const FIELDS: Array<{
58
57
  { name: "address", label: "Địa chỉ" },
59
58
  { name: "phone", label: "Số điện thoại" },
60
59
  { name: "email", label: "Email" },
61
- { name: "systemName", label: "Tên hệ thống (hiển thị trên app)", placeholder: "Bỏ trống = dùng tên công ty" },
60
+ {
61
+ name: "systemName",
62
+ label: "Tên hệ thống (hiển thị trên app)",
63
+ placeholder: "Bỏ trống = dùng tên công ty",
64
+ },
62
65
  ]
63
66
 
64
67
  export function CompanyProfileForm({ initialData }: CompanyProfileFormProps) {
@@ -129,9 +132,15 @@ export function CompanyProfileForm({ initialData }: CompanyProfileFormProps) {
129
132
  <div className="flex h-20 w-20 shrink-0 items-center justify-center overflow-hidden rounded-lg border border-border bg-muted">
130
133
  {logoUrl ? (
131
134
  // eslint-disable-next-line @next/next/no-img-element -- ảnh động theo DB, không qua next/image
132
- <img src={logoUrl} alt="Logo công ty" className="h-full w-full object-contain" />
135
+ <img
136
+ src={logoUrl}
137
+ alt="Logo công ty"
138
+ className="h-full w-full object-contain"
139
+ />
133
140
  ) : (
134
- <span className="text-[11px] text-muted-foreground">Chưa có</span>
141
+ <span className="text-[11px] text-muted-foreground">
142
+ Chưa có
143
+ </span>
135
144
  )}
136
145
  </div>
137
146
  <div className="flex flex-col gap-2">
@@ -148,7 +157,9 @@ export function CompanyProfileForm({ initialData }: CompanyProfileFormProps) {
148
157
  <button
149
158
  type="button"
150
159
  className="underline"
151
- onClick={() => form.setValue("logoUrl", "", { shouldDirty: true })}
160
+ onClick={() =>
161
+ form.setValue("logoUrl", "", { shouldDirty: true })
162
+ }
152
163
  >
153
164
  Gỡ logo
154
165
  </button>
@@ -1,7 +1,8 @@
1
+ import { getSystemCompany } from "@/server/services/company-service"
2
+
1
3
  import type { Metadata } from "next"
2
4
 
3
5
  import { requirePageAccess } from "@/lib/page-guard"
4
- import { getSystemCompany } from "@/server/services/company-service"
5
6
 
6
7
  import { CompanyProfileForm } from "./company-profile-form"
7
8
 
@@ -2,8 +2,9 @@ import { EntityCrudPage } from "@goerp/core/crud"
2
2
 
3
3
  import type { LocaleType } from "@goerp/core/types"
4
4
 
5
- import { getEntityConfig } from "@/configs/entities"
6
5
  import { dictionary } from "@/data/dictionary"
6
+
7
+ import { getEntityConfig } from "@/configs/entities"
7
8
  import { getSession } from "@/lib/auth"
8
9
 
9
10
  export const dynamic = "force-dynamic"
@@ -22,7 +23,8 @@ export default async function CrudEntityPage({
22
23
  if (!config) {
23
24
  return (
24
25
  <div className="p-6 text-sm text-muted-foreground">
25
- Không tìm thấy entity: <code className="rounded bg-muted px-1">{entity}</code>
26
+ Không tìm thấy entity:{" "}
27
+ <code className="rounded bg-muted px-1">{entity}</code>
26
28
  </div>
27
29
  )
28
30
  }
@@ -1,9 +1,10 @@
1
1
  import type { ReactNode } from "react"
2
2
 
3
- import { MainLayoutWrapper } from "@/components/layout/main-layout-wrapper"
4
3
  import { dictionary } from "@/data/dictionary"
5
4
  import { navigations } from "@/data/navigations"
6
5
 
6
+ import { MainLayoutWrapper } from "@/components/layout/main-layout-wrapper"
7
+
7
8
  export default function MainAreaLayout({ children }: { children: ReactNode }) {
8
9
  return (
9
10
  <MainLayoutWrapper dictionary={dictionary} navigation={navigations}>
@@ -1,7 +1,7 @@
1
- import { StatBar } from "@goerp/core/ui"
2
- import { ArrowRight, Building2, ShieldCheck, Sparkles } from "lucide-react"
3
1
  import Link from "next/link"
4
2
  import { redirect } from "next/navigation"
3
+ import { StatBar } from "@goerp/core/ui"
4
+ import { ArrowRight, Building2, ShieldCheck, Sparkles } from "lucide-react"
5
5
 
6
6
  import { i18n } from "@/configs/i18n"
7
7
  import { getSession } from "@/lib/auth"
@@ -33,8 +33,8 @@ export default async function HomePage() {
33
33
  </h1>
34
34
  <p className="text-sm text-muted-foreground">
35
35
  App mẫu chạy trên{" "}
36
- <code className="rounded bg-muted px-1">@goerp/core</code> — auth, RBAC,
37
- CRUD engine và design system đều từ core.
36
+ <code className="rounded bg-muted px-1">@goerp/core</code> — auth,
37
+ RBAC, CRUD engine và design system đều từ core.
38
38
  </p>
39
39
  </div>
40
40
 
@@ -1,15 +1,15 @@
1
- import { requirePageAccess } from "@/lib/page-guard"
2
- import {
3
- getActionsForPermissionMatrix,
4
- getResourcesForPermissionMatrix,
5
- } from "@goerp/core/rbac/resource-service"
6
1
  // Trang tài nguyên = catalog TRA CỨU (đọc từ DB đã rbac-sync). Khai báo/chỉnh
7
2
  // sửa nằm trong Permission Registry (src/configs/permissions) + rbac-sync,
8
3
  // không sửa trực tiếp ở đây.
9
4
  import { ResourceCatalogPage } from "@goerp/core/rbac/pages"
5
+ import {
6
+ getActionsForPermissionMatrix,
7
+ getResourcesForPermissionMatrix,
8
+ } from "@goerp/core/rbac/resource-service"
10
9
 
11
10
  import { getActionMetaMap, getLookupPolicyMap } from "@/configs/permissions"
12
11
  import { MENU_TREE } from "@/configs/permissions/menu-tree"
12
+ import { requirePageAccess } from "@/lib/page-guard"
13
13
  import { db } from "@/lib/prisma"
14
14
 
15
15
  export const metadata = { title: "Tài nguyên" }