@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
@@ -1,26 +1,30 @@
1
+ import { buildAppManifest, resolveBranding } from "@goerp/core/branding"
2
+
1
3
  import type { MetadataRoute } from "next"
2
4
 
5
+ import { i18n } from "@/configs/i18n"
3
6
  import { tenant } from "@/configs/tenant"
7
+ import { getSystemCompany } from "@/server/services/company-service"
4
8
 
5
9
  /**
6
- * Web App Manifest — mức PWA "cài được lên màn hình chính" (Add to Home
7
- * Screen), sinh động theo tenant config. Offline/service-worker CHƯA nằm
8
- * đây app cần chạy không mạng (nhà máy sóng yếu) thì thêm serwist ở tầng
9
- * app, đó là quyết định riêng từng dự án.
10
+ * Web App Manifest — mức PWA "cài được lên màn hình chính", sinh ĐỘNG theo hồ
11
+ * công ty: đổi tên/logo /admin/system/company tên app và icon trên màn
12
+ * hình chính đổi theo, không cần build lại.
13
+ *
14
+ * Offline/service-worker CHƯA nằm ở đây — app cần chạy không mạng (nhà máy
15
+ * sóng yếu) thì thêm serwist ở tầng app, đó là quyết định riêng từng dự án.
10
16
  */
