feat: complete administration and reporting
This commit is contained in:
109
app/Http/Controllers/ReportController.php
Normal file
109
app/Http/Controllers/ReportController.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Domain\Audit\Models\AuditLog;
|
||||
use App\Domain\Reservations\Enums\ReservationItemStatus;
|
||||
use App\Domain\Reservations\Enums\ReservationStatus;
|
||||
use App\Domain\Reservations\Models\Reservation;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class ReportController extends Controller
|
||||
{
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
[$filters, $from, $to] = $this->period($request);
|
||||
$reservations = $this->query($from, $to)->get();
|
||||
$items = $reservations->flatMap->items->where('status', '!=', ReservationItemStatus::Cancelled);
|
||||
|
||||
return Inertia::render('reports/index', [
|
||||
'filters' => $filters,
|
||||
'summary' => [
|
||||
'total' => $reservations->count(),
|
||||
'completed' => $reservations->where('status', ReservationStatus::Completed)->count(),
|
||||
'incidents' => AuditLog::query()->where('event', 'reservation.incident_reported')->whereBetween('created_at', [$from, $to])->count(),
|
||||
'overdue' => AuditLog::query()->where('event', 'reservation.overdue')->whereBetween('created_at', [$from, $to])->count(),
|
||||
'utilizationHours' => round($items->sum(function ($item): float {
|
||||
$end = $item->returned_at ?? ($item->ends_at->isPast() ? $item->ends_at : now());
|
||||
|
||||
return $item->delivered_at === null ? 0 : max(0, $item->delivered_at->diffInMinutes($end) / 60);
|
||||
}), 1),
|
||||
],
|
||||
'byStatus' => $reservations->countBy(fn ($reservation): string => $reservation->status->value)->sortDesc(),
|
||||
'byCategory' => $items->countBy(fn ($item): string => $item->asset->category_name ?? 'Sem categoria')->sortDesc()->take(10),
|
||||
'byDepartment' => $reservations->countBy(fn ($reservation): string => $reservation->requester->department ?? 'Sem departamento')->sortDesc()->take(10),
|
||||
'topAssets' => $items->countBy(fn ($item): string => "{$item->asset->asset_tag} — {$item->asset->name}")->sortDesc()->take(10),
|
||||
]);
|
||||
}
|
||||
|
||||
public function export(Request $request): StreamedResponse
|
||||
{
|
||||
[$filters, $from, $to] = $this->period($request);
|
||||
$reservations = $this->query($from, $to)->lazy(200);
|
||||
|
||||
return response()->streamDownload(function () use ($reservations): void {
|
||||
$output = fopen('php://output', 'w');
|
||||
if ($output === false) {
|
||||
return;
|
||||
}
|
||||
fwrite($output, "\xEF\xBB\xBF");
|
||||
fputcsv($output, ['Referência', 'Requisitante', 'Departamento', 'Estado', 'Início', 'Fim', 'Equipamentos'], ';', '"', '');
|
||||
foreach ($reservations as $reservation) {
|
||||
fputcsv($output, [
|
||||
$this->csvCell($reservation->reference),
|
||||
$this->csvCell($reservation->requester->name),
|
||||
$this->csvCell($reservation->requester->department),
|
||||
$this->csvCell($reservation->status->value),
|
||||
$reservation->starts_at->toIso8601String(),
|
||||
$reservation->ends_at->toIso8601String(),
|
||||
$this->csvCell($reservation->items->pluck('asset.asset_tag')->join(', ')),
|
||||
], ';', '"', '');
|
||||
}
|
||||
fclose($output);
|
||||
}, "reservas-{$filters['date_from']}-{$filters['date_to']}.csv", [
|
||||
'Content-Type' => 'text/csv; charset=UTF-8',
|
||||
]);
|
||||
}
|
||||
|
||||
/** @return array{array{date_from:string,date_to:string},CarbonImmutable,CarbonImmutable} */
|
||||
private function period(Request $request): array
|
||||
{
|
||||
$filters = $request->validate([
|
||||
'date_from' => ['nullable', 'date'],
|
||||
'date_to' => ['nullable', 'date', 'after_or_equal:date_from'],
|
||||
]);
|
||||
$from = isset($filters['date_from']) ? CarbonImmutable::parse($filters['date_from'])->startOfDay() : now()->subDays(29)->startOfDay();
|
||||
$to = isset($filters['date_to']) ? CarbonImmutable::parse($filters['date_to'])->endOfDay() : now()->endOfDay();
|
||||
if ($from->diffInDays($to) > 366) {
|
||||
throw ValidationException::withMessages(['date_to' => 'O relatório está limitado a um período máximo de 366 dias.']);
|
||||
}
|
||||
|
||||
return [[
|
||||
'date_from' => $from->toDateString(),
|
||||
'date_to' => $to->toDateString(),
|
||||
], $from, $to];
|
||||
}
|
||||
|
||||
/** @return Builder<Reservation> */
|
||||
private function query(CarbonImmutable $from, CarbonImmutable $to): Builder
|
||||
{
|
||||
return Reservation::query()
|
||||
->with(['requester:id,name,department', 'items.asset:id,name,asset_tag,category_name'])
|
||||
->where('starts_at', '<=', $to)
|
||||
->where('ends_at', '>=', $from)
|
||||
->orderBy('starts_at');
|
||||
}
|
||||
|
||||
private function csvCell(?string $value): string
|
||||
{
|
||||
$value ??= '';
|
||||
|
||||
return preg_match('/^\s*[=+\-@]/', $value) === 1 ? "'{$value}" : $value;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user