Добавил реанимацию

Правки окна операций
This commit is contained in:
brusnitsyn
2026-05-07 22:37:07 +09:00
parent bb9e67ab3d
commit 6cf1ffbb2b
11 changed files with 137 additions and 45 deletions

View File

@@ -84,6 +84,9 @@ class DutyReportController extends Controller
$transferredHistories = MedicalHistoryResource::collection( $transferredHistories = MedicalHistoryResource::collection(
$this->medicalHistoryService->getTransferredHistories($dateRange, $department->rf_mis_department_id) $this->medicalHistoryService->getTransferredHistories($dateRange, $department->rf_mis_department_id)
); );
$reanimationHistories = MedicalHistoryResource::collection(
$this->medicalHistoryService->getReanimationHistories($dateRange, $department->rf_mis_department_id)
);
} }
return Inertia::render('Report/Index', [ return Inertia::render('Report/Index', [
@@ -94,6 +97,7 @@ class DutyReportController extends Controller
'dischargedHistories' => $dischargedHistories, 'dischargedHistories' => $dischargedHistories,
'deceasedHistories' => $deceasedHistories, 'deceasedHistories' => $deceasedHistories,
'transferredHistories' => $transferredHistories, 'transferredHistories' => $transferredHistories,
'reanimationHistories' => $reanimationHistories,
'dates' => [ 'dates' => [
$dateRange->startDate->getTimestampMs(), $dateRange->startDate->getTimestampMs(),
$dateRange->endDate->getTimestampMs(), $dateRange->endDate->getTimestampMs(),

View File

@@ -36,6 +36,11 @@ class MedicalHistory extends MaterializedViewModel
->latest('ingoing_date'); ->latest('ingoing_date');
} }
public function reanimations()
{
return $this->hasMany(Reanimation::class, 'medical_history_id', 'id');
}
public function operationsInDepartment($query, $departmentId) public function operationsInDepartment($query, $departmentId)
{ {
return $this->operations()->where('department_id', $departmentId); return $this->operations()->where('department_id', $departmentId);

View File

@@ -31,6 +31,12 @@ class MigrationPatient extends MaterializedViewModel
->orderBy('end_date', 'desc'); ->orderBy('end_date', 'desc');
} }
public function reanimations()
{
return $this->hasMany(Reanimation::class, 'migration_patient_id', 'id')
->orderBy('out_date', 'desc');
}
// Пересечение с отчетным периодом // Пересечение с отчетным периодом
public function scopeDateRange($query, string $from, string $to) public function scopeDateRange($query, string $from, string $to)
{ {

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Models;
use App\Services\DateRange;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
/**
* @property int $id
* @property int $migration_patient_id
* @property int $medical_history_id
* @property \Carbon\Carbon $in_date
* @property \Carbon\Carbon|null $out_date
* @property string|null $description
* @property int|null $stationar_branch_id
* @property int|null $department_id
* @property int|null $migration_stationar_branch_id
* @property int|null $migration_department_id
* @property int|null $doctor_id
*/
class Reanimation extends MaterializedViewModel
{
protected $table = 'mv_reanimation_summary';
protected $primaryKey = 'id';
public function medicalHistory()
{
return $this->belongsTo(MedicalHistory::class, 'medical_history_id', 'id');
}
public function migration()
{
return $this->belongsTo(MigrationPatient::class, 'migration_patient_id', 'id');
}
// Фильтр по подразделению
public function scopeDepartment($query, int $departmentId)
{
return $query->where('migration_department_id', $departmentId);
}
public function scopeCurrentOrAdmitted($query, DateRange $dateRange)
{
return $query->where(function ($q) use ($dateRange) {
// Вариант А: Пациент уже лежит (текущий)
$q->whereNull('out_date')
->whereNotNull('medical_history_id')
->where('in_date', '<', $dateRange->startSql());
})
->orWhere(function ($q) use ($dateRange) {
$q->where('in_date', '<=', $dateRange->endSql())
->where('in_date', '>', $dateRange->startSql());
});
}
}

View File

@@ -76,6 +76,7 @@ class MedicalHistoryService
->sortByDesc(fn ($mh) => $mh->latestMigration->ingoing_date ?? $mh->recipient_date) ->sortByDesc(fn ($mh) => $mh->latestMigration->ingoing_date ?? $mh->recipient_date)
->values(); ->values();
} }
public function getEmergencyHistories(DateRange $dateRange, int $departmentId) public function getEmergencyHistories(DateRange $dateRange, int $departmentId)
{ {
return MedicalHistory::query() return MedicalHistory::query()
@@ -146,4 +147,27 @@ class MedicalHistoryService
}, 'latestMigration.operations']) }, 'latestMigration.operations'])
->get(); ->get();
} }
public function getReanimationHistories(DateRange $dateRange, int $departmentId)
{
return MedicalHistory::query()
->whereHas('migrations', function ($q) use ($departmentId, $dateRange) {
$q->department($departmentId)->currentOrAdmitted($dateRange);
})
->whereHas('latestMigration.reanimations', function ($q) use ($dateRange) {
$q->currentOrAdmitted($dateRange);
})
->with(
[
'latestMigration' => function ($q) use ($departmentId, $dateRange) {
$q->department($departmentId)->currentOrAdmitted($dateRange)->latest('ingoing_date'); // подгружаем только отфильтрованные движения
},
'latestMigration.operations',
'latestMigration.reanimations'
])
->get()
// Сортировка по дате поступления в отделение (поле дочерней таблицы)
->sortByDesc(fn ($mh) => $mh->latestMigration->ingoing_date ?? $mh->recipient_date)
->values();
}
} }

