Files
compras/app/Http/Controllers/SupplierEvaluationController.php
T
2026-11-03 11:51:44 +02:00

168 lines
12 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Models\AuditLog;
use App\Models\PurchaseOrder;
use App\Models\Supplier;
use App\Models\SupplierEvaluation;
use App\Models\SupplierImprovement;
use App\Models\SupplierResponse;
use App\Models\User;
use App\Services\SupplierEvaluationWorkflow;
use App\Services\SupplierImprovementWorkflow;
use App\Services\SupplierResponseWorkflow;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Schema;
use Inertia\Inertia;
use Symfony\Component\HttpFoundation\Response;
class SupplierEvaluationController extends Controller
{
private function actor(Request $request): User
{
$actor = $request->user('web');
abort_unless($actor instanceof User, 403);
return $actor;
}
private function ready(): void
{
abort_unless(Schema::hasTable('supplier_evaluations'), 503, 'A instalação do módulo de avaliações ainda não foi concluída.');
}
/** @return array<string,mixed> */
private function payload(Request $request): array
{
return $request->validate([
'payload' => 'required|array:title,nature,scope,period_start,period_end,order_ids,criteria,conclusion,critical_occurrences,critical_evidence',
'payload.title' => 'required|string|min:3|max:200', 'payload.nature' => 'required|in:goods,services',
'payload.scope' => 'nullable|string|max:10000', 'payload.period_start' => 'nullable|date_format:Y-m-d', 'payload.period_end' => 'nullable|date_format:Y-m-d',
'payload.order_ids' => 'present|array|max:50', 'payload.order_ids.*' => 'integer|distinct|min:1',
'payload.criteria' => 'present|array|max:30', 'payload.criteria.*' => 'required|array:label,result,reason,evidence',
'payload.criteria.*.label' => 'nullable|string|max:300', 'payload.criteria.*.result' => 'required|in:pending,observed,insufficient,not_applicable',
'payload.criteria.*.reason' => 'nullable|string|max:4000', 'payload.criteria.*.evidence' => 'nullable|string|max:4000',
'payload.conclusion' => 'nullable|string|max:10000', 'payload.critical_occurrences' => 'nullable|string|max:4000', 'payload.critical_evidence' => 'nullable|string|max:4000',
'tenant_id' => 'prohibited', 'created_by' => 'prohibited', 'status' => 'prohibited', 'contributors' => 'prohibited', 'supplier_snapshot' => 'prohibited',
])['payload'];
}
public function index(Request $request, SupplierEvaluationWorkflow $workflow): Response
{
$actor = $workflow->reader($this->actor($request));
$filters = $request->validate(['supplier_id' => 'nullable|integer|min:1', 'status' => 'nullable|in:draft,review,returned,approved']);
$ready = Schema::hasTable('supplier_evaluations');
$records = $ready ? SupplierEvaluation::query()->visibleTo($actor)
->when($filters['supplier_id'] ?? null, fn ($q, $id) => $q->where('supplier_id', $id))
->when($filters['status'] ?? null, fn ($q, $status) => $q->where('status', $status))
->latest('id')->paginate(20)->withQueryString()->through(fn (SupplierEvaluation $e) => [
...$e->only(['id', 'status', 'revision', 'version', 'previous_id']), 'supplier' => $e->supplier_snapshot['name'],
'title' => $e->payload['title'], 'period_start' => $e->payload['period_start'], 'period_end' => $e->payload['period_end'],
]) : null;
return $this->render($request, 'suppliers/evaluations', ['ready' => $ready, 'canCreate' => $ready && $workflow->canPrepare($actor), 'records' => $records, 'filters' => $filters,
'suppliers' => Supplier::query()->orderBy('name')->get(['id', 'name', 'nuit']),
'orders' => PurchaseOrder::query()->visibleTo($actor)->orderByDesc('id')->get(['id', 'number', 'supplier_id'])]);
}
public function show(Request $request, int $evaluation, SupplierEvaluationWorkflow $workflow): Response
{
return $this->view($request, $evaluation, $workflow, false);
}
public function report(Request $request, int $evaluation, SupplierEvaluationWorkflow $workflow): Response
{
return $this->view($request, $evaluation, $workflow, true);
}
private function view(Request $request, int $id, SupplierEvaluationWorkflow $workflow, bool $report): Response
{
$actor = $workflow->reader($this->actor($request));
$this->ready();
$evaluation = SupplierEvaluation::query()->visibleTo($actor)->findOrFail($id);
$improvementWorkflow = app(SupplierImprovementWorkflow::class);
$measuresReady = Schema::hasTable('supplier_improvements');
$measures = $measuresReady ? SupplierImprovement::query()->where('supplier_evaluation_id', $id)->orderBy('due_on')->orderBy('id')->get() : collect();
$measureHistory = $measuresReady ? AuditLog::query()->where('action', 'like', 'supplier_improvement.%')->where('after->supplier_evaluation_id', $id)->latest('id')->paginate(10, ['id', 'action', 'user_id', 'after', 'created_at'], 'measures_history_page')->withQueryString() : null;
$responsesReady = Schema::hasTable('supplier_responses');
$responses = $responsesReady ? SupplierResponse::query()->where('supplier_evaluation_id', $id)->orderBy('id')->get() : collect();
$responseHistory = $responsesReady ? AuditLog::query()->where('action', 'like', 'supplier_response.%')->where('after->supplier_evaluation_id', $id)->latest('id')->paginate(10, ['id', 'action', 'user_id', 'after', 'created_at'], 'responses_history_page')->withQueryString() : null;
$history = AuditLog::query()->where('action', 'like', 'supplier_evaluation.%')->where('after->evaluation_id', $id)->latest('id')->paginate(10, ['id', 'action', 'user_id', 'after', 'created_at'], 'history_page')->withQueryString();
$peopleIds = collect($evaluation->contributors)->merge($history->pluck('user_id'))->merge($measures->flatMap(fn (SupplierImprovement $m) => $m->contributors))->merge($measures->pluck('reviewed_by'))->merge($measureHistory?->pluck('user_id') ?? []);
$peopleIds = $peopleIds->merge($responses->flatMap(fn (SupplierResponse $r) => $r->contributors))->merge($responses->pluck('reviewed_by'))->merge($responseHistory?->pluck('user_id') ?? []);
$people = User::query()->where('tenant_id', $actor->tenant_id)->whereIn('id', $peopleIds)->pluck('name', 'id');
$writable = $improvementWorkflow->canParticipate($actor, $evaluation);
$next = SupplierEvaluation::query()->visibleTo($actor)->where('previous_id', $id)->value('id');
return $this->render($request, 'suppliers/evaluation', ['evaluation' => $workflow->snapshot($evaluation), 'reportOnly' => $report,
'canEdit' => ! $report && $workflow->canAct($actor, $evaluation, 'edit'),
'canReview' => ! $report && $workflow->canAct($actor, $evaluation, 'review'),
'canRevise' => ! $report && $workflow->canAct($actor, $evaluation, 'revise') && $next === null,
'nextRevisionId' => $next, 'history' => $history, 'people' => $people,
'responses' => ['ready' => $responsesReady, 'canCreate' => ! $report && $responsesReady && $writable && $evaluation->status === 'approved',
'history' => $responseHistory,
'records' => $responses->map(fn (SupplierResponse $r) => [...app(SupplierResponseWorkflow::class)->snapshot($r),
'canEdit' => ! $report && app(SupplierResponseWorkflow::class)->canAct($actor, $evaluation, $r, 'edit'),
'canReview' => ! $report && app(SupplierResponseWorkflow::class)->canAct($actor, $evaluation, $r, 'review')])],
'improvements' => ['ready' => $measuresReady, 'canCreate' => ! $report && $measuresReady && $writable && $evaluation->status === 'approved',
'today' => today()->toDateString(),
'records' => $measures->map(fn (SupplierImprovement $m) => [...$improvementWorkflow->snapshot($m),
'canPlan' => ! $report && $improvementWorkflow->canAct($actor, $evaluation, $m, 'plan'),
'canExecute' => ! $report && $improvementWorkflow->canAct($actor, $evaluation, $m, 'execute'),
'canReview' => ! $report && $improvementWorkflow->canAct($actor, $evaluation, $m, 'review')]),
'history' => $measureHistory,
'assignees' => $report ? [] : User::query()->where('tenant_id', $actor->tenant_id)->orderBy('name')->get()->filter(fn (User $u) => $u->hasFunctions(SupplierEvaluationWorkflow::ROLES) && (! $u->departmentScoped() || ($u->department_id !== null && $u->department_id === $evaluation->department_id)))->map(fn (User $u) => $u->only(['id', 'name']))->values()],
'orders' => PurchaseOrder::query()->visibleTo($actor)->where('supplier_id', $evaluation->supplier_id)->orderByDesc('id')->get(['id', 'number', 'supplier_id']),
'generatedAt' => now()->toIso8601String()]);
}
/** @param array<string,mixed> $props */
private function render(Request $request, string $component, array $props): Response
{
Inertia::encryptHistory();
$response = Inertia::render($component, [...$props, 'notice' => $request->session()->get('supplier_evaluation_notice')])->toResponse($request);
$response->headers->set('Cache-Control', 'private, no-store, max-age=0');
return $response;
}
public function store(Request $request, SupplierEvaluationWorkflow $workflow): RedirectResponse
{
$this->ready();
$data = $request->validate(['supplier_id' => 'required|integer|min:1', 'operation_key' => 'required|uuid']);
$evaluation = $workflow->create($this->actor($request), (int) $data['supplier_id'], $data['operation_key'], $this->payload($request));
return to_route('supplier-evaluations.show', $evaluation->id)->with('supplier_evaluation_notice', 'Rascunho guardado. Os dados podem ser recuperados nesta avaliação.');
}
public function update(Request $request, int $evaluation, SupplierEvaluationWorkflow $workflow): RedirectResponse
{
$this->ready();
$data = $request->validate(['version' => 'required|integer|min:1']);
$workflow->save($this->actor($request), $evaluation, (int) $data['version'], $this->payload($request));
return to_route('supplier-evaluations.show', $evaluation)->with('supplier_evaluation_notice', 'Alterações guardadas no histórico.');
}
public function transition(Request $request, int $evaluation, string $action, SupplierEvaluationWorkflow $workflow): RedirectResponse
{
$this->ready();
$data = $request->validate(['version' => 'required|integer|min:1', 'reason' => $action === 'submit' ? 'nullable|string|max:4000' : 'required|string|min:10|max:4000',
'payload' => 'prohibited', 'supplier_id' => 'prohibited', 'tenant_id' => 'prohibited', 'reviewed_by' => 'prohibited']);
$workflow->transition($this->actor($request), $evaluation, (int) $data['version'], $action, $data['reason'] ?? null);
return to_route('supplier-evaluations.show', $evaluation)->with('supplier_evaluation_notice', 'Estado e decisão registados.');
}
public function revise(Request $request, int $evaluation, SupplierEvaluationWorkflow $workflow): RedirectResponse
{
$this->ready();
$data = $request->validate(['version' => 'required|integer|min:1']);
$revision = $workflow->revise($this->actor($request), $evaluation, (int) $data['version']);
return to_route('supplier-evaluations.show', $revision->id)->with('supplier_evaluation_notice', 'Nova revisão preparada. A avaliação aprovada anterior mantém-se preservada.');
}
}