532 lines
19 KiB
PHP
532 lines
19 KiB
PHP
<?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(),
|
||
];
|
||
}
|
||
}
|