first commit
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

This commit is contained in:
brusnitsyn
2026-04-03 17:20:05 +09:00
commit 3edc8e667e
358 changed files with 39258 additions and 0 deletions

View File

@@ -0,0 +1,531 @@
<?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(),
];
}
}