Много всего

This commit is contained in:
brusnitsyn
2025-12-12 17:10:05 +09:00
parent 54f36e91fa
commit 98e9f8b52e
25 changed files with 1118 additions and 145 deletions

View File

@@ -1,27 +1,66 @@
// 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";
import { ref, computed, watch } from 'vue'
import { usePage, router } from '@inertiajs/vue3'
import { useDebounceFn } from '@vueuse/core'
import { format, parse, isValid, parseISO } from 'date-fns'
export const useMedicalHistoryFilter = (filters) => {
export const useMedicalHistoryFilter = (initialFilters = {}) => {
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'
search: initialFilters?.search || '',
date_extract_from: initialFilters?.date_extract_from || null,
date_extract_to: initialFilters?.date_extract_to || null,
page: initialFilters?.page || 1,
page_size: initialFilters?.page_size || 50,
sort_by: initialFilters?.sort_by || 'dateextract',
sort_order: initialFilters?.sort_order || 'desc',
view_type: initialFilters?.view_type || 'si',
status: initialFilters?.status || null,
database: initialFilters?.database || 'separate', // НОВЫЙ ПАРАМЕТР: postgresql, mssql, smart, separate
})
// Метаданные для разных типов данных
const meta = computed(() => {
const cards = page.props.cards
// Для раздельного поиска
if (filtersRef.value.database === 'separate') {
return {
si: cards.si.meta || {},
mis: cards.mis.meta || {},
stats: cards.stats || {},
}
} else if (filtersRef.value.database === 'mis') {
return {
mis: cards.mis?.meta
}
} else {
return {
si: cards.si?.meta
}
}
})
const meta = computed(() => page.props.cards?.meta || {})
const isLoading = ref(false)
const databaseStats = computed(() => page.props.databaseStats || {})
// Статистика по базам
const databaseInfo = computed(() => ({
postgresql: {
count: databaseStats.value.postgresql?.total || 0,
label: 'PostgreSQL',
color: 'blue',
description: 'Основной архив'
},
mssql: {
count: databaseStats.value.mssql?.total || 0,
label: 'MSSQL',
color: 'purple',
description: 'Исторический архив'
}
}))
// Форматирование даты для URL
const formatDateForUrl = (date) => {
@@ -32,17 +71,8 @@ export const useMedicalHistoryFilter = (filters) => {
return date
}
// Навигация с фильтрами
const applyFilters = (updates = {}, resetPage = false) => {
// Обновляем фильтры
Object.assign(filtersRef.value, updates)
// Если сбрасываем фильтры, обнуляем страницу
if (resetPage) {
filtersRef.value.page = 1
}
// Подготавливаем параметры для URL
// Подготовка параметров запроса
const prepareParams = () => {
const params = {
search: filtersRef.value.search || null,
date_extract_from: formatDateForUrl(filtersRef.value.date_extract_from),
@@ -52,19 +82,33 @@ export const useMedicalHistoryFilter = (filters) => {
sort_by: filtersRef.value.sort_by,
sort_order: filtersRef.value.sort_order,
view_type: filtersRef.value.view_type,
database: filtersRef.value.database,
status: filtersRef.value.status
}
// Очищаем пустые значения
const cleanParams = Object.fromEntries(
return Object.fromEntries(
Object.entries(params).filter(([_, value]) =>
value !== undefined && value !== null && value !== ''
)
)
}
const query = stringifyQuery(cleanParams)
// Навигация с фильтрами
const applyFilters = (updates = {}, resetPage = true) => {
// Обновляем фильтры
Object.assign(filtersRef.value, updates)
// Если сбрасываем фильтры, обнуляем страницу
if (resetPage) {
filtersRef.value.page = 1
}
const params = prepareParams()
const query = new URLSearchParams(params).toString()
isLoading.value = true
router.visit(`/${query ? `?${query}` : ''}`, {
router.get(`/${query ? `?${query}` : ''}`, params, {
preserveState: true,
preserveScroll: true,
onFinish: () => {
@@ -84,6 +128,10 @@ export const useMedicalHistoryFilter = (filters) => {
debouncedSearch(value)
}
const handleDatabaseChange = (database) => {
applyFilters({ database, page: 1 }, false)
}
// Конвертация строки даты в timestamp для NaiveUI
const convertToTimestamp = (dateString) => {
if (!dateString) return null
@@ -128,12 +176,8 @@ export const useMedicalHistoryFilter = (filters) => {
applyFilters(updates, true)
}
const handleStatusChange = (status) => {
applyFilters({ status }, true)
}
const handlePageChange = (page) => {
applyFilters({ page })
const handlePageChange = (pageNum) => {
applyFilters({ page: pageNum }, false)
}
const handlePageSizeChange = (size) => {
@@ -148,7 +192,14 @@ export const useMedicalHistoryFilter = (filters) => {
applyFilters({
sort_by: sorter.columnKey,
sort_order: sorter.order
})
}, true)
}
const handleStatusChange = (status_id) => {
applyFilters({
status: status_id
}, true)
}
const resetAllFilters = () => {
@@ -158,34 +209,112 @@ export const useMedicalHistoryFilter = (filters) => {
date_extract_to: null,
page: 1,
page_size: 15,
sort_by: 'created_at',
sort_by: 'dateextract',
sort_order: 'desc',
view_type: 'archive',
database: 'postgresql',
}
dateRange.value = [null, null]
applyFilters({}, true)
}
// Быстрый поиск по типу (для тулбара)
const quickSearch = (type, value) => {
const params = {
type,
value,
database: filtersRef.value.database,
}
router.get('/quick-search', params, {
preserveState: true,
preserveScroll: true,
})
}
// Приоритетный поиск
const prioritySearch = (searchTerm) => {
router.get('/priority-search', {
q: searchTerm
}, {
preserveState: true,
preserveScroll: true,
})
}
// Активные фильтры для отображения
const activeFilters = computed(() => {
const active = []
if (filtersRef.value.search) {
active.push({ key: 'search', label: `Поиск: ${filtersRef.value.search}` })
active.push({
key: 'search',
label: `Поиск: ${filtersRef.value.search}`,
icon: '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}` })
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 ? '- ' + to : ''}`,
icon: 'calendar'
})
}
// Добавьте другие фильтры по необходимости
// Фильтр по базе данных
if (filtersRef.value.database) {
const dbLabel = {
postgresql: 'PostgreSQL',
mssql: 'MSSQL',
smart: 'Умный поиск',
separate: 'Раздельно'
}[filtersRef.value.database] || filtersRef.value.database
active.push({
key: 'database',
label: `База: ${dbLabel}`,
icon: 'database'
})
}
return active
})
// Удаление конкретного фильтра
const removeFilter = (key) => {
const updates = {}
switch (key) {
case 'search':
updates.search = ''
break
case 'date':
updates.date_extract_from = null
updates.date_extract_to = null
dateRange.value = [null, null]
break
case 'database':
updates.database = 'postgresql'
break
}
Object.assign(filtersRef.value, updates)
applyFilters(updates, true)
}
// Следим за изменениями в page.props.filters
watch(
() => page.props.filters,
(newFilters) => {
if (newFilters) {
// Сохраняем все фильтры, включая search!
// Сохраняем все фильтры
filtersRef.value = {
...filtersRef.value,
...newFilters
@@ -209,14 +338,22 @@ export const useMedicalHistoryFilter = (filters) => {
isLoading,
activeFilters,
dateRange,
databaseInfo,
databaseStats,
// Обработчики
handleDatabaseChange,
handleViewTypeChange,
handleSearch,
handleDateRangeChange,
handleStatusChange,
handlePageChange,
handlePageSizeChange,
handleSortChange,
handleStatusChange,
resetAllFilters,
applyFilters
removeFilter,
applyFilters,
quickSearch,
prioritySearch
}
}

View File

@@ -19,19 +19,17 @@ const themeOverrides = {
<template>
<NConfigProvider :theme-overrides="themeOverrides" :locale="ruRU" :date-locale="dateRuRU">
<NLayout class="h-screen">
<NLayout position="absolute" has-sider>
<NLayoutSider
:native-scrollbar="false"
bordered
>
<SideMenu />
</NLayoutSider>
<NLayout content-class="p-6 relative" :native-scrollbar="false">
<div>
<slot name="header" />
</div>
<slot />
</NLayout>
<NLayout position="absolute" content-class="p-6 relative" :native-scrollbar="false">
<!-- <NLayoutSider-->
<!-- :native-scrollbar="false"-->
<!-- bordered-->
<!-- >-->
<!-- <SideMenu />-->
<!-- </NLayoutSider>-->
<div>
<slot name="header" />
</div>
<slot />
</NLayout>
</NLayout>
</NConfigProvider>

View File

@@ -1,7 +1,8 @@
<script setup>
import { NModal, NDataTable, NSpace, NFlex, NButton, NForm, NFormItem, NInput, NDatePicker, NDivider, NSwitch, NTag } from 'naive-ui'
import ArchiveHistoryMoveModal from '../ArchiveHistoryMoveModal/Index.vue'
import {ref, watch} from "vue";
import {computed, ref, watch} from "vue";
import {useMedicalHistoryFilter} from "../../../Composables/useMedicalHistoryFilter.js";
const open = defineModel('open')
const props = defineProps({
@@ -10,10 +11,29 @@ const props = defineProps({
}
})
const {filtersRef} = useMedicalHistoryFilter()
const loading = ref(true)
const patient = ref({})
const showArchiveHistoryModal = ref(false)
const selectedArchiveHistoryId = ref(null)
const selectedArchiveHistoryType = ref(null)
const isCreateNewArchiveHistoryModal = ref(false)
const archiveInfo = ref({
id: props.patientId,
num: null,
post_in: null,
status: null
})
const emits = defineEmits(['historyUpdated'])
const onResetData = () => {
archiveInfo.value = {
id: props.patientId,
num: null,
post_in: null,
status: null
}
}
const patientData = ref({
@@ -25,9 +45,16 @@ const loadPatientData = async () => {
loading.value = true
try {
axios.get(`/api/si/patients/${props.patientId}`).then(res => {
axios.get(`/api/si/patients/${props.patientId}?view_type=${filtersRef.value.view_type}`).then(res => {
patient.value = res.data
patientData.value = res.data.info
archiveInfo.value = res.data.archiveInfo ?? {
historyable_type: res.data.historyable_type,
id: props.patientId,
num: null,
post_in: null,
status: null,
}
})
} catch (error) {
// message.error('Ошибка при загрузке данных пациента')
@@ -38,7 +65,14 @@ const loadPatientData = async () => {
}
const onShowArchiveHistoryModal = (id) => {
selectedArchiveHistoryId.value = id
if (id === null) {
isCreateNewArchiveHistoryModal.value = true
selectedArchiveHistoryId.value = props.patientId
} else {
isCreateNewArchiveHistoryModal.value = false
selectedArchiveHistoryId.value = id
}
selectedArchiveHistoryType.value = archiveInfo.value.historyable_type
showArchiveHistoryModal.value = true
}
@@ -75,6 +109,30 @@ const rowProps = (row) => ({
}
})
const onUpdateHistory = (updatedData) => {
loadPatientData()
}
const hasCreateNew = computed(() => archiveInfo.value === null)
const onSubmit = async () => {
try {
await axios.post(`/api/archive/histories/info/${props.patientId}`, archiveInfo.value).then(res => {
// onCloseWithoutSave()
emits('historyUpdated', {
type: hasCreateNew.value ? 'created' : 'updated',
data: res.data,
patientId: props.patientId
})
})
} catch (error) {
console.error(error)
} finally {
loading.value = false
}
}
// Наблюдаем за изменением patientId
watch(() => props.patientId, (newId) => {
if (newId) {
@@ -92,19 +150,19 @@ watch(() => props.patientId, (newId) => {
<NFlex inline justify="space-between" align="center" :wrap="false">
<NForm inline :show-feedback="false">
<NFormItem label="Статус карты">
<NTag :type="patient.archiveInfo?.status?.variant ?? 'error'" :bordered="false" round>
{{ patient.archiveInfo?.status?.text ?? 'Нет в архиве' }}
<NTag :type="archiveInfo.status?.variant ?? 'error'" :bordered="false" round>
{{ archiveInfo.status?.text ?? 'Нет в архиве' }}
</NTag>
</NFormItem>
<NFormItem label="№ в архиве">
<NInput v-model:value="patientData.narhiv" />
<NInput v-model:value="archiveInfo.num" />
</NFormItem>
<NFormItem label="Дата поступления карты в архив">
<NDatePicker v-model:value="patientData.datearhiv" format="dd.MM.yyyy" />
<NDatePicker v-model:value="archiveInfo.post_in" format="dd.MM.yyyy" />
</NFormItem>
</NForm>
</NFlex>
<NButton @click="onShowArchiveHistoryModal(null)">
<NButton @click="onShowArchiveHistoryModal(null)" :disabled="!patientData.can_be_issued">
Добавить
</NButton>
<NDataTable :row-props="rowProps" min-height="420px" max-height="420px" :columns="columns" :data="patient?.journal" />
@@ -116,14 +174,14 @@ watch(() => props.patientId, (newId) => {
Закрыть без сохранения
</NButton>
<NDivider vertical />
<NButton secondary type="primary">
<NButton secondary type="primary" @click="onSubmit">
Сохранить
</NButton>
</NSpace>
</NFlex>
</template>
</NModal>
<ArchiveHistoryMoveModal @close-without-save="selectedArchiveHistoryId = null" v-model:open="showArchiveHistoryModal" :archive-history-id="selectedArchiveHistoryId" />
<ArchiveHistoryMoveModal @history-updated="onUpdateHistory" @close-without-save="selectedArchiveHistoryId = null" :is-create-new="isCreateNewArchiveHistoryModal" v-model:open="showArchiveHistoryModal" :active-history-type="selectedArchiveHistoryType" :archive-history-id="selectedArchiveHistoryId" />
</template>
<style scoped>

View File

@@ -1,17 +1,25 @@
<script setup>
import {NButton, NDatePicker, NDivider, NFlex, NForm, NFormItem, NInput, NModal, NSpace, NSwitch, NSelect} from "naive-ui";
import {ref, watch} from "vue";
import {router} from "@inertiajs/vue3";
const open = defineModel('open')
const props = defineProps({
archiveHistoryId: {
type: Number
},
activeHistoryType: {
type: String
},
isCreateNew: {
type: Boolean
}
})
const emits = defineEmits(['closeWithoutSave'])
const emits = defineEmits(['closeWithoutSave', 'historyUpdated'])
const loading = ref(false)
const archiveHistory = ref({
historyable_id: props.archiveHistoryId,
historyable_type: props.activeHistoryType,
issue_at: null,
org_id: null,
return_at: null,
@@ -25,6 +33,7 @@ const orgs = ref([])
const onResetData = () => {
archiveHistory.value = {
historyable_id: props.archiveHistoryId,
historyable_type: props.activeHistoryType,
issue_at: null,
org_id: null,
return_at: null,
@@ -59,9 +68,19 @@ const loadArchiveHistoryData = async () => {
const submit = () => {
try {
axios.post('/api/archive/histories/move', archiveHistory.value).then(res => {
const url = props.isCreateNew
? '/api/archive/histories/move'
: `/api/archive/histories/move/${archiveHistory.value.id}`
axios.post(url, archiveHistory.value).then(res => {
onCloseWithoutSave()
emits('historyUpdated', {
type: props.isCreateNew ? 'created' : 'updated',
data: res.data,
historyId: archiveHistory.value.id
})
})
} catch (error) {
console.error(error)
} finally {
@@ -71,7 +90,7 @@ const submit = () => {
// Наблюдаем за изменением archiveHistoryId
watch(() => props.archiveHistoryId, async (newId) => {
if (newId) {
if (!props.isCreateNew) {
await loadArchiveHistoryData()
} else {
onResetData()

View File

@@ -23,7 +23,7 @@ const props = defineProps({
},
})
const { isLoading, handlePageChange, handlePageSizeChange, meta } = useMedicalHistoryFilter(props.filters)
const { isLoading, handlePageChange, handlePageSizeChange, meta, filtersRef } = useMedicalHistoryFilter(props.filters)
const archiveStatusColumn = (status) => {
const tagType = status?.variant ?? 'error'
@@ -97,21 +97,41 @@ const rowProps = (row) => ({
}
})
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) => {
handlePageChange(page)
},
onUpdatePageSize: handlePageSizeChange
}))
const pagination = computed(() => {
if (filtersRef.value.view_type === 'si') {
return {
page: meta.value.si.current_page || 1,
pageCount: meta.value.si.last_page || 1,
pageSize: meta.value.si.per_page || 15,
itemCount: meta.value.si.total || 0,
pageSizes: [15, 50, 100],
showSizePicker: true,
prefix({ itemCount }) {
return `Всего карт ${itemCount}.`
},
onUpdatePage: (page) => {
handlePageChange(page)
},
onUpdatePageSize: handlePageSizeChange
}
} else {
return {
page: meta.value.mis.current_page || 1,
pageCount: meta.value.mis.last_page || 1,
pageSize: meta.value.mis.per_page || 15,
itemCount: meta.value.mis.total || 0,
pageSizes: [15, 50, 100],
showSizePicker: true,
prefix({ itemCount }) {
return `Всего карт ${itemCount}.`
},
onUpdatePage: (page) => {
handlePageChange(page)
},
onUpdatePageSize: handlePageSizeChange
}
}
})
</script>
<template>

View File

@@ -1,7 +1,7 @@
<script setup>
import AppLayout from "../../Layouts/AppLayout.vue"
import TableCards from './DataTable/Index.vue'
import { NInput, NFlex, NDivider, NDatePicker, NSpace, NRadioGroup, NRadioButton, NH1 } from 'naive-ui'
import { NInput, NFlex, NDivider, NDatePicker, NSpace, NFormItem, NRadioButton, NH1, NTabs, NTabPane, NSelect } from 'naive-ui'
import {useMedicalHistory} from "../../Composables/useMedicalHistory.js";
import {useDebounceFn} from "@vueuse/core";
import {useMedicalHistoryFilter} from "../../Composables/useMedicalHistoryFilter.js";
@@ -12,6 +12,10 @@ const props = defineProps({
type: Array,
default: []
},
statuses: {
type: Array,
default: []
},
filters: {
type: Array,
default: []
@@ -20,35 +24,56 @@ const props = defineProps({
const {
isLoading, applyFilters, filtersRef, handleSearch, dateRange,
handleDateRangeChange, handleViewTypeChange
handleDateRangeChange, handleViewTypeChange, handleStatusChange
} = useMedicalHistoryFilter(props.filters)
const viewType = ref('archive')
const handleBeforeLeave = (tabName) => {
handleViewTypeChange(tabName)
return true
}
</script>
<template>
<AppLayout>
<template #header>
<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>
<!-- <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)"/>
<NFormItem class="w-[720px]" label="Поиск" :show-feedback="false">
<NInput placeholder="Поиск по ФИО, № карты" @update:value="val => handleSearch(val)" size="large" />
</NFormItem>
<NDivider vertical />
<NDatePicker v-model:value="dateRange" @update:value="handleDateRangeChange" type="daterange" clearable format="dd.MM.yyyy" start-placeholder="Дата выписки с" end-placeholder="по" />
<NFormItem class="w-[340px]" label="Дата выписки" :show-feedback="false">
<NDatePicker v-model:value="dateRange" @update:value="handleDateRangeChange" type="daterange" clearable format="dd.MM.yyyy" start-placeholder="Дата выписки с" end-placeholder="по" size="large" />
</NFormItem>
<NFormItem class="w-[340px]" label="Статус карты" :show-feedback="false">
<NSelect :options="statuses" @update:value="val => handleStatusChange(val)" clearable size="large" />
</NFormItem>
</NFlex>
</NSpace>
</template>
<TableCards :filters="filters" :data="cards.data" :meta="cards.meta" min-height="calc(100vh - 277px)" max-height="calc(100vh - 320px)" />
<NTabs :value="filtersRef.view_type" type="segment" animated @before-leave="handleBeforeLeave">
<NTabPane name="si" :tab="`СофтИнфо (${cards.si?.meta.total})`">
<TableCards :filters="filters" :data="cards.si?.data" :meta="cards.si?.meta" min-height="calc(100vh - 286px)" max-height="calc(100vh - 320px)" />
</NTabPane>
<NTabPane name="mis" :tab="`МИС (${cards.mis?.meta.total})`">
<TableCards :filters="filters" :data="cards.mis?.data" :meta="cards.mis?.meta" min-height="calc(100vh - 286px)" max-height="calc(100vh - 320px)" />
</NTabPane>
</NTabs>
</AppLayout>
</template>
<style scoped>
:deep(.n-tabs .n-tabs-nav) {
max-width: 420px;
}
</style>