@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.
- package/CHANGELOG.md +45 -0
- package/bin/goerp-init.mjs +15 -0
- package/package.json +2 -1
- package/src/auth/index.ts +5 -1
- package/src/auth/proxy-gate.ts +21 -2
- package/src/configs/entities/departments.config.ts +1 -0
- package/src/configs/entities/material-categories.config.ts +1 -0
- package/src/cron/__tests__/cron-schedule.test.ts +76 -0
- package/src/cron/cron-schedule.ts +128 -0
- package/src/cron/simple-cron-job.ts +83 -69
- package/src/crud/components/crud-export-button.tsx +3 -2
- package/src/crud/components/crud-page.tsx +56 -25
- package/src/crud/components/crud-row-actions.tsx +16 -4
- package/src/crud/components/crud-table.tsx +13 -1
- package/src/crud/crud-route-handlers.test.ts +352 -0
- package/src/crud/crud-route-handlers.ts +260 -13
- package/src/crud/index.ts +3 -0
- package/src/crud/lib/coerce.test.ts +118 -0
- package/src/crud/lib/coerce.ts +95 -0
- package/src/crud/lib/crud-utils.ts +4 -0
- package/src/crud/lib/entity-endpoints.test.ts +41 -0
- package/src/crud/lib/entity-endpoints.ts +23 -0
- package/src/crud/lib/errors.ts +14 -0
- package/src/crud/lib/import-request.ts +99 -0
- package/src/crud/lib/mutation-builder.test.ts +114 -0
- package/src/crud/lib/mutation-builder.ts +27 -34
- package/src/crud/lib/permissions.test.ts +57 -0
- package/src/crud/lib/permissions.ts +25 -13
- package/src/crud/lib/query-builder.ts +13 -6
- package/src/crud/lib/translate-config.ts +16 -2
- package/src/crud/pages/entity-crud-page.tsx +11 -8
- package/src/crud/server-service.test.ts +112 -0
- package/src/crud/server-service.ts +52 -27
- package/src/crud/server.ts +11 -1
- package/src/guardrails/index.ts +1 -0
- package/src/guardrails/rules/auth.ts +54 -1
- package/src/guardrails/rules/design.ts +3 -2
- package/src/guardrails/types.ts +17 -8
- package/src/providers/brand-theme.ts +20 -0
- package/src/rbac/pages/permission-catalog-pages.tsx +3 -1
- package/src/security/index.ts +2 -0
- package/src/security/pages/sessions-page.tsx +516 -0
- package/src/types/index.ts +75 -10
- package/src/ui/auth/sign-in-form.tsx +64 -9
- package/src/ui/data-display/__tests__/use-client-pagination.test.ts +59 -0
- package/src/ui/data-display/data-table/__tests__/data-table-row-memo.test.tsx +96 -0
- package/src/ui/data-display/data-table/data-table-context.tsx +5 -0
- package/src/ui/data-display/data-table/data-table.tsx +68 -23
- package/src/ui/data-display/data-table-pagination.tsx +25 -8
- package/src/ui/data-display/index.tsx +1 -0
- package/src/ui/data-display/use-client-pagination.ts +48 -0
- package/src/ui/errors/error-view.tsx +164 -0
- package/src/ui/errors/index.ts +2 -0
- package/src/ui/errors/not-found-view.tsx +44 -0
- package/src/ui/index.tsx +1 -0
- package/src/ui/layout/logo.tsx +54 -19
- package/src/user/pages/users-client-page.tsx +12 -7
- package/templates/starter-app/.env.example +4 -0
- package/templates/starter-app/AGENTS.md +1 -1
- package/templates/starter-app/README.md +6 -2
- package/templates/starter-app/husky/pre-commit +10 -0
- package/templates/starter-app/package.json +27 -2
- package/templates/starter-app/prisma/migrations/20260807115347_platform_pages/migration.sql +87 -0
- package/templates/starter-app/prisma/migrations/20260808003811_company_profile/migration.sql +25 -0
- package/templates/starter-app/prisma/schema/organization.prisma +20 -0
- package/templates/starter-app/prisma/schema/system.prisma +74 -0
- package/templates/starter-app/prisma/seed.ts +7 -5
- package/templates/starter-app/scripts/migration-new.mjs +89 -0
- package/templates/starter-app/scripts/rbac-sync.ts +49 -21
- package/templates/starter-app/src/__tests__/architecture.test.ts +11 -2
- package/templates/starter-app/src/app/[lang]/(main)/actions/page.tsx +37 -0
- package/templates/starter-app/src/app/[lang]/(main)/admin/system/cache/page.tsx +68 -0
- package/templates/starter-app/src/app/[lang]/(main)/admin/system/company/actions.ts +33 -0
- package/templates/starter-app/src/app/[lang]/(main)/admin/system/company/company-profile-form.tsx +195 -0
- package/templates/starter-app/src/app/[lang]/(main)/admin/system/company/page.tsx +29 -0
- package/templates/starter-app/src/app/[lang]/(main)/crud/[entity]/page.tsx +4 -2
- package/templates/starter-app/src/app/[lang]/(main)/error.tsx +24 -0
- package/templates/starter-app/src/app/[lang]/(main)/layout.tsx +2 -1
- package/templates/starter-app/src/app/[lang]/(main)/not-found.tsx +6 -0
- package/templates/starter-app/src/app/[lang]/(main)/page.tsx +13 -3
- package/templates/starter-app/src/app/[lang]/(main)/resources/page.tsx +39 -0
- package/templates/starter-app/src/app/[lang]/(main)/roles/[id]/edit/page.tsx +5 -2
- package/templates/starter-app/src/app/[lang]/(main)/roles/new/page.tsx +1 -0
- package/templates/starter-app/src/app/[lang]/(main)/roles/page.tsx +1 -0
- package/templates/starter-app/src/app/[lang]/(main)/security/sessions/page.tsx +24 -0
- package/templates/starter-app/src/app/[lang]/(main)/system-categories/page.tsx +30 -0
- package/templates/starter-app/src/app/[lang]/(main)/user/profile/actions.ts +62 -0
- package/templates/starter-app/src/app/[lang]/(main)/user/profile/page.tsx +41 -0
- package/templates/starter-app/src/app/[lang]/(main)/user/profile/profile-client-page.tsx +157 -0
- package/templates/starter-app/src/app/[lang]/(main)/users/page.tsx +69 -0
- package/templates/starter-app/src/app/[lang]/[...not-found]/page.tsx +9 -0
- package/templates/starter-app/src/app/[lang]/error.tsx +26 -0
- package/templates/starter-app/src/app/[lang]/layout.tsx +2 -2
- package/templates/starter-app/src/app/[lang]/not-found.tsx +9 -0
- package/templates/starter-app/src/app/api/admin/system/audit/route.ts +4 -3
- package/templates/starter-app/src/app/api/admin/system/jobs/[name]/history/route.ts +1 -1
- package/templates/starter-app/src/app/api/admin/system/jobs/route.ts +31 -12
- package/templates/starter-app/src/app/api/admin/system/settings/all/route.ts +2 -2
- package/templates/starter-app/src/app/api/admin/system/settings/create/route.ts +10 -3
- package/templates/starter-app/src/app/api/admin/system/settings/delete/route.ts +10 -3
- package/templates/starter-app/src/app/api/admin/system/settings/route.ts +14 -5
- package/templates/starter-app/src/app/api/admin/system/settings/toggle-status/route.ts +16 -7
- package/templates/starter-app/src/app/api/admin/system/settings/update/route.ts +12 -6
- package/templates/starter-app/src/app/api/admin/system/settings/update-full/route.ts +12 -6
- package/templates/starter-app/src/app/api/better-auth/[...all]/route.ts +1 -1
- package/templates/starter-app/src/app/api/branches/route.ts +26 -0
- package/templates/starter-app/src/app/api/crud/[entity]/[id]/route.ts +2 -2
- package/templates/starter-app/src/app/api/crud/[entity]/export/route.ts +16 -0
- package/templates/starter-app/src/app/api/crud/[entity]/import/route.ts +17 -0
- package/templates/starter-app/src/app/api/crud/[entity]/route.ts +2 -2
- package/templates/starter-app/src/app/api/departments/route.ts +26 -0
- package/templates/starter-app/src/app/api/error-logs/[id]/route.ts +4 -2
- package/templates/starter-app/src/app/api/error-logs/route.ts +26 -19
- package/templates/starter-app/src/app/api/files/[...key]/route.ts +1 -2
- package/templates/starter-app/src/app/api/job-titles/route.ts +46 -0
- package/templates/starter-app/src/app/api/notifications/read/route.ts +1 -1
- package/templates/starter-app/src/app/api/notifications/route.ts +1 -1
- package/templates/starter-app/src/app/api/notifications/unread-count/route.ts +1 -1
- package/templates/starter-app/src/app/api/rbac/permissions-version/route.ts +4 -1
- package/templates/starter-app/src/app/api/roles/[id]/route.ts +2 -1
- package/templates/starter-app/src/app/api/roles/route.ts +2 -1
- package/templates/starter-app/src/app/api/security/sessions/[id]/route.ts +52 -0
- package/templates/starter-app/src/app/api/security/sessions/revoke-user/route.ts +44 -0
- package/templates/starter-app/src/app/api/security/sessions/route.ts +101 -0
- package/templates/starter-app/src/app/api/suppliers/route.ts +13 -0
- package/templates/starter-app/src/app/api/system-categories/route.ts +179 -0
- package/templates/starter-app/src/app/api/system-category-groups/route.ts +149 -0
- package/templates/starter-app/src/app/api/tasks/[id]/download/route.ts +7 -3
- package/templates/starter-app/src/app/api/tasks/route.ts +5 -2
- package/templates/starter-app/src/app/api/upload/route.ts +1 -2
- package/templates/starter-app/src/app/api/users/[id]/route.ts +230 -0
- package/templates/starter-app/src/app/api/users/route.ts +141 -0
- package/templates/starter-app/src/app/global-error.tsx +135 -0
- package/templates/starter-app/src/app/globals.css +0 -1
- package/templates/starter-app/src/app/icon.svg +6 -0
- package/templates/starter-app/src/app/manifest.ts +26 -0
- package/templates/starter-app/src/components/layout/main-layout-wrapper.tsx +5 -4
- package/templates/starter-app/src/configs/entities/department.config.ts +7 -4
- package/templates/starter-app/src/configs/entities/index.ts +5 -1
- package/templates/starter-app/src/configs/entities/job-titles.config.ts +121 -0
- package/templates/starter-app/src/configs/entities/system-alerts.config.ts +93 -0
- package/templates/starter-app/src/configs/entities/users.config.ts +80 -0
- package/templates/starter-app/src/configs/i18n.ts +6 -6
- package/templates/starter-app/src/configs/permissions/index.ts +37 -0
- package/templates/starter-app/src/configs/permissions/master-data.permissions.ts +11 -0
- package/templates/starter-app/src/configs/permissions/menu-tree.ts +37 -0
- package/templates/starter-app/src/configs/permissions/system.permissions.ts +29 -0
- package/templates/starter-app/src/configs/tenant.ts +2 -1
- package/templates/starter-app/src/data/dictionary.ts +77 -4
- package/templates/starter-app/src/data/navigations.ts +60 -1
- package/templates/starter-app/src/instrumentation.ts +5 -16
- package/templates/starter-app/src/lib/action-guard.ts +25 -0
- package/templates/starter-app/src/lib/api-handler.ts +27 -19
- package/templates/starter-app/src/lib/auth.ts +3 -1
- package/templates/starter-app/src/lib/better-auth.ts +8 -0
- package/templates/starter-app/src/lib/branch-scope.ts +1 -1
- package/templates/starter-app/src/lib/crud/index.ts +5 -10
- package/templates/starter-app/src/lib/errors/log-server-error.ts +5 -4
- package/templates/starter-app/src/lib/logger.ts +11 -4
- package/templates/starter-app/src/lib/page-guard.ts +2 -3
- package/templates/starter-app/src/lib/prisma.ts +16 -7
- package/templates/starter-app/src/lib/rbac/access.ts +2 -2
- package/templates/starter-app/src/lib/storage.ts +3 -1
- package/templates/starter-app/src/providers/index.tsx +2 -1
- package/templates/starter-app/src/providers/mode-provider.tsx +13 -18
- package/templates/starter-app/src/providers/theme-provider.tsx +27 -12
- package/templates/starter-app/src/proxy.ts +8 -3
- package/templates/starter-app/src/server/services/company-service.ts +49 -0
- package/templates/starter-app/src/server/services/notification-service.ts +6 -6
- package/templates/starter-app/src/server/services/system-config-service.ts +21 -9
- package/templates/starter-app/src/server/services/user-service.ts +104 -0
- package/templates/starter-app/src/server/tasks/handlers/export-departments.ts +15 -9
- package/templates/starter-app/src/server/tasks/task-runner.ts +6 -7
- package/templates/starter-app/tsconfig.json +10 -0
- 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,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, {
|
|
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
|
)
|
|
@@ -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(
|
|
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) => [
|
|
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(
|
|
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, {
|
|
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(
|
|
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(
|
|
87
|
+
return NextResponse.json(
|
|
88
|
+
{ error: `Không tìm thấy job "${name}"` },
|
|
89
|
+
{ status: 404 }
|
|
90
|
+
)
|
|
78
91
|
}
|
|
79
|
-
return NextResponse.json({
|
|
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({
|
|
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)
|
|
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)
|
|
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({
|
|
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({
|
|
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(
|
|
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:
|
|
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)
|
|
24
|
-
|
|
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)
|
|
20
|
-
|
|
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)
|
|
19
|
-
|
|
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
|
)
|
|
@@ -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
|
+
})
|