feat(emr): 病历时效监控
This commit is contained in:
@@ -0,0 +1,14 @@
|
|||||||
|
package com.healthlink.his.web.emr.appservice;
|
||||||
|
|
||||||
|
import com.healthlink.his.emr.domain.EmrTimeliness;
|
||||||
|
import com.healthlink.his.emr.dto.EmrTimelinessStatisticsDto;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public interface IEmrTimelinessAppService {
|
||||||
|
|
||||||
|
EmrTimelinessStatisticsDto checkTimeliness(Long encounterId);
|
||||||
|
|
||||||
|
Map<String, Object> getTimelinessAlerts(String emrType, String status, String departmentName, int pageNum, int pageSize);
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package com.healthlink.his.web.emr.appservice.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.healthlink.his.emr.domain.EmrTimeliness;
|
||||||
|
import com.healthlink.his.emr.dto.EmrTimelinessStatisticsDto;
|
||||||
|
import com.healthlink.his.emr.service.IEmrTimelinessService;
|
||||||
|
import com.healthlink.his.web.emr.appservice.IEmrTimelinessAppService;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class EmrTimelinessAppServiceImpl implements IEmrTimelinessAppService {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IEmrTimelinessService emrTimelinessService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public EmrTimelinessStatisticsDto checkTimeliness(Long encounterId) {
|
||||||
|
LambdaQueryWrapper<EmrTimeliness> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
if (encounterId != null) {
|
||||||
|
wrapper.eq(EmrTimeliness::getEncounterId, encounterId);
|
||||||
|
}
|
||||||
|
wrapper.eq(EmrTimeliness::getStatus, "PENDING");
|
||||||
|
List<EmrTimeliness> pendingList = emrTimelinessService.list(wrapper);
|
||||||
|
|
||||||
|
Date now = new Date();
|
||||||
|
int overdueCount = 0;
|
||||||
|
for (EmrTimeliness record : pendingList) {
|
||||||
|
if (record.getDeadlineTime() != null && now.after(record.getDeadlineTime())) {
|
||||||
|
record.setStatus("OVERDUE");
|
||||||
|
emrTimelinessService.updateById(record);
|
||||||
|
overdueCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EmrTimelinessStatisticsDto stats = new EmrTimelinessStatisticsDto();
|
||||||
|
LambdaQueryWrapper<EmrTimeliness> countWrapper = new LambdaQueryWrapper<>();
|
||||||
|
if (encounterId != null) {
|
||||||
|
countWrapper.eq(EmrTimeliness::getEncounterId, encounterId);
|
||||||
|
}
|
||||||
|
long total = emrTimelinessService.count(countWrapper);
|
||||||
|
|
||||||
|
countWrapper.eq(EmrTimeliness::getStatus, "COMPLETED");
|
||||||
|
long completed = emrTimelinessService.count(countWrapper);
|
||||||
|
|
||||||
|
countWrapper.eq(EmrTimeliness::getStatus, "OVERDUE");
|
||||||
|
long overdue = emrTimelinessService.count(countWrapper);
|
||||||
|
|
||||||
|
long pending = total - completed - overdue;
|
||||||
|
double rate = total > 0 ? Math.round(completed * 10000.0 / total) / 100.0 : 0;
|
||||||
|
|
||||||
|
stats.setTotalCount(total);
|
||||||
|
stats.setCompletedCount(completed);
|
||||||
|
stats.setOverdueCount(overdue);
|
||||||
|
stats.setPendingCount(pending);
|
||||||
|
stats.setCompletionRate(rate);
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> getTimelinessAlerts(String emrType, String status, String departmentName, int pageNum, int pageSize) {
|
||||||
|
LambdaQueryWrapper<EmrTimeliness> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(StringUtils.hasText(status), EmrTimeliness::getStatus, status);
|
||||||
|
wrapper.eq(StringUtils.hasText(emrType), EmrTimeliness::getEmrType, emrType);
|
||||||
|
wrapper.eq(StringUtils.hasText(departmentName), EmrTimeliness::getDepartmentName, departmentName);
|
||||||
|
wrapper.orderByAsc(EmrTimeliness::getDeadlineTime);
|
||||||
|
|
||||||
|
Page<EmrTimeliness> page = emrTimelinessService.page(new Page<>(pageNum, pageSize), wrapper);
|
||||||
|
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
result.put("total", page.getTotal());
|
||||||
|
result.put("rows", page.getRecords());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.healthlink.his.web.emr.controller;
|
||||||
|
|
||||||
|
import com.core.common.core.domain.R;
|
||||||
|
import com.healthlink.his.emr.domain.EmrTimeliness;
|
||||||
|
import com.healthlink.his.emr.dto.EmrTimelinessStatisticsDto;
|
||||||
|
import com.healthlink.his.web.emr.appservice.IEmrTimelinessAppService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/emr/timeliness")
|
||||||
|
@Slf4j
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Tag(name = "病历时限监控")
|
||||||
|
public class EmrTimelinessController {
|
||||||
|
|
||||||
|
private final IEmrTimelinessAppService emrTimelinessAppService;
|
||||||
|
|
||||||
|
@PostMapping("/check")
|
||||||
|
@PreAuthorize("@ss.hasPermi('inpatient:emr:edit')")
|
||||||
|
@Operation(summary = "执行病历时限检查")
|
||||||
|
public R<EmrTimelinessStatisticsDto> checkTimeliness(
|
||||||
|
@RequestParam(value = "encounterId", required = false) Long encounterId) {
|
||||||
|
return R.ok(emrTimelinessAppService.checkTimeliness(encounterId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/alerts")
|
||||||
|
@PreAuthorize("@ss.hasPermi('inpatient:emr:list')")
|
||||||
|
@Operation(summary = "获取病历时限提醒列表")
|
||||||
|
public R<Map<String, Object>> getTimelinessAlerts(
|
||||||
|
@RequestParam(value = "emrType", required = false) String emrType,
|
||||||
|
@RequestParam(value = "status", required = false) String status,
|
||||||
|
@RequestParam(value = "departmentName", required = false) String departmentName,
|
||||||
|
@RequestParam(value = "pageNum", defaultValue = "1") int pageNum,
|
||||||
|
@RequestParam(value = "pageSize", defaultValue = "20") int pageSize) {
|
||||||
|
return R.ok(emrTimelinessAppService.getTimelinessAlerts(emrType, status, departmentName, pageNum, pageSize));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,3 +16,6 @@ export function compareEmrVersions(id1, id2) { return request({ url: "/emr/versi
|
|||||||
|
|
||||||
export function checkCompleteness(emrId, encounterId) { return request({ url: "/emr/completeness/check", method: "post", params: { emrId, encounterId } }) }
|
export function checkCompleteness(emrId, encounterId) { return request({ url: "/emr/completeness/check", method: "post", params: { emrId, encounterId } }) }
|
||||||
export function getCompletenessResults(emrId) { return request({ url: "/emr/completeness/results/" + emrId, method: "get" }) }
|
export function getCompletenessResults(emrId) { return request({ url: "/emr/completeness/results/" + emrId, method: "get" }) }
|
||||||
|
|
||||||
|
export function checkTimeliness(encounterId) { return request({ url: "/emr/timeliness/check", method: "post", params: { encounterId } }) }
|
||||||
|
export function getTimelinessAlerts(params) { return request({ url: "/emr/timeliness/alerts", method: "get", params }) }
|
||||||
|
|||||||
@@ -0,0 +1,254 @@
|
|||||||
|
<template>
|
||||||
|
<div class="emr-timeliness-monitor">
|
||||||
|
<el-row :gutter="16" class="stats-row">
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card shadow="hover" class="stat-card">
|
||||||
|
<div class="stat-value total">{{ stats.totalCount || 0 }}</div>
|
||||||
|
<div class="stat-label">病历总数</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card shadow="hover" class="stat-card">
|
||||||
|
<div class="stat-value completed">{{ stats.completedCount || 0 }}</div>
|
||||||
|
<div class="stat-label">已完成</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card shadow="hover" class="stat-card">
|
||||||
|
<div class="stat-value pending">{{ stats.pendingCount || 0 }}</div>
|
||||||
|
<div class="stat-label">待完成</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card shadow="hover" class="stat-card">
|
||||||
|
<div class="stat-value overdue">{{ stats.overdueCount || 0 }}</div>
|
||||||
|
<div class="stat-label">已超时</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-card shadow="never" class="filter-card">
|
||||||
|
<el-form :inline="true" :model="queryParams" class="filter-form">
|
||||||
|
<el-form-item label="病历类型">
|
||||||
|
<el-select v-model="queryParams.emrType" clearable placeholder="全部" style="width: 160px">
|
||||||
|
<el-option label="入院记录" value="ADMISSION" />
|
||||||
|
<el-option label="首次病程" value="FIRST_COURSE" />
|
||||||
|
<el-option label="日常病程" value="DAILY_COURSE" />
|
||||||
|
<el-option label="出院记录" value="DISCHARGE" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-select v-model="queryParams.status" clearable placeholder="全部" style="width: 140px">
|
||||||
|
<el-option label="待完成" value="PENDING" />
|
||||||
|
<el-option label="已完成" value="COMPLETED" />
|
||||||
|
<el-option label="已超时" value="OVERDUE" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="科室">
|
||||||
|
<el-input v-model="queryParams.departmentName" clearable placeholder="输入科室名" style="width: 160px" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleSearch">
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="handleReset">
|
||||||
|
重置
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card shadow="never" class="table-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>时限提醒列表</span>
|
||||||
|
<el-button type="primary" size="small" @click="handleCheckAll">
|
||||||
|
执行检查
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" :data="alertList" stripe border size="default">
|
||||||
|
<el-table-column prop="emrType" label="病历类型" width="120" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag size="small" :type="getEmrTypeTag(row.emrType)">
|
||||||
|
{{ getEmrTypeLabel(row.emrType) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="patientId" label="患者ID" width="100" align="center" />
|
||||||
|
<el-table-column prop="doctorName" label="主治医生" width="110" align="center" />
|
||||||
|
<el-table-column prop="departmentName" label="科室" width="130" align="center" />
|
||||||
|
<el-table-column prop="requiredHours" label="时限(小时)" width="100" align="center" />
|
||||||
|
<el-table-column prop="deadlineTime" label="截止时间" width="170" align="center" />
|
||||||
|
<el-table-column prop="actualCompleteTime" label="完成时间" width="170" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.actualCompleteTime || '-' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag size="small" :type="getStatusTag(row.status)">
|
||||||
|
{{ getStatusLabel(row.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<el-pagination
|
||||||
|
v-if="total > 0"
|
||||||
|
v-model:current-page="queryParams.pageNum"
|
||||||
|
v-model:page-size="queryParams.pageSize"
|
||||||
|
:page-sizes="[10, 20, 50]"
|
||||||
|
:total="total"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
background
|
||||||
|
@size-change="loadAlerts"
|
||||||
|
@current-change="loadAlerts"
|
||||||
|
/>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { checkTimeliness, getTimelinessAlerts } from '@/api/emr'
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const stats = ref({})
|
||||||
|
const alertList = ref([])
|
||||||
|
const total = ref(0)
|
||||||
|
|
||||||
|
const queryParams = reactive({
|
||||||
|
emrType: '',
|
||||||
|
status: '',
|
||||||
|
departmentName: '',
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 20
|
||||||
|
})
|
||||||
|
|
||||||
|
const EMR_TYPE_MAP = {
|
||||||
|
ADMISSION: '入院记录',
|
||||||
|
FIRST_COURSE: '首次病程',
|
||||||
|
DAILY_COURSE: '日常病程',
|
||||||
|
DISCHARGE: '出院记录'
|
||||||
|
}
|
||||||
|
const EMR_TYPE_TAG = {
|
||||||
|
ADMISSION: 'primary',
|
||||||
|
FIRST_COURSE: 'success',
|
||||||
|
DAILY_COURSE: 'warning',
|
||||||
|
DISCHARGE: 'info'
|
||||||
|
}
|
||||||
|
const STATUS_MAP = {
|
||||||
|
PENDING: '待完成',
|
||||||
|
COMPLETED: '已完成',
|
||||||
|
OVERDUE: '已超时'
|
||||||
|
}
|
||||||
|
const STATUS_TAG = {
|
||||||
|
PENDING: 'warning',
|
||||||
|
COMPLETED: 'success',
|
||||||
|
OVERDUE: 'danger'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEmrTypeLabel(type) { return EMR_TYPE_MAP[type] || type }
|
||||||
|
function getEmrTypeTag(type) { return EMR_TYPE_TAG[type] || '' }
|
||||||
|
function getStatusLabel(s) { return STATUS_MAP[s] || s }
|
||||||
|
function getStatusTag(s) { return STATUS_TAG[s] || '' }
|
||||||
|
|
||||||
|
function loadStats() {
|
||||||
|
checkTimeliness(null).then(res => {
|
||||||
|
stats.value = res.data || {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadAlerts() {
|
||||||
|
loading.value = true
|
||||||
|
const params = { ...queryParams }
|
||||||
|
if (!params.emrType) delete params.emrType
|
||||||
|
if (!params.status) delete params.status
|
||||||
|
if (!params.departmentName) delete params.departmentName
|
||||||
|
getTimelinessAlerts(params).then(res => {
|
||||||
|
alertList.value = res.data?.rows || []
|
||||||
|
total.value = res.data?.total || 0
|
||||||
|
}).catch(() => {
|
||||||
|
ElMessage.error('查询时限提醒失败')
|
||||||
|
}).finally(() => {
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSearch() {
|
||||||
|
queryParams.pageNum = 1
|
||||||
|
loadAlerts()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleReset() {
|
||||||
|
queryParams.emrType = ''
|
||||||
|
queryParams.status = ''
|
||||||
|
queryParams.departmentName = ''
|
||||||
|
queryParams.pageNum = 1
|
||||||
|
loadAlerts()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCheckAll() {
|
||||||
|
loading.value = true
|
||||||
|
checkTimeliness(null).then(res => {
|
||||||
|
stats.value = res.data || {}
|
||||||
|
ElMessage.success('时限检查完成')
|
||||||
|
loadAlerts()
|
||||||
|
}).catch(() => {
|
||||||
|
ElMessage.error('执行检查失败')
|
||||||
|
}).finally(() => {
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadStats()
|
||||||
|
loadAlerts()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.emr-timeliness-monitor {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.stats-row {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.stat-card {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.stat-value {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.stat-value.total { color: #409eff; }
|
||||||
|
.stat-value.completed { color: #67c23a; }
|
||||||
|
.stat-value.pending { color: #e6a23c; }
|
||||||
|
.stat-value.overdue { color: #f56c6c; }
|
||||||
|
.stat-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #909399;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.filter-card {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.filter-form {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.table-card .el-pagination {
|
||||||
|
margin-top: 16px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user