调压任务收束

This commit is contained in:
EL 2026-03-24 20:09:20 +08:00
parent 6a3eb49ab6
commit 21941e94fd
5 changed files with 132 additions and 24 deletions

View File

@ -84,7 +84,7 @@ export class TaskService {
const page = query.page ?? 1;
const pageSize = query.pageSize ?? 20;
const skip = (page - 1) * pageSize;
const where = this.buildTaskRecordWhere(query, hospitalId);
const where = this.buildTaskRecordWhere(actor, query, hospitalId);
const [total, items] = await Promise.all([
this.prisma.taskItem.count({ where }),
@ -608,16 +608,23 @@ export class TaskService {
*
*/
private buildTaskRecordWhere(
actor: ActorContext,
query: TaskRecordQueryDto,
hospitalId?: number,
): Prisma.TaskItemWhereInput {
const keyword = query.keyword?.trim();
const patientScope = this.buildTaskPatientScope(actor);
const where: Prisma.TaskItemWhereInput = {
task: {
hospitalId,
status: query.status,
},
device: patientScope
? {
patient: patientScope,
}
: undefined,
};
if (!keyword) {
@ -676,6 +683,42 @@ export class TaskService {
return where;
}
/**
*
*/
private buildTaskPatientScope(actor: ActorContext): Prisma.PatientWhereInput | null {
switch (actor.role) {
case Role.SYSTEM_ADMIN:
case Role.HOSPITAL_ADMIN:
case Role.ENGINEER:
return null;
case Role.DOCTOR:
return {
doctorId: actor.id,
};
case Role.LEADER:
if (!actor.groupId) {
throw new BadRequestException(MESSAGES.PATIENT.GROUP_REQUIRED);
}
return {
doctor: {
groupId: actor.groupId,
},
};
case Role.DIRECTOR:
if (!actor.departmentId) {
throw new BadRequestException(MESSAGES.PATIENT.DEPARTMENT_REQUIRED);
}
return {
doctor: {
departmentId: actor.departmentId,
},
};
default:
throw new ForbiddenException(MESSAGES.TASK.ACTOR_ROLE_FORBIDDEN);
}
}
/**
*
*/

View File

@ -26,6 +26,7 @@ describe('BTasksController (e2e)', () => {
let ctx: E2EContext;
let samplePngBuffer: Buffer;
let doctorBToken = '';
let doctorA2Token = '';
beforeAll(async () => {
ctx = await createE2EContext();
@ -44,6 +45,11 @@ describe('BTasksController (e2e)', () => {
Role.DOCTOR,
ctx.fixtures.hospitalBId,
);
doctorA2Token = await loginByUser(
ctx.fixtures.users.doctorA2Id,
Role.DOCTOR,
ctx.fixtures.hospitalAId,
);
});
afterAll(async () => {
@ -293,21 +299,53 @@ describe('BTasksController (e2e)', () => {
);
});
it('成功DOCTOR 仅可查看本院调压记录', async () => {
it('成功DOCTOR 仅可查看本人患者调压记录', async () => {
const keywordPrefix = uniqueSeedValue('scope-doctor');
const [selfDevice] = await createAdjustableDevices({
actorToken: ctx.tokens[Role.DOCTOR],
doctorId: ctx.fixtures.users.doctorAId,
patientName: `${keywordPrefix}-self`,
});
const [peerDevice] = await createAdjustableDevices({
actorToken: doctorA2Token,
doctorId: ctx.fixtures.users.doctorA2Id,
patientName: `${keywordPrefix}-peer`,
});
await publishPendingTask(
selfDevice.id,
'1.5',
ctx.tokens[Role.SYSTEM_ADMIN],
);
await publishPendingTask(
peerDevice.id,
'1.5',
ctx.tokens[Role.SYSTEM_ADMIN],
);
const response = await request(ctx.app.getHttpServer())
.get('/b/tasks')
.set('Authorization', `Bearer ${ctx.tokens[Role.DOCTOR]}`)
.query({ page: 1, pageSize: 20 });
.query({ page: 1, pageSize: 50, keyword: keywordPrefix });
expectSuccessEnvelope(response, 200);
expect(Array.isArray(response.body.data.list)).toBe(true);
expect(response.body.data.total).toBeGreaterThan(0);
expect(
response.body.data.list.every(
(item: { hospital?: { id?: number } }) =>
item.hospital?.id === ctx.fixtures.hospitalAId,
(item: {
hospital?: { id?: number };
patient?: { name?: string };
}) =>
item.hospital?.id === ctx.fixtures.hospitalAId &&
item.patient?.name?.includes(`${keywordPrefix}-self`),
),
).toBe(true);
expect(
response.body.data.list.some((item: { patient?: { name?: string } }) =>
item.patient?.name?.includes(`${keywordPrefix}-peer`),
),
).toBe(false);
});
it('失败hospitalId 非法返回 400', async () => {

View File

@ -78,7 +78,7 @@
</el-table-column>
<el-table-column prop="phone" label="联系电话" min-width="140" />
<el-table-column prop="idCard" label="身份证号" min-width="200" />
<el-table-column label="归属医院" min-width="150">
<el-table-column v-if="canViewHospitalInfo" label="归属医院" min-width="150">
<template #default="{ row }">
{{ row.hospital?.name || '-' }}
</template>
@ -421,7 +421,7 @@
<el-descriptions-item label="创建人">
{{ detailPatient.creator?.name || '-' }}
</el-descriptions-item>
<el-descriptions-item label="归属医院">
<el-descriptions-item v-if="canViewHospitalInfo" label="归属医院">
{{ detailPatient.hospital?.name || '-' }}
</el-descriptions-item>
<el-descriptions-item label="建档时间">
@ -716,7 +716,7 @@
{{ row.surgery?.surgeryName || '-' }}
</template>
</el-table-column>
<el-table-column label="医院" min-width="150">
<el-table-column v-if="canViewHospitalInfo" label="医院" min-width="150">
<template #default="{ row }">
{{ row.hospital?.name || '-' }}
</template>
@ -791,6 +791,9 @@ const doctorTreeProps = {
};
const isSystemAdmin = computed(() => userStore.role === 'SYSTEM_ADMIN');
const canViewHospitalInfo = computed(() =>
['SYSTEM_ADMIN', 'HOSPITAL_ADMIN'].includes(userStore.role),
);
const canPublishAdjustTask = computed(() =>
['SYSTEM_ADMIN', 'HOSPITAL_ADMIN', 'DOCTOR', 'DIRECTOR', 'LEADER'].includes(
userStore.role,

View File

@ -193,13 +193,7 @@
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column
v-if="isEngineer"
label="操作"
width="220"
fixed="right"
align="center"
>
<el-table-column label="操作" width="220" fixed="right" align="center">
<template #default="{ row }">
<el-button
v-if="canAccept(row)"
@ -218,7 +212,7 @@
:loading="actionTaskId === row.taskId && actionType === 'cancel'"
@click="handleCancel(row)"
>
取消接收
{{ getCancelButtonText(row) }}
</el-button>
<el-button
v-if="canComplete(row)"
@ -372,6 +366,11 @@ const userStore = useUserStore();
const isSystemAdmin = computed(() => userStore.role === 'SYSTEM_ADMIN');
const isEngineer = computed(() => userStore.role === 'ENGINEER');
const canCreatorCancel = computed(() =>
['SYSTEM_ADMIN', 'HOSPITAL_ADMIN', 'DOCTOR', 'DIRECTOR', 'LEADER'].includes(
userStore.role,
),
);
const currentUserId = computed(() => userStore.userInfo?.id || null);
const uploadHospitalId = computed(() => userStore.userInfo?.hospitalId || null);
const pageAlertTitle = computed(() =>
@ -454,11 +453,27 @@ function canComplete(row) {
}
function canCancel(row) {
return (
const canEngineerRelease =
isEngineer.value &&
row?.status === 'ACCEPTED' &&
row?.engineer?.id === currentUserId.value
);
row?.engineer?.id === currentUserId.value;
const canCreatorCancelTask =
canCreatorCancel.value &&
row?.status &&
['PENDING', 'ACCEPTED'].includes(row.status) &&
row?.creator?.id === currentUserId.value;
return canEngineerRelease || canCreatorCancelTask;
}
function getCancelButtonText(row) {
const isEngineerRelease =
isEngineer.value &&
row?.status === 'ACCEPTED' &&
row?.engineer?.id === currentUserId.value;
return isEngineerRelease ? '取消接收' : '取消任务';
}
function isImageMaterial(material) {
@ -580,13 +595,22 @@ function removeCompletionMaterial(index) {
}
async function handleCancel(row) {
const isEngineerRelease =
isEngineer.value && row?.status === 'ACCEPTED' && row?.engineer?.id === currentUserId.value;
const confirmTitle = isEngineerRelease ? '取消接收' : '取消任务';
const confirmText = isEngineerRelease
? '取消接收后,任务会退回待接收状态,其他同院工程师可重新接收,是否继续?'
: '取消后任务将变更为已取消状态,是否继续?';
const confirmButtonText = isEngineerRelease ? '确认取消接收' : '确认取消任务';
const successText = isEngineerRelease ? '任务已退回待接收' : '任务已取消';
try {
await ElMessageBox.confirm(
'取消接收后,任务会退回待接收状态,其他同院工程师可重新接收,是否继续?',
'取消接收',
confirmText,
confirmTitle,
{
type: 'warning',
confirmButtonText: '确认取消接收',
confirmButtonText,
cancelButtonText: '返回',
},
);
@ -598,7 +622,7 @@ async function handleCancel(row) {
actionType.value = 'cancel';
try {
await cancelTask({ taskId: row.taskId });
ElMessage.success('任务已退回待接收');
ElMessage.success(successText);
await fetchData();
} finally {
actionTaskId.value = null;

View File

@ -58,7 +58,7 @@
</el-tag>
</template>
</el-table-column>
<el-table-column label="所属医院" min-width="150">
<el-table-column v-if="canManageUsers" label="所属医院" min-width="150">
<template #default="{ row }">
{{ resolveHospitalName(row.hospitalId) }}
</template>