commit13
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\PeopleProcess;
|
||||
use App\Models\User;
|
||||
use App\Services\PeopleAccess;
|
||||
use App\Services\PeopleApplicationReport;
|
||||
use App\Services\PeopleDeadlineReport;
|
||||
use App\Services\PeopleHiring;
|
||||
use App\Services\PeopleSelectionReport;
|
||||
use App\Services\PeopleWorkdesk;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Inertia\Inertia;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class PeopleReportController extends Controller
|
||||
{
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
return DB::transaction(function () use ($request) {
|
||||
if (DB::getDriverName() === 'pgsql' && DB::transactionLevel() === 1) {
|
||||
DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');
|
||||
}
|
||||
$actor = $request->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<string,mixed> $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<string,string> */
|
||||
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'];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user