* связка таблицы архива с пациентами

* добавил разграничение карт по типам баз
* модель для хранения изменений статуса карт
* добавил окно с просмотром выдачи карты
* добавил фильтрацию вывода карт
This commit is contained in:
brusnitsyn
2025-12-02 17:15:28 +09:00
parent 063ddafdfb
commit 2dfa45707c
21 changed files with 656 additions and 71 deletions

View File

@@ -0,0 +1,222 @@
// composables/useMedicalHistoryFilter.js
import { router, usePage } from '@inertiajs/vue3'
import {ref, computed, watch} from 'vue'
import { stringifyQuery } from 'ufo'
import {format, isValid, parse, parseISO} from 'date-fns'
import {useDebounceFn} from "@vueuse/core";
export const useMedicalHistoryFilter = (filters) => {
const page = usePage()
// Реактивные фильтры с начальными значениями из URL
const filtersRef = ref({
search: filters?.search || '',
date_extract_from: filters?.date_extract_from || null,
date_extract_to: filters?.date_extract_to || null,
page: filters?.page || 1,
page_size: filters?.page_size || 15,
sort_by: filters?.sort_by || 'date_extract',
sort_order: filters?.sort_order || 'desc',
view_type: filters?.view_type || 'archive'
})
const meta = computed(() => page.props.cards?.meta || {})
const isLoading = ref(false)
// Форматирование даты для URL
const formatDateForUrl = (date) => {
if (!date) return null
if (date instanceof Date) {
return format(date, 'yyyy-MM-dd')
}
return date
}
// Навигация с фильтрами
const applyFilters = (updates = {}, resetPage = false) => {
// Обновляем фильтры
Object.assign(filtersRef.value, updates)
// Если сбрасываем фильтры, обнуляем страницу
if (resetPage) {
filtersRef.value.page = 1
}
// Подготавливаем параметры для URL
const params = {
search: filtersRef.value.search || null,
date_extract_from: formatDateForUrl(filtersRef.value.date_extract_from),
date_extract_to: formatDateForUrl(filtersRef.value.date_extract_to),
page: filtersRef.value.page,
page_size: filtersRef.value.page_size,
sort_by: filtersRef.value.sort_by,
sort_order: filtersRef.value.sort_order,
view_type: filtersRef.value.view_type,
}
// Очищаем пустые значения
const cleanParams = Object.fromEntries(
Object.entries(params).filter(([_, value]) =>
value !== undefined && value !== null && value !== ''
)
)
const query = stringifyQuery(cleanParams)
isLoading.value = true
router.visit(`/${query ? `?${query}` : ''}`, {
preserveState: true,
preserveScroll: true,
onFinish: () => {
isLoading.value = false
}
})
}
// Дебаунсированный поиск
const debouncedSearch = useDebounceFn((value) => {
applyFilters({ search: value }, true)
}, 500)
// Обработчики событий
const handleSearch = (value) => {
filtersRef.value.search = value
debouncedSearch(value)
}
// Конвертация строки даты в timestamp для NaiveUI
const convertToTimestamp = (dateString) => {
if (!dateString) return null
try {
// Предполагаем формат yyyy-MM-dd
const date = parse(dateString, 'yyyy-MM-dd', new Date())
return isValid(date) ? date.getTime() : null
} catch (error) {
console.error('Error converting date to timestamp:', error)
return null
}
}
// Конвертация timestamp в строку для URL
const convertTimestampToString = (timestamp) => {
if (!timestamp) return null
try {
const date = new Date(timestamp)
return isValid(date) ? format(date, 'yyyy-MM-dd') : null
} catch (error) {
console.error('Error converting timestamp to string:', error)
return null
}
}
const dateRange = ref([
filtersRef.value.date_extract_from ? convertToTimestamp(filtersRef.value.date_extract_from) : null,
filtersRef.value.date_extract_to ? convertToTimestamp(filtersRef.value.date_extract_to) : null
])
const handleDateRangeChange = (timestamps) => {
dateRange.value = timestamps || [null, null]
const updates = {
date_extract_from: timestamps?.[0] ? convertTimestampToString(timestamps[0]) : null,
date_extract_to: timestamps?.[1] ? convertTimestampToString(timestamps[1]) : null
}
Object.assign(filtersRef.value, updates)
applyFilters(updates, true)
}
const handleStatusChange = (status) => {
applyFilters({ status }, true)
}
const handlePageChange = (page) => {
applyFilters({ page })
}
const handlePageSizeChange = (size) => {
applyFilters({ page_size: size, page: 1 })
}
const handleViewTypeChange = (view_type) => {
applyFilters({ view_type, page: 1 })
}
const handleSortChange = (sorter) => {
applyFilters({
sort_by: sorter.columnKey,
sort_order: sorter.order
})
}
const resetAllFilters = () => {
filtersRef.value = {
search: '',
date_extract_from: null,
date_extract_to: null,
page: 1,
page_size: 15,
sort_by: 'created_at',
sort_order: 'desc',
view_type: 'archive',
}
applyFilters({}, true)
}
// Активные фильтры для отображения
const activeFilters = computed(() => {
const active = []
if (filtersRef.value.search) {
active.push({ key: 'search', label: `Поиск: ${filtersRef.value.search}` })
}
if (filtersRef.value.date_extract_from || filtersRef.value.date_extract_to) {
const from = filtersRef.value.date_extract_from ? format(parseISO(filtersRef.value.date_extract_from), 'dd.MM.yyyy') : ''
const to = filtersRef.value.date_extract_to ? format(parseISO(filtersRef.value.date_extract_to), 'dd.MM.yyyy') : ''
active.push({ key: 'date', label: `Дата: ${from} - ${to}` })
}
// Добавьте другие фильтры по необходимости
return active
})
// Следим за изменениями в page.props.filters
watch(
() => page.props.filters,
(newFilters) => {
if (newFilters) {
// Сохраняем все фильтры, включая search!
filtersRef.value = {
...filtersRef.value,
...newFilters
}
// Синхронизируем dateRange с фильтрами
const formattedDate = [
newFilters.date_extract_from ? convertToTimestamp(newFilters.date_extract_from) : null,
newFilters.date_extract_to ? convertToTimestamp(newFilters.date_extract_to) : null
]
dateRange.value = formattedDate.every(date => date === null) ? null : formattedDate
}
},
{ deep: true, immediate: true }
)
return {
filtersRef,
meta,
isLoading,
activeFilters,
dateRange,
handleViewTypeChange,
handleSearch,
handleDateRangeChange,
handleStatusChange,
handlePageChange,
handlePageSizeChange,
handleSortChange,
resetAllFilters,
applyFilters
}
}

