@goplusvn/core 0.1.59 → 0.1.60

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 (139) hide show
  1. package/PLATFORM.md +18 -0
  2. package/bin/goerp-init.mjs +141 -0
  3. package/package.json +4 -4
  4. package/src/cron/db-cron-manager.ts +3 -3
  5. package/src/cron/index.ts +2 -2
  6. package/src/{infrastructure/cron/cron-manager.ts → cron/simple-cron-job.ts} +1 -1
  7. package/src/infrastructure/__tests__/architecture-verification.spec.ts +13 -97
  8. package/src/infrastructure/index.ts +4 -7
  9. package/src/ui/management/index.ts +3 -2
  10. package/templates/starter-app/.dockerignore +41 -0
  11. package/templates/starter-app/.env.example +25 -0
  12. package/templates/starter-app/AGENTS.md +52 -0
  13. package/templates/starter-app/Dockerfile +74 -0
  14. package/templates/starter-app/README.md +141 -0
  15. package/templates/starter-app/gitignore +9 -0
  16. package/templates/starter-app/next.config.mjs +50 -0
  17. package/templates/starter-app/package.json +55 -0
  18. package/templates/starter-app/postcss.config.mjs +5 -0
  19. package/templates/starter-app/prisma/migrations/20260801000000_init/migration.sql +504 -0
  20. package/templates/starter-app/prisma/migrations/20260801091814_goerp_audit_logs_0001_init/migration.sql +33 -0
  21. package/templates/starter-app/prisma/migrations/20260801091815_goerp_background_tasks_0001_init/migration.sql +23 -0
  22. package/templates/starter-app/prisma/migrations/20260801091816_goerp_error_logs_0001_init/migration.sql +32 -0
  23. package/templates/starter-app/prisma/migrations/20260801091817_goerp_notifications_0001_init/migration.sql +59 -0
  24. package/templates/starter-app/prisma/migrations/20260801091818_goerp_system_jobs_0001_init/migration.sql +47 -0
  25. package/templates/starter-app/prisma/schema/auth.prisma +87 -0
  26. package/templates/starter-app/prisma/schema/domain.prisma +24 -0
  27. package/templates/starter-app/prisma/schema/goerp-audit-logs.prisma +23 -0
  28. package/templates/starter-app/prisma/schema/goerp-background-tasks.prisma +25 -0
  29. package/templates/starter-app/prisma/schema/goerp-error-logs.prisma +31 -0
  30. package/templates/starter-app/prisma/schema/goerp-notifications.prisma +49 -0
  31. package/templates/starter-app/prisma/schema/goerp-system-jobs.prisma +45 -0
  32. package/templates/starter-app/prisma/schema/organization.prisma +31 -0
  33. package/templates/starter-app/prisma/schema/rbac.prisma +100 -0
  34. package/templates/starter-app/prisma/schema/schema.prisma +8 -0
  35. package/templates/starter-app/prisma/schema/system.prisma +22 -0
  36. package/templates/starter-app/prisma/seed.ts +127 -0
  37. package/templates/starter-app/prisma.config.ts +20 -0
  38. package/templates/starter-app/public/.gitkeep +2 -0
  39. package/templates/starter-app/scripts/rbac-sync.ts +235 -0
  40. package/templates/starter-app/src/__tests__/architecture.test.ts +151 -0
  41. package/templates/starter-app/src/app/[lang]/(main)/admin/system/audit/page.tsx +24 -0
  42. package/templates/starter-app/src/app/[lang]/(main)/admin/system/error-logs/page.tsx +21 -0
  43. package/templates/starter-app/src/app/[lang]/(main)/admin/system/jobs/page.tsx +18 -0
  44. package/templates/starter-app/src/app/[lang]/(main)/admin/system/settings/page.tsx +22 -0
  45. package/templates/starter-app/src/app/[lang]/(main)/crud/[entity]/page.tsx +41 -0
  46. package/templates/starter-app/src/app/[lang]/(main)/layout.tsx +13 -0
  47. package/templates/starter-app/src/app/[lang]/(main)/notifications/page.tsx +13 -0
  48. package/templates/starter-app/src/app/[lang]/(main)/page.tsx +130 -0
  49. package/templates/starter-app/src/app/[lang]/(main)/roles/[id]/edit/page.tsx +60 -0
  50. package/templates/starter-app/src/app/[lang]/(main)/roles/new/page.tsx +41 -0
  51. package/templates/starter-app/src/app/[lang]/(main)/roles/page.tsx +48 -0
  52. package/templates/starter-app/src/app/[lang]/(main)/tasks/page.tsx +13 -0
  53. package/templates/starter-app/src/app/[lang]/(plain)/layout.tsx +10 -0
  54. package/templates/starter-app/src/app/[lang]/(plain)/sign-in/page.tsx +39 -0
  55. package/templates/starter-app/src/app/[lang]/layout.tsx +21 -0
  56. package/templates/starter-app/src/app/api/admin/system/audit/route.ts +60 -0
  57. package/templates/starter-app/src/app/api/admin/system/jobs/[name]/history/route.ts +39 -0
  58. package/templates/starter-app/src/app/api/admin/system/jobs/route.ts +96 -0
  59. package/templates/starter-app/src/app/api/admin/system/settings/all/route.ts +17 -0
  60. package/templates/starter-app/src/app/api/admin/system/settings/create/route.ts +19 -0
  61. package/templates/starter-app/src/app/api/admin/system/settings/delete/route.ts +20 -0
  62. package/templates/starter-app/src/app/api/admin/system/settings/route.ts +49 -0
  63. package/templates/starter-app/src/app/api/admin/system/settings/toggle-status/route.ts +28 -0
  64. package/templates/starter-app/src/app/api/admin/system/settings/update/route.ts +24 -0
  65. package/templates/starter-app/src/app/api/admin/system/settings/update-full/route.ts +23 -0
  66. package/templates/starter-app/src/app/api/better-auth/[...all]/route.ts +13 -0
  67. package/templates/starter-app/src/app/api/crud/[entity]/[id]/route.ts +13 -0
  68. package/templates/starter-app/src/app/api/crud/[entity]/route.ts +14 -0
  69. package/templates/starter-app/src/app/api/error-logs/[id]/route.ts +23 -0
  70. package/templates/starter-app/src/app/api/error-logs/route.ts +124 -0
  71. package/templates/starter-app/src/app/api/files/[...key]/route.ts +25 -0
  72. package/templates/starter-app/src/app/api/notifications/read/route.ts +29 -0
  73. package/templates/starter-app/src/app/api/notifications/route.ts +26 -0
  74. package/templates/starter-app/src/app/api/notifications/unread-count/route.ts +14 -0
  75. package/templates/starter-app/src/app/api/rbac/permissions-version/route.ts +11 -0
  76. package/templates/starter-app/src/app/api/roles/[id]/route.ts +14 -0
  77. package/templates/starter-app/src/app/api/roles/route.ts +18 -0
  78. package/templates/starter-app/src/app/api/tasks/[id]/download/route.ts +52 -0
  79. package/templates/starter-app/src/app/api/tasks/route.ts +37 -0
  80. package/templates/starter-app/src/app/api/upload/route.ts +15 -0
  81. package/templates/starter-app/src/app/globals.css +15 -0
  82. package/templates/starter-app/src/app/layout.tsx +16 -0
  83. package/templates/starter-app/src/app/page.tsx +8 -0
  84. package/templates/starter-app/src/components/layout/main-layout-wrapper.tsx +30 -0
  85. package/templates/starter-app/src/configs/entities/department.config.ts +24 -0
  86. package/templates/starter-app/src/configs/entities/index.ts +13 -0
  87. package/templates/starter-app/src/configs/i18n.ts +12 -0
  88. package/templates/starter-app/src/configs/permissions/index.ts +45 -0
  89. package/templates/starter-app/src/configs/permissions/master-data.permissions.ts +31 -0
  90. package/templates/starter-app/src/configs/permissions/system.permissions.ts +131 -0
  91. package/templates/starter-app/src/configs/permissions/types.ts +63 -0
  92. package/templates/starter-app/src/configs/tenant.ts +18 -0
  93. package/templates/starter-app/src/data/dictionary.ts +8 -0
  94. package/templates/starter-app/src/data/navigations.ts +61 -0
  95. package/templates/starter-app/src/instrumentation.ts +105 -0
  96. package/templates/starter-app/src/lib/api-handler.ts +157 -0
  97. package/templates/starter-app/src/lib/auth-client.ts +57 -0
  98. package/templates/starter-app/src/lib/auth.ts +62 -0
  99. package/templates/starter-app/src/lib/better-auth.ts +107 -0
  100. package/templates/starter-app/src/lib/branch-scope.ts +53 -0
  101. package/templates/starter-app/src/lib/cron/db-cron-manager.ts +42 -0
  102. package/templates/starter-app/src/lib/crud/index.ts +13 -0
  103. package/templates/starter-app/src/lib/errors/app-error.ts +2 -0
  104. package/templates/starter-app/src/lib/errors/error-handler.ts +5 -0
  105. package/templates/starter-app/src/lib/errors/log-server-error.ts +15 -0
  106. package/templates/starter-app/src/lib/errors/server-error.ts +11 -0
  107. package/templates/starter-app/src/lib/logger.ts +30 -0
  108. package/templates/starter-app/src/lib/page-guard.ts +35 -0
  109. package/templates/starter-app/src/lib/prisma.ts +80 -0
  110. package/templates/starter-app/src/lib/rbac/access.ts +87 -0
  111. package/templates/starter-app/src/lib/storage.ts +28 -0
  112. package/templates/starter-app/src/providers/index.tsx +54 -0
  113. package/templates/starter-app/src/providers/mode-provider.tsx +31 -0
  114. package/templates/starter-app/src/providers/theme-provider.tsx +21 -0
  115. package/templates/starter-app/src/proxy.ts +45 -0
  116. package/templates/starter-app/src/server/services/notification-service.ts +31 -0
  117. package/templates/starter-app/src/server/services/system-config-service.ts +163 -0
  118. package/templates/starter-app/src/server/tasks/handlers/export-departments.ts +58 -0
  119. package/templates/starter-app/src/server/tasks/index.ts +16 -0
  120. package/templates/starter-app/src/server/tasks/task-runner.ts +40 -0
  121. package/templates/starter-app/src/types/session.ts +29 -0
  122. package/templates/starter-app/tsconfig.json +47 -0
  123. package/templates/starter-app/vitest.config.ts +17 -0
  124. package/src/infrastructure/cron/index.ts +0 -6
  125. package/src/infrastructure/event-bus/event-bus.ts +0 -145
  126. package/src/infrastructure/event-bus/index.ts +0 -2
  127. package/src/infrastructure/event-bus/types.ts +0 -22
  128. package/src/infrastructure/lock/decorators.ts +0 -67
  129. package/src/infrastructure/lock/index.ts +0 -2
  130. package/src/infrastructure/lock/lock-manager.ts +0 -33
  131. package/src/plugin/apps-registry.ts +0 -97
  132. package/src/plugin/index.ts +0 -5
  133. package/src/plugin/types.ts +0 -41
  134. package/src/ui/management/audit-log-page.tsx +0 -14
  135. package/src/ui/management/job-management.tsx +0 -308
  136. package/src/workflow/activity-timeline.tsx +0 -412
  137. package/src/workflow/approval-workflow.tsx +0 -31
  138. package/src/workflow/index.ts +0 -2
  139. /package/src/{infrastructure/cron → cron}/types.ts +0 -0
