Добавил список умерших в статистику #4
This commit is contained in:
@@ -5,6 +5,7 @@ namespace App\Http\Controllers\Api;
|
|||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Department;
|
use App\Models\Department;
|
||||||
use App\Models\DutyUnwantedEvent;
|
use App\Models\DutyUnwantedEvent;
|
||||||
|
use App\Models\ReportDutyPatient;
|
||||||
use App\Services\DateRangeService;
|
use App\Services\DateRangeService;
|
||||||
use App\Services\ReportService;
|
use App\Services\ReportService;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
@@ -89,4 +90,83 @@ class StatisticController extends Controller
|
|||||||
|
|
||||||
return response()->json($observablePatients);
|
return response()->json($observablePatients);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getDeadPatients(Request $request)
|
||||||
|
{
|
||||||
|
$user = Auth::user();
|
||||||
|
$departmentType = $request->query('departmentType');
|
||||||
|
$availableDepartments = $user->availableDepartments()->pluck('department_id')->all();
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'startAt' => 'required',
|
||||||
|
'endAt' => 'required',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$dateRange = $this->dateRangeService->getNormalizedDateRange($user, $validated['startAt'], $validated['endAt']);
|
||||||
|
|
||||||
|
$lastMigrations = DB::table('report_duty_migration_patients')
|
||||||
|
->select('id', DB::raw('MAX(ingoing_date) as max_ingoing_date'))
|
||||||
|
->where('ingoing_date', '>=', $dateRange->startSql())
|
||||||
|
->where('ingoing_date', '<=', $dateRange->endSql())
|
||||||
|
->groupBy('id');
|
||||||
|
|
||||||
|
$deadPatients = DB::table('report_duty_migration_patients as rdm')
|
||||||
|
->joinSub($lastMigrations, 'last_rdm', function ($join) {
|
||||||
|
$join->on('rdm.id', '=', 'last_rdm.id')
|
||||||
|
->on('rdm.ingoing_date', '=', 'last_rdm.max_ingoing_date');
|
||||||
|
})
|
||||||
|
->leftJoin('report_duty_patients as rdp', 'rdm.id', '=', 'rdp.id')
|
||||||
|
->join('report_duties as rd', 'rd.id', '=', 'rdp.report_duty_id')
|
||||||
|
->join('departments as dep', 'dep.department_id', '=', 'rd.rf_department_id')
|
||||||
|
->whereIn('rd.rf_department_id', $availableDepartments)
|
||||||
|
->whereNotNull('rdp.death_date')
|
||||||
|
->select(
|
||||||
|
'rdp.id', 'rdp.full_name', 'rdp.birth_date', 'rdp.recipient_date', 'rdm.ingoing_date',
|
||||||
|
'rdm.diagnosis_code', 'rdm.diagnosis_name', 'dep.name_full'
|
||||||
|
)
|
||||||
|
->orderBy('rdm.ingoing_date', 'desc')
|
||||||
|
->orderBy('rdm.id', 'desc')
|
||||||
|
->get()
|
||||||
|
->groupBy('medical_history_id') // Группируем по id пациента
|
||||||
|
->map(fn ($rows) => $rows->first()) // Берем первую (самую последнюю) запись
|
||||||
|
->values()
|
||||||
|
->map(fn ($row) => [
|
||||||
|
'id' => $row->id,
|
||||||
|
'full_name' => $row->full_name,
|
||||||
|
'birth_date' => $row->birth_date,
|
||||||
|
'ingoing_date' => $row->ingoing_date ?? $row->recipient_date,
|
||||||
|
'diagnosis_code' => $row->diagnosis_code,
|
||||||
|
'diagnosis_name' => $row->diagnosis_name,
|
||||||
|
'department_name' => $row->name_full,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$deadPatients = ReportDutyPatient::query()
|
||||||
|
->whereHas('latestMigration', function ($query) use ($dateRange) {
|
||||||
|
$query->where('ingoing_date', '<=', $dateRange->endSql())
|
||||||
|
->where(function ($sub) use ($dateRange) {
|
||||||
|
// Миграции без out_date (еще лежат)
|
||||||
|
$sub->whereNull('out_date');
|
||||||
|
|
||||||
|
// Миграции с out_date (закрытые)
|
||||||
|
$sub->orWhere(function ($sub2) use ($dateRange) {
|
||||||
|
$sub2->whereNotNull('out_date')
|
||||||
|
->where('out_date', '>', $dateRange->startSql());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->with('latestMigration')
|
||||||
|
->whereNotNull('death_date')
|
||||||
|
->get()->unique('original_id')->values()
|
||||||
|
->map(fn ($row) => [
|
||||||
|
'id' => $row->id,
|
||||||
|
'full_name' => $row->full_name,
|
||||||
|
'birth_date' => $row->birth_date,
|
||||||
|
'ingoing_date' => $row->latestMigration->ingoing_date ?? $row->recipient_date,
|
||||||
|
'diagnosis_code' => $row->latestMigration->diagnosis_code,
|
||||||
|
'diagnosis_name' => $row->latestMigration->diagnosis_name,
|
||||||
|
'department_name' => $row->latestMigration->department->name_full,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json($deadPatients);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,11 @@ class ReportDutyMigrationPatient extends Model
|
|||||||
return $this->hasMany(SurgicalOperation::class, 'migration_patient_id', 'original_id');
|
return $this->hasMany(SurgicalOperation::class, 'migration_patient_id', 'original_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function department()
|
||||||
|
{
|
||||||
|
return $this->hasOne(Department::class, 'rf_mis_department_id', 'department_id');
|
||||||
|
}
|
||||||
|
|
||||||
public function reanimations()
|
public function reanimations()
|
||||||
{
|
{
|
||||||
return $this->hasMany(Reanimation::class, 'migration_patient_id', 'original_id')
|
return $this->hasMany(Reanimation::class, 'migration_patient_id', 'original_id')
|
||||||
|
|||||||
160
resources/js/Pages/Statistic/Components/ModalDeathPatients.vue
Normal file
160
resources/js/Pages/Statistic/Components/ModalDeathPatients.vue
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
NModal, NDataTable, NEmpty, NSpin, NDrawer, NDrawerContent, NInput, NIcon, NTooltip, NFlex, NText
|
||||||
|
} from 'naive-ui'
|
||||||
|
import { TbEye } from 'vue-icons-plus/tb'
|
||||||
|
import { format, formatDistanceStrict } from 'date-fns'
|
||||||
|
import { ru } from 'date-fns/locale'
|
||||||
|
import { computed, h, ref, watch } from 'vue'
|
||||||
|
import TooltipColumn from '../../Report/Components/DataTableColumns/TooltipColumn.vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
startAt: { required: true },
|
||||||
|
endAt: { required: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
const open = defineModel('open')
|
||||||
|
const loading = ref(true)
|
||||||
|
const deadPatients = ref([])
|
||||||
|
const currentPatient = ref(null)
|
||||||
|
const showDrawer = ref(false)
|
||||||
|
|
||||||
|
// Получаем уникальные отделения для фильтра
|
||||||
|
const departmentOptions = computed(() => {
|
||||||
|
const departments = new Set()
|
||||||
|
deadPatients.value.forEach(patient => {
|
||||||
|
if (patient.department_name) {
|
||||||
|
departments.add(patient.department_name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return Array.from(departments).map(dept => ({
|
||||||
|
label: dept,
|
||||||
|
value: dept
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Состояние для выбранных фильтров
|
||||||
|
const filteredDepartments = ref([])
|
||||||
|
|
||||||
|
// Отфильтрованные данные
|
||||||
|
const filteredPatients = computed(() => {
|
||||||
|
if (!filteredDepartments.value || filteredDepartments.value.length === 0) {
|
||||||
|
return deadPatients.value
|
||||||
|
}
|
||||||
|
return deadPatients.value.filter(
|
||||||
|
patient => filteredDepartments.value.includes(patient.department_name)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const columns = computed(() => [
|
||||||
|
{
|
||||||
|
title: '№',
|
||||||
|
key: 'index',
|
||||||
|
width: 30,
|
||||||
|
wrap: false,
|
||||||
|
render: (_, i) => i + 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'ФИО',
|
||||||
|
key: 'full_name',
|
||||||
|
width: 200,
|
||||||
|
ellipsis: { tooltip: { arrow: false } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Возраст',
|
||||||
|
key: 'age',
|
||||||
|
width: 80,
|
||||||
|
render: (row) => row.birth_date
|
||||||
|
? formatDistanceStrict(new Date(row.birth_date), new Date(), { locale: ru })
|
||||||
|
: '—',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Диагноз',
|
||||||
|
key: 'diagnosis',
|
||||||
|
width: 90,
|
||||||
|
render: (row) => {
|
||||||
|
return row.diagnosis_code
|
||||||
|
? h(TooltipColumn, { triggerText: row.diagnosis_code, contentText: row.diagnosis_name })
|
||||||
|
: '—'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Отделение',
|
||||||
|
key: 'department_name',
|
||||||
|
width: 220,
|
||||||
|
// Добавляем фильтр для множественного выбора
|
||||||
|
filterMultiple: true,
|
||||||
|
filterOptions: departmentOptions.value,
|
||||||
|
// Функция onFilter обновляет состояние фильтра
|
||||||
|
filter: (value, row) => {
|
||||||
|
return !!~row.department_name.indexOf(value)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const fetch = () => {
|
||||||
|
loading.value = true
|
||||||
|
axios.get('/api/statistics/reports/dead-patients', {
|
||||||
|
params: { startAt: props.startAt, endAt: props.endAt }
|
||||||
|
}).then((res) => {
|
||||||
|
deadPatients.value = res.data ?? []
|
||||||
|
|
||||||
|
// Сбрасываем фильтр при загрузке новых данных
|
||||||
|
filteredDepartments.value = []
|
||||||
|
}).finally(() => {
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(open, (isOpen) => {
|
||||||
|
if (isOpen && props.endAt && props.startAt) {
|
||||||
|
fetch()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NModal
|
||||||
|
v-model:show="open"
|
||||||
|
title="Умершие"
|
||||||
|
preset="card"
|
||||||
|
:mask-closable="false"
|
||||||
|
:close-on-esc="false"
|
||||||
|
class="max-w-5xl h-[calc(100vh-220px)]"
|
||||||
|
id="modal-observable-patients"
|
||||||
|
>
|
||||||
|
<template v-if="loading">
|
||||||
|
<div class="flex items-center justify-center h-full">
|
||||||
|
<NSpin />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="deadPatients.length">
|
||||||
|
<NDataTable
|
||||||
|
:columns="columns"
|
||||||
|
:data="filteredPatients"
|
||||||
|
table-layout="fixed"
|
||||||
|
size="small"
|
||||||
|
:loading="loading"
|
||||||
|
max-height="calc(100vh - 350px)"
|
||||||
|
:row-key="(row) => row.id"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="h-full flex items-center justify-center">
|
||||||
|
<NEmpty description="Нет данных!" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</NModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(.n-data-table-th),
|
||||||
|
:deep(.n-data-table-td) {
|
||||||
|
font-size: var(--n-font-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-modal .v-binder-follower-content) {
|
||||||
|
position: fixed;
|
||||||
|
inset: auto auto auto auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -23,6 +23,7 @@ import ModalObservablePatients from "./Components/ModalObservablePatients.vue";
|
|||||||
import StatisticRecipientPlanOfYear from "../../Layouts/Components/Statistic/StatisticRecipientPlanOfYear.vue";
|
import StatisticRecipientPlanOfYear from "../../Layouts/Components/Statistic/StatisticRecipientPlanOfYear.vue";
|
||||||
import {percentType} from "../../Utils/numbers.js";
|
import {percentType} from "../../Utils/numbers.js";
|
||||||
import OutcomeColumn from "./Components/OutcomeColumn.vue";
|
import OutcomeColumn from "./Components/OutcomeColumn.vue";
|
||||||
|
import ModalDeathPatients from "./Components/ModalDeathPatients.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
data: {
|
data: {
|
||||||
@@ -248,6 +249,19 @@ const columns = ref([
|
|||||||
width: 48,
|
width: 48,
|
||||||
titleAlign: 'center',
|
titleAlign: 'center',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
|
render: (row) => {
|
||||||
|
if (row.isTotalRow) {
|
||||||
|
return h(
|
||||||
|
'span',
|
||||||
|
{
|
||||||
|
onClick: () => onShowDeathPatientsModal()
|
||||||
|
},
|
||||||
|
row.deceased
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return row.deceased
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Мед. персонал',
|
title: 'Мед. персонал',
|
||||||
@@ -261,6 +275,8 @@ const columns = ref([
|
|||||||
const currentDepartmentId = ref(null)
|
const currentDepartmentId = ref(null)
|
||||||
const showUnwantedEventsModal = ref(false)
|
const showUnwantedEventsModal = ref(false)
|
||||||
const showObservablePatientsModal = ref(false)
|
const showObservablePatientsModal = ref(false)
|
||||||
|
const showDeathPatientsModal = ref(false)
|
||||||
|
|
||||||
const onShowUnwantedEventsModal = (departmentId) => {
|
const onShowUnwantedEventsModal = (departmentId) => {
|
||||||
currentDepartmentId.value = departmentId
|
currentDepartmentId.value = departmentId
|
||||||
showUnwantedEventsModal.value = true
|
showUnwantedEventsModal.value = true
|
||||||
@@ -269,6 +285,9 @@ const onShowObservablePatientsModal = (departmentId) => {
|
|||||||
currentDepartmentId.value = departmentId
|
currentDepartmentId.value = departmentId
|
||||||
showObservablePatientsModal.value = true
|
showObservablePatientsModal.value = true
|
||||||
}
|
}
|
||||||
|
const onShowDeathPatientsModal = () => {
|
||||||
|
showDeathPatientsModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
const rowProps = (row) => {
|
const rowProps = (row) => {
|
||||||
if (row.isGroupHeader) return {
|
if (row.isGroupHeader) return {
|
||||||
@@ -371,6 +390,7 @@ const buildReportHref = (departmentId, startAt, endAt) => {
|
|||||||
</NDataTable>
|
</NDataTable>
|
||||||
<ModalUnwantedEvents v-model:open="showUnwantedEventsModal" :start-at="date[0]" :end-at="date[1]" :department-id="currentDepartmentId" />
|
<ModalUnwantedEvents v-model:open="showUnwantedEventsModal" :start-at="date[0]" :end-at="date[1]" :department-id="currentDepartmentId" />
|
||||||
<ModalObservablePatients v-model:open="showObservablePatientsModal" :start-at="date[0]" :end-at="date[1]" :department-id="currentDepartmentId" />
|
<ModalObservablePatients v-model:open="showObservablePatientsModal" :start-at="date[0]" :end-at="date[1]" :department-id="currentDepartmentId" />
|
||||||
|
<ModalDeathPatients v-model:open="showDeathPatientsModal" :start-at="date[0]" :end-at="date[1]" />
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ Route::middleware(['auth:sanctum'])->group(function () {
|
|||||||
Route::prefix('reports')->group(function () {
|
Route::prefix('reports')->group(function () {
|
||||||
Route::get('/unwanted-events', [\App\Http\Controllers\Api\StatisticController::class, 'getUnwantedEvents']);
|
Route::get('/unwanted-events', [\App\Http\Controllers\Api\StatisticController::class, 'getUnwantedEvents']);
|
||||||
Route::get('/observable-patients', [\App\Http\Controllers\Api\StatisticController::class, 'getObservablePatients']);
|
Route::get('/observable-patients', [\App\Http\Controllers\Api\StatisticController::class, 'getObservablePatients']);
|
||||||
|
Route::get('/dead-patients', [\App\Http\Controllers\Api\StatisticController::class, 'getDeadPatients']);
|
||||||
});
|
});
|
||||||
Route::get('/headquarters', [\App\Http\Controllers\Api\HeadquartersController::class, 'stats']);
|
Route::get('/headquarters', [\App\Http\Controllers\Api\HeadquartersController::class, 'stats']);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user