@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
@@ -1,10 +1,10 @@
1
1
  import { NextResponse } from "next/server"
2
+ import { getUsersList } from "@/server/services/user-service"
2
3
  import bcrypt from "bcryptjs"
3
4
 
4
5
  import { apiHandler } from "@/lib/api-handler"
5
6
  import { serverError } from "@/lib/errors/server-error"
6
7
  import { db } from "@/lib/prisma"
7
- import { getUsersList } from "@/server/services/user-service"
8
8
 
9
9
  /**
10
10
  * /api/users — danh sách + tạo người dùng.
@@ -27,10 +27,12 @@ export const GET = apiHandler(
27
27
  })
28
28
  return NextResponse.json(result)
29
29
  } catch (error) {
30
- return serverError(error, req, { message: "Lỗi tải danh sách người dùng" })
30
+ return serverError(error, req, {
31
+ message: "Lỗi tải danh sách người dùng",
32
+ })
31
33
  }
32
34
  },
33
- { resource: "user", action: "view" },
35
+ { resource: "user", action: "view" }
34
36
  )
35
37
 
36
38
  export const POST = apiHandler(
@@ -52,20 +54,20 @@ export const POST = apiHandler(
52
54
  if (!name) {
53
55
  return NextResponse.json(
54
56
  { error: "Tên là bắt buộc", field: "name" },
55
- { status: 400 },
57
+ { status: 400 }
56
58
  )
57
59
  }
58
60
  // Template dùng email làm định danh đăng nhập (User.email là unique).
59
61
  if (!email) {
60
62
  return NextResponse.json(
61
63
  { error: "Email là bắt buộc", field: "email" },
62
- { status: 400 },
64
+ { status: 400 }
63
65
  )
64
66
  }
65
67
  if (password && password.length < 6) {
66
68
  return NextResponse.json(
67
69
  { error: "Mật khẩu phải có ít nhất 6 ký tự", field: "password" },
68
- { status: 400 },
70
+ { status: 400 }
69
71
  )
70
72
  }
71
73
 
@@ -73,7 +75,7 @@ export const POST = apiHandler(
73
75
  if (existing) {
74
76
  return NextResponse.json(
75
77
  { error: "Email đã tồn tại", field: "email" },
76
- { status: 400 },
78
+ { status: 400 }
77
79
  )
78
80
  }
79
81
 
@@ -129,11 +131,11 @@ export const POST = apiHandler(
129
131
 
130
132
  return NextResponse.json(
131
133
  { id: created.id, name: created.name, email: created.email },
132
- { status: 201 },
134
+ { status: 201 }
133
135
  )
134
136
  } catch (error) {
135
137
  return serverError(error, req, { message: "Lỗi tạo người dùng" })
136
138
  }
137
139
  },
138
- { resource: "user", action: "create" },
140
+ { resource: "user", action: "create" }
139
141
  )
@@ -8,7 +8,6 @@
8
8
  /* Tailwind v4 must scan core's component classes (this app's own src is
9
9
  auto-scanned). */
10
10
  @source "../../node_modules/@goerp/core/src/**/*.tsx";
11
- @source "../../node_modules/@goplusvn/core/src/**/*.tsx";
12
11
 
13
12
  /* App brand overrides. Core's default brand is indigo. Re-skin either here
14
13
  statically, or at runtime via `branding.primaryColor` on <TenantProvider>. */
@@ -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
  )
@@ -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
 
@@ -10,8 +10,9 @@ import { Toaster } from "sonner"
10
10
  import type { LocaleType } from "@goerp/core/types"
11
11
  import type { ReactNode } from "react"
12
12
 
13
- import { tenant } from "@/configs/tenant"
14
13
  import { navigations } from "@/data/navigations"
14
+
15
+ import { tenant } from "@/configs/tenant"
15
16
  import { authBridgeClient } from "@/lib/auth-client"
16
17
 
17
18
  import { ModeProvider } from "./mode-provider"
@@ -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 ở
@@ -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,