@goplusvn/core 0.1.70 → 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 (175) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/bin/goerp-init.mjs +15 -0
  3. package/package.json +2 -1
  4. package/src/auth/index.ts +5 -1
  5. package/src/auth/proxy-gate.ts +21 -2
  6. package/src/configs/entities/departments.config.ts +1 -0
  7. package/src/configs/entities/material-categories.config.ts +1 -0
  8. package/src/cron/__tests__/cron-schedule.test.ts +76 -0
  9. package/src/cron/cron-schedule.ts +128 -0
  10. package/src/cron/simple-cron-job.ts +83 -69
  11. package/src/crud/components/crud-export-button.tsx +3 -2
  12. package/src/crud/components/crud-page.tsx +56 -25
  13. package/src/crud/components/crud-row-actions.tsx +16 -4
  14. package/src/crud/components/crud-table.tsx +13 -1
  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/lib/translate-config.ts +16 -2
  31. package/src/crud/pages/entity-crud-page.tsx +11 -8
  32. package/src/crud/server-service.test.ts +112 -0
  33. package/src/crud/server-service.ts +52 -27
  34. package/src/crud/server.ts +11 -1
  35. package/src/guardrails/index.ts +1 -0
  36. package/src/guardrails/rules/auth.ts +54 -1
  37. package/src/guardrails/rules/design.ts +3 -2
  38. package/src/guardrails/types.ts +17 -8
  39. package/src/providers/brand-theme.ts +20 -0
  40. package/src/rbac/pages/permission-catalog-pages.tsx +3 -1
  41. package/src/security/index.ts +2 -0
  42. package/src/security/pages/sessions-page.tsx +516 -0
  43. package/src/types/index.ts +75 -10
  44. package/src/ui/auth/sign-in-form.tsx +64 -9
  45. package/src/ui/data-display/__tests__/use-client-pagination.test.ts +59 -0
  46. package/src/ui/data-display/data-table/__tests__/data-table-row-memo.test.tsx +96 -0
  47. package/src/ui/data-display/data-table/data-table-context.tsx +5 -0
  48. package/src/ui/data-display/data-table/data-table.tsx +68 -23
  49. package/src/ui/data-display/data-table-pagination.tsx +25 -8
  50. package/src/ui/data-display/index.tsx +1 -0
  51. package/src/ui/data-display/use-client-pagination.ts +48 -0
  52. package/src/ui/errors/error-view.tsx +164 -0
  53. package/src/ui/errors/index.ts +2 -0
  54. package/src/ui/errors/not-found-view.tsx +44 -0
  55. package/src/ui/index.tsx +1 -0
  56. package/src/ui/layout/logo.tsx +54 -19
  57. package/src/user/pages/users-client-page.tsx +12 -7
  58. package/templates/starter-app/.env.example +4 -0
  59. package/templates/starter-app/AGENTS.md +1 -1
  60. package/templates/starter-app/README.md +6 -2
  61. package/templates/starter-app/husky/pre-commit +10 -0
  62. package/templates/starter-app/package.json +27 -2
  63. package/templates/starter-app/prisma/migrations/20260807115347_platform_pages/migration.sql +87 -0
  64. package/templates/starter-app/prisma/migrations/20260808003811_company_profile/migration.sql +25 -0
  65. package/templates/starter-app/prisma/schema/organization.prisma +20 -0
  66. package/templates/starter-app/prisma/schema/system.prisma +74 -0
  67. package/templates/starter-app/prisma/seed.ts +7 -5
  68. package/templates/starter-app/scripts/migration-new.mjs +89 -0
  69. package/templates/starter-app/scripts/rbac-sync.ts +49 -21
  70. package/templates/starter-app/src/__tests__/architecture.test.ts +11 -2
  71. package/templates/starter-app/src/app/[lang]/(main)/actions/page.tsx +37 -0
  72. package/templates/starter-app/src/app/[lang]/(main)/admin/system/cache/page.tsx +68 -0
  73. package/templates/starter-app/src/app/[lang]/(main)/admin/system/company/actions.ts +33 -0
  74. package/templates/starter-app/src/app/[lang]/(main)/admin/system/company/company-profile-form.tsx +195 -0
  75. package/templates/starter-app/src/app/[lang]/(main)/admin/system/company/page.tsx +29 -0
  76. package/templates/starter-app/src/app/[lang]/(main)/crud/[entity]/page.tsx +4 -2
  77. package/templates/starter-app/src/app/[lang]/(main)/error.tsx +24 -0
  78. package/templates/starter-app/src/app/[lang]/(main)/layout.tsx +2 -1
  79. package/templates/starter-app/src/app/[lang]/(main)/not-found.tsx +6 -0
  80. package/templates/starter-app/src/app/[lang]/(main)/page.tsx +13 -3
  81. package/templates/starter-app/src/app/[lang]/(main)/resources/page.tsx +39 -0
  82. package/templates/starter-app/src/app/[lang]/(main)/roles/[id]/edit/page.tsx +5 -2
  83. package/templates/starter-app/src/app/[lang]/(main)/roles/new/page.tsx +1 -0
  84. package/templates/starter-app/src/app/[lang]/(main)/roles/page.tsx +1 -0
  85. package/templates/starter-app/src/app/[lang]/(main)/security/sessions/page.tsx +24 -0
  86. package/templates/starter-app/src/app/[lang]/(main)/system-categories/page.tsx +30 -0
  87. package/templates/starter-app/src/app/[lang]/(main)/user/profile/actions.ts +62 -0
  88. package/templates/starter-app/src/app/[lang]/(main)/user/profile/page.tsx +41 -0
  89. package/templates/starter-app/src/app/[lang]/(main)/user/profile/profile-client-page.tsx +157 -0
  90. package/templates/starter-app/src/app/[lang]/(main)/users/page.tsx +69 -0
  91. package/templates/starter-app/src/app/[lang]/[...not-found]/page.tsx +9 -0
  92. package/templates/starter-app/src/app/[lang]/error.tsx +26 -0
  93. package/templates/starter-app/src/app/[lang]/layout.tsx +2 -2
  94. package/templates/starter-app/src/app/[lang]/not-found.tsx +9 -0
  95. package/templates/starter-app/src/app/api/admin/system/audit/route.ts +4 -3
  96. package/templates/starter-app/src/app/api/admin/system/jobs/[name]/history/route.ts +1 -1
  97. package/templates/starter-app/src/app/api/admin/system/jobs/route.ts +31 -12
  98. package/templates/starter-app/src/app/api/admin/system/settings/all/route.ts +2 -2
  99. package/templates/starter-app/src/app/api/admin/system/settings/create/route.ts +10 -3
  100. package/templates/starter-app/src/app/api/admin/system/settings/delete/route.ts +10 -3
  101. package/templates/starter-app/src/app/api/admin/system/settings/route.ts +14 -5
  102. package/templates/starter-app/src/app/api/admin/system/settings/toggle-status/route.ts +16 -7
  103. package/templates/starter-app/src/app/api/admin/system/settings/update/route.ts +12 -6
  104. package/templates/starter-app/src/app/api/admin/system/settings/update-full/route.ts +12 -6
  105. package/templates/starter-app/src/app/api/better-auth/[...all]/route.ts +1 -1
  106. package/templates/starter-app/src/app/api/branches/route.ts +26 -0
  107. package/templates/starter-app/src/app/api/crud/[entity]/[id]/route.ts +2 -2
  108. package/templates/starter-app/src/app/api/crud/[entity]/export/route.ts +16 -0
  109. package/templates/starter-app/src/app/api/crud/[entity]/import/route.ts +17 -0
  110. package/templates/starter-app/src/app/api/crud/[entity]/route.ts +2 -2
  111. package/templates/starter-app/src/app/api/departments/route.ts +26 -0
  112. package/templates/starter-app/src/app/api/error-logs/[id]/route.ts +4 -2
  113. package/templates/starter-app/src/app/api/error-logs/route.ts +26 -19
  114. package/templates/starter-app/src/app/api/files/[...key]/route.ts +1 -2
  115. package/templates/starter-app/src/app/api/job-titles/route.ts +46 -0
  116. package/templates/starter-app/src/app/api/notifications/read/route.ts +1 -1
  117. package/templates/starter-app/src/app/api/notifications/route.ts +1 -1
  118. package/templates/starter-app/src/app/api/notifications/unread-count/route.ts +1 -1
  119. package/templates/starter-app/src/app/api/rbac/permissions-version/route.ts +4 -1
  120. package/templates/starter-app/src/app/api/roles/[id]/route.ts +2 -1
  121. package/templates/starter-app/src/app/api/roles/route.ts +2 -1
  122. package/templates/starter-app/src/app/api/security/sessions/[id]/route.ts +52 -0
  123. package/templates/starter-app/src/app/api/security/sessions/revoke-user/route.ts +44 -0
  124. package/templates/starter-app/src/app/api/security/sessions/route.ts +101 -0
  125. package/templates/starter-app/src/app/api/suppliers/route.ts +13 -0
  126. package/templates/starter-app/src/app/api/system-categories/route.ts +179 -0
  127. package/templates/starter-app/src/app/api/system-category-groups/route.ts +149 -0
  128. package/templates/starter-app/src/app/api/tasks/[id]/download/route.ts +7 -3
  129. package/templates/starter-app/src/app/api/tasks/route.ts +5 -2
  130. package/templates/starter-app/src/app/api/upload/route.ts +1 -2
  131. package/templates/starter-app/src/app/api/users/[id]/route.ts +230 -0
  132. package/templates/starter-app/src/app/api/users/route.ts +141 -0
  133. package/templates/starter-app/src/app/global-error.tsx +135 -0
  134. package/templates/starter-app/src/app/globals.css +0 -1
  135. package/templates/starter-app/src/app/icon.svg +6 -0
  136. package/templates/starter-app/src/app/manifest.ts +26 -0
  137. package/templates/starter-app/src/components/layout/main-layout-wrapper.tsx +5 -4
  138. package/templates/starter-app/src/configs/entities/department.config.ts +7 -4
  139. package/templates/starter-app/src/configs/entities/index.ts +5 -1
  140. package/templates/starter-app/src/configs/entities/job-titles.config.ts +121 -0
  141. package/templates/starter-app/src/configs/entities/system-alerts.config.ts +93 -0
  142. package/templates/starter-app/src/configs/entities/users.config.ts +80 -0
  143. package/templates/starter-app/src/configs/i18n.ts +6 -6
  144. package/templates/starter-app/src/configs/permissions/index.ts +37 -0
  145. package/templates/starter-app/src/configs/permissions/master-data.permissions.ts +11 -0
  146. package/templates/starter-app/src/configs/permissions/menu-tree.ts +37 -0
  147. package/templates/starter-app/src/configs/permissions/system.permissions.ts +29 -0
  148. package/templates/starter-app/src/configs/tenant.ts +2 -1
  149. package/templates/starter-app/src/data/dictionary.ts +77 -4
  150. package/templates/starter-app/src/data/navigations.ts +60 -1
  151. package/templates/starter-app/src/instrumentation.ts +5 -16
  152. package/templates/starter-app/src/lib/action-guard.ts +25 -0
  153. package/templates/starter-app/src/lib/api-handler.ts +27 -19
  154. package/templates/starter-app/src/lib/auth.ts +3 -1
  155. package/templates/starter-app/src/lib/better-auth.ts +8 -0
  156. package/templates/starter-app/src/lib/branch-scope.ts +1 -1
  157. package/templates/starter-app/src/lib/crud/index.ts +5 -10
  158. package/templates/starter-app/src/lib/errors/log-server-error.ts +5 -4
  159. package/templates/starter-app/src/lib/logger.ts +11 -4
  160. package/templates/starter-app/src/lib/page-guard.ts +2 -3
  161. package/templates/starter-app/src/lib/prisma.ts +16 -7
  162. package/templates/starter-app/src/lib/rbac/access.ts +2 -2
  163. package/templates/starter-app/src/lib/storage.ts +3 -1
  164. package/templates/starter-app/src/providers/index.tsx +2 -1
  165. package/templates/starter-app/src/providers/mode-provider.tsx +13 -18
  166. package/templates/starter-app/src/providers/theme-provider.tsx +27 -12
  167. package/templates/starter-app/src/proxy.ts +8 -3
  168. package/templates/starter-app/src/server/services/company-service.ts +49 -0
  169. package/templates/starter-app/src/server/services/notification-service.ts +6 -6
  170. package/templates/starter-app/src/server/services/system-config-service.ts +21 -9
  171. package/templates/starter-app/src/server/services/user-service.ts +104 -0
  172. package/templates/starter-app/src/server/tasks/handlers/export-departments.ts +15 -9
  173. package/templates/starter-app/src/server/tasks/task-runner.ts +6 -7
  174. package/templates/starter-app/tsconfig.json +10 -0
  175. package/templates/starter-app/vitest.config.ts +17 -1
