@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
@@ -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"
@@ -1,31 +1,26 @@
1
1
  "use client"
2
2
 
3
3
  import { useEffect } from "react"
4
+ import { useIsDarkMode } from "@goerp/core/hooks"
4
5
 
5
6
  import type { ReactNode } from "react"
6
7
 
8
+ const MODES = ["light", "dark"]
9
+
7
10
  /**
8
- * Sáng/tối theo tuỳ chọn hệ điều hành. Cố ý viết bằng matchMedia thay vì kéo
9
- * thêm thư viện: starter càng ít phụ thuộc càng dễ nâng cấp. Muốn người dùng
10
- * tự chọn chế độ thì thay bằng next-themes bỏ file này.
11
+ * Áp chế độ Sáng/Tối/Hệ thống người dùng chọn trong Customizer (bánh răng).
12
+ * `useIsDarkMode()` của core đã resolve settings.mode (lưu cookie) + theo dõi
13
+ * prefers-color-scheme khi chọn "Hệ thống" đây chỉ việc gắn class lên <html>.
11
14
  */
12
- const MODES = ["light", "dark"] as const
13
-
14
15
  export function ModeProvider({ children }: { children: ReactNode }) {
15
- useEffect(() => {
16
- const query = window.matchMedia("(prefers-color-scheme: dark)")
16
+ const isDarkMode = useIsDarkMode()
17
+ const mode = isDarkMode ? "dark" : "light"
17
18
 
18
- const apply = (isDark: boolean) => {
19
- const root = document.documentElement
20
- root.classList.remove(...MODES)
21
- root.classList.add(isDark ? "dark" : "light")
22
- }
23
-
24
- apply(query.matches)
25
- const onChange = (e: MediaQueryListEvent) => apply(e.matches)
26
- query.addEventListener("change", onChange)
27
- return () => query.removeEventListener("change", onChange)
28
- }, [])
19
+ useEffect(() => {
20
+ const root = document.documentElement
21
+ root.classList.remove(...MODES)
22
+ root.classList.add(mode)
23
+ }, [mode])
29
24
 
30
25
  return <>{children}</>
31
26
  }
@@ -1,21 +1,36 @@
1
- 'use client'
1
+ "use client"
2
2
 
3
- import { useEffect } from 'react'
3
+ import { useEffect } from "react"
4
+ import { useSettings } from "@goerp/core/hooks"
4
5
 
5
- import type { ReactNode } from 'react'
6
+ import type { ReactNode } from "react"
6
7
 
7
- const DEFAULT_THEME = 'blue'
8
- const DEFAULT_RADIUS = 0.5
8
+ import {
9
+ THEME_PRESETS,
10
+ applyBrandThemeTokens,
11
+ } from "@goerp/core/providers/brand-theme"
9
12
 
13
+ /**
14
+ * Áp màu thương hiệu người dùng chọn trong Customizer vào CSS token
15
+ * (--primary, --ring, --sidebar-background…). Bảng màu calibrate sẵn nằm ở
16
+ * core (`THEME_PRESETS`); app muốn nắn tay từng hue thì thay bằng bảng riêng
17
+ * (xem vinhhoa src/providers/theme-provider.tsx).
18
+ *
19
+ * Radius ("Bo góc") + density ("Mật độ") do SettingsProvider của core tự áp.
20
+ */
10
21
  export function ThemeProvider({ children }: { children: ReactNode }) {
22
+ const { settings } = useSettings()
23
+
11
24
  useEffect(() => {
12
- const bodyElement = document.body
13
- Array.from(bodyElement.classList)
14
- .filter((c) => c.startsWith('theme-') || c.startsWith('radius-'))
15
- .forEach((c) => bodyElement.classList.remove(c))
16
- bodyElement.classList.add(`theme-${DEFAULT_THEME}`)
17
- bodyElement.classList.add(`radius-${DEFAULT_RADIUS}`)
18
- }, [])
25
+ const theme = THEME_PRESETS[settings.theme] ?? THEME_PRESETS.blue
26
+ const isDark =
27
+ settings.mode === "dark" ||
28
+ (settings.mode === "system" &&
29
+ typeof window !== "undefined" &&
30
+ !!window.matchMedia?.("(prefers-color-scheme: dark)").matches)
31
+
32
+ applyBrandThemeTokens(document.documentElement, theme, isDark)
33
+ }, [settings.theme, settings.mode])
19
34
 
20
35
  return <>{children}</>
21
36
  }
