user('web'); abort_unless($actor instanceof User, 403); $kinds = app(PeopleHiring::class)->kinds($actor); abort_unless($kinds !== [], 403); $filters = $request->validate([ 'view' => 'nullable|in:processes,applications,selections,deadlines', 'deadline_window' => 'nullable|in:all,overdue,today,future,undated', 'deadline_page' => 'nullable|integer|min:1|max:100000', 'kind' => ['nullable', Rule::in($kinds)], 'status' => ['nullable', Rule::in(PeopleWorkdesk::STATES)], 'search' => 'nullable|string|max:100', 'from' => 'nullable|date_format:Y-m-d', 'to' => ['nullable', 'date_format:Y-m-d', Rule::when($request->filled('from'), 'after_or_equal:from')], ]); $query = app(PeopleAccess::class)->visible(PeopleProcess::query()->whereIn('kind', $kinds), $actor); foreach (['kind', 'status'] as $field) { if (! empty($filters[$field])) { $query->where($field, $filters[$field]); } } if (! empty($filters['search'])) { $search = $filters['search']; $query->where(function ($q) use ($search) { $q->whereLike('title', '%'.$search.'%'); if (preg_match('/^(?:PES-)?([1-9][0-9]*)$/D', $search, $matches)) { $id = filter_var($matches[1], FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]); if ($id !== false) { $q->orWhere('id', $id); } } }); } if (! empty($filters['from'])) { $query->whereDate('created_at', '>=', $filters['from']); } if (! empty($filters['to'])) { $query->whereDate('created_at', '<=', $filters['to']); } $columns = ['id', 'title', 'kind', 'status', 'closes_on', 'created_at']; $page = $request->routeIs('people.reports.csv') ? null : (clone $query)->latest('id')->paginate(25, $columns)->withQueryString(); $applications = ($filters['view'] ?? 'processes') === 'applications'; $selections = ($filters['view'] ?? 'processes') === 'selections'; $deadlines = ($filters['view'] ?? 'processes') === 'deadlines'; $deadlineReport = $deadlines ? app(PeopleDeadlineReport::class)->build(clone $query, $actor, $filters['deadline_window'] ?? 'all') : null; if ($deadlineReport !== null && ! $request->routeIs('people.reports.csv')) { $number = (int) ($filters['deadline_page'] ?? 1); $pager = (new LengthAwarePaginator(array_slice($deadlineReport['items'], ($number - 1) * 25, 25), $deadlineReport['total_items'], 25, $number, ['path' => $request->url(), 'pageName' => 'deadline_page']))->appends($request->except('deadline_page')); $deadlineReport['items'] = $pager->items(); $deadlineReport['pagination'] = ['current_page' => $pager->currentPage(), 'last_page' => $pager->lastPage(), 'prev_page_url' => $pager->previousPageUrl(), 'next_page_url' => $pager->nextPageUrl()]; } $metrics = []; if ($applications || $selections) { $metricQuery = clone $query; if ($page !== null) { $metricQuery->whereIn('id', $page->getCollection()->modelKeys()); } foreach ($metricQuery->orderBy('id')->lazyById(100) as $process) { $metrics[$process->id] = $applications ? app(PeopleApplicationReport::class)->counts($process, $actor) : app(PeopleSelectionReport::class)->facts($process, $actor); } } if ($request->routeIs('people.reports.csv') && $deadlineReport !== null) { $response = $this->deadlineCsv($deadlineReport); } elseif ($request->routeIs('people.reports.csv')) { // Materialise within the read-only snapshot; streaming runs after the transaction. $rows = (clone $query)->orderBy('id')->get($columns); $response = response()->streamDownload(function () use ($rows, $applications, $selections, $metrics) { $file = fopen('php://output', 'w'); if ($file === false) { throw new \RuntimeException('Não foi possível abrir a exportação.'); } fwrite($file, "\xEF\xBB\xBF"); $extra = ['Registadas para avaliação', 'Avaliação completa', 'Por concluir avaliação', 'Sem avaliação', 'Avaliação em curso', 'Base da avaliação', 'Recebidas no portal', 'Admitidas para análise no portal', 'Não admitidas no portal', 'Triagem pendente']; $selectionColumns = ['Situação da selecção', 'Proposta em', 'Decisão em', 'Pendência', 'Acesso à minuta', 'Referência da minuta', 'Minuta preparada em', 'Início previsto na minuta']; fputcsv($file, ['Referência', 'Título', 'Percurso', 'Estado', 'Fecho de candidaturas', 'Criado em', ...($applications ? $extra : ($selections ? $selectionColumns : []))], ';', '"', ''); foreach ($rows as $row) { $title = preg_match('/^[\s]*[=+@-]/u', $row->title) ? "'".$row->title : $row->title; $counts = $metrics[$row->id] ?? []; $values = $applications ? array_values(array_map(fn ($value) => $value === null ? 'Sem acesso' : ($value === 'selection_snapshot' ? 'Selecção preservada' : ($value === 'current_eligibility' ? 'Elegibilidade actual' : $value)), $counts)) : []; if ($selections) { $values = array_values(array_map(fn ($value) => $value === null ? 'Não registado' : (is_string($value) && preg_match('/^[\s]*[=+@-]/u', $value) ? "'".$value : $value), $counts)); } fputcsv($file, ['PES-'.$row->id, $title, $row->kind === 'service' ? 'Serviços individuais' : 'Recrutamento e selecção', self::states()[$row->status] ?? $row->status, $row->closes_on, $row->created_at?->toDateString(), ...$values], ';', '"', ''); } fclose($file); }, 'relatorio-pessoas.csv', ['Content-Type' => 'text/csv; charset=UTF-8']); } else { $summary = (clone $query)->selectRaw('kind, status, count(*) as total')->groupBy('kind', 'status')->get(); Inertia::encryptHistory(); $response = Inertia::render('people/reports', [ 'filters' => $filters, 'kinds' => $kinds, 'summary' => $summary, 'applicationMetrics' => $applications ? $metrics : [], 'selectionMetrics' => $selections ? $metrics : [], 'deadlineReport' => $deadlineReport, 'generatedAt' => now()->toIso8601String(), 'processes' => $page, ])->toResponse($request); } $response->headers->set('Cache-Control', 'private, no-store, max-age=0'); return $response; }); } /** @param array $report */ private function deadlineCsv(array $report): Response { return response()->streamDownload(function () use ($report) { $file = fopen('php://output', 'w'); if ($file === false) { throw new \RuntimeException('Não foi possível abrir a exportação.'); } fwrite($file, "\xEF\xBB\xBF"); fputcsv($file, ['Referência', 'Processo', 'Categoria', 'Intervenção', 'Responsabilidade', 'Prazo', 'Dias de atraso', 'Consulta em', 'Filtro de prazo'], ';', '"', ''); $windows = ['all' => 'Todos', 'overdue' => 'Vencidos', 'today' => 'Hoje', 'future' => 'Futuros', 'undated' => 'Sem prazo definido']; foreach ($report['items'] as $item) { $values = [$item['reference'], $item['title'], $item['category'], $item['label'], $item['responsibility'], $item['due_on'] ?? 'Sem prazo definido', $item['days_overdue'] ?? 'Não determinável', $report['as_of'], $windows[$report['window']]]; $values = array_map(fn ($value) => is_string($value) && preg_match('/^[\s]*[=+@-]/u', $value) ? "'".$value : $value, $values); fputcsv($file, $values, ';', '"', ''); } fclose($file); }, 'relatorio-prazos-pessoas.csv', ['Content-Type' => 'text/csv; charset=UTF-8']); } /** @return array */ private static function states(): array { return ['draft' => 'Abertura por conferir', 'open' => 'Recepção de candidaturas', 'assessment' => 'Avaliação', 'selection_review' => 'Selecção por conferir', 'selected' => 'Selecção aprovada', 'contract_draft' => 'Minuta contratual preparada']; } }