@@ -0,0 +1,141 @@
1
+ import { NextResponse } from "next/server"
2
+ import { getUsersList } from "@/server/services/user-service"
3
+ import bcrypt from "bcryptjs"
4
+
5
+ import { apiHandler } from "@/lib/api-handler"
6
+ import { serverError } from "@/lib/errors/server-error"
7
+ import { db } from "@/lib/prisma"
8
+
9
+ /**
10
+ * /api/users — danh sách + tạo người dùng.
11
+ *
12
+ * Đơn giản hóa so với vinhhoa: không có profile (phòng ban/chức danh/SĐT),
13
+ * không sync better-auth riêng — mật khẩu nằm ở Account providerId="credential"
14
+ * (nơi DUY NHẤT Better Auth đọc, xem prisma/schema/auth.prisma). Các field
15
+ * vinhhoa-specific mà UnifiedProfileDialog của core vẫn gửi lên (phone,
16
+ * address, departmentId, jobTitleId, supplierId, userType…) được BỎ QUA.
17
+ */
18
+
19
+ export const GET = apiHandler(
20
+ async (req) => {
21
+ try {
22
+ const sp = new URL(req.url).searchParams
23
+ const result = await getUsersList({
24
+ search: sp.get("search")?.trim() || sp.get("q")?.trim() || undefined,
25
+ roleCode: sp.get("role")?.trim() || undefined,
26
+ status: sp.get("status")?.trim() || undefined,
27
+ })
28
+ return NextResponse.json(result)
29
+ } catch (error) {
30
+ return serverError(error, req, {
31
+ message: "Lỗi tải danh sách người dùng",
32
+ })
33
+ }
34
+ },
35
+ { resource: "user", action: "view" }
36
+ )
37
+
38
+ export const POST = apiHandler(
39
+ async (req) => {
40
+ try {
41
+ const body = await req.json()
42
+ const name = typeof body.name === "string" ? body.name.trim() : ""
43
+ const email =
44
+ typeof body.email === "string" ? body.email.toLowerCase().trim() : ""
45
+ const password = typeof body.password === "string" ? body.password : ""
46
+ const isActive = body.status ? body.status === "active" : true
47
+ const roleCodes: string[] = Array.isArray(body.roleCodes)
48
+ ? body.roleCodes
49
+ : []
50
+ const branchIds: string[] = Array.isArray(body.branchIds)
51
+ ? body.branchIds
52
+ : []
53
+
54
+ if (!name) {
55
+ return NextResponse.json(
56
+ { error: "Tên là bắt buộc", field: "name" },
57
+ { status: 400 }
58
+ )
59
+ }
60
+ // Template dùng email làm định danh đăng nhập (User.email là unique).
61
+ if (!email) {
62
+ return NextResponse.json(
63
+ { error: "Email là bắt buộc", field: "email" },
64
+ { status: 400 }
65
+ )
66
+ }
67
+ if (password && password.length < 6) {
68
+ return NextResponse.json(
69
+ { error: "Mật khẩu phải có ít nhất 6 ký tự", field: "password" },
70
+ { status: 400 }
71
+ )
72
+ }
73
+
74
+ const existing = await db.user.findUnique({ where: { email } })
75
+ if (existing) {
76
+ return NextResponse.json(
77
+ { error: "Email đã tồn tại", field: "email" },
78
+ { status: 400 }
79
+ )
80
+ }
81
+
82
+ const hashedPassword = password ? await bcrypt.hash(password, 10) : null
83
+
84
+ const created = await db.$transaction(async (tx) => {
85
+ const user = await tx.user.create({
86
+ data: { name, email, isActive, emailVerified: true },
87
+ })
88
+
89
+ // Không có mật khẩu = không có Account credential = không đăng nhập
90
+ // được (vẫn tạo được để gán vai trò trước).
91
+ if (hashedPassword) {
92
+ await tx.account.create({
93
+ data: {
94
+ userId: user.id,
95
+ providerId: "credential",
96
+ accountId: user.id,
97
+ password: hashedPassword,
98
+ },
99
+ })
100
+ }
101
+
102
+ if (roleCodes.length > 0) {
103
+ await tx.userRole.createMany({
104
+ data: roleCodes.map((roleCode, index) => ({
105
+ userId: user.id,
106
+ roleCode,
107
+ isPrimary: index === 0,
108
+ })),
109
+ skipDuplicates: true,
110
+ })
111
+ }
112
+
113
+ if (branchIds.length > 0) {
114
+ const defaultBranchId =
115
+ typeof body.defaultBranchId === "string" &&
116
+ branchIds.includes(body.defaultBranchId)
117
+ ? body.defaultBranchId
118
+ : branchIds[0]
119
+ await tx.userBranch.createMany({
120
+ data: branchIds.map((branchId) => ({
121
+ userId: user.id,
122
+ branchId,
123
+ isDefault: branchId === defaultBranchId,
124
+ })),
125
+ skipDuplicates: true,
126
+ })
127
+ }
128
+
129
+ return user
130
+ })
131
+
132
+ return NextResponse.json(
133
+ { id: created.id, name: created.name, email: created.email },
134
+ { status: 201 }
135
+ )
136
+ } catch (error) {
137
+ return serverError(error, req, { message: "Lỗi tạo người dùng" })
138
+ }
139
+ },
140
+ { resource: "user", action: "create" }
141
+ )
@@ -0,0 +1,135 @@
1
+ "use client"
2
+
3
+ /**
4
+ * Lưới an toàn CUỐI CÙNG — bắt lỗi ném từ chính root layout
5
+ * (`app/[lang]/layout.tsx`, nơi gọi `getSession` + `getSystemCompany` nên một
6
+ * cú nghẽn DB là đủ để cả app sập).
7
+ *
8
+ * PHẢI nằm ở `app/global-error.tsx`. Next chỉ đọc convention này ở GỐC cây
9
+ * route (`create-component-tree.tsx`: `isRoot ? getConventionPathByType(…,
10
+ * 'global-error') : undefined`) — bản cũ đặt ở `app/[lang]/global-error.tsx`
11
+ * chưa từng được nạp, nên mọi lần app sập người dùng chỉ thấy trang trắng với
12
+ * dòng "Application error: a server-side exception has occurred".
13
+ *
14
+ * Cố ý KHÔNG import `globals.css`, `@goerp/core/ui` hay bất cứ thứ gì khác:
15
+ * global-error thay luôn root layout, và nó thường chạy đúng lúc app đang hỏng
16
+ * (chunk lỗi, CSS chưa tải, provider chết). Style nội tuyến + thẻ HTML thuần là
17
+ * thứ duy nhất chắc chắn hiển thị được trong hoàn cảnh đó. Đừng "dọn cho đẹp"
18
+ * bằng cách kéo design system vào đây.
19
+ */
20
+ export default function GlobalError({
21
+ error,
22
+ reset,
23
+ }: {
24
+ error: Error & { digest?: string }
25
+ reset: () => void
26
+ }) {
27
+ return (
28
+ <html lang="vi">
29
+ <body
30
+ style={{
31
+ margin: 0,
32
+ minHeight: "100vh",
33
+ display: "flex",
34
+ alignItems: "center",
35
+ justifyContent: "center",
36
+ padding: "16px",
37
+ background: "#f8fafc",
38
+ color: "#0f172a",
39
+ fontFamily:
40
+ 'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif',
41
+ }}
42
+ >
43
+ <div
44
+ style={{
45
+ width: "100%",
46
+ maxWidth: "480px",
47
+ background: "#ffffff",
48
+ border: "1px solid #e2e8f0",
49
+ borderRadius: "8px",
50
+ padding: "24px",
51
+ boxShadow: "0 1px 3px rgba(15, 23, 42, 0.08)",
52
+ }}
53
+ >
54
+ <h1
55
+ style={{
56
+ margin: "0 0 12px",
57
+ fontSize: "18px",
58
+ fontWeight: 700,
59
+ color: "#b91c1c",
60
+ }}
61
+ >
62
+ Hệ thống đang gặp sự cố
63
+ </h1>
64
+ <p
65
+ style={{
66
+ margin: "0 0 16px",
67
+ fontSize: "14px",
68
+ lineHeight: 1.6,
69
+ color: "#475569",
70
+ }}
71
+ >
72
+ Không dựng được giao diện. Dữ liệu của bạn vẫn an toàn — đây là lỗi
73
+ kỹ thuật tạm thời. Vui lòng thử lại; nếu vẫn lỗi, gửi mã bên dưới
74
+ cho quản trị viên.
75
+ </p>
76
+
77
+ {error.digest ? (
78
+ <p
79
+ style={{
80
+ margin: "0 0 16px",
81
+ padding: "8px 12px",
82
+ borderRadius: "6px",
83
+ background: "#f1f5f9",
84
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
85
+ fontSize: "12px",
86
+ color: "#334155",
87
+ wordBreak: "break-all",
88
+ }}
89
+ >
90
+ Mã lỗi: {error.digest}
91
+ </p>
92
+ ) : null}
93
+
94
+ <div style={{ display: "flex", flexWrap: "wrap", gap: "8px" }}>
95
+ <button
96
+ type="button"
97
+ onClick={() => reset()}
98
+ style={{
99
+ padding: "8px 16px",
100
+ borderRadius: "6px",
101
+ border: "none",
102
+ background: "#4f46e5",
103
+ color: "#ffffff",
104
+ fontSize: "14px",
105
+ fontWeight: 500,
106
+ cursor: "pointer",
107
+ }}
108
+ >
109
+ Thử lại
110
+ </button>
111
+ {/* CỐ Ý dùng <a> chứ không phải next/link: ở đây app đang hỏng, và
112
+ điều hướng client-side sẽ giữ nguyên cái cây React đã chết. Tải
113
+ lại cả tài liệu mới thật sự thoát được. */}
114
+ {/* eslint-disable-next-line @next/next/no-html-link-for-pages */}
115
+ <a
116
+ href="/"
117
+ style={{
118
+ padding: "8px 16px",
119
+ borderRadius: "6px",
120
+ border: "1px solid #cbd5e1",
121
+ background: "#ffffff",
122
+ color: "#334155",
123
+ fontSize: "14px",
124
+ fontWeight: 500,
125
+ textDecoration: "none",
126
+ }}
127
+ >
128
+ Về trang chủ
129
+ </a>
130
+ </div>
131
+ </div>
132
+ </body>
133
+ </html>
134
+ )
135
+ }
@@ -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>. */
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
2
+ <!-- Favicon mặc định của starter-app — thay bằng icon thương hiệu của bạn.
3
+ Đặt ở src/app/icon.svg là Next tự phục vụ /icon (App Router metadata). -->
4
+ <rect width="64" height="64" rx="14" fill="#4f46e5"/>
5
+ <text x="32" y="43" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, sans-serif" font-size="34" font-weight="700" fill="#ffffff">G</text>
6
+ </svg>
@@ -0,0 +1,26 @@
1
+ import type { MetadataRoute } from "next"
2
+
3
+ import { tenant } from "@/configs/tenant"
4
+
5
+ /**
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
+ */
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
+ }
26
+ }
@@ -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
  }
