first commit
This commit is contained in:
531
app/Support/MedicalReport/EconomistWorkbook.php
Normal file
531
app/Support/MedicalReport/EconomistWorkbook.php
Normal 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user