@goplusvn/core 0.1.70 → 0.1.71
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 +1 -1
- package/src/auth/proxy-gate.ts +21 -2
- 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-table.tsx +13 -1
- package/src/crud/lib/translate-config.ts +16 -2
- package/src/guardrails/index.ts +1 -0
- package/src/guardrails/rules/auth.ts +54 -1
- 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/ui/auth/sign-in-form.tsx +64 -9
- 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/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/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/src/__tests__/architecture.test.ts +8 -0
- 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 +69 -0
- package/templates/starter-app/src/app/[lang]/(main)/admin/system/company/actions.ts +31 -0
- package/templates/starter-app/src/app/[lang]/(main)/admin/system/company/company-profile-form.tsx +184 -0
- package/templates/starter-app/src/app/[lang]/(main)/admin/system/company/page.tsx +28 -0
- package/templates/starter-app/src/app/[lang]/(main)/error.tsx +24 -0
- package/templates/starter-app/src/app/[lang]/(main)/not-found.tsx +6 -0
- package/templates/starter-app/src/app/[lang]/(main)/page.tsx +10 -0
- package/templates/starter-app/src/app/[lang]/(main)/resources/page.tsx +39 -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 +60 -0
- package/templates/starter-app/src/app/[lang]/(main)/user/profile/page.tsx +39 -0
- package/templates/starter-app/src/app/[lang]/(main)/user/profile/profile-client-page.tsx +149 -0
- package/templates/starter-app/src/app/[lang]/(main)/users/page.tsx +68 -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]/not-found.tsx +9 -0
- package/templates/starter-app/src/app/api/branches/route.ts +26 -0
- package/templates/starter-app/src/app/api/departments/route.ts +26 -0
- package/templates/starter-app/src/app/api/job-titles/route.ts +46 -0
- 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 +154 -0
- package/templates/starter-app/src/app/api/system-category-groups/route.ts +131 -0
- package/templates/starter-app/src/app/api/users/[id]/route.ts +222 -0
- package/templates/starter-app/src/app/api/users/route.ts +139 -0
- package/templates/starter-app/src/app/global-error.tsx +135 -0
- 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/configs/entities/index.ts +5 -1
- package/templates/starter-app/src/configs/entities/job-titles.config.ts +119 -0
- package/templates/starter-app/src/configs/entities/system-alerts.config.ts +91 -0
- package/templates/starter-app/src/configs/entities/users.config.ts +67 -0
- 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 +76 -4
- package/templates/starter-app/src/data/navigations.ts +49 -0
- package/templates/starter-app/src/instrumentation.ts +5 -16
- package/templates/starter-app/src/lib/action-guard.ts +26 -0
- package/templates/starter-app/src/lib/better-auth.ts +8 -0
- package/templates/starter-app/src/lib/crud/index.ts +2 -0
- package/templates/starter-app/src/lib/prisma.ts +3 -2
- package/templates/starter-app/src/providers/mode-provider.tsx +13 -18
- package/templates/starter-app/src/providers/theme-provider.tsx +27 -13
- package/templates/starter-app/src/proxy.ts +8 -3
- package/templates/starter-app/src/server/services/company-service.ts +47 -0
- package/templates/starter-app/src/server/services/user-service.ts +104 -0
- package/templates/starter-app/tsconfig.json +2 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { NextResponse } from "next/server"
|
|
2
|
+
import bcrypt from "bcryptjs"
|
|
3
|
+
|
|
4
|
+
import { apiHandler } from "@/lib/api-handler"
|
|
5
|
+
import { serverError } from "@/lib/errors/server-error"
|
|
6
|
+
import { db } from "@/lib/prisma"
|
|
7
|
+
import { getUsersList } from "@/server/services/user-service"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* /api/users — danh sách + tạo người dùng.
|
|
11
|
+
*
|
|
12
|
+
* Đơn giản hóa so với vinhhoa: không có profile (phòng ban/chức danh/SĐT),
|
|
13
|
+
* không sync better-auth riêng — mật khẩu nằm ở Account providerId="credential"
|
|
14
|
+
* (nơi DUY NHẤT Better Auth đọc, xem prisma/schema/auth.prisma). Các field
|
|
15
|
+
* vinhhoa-specific mà UnifiedProfileDialog của core vẫn gửi lên (phone,
|
|
16
|
+
* address, departmentId, jobTitleId, supplierId, userType…) được BỎ QUA.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export const GET = apiHandler(
|
|
20
|
+
async (req) => {
|
|
21
|
+
try {
|
|
22
|
+
const sp = new URL(req.url).searchParams
|
|
23
|
+
const result = await getUsersList({
|
|
24
|
+
search: sp.get("search")?.trim() || sp.get("q")?.trim() || undefined,
|
|
25
|
+
roleCode: sp.get("role")?.trim() || undefined,
|
|
26
|
+
status: sp.get("status")?.trim() || undefined,
|
|
27
|
+
})
|
|
28
|
+
return NextResponse.json(result)
|
|
29
|
+
} catch (error) {
|
|
30
|
+
return serverError(error, req, { message: "Lỗi tải danh sách người dùng" })
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
{ resource: "user", action: "view" },
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
export const POST = apiHandler(
|
|
37
|
+
async (req) => {
|
|
38
|
+
try {
|
|
39
|
+
const body = await req.json()
|
|
40
|
+
const name = typeof body.name === "string" ? body.name.trim() : ""
|
|
41
|
+
const email =
|
|
42
|
+
typeof body.email === "string" ? body.email.toLowerCase().trim() : ""
|
|
43
|
+
const password = typeof body.password === "string" ? body.password : ""
|
|
44
|
+
const isActive = body.status ? body.status === "active" : true
|
|
45
|
+
const roleCodes: string[] = Array.isArray(body.roleCodes)
|
|
46
|
+
? body.roleCodes
|
|
47
|
+
: []
|
|
48
|
+
const branchIds: string[] = Array.isArray(body.branchIds)
|
|
49
|
+
? body.branchIds
|
|
50
|
+
: []
|
|
51
|
+
|
|
52
|
+
if (!name) {
|
|
53
|
+
return NextResponse.json(
|
|
54
|
+
{ error: "Tên là bắt buộc", field: "name" },
|
|
55
|
+
{ status: 400 },
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
// Template dùng email làm định danh đăng nhập (User.email là unique).
|
|
59
|
+
if (!email) {
|
|
60
|
+
return NextResponse.json(
|
|
61
|
+
{ error: "Email là bắt buộc", field: "email" },
|
|
62
|
+
{ status: 400 },
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
if (password && password.length < 6) {
|
|
66
|
+
return NextResponse.json(
|
|
67
|
+
{ error: "Mật khẩu phải có ít nhất 6 ký tự", field: "password" },
|
|
68
|
+
{ status: 400 },
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const existing = await db.user.findUnique({ where: { email } })
|
|
73
|
+
if (existing) {
|
|
74
|
+
return NextResponse.json(
|
|
75
|
+
{ error: "Email đã tồn tại", field: "email" },
|
|
76
|
+
{ status: 400 },
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const hashedPassword = password ? await bcrypt.hash(password, 10) : null
|
|
81
|
+
|
|
82
|
+
const created = await db.$transaction(async (tx) => {
|
|
83
|
+
const user = await tx.user.create({
|
|
84
|
+
data: { name, email, isActive, emailVerified: true },
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
// Không có mật khẩu = không có Account credential = không đăng nhập
|
|
88
|
+
// được (vẫn tạo được để gán vai trò trước).
|
|
89
|
+
if (hashedPassword) {
|
|
90
|
+
await tx.account.create({
|
|
91
|
+
data: {
|
|
92
|
+
userId: user.id,
|
|
93
|
+
providerId: "credential",
|
|
94
|
+
accountId: user.id,
|
|
95
|
+
password: hashedPassword,
|
|
96
|
+
},
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (roleCodes.length > 0) {
|
|
101
|
+
await tx.userRole.createMany({
|
|
102
|
+
data: roleCodes.map((roleCode, index) => ({
|
|
103
|
+
userId: user.id,
|
|
104
|
+
roleCode,
|
|
105
|
+
isPrimary: index === 0,
|
|
106
|
+
})),
|
|
107
|
+
skipDuplicates: true,
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (branchIds.length > 0) {
|
|
112
|
+
const defaultBranchId =
|
|
113
|
+
typeof body.defaultBranchId === "string" &&
|
|
114
|
+
branchIds.includes(body.defaultBranchId)
|
|
115
|
+
? body.defaultBranchId
|
|
116
|
+
: branchIds[0]
|
|
117
|
+
await tx.userBranch.createMany({
|
|
118
|
+
data: branchIds.map((branchId) => ({
|
|
119
|
+
userId: user.id,
|
|
120
|
+
branchId,
|
|
121
|
+
isDefault: branchId === defaultBranchId,
|
|
122
|
+
})),
|
|
123
|
+
skipDuplicates: true,
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return user
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
return NextResponse.json(
|
|
131
|
+
{ id: created.id, name: created.name, email: created.email },
|
|
132
|
+
{ status: 201 },
|
|
133
|
+
)
|
|
134
|
+
} catch (error) {
|
|
135
|
+
return serverError(error, req, { message: "Lỗi tạo người dùng" })
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
{ resource: "user", action: "create" },
|
|
139
|
+
)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lưới an toàn CUỐI CÙNG — bắt lỗi ném từ chính root layout
|
|
5
|
+
* (`app/[lang]/layout.tsx`, nơi gọi `getSession` + `getSystemCompany` nên một
|
|
6
|
+
* cú nghẽn DB là đủ để cả app sập).
|
|
7
|
+
*
|
|
8
|
+
* PHẢI nằm ở `app/global-error.tsx`. Next chỉ đọc convention này ở GỐC cây
|
|
9
|
+
* route (`create-component-tree.tsx`: `isRoot ? getConventionPathByType(…,
|
|
10
|
+
* 'global-error') : undefined`) — bản cũ đặt ở `app/[lang]/global-error.tsx`
|
|
11
|
+
* chưa từng được nạp, nên mọi lần app sập người dùng chỉ thấy trang trắng với
|
|
12
|
+
* dòng "Application error: a server-side exception has occurred".
|
|
13
|
+
*
|
|
14
|
+
* Cố ý KHÔNG import `globals.css`, `@goerp/core/ui` hay bất cứ thứ gì khác:
|
|
15
|
+
* global-error thay luôn root layout, và nó thường chạy đúng lúc app đang hỏng
|
|
16
|
+
* (chunk lỗi, CSS chưa tải, provider chết). Style nội tuyến + thẻ HTML thuần là
|
|
17
|
+
* thứ duy nhất chắc chắn hiển thị được trong hoàn cảnh đó. Đừng "dọn cho đẹp"
|
|
18
|
+
* bằng cách kéo design system vào đây.
|
|
19
|
+
*/
|
|
20
|
+
export default function GlobalError({
|
|
21
|
+
error,
|
|
22
|
+
reset,
|
|
23
|
+
}: {
|
|
24
|
+
error: Error & { digest?: string }
|
|
25
|
+
reset: () => void
|
|
26
|
+
}) {
|
|
27
|
+
return (
|
|
28
|
+
<html lang="vi">
|
|
29
|
+
<body
|
|
30
|
+
style={{
|
|
31
|
+
margin: 0,
|
|
32
|
+
minHeight: "100vh",
|
|
33
|
+
display: "flex",
|
|
34
|
+
alignItems: "center",
|
|
35
|
+
justifyContent: "center",
|
|
36
|
+
padding: "16px",
|
|
37
|
+
background: "#f8fafc",
|
|
38
|
+
color: "#0f172a",
|
|
39
|
+
fontFamily:
|
|
40
|
+
'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif',
|
|
41
|
+
}}
|
|
42
|
+
>
|
|
43
|
+
<div
|
|
44
|
+
style={{
|
|
45
|
+
width: "100%",
|
|
46
|
+
maxWidth: "480px",
|
|
47
|
+
background: "#ffffff",
|
|
48
|
+
border: "1px solid #e2e8f0",
|
|
49
|
+
borderRadius: "8px",
|
|
50
|
+
padding: "24px",
|
|
51
|
+
boxShadow: "0 1px 3px rgba(15, 23, 42, 0.08)",
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
<h1
|
|
55
|
+
style={{
|
|
56
|
+
margin: "0 0 12px",
|
|
57
|
+
fontSize: "18px",
|
|
58
|
+
fontWeight: 700,
|
|
59
|
+
color: "#b91c1c",
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
Hệ thống đang gặp sự cố
|
|
63
|
+
</h1>
|
|
64
|
+
<p
|
|
65
|
+
style={{
|
|
66
|
+
margin: "0 0 16px",
|
|
67
|
+
fontSize: "14px",
|
|
68
|
+
lineHeight: 1.6,
|
|
69
|
+
color: "#475569",
|
|
70
|
+
}}
|
|
71
|
+
>
|
|
72
|
+
Không dựng được giao diện. Dữ liệu của bạn vẫn an toàn — đây là lỗi
|
|
73
|
+
kỹ thuật tạm thời. Vui lòng thử lại; nếu vẫn lỗi, gửi mã bên dưới
|
|
74
|
+
cho quản trị viên.
|
|
75
|
+
</p>
|
|
76
|
+
|
|
77
|
+
{error.digest ? (
|
|
78
|
+
<p
|
|
79
|
+
style={{
|
|
80
|
+
margin: "0 0 16px",
|
|
81
|
+
padding: "8px 12px",
|
|
82
|
+
borderRadius: "6px",
|
|
83
|
+
background: "#f1f5f9",
|
|
84
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
85
|
+
fontSize: "12px",
|
|
86
|
+
color: "#334155",
|
|
87
|
+
wordBreak: "break-all",
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
Mã lỗi: {error.digest}
|
|
91
|
+
</p>
|
|
92
|
+
) : null}
|
|
93
|
+
|
|
94
|
+
<div style={{ display: "flex", flexWrap: "wrap", gap: "8px" }}>
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
onClick={() => reset()}
|
|
98
|
+
style={{
|
|
99
|
+
padding: "8px 16px",
|
|
100
|
+
borderRadius: "6px",
|
|
101
|
+
border: "none",
|
|
102
|
+
background: "#4f46e5",
|
|
103
|
+
color: "#ffffff",
|
|
104
|
+
fontSize: "14px",
|
|
105
|
+
fontWeight: 500,
|
|
106
|
+
cursor: "pointer",
|
|
107
|
+
}}
|
|
108
|
+
>
|
|
109
|
+
Thử lại
|
|
110
|
+
</button>
|
|
111
|
+
{/* CỐ Ý dùng <a> chứ không phải next/link: ở đây app đang hỏng, và
|
|
112
|
+
điều hướng client-side sẽ giữ nguyên cái cây React đã chết. Tải
|
|
113
|
+
lại cả tài liệu mới thật sự thoát được. */}
|
|
114
|
+
{/* eslint-disable-next-line @next/next/no-html-link-for-pages */}
|
|
115
|
+
<a
|
|
116
|
+
href="/"
|
|
117
|
+
style={{
|
|
118
|
+
padding: "8px 16px",
|
|
119
|
+
borderRadius: "6px",
|
|
120
|
+
border: "1px solid #cbd5e1",
|
|
121
|
+
background: "#ffffff",
|
|
122
|
+
color: "#334155",
|
|
123
|
+
fontSize: "14px",
|
|
124
|
+
fontWeight: 500,
|
|
125
|
+
textDecoration: "none",
|
|
126
|
+
}}
|
|
127
|
+
>
|
|
128
|
+
Về trang chủ
|
|
129
|
+
</a>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
</body>
|
|
133
|
+
</html>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
|
2
|
+
<!-- Favicon mặc định của starter-app — thay bằng icon thương hiệu của bạn.
|
|
3
|
+
Đặt ở src/app/icon.svg là Next tự phục vụ /icon (App Router metadata). -->
|
|
4
|
+
<rect width="64" height="64" rx="14" fill="#4f46e5"/>
|
|
5
|
+
<text x="32" y="43" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, sans-serif" font-size="34" font-weight="700" fill="#ffffff">G</text>
|
|
6
|
+
</svg>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { MetadataRoute } from "next"
|
|
2
|
+
|
|
3
|
+
import { tenant } from "@/configs/tenant"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Web App Manifest — mức PWA "cài được lên màn hình chính" (Add to Home
|
|
7
|
+
* Screen), sinh động theo tenant config. Offline/service-worker CHƯA nằm ở
|
|
8
|
+
* đây — app cần chạy không mạng (nhà máy sóng yếu) thì thêm serwist ở tầng
|
|
9
|
+
* app, đó là quyết định riêng từng dự án.
|
|
10
|
+
*/
|
|
11
|
+
export default function manifest(): MetadataRoute.Manifest {
|
|
12
|
+
const name = tenant.branding?.companyName ?? tenant.name
|
|
13
|
+
return {
|
|
14
|
+
name,
|
|
15
|
+
short_name: tenant.name,
|
|
16
|
+
description: tenant.branding?.tagline || name,
|
|
17
|
+
start_url: "/",
|
|
18
|
+
display: "standalone",
|
|
19
|
+
background_color: "#ffffff",
|
|
20
|
+
theme_color: "#4f46e5",
|
|
21
|
+
icons: [
|
|
22
|
+
// Next tự phục vụ src/app/icon.svg tại /icon — thay icon là đổi luôn ở đây.
|
|
23
|
+
{ src: "/icon", sizes: "any", type: "image/svg+xml", purpose: "any" },
|
|
24
|
+
],
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import type { EntityConfig } from "@goerp/core/types"
|
|
2
2
|
|
|
3
3
|
import { departmentConfig } from "./department.config"
|
|
4
|
+
import { jobTitlesConfig } from "./job-titles.config"
|
|
5
|
+
import { systemAlertsConfig } from "./system-alerts.config"
|
|
4
6
|
|
|
5
7
|
// Đăng ký entity → cấu hình. Khóa = số nhiều, khớp URL /crud/<key> + apiEndpoint.
|
|
6
|
-
// Thêm entity mới: import config rồi thêm 1 dòng ở đây.
|
|
8
|
+
// Thêm entity mới: import config rồi thêm 1 dòng ở đây (kèm MODEL_MAP ở src/lib/crud).
|
|
7
9
|
export const entityConfigs: Record<string, EntityConfig> = {
|
|
8
10
|
departments: departmentConfig,
|
|
11
|
+
"job-titles": jobTitlesConfig,
|
|
12
|
+
"system-alerts": systemAlertsConfig,
|
|
9
13
|
}
|
|
10
14
|
|
|
11
15
|
export function getEntityConfig(entity: string): EntityConfig | undefined {
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { Briefcase } from "lucide-react"
|
|
2
|
+
|
|
3
|
+
import type { EntityConfig } from "@goerp/core/types"
|
|
4
|
+
|
|
5
|
+
/** Chức danh trong tổ chức — entity CRUD generic. URL: /crud/job-titles. */
|
|
6
|
+
export const jobTitlesConfig: EntityConfig = {
|
|
7
|
+
name: "jobTitle",
|
|
8
|
+
permissionResource: "job-title",
|
|
9
|
+
label: "Chức danh",
|
|
10
|
+
pluralLabel: "Chức danh",
|
|
11
|
+
icon: Briefcase,
|
|
12
|
+
description: "Quản lý danh sách chức danh trong tổ chức",
|
|
13
|
+
apiEndpoint: "/api/crud/job-titles",
|
|
14
|
+
idField: "id",
|
|
15
|
+
displayField: "name",
|
|
16
|
+
|
|
17
|
+
fields: [
|
|
18
|
+
{
|
|
19
|
+
name: "id",
|
|
20
|
+
label: "ID",
|
|
21
|
+
type: "text",
|
|
22
|
+
hideInForm: true,
|
|
23
|
+
hideInTable: true,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: "code",
|
|
27
|
+
label: "Mã chức danh",
|
|
28
|
+
type: "text",
|
|
29
|
+
required: true,
|
|
30
|
+
placeholder: "VD: DIRECTOR, MANAGER, STAFF",
|
|
31
|
+
filterable: true,
|
|
32
|
+
width: 140,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "name",
|
|
36
|
+
label: "Tên chức danh",
|
|
37
|
+
type: "text",
|
|
38
|
+
required: true,
|
|
39
|
+
placeholder: "Nhập tên chức danh",
|
|
40
|
+
filterable: true,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "description",
|
|
44
|
+
label: "Mô tả",
|
|
45
|
+
type: "textarea",
|
|
46
|
+
placeholder: "Nhập mô tả chức danh",
|
|
47
|
+
hideInTable: true,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "level",
|
|
51
|
+
label: "Cấp bậc",
|
|
52
|
+
type: "number",
|
|
53
|
+
required: true,
|
|
54
|
+
defaultValue: 0,
|
|
55
|
+
placeholder: "Cấp bậc (số lớn = cao hơn)",
|
|
56
|
+
width: 100,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "status",
|
|
60
|
+
label: "Trạng thái",
|
|
61
|
+
type: "switch",
|
|
62
|
+
required: true,
|
|
63
|
+
defaultValue: "active",
|
|
64
|
+
filterable: true,
|
|
65
|
+
options: [
|
|
66
|
+
{ label: "Hoạt động", value: "active" },
|
|
67
|
+
{ label: "Vô hiệu hóa", value: "inactive" },
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "createdAt",
|
|
72
|
+
label: "Ngày tạo",
|
|
73
|
+
type: "datetime",
|
|
74
|
+
hideInForm: true,
|
|
75
|
+
width: 120,
|
|
76
|
+
renderCell: (value) =>
|
|
77
|
+
new Date(value as string).toLocaleDateString("vi-VN", {
|
|
78
|
+
timeZone: "Asia/Ho_Chi_Minh",
|
|
79
|
+
}),
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
|
|
83
|
+
filters: [
|
|
84
|
+
{
|
|
85
|
+
name: "search",
|
|
86
|
+
label: "Tìm kiếm",
|
|
87
|
+
type: "text",
|
|
88
|
+
field: "name",
|
|
89
|
+
operator: "contains",
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: "status",
|
|
93
|
+
label: "Trạng thái",
|
|
94
|
+
type: "select",
|
|
95
|
+
field: "status",
|
|
96
|
+
operator: "eq",
|
|
97
|
+
options: [
|
|
98
|
+
{ label: "Hoạt động", value: "active" },
|
|
99
|
+
{ label: "Vô hiệu hóa", value: "inactive" },
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
|
|
104
|
+
permissions: {
|
|
105
|
+
create: true,
|
|
106
|
+
read: true,
|
|
107
|
+
update: true,
|
|
108
|
+
delete: true,
|
|
109
|
+
export: true,
|
|
110
|
+
import: true,
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
features: {
|
|
114
|
+
search: true,
|
|
115
|
+
bulkActions: true,
|
|
116
|
+
export: true,
|
|
117
|
+
import: true,
|
|
118
|
+
},
|
|
119
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Bell } from "lucide-react"
|
|
2
|
+
|
|
3
|
+
import type { EntityConfig } from "@goerp/core/types"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Cảnh báo hệ thống — ví dụ entity CRUD generic viết config TỪ ĐẦU (department
|
|
7
|
+
* là ví dụ kế thừa config core). URL: /crud/system-alerts.
|
|
8
|
+
*/
|
|
9
|
+
export const systemAlertsConfig: EntityConfig = {
|
|
10
|
+
name: "system-alert",
|
|
11
|
+
permissionResource: "system-alert",
|
|
12
|
+
label: "Cảnh báo hệ thống",
|
|
13
|
+
pluralLabel: "Cảnh báo hệ thống",
|
|
14
|
+
icon: Bell,
|
|
15
|
+
description: "Quản lý các thông báo và cảnh báo hệ thống",
|
|
16
|
+
apiEndpoint: "/api/crud/system-alerts",
|
|
17
|
+
idField: "id",
|
|
18
|
+
displayField: "title",
|
|
19
|
+
|
|
20
|
+
fields: [
|
|
21
|
+
{
|
|
22
|
+
name: "id",
|
|
23
|
+
label: "ID",
|
|
24
|
+
type: "text",
|
|
25
|
+
hideInForm: true,
|
|
26
|
+
hideInTable: true,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "title",
|
|
30
|
+
label: "Tiêu đề",
|
|
31
|
+
type: "text",
|
|
32
|
+
required: true,
|
|
33
|
+
filterable: true,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: "message",
|
|
37
|
+
label: "Nội dung",
|
|
38
|
+
type: "textarea",
|
|
39
|
+
required: true,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: "type",
|
|
43
|
+
label: "Loại",
|
|
44
|
+
type: "select",
|
|
45
|
+
filterable: true,
|
|
46
|
+
defaultValue: "info",
|
|
47
|
+
options: [
|
|
48
|
+
{ label: "Thông tin", value: "info" },
|
|
49
|
+
{ label: "Cảnh báo", value: "warning" },
|
|
50
|
+
{ label: "Lỗi", value: "error" },
|
|
51
|
+
{ label: "Thành công", value: "success" },
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "status",
|
|
56
|
+
label: "Trạng thái",
|
|
57
|
+
type: "select",
|
|
58
|
+
filterable: true,
|
|
59
|
+
defaultValue: "pending",
|
|
60
|
+
options: [
|
|
61
|
+
{ label: "Chờ gửi", value: "pending" },
|
|
62
|
+
{ label: "Đã gửi", value: "sent" },
|
|
63
|
+
{ label: "Đã đọc", value: "read" },
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "createdAt",
|
|
68
|
+
label: "Ngày tạo",
|
|
69
|
+
type: "datetime",
|
|
70
|
+
hideInForm: true,
|
|
71
|
+
renderCell: (value) =>
|
|
72
|
+
new Date(value as string).toLocaleString("vi-VN", {
|
|
73
|
+
timeZone: "Asia/Ho_Chi_Minh",
|
|
74
|
+
}),
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
|
|
78
|
+
permissions: {
|
|
79
|
+
create: true,
|
|
80
|
+
read: true,
|
|
81
|
+
update: true,
|
|
82
|
+
delete: true,
|
|
83
|
+
export: true,
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
features: {
|
|
87
|
+
search: true,
|
|
88
|
+
bulkActions: true,
|
|
89
|
+
export: true,
|
|
90
|
+
},
|
|
91
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { EntityConfig } from "@goerp/core/types"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cấu hình entity cho trang /users (UsersClientPage của core).
|
|
5
|
+
*
|
|
6
|
+
* KHÔNG đăng ký vào `entityConfigs` (src/configs/entities/index.ts): người
|
|
7
|
+
* dùng không đi qua CRUD engine generic (/crud/users) — tạo/sửa phải qua
|
|
8
|
+
* /api/users để còn hash mật khẩu vào Account credential + gán vai trò/chi
|
|
9
|
+
* nhánh trong transaction.
|
|
10
|
+
*
|
|
11
|
+
* Toàn bộ giá trị ở đây là PURE DATA (không icon component, không hàm) nên
|
|
12
|
+
* truyền thẳng từ server page xuống client không cần bước serialize như
|
|
13
|
+
* vinhhoa.
|
|
14
|
+
*/
|
|
15
|
+
export const usersConfig: EntityConfig = {
|
|
16
|
+
name: "user",
|
|
17
|
+
permissionResource: "user",
|
|
18
|
+
label: "Người dùng",
|
|
19
|
+
pluralLabel: "Người dùng",
|
|
20
|
+
iconName: "Users",
|
|
21
|
+
description: "Quản lý tài khoản đăng nhập, vai trò và chi nhánh",
|
|
22
|
+
apiEndpoint: "/api/users",
|
|
23
|
+
idField: "id",
|
|
24
|
+
displayField: "name",
|
|
25
|
+
|
|
26
|
+
fields: [
|
|
27
|
+
{ name: "id", label: "ID", type: "text", hideInForm: true, hideInTable: true },
|
|
28
|
+
{ name: "name", label: "Họ tên", type: "text", required: true },
|
|
29
|
+
{ name: "email", label: "Email", type: "email", required: true, width: 200 },
|
|
30
|
+
{
|
|
31
|
+
name: "roleNames",
|
|
32
|
+
label: "Vai trò",
|
|
33
|
+
type: "text",
|
|
34
|
+
hideInForm: true,
|
|
35
|
+
width: 150,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: "branchNames",
|
|
39
|
+
label: "Chi nhánh",
|
|
40
|
+
type: "text",
|
|
41
|
+
hideInForm: true,
|
|
42
|
+
width: 150,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "status",
|
|
46
|
+
label: "Trạng thái",
|
|
47
|
+
type: "select",
|
|
48
|
+
hideInForm: true,
|
|
49
|
+
options: [
|
|
50
|
+
{ label: "Hoạt động", value: "active" },
|
|
51
|
+
{ label: "Đã khóa", value: "inactive" },
|
|
52
|
+
],
|
|
53
|
+
width: 120,
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
|
|
57
|
+
permissions: {
|
|
58
|
+
create: true,
|
|
59
|
+
read: true,
|
|
60
|
+
update: true,
|
|
61
|
+
delete: true,
|
|
62
|
+
// Toolbar user của core export qua /api/crud/users/export — endpoint đó
|
|
63
|
+
// không tồn tại trong template (users không nằm trong CRUD engine).
|
|
64
|
+
export: false,
|
|
65
|
+
import: false,
|
|
66
|
+
},
|
|
67
|
+
}
|
|
@@ -43,3 +43,40 @@ export function getRegistryResourceCodes(): string[] {
|
|
|
43
43
|
export function isLookupOpenToAuthenticated(code: string): boolean {
|
|
44
44
|
return resourceByCode.get(code)?.lookupPolicy === "authenticated"
|
|
45
45
|
}
|
|
46
|
+
|
|
47
|
+
/** Metadata action cho trang phân quyền / catalog (nhãn tiếng Việt). */
|
|
48
|
+
export interface ActionMetaEntry {
|
|
49
|
+
label?: string
|
|
50
|
+
description?: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const BASE_ACTION_META: Record<string, ActionMetaEntry> = {
|
|
54
|
+
view: { label: "Xem" },
|
|
55
|
+
create: { label: "Tạo" },
|
|
56
|
+
update: { label: "Sửa" },
|
|
57
|
+
delete: { label: "Xóa" },
|
|
58
|
+
export: { label: "Xuất" },
|
|
59
|
+
import: { label: "Nhập" },
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let actionMetaCache: Record<string, ActionMetaEntry> | null = null
|
|
63
|
+
|
|
64
|
+
export function getActionMetaMap(): Record<string, ActionMetaEntry> {
|
|
65
|
+
if (actionMetaCache) return actionMetaCache
|
|
66
|
+
const map: Record<string, ActionMetaEntry> = { ...BASE_ACTION_META }
|
|
67
|
+
for (const feature of permissionRegistry) {
|
|
68
|
+
for (const a of feature.customActions ?? []) {
|
|
69
|
+
map[a.code] = { label: a.name, description: a.description }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
actionMetaCache = map
|
|
73
|
+
return map
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function getLookupPolicyMap(): Record<string, LookupPolicy> {
|
|
77
|
+
const map: Record<string, LookupPolicy> = {}
|
|
78
|
+
resourceByCode.forEach((r, code) => {
|
|
79
|
+
map[code] = r.lookupPolicy ?? "view-only"
|
|
80
|
+
})
|
|
81
|
+
return map
|
|
82
|
+
}
|
|
@@ -25,6 +25,17 @@ const masterDataPermissions: FeaturePermissions = {
|
|
|
25
25
|
staff: ["view", "export"],
|
|
26
26
|
},
|
|
27
27
|
},
|
|
28
|
+
{
|
|
29
|
+
code: "job-title",
|
|
30
|
+
name: "Chức danh",
|
|
31
|
+
group: "Danh mục",
|
|
32
|
+
icon: "Briefcase",
|
|
33
|
+
order: 2,
|
|
34
|
+
actions: ["view", "create", "update", "delete", "export", "import"],
|
|
35
|
+
// Chức danh đổ vào picker hồ sơ nhân sự.
|
|
36
|
+
lookupPolicy: "authenticated",
|
|
37
|
+
defaultGrants: { admin: "*", staff: ["view"] },
|
|
38
|
+
},
|
|
28
39
|
],
|
|
29
40
|
}
|
|
30
41
|
|