新增调压记录详情接口并完善设备删除能力与前台操作
This commit is contained in:
parent
cfd2f1e8dc
commit
5f7d66ce54
@ -74,6 +74,9 @@ export const MESSAGES = {
|
||||
},
|
||||
|
||||
TASK: {
|
||||
RECORD_NOT_FOUND: '调压记录不存在或无权限访问',
|
||||
UPDATE_ONLY_PENDING: '仅待处理调压记录可编辑',
|
||||
DELETE_ONLY_PENDING_CANCELLED: '仅待处理/已取消调压记录可删除',
|
||||
ITEMS_REQUIRED: '任务明细 items 不能为空',
|
||||
DEVICE_NOT_FOUND: '存在设备不在当前医院或设备不存在',
|
||||
DEVICE_MULTI_HOSPITAL: '同一批调压任务中的设备必须属于同一家医院',
|
||||
|
||||
@ -63,6 +63,7 @@ export function buildPatientLifecyclePatient(patient: PatientLifecycleSource) {
|
||||
id: toJsonNumber(patient.id),
|
||||
name: patient.name,
|
||||
inpatientNo: patient.inpatientNo,
|
||||
phone: patient.phone,
|
||||
projectName: patient.projectName,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,4 +1,13 @@
|
||||
import { Body, Controller, Get, Post, Query, UseGuards } from '@nestjs/common';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Param,
|
||||
ParseIntPipe,
|
||||
Post,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiBearerAuth,
|
||||
ApiOperation,
|
||||
@ -19,9 +28,6 @@ import { CancelTaskDto } from '../dto/cancel-task.dto.js';
|
||||
import { TaskRecordQueryDto } from '../dto/task-record-query.dto.js';
|
||||
import { AssignableEngineerQueryDto } from '../dto/assignable-engineer-query.dto.js';
|
||||
|
||||
/**
|
||||
* B 端任务控制器:封装调压任务状态流转接口。
|
||||
*/
|
||||
@ApiTags('调压任务(B端)')
|
||||
@ApiBearerAuth('bearer')
|
||||
@Controller('b/tasks')
|
||||
@ -29,9 +35,6 @@ import { AssignableEngineerQueryDto } from '../dto/assignable-engineer-query.dto
|
||||
export class BTasksController {
|
||||
constructor(private readonly taskService: TaskService) {}
|
||||
|
||||
/**
|
||||
* 查询当前角色可见的医院工程师列表。
|
||||
*/
|
||||
@Get('engineers')
|
||||
@Roles(
|
||||
Role.SYSTEM_ADMIN,
|
||||
@ -53,9 +56,6 @@ export class BTasksController {
|
||||
return this.taskService.findAssignableEngineers(actor, query.hospitalId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前角色可见的调压记录列表。
|
||||
*/
|
||||
@Get()
|
||||
@Roles(
|
||||
Role.SYSTEM_ADMIN,
|
||||
@ -78,9 +78,28 @@ export class BTasksController {
|
||||
return this.taskService.findTaskRecords(actor, query);
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统管理员/医院管理员/医生/主任/组长发布调压任务。
|
||||
*/
|
||||
@Get('items/:id')
|
||||
@Roles(
|
||||
Role.SYSTEM_ADMIN,
|
||||
Role.HOSPITAL_ADMIN,
|
||||
Role.DOCTOR,
|
||||
Role.DIRECTOR,
|
||||
Role.LEADER,
|
||||
Role.ENGINEER,
|
||||
)
|
||||
@ApiOperation({ summary: '查询单条调压记录详情' })
|
||||
@ApiQuery({
|
||||
name: 'id',
|
||||
required: true,
|
||||
description: '调压记录 ID (taskItem.id)',
|
||||
})
|
||||
findRecordItem(
|
||||
@CurrentActor() actor: ActorContext,
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
) {
|
||||
return this.taskService.getTaskRecordItem(actor, id);
|
||||
}
|
||||
|
||||
@Post('publish')
|
||||
@Roles(
|
||||
Role.SYSTEM_ADMIN,
|
||||
@ -96,9 +115,6 @@ export class BTasksController {
|
||||
return this.taskService.publishTask(actor, dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 工程师接收调压任务。
|
||||
*/
|
||||
@Post('accept')
|
||||
@Roles(Role.ENGINEER)
|
||||
@ApiOperation({ summary: '接收任务(ENGINEER)' })
|
||||
@ -106,9 +122,6 @@ export class BTasksController {
|
||||
return this.taskService.acceptTask(actor, dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 工程师完成调压任务。
|
||||
*/
|
||||
@Post('complete')
|
||||
@Roles(Role.ENGINEER)
|
||||
@ApiOperation({ summary: '完成任务(ENGINEER)' })
|
||||
@ -116,9 +129,6 @@ export class BTasksController {
|
||||
return this.taskService.completeTask(actor, dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统管理员/医院管理员/医生/主任/组长可取消自己创建的任务;工程师可取消自己已接收的任务。
|
||||
*/
|
||||
@Post('cancel')
|
||||
@Roles(
|
||||
Role.SYSTEM_ADMIN,
|
||||
|
||||
@ -23,6 +23,64 @@ import { TaskRecordQueryDto } from './dto/task-record-query.dto.js';
|
||||
import { MESSAGES } from '../common/messages.js';
|
||||
import { normalizePressureLabel } from '../common/pressure-level.util.js';
|
||||
|
||||
const TASK_RECORD_DETAIL_SELECT = {
|
||||
id: true,
|
||||
oldPressure: true,
|
||||
targetPressure: true,
|
||||
task: {
|
||||
select: {
|
||||
id: true,
|
||||
status: true,
|
||||
createdAt: true,
|
||||
creatorId: true,
|
||||
engineerId: true,
|
||||
hospitalId: true,
|
||||
completionMaterials: true,
|
||||
},
|
||||
},
|
||||
device: {
|
||||
select: {
|
||||
id: true,
|
||||
currentPressure: true,
|
||||
status: true,
|
||||
isAbandoned: true,
|
||||
implantModel: true,
|
||||
implantManufacturer: true,
|
||||
implantName: true,
|
||||
isPressureAdjustable: true,
|
||||
implantCatalog: {
|
||||
select: {
|
||||
pressureLevels: true,
|
||||
},
|
||||
},
|
||||
patient: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
inpatientNo: true,
|
||||
phone: true,
|
||||
hospitalId: true,
|
||||
doctorId: true,
|
||||
doctor: {
|
||||
select: {
|
||||
departmentId: true,
|
||||
groupId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
surgery: {
|
||||
select: {
|
||||
id: true,
|
||||
surgeryName: true,
|
||||
surgeryDate: true,
|
||||
primaryDisease: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 任务服务:封装调压任务状态机、院内隔离与事件发布逻辑。
|
||||
*/
|
||||
@ -67,6 +125,20 @@ export class TaskService {
|
||||
});
|
||||
}
|
||||
|
||||
async getTaskRecordItem(actor: ActorContext, id: number) {
|
||||
this.assertRole(actor, [
|
||||
Role.SYSTEM_ADMIN,
|
||||
Role.HOSPITAL_ADMIN,
|
||||
Role.DOCTOR,
|
||||
Role.DIRECTOR,
|
||||
Role.LEADER,
|
||||
Role.ENGINEER,
|
||||
]);
|
||||
|
||||
const item = await this.findTaskRecordItemWithScope(actor, id);
|
||||
return this.mapTaskRecordItemDetail(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前角色可见的调压记录列表。
|
||||
*/
|
||||
@ -530,6 +602,106 @@ export class TaskService {
|
||||
/**
|
||||
* 校验角色权限。
|
||||
*/
|
||||
private async findTaskRecordItemWithScope(
|
||||
actor: ActorContext,
|
||||
id: number,
|
||||
) {
|
||||
const item = await this.prisma.taskItem.findUnique({
|
||||
where: { id },
|
||||
select: TASK_RECORD_DETAIL_SELECT,
|
||||
});
|
||||
|
||||
if (!item) {
|
||||
throw new NotFoundException(MESSAGES.TASK.RECORD_NOT_FOUND);
|
||||
}
|
||||
|
||||
this.assertTaskRecordScope(actor, item);
|
||||
return item;
|
||||
}
|
||||
|
||||
private assertTaskRecordScope(
|
||||
actor: ActorContext,
|
||||
item: Prisma.TaskItemGetPayload<{
|
||||
select: typeof TASK_RECORD_DETAIL_SELECT;
|
||||
}>,
|
||||
) {
|
||||
const patient = item.device.patient;
|
||||
|
||||
switch (actor.role) {
|
||||
case Role.SYSTEM_ADMIN:
|
||||
return;
|
||||
case Role.HOSPITAL_ADMIN:
|
||||
case Role.ENGINEER: {
|
||||
const hospitalId = this.requireHospitalId(actor);
|
||||
if (hospitalId !== item.task.hospitalId) {
|
||||
throw new NotFoundException(MESSAGES.TASK.RECORD_NOT_FOUND);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case Role.DOCTOR:
|
||||
if (patient.doctorId !== actor.id) {
|
||||
throw new NotFoundException(MESSAGES.TASK.RECORD_NOT_FOUND);
|
||||
}
|
||||
return;
|
||||
case Role.LEADER:
|
||||
if (!actor.groupId) {
|
||||
throw new BadRequestException(MESSAGES.PATIENT.GROUP_REQUIRED);
|
||||
}
|
||||
if (patient.doctor?.groupId !== actor.groupId) {
|
||||
throw new NotFoundException(MESSAGES.TASK.RECORD_NOT_FOUND);
|
||||
}
|
||||
return;
|
||||
case Role.DIRECTOR:
|
||||
if (!actor.departmentId) {
|
||||
throw new BadRequestException(MESSAGES.PATIENT.DEPARTMENT_REQUIRED);
|
||||
}
|
||||
if (patient.doctor?.departmentId !== actor.departmentId) {
|
||||
throw new NotFoundException(MESSAGES.TASK.RECORD_NOT_FOUND);
|
||||
}
|
||||
return;
|
||||
default:
|
||||
throw new ForbiddenException(MESSAGES.TASK.ACTOR_ROLE_FORBIDDEN);
|
||||
}
|
||||
}
|
||||
|
||||
private mapTaskRecordItemDetail(
|
||||
item: Prisma.TaskItemGetPayload<{
|
||||
select: typeof TASK_RECORD_DETAIL_SELECT;
|
||||
}>,
|
||||
) {
|
||||
return {
|
||||
id: item.id,
|
||||
oldPressure: item.oldPressure,
|
||||
targetPressure: item.targetPressure,
|
||||
task: {
|
||||
id: item.task.id,
|
||||
status: item.task.status,
|
||||
createdAt: item.task.createdAt,
|
||||
creatorId: item.task.creatorId,
|
||||
engineerId: item.task.engineerId,
|
||||
hospitalId: item.task.hospitalId,
|
||||
completionMaterials: Array.isArray(item.task.completionMaterials)
|
||||
? item.task.completionMaterials
|
||||
: [],
|
||||
},
|
||||
patient: item.device.patient,
|
||||
surgery: item.device.surgery,
|
||||
device: {
|
||||
id: item.device.id,
|
||||
currentPressure: item.device.currentPressure,
|
||||
status: item.device.status,
|
||||
isAbandoned: item.device.isAbandoned,
|
||||
implantModel: item.device.implantModel,
|
||||
implantManufacturer: item.device.implantManufacturer,
|
||||
implantName: item.device.implantName,
|
||||
isPressureAdjustable: item.device.isPressureAdjustable,
|
||||
pressureLevels: Array.isArray(item.device.implantCatalog?.pressureLevels)
|
||||
? item.device.implantCatalog.pressureLevels
|
||||
: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private assertRole(actor: ActorContext, allowedRoles: Role[]) {
|
||||
if (!allowedRoles.includes(actor.role)) {
|
||||
throw new ForbiddenException(MESSAGES.TASK.ACTOR_ROLE_FORBIDDEN);
|
||||
|
||||
@ -632,6 +632,16 @@
|
||||
>
|
||||
调压
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canDeleteDeviceAction"
|
||||
size="small"
|
||||
type="danger"
|
||||
plain
|
||||
:loading="deletingDeviceId === device.id"
|
||||
@click="handleDeleteDevice(device)"
|
||||
>
|
||||
删除设备
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -806,7 +816,7 @@ import {
|
||||
updatePatient,
|
||||
updatePatientSurgery,
|
||||
} from '../../api/patients';
|
||||
import { getImplantCatalogs } from '../../api/devices';
|
||||
import { deleteDevice, getImplantCatalogs } from '../../api/devices';
|
||||
import { getDictionaries } from '../../api/dictionaries';
|
||||
import { publishTask } from '../../api/tasks';
|
||||
import {
|
||||
@ -847,6 +857,11 @@ const canPublishAdjustTask = computed(() =>
|
||||
userStore.role,
|
||||
),
|
||||
);
|
||||
const canDeleteDeviceAction = computed(() =>
|
||||
['SYSTEM_ADMIN', 'HOSPITAL_ADMIN', 'DOCTOR', 'DIRECTOR', 'LEADER'].includes(
|
||||
userStore.role,
|
||||
),
|
||||
);
|
||||
|
||||
const loading = ref(false);
|
||||
const submitLoading = ref(false);
|
||||
@ -891,6 +906,7 @@ const detailTab = ref('profile');
|
||||
const detailPatient = ref(null);
|
||||
const detailLifecycle = ref([]);
|
||||
const detailAdjustDeviceId = ref(null);
|
||||
const deletingDeviceId = ref(null);
|
||||
|
||||
const patientForm = reactive({
|
||||
name: '',
|
||||
@ -1379,6 +1395,15 @@ function formatAdjustDeviceLabel(device) {
|
||||
].join(' | ');
|
||||
}
|
||||
|
||||
function formatDeviceDetailLabel(device) {
|
||||
return (
|
||||
device?.implantName ||
|
||||
device?.implantModel ||
|
||||
device?.implantManufacturer ||
|
||||
`设备 ${device?.id || ''}`
|
||||
);
|
||||
}
|
||||
|
||||
function formatDateTime(value) {
|
||||
if (!value) {
|
||||
return '-';
|
||||
@ -1934,6 +1959,37 @@ async function handleSubmitAdjustTask() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteDevice(device) {
|
||||
if (!detailPatient.value?.id || !device?.id || deletingDeviceId.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除设备“${formatDeviceDetailLabel(device)}”吗?`,
|
||||
'删除设备',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
deletingDeviceId.value = device.id;
|
||||
try {
|
||||
await deleteDevice(device.id);
|
||||
ElMessage.success('设备已删除');
|
||||
await fetchData();
|
||||
await openDetailDialog({ id: detailPatient.value.id });
|
||||
detailTab.value = 'surgeries';
|
||||
} finally {
|
||||
deletingDeviceId.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
function handleDelete(row) {
|
||||
ElMessageBox.confirm(`确定要删除患者 "${row.name}" 吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
@ -1972,9 +2028,14 @@ async function openDetailDialog(row) {
|
||||
const fullLifecycle = Array.isArray(lifecycle?.lifecycle)
|
||||
? lifecycle.lifecycle
|
||||
: [];
|
||||
detailLifecycle.value = fullLifecycle.filter(
|
||||
(item) => item.patient?.id === detail.id,
|
||||
);
|
||||
detailLifecycle.value = fullLifecycle.filter((item) => {
|
||||
const eventPatientId = item?.patient?.id;
|
||||
if (eventPatientId == null || eventPatientId === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return Number(eventPatientId) === Number(detail.id);
|
||||
});
|
||||
const deviceOptions = buildDetailAdjustDeviceOptions(
|
||||
detail,
|
||||
detailLifecycle.value,
|
||||
|
||||
@ -222,7 +222,12 @@
|
||||
>
|
||||
完成
|
||||
</el-button>
|
||||
<span v-if="!canAccept(row) && !canCancel(row) && !canComplete(row)"
|
||||
<span
|
||||
v-if="
|
||||
!canAccept(row) &&
|
||||
!canCancel(row) &&
|
||||
!canComplete(row)
|
||||
"
|
||||
>-</span
|
||||
>
|
||||
</template>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user