diff --git a/app/Http/Controllers/Api/StatisticController.php b/app/Http/Controllers/Api/StatisticController.php index 58975ad..460c8d7 100644 --- a/app/Http/Controllers/Api/StatisticController.php +++ b/app/Http/Controllers/Api/StatisticController.php @@ -125,4 +125,123 @@ class StatisticController extends Controller return response()->json($deadPatients); } + + public function getTransferredPatients(Request $request) + { + $user = Auth::user(); + $departmentType = $request->query('departmentType'); + $availableDepartments = $user->misDepartments->pluck('rf_mis_department_id')->toArray(); + + $validated = $request->validate([ + 'startAt' => 'required', + 'endAt' => 'required', + ]); + + $dateRange = $this->dateRangeService->getNormalizedDateRange($user, $validated['startAt'], $validated['endAt']); + + $deadPatients = ReportDutyPatient::query() + ->whereHas('latestMigration', function ($q) use ($availableDepartments) { + $q->whereIn('department_id', $availableDepartments); + }) + ->with('latestMigration') + ->where('death_date', '>', $dateRange->startSql()) + ->where('death_date', '<=', $dateRange->endSql()) + ->orderBy('death_date', 'desc') + ->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); + } + + public function getPatients(Request $request) + { + $user = Auth::user(); + $departmentType = $request->query('departmentType'); + $availableDepartments = $user->misDepartments->pluck('rf_mis_department_id')->toArray(); + $validated = $request->validate([ + 'startAt' => 'required', + 'endAt' => 'required', + 'type' => 'required', + 'departmentId' => 'nullable' + ]); + $dateRange = $this->dateRangeService->getNormalizedDateRange($user, $validated['startAt'], $validated['endAt']); + + $query = ReportDutyPatient::join('report_duties', 'report_duty_patients.report_duty_id', '=', 'report_duties.id') + ->where('report_duties.period_start', '>=', $dateRange->startSql()) + ->where('report_duties.period_start', '<=', $dateRange->endSql()) + ->select('report_duty_patients.*') + ->when(isset($validated['departmentId']), function ($query) use ($validated) { + return $query->where('rf_department_id', $validated['departmentId']); + }) + ->distinct(['original_id']); + + $filterQuery = $this->getQueryByType($validated['type']); + if (isset($validated['type'])) { + $filterQuery($query, $dateRange->startDate, $dateRange->endDate); + } + + $patients = $query->get(); + + return response()->json($patients); + } + + private function getQueryByType($type) { + $types = [ + 'RECIPIENT_ALL' => function ($query, $start, $end) { + $query->where('report_duty_patients.recipient_date', '>=', $start) + ->where('report_duty_patients.recipient_date', '<=', $end); + }, + 'RECIPIENT_PLAN' => function ($query, $start, $end) { + $query->where('report_duty_patients.urgency_id', 1); + }, + 'RECIPIENT_EMERGENCY' => function ($query, $start, $end) { + $query->where('report_duty_patients.recipient_date', '>', $start) + ->where('report_duty_patients.recipient_date', '<', $end) + ->where('report_duty_patients.urgency_id', 2); + }, + 'RECIPIENT_TRANSFERRED' => function ($query, $start, $end) { + $query->whereIn('report_duty_patients.visit_result_id', [4, 14]); + }, + 'OUTCOME' => function ($query, $start, $end) { + $query->whereBetween('report_duty_patients.extract_date', [$start, $end]); + }, + 'CONSIST' => function ($query, $start, $end) { + // Пациенты, которые числятся в отчётах за период и не выписаны/не умерли на конец периода + $query->where(function ($q) use ($end) { + $q->whereNull('report_duty_patients.extract_date') + ->orWhere('report_duty_patients.extract_date', '>', $end); + }) + ->where(function ($q) use ($end) { + $q->whereNull('report_duty_patients.death_date') + ->orWhere('report_duty_patients.death_date', '>', $end); + }); + }, + 'DECEASED' => function ($query, $start, $end) { + $query->whereBetween('report_duty_patients.death_date', [$start, $end]); + }, + 'SURGICAL_PLAN' => function ($query, $start, $end) { + $query->whereHas('operations', function ($q) use ($start, $end) { + $q->whereBetween('surgical_operations.Date', [$start, $end]) + ->where('surgical_operations.type', 'plan'); + }); + }, + 'SURGICAL_EMERGENCY' => function ($query, $start, $end) { + $query->whereHas('operations', function ($q) use ($start, $end) { + $q->whereBetween('surgical_operations.Date', [$start, $end]) + ->where('surgical_operations.type', 'emergency'); + }); + }, + // ... остальные + ]; + + return $types[$type]; + } } diff --git a/resources/js/Pages/Statistic/Components/InfoModal/PatientListModal.vue b/resources/js/Pages/Statistic/Components/InfoModal/PatientListModal.vue new file mode 100644 index 0000000..6b4662f --- /dev/null +++ b/resources/js/Pages/Statistic/Components/InfoModal/PatientListModal.vue @@ -0,0 +1,119 @@ + + + + + diff --git a/resources/js/Pages/Statistic/Components/InfoModal/columns/index.js b/resources/js/Pages/Statistic/Components/InfoModal/columns/index.js new file mode 100644 index 0000000..2808bf9 --- /dev/null +++ b/resources/js/Pages/Statistic/Components/InfoModal/columns/index.js @@ -0,0 +1,53 @@ +// columns/index.js +import { h } from 'vue' +import { formatDistanceStrict } from 'date-fns' +import { ru } from 'date-fns/locale' +import TooltipColumn from '../../../../Report/Components/DataTableColumns/TooltipColumn.vue' + +export function getColumns(type) { + // Базовые общие колонки + const base = [ + { title: '№', key: 'index', width: 30, 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 => + row.diagnosis_code + ? h(TooltipColumn, { triggerText: row.diagnosis_code, contentText: row.diagnosis_name }) + : '—' }, + { title: 'Отделение', key: 'department_name', width: 220 }, + ] + + // Дополнительные колонки для конкретных типов + const extra = { + RECIPIENT_ALL: [ + { title: 'Дата поступления', key: 'admission_date', width: 130 }, + { title: 'Тип', key: 'admission_type', width: 100 }, // планово/экстр + ], + RECIPIENT_PLAN: [ + { title: 'Дата поступления', key: 'admission_date', width: 130 }, + { title: 'Тип', key: 'admission_type', width: 100 }, // планово/экстр + ], + RECIPIENT_EMERGENCY: [ + { title: 'Дата поступления', key: 'admission_date', width: 130 }, + { title: 'Тип', key: 'admission_type', width: 100 }, // планово/экстр + ], + SURGICAL_EMERGENCY: [ + { title: 'Дата операции', key: 'operation_date', width: 130 }, + { title: 'Хирург', key: 'surgeon', width: 150 }, + { title: 'Операция', key: 'operation_name', width: 200 }, + ], + SURGICAL_PLAN: [ + { title: 'Дата операции', key: 'operation_date', width: 130 }, + { title: 'Хирург', key: 'surgeon', width: 150 }, + { title: 'Операция', key: 'operation_name', width: 200 }, + ], + DECEASED: [ + { title: 'Дата смерти', key: 'death_date', width: 120 }, + { title: 'Причина смерти', key: 'cause_of_death', width: 150 }, + ], + } + + return [...base, ...(extra[type] || [])] +} diff --git a/resources/js/Pages/Statistic/Components/InfoModal/config.js b/resources/js/Pages/Statistic/Components/InfoModal/config.js new file mode 100644 index 0000000..43dadae --- /dev/null +++ b/resources/js/Pages/Statistic/Components/InfoModal/config.js @@ -0,0 +1,72 @@ +export const MODAL_TYPES = { + RECIPIENT_ALL: { + title: 'Поступившие', + endpoint: '/api/statistics/reports/admitted-patients', + columnsFactory: 'admittedColumns', + }, + RECIPIENT_PLAN: { + title: 'Плановые поступления', + endpoint: '/api/statistics/reports/admitted-patients', + columnsFactory: 'admittedColumns', + }, + RECIPIENT_EMERGENCY: { + title: 'Экстренные поступления', + endpoint: '/api/statistics/reports/emergency-patients', + columnsFactory: 'emergencyColumns', + }, + RECIPIENT_TRANSFERRED: { + title: 'Переведённые', + endpoint: '/api/statistics/reports/transferred-patients', + columnsFactory: 'transferredColumns', + }, + OUTCOME: { + title: 'Выбывшие', + endpoint: '/api/statistics/reports/discharged-patients', + columnsFactory: 'dischargedColumns', + }, + CONSIST: { + title: 'Состоит', + endpoint: '/api/statistics/reports/admitted-patients', + columnsFactory: 'admittedColumns', + }, + AVERAGE_BED_DAYS: { + title: 'Средний койко-день', + endpoint: '/api/statistics/reports/admitted-patients', + columnsFactory: 'admittedColumns', + }, + PREOPERATIVE_DAYS: { + title: 'Предоперационный койко-день', + endpoint: '/api/statistics/reports/admitted-patients', + columnsFactory: 'admittedColumns', + }, + PERCENT_LOADED_BEDS: { + title: 'Процент загруженности', + endpoint: '/api/statistics/reports/admitted-patients', + columnsFactory: 'admittedColumns', + }, + LETHALITY: { + title: 'Процент летальности', + endpoint: '/api/statistics/reports/dead-patients', + columnsFactory: 'deadColumns', // или функция + }, + SURGICAL_EMERGENCY: { + title: 'Экстренные операции', + endpoint: '/api/statistics/reports/dead-patients', + columnsFactory: 'deadColumns', // или функция + }, + SURGICAL_PLAN: { + title: 'Плановые операции', + endpoint: '/api/statistics/reports/dead-patients', + columnsFactory: 'deadColumns', // или функция + }, + DECEASED: { + title: 'Умершие', + endpoint: '/api/statistics/reports/dead-patients', + columnsFactory: 'deadColumns', // или функция + }, + COUNT_STAFF: { + title: 'Дежурный персонал', + endpoint: '/api/statistics/reports/dead-patients', + columnsFactory: 'deadColumns', // или функция + }, +} diff --git a/resources/js/Pages/Statistic/Index.vue b/resources/js/Pages/Statistic/Index.vue index 2fa174d..29d0c20 100644 --- a/resources/js/Pages/Statistic/Index.vue +++ b/resources/js/Pages/Statistic/Index.vue @@ -25,6 +25,7 @@ import {percentType} from "../../Utils/numbers.js"; import OutcomeColumn from "./Components/OutcomeColumn.vue"; import ModalDeathPatients from "./Components/ModalDeathPatients.vue"; import DeceasedColumn from "./Components/DeceasedColumn.vue"; +import PatientListModal from "./Components/InfoModal/PatientListModal.vue"; const props = defineProps({ data: { @@ -150,6 +151,11 @@ const columns = ref([ width: 60, titleAlign: 'center', align: 'center', + cellProps: (row, i) => ({ + onClick: () => { + onShowPatientsModal('RECIPIENT_ALL', row.department_id) + } + }) }, { title: 'План', @@ -157,6 +163,11 @@ const columns = ref([ width: 60, titleAlign: 'center', align: 'center', + cellProps: (row, i) => ({ + onClick: () => { + onShowPatientsModal('RECIPIENT_PLAN', row.department_id) + } + }) }, { title: 'Экстр', @@ -164,6 +175,11 @@ const columns = ref([ width: 60, titleAlign: 'center', align: 'center', + cellProps: (row, i) => ({ + onClick: () => { + onShowPatientsModal('RECIPIENT_EMERGENCY', row.department_id) + } + }) }, { title: 'Перевод', @@ -171,6 +187,11 @@ const columns = ref([ width: 84, titleAlign: 'center', align: 'center', + cellProps: (row, i) => ({ + onClick: () => { + onShowPatientsModal('RECIPIENT_TRANSFERRED', row.department_id) + } + }) }, ] }, @@ -281,6 +302,14 @@ const currentDepartmentId = ref(null) const showUnwantedEventsModal = ref(false) const showObservablePatientsModal = ref(false) const showDeathPatientsModal = ref(false) +const patientsModalType = ref(null) +const showPatientsModalType = ref(false) + +const onShowPatientsModal = (type, departmentId) => { + patientsModalType.value = type + currentDepartmentId.value = departmentId + showPatientsModalType.value = true +} const onShowUnwantedEventsModal = (departmentId) => { currentDepartmentId.value = departmentId @@ -399,6 +428,9 @@ const buildReportHref = (departmentId, startAt, endAt) => { :start-at="date[0]" :end-at="date[1]" /> + + diff --git a/resources/js/app.js b/resources/js/app.js index 9cdfd0d..32129bf 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -94,12 +94,35 @@ createInertiaApp({ sendDefaultPii: true, integrations: [ Sentry.replayIntegration(), + // Sentry.feedbackIntegration({ + // colorScheme: "system", + // showBranding: false, + // triggerLabel: "Сообщить об ошибке", + // triggerAriaLabel: "Открыть форму обратной связи", + // formTitle: "Сообщить об ошибке", + // submitButtonLabel: "Отправить отчёт", + // cancelButtonLabel: "Отмена", + // confirmButtonLabel: "Подтвердить", + // addScreenshotButtonLabel: "Добавить скриншот", + // removeScreenshotButtonLabel: "Удалить скриншот", + // nameLabel: "Имя", + // namePlaceholder: "Ваше имя", + // emailLabel: "Email", + // emailPlaceholder: "ваш.email@пример.рф", + // isRequiredLabel: "(обязательно)", + // messageLabel: "Описание проблемы", + // messagePlaceholder: "В чём ошибка? Что вы ожидали?", + // successMessageText: "Спасибо за ваш отчёт!", + // highlightToolText: "Выделить", + // hideToolText: "Скрыть", + // removeHighlightText: "Убрать", + // }), ], // Session Replay replaysSessionSampleRate: 0, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production. replaysOnErrorSampleRate: 0.5, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur., // Logs - enableLogs: true + enableLogs: true, }); vueApp.mount(el) diff --git a/routes/api.php b/routes/api.php index 2ecae44..6d7d371 100644 --- a/routes/api.php +++ b/routes/api.php @@ -58,6 +58,8 @@ Route::middleware(['auth:sanctum'])->group(function () { Route::get('/unwanted-events', [\App\Http\Controllers\Api\StatisticController::class, 'getUnwantedEvents']); Route::get('/observable-patients', [\App\Http\Controllers\Api\StatisticController::class, 'getObservablePatients']); Route::get('/dead-patients', [\App\Http\Controllers\Api\StatisticController::class, 'getDeadPatients']); + Route::get('/transferred-patients', [\App\Http\Controllers\Api\StatisticController::class, 'getTransferredPatients']); + Route::get('/patients', [\App\Http\Controllers\Api\StatisticController::class, 'getPatients']); }); Route::get('/headquarters', [\App\Http\Controllers\Api\HeadquartersController::class, 'stats']); });