@goplusvn/core 0.1.71 → 0.1.73

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/package.json +2 -1
  2. package/src/auth/index.ts +5 -1
  3. package/src/branding/__tests__/identity.test.ts +101 -0
  4. package/src/branding/__tests__/logo-route.test.ts +143 -0
  5. package/src/branding/identity.ts +74 -0
  6. package/src/branding/index.ts +36 -0
  7. package/src/branding/logo-route.ts +249 -0
  8. package/src/branding/pwa.tsx +146 -0
  9. package/src/branding/types.ts +45 -0
  10. package/src/configs/entities/departments.config.ts +1 -0
  11. package/src/configs/entities/material-categories.config.ts +1 -0
  12. package/src/crud/components/crud-export-button.tsx +3 -2
  13. package/src/crud/components/crud-page.tsx +56 -25
  14. package/src/crud/components/crud-row-actions.tsx +16 -4
  15. package/src/crud/crud-route-handlers.test.ts +352 -0
  16. package/src/crud/crud-route-handlers.ts +260 -13
  17. package/src/crud/index.ts +3 -0
  18. package/src/crud/lib/coerce.test.ts +118 -0
  19. package/src/crud/lib/coerce.ts +95 -0
  20. package/src/crud/lib/crud-utils.ts +4 -0
  21. package/src/crud/lib/entity-endpoints.test.ts +41 -0
  22. package/src/crud/lib/entity-endpoints.ts +23 -0
  23. package/src/crud/lib/errors.ts +14 -0
  24. package/src/crud/lib/import-request.ts +99 -0
  25. package/src/crud/lib/mutation-builder.test.ts +114 -0
  26. package/src/crud/lib/mutation-builder.ts +27 -34
  27. package/src/crud/lib/permissions.test.ts +57 -0
  28. package/src/crud/lib/permissions.ts +25 -13
  29. package/src/crud/lib/query-builder.ts +13 -6
  30. package/src/crud/pages/entity-crud-page.tsx +11 -8
  31. package/src/crud/server-service.test.ts +112 -0
  32. package/src/crud/server-service.ts +52 -27
  33. package/src/crud/server.ts +11 -1
  34. package/src/guardrails/rules/design.ts +3 -2
  35. package/src/styles/base.css +16 -0
  36. package/src/types/index.ts +75 -10
  37. package/src/ui/data-display/__tests__/use-client-pagination.test.ts +59 -0
  38. package/src/ui/data-display/data-table/__tests__/data-table-row-memo.test.tsx +96 -0
  39. package/src/ui/data-display/data-table/data-table-context.tsx +5 -0
  40. package/src/ui/data-display/data-table/data-table.tsx +68 -23
  41. package/src/ui/data-display/data-table-pagination.tsx +25 -8
  42. package/src/ui/data-display/index.tsx +1 -0
  43. package/src/ui/data-display/use-client-pagination.ts +48 -0
  44. package/src/ui/errors/error-view.tsx +80 -87
  45. package/src/ui/errors/index.ts +6 -1
  46. package/src/ui/errors/not-found-view.tsx +89 -16
  47. package/src/ui/errors/status-page.tsx +132 -0
  48. package/src/ui/layout/logo.tsx +5 -0
  49. package/src/ui/pages/not-found.tsx +11 -27
  50. package/templates/starter-app/AGENTS.md +1 -1
  51. package/templates/starter-app/next.config.mjs +8 -0
  52. package/templates/starter-app/package.json +1 -0
  53. package/templates/starter-app/prisma/migrations/20260807115347_platform_pages/migration.sql +87 -0
  54. package/templates/starter-app/prisma/migrations/20260808003811_company_profile/migration.sql +25 -0
  55. package/templates/starter-app/public/images/illustrations/characters/character-01.svg +33 -0
  56. package/templates/starter-app/public/images/illustrations/characters/character-02.svg +30 -0
  57. package/templates/starter-app/public/images/illustrations/characters/character-03.svg +30 -0
  58. package/templates/starter-app/public/images/illustrations/characters/character-04.svg +27 -0
  59. package/templates/starter-app/public/images/logos/goeat_logo.png +0 -0
  60. package/templates/starter-app/scripts/rbac-sync.ts +49 -21
  61. package/templates/starter-app/src/__tests__/architecture.test.ts +3 -2
  62. package/templates/starter-app/src/app/[lang]/(main)/actions/page.tsx +4 -4
  63. package/templates/starter-app/src/app/[lang]/(main)/admin/system/cache/page.tsx +1 -2
  64. package/templates/starter-app/src/app/[lang]/(main)/admin/system/company/actions.ts +7 -5
  65. package/templates/starter-app/src/app/[lang]/(main)/admin/system/company/company-profile-form.tsx +38 -11
  66. package/templates/starter-app/src/app/[lang]/(main)/admin/system/company/page.tsx +2 -1
  67. package/templates/starter-app/src/app/[lang]/(main)/crud/[entity]/page.tsx +4 -2
  68. package/templates/starter-app/src/app/[lang]/(main)/layout.tsx +2 -1
  69. package/templates/starter-app/src/app/[lang]/(main)/page.tsx +4 -4
  70. package/templates/starter-app/src/app/[lang]/(main)/resources/page.tsx +5 -5
  71. package/templates/starter-app/src/app/[lang]/(main)/roles/[id]/edit/page.tsx +5 -2
  72. package/templates/starter-app/src/app/[lang]/(main)/roles/new/page.tsx +1 -0
  73. package/templates/starter-app/src/app/[lang]/(main)/roles/page.tsx +1 -0
  74. package/templates/starter-app/src/app/[lang]/(main)/user/profile/actions.ts +4 -2
  75. package/templates/starter-app/src/app/[lang]/(main)/user/profile/page.tsx +3 -1
  76. package/templates/starter-app/src/app/[lang]/(main)/user/profile/profile-client-page.tsx +13 -5
  77. package/templates/starter-app/src/app/[lang]/(main)/users/page.tsx +3 -2
  78. package/templates/starter-app/src/app/[lang]/layout.tsx +27 -2
  79. package/templates/starter-app/src/app/api/admin/system/audit/route.ts +4 -3
  80. package/templates/starter-app/src/app/api/admin/system/jobs/[name]/history/route.ts +1 -1
  81. package/templates/starter-app/src/app/api/admin/system/jobs/route.ts +31 -12
  82. package/templates/starter-app/src/app/api/admin/system/settings/all/route.ts +2 -2
  83. package/templates/starter-app/src/app/api/admin/system/settings/create/route.ts +10 -3
  84. package/templates/starter-app/src/app/api/admin/system/settings/delete/route.ts +10 -3
  85. package/templates/starter-app/src/app/api/admin/system/settings/route.ts +14 -5
  86. package/templates/starter-app/src/app/api/admin/system/settings/toggle-status/route.ts +16 -7
  87. package/templates/starter-app/src/app/api/admin/system/settings/update/route.ts +12 -6
  88. package/templates/starter-app/src/app/api/admin/system/settings/update-full/route.ts +12 -6
  89. package/templates/starter-app/src/app/api/better-auth/[...all]/route.ts +1 -1
  90. package/templates/starter-app/src/app/api/branding/logo/route.ts +25 -0
  91. package/templates/starter-app/src/app/api/crud/[entity]/[id]/route.ts +2 -2
  92. package/templates/starter-app/src/app/api/crud/[entity]/export/route.ts +16 -0
  93. package/templates/starter-app/src/app/api/crud/[entity]/import/route.ts +17 -0
  94. package/templates/starter-app/src/app/api/crud/[entity]/route.ts +2 -2
  95. package/templates/starter-app/src/app/api/error-logs/[id]/route.ts +4 -2
  96. package/templates/starter-app/src/app/api/error-logs/route.ts +26 -19
  97. package/templates/starter-app/src/app/api/files/[...key]/route.ts +1 -2
  98. package/templates/starter-app/src/app/api/job-titles/route.ts +1 -1
  99. package/templates/starter-app/src/app/api/notifications/read/route.ts +1 -1
  100. package/templates/starter-app/src/app/api/notifications/route.ts +1 -1
  101. package/templates/starter-app/src/app/api/notifications/unread-count/route.ts +1 -1
  102. package/templates/starter-app/src/app/api/rbac/permissions-version/route.ts +4 -1
  103. package/templates/starter-app/src/app/api/roles/[id]/route.ts +2 -1
  104. package/templates/starter-app/src/app/api/roles/route.ts +2 -1
  105. package/templates/starter-app/src/app/api/security/sessions/[id]/route.ts +5 -5
  106. package/templates/starter-app/src/app/api/security/sessions/revoke-user/route.ts +1 -1
  107. package/templates/starter-app/src/app/api/security/sessions/route.ts +2 -2
  108. package/templates/starter-app/src/app/api/system-categories/route.ts +39 -14
  109. package/templates/starter-app/src/app/api/system-category-groups/route.ts +32 -14
  110. package/templates/starter-app/src/app/api/tasks/[id]/download/route.ts +7 -3
  111. package/templates/starter-app/src/app/api/tasks/route.ts +5 -2
  112. package/templates/starter-app/src/app/api/upload/route.ts +1 -2
  113. package/templates/starter-app/src/app/api/users/[id]/route.ts +20 -12
  114. package/templates/starter-app/src/app/api/users/route.ts +11 -9
  115. package/templates/starter-app/src/app/globals.css +0 -1
  116. package/templates/starter-app/src/app/layout.tsx +42 -1
  117. package/templates/starter-app/src/app/manifest.ts +23 -19
  118. package/templates/starter-app/src/components/layout/main-layout-wrapper.tsx +5 -4
  119. package/templates/starter-app/src/configs/entities/department.config.ts +7 -4
  120. package/templates/starter-app/src/configs/entities/index.ts +1 -1
  121. package/templates/starter-app/src/configs/entities/job-titles.config.ts +2 -0
  122. package/templates/starter-app/src/configs/entities/system-alerts.config.ts +2 -0
  123. package/templates/starter-app/src/configs/entities/users.config.ts +15 -2
  124. package/templates/starter-app/src/configs/i18n.ts +6 -6
  125. package/templates/starter-app/src/configs/permissions/menu-tree.ts +1 -1
  126. package/templates/starter-app/src/configs/tenant.ts +6 -1
  127. package/templates/starter-app/src/data/dictionary.ts +2 -1
  128. package/templates/starter-app/src/data/navigations.ts +12 -2
  129. package/templates/starter-app/src/lib/action-guard.ts +3 -4
  130. package/templates/starter-app/src/lib/api-handler.ts +27 -19
  131. package/templates/starter-app/src/lib/auth.ts +3 -1
  132. package/templates/starter-app/src/lib/branch-scope.ts +1 -1
  133. package/templates/starter-app/src/lib/crud/index.ts +5 -12
  134. package/templates/starter-app/src/lib/errors/log-server-error.ts +5 -4
  135. package/templates/starter-app/src/lib/logger.ts +11 -4
  136. package/templates/starter-app/src/lib/page-guard.ts +2 -3
  137. package/templates/starter-app/src/lib/prisma.ts +13 -5
  138. package/templates/starter-app/src/lib/rbac/access.ts +2 -2
  139. package/templates/starter-app/src/lib/storage.ts +3 -1
  140. package/templates/starter-app/src/providers/index.tsx +13 -2
  141. package/templates/starter-app/src/providers/theme-provider.tsx +4 -3
  142. package/templates/starter-app/src/proxy.ts +3 -1
  143. package/templates/starter-app/src/server/services/company-service.ts +4 -2
  144. package/templates/starter-app/src/server/services/notification-service.ts +6 -6
  145. package/templates/starter-app/src/server/services/system-config-service.ts +21 -9
  146. package/templates/starter-app/src/server/services/user-service.ts +2 -2
  147. package/templates/starter-app/src/server/tasks/handlers/export-departments.ts +15 -9
  148. package/templates/starter-app/src/server/tasks/task-runner.ts +6 -7
  149. package/templates/starter-app/tsconfig.json +8 -0
  150. package/templates/starter-app/vitest.config.ts +17 -1
