@goplusvn/core 0.1.44 → 0.1.45

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@goplusvn/core",
3
3
  "description": "GoPlusVN Platform Kit - ERP kernel: layout, RBAC, CRUD, multi-tenant, system pages",
4
- "version": "0.1.44",
4
+ "version": "0.1.45",
5
5
  "private": false,
6
6
  "publishConfig": {
7
7
  "registry": "https://registry.npmjs.org",
@@ -158,7 +158,7 @@
158
158
  "swr": "^2.3.6",
159
159
  "tailwind-merge": "2.5.2",
160
160
  "vaul": "1.1.2",
161
- "xlsx": "0.18.5",
161
+ "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
162
162
  "zod": "3.23.8"
163
163
  },
164
164
  "scripts": {
@@ -11,7 +11,6 @@
11
11
  // export const config = { matcher: ["/((?!_next/static|_next/image|favicon.ico|.*\\.[^/]+$).*)"] };
12
12
 
13
13
  import { NextResponse } from "next/server";
14
- import { getToken } from "next-auth/jwt";
15
14
  import type { NextRequest } from "next/server";
16
15
 
17
16
  export interface AuthProxyOptions {
@@ -35,7 +34,16 @@ export function createAuthProxy(options: AuthProxyOptions = {}) {
35
34
  const publicPages = options.publicPages ?? ["/sign-in"];
36
35
  const signInPath = options.signInPath ?? "/sign-in";
37
36
  const homePath = options.homePath ?? "/";
38
- const readToken = options.getToken ?? ((req: NextRequest) => getToken({ req }));
37
+ // next-auth chỉ được LAZY-load khi app không truyền getToken riêng — app đã
38
+ // sang Better Auth (không cài next-auth) sẽ không dính module-not-found lúc
39
+ // import proxy-gate (trước đây import top-level, next-auth lại chỉ nằm ở
40
+ // devDependencies của core).
41
+ const readToken =
42
+ options.getToken ??
43
+ (async (req: NextRequest) => {
44
+ const { getToken } = await import("next-auth/jwt");
45
+ return getToken({ req });
46
+ });
39
47
 
40
48
  return async function proxy(request: NextRequest) {
41
49
  const { pathname, search } = request.nextUrl;
@@ -22,11 +22,25 @@ type MaybePromise<T> = T | Promise<T>;
22
22
  export interface CrudHandlerDeps {
23
23
  getSession: () => MaybePromise<any | null>;
24
24
  getEntityConfig: (entity: string) => EntityConfig | undefined;
25
- service: ServerCrudService;
25
+ /** Một service dùng chung, HOẶC resolver theo entity — app có service
26
+ * chuyên biệt (auto-gen số phiếu, stock-count…) trả service riêng cho
27
+ * entity đó thay vì viết factory if-chain ngoài core (seam 2026-07). */
28
+ service:
29
+ | ServerCrudService
30
+ | ((entity: string) => MaybePromise<ServerCrudService>);
26
31
  /** Optional error mapper (e.g. app's serverError). Defaults to a 500 JSON. */
27
32
  onError?: (error: unknown, req: Request) => Response | Promise<Response>;
28
33
  }
29
34
 
35
+ function makeServiceResolver(
36
+ service: CrudHandlerDeps["service"],
37
+ ): (entity: string) => Promise<ServerCrudService> {
38
+ if (typeof service === "function") {
39
+ return async (entity) => await service(entity);
40
+ }
41
+ return async () => service;
42
+ }
43
+
30
44
  const json = (data: unknown, status = 200) =>
31
45
  new Response(JSON.stringify(data), { status, headers: { "content-type": "application/json" } });
32
46
 
@@ -40,7 +54,8 @@ async function resolvePerms(session: any, config: EntityConfig, entity: string)
40
54
 
41
55
  // GET/POST for the collection route `/api/crud/[entity]`.
42
56
  export function createCrudCollectionHandlers(deps: CrudHandlerDeps) {
43
- const { getSession, getEntityConfig, service, onError } = deps;
57
+ const { getSession, getEntityConfig, onError } = deps;
58
+ const resolveService = makeServiceResolver(deps.service);
44
59
  const fail = (e: unknown, req: Request) =>
45
60
  onError ? onError(e, req) : json({ error: "Internal error" }, 500);
46
61
 
@@ -64,6 +79,7 @@ export function createCrudCollectionHandlers(deps: CrudHandlerDeps) {
64
79
  : undefined,
65
80
  filters: sp.get("filters") ? JSON.parse(sp.get("filters")!) : undefined,
66
81
  };
82
+ const service = await resolveService(entity);
67
83
  const data = await service.list(entity, config, params as any);
68
84
  return json(data);
69
85
  } catch (e) {
@@ -82,6 +98,7 @@ export function createCrudCollectionHandlers(deps: CrudHandlerDeps) {
82
98
  if (!perms.create) return forbidden();
83
99
 
84
100
  const body = await req.json();
101
+ const service = await resolveService(entity);
85
102
  if (Array.isArray(body)) {
86
103
  const results = [];
87
104
  for (const item of body) results.push(await service.create(entity, item, config));
@@ -98,7 +115,8 @@ export function createCrudCollectionHandlers(deps: CrudHandlerDeps) {
98
115
 
99
116
  // GET/PUT/PATCH/DELETE for the item route `/api/crud/[entity]/[id]`.
100
117
  export function createCrudItemHandlers(deps: CrudHandlerDeps) {
101
- const { getSession, getEntityConfig, service, onError } = deps;
118
+ const { getSession, getEntityConfig, onError } = deps;
119
+ const resolveService = makeServiceResolver(deps.service);
102
120
  const fail = (e: unknown, req: Request) =>
103
121
  onError ? onError(e, req) : json({ error: "Internal error" }, 500);
104
122
 
@@ -113,7 +131,9 @@ export function createCrudItemHandlers(deps: CrudHandlerDeps) {
113
131
  if (!config) return unknownEntity();
114
132
  const perms = await resolvePerms(session, config, entity);
115
133
  if (!perms.read) return forbidden();
116
- const item = await service.getById(entity, id);
134
+ const service = await resolveService(entity);
135
+ // Truyền config để detail áp cùng include như list (label thay raw FK id).
136
+ const item = await service.getById(entity, id, config);
117
137
  if (!item) return json({ error: "Not found" }, 404);
118
138
  return json(item);
119
139
  } catch (e) {
@@ -131,6 +151,7 @@ export function createCrudItemHandlers(deps: CrudHandlerDeps) {
131
151
  const perms = await resolvePerms(session, config, entity);
132
152
  if (!perms.update) return forbidden();
133
153
  const body = await req.json();
154
+ const service = await resolveService(entity);
134
155
  return json(await service.update(entity, id, body, config));
135
156
  } catch (e) {
136
157
  return fail(e, req);
@@ -146,6 +167,7 @@ export function createCrudItemHandlers(deps: CrudHandlerDeps) {
146
167
  if (!config) return unknownEntity();
147
168
  const perms = await resolvePerms(session, config, entity);
148
169
  if (!perms.delete) return forbidden();
170
+ const service = await resolveService(entity);
149
171
  await service.delete(entity, id);
150
172
  return new Response(null, { status: 204 });
151
173
  } catch (e) {
@@ -1,7 +1,10 @@
1
1
  import type { CrudPermissions, EntityConfig } from "../../types";
2
2
  import type { Session } from "../../types";
3
3
 
4
- import { getCrudPermissionsFromSession } from "../../rbac/permissions";
4
+ // Nguồn permission DUY NHẤT đã harden (BYPASS_AUTH chỉ hiệu lực ngoài
5
+ // production, admin-role check chạy trước empty-permissions guard) — bản
6
+ // divergent rbac/permissions.ts cũ đã xóa 2026-07 vì thiếu guard NODE_ENV.
7
+ import { getCrudPermissionsFromSession } from "../../auth";
5
8
 
6
9
  /**
7
10
  * Get CRUD permissions for a user based on their session
@@ -22,9 +22,12 @@ type PrismaLike = Record<string, any>;
22
22
 
23
23
  export interface ServerCrudService {
24
24
  list(entity: string, config: EntityConfig, params: CrudQueryParams): Promise<CrudResponse>;
25
- getById(entity: string, id: string): Promise<any>;
26
- create(entity: string, data: any, config?: EntityConfig): Promise<any>;
27
- update(entity: string, id: string, data: any, config?: EntityConfig): Promise<any>;
25
+ /** `config` (tùy chọn) để áp cùng `include` như list — detail không hiện raw FK id. */
26
+ getById(entity: string, id: string, config?: EntityConfig): Promise<any>;
27
+ /** `tx` (tùy chọn) để app chạy create CÙNG transaction với sinh số phiếu
28
+ * advisory-lock (document-number yêu cầu cùng tx với insert). */
29
+ create(entity: string, data: any, config?: EntityConfig, tx?: PrismaLike): Promise<any>;
30
+ update(entity: string, id: string, data: any, config?: EntityConfig, tx?: PrismaLike): Promise<any>;
28
31
  delete(entity: string, id: string): Promise<any>;
29
32
  deleteMany(entity: string, ids: string[]): Promise<any>;
30
33
  }
@@ -43,8 +46,35 @@ export interface ServerCrudDeps {
43
46
  * {@link createAuditUserNameResolver} if your User model exposes a name field. */
44
47
  resolveAuditNames?: (records: any[]) => Promise<any[]>;
45
48
  logger?: ServerCrudLogger;
49
+
50
+ // ── Seams 2026-07: đủ điểm cắm để app KHÔNG phải fork nguyên engine ──
51
+
52
+ /** WHERE mặc định theo entity, AND vào mọi list/count — vd 2 entity chia
53
+ * chung 1 model: stock-in → { type: "in" }, stock-out → { type: "out" }. */
54
+ scopeWhere?: (
55
+ entity: string,
56
+ config: EntityConfig,
57
+ ) => Record<string, unknown> | undefined;
58
+ /** Field hệ thống luôn cho qua filterValidFields dù không khai trong
59
+ * config.fields. Default: id + bộ audit (createdAt/updatedAt/createdBy/
60
+ * updatedBy). App có cột riêng (companyId…) thì truyền thêm. */
61
+ systemFields?: string[];
62
+ /** Set updatedAt = new Date() khi create nếu payload chưa có — cho model
63
+ * thiếu `@updatedAt`/default trong schema. Default: false. */
64
+ touchUpdatedAtOnCreate?: boolean;
65
+ /** Các cột `*Id` KHÔNG phải FK — bỏ qua transform connect/disconnect
66
+ * (scalar như số CCCD, mã tra cứu…). Gộp thêm vào skip-list mặc định. */
67
+ relationFieldSkip?: string[];
46
68
  }
47
69
 
70
+ const DEFAULT_SYSTEM_FIELDS = [
71
+ "id",
72
+ "createdAt",
73
+ "updatedAt",
74
+ "createdBy",
75
+ "updatedBy",
76
+ ];
77
+
48
78
  const MAX_PAGE_SIZE = 200;
49
79
 
50
80
  // Default plural→model resolver: strip trailing "s", camelCase kebab. Apps with
@@ -96,16 +126,18 @@ export function createServerCrudService(deps: ServerCrudDeps): ServerCrudService
96
126
  error: (m, ...r) => console.error(m, ...r),
97
127
  };
98
128
 
99
- const model = (entity: string) => {
129
+ const systemFields = deps.systemFields ?? DEFAULT_SYSTEM_FIELDS;
130
+
131
+ const model = (entity: string, client: PrismaLike = prisma) => {
100
132
  const name = resolveModel(entity);
101
- const m = prisma[name];
133
+ const m = client[name];
102
134
  if (!m) throw new Error(`Prisma model not found for entity: ${entity} (${name})`);
103
135
  return m;
104
136
  };
105
137
 
106
138
  const filterValidFields = (data: any, config: EntityConfig) => {
107
139
  const valid = new Set(config.fields.filter((f) => !(f as any).isDisplayOnly).map((f) => f.name));
108
- valid.add("id");
140
+ for (const f of systemFields) valid.add(f);
109
141
  const out: Record<string, unknown> = {};
110
142
  for (const [k, v] of Object.entries(data)) {
111
143
  if (valid.has(k)) out[k] = v;
@@ -143,7 +175,16 @@ export function createServerCrudService(deps: ServerCrudDeps): ServerCrudService
143
175
 
144
176
  const transformRelationFields = (data: any, mode: "create" | "update") => {
145
177
  const out = { ...data };
146
- const skip = new Set(["id", "createdBy", "updatedBy", "citizenId", "targetId"]);
178
+ // citizenId/targetId legacy-default từ consumer đầu tiên — app mới khai
179
+ // cột scalar *Id của mình qua deps.relationFieldSkip thay vì sửa core.
180
+ const skip = new Set([
181
+ "id",
182
+ "createdBy",
183
+ "updatedBy",
184
+ "citizenId",
185
+ "targetId",
186
+ ...(deps.relationFieldSkip ?? []),
187
+ ]);
147
188
  for (const key of Object.keys(out)) {
148
189
  if (skip.has(key)) continue;
149
190
  if (key.endsWith("Id") && key.length > 2) {
@@ -181,7 +222,7 @@ export function createServerCrudService(deps: ServerCrudDeps): ServerCrudService
181
222
  return allowedFields.has(name);
182
223
  };
183
224
 
184
- const where: any = {};
225
+ const where: any = { ...(deps.scopeWhere?.(entity, config) ?? {}) };
185
226
  if (search && search.trim()) {
186
227
  const term = search.trim();
187
228
  const searchFields = config.fields.filter((f) => f.filterable && f.type === "text").map((f) => f.name);
@@ -253,21 +294,28 @@ export function createServerCrudService(deps: ServerCrudDeps): ServerCrudService
253
294
  }
254
295
  },
255
296
 
256
- async getById(entity, id) {
297
+ async getById(entity, id, config) {
257
298
  const prismaModel = model(entity);
258
- const result = await prismaModel.findUnique({ where: { id } });
299
+ // Áp cùng include như list detail dialog hiện label relation thay raw FK id.
300
+ const include: any = {};
301
+ if (config?.include?.length) config.include.forEach((inc) => (include[inc] = true));
302
+ const includeOption = Object.keys(include).length ? { include } : {};
303
+ const result = await prismaModel.findUnique({ where: { id }, ...includeOption });
259
304
  if (!result) return null;
260
305
  const serialized = serializeDecimalFields(result);
261
306
  const [resolved] = await resolveAuditNames([serialized]);
262
307
  return resolved;
263
308
  },
264
309
 
265
- async create(entity, data, config) {
266
- const prismaModel = model(entity);
310
+ async create(entity, data, config, tx) {
311
+ const prismaModel = model(entity, tx ?? prisma);
267
312
  try {
268
313
  let filtered = config ? filterValidFields(data, config) : data;
269
314
  if (config) filtered = castFieldValues(filtered, config);
270
315
  if (!filtered.id) filtered.id = crypto.randomUUID();
316
+ if (deps.touchUpdatedAtOnCreate && !filtered.updatedAt) {
317
+ filtered.updatedAt = new Date();
318
+ }
271
319
  const prismaData = transformRelationFields(filtered, "create");
272
320
  return serializeDecimalFields(await prismaModel.create({ data: prismaData }));
273
321
  } catch (error) {
@@ -276,8 +324,8 @@ export function createServerCrudService(deps: ServerCrudDeps): ServerCrudService
276
324
  }
277
325
  },
278
326
 
279
- async update(entity, id, data, config) {
280
- const prismaModel = model(entity);
327
+ async update(entity, id, data, config, tx) {
328
+ const prismaModel = model(entity, tx ?? prisma);
281
329
  try {
282
330
  let filtered = config ? filterValidFields(data, config) : data;
283
331
  if (config) filtered = castFieldValues(filtered, config);
@@ -17,11 +17,27 @@ import { AppError, toAppError } from "./app-error"
17
17
  */
18
18
  export async function withErrorHandler(
19
19
  handler: () => Promise<Response>,
20
- options?: { operation?: string; req?: NextRequest | Request }
20
+ options?: {
21
+ operation?: string
22
+ req?: NextRequest | Request
23
+ /**
24
+ * Seam persist lỗi (2026-07): mặc định chỉ console.error — lỗi KHÔNG
25
+ * vào error_logs. App truyền `onError: (e, req) => serverError(e, req)`
26
+ * (buildServerError ở ./server-error) để lỗi được lưu bền + giữ nguyên
27
+ * shape response (cả hai cùng kết thúc bằng createErrorResponse).
28
+ */
29
+ onError?: (
30
+ error: unknown,
31
+ req?: NextRequest | Request
32
+ ) => Response | Promise<Response>
33
+ }
21
34
  ): Promise<Response> {
22
35
  try {
23
36
  return await handler()
24
37
  } catch (error) {
38
+ if (options?.onError) {
39
+ return options.onError(error, options.req)
40
+ }
25
41
  const appError = toAppError(error)
26
42
 
27
43
  const requestContext = options?.req
@@ -1,5 +1,4 @@
1
1
  import type { NextResponse } from "next/server"
2
- import { createHash } from "crypto"
3
2
 
4
3
  import { AppError, toAppError } from "./app-error"
5
4
  import { createErrorResponse } from "./error-handler"
@@ -68,7 +67,24 @@ function buildFingerprint(
68
67
  /* keep raw */
69
68
  }
70
69
  const raw = [message.slice(0, 200), code || "", moduleTag || "", path].join("|")
71
- return createHash("sha256").update(raw).digest("hex").slice(0, 16)
70
+ return fingerprint16(raw)
71
+ }
72
+
73
+ /**
74
+ * Fingerprint 16 hex để GOM NHÓM lỗi — không phải mật mã học. FNV-1a thuần
75
+ * (không import "crypto" của Node) vì error path bị instrumentation/proxy kéo
76
+ * vào Edge bundle, nơi không có module crypto (2026-07: dev webpack vỡ vì
77
+ * import node:crypto ở file này).
78
+ */
79
+ function fingerprint16(raw: string): string {
80
+ let h1 = 0x811c9dc5
81
+ let h2 = 0xcbf29ce4
82
+ for (let i = 0; i < raw.length; i++) {
83
+ const c = raw.charCodeAt(i)
84
+ h1 = Math.imul(h1 ^ c, 0x01000193) >>> 0
85
+ h2 = Math.imul(h2 ^ ((c << 1) | 1), 0x01000193) >>> 0
86
+ }
87
+ return h1.toString(16).padStart(8, "0") + h2.toString(16).padStart(8, "0")
72
88
  }
73
89
 
74
90
  /**
@@ -1,8 +1,54 @@
1
- const db: any = { systemConfig: { findUnique: async () => null, upsert: async () => null } }; // Mocked for build
2
-
3
1
  import { cache } from "react";
4
2
  import { revalidateTag, unstable_cache } from "next/cache";
5
3
 
4
+ /**
5
+ * DB được INJECT từ app (core không import prisma client cụ thể). Trước
6
+ * 2026-07 chỗ này là `const db: any = {...} // Mocked for build` — mọi
7
+ * getSettings âm thầm trả default và saveSettings ghi vào hư không (gốc của
8
+ * regression "settings admin chỉnh không ăn"). App gọi
9
+ * `configureSettingsService(db)` MỘT lần ở composition root (cạnh chỗ tạo
10
+ * prisma client).
11
+ */
12
+ // Structural + schema-agnostic (giống PrismaLike ở crud/server-service): args
13
+ // để lỏng vì generated types của Prisma mỗi app một khác — chỉ chốt shape trả
14
+ // về mà service thật sự đọc.
15
+ export interface SettingsDb {
16
+ systemConfig: {
17
+ findUnique(args: any): Promise<{ value: string | null } | null>;
18
+ upsert(args: any): Promise<unknown>;
19
+ };
20
+ }
21
+
22
+ let configuredDb: SettingsDb | null = null;
23
+ let warnedMissingDb = false;
24
+
25
+ export function configureSettingsService(db: SettingsDb): void {
26
+ configuredDb = db;
27
+ }
28
+
29
+ /** Đọc: thiếu db → cảnh báo TO (một lần) + trả default — không sập build/SSR. */
30
+ function dbForRead(): SettingsDb | null {
31
+ if (configuredDb) return configuredDb;
32
+ if (!warnedMissingDb) {
33
+ warnedMissingDb = true;
34
+ console.warn(
35
+ "[SettingsService] Chưa configureSettingsService(db) — mọi getSettings " +
36
+ "đang trả DEFAULT (setting admin chỉnh sẽ không có hiệu lực). " +
37
+ "Gọi configureSettingsService(db) ở composition root của app.",
38
+ );
39
+ }
40
+ return null;
41
+ }
42
+
43
+ /** Ghi: thiếu db → THROW. Ghi-vào-hư-không tệ hơn lỗi ồn ào. */
44
+ function dbForWrite(): SettingsDb {
45
+ if (configuredDb) return configuredDb;
46
+ throw new Error(
47
+ "[SettingsService] saveSettings cần configureSettingsService(db) — " +
48
+ "từ chối ghi vào hư không (trước đây mock db nuốt mất dữ liệu).",
49
+ );
50
+ }
51
+
6
52
  export interface AuditSettings {
7
53
  retentionDays: number;
8
54
  enabledResources: string[];
@@ -52,6 +98,8 @@ const DEFAULT_SALES_RULES_SETTINGS: SalesRulesSettings = {
52
98
  export class SettingsService {
53
99
  /** Get all settings cached */
54
100
  static async getSettings<T>(key: string, defaultValue: T): Promise<T> {
101
+ const db = dbForRead();
102
+ if (!db) return defaultValue;
55
103
  return unstable_cache(
56
104
  async () => {
57
105
  const config = await db.systemConfig.findUnique({
@@ -88,6 +136,7 @@ export class SettingsService {
88
136
  value: T,
89
137
  category = "general",
90
138
  ): Promise<void> {
139
+ const db = dbForWrite();
91
140
  await db.systemConfig.upsert({
92
141
  where: { key },
93
142
  update: {
@@ -1,134 +0,0 @@
1
- // @goerp/core/rbac/permissions
2
- // Server-only permission utilities - no client/UI imports
3
- // Use this in Server Components and API routes to avoid bundling client code
4
-
5
- import type { Permission, Session } from "../types";
6
-
7
- // ============================================================================
8
- // Action Mapping
9
- // ============================================================================
10
-
11
- export const CRUD_ACTIONS = {
12
- create: "create",
13
- view: "view",
14
- update: "update",
15
- delete: "delete",
16
- export: "export",
17
- import: "import",
18
- approve: "approve",
19
- reject: "reject",
20
- } as const;
21
-
22
- export type CrudAction = keyof typeof CRUD_ACTIONS;
23
-
24
- export function getActionCode(operation: CrudAction): string {
25
- return CRUD_ACTIONS[operation];
26
- }
27
-
28
- // ============================================================================
29
- // Permission Helpers
30
- // ============================================================================
31
-
32
- interface ExtendedUser {
33
- id: string;
34
- name?: string | null;
35
- email?: string | null;
36
- image?: string | null;
37
- roles?: string[];
38
- permissions?: Permission[];
39
- }
40
-
41
- const BYPASS_AUTH =
42
- process.env.BYPASS_AUTH === "true" || process.env.BYPASS_AUTH === "1";
43
-
44
- const ADMIN_ROLE_CODES = ["admin", "SUPER_ADMIN"];
45
-
46
- export function getCrudPermissionsFromSession(
47
- session: Session | null,
48
- entity: string,
49
- ): {
50
- create: boolean;
51
- view: boolean;
52
- update: boolean;
53
- delete: boolean;
54
- export: boolean;
55
- import: boolean;
56
- approve: boolean;
57
- reject: boolean;
58
- } {
59
- if (BYPASS_AUTH) {
60
- return {
61
- create: true,
62
- view: true,
63
- update: true,
64
- delete: true,
65
- export: true,
66
- import: true,
67
- approve: true,
68
- reject: true,
69
- };
70
- }
71
-
72
- if (!session?.user) {
73
- return {
74
- create: false,
75
- view: false,
76
- update: false,
77
- delete: false,
78
- export: false,
79
- import: false,
80
- approve: false,
81
- reject: false,
82
- };
83
- }
84
-
85
- const user = session.user as ExtendedUser;
86
-
87
- if (!user.id) {
88
- return {
89
- create: false,
90
- view: false,
91
- update: false,
92
- delete: false,
93
- export: false,
94
- import: false,
95
- approve: false,
96
- reject: false,
97
- };
98
- }
99
-
100
- const isAdmin = user.roles?.some((role) => ADMIN_ROLE_CODES.includes(role));
101
- if (isAdmin) {
102
- return {
103
- create: true,
104
- view: true,
105
- update: true,
106
- delete: true,
107
- export: true,
108
- import: true,
109
- approve: true,
110
- reject: true,
111
- };
112
- }
113
-
114
- const permissions = user.permissions || [];
115
- const permissionKeys = new Set(
116
- permissions.map((p) => `${p.resourceCode}:${p.actionCode}`),
117
- );
118
-
119
- const hasPermission = (action: string) => {
120
- const key = `${entity}:${action}`;
121
- return permissionKeys.has(key);
122
- };
123
-
124
- return {
125
- create: hasPermission(getActionCode("create")),
126
- view: hasPermission(getActionCode("view")),
127
- update: hasPermission(getActionCode("update")),
128
- delete: hasPermission(getActionCode("delete")),
129
- export: hasPermission(getActionCode("export")),
130
- import: hasPermission(getActionCode("import")),
131
- approve: hasPermission(getActionCode("approve")),
132
- reject: hasPermission(getActionCode("reject")),
133
- };
134
- }