Files
econom-calculator/app/Support/MedicalReport/EconomistWorkbook.php
brusnitsyn 3edc8e667e
Some checks failed
tests / ci (8.5) (push) Has been cancelled
linter / quality (push) Has been cancelled
tests / ci (8.3) (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
first commit
2026-04-03 17:20:05 +09:00

532 lines
19 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Support\MedicalReport;
use App\Models\Department;
use App\Models\HospitalUnit;
use App\Models\MedicalReport;
use Illuminate\Support\Arr;
class EconomistWorkbook
{
public function __construct(
private readonly TemplateWorkbook $templateWorkbook,
private readonly ReportWorkbook $reportWorkbook,
private readonly StructuredTemplateRegistry $structuredTemplateRegistry,
) {}
/**
* @return array<string, mixed>
*/
public function pageData(MedicalReport $medicalReport): array
{
$structuredSummary = $this->structuredSummary($medicalReport);
$analysisSheet = $this->analysisSheet($medicalReport);
$analysisIssues = $this->analysisIssues($medicalReport);
if ($structuredSummary !== null || $analysisSheet !== null) {
return [
'report' => [
'id' => $medicalReport->id,
'name' => $medicalReport->name,
'year' => $medicalReport->year,
'updated_at' => $medicalReport->updated_at?->toIso8601String(),
],
'summarySheet' => $structuredSummary,
'analysisSheet' => $analysisSheet,
'analysisIssues' => $analysisIssues,
];
}
$sheetKey = $this->templateWorkbook->firstSheetKey();
$sheet = $sheetKey === null ? null : $this->templateWorkbook->sheet($sheetKey);
$departments = $this->templateWorkbook->departments();
$columns = $sheetKey === null ? [] : $this->columns($sheetKey);
$rows = $sheetKey === null
? []
: array_values(array_filter(array_map(
fn (array $department): ?array => $this->departmentRow($medicalReport, $sheetKey, $department, $columns),
$departments,
)));
return [
'report' => [
'id' => $medicalReport->id,
'name' => $medicalReport->name,
'year' => $medicalReport->year,
'updated_at' => $medicalReport->updated_at?->toIso8601String(),
],
'summarySheet' => $sheet === null ? null : [
'key' => $sheetKey,
'name' => $sheet['name'],
'columns' => $columns,
'rows' => $rows,
'filled_departments' => collect($rows)
->filter(fn (array $row): bool => $row['filled_count'] > 0)
->count(),
'filled_cells' => collect($rows)->sum('filled_count'),
],
'analysisSheet' => null,
'analysisIssues' => $analysisIssues,
];
}
/**
* @return array{unmapped_departments: list<array<string, string>>, unmapped_metrics: list<array<string, string>>, total_count: int}
*/
private function analysisIssues(MedicalReport $medicalReport): array
{
$unmappedDepartments = [];
$unmappedMetrics = [];
foreach ($this->templateWorkbook->departments() as $department) {
$template = $this->structuredTemplateRegistry->templateForDepartment($department['key']);
if ($template === null) {
continue;
}
foreach (($template['sections'] ?? []) as $section) {
$entries = Arr::get(
$medicalReport->input_overrides ?? [],
'structured_departments.'.$department['key'].'.'.$template['key'].'.sections.'.$section['key'].'.entries',
[],
);
if ($entries === []) {
continue;
}
foreach (($section['export_metrics'] ?? []) as $metric) {
$analysisColumn = trim((string) ($metric['analysis_column'] ?? ''));
if ($analysisColumn === '') {
$unmappedMetrics[] = [
'department' => (string) $department['name'],
'section' => (string) ($section['title'] ?? ''),
'metric' => (string) ($metric['label'] ?? $metric['key'] ?? ''),
'reason' => 'Не указана колонка анализа',
];
}
}
foreach ($entries as $entry) {
$departmentId = trim((string) ($entry['department_id'] ?? ''));
if ($departmentId === '') {
continue;
}
if ($this->hospitalUnitSlugForEntry($entry) !== null) {
continue;
}
$sourceDepartment = Department::query()
->whereKey((int) $departmentId)
->value('name');
$unmappedDepartments[] = [
'department' => (string) $department['name'],
'section' => (string) ($section['title'] ?? ''),
'source_department' => (string) ($sourceDepartment ?? $departmentId),
'reason' => 'Нет связи с подразделением финансистов',
];
}
}
}
$unmappedDepartments = collect($unmappedDepartments)
->unique(fn (array $issue): string => implode('|', $issue))
->values()
->all();
$unmappedMetrics = collect($unmappedMetrics)
->unique(fn (array $issue): string => implode('|', $issue))
->values()
->all();
return [
'unmapped_departments' => $unmappedDepartments,
'unmapped_metrics' => $unmappedMetrics,
'total_count' => count($unmappedDepartments) + count($unmappedMetrics),
];
}
/**
* @return array<string, mixed>|null
*/
private function analysisSheet(MedicalReport $medicalReport): ?array
{
$columns = collect(config('medical-report.economist_analysis_columns', []))
->filter(fn (mixed $column): bool => is_array($column))
->map(fn (array $column): array => [
'key' => (string) ($column['key'] ?? ''),
'name' => (string) ($column['label'] ?? ''),
'coordinate' => (string) ($column['coordinate'] ?? ''),
])
->filter(fn (array $column): bool => $column['key'] !== '')
->values();
if ($columns->isEmpty()) {
return null;
}
$units = HospitalUnit::query()
->with('profile:id,name,sort_order')
->where('is_active', true)
->get()
->sortBy([
fn (HospitalUnit $unit): int => $unit->profile?->sort_order ?? PHP_INT_MAX,
fn (HospitalUnit $unit): string => $unit->name,
])
->values();
$valuesByUnit = [];
foreach ($this->templateWorkbook->departments() as $department) {
$template = $this->structuredTemplateRegistry->templateForDepartment($department['key']);
if ($template === null) {
continue;
}
foreach ($template['sections'] ?? [] as $section) {
$entries = Arr::get(
$medicalReport->input_overrides ?? [],
'structured_departments.'.$department['key'].'.'.$template['key'].'.sections.'.$section['key'].'.entries',
[],
);
if ($entries === []) {
continue;
}
foreach (($section['export_metrics'] ?? []) as $metric) {
$analysisColumn = (string) ($metric['analysis_column'] ?? '');
if ($analysisColumn === '') {
continue;
}
$rowMode = (string) ($metric['row_mode'] ?? '');
if ($rowMode === 'entry_department') {
foreach ($entries as $entry) {
$unitSlug = $this->hospitalUnitSlugForEntry($entry);
if ($unitSlug === null) {
continue;
}
$value = $this->numericValue($entry[$metric['source_field']] ?? null);
$valuesByUnit[$unitSlug][$analysisColumn] = ($valuesByUnit[$unitSlug][$analysisColumn] ?? 0) + $value;
}
continue;
}
if ($rowMode === 'fixed_unit') {
$unitSlug = (string) ($metric['target_unit_slug'] ?? '');
if ($unitSlug === '') {
continue;
}
$sum = collect($entries)->sum(fn (array $entry): float => $this->numericValue($entry[$metric['source_field']] ?? null));
$valuesByUnit[$unitSlug][$analysisColumn] = ($valuesByUnit[$unitSlug][$analysisColumn] ?? 0) + $sum;
}
}
}
}
if ($valuesByUnit === []) {
return null;
}
$rows = $units->map(function (HospitalUnit $unit) use ($columns, $valuesByUnit): array {
$unitValues = $valuesByUnit[$unit->slug] ?? [];
return [
'profile_name' => $unit->profile?->name,
'unit_name' => $unit->name,
'filled_count' => collect($unitValues)
->filter(fn (float|int $value): bool => (float) $value !== 0.0)
->count(),
'values' => $columns->map(fn (array $column): array => [
'key' => $column['key'],
'value' => $this->formatNumber((float) ($unitValues[$column['key']] ?? 0)),
])->all(),
];
})->all();
return [
'key' => 'economist-analysis',
'name' => 'Матрица экономистов',
'columns' => $columns->all(),
'rows' => $rows,
'filled_units' => collect($rows)->filter(fn (array $row): bool => $row['filled_count'] > 0)->count(),
'filled_cells' => collect($rows)->sum('filled_count'),
];
}
/**
* @return array<string, mixed>|null
*/
private function structuredSummary(MedicalReport $medicalReport): ?array
{
$rows = [];
foreach ($this->templateWorkbook->departments() as $department) {
$template = $this->structuredTemplateRegistry->templateForDepartment($department['key']);
if ($template === null) {
continue;
}
if (isset($template['sections']) && is_array($template['sections'])) {
foreach ($template['sections'] as $section) {
$entries = Arr::get(
$medicalReport->input_overrides ?? [],
'structured_departments.'.$department['key'].'.'.$template['key'].'.sections.'.$section['key'].'.entries',
[],
);
if ($entries === []) {
continue;
}
$totals = $this->exportTotals(
$entries,
$section['fields'] ?? [],
$section['export_metrics'] ?? [],
);
if ($totals === []) {
continue;
}
$rows[] = [
'department' => (string) ($section['economist_label'] ?? ($department['name'].' / '.$section['title'])),
'filled_count' => count(array_filter($totals, fn (array $value): bool => $value['value'] !== '0')),
'values' => $totals,
];
}
continue;
}
$entries = Arr::get(
$medicalReport->input_overrides ?? [],
'structured_departments.'.$department['key'].'.'.$template['key'].'.entries',
[],
);
if ($entries === []) {
continue;
}
$totals = $this->exportTotals(
$entries,
$template['fields'] ?? [],
$template['export_metrics'] ?? [],
);
if ($totals === []) {
continue;
}
$rows[] = [
'department' => $department['name'],
'filled_count' => count(array_filter($totals, fn (array $value): bool => $value['value'] !== '0')),
'values' => $totals,
];
}
if ($rows === []) {
return null;
}
$columns = collect($rows)
->flatMap(fn (array $row): array => $row['values'])
->unique('key')
->values()
->map(fn (array $value): array => [
'name' => $value['name'],
'coordinate' => $value['key'],
])
->all();
return [
'key' => 'structured-summary',
'name' => 'Итоги отделений',
'columns' => $columns,
'rows' => collect($rows)
->map(function (array $row) use ($columns): array {
$indexedValues = collect($row['values'])->keyBy('key');
return [
'department' => $row['department'],
'filled_count' => $row['filled_count'],
'values' => collect($columns)
->map(fn (array $column): array => [
'name' => $column['name'],
'value' => (string) ($indexedValues->get($column['coordinate'])['value'] ?? '0'),
])
->all(),
];
})
->all(),
'filled_departments' => count($rows),
'filled_cells' => (int) collect($rows)->sum('filled_count'),
];
}
/**
* @param list<array<string, mixed>> $entries
* @param list<array<string, mixed>> $fields
* @return list<array{key: string, name: string, value: string}>
*/
private function exportTotals(array $entries, array $fields, array $exportMetrics): array
{
$resolvedMetrics = $exportMetrics !== []
? collect($exportMetrics)
: collect($fields)
->filter(fn (array $field): bool => ($field['type'] ?? 'text') === 'number')
->map(fn (array $field): array => [
'key' => (string) $field['key'].'_total',
'label' => (string) $field['label'],
'source_field' => (string) $field['key'],
'aggregation' => 'sum',
]);
return $resolvedMetrics
->map(function (array $metric) use ($entries): array {
$sum = collect($entries)->sum(function (array $entry) use ($metric): float {
$value = trim((string) ($entry[$metric['source_field']] ?? ''));
if ($value === '') {
return 0;
}
return (float) str_replace(',', '.', $value);
});
return [
'key' => (string) $metric['key'],
'name' => (string) $metric['label'],
'value' => $this->formatNumber($sum),
];
})
->values()
->all();
}
private function hospitalUnitSlugForEntry(array $entry): ?string
{
$departmentId = $entry['department_id'] ?? null;
if ($departmentId === null || trim((string) $departmentId) === '') {
return null;
}
/** @var Department|null $department */
$department = Department::query()
->find((int) $departmentId);
if ($department === null) {
return null;
}
if ($department->hospital_unit_id !== null) {
return HospitalUnit::query()
->whereKey($department->hospital_unit_id)
->value('slug');
}
return HospitalUnit::query()
->where('name', $department->name)
->value('slug');
}
private function numericValue(mixed $value): float
{
$stringValue = trim((string) $value);
if ($stringValue === '') {
return 0;
}
return (float) str_replace(',', '.', $stringValue);
}
private function formatNumber(float $value): string
{
if ((float) ((int) $value) === $value) {
return (string) ((int) $value);
}
return rtrim(rtrim(number_format($value, 2, '.', ''), '0'), '.');
}
/**
* @return list<array{name: string, coordinate: string}>
*/
private function columns(string $sheetKey): array
{
$sheet = $this->templateWorkbook->sheet($sheetKey);
return collect($sheet['fields_by_department'])
->flatten(1)
->sortBy('column')
->unique('column')
->values()
->map(fn (array $field): array => [
'name' => $field['column_label'],
'coordinate' => $field['coordinate'],
])
->all();
}
/**
* @param list<array{name: string, coordinate: string}> $columns
* @return array<string, mixed>|null
*/
private function departmentRow(
MedicalReport $medicalReport,
string $sheetKey,
array $department,
array $columns,
): ?array {
$fields = $this->templateWorkbook->sheet($sheetKey)['fields_by_department'][$department['key']] ?? [];
if ($fields === []) {
return null;
}
$valuesByColumn = collect($fields)
->keyBy('column')
->map(function (array $field) use ($medicalReport, $sheetKey, $department): string {
$persistedValues = $this->reportWorkbook->persistedSheetValues(
$medicalReport,
$department['key'],
$sheetKey,
);
return (string) Arr::get($persistedValues, $field['coordinate'], $field['default']);
});
return [
'department' => $department['name'],
'filled_count' => $valuesByColumn
->filter(fn (string $value): bool => trim($value) !== '' && trim($value) !== '0')
->count(),
'values' => collect($columns)
->map(fn (array $column): array => [
'name' => $column['name'],
'value' => (string) ($valuesByColumn->get(Coordinates::split($column['coordinate'])['column']) ?? ''),
])
->all(),
];
}
}