Доработал отчет для экономистов

This commit is contained in:
brusnitsyn
2026-06-25 17:04:42 +09:00
parent 7495b0e3cb
commit d322317d06
6 changed files with 873 additions and 396 deletions

View File

@@ -70,7 +70,7 @@ class DepartmentStatisticsDataSet implements DataSet
new Column('avg_bed_days', 'Ср. койко-день', 'measure', 'number', null),
new Column('preoperative_days', 'Пред. опер. койко-день', 'measure', 'number', null),
new Column('percent_loaded', '% загруженности', 'measure', 'number', 'percent'),
new Column('lethality', '% смертности', 'measure', 'number', 'percent'),
new Column('lethality', '% летальности', 'measure', 'number', 'percent'),
new Column('surgery_emergency', 'Операции Э', 'measure', 'number', 'count'),
new Column('surgery_plan', 'Операции П', 'measure', 'number', 'count'),
new Column('deceased', 'Умерло', 'measure', 'number', 'count'),

View File

@@ -16,17 +16,29 @@ use Illuminate\Support\Facades\DB;
class StatisticsService
{
/**
* Справочник типов оплаты (profit_type_id) берётся из классификатора МИС (kl_ProfitType).
*/
public const PROFIT_TYPE_LABELS = [
0 => 'Не определено',
3 => 'ОМС',
4 => 'Бюджет',
5 => 'Платные услуги',
6 => 'ДМС',
7 => 'Другие',
];
public function __construct(
protected BedDayService $bedDayService,
protected PlanCalculator $planCalculator,
) {}
public function getStatisticsData(User $user, string $startDate, string $endDate, bool $isRangeOneDay): array
/**
* Отделения, доступные пользователю, сгруппированные по типу отделения.
*/
private function resolveUserDepartments(User $user)
{
$this->bedDayService->clearMemoryCache();
// 1. Получаем отделения
$departments = Department::select('department_id', 'name_short', 'rf_department_type', 'user_name', 'order')
return Department::select('department_id', 'name_short', 'rf_department_type', 'user_name', 'order')
->with('departmentType')
->join((new UserDepartment)->getTable(), (new Department)->getTable().'.department_id', (new UserDepartment)->getTable().'.rf_department_id')
->where((new UserDepartment)->getTable().'.rf_user_id', $user->id)
@@ -34,6 +46,14 @@ class StatisticsService
->orderBy((new UserDepartment)->getTable().'.order', 'asc')
->get()
->groupBy('departmentType.name_full');
}
public function getStatisticsData(User $user, string $startDate, string $endDate, bool $isRangeOneDay): array
{
$this->bedDayService->clearMemoryCache();
// 1. Получаем отделения
$departments = $this->resolveUserDepartments($user);
if ($departments->isEmpty()) {
return $this->emptyResponse();
@@ -289,6 +309,106 @@ class StatisticsService
];
}
/**
* Количество поступлений по типу оплаты (profit_type_id), с разбивкой по отделениям.
* "Поступление" определяется так же, как метрика "Поступило" на основном листе:
* дата поступления миграции пациента попадает в период конкретного дежурного отчёта.
*/
public function getProfitTypeBreakdown(User $user, string $startDate, string $endDate): array
{
$departments = $this->resolveUserDepartments($user);
if ($departments->isEmpty()) {
return $this->emptyProfitTypeResponse();
}
$allDeptIds = $departments->flatten()->pluck('department_id')->toArray();
$rows = DB::table('report_duties as rd')
->join('report_duty_patients as rdp', 'rdp.report_duty_id', '=', 'rd.id')
->join('report_duty_migration_patients as rdmp', 'rdmp.medical_history_id', '=', 'rdp.id')
->whereIn('rd.rf_department_id', $allDeptIds)
->where('rd.period_start', '>=', $startDate)
->where('rd.period_end', '<=', $endDate)
->whereColumn('rdmp.ingoing_date', '>', 'rd.period_start')
->whereColumn('rdmp.ingoing_date', '<=', 'rd.period_end')
->select('rd.rf_department_id', 'rdp.profit_type_id', DB::raw('COUNT(DISTINCT rdp.id) as count'))
->groupBy('rd.rf_department_id', 'rdp.profit_type_id')
->get();
// Поддерживаем коды, которых пока нет в справочнике (на случай новых типов из МИС)
$profitTypeLabels = self::PROFIT_TYPE_LABELS;
foreach ($rows->pluck('profit_type_id')->unique() as $profitTypeId) {
$profitTypeId = (int) $profitTypeId;
if (! array_key_exists($profitTypeId, $profitTypeLabels)) {
$profitTypeLabels[$profitTypeId] = "Тип оплаты #{$profitTypeId}";
}
}
$countsByDept = $rows->groupBy('rf_department_id');
$emptyCounts = array_fill_keys(array_keys($profitTypeLabels), 0);
$data = [];
$grandTotals = $emptyCounts;
foreach ($departments as $typeName => $deptList) {
$groupTotals = $emptyCounts;
$groupRows = [];
foreach ($deptList as $dept) {
$deptId = $dept->department_id;
$counts = $emptyCounts;
foreach ($countsByDept->get($deptId, collect()) as $row) {
$counts[(int) $row->profit_type_id] = (int) $row->count;
}
$groupRows[] = [
'department' => $dept->user_name ?? $dept->name_short,
'counts' => $counts,
'total' => array_sum($counts),
];
foreach ($counts as $profitTypeId => $count) {
$groupTotals[$profitTypeId] += $count;
$grandTotals[$profitTypeId] += $count;
}
}
$data[] = ['isGroupHeader' => true, 'groupName' => $typeName];
array_push($data, ...$groupRows);
$data[] = [
'isTotalRow' => true,
'department' => 'ИТОГО:',
'counts' => $groupTotals,
'total' => array_sum($groupTotals),
];
}
return [
'data' => $data,
'grandTotals' => [
'department' => 'ОБЩИЕ ИТОГИ:',
'counts' => $grandTotals,
'total' => array_sum($grandTotals),
],
'profitTypes' => $profitTypeLabels,
];
}
private function emptyProfitTypeResponse(): array
{
return [
'data' => [],
'grandTotals' => [
'department' => 'ОБЩИЕ ИТОГИ:',
'counts' => array_fill_keys(array_keys(self::PROFIT_TYPE_LABELS), 0),
'total' => 0,
],
'profitTypes' => self::PROFIT_TYPE_LABELS,
];
}
/**
* Посуточные ряды по каждому KPI за период для фоновых спарклайнов.
* Источник суточные дежурные отчёты (report_duties + duty_report_metric_results).