first commit
This commit is contained in:
126
app/Services/Reports/AnalysisReportService.php
Normal file
126
app/Services/Reports/AnalysisReportService.php
Normal file
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Reports;
|
||||
|
||||
use App\Models\Department;
|
||||
use App\Models\ReportPeriod;
|
||||
use App\Models\ServiceCatalog;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class AnalysisReportService
|
||||
{
|
||||
/**
|
||||
* Create a new service instance.
|
||||
*/
|
||||
public function __construct(
|
||||
protected ServiceAllocationService $serviceAllocationService,
|
||||
protected MedicationExpenseService $medicationExpenseService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Build the analysis report for the given team and period.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function build(Team $team, int $year, int $month): array
|
||||
{
|
||||
$period = ReportPeriod::query()
|
||||
->whereBelongsTo($team)
|
||||
->where('year', $year)
|
||||
->where('month', $month)
|
||||
->first();
|
||||
|
||||
$departments = Department::query()
|
||||
->with('departmentProfile:id,name')
|
||||
->where('is_active', true)
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
$serviceCatalogs = ServiceCatalog::query()
|
||||
->where('is_active', true)
|
||||
->orderBy('sort_order')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
if ($period === null) {
|
||||
return [
|
||||
'period' => null,
|
||||
'columns' => $this->columns($serviceCatalogs),
|
||||
'rows' => [],
|
||||
'meta' => [
|
||||
'status' => null,
|
||||
'statusLabel' => null,
|
||||
'updatedAt' => null,
|
||||
'canEditSources' => false,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$serviceMatrix = $this->serviceAllocationService->incomingAllocationsForPeriod($period, $departments, $serviceCatalogs);
|
||||
$expenseMatrix = $this->medicationExpenseService->calculateForDepartments($period, $departments);
|
||||
|
||||
$rows = $departments->map(function (Department $department) use ($serviceMatrix, $expenseMatrix): array {
|
||||
return [
|
||||
'department' => [
|
||||
'id' => $department->id,
|
||||
'name' => $department->name,
|
||||
'profileName' => $department->departmentProfile->name,
|
||||
],
|
||||
'services' => $serviceMatrix[$department->id]['services'] ?? [],
|
||||
'serviceTotals' => [
|
||||
'total_quantity' => $serviceMatrix[$department->id]['totalQuantity'] ?? 0,
|
||||
'total_amount' => $serviceMatrix[$department->id]['totalAmount'] ?? 0,
|
||||
],
|
||||
'expense' => $expenseMatrix[$department->id]['totals'],
|
||||
];
|
||||
})->values()->all();
|
||||
|
||||
return [
|
||||
'period' => [
|
||||
'id' => $period->id,
|
||||
'year' => $period->year,
|
||||
'month' => $period->month,
|
||||
'label' => $period->label(),
|
||||
],
|
||||
'columns' => $this->columns($serviceCatalogs),
|
||||
'rows' => $rows,
|
||||
'meta' => [
|
||||
'status' => $period->status->value,
|
||||
'statusLabel' => $period->status->label(),
|
||||
'updatedAt' => $period->updated_at?->toIso8601String(),
|
||||
'canEditSources' => $period->isEditable(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the visible analysis columns for the active services.
|
||||
*
|
||||
* @param Collection<int, ServiceCatalog> $serviceCatalogs
|
||||
* @return array<int, array<string, string>>
|
||||
*/
|
||||
protected function columns(Collection $serviceCatalogs): array
|
||||
{
|
||||
return [
|
||||
...$serviceCatalogs->flatMap(fn (ServiceCatalog $serviceCatalog): array => [
|
||||
[
|
||||
'key' => "services.{$serviceCatalog->code}.quantity",
|
||||
'label' => "{$serviceCatalog->name} (кол-во)",
|
||||
'unit' => $serviceCatalog->unit ?? '',
|
||||
],
|
||||
[
|
||||
'key' => "services.{$serviceCatalog->code}.amount",
|
||||
'label' => "{$serviceCatalog->name} (затраты)",
|
||||
'unit' => 'руб.',
|
||||
],
|
||||
])->all(),
|
||||
['key' => 'serviceTotals.total_quantity', 'label' => 'Всего услуг', 'unit' => 'усл.'],
|
||||
['key' => 'serviceTotals.total_amount', 'label' => 'Затраты по услугам', 'unit' => 'руб.'],
|
||||
['key' => 'expense.total_without_dressing', 'label' => 'Без перевязки', 'unit' => 'руб.'],
|
||||
['key' => 'expense.total_dressing', 'label' => 'Перевязка / ИМН', 'unit' => 'руб.'],
|
||||
['key' => 'expense.total_expense', 'label' => 'Общий расход', 'unit' => 'руб.'],
|
||||
['key' => 'expense.without_budget_total', 'label' => 'Без бюджета', 'unit' => 'руб.'],
|
||||
];
|
||||
}
|
||||
}
|
||||
142
app/Services/Reports/MedicationExpenseService.php
Normal file
142
app/Services/Reports/MedicationExpenseService.php
Normal file
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Reports;
|
||||
|
||||
use App\Enums\ExpenseCategory;
|
||||
use App\Enums\FundingSource;
|
||||
use App\Models\Department;
|
||||
use App\Models\MedicationExpenseRow;
|
||||
use App\Models\ReportPeriod;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class MedicationExpenseService
|
||||
{
|
||||
/**
|
||||
* Get all funding and category values for the given row.
|
||||
*
|
||||
* @return array<string, array<string, float>>
|
||||
*/
|
||||
public function valueMatrixForRow(?MedicationExpenseRow $row): array
|
||||
{
|
||||
$matrix = [];
|
||||
|
||||
foreach (FundingSource::cases() as $fundingSource) {
|
||||
foreach (ExpenseCategory::cases() as $expenseCategory) {
|
||||
$matrix[$fundingSource->value][$expenseCategory->value] = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
if ($row === null) {
|
||||
return $matrix;
|
||||
}
|
||||
|
||||
$row->loadMissing('values');
|
||||
|
||||
foreach ($row->values as $value) {
|
||||
$matrix[$value->funding_source->value][$value->expense_category->value] = (float) $value->amount;
|
||||
}
|
||||
|
||||
return $matrix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate medication expense totals for the given matrix.
|
||||
*
|
||||
* @param array<string, array<string, float|int|string|null>> $matrix
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function calculateFromMatrix(array $matrix): array
|
||||
{
|
||||
$byFundingSource = [];
|
||||
$totalWithoutDressing = 0.0;
|
||||
$totalDressing = 0.0;
|
||||
|
||||
foreach (FundingSource::cases() as $fundingSource) {
|
||||
$withoutDressing = 0.0;
|
||||
$dressing = 0.0;
|
||||
|
||||
foreach (ExpenseCategory::cases() as $expenseCategory) {
|
||||
$amount = (float) ($matrix[$fundingSource->value][$expenseCategory->value] ?? 0);
|
||||
|
||||
if ($expenseCategory->countsTowardsWithoutDressing()) {
|
||||
$withoutDressing += $amount;
|
||||
} else {
|
||||
$dressing += $amount;
|
||||
}
|
||||
}
|
||||
|
||||
$byFundingSource[$fundingSource->value] = [
|
||||
'label' => $fundingSource->label(),
|
||||
'without_dressing' => round($withoutDressing, 2),
|
||||
'dressing' => round($dressing, 2),
|
||||
'total' => round($withoutDressing + $dressing, 2),
|
||||
];
|
||||
|
||||
$totalWithoutDressing += $withoutDressing;
|
||||
$totalDressing += $dressing;
|
||||
}
|
||||
|
||||
$budgetWithoutDressing = $byFundingSource[FundingSource::Budget->value]['without_dressing'];
|
||||
$budgetDressing = $byFundingSource[FundingSource::Budget->value]['dressing'];
|
||||
|
||||
return [
|
||||
'byFundingSource' => $byFundingSource,
|
||||
'total_without_dressing' => round($totalWithoutDressing, 2),
|
||||
'total_dressing' => round($totalDressing, 2),
|
||||
'total_expense' => round($totalWithoutDressing + $totalDressing, 2),
|
||||
'without_budget_no_dressing' => round($totalWithoutDressing - $budgetWithoutDressing, 2),
|
||||
'without_budget_dressing' => round($totalDressing - $budgetDressing, 2),
|
||||
'without_budget_total' => round(($totalWithoutDressing - $budgetWithoutDressing) + ($totalDressing - $budgetDressing), 2),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate medication expense totals for a department in the given period.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function calculateForDepartmentPeriod(ReportPeriod $period, Department $department): array
|
||||
{
|
||||
$row = MedicationExpenseRow::query()
|
||||
->with('values')
|
||||
->whereBelongsTo($period)
|
||||
->whereBelongsTo($department)
|
||||
->first();
|
||||
|
||||
$matrix = $this->valueMatrixForRow($row);
|
||||
|
||||
return [
|
||||
'rowId' => $row?->id,
|
||||
'matrix' => $matrix,
|
||||
'totals' => $this->calculateFromMatrix($matrix),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build totals for many departments in the period.
|
||||
*
|
||||
* @param Collection<int, Department> $departments
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function calculateForDepartments(ReportPeriod $period, Collection $departments): array
|
||||
{
|
||||
$rows = MedicationExpenseRow::query()
|
||||
->with('values')
|
||||
->whereBelongsTo($period)
|
||||
->whereIn('department_id', $departments->pluck('id'))
|
||||
->get()
|
||||
->keyBy('department_id');
|
||||
|
||||
return $departments->mapWithKeys(function (Department $department) use ($rows): array {
|
||||
/** @var MedicationExpenseRow|null $row */
|
||||
$row = $rows->get($department->id);
|
||||
$matrix = $this->valueMatrixForRow($row);
|
||||
|
||||
return [$department->id => [
|
||||
'rowId' => $row?->id,
|
||||
'matrix' => $matrix,
|
||||
'totals' => $this->calculateFromMatrix($matrix),
|
||||
]];
|
||||
})->all();
|
||||
}
|
||||
}
|
||||
93
app/Services/Reports/ServiceAllocationService.php
Normal file
93
app/Services/Reports/ServiceAllocationService.php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Reports;
|
||||
|
||||
use App\Models\Department;
|
||||
use App\Models\ReportPeriod;
|
||||
use App\Models\ServiceCatalog;
|
||||
use App\Models\ServiceEntry;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class ServiceAllocationService
|
||||
{
|
||||
/**
|
||||
* Build a provider sheet for the given period and service.
|
||||
*
|
||||
* @param Collection<int, Department> $recipientDepartments
|
||||
* @return array<int, array<string, float>>
|
||||
*/
|
||||
public function entriesForProviderPeriodService(
|
||||
ReportPeriod $period,
|
||||
Department $providerDepartment,
|
||||
ServiceCatalog $serviceCatalog,
|
||||
Collection $recipientDepartments,
|
||||
): array {
|
||||
$entries = ServiceEntry::query()
|
||||
->whereBelongsTo($period)
|
||||
->whereBelongsTo($serviceCatalog)
|
||||
->where('provider_department_id', $providerDepartment->id)
|
||||
->whereIn('recipient_department_id', $recipientDepartments->pluck('id'))
|
||||
->get()
|
||||
->keyBy('recipient_department_id');
|
||||
|
||||
return $recipientDepartments->mapWithKeys(function (Department $recipientDepartment) use ($entries, $serviceCatalog): array {
|
||||
/** @var ServiceEntry|null $entry */
|
||||
$entry = $entries->get($recipientDepartment->id);
|
||||
|
||||
$quantity = (float) ($entry?->quantity ?? 0);
|
||||
$unitPrice = (float) ($entry?->unit_price ?? $serviceCatalog->default_price);
|
||||
|
||||
return [$recipientDepartment->id => [
|
||||
'quantity' => $quantity,
|
||||
'unitPrice' => $unitPrice,
|
||||
'totalAmount' => round($quantity * $unitPrice, 2),
|
||||
]];
|
||||
})->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregate incoming services by recipient department.
|
||||
*
|
||||
* @param Collection<int, Department> $departments
|
||||
* @param Collection<int, ServiceCatalog> $serviceCatalogs
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function incomingAllocationsForPeriod(
|
||||
ReportPeriod $period,
|
||||
Collection $departments,
|
||||
Collection $serviceCatalogs,
|
||||
): array {
|
||||
$entries = ServiceEntry::query()
|
||||
->with('serviceCatalog:id,code')
|
||||
->whereBelongsTo($period)
|
||||
->whereIn('recipient_department_id', $departments->pluck('id'))
|
||||
->whereIn('service_catalog_id', $serviceCatalogs->pluck('id'))
|
||||
->get()
|
||||
->groupBy('recipient_department_id');
|
||||
|
||||
return $departments->mapWithKeys(function (Department $department) use ($entries, $serviceCatalogs): array {
|
||||
/** @var Collection<int, ServiceEntry> $recipientEntries */
|
||||
$recipientEntries = $entries->get($department->id, collect());
|
||||
|
||||
$services = $serviceCatalogs->mapWithKeys(function (ServiceCatalog $serviceCatalog) use ($recipientEntries): array {
|
||||
$matchingEntries = $recipientEntries->filter(
|
||||
fn (ServiceEntry $entry) => $entry->service_catalog_id === $serviceCatalog->id
|
||||
);
|
||||
|
||||
$quantity = round((float) $matchingEntries->sum(fn (ServiceEntry $entry) => (float) $entry->quantity), 2);
|
||||
$amount = round((float) $matchingEntries->sum(fn (ServiceEntry $entry) => $entry->totalAmount()), 2);
|
||||
|
||||
return [$serviceCatalog->code => [
|
||||
'quantity' => $quantity,
|
||||
'amount' => $amount,
|
||||
]];
|
||||
})->all();
|
||||
|
||||
return [$department->id => [
|
||||
'services' => $services,
|
||||
'totalQuantity' => round((float) collect($services)->sum('quantity'), 2),
|
||||
'totalAmount' => round((float) collect($services)->sum('amount'), 2),
|
||||
]];
|
||||
})->all();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user