@@ -2,6 +2,7 @@ import { createAuthProxy } from "@goerp/core/auth/proxy-gate"
2
2
  import { getSessionCookie } from "better-auth/cookies"
3
3
 
4
4
  import { i18n } from "@/configs/i18n"
5
+ import { tenant } from "@/configs/tenant"
5
6
 
6
7
  /**
7
8
  * Cổng xác thực DEFAULT-DENY, dùng thẳng proxy-gate của core.
@@ -19,14 +20,18 @@ const BYPASS =
19
20
  (process.env.BYPASS_AUTH === "true" || process.env.BYPASS_AUTH === "1")
20
21
 
21
22
  export const proxy = createAuthProxy({
22
- // Thêm prefix vào đây khi có endpoint tự xác thực (webhook, token khách…).
23
- publicApiPrefixes: ["/api/better-auth", "/api/public"],
23
+ // Thêm prefix vào đây khi có endpoint tự xác thực webhook, token khách,
24
+ // và cả đường NGOÀI /api (máy móc không có cookie kiểu /pub, /iclock của
25
+ // máy chấm công). Mỗi entry phải ghi rõ nó tự bảo vệ bằng gì; guardrail
26
+ // `auth/route-outside-api-declared` sẽ bắt route ngoài /api chưa khai ở đây.
27
+ publicPrefixes: ["/api/better-auth", "/api/public"],
24
28
  publicPages: [...i18n.locales.map((l) => `/${l}/sign-in`), "/sign-in"],
25
29
  signInPath: `/${i18n.defaultLocale}/sign-in`,
26
30
  homePath: `/${i18n.defaultLocale}`,
31
+ // cookiePrefix PHẢI khớp advanced.cookiePrefix trong src/lib/better-auth.ts.
27
32
  getToken: BYPASS
28
33
  ? async () => ({ dev: true })
29
- : async (req) => getSessionCookie(req),
34
+ : async (req) => getSessionCookie(req, { cookiePrefix: tenant.id }),
30
35
  })
31
36
 
32
37
  export default proxy
@@ -0,0 +1,49 @@
1
+ import { cache } from "react"
2
+
3
+ import { tenant } from "@/configs/tenant"
4
+ import { db } from "@/lib/prisma"
5
+
6
+ /**
7
+ * Công ty "hệ thống" = dòng ĐẦU TIÊN của bảng companies. Chưa có dòng nào thì
8
+ * trả mặc định từ tenant config để biểu mẫu không trống — lưu lần đầu sẽ tạo.
9
+ */
10
+ export const getSystemCompany = cache(async function getSystemCompany() {
11
+ const company = await db.company.findFirst({ orderBy: { createdAt: "asc" } })
12
+ if (company) return company
13
+ return {
14
+ id: null as string | null,
15
+ name: tenant.branding?.companyName ?? tenant.name,
16
+ taxCode: "",
17
+ address: "",
18
+ phone: "",
19
+ email: "",
20
+ logoUrl: null as string | null,
21
+ systemName: tenant.name,
22
+ }
23
+ })
24
+
25
+ export interface CompanyProfileInput {
26
+ name: string
27
+ taxCode: string
28
+ address: string
29
+ phone: string
30
+ email: string
31
+ logoUrl?: string | null
32
+ systemName?: string | null
33
+ }
34
+
35
+ export async function updateSystemCompany(
36
+ data: CompanyProfileInput,
37
+ updatedBy: string
38
+ ) {
39
+ const existing = await db.company.findFirst({ orderBy: { createdAt: "asc" } })
40
+ if (existing) {
41
+ return db.company.update({
42
+ where: { id: existing.id },
43
+ data: { ...data, updatedBy },
44
+ })
45
+ }
46
+ return db.company.create({
47
+ data: { ...data, createdBy: updatedBy, updatedBy },
48
+ })
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,
@@ -18,7 +18,7 @@ const MASK = "••••••••"
18
18
  export class ConfigError extends Error {
19
19
  constructor(
20
20
  message: string,
21
- readonly status: number,
21
+ readonly status: number
22
22
  ) {
23
23
  super(message)
24
24
  }
@@ -29,14 +29,13 @@ export class ConfigError extends Error {
29
29
  * (khi đó route phải để `serverError` ghi lại stack thật).
30
30
  */
31
31
  export function configErrorInfo(
32
- error: unknown,
32
+ error: unknown
33
33
  ): { message: string; status: number } | null {
34
34
  return error instanceof ConfigError
35
35
  ? { message: error.message, status: error.status }
36
36
  : null
37
37
  }
38
38
 
39
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
40
39
  const mask = (config: any) => ({
41
40
  ...config,
42
41
  value: config.isEncrypted ? MASK : config.value,
@@ -77,7 +76,7 @@ export async function createConfig(input: ConfigInput, userId: string) {
77
76
  if (!/^[a-zA-Z0-9._-]+$/.test(key)) {
78
77
  throw new ConfigError(
79
78
  "Key chỉ được chứa chữ, số, dấu chấm, gạch dưới và gạch ngang",
80
- 400,
79
+ 400
81
80
  )
82
81
  }
83
82
  if (await db.systemConfig.findUnique({ where: { key } })) {
@@ -103,8 +102,13 @@ export async function createConfig(input: ConfigInput, userId: string) {
103
102
  }
104
103
 
105
104
  /** Đổi MỖI giá trị — đường dùng nhiều nhất trên UI. */
106
- export async function updateConfigValue(key: string, value: unknown, userId: string) {
107
- if (!key || value === undefined) throw new ConfigError("Thiếu key hoặc value", 400)
105
+ export async function updateConfigValue(
106
+ key: string,
107
+ value: unknown,
108
+ userId: string
109
+ ) {
110
+ if (!key || value === undefined)
111
+ throw new ConfigError("Thiếu key hoặc value", 400)
108
112
  const existing = await requireConfig(key)
109
113
  if (existing.isReadOnly) throw new ConfigError("Cấu hình này chỉ đọc", 403)
110
114
 
@@ -128,7 +132,10 @@ export async function updateConfigFull(input: ConfigInput, userId: string) {
128
132
  value: input.value ?? existing.value,
129
133
  type: input.type ?? existing.type,
130
134
  category: input.category ?? existing.category,
131
- description: input.description !== undefined ? input.description : existing.description,
135
+ description:
136
+ input.description !== undefined
137
+ ? input.description
138
+ : existing.description,
132
139
  isEncrypted: input.isEncrypted ?? existing.isEncrypted,
133
140
  isReadOnly: input.isReadOnly ?? existing.isReadOnly,
134
141
  updatedBy: userId,
@@ -141,13 +148,18 @@ export async function updateConfigFull(input: ConfigInput, userId: string) {
141
148
  export async function deleteConfig(key: string) {
142
149
  if (!key) throw new ConfigError("Thiếu key", 400)
143
150
  const existing = await requireConfig(key)
144
- if (existing.isReadOnly) throw new ConfigError("Không thể xóa cấu hình được bảo vệ", 403)
151
+ if (existing.isReadOnly)
152
+ throw new ConfigError("Không thể xóa cấu hình được bảo vệ", 403)
145
153
 
146
154
  await db.systemConfig.delete({ where: { key } })
147
155
  invalidate()
148
156
  }
149
157
 
150
- export async function toggleConfigStatus(key: string, status: string, userId: string) {
158
+ export async function toggleConfigStatus(
159
+ key: string,
160
+ status: string,
161
+ userId: string
162
+ ) {
151
163
  if (!key) throw new ConfigError("Thiếu key", 400)
152
164
  if (!["active", "inactive"].includes(status)) {
153
165
  throw new ConfigError("status phải là 'active' hoặc 'inactive'", 400)
@@ -0,0 +1,104 @@
1
+ import type { Prisma } from "@prisma/client"
2
+
3
+ import { db } from "@/lib/prisma"
4
+
5
+ /**
6
+ * Danh sách người dùng cho trang /users và GET /api/users.
7
+ *
8
+ * CỐ Ý không dùng `getUsersData` của @goerp/core/user: bản đó select các cột
9
+ * vinhhoa-specific (`userType`, quan hệ `profiles`/`supplier`) không tồn tại
10
+ * trong schema template — Prisma sẽ ném PrismaClientValidationError ngay
11
+ * request đầu tiên. Query ở đây chỉ đụng User/UserRole/UserBranch của template
12
+ * và trả đúng shape mà UsersTable/UsersCardView/UnifiedProfileDialog của core
13
+ * đọc: `status` ("active"/"inactive"), `roleNames`/`roleCodes`,
14
+ * `branchIds`/`branchNames`, `defaultBranchId`.
15
+ * (`getUserStats`/`getActiveRoles` của core thì an toàn — chỗ đếm theo
16
+ * `userType` đã có `.catch(() => 0)`.)
17
+ */
18
+ export interface UsersListParams {
19
+ search?: string
20
+ /** Lọc theo mã vai trò (Role.code). */
21
+ roleCode?: string
22
+ /** "active" | "inactive" — map sang cột isActive. */
23
+ status?: string
24
+ }
25
+
26
+ type Where = Prisma.UserWhereInput
27
+
28
+ export async function getUsersList(params: UsersListParams = {}) {
29
+ const { search, roleCode, status } = params
30
+
31
+ const where: Where = {}
32
+ if (search) {
33
+ where.OR = [
34
+ { name: { contains: search, mode: "insensitive" } },
35
+ { email: { contains: search, mode: "insensitive" } },
36
+ ]
37
+ }
38
+ if (status === "active" || status === "inactive") {
39
+ where.isActive = status === "active"
40
+ }
41
+ if (roleCode) {
42
+ where.userRoles = { some: { roleCode } }
43
+ }
44
+
45
+ // Trả TOÀN BỘ danh sách đã lọc: UsersClientPage của core tự phân trang phía
46
+ // client theo ?page&pageSize (danh sách người dùng nội bộ đủ nhỏ).
47
+ const [users, total] = await Promise.all([
48
+ db.user.findMany({
49
+ where,
50
+ orderBy: { createdAt: "desc" },
51
+ select: {
52
+ id: true,
53
+ name: true,
54
+ email: true,
55
+ image: true,
56
+ isActive: true,
57
+ lastLoginAt: true,
58
+ createdAt: true,
59
+ updatedAt: true,
60
+ userRoles: {
61
+ select: { role: { select: { code: true, name: true } } },
62
+ },
63
+ userBranches: {
64
+ select: {
65
+ isDefault: true,
66
+ branch: { select: { id: true, name: true } },
67
+ },
68
+ orderBy: [{ isDefault: "desc" }, { createdAt: "asc" }],
69
+ },
70
+ },
71
+ }),
72
+ db.user.count({ where }),
73
+ ])
74
+
75
+ const data = users.map((user) => {
76
+ const branches = user.userBranches
77
+ .filter((ub) => ub.branch)
78
+ .map((ub) => ({
79
+ id: ub.branch.id,
80
+ name: ub.branch.name,
81
+ isDefault: ub.isDefault,
82
+ }))
83
+ return {
84
+ id: user.id,
85
+ name: user.name,
86
+ email: user.email,
87
+ image: user.image,
88
+ isActive: user.isActive,
89
+ status: user.isActive ? "active" : "inactive",
90
+ lastLoginAt: user.lastLoginAt,
91
+ createdAt: user.createdAt,
92
+ updatedAt: user.updatedAt,
93
+ roleNames: user.userRoles.map((ur) => ur.role.name),
94
+ roleCodes: user.userRoles.map((ur) => ur.role.code),
95
+ branches,
96
+ branchIds: branches.map((b) => b.id),
97
+ branchNames: branches.map((b) => b.name),
98
+ defaultBranchId:
99
+ branches.find((b) => b.isDefault)?.id ?? branches[0]?.id ?? null,
100
+ }
101
+ })
102
+
103
+ return { data, total }
104
+ }
@@ -7,11 +7,11 @@
7
7
  * không lộ đường dẫn thật.
8
8
  * 3. Ném lỗi thoải mái: engine bắt, ghi status=error kèm message và báo chuông.
9
9
  */
10
- import { registerTaskHandler, saveTaskFile } from "../task-runner"
10
+ import type { TaskContext, TaskFileResult } from "@goerp/core/tasks"
11
11
 
12
12
  import { db } from "@/lib/prisma"
13
13
 
14
- import type { TaskContext, TaskFileResult } from "@goerp/core/tasks"
14
+ import { registerTaskHandler, saveTaskFile } from "../task-runner"
15
15
 
16
16
  export const EXPORT_DEPARTMENTS_TASK = "export-departments"
17
17
 
@@ -28,7 +28,9 @@ registerTaskHandler(
28
28
  EXPORT_DEPARTMENTS_TASK,
29
29
  async ({ setProgress }: TaskContext): Promise<TaskFileResult> => {
30
30
  const total = await db.department.count()
31
- const rows: string[] = [["Mã", "Tên", "Mô tả", "Thứ tự", "Trạng thái"].join(",")]
31
+ const rows: string[] = [
32
+ ["Mã", "Tên", "Mô tả", "Thứ tự", "Trạng thái"].join(","),
33
+ ]
32
34
 
33
35
  for (let skip = 0; skip < total; skip += PAGE_SIZE) {
34
36
  const page = await db.department.findMany({
@@ -38,21 +40,25 @@ registerTaskHandler(
38
40
  })
39
41
  for (const d of page) {
40
42
  rows.push(
41
- [d.code, d.name, d.description, d.order, d.status].map(csvCell).join(","),
43
+ [d.code, d.name, d.description, d.order, d.status]
44
+ .map(csvCell)
45
+ .join(",")
42
46
  )
43
47
  }
44
- await setProgress(Math.round(((skip + page.length) / Math.max(total, 1)) * 100))
48
+ await setProgress(
49
+ Math.round(((skip + page.length) / Math.max(total, 1)) * 100)
50
+ )
45
51
  }
46
52
 
47
- // BOM  để Excel bản tiếng Việt không đọc UTF-8 thành ký tự lỗi.
48
- const buffer = Buffer.from(`${rows.join("\n")}`, "utf8")
53
+ // BOM để Excel bản tiếng Việt không đọc UTF-8 thành ký tự lỗi.
54
+ const buffer = Buffer.from(` ${rows.join("\n")}`, "utf8")
49
55
  const fileName = "phong-ban.csv"
50
56
  const fileKey = await saveTaskFile(
51
57
  buffer,
52
58
  `${EXPORT_DEPARTMENTS_TASK}/${fileName}`,
53
- "text/csv",
59
+ "text/csv"
54
60
  )
55
61
 
56
62
  return { fileKey, fileName, contentType: "text/csv", rowCount: total }
57
- },
63
+ }
58
64
  )
@@ -1,3 +1,9 @@
1
+ import { configureTaskRunner } from "@goerp/core/tasks"
2
+
3
+ import type { TaskDb, TaskNotifyInput } from "@goerp/core/tasks"
4
+
5
+ import { db } from "@/lib/prisma"
6
+
1
7
  /**
2
8
  * Tác vụ nền — engine ở @goerp/core/tasks (bảng `background_tasks` ship qua
3
9
  * `goerp-features sync`). Hàng đợi nằm trong DB, worker chạy NGAY trong tiến
@@ -7,13 +13,6 @@
7
13
  * Đừng import file này ở nơi khác — import `@/server/tasks` để các handler
8
14
  * được đăng ký trước khi có ai enqueue.
9
15
  */
10
- import {
11
- configureTaskRunner,
12
- type TaskDb,
13
- type TaskNotifyInput,
14
- } from "@goerp/core/tasks"
15
-
16
- import { db } from "@/lib/prisma"
17
16
 
18
17
  configureTaskRunner({
19
18
  db: db as unknown as TaskDb,
@@ -27,9 +27,19 @@
27
27
  "./src/*"
28
28
  ],
29
29
  "@goerp/core": [
30
+ "../../src",
30
31
  "./node_modules/@goerp/core/src"
31
32
  ],
33
+ "@goerp/core/user/client-page": [
34
+ "../../src/user/pages/users-client-page.tsx",
35
+ "./node_modules/@goerp/core/src/user/pages/users-client-page.tsx"
36
+ ],
37
+ "@goerp/core/tasks/ui": [
38
+ "../../src/tasks/ui/task-list-client.tsx",
39
+ "./node_modules/@goerp/core/src/tasks/ui/task-list-client.tsx"
40
+ ],
32
41
  "@goerp/core/*": [
42
+ "../../src/*",
33
43
  "./node_modules/@goerp/core/src/*"
34
44
  ]
35
45
  }
@@ -12,6 +12,22 @@ export default defineConfig({
12
12
  },
13
13
  resolve: {
14
14
  // Phải khai lại alias của tsconfig: vitest không đọc tsconfig paths.
15
- alias: { "@": path.resolve(__dirname, "./src") },
15
+ // @goerp/core trỏ THẲNG src của core local (../../src trong repo core)
16
+ // thiếu là guardrails không resolve được, hoặc rơi về bản core CŨ trong
17
+ // node_modules → mixed-type khi core local đổi type. Hai subpath
18
+ // user/client-page và tasks/ui exports-map sang file không trùng đường
19
+ // dẫn src nên phải khai riêng, y như tsconfig.
20
+ alias: {
21
+ "@": path.resolve(__dirname, "./src"),
22
+ "@goerp/core/user/client-page": path.resolve(
23
+ __dirname,
24
+ "../../src/user/pages/users-client-page.tsx",
25
+ ),
26
+ "@goerp/core/tasks/ui": path.resolve(
27
+ __dirname,
28
+ "../../src/tasks/ui/task-list-client.tsx",
29
+ ),
30
+ "@goerp/core": path.resolve(__dirname, "../../src"),
31
+ },
16
32
  },
17
33
  })