@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,157 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { useActionState } from "react"
5
+ import {
6
+ Avatar,
7
+ AvatarFallback,
8
+ AvatarImage,
9
+ Badge,
10
+ Card,
11
+ CardContent,
12
+ CardHeader,
13
+ CardTitle,
14
+ } from "@goerp/core/ui"
15
+ import { Button } from "@goerp/core/ui/primitives/button"
16
+ import { Input } from "@goerp/core/ui/primitives/input"
17
+ import { Label } from "@goerp/core/ui/primitives/label"
18
+ import { toast } from "sonner"
19
+
20
+ import type { ChangePasswordState } from "./actions"
21
+
22
+ import { changePassword } from "./actions"
23
+
24
+ interface ProfileUser {
25
+ id: string
26
+ name: string | null
27
+ email: string
28
+ image: string | null
29
+ createdAt: Date
30
+ userRoles: { roleCode: string; role: { name: string } }[]
31
+ userBranches: { isDefault: boolean; branch: { name: string } }[]
32
+ }
33
+
34
+ export function ProfileClientPage({ user }: { user: ProfileUser }) {
35
+ const [state, formAction, pending] = useActionState<
36
+ ChangePasswordState,
37
+ FormData
38
+ >(changePassword, {})
39
+ const formRef = React.useRef<HTMLFormElement>(null)
40
+
41
+ React.useEffect(() => {
42
+ if (!state.message) return
43
+ if (state.success) {
44
+ toast.success(state.message)
45
+ formRef.current?.reset()
46
+ } else {
47
+ toast.error(state.message)
48
+ }
49
+ }, [state])
50
+
51
+ return (
52
+ <div className="mx-auto flex w-full max-w-3xl flex-col gap-4 p-4 md:p-6">
53
+ <h1 className="text-2xl font-bold tracking-tight">Hồ sơ cá nhân</h1>
54
+
55
+ <Card>
56
+ <CardHeader>
57
+ <CardTitle>Thông tin chung</CardTitle>
58
+ </CardHeader>
59
+ <CardContent className="flex flex-col gap-6">
60
+ <div className="flex items-center gap-4">
61
+ <Avatar className="size-14">
62
+ <AvatarImage
63
+ src={user.image ?? undefined}
64
+ alt={user.name ?? ""}
65
+ />
66
+ <AvatarFallback>
67
+ {(user.name ?? user.email).slice(0, 2).toUpperCase()}
68
+ </AvatarFallback>
69
+ </Avatar>
70
+ <div>
71
+ <p className="font-medium">{user.name ?? "—"}</p>
72
+ <p className="text-sm text-muted-foreground">{user.email}</p>
73
+ </div>
74
+ </div>
75
+ <div className="grid gap-6 md:grid-cols-2">
76
+ <div className="space-y-2">
77
+ <Label>Vai trò</Label>
78
+ <div className="flex flex-wrap gap-2">
79
+ {user.userRoles.length ? (
80
+ user.userRoles.map((ur) => (
81
+ <Badge key={ur.roleCode} variant="secondary">
82
+ {ur.role.name}
83
+ </Badge>
84
+ ))
85
+ ) : (
86
+ <span className="text-sm text-muted-foreground">—</span>
87
+ )}
88
+ </div>
89
+ </div>
90
+ <div className="space-y-2">
91
+ <Label>Chi nhánh</Label>
92
+ <div className="flex flex-wrap gap-2">
93
+ {user.userBranches.length ? (
94
+ user.userBranches.map((ub, i) => (
95
+ <Badge key={i} variant="secondary">
96
+ {ub.branch.name}
97
+ {ub.isDefault ? " (mặc định)" : ""}
98
+ </Badge>
99
+ ))
100
+ ) : (
101
+ <span className="text-sm text-muted-foreground">—</span>
102
+ )}
103
+ </div>
104
+ </div>
105
+ </div>
106
+ </CardContent>
107
+ </Card>
108
+
109
+ <Card>
110
+ <CardHeader>
111
+ <CardTitle>Đổi mật khẩu</CardTitle>
112
+ </CardHeader>
113
+ <CardContent>
114
+ <form
115
+ ref={formRef}
116
+ action={formAction}
117
+ className="grid gap-4 md:max-w-md"
118
+ >
119
+ <div className="space-y-2">
120
+ <Label htmlFor="currentPassword">Mật khẩu hiện tại</Label>
121
+ <Input
122
+ id="currentPassword"
123
+ name="currentPassword"
124
+ type="password"
125
+ autoComplete="current-password"
126
+ required
127
+ />
128
+ </div>
129
+ <div className="space-y-2">
130
+ <Label htmlFor="newPassword">Mật khẩu mới</Label>
131
+ <Input
132
+ id="newPassword"
133
+ name="newPassword"
134
+ type="password"
135
+ autoComplete="new-password"
136
+ required
137
+ />
138
+ </div>
139
+ <div className="space-y-2">
140
+ <Label htmlFor="confirmPassword">Nhập lại mật khẩu mới</Label>
141
+ <Input
142
+ id="confirmPassword"
143
+ name="confirmPassword"
144
+ type="password"
145
+ autoComplete="new-password"
146
+ required
147
+ />
148
+ </div>
149
+ <Button type="submit" disabled={pending} className="w-fit">
150
+ {pending ? "Đang cập nhật..." : "Đổi mật khẩu"}
151
+ </Button>
152
+ </form>
153
+ </CardContent>
154
+ </Card>
155
+ </div>
156
+ )
157
+ }
@@ -0,0 +1,69 @@
1
+ import { getUsersList } from "@/server/services/user-service"
2
+ import { getCrudPermissions } from "@goerp/core/crud/server"
3
+ import { getActiveRoles, getUserStats } from "@goerp/core/user"
4
+ import { UsersClientPage } from "@goerp/core/user/client-page"
5
+
6
+ import type { Metadata } from "next"
7
+
8
+ import { dictionary } from "@/data/dictionary"
9
+
10
+ import { usersConfig } from "@/configs/entities/users.config"
11
+ import { getActionMetaMap } from "@/configs/permissions"
12
+ import { MENU_TREE } from "@/configs/permissions/menu-tree"
13
+ import { requirePageAccess } from "@/lib/page-guard"
14
+ import { db } from "@/lib/prisma"
15
+
16
+ export const metadata: Metadata = { title: "Người dùng" }
17
+ export const dynamic = "force-dynamic"
18
+
19
+ interface UsersPageProps {
20
+ params: Promise<{ lang: string }>
21
+ searchParams: Promise<{ [key: string]: string | string[] | undefined }>
22
+ }
23
+
24
+ /**
25
+ * Quản lý người dùng — màn hình sẵn của core (UsersClientPage), trang chỉ nạp
26
+ * dữ liệu và truyền props. Danh sách lọc theo ?search&role&status trên URL
27
+ * (toolbar của core ghi query), phân trang chạy phía client.
28
+ *
29
+ * Tạo/sửa đi qua /api/users (POST/PUT) — UnifiedProfileDialog của core tự gọi;
30
+ * không truyền onAssignRoles/onResetPassword vì vai trò + mật khẩu đã nằm
31
+ * trong payload PUT (dialog gửi cả roleCodes/branchIds/password một lượt).
32
+ */
33
+ export default async function UsersPage({
34
+ params,
35
+ searchParams,
36
+ }: UsersPageProps) {
37
+ const { lang } = await params
38
+ const session = await requirePageAccess(lang, "user")
39
+
40
+ const sp = await searchParams
41
+ const search = typeof sp.search === "string" ? sp.search.trim() : undefined
42
+ const role = typeof sp.role === "string" ? sp.role : undefined
43
+ const status = typeof sp.status === "string" ? sp.status : undefined
44
+
45
+ const [permissions, users, roles, stats] = await Promise.all([
46
+ getCrudPermissions(session, "user"),
47
+ getUsersList({
48
+ search: search || undefined,
49
+ roleCode: role && role !== "all" ? role : undefined,
50
+ status,
51
+ }),
52
+ getActiveRoles(db),
53
+ getUserStats(db),
54
+ ])
55
+
56
+ return (
57
+ <UsersClientPage
58
+ initialData={users.data}
59
+ config={usersConfig}
60
+ // export tắt cứng — xem ghi chú trong users.config.ts.
61
+ permissions={{ ...permissions, export: false }}
62
+ dictionary={dictionary}
63
+ roles={roles}
64
+ stats={stats}
65
+ menuTree={MENU_TREE}
66
+ actionMeta={getActionMetaMap()}
67
+ />
68
+ )
69
+ }
@@ -0,0 +1,9 @@
1
+ import { notFound } from "next/navigation"
2
+
3
+ /**
4
+ * URL gõ sai bất kỳ dưới /[lang] rơi vào catch-all này → kích hoạt
5
+ * [lang]/not-found.tsx thay vì trang trắng mặc định của Next.
6
+ */
7
+ export default function CatchAllNotFound() {
8
+ notFound()
9
+ }
@@ -0,0 +1,26 @@
1
+ "use client"
2
+
3
+ import { ErrorView } from "@goerp/core/ui"
4
+
5
+ /**
6
+ * Boundary bắt LỖI CỦA LAYOUT: error.tsx chỉ bọc children của segment chứa nó,
7
+ * nên lỗi ném từ (main)/layout.tsx phải leo lên đây mới có người bắt. Boundary
8
+ * này thay CẢ vỏ app → fullScreen.
9
+ */
10
+ export default function LangError({
11
+ error,
12
+ reset,
13
+ }: {
14
+ error: Error & { digest?: string }
15
+ reset: () => void
16
+ }) {
17
+ return (
18
+ <ErrorView
19
+ error={error}
20
+ reset={reset}
21
+ title="Không tải được dữ liệu"
22
+ description="Hệ thống chưa lấy được dữ liệu cho trang này. Đây là lỗi kỹ thuật tạm thời — quyền truy cập của bạn không thay đổi. Vui lòng thử lại; nếu vẫn lỗi, báo quản trị viên kèm mã lỗi bên dưới."
23
+ fullScreen
24
+ />
25
+ )
26
+ }
@@ -1,8 +1,8 @@
1
+ import { Providers } from "@/providers"
2
+
1
3
  import type { LocaleType } from "@goerp/core/types"
