Files
xh-medical-sign-web/clinical-web/src/composables/useSigningTaskForm.ts

317 lines
8.0 KiB
TypeScript

import { computed, reactive, ref, toValue, type MaybeRefOrGetter } from 'vue'
import { ElMessage } from 'element-plus'
import { sendSigningSms } from '@/api/workbench/deliveries'
import {
createSigningTask,
getPatientProfile,
getSigningTemplates,
isSigningMockEnabled,
} from '@/api/workbench/signing'
import type {
PatientProfile,
PatientVisit,
SigningMethod,
SigningTaskRecord,
SigningTemplate,
WorkbenchCampus,
} from '@/api/workbench/types'
export interface UseSigningTaskFormOptions {
campus: MaybeRefOrGetter<WorkbenchCampus>
initialTemplate?: MaybeRefOrGetter<SigningTemplate | null | undefined>
}
interface TemplateCategoryGroup {
name: string
templates: SigningTemplate[]
}
interface TemplateDepartmentGroup {
name: string
categories: TemplateCategoryGroup[]
}
export function useSigningTaskForm(options: UseSigningTaskFormOptions) {
const patientId = ref('')
const patient = ref<PatientProfile | null>(null)
const visits = ref<PatientVisit[]>([])
const templates = ref<SigningTemplate[]>([])
const documentSearch = ref('')
const selectedTemplateId = ref('')
const selectedVisitId = ref('')
const method = ref<SigningMethod>('pad')
const smsDestination = ref('')
const loadingTemplates = ref(false)
const locating = ref(false)
const submitting = ref(false)
const errors = reactive({
patient: '',
visit: '',
document: '',
sms: '',
})
const selectedVisit = computed(
() => visits.value.find((visit) => visit.id === selectedVisitId.value) ?? null,
)
const selectedTemplate = computed(
() => templates.value.find((template) => template.id === selectedTemplateId.value) ?? null,
)
const filteredTemplateGroups = computed<TemplateDepartmentGroup[]>(() => {
const keyword = documentSearch.value.trim().toLowerCase()
const grouped = new Map<string, Map<string, SigningTemplate[]>>()
templates.value.forEach((template) => {
const matches =
!keyword ||
[template.name, template.code, template.department, template.category]
.join(' ')
.toLowerCase()
.includes(keyword)
if (!matches) {
return
}
const categories = grouped.get(template.department) ?? new Map<string, SigningTemplate[]>()
const categoryTemplates = categories.get(template.category) ?? []
categoryTemplates.push(template)
categories.set(template.category, categoryTemplates)
grouped.set(template.department, categories)
})
return [...grouped.entries()].map(([department, categories]) => ({
name: department,
categories: [...categories.entries()].map(([category, categoryTemplates]) => ({
name: category,
templates: categoryTemplates,
})),
}))
})
function getInitialTemplate() {
return options.initialTemplate ? (toValue(options.initialTemplate) ?? null) : null
}
function includeInitialTemplate() {
const initialTemplate = getInitialTemplate()
if (
initialTemplate &&
!templates.value.some((template) => template.id === initialTemplate.id)
) {
templates.value = [initialTemplate, ...templates.value]
}
}
function clearErrors() {
errors.patient = ''
errors.visit = ''
errors.document = ''
errors.sms = ''
}
function resetForm() {
const initialTemplate = getInitialTemplate()
patientId.value = ''
patient.value = null
visits.value = []
documentSearch.value = ''
selectedTemplateId.value = initialTemplate?.id ?? ''
selectedVisitId.value = ''
method.value = initialTemplate?.supportedMethods[0] ?? 'pad'
smsDestination.value = ''
clearErrors()
}
function resetForNextTask() {
const initialTemplate = getInitialTemplate()
documentSearch.value = ''
selectedTemplateId.value = initialTemplate?.id ?? ''
method.value = initialTemplate?.supportedMethods[0] ?? method.value
smsDestination.value = ''
errors.document = ''
errors.sms = ''
}
async function loadTemplates() {
if (!templates.value.length) {
loadingTemplates.value = true
try {
templates.value = await getSigningTemplates()
} catch {
ElMessage.error('文档模板加载失败,请稍后重试')
} finally {
loadingTemplates.value = false
}
}
includeInitialTemplate()
}
async function locatePatient() {
const keyword = patientId.value.trim()
errors.patient = ''
errors.visit = ''
patient.value = null
visits.value = []
selectedVisitId.value = ''
selectedTemplateId.value = getInitialTemplate()?.id ?? ''
if (!keyword) {
errors.patient = '请输入患者 ID、门诊号或住院号'
return
}
locating.value = true
try {
const profile = await getPatientProfile(keyword)
if (!profile) {
errors.patient = '未找到该患者,请核对输入内容'
return
}
patient.value = profile
visits.value = profile.visits
selectedVisitId.value =
profile.visits.find((visit) => visit.isCurrent)?.id ?? profile.visits[0]?.id ?? ''
} catch {
errors.patient = '患者信息查询失败,请稍后重试'
} finally {
locating.value = false
}
}
function selectVisit(visit: PatientVisit) {
selectedVisitId.value = visit.id
errors.visit = ''
}
function selectTemplate(template: SigningTemplate) {
selectedTemplateId.value = template.id
errors.document = ''
if (!template.supportedMethods.includes(method.value)) {
method.value = template.supportedMethods[0] ?? 'pad'
}
}
function selectMethod(nextMethod: SigningMethod) {
if (selectedTemplate.value && !selectedTemplate.value.supportedMethods.includes(nextMethod)) {
return
}
method.value = nextMethod
}
function maskPhone(value: string) {
if (value.length < 7) {
return value
}
return `${value.slice(0, 3)}****${value.slice(-4)}`
}
function validate() {
clearErrors()
let valid = true
if (!patient.value) {
errors.patient = '请先定位患者'
valid = false
}
if (!selectedVisit.value) {
errors.visit = '请选择关联就诊'
valid = false
}
if (!selectedTemplate.value) {
errors.document = '请至少选择一份文档'
valid = false
}
if (!isSigningMockEnabled && method.value === 'sms') {
const destination = smsDestination.value.trim().replace(/\s+/g, '')
if (!/^\+?[1-9]\d{6,14}$/.test(destination)) {
errors.sms = '请输入完整的手机号或 E.164 地址,才能发送短信'
valid = false
}
}
return valid
}
async function submit(): Promise<SigningTaskRecord | null> {
if (!validate() || !patient.value || !selectedVisit.value || !selectedTemplate.value) {
return null
}
submitting.value = true
try {
const task = await createSigningTask({
campus: toValue(options.campus),
patientId: patient.value.id,
visitId: selectedVisit.value.id,
documentId: selectedTemplate.value.id,
documentName: selectedTemplate.value.name,
templateVersionId: selectedTemplate.value.versionId,
method: method.value,
visitType: selectedVisit.value.type,
visitNo: selectedVisit.value.visitNo,
department: selectedVisit.value.department,
})
if (!isSigningMockEnabled && method.value === 'sms') {
await sendSigningSms(task.id, {
destination: smsDestination.value.trim().replace(/\s+/g, ''),
templateCode: 'SIGN_LINK',
})
}
return task
} finally {
submitting.value = false
}
}
return {
documentSearch,
errors,
filteredTemplateGroups,
loadingTemplates,
loadTemplates,
locatePatient,
locating,
maskPhone,
method,
patient,
patientId,
resetForNextTask,
resetForm,
selectMethod,
selectTemplate,
selectVisit,
selectedTemplate,
selectedTemplateId,
selectedVisit,
selectedVisitId,
smsDestination,
submit,
submitting,
templates,
visits,
}
}