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 */ 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; } }