first commit
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (8.3) (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
tests / ci (8.5) (push) Has been cancelled

This commit is contained in:
brusnitsyn
2026-04-06 00:06:00 +09:00
commit fb2e6c58e3
409 changed files with 42953 additions and 0 deletions

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Http\Controllers\Reports;
use App\Http\Controllers\Controller;
use App\Models\ReportPeriod;
use App\Models\Team;
use App\Services\Reports\AnalysisReportService;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class AnalysisController extends Controller
{
/**
* Display the analysis page.
*/
public function index(Request $request, Team $current_team, AnalysisReportService $analysisReportService): Response
{
$periods = ReportPeriod::query()
->whereBelongsTo($current_team)
->orderByDesc('year')
->orderByDesc('month')
->get();
/** @var ReportPeriod|null $selectedPeriod */
$selectedPeriod = $periods->firstWhere('id', $request->integer('period', $periods->first()?->id));
$analysis = $selectedPeriod
? $analysisReportService->build($current_team, $selectedPeriod->year, $selectedPeriod->month)
: [
'period' => null,
'columns' => [],
'rows' => [],
'meta' => [
'status' => null,
'statusLabel' => null,
'updatedAt' => null,
'canEditSources' => false,
],
];
return Inertia::render('reports/analysis/Index', [
'periods' => $periods->map(fn (ReportPeriod $period) => [
'id' => $period->id,
'label' => $period->label(),
'status' => $period->status->value,
'statusLabel' => $period->status->label(),
])->values(),
'selectedPeriodId' => $selectedPeriod?->id,
...$analysis,
]);
}
}

View File

@@ -0,0 +1,111 @@
<?php
namespace App\Http\Controllers\Reports;
use App\Enums\ExpenseCategory;
use App\Enums\FundingSource;
use App\Http\Controllers\Controller;
use App\Http\Requests\Reports\StoreMedicationExpenseValuesRequest;
use App\Models\Department;
use App\Models\MedicationExpenseRow;
use App\Models\MedicationExpenseValue;
use App\Models\ReportPeriod;
use App\Models\Team;
use App\Services\Reports\MedicationExpenseService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class MedicationExpenseController extends Controller
{
/**
* Display the medication expense page.
*/
public function index(Request $request, Team $current_team, MedicationExpenseService $medicationExpenseService): Response
{
$periods = ReportPeriod::query()
->whereBelongsTo($current_team)
->orderByDesc('year')
->orderByDesc('month')
->get();
$departments = Department::query()
->where('is_active', true)
->orderBy('name')
->get();
/** @var ReportPeriod|null $selectedPeriod */
$selectedPeriod = $periods->firstWhere('id', $request->integer('period', $periods->first()?->id));
/** @var Department|null $selectedDepartment */
$selectedDepartment = $departments->firstWhere('id', $request->integer('department', $departments->first()?->id));
$expense = $selectedPeriod && $selectedDepartment
? $medicationExpenseService->calculateForDepartmentPeriod($selectedPeriod, $selectedDepartment)
: ['matrix' => [], 'totals' => []];
return Inertia::render('reports/medication-expenses/Index', [
'periods' => $periods->map(fn (ReportPeriod $period) => [
'id' => $period->id,
'label' => $period->label(),
'status' => $period->status->value,
'statusLabel' => $period->status->label(),
'isEditable' => $period->isEditable(),
])->values(),
'departments' => $departments->map(fn (Department $department) => [
'id' => $department->id,
'name' => $department->name,
])->values(),
'selectedPeriodId' => $selectedPeriod?->id,
'selectedDepartmentId' => $selectedDepartment?->id,
'fundingSources' => collect(FundingSource::cases())->map(fn (FundingSource $source) => [
'value' => $source->value,
'label' => $source->label(),
])->all(),
'expenseCategories' => collect(ExpenseCategory::cases())->map(fn (ExpenseCategory $category) => [
'value' => $category->value,
'label' => $category->label(),
])->all(),
'values' => $expense['matrix'],
'totals' => $expense['totals'],
'canEdit' => $selectedPeriod?->isEditable() ?? false,
]);
}
/**
* Store medication expense values for a department and period.
*/
public function storeValues(StoreMedicationExpenseValuesRequest $request, Team $current_team): RedirectResponse
{
$period = ReportPeriod::query()
->whereBelongsTo($current_team)
->findOrFail($request->integer('report_period_id'));
abort_unless($period->isEditable(), 422);
$department = Department::query()->findOrFail($request->integer('department_id'));
$row = MedicationExpenseRow::query()->firstOrCreate([
'report_period_id' => $period->id,
'department_id' => $department->id,
]);
foreach (FundingSource::cases() as $fundingSource) {
foreach (ExpenseCategory::cases() as $expenseCategory) {
MedicationExpenseValue::updateOrCreate([
'medication_expense_row_id' => $row->id,
'funding_source' => $fundingSource,
'expense_category' => $expenseCategory,
], [
'amount' => (float) ($request->input("values.{$fundingSource->value}.{$expenseCategory->value}") ?? 0),
]);
}
}
return to_route('reports.medication-expenses.index', [
'current_team' => $current_team,
'period' => $period->id,
'department' => $department->id,
]);
}
}

View File

@@ -0,0 +1,143 @@
<?php
namespace App\Http\Controllers\Reports;
use App\Http\Controllers\Controller;
use App\Http\Requests\Reports\StoreServiceCatalogRequest;
use App\Http\Requests\Reports\StoreServiceEntriesRequest;
use App\Models\Department;
use App\Models\ReportPeriod;
use App\Models\ServiceCatalog;
use App\Models\ServiceEntry;
use App\Models\Team;
use App\Services\Reports\ServiceAllocationService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class OperationalReportController extends Controller
{
/**
* Display the operational values page.
*/
public function index(Request $request, Team $current_team, ServiceAllocationService $serviceAllocationService): Response
{
$periods = ReportPeriod::query()
->whereBelongsTo($current_team)
->orderByDesc('year')
->orderByDesc('month')
->get();
$departments = Department::query()
->where('is_active', true)
->orderBy('name')
->get();
$serviceCatalogs = ServiceCatalog::query()
->orderBy('sort_order')
->orderBy('name')
->get();
/** @var ReportPeriod|null $selectedPeriod */
$selectedPeriod = $periods->firstWhere('id', $request->integer('period', $periods->first()?->id));
/** @var Department|null $selectedProviderDepartment */
$selectedProviderDepartment = $departments->firstWhere('id', $request->integer('provider_department', $departments->first()?->id));
/** @var ServiceCatalog|null $selectedServiceCatalog */
$selectedServiceCatalog = $serviceCatalogs->firstWhere('id', $request->integer('service', $serviceCatalogs->first()?->id));
$recipientDepartments = $departments
->reject(fn (Department $department) => $department->id === $selectedProviderDepartment?->id)
->values();
return Inertia::render('reports/operations/Index', [
'periods' => $periods->map(fn (ReportPeriod $period) => [
'id' => $period->id,
'label' => $period->label(),
'status' => $period->status->value,
'statusLabel' => $period->status->label(),
'isEditable' => $period->isEditable(),
])->values(),
'departments' => $departments->map(fn (Department $department) => [
'id' => $department->id,
'name' => $department->name,
])->values(),
'serviceCatalogs' => $serviceCatalogs->map(fn (ServiceCatalog $serviceCatalog) => [
'id' => $serviceCatalog->id,
'code' => $serviceCatalog->code,
'name' => $serviceCatalog->name,
'unit' => $serviceCatalog->unit,
'defaultPrice' => (float) $serviceCatalog->default_price,
'sortOrder' => $serviceCatalog->sort_order,
'isActive' => $serviceCatalog->is_active,
])->values(),
'selectedPeriodId' => $selectedPeriod?->id,
'selectedProviderDepartmentId' => $selectedProviderDepartment?->id,
'selectedServiceCatalogId' => $selectedServiceCatalog?->id,
'recipientDepartments' => $recipientDepartments->map(fn (Department $department) => [
'id' => $department->id,
'name' => $department->name,
])->values(),
'entries' => $selectedPeriod && $selectedProviderDepartment && $selectedServiceCatalog
? $serviceAllocationService->entriesForProviderPeriodService(
$selectedPeriod,
$selectedProviderDepartment,
$selectedServiceCatalog,
$recipientDepartments,
)
: [],
'canEdit' => $selectedPeriod?->isEditable() ?? false,
]);
}
/**
* Store a new service catalog item.
*/
public function storeService(StoreServiceCatalogRequest $request, Team $current_team): RedirectResponse
{
ServiceCatalog::create([
...$request->validated(),
'sort_order' => $request->integer('sort_order'),
'is_active' => $request->boolean('is_active'),
'default_price' => (float) $request->input('default_price', 0),
]);
return to_route('reports.operations.index', $current_team);
}
/**
* Store service entries for a provider department and period.
*/
public function storeEntries(StoreServiceEntriesRequest $request, Team $current_team): RedirectResponse
{
$period = ReportPeriod::query()
->whereBelongsTo($current_team)
->findOrFail($request->integer('report_period_id'));
abort_unless($period->isEditable(), 422);
$providerDepartment = Department::query()->findOrFail($request->integer('provider_department_id'));
$serviceCatalog = ServiceCatalog::query()->findOrFail($request->integer('service_catalog_id'));
foreach ($request->array('entries') as $recipientDepartmentId => $entry) {
$quantity = (float) ($entry['quantity'] ?? 0);
$unitPrice = (float) ($entry['unit_price'] ?? $serviceCatalog->default_price);
ServiceEntry::updateOrCreate([
'report_period_id' => $period->id,
'service_catalog_id' => $serviceCatalog->id,
'provider_department_id' => $providerDepartment->id,
'recipient_department_id' => (int) $recipientDepartmentId,
], [
'quantity' => $quantity,
'unit_price' => $unitPrice,
]);
}
return to_route('reports.operations.index', [
'current_team' => $current_team,
'period' => $period->id,
'provider_department' => $providerDepartment->id,
'service' => $serviceCatalog->id,
]);
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace App\Http\Controllers\Reports;
use App\Enums\ReportPeriodStatus;
use App\Http\Controllers\Controller;
use App\Http\Requests\Reports\StoreReportPeriodRequest;
use App\Models\ReportPeriod;
use App\Models\Team;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class ReportPeriodController extends Controller
{
/**
* Display the report periods page.
*/
public function index(Request $request, Team $current_team): Response
{
$periods = ReportPeriod::query()
->whereBelongsTo($current_team)
->orderByDesc('year')
->orderByDesc('month')
->get();
return Inertia::render('reports/periods/Index', [
'periods' => $periods->map(fn (ReportPeriod $period) => [
'id' => $period->id,
'year' => $period->year,
'month' => $period->month,
'label' => $period->label(),
'status' => $period->status->value,
'statusLabel' => $period->status->label(),
'isEditable' => $period->isEditable(),
'updatedAt' => $period->updated_at?->toIso8601String(),
])->values(),
'months' => collect(range(1, 12))->map(fn (int $month) => [
'value' => $month,
'label' => (new ReportPeriod([
'month' => $month,
'year' => now()->year,
'status' => ReportPeriodStatus::Draft,
]))->label(),
])->all(),
'years' => collect(range((int) now()->year - 1, (int) now()->year + 2))->values()->all(),
]);
}
/**
* Store a newly created report period.
*/
public function store(StoreReportPeriodRequest $request, Team $current_team): RedirectResponse
{
ReportPeriod::create([
...$request->validated(),
'team_id' => $current_team->id,
'status' => ReportPeriodStatus::Draft,
]);
return to_route('reports.periods.index', $current_team);
}
/**
* Approve the given report period.
*/
public function approve(Team $current_team, ReportPeriod $report_period): RedirectResponse
{
abort_unless($report_period->team?->is($current_team), 404);
$report_period->update([
'status' => ReportPeriodStatus::Approved,
]);
return to_route('reports.periods.index', $current_team);
}
}