import { PrismaClient } from '@prisma/client'; import { DIRECTOR_ROLES, WORK_ASSIGN_ROLES } from '../src/common/role-groups'; import { MENUS, isHrSettingCode, type MenuNode } from './menus'; const prisma = new PrismaClient(); async function upsertNode(node: MenuNode, parentId: string | null, sortNo: number) { const existing = await prisma.permission.findUnique({ where: { code: node.code } }); const data = { parentId, name: node.name, permType: 'MENU', path: node.path, icon: node.icon ?? null, sortNo, visible: true, }; const row = existing ? await prisma.permission.update({ where: { id: existing.id }, data }) : await prisma.permission.create({ data: { ...data, code: node.code } }); return row; } const ACTION_DEFS = [ { name: '查看', suffix: 'view' }, { name: '新增', suffix: 'create' }, { name: '编辑', suffix: 'edit' }, { name: '删除', suffix: 'delete' }, { name: '审批', suffix: 'approve' }, ]; async function upsertActions(parentCode: string, parentId: string) { const ids: string[] = []; for (let i = 0; i < ACTION_DEFS.length; i++) { const a = ACTION_DEFS[i]; const code = `${parentCode}:${a.suffix}`; const existing = await prisma.permission.findUnique({ where: { code } }); const data = { parentId, name: a.name, permType: 'ACTION', path: null as string | null, icon: null as string | null, sortNo: i + 1, visible: true, }; const row = existing ? await prisma.permission.update({ where: { id: existing.id }, data }) : await prisma.permission.create({ data: { ...data, code } }); ids.push(row.id); } return ids; } async function walk(nodes: MenuNode[], parentId: string | null, root = true) { const ids: string[] = []; for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; const row = await upsertNode(node, parentId, root ? (i + 1) * 10 : i + 1); ids.push(row.id); if (node.children?.length) ids.push(...(await walk(node.children, row.id, false))); else ids.push(...(await upsertActions(node.code, row.id))); } return ids; } async function main() { const kept = await walk(MENUS, null); const stale = await prisma.permission.findMany({ where: { permType: 'MENU', OR: [ { id: { notIn: kept }, code: { in: ['system:org'] } }, { code: { in: ['office:loan-approve', 'finance:expense-approve', 'finance:loan-approve', 'finance:bonds', 'contract:milestones', 'office:attendance', 'office:policies', 'project:budgets', 'project:changes'] } }, ], }, }); if (stale.length) { await prisma.permission.updateMany({ where: { id: { in: stale.map((s) => s.id) } }, data: { visible: false }, }); } const all = await prisma.permission.findMany({ where: { visible: true } }); const grantByCodes = (codes: string[]) => { if (codes[0] === '*') return all; return all.filter((p) => codes.some((c) => { if (c.endsWith(':')) return p.code === c.slice(0, -1) || p.code.startsWith(c); return p.code === c || p.code.startsWith(`${c}:`); }), ); }; const STAFF_OFFICE = [ 'office:overview', 'office', 'office:todos', 'office:approvals', 'office:calendar', 'office:reports', 'office:expenses', 'office:flow', 'office:hr-apply', 'office:seal-apply', 'office:apply', 'office:notices', 'office:directory', 'office:im', ]; const ROLE_GRANT: Record = { admin: ['*'], owner: ['*'], biz_director: ['office:', 'party:', 'seal:', 'bid:', 'contract:', 'finance:', 'report:', 'hr:', 'asset:', 'system:hr', 'system:org', 'system:attendance-rules'], biz_staff: ['office:', 'party:', 'seal:', 'bid:', 'contract:', 'finance:', 'asset:', 'hr:employees', 'hr:payroll', 'hr:performance', 'hr:attendance', 'hr:lifecycle'], tech_director: ['office:', 'bid:', 'project:', 'report:', 'hr:performance', 'hr:notices'], rd_director: ['*'], '3d_director': ['office:', 'bid:', 'project:', 'report:', 'hr:performance', 'hr:notices'], video_director: ['office:', 'bid:', 'project:', 'report:', 'hr:performance', 'hr:notices'], material_director: ['office:', 'bid:', 'project:', 'report:', 'hr:performance', 'hr:notices'], finance: ['office:', 'finance:', 'contract:', 'report:', 'bid:result', 'asset:'], hr: ['office:', 'hr:', 'contract:hr', 'system:hr', 'system:org', 'system:attendance-rules', 'system:acl'], employee: [...STAFF_OFFICE, 'project:timesheets'], bid_screener: ['office', 'office:overview', 'bid', 'bid:screening'], pm: [...STAFF_OFFICE, 'office:assign', 'project:'], rd_staff: [...STAFF_OFFICE, 'project:timesheets'], '3d_staff': [...STAFF_OFFICE, 'project:timesheets'], video_staff: [...STAFF_OFFICE, 'project:timesheets'], material_staff: [...STAFF_OFFICE, 'project:timesheets'], tech: [...STAFF_OFFICE, 'project:timesheets'], }; const roles = await prisma.role.findMany(); for (const role of roles) { if (role.aclCustom) continue; const codes = ROLE_GRANT[role.code]; if (!codes) continue; const wanted = grantByCodes(codes); const extra = role.code === 'hr' ? all.filter((p) => isHrSettingCode(p.code)) : []; const smsRoles = ['admin', 'owner', 'hr', ...DIRECTOR_ROLES]; const merged = [...new Map([...wanted, ...extra].map((p) => [p.id, p])).values()].filter((p) => { if (p.code === 'office:assign') return WORK_ASSIGN_ROLES.includes(role.code); if (p.code === 'office:sms') return smsRoles.includes(role.code); return true; }); const wantedIds = merged.map((p) => p.id); await prisma.rolePermission.deleteMany({ where: { roleId: role.id, permissionId: { notIn: wantedIds.length ? wantedIds : [''] } }, }); await prisma.rolePermission.createMany({ data: merged.map((p) => ({ roleId: role.id, permissionId: p.id })), skipDuplicates: true, }); } console.log(`menus synced: ${kept.length} visible, hid ${stale.length}`); } main() .then(async () => { await prisma.$disconnect(); }) .catch(async (e) => { console.error(e); await prisma.$disconnect(); process.exit(1); });