@goplusvn/core 0.1.59 → 0.1.61

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/PLATFORM.md +18 -0
  2. package/bin/goerp-init.mjs +141 -0
  3. package/package.json +4 -4
  4. package/src/cron/db-cron-manager.ts +3 -3
  5. package/src/cron/index.ts +2 -2
  6. package/src/{infrastructure/cron/cron-manager.ts → cron/simple-cron-job.ts} +1 -1
  7. package/src/crud/lib/mutation-builder.ts +105 -0
  8. package/src/crud/lib/query-builder.ts +119 -0
  9. package/src/crud/server-service.ts +35 -163
  10. package/src/infrastructure/__tests__/architecture-verification.spec.ts +13 -97
  11. package/src/infrastructure/index.ts +4 -7
  12. package/src/ui/management/index.ts +3 -2
  13. package/templates/starter-app/.dockerignore +41 -0
  14. package/templates/starter-app/.env.example +25 -0
  15. package/templates/starter-app/AGENTS.md +52 -0
  16. package/templates/starter-app/Dockerfile +74 -0
  17. package/templates/starter-app/README.md +141 -0
  18. package/templates/starter-app/gitignore +9 -0
  19. package/templates/starter-app/next.config.mjs +50 -0
  20. package/templates/starter-app/package.json +55 -0
  21. package/templates/starter-app/postcss.config.mjs +5 -0
  22. package/templates/starter-app/prisma/migrations/20260801000000_init/migration.sql +504 -0
  23. package/templates/starter-app/prisma/migrations/20260801091814_goerp_audit_logs_0001_init/migration.sql +33 -0
  24. package/templates/starter-app/prisma/migrations/20260801091815_goerp_background_tasks_0001_init/migration.sql +23 -0
  25. package/templates/starter-app/prisma/migrations/20260801091816_goerp_error_logs_0001_init/migration.sql +32 -0
  26. package/templates/starter-app/prisma/migrations/20260801091817_goerp_notifications_0001_init/migration.sql +59 -0
  27. package/templates/starter-app/prisma/migrations/20260801091818_goerp_system_jobs_0001_init/migration.sql +47 -0
  28. package/templates/starter-app/prisma/schema/auth.prisma +87 -0
  29. package/templates/starter-app/prisma/schema/domain.prisma +24 -0
  30. package/templates/starter-app/prisma/schema/goerp-audit-logs.prisma +23 -0
  31. package/templates/starter-app/prisma/schema/goerp-background-tasks.prisma +25 -0
  32. package/templates/starter-app/prisma/schema/goerp-error-logs.prisma +31 -0
  33. package/templates/starter-app/prisma/schema/goerp-notifications.prisma +49 -0
  34. package/templates/starter-app/prisma/schema/goerp-system-jobs.prisma +45 -0
  35. package/templates/starter-app/prisma/schema/organization.prisma +31 -0
  36. package/templates/starter-app/prisma/schema/rbac.prisma +100 -0
  37. package/templates/starter-app/prisma/schema/schema.prisma +8 -0
  38. package/templates/starter-app/prisma/schema/system.prisma +22 -0
  39. package/templates/starter-app/prisma/seed.ts +127 -0
  40. package/templates/starter-app/prisma.config.ts +20 -0
  41. package/templates/starter-app/public/.gitkeep +2 -0
  42. package/templates/starter-app/scripts/rbac-sync.ts +235 -0
  43. package/templates/starter-app/src/__tests__/architecture.test.ts +151 -0
  44. package/templates/starter-app/src/app/[lang]/(main)/admin/system/audit/page.tsx +24 -0
  45. package/templates/starter-app/src/app/[lang]/(main)/admin/system/error-logs/page.tsx +21 -0
  46. package/templates/starter-app/src/app/[lang]/(main)/admin/system/jobs/page.tsx +18 -0
  47. package/templates/starter-app/src/app/[lang]/(main)/admin/system/settings/page.tsx +22 -0
  48. package/templates/starter-app/src/app/[lang]/(main)/crud/[entity]/page.tsx +41 -0
  49. package/templates/starter-app/src/app/[lang]/(main)/layout.tsx +13 -0
  50. package/templates/starter-app/src/app/[lang]/(main)/notifications/page.tsx +13 -0
  51. package/templates/starter-app/src/app/[lang]/(main)/page.tsx +130 -0
  52. package/templates/starter-app/src/app/[lang]/(main)/roles/[id]/edit/page.tsx +60 -0
  53. package/templates/starter-app/src/app/[lang]/(main)/roles/new/page.tsx +41 -0
  54. package/templates/starter-app/src/app/[lang]/(main)/roles/page.tsx +48 -0
  55. package/templates/starter-app/src/app/[lang]/(main)/tasks/page.tsx +13 -0
  56. package/templates/starter-app/src/app/[lang]/(plain)/layout.tsx +10 -0
  57. package/templates/starter-app/src/app/[lang]/(plain)/sign-in/page.tsx +39 -0
  58. package/templates/starter-app/src/app/[lang]/layout.tsx +21 -0
  59. package/templates/starter-app/src/app/api/admin/system/audit/route.ts +60 -0
  60. package/templates/starter-app/src/app/api/admin/system/jobs/[name]/history/route.ts +39 -0
  61. package/templates/starter-app/src/app/api/admin/system/jobs/route.ts +96 -0
  62. package/templates/starter-app/src/app/api/admin/system/settings/all/route.ts +17 -0
  63. package/templates/starter-app/src/app/api/admin/system/settings/create/route.ts +19 -0
  64. package/templates/starter-app/src/app/api/admin/system/settings/delete/route.ts +20 -0
  65. package/templates/starter-app/src/app/api/admin/system/settings/route.ts +49 -0
  66. package/templates/starter-app/src/app/api/admin/system/settings/toggle-status/route.ts +28 -0
  67. package/templates/starter-app/src/app/api/admin/system/settings/update/route.ts +24 -0
  68. package/templates/starter-app/src/app/api/admin/system/settings/update-full/route.ts +23 -0
  69. package/templates/starter-app/src/app/api/better-auth/[...all]/route.ts +13 -0
  70. package/templates/starter-app/src/app/api/crud/[entity]/[id]/route.ts +13 -0
  71. package/templates/starter-app/src/app/api/crud/[entity]/route.ts +14 -0
  72. package/templates/starter-app/src/app/api/error-logs/[id]/route.ts +23 -0
  73. package/templates/starter-app/src/app/api/error-logs/route.ts +124 -0
  74. package/templates/starter-app/src/app/api/files/[...key]/route.ts +25 -0
  75. package/templates/starter-app/src/app/api/notifications/read/route.ts +29 -0
  76. package/templates/starter-app/src/app/api/notifications/route.ts +26 -0
  77. package/templates/starter-app/src/app/api/notifications/unread-count/route.ts +14 -0
  78. package/templates/starter-app/src/app/api/rbac/permissions-version/route.ts +11 -0
  79. package/templates/starter-app/src/app/api/roles/[id]/route.ts +14 -0
  80. package/templates/starter-app/src/app/api/roles/route.ts +18 -0
  81. package/templates/starter-app/src/app/api/tasks/[id]/download/route.ts +52 -0
  82. package/templates/starter-app/src/app/api/tasks/route.ts +37 -0
  83. package/templates/starter-app/src/app/api/upload/route.ts +15 -0
  84. package/templates/starter-app/src/app/globals.css +15 -0
  85. package/templates/starter-app/src/app/layout.tsx +16 -0
  86. package/templates/starter-app/src/app/page.tsx +8 -0
  87. package/templates/starter-app/src/components/layout/main-layout-wrapper.tsx +30 -0
  88. package/templates/starter-app/src/configs/entities/department.config.ts +24 -0
  89. package/templates/starter-app/src/configs/entities/index.ts +13 -0
  90. package/templates/starter-app/src/configs/i18n.ts +12 -0
  91. package/templates/starter-app/src/configs/permissions/index.ts +45 -0
  92. package/templates/starter-app/src/configs/permissions/master-data.permissions.ts +31 -0
  93. package/templates/starter-app/src/configs/permissions/system.permissions.ts +131 -0
  94. package/templates/starter-app/src/configs/permissions/types.ts +63 -0
  95. package/templates/starter-app/src/configs/tenant.ts +18 -0
  96. package/templates/starter-app/src/data/dictionary.ts +8 -0
  97. package/templates/starter-app/src/data/navigations.ts +61 -0
  98. package/templates/starter-app/src/instrumentation.ts +105 -0
  99. package/templates/starter-app/src/lib/api-handler.ts +157 -0
  100. package/templates/starter-app/src/lib/auth-client.ts +57 -0
  101. package/templates/starter-app/src/lib/auth.ts +62 -0
  102. package/templates/starter-app/src/lib/better-auth.ts +107 -0
  103. package/templates/starter-app/src/lib/branch-scope.ts +53 -0
  104. package/templates/starter-app/src/lib/cron/db-cron-manager.ts +42 -0
  105. package/templates/starter-app/src/lib/crud/index.ts +13 -0
  106. package/templates/starter-app/src/lib/errors/app-error.ts +2 -0
  107. package/templates/starter-app/src/lib/errors/error-handler.ts +5 -0
  108. package/templates/starter-app/src/lib/errors/log-server-error.ts +15 -0
  109. package/templates/starter-app/src/lib/errors/server-error.ts +11 -0
  110. package/templates/starter-app/src/lib/logger.ts +30 -0
  111. package/templates/starter-app/src/lib/page-guard.ts +35 -0
  112. package/templates/starter-app/src/lib/prisma.ts +80 -0
  113. package/templates/starter-app/src/lib/rbac/access.ts +87 -0
  114. package/templates/starter-app/src/lib/storage.ts +28 -0
  115. package/templates/starter-app/src/providers/index.tsx +54 -0
  116. package/templates/starter-app/src/providers/mode-provider.tsx +31 -0
  117. package/templates/starter-app/src/providers/theme-provider.tsx +21 -0
  118. package/templates/starter-app/src/proxy.ts +45 -0
  119. package/templates/starter-app/src/server/services/notification-service.ts +31 -0
  120. package/templates/starter-app/src/server/services/system-config-service.ts +163 -0
  121. package/templates/starter-app/src/server/tasks/handlers/export-departments.ts +58 -0
  122. package/templates/starter-app/src/server/tasks/index.ts +16 -0
  123. package/templates/starter-app/src/server/tasks/task-runner.ts +40 -0
  124. package/templates/starter-app/src/types/session.ts +29 -0
  125. package/templates/starter-app/tsconfig.json +47 -0
  126. package/templates/starter-app/vitest.config.ts +17 -0
  127. package/src/infrastructure/cron/index.ts +0 -6
  128. package/src/infrastructure/event-bus/event-bus.ts +0 -145
  129. package/src/infrastructure/event-bus/index.ts +0 -2
  130. package/src/infrastructure/event-bus/types.ts +0 -22
  131. package/src/infrastructure/lock/decorators.ts +0 -67
  132. package/src/infrastructure/lock/index.ts +0 -2
  133. package/src/infrastructure/lock/lock-manager.ts +0 -33
  134. package/src/plugin/apps-registry.ts +0 -97
  135. package/src/plugin/index.ts +0 -5
  136. package/src/plugin/types.ts +0 -41
  137. package/src/ui/management/audit-log-page.tsx +0 -14
  138. package/src/ui/management/job-management.tsx +0 -308
  139. package/src/workflow/activity-timeline.tsx +0 -412
  140. package/src/workflow/approval-workflow.tsx +0 -31
  141. package/src/workflow/index.ts +0 -2
  142. /package/src/{infrastructure/cron → cron}/types.ts +0 -0
