This commit is contained in:
2026-11-03 11:51:44 +02:00
parent badaada043
commit e753f7a0a4
309 changed files with 14537 additions and 529 deletions
+46 -17
View File
@@ -5,7 +5,11 @@ namespace App\Http\Controllers;
use App\Enums\PurchasingRole;
use App\Http\Requests\SaveSupplierRequest;
use App\Models\Supplier;
use App\Models\User;
use App\Services\ConflictGuard;
use App\Services\CurrentActor;
use App\Services\RequisitionWorkflow;
use App\Services\SupplierParticipation;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
@@ -14,44 +18,67 @@ use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Inertia\Response;
use Symfony\Component\HttpFoundation\Response as HttpResponse;
class SupplierController extends Controller
{
private function actor(Request $request): User
{
$actor = $request->user('web');
abort_unless($actor instanceof User, 403);
return $actor;
}
private function currentActor(Request $request, PurchasingRole $role): User
{
$actor = $this->actor($request);
app(ConflictGuard::class)->lockCompany($actor);
return app(CurrentActor::class)->lock($actor, [$role]);
}
public function index(Request $request): Response
{
abort_unless($request->user()->hasFunctions([PurchasingRole::Admin, PurchasingRole::Procurement, PurchasingRole::Finance, PurchasingRole::DepartmentHead]), 403);
abort_unless($this->actor($request)->hasFunctions([PurchasingRole::Admin, PurchasingRole::Procurement, PurchasingRole::Finance, PurchasingRole::DepartmentHead]), 403);
$filters = $request->validate(['search' => 'nullable|string|max:100']);
$query = Supplier::query();
if (! empty($filters['search'])) {
$query->where(fn ($q) => $q->where('name', 'like', '%'.$filters['search'].'%')->orWhere('nuit', 'like', '%'.$filters['search'].'%'));
}
return Inertia::render('suppliers/index', ['suppliers' => $query->orderBy('name')->paginate(15)->withQueryString(), 'filters' => $filters, 'canCreate' => $request->user()->hasFunctions([PurchasingRole::Procurement]), 'canReview' => $request->user()->hasFunctions([PurchasingRole::Finance])]);
return Inertia::render('suppliers/index', ['suppliers' => $query->orderBy('name')->paginate(15)->withQueryString(), 'filters' => $filters, 'canCreate' => app(SupplierParticipation::class)->canPrepare($this->actor($request)), 'canReview' => app(SupplierParticipation::class)->canReviewRole($this->actor($request))]);
}
public function store(SaveSupplierRequest $request, RequisitionWorkflow $workflow): RedirectResponse
{
$data = $request->validated();
DB::transaction(function () use ($request, $data, $workflow) {
$supplier = Supplier::query()->create([...$data, 'tenant_id' => $request->user()->tenant_id, 'created_by' => $request->user()->id, 'last_changed_by' => $request->user()->id, 'status' => 'pending']);
$workflow->audit($request->user(), 'supplier.created', null, null, $supplier->toArray());
$actor = $this->currentActor($request, PurchasingRole::Procurement);
abort_unless(app(SupplierParticipation::class)->canPrepare($actor), 403);
$supplier = Supplier::query()->create([...$data, 'tenant_id' => $actor->tenant_id, 'created_by' => $actor->id, 'last_changed_by' => $actor->id, 'status' => 'pending']);
$workflow->audit($actor, 'supplier.created', null, null, $supplier->toArray());
});
return back();
}
public function show(Request $request, Supplier $supplier): Response
public function show(Request $request, Supplier $supplier): HttpResponse
{
Gate::authorize('view', $supplier);
$canDocuments = $request->user()->can('viewDocuments', $supplier);
$canDocuments = $this->actor($request)->can('viewDocuments', $supplier);
return Inertia::render('suppliers/show', [
Inertia::encryptHistory();
$response = Inertia::render('suppliers/show', [
'supplier' => $supplier,
'documents' => $canDocuments ? $supplier->documents()->latest('id')->get() : [],
'canEdit' => $request->user()->can('update', $supplier),
'canEdit' => $this->actor($request)->can('update', $supplier),
'canDocuments' => $canDocuments,
'canReview' => $request->user()->hasFunctions([PurchasingRole::Finance]),
]);
'canReview' => app(SupplierParticipation::class)->canReview($this->actor($request), $supplier),
])->toResponse($request);
$response->headers->set('Cache-Control', 'private, no-store, max-age=0');
return $response;
}
public function edit(Supplier $supplier): Response
@@ -66,6 +93,8 @@ class SupplierController extends Controller
Gate::authorize('update', $supplier);
$data = $request->validated();
DB::transaction(function () use ($request, $supplier, $workflow, $data) {
$actor = $this->currentActor($request, PurchasingRole::Procurement);
abort_unless(app(SupplierParticipation::class)->canPrepare($actor), 403);
$supplier = Supplier::query()->lockForUpdate()->findOrFail($supplier->id);
if ($supplier->version !== (int) $data['version']) {
throw ValidationException::withMessages(['version' => 'A ficha foi alterada por outro utilizador. Reabra a edição para carregar os dados actuais.']);
@@ -74,8 +103,8 @@ class SupplierController extends Controller
unset($data['version']);
$supplier->fill($data);
if ($supplier->isDirty()) {
$supplier->fill(['status' => 'pending', 'reviewed_by' => null, 'review_comment' => null, 'last_changed_by' => $request->user()->id, 'version' => $supplier->version + 1])->save();
$workflow->audit($request->user(), 'supplier.updated', null, $before, $supplier->toArray());
$supplier->fill(['status' => 'pending', 'reviewed_by' => null, 'review_comment' => null, 'last_changed_by' => $actor->id, 'version' => $supplier->version + 1])->save();
$workflow->audit($actor, 'supplier.updated', null, $before, $supplier->toArray());
}
});
@@ -84,12 +113,12 @@ class SupplierController extends Controller
public function review(Request $request, Supplier $supplier, RequisitionWorkflow $workflow): RedirectResponse
{
abort_unless($request->user()->hasFunctions([PurchasingRole::Finance]), 403);
abort_unless($this->actor($request)->hasFunctions([PurchasingRole::Finance]), 403);
$data = $request->validate(['status' => ['required', Rule::in(['active', 'suspended'])], 'comment' => 'required|string|min:10|max:2000', 'version' => 'required|integer|min:1']);
DB::transaction(function () use ($request, $supplier, $data, $workflow) {
$actor = $this->currentActor($request, PurchasingRole::Finance);
$supplier = Supplier::query()->lockForUpdate()->findOrFail($supplier->id);
abort_if($supplier->created_by === $request->user()->id, 403);
abort_if($supplier->last_changed_by === $request->user()->id, 403);
abort_unless(app(SupplierParticipation::class)->canReview($actor, $supplier), 403);
if ($supplier->version !== (int) $data['version']) {
throw ValidationException::withMessages(['version' => 'A ficha ou os documentos foram alterados. Reabra a ficha antes de validar.']);
}
@@ -97,8 +126,8 @@ class SupplierController extends Controller
throw ValidationException::withMessages(['status' => 'Fornecedor impedido ou com revisão vencida. Não pode ser activado.']);
}
$before = $supplier->toArray();
$supplier->update(['status' => $data['status'], 'review_comment' => $data['comment'], 'reviewed_by' => $request->user()->id, 'version' => $supplier->version + 1]);
$workflow->audit($request->user(), 'supplier.reviewed', null, $before, $supplier->toArray());
$supplier->update(['status' => $data['status'], 'review_comment' => $data['comment'], 'reviewed_by' => $actor->id, 'version' => $supplier->version + 1]);
$workflow->audit($actor, 'supplier.reviewed', null, $before, $supplier->toArray());
});
return back();