2
4
  import type { ReactNode } from "react"
3
5
 
4
- import { Providers } from "@/providers"
5
-
6
6
  /**
7
7
  * Không truyền session xuống Providers: Better Auth lấy session ở CLIENT qua
8
8
  * authClient.useSession() (bridge). Server component nào cần session thì tự gọi
@@ -0,0 +1,9 @@
1
+ import { NotFoundView } from "@goerp/core/ui"
2
+
3
+ /**
4
+ * `notFound()` gọi từ trang NGOÀI vỏ app (không sidebar) rơi vào đây —
5
+ * chiếm trọn màn hình. Trang trong vỏ đã có (main)/not-found.tsx giữ lại.
6
+ */
7
+ export default function LangNotFound() {
8
+ return <NotFoundView fullScreen />
9
+ }
@@ -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,26 @@
1
+ import { NextResponse } from "next/server"
2
+
3
+ import { apiHandler } from "@/lib/api-handler"
4
+ import { serverError } from "@/lib/errors/server-error"
5
+ import { db } from "@/lib/prisma"
6
+
7
+ /**
8
+ * Lookup chi nhánh cho picker (UnifiedProfileDialog của core fetch cứng
9
+ * /api/branches). Registry khai `branch` với lookupPolicy "authenticated" —
10
+ * đăng nhập là đọc được, nên KHÔNG gác resource/action (quyền `branch:view`
11
+ * chỉ gate trang quản lý chi nhánh, không gate combo).
12
+ *
13
+ * Fetcher của dialog nhận `{ data: [...] }` (data.items || data.data || mảng).
14
+ */
15
+ export const GET = apiHandler(async (req) => {
16
+ try {
17
+ const branches = await db.branch.findMany({
18
+ where: { isActive: true },
19
+ select: { id: true, code: true, name: true },
20
+ orderBy: { name: "asc" },
21
+ })
22
+ return NextResponse.json({ data: branches })
23
+ } catch (error) {
24
+ return serverError(error, req, { message: "Lỗi tải danh sách chi nhánh" })
25
+ }
26
+ })