@@ -1,11 +1,15 @@
1
1
  import type { EntityConfig } from "@goerp/core/types"
2
2
 
3
3
  import { departmentConfig } from "./department.config"
4
+ import { jobTitlesConfig } from "./job-titles.config"
5
+ import { systemAlertsConfig } from "./system-alerts.config"
4
6
 
5
7
  // Đăng ký entity → cấu hình. Khóa = số nhiều, khớp URL /crud/<key> + apiEndpoint.
6
- // Thêm entity mới: import config rồi thêm 1 dòng ở đây.
8
+ // Thêm entity mới: import config rồi thêm 1 dòng ở đây (khai `modelName` trong config).
7
9
  export const entityConfigs: Record<string, EntityConfig> = {
8
10
  departments: departmentConfig,
11
+ "job-titles": jobTitlesConfig,
12
+ "system-alerts": systemAlertsConfig,
9
13
  }
10
14
 
11
15
  export function getEntityConfig(entity: string): EntityConfig | undefined {
@@ -0,0 +1,121 @@
1
+ import { Briefcase } from "lucide-react"
2
+
3
+ import type { EntityConfig } from "@goerp/core/types"
4
+
5
+ /** Chức danh trong tổ chức — entity CRUD generic. URL: /crud/job-titles. */
6
+ export const jobTitlesConfig: EntityConfig = {
7
+ name: "jobTitle",
8
+ permissionResource: "job-title",
9
+ label: "Chức danh",
10
+ pluralLabel: "Chức danh",
11
+ icon: Briefcase,
12
+ description: "Quản lý danh sách chức danh trong tổ chức",
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",
16
+ idField: "id",
17
+ displayField: "name",
18
+
19
+ fields: [
20
+ {
21
+ name: "id",
22
+ label: "ID",
23
+ type: "text",
24
+ hideInForm: true,
25
+ hideInTable: true,
26
+ },
27
+ {
28
+ name: "code",
29
+ label: "Mã chức danh",
30
+ type: "text",
31
+ required: true,
32
+ placeholder: "VD: DIRECTOR, MANAGER, STAFF",
33
+ filterable: true,
34
+ width: 140,
35
+ },
36
+ {
37
+ name: "name",
38
+ label: "Tên chức danh",
39
+ type: "text",
40
+ required: true,
41
+ placeholder: "Nhập tên chức danh",
42
+ filterable: true,
43
+ },
44
+ {
45
+ name: "description",
46
+ label: "Mô tả",
47
+ type: "textarea",
48
+ placeholder: "Nhập mô tả chức danh",
49
+ hideInTable: true,
50
+ },
51
+ {
52
+ name: "level",
53
+ label: "Cấp bậc",
54
+ type: "number",
55
+ required: true,
56
+ defaultValue: 0,
57
+ placeholder: "Cấp bậc (số lớn = cao hơn)",
58
+ width: 100,
59
+ },
60
+ {
61
+ name: "status",
62
+ label: "Trạng thái",
63
+ type: "switch",
64
+ required: true,
65
+ defaultValue: "active",
66
+ filterable: true,
67
+ options: [
68
+ { label: "Hoạt động", value: "active" },
69
+ { label: "Vô hiệu hóa", value: "inactive" },
70
+ ],
71
+ },
72
+ {
73
+ name: "createdAt",
74
+ label: "Ngày tạo",
75
+ type: "datetime",
76
+ hideInForm: true,
77
+ width: 120,
78
+ renderCell: (value) =>
79
+ new Date(value as string).toLocaleDateString("vi-VN", {
80
+ timeZone: "Asia/Ho_Chi_Minh",
81
+ }),
82
+ },
83
+ ],
84
+
85
+ filters: [
86
+ {
87
+ name: "search",
88
+ label: "Tìm kiếm",
89
+ type: "text",
90
+ field: "name",
91
+ operator: "contains",
92
+ },
93
+ {
94
+ name: "status",
95
+ label: "Trạng thái",
96
+ type: "select",
97
+ field: "status",
98
+ operator: "eq",
99
+ options: [
100
+ { label: "Hoạt động", value: "active" },
101
+ { label: "Vô hiệu hóa", value: "inactive" },
102
+ ],
103
+ },
104
+ ],
105
+
106
+ permissions: {
107
+ create: true,
108
+ read: true,
109
+ update: true,
110
+ delete: true,
111
+ export: true,
112
+ import: true,
113
+ },
114
+
115
+ features: {
116
+ search: true,
117
+ bulkActions: true,
118
+ export: true,
119
+ import: true,
120
+ },
121
+ }
@@ -0,0 +1,93 @@
1
+ import { Bell } from "lucide-react"
2
+
3
+ import type { EntityConfig } from "@goerp/core/types"
4
+
5
+ /**
6
+ * Cảnh báo hệ thống — ví dụ entity CRUD generic viết config TỪ ĐẦU (department
7
+ * là ví dụ kế thừa config core). URL: /crud/system-alerts.
8
+ */
9
+ export const systemAlertsConfig: EntityConfig = {
10
+ name: "system-alert",
11
+ permissionResource: "system-alert",
12
+ label: "Cảnh báo hệ thống",
13
+ pluralLabel: "Cảnh báo hệ thống",
14
+ icon: Bell,
15
+ description: "Quản lý các thông báo và cảnh báo hệ thống",
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",
19
+ idField: "id",
20
+ displayField: "title",
21
+
22
+ fields: [
23
+ {
24
+ name: "id",
25
+ label: "ID",
26
+ type: "text",
27
+ hideInForm: true,
28
+ hideInTable: true,
29
+ },
30
+ {
31
+ name: "title",
32
+ label: "Tiêu đề",
33
+ type: "text",
34
+ required: true,
35
+ filterable: true,
36
+ },
37
+ {
38
+ name: "message",
39
+ label: "Nội dung",
40
+ type: "textarea",
41
+ required: true,
42
+ },
43
+ {
44
+ name: "type",
45
+ label: "Loại",
46
+ type: "select",
47
+ filterable: true,
48
+ defaultValue: "info",
49
+ options: [
50
+ { label: "Thông tin", value: "info" },
51
+ { label: "Cảnh báo", value: "warning" },
52
+ { label: "Lỗi", value: "error" },
53
+ { label: "Thành công", value: "success" },
54
+ ],
55
+ },
56
+ {
57
+ name: "status",
58
+ label: "Trạng thái",
59
+ type: "select",
60
+ filterable: true,
61
+ defaultValue: "pending",
62
+ options: [
63
+ { label: "Chờ gửi", value: "pending" },
64
+ { label: "Đã gửi", value: "sent" },
65
+ { label: "Đã đọc", value: "read" },
66
+ ],
67
+ },
68
+ {
69
+ name: "createdAt",
70
+ label: "Ngày tạo",
71
+ type: "datetime",
72
+ hideInForm: true,
73
+ renderCell: (value) =>
74
+ new Date(value as string).toLocaleString("vi-VN", {
75
+ timeZone: "Asia/Ho_Chi_Minh",
76
+ }),
77
+ },
78
+ ],
79
+
80
+ permissions: {
81
+ create: true,
82
+ read: true,
83
+ update: true,
84
+ delete: true,
85
+ export: true,
86
+ },
87
+
88
+ features: {
89
+ search: true,
90
+ bulkActions: true,
91
+ export: true,
92
+ },
93
+ }
@@ -0,0 +1,80 @@
1
+ import type { EntityConfig } from "@goerp/core/types"
2
+
3
+ /**
4
+ * Cấu hình entity cho trang /users (UsersClientPage của core).
5
+ *
6
+ * KHÔNG đăng ký vào `entityConfigs` (src/configs/entities/index.ts): người
7
+ * dùng không đi qua CRUD engine generic (/crud/users) — tạo/sửa phải qua
8
+ * /api/users để còn hash mật khẩu vào Account credential + gán vai trò/chi
9
+ * nhánh trong transaction.
10
+ *
11
+ * Toàn bộ giá trị ở đây là PURE DATA (không icon component, không hàm) nên
12
+ * truyền thẳng từ server page xuống client không cần bước serialize như
13
+ * vinhhoa.
14
+ */
15
+ export const usersConfig: EntityConfig = {
16
+ name: "user",
17
+ permissionResource: "user",
18
+ label: "Người dùng",
19
+ pluralLabel: "Người dùng",
20
+ iconName: "Users",
21
+ description: "Quản lý tài khoản đăng nhập, vai trò và chi nhánh",
22
+ apiEndpoint: "/api/users",
23
+ modelName: "user",
24
+ idField: "id",
25
+ displayField: "name",
26
+
27
+ fields: [
28
+ {
29
+ name: "id",
30
+ label: "ID",
31
+ type: "text",
32
+ hideInForm: true,
33
+ hideInTable: true,
34
+ },
35
+ { name: "name", label: "Họ tên", type: "text", required: true },
36
+ {
37
+ name: "email",
38
+ label: "Email",
39
+ type: "email",
40
+ required: true,
41
+ width: 200,
42
+ },
43
+ {
44
+ name: "roleNames",
45
+ label: "Vai trò",
46
+ type: "text",
47
+ hideInForm: true,
48
+ width: 150,
49
+ },
50
+ {
51
+ name: "branchNames",
52
+ label: "Chi nhánh",
53
+ type: "text",
54
+ hideInForm: true,
55
+ width: 150,
56
+ },
57
+ {
58
+ name: "status",
59
+ label: "Trạng thái",
60
+ type: "select",
61
+ hideInForm: true,
62
+ options: [
63
+ { label: "Hoạt động", value: "active" },
64
+ { label: "Đã khóa", value: "inactive" },
65
+ ],
66
+ width: 120,
67
+ },
68
+ ],
69
+
70
+ permissions: {
71
+ create: true,
72
+ read: true,
73
+ update: true,
74
+ delete: true,
75
+ // Toolbar user của core export qua /api/crud/users/export — endpoint đó
76
+ // không tồn tại trong template (users không nằm trong CRUD engine).
77
+ export: false,
78
+ import: false,
79
+ },
80
+ }
@@ -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