feat(c5): 角色权限矩阵 + 测试机构 seed-demo

- BUSINESS_ROLES 补全业务权限(director 只读/qc 主角色/leader 派单/nurse 留痕)
- seed-demo.ts 初始化「投云养老服务机构」+ 5 账号 + 完整样例数据

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
liuyun 2026-10-15 08:36:09 +08:00
parent 9d1a3b2374
commit f0e3deb6c8
3 changed files with 628 additions and 7 deletions

View File

@ -27,6 +27,7 @@
"test:debug": "vitest --inspect-brk --no-file-parallelism", "test:debug": "vitest --inspect-brk --no-file-parallelism",
"test:e2e": "vitest run --config ./vitest.config.e2e.ts", "test:e2e": "vitest run --config ./vitest.config.e2e.ts",
"seed": "prisma db seed", "seed": "prisma db seed",
"seed:demo": "tsx prisma/seed-demo.ts",
"sync:resources": "tsx prisma/sync-resources.ts" "sync:resources": "tsx prisma/sync-resources.ts"
}, },
"dependencies": { "dependencies": {

557
prisma/seed-demo.ts Normal file
View File

@ -0,0 +1,557 @@
import { PrismaClient } from '@prisma/client';
import bcrypt from 'bcryptjs';
import { TENANT_ADMIN_ROLE_CODE } from '../src/common/constants.js';
import { initTenant } from '../src/common/tenant-template.js';
/**
* 测试机构(TY20261015 投云养老服务机构)演示数据 seed。
* 幂等:可重复执行(tenant/user/userRole/staff/elder/svc_org 按业务键 upsert,plan/order/record/abnormal/qc 用固定 id)。
*
* 运行:pnpm exec tsx prisma/seed-demo.ts
*/
const prisma = new PrismaClient();
const TENANT_CODE = 'TY20261015';
const TENANT_NAME = '投云养老服务机构';
const ADMIN_USERNAME = 'admin01';
const PASSWORD = 'Touyun@123';
const BIZ_LINE = 'ltci';
const LOC_CODE = '530100';
// 固定业务 id(幂等锚点)
const IDS = {
staffLeader: 'demo-staff-leader-0001',
staffNurseZhang: 'demo-staff-nurse-0001',
staffNurseLi: 'demo-staff-nurse-0002',
staffNurseWang: 'demo-staff-nurse-0003',
planZhang: 'demo-plan-zhang-0001',
orderZhang: 'demo-order-zhang-0001',
recordZhang: 'demo-record-zhang-0001',
abnormalZhang: 'demo-abnormal-zhang-0001',
qcZhang: 'demo-qc-zhang-0001',
} as const;
// L1 目录样例(10 项)+ L4 拍照分档/配价(photoTier:沐浴/压疮/鼻饲/导管=must,擦浴=optional,其余 free)
const SERVICES: { natCode: string; natName: string; natCategory: string; isMedical: boolean; photoTier: string; price: string }[] = [
{ natCode: 'L0000106010020001', natName: '协助翻身', natCategory: 'life', isMedical: false, photoTier: 'free', price: '25.00' },
{ natCode: 'L0000106010020002', natName: '协助进食', natCategory: 'life', isMedical: false, photoTier: 'free', price: '25.00' },
{ natCode: 'L0000106010020003', natName: '口腔清洁', natCategory: 'life', isMedical: false, photoTier: 'free', price: '20.00' },
{ natCode: 'L0000106010020004', natName: '沐浴', natCategory: 'life', isMedical: false, photoTier: 'must', price: '50.00' },
{ natCode: 'L0000106010020005', natName: '压疮护理', natCategory: 'medical', isMedical: true, photoTier: 'must', price: '60.00' },
{ natCode: 'L0000106010020006', natName: '擦浴', natCategory: 'life', isMedical: false, photoTier: 'optional', price: '40.00' },
{ natCode: 'L0000106010020007', natName: '鼻饲', natCategory: 'medical', isMedical: true, photoTier: 'must', price: '45.00' },
{ natCode: 'L0000106010020008', natName: '导管护理', natCategory: 'medical', isMedical: true, photoTier: 'must', price: '55.00' },
{ natCode: 'L0000106010020009', natName: '血糖监测', natCategory: 'medical', isMedical: true, photoTier: 'free', price: '15.00' },
{ natCode: 'L0000106010020010', natName: '关节活动', natCategory: 'life', isMedical: false, photoTier: 'free', price: '30.00' },
];
// 业务角色账号(admin01 由 initTenant 创建并绑 tenant_super_admin)
const ACCOUNTS = [
{ username: 'director01', roleCode: 'director', nickname: '院长' },
{ username: 'qc01', roleCode: 'qc', nickname: '质控员' },
{ username: 'leader01', roleCode: 'leader', nickname: '组长' },
{ username: 'nurse01', roleCode: 'nurse', nickname: '护理员' },
];
// ---- 日期工具(@db.Date 用 UTC 零点,datetime 用本地墙上时间) ----
function pad(n: number): string {
return String(n).padStart(2, '0');
}
/** YYYY-MM-DD → @db.Date(UTC 零点)。 */
function dateOnly(s: string): Date {
return new Date(`${s}T00:00:00Z`);
}
/** 今天(本地)YYYY-MM-DD。 */
function todayStr(): string {
const d = new Date();
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
}
/** 距今天 N 天的 YYYY-MM-DD(本地)。 */
function daysFromToday(days: number): string {
const d = new Date();
d.setDate(d.getDate() + days);
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
}
/** 本月最后一天 YYYY-MM-DD。 */
function lastDayOfMonth(): string {
const d = new Date();
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate())}`;
}
/** 今天本地时间 HH:mm 的 Date。 */
function todayAt(hour: number, minute: number): Date {
const d = new Date();
d.setHours(hour, minute, 0, 0);
return d;
}
// ---- GB11643 校验位(生成合法 18 位身份证) ----
const ID_WEIGHTS = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
const ID_CHECK_CODES = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
function idCard(prefix17: string): string {
const sum = ID_WEIGHTS.reduce((acc, w, i) => acc + w * Number(prefix17[i]), 0);
return prefix17 + ID_CHECK_CODES[sum % 11];
}
// 关键老人身份证(张三用任务给定值,李四与 staff 用校验位生成)
const ID_ZHANG = '530102199001011231';
const ID_LI = idCard('53010219900303342');
// ---- 机构项目快照结构 ----
interface SvcRecord {
svcId: string;
svcName: string;
svcCode: string;
photoTier: string;
isMedical: boolean;
needCert: boolean;
}
function itemSnapshot(s: SvcRecord): Record<string, unknown> {
return {
svcId: s.svcId,
svcName: s.svcName,
svcCode: s.svcCode,
freqType: null,
freqValue: null,
durationMin: null,
photoTier: s.photoTier,
needCert: s.needCert,
isMedical: s.isMedical,
};
}
async function main(): Promise<void> {
// 1. 机构(租户)+ 平台化初始化(admin01 + 资源 + 内置字典 + business roles 授权)
await prisma.tenant.upsert({
where: { code: TENANT_CODE },
update: { name: TENANT_NAME, status: 'enabled' },
create: { code: TENANT_CODE, name: TENANT_NAME, status: 'enabled' },
});
await initTenant({
prisma,
tenantCode: TENANT_CODE,
tenantName: TENANT_NAME,
adminRoleCode: TENANT_ADMIN_ROLE_CODE,
adminRoleName: '机构管理员',
adminUsername: ADMIN_USERNAME,
adminPassword: PASSWORD,
includePlatformPerms: false,
forcePasswordChange: false,
});
// 2. 角色账号 + 绑角色
const passwordHash = await bcrypt.hash(PASSWORD, 10);
const userIds: Record<string, string> = {};
for (const acc of ACCOUNTS) {
const user = await prisma.user.upsert({
where: { tenantCode_username: { tenantCode: TENANT_CODE, username: acc.username } },
update: { nickname: acc.nickname, status: 'enabled' },
create: {
tenantCode: TENANT_CODE,
username: acc.username,
passwordHash,
nickname: acc.nickname,
status: 'enabled',
orgCode: TENANT_CODE,
},
});
userIds[acc.username] = user.id;
await prisma.userRole.upsert({
where: { tenantCode_userId_roleCode: { tenantCode: TENANT_CODE, userId: user.id, roleCode: acc.roleCode } },
update: {},
create: { tenantCode: TENANT_CODE, userId: user.id, roleCode: acc.roleCode },
});
}
// 3. staff:1 组长(刘组长,绑 leader01)+ 3 护理员(张/李/王,张护理绑 nurse01)
const leaderId = IDS.staffLeader;
await prisma.staff.upsert({
where: { id: leaderId },
update: {
userId: userIds.leader01,
name: '刘组长',
idCardNo: idCard('53010219850101004'),
jobType: 'leader',
workType: 'fulltime',
employStatus: 'on_job',
groupLeaderId: null,
},
create: {
id: leaderId,
tenantCode: TENANT_CODE,
userId: userIds.leader01,
name: '刘组长',
idCardNo: idCard('53010219850101004'),
phone: '13800000004',
jobType: 'leader',
workType: 'fulltime',
entryDate: dateOnly('2024-01-01'),
employStatus: 'on_job',
groupLeaderId: null,
},
});
const nurseDefs = [
{ id: IDS.staffNurseZhang, userId: userIds.nurse01, name: '张护理', idPrefix: '53010219880515001', phone: '13800000001' },
{ id: IDS.staffNurseLi, userId: null, name: '李护理', idPrefix: '53010219920310002', phone: '13800000002' },
{ id: IDS.staffNurseWang, userId: null, name: '王护理', idPrefix: '53010219941025003', phone: '13800000003' },
];
const nurseStaffIds: Record<string, string> = {};
for (const nd of nurseDefs) {
await prisma.staff.upsert({
where: { id: nd.id },
update: {
userId: nd.userId,
name: nd.name,
idCardNo: idCard(nd.idPrefix),
jobType: 'nurse',
workType: 'fulltime',
employStatus: 'on_job',
groupLeaderId: leaderId,
},
create: {
id: nd.id,
tenantCode: TENANT_CODE,
userId: nd.userId,
name: nd.name,
idCardNo: idCard(nd.idPrefix),
phone: nd.phone,
jobType: 'nurse',
workType: 'fulltime',
entryDate: dateOnly('2024-06-01'),
employStatus: 'on_job',
groupLeaderId: leaderId,
},
});
nurseStaffIds[nd.name] = nd.id;
}
const zhangStaffId = nurseStaffIds['张护理'];
// 4. L1 国家目录 + L4 机构项目库(srcId = svc_national.id,与 plan/record 解析逻辑一致)
const svcs: SvcRecord[] = [];
for (const svc of SERVICES) {
const national = await prisma.svcNational.upsert({
where: { natCode: svc.natCode },
update: { natName: svc.natName, natCategory: svc.natCategory, isMedical: svc.isMedical },
create: { natCode: svc.natCode, natName: svc.natName, natCategory: svc.natCategory, isMedical: svc.isMedical },
});
const existing = await prisma.svcOrg.findFirst({ where: { tenantCode: TENANT_CODE, srcId: national.id } });
let svcOrgId: string;
if (existing) {
await prisma.svcOrg.update({
where: { id: existing.id },
data: { srcType: 'national', isEnabled: true, price: svc.price, photoTier: svc.photoTier },
});
svcOrgId = existing.id;
} else {
const created = await prisma.svcOrg.create({
data: {
tenantCode: TENANT_CODE,
srcType: 'national',
srcId: national.id,
isEnabled: true,
price: svc.price,
photoTier: svc.photoTier,
},
});
svcOrgId = created.id;
}
svcs.push({
svcId: svcOrgId,
svcName: svc.natName,
svcCode: svc.natCode,
photoTier: svc.photoTier,
isMedical: svc.isMedical,
needCert: svc.isMedical,
});
}
// 5. 老人:张三(active)、李四(waiting_period,垫付演示)
const zhang = await prisma.elder.upsert({
where: { tenantCode_idCardNo: { tenantCode: TENANT_CODE, idCardNo: ID_ZHANG } },
update: {},
create: {
tenantCode: TENANT_CODE,
locCode: LOC_CODE,
name: '张三',
gender: 'M',
birthDate: dateOnly('1990-01-01'),
idCardNo: ID_ZHANG,
phone: '13900000001',
address: '昆明市五华区春城路 1 号',
guardianName: '张小明',
guardianPhone: '13900000002',
insurNo: 'YN5301000001',
insurType: 'employee',
careLevel: 'care_1',
treatStatus: 'active',
benefitStart: dateOnly(daysFromToday(-365)),
benefitEnd: dateOnly(daysFromToday(365)),
bizLine: BIZ_LINE,
},
});
await prisma.elder.upsert({
where: { tenantCode_idCardNo: { tenantCode: TENANT_CODE, idCardNo: ID_LI } },
update: {},
create: {
tenantCode: TENANT_CODE,
locCode: LOC_CODE,
name: '李四',
gender: 'F',
birthDate: dateOnly('1990-03-03'),
idCardNo: ID_LI,
phone: '13900000003',
address: '昆明市盘龙区东风路 2 号',
insurNo: 'YN5301000002',
insurType: 'resident',
careLevel: 'care_2',
treatStatus: 'waiting_period',
benefitStart: dateOnly(daysFromToday(30)),
benefitEnd: dateOnly(daysFromToday(400)),
bizLine: BIZ_LINE,
},
});
// 6. 计划(张三,active,覆盖本月)+ 计划项(勾选全部 10 项,photoTier 快照自 svc_org)
const planId = IDS.planZhang;
const planPeriodStart = `${new Date().getFullYear()}-${pad(new Date().getMonth() + 1)}-01`;
await prisma.plan.upsert({
where: { id: planId },
update: {
elderId: zhang.id,
planPeriodStart: dateOnly(planPeriodStart),
planPeriodEnd: dateOnly(lastDayOfMonth()),
careLevel: 'care_1',
careMode: 'home_individual',
itemCount: svcs.length,
planStatus: 'active',
},
create: {
id: planId,
tenantCode: TENANT_CODE,
elderId: zhang.id,
planPeriodStart: dateOnly(planPeriodStart),
planPeriodEnd: dateOnly(lastDayOfMonth()),
careLevel: 'care_1',
careMode: 'home_individual',
itemCount: svcs.length,
planStatus: 'active',
bizLine: BIZ_LINE,
},
});
await prisma.planItem.deleteMany({ where: { planId } });
for (const s of svcs) {
await prisma.planItem.create({
data: {
planId,
tenantCode: TENANT_CODE,
svcId: s.svcId,
svcNameSnapshot: s.svcName,
svcCodeSnapshot: s.svcCode,
photoTier: s.photoTier,
isMedical: s.isMedical,
needCert: s.needCert,
},
});
}
// 7. 工单(张三,今天 09:00-10:00,派给张护理,itemList 快照自 plan_item)
const orderId = IDS.orderZhang;
const itemList = svcs.map(itemSnapshot);
const needPhoto = svcs.some((s) => s.photoTier === 'must');
await prisma.order.upsert({
where: { id: orderId },
update: {
elderId: zhang.id,
planId,
serviceDate: dateOnly(todayStr()),
timeSlotStart: todayAt(9, 0),
timeSlotEnd: todayAt(10, 0),
assigneeId: zhangStaffId,
orderStatus: 'assigned',
itemList,
needPhoto,
},
create: {
id: orderId,
tenantCode: TENANT_CODE,
elderId: zhang.id,
planId,
serviceDate: dateOnly(todayStr()),
timeSlotStart: todayAt(9, 0),
timeSlotEnd: todayAt(10, 0),
assigneeId: zhangStaffId,
assignTime: new Date(),
assignMode: 'manual',
orderStatus: 'assigned',
itemList,
needPhoto,
billable: true,
bizLine: BIZ_LINE,
},
});
// 8. 留痕(张三,submitted)+ 明细 + 照片
const recordId = IDS.recordZhang;
await prisma.record.upsert({
where: { id: recordId },
update: {
orderId,
elderId: zhang.id,
assigneeId: zhangStaffId,
checkinTime: todayAt(9, 0),
checkoutTime: todayAt(10, 0),
durationMin: 60,
recordStatus: 'submitted',
},
create: {
id: recordId,
tenantCode: TENANT_CODE,
orderId,
elderId: zhang.id,
assigneeId: zhangStaffId,
checkinTime: todayAt(9, 0),
checkoutTime: todayAt(10, 0),
durationMin: 60,
recordStatus: 'submitted',
submitTime: todayAt(10, 0),
isPrepaid: false,
billable: true,
bizLine: BIZ_LINE,
},
});
await prisma.recordItem.deleteMany({ where: { recordId } });
for (const s of svcs) {
await prisma.recordItem.create({
data: {
tenantCode: TENANT_CODE,
recordId,
svcId: s.svcId,
svcNameSnapshot: s.svcName,
execResult: 'done',
photoRequired: s.photoTier === 'must',
},
});
}
await prisma.recordPhoto.deleteMany({ where: { recordId } });
const mustSvc = svcs.find((s) => s.photoTier === 'must')!;
await prisma.recordPhoto.create({
data: {
tenantCode: TENANT_CODE,
recordId,
svcId: mustSvc.svcId,
filePath: '/demo/TY20261015/record/zhang/photo1.jpg',
fileSize: 128000,
visibleToFamily: false,
},
});
// 9. 异常上报(压疮)
await prisma.recordAbnormal.upsert({
where: { id: IDS.abnormalZhang },
update: {
recordId,
elderId: zhang.id,
reporterId: zhangStaffId,
abnormalType: 'pressure_sore',
notifyStatus: 'pending',
handleStatus: 'open',
},
create: {
id: IDS.abnormalZhang,
tenantCode: TENANT_CODE,
recordId,
orderId,
elderId: zhang.id,
reporterId: zhangStaffId,
abnormalType: 'pressure_sore',
abnormalDesc: '发现背部皮肤压红,需关注',
notifyStatus: 'pending',
handleStatus: 'open',
},
});
// 10. 质控(pending)
await prisma.qc.upsert({
where: { id: IDS.qcZhang },
update: { recordId, qcType: 'normal', qcResult: 'pending' },
create: {
id: IDS.qcZhang,
tenantCode: TENANT_CODE,
recordId,
qcType: 'normal',
qcResult: 'pending',
},
});
console.log(`seed-demo 完成:机构 ${TENANT_CODE} ${TENANT_NAME}`);
}
async function verify(): Promise<void> {
const tenant = await prisma.tenant.findUnique({ where: { code: TENANT_CODE } });
const users = await prisma.user.findMany({ where: { tenantCode: TENANT_CODE }, orderBy: { username: 'asc' } });
const roles = await prisma.role.findMany({ where: { tenantCode: TENANT_CODE }, orderBy: { code: 'asc' } });
const userRoles = await prisma.userRole.findMany({ where: { tenantCode: TENANT_CODE } });
const staff = await prisma.staff.findMany({ where: { tenantCode: TENANT_CODE }, orderBy: { name: 'asc' } });
const elders = await prisma.elder.findMany({ where: { tenantCode: TENANT_CODE } });
const plans = await prisma.plan.findMany({ where: { tenantCode: TENANT_CODE } });
const planItems = await prisma.planItem.findMany({ where: { tenantCode: TENANT_CODE } });
const orders = await prisma.order.findMany({ where: { tenantCode: TENANT_CODE } });
const records = await prisma.record.findMany({ where: { tenantCode: TENANT_CODE } });
const recordItems = await prisma.recordItem.findMany({ where: { tenantCode: TENANT_CODE } });
const recordPhotos = await prisma.recordPhoto.findMany({ where: { tenantCode: TENANT_CODE } });
const abnormals = await prisma.recordAbnormal.findMany({ where: { tenantCode: TENANT_CODE } });
const qcs = await prisma.qc.findMany({ where: { tenantCode: TENANT_CODE } });
const svcOrgs = await prisma.svcOrg.findMany({ where: { tenantCode: TENANT_CODE } });
const roleByUser: Record<string, string[]> = {};
for (const ur of userRoles) {
(roleByUser[ur.userId] ??= []).push(ur.roleCode);
}
const nameByUser = new Map(users.map((u) => [u.id, u.username]));
console.log('\n===== 验证结果 =====');
console.log(`tenant: ${tenant ? '存在 ' + tenant.name : '缺失'} (code=${TENANT_CODE})`);
console.log(`账号数: ${users.length}(期望 5)`);
for (const u of users) {
console.log(` - ${u.username} → [${(roleByUser[u.id] ?? []).join(',')}]`);
}
console.log(`角色数: ${roles.length}(business roles: ${roles.map((r) => r.code).join('/')})`);
console.log(`staff: ${staff.length}(期望 4)→ ${staff.map((s) => `${s.name}(${s.jobType},${s.employStatus})`).join(' / ')}`);
console.log(`elder: ${elders.length}(期望 2)→ ${elders.map((e) => `${e.name}(${e.treatStatus},${e.careLevel})`).join(' / ')}`);
console.log(`svc_org(L4): ${svcOrgs.length}(期望 10)`);
console.log(`plan: ${plans.length}(期望 1); plan_item: ${planItems.length}(期望 10)`);
console.log(`order: ${orders.length}(期望 1); record: ${records.length}(期望 1); record_item: ${recordItems.length}(期望 10); record_photo: ${recordPhotos.length}(期望 1)`);
console.log(`record_abnormal: ${abnormals.length}(期望 1); qc: ${qcs.length}(期望 1)`);
console.log('\n===== 测试账号 =====');
const expected = ['admin01', 'director01', 'qc01', 'leader01', 'nurse01'];
for (const uname of expected) {
const u = users.find((x) => x.username === uname);
console.log(` ${uname} / ${PASSWORD} → [${(u ? roleByUser[u.id] ?? [] : []).join(',')}]`);
}
// 未绑任何角色的多余账号告警
const orphans = users.filter((u) => !expected.includes(u.username));
if (orphans.length > 0) {
console.log(`\n⚠️ 存在非预期账号: ${orphans.map((u) => `${u.username}[${nameByUser.get(u.id)}]`).join(', ')}`);
}
}
main()
.then(() => verify())
.catch((e) => {
console.error(e);
process.exitCode = 1;
})
.finally(async () => {
await prisma.$disconnect();
});

View File

@ -145,7 +145,24 @@ export const BUTTONS = [
/** 业务租户预置角色(C0:nurse/leader/qc/admin/director)及初始授权。 */ /** 业务租户预置角色(C0:nurse/leader/qc/admin/director)及初始授权。 */
export const BUSINESS_ROLES: { code: string; name: string; permissions: string[] }[] = [ export const BUSINESS_ROLES: { code: string; name: string; permissions: string[] }[] = [
{ code: 'director', name: '院长', permissions: [Permissions.agency.view] }, {
code: 'director',
name: '院长',
permissions: [
Permissions.agency.view,
Permissions.elder.list,
Permissions.elder.detail,
Permissions.plan.detail,
Permissions.order.detail,
Permissions.order.today,
Permissions.record.list,
Permissions.record.detail,
Permissions.abnormal.list,
Permissions.qc.pendingList,
Permissions.qc.detail,
Permissions.export.export,
],
},
{ {
code: 'admin', code: 'admin',
name: '机构管理员', name: '机构管理员',
@ -169,21 +186,67 @@ export const BUSINESS_ROLES: { code: string; name: string; permissions: string[]
Permissions.agency.view, Permissions.agency.view,
Permissions.staff.list, Permissions.staff.list,
Permissions.svcOrg.list, Permissions.svcOrg.list,
Permissions.svcOrg.config, Permissions.elder.list,
Permissions.svcOrg.newNational, Permissions.elder.create,
Permissions.elder.detail,
Permissions.elder.statusChange,
Permissions.elder.svcSave,
Permissions.plan.create,
Permissions.plan.change,
Permissions.plan.detail,
Permissions.plan.confirm,
Permissions.order.detail,
Permissions.order.today,
Permissions.record.list,
Permissions.record.detail,
Permissions.abnormal.list,
Permissions.abnormal.report,
Permissions.abnormal.handle,
Permissions.qc.pendingList, Permissions.qc.pendingList,
Permissions.qc.detail, Permissions.qc.detail,
Permissions.qc.pass, Permissions.qc.pass,
Permissions.qc.return, Permissions.qc.return,
Permissions.qc.exceptionLink, Permissions.qc.exceptionLink,
Permissions.qc.batchPass, Permissions.qc.batchPass,
Permissions.abnormal.list,
Permissions.abnormal.handle,
Permissions.export.export, Permissions.export.export,
], ],
}, },
{ code: 'leader', name: '组长', permissions: [Permissions.staff.list, Permissions.svcOrg.list] }, {
{ code: 'nurse', name: '护理员', permissions: [] }, code: 'leader',
name: '组长',
permissions: [
Permissions.staff.list,
Permissions.svcOrg.list,
Permissions.elder.list,
Permissions.elder.detail,
Permissions.plan.detail,
Permissions.order.detail,
Permissions.order.today,
Permissions.order.assign,
Permissions.order.reassign,
Permissions.record.list,
Permissions.record.detail,
Permissions.abnormal.list,
Permissions.qc.pendingList,
Permissions.qc.detail,
],
},
{
code: 'nurse',
name: '护理员',
permissions: [
Permissions.record.checkin,
Permissions.record.itemSave,
Permissions.record.photoUpload,
Permissions.record.checkout,
Permissions.record.offlineSync,
Permissions.record.detail,
Permissions.record.list,
Permissions.qc.correct,
Permissions.order.detail,
Permissions.order.today,
],
},
]; ];
/** 内置枚举字典(值对齐 enums.ts,仅展示层 label/颜色/排序可改)。 */ /** 内置枚举字典(值对齐 enums.ts,仅展示层 label/颜色/排序可改)。 */