11
- export default function manifest(): MetadataRoute.Manifest {
12
- const name = tenant.branding?.companyName ?? tenant.name
13
- return {
14
- name,
15
- short_name: tenant.name,
16
- description: tenant.branding?.tagline || name,
17
- start_url: "/",
18
- display: "standalone",
19
- background_color: "#ffffff",
20
- theme_color: "#4f46e5",
21
- icons: [
22
- // Next tự phục vụ src/app/icon.svg tại /icon — thay icon là đổi luôn ở đây.
23
- { src: "/icon", sizes: "any", type: "image/svg+xml", purpose: "any" },
24
- ],
25
- }
17
+ export const dynamic = "force-dynamic"
18
+
19
+ export default async function manifest(): Promise<MetadataRoute.Manifest> {
20
+ const company = await getSystemCompany()
21
+ const identity = resolveBranding({
22
+ company,
23
+ tenant,
24
+ // start_url có locale sẵn: "/" bắt mỗi lần mở app đi thêm chặng redirect
25
+ // server, chặng đó không vẽ được gì ngoài màn trắng.
26
+ startUrl: `/${i18n.defaultLocale}`,
27
+ })
28
+
29
+ return buildAppManifest(identity)
26
30
  }
@@ -1,8 +1,9 @@
1
- 'use client'
1
+ "use client"
2
2
 
3
- import type { NavigationType } from '@goerp/core/types'
4
- import { NotificationBell } from '@goerp/core/notification/ui'
5
- import { MainLayout } from '@goerp/core/ui'
3
+ import { NotificationBell } from "@goerp/core/notification/ui"
4
+ import { MainLayout } from "@goerp/core/ui"
5
+
6
+ import type { NavigationType } from "@goerp/core/types"
6
7
 
7
8
  export function MainLayoutWrapper({
8
9
  children,
@@ -1,7 +1,7 @@
1
- import { departmentsConfig } from "@goerp/core/configs/entities"
2
-
3
1
  import type { EntityConfig } from "@goerp/core/types"
4
2
 
3
+ import { departmentsConfig } from "@goerp/core/configs/entities"
4
+
5
5
  const CRUD_ENDPOINT = "/api/crud/departments"
6
6
 
7
7
  /**
@@ -18,7 +18,10 @@ export const departmentConfig: EntityConfig = {
18
18
  // generic endpoint (core's default points at /api/departments).
19
19
  fields: departmentsConfig.fields.map((field) =>
20
20
  field.dataSource?.type === "api"
21
- ? { ...field, dataSource: { ...field.dataSource, endpoint: CRUD_ENDPOINT } }
22
- : field,
21
+ ? {
22
+ ...field,
23
+ dataSource: { ...field.dataSource, endpoint: CRUD_ENDPOINT },
24
+ }
25
+ : field
23
26
  ),
24
27
  }
@@ -5,7 +5,7 @@ import { jobTitlesConfig } from "./job-titles.config"
5
5
  import { systemAlertsConfig } from "./system-alerts.config"
6
6
 
7
7
  // Đăng ký entity → cấu hình. Khóa = số nhiều, khớp URL /crud/<key> + apiEndpoint.
8
- // Thêm entity mới: import config rồi thêm 1 dòng ở đây (kèm MODEL_MAP src/lib/crud).
8
+ // Thêm entity mới: import config rồi thêm 1 dòng ở đây (khai `modelName` trong config).
9
9
  export const entityConfigs: Record<string, EntityConfig> = {
10
10
  departments: departmentConfig,
11
11
  "job-titles": jobTitlesConfig,
@@ -11,6 +11,8 @@ export const jobTitlesConfig: EntityConfig = {
11
11
  icon: Briefcase,
12
12
  description: "Quản lý danh sách chức danh trong tổ chức",
13
13
  apiEndpoint: "/api/crud/job-titles",
14
+ // Tên model Prisma — engine core đọc từ đây, không cần MODEL_MAP ở lib/crud.
15
+ modelName: "jobTitle",
14
16
  idField: "id",
15
17
  displayField: "name",
16
18
 
@@ -14,6 +14,8 @@ export const systemAlertsConfig: EntityConfig = {
14
14
  icon: Bell,
15
15
  description: "Quản lý các thông báo và cảnh báo hệ thống",
16
16
  apiEndpoint: "/api/crud/system-alerts",
17
+ // Tên model Prisma — engine core đọc từ đây, không cần MODEL_MAP ở lib/crud.
18
+ modelName: "systemAlert",
17
19
  idField: "id",
18
20
  displayField: "title",
19
21
 
@@ -20,13 +20,26 @@ export const usersConfig: EntityConfig = {
20
20
  iconName: "Users",
21
21
  description: "Quản lý tài khoản đăng nhập, vai trò và chi nhánh",
22
22
  apiEndpoint: "/api/users",
23
+ modelName: "user",
23
24
  idField: "id",
24
25
  displayField: "name",
25
26
 
26
27
  fields: [
27
- { name: "id", label: "ID", type: "text", hideInForm: true, hideInTable: true },
28
+ {
29
+ name: "id",
30
+ label: "ID",
31
+ type: "text",
32
+ hideInForm: true,
33
+ hideInTable: true,
34
+ },
28
35
  { name: "name", label: "Họ tên", type: "text", required: true },
29
- { name: "email", label: "Email", type: "email", required: true, width: 200 },
36
+ {
37
+ name: "email",
38
+ label: "Email",
39
+ type: "email",
40
+ required: true,
41
+ width: 200,
42
+ },
30
43
  {
31
44
  name: "roleNames",
32
45
  label: "Vai trò",
@@ -1,12 +1,12 @@
1
1
  export const i18n = {
2
- defaultLocale: 'vi',
3
- locales: ['vi', 'en'] as const,
2
+ defaultLocale: "vi",
3
+ locales: ["vi", "en"] as const,
4
4
  localeDirection: {
5
- vi: 'ltr',
6
- en: 'ltr',
5
+ vi: "ltr",
6
+ en: "ltr",
7
7
  } as const,
8
8
  localeNames: {
9
- vi: 'vietnamese',
10
- en: 'english',
9
+ vi: "vietnamese",
10
+ en: "english",
11
11
  } as const,
12
12
  } as const
@@ -33,5 +33,5 @@ export const MENU_TREE: MenuTreeSection[] = navigations
33
33
 
34
34
  /** resource → href của trang chính (ActionCatalogPage dùng để link chéo). */
35
35
  export const RESOURCE_PAGES: Record<string, string> = Object.fromEntries(
36
- MENU_TREE.flatMap((s) => s.items.map((i) => [i.resource, i.href ?? ""])),
36
+ MENU_TREE.flatMap((s) => s.items.map((i) => [i.resource, i.href ?? ""]))
37
37
  )
@@ -8,7 +8,12 @@ export const tenant: TenantConfig = {
8
8
  id: "starter",
9
9
  name: "GoERP Starter",
10
10
  branding: {
11
- // logo: "/logo.png",
11
+ // Logo MẶC ĐỊNH của app mới init — file có thật trong public/ của template
12
+ // (goerp-init chép cả public/), nên không 404 như lần trước khi default này
13
+ // là hằng số hardcode trong component Logo. Admin upload logo ở
14
+ // /admin/system/company thì `Company.logoUrl` ĐÈ lên nó (xem
15
+ // src/providers/index.tsx); xoá dòng này thì Logo rơi về monogram chữ cái.
16
+ logo: "/images/logos/goeat_logo.png",
12
17
  companyName: "GoERP Starter",
13
18
  // Dòng phụ dưới tên app ở sidebar/đăng nhập — để "" là ẩn hẳn.
14
19
  tagline: "",
@@ -67,7 +67,8 @@ export const dictionary: DictionaryType = {
67
67
  deleted: "{{entity}} đã được xóa thành công",
68
68
  bulkDeleted: "{{count}} {{entities}} đã được xóa thành công",
69
69
  deleteConfirm: "Bạn có chắc chắn muốn xóa {{entity}} này không?",
70
- bulkDeleteConfirm: "Bạn có chắc chắn muốn xóa {{count}} {{entities}} không?",
70
+ bulkDeleteConfirm:
71
+ "Bạn có chắc chắn muốn xóa {{count}} {{entities}} không?",
71
72
  deleteFailed: "Không thể xóa {{entity}}",
72
73
  saveFailed: "Không thể lưu {{entity}}",
73
74
  loadFailed: "Không thể tải dữ liệu. Vui lòng thử lại.",
@@ -37,14 +37,24 @@ export const navigations: NavigationType[] = [
37
37
  title: "Hệ thống",
38
38
  iconName: "ShieldCheck",
39
39
  items: [
40
- { title: "Người dùng", iconName: "Users", href: "/users", resource: "user" },
40
+ {
41
+ title: "Người dùng",
42
+ iconName: "Users",
43
+ href: "/users",
44
+ resource: "user",
45
+ },
41
46
  {
42
47
  title: "Phiên đăng nhập",
43
48
  iconName: "KeyRound",
44
49
  href: "/security/sessions",
45
50
  resource: "user",
46
51
  },
47
- { title: "Vai trò", iconName: "ShieldCheck", href: "/roles", resource: "role" },
52
+ {
53
+ title: "Vai trò",
54
+ iconName: "ShieldCheck",
55
+ href: "/roles",
56
+ resource: "role",
57
+ },
48
58
  {
49
59
  title: "Tài nguyên",
50
60
  iconName: "Boxes",
@@ -14,13 +14,12 @@ import { getSession } from "@/lib/auth"
14
14
  */
15
15
  export async function requirePermission(
16
16
  resource: string,
17
- action = "view",
17
+ action = "view"
18
18
  ): Promise<Session> {
19
19
  const session = await getSession()
20
20
  if (!session?.user) throw new Error("Unauthorized")
21
- const s = session as Parameters<typeof checkPermission>[0]
22
- if (!checkPermission(s, resource, action)) {
21
+ if (!checkPermission(session, resource, action)) {
23
22
  throw new Error(`Forbidden: ${resource}:${action}`)
24
23
  }
25
- return session as Session
24
+ return session
26
25
  }
@@ -15,13 +15,11 @@
15
15
  import { NextResponse } from "next/server"
16
16
  import { withAuditContext } from "@goerp/core/audit"
17
17
  import { checkPermission as coreCheckPermission } from "@goerp/core/auth"
18
- import {
19
- createApiHandler,
20
- type ApiHandlerMiddleware,
21
- } from "@goerp/core/auth/api-handler"
18
+ import { createApiHandler } from "@goerp/core/auth/api-handler"
22
19
 
23
- import type { NextRequest } from "next/server"
24
20
  import type { Session } from "@/types/session"
21
+ import type { ApiHandlerMiddleware } from "@goerp/core/auth/api-handler"
22
+ import type { NextRequest } from "next/server"
25
23
 
26
24
  import { getRegistryResource } from "@/configs/permissions"
27
25
  import { getSession } from "@/lib/auth"
@@ -40,7 +38,7 @@ interface HandlerContext<TParams = Record<string, string>> {
40
38
 
41
39
  type Handler<TParams = Record<string, string>> = (
42
40
  req: NextRequest,
43
- ctx: HandlerContext<TParams>,
41
+ ctx: HandlerContext<TParams>
44
42
  ) => Promise<Response>
45
43
 
46
44
  interface ApiHandlerOptions {
@@ -70,7 +68,10 @@ function assertResourceDeclared(resource: string) {
70
68
  * trong lib/prisma.ts đọc context này; thiếu nó thì mọi dòng audit_logs đều
71
69
  * ghi tác nhân rỗng.
72
70
  */
73
- const auditContext: ApiHandlerMiddleware<Session> = async (next, { req, session }) => {
71
+ const auditContext: ApiHandlerMiddleware<Session> = async (
72
+ next,
73
+ { req, session }
74
+ ) => {
74
75
  if (!session) return next()
75
76
  const ip =
76
77
  req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||
@@ -78,7 +79,7 @@ const auditContext: ApiHandlerMiddleware<Session> = async (next, { req, session
78
79
  undefined
79
80
  return (await withAuditContext(
80
81
  { userId: session.user.id, userName: session.user.name, ip },
81
- () => next(),
82
+ () => next()
82
83
  )) as Response
83
84
  }
84
85
 
@@ -87,11 +88,14 @@ const auditContext: ApiHandlerMiddleware<Session> = async (next, { req, session
87
88
  * biết user được xem chi nhánh nào. Phạm vi được tính LƯỜI — request không đụng
88
89
  * model nào bị guard thì `resolve` không bao giờ chạy.
89
90
  */
90
- const branchScope: ApiHandlerMiddleware<Session> = async (next, { session }) => {
91
+ const branchScope: ApiHandlerMiddleware<Session> = async (
92
+ next,
93
+ { session }
94
+ ) => {
91
95
  if (!session) return next()
92
96
  return (await runWithBranchScope(
93
97
  { resolve: () => getBranchScope(session) },
94
- () => next(),
98
+ () => next()
95
99
  )) as Response
96
100
  }
97
101
 
@@ -108,7 +112,8 @@ async function onApiError(error: unknown, req: Request): Promise<Response> {
108
112
  userAgent: req.headers.get("user-agent") || undefined,
109
113
  })
110
114
 
111
- const message = error instanceof Error ? error.message : "Internal Server Error"
115
+ const message =
116
+ error instanceof Error ? error.message : "Internal Server Error"
112
117
  return NextResponse.json(
113
118
  {
114
119
  error: message,
@@ -116,9 +121,14 @@ async function onApiError(error: unknown, req: Request): Promise<Response> {
116
121
  // đang in — production thì tuyệt đối không lộ.
117
122
  ...(process.env.NODE_ENV === "production"
118
123
  ? {}
119
- : { debug: { message, stack: error instanceof Error ? error.stack : undefined } }),
124
+ : {
125
+ debug: {
126
+ message,
127
+ stack: error instanceof Error ? error.stack : undefined,
128
+ },
129
+ }),
120
130
  },
121
- { status: 500 },
131
+ { status: 500 }
122
132
  )
123
133
  }
124
134
 
@@ -128,9 +138,7 @@ const coreApiHandler = createApiHandler<Session>({
128
138
  const session = await getSession()
129
139
  return session?.user ? session : null
130
140
  },
131
- checkPermission: (session, resource, action) =>
132
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
133
- coreCheckPermission(session as any, resource, action),
141
+ checkPermission: coreCheckPermission,
134
142
  assertResource: assertResourceDeclared,
135
143
  wrap: [auditContext, branchScope],
136
144
  onError: onApiError,
@@ -138,7 +146,7 @@ const coreApiHandler = createApiHandler<Session>({
138
146
 
139
147
  export function apiHandler<TParams = Record<string, string>>(
140
148
  handler: Handler<TParams>,
141
- options?: ApiHandlerOptions,
149
+ options?: ApiHandlerOptions
142
150
  ) {
143
151
  const route = coreApiHandler(
144
152
  (req, ctx) =>
@@ -147,11 +155,11 @@ export function apiHandler<TParams = Record<string, string>>(
147
155
  session: ctx.session ?? ({} as Session),
148
156
  params: ctx.params as Promise<TParams>,
149
157
  }),
150
- options,
158
+ options
151
159
  )
152
160
  return async (
153
161
  req: NextRequest,
154
162
  // Route tĩnh (không có segment động) được Next gọi mà không kèm ctx.
155
- routeCtx?: { params: Promise<TParams> },
163
+ routeCtx?: { params: Promise<TParams> }
156
164
  ): Promise<Response> => route(req, routeCtx as never)
157
165
  }
@@ -50,7 +50,9 @@ export const getSession = cache(async (): Promise<Session | null> => {
50
50
  return {
51
51
  user: result.user,
52
52
  expires:
53
- expiresAt instanceof Date ? expiresAt.toISOString() : String(expiresAt ?? ""),
53
+ expiresAt instanceof Date
54
+ ? expiresAt.toISOString()
55
+ : String(expiresAt ?? ""),
54
56
  } as unknown as Session
55
57
  })
56
58
 
@@ -27,7 +27,7 @@ import type { Session } from "@/types/session"
27
27
  export function canViewAllBranches(session: Session): boolean {
28
28
  return Boolean(
29
29
  session.user?.roles?.includes("admin") ||
30
- checkPermission(session as never, "branch", "view-all-branches"),
30
+ checkPermission(session, "branch", "view-all-branches")
31
31
  )
32
32
  }
33
33
 
@@ -1,15 +1,8 @@
1
- import { createServerCrudService, getModelName } from "@goerp/core/crud/server"
1
+ import { createServerCrudService } from "@goerp/core/crud/server"
2
2
 
3
3
  import { db } from "@/lib/prisma"
4
4
 
5
- // Engine CRUD generic ở CORE — app chỉ tiêm prisma + map entity(số nhiều)→model Prisma.
6
- const MODEL_MAP: Record<string, string> = {
7
- departments: "department",
8
- "job-titles": "jobTitle",
9
- "system-alerts": "systemAlert",
10
- }
11
-
12
- export const crudService = createServerCrudService({
13
- prisma: db,
14
- getModelName: (entity) => getModelName(entity, MODEL_MAP),
15
- })
5
+ // Engine CRUD generic ở CORE — app chỉ tiêm prisma. Tên model Prisma khai
6
+ // ngay trong từng entity config (`modelName: "jobTitle"`…), không còn
7
+ // MODEL_MAP song song ở đây.
8
+ export const crudService = createServerCrudService({ prisma: db })
@@ -1,7 +1,8 @@
1
- import {
2
- createErrorLogger,
3
- type ErrorLoggerDb,
4
- type ServerErrorContext,
1
+ import { createErrorLogger } from "@goerp/core/errors/server-error"
2
+
3
+ import type {
4
+ ErrorLoggerDb,
5
+ ServerErrorContext,
5
6
  } from "@goerp/core/errors/server-error"
6
7
 
7
8
  import { db } from "@/lib/prisma"
@@ -12,9 +12,12 @@ type LogContext = Record<string, unknown>
12
12
  * được stack — bọc lại ở đây thay vì bắt mỗi call site tự trải ra.
13
13
  */
14
14
  export const logger = {
15
- info: (message: string, context?: LogContext) => coreLogger.info(message, context),
16
- warn: (message: string, context?: LogContext) => coreLogger.warn(message, context),
17
- debug: (message: string, context?: LogContext) => coreLogger.debug(message, context),
15
+ info: (message: string, context?: LogContext) =>
16
+ coreLogger.info(message, context),
17
+ warn: (message: string, context?: LogContext) =>
18
+ coreLogger.warn(message, context),
19
+ debug: (message: string, context?: LogContext) =>
20
+ coreLogger.debug(message, context),
18
21
  error: (message: string, error?: unknown, context?: LogContext) =>
19
22
  coreLogger.error(message, {
20
23
  ...context,
@@ -22,7 +25,11 @@ export const logger = {
22
25
  ? {
23
26
  error:
24
27
  error instanceof Error
25
- ? { name: error.name, message: error.message, stack: error.stack }
28
+ ? {
29
+ name: error.name,
30
+ message: error.message,
31
+ stack: error.stack,
32
+ }
26
33
  : error,
27
34
  }
28
35
  : {}),
@@ -22,14 +22,13 @@ import { getSession } from "@/lib/auth"
22
22
  export async function requirePageAccess(
23
23
  lang: string,
24
24
  resource: string,
25
- action = "view",
25
+ action = "view"
26
26
  ): Promise<Session> {
27
27
  const session = await getSession()
28
28
  const locale = lang || i18n.defaultLocale
29
29
 
30
30
  if (!session?.user) redirect(`/${locale}/sign-in`)
31
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
32
- if (!checkPermission(session as any, resource, action)) redirect(`/${locale}`)
31
+ if (!checkPermission(session, resource, action)) redirect(`/${locale}`)
33
32
 
34
33
  return session
35
34
  }
@@ -1,8 +1,8 @@
1
- import { PrismaPg } from "@prisma/adapter-pg"
2
- import { Prisma, PrismaClient } from "@prisma/client"
3
1
  import { createAuditExtension } from "@goerp/core/audit"
4
2
  import { createBranchGuardExtension } from "@goerp/core/branch-scope"
5
3
  import { configureSettingsService } from "@goerp/core/system/services/settings-service"
4
+ import { PrismaPg } from "@prisma/adapter-pg"
5
+ import { Prisma, PrismaClient } from "@prisma/client"
6
6
 
7
7
  /**
8
8
  * COMPOSITION ROOT của app — nơi duy nhất tạo Prisma client và cắm dây cho các
@@ -71,10 +71,18 @@ export const db = rawClient
71
71
  rawClient,
72
72
  // Bảng ghi rất nhiều mà không có giá trị điều tra thì loại ra, kẻo dấu vết
73
73
  // nghiệp vụ bị nhấn chìm (vinhhoa đo được 1 bảng chiếm 64% số dòng audit).
74
- skipModels: ["AuditLog", "ErrorLog", "Session", "Account", "Verification"],
75
- }),
74
+ skipModels: [
75
+ "AuditLog",
76
+ "ErrorLog",
77
+ "Session",
78
+ "Account",
79
+ "Verification",
80
+ ],
81
+ })
76
82
  )
77
- .$extends(createBranchGuardExtension({ models: GUARDED_MODELS })) as unknown as PrismaClient
83
+ .$extends(
84
+ createBranchGuardExtension({ models: GUARDED_MODELS })
85
+ ) as unknown as PrismaClient
78
86
 
79
87
  // Core đọc SystemConfig qua service này. Thiếu dòng dưới thì mọi getSettings()
80
88
  // lặng lẽ trả giá trị mặc định và saveSettings() ném lỗi.
@@ -1,9 +1,9 @@
1
1
  import "server-only"
2
2
 
3
- import { db } from "@/lib/prisma"
4
-
5
3
  import type { Permission } from "@/types/session"
6
4
 
5
+ import { db } from "@/lib/prisma"
6
+
7
7
  export interface UserAccess {
8
8
  roles: string[]
9
9
  permissions: Permission[]
@@ -1,4 +1,6 @@
1
- import { configureStorage, type StorageDb } from "@goerp/core/storage"
1
+ import { configureStorage } from "@goerp/core/storage"
2
+
3
+ import type { StorageDb } from "@goerp/core/storage"
2
4
 
3
5
  import { db } from "@/lib/prisma"
4
6
 
@@ -7,11 +7,13 @@ import { ErrorDialogProvider } from "@goerp/core/ui/feedback"
7
7
  import { SidebarProvider } from "@goerp/core/ui/primitives"
8
8
  import { Toaster } from "sonner"
9
9
 
10
+ import type { TenantConfig } from "@goerp/core/providers"
10
11
  import type { LocaleType } from "@goerp/core/types"
11
12
  import type { ReactNode } from "react"
12
13
 
13
- import { tenant } from "@/configs/tenant"
14
14
  import { navigations } from "@/data/navigations"
15
+
16
+ import { tenant as staticTenant } from "@/configs/tenant"
15
17
  import { authBridgeClient } from "@/lib/auth-client"
16
18
 
17
19
  import { ModeProvider } from "./mode-provider"
@@ -20,14 +22,23 @@ import { ThemeProvider } from "./theme-provider"
20
22
  export function Providers({
21
23
  children,
22
24
  locale,
25
+ tenant = staticTenant,
23
26
  }: {
24
27
  children: ReactNode
25
28
  locale: LocaleType
29
+ /**
30
+ * Bản tenant đã áp hồ sơ công ty từ DB — layout server dựng bằng
31
+ * `resolveBranding` + `tenantWithBranding` của `@goerp/core/branding`.
32
+ * Thứ tự thương hiệu: HỒ SƠ CÔNG TY (admin upload) → tenant config tĩnh →
33
+ * logo GoEat mặc định trong `public/`. Bỏ trống ⇒ dùng cấu hình tĩnh.
34
+ */
35
+ tenant?: TenantConfig
26
36
  }) {
27
37
  return (
28
38
  <SettingsProvider locale={locale}>
29
39
  {/* TenantProvider cấp branding (logo / tên / màu chủ đạo) cho Logo và cả
30
- vỏ app. Sửa src/configs/tenant.ts đổi da toàn bộ. */}
40
+ vỏ app. Logo/tên đến từ "Hồ công ty"; màu chủ đạo và tagline vẫn ở
41
+ src/configs/tenant.ts. */}
31
42
  <TenantProvider tenant={tenant}>
32
43
  <ModeProvider>
33
44
  <ThemeProvider>
@@ -2,13 +2,14 @@
2
2
 
3
3
  import { useEffect } from "react"
4
4
  import { useSettings } from "@goerp/core/hooks"
5
+
6
+ import type { ReactNode } from "react"
7
+
5
8
  import {
6
- applyBrandThemeTokens,
7
9
  THEME_PRESETS,
10
+ applyBrandThemeTokens,
8
11
  } from "@goerp/core/providers/brand-theme"
9
12
 
10
- import type { ReactNode } from "react"
11
-
12
13
  /**
13
14
  * Áp màu thương hiệu người dùng chọn trong Customizer vào CSS token
14
15
  * (--primary, --ring, --sidebar-background…). Bảng màu calibrate sẵn nằm ở
@@ -24,7 +24,9 @@ export const proxy = createAuthProxy({
24
24
  // và cả đường NGOÀI /api (máy móc không có cookie kiểu /pub, /iclock của
25
25
  // máy chấm công). Mỗi entry phải ghi rõ nó tự bảo vệ bằng gì; guardrail
26
26
  // `auth/route-outside-api-declared` sẽ bắt route ngoài /api chưa khai ở đây.
27
- publicPrefixes: ["/api/better-auth", "/api/public"],
27
+ // /api/branding: logo công ty phục vụ favicon + trang đăng nhập — hai bề mặt
28
+ // render TRƯỚC khi có phiên. Route chỉ lộ đúng cột logo, không đọc gì khác.
29
+ publicPrefixes: ["/api/better-auth", "/api/public", "/api/branding"],
28
30
  publicPages: [...i18n.locales.map((l) => `/${l}/sign-in`), "/sign-in"],
29
31
  signInPath: `/${i18n.defaultLocale}/sign-in`,
30
32
  homePath: `/${i18n.defaultLocale}`,
@@ -34,7 +34,7 @@ export interface CompanyProfileInput {
34
34
 
35
35
  export async function updateSystemCompany(
36
36
  data: CompanyProfileInput,
37
- updatedBy: string,
37
+ updatedBy: string
38
38
  ) {
39
39
  const existing = await db.company.findFirst({ orderBy: { createdAt: "asc" } })
40
40
  if (existing) {
@@ -43,5 +43,7 @@ export async function updateSystemCompany(
43
43
  data: { ...data, updatedBy },
44
44
  })
45
45
  }
46
- return db.company.create({ data: { ...data, createdBy: updatedBy, updatedBy } })
46
+ return db.company.create({
47
+ data: { ...data, createdBy: updatedBy, updatedBy },
48
+ })
47
49
  }
@@ -1,3 +1,9 @@
1
+ import { configureNotificationService } from "@goerp/core/notification"
2
+
3
+ import type { NotificationDb } from "@goerp/core/notification"
4
+
5
+ import { db } from "@/lib/prisma"
6
+
1
7
  /**
2
8
  * Thông báo trong ứng dụng (chuông ở thanh trên). Engine ở
3
9
  * @goerp/core/notification, bảng `notifications` + `push_subscriptions` ship
@@ -8,12 +14,6 @@
8
14
  * push vào bundle. Core đã fire-and-forget + catch nên push lỗi không làm hỏng
9
15
  * nghiệp vụ đang chạy.
10
16
  */
11
- import {
12
- configureNotificationService,
13
- type NotificationDb,
14
- } from "@goerp/core/notification"
15
-
16
- import { db } from "@/lib/prisma"
17
17
 
18
18
  configureNotificationService({
19
19
  db: db as unknown as NotificationDb,