@@ -0,0 +1,60 @@
1
+ import { notFound } from "next/navigation"
2
+ import { getCrudPermissions } from "@goerp/core/crud/server"
3
+ import { RoleFormPage } from "@goerp/core/rbac/pages"
4
+ import {
5
+ getActionsForPermissionMatrix,
6
+ getResourcesForPermissionMatrix,
7
+ } from "@goerp/core/rbac/resource-service"
8
+
9
+ import type { Metadata } from "next"
10
+
11
+ import { dictionary } from "@/data/dictionary"
12
+ import { requirePageAccess } from "@/lib/page-guard"
13
+ import { db } from "@/lib/prisma"
14
+
15
+ export const metadata: Metadata = { title: "Sửa vai trò" }
16
+ export const dynamic = "force-dynamic"
17
+
18
+ export default async function EditRolePage({
19
+ params,
20
+ }: {
21
+ params: Promise<{ lang: string; id: string }>
22
+ }) {
23
+ const { lang, id } = await params
24
+ const session = await requirePageAccess(lang, "role", "update")
25
+
26
+ const [permissions, resources, actions, role] = await Promise.all([
27
+ getCrudPermissions(session, "role"),
28
+ getResourcesForPermissionMatrix(db),
29
+ getActionsForPermissionMatrix(db),
30
+ db.role.findUnique({
31
+ where: { id },
32
+ include: { rolePermissions: { select: { resourceCode: true, actionCode: true } } },
33
+ }),
34
+ ])
35
+
36
+ if (!role) notFound()
37
+
38
+ return (
39
+ <RoleFormPage
40
+ dictionary={dictionary}
41
+ permissions={permissions}
42
+ lang={lang}
43
+ resources={resources}
44
+ actions={actions}
45
+ mode="edit"
46
+ initialData={{
47
+ id: role.id,
48
+ name: role.name,
49
+ code: role.code,
50
+ description: role.description ?? undefined,
51
+ status: role.status,
52
+ // Ma trận quyền của core đọc dạng "action:resource" — đảo thứ tự là
53
+ // không ô nào tick, mà cũng chẳng có lỗi nào hiện ra.
54
+ permissions: role.rolePermissions.map(
55
+ (rp) => `${rp.actionCode}:${rp.resourceCode}`,
56
+ ),
57
+ }}
58
+ />
59
+ )
60
+ }
@@ -0,0 +1,41 @@
1
+ import { getCrudPermissions } from "@goerp/core/crud/server"
2
+ import { RoleFormPage } from "@goerp/core/rbac/pages"
3
+ import {
4
+ getActionsForPermissionMatrix,
5
+ getResourcesForPermissionMatrix,
6
+ } from "@goerp/core/rbac/resource-service"
7
+
8
+ import type { Metadata } from "next"
9
+
10
+ import { dictionary } from "@/data/dictionary"
11
+ import { requirePageAccess } from "@/lib/page-guard"
12
+ import { db } from "@/lib/prisma"
13
+
14
+ export const metadata: Metadata = { title: "Thêm vai trò" }
15
+ export const dynamic = "force-dynamic"
16
+
17
+ export default async function NewRolePage({
18
+ params,
19
+ }: {
20
+ params: Promise<{ lang: string }>
21
+ }) {
22
+ const { lang } = await params
23
+ const session = await requirePageAccess(lang, "role", "create")
24
+
25
+ const [permissions, resources, actions] = await Promise.all([
26
+ getCrudPermissions(session, "role"),
27
+ getResourcesForPermissionMatrix(db),
28
+ getActionsForPermissionMatrix(db),
29
+ ])
30
+
31
+ return (
32
+ <RoleFormPage
33
+ dictionary={dictionary}
34
+ permissions={permissions}
35
+ lang={lang}
36
+ resources={resources}
37
+ actions={actions}
38
+ mode="create"
39
+ />
40
+ )
41
+ }
@@ -0,0 +1,48 @@
1
+ import { getCrudPermissions } from "@goerp/core/crud/server"
2
+ import { RoleListPage } from "@goerp/core/rbac/pages"
3
+ import {
4
+ getActionsForPermissionMatrix,
5
+ getResourcesForPermissionMatrix,
6
+ } from "@goerp/core/rbac/resource-service"
7
+ import { getRolesData } from "@goerp/core/rbac/role-service"
8
+
9
+ import type { Metadata } from "next"
10
+
11
+ import { dictionary } from "@/data/dictionary"
12
+ import { requirePageAccess } from "@/lib/page-guard"
13
+ import { db } from "@/lib/prisma"
14
+
15
+ export const metadata: Metadata = { title: "Vai trò" }
16
+ export const dynamic = "force-dynamic"
17
+
18
+ /**
19
+ * Danh sách vai trò — màn hình sẵn của core, dữ liệu do trang nạp và truyền
20
+ * vào dạng props (core không tự truy vấn DB). Ma trận quyền hiển thị từ bảng
21
+ * `Resource`/`Action`, tức là từ những gì `pnpm rbac-sync` đã đồng bộ.
22
+ */
23
+ export default async function RolesPage({
24
+ params,
25
+ }: {
26
+ params: Promise<{ lang: string }>
27
+ }) {
28
+ const { lang } = await params
29
+ const session = await requirePageAccess(lang, "role")
30
+
31
+ const [permissions, initialData, resources, actions] = await Promise.all([
32
+ getCrudPermissions(session, "role"),
33
+ getRolesData(db, { page: 1, pageSize: 20 }),
34
+ getResourcesForPermissionMatrix(db),
35
+ getActionsForPermissionMatrix(db),
36
+ ])
37
+
38
+ return (
39
+ <RoleListPage
40
+ dictionary={dictionary}
41
+ permissions={permissions}
42
+ lang={lang}
43
+ initialData={initialData}
44
+ resources={resources}
45
+ actions={actions}
46
+ />
47
+ )
48
+ }
@@ -0,0 +1,13 @@
1
+ import { TaskListClient } from "@goerp/core/tasks/ui"
2
+
3
+ import type { Metadata } from "next"
4
+
5
+ export const metadata: Metadata = { title: "Tác vụ nền" }
6
+
7
+ /**
8
+ * Tác vụ nền của chính người dùng (xuất/nhập file dài hơi). /api/tasks lọc
9
+ * theo `createdBy` nên trang này cũng không cần quyền riêng.
10
+ */
11
+ export default function TasksPage() {
12
+ return <TaskListClient />
13
+ }
@@ -0,0 +1,10 @@
1
+ import type { ReactNode } from "react"
2
+
3
+ /**
4
+ * Vỏ TRỐNG — dành cho các trang không có sidebar/topbar: đăng nhập, trang in,
5
+ * màn hình công khai. Vẫn nằm trong [lang] nên vẫn có Providers (theme, tenant,
6
+ * auth bridge) — SignInForm của core cần chúng.
7
+ */
8
+ export default function PlainLayout({ children }: { children: ReactNode }) {
9
+ return <main className="min-h-screen w-full">{children}</main>
10
+ }
@@ -0,0 +1,39 @@
1
+ import {
2
+ Auth,
3
+ AuthDescription,
4
+ AuthForm,
5
+ AuthHeader,
6
+ AuthTitle,
7
+ SignInForm,
8
+ } from "@goerp/core/ui/auth"
9
+
10
+ import type { Metadata } from "next"
11
+
12
+ import wallpaper from "@goerp/core/assets/erp_wallpaper.png"
13
+
14
+ import { dictionary } from "@/data/dictionary"
15
+
16
+ export const metadata: Metadata = { title: "Đăng nhập" }
17
+
18
+ /**
19
+ * Trang đăng nhập = ghép sẵn từ core. `SignInForm` gọi qua AuthBridge (xem
20
+ * src/lib/auth-client.ts) nên không biết gì về Better Auth — đổi thư viện auth
21
+ * chỉ cần sửa bridge, trang này giữ nguyên.
22
+ *
23
+ * Đổi ảnh nền bằng cách trỏ `imgSrc` sang file trong public/ của app.
24
+ */
25
+ export default function SignInPage() {
26
+ return (
27
+ <Auth imgSrc={wallpaper} dictionary={dictionary}>
28
+ <AuthHeader>
29
+ <AuthTitle>Đăng nhập</AuthTitle>
30
+ <AuthDescription>
31
+ Nhập email và mật khẩu để vào hệ thống.
32
+ </AuthDescription>
33
+ </AuthHeader>
34
+ <AuthForm>
35
+ <SignInForm />
36
+ </AuthForm>
37
+ </Auth>
38
+ )
39
+ }
@@ -0,0 +1,21 @@
1
+ import type { LocaleType } from "@goerp/core/types"
2
+ import type { ReactNode } from "react"
3
+
4
+ import { Providers } from "@/providers"
5
+
6
+ /**
7
+ * Không truyền session xuống Providers: Better Auth lấy session ở CLIENT qua
8
+ * authClient.useSession() (bridge). Server component nào cần session thì tự gọi
9
+ * getSession() — nó đã được React cache theo request.
10
+ */
11
+ export default async function LangLayout({
12
+ children,
13
+ params,
14
+ }: {
15
+ children: ReactNode
16
+ params: Promise<{ lang: string }>
17
+ }) {
18
+ const { lang } = await params
19
+
20
+ return <Providers locale={lang as LocaleType}>{children}</Providers>
21
+ }
@@ -0,0 +1,60 @@
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
+ /** Nhật ký thao tác cho trang /admin/system/audit (SystemAuditPage của core). */
8
+ export const GET = apiHandler(
9
+ async (req) => {
10
+ try {
11
+ const sp = new URL(req.url).searchParams
12
+ const action = sp.get("action") || undefined
13
+ const resource = sp.get("resource") || undefined
14
+ const userId = sp.get("userId") || undefined
15
+ const search = sp.get("search") || undefined
16
+ const startDate = sp.get("startDate")
17
+ const endDate = sp.get("endDate")
18
+ const skip = parseInt(sp.get("skip") || "0", 10)
19
+ const take = Math.min(parseInt(sp.get("take") || "50", 10), 100)
20
+
21
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
+ const where: Record<string, any> = {}
23
+ if (action && action !== "all") where.action = action
24
+ if (resource && resource !== "all") where.resource = resource
25
+ if (userId) where.userId = userId
26
+ if (search) {
27
+ where.OR = [
28
+ { description: { contains: search, mode: "insensitive" } },
29
+ { resourceId: { contains: search, mode: "insensitive" } },
30
+ { action: { contains: search, mode: "insensitive" } },
31
+ { resource: { contains: search, mode: "insensitive" } },
32
+ ]
33
+ }
34
+ if (startDate || endDate) {
35
+ where.createdAt = {}
36
+ if (startDate) where.createdAt.gte = new Date(startDate)
37
+ if (endDate) where.createdAt.lte = new Date(endDate)
38
+ }
39
+
40
+ const [logs, total] = await Promise.all([
41
+ db.auditLog.findMany({
42
+ where,
43
+ orderBy: { createdAt: "desc" },
44
+ skip,
45
+ take,
46
+ include: { user: { select: { id: true, name: true, email: true } } },
47
+ }),
48
+ db.auditLog.count({ where }),
49
+ ])
50
+
51
+ return NextResponse.json({
52
+ data: logs,
53
+ meta: { total, skip, take, hasMore: skip + take < total },
54
+ })
55
+ } catch (error) {
56
+ return serverError(error, req, { message: "Không tải được nhật ký thao tác" })
57
+ }
58
+ },
59
+ { resource: "audit-log", action: "view" },
60
+ )
@@ -0,0 +1,39 @@
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
+ * Lịch sử chạy của một job. Mỗi dòng có `actions` — nhật ký diễn biến do chính
9
+ * job ghi qua `getJobExecutionContext().log()`, thứ cho biết lần chạy đó thực
10
+ * sự làm được gì chứ không chỉ thành công/thất bại.
11
+ */
12
+ export const GET = apiHandler(
13
+ async (req, { params }) => {
14
+ try {
15
+ const { name } = await params
16
+ const sp = new URL(req.url).searchParams
17
+ const skip = parseInt(sp.get("skip") || "0", 10)
18
+ const take = Math.min(parseInt(sp.get("take") || "20", 10), 100)
19
+
20
+ const [logs, total] = await Promise.all([
21
+ db.jobExecutionLog.findMany({
22
+ where: { jobName: name },
23
+ orderBy: { startedAt: "desc" },
24
+ skip,
25
+ take,
26
+ }),
27
+ db.jobExecutionLog.count({ where: { jobName: name } }),
28
+ ])
29
+
30
+ return NextResponse.json({
31
+ data: logs,
32
+ meta: { total, skip, take, hasMore: skip + take < total },
33
+ })
34
+ } catch (error) {
35
+ return serverError(error, req, { message: "Không tải được lịch sử job" })
36
+ }
37
+ },
38
+ { resource: "system-job", action: "view" },
39
+ )
@@ -0,0 +1,96 @@
1
+ import { NextResponse } from "next/server"
2
+
3
+ import { apiHandler } from "@/lib/api-handler"
4
+ import { cronManager } from "@/lib/cron/db-cron-manager"
5
+ import { serverError } from "@/lib/errors/server-error"
6
+ import { db } from "@/lib/prisma"
7
+
8
+ /**
9
+ * Danh sách job cho trang /admin/system/jobs (SystemJobsPage của core).
10
+ *
11
+ * Hợp nhất HAI nguồn: bảng `system_jobs` (nguồn sự thật của lịch + cờ bật/tắt,
12
+ * còn nguyên sau khi restart) và registry trong RAM của tiến trình đang phục vụ
13
+ * request (biết job có đang chạy không). Tiến trình này có thể chưa chạy
14
+ * `register()` — khi đó `inMemory=false` và nút chạy tay sẽ không tác dụng, nên
15
+ * cờ đó được trả về cho UI biết đường hiển thị.
16
+ */
17
+ export const GET = apiHandler(
18
+ async (req) => {
19
+ try {
20
+ const memoryJobs = new Map(cronManager.getJobsInfo().map((j) => [j.name, j]))
21
+ const dbJobs = new Map(
22
+ (await db.systemJob.findMany({ orderBy: { name: "asc" } })).map((j) => [j.name, j]),
23
+ )
24
+
25
+ const names = [...new Set([...dbJobs.keys(), ...memoryJobs.keys()])].sort((a, b) =>
26
+ a.localeCompare(b),
27
+ )
28
+
29
+ const jobs = names.map((name) => {
30
+ const mem = memoryJobs.get(name)
31
+ const row = dbJobs.get(name)
32
+ return {
33
+ name,
34
+ cronTime: row?.cronTime ?? mem?.cronTime ?? "",
35
+ isRunning: mem?.isRunning ?? false,
36
+ nextDate: mem?.nextDate ?? null,
37
+ inMemory: !!mem,
38
+ enabled: row?.enabled ?? mem?.isRunning ?? false,
39
+ status: row?.status ?? (mem?.isRunning ? "idle" : "stopped"),
40
+ lastRun: row?.lastRun ?? null,
41
+ nextRun: row?.nextRun ?? null,
42
+ error: row?.error ?? null,
43
+ }
44
+ })
45
+
46
+ return NextResponse.json({ data: jobs })
47
+ } catch (error) {
48
+ return serverError(error, req, { message: "Không tải được danh sách job" })
49
+ }
50
+ },
51
+ { resource: "system-job", action: "view" },
52
+ )
53
+
54
+ /** POST { name, action: "toggle" | "run" | "remove" }. */
55
+ export const POST = apiHandler(
56
+ async (req) => {
57
+ try {
58
+ const { name, action } = await req.json()
59
+ if (!name) {
60
+ return NextResponse.json({ error: "Thiếu tên job" }, { status: 400 })
61
+ }
62
+
63
+ if (action === "toggle") {
64
+ if (!(await cronManager.toggleJob(name))) {
65
+ return NextResponse.json({ error: `Không tìm thấy job "${name}"` }, { status: 404 })
66
+ }
67
+ const info = cronManager.getJobsInfo().find((j) => j.name === name)
68
+ return NextResponse.json({
69
+ success: true,
70
+ message: `Job "${name}" đã ${info?.isRunning ? "bật" : "tắt"}`,
71
+ isRunning: info?.isRunning,
72
+ })
73
+ }
74
+
75
+ if (action === "run") {
76
+ if (!(await cronManager.runJobNow(name))) {
77
+ return NextResponse.json({ error: `Không tìm thấy job "${name}"` }, { status: 404 })
78
+ }
79
+ return NextResponse.json({ success: true, message: `Đã chạy tay job "${name}"` })
80
+ }
81
+
82
+ if (action === "remove") {
83
+ cronManager.removeJob(name)
84
+ return NextResponse.json({ success: true, message: `Đã gỡ job "${name}"` })
85
+ }
86
+
87
+ return NextResponse.json(
88
+ { error: "action phải là 'toggle', 'run' hoặc 'remove'" },
89
+ { status: 400 },
90
+ )
91
+ } catch (error) {
92
+ return serverError(error, req, { message: "Không thao tác được với job" })
93
+ }
94
+ },
95
+ { resource: "system-job", action: "update" },
96
+ )
@@ -0,0 +1,17 @@
1
+ import { NextResponse } from "next/server"
2
+
3
+ import { apiHandler } from "@/lib/api-handler"
4
+ import { serverError } from "@/lib/errors/server-error"
5
+ import { listConfigs } from "@/server/services/system-config-service"
6
+
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
+ export const GET = apiHandler(
9
+ async (req) => {
10
+ try {
11
+ return NextResponse.json({ data: await listConfigs() })
12
+ } catch (error) {
13
+ return serverError(error, req, { message: "Không tải được cấu hình" })
14
+ }
15
+ },
16
+ { resource: "system-setting", action: "view" },
17
+ )
@@ -0,0 +1,19 @@
1
+ import { NextResponse } from "next/server"
2
+
3
+ import { apiHandler } from "@/lib/api-handler"
4
+ import { serverError } from "@/lib/errors/server-error"
5
+ import { configErrorInfo, createConfig } from "@/server/services/system-config-service"
6
+
7
+ export const POST = apiHandler(
8
+ async (req, { session }) => {
9
+ try {
10
+ const data = await createConfig(await req.json(), session.user.id)
11
+ return NextResponse.json({ success: true, data })
12
+ } catch (error) {
13
+ const known = configErrorInfo(error)
14
+ if (known) return NextResponse.json({ error: known.message }, { status: known.status })
15
+ return serverError(error, req, { message: "Không tạo được cấu hình" })
16
+ }
17
+ },
18
+ { resource: "system-setting", action: "create" },
19
+ )
@@ -0,0 +1,20 @@
1
+ import { NextResponse } from "next/server"
2
+
3
+ import { apiHandler } from "@/lib/api-handler"
4
+ import { serverError } from "@/lib/errors/server-error"
5
+ import { configErrorInfo, deleteConfig } from "@/server/services/system-config-service"
6
+
7
+ export const POST = apiHandler(
8
+ async (req) => {
9
+ try {
10
+ const { key } = await req.json()
11
+ await deleteConfig(key)
12
+ return NextResponse.json({ success: true, message: "Đã xóa cấu hình" })
13
+ } catch (error) {
14
+ const known = configErrorInfo(error)
15
+ if (known) return NextResponse.json({ error: known.message }, { status: known.status })
16
+ return serverError(error, req, { message: "Không xóa được cấu hình" })
17
+ }
18
+ },
19
+ { resource: "system-setting", action: "delete" },
20
+ )
@@ -0,0 +1,49 @@
1
+ import { NextResponse } from "next/server"
2
+ import { SettingsService } from "@goerp/core/system/services/settings-service"
3
+
4
+ import { apiHandler } from "@/lib/api-handler"
5
+ import { serverError } from "@/lib/errors/server-error"
6
+
7
+ /**
8
+ * Nhóm cấu hình có KIỂU (lưu dạng JSON dưới một key) — khác với
9
+ * /settings/all vốn liệt kê từng dòng key-value thô.
10
+ *
11
+ * Thêm nhóm của app: khai thêm nhánh `type` bên dưới và dùng
12
+ * `SettingsService.getSettings/saveSettings` với key riêng.
13
+ */
14
+ export const GET = apiHandler(
15
+ async (req) => {
16
+ try {
17
+ const [audit, notification] = await Promise.all([
18
+ SettingsService.getAuditSettings(),
19
+ SettingsService.getNotificationSettings(),
20
+ ])
21
+ return NextResponse.json({ data: { audit, notification } })
22
+ } catch (error) {
23
+ return serverError(error, req, { message: "Không tải được cấu hình" })
24
+ }
25
+ },
26
+ { resource: "system-setting", action: "view" },
27
+ )
28
+
29
+ export const POST = apiHandler(
30
+ async (req) => {
31
+ try {
32
+ const { type, settings } = await req.json()
33
+
34
+ if (type === "audit") {
35
+ await SettingsService.saveAuditSettings(settings)
36
+ return NextResponse.json({ success: true, message: "Đã lưu cấu hình nhật ký" })
37
+ }
38
+ if (type === "notification") {
39
+ await SettingsService.saveNotificationSettings(settings)
40
+ return NextResponse.json({ success: true, message: "Đã lưu cấu hình thông báo" })
41
+ }
42
+
43
+ return NextResponse.json({ error: "Loại cấu hình không hợp lệ" }, { status: 400 })
44
+ } catch (error) {
45
+ return serverError(error, req, { message: "Không lưu được cấu hình" })
46
+ }
47
+ },
48
+ { resource: "system-setting", action: "update" },
49
+ )
@@ -0,0 +1,28 @@
1
+ import { NextResponse } from "next/server"
2
+
3
+ import { apiHandler } from "@/lib/api-handler"
4
+ import { serverError } from "@/lib/errors/server-error"
5
+ import {
6
+ configErrorInfo,
7
+ toggleConfigStatus,
8
+ } from "@/server/services/system-config-service"
9
+
10
+ /** Bật/tạm ngưng một cấu hình mà không xóa nó. */
11
+ export const POST = apiHandler(
12
+ async (req, { session }) => {
13
+ try {
14
+ const { key, status } = await req.json()
15
+ const data = await toggleConfigStatus(key, status, session.user.id)
16
+ return NextResponse.json({
17
+ success: true,
18
+ data,
19
+ message: status === "active" ? "Đã kích hoạt cấu hình" : "Đã tạm ngưng cấu hình",
20
+ })
21
+ } catch (error) {
22
+ 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" })
25
+ }
26
+ },
27
+ { resource: "system-setting", action: "update" },
28
+ )
@@ -0,0 +1,24 @@
1
+ import { NextResponse } from "next/server"
2
+
3
+ import { apiHandler } from "@/lib/api-handler"
4
+ import { serverError } from "@/lib/errors/server-error"
5
+ import {
6
+ configErrorInfo,
7
+ updateConfigValue,
8
+ } from "@/server/services/system-config-service"
9
+
10
+ /** Đổi mỗi giá trị của một cấu hình theo key. */
11
+ export const POST = apiHandler(
12
+ async (req, { session }) => {
13
+ try {
14
+ const { key, value } = await req.json()
15
+ const data = await updateConfigValue(key, value, session.user.id)
16
+ return NextResponse.json({ success: true, data })
17
+ } catch (error) {
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" })
21
+ }
22
+ },
23
+ { resource: "system-setting", action: "update" },
24
+ )
@@ -0,0 +1,23 @@
1
+ import { NextResponse } from "next/server"
2
+
3
+ import { apiHandler } from "@/lib/api-handler"
4
+ import { serverError } from "@/lib/errors/server-error"
5
+ import {
6
+ configErrorInfo,
7
+ updateConfigFull,
8
+ } from "@/server/services/system-config-service"
9
+
10
+ /** Sửa toàn bộ thuộc tính của một cấu hình (dialog "Sửa"). */
11
+ export const POST = apiHandler(
12
+ async (req, { session }) => {
13
+ try {
14
+ const data = await updateConfigFull(await req.json(), session.user.id)
15
+ return NextResponse.json({ success: true, data })
16
+ } catch (error) {
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" })
20
+ }
21
+ },
22
+ { resource: "system-setting", action: "update" },
23
+ )
@@ -0,0 +1,13 @@
1
+ import { toNextJsHandler } from "better-auth/next-js"
2
+
3
+ import { getAuth } from "@/lib/better-auth"
4
+
5
+ /**
6
+ * Toàn bộ endpoint của Better Auth (sign-in, sign-out, get-session…). Prefix
7
+ * `/api/better-auth` nằm trong danh sách public của src/proxy.ts vì các
8
+ * endpoint này tự xác thực — không có nó thì cổng default-deny chặn luôn cả
9
+ * đường đăng nhập.
10
+ */
11
+ export const { GET, POST } = toNextJsHandler((req: Request) =>
12
+ getAuth().handler(req),
13
+ )
@@ -0,0 +1,13 @@
1
+ import { createCrudItemHandlers } from "@goerp/core/crud/server"
2
+
3
+ import { getSession } from "@/lib/auth"
4
+ import { getEntityConfig } from "@/configs/entities"
5
+ import { serverError } from "@/lib/errors/server-error"
6
+ import { crudService } from "@/lib/crud"
7
+
8
+ export const { GET, PUT, PATCH, DELETE } = createCrudItemHandlers({
9
+ getSession,
10
+ getEntityConfig,
11
+ service: crudService,
12
+ onError: serverError,
13
+ })
@@ -0,0 +1,14 @@
1
+ import { createCrudCollectionHandlers } from "@goerp/core/crud/server"
2
+
3
+ import { getSession } from "@/lib/auth"
4
+ import { getEntityConfig } from "@/configs/entities"
5
+ import { serverError } from "@/lib/errors/server-error"
6
+ import { crudService } from "@/lib/crud"
7
+
8
+ // GET danh sách + POST tạo. Gác quyền + query đều ở core.
9
+ export const { GET, POST } = createCrudCollectionHandlers({
10
+ getSession,
11
+ getEntityConfig,
12
+ service: crudService,
13
+ onError: serverError,
14
+ })