新增调压记录详情接口并完善设备删除能力与前台操作
This commit is contained in:
parent
cfd2f1e8dc
commit
5f7d66ce54
@ -74,6 +74,9 @@ export const MESSAGES = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
TASK: {
|
TASK: {
|
||||||
|
RECORD_NOT_FOUND: '调压记录不存在或无权限访问',
|
||||||
|
UPDATE_ONLY_PENDING: '仅待处理调压记录可编辑',
|
||||||
|
DELETE_ONLY_PENDING_CANCELLED: '仅待处理/已取消调压记录可删除',
|
||||||
ITEMS_REQUIRED: '任务明细 items 不能为空',
|
ITEMS_REQUIRED: '任务明细 items 不能为空',
|
||||||
DEVICE_NOT_FOUND: '存在设备不在当前医院或设备不存在',
|
DEVICE_NOT_FOUND: '存在设备不在当前医院或设备不存在',
|
||||||
DEVICE_MULTI_HOSPITAL: '同一批调压任务中的设备必须属于同一家医院',
|
DEVICE_MULTI_HOSPITAL: '同一批调压任务中的设备必须属于同一家医院',
|
||||||
|
|||||||
@ -63,6 +63,7 @@ export function buildPatientLifecyclePatient(patient: PatientLifecycleSource) {
|
|||||||
id: toJsonNumber(patient.id),
|
id: toJsonNumber(patient.id),
|
||||||
name: patient.name,
|
name: patient.name,
|
||||||
inpatientNo: patient.inpatientNo,
|
inpatientNo: patient.inpatientNo,
|
||||||
|
phone: patient.phone,
|
||||||
projectName: patient.projectName,
|
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 {
|
import {
|
||||||
ApiBearerAuth,
|
ApiBearerAuth,
|
||||||
ApiOperation,
|
ApiOperation,
|
||||||
@ -19,9 +28,6 @@ import { CancelTaskDto } from '../dto/cancel-task.dto.js';
|
|||||||
import { TaskRecordQueryDto } from '../dto/task-record-query.dto.js';
|
import { TaskRecordQueryDto } from '../dto/task-record-query.dto.js';
|
||||||
import { AssignableEngineerQueryDto } from '../dto/assignable-engineer-query.dto.js';
|
import { AssignableEngineerQueryDto } from '../dto/assignable-engineer-query.dto.js';
|
||||||
|
|
||||||
/**
|
|
||||||
* B 端任务控制器:封装调压任务状态流转接口。
|
|
||||||
*/
|
|
||||||
@ApiTags('调压任务(B端)')
|
@ApiTags('调压任务(B端)')
|
||||||
@ApiBearerAuth('bearer')
|
@ApiBearerAuth('bearer')
|
||||||
@Controller('b/tasks')
|
@Controller('b/tasks')
|
||||||
@ -29,9 +35,6 @@ import { AssignableEngineerQueryDto } from '../dto/assignable-engineer-query.dto
|
|||||||
export class BTasksController {
|
export class BTasksController {
|
||||||
constructor(private readonly taskService: TaskService) {}
|
constructor(private readonly taskService: TaskService) {}
|
||||||
|
|
||||||
/**
|
|
||||||
* 查询当前角色可见的医院工程师列表。
|
|
||||||
*/
|
|
||||||
@Get('engineers')
|
@Get('engineers')
|
||||||
@Roles(
|
@Roles(
|
||||||
Role.SYSTEM_ADMIN,
|
Role.SYSTEM_ADMIN,
|
||||||
@ -53,9 +56,6 @@ export class BTasksController {
|
|||||||
return this.taskService.findAssignableEngineers(actor, query.hospitalId);
|
return this.taskService.findAssignableEngineers(actor, query.hospitalId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 查询当前角色可见的调压记录列表。
|
|
||||||
*/
|
|
||||||
@Get()
|
@Get()
|
||||||
@Roles(
|
@Roles(
|
||||||
Role.SYSTEM_ADMIN,
|
Role.SYSTEM_ADMIN,
|
||||||
@ -78,9 +78,28 @@ export class BTasksController {
|
|||||||
return this.taskService.findTaskRecords(actor, query);
|
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')
|
@Post('publish')
|
||||||
@Roles(
|
@Roles(
|
||||||
Role.SYSTEM_ADMIN,
|
Role.SYSTEM_ADMIN,
|
||||||
@ -96,9 +115,6 @@ export class BTasksController {
|
|||||||
return this.taskService.publishTask(actor, dto);
|
return this.taskService.publishTask(actor, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 工程师接收调压任务。
|
|
||||||
*/
|
|
||||||
@Post('accept')
|
@Post('accept')
|
||||||
@Roles(Role.ENGINEER)
|
@Roles(Role.ENGINEER)
|
||||||
@ApiOperation({ summary: '接收任务(ENGINEER)' })
|
@ApiOperation({ summary: '接收任务(ENGINEER)' })
|
||||||
@ -106,9 +122,6 @@ export class BTasksController {
|
|||||||
return this.taskService.acceptTask(actor, dto);
|
return this.taskService.acceptTask(actor, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 工程师完成调压任务。
|
|
||||||
*/
|
|
||||||
@Post('complete')
|
@Post('complete')
|
||||||
@Roles(Role.ENGINEER)
|
@Roles(Role.ENGINEER)
|
||||||
@ApiOperation({ summary: '完成任务(ENGINEER)' })
|
@ApiOperation({ summary: '完成任务(ENGINEER)' })
|
||||||
@ -116,9 +129,6 @@ export class BTasksController {
|
|||||||
return this.taskService.completeTask(actor, dto);
|
return this.taskService.completeTask(actor, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 系统管理员/医院管理员/医生/主任/组长可取消自己创建的任务;工程师可取消自己已接收的任务。
|
|
||||||
*/
|
|
||||||
@Post('cancel')
|
@Post('cancel')
|
||||||
@Roles(
|
@Roles(
|
||||||
Role.SYSTEM_ADMIN,
|
Role.SYSTEM_ADMIN,
|
||||||
|
|||||||
@ -23,6 +23,64 @@ import { TaskRecordQueryDto } from './dto/task-record-query.dto.js';
|
|||||||
import { MESSAGES } from '../common/messages.js';
|
import { MESSAGES } from '../common/messages.js';
|
||||||
import { normalizePressureLabel } from '../common/pressure-level.util.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[]) {
|
private assertRole(actor: ActorContext, allowedRoles: Role[]) {
|
||||||
if (!allowedRoles.includes(actor.role)) {
|
if (!allowedRoles.includes(actor.role)) {
|
||||||
throw new ForbiddenException(MESSAGES.TASK.ACTOR_ROLE_FORBIDDEN);
|
throw new ForbiddenException(MESSAGES.TASK.ACTOR_ROLE_FORBIDDEN);
|
||||||
|
|||||||
@ -632,6 +632,16 @@
|
|||||||
>
|
>
|
||||||
调压
|
调压
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="canDeleteDeviceAction"
|
||||||
|
size="small"
|
||||||
|
type="danger"
|
||||||
|
plain
|
||||||
|
:loading="deletingDeviceId === device.id"
|
||||||
|
@click="handleDeleteDevice(device)"
|
||||||
|
>
|
||||||
|
删除设备
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -806,7 +816,7 @@ import {
|
|||||||
updatePatient,
|
updatePatient,
|
||||||
updatePatientSurgery,
|
updatePatientSurgery,
|
||||||
} from '../../api/patients';
|
} from '../../api/patients';
|
||||||
import { getImplantCatalogs } from '../../api/devices';
|
import { deleteDevice, getImplantCatalogs } from '../../api/devices';
|
||||||
import { getDictionaries } from '../../api/dictionaries';
|
import { getDictionaries } from '../../api/dictionaries';
|
||||||
import { publishTask } from '../../api/tasks';
|
import { publishTask } from '../../api/tasks';
|
||||||
import {
|
import {
|
||||||
@ -847,6 +857,11 @@ const canPublishAdjustTask = computed(() =>
|
|||||||
userStore.role,
|
userStore.role,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
const canDeleteDeviceAction = computed(() =>
|
||||||
|
['SYSTEM_ADMIN', 'HOSPITAL_ADMIN', 'DOCTOR', 'DIRECTOR', 'LEADER'].includes(
|
||||||
|
userStore.role,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const submitLoading = ref(false);
|
const submitLoading = ref(false);
|
||||||
@ -891,6 +906,7 @@ const detailTab = ref('profile');
|
|||||||
const detailPatient = ref(null);
|
const detailPatient = ref(null);
|
||||||
const detailLifecycle = ref([]);
|
const detailLifecycle = ref([]);
|
||||||
const detailAdjustDeviceId = ref(null);
|
const detailAdjustDeviceId = ref(null);
|
||||||
|
const deletingDeviceId = ref(null);
|
||||||
|
|
||||||
const patientForm = reactive({
|
const patientForm = reactive({
|
||||||
name: '',
|
name: '',
|
||||||
@ -1379,6 +1395,15 @@ function formatAdjustDeviceLabel(device) {
|
|||||||
].join(' | ');
|
].join(' | ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatDeviceDetailLabel(device) {
|
||||||
|
return (
|
||||||
|
device?.implantName ||
|
||||||
|
device?.implantModel ||
|
||||||
|
device?.implantManufacturer ||
|
||||||
|
`设备 ${device?.id || ''}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function formatDateTime(value) {
|
function formatDateTime(value) {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return '-';
|
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) {
|
function handleDelete(row) {
|
||||||
ElMessageBox.confirm(`确定要删除患者 "${row.name}" 吗?`, '警告', {
|
ElMessageBox.confirm(`确定要删除患者 "${row.name}" 吗?`, '警告', {
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: '确定',
|
||||||
@ -1972,9 +2028,14 @@ async function openDetailDialog(row) {
|
|||||||
const fullLifecycle = Array.isArray(lifecycle?.lifecycle)
|
const fullLifecycle = Array.isArray(lifecycle?.lifecycle)
|
||||||
? lifecycle.lifecycle
|
? lifecycle.lifecycle
|
||||||
: [];
|
: [];
|
||||||
detailLifecycle.value = fullLifecycle.filter(
|
detailLifecycle.value = fullLifecycle.filter((item) => {
|
||||||
(item) => item.patient?.id === detail.id,
|
const eventPatientId = item?.patient?.id;
|
||||||
);
|
if (eventPatientId == null || eventPatientId === '') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Number(eventPatientId) === Number(detail.id);
|
||||||
|
});
|
||||||
const deviceOptions = buildDetailAdjustDeviceOptions(
|
const deviceOptions = buildDetailAdjustDeviceOptions(
|
||||||
detail,
|
detail,
|
||||||
detailLifecycle.value,
|
detailLifecycle.value,
|
||||||
|
|||||||
@ -222,7 +222,12 @@
|
|||||||
>
|
>
|
||||||
完成
|
完成
|
||||||
</el-button>
|
</el-button>
|
||||||
<span v-if="!canAccept(row) && !canCancel(row) && !canComplete(row)"
|
<span
|
||||||
|
v-if="
|
||||||
|
!canAccept(row) &&
|
||||||
|
!canCancel(row) &&
|
||||||
|
!canComplete(row)
|
||||||
|
"
|
||||||
>-</span
|
>-</span
|
||||||
>
|
>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user