@@ -2,8 +2,6 @@
2
2
 
3
3
  import * as React from "react"
4
4
  import { useActionState } from "react"
5
- import { toast } from "sonner"
6
-
7
5
  import {
8
6
  Avatar,
9
7
  AvatarFallback,
@@ -17,8 +15,11 @@ import {
17
15
  import { Button } from "@goerp/core/ui/primitives/button"
18
16
  import { Input } from "@goerp/core/ui/primitives/input"
19
17
  import { Label } from "@goerp/core/ui/primitives/label"
18
+ import { toast } from "sonner"
20
19
 
21
- import { changePassword, type ChangePasswordState } from "./actions"
20
+ import type { ChangePasswordState } from "./actions"
21
+
22
+ import { changePassword } from "./actions"
22
23
 
23
24
  interface ProfileUser {
24
25
  id: string
@@ -58,7 +59,10 @@ export function ProfileClientPage({ user }: { user: ProfileUser }) {
58
59
  <CardContent className="flex flex-col gap-6">
59
60
  <div className="flex items-center gap-4">
60
61
  <Avatar className="size-14">
61
- <AvatarImage src={user.image ?? undefined} alt={user.name ?? ""} />
62
+ <AvatarImage
63
+ src={user.image ?? undefined}
64
+ alt={user.name ?? ""}
65
+ />
62
66
  <AvatarFallback>
63
67
  {(user.name ?? user.email).slice(0, 2).toUpperCase()}
64
68
  </AvatarFallback>
@@ -107,7 +111,11 @@ export function ProfileClientPage({ user }: { user: ProfileUser }) {
107
111
  <CardTitle>Đổi mật khẩu</CardTitle>
108
112
  </CardHeader>
109
113
  <CardContent>
110
- <form ref={formRef} action={formAction} className="grid gap-4 md:max-w-md">
114
+ <form
115
+ ref={formRef}
116
+ action={formAction}
117
+ className="grid gap-4 md:max-w-md"
118
+ >
111
119
  <div className="space-y-2">
112
120
  <Label htmlFor="currentPassword">Mật khẩu hiện tại</Label>
113
121
  <Input
@@ -1,16 +1,17 @@
1
+ import { getUsersList } from "@/server/services/user-service"
1
2
  import { getCrudPermissions } from "@goerp/core/crud/server"
2
3
  import { getActiveRoles, getUserStats } from "@goerp/core/user"
3
4
  import { UsersClientPage } from "@goerp/core/user/client-page"
4
5
 
5
6
  import type { Metadata } from "next"
6
7
 
8
+ import { dictionary } from "@/data/dictionary"
9
+
7
10
  import { usersConfig } from "@/configs/entities/users.config"
8
11
  import { getActionMetaMap } from "@/configs/permissions"
9
12
  import { MENU_TREE } from "@/configs/permissions/menu-tree"
10
- import { dictionary } from "@/data/dictionary"
11
13
  import { requirePageAccess } from "@/lib/page-guard"
12
14
  import { db } from "@/lib/prisma"
13
- import { getUsersList } from "@/server/services/user-service"
14
15
 
15
16
  export const metadata: Metadata = { title: "Người dùng" }
16
17
  export const dynamic = "force-dynamic"
@@ -1,12 +1,28 @@
1
+ import { resolveBranding, tenantWithBranding } from "@goerp/core/branding"
2
+
3
+ import { Providers } from "@/providers"
4
+ import { getSystemCompany } from "@/server/services/company-service"
5
+
1
6
  import type { LocaleType } from "@goerp/core/types"
2
7
  import type { ReactNode } from "react"
3
8
 
4
- import { Providers } from "@/providers"
9
+ import { tenant } from "@/configs/tenant"
5
10
 
6
11
  /**
7
12
  * Không truyền session xuống Providers: Better Auth lấy session ở CLIENT qua
8
13
  * authClient.useSession() (bridge). Server component nào cần session thì tự gọi
9
14
  * getSession() — nó đã được React cache theo request.
15
+ *
16
+ * CÓ đọc hồ sơ công ty: logo/tên admin lưu ở /admin/system/company phải đè lên
17
+ * cấu hình tĩnh trong src/configs/tenant.ts, và phải có mặt ở CẢ vỏ app lẫn
18
+ * trang đăng nhập (hai nhóm route (main)/(plain) đều nằm dưới layout này).
19
+ * Đây là MỘT findFirst đã bọc `cache()`; `<html>` do app/layout.tsx (sync) xả
20
+ * ra trước nên trình duyệt vẫn nhận CSS ngay — đừng thêm truy vấn DB nào nữa ở
21
+ * tầng này, chỗ cần dữ liệu nặng thì bọc Suspense trong trang.
22
+ *
23
+ * `resolveBranding` trả logo dưới dạng `/api/branding/logo?v=<updatedAt>` chứ
24
+ * KHÔNG phải `Company.logoUrl` thô: URL thô trỏ `/api/files/<key>` cần phiên
25
+ * đăng nhập, dùng thẳng thì logo trên trang đăng nhập 401 và mất ảnh.
10
26
  */
11
27
  export default async function LangLayout({
12
28
  children,
@@ -16,6 +32,15 @@ export default async function LangLayout({
16
32
  params: Promise<{ lang: string }>
17
33
  }) {
18
34
  const { lang } = await params
35
+ const company = await getSystemCompany()
36
+ const identity = resolveBranding({ company, tenant })
19
37
 
20
- return <Providers locale={lang as LocaleType}>{children}</Providers>
38
+ return (
39
+ <Providers
40
+ locale={lang as LocaleType}
41
+ tenant={tenantWithBranding(tenant, identity)}
42
+ >
43
+ {children}
44
+ </Providers>
45
+ )
21
46
  }
@@ -18,7 +18,6 @@ export const GET = apiHandler(
18
18
  const skip = parseInt(sp.get("skip") || "0", 10)
19
19
  const take = Math.min(parseInt(sp.get("take") || "50", 10), 100)
20
20
 
21
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
21
  const where: Record<string, any> = {}
23
22
  if (action && action !== "all") where.action = action
24
23
  if (resource && resource !== "all") where.resource = resource
@@ -53,8 +52,10 @@ export const GET = apiHandler(
53
52
  meta: { total, skip, take, hasMore: skip + take < total },
54
53
  })
55
54
  } catch (error) {
56
- return serverError(error, req, { message: "Không tải được nhật ký thao tác" })
55
+ return serverError(error, req, {
56
+ message: "Không tải được nhật ký thao tác",
57
+ })
57
58
  }
58
59
  },
59
- { resource: "audit-log", action: "view" },
60
+ { resource: "audit-log", action: "view" }
60
61
  )
@@ -35,5 +35,5 @@ export const GET = apiHandler(
35
35
  return serverError(error, req, { message: "Không tải được lịch sử job" })
36
36
  }
37
37
  },
38
- { resource: "system-job", action: "view" },
38
+ { resource: "system-job", action: "view" }
39
39
  )
@@ -17,13 +17,18 @@ import { db } from "@/lib/prisma"
17
17
  export const GET = apiHandler(
18
18
  async (req) => {
19
19
  try {
20
- const memoryJobs = new Map(cronManager.getJobsInfo().map((j) => [j.name, j]))
20
+ const memoryJobs = new Map(
21
+ cronManager.getJobsInfo().map((j) => [j.name, j])
22
+ )
21
23
  const dbJobs = new Map(
22
- (await db.systemJob.findMany({ orderBy: { name: "asc" } })).map((j) => [j.name, j]),
24
+ (await db.systemJob.findMany({ orderBy: { name: "asc" } })).map((j) => [
25
+ j.name,
26
+ j,
27
+ ])
23
28
  )
24
29
 
25
- const names = [...new Set([...dbJobs.keys(), ...memoryJobs.keys()])].sort((a, b) =>
26
- a.localeCompare(b),
30
+ const names = [...new Set([...dbJobs.keys(), ...memoryJobs.keys()])].sort(
31
+ (a, b) => a.localeCompare(b)
27
32
  )
28
33
 
29
34
  const jobs = names.map((name) => {
@@ -45,10 +50,12 @@ export const GET = apiHandler(
45
50
 
46
51
  return NextResponse.json({ data: jobs })
47
52
  } catch (error) {
48
- return serverError(error, req, { message: "Không tải được danh sách job" })
53
+ return serverError(error, req, {
54
+ message: "Không tải được danh sách job",
55
+ })
49
56
  }
50
57
  },
51
- { resource: "system-job", action: "view" },
58
+ { resource: "system-job", action: "view" }
52
59
  )
53
60
 
54
61
  /** POST { name, action: "toggle" | "run" | "remove" }. */
@@ -62,7 +69,10 @@ export const POST = apiHandler(
62
69
 
63
70
  if (action === "toggle") {
64
71
  if (!(await cronManager.toggleJob(name))) {
65
- return NextResponse.json({ error: `Không tìm thấy job "${name}"` }, { status: 404 })
72
+ return NextResponse.json(
73
+ { error: `Không tìm thấy job "${name}"` },
74
+ { status: 404 }
75
+ )
66
76
  }
67
77
  const info = cronManager.getJobsInfo().find((j) => j.name === name)
68
78
  return NextResponse.json({
@@ -74,23 +84,32 @@ export const POST = apiHandler(
74
84
 
75
85
  if (action === "run") {
76
86
  if (!(await cronManager.runJobNow(name))) {
77
- return NextResponse.json({ error: `Không tìm thấy job "${name}"` }, { status: 404 })
87
+ return NextResponse.json(
88
+ { error: `Không tìm thấy job "${name}"` },
89
+ { status: 404 }
90
+ )
78
91
  }
79
- return NextResponse.json({ success: true, message: `Đã chạy tay job "${name}"` })
92
+ return NextResponse.json({
93
+ success: true,
94
+ message: `Đã chạy tay job "${name}"`,
95
+ })
80
96
  }
81
97
 
82
98
  if (action === "remove") {
83
99
  cronManager.removeJob(name)
84
- return NextResponse.json({ success: true, message: `Đã gỡ job "${name}"` })
100
+ return NextResponse.json({
101
+ success: true,
102
+ message: `Đã gỡ job "${name}"`,
103
+ })
85
104
  }
86
105
 
87
106
  return NextResponse.json(
88
107
  { error: "action phải là 'toggle', 'run' hoặc 'remove'" },
89
- { status: 400 },
108
+ { status: 400 }
90
109
  )
91
110
  } catch (error) {
92
111
  return serverError(error, req, { message: "Không thao tác được với job" })
93
112
  }
94
113
  },
95
- { resource: "system-job", action: "update" },
114
+ { resource: "system-job", action: "update" }
96
115
  )
@@ -1,8 +1,8 @@
1
1
  import { NextResponse } from "next/server"
2
+ import { listConfigs } from "@/server/services/system-config-service"
2
3
 
3
4
  import { apiHandler } from "@/lib/api-handler"
4
5
  import { serverError } from "@/lib/errors/server-error"
5
- import { listConfigs } from "@/server/services/system-config-service"
6
6
 
7
7
  /** Toàn bộ cấu hình đang bật — bảng chính của trang cài đặt hệ thống. */
8
8
  export const GET = apiHandler(
@@ -13,5 +13,5 @@ export const GET = apiHandler(
13
13
  return serverError(error, req, { message: "Không tải được cấu hình" })
14
14
  }
15
15
  },
16
- { resource: "system-setting", action: "view" },
16
+ { resource: "system-setting", action: "view" }
17
17
  )
@@ -1,8 +1,11 @@
1
1
  import { NextResponse } from "next/server"
2
+ import {
3
+ configErrorInfo,
4
+ createConfig,
5
+ } from "@/server/services/system-config-service"
2
6
 
3
7
  import { apiHandler } from "@/lib/api-handler"
4
8
  import { serverError } from "@/lib/errors/server-error"
5
- import { configErrorInfo, createConfig } from "@/server/services/system-config-service"
6
9
 
7
10
  export const POST = apiHandler(
8
11
  async (req, { session }) => {
@@ -11,9 +14,13 @@ export const POST = apiHandler(
11
14
  return NextResponse.json({ success: true, data })
12
15
  } catch (error) {
13
16
  const known = configErrorInfo(error)
14
- if (known) return NextResponse.json({ error: known.message }, { status: known.status })
17
+ if (known)
18
+ return NextResponse.json(
19
+ { error: known.message },
20
+ { status: known.status }
21
+ )
15
22
  return serverError(error, req, { message: "Không tạo được cấu hình" })
16
23
  }
17
24
  },
18
- { resource: "system-setting", action: "create" },
25
+ { resource: "system-setting", action: "create" }
19
26
  )
@@ -1,8 +1,11 @@
1
1
  import { NextResponse } from "next/server"
2
+ import {
3
+ configErrorInfo,
4
+ deleteConfig,
5
+ } from "@/server/services/system-config-service"
2
6
 
3
7
  import { apiHandler } from "@/lib/api-handler"
4
8
  import { serverError } from "@/lib/errors/server-error"
5
- import { configErrorInfo, deleteConfig } from "@/server/services/system-config-service"
6
9
 
7
10
  export const POST = apiHandler(
8
11
  async (req) => {
@@ -12,9 +15,13 @@ export const POST = apiHandler(
12
15
  return NextResponse.json({ success: true, message: "Đã xóa cấu hình" })
13
16
  } catch (error) {
14
17
  const known = configErrorInfo(error)
15
- if (known) return NextResponse.json({ error: known.message }, { status: known.status })
18
+ if (known)
19
+ return NextResponse.json(
20
+ { error: known.message },
21
+ { status: known.status }
22
+ )
16
23
  return serverError(error, req, { message: "Không xóa được cấu hình" })
17
24
  }
18
25
  },
19
- { resource: "system-setting", action: "delete" },
26
+ { resource: "system-setting", action: "delete" }
20
27
  )
@@ -23,7 +23,7 @@ export const GET = apiHandler(
23
23
  return serverError(error, req, { message: "Không tải được cấu hình" })
24
24
  }
25
25
  },
26
- { resource: "system-setting", action: "view" },
26
+ { resource: "system-setting", action: "view" }
27
27
  )
28
28
 
29
29
  export const POST = apiHandler(
@@ -33,17 +33,26 @@ export const POST = apiHandler(
33
33
 
34
34
  if (type === "audit") {
35
35
  await SettingsService.saveAuditSettings(settings)
36
- return NextResponse.json({ success: true, message: "Đã lưu cấu hình nhật ký" })
36
+ return NextResponse.json({
37
+ success: true,
38
+ message: "Đã lưu cấu hình nhật ký",
39
+ })
37
40
  }
38
41
  if (type === "notification") {
39
42
  await SettingsService.saveNotificationSettings(settings)
40
- return NextResponse.json({ success: true, message: "Đã lưu cấu hình thông báo" })
43
+ return NextResponse.json({
44
+ success: true,
45
+ message: "Đã lưu cấu hình thông báo",
46
+ })
41
47
  }
42
48
 
43
- return NextResponse.json({ error: "Loại cấu hình không hợp lệ" }, { status: 400 })
49
+ return NextResponse.json(
50
+ { error: "Loại cấu hình không hợp lệ" },
51
+ { status: 400 }
52
+ )
44
53
  } catch (error) {
45
54
  return serverError(error, req, { message: "Không lưu được cấu hình" })
46
55
  }
47
56
  },
48
- { resource: "system-setting", action: "update" },
57
+ { resource: "system-setting", action: "update" }
49
58
  )
@@ -1,12 +1,12 @@
1
1
  import { NextResponse } from "next/server"
2
-
3
- import { apiHandler } from "@/lib/api-handler"
4
- import { serverError } from "@/lib/errors/server-error"
5
2
  import {
6
3
  configErrorInfo,
7
4
  toggleConfigStatus,
8
5
  } from "@/server/services/system-config-service"
9
6
 
7
+ import { apiHandler } from "@/lib/api-handler"
8
+ import { serverError } from "@/lib/errors/server-error"
9
+
10
10
  /** Bật/tạm ngưng một cấu hình mà không xóa nó. */
11
11
  export const POST = apiHandler(
12
12
  async (req, { session }) => {
@@ -16,13 +16,22 @@ export const POST = apiHandler(
16
16
  return NextResponse.json({
17
17
  success: true,
18
18
  data,
19
- message: status === "active" ? "Đã kích hoạt cấu hình" : "Đã tạm ngưng cấu hình",
19
+ message:
20
+ status === "active"
21
+ ? "Đã kích hoạt cấu hình"
22
+ : "Đã tạm ngưng cấu hình",
20
23
  })
21
24
  } catch (error) {
22
25
  const known = configErrorInfo(error)
23
- if (known) return NextResponse.json({ error: known.message }, { status: known.status })
24
- return serverError(error, req, { message: "Không cập nhật được trạng thái" })
26
+ if (known)
27
+ return NextResponse.json(
28
+ { error: known.message },
29
+ { status: known.status }
30
+ )
31
+ return serverError(error, req, {
32
+ message: "Không cập nhật được trạng thái",
33
+ })
25
34
  }
26
35
  },
27
- { resource: "system-setting", action: "update" },
36
+ { resource: "system-setting", action: "update" }
28
37
  )
@@ -1,12 +1,12 @@
1
1
  import { NextResponse } from "next/server"
2
-
3
- import { apiHandler } from "@/lib/api-handler"
4
- import { serverError } from "@/lib/errors/server-error"
5
2
  import {
6
3
  configErrorInfo,
7
4
  updateConfigValue,
8
5
  } from "@/server/services/system-config-service"
9
6
 
7
+ import { apiHandler } from "@/lib/api-handler"
8
+ import { serverError } from "@/lib/errors/server-error"
9
+
10
10
  /** Đổi mỗi giá trị của một cấu hình theo key. */
11
11
  export const POST = apiHandler(
12
12
  async (req, { session }) => {
@@ -16,9 +16,15 @@ export const POST = apiHandler(
16
16
  return NextResponse.json({ success: true, data })
17
17
  } catch (error) {
18
18
  const known = configErrorInfo(error)
19
- if (known) return NextResponse.json({ error: known.message }, { status: known.status })
20
- return serverError(error, req, { message: "Không cập nhật được cấu hình" })
19
+ if (known)
20
+ return NextResponse.json(
21
+ { error: known.message },
22
+ { status: known.status }
23
+ )
24
+ return serverError(error, req, {
25
+ message: "Không cập nhật được cấu hình",
26
+ })
21
27
  }
22
28
  },
23
- { resource: "system-setting", action: "update" },
29
+ { resource: "system-setting", action: "update" }
24
30
  )
@@ -1,12 +1,12 @@
1
1
  import { NextResponse } from "next/server"
2
-
3
- import { apiHandler } from "@/lib/api-handler"
4
- import { serverError } from "@/lib/errors/server-error"
5
2
  import {
6
3
  configErrorInfo,
7
4
  updateConfigFull,
8
5
  } from "@/server/services/system-config-service"
9
6
 
7
+ import { apiHandler } from "@/lib/api-handler"
8
+ import { serverError } from "@/lib/errors/server-error"
9
+
10
10
  /** Sửa toàn bộ thuộc tính của một cấu hình (dialog "Sửa"). */
11
11
  export const POST = apiHandler(
12
12
  async (req, { session }) => {
@@ -15,9 +15,15 @@ export const POST = apiHandler(
15
15
  return NextResponse.json({ success: true, data })
16
16
  } catch (error) {
17
17
  const known = configErrorInfo(error)
18
- if (known) return NextResponse.json({ error: known.message }, { status: known.status })
19
- return serverError(error, req, { message: "Không cập nhật được cấu hình" })
18
+ if (known)
19
+ return NextResponse.json(
20
+ { error: known.message },
21
+ { status: known.status }
22
+ )
23
+ return serverError(error, req, {
24
+ message: "Không cập nhật được cấu hình",
25
+ })
20
26
  }
21
27
  },
22
- { resource: "system-setting", action: "update" },
28
+ { resource: "system-setting", action: "update" }
23
29
  )
@@ -9,5 +9,5 @@ import { getAuth } from "@/lib/better-auth"
9
9
  * đường đăng nhập.
10
10
  */
11
11
  export const { GET, POST } = toNextJsHandler((req: Request) =>
12
- getAuth().handler(req),
12
+ getAuth().handler(req)
13
13
  )
@@ -0,0 +1,25 @@
1
+ import { createBrandingLogoHandler } from "@goerp/core/branding"
2
+
3
+ import { getSystemCompany } from "@/server/services/company-service"
4
+ import { getFile } from "@/lib/storage"
5
+
6
+ /**
7
+ * GET /api/branding/logo — logo cấu hình ở "Hồ sơ công ty".
8
+ *
9
+ * URL DUY NHẤT mà mọi bề mặt tham chiếu: sidebar, trang đăng nhập, đầu trang
10
+ * in, favicon, icon PWA, thẻ chia sẻ. Chưa upload gì ⇒ logo GoEat mặc định
11
+ * (`public/images/logos/goeat_logo.png`).
12
+ *
13
+ * Công khai — đã khai `/api/branding` trong `publicPrefixes` của `proxy.ts`:
14
+ * favicon và trang đăng nhập render trước khi có phiên.
15
+ */
16
+ export const dynamic = "force-dynamic"
17
+
18
+ export const GET = createBrandingLogoHandler({
19
+ getCompany: getSystemCompany,
20
+ getFile,
21
+ // `import` chữ literal phải nằm ở đây (mã app) thì Next mới trace `sharp`
22
+ // vào bản standalone. Core cố ý không tự import — xem chú thích ở
23
+ // `createBrandingLogoHandler`.
24
+ loadSharp: () => import("sharp"),
25
+ })
@@ -1,9 +1,9 @@
1
1
  import { createCrudItemHandlers } from "@goerp/core/crud/server"
2
2
 
3
- import { getSession } from "@/lib/auth"
4
3
  import { getEntityConfig } from "@/configs/entities"
5
- import { serverError } from "@/lib/errors/server-error"
4
+ import { getSession } from "@/lib/auth"
6
5
  import { crudService } from "@/lib/crud"
6
+ import { serverError } from "@/lib/errors/server-error"
7
7
 
8
8
  export const { GET, PUT, PATCH, DELETE } = createCrudItemHandlers({
9
9
  getSession,
@@ -0,0 +1,16 @@
1
+ import { createCrudExportHandler } from "@goerp/core/crud/server"
2
+
3
+ import { getEntityConfig } from "@/configs/entities"
4
+ import { getSession } from "@/lib/auth"
5
+ import { crudService } from "@/lib/crud"
6
+ import { serverError } from "@/lib/errors/server-error"
7
+
8
+ // Xuất xlsx/csv/json theo đúng search/sort/filter đang áp trên bảng. Nút
9
+ // Export của CrudPage mặc định trỏ về đây (`${apiEndpoint}/export`); gác
10
+ // quyền `<resource>:export` ở core.
11
+ export const { GET } = createCrudExportHandler({
12
+ getSession,
13
+ getEntityConfig,
14
+ service: crudService,
15
+ onError: serverError,
16
+ })
@@ -0,0 +1,17 @@
1
+ import { createCrudImportHandler } from "@goerp/core/crud/server"
2
+
3
+ import { getEntityConfig } from "@/configs/entities"
4
+ import { getSession } from "@/lib/auth"
5
+ import { crudService } from "@/lib/crud"
6
+ import { serverError } from "@/lib/errors/server-error"
7
+
8
+ // Nhập từ file (multipart của ImportDialog) hoặc JSON array. Mỗi dòng đi qua
9
+ // service.create như tạo tay — coercion/validate/audit của engine áp đủ; dòng
10
+ // lỗi gom vào ImportResult.errors, không chặn dòng sau. Gác quyền
11
+ // `<resource>:import` ở core.
12
+ export const { POST } = createCrudImportHandler({
13
+ getSession,
14
+ getEntityConfig,
15
+ service: crudService,
16
+ onError: serverError,
17
+ })
@@ -1,9 +1,9 @@
1
1
  import { createCrudCollectionHandlers } from "@goerp/core/crud/server"
2
2
 
3
- import { getSession } from "@/lib/auth"
4
3
  import { getEntityConfig } from "@/configs/entities"
5
- import { serverError } from "@/lib/errors/server-error"
4
+ import { getSession } from "@/lib/auth"
6
5
  import { crudService } from "@/lib/crud"
6
+ import { serverError } from "@/lib/errors/server-error"
7
7
 
8
8
  // GET danh sách + POST tạo. Gác quyền + query đều ở core.
9
9
  export const { GET, POST } = createCrudCollectionHandlers({
@@ -16,8 +16,10 @@ export const PATCH = apiHandler(
16
16
  })
17
17
  return NextResponse.json({ success: true, log })
18
18
  } catch (error) {
19
- return serverError(error, req, { message: "Không cập nhật được trạng thái lỗi" })
19
+ return serverError(error, req, {
20
+ message: "Không cập nhật được trạng thái lỗi",
21
+ })
20
22
  }
21
23
  },
22
- { resource: "error-log", action: "update" },
24
+ { resource: "error-log", action: "update" }
23
25
  )
@@ -12,7 +12,6 @@ import { db } from "@/lib/prisma"
12
12
  * và dọn.
13
13
  */
14
14
 
15
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
15
  type Where = Record<string, any>
17
16
 
18
17
  export const GET = apiHandler(
@@ -21,7 +20,7 @@ export const GET = apiHandler(
21
20
  const sp = new URL(req.url).searchParams
22
21
  const search = sp.get("search") || undefined
23
22
  const severity = sp.get("severity") || undefined
24
- const module = sp.get("module") || undefined
23
+ const moduleFilter = sp.get("module") || undefined
25
24
  const resolved = sp.get("resolved")
26
25
  const startDate = sp.get("startDate")
27
26
  const endDate = sp.get("endDate")
@@ -38,7 +37,7 @@ export const GET = apiHandler(
38
37
  ]
39
38
  }
40
39
  if (severity && severity !== "all") where.severity = severity
41
- if (module && module !== "all") where.module = module
40
+ if (moduleFilter && moduleFilter !== "all") where.module = moduleFilter
42
41
  if (resolved === "true") where.resolved = true
43
42
  else if (resolved === "false") where.resolved = false
44
43
  if (startDate || endDate) {
@@ -50,23 +49,31 @@ export const GET = apiHandler(
50
49
  const startOfToday = new Date()
51
50
  startOfToday.setHours(0, 0, 0, 0)
52
51
 
53
- const [logs, total, todayCount, unresolvedCount, moduleStats] = await Promise.all([
54
- db.errorLog.findMany({ where, orderBy: { lastSeenAt: "desc" }, skip, take }),
55
- db.errorLog.count({ where }),
56
- db.errorLog.count({ where: { createdAt: { gte: startOfToday } } }),
57
- db.errorLog.count({ where: { resolved: false } }),
58
- db.errorLog.groupBy({
59
- by: ["module"],
60
- _count: { module: true },
61
- where: { resolved: false },
62
- orderBy: { _count: { module: "desc" } },
63
- take: 8,
64
- }),
65
- ])
52
+ const [logs, total, todayCount, unresolvedCount, moduleStats] =
53
+ await Promise.all([
54
+ db.errorLog.findMany({
55
+ where,
56
+ orderBy: { lastSeenAt: "desc" },
57
+ skip,
58
+ take,
59
+ }),
60
+ db.errorLog.count({ where }),
61
+ db.errorLog.count({ where: { createdAt: { gte: startOfToday } } }),
62
+ db.errorLog.count({ where: { resolved: false } }),
63
+ db.errorLog.groupBy({
64
+ by: ["module"],
65
+ _count: { module: true },
66
+ where: { resolved: false },
67
+ orderBy: { _count: { module: "desc" } },
68
+ take: 8,
69
+ }),
70
+ ])
66
71
 
67
72
  // ErrorLog không có quan hệ tới User (lỗi có thể xảy ra khi chưa đăng
68
73
  // nhập) — tra tên riêng thay vì include.
69
- const userIds = [...new Set(logs.map((l) => l.userId).filter(Boolean))] as string[]
74
+ const userIds = [
75
+ ...new Set(logs.map((l) => l.userId).filter(Boolean)),
76
+ ] as string[]
70
77
  const nameById = new Map<string, string>()
71
78
  if (userIds.length) {
72
79
  const users = await db.user.findMany({
@@ -98,7 +105,7 @@ export const GET = apiHandler(
98
105
  return serverError(error, req, { message: "Không tải được nhật ký lỗi" })
99
106
  }
100
107
  },
101
- { resource: "error-log", action: "view" },
108
+ { resource: "error-log", action: "view" }
102
109
  )
103
110
 
104
111
  /** Dọn log: ?mode=resolved (đã xử lý) hoặc ?mode=older_than&days=30. */
@@ -120,5 +127,5 @@ export const DELETE = apiHandler(
120
127
  return serverError(error, req, { message: "Không xóa được nhật ký lỗi" })
121
128
  }
122
129
  },
123
- { resource: "error-log", action: "delete" },
130
+ { resource: "error-log", action: "delete" }
124
131
  )