17
package-lock.json generated
View File

@@ -1366,13 +1366,6 @@
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/katex": {
"version": "0.16.7",
"resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.7.tgz",
"integrity": "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/lodash": { "node_modules/@types/lodash": {
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.21.tgz",
@@ -3085,15 +3078,14 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/naive-ui": { "node_modules/naive-ui": {
"version": "2.43.2", "version": "2.44.1",
"resolved": "https://registry.npmjs.org/naive-ui/-/naive-ui-2.43.2.tgz", "resolved": "https://registry.npmjs.org/naive-ui/-/naive-ui-2.44.1.tgz",
"integrity": "sha512-YlLMnGrwGTOc+zMj90sG3ubaH5/7czsgLgGcjTLA981IUaz8r6t4WIujNt8r9PNr+dqv6XNEr0vxkARgPPjfBQ==", "integrity": "sha512-reo8Esw0p58liZwbUutC7meW24Xbn3EwNv91zReWKm2W4JPu+zfgJRn/F7aO0BFmvN+h2brA2M5lRvYqLq4kuA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@css-render/plugin-bem": "^0.15.14", "@css-render/plugin-bem": "^0.15.14",
"@css-render/vue3-ssr": "^0.15.14", "@css-render/vue3-ssr": "^0.15.14",
"@types/katex": "^0.16.2",
"@types/lodash": "^4.17.20", "@types/lodash": "^4.17.20",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"async-validator": "^4.2.5", "async-validator": "^4.2.5",
@@ -3111,6 +3103,9 @@
"vooks": "^0.2.12", "vooks": "^0.2.12",
"vueuc": "^0.4.65" "vueuc": "^0.4.65"
}, },
"engines": {
"node": ">=20"
},
"peerDependencies": { "peerDependencies": {
"vue": "^3.0.0" "vue": "^3.0.0"
} }

View File

@@ -76,7 +76,7 @@ watch(() => [props.minH, props.maxH], ([minH, maxH]) => {
</template> </template>
<style scoped> <style scoped>
.no-padding :deep(.n-card__content) { .no-padding :deep(.n-card-content) {
padding: 0 !important; padding: 0 !important;
} }
</style> </style>

View File

@@ -5,13 +5,15 @@ const props = defineProps({
operations: Array operations: Array
}) })
const emits = defineEmits(['click'])
const firstOperation = computed(() => props.operations[0]) const firstOperation = computed(() => props.operations[0])
</script> </script>
<template> <template>
<NTooltip v-if="operations.length" :arrow="false"> <NTooltip v-if="operations.length" :arrow="false">
<template #trigger> <template #trigger>
<div class="absolute inset-0 p-2 pt-2.5"> <div class="absolute inset-0 p-2 pt-2.5" @click="emits('click', operations)">
{{ firstOperation?.code_service }} {{ firstOperation?.code_service }}
</div> </div>
</template> </template>

