177 lines
9.6 KiB
PHP
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']);
|
|
}
|
|
}
|