View File

@@ -26,7 +26,7 @@ const themeOverrides = {
>
<SideMenu />
</NLayoutSider>
<NLayout content-class="p-6 pt-2 relative" :native-scrollbar="false">
<NLayout content-class="p-6 relative" :native-scrollbar="false">
<div>
<slot name="header" />
</div>

View File

@@ -1,13 +1,70 @@
<script setup>
import { NModal } from 'naive-ui'
import { NModal, NDataTable } from 'naive-ui'
import {ref, watch} from "vue";
const open = defineModel('open')
const props = defineProps({
patientId: {
type: Number,
}
})
const loading = ref(true)
const patient = ref({})
const loadPatientData = async () => {
if (!props.patientId) return
loading.value = true
try {
axios.get(`/api/si/patients/${props.patientId}`).then(res => {
patient.value = res.data
})
} catch (error) {
// message.error('Ошибка при загрузке данных пациента')
console.error(error)
} finally {
loading.value = false
}
}
const columns = [
{
title: 'Выдача',
key: 'issue_at'
},
{
title: 'Возврат',
key: 'return_at'
},
{
title: 'Организация',
key: 'org_id'
},
{
title: 'ФИО',
key: 'employee_name'
},
{
title: 'Должность',
key: 'employee_post'
},
]
// Наблюдаем за изменением patientId
watch(() => props.patientId, (newId) => {
if (newId) {
loadPatientData()
}
}, { immediate: true })
</script>
<template>
<NModal v-model:show="open">
<NModal v-model:show="open" preset="card" class="max-w-4xl" closable @close="open = false">
<template #header>
{{ patient.info?.medcardnum }} {{ patient.info?.family }} {{ patient.info?.name }} {{ patient.info?.ot }}
</template>
<NDataTable :columns="columns" :data="patient?.journal" />
</NModal>
</template>

View File

@@ -1,17 +1,17 @@
<script setup>
import {NDataTable, NEllipsis} from "naive-ui"
import {h, reactive, ref} from "vue"
import {useMedicalHistory} from "../../../Composables/useMedicalHistory.js";
import {computed, h, reactive, ref} from "vue"
import {useMedicalHistoryFilter} from "../../../Composables/useMedicalHistoryFilter.js";
import ArchiveHistoryModal from '../ArchiveHistoryModal/Index.vue'
const { navigate } = useMedicalHistory('/')
const props = defineProps({
data: {
filters: {
type: Array,
default: []
},
meta: {
type: Object,
default: {}
data: {
type: Array,
default: []
},
maxHeight: {
type: String,
@@ -20,15 +20,17 @@ const props = defineProps({
minHeight: {
type: String,
default: null
}
},
})
const { isLoading, handlePageChange, handlePageSizeChange, meta } = useMedicalHistoryFilter(props.filters)
const columns = ref([
{
title: '№ карты',
key: 'nkarta',
key: 'medcardnum',
width: 100,
render: (row) => h(NEllipsis, null, { default: () => row.nkarta })
render: (row) => h(NEllipsis, null, { default: () => row.medcardnum })
},
{
title: 'ФИО',
@@ -44,21 +46,21 @@ const columns = ref([
},
{
title: 'Дата поступления',
key: 'mpostdate',
key: 'daterecipient',
width: 150,
render: (row) => h(NEllipsis, null, { default: () => row.mpostdate })
render: (row) => h(NEllipsis, null, { default: () => row.daterecipient })
},
{
title: 'Дата выписки',
key: 'menddate',
key: 'dateextract',
width: 130,
render: (row) => h(NEllipsis, null, { default: () => row.menddate })
render: (row) => h(NEllipsis, null, { default: () => row.dateextract })
},
{
title: '№ архива',
key: 'narhiv',
width: 120,
render: (row) => h(NEllipsis, null, { default: () => row.narhiv })
render: (row) => h(NEllipsis, null, { default: () => row.narhiv || '-' })
},
{
title: 'Дата архива',
@@ -73,28 +75,35 @@ const columns = ref([
render: (row) => h(NEllipsis, null, { default: () => row.status || '-' })
}
])
const showArchiveHistoryModal = ref(false)
const selectedPatientId = ref(null)
const rowProps = (row) => ({
onDblclick: () => {
selectedPatientId.value = row.id
showArchiveHistoryModal.value = true
}
})
const paginationReactive = reactive({
page: props.meta.current_page,
pageCount: props.meta.last_page,
pageSize: props.meta.per_page,
itemCount: props.meta.total,
const pagination = computed(() => ({
page: meta.value.current_page || 1,
pageCount: meta.value.last_page || 1,
pageSize: meta.value.per_page || 15,
itemCount: meta.value.total || 0,
pageSizes: [15, 50, 100],
showSizePicker: true,
prefix({ itemCount }) {
return `Всего карт ${itemCount}.`
},
onUpdatePage(page) {
navigate(undefined, page)
onUpdatePage: (page) => {
handlePageChange(page)
},
onUpdatePageSize(pageSize) {
navigate(undefined, 1, pageSize)
}
})
onUpdatePageSize: handlePageSizeChange
}))
</script>
<template>
<NDataTable remote striped :columns="columns" :pagination="paginationReactive" :max-height="maxHeight" size="small" :min-height="minHeight" :data="data" />
<NDataTable remote striped :loading="isLoading" :row-props="rowProps" :columns="columns" :pagination="pagination" :max-height="maxHeight" size="small" :min-height="minHeight" :data="data" />
<ArchiveHistoryModal v-model:open="showArchiveHistoryModal" :patient-id="selectedPatientId" />
</template>
<style scoped>

View File

@@ -1,38 +1,51 @@
<script setup>
import AppLayout from "../../Layouts/AppLayout.vue"
import TableCards from './DataTable/Index.vue'
import { NInput, NFlex, NDivider, NDatePicker } from 'naive-ui'
import { NInput, NFlex, NDivider, NDatePicker, NSpace, NRadioGroup, NRadioButton, NH1 } from 'naive-ui'
import {useMedicalHistory} from "../../Composables/useMedicalHistory.js";
import {useDebounceFn} from "@vueuse/core";
import {useMedicalHistoryFilter} from "../../Composables/useMedicalHistoryFilter.js";
import {computed, ref} from "vue";
const props = defineProps({
cards: {
type: Array,
default: []
},
filters: {
type: Array,
default: []
}
})
const { navigate } = useMedicalHistory('/')
const {
isLoading, applyFilters, filtersRef, handleSearch, dateRange,
handleDateRangeChange, handleViewTypeChange
} = useMedicalHistoryFilter(props.filters)
const search = (value) => {
navigate(value)
}
const debounceSearch = useDebounceFn((searchValue) => {
search(searchValue)
}, 500)
const viewType = ref('archive')
</script>
<template>
<AppLayout>
<template #header>
<NFlex class="py-4" align="center" :wrap="false">
<NInput placeholder="Поиск по ФИО, № карты" @update:value="val => debounceSearch(val)"/>
<NDivider vertical />
<NDatePicker type="daterange" clearable format="dd.MM.yyyy" start-placeholder="Дата выписки с" end-placeholder="по" />
</NFlex>
<NSpace vertical>
<NH1 class="!my-0 mb-5">
Архив
</NH1>
<NRadioGroup v-model:value="viewType" @update:value="val => handleViewTypeChange(val)">
<NRadioButton value="archive">Архив</NRadioButton>
<NRadioButton value="mis">МИС</NRadioButton>
<NRadioButton value="softinfo">СофтИнфо</NRadioButton>
</NRadioGroup>
<NFlex class="pb-4" align="center" :wrap="false">
<NInput placeholder="Поиск по ФИО, № карты" @update:value="val => handleSearch(val)"/>
<NDivider vertical />
<NDatePicker v-model:value="dateRange" @update:value="handleDateRangeChange" type="daterange" clearable format="dd.MM.yyyy" start-placeholder="Дата выписки с" end-placeholder="по" />
</NFlex>
</NSpace>
</template>
<TableCards :data="cards.data" :meta="cards.meta" min-height="calc(100vh - 179px)" max-height="calc(100vh - 196px)" />
<TableCards :filters="filters" :data="cards.data" :meta="cards.meta" min-height="calc(100vh - 277px)" max-height="calc(100vh - 320px)" />
</AppLayout>
</template>