@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
|
@@ -10,8 +10,9 @@ import { Toaster } from "sonner"
|
|
|
10
10
|
import type { LocaleType } from "@goerp/core/types"
|
|
11
11
|
import type { ReactNode } from "react"
|
|
12
12
|
|
|
13
|
-
import { tenant } from "@/configs/tenant"
|
|
14
13
|
import { navigations } from "@/data/navigations"
|
|
14
|
+
|
|
15
|
+
import { tenant } from "@/configs/tenant"
|
|
15
16
|
import { authBridgeClient } from "@/lib/auth-client"
|
|
16
17
|
|
|
17
18
|
import { ModeProvider } from "./mode-provider"
|
|
@@ -1,31 +1,26 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import { useEffect } from "react"
|
|
4
|
+
import { useIsDarkMode } from "@goerp/core/hooks"
|
|
4
5
|
|
|
5
6
|
import type { ReactNode } from "react"
|
|
6
7
|
|
|
8
|
+
const MODES = ["light", "dark"]
|
|
9
|
+
|
|
7
10
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
+
* Áp chế độ Sáng/Tối/Hệ thống mà người dùng chọn trong Customizer (bánh răng).
|
|
12
|
+
* `useIsDarkMode()` của core đã resolve settings.mode (lưu cookie) + theo dõi
|
|
13
|
+
* prefers-color-scheme khi chọn "Hệ thống" — ở đây chỉ việc gắn class lên <html>.
|
|
11
14
|
*/
|
|
12
|
-
const MODES = ["light", "dark"] as const
|
|
13
|
-
|
|
14
15
|
export function ModeProvider({ children }: { children: ReactNode }) {
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
const isDarkMode = useIsDarkMode()
|
|
17
|
+
const mode = isDarkMode ? "dark" : "light"
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
apply(query.matches)
|
|
25
|
-
const onChange = (e: MediaQueryListEvent) => apply(e.matches)
|
|
26
|
-
query.addEventListener("change", onChange)
|
|
27
|
-
return () => query.removeEventListener("change", onChange)
|
|
28
|
-
}, [])
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const root = document.documentElement
|
|
21
|
+
root.classList.remove(...MODES)
|
|
22
|
+
root.classList.add(mode)
|
|
23
|
+
}, [mode])
|
|
29
24
|
|
|
30
25
|
return <>{children}</>
|
|
31
26
|
}
|
|
@@ -1,21 +1,36 @@
|
|
|
1
|
-
|
|
1
|
+
"use client"
|
|
2
2
|
|
|
3
|
-
import { useEffect } from
|
|
3
|
+
import { useEffect } from "react"
|
|
4
|
+
import { useSettings } from "@goerp/core/hooks"
|
|
4
5
|
|
|
5
|
-
import type { ReactNode } from
|
|
6
|
+
import type { ReactNode } from "react"
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
import {
|
|
9
|
+
THEME_PRESETS,
|
|
10
|
+
applyBrandThemeTokens,
|
|
11
|
+
} from "@goerp/core/providers/brand-theme"
|
|
9
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Áp màu thương hiệu người dùng chọn trong Customizer vào CSS token
|
|
15
|
+
* (--primary, --ring, --sidebar-background…). Bảng màu calibrate sẵn nằm ở
|
|
16
|
+
* core (`THEME_PRESETS`); app muốn nắn tay từng hue thì thay bằng bảng riêng
|
|
17
|
+
* (xem vinhhoa src/providers/theme-provider.tsx).
|
|
18
|
+
*
|
|
19
|
+
* Radius ("Bo góc") + density ("Mật độ") do SettingsProvider của core tự áp.
|
|
20
|
+
*/
|
|
10
21
|
export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
22
|
+
const { settings } = useSettings()
|
|
23
|
+
|
|
11
24
|
useEffect(() => {
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
.
|
|
15
|
-
.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
25
|
+
const theme = THEME_PRESETS[settings.theme] ?? THEME_PRESETS.blue
|
|
26
|
+
const isDark =
|
|
27
|
+
settings.mode === "dark" ||
|
|
28
|
+
(settings.mode === "system" &&
|
|
29
|
+
typeof window !== "undefined" &&
|
|
30
|
+
!!window.matchMedia?.("(prefers-color-scheme: dark)").matches)
|
|
31
|
+
|
|
32
|
+
applyBrandThemeTokens(document.documentElement, theme, isDark)
|
|
33
|
+
}, [settings.theme, settings.mode])
|
|
19
34
|
|
|
20
35
|
return <>{children}</>
|
|
21
36
|
}
|
|
@@ -2,6 +2,7 @@ import { createAuthProxy } from "@goerp/core/auth/proxy-gate"
|
|
|
2
2
|
import { getSessionCookie } from "better-auth/cookies"
|
|
3
3
|
|
|
4
4
|
import { i18n } from "@/configs/i18n"
|
|
5
|
+
import { tenant } from "@/configs/tenant"
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Cổng xác thực DEFAULT-DENY, dùng thẳng proxy-gate của core.
|
|
@@ -19,14 +20,18 @@ const BYPASS =
|
|
|
19
20
|
(process.env.BYPASS_AUTH === "true" || process.env.BYPASS_AUTH === "1")
|
|
20
21
|
|
|
21
22
|
export const proxy = createAuthProxy({
|
|
22
|
-
// Thêm prefix vào đây khi có endpoint tự xác thực
|
|
23
|
-
|
|
23
|
+
// Thêm prefix vào đây khi có endpoint tự xác thực — webhook, token khách,
|
|
24
|
+
// và cả đường NGOÀI /api (máy móc không có cookie kiểu /pub, /iclock của
|
|
25
|
+
// máy chấm công). Mỗi entry phải ghi rõ nó tự bảo vệ bằng gì; guardrail
|
|
26
|
+
// `auth/route-outside-api-declared` sẽ bắt route ngoài /api chưa khai ở đây.
|
|
27
|
+
publicPrefixes: ["/api/better-auth", "/api/public"],
|
|
24
28
|
publicPages: [...i18n.locales.map((l) => `/${l}/sign-in`), "/sign-in"],
|
|
25
29
|
signInPath: `/${i18n.defaultLocale}/sign-in`,
|
|
26
30
|
homePath: `/${i18n.defaultLocale}`,
|
|
31
|
+
// cookiePrefix PHẢI khớp advanced.cookiePrefix trong src/lib/better-auth.ts.
|
|
27
32
|
getToken: BYPASS
|
|
28
33
|
? async () => ({ dev: true })
|
|
29
|
-
: async (req) => getSessionCookie(req),
|
|
34
|
+
: async (req) => getSessionCookie(req, { cookiePrefix: tenant.id }),
|
|
30
35
|
})
|
|
31
36
|
|
|
32
37
|
export default proxy
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { cache } from "react"
|
|
2
|
+
|
|
3
|
+
import { tenant } from "@/configs/tenant"
|
|
4
|
+
import { db } from "@/lib/prisma"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Công ty "hệ thống" = dòng ĐẦU TIÊN của bảng companies. Chưa có dòng nào thì
|
|
8
|
+
* trả mặc định từ tenant config để biểu mẫu không trống — lưu lần đầu sẽ tạo.
|
|
9
|
+
*/
|
|
10
|
+
export const getSystemCompany = cache(async function getSystemCompany() {
|
|
11
|
+
const company = await db.company.findFirst({ orderBy: { createdAt: "asc" } })
|
|
12
|
+
if (company) return company
|
|
13
|
+
return {
|
|
14
|
+
id: null as string | null,
|
|
15
|
+
name: tenant.branding?.companyName ?? tenant.name,
|
|
16
|
+
taxCode: "",
|
|
17
|
+
address: "",
|
|
18
|
+
phone: "",
|
|
19
|
+
email: "",
|
|
20
|
+
logoUrl: null as string | null,
|
|
21
|
+
systemName: tenant.name,
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
export interface CompanyProfileInput {
|
|
26
|
+
name: string
|
|
27
|
+
taxCode: string
|
|
28
|
+
address: string
|
|
29
|
+
phone: string
|
|
30
|
+
email: string
|
|
31
|
+
logoUrl?: string | null
|
|
32
|
+
systemName?: string | null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function updateSystemCompany(
|
|
36
|
+
data: CompanyProfileInput,
|
|
37
|
+
updatedBy: string
|
|
38
|
+
) {
|
|
39
|
+
const existing = await db.company.findFirst({ orderBy: { createdAt: "asc" } })
|
|
40
|
+
if (existing) {
|
|
41
|
+
return db.company.update({
|
|
42
|
+
where: { id: existing.id },
|
|
43
|
+
data: { ...data, updatedBy },
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
return db.company.create({
|
|
47
|
+
data: { ...data, createdBy: updatedBy, updatedBy },
|
|
48
|
+
})
|
|
49
|
+
}
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import { configureNotificationService } from "@goerp/core/notification"
|
|
2
|
+
|
|
3
|
+
import type { NotificationDb } from "@goerp/core/notification"
|
|
4
|
+
|
|
5
|
+
import { db } from "@/lib/prisma"
|
|
6
|
+
|
|
1
7
|
/**
|
|
2
8
|
* Thông báo trong ứng dụng (chuông ở thanh trên). Engine ở
|
|
3
9
|
* @goerp/core/notification, bảng `notifications` + `push_subscriptions` ship
|
|
@@ -8,12 +14,6 @@
|
|
|
8
14
|
* push vào bundle. Core đã fire-and-forget + catch nên push lỗi không làm hỏng
|
|
9
15
|
* nghiệp vụ đang chạy.
|
|
10
16
|
*/
|
|
11
|
-
import {
|
|
12
|
-
configureNotificationService,
|
|
13
|
-
type NotificationDb,
|
|
14
|
-
} from "@goerp/core/notification"
|
|
15
|
-
|
|
16
|
-
import { db } from "@/lib/prisma"
|
|
17
17
|
|
|
18
18
|
configureNotificationService({
|
|
19
19
|
db: db as unknown as NotificationDb,
|
|
@@ -18,7 +18,7 @@ const MASK = "••••••••"
|
|
|
18
18
|
export class ConfigError extends Error {
|
|
19
19
|
constructor(
|
|
20
20
|
message: string,
|
|
21
|
-
readonly status: number
|
|
21
|
+
readonly status: number
|
|
22
22
|
) {
|
|
23
23
|
super(message)
|
|
24
24
|
}
|
|
@@ -29,14 +29,13 @@ export class ConfigError extends Error {
|
|
|
29
29
|
* (khi đó route phải để `serverError` ghi lại stack thật).
|
|
30
30
|
*/
|
|
31
31
|
export function configErrorInfo(
|
|
32
|
-
error: unknown
|
|
32
|
+
error: unknown
|
|
33
33
|
): { message: string; status: number } | null {
|
|
34
34
|
return error instanceof ConfigError
|
|
35
35
|
? { message: error.message, status: error.status }
|
|
36
36
|
: null
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
40
39
|
const mask = (config: any) => ({
|
|
41
40
|
...config,
|
|
42
41
|
value: config.isEncrypted ? MASK : config.value,
|
|
@@ -77,7 +76,7 @@ export async function createConfig(input: ConfigInput, userId: string) {
|
|
|
77
76
|
if (!/^[a-zA-Z0-9._-]+$/.test(key)) {
|
|
78
77
|
throw new ConfigError(
|
|
79
78
|
"Key chỉ được chứa chữ, số, dấu chấm, gạch dưới và gạch ngang",
|
|
80
|
-
400
|
|
79
|
+
400
|
|
81
80
|
)
|
|
82
81
|
}
|
|
83
82
|
if (await db.systemConfig.findUnique({ where: { key } })) {
|
|
@@ -103,8 +102,13 @@ export async function createConfig(input: ConfigInput, userId: string) {
|
|
|
103
102
|
}
|
|
104
103
|
|
|
105
104
|
/** Đổi MỖI giá trị — đường dùng nhiều nhất trên UI. */
|
|
106
|
-
export async function updateConfigValue(
|
|
107
|
-
|
|
105
|
+
export async function updateConfigValue(
|
|
106
|
+
key: string,
|
|
107
|
+
value: unknown,
|
|
108
|
+
userId: string
|
|
109
|
+
) {
|
|
110
|
+
if (!key || value === undefined)
|
|
111
|
+
throw new ConfigError("Thiếu key hoặc value", 400)
|
|
108
112
|
const existing = await requireConfig(key)
|
|
109
113
|
if (existing.isReadOnly) throw new ConfigError("Cấu hình này chỉ đọc", 403)
|
|
110
114
|
|
|
@@ -128,7 +132,10 @@ export async function updateConfigFull(input: ConfigInput, userId: string) {
|
|
|
128
132
|
value: input.value ?? existing.value,
|
|
129
133
|
type: input.type ?? existing.type,
|
|
130
134
|
category: input.category ?? existing.category,
|
|
131
|
-
description:
|
|
135
|
+
description:
|
|
136
|
+
input.description !== undefined
|
|
137
|
+
? input.description
|
|
138
|
+
: existing.description,
|
|
132
139
|
isEncrypted: input.isEncrypted ?? existing.isEncrypted,
|
|
133
140
|
isReadOnly: input.isReadOnly ?? existing.isReadOnly,
|
|
134
141
|
updatedBy: userId,
|
|
@@ -141,13 +148,18 @@ export async function updateConfigFull(input: ConfigInput, userId: string) {
|
|
|
141
148
|
export async function deleteConfig(key: string) {
|
|
142
149
|
if (!key) throw new ConfigError("Thiếu key", 400)
|
|
143
150
|
const existing = await requireConfig(key)
|
|
144
|
-
if (existing.isReadOnly)
|
|
151
|
+
if (existing.isReadOnly)
|
|
152
|
+
throw new ConfigError("Không thể xóa cấu hình được bảo vệ", 403)
|
|
145
153
|
|
|
146
154
|
await db.systemConfig.delete({ where: { key } })
|
|
147
155
|
invalidate()
|
|
148
156
|
}
|
|
149
157
|
|
|
150
|
-
export async function toggleConfigStatus(
|
|
158
|
+
export async function toggleConfigStatus(
|
|
159
|
+
key: string,
|
|
160
|
+
status: string,
|
|
161
|
+
userId: string
|
|
162
|
+
) {
|
|
151
163
|
if (!key) throw new ConfigError("Thiếu key", 400)
|
|
152
164
|
if (!["active", "inactive"].includes(status)) {
|
|
153
165
|
throw new ConfigError("status phải là 'active' hoặc 'inactive'", 400)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { Prisma } from "@prisma/client"
|
|
2
|
+
|
|
3
|
+
import { db } from "@/lib/prisma"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Danh sách người dùng cho trang /users và GET /api/users.
|
|
7
|
+
*
|
|
8
|
+
* CỐ Ý không dùng `getUsersData` của @goerp/core/user: bản đó select các cột
|
|
9
|
+
* vinhhoa-specific (`userType`, quan hệ `profiles`/`supplier`) không tồn tại
|
|
10
|
+
* trong schema template — Prisma sẽ ném PrismaClientValidationError ngay
|
|
11
|
+
* request đầu tiên. Query ở đây chỉ đụng User/UserRole/UserBranch của template
|
|
12
|
+
* và trả đúng shape mà UsersTable/UsersCardView/UnifiedProfileDialog của core
|
|
13
|
+
* đọc: `status` ("active"/"inactive"), `roleNames`/`roleCodes`,
|
|
14
|
+
* `branchIds`/`branchNames`, `defaultBranchId`.
|
|
15
|
+
* (`getUserStats`/`getActiveRoles` của core thì an toàn — chỗ đếm theo
|
|
16
|
+
* `userType` đã có `.catch(() => 0)`.)
|
|
17
|
+
*/
|
|
18
|
+
export interface UsersListParams {
|
|
19
|
+
search?: string
|
|
20
|
+
/** Lọc theo mã vai trò (Role.code). */
|
|
21
|
+
roleCode?: string
|
|
22
|
+
/** "active" | "inactive" — map sang cột isActive. */
|
|
23
|
+
status?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type Where = Prisma.UserWhereInput
|
|
27
|
+
|
|
28
|
+
export async function getUsersList(params: UsersListParams = {}) {
|
|
29
|
+
const { search, roleCode, status } = params
|
|
30
|
+
|
|
31
|
+
const where: Where = {}
|
|
32
|
+
if (search) {
|
|
33
|
+
where.OR = [
|
|
34
|
+
{ name: { contains: search, mode: "insensitive" } },
|
|
35
|
+
{ email: { contains: search, mode: "insensitive" } },
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
if (status === "active" || status === "inactive") {
|
|
39
|
+
where.isActive = status === "active"
|
|
40
|
+
}
|
|
41
|
+
if (roleCode) {
|
|
42
|
+
where.userRoles = { some: { roleCode } }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Trả TOÀN BỘ danh sách đã lọc: UsersClientPage của core tự phân trang phía
|
|
46
|
+
// client theo ?page&pageSize (danh sách người dùng nội bộ đủ nhỏ).
|
|
47
|
+
const [users, total] = await Promise.all([
|
|
48
|
+
db.user.findMany({
|
|
49
|
+
where,
|
|
50
|
+
orderBy: { createdAt: "desc" },
|
|
51
|
+
select: {
|
|
52
|
+
id: true,
|
|
53
|
+
name: true,
|
|
54
|
+
email: true,
|
|
55
|
+
image: true,
|
|
56
|
+
isActive: true,
|
|
57
|
+
lastLoginAt: true,
|
|
58
|
+
createdAt: true,
|
|
59
|
+
updatedAt: true,
|
|
60
|
+
userRoles: {
|
|
61
|
+
select: { role: { select: { code: true, name: true } } },
|
|
62
|
+
},
|
|
63
|
+
userBranches: {
|
|
64
|
+
select: {
|
|
65
|
+
isDefault: true,
|
|
66
|
+
branch: { select: { id: true, name: true } },
|
|
67
|
+
},
|
|
68
|
+
orderBy: [{ isDefault: "desc" }, { createdAt: "asc" }],
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
}),
|
|
72
|
+
db.user.count({ where }),
|
|
73
|
+
])
|
|
74
|
+
|
|
75
|
+
const data = users.map((user) => {
|
|
76
|
+
const branches = user.userBranches
|
|
77
|
+
.filter((ub) => ub.branch)
|
|
78
|
+
.map((ub) => ({
|
|
79
|
+
id: ub.branch.id,
|
|
80
|
+
name: ub.branch.name,
|
|
81
|
+
isDefault: ub.isDefault,
|
|
82
|
+
}))
|
|
83
|
+
return {
|
|
84
|
+
id: user.id,
|
|
85
|
+
name: user.name,
|
|
86
|
+
email: user.email,
|
|
87
|
+
image: user.image,
|
|
88
|
+
isActive: user.isActive,
|
|
89
|
+
status: user.isActive ? "active" : "inactive",
|
|
90
|
+
lastLoginAt: user.lastLoginAt,
|
|
91
|
+
createdAt: user.createdAt,
|
|
92
|
+
updatedAt: user.updatedAt,
|
|
93
|
+
roleNames: user.userRoles.map((ur) => ur.role.name),
|
|
94
|
+
roleCodes: user.userRoles.map((ur) => ur.role.code),
|
|
95
|
+
branches,
|
|
96
|
+
branchIds: branches.map((b) => b.id),
|
|
97
|
+
branchNames: branches.map((b) => b.name),
|
|
98
|
+
defaultBranchId:
|
|
99
|
+
branches.find((b) => b.isDefault)?.id ?? branches[0]?.id ?? null,
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
return { data, total }
|
|
104
|
+
}
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
* không lộ đường dẫn thật.
|
|
8
8
|
* 3. Ném lỗi thoải mái: engine bắt, ghi status=error kèm message và báo chuông.
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
10
|
+
import type { TaskContext, TaskFileResult } from "@goerp/core/tasks"
|
|
11
11
|
|
|
12
12
|
import { db } from "@/lib/prisma"
|
|
13
13
|
|
|
14
|
-
import
|
|
14
|
+
import { registerTaskHandler, saveTaskFile } from "../task-runner"
|
|
15
15
|
|
|
16
16
|
export const EXPORT_DEPARTMENTS_TASK = "export-departments"
|
|
17
17
|
|
|
@@ -28,7 +28,9 @@ registerTaskHandler(
|
|
|
28
28
|
EXPORT_DEPARTMENTS_TASK,
|
|
29
29
|
async ({ setProgress }: TaskContext): Promise<TaskFileResult> => {
|
|
30
30
|
const total = await db.department.count()
|
|
31
|
-
const rows: string[] = [
|
|
31
|
+
const rows: string[] = [
|
|
32
|
+
["Mã", "Tên", "Mô tả", "Thứ tự", "Trạng thái"].join(","),
|
|
33
|
+
]
|
|
32
34
|
|
|
33
35
|
for (let skip = 0; skip < total; skip += PAGE_SIZE) {
|
|
34
36
|
const page = await db.department.findMany({
|
|
@@ -38,21 +40,25 @@ registerTaskHandler(
|
|
|
38
40
|
})
|
|
39
41
|
for (const d of page) {
|
|
40
42
|
rows.push(
|
|
41
|
-
[d.code, d.name, d.description, d.order, d.status]
|
|
43
|
+
[d.code, d.name, d.description, d.order, d.status]
|
|
44
|
+
.map(csvCell)
|
|
45
|
+
.join(",")
|
|
42
46
|
)
|
|
43
47
|
}
|
|
44
|
-
await setProgress(
|
|
48
|
+
await setProgress(
|
|
49
|
+
Math.round(((skip + page.length) / Math.max(total, 1)) * 100)
|
|
50
|
+
)
|
|
45
51
|
}
|
|
46
52
|
|
|
47
|
-
// BOM
|
|
48
|
-
const buffer = Buffer.from(`
|
|
53
|
+
// BOM để Excel bản tiếng Việt không đọc UTF-8 thành ký tự lỗi.
|
|
54
|
+
const buffer = Buffer.from(` ${rows.join("\n")}`, "utf8")
|
|
49
55
|
const fileName = "phong-ban.csv"
|
|
50
56
|
const fileKey = await saveTaskFile(
|
|
51
57
|
buffer,
|
|
52
58
|
`${EXPORT_DEPARTMENTS_TASK}/${fileName}`,
|
|
53
|
-
"text/csv"
|
|
59
|
+
"text/csv"
|
|
54
60
|
)
|
|
55
61
|
|
|
56
62
|
return { fileKey, fileName, contentType: "text/csv", rowCount: total }
|
|
57
|
-
}
|
|
63
|
+
}
|
|
58
64
|
)
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import { configureTaskRunner } from "@goerp/core/tasks"
|
|
2
|
+
|
|
3
|
+
import type { TaskDb, TaskNotifyInput } from "@goerp/core/tasks"
|
|
4
|
+
|
|
5
|
+
import { db } from "@/lib/prisma"
|
|
6
|
+
|
|
1
7
|
/**
|
|
2
8
|
* Tác vụ nền — engine ở @goerp/core/tasks (bảng `background_tasks` ship qua
|
|
3
9
|
* `goerp-features sync`). Hàng đợi nằm trong DB, worker chạy NGAY trong tiến
|
|
@@ -7,13 +13,6 @@
|
|
|
7
13
|
* Đừng import file này ở nơi khác — import `@/server/tasks` để các handler
|
|
8
14
|
* được đăng ký trước khi có ai enqueue.
|
|
9
15
|
*/
|
|
10
|
-
import {
|
|
11
|
-
configureTaskRunner,
|
|
12
|
-
type TaskDb,
|
|
13
|
-
type TaskNotifyInput,
|
|
14
|
-
} from "@goerp/core/tasks"
|
|
15
|
-
|
|
16
|
-
import { db } from "@/lib/prisma"
|
|
17
16
|
|
|
18
17
|
configureTaskRunner({
|
|
19
18
|
db: db as unknown as TaskDb,
|
|
@@ -27,9 +27,19 @@
|
|
|
27
27
|
"./src/*"
|
|
28
28
|
],
|
|
29
29
|
"@goerp/core": [
|
|
30
|
+
"../../src",
|
|
30
31
|
"./node_modules/@goerp/core/src"
|
|
31
32
|
],
|
|
33
|
+
"@goerp/core/user/client-page": [
|
|
34
|
+
"../../src/user/pages/users-client-page.tsx",
|
|
35
|
+
"./node_modules/@goerp/core/src/user/pages/users-client-page.tsx"
|
|
36
|
+
],
|
|
37
|
+
"@goerp/core/tasks/ui": [
|
|
38
|
+
"../../src/tasks/ui/task-list-client.tsx",
|
|
39
|
+
"./node_modules/@goerp/core/src/tasks/ui/task-list-client.tsx"
|
|
40
|
+
],
|
|
32
41
|
"@goerp/core/*": [
|
|
42
|
+
"../../src/*",
|
|
33
43
|
"./node_modules/@goerp/core/src/*"
|
|
34
44
|
]
|
|
35
45
|
}
|
|
@@ -12,6 +12,22 @@ export default defineConfig({
|
|
|
12
12
|
},
|
|
13
13
|
resolve: {
|
|
14
14
|
// Phải khai lại alias của tsconfig: vitest không đọc tsconfig paths.
|
|
15
|
-
|
|
15
|
+
// @goerp/core trỏ THẲNG src của core local (../../src trong repo core) —
|
|
16
|
+
// thiếu là guardrails không resolve được, hoặc rơi về bản core CŨ trong
|
|
17
|
+
// node_modules → mixed-type khi core local đổi type. Hai subpath
|
|
18
|
+
// user/client-page và tasks/ui exports-map sang file không trùng đường
|
|
19
|
+
// dẫn src nên phải khai riêng, y như tsconfig.
|
|
20
|
+
alias: {
|
|
21
|
+
"@": path.resolve(__dirname, "./src"),
|
|
22
|
+
"@goerp/core/user/client-page": path.resolve(
|
|
23
|
+
__dirname,
|
|
24
|
+
"../../src/user/pages/users-client-page.tsx",
|
|
25
|
+
),
|
|
26
|
+
"@goerp/core/tasks/ui": path.resolve(
|
|
27
|
+
__dirname,
|
|
28
|
+
"../../src/tasks/ui/task-list-client.tsx",
|
|
29
|
+
),
|
|
30
|
+
"@goerp/core": path.resolve(__dirname, "../../src"),
|
|
31
|
+
},
|
|
16
32
|
},
|
|
17
33
|
})
|