@goplusvn/core 0.1.71 → 0.1.73

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 (150) hide show
  1. package/package.json +2 -1
  2. package/src/auth/index.ts +5 -1
  3. package/src/branding/__tests__/identity.test.ts +101 -0
  4. package/src/branding/__tests__/logo-route.test.ts +143 -0
  5. package/src/branding/identity.ts +74 -0
  6. package/src/branding/index.ts +36 -0
  7. package/src/branding/logo-route.ts +249 -0
  8. package/src/branding/pwa.tsx +146 -0
  9. package/src/branding/types.ts +45 -0
  10. package/src/configs/entities/departments.config.ts +1 -0
  11. package/src/configs/entities/material-categories.config.ts +1 -0
  12. package/src/crud/components/crud-export-button.tsx +3 -2
  13. package/src/crud/components/crud-page.tsx +56 -25
  14. package/src/crud/components/crud-row-actions.tsx +16 -4
  15. package/src/crud/crud-route-handlers.test.ts +352 -0
  16. package/src/crud/crud-route-handlers.ts +260 -13
  17. package/src/crud/index.ts +3 -0
  18. package/src/crud/lib/coerce.test.ts +118 -0
  19. package/src/crud/lib/coerce.ts +95 -0
  20. package/src/crud/lib/crud-utils.ts +4 -0
  21. package/src/crud/lib/entity-endpoints.test.ts +41 -0
  22. package/src/crud/lib/entity-endpoints.ts +23 -0
  23. package/src/crud/lib/errors.ts +14 -0
  24. package/src/crud/lib/import-request.ts +99 -0
  25. package/src/crud/lib/mutation-builder.test.ts +114 -0
  26. package/src/crud/lib/mutation-builder.ts +27 -34
  27. package/src/crud/lib/permissions.test.ts +57 -0
  28. package/src/crud/lib/permissions.ts +25 -13
  29. package/src/crud/lib/query-builder.ts +13 -6
  30. package/src/crud/pages/entity-crud-page.tsx +11 -8
  31. package/src/crud/server-service.test.ts +112 -0
  32. package/src/crud/server-service.ts +52 -27
  33. package/src/crud/server.ts +11 -1
  34. package/src/guardrails/rules/design.ts +3 -2
  35. package/src/styles/base.css +16 -0
  36. package/src/types/index.ts +75 -10
  37. package/src/ui/data-display/__tests__/use-client-pagination.test.ts +59 -0
  38. package/src/ui/data-display/data-table/__tests__/data-table-row-memo.test.tsx +96 -0
  39. package/src/ui/data-display/data-table/data-table-context.tsx +5 -0
  40. package/src/ui/data-display/data-table/data-table.tsx +68 -23
  41. package/src/ui/data-display/data-table-pagination.tsx +25 -8
  42. package/src/ui/data-display/index.tsx +1 -0
  43. package/src/ui/data-display/use-client-pagination.ts +48 -0
  44. package/src/ui/errors/error-view.tsx +80 -87
  45. package/src/ui/errors/index.ts +6 -1
  46. package/src/ui/errors/not-found-view.tsx +89 -16
  47. package/src/ui/errors/status-page.tsx +132 -0
  48. package/src/ui/layout/logo.tsx +5 -0
  49. package/src/ui/pages/not-found.tsx +11 -27
  50. package/templates/starter-app/AGENTS.md +1 -1
  51. package/templates/starter-app/next.config.mjs +8 -0
  52. package/templates/starter-app/package.json +1 -0
  53. package/templates/starter-app/prisma/migrations/20260807115347_platform_pages/migration.sql +87 -0
  54. package/templates/starter-app/prisma/migrations/20260808003811_company_profile/migration.sql +25 -0
  55. package/templates/starter-app/public/images/illustrations/characters/character-01.svg +33 -0
  56. package/templates/starter-app/public/images/illustrations/characters/character-02.svg +30 -0
  57. package/templates/starter-app/public/images/illustrations/characters/character-03.svg +30 -0
  58. package/templates/starter-app/public/images/illustrations/characters/character-04.svg +27 -0
  59. package/templates/starter-app/public/images/logos/goeat_logo.png +0 -0
  60. package/templates/starter-app/scripts/rbac-sync.ts +49 -21
  61. package/templates/starter-app/src/__tests__/architecture.test.ts +3 -2
  62. package/templates/starter-app/src/app/[lang]/(main)/actions/page.tsx +4 -4
  63. package/templates/starter-app/src/app/[lang]/(main)/admin/system/cache/page.tsx +1 -2
  64. package/templates/starter-app/src/app/[lang]/(main)/admin/system/company/actions.ts +7 -5
  65. package/templates/starter-app/src/app/[lang]/(main)/admin/system/company/company-profile-form.tsx +38 -11
  66. package/templates/starter-app/src/app/[lang]/(main)/admin/system/company/page.tsx +2 -1
  67. package/templates/starter-app/src/app/[lang]/(main)/crud/[entity]/page.tsx +4 -2
  68. package/templates/starter-app/src/app/[lang]/(main)/layout.tsx +2 -1
  69. package/templates/starter-app/src/app/[lang]/(main)/page.tsx +4 -4
  70. package/templates/starter-app/src/app/[lang]/(main)/resources/page.tsx +5 -5
  71. package/templates/starter-app/src/app/[lang]/(main)/roles/[id]/edit/page.tsx +5 -2
  72. package/templates/starter-app/src/app/[lang]/(main)/roles/new/page.tsx +1 -0
  73. package/templates/starter-app/src/app/[lang]/(main)/roles/page.tsx +1 -0
  74. package/templates/starter-app/src/app/[lang]/(main)/user/profile/actions.ts +4 -2
  75. package/templates/starter-app/src/app/[lang]/(main)/user/profile/page.tsx +3 -1
  76. package/templates/starter-app/src/app/[lang]/(main)/user/profile/profile-client-page.tsx +13 -5
  77. package/templates/starter-app/src/app/[lang]/(main)/users/page.tsx +3 -2
  78. package/templates/starter-app/src/app/[lang]/layout.tsx +27 -2
  79. package/templates/starter-app/src/app/api/admin/system/audit/route.ts +4 -3
  80. package/templates/starter-app/src/app/api/admin/system/jobs/[name]/history/route.ts +1 -1
  81. package/templates/starter-app/src/app/api/admin/system/jobs/route.ts +31 -12
  82. package/templates/starter-app/src/app/api/admin/system/settings/all/route.ts +2 -2
  83. package/templates/starter-app/src/app/api/admin/system/settings/create/route.ts +10 -3
  84. package/templates/starter-app/src/app/api/admin/system/settings/delete/route.ts +10 -3
  85. package/templates/starter-app/src/app/api/admin/system/settings/route.ts +14 -5
  86. package/templates/starter-app/src/app/api/admin/system/settings/toggle-status/route.ts +16 -7
  87. package/templates/starter-app/src/app/api/admin/system/settings/update/route.ts +12 -6
  88. package/templates/starter-app/src/app/api/admin/system/settings/update-full/route.ts +12 -6
  89. package/templates/starter-app/src/app/api/better-auth/[...all]/route.ts +1 -1
  90. package/templates/starter-app/src/app/api/branding/logo/route.ts +25 -0
  91. package/templates/starter-app/src/app/api/crud/[entity]/[id]/route.ts +2 -2
  92. package/templates/starter-app/src/app/api/crud/[entity]/export/route.ts +16 -0
  93. package/templates/starter-app/src/app/api/crud/[entity]/import/route.ts +17 -0
  94. package/templates/starter-app/src/app/api/crud/[entity]/route.ts +2 -2
  95. package/templates/starter-app/src/app/api/error-logs/[id]/route.ts +4 -2
  96. package/templates/starter-app/src/app/api/error-logs/route.ts +26 -19
  97. package/templates/starter-app/src/app/api/files/[...key]/route.ts +1 -2
  98. package/templates/starter-app/src/app/api/job-titles/route.ts +1 -1
  99. package/templates/starter-app/src/app/api/notifications/read/route.ts +1 -1
  100. package/templates/starter-app/src/app/api/notifications/route.ts +1 -1
  101. package/templates/starter-app/src/app/api/notifications/unread-count/route.ts +1 -1
  102. package/templates/starter-app/src/app/api/rbac/permissions-version/route.ts +4 -1
  103. package/templates/starter-app/src/app/api/roles/[id]/route.ts +2 -1
  104. package/templates/starter-app/src/app/api/roles/route.ts +2 -1
  105. package/templates/starter-app/src/app/api/security/sessions/[id]/route.ts +5 -5
  106. package/templates/starter-app/src/app/api/security/sessions/revoke-user/route.ts +1 -1
  107. package/templates/starter-app/src/app/api/security/sessions/route.ts +2 -2
  108. package/templates/starter-app/src/app/api/system-categories/route.ts +39 -14
  109. package/templates/starter-app/src/app/api/system-category-groups/route.ts +32 -14
  110. package/templates/starter-app/src/app/api/tasks/[id]/download/route.ts +7 -3
  111. package/templates/starter-app/src/app/api/tasks/route.ts +5 -2
  112. package/templates/starter-app/src/app/api/upload/route.ts +1 -2
  113. package/templates/starter-app/src/app/api/users/[id]/route.ts +20 -12
  114. package/templates/starter-app/src/app/api/users/route.ts +11 -9
  115. package/templates/starter-app/src/app/globals.css +0 -1
  116. package/templates/starter-app/src/app/layout.tsx +42 -1
  117. package/templates/starter-app/src/app/manifest.ts +23 -19
  118. package/templates/starter-app/src/components/layout/main-layout-wrapper.tsx +5 -4
  119. package/templates/starter-app/src/configs/entities/department.config.ts +7 -4
  120. package/templates/starter-app/src/configs/entities/index.ts +1 -1
  121. package/templates/starter-app/src/configs/entities/job-titles.config.ts +2 -0
  122. package/templates/starter-app/src/configs/entities/system-alerts.config.ts +2 -0
  123. package/templates/starter-app/src/configs/entities/users.config.ts +15 -2
  124. package/templates/starter-app/src/configs/i18n.ts +6 -6
  125. package/templates/starter-app/src/configs/permissions/menu-tree.ts +1 -1
  126. package/templates/starter-app/src/configs/tenant.ts +6 -1
  127. package/templates/starter-app/src/data/dictionary.ts +2 -1
  128. package/templates/starter-app/src/data/navigations.ts +12 -2
  129. package/templates/starter-app/src/lib/action-guard.ts +3 -4
  130. package/templates/starter-app/src/lib/api-handler.ts +27 -19
  131. package/templates/starter-app/src/lib/auth.ts +3 -1
  132. package/templates/starter-app/src/lib/branch-scope.ts +1 -1
  133. package/templates/starter-app/src/lib/crud/index.ts +5 -12
  134. package/templates/starter-app/src/lib/errors/log-server-error.ts +5 -4
  135. package/templates/starter-app/src/lib/logger.ts +11 -4
  136. package/templates/starter-app/src/lib/page-guard.ts +2 -3
  137. package/templates/starter-app/src/lib/prisma.ts +13 -5
  138. package/templates/starter-app/src/lib/rbac/access.ts +2 -2
  139. package/templates/starter-app/src/lib/storage.ts +3 -1
  140. package/templates/starter-app/src/providers/index.tsx +13 -2
  141. package/templates/starter-app/src/providers/theme-provider.tsx +4 -3
  142. package/templates/starter-app/src/proxy.ts +3 -1
  143. package/templates/starter-app/src/server/services/company-service.ts +4 -2
  144. package/templates/starter-app/src/server/services/notification-service.ts +6 -6
  145. package/templates/starter-app/src/server/services/system-config-service.ts +21 -9
  146. package/templates/starter-app/src/server/services/user-service.ts +2 -2
  147. package/templates/starter-app/src/server/tasks/handlers/export-departments.ts +15 -9
  148. package/templates/starter-app/src/server/tasks/task-runner.ts +6 -7
  149. package/templates/starter-app/tsconfig.json +8 -0
  150. package/templates/starter-app/vitest.config.ts +17 -1