@@ -0,0 +1,47 @@
1
+ -- goerp feature: system-jobs — bước 0001 (idempotent). 2 bảng cho engine cron
2
+ -- có trạng thái DB (@goerp/core/cron, configureCronManager) + trang admin
3
+ -- SystemJobsPage: cấu hình job (bật/tắt, lịch) và lịch sử từng lần chạy.
4
+ -- Tên index/constraint giữ đúng bản Prisma sinh ra để app đã có bảng (vinhhoa)
5
+ -- chạy lại là no-op, không đẻ index trùng.
6
+ CREATE TABLE IF NOT EXISTS "system_jobs" (
7
+ "id" TEXT NOT NULL,
8
+ "name" TEXT NOT NULL,
9
+ "cron_time" TEXT NOT NULL,
10
+ "enabled" BOOLEAN NOT NULL DEFAULT true,
11
+ "last_run" TIMESTAMP(3),
12
+ "next_run" TIMESTAMP(3),
13
+ "status" TEXT NOT NULL DEFAULT 'idle',
14
+ "error" TEXT,
15
+ "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
16
+ "updated_at" TIMESTAMP(3) NOT NULL,
17
+ "metadata" JSONB,
18
+
19
+ CONSTRAINT "system_jobs_pkey" PRIMARY KEY ("id")
20
+ );
21
+
22
+ CREATE UNIQUE INDEX IF NOT EXISTS "system_jobs_name_key" ON "system_jobs"("name");
23
+
24
+ CREATE TABLE IF NOT EXISTS "job_execution_logs" (
25
+ "id" TEXT NOT NULL,
26
+ "job_name" TEXT NOT NULL,
27
+ "started_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
28
+ "finished_at" TIMESTAMP(3),
29
+ "duration_ms" INTEGER,
30
+ "status" TEXT NOT NULL DEFAULT 'running',
31
+ "error" TEXT,
32
+ "actions" JSONB,
33
+ "summary" TEXT,
34
+
35
+ CONSTRAINT "job_execution_logs_pkey" PRIMARY KEY ("id")
36
+ );
37
+
38
+ CREATE INDEX IF NOT EXISTS "job_execution_logs_job_name_idx" ON "job_execution_logs"("job_name");
39
+ CREATE INDEX IF NOT EXISTS "job_execution_logs_started_at_idx" ON "job_execution_logs"("started_at");
40
+
41
+ -- FK: PG không có ADD CONSTRAINT IF NOT EXISTS → DO-block kiểm tra pg_constraint.
42
+ DO $$ BEGIN
43
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'job_execution_logs_job_name_fkey') THEN
44
+ ALTER TABLE "job_execution_logs" ADD CONSTRAINT "job_execution_logs_job_name_fkey"
45
+ FOREIGN KEY ("job_name") REFERENCES "system_jobs"("name") ON DELETE CASCADE ON UPDATE CASCADE;
46
+ END IF;
47
+ END $$;
@@ -0,0 +1,87 @@
1
+ // ============================================================
2
+ // NGƯỜI DÙNG + BẢNG CỦA BETTER AUTH
3
+ //
4
+ // Better Auth cần đúng 4 model: User, Session, Account, Verification.
5
+ // Tên model map trong src/lib/better-auth.ts. App greenfield đặt tên bảng
6
+ // gọn (`sessions`/`accounts`); app migrate từ NextAuth thì thêm tiền tố
7
+ // (vinhhoa dùng `ba_*`) để chạy song song rồi mới drop bảng cũ.
8
+ //
9
+ // `password` nằm ở Account (providerId="credential") — đó là nơi Better Auth
10
+ // đọc/ghi. Đừng tự thêm cột password vào User rồi tự so bcrypt.
11
+ // ============================================================
12
+
13
+ model User {
14
+ id String @id @default(cuid()) @map("id")
15
+ email String @unique @map("email")
16
+ name String? @map("name")
17
+ image String? @map("image")
18
+ emailVerified Boolean @default(false) @map("email_verified")
19
+ isActive Boolean @default(true) @map("is_active")
20
+ lastLoginAt DateTime? @map("last_login_at")
21
+ createdAt DateTime @default(now()) @map("created_at")
22
+ updatedAt DateTime @updatedAt @map("updated_at")
23
+
24
+ accounts Account[]
25
+ sessions Session[]
26
+ userRoles UserRole[]
27
+ userBranches UserBranch[]
28
+
29
+ // Back-relation của các feature core (goerp-*.prisma). Prisma bắt buộc khai
30
+ // hai chiều — bỏ feature nào thì xóa dòng tương ứng, thêm feature nào thì
31
+ // đọc features/<name>/README.md trong core xem cần thêm dòng gì.
32
+ notifications Notification[]
33
+ pushSubscriptions PushSubscription[]
34
+ auditLogs AuditLog[]
35
+
36
+ @@index([email])
37
+ @@index([isActive])
38
+ @@map("users")
39
+ }
40
+
41
+ model Session {
42
+ id String @id @default(cuid()) @map("id")
43
+ userId String @map("user_id")
44
+ token String @unique @map("token")
45
+ expiresAt DateTime @map("expires_at")
46
+ ipAddress String? @map("ip_address")
47
+ userAgent String? @map("user_agent")
48
+ createdAt DateTime @default(now()) @map("created_at")
49
+ updatedAt DateTime @updatedAt @map("updated_at")
50
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
51
+
52
+ @@index([userId])
53
+ @@map("sessions")
54
+ }
55
+
56
+ model Account {
57
+ id String @id @default(cuid()) @map("id")
58
+ userId String @map("user_id")
59
+ accountId String @map("account_id")
60
+ providerId String @map("provider_id")
61
+ password String? @map("password")
62
+ accessToken String? @map("access_token")
63
+ refreshToken String? @map("refresh_token")
64
+ idToken String? @map("id_token")
65
+ accessTokenExpiresAt DateTime? @map("access_token_expires_at")
66
+ refreshTokenExpiresAt DateTime? @map("refresh_token_expires_at")
67
+ scope String? @map("scope")
68
+ createdAt DateTime @default(now()) @map("created_at")
69
+ updatedAt DateTime @updatedAt @map("updated_at")
70
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
71
+
72
+ @@unique([providerId, accountId])
73
+ @@index([userId])
74
+ @@map("accounts")
75
+ }
76
+
77
+ model Verification {
78
+ id String @id @default(cuid()) @map("id")
79
+ identifier String @map("identifier")
80
+ value String @map("value")
81
+ expiresAt DateTime @map("expires_at")
82
+ createdAt DateTime @default(now()) @map("created_at")
83
+ updatedAt DateTime @updatedAt @map("updated_at")
84
+
85
+ @@index([identifier])
86
+ @@map("verifications")
87
+ }
@@ -0,0 +1,24 @@
1
+ // ============================================================
2
+ // NGHIỆP VỤ CỦA APP — chỗ duy nhất bạn thật sự phải viết.
3
+ //
4
+ // Department ở đây chỉ là ví dụ mẫu, đi kèm
5
+ // src/configs/entities/department.config.ts và phục vụ ở /crud/departments.
6
+ // Xóa nó đi và khai model của mình theo đúng khuôn: mỗi entity một
7
+ // EntityConfig, một dòng trong CRUD model map, một mục navigation, một
8
+ // resource trong Permission Registry.
9
+ // ============================================================
10
+
11
+ model Department {
12
+ id String @id @default(cuid()) @map("id")
13
+ code String @unique @map("code")
14
+ name String @map("name")
15
+ description String? @map("description")
16
+ parentId String? @map("parent_id")
17
+ order Int @default(0) @map("order")
18
+ status String @default("active") @map("status")
19
+ createdAt DateTime @default(now()) @map("created_at")
20
+ updatedAt DateTime @updatedAt @map("updated_at")
21
+
22
+ @@index([status])
23
+ @@map("departments")
24
+ }
@@ -0,0 +1,23 @@
1
+ // GENERATED từ @goerp/core (features/audit-logs/schema.prisma) — ĐỪNG sửa tay.
2
+ // Nâng cấp core xong chạy: pnpm goerp-features sync
3
+
4
+ model AuditLog {
5
+ id String @id @default(cuid()) @map("id")
6
+ userId String? @map("user_id")
7
+ action String @map("action")
8
+ resource String @map("resource")
9
+ resourceId String? @map("resource_id")
10
+ oldData Json? @map("old_data")
11
+ newData Json? @map("new_data")
12
+ ipAddress String? @map("ip_address")
13
+ userAgent String? @map("user_agent")
14
+ description String? @map("description")
15
+ createdAt DateTime @default(now()) @map("created_at")
16
+ user User? @relation(fields: [userId], references: [id])
17
+
18
+ @@index([userId])
19
+ @@index([action])
20
+ @@index([resource])
21
+ @@index([createdAt])
22
+ @@map("audit_logs")
23
+ }
@@ -0,0 +1,25 @@
1
+ // GENERATED từ @goerp/core (features/background-tasks/schema.prisma) — ĐỪNG sửa tay.
2
+ // Nâng cấp core xong chạy: pnpm goerp-features sync
3
+
4
+ // Tác vụ nền (export/import lớn…): hàng đợi trong DB, worker in-process.
5
+ // status: pending → running → success | error.
6
+ // result: { fileKey?, fileName?, rowCount?, errorFileKey?, summary? }.
7
+ model BackgroundTask {
8
+ id String @id @default(cuid()) @map("id")
9
+ type String @map("type")
10
+ title String @map("title")
11
+ status String @default("pending") @map("status")
12
+ progress Int @default(0) @map("progress")
13
+ params Json? @map("params")
14
+ result Json? @map("result")
15
+ error String? @map("error")
16
+ createdBy String @map("created_by")
17
+ branchId String? @map("branch_id")
18
+ createdAt DateTime @default(now()) @map("created_at")
19
+ startedAt DateTime? @map("started_at")
20
+ finishedAt DateTime? @map("finished_at")
21
+
22
+ @@index([createdBy, createdAt])
23
+ @@index([status])
24
+ @@map("background_tasks")
25
+ }
@@ -0,0 +1,31 @@
1
+ // GENERATED từ @goerp/core (features/error-logs/schema.prisma) — ĐỪNG sửa tay.
2
+ // Nâng cấp core xong chạy: pnpm goerp-features sync
3
+
4
+ model ErrorLog {
5
+ id String @id @default(dbgenerated("gen_random_uuid()")) @map("id")
6
+ errorId String @map("error_id")
7
+ fingerprint String? @map("fingerprint")
8
+ code String? @map("code")
9
+ message String @map("message")
10
+ detail String? @map("detail")
11
+ context String? @map("context")
12
+ module String? @map("module")
13
+ userId String? @map("user_id")
14
+ url String? @map("url")
15
+ userAgent String? @map("user_agent")
16
+ severity String @default("error") @map("severity")
17
+ resolved Boolean @default(false) @map("resolved")
18
+ occurrenceCount Int @default(1) @map("occurrence_count")
19
+ lastSeenAt DateTime @default(now()) @map("last_seen_at") @db.Timestamptz(6)
20
+ createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
21
+
22
+ @@index([code], map: "idx_error_logs_code")
23
+ @@index([createdAt], map: "idx_error_logs_created_at")
24
+ @@index([errorId], map: "idx_error_logs_error_id")
25
+ @@index([fingerprint], map: "idx_error_logs_fingerprint")
26
+ @@index([module], map: "idx_error_logs_module")
27
+ @@index([resolved], map: "idx_error_logs_resolved")
28
+ @@index([severity], map: "idx_error_logs_severity")
29
+ @@index([userId], map: "idx_error_logs_user_id")
30
+ @@map("error_logs")
31
+ }
@@ -0,0 +1,49 @@
1
+ // GENERATED từ @goerp/core (features/notifications/schema.prisma) — ĐỪNG sửa tay.
2
+ // Nâng cấp core xong chạy: pnpm goerp-features sync
3
+
4
+ /// Thông báo in-app cho NHÂN VIÊN (khác ZaloNotification / TriAnhZnsMessage — đó
5
+ /// là ZNS gửi KHÁCH). Mỗi row là 1 tin gửi tới 1 user (fan-out ở tầng service,
6
+ /// không broadcast 1-row-nhiều-người). Best-effort: emit không được chặn nghiệp vụ.
7
+ model Notification {
8
+ id String @id @default(cuid()) @map("id")
9
+ userId String @map("user_id") // người NHẬN
10
+ type String @default("info") @map("type") // info | success | warning | error | approval
11
+ category String? @map("category") // sales-order | payment | payment-request | misa | purchase-order | system
12
+ title String @map("title")
13
+ content String @map("content") @db.Text
14
+ url String? @map("url") // deep-link tới đối tượng
15
+ iconName String? @map("icon_name") // lucide icon cho dropdown
16
+ resourceType String? @map("resource_type") // "SalesOrder" | "PaymentRequest" | "PurchaseOrder"…
17
+ resourceId String? @map("resource_id")
18
+ isRead Boolean @default(false) @map("is_read")
19
+ readAt DateTime? @map("read_at")
20
+ createdBy String? @map("created_by") // actor gây ra sự kiện (null = hệ thống)
21
+ meta Json? @map("meta")
22
+ createdAt DateTime @default(now()) @map("created_at")
23
+
24
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
25
+
26
+ @@index([userId, isRead])
27
+ @@index([userId, createdAt])
28
+ @@index([resourceType, resourceId])
29
+ @@map("notifications")
30
+ }
31
+
32
+ /// Web Push subscription của 1 thiết bị (PWA iOS/Android/desktop). 1 user nhiều
33
+ /// row = nhiều thiết bị. `endpoint` (URL APNs/FCM) là khoá dedup tự nhiên; push
34
+ /// service trả 404/410 → XÓA row (chuẩn giao thức), không dùng cờ active.
35
+ model PushSubscription {
36
+ id String @id @default(cuid()) @map("id")
37
+ userId String @map("user_id")
38
+ endpoint String @unique @map("endpoint")
39
+ p256dh String @map("p256dh") // khoá mã hoá payload (từ PushSubscription.toJSON)
40
+ auth String @map("auth")
41
+ userAgent String? @map("user_agent") // nhận diện thiết bị khi user quản lý danh sách
42
+ createdAt DateTime @default(now()) @map("created_at")
43
+ updatedAt DateTime @updatedAt @map("updated_at")
44
+
45
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
46
+
47
+ @@index([userId])
48
+ @@map("push_subscriptions")
49
+ }
@@ -0,0 +1,45 @@
1
+ // GENERATED từ @goerp/core (features/system-jobs/schema.prisma) — ĐỪNG sửa tay.
2
+ // Nâng cấp core xong chạy: pnpm goerp-features sync
3
+
4
+ /// Job định kỳ có TRẠNG THÁI TRONG DB (engine `@goerp/core/cron`). DB là nguồn
5
+ /// sự thật của cờ enabled + lịch chạy: admin tắt job thì deploy mới không tự
6
+ /// bật lại, và restart tiến trình không mất cấu hình. `name` trùng tên đăng ký
7
+ /// trong code (`cronManager.addJob({ name })`) nên là khoá tự nhiên.
8
+ model SystemJob {
9
+ id String @id @default(cuid()) @map("id")
10
+ name String @unique @map("name")
11
+ cronTime String @map("cron_time") // "5m" | "1h" | "0 * * * *"
12
+ enabled Boolean @default(true) @map("enabled")
13
+ lastRun DateTime? @map("last_run")
14
+ nextRun DateTime? @map("next_run")
15
+ status String @default("idle") @map("status") // idle | running | failed
16
+ error String? @map("error") // lỗi lần chạy gần nhất (xoá khi chạy lại được)
17
+ createdAt DateTime @default(now()) @map("created_at")
18
+ updatedAt DateTime @updatedAt @map("updated_at")
19
+ metadata Json? @map("metadata")
20
+ executionLogs JobExecutionLog[]
21
+
22
+ @@map("system_jobs")
23
+ }
24
+
25
+ /// Một LẦN chạy của job: mở row lúc bắt đầu (status=running) và đóng lúc kết
26
+ /// thúc. `actions` là nhật ký diễn biến do chính job ghi qua
27
+ /// `getJobExecutionContext().log()` — thứ giúp admin biết job "chạy rồi" đã
28
+ /// làm được gì, không chỉ thành công/thất bại.
29
+ model JobExecutionLog {
30
+ id String @id @default(cuid()) @map("id")
31
+ jobName String @map("job_name")
32
+ startedAt DateTime @default(now()) @map("started_at")
33
+ finishedAt DateTime? @map("finished_at")
34
+ durationMs Int? @map("duration_ms")
35
+ status String @default("running") @map("status") // running | success | failed
36
+ error String? @map("error")
37
+ actions Json? @map("actions") // [{ time, action, details? }]
38
+ summary String? @map("summary")
39
+
40
+ job SystemJob @relation(fields: [jobName], references: [name], onDelete: Cascade)
41
+
42
+ @@index([jobName])
43
+ @@index([startedAt])
44
+ @@map("job_execution_logs")
45
+ }
@@ -0,0 +1,31 @@
1
+ // Chi nhánh + gán người dùng vào chi nhánh. Giữ ngay từ đầu dù app một chi
2
+ // nhánh: thêm sau thì phải sửa lại mọi chứng từ đã phát sinh. `isDefault`
3
+ // quyết định chi nhánh mở sẵn khi đăng nhập.
4
+ model Branch {
5
+ id String @id @default(cuid()) @map("id")
6
+ code String @unique @map("code")
7
+ name String @map("name")
8
+ address String? @map("address")
9
+ phone String? @map("phone")
10
+ isActive Boolean @default(true) @map("is_active")
11
+ createdAt DateTime @default(now()) @map("created_at")
12
+ updatedAt DateTime @updatedAt @map("updated_at")
13
+
14
+ userBranches UserBranch[]
15
+
16
+ @@map("branches")
17
+ }
18
+
19
+ model UserBranch {
20
+ id String @id @default(cuid()) @map("id")
21
+ userId String @map("user_id")
22
+ branchId String @map("branch_id")
23
+ isDefault Boolean @default(false) @map("is_default")
24
+ createdAt DateTime @default(now()) @map("created_at")
25
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
26
+ branch Branch @relation(fields: [branchId], references: [id], onDelete: Cascade)
27
+
28
+ @@unique([userId, branchId])
29
+ @@index([userId])
30
+ @@map("user_branches")
31
+ }
@@ -0,0 +1,100 @@
1
+ // ============================================================
2
+ // RBAC — HÌNH DẠNG BẮT BUỘC, core đọc thẳng mấy model này.
3
+ //
4
+ // `@goerp/core/rbac` query đúng chuỗi:
5
+ // userRole.findMany({ where: { userId }, select: { roleCode, role: {
6
+ // select: { rolePermissions: { select: { resourceCode, actionCode } } } } } })
7
+ // Đổi tên field/model ở đây là gãy permission-service, RoleListPage và
8
+ // createRolesCollectionHandlers. Tên BẢNG thì đổi thoải mái qua @@map.
9
+ //
10
+ // Quyền = cặp (resourceCode, actionCode) gán cho ROLE, người dùng nhận qua
11
+ // UserRole. Không có bảng "quyền gán thẳng cho user" — cố tình, để chỗ cấp
12
+ // quyền chỉ có một.
13
+ // ============================================================
14
+
15
+ model Role {
16
+ id String @id @default(cuid()) @map("id")
17
+ code String @unique @map("code")
18
+ name String @unique @map("name")
19
+ description String @default("") @map("description")
20
+ status String @default("active") @map("status")
21
+ createdAt DateTime @default(now()) @map("created_at")
22
+ createdBy String @default("system") @map("created_by")
23
+ updatedAt DateTime @updatedAt @map("updated_at")
24
+ updatedBy String @default("system") @map("updated_by")
25
+
26
+ rolePermissions RolePermission[]
27
+ userRoles UserRole[]
28
+
29
+ @@index([code])
30
+ @@index([status])
31
+ @@map("roles")
32
+ }
33
+
34
+ model Resource {
35
+ id String @id @default(cuid()) @map("id")
36
+ code String @unique @map("code")
37
+ name String @map("name")
38
+ group String? @map("group")
39
+ description String? @map("description")
40
+ status String @default("active") @map("status")
41
+ parentCode String? @map("parent_code")
42
+ order Int? @map("order")
43
+ type String? @map("type")
44
+ icon String? @map("icon")
45
+ path String? @map("path")
46
+ // JSON chuỗi. Khóa `actions` = danh sách hành động HIỆN trên ma trận phân
47
+ // quyền của core cho resource này; scripts/rbac-sync.ts ghi khóa đó từ
48
+ // registry và giữ nguyên các khóa admin tự thêm.
49
+ config String? @map("config")
50
+ createdAt DateTime @default(now()) @map("created_at")
51
+ updatedAt DateTime @updatedAt @map("updated_at")
52
+
53
+ rolePermissions RolePermission[]
54
+
55
+ @@map("resources")
56
+ }
57
+
58
+ model Action {
59
+ id String @id @default(cuid()) @map("id")
60
+ code String @unique @map("code")
61
+ name String @map("name")
62
+ description String? @map("description")
63
+ status String @default("active") @map("status")
64
+ isDefault Boolean @default(false) @map("is_default")
65
+ createdAt DateTime @default(now()) @map("created_at")
66
+ updatedAt DateTime @updatedAt @map("updated_at")
67
+
68
+ rolePermissions RolePermission[]
69
+
70
+ @@map("actions")
71
+ }
72
+
73
+ model RolePermission {
74
+ id String @id @default(cuid()) @map("id")
75
+ roleCode String @map("role_code")
76
+ resourceCode String @map("resource_code")
77
+ actionCode String @map("action_code")
78
+ role Role @relation(fields: [roleCode], references: [code], onDelete: Cascade)
79
+ resource Resource @relation(fields: [resourceCode], references: [code], onDelete: Cascade)
80
+ action Action @relation(fields: [actionCode], references: [code], onDelete: Cascade)
81
+
82
+ @@unique([roleCode, resourceCode, actionCode])
83
+ @@index([roleCode])
84
+ @@map("role_permissions")
85
+ }
86
+
87
+ model UserRole {
88
+ id String @id @default(cuid()) @map("id")
89
+ userId String @map("user_id")
90
+ roleCode String @map("role_code")
91
+ isPrimary Boolean @default(false) @map("is_primary")
92
+ createdAt DateTime @default(now()) @map("created_at")
93
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
94
+ role Role @relation(fields: [roleCode], references: [code], onDelete: Cascade)
95
+
96
+ @@unique([userId, roleCode])
97
+ @@index([userId])
98
+ @@index([roleCode])
99
+ @@map("user_roles")
100
+ }
@@ -0,0 +1,8 @@
1
+ // Datasource + generator. URL nằm ở prisma.config.ts (Prisma 7), không ở đây.
2
+ generator client {
3
+ provider = "prisma-client-js"
4
+ }
5
+
6
+ datasource db {
7
+ provider = "postgresql"
8
+ }
@@ -0,0 +1,22 @@
1
+ // Kho cấu hình key/value của app. `@goerp/core/system/services/settings-service`
2
+ // đọc/ghi thẳng model này (findUnique theo `key`, upsert `value`) — nối dây một
3
+ // lần bằng configureSettingsService(db) trong src/lib/prisma.ts.
4
+ model SystemConfig {
5
+ id String @id @default(cuid()) @map("id")
6
+ key String @unique @map("key")
7
+ value String @map("value")
8
+ type String @default("string") @map("type")
9
+ category String @default("general") @map("category")
10
+ description String? @map("description")
11
+ isReadOnly Boolean @default(false) @map("is_read_only")
12
+ // Giá trị nhạy cảm (API key, secret): API che thành •••••••• khi trả về danh
13
+ // sách. Giao diện cấu hình của core đọc cờ này để hiện ô dạng mật khẩu.
14
+ isEncrypted Boolean @default(false) @map("is_encrypted")
15
+ status String @default("active") @map("status")
16
+ createdAt DateTime @default(now()) @map("created_at")
17
+ createdBy String @default("system") @map("created_by")
18
+ updatedAt DateTime @updatedAt @map("updated_at")
19
+ updatedBy String @default("system") @map("updated_by")
20
+
21
+ @@map("system_configs")
22
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Seed dữ liệu khởi tạo — chạy được nhiều lần (toàn upsert).
3
+ *
4
+ * pnpm prisma:deploy && pnpm rbac-sync && pnpm seed
5
+ *
6
+ * Phân công rõ ràng: `rbac-sync` sở hữu resource/action/grant mặc định (đọc từ
7
+ * Permission Registry); file này chỉ tạo những thứ registry không biết —
8
+ * VAI TRÒ, tài khoản quản trị, chi nhánh, cấu hình và vài dòng dữ liệu mẫu.
9
+ * Vì vậy hãy chạy rbac-sync TRƯỚC seed ở lần đầu... nhưng grant mặc định cần
10
+ * vai trò tồn tại sẵn, nên seed cũng tạo vai trò và bạn chạy rbac-sync lần nữa
11
+ * sau seed. Thứ tự an toàn nhất: seed → rbac-sync.
12
+ */
13
+
14
+ import { PrismaPg } from "@prisma/adapter-pg"
15
+ import { PrismaClient } from "@prisma/client"
16
+ import bcrypt from "bcryptjs"
17
+ import "dotenv/config"
18
+
19
+ const connectionString = process.env.DATABASE_URL
20
+ if (!connectionString) throw new Error("DATABASE_URL chưa được set (xem .env.example)")
21
+ const prisma = new PrismaClient({ adapter: new PrismaPg({ connectionString }) })
22
+
23
+ const ADMIN_EMAIL = process.env.SEED_ADMIN_EMAIL ?? "admin@example.com"
24
+ const ADMIN_PASSWORD = process.env.SEED_ADMIN_PASSWORD ?? "admin123"
25
+
26
+ async function main() {
27
+ // ── Vai trò ────────────────────────────────────────────────────────────
28
+ // "admin" là mã core coi là toàn quyền (ADMIN_ROLE_CODE) — đổi mã này thì
29
+ // phải đổi cả trong core, đừng đổi.
30
+ const roles = [
31
+ { code: "admin", name: "Quản trị hệ thống", description: "Toàn quyền" },
32
+ { code: "staff", name: "Nhân viên", description: "Quyền xem cơ bản" },
33
+ ]
34
+ for (const r of roles) {
35
+ await prisma.role.upsert({ where: { code: r.code }, update: {}, create: r })
36
+ }
37
+ console.log(`✓ ${roles.length} vai trò`)
38
+
39
+ // ── Chi nhánh ──────────────────────────────────────────────────────────
40
+ const branch = await prisma.branch.upsert({
41
+ where: { code: "HQ" },
42
+ update: {},
43
+ create: { code: "HQ", name: "Trụ sở chính" },
44
+ })
45
+ console.log(`✓ chi nhánh ${branch.code}`)
46
+
47
+ // ── Tài khoản quản trị ─────────────────────────────────────────────────
48
+ const admin = await prisma.user.upsert({
49
+ where: { email: ADMIN_EMAIL },
50
+ update: {},
51
+ create: {
52
+ email: ADMIN_EMAIL,
53
+ name: "Quản trị viên",
54
+ emailVerified: true,
55
+ isActive: true,
56
+ },
57
+ })
58
+
59
+ // Mật khẩu nằm ở Account với providerId="credential" — đó là nơi DUY NHẤT
60
+ // Better Auth đọc. Thêm cột password vào User rồi tự so bcrypt sẽ không có
61
+ // tác dụng gì.
62
+ await prisma.account.upsert({
63
+ where: {
64
+ providerId_accountId: { providerId: "credential", accountId: admin.id },
65
+ },
66
+ update: {},
67
+ create: {
68
+ userId: admin.id,
69
+ providerId: "credential",
70
+ accountId: admin.id,
71
+ password: bcrypt.hashSync(ADMIN_PASSWORD, 10),
72
+ },
73
+ })
74
+
75
+ await prisma.userRole.upsert({
76
+ where: { userId_roleCode: { userId: admin.id, roleCode: "admin" } },
77
+ update: {},
78
+ create: { userId: admin.id, roleCode: "admin", isPrimary: true },
79
+ })
80
+ await prisma.userBranch.upsert({
81
+ where: { userId_branchId: { userId: admin.id, branchId: branch.id } },
82
+ update: {},
83
+ create: { userId: admin.id, branchId: branch.id, isDefault: true },
84
+ })
85
+ console.log(`✓ tài khoản quản trị: ${ADMIN_EMAIL} / ${ADMIN_PASSWORD}`)
86
+
87
+ // ── Cấu hình hệ thống ──────────────────────────────────────────────────
88
+ const configs = [
89
+ { key: "company_name", value: "Công ty của bạn", category: "company" },
90
+ { key: "company_address", value: "", category: "company" },
91
+ { key: "company_phone", value: "", category: "company" },
92
+ ]
93
+ for (const c of configs) {
94
+ await prisma.systemConfig.upsert({
95
+ where: { key: c.key },
96
+ update: {},
97
+ create: c,
98
+ })
99
+ }
100
+ console.log(`✓ ${configs.length} cấu hình hệ thống`)
101
+
102
+ // ── Dữ liệu mẫu (xoá khi bạn thay Department bằng entity thật) ─────────
103
+ const departments = [
104
+ { code: "BOD", name: "Ban giám đốc", order: 1 },
105
+ { code: "SALES", name: "Kinh doanh", order: 2 },
106
+ { code: "ACC", name: "Kế toán", order: 3 },
107
+ { code: "HR", name: "Nhân sự", order: 4 },
108
+ { code: "IT", name: "Công nghệ thông tin", order: 5 },
109
+ ]
110
+ for (const d of departments) {
111
+ await prisma.department.upsert({
112
+ where: { code: d.code },
113
+ update: {},
114
+ create: { ...d, status: "active" },
115
+ })
116
+ }
117
+ console.log(`✓ ${departments.length} phòng ban mẫu`)
118
+
119
+ console.log("\nTiếp theo: pnpm rbac-sync (nạp resource/action/quyền từ registry)")
120
+ }
121
+
122
+ main()
123
+ .catch((e) => {
124
+ console.error(e)
125
+ process.exit(1)
126
+ })
127
+ .finally(() => prisma.$disconnect())
@@ -0,0 +1,20 @@
1
+ import path from "node:path"
2
+
3
+ import "dotenv/config"
4
+ import { defineConfig } from "prisma/config"
5
+
6
+ /**
7
+ * Prisma 7 đọc cấu hình từ file này, KHÔNG còn từ block `datasource.url` trong
8
+ * schema (schema chỉ khai provider). Schema để dạng THƯ MỤC nhiều file: mỗi
9
+ * phân hệ một `.prisma`, và `goerp-features sync` thả fragment của core vào
10
+ * cùng chỗ (`goerp-*.prisma`) — đừng sửa tay mấy file đó.
11
+ */
12
+ export default defineConfig({
13
+ schema: path.join(__dirname, "prisma/schema"),
14
+ migrations: {
15
+ path: path.join(__dirname, "prisma/migrations"),
16
+ },
17
+ datasource: {
18
+ url: process.env.DATABASE_URL!,
19
+ },
20
+ })
@@ -0,0 +1,2 @@
1
+ # File tĩnh của app (favicon, ảnh, manifest…). Dockerfile COPY cả thư mục này
2
+ # nên đừng xoá — mất nó là build image gãy ở bước COPY.