View File

@@ -6,14 +6,10 @@ import {format} from "date-fns";
import AppPanel from "../../../Components/AppPanel.vue"; import AppPanel from "../../../Components/AppPanel.vue";
const props = defineProps({ const props = defineProps({
historyId: { operations: Array
type: Number,
required: true
}
}) })
const show = defineModel('show') const show = defineModel('show')
const operations = ref(null)
const showInfoDrawer = ref(false) const showInfoDrawer = ref(false)
const showedOperation = ref(null) const showedOperation = ref(null)
@@ -21,39 +17,30 @@ const onShowInfoDrawer = (operation) => {
showedOperation.value = {...operation} showedOperation.value = {...operation}
showInfoDrawer.value = true showInfoDrawer.value = true
} }
watch(() => props.historyId, (value) => {
console.log(value)
axios.get(`/api/mis/operations`, {
params: {
historyId: value
}
})
.then(response => {
operations.value = response.data
})
})
</script> </script>
<template> <template>
<NModal v-model:show="show" <NModal v-model:show="show"
title="Операции" title="Операции"
:mask-closable="false" :mask-closable="false"
class="max-w-xl overflow-clip min-h-[420px] max-h-[600px]" :segmented="{ content: true }"
class="max-w-xl overflow-clip h-[400px]"
draggable
content-scrollable
preset="card" preset="card"
id="modal-operation" id="modal-operation"
> >
<div class="h-full flex flex-col gap-y-4"> <div class="space-y-4">
<NThing v-for="operation in operations"> <NThing v-for="operation in operations">
<template #header> <template #header>
Операция {{operation.num}} Операция
</template> </template>
<template #header-extra> <template #header-extra>
<NTag size="small" type="info" round :bordered="false"> <NTag size="small" type="info" round :bordered="false">
<template #icon> <template #icon>
<TbClockCheck size="16" /> <TbClockCheck size="16" />
</template> </template>
{{operation.duration}} мин. мин.
</NTag> </NTag>
</template> </template>
<template #description> <template #description>
@@ -62,17 +49,17 @@ watch(() => props.historyId, (value) => {
<template #icon> <template #icon>
<TbCalendar size="18" /> <TbCalendar size="18" />
</template> </template>
{{format(operation.startAt, 'dd.MM.yyyy')}} {{format(operation.start_date, 'dd.MM.yyyy')}}
</NTag> </NTag>
<NTag type="success" round :bordered="false"> <NTag type="success" round :bordered="false">
<template #icon> <template #icon>
<TbTag size="18" /> <TbTag size="18" />
</template> </template>
{{operation.service.ServiceMedicalCode}} {{operation.code_service}}
</NTag> </NTag>
</NSpace> </NSpace>
</template> </template>
{{operation.service.ServiceMedicalName}} {{operation.name_service}}
<template #action> <template #action>
<NButton type="primary" size="small" @click="onShowInfoDrawer(operation)"> <NButton type="primary" size="small" @click="onShowInfoDrawer(operation)">
Просмотреть Просмотреть
@@ -83,10 +70,10 @@ watch(() => props.historyId, (value) => {
<NDrawer v-model:show="showInfoDrawer" width="100%" height="100%" to="#modal-operation" placement="bottom"> <NDrawer v-model:show="showInfoDrawer" width="100%" height="100%" to="#modal-operation" placement="bottom">
<NDrawerContent closable> <NDrawerContent closable>
<template #header> <template #header>
Операция {{showedOperation.num}} Операция
</template> </template>
<AppPanel no-padding header="Описание" class="h-full!"> <AppPanel no-padding header="Описание" class="h-full!">
<NInput type="textarea" :resizable="false" class="h-full" readonly v-model:value="showedOperation.description" /> <NInput type="textarea" :resizable="false" class="h-full!" readonly v-model:value="showedOperation.description" />
</AppPanel> </AppPanel>
</NDrawerContent> </NDrawerContent>
</NDrawer> </NDrawer>

View File

@@ -39,7 +39,7 @@ const tableColumns = computed(() => {
const searchArg = ref(null) const searchArg = ref(null)
const findPatient = (arg) => { const findPatient = (arg) => {
patients.value = patients.value.find(itm => itm.full_name === arg) // TODO: сделать поиск пациента через БДц
} }
const rowProps = (row) => { const rowProps = (row) => {
@@ -69,8 +69,8 @@ const rowProps = (row) => {
<NDataTable :columns="tableColumns" <NDataTable :columns="tableColumns"
:data="patients" :data="patients"
table-layout="fixed" table-layout="fixed"
max-height="280" max-height="234"
min-height="280" min-height="234"
:loading="loading" :loading="loading"
size="small" size="small"
:row-props="rowProps" :row-props="rowProps"

View File

@@ -16,6 +16,7 @@ import TooltipColumn from "./Components/DataTableColumns/TooltipColumn.vue";
import ActionsColumn from "./Components/DataTableColumns/ActionsColumn.vue"; import ActionsColumn from "./Components/DataTableColumns/ActionsColumn.vue";
import OperationsColumn from "./Components/DataTableColumns/OperationsColumn.vue"; import OperationsColumn from "./Components/DataTableColumns/OperationsColumn.vue";
import ReportWidget from "./Components/ReportWidget.vue"; import ReportWidget from "./Components/ReportWidget.vue";
import OperationInfoModal from "./Components/OperationInfoModal.vue";
const props = defineProps({ const props = defineProps({
department: { department: {
@@ -63,6 +64,10 @@ const props = defineProps({
type: Object, type: Object,
default: { data: [] } default: { data: [] }
}, },
reanimationHistories: {
type: Object,
default: { data: [] }
},
dates: { dates: {
type: Array, type: Array,
default: [] default: []
@@ -73,6 +78,8 @@ const reportStore = useReportStore()
const authStore = useAuthStore() const authStore = useAuthStore()
const userDepartment = authStore.userDepartment const userDepartment = authStore.userDepartment
const loading = ref(false) const loading = ref(false)
const operationsInModal = ref(null)
const showOperationsModal = ref(false)
const columns = [ const columns = [
{ {
@@ -116,7 +123,7 @@ const columns = [
key: 'latest_migration.operations', key: 'latest_migration.operations',
width: 140, width: 140,
className: 'relative', className: 'relative',
render: (row) => h(OperationsColumn, { operations: row.latest_migration.operations }) render: (row) => h(OperationsColumn, { operations: row.latest_migration.operations, onClick: (operations) => onShowOperationsModal(operations) })
}, },
{ {
title: '', title: '',
@@ -134,6 +141,11 @@ const columns = [
} }
] ]
const onShowOperationsModal = (operations) => {
operationsInModal.value = operations
showOperationsModal.value = true
}
const syncPageProps = () => reportStore.initializeFromPage(props) const syncPageProps = () => reportStore.initializeFromPage(props)
onMounted(syncPageProps) onMounted(syncPageProps)
@@ -199,8 +211,8 @@ watch(() => props, (newProps) => {
<PatientTypeSectionItem label="Находятся на контроле" :counter="recipientHistories.data.length"> <PatientTypeSectionItem label="Находятся на контроле" :counter="recipientHistories.data.length">
<PatientDataTable :data="recipientHistories.data" :columns="columns" /> <PatientDataTable :data="recipientHistories.data" :columns="columns" />
</PatientTypeSectionItem> </PatientTypeSectionItem>
<PatientTypeSectionItem label="Находятся в реанимации" :counter="recipientHistories.data.length"> <PatientTypeSectionItem label="Находятся в реанимации" :counter="reanimationHistories.data.length">
<PatientDataTable :data="recipientHistories.data" :columns="columns" /> <PatientDataTable :data="reanimationHistories.data" :columns="columns" />
</PatientTypeSectionItem> </PatientTypeSectionItem>
<PatientTypeSectionItem label="Выбывшие" :counter="dischargedHistories.data.length + deceasedHistories.data.length"> <PatientTypeSectionItem label="Выбывшие" :counter="dischargedHistories.data.length + deceasedHistories.data.length">
<NTabs type="segment" animated> <NTabs type="segment" animated>
@@ -247,4 +259,5 @@ watch(() => props, (newProps) => {
</AppPanel> </AppPanel>
</AppContainer> </AppContainer>
</AppLayout> </AppLayout>
<OperationInfoModal :operations="operationsInModal" v-model:show="showOperationsModal" />
</template> </template>