Files
compras/app/Http/Controllers/SupplierImprovementReportController.php
nala 63d20b10fc
tests / ci (push) Canceled after 0s
commit de armazens
2026-11-04 09:40:26 +02:00

177 lines
9.6 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Models\SupplierEvaluation;
use App\Models\SupplierImprovement;
use App\Models\User;
use App\Services\AdministrativeGeography;
use App\Services\SupplierEvaluationWorkflow;
use App\Services\SupplierGeography;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Symfony\Component\HttpFoundation\Response;
class SupplierImprovementReportController extends Controller
{
private function actor(Request $request): User
{
$actor = $request->user('web');
abort_unless($actor instanceof User, 403);
return $actor;
}
/** @param array<string,mixed> $filters
* @return array<string,mixed>|null
*/
private function territory(array $filters): ?array
{
return empty($filters['location']) ? null : app(AdministrativeGeography::class)->describe(['country' => 'MZ', 'catalogue' => config('geography.source_sha256'), 'record_id' => $filters['location']]);
}
private function snapshot(\Closure $read): mixed
{
if (DB::getDriverName() === 'pgsql' && DB::transactionLevel() !== 0) {
throw new \LogicException('O relatório exige uma transacção própria de leitura.');
}
return DB::transaction(function () use ($read) {
if (DB::getDriverName() === 'pgsql') {
DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ, READ ONLY');
}
return $read();
});
}
/** @return array<string,mixed> */
private function filters(Request $request): array
{
$actor = app(SupplierEvaluationWorkflow::class)->reader($this->actor($request));
$request->setUserResolver(fn () => $actor);
abort_unless(Schema::hasTable('supplier_improvements'), 503, 'A instalação das medidas de melhoria ainda não está concluída.');
$filters = $request->validate(['supplier_id' => 'nullable|integer|min:1', 'responsible_id' => 'nullable|integer|min:1',
'status' => 'nullable|in:open,review,returned,closed', 'from' => 'nullable|date_format:Y-m-d', 'to' => 'nullable|date_format:Y-m-d',
'overdue' => 'nullable|in:1', 'search' => 'nullable|string|max:200', 'location' => 'nullable|string|regex:/^[0-9]+$/D|max:20']);
if (! empty($filters['from']) && ! empty($filters['to']) && $filters['to'] < $filters['from']) {
throw ValidationException::withMessages(['to' => 'O fim do intervalo deve ser igual ou posterior ao início.']);
}
return $filters;
}
/** @param array<string,mixed> $filters
* @return Builder<SupplierImprovement>
*/
private function query(Request $request, array $filters, string $today): Builder
{
$query = SupplierImprovement::query()->visibleTo($this->actor($request));
if (! empty($filters['location'])) {
$suppliers = app(SupplierGeography::class)->matching($filters['location'])->select('id');
$query->whereHas('evaluation', fn ($evaluation) => $evaluation->whereIn('supplier_id', $suppliers));
}
if (! empty($filters['supplier_id'])) {
$query->whereHas('evaluation', fn ($q) => $q->where('supplier_id', $filters['supplier_id']));
}
foreach (['responsible_id', 'status'] as $field) {
if (! empty($filters[$field])) {
$query->where($field, $filters[$field]);
}
}
foreach (['from' => '>=', 'to' => '<='] as $field => $operator) {
if (! empty($filters[$field])) {
$query->where('due_on', $operator, $filters[$field]);
}
}
if (! empty($filters['overdue'])) {
$query->where('status', '!=', 'closed')->where('due_on', '<', $today);
}
if (isset($filters['search']) && $filters['search'] !== '') {
$query->whereRaw('LOWER(title) LIKE ?', ['%'.mb_strtolower($filters['search']).'%']);
}
return $query;
}
/** @param Builder<SupplierImprovement> $query
* @return Builder<SupplierImprovement>
*/
private function records(Builder $query, Request $request): Builder
{
return $query->with(['evaluation:id,supplier_id,supplier_snapshot,revision', 'responsible' => fn ($q) => $q->where('tenant_id', $this->actor($request)->tenant_id)->select(['id', 'name'])]);
}
/** @return array<string,mixed> */
private function row(SupplierImprovement $m, string $today): array
{
return ['id' => $m->id, 'evaluation_id' => $m->supplier_evaluation_id, 'revision' => $m->evaluation?->revision,
'supplier' => $m->evaluation?->supplier_snapshot['name'] ?? 'Fornecedor não disponível',
'responsible' => $m->responsible->name ?? '#'.$m->responsible_id,
...$m->only(['title', 'description', 'due_on', 'status', 'version', 'result', 'evidence', 'review_reason', 'reviewed_at']),
'overdue' => $m->status !== 'closed' && $m->due_on < $today];
}
public function index(Request $request): Response
{
$props = $this->snapshot(function () use ($request): array {
$filters = $this->filters($request);
$today = today()->toDateString();
$all = SupplierImprovement::query()->visibleTo($this->actor($request));
$query = $this->query($request, $filters, $today);
$counts = (clone $query)->selectRaw('status, COUNT(*) AS count')->groupBy('status')->pluck('count', 'status')->map(fn ($n) => (int) $n);
$overdue = (clone $query)->where('status', '!=', 'closed')->where('due_on', '<', $today)->count();
// Choices derive only from visible measures, including prior revisions and former assignees.
$evaluations = SupplierEvaluation::query()->visibleTo($this->actor($request))->whereIn('id', (clone $all)->select('supplier_evaluation_id'))->orderByDesc('id')->get(['supplier_id', 'supplier_snapshot']);
return ['filters' => $filters, 'geographyEnabled' => app(AdministrativeGeography::class)->enabled(), 'geographyFilter' => $this->territory($filters), 'today' => $today, 'generatedAt' => now()->toIso8601String(), 'counts' => $counts, 'overdue' => $overdue,
'records' => $this->records($query, $request)->orderBy('due_on')->orderBy('id')->paginate(25)->withQueryString()->through(fn (SupplierImprovement $m) => $this->row($m, $today)),
'suppliers' => $evaluations->map(fn (SupplierEvaluation $e) => ['id' => $e->supplier_id, 'name' => $e->supplier_snapshot['name']])->unique('id')->values(),
'responsibles' => User::query()->where('tenant_id', $this->actor($request)->tenant_id)->whereIn('id', (clone $all)->select('responsible_id'))->orderBy('name')->get(['id', 'name'])];
});
Inertia::encryptHistory();
$response = Inertia::render('reports/supplier-improvements', $props)->toResponse($request);
$response->headers->set('Cache-Control', 'private, no-store, max-age=0');
return $response;
}
public function csv(Request $request): Response
{
$filters = $this->filters($request);
if (! empty($filters['location'])) {
app(SupplierGeography::class)->matching($filters['location']);
}
$text = fn (?string $value) => preg_match('/^[\s]*[=+\-@]|^[\t\r\n]/u', $value ?? '') ? "'".$value : ($value ?? '');
return response()->streamDownload(fn () => $this->snapshot(function () use ($request, $filters, $text) {
// Refresh the profile inside the export snapshot as well.
$this->filters($request);
$today = today()->toDateString();
$out = fopen('php://output', 'w');
if ($out === false) {
throw new \RuntimeException('Não foi possível gerar o ficheiro.');
}
fwrite($out, "\xEF\xBB\xBF");
fputcsv($out, ['Medidas de melhoria de fornecedores', 'Consulta', now()->toIso8601String(), 'Data de referência', $today], ';', '"', '');
$exportFilters = $filters;
if (! empty($filters['location'])) {
$territory = $this->territory($filters);
$exportFilters['localizacao_actual_da_ficha'] = implode(' / ', array_column($territory['path'] ?? [], 'name'));
}
fputcsv($out, ['Filtros', $text(json_encode($exportFilters, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR))], ';', '"', '');
fputcsv($out, ['Medida', 'Fornecedor preservado', 'Avaliação', 'Revisão', 'Título', 'Objectivo', 'Responsável', 'Prazo', 'Estado', 'Prazo ultrapassado', 'Versão', 'Resultado', 'Evidência', 'Última conferência', 'Conferida em', 'Avaliação e acompanhamento', 'Relatório individual'], ';', '"', '');
foreach ($this->records($this->query($request, $filters, $today), $request)->lazyById(200) as $m) {
$row = $this->row($m, $today);
$url = url('/fornecedores/avaliacoes/'.$m->supplier_evaluation_id);
fputcsv($out, ['MM-'.$m->id, $text($row['supplier']), 'AVF-'.$m->supplier_evaluation_id, $row['revision'], $text($m->title), $text($m->description), $text($row['responsible']), $m->due_on, $m->status, $row['overdue'] ? 'Sim' : 'Não', $m->version, $text($m->result), $text($m->evidence), $text($m->review_reason), $m->reviewed_at?->toIso8601String(), $url.'#medida-'.$m->id, $url.'/relatorio#medida-'.$m->id], ';', '"', '');
}
fclose($out);
}), 'medidas-fornecedores-'.now()->format('Ymd-His').'.csv', ['Content-Type' => 'text/csv; charset=UTF-8', 'Cache-Control' => 'private, no-store, max-age=0']);
}
}