@@ -0,0 +1,146 @@
1
+ import * as React from "react";
2
+
3
+ import type { Metadata, MetadataRoute } from "next";
4
+
5
+ import type { BrandingIdentity } from "./types";
6
+
7
+ /**
8
+ * PWA "cài được lên màn hình chính" — mức mặc định cho mọi app dựng từ core.
9
+ *
10
+ * Ba mảnh phải khớp nhau, thiếu một là iOS lặng lẽ tạo web clip THƯỜNG (bấm ra
11
+ * mở Safari, mất thanh trạng thái riêng, mất standalone):
12
+ * 1. `buildAppManifest` — `/manifest.webmanifest`, icon lấy từ logo công ty.
13
+ * 2. `buildBrandingMetadata` — `icons` + `appleWebApp` cho Metadata API.
14
+ * 3. `<PwaMetaTags>` — render CỨNG trong shell, xem ghi chú bên dưới.
15
+ *
16
+ * Service worker/offline KHÔNG nằm ở đây: app chạy trong nhà máy sóng yếu thì
17
+ * tự thêm serwist ở tầng app, đó là quyết định riêng từng dự án.
18
+ */
19
+
20
+ export interface AppManifestOptions {
21
+ /** Mô tả ngắn. Bỏ trống ⇒ dùng tagline, rồi tới tên app. */
22
+ description?: string;
23
+ display?: MetadataRoute.Manifest["display"];
24
+ orientation?: MetadataRoute.Manifest["orientation"];
25
+ /** `id` PHẢI ổn định — đổi nó là Android coi như một app KHÁC. */
26
+ id?: string;
27
+ scope?: string;
28
+ }
29
+
30
+ export function buildAppManifest(
31
+ identity: BrandingIdentity,
32
+ options: AppManifestOptions = {},
33
+ ): MetadataRoute.Manifest {
34
+ const { logoPath } = identity;
35
+ return {
36
+ id: options.id ?? "/",
37
+ name: identity.appName,
38
+ short_name: identity.appName,
39
+ description: options.description || identity.tagline || identity.appName,
40
+ // start_url có locale sẵn: "/" bắt mỗi lần mở app đi 2–3 chặng redirect
41
+ // server, mấy chặng đó không vẽ được gì ngoài màn trắng.
42
+ start_url: identity.startUrl,
43
+ // Mặc định scope là THƯ MỤC của start_url — khai tường minh, nếu không
44
+ // start_url "/vi/home" sẽ khoá app trong "/vi/".
45
+ scope: options.scope ?? "/",
46
+ display: options.display ?? "standalone",
47
+ orientation: options.orientation,
48
+ background_color: identity.backgroundColor,
49
+ theme_color: identity.themeColor,
50
+ icons: [
51
+ {
52
+ src: `${logoPath}?variant=icon`,
53
+ sizes: "512x512",
54
+ type: "image/png",
55
+ purpose: "any",
56
+ },
57
+ {
58
+ src: `${logoPath}?variant=maskable`,
59
+ sizes: "512x512",
60
+ type: "image/png",
61
+ purpose: "maskable",
62
+ },
63
+ ],
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Phần `Metadata` phụ thuộc thương hiệu — trải vào `generateMetadata` của root
69
+ * layout. Khai `icons` ở đây khiến Next BỎ QUA file `app/icon.png` /
70
+ * `app/apple-icon.png`; ngược lại, còn hai file đó thì convention file-based
71
+ * THẮNG và favicon sẽ mãi là logo cũ.
72
+ */
73
+ export function buildBrandingMetadata(identity: BrandingIdentity): Metadata {
74
+ const { logoPath, logoUrl } = identity;
75
+ const version = logoUrl.includes("?v=") ? `&${logoUrl.split("?")[1]}` : "";
76
+
77
+ return {
78
+ applicationName: identity.appName,
79
+ manifest: "/manifest.webmanifest",
80
+ icons: {
81
+ icon: [
82
+ { url: `${logoPath}?variant=favicon${version}`, type: "image/png" },
83
+ ],
84
+ shortcut: [{ url: `${logoPath}?variant=favicon${version}` }],
85
+ apple: [
86
+ {
87
+ url: `${logoPath}?variant=apple${version}`,
88
+ sizes: "180x180",
89
+ type: "image/png",
90
+ },
91
+ ],
92
+ },
93
+ appleWebApp: {
94
+ capable: true,
95
+ title: identity.appName,
96
+ statusBarStyle: "default",
97
+ },
98
+ openGraph: {
99
+ siteName: identity.appName,
100
+ images: [{ url: `${logoPath}?variant=og${version}`, width: 1200, height: 630 }],
101
+ },
102
+ };
103
+ }
104
+
105
+ /**
106
+ * Thẻ PWA render CỨNG trong shell (đặt ngay đầu `<body>` của root layout).
107
+ *
108
+ * Vì sao không trông vào Metadata API: từ khi layout stream qua `<Suspense>`,
109
+ * Next đẩy output của `generateMetadata` (phải chờ DB) xuống SAU vài chục KB
110
+ * đầu tiên của body. Safari lúc "Thêm vào MH chính" đọc `<head>` ở khoảnh khắc
111
+ * đó chưa thấy `<link rel="manifest">` nên tạo web clip thường. React hoist các
112
+ * thẻ này lên `<head>` ngay từ flush đầu.
113
+ *
114
+ * Next 16 cũng KHÔNG còn phát `apple-mobile-web-app-capable` từ
115
+ * `appleWebApp.capable` (chỉ phát bản `mobile-web-app-capable`) — thẻ apple-*
116
+ * chỉ tồn tại nhờ component này. Trùng thẻ với Metadata API là vô hại: thẻ đầu
117
+ * theo tree order thắng.
118
+ */
119
+ export function PwaMetaTags({
120
+ manifestHref = "/manifest.webmanifest",
121
+ logoPath = "/api/branding/logo",
122
+ appName,
123
+ themeColor,
124
+ }: {
125
+ manifestHref?: string;
126
+ logoPath?: string;
127
+ /** Tên trên biểu tượng iOS. Bỏ trống ⇒ để Metadata API lo. */
128
+ appName?: string;
129
+ themeColor?: string;
130
+ }) {
131
+ // Không nhận `BrandingIdentity`: dựng identity phải chờ DB, mà root layout
132
+ // `await` là màn hình TRẮNG cho tới khi DB trả lời. Các thẻ ở đây cố ý là
133
+ // hằng số thuần.
134
+ return (
135
+ <>
136
+ <link rel="manifest" href={manifestHref} />
137
+ <meta name="apple-mobile-web-app-capable" content="yes" />
138
+ <meta name="mobile-web-app-capable" content="yes" />
139
+ {appName ? (
140
+ <meta name="apple-mobile-web-app-title" content={appName} />
141
+ ) : null}
142
+ {themeColor ? <meta name="theme-color" content={themeColor} /> : null}
143
+ <link rel="apple-touch-icon" href={`${logoPath}?variant=apple`} />
144
+ </>
145
+ );
146
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Hồ sơ công ty ở mức core CẦN BIẾT — cố ý hẹp.
3
+ *
4
+ * Mỗi app có schema `companies` hơi khác nhau (vinhhoa/spartronics dùng cột
5
+ * `logo_url`, thingtodo dùng `logo`), nên core nhận cả hai tên và không đụng
6
+ * tới phần còn lại của bản ghi.
7
+ */
8
+ export interface BrandingCompany {
9
+ /** Tên pháp lý — in trên hợp đồng/hoá đơn. */
10
+ name?: string | null;
11
+ /** Tên hiển thị trên app/PWA. Rỗng ⇒ rơi về `name`, rồi tới tenant. */
12
+ systemName?: string | null;
13
+ /** URL logo đã lưu: `/api/files/<key>`, `/uploads/...` hoặc http(s). */
14
+ logoUrl?: string | null;
15
+ /** Bí danh của `logoUrl` cho app đặt tên cột là `logo`. */
16
+ logo?: string | null;
17
+ /** Dùng làm `?v=` phá cache của URL logo. */
18
+ updatedAt?: Date | string | null;
19
+ }
20
+
21
+ /** Bản sắc thương hiệu đã hoà giải (DB đè lên tenant config tĩnh). */
22
+ export interface BrandingIdentity {
23
+ /** Tên ngắn hiển thị sidebar/PWA. */
24
+ appName: string;
25
+ /** Tên pháp lý đầy đủ. */
26
+ companyName: string;
27
+ /** Dòng phụ dưới tên ở sidebar/đăng nhập. `""` = ẩn. */
28
+ tagline: string;
29
+ /** URL logo dùng chung cho MỌI bề mặt, đã kèm `?v=`. */
30
+ logoUrl: string;
31
+ /** Đường dẫn gốc của route logo (mặc định `/api/branding/logo`). */
32
+ logoPath: string;
33
+ themeColor: string;
34
+ backgroundColor: string;
35
+ /** `start_url` của manifest — PHẢI là đường đã có locale. */
36
+ startUrl: string;
37
+ }
38
+
39
+ /** Các biến thể kích thước mà route logo tự render. */
40
+ export type BrandingLogoVariant =
41
+ | "favicon"
42
+ | "apple"
43
+ | "icon"
44
+ | "maskable"
45
+ | "og";
@@ -17,6 +17,7 @@ export const departmentsConfig: EntityConfig = {
17
17
  icon: Building2,
18
18
  description: "Quản lý cơ cấu tổ chức phòng ban",
19
19
  apiEndpoint: "/api/departments",
20
+ modelName: "department",
20
21
  idField: "id",
21
22
  displayField: "name",
22
23
 
@@ -11,6 +11,7 @@ export const materialCategoriesConfig: EntityConfig = {
11
11
  icon: FolderTree,
12
12
  description: "Quản lý danh mục nguyên vật liệu",
13
13
  apiEndpoint: "/api/material-categories",
14
+ modelName: "materialCategory",
14
15
  idField: "id",
15
16
  displayField: "name",
16
17
 
@@ -53,8 +53,9 @@ export function CrudExportButton({
53
53
  search: activeSearch,
54
54
  });
55
55
 
56
- // Always force format to xlsx
57
- const url = `${endpoint}?${queryParams}&format=xlsx`;
56
+ // Always force format to xlsx. Endpoint từ getEntityEndpoints có thể đã
57
+ // mang sẵn query params — nối bằng "&" thay vì "?" thứ hai.
58
+ const url = `${endpoint}${endpoint.includes("?") ? "&" : "?"}${queryParams}&format=xlsx`;
58
59
 
59
60
  const response = await fetch(url);
60
61
  if (!response.ok) {
@@ -32,6 +32,7 @@ import { useRouter, useParams } from "next/navigation";
32
32
  import { CrudBulkActions } from "./crud-bulk-actions";
33
33
  import { CrudDeleteDialog } from "./crud-delete-dialog";
34
34
  import { crudService } from "../lib/crud-service";
35
+ import { getEntityEndpoints } from "../lib/entity-endpoints";
35
36
  import { usePrefetch } from "../../hooks";
36
37
  import type { DictionaryType } from "../../hooks";
37
38
 
@@ -618,31 +619,65 @@ function CrudPageContent({
618
619
  }
619
620
 
620
621
  default: {
621
- // Custom handler: Get original config to access handler function
622
- // NOTE: This logic is deprecated because Core package cannot import App configs directly.
623
- // Custom handlers passed via config functions are lost during serialization.
624
- // If you need custom actions with handlers, please implement them via `customActions` prop
625
- // or a registered event handler mechanism.
622
+ // Action HTTP thuần data (urlTemplate + method) sống sót qua
623
+ // serializeConfig nên khai được ngay trong entity config phía server.
624
+ if (rowAction.urlTemplate) {
625
+ // Fail-closed: khai permissionAction user không quyền thì thôi
626
+ // (menu đã ẩn, đây chốt chặn thứ hai).
627
+ if (
628
+ rowAction.permissionAction &&
629
+ !permissions[rowAction.permissionAction]
630
+ ) {
631
+ return;
632
+ }
633
+ if (
634
+ rowAction.confirmMessage &&
635
+ !window.confirm(rowAction.confirmMessage)
636
+ ) {
637
+ return;
638
+ }
639
+ const url = rowAction.urlTemplate.replace(
640
+ /\{id\}/g,
641
+ encodeURIComponent(rowId),
642
+ );
643
+ const response = await fetch(url, {
644
+ method: rowAction.method ?? "POST",
645
+ });
646
+ if (!response.ok) {
647
+ let message = `${response.status} ${response.statusText}`;
648
+ try {
649
+ const body = await response.json();
650
+ message = body.error || body.message || message;
651
+ } catch {
652
+ // body không phải JSON — giữ status text
653
+ }
654
+ throw new Error(message);
655
+ }
656
+ await fetchData(false);
657
+ toast.success(t("success", "Success"), {
658
+ description:
659
+ rowAction.successMessage ??
660
+ tMessage("updated", { entity: config.label }),
661
+ });
662
+ break;
663
+ }
626
664
 
665
+ // Custom handler qua config function bị serializeConfig strip —
666
+ // action không có urlTemplate thì phải đi qua customActions prop.
627
667
  console.warn(
628
- `Action "${action}" has no built-in handler and custom handlers via config are not supported in this architecture. Please use customActions prop.`,
668
+ `Action "${action}" has no built-in handler and custom handlers via config are not supported in this architecture. Please use customActions prop or declare urlTemplate.`,
629
669
  );
630
-
631
- /* Deprecated logic removed to fix build:
632
- if (entityName) {
633
- try {
634
- const originalConfig = getEntityConfig(entityName)
635
- ...
636
- }
637
- }
638
- */
639
670
  }
640
671
  }
641
672
  } catch (error) {
642
673
  console.error(`Error executing ${action} action:`, error);
643
674
  globalError.emit({
644
675
  title: t("error", "Error"),
645
- description: tMessage("saveFailed", { entity: rowAction.label }),
676
+ // Ưu tiên message từ API (đọc từ body JSON) — fallback câu chung.
677
+ description:
678
+ error instanceof Error && error.message
679
+ ? error.message
680
+ : tMessage("saveFailed", { entity: rowAction.label }),
646
681
  trace: error instanceof Error ? error.stack : undefined,
647
682
  });
648
683
  }
@@ -858,18 +893,14 @@ function CrudPageContent({
858
893
  className="h-6 hidden sm:block"
859
894
  />
860
895
  <div className="flex items-center gap-2 flex-wrap sm:flex-nowrap">
896
+ {/* Import/Export dùng CÙNG quy tắc endpoint từ config
897
+ (getEntityEndpoints) — trước đây export hardcode
898
+ /api/crud/... lệch với import → 404. */}
861
899
  {permissions.import && config.features?.import && (
862
900
  <Suspense fallback={null}>
863
901
  <ImportDialog
864
902
  config={config}
865
- apiUrl={(() => {
866
- // Handle endpoint with query params correctly
867
- const baseUrl = config.apiEndpoint.split("?")[0];
868
- const queryParams = config.apiEndpoint.includes("?")
869
- ? config.apiEndpoint.split("?")[1]
870
- : "";
871
- return `${baseUrl}/import${queryParams ? `?${queryParams}` : ""}`;
872
- })()}
903
+ apiUrl={getEntityEndpoints(config).import}
873
904
  canImport={permissions.import}
874
905
  />
875
906
  </Suspense>
@@ -877,7 +908,7 @@ function CrudPageContent({
877
908
  {permissions.export && config.features?.export && (
878
909
  <Suspense fallback={null}>
879
910
  <CrudExportButton
880
- endpoint={`/api/crud/${entityName || config.name}/export`}
911
+ endpoint={getEntityEndpoints(config).export}
881
912
  filters={filters}
882
913
  search={search}
883
914
  canExport={permissions.export}
@@ -4,8 +4,11 @@ import {
4
4
  Archive,
5
5
  Copy,
6
6
  Eye,
7
+ History,
7
8
  MoreHorizontal,
8
9
  Pencil,
10
+ Pin,
11
+ SquareArrowOutUpRight,
9
12
  Trash2,
10
13
  CheckCircle2,
11
14
  XCircle,
@@ -81,8 +84,15 @@ export function CrudRowActions({
81
84
  return <CheckCircle2 className="mr-2 h-4 w-4" aria-hidden="true" />;
82
85
  case "reject":
83
86
  return <XCircle className="mr-2 h-4 w-4" aria-hidden="true" />;
87
+ case "set-default":
88
+ return <Pin className="mr-2 h-4 w-4" aria-hidden="true" />;
89
+ case "view-history":
90
+ return <History className="mr-2 h-4 w-4" aria-hidden="true" />;
84
91
  default:
85
- return null;
92
+ // Action tự khai không có icon riêng — icon fallback trung tính.
93
+ return (
94
+ <SquareArrowOutUpRight className="mr-2 h-4 w-4" aria-hidden="true" />
95
+ );
86
96
  }
87
97
  };
88
98
 
@@ -139,9 +149,11 @@ export function CrudRowActions({
139
149
  }
140
150
  };
141
151
 
142
- // Check if user has permission for this custom action
143
- const hasActionPermission =
144
- action.action === "approve"
152
+ // Gate theo permissionAction khai trong config; không khai thì
153
+ // giữ special-case approve/reject cũ, còn lại luôn hiện.
154
+ const hasActionPermission = action.permissionAction
155
+ ? Boolean(permissions[action.permissionAction])
156
+ : action.action === "approve"
145
157
  ? permissions.approve
146
158
  : action.action === "reject"
147
159
  ? permissions.reject