Files
onboard/resources/js/Pages/Report/Components/ManualPatientOperationsModal.vue
2026-04-21 10:08:14 +09:00

413 lines
12 KiB
Vue

<script setup>
import {computed, h, reactive, ref, watch} from "vue";
import {
NButton,
NDataTable,
NDatePicker,
NEllipsis,
NForm,
NFormItem,
NIcon,
NModal,
NPopconfirm,
NSelect,
NSpin,
NText,
} from "naive-ui";
import {TbPencil, TbTrash} from "vue-icons-plus/tb";
import {useDebounceFn} from "@vueuse/core";
import {useReportStore} from "../../../Stores/report.js";
const props = defineProps({
patient: {
type: Object,
default: null,
},
sourceStatus: {
type: String,
default: null,
},
})
const show = defineModel('show', {type: Boolean, default: false})
const reportStore = useReportStore()
const formRef = ref(null)
const loading = ref(false)
const submitting = ref(false)
const operations = ref([])
const serviceLoading = ref(false)
const serviceOptions = ref([])
const editingOperationId = ref(null)
const form = reactive({
service_id: null,
service_code: '',
service_name: '',
urgency: 'plan',
period: null,
})
const rules = {
service_id: {
required: true,
message: 'Укажите услугу',
trigger: ['change'],
validator: (_rule, value) => {
if (value === null || value === undefined || value === '') return false
const id = Number(value)
return Number.isInteger(id) && id > 0
},
},
urgency: {
required: true,
message: 'Укажите срочность операции',
trigger: ['change'],
},
period: {
required: true,
type: 'array',
message: 'Укажите дату и время начала/окончания',
trigger: ['change'],
validator: (_rule, value) => {
if (!Array.isArray(value) || value.length !== 2) return false
if (!value[0] || !value[1]) return false
return value[1] >= value[0]
},
},
}
const resetForm = () => {
editingOperationId.value = null
form.service_id = null
form.service_code = ''
form.service_name = ''
form.urgency = 'plan'
form.period = null
serviceOptions.value = []
}
const urgencyOptions = [
{ label: 'Плановая', value: 'plan' },
{ label: 'Экстренная', value: 'emergency' },
]
const pad = (value) => String(value).padStart(2, '0')
const formatLocalDateTime = (timestamp) => {
const date = new Date(timestamp)
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
}
const loadOperations = async () => {
if (!props.patient?.department_patient_id) {
operations.value = []
return
}
loading.value = true
try {
operations.value = await reportStore.getManualPatientOperations(props.patient.department_patient_id)
} finally {
loading.value = false
}
}
const loadServiceOptions = useDebounceFn(async (query) => {
if (!query || query.trim().length < 2) {
serviceOptions.value = []
return
}
serviceLoading.value = true
try {
const items = await reportStore.searchMedicalServices(query.trim())
serviceOptions.value = (items ?? []).map((item) => ({
label: item.label,
value: item.id,
code: item.code,
name: item.name,
}))
} finally {
serviceLoading.value = false
}
}, 300)
const handleServiceSearch = async (query) => {
await loadServiceOptions(query)
}
const handleServiceChange = (value, option) => {
const normalizedId = value === null || value === undefined || value === '' ? null : Number(value)
form.service_id = Number.isInteger(normalizedId) && normalizedId > 0 ? normalizedId : null
if (!option && form.service_id === null) {
form.service_id = null
form.service_code = ''
form.service_name = ''
return
}
if (!option || Array.isArray(option)) {
return
}
form.service_code = option.code ?? ''
form.service_name = option.name ?? ''
}
const toPeriod = (operation) => {
if (!operation?.startAt || !operation?.endAt) return null
return [new Date(operation.startAt).getTime(), new Date(operation.endAt).getTime()]
}
const startEdit = (operation) => {
editingOperationId.value = operation.id
form.period = toPeriod(operation)
form.service_id = operation.service?.id ? Number(operation.service.id) : null
form.service_code = operation.service?.code ?? ''
form.service_name = operation.service?.name ?? ''
form.urgency = operation.urgency ?? 'plan'
if (operation.service?.label) {
serviceOptions.value = [{
label: operation.service.label,
value: operation.service.id,
code: operation.service.code,
name: operation.service.name,
}]
}
}
const submit = async () => {
if (!props.patient?.department_patient_id || submitting.value) return
try {
await formRef.value?.validate()
} catch {
return
}
submitting.value = true
try {
const payload = {
service_id: Number(form.service_id),
urgency: form.urgency,
started_at: formatLocalDateTime(form.period[0]),
ended_at: formatLocalDateTime(form.period[1]),
}
if (editingOperationId.value) {
await reportStore.updateManualPatientOperation(
props.patient.department_patient_id,
editingOperationId.value,
payload
)
window.$message.success('Операция обновлена')
} else {
await reportStore.createManualPatientOperation(props.patient.department_patient_id, payload)
window.$message.success('Операция добавлена')
}
await reportStore.reloadReportPage()
await Promise.all([
loadOperations(),
props.sourceStatus
? reportStore.loadPatientsByStatus(props.sourceStatus, { resetPage: true })
: Promise.resolve(),
])
resetForm()
} finally {
submitting.value = false
}
}
const remove = async (operation) => {
if (!props.patient?.department_patient_id) return
await reportStore.deleteManualPatientOperation(props.patient.department_patient_id, operation.id)
window.$message.success('Операция удалена')
await reportStore.reloadReportPage()
await Promise.all([
loadOperations(),
props.sourceStatus
? reportStore.loadPatientsByStatus(props.sourceStatus, { resetPage: true })
: Promise.resolve(),
])
if (editingOperationId.value === operation.id) {
resetForm()
}
}
const columns = computed(() => ([
{
title: 'Услуга',
key: 'service',
width: 320,
render: (row) => h(
NEllipsis,
{
tooltip: true,
},
{
default: () => row.service?.label || 'Без услуги',
}
),
},
{
title: 'Срочность',
key: 'urgency',
width: 110,
render: (row) => row.urgency === 'emergency' ? 'Э' : 'П',
},
{
title: 'Начало',
key: 'startAt',
width: 165,
render: (row) => row.startAt ? new Date(row.startAt).toLocaleString('ru-RU') : '-',
},
{
title: 'Окончание',
key: 'endAt',
width: 165,
render: (row) => row.endAt ? new Date(row.endAt).toLocaleString('ru-RU') : '-',
},
{
title: 'Мин.',
key: 'duration',
width: 80,
render: (row) => {
const duration = Number(row.duration)
return Number.isFinite(duration) ? Math.round(duration) : '-'
},
},
{
title: '',
key: 'actions',
width: 92,
render: (row) => h(
'div',
{ style: { display: 'flex', gap: '8px', justifyContent: 'center' } },
[
h(
NButton,
{
quaternary: true,
size: 'small',
onClick: () => startEdit(row),
},
{
icon: () => h(NIcon, {}, { default: () => h(TbPencil) }),
}
),
h(
NPopconfirm,
{
onPositiveClick: () => remove(row),
},
{
trigger: () => h(
NButton,
{ quaternary: true, type: 'error', size: 'small' },
{ icon: () => h(NIcon, {}, { default: () => h(TbTrash) }) }
),
default: () => 'Удалить операцию?',
}
)
]
)
}
]))
watch(() => show.value, async (opened) => {
if (!opened) {
resetForm()
return
}
await loadOperations()
}, {immediate: false})
watch(() => props.patient?.department_patient_id, async () => {
if (show.value) {
resetForm()
await loadOperations()
}
})
</script>
<template>
<NModal
v-model:show="show"
preset="card"
title="Операции спецконтингента"
class="max-w-5xl"
:mask-closable="false"
>
<NText class="mb-3 block">
{{ patient?.fullname }}
</NText>
<NForm ref="formRef" :model="form" :rules="rules" label-placement="top" class="mb-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
<NFormItem label="Услуга" path="service_id" class="mb-0">
<NSelect
v-model:value="form.service_id"
filterable
clearable
remote
:options="serviceOptions"
:loading="serviceLoading"
placeholder="Начните ввод кода или названия услуги"
@search="handleServiceSearch"
@update:value="handleServiceChange"
/>
</NFormItem>
<NFormItem label="Срочность" path="urgency" class="mb-0">
<NSelect
v-model:value="form.urgency"
:options="urgencyOptions"
/>
</NFormItem>
<NFormItem label="Начало и окончание" path="period" class="mb-0 md:col-span-1">
<NDatePicker
v-model:value="form.period"
type="datetimerange"
class="w-full"
clearable
/>
</NFormItem>
</div>
<div class="mt-3 flex items-center gap-2 justify-end">
<NButton v-if="editingOperationId" @click="resetForm">Отмена редактирования</NButton>
<NButton type="primary" :loading="submitting" @click="submit">
{{ editingOperationId ? 'Сохранить изменения' : 'Добавить операцию' }}
</NButton>
</div>
</NForm>
<NSpin :show="loading">
<NDataTable
:columns="columns"
:data="operations"
size="small"
max-height="320"
:row-key="(row) => row.id"
class="text-sm!"
/>
</NSpin>
</NModal>
</template>
<style scoped>
:deep(.n-data-table-th),
:deep(.n-data-table-td) {
font-size: var(--n-font-size);
}
</style>