commit5
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-10-30 10:55:18 +02:00
parent 0235c7609e
commit 48347e2d3a
161 changed files with 11814 additions and 265 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ class BudgetController extends Controller
{
private function access(Request $request): void
{
abort_unless(in_array($request->user()->purchasing_role, [PurchasingRole::Admin, PurchasingRole::Finance]), 403);
abort_unless($request->user()->hasFunctions([PurchasingRole::Admin, PurchasingRole::Finance]), 403);
}
public function index(Request $request): Response
@@ -13,7 +13,7 @@ class BudgetReinforcementController extends Controller
{
public function store(Request $request, Budget $budget, BudgetReinforcementWorkflow $workflow): RedirectResponse
{
abort_unless(in_array($request->user()->purchasing_role, [PurchasingRole::Admin, PurchasingRole::Finance]), 403);
abort_unless($request->user()->hasFunctions([PurchasingRole::Admin, PurchasingRole::Finance]), 403);
$data = $request->validate(['reference' => 'required|string|max:100', 'amount' => ['required', 'regex:/^\d{1,12}(\.\d{1,2})?$/'], 'reason' => 'required|string|max:4000']);
$workflow->propose($request->user(), $budget, $data);
+19 -7
View File
@@ -7,6 +7,8 @@ use App\Models\CostCenter;
use App\Models\Department;
use App\Models\Tenant;
use App\Models\User;
use App\Services\OrganizationAccess;
use App\Services\OrganizationConfiguration;
use App\Services\RequisitionWorkflow;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
@@ -24,30 +26,39 @@ class CompanyController extends Controller
public function store(Request $request, RequisitionWorkflow $workflow): RedirectResponse
{
$data = $request->validate(['name' => 'required|string|max:150', 'department' => 'required|string|max:100']);
abort_if($request->user()->tenant_id !== null, 403);
$data = $request->validate(['name' => 'required|string|max:150', 'department' => 'required|string|max:100',
'second_name' => 'required|string|max:100', 'second_email' => 'required|email|max:255|unique:users,email', 'second_password' => 'required|string|min:12|confirmed',
'first_limit' => ['required', 'string', 'regex:/^\d{1,12}(\.\d{1,2})?$/D'], 'second_limit' => ['required', 'string', 'regex:/^\d{1,12}(\.\d{1,2})?$/D'], 'includes_tax' => 'required|boolean']);
DB::transaction(function () use ($request, $data, $workflow) {
$user = User::query()->lockForUpdate()->findOrFail($request->user()->id);
abort_if($user->tenant_id !== null, 403);
$tenant = Tenant::query()->create(['name' => $data['name']]);
$tenant = Tenant::query()->create(['name' => $data['name'], ...OrganizationConfiguration::rules($data)]);
$department = Department::query()->create(['tenant_id' => $tenant->id, 'name' => $data['department']]);
CostCenter::query()->create(['tenant_id' => $tenant->id, 'department_id' => $department->id, 'code' => 'GERAL', 'name' => 'Centro de custo geral']);
$user->forceFill(['tenant_id' => $tenant->id, 'department_id' => $department->id, 'purchasing_role' => PurchasingRole::Admin])->save();
$workflow->audit($user, 'company.created', null, null, ['name' => $data['name']]);
$user->forceFill(['tenant_id' => $tenant->id, 'department_id' => $department->id, 'purchasing_role' => PurchasingRole::Configured])->save();
$second = new User;
$second->forceFill(['tenant_id' => $tenant->id, 'department_id' => $department->id, 'name' => $data['second_name'], 'email' => $data['second_email'], 'password' => $data['second_password'], 'purchasing_role' => PurchasingRole::Configured])->save();
app(OrganizationConfiguration::class)->initialise($user, $second);
$workflow->audit($user, 'company.created', null, null, ['name' => $data['name'], 'initial_administrators' => [$user->id, $second->id], 'rules' => $tenant->only(['first_limit_minor', 'second_limit_minor', 'approval_includes_tax', 'rules_provisional'])]);
});
return to_route('dashboard');
}
public function index(Request $request): Response
public function index(Request $request): Response|RedirectResponse
{
$this->admin($request);
if (app(OrganizationAccess::class)->active($request->user()->tenant_id)) {
return to_route('organization.parameters');
}
return Inertia::render('company/index', [
'company' => Tenant::query()->findOrFail($request->user()->tenant_id),
'departments' => Department::query()->orderBy('name')->get(),
'costCenters' => CostCenter::query()->orderBy('code')->get(),
'users' => User::query()->where('tenant_id', $request->user()->tenant_id)->orderBy('name')->get(['id', 'name', 'email', 'department_id', 'purchasing_role']),
'roles' => collect(PurchasingRole::cases())->map(fn (PurchasingRole $role) => ['value' => $role->value, 'label' => $role->label()]),
'roles' => collect(PurchasingRole::cases())->reject(fn (PurchasingRole $role) => $role === PurchasingRole::Configured)->values()->map(fn (PurchasingRole $role) => ['value' => $role->value, 'label' => $role->label()]),
]);
}
@@ -82,6 +93,7 @@ class CompanyController extends Controller
public function user(Request $request, RequisitionWorkflow $workflow): RedirectResponse
{
$this->admin($request);
abort_if(app(OrganizationAccess::class)->active($request->user()->tenant_id), 409, 'Crie utilizadores e atribua perfis na parametrização.');
$data = $request->validate([
'name' => 'required|string|max:100', 'email' => 'required|email|max:255|unique:users',
'password' => 'required|string|min:12|confirmed',
@@ -99,6 +111,6 @@ class CompanyController extends Controller
private function admin(Request $request): void
{
abort_unless($request->user()->purchasing_role === PurchasingRole::Admin, 403);
app(OrganizationConfiguration::class)->administrator($request->user());
}
}
@@ -20,7 +20,7 @@ class DocumentaryQualificationController extends Controller
{
private function access(Request $request, int $id): Requisition
{
abort_unless(in_array($request->user()->purchasing_role, [PurchasingRole::Admin, PurchasingRole::Procurement, PurchasingRole::Finance, PurchasingRole::DepartmentHead]), 403);
abort_unless($request->user()->hasFunctions([PurchasingRole::Admin, PurchasingRole::Procurement, PurchasingRole::Finance, PurchasingRole::DepartmentHead]), 403);
return Requisition::query()->visibleTo($request->user())->findOrFail($id);
}
@@ -47,7 +47,7 @@ class DocumentaryQualificationController extends Controller
'proposals' => $proposals,
'closedForChanges' => $closed,
'records' => $records, 'history' => $query->latest('id')->paginate(20)->withQueryString(), 'actorId' => $request->user()->id,
'canDefine' => $open && $proposals !== [] && $request->user()->purchasing_role === PurchasingRole::Procurement,
'canDefine' => $open && $proposals !== [] && $request->user()->hasFunctions([PurchasingRole::Procurement]),
'canAssess' => $open && $current && $request->user()->id !== $requisition->requester_id && app(ProposalComparison::class)->eligibleRole($request->user(), $requisition),
])->toResponse($request);
$response->headers->set('Cache-Control', 'private, no-store, max-age=0');
+7 -7
View File
@@ -28,15 +28,15 @@ class EquipmentController extends Controller
{
public function index(Request $request): Response
{
abort_unless(in_array($request->user()->purchasing_role, [PurchasingRole::Admin, PurchasingRole::Warehouse, PurchasingRole::Procurement, PurchasingRole::Finance, PurchasingRole::DepartmentHead]), 403);
abort_unless($request->user()->hasFunctions([PurchasingRole::Admin, PurchasingRole::Warehouse, PurchasingRole::Procurement, PurchasingRole::Finance, PurchasingRole::DepartmentHead]), 403);
$assets = Equipment::query();
if ($request->user()->purchasing_role === PurchasingRole::DepartmentHead) {
if ($request->user()->departmentScoped()) {
$assets->where('department_id', $request->user()->department_id);
}
$equipment = $assets->orderBy('code')->get();
$links = EquipmentSparePart::query()->whereIn('equipment_id', $equipment->pluck('id'))->get();
$parts = SparePart::query();
if ($request->user()->purchasing_role === PurchasingRole::DepartmentHead) {
if ($request->user()->departmentScoped()) {
$parts->whereIn('id', $links->pluck('spare_part_id'));
}
$parts = $parts->get()->map(function (SparePart $part) {
@@ -47,7 +47,7 @@ class EquipmentController extends Controller
});
$items = StockItem::query();
$departments = Department::query();
if ($request->user()->purchasing_role === PurchasingRole::DepartmentHead) {
if ($request->user()->departmentScoped()) {
$items->whereIn('id', $parts->pluck('stock_item_id')->merge($equipment->pluck('stock_item_id')->filter()));
$departments->whereKey($request->user()->department_id);
}
@@ -57,8 +57,8 @@ class EquipmentController extends Controller
'items' => $items->orderBy('code')->get(['id', 'code', 'name', 'unit']),
'departments' => $departments->orderBy('name')->get(['id', 'name']),
'history' => StockOperation::query()->whereNotNull('equipment_id')->whereIn('equipment_id', $equipment->pluck('id'))->latest('id')->paginate(15),
'canManage' => in_array($request->user()->purchasing_role, [PurchasingRole::Admin, PurchasingRole::Warehouse, PurchasingRole::Procurement]),
'canConfigure' => $request->user()->purchasing_role === PurchasingRole::Admin,
'canManage' => $request->user()->hasFunctions([PurchasingRole::Admin, PurchasingRole::Warehouse, PurchasingRole::Procurement]),
'canConfigure' => $request->user()->hasFunctions([PurchasingRole::Admin]),
]);
}
@@ -66,7 +66,7 @@ class EquipmentController extends Controller
{
$user = $request->user();
abort_unless(in_array($kind, ['categorias', 'activos', 'pecas', 'associacoes']), 404);
abort_unless($kind === 'categorias' ? $user->purchasing_role === PurchasingRole::Admin : in_array($user->purchasing_role, [PurchasingRole::Admin, PurchasingRole::Warehouse, PurchasingRole::Procurement]), 403);
abort_unless($kind === 'categorias' ? $user->hasFunctions([PurchasingRole::Admin]) : $user->hasFunctions([PurchasingRole::Admin, PurchasingRole::Warehouse, PurchasingRole::Procurement]), 403);
$tenant = $user->tenant_id;
$exists = fn (string $table) => Rule::exists($table, 'id')->where('tenant_id', $tenant);
$request->merge(['code' => Str::upper(Str::squish($request->string('code')->toString()))]);
@@ -19,8 +19,8 @@ class ExternalPaymentController extends Controller
{
public function index(Request $request, ExternalPaymentWorkflow $workflow): Response
{
abort_unless(in_array($request->user()->purchasing_role, [PurchasingRole::Admin, PurchasingRole::Finance]), 403);
$canManage = $request->user()->purchasing_role === PurchasingRole::Finance;
abort_unless($request->user()->hasFunctions([PurchasingRole::Admin, PurchasingRole::Finance]), 403);
$canManage = $request->user()->hasFunctions([PurchasingRole::Finance]);
$payments = ExternalPayment::query()->latest('id')->paginate(15)->through(fn ($payment) => [...$payment->toArray(), 'external_reference' => $canManage ? $payment->external_reference : null, 'evidence_reference' => $canManage ? $payment->evidence_reference : null]);
if ($canManage && $payments->count() > 0) {
app(RequisitionWorkflow::class)->audit($request->user(), 'payment.viewed', null, null, ['payment_ids' => $payments->pluck('id')->all()]);
@@ -40,7 +40,7 @@ class ExternalPaymentController extends Controller
public function store(Request $request, Invoice $invoice, ExternalPaymentWorkflow $workflow): RedirectResponse
{
abort_unless($request->user()->purchasing_role === PurchasingRole::Finance, 403);
abort_unless($request->user()->hasFunctions([PurchasingRole::Finance]), 403);
$data = $request->validate(['supplier_bank_detail_id' => 'required|integer|min:1', 'amount' => ['required', 'regex:/^\d{1,12}(\.\d{1,2})?$/'], 'paid_on' => 'required|date_format:Y-m-d', 'external_reference' => 'required|string|max:150', 'evidence_reference' => 'required|string|max:1000']);
$workflow->register($request->user(), $invoice, $data);
@@ -21,14 +21,14 @@ class FormalAdmissionController extends Controller
{
private function access(Request $request, int $id): Requisition
{
abort_unless(in_array($request->user()->purchasing_role, [PurchasingRole::Admin, PurchasingRole::Procurement, PurchasingRole::Finance, PurchasingRole::DepartmentHead]), 403);
abort_unless($request->user()->hasFunctions([PurchasingRole::Admin, PurchasingRole::Procurement, PurchasingRole::Finance, PurchasingRole::DepartmentHead]), 403);
return Requisition::query()->visibleTo($request->user())->findOrFail($id);
}
public function policy(Request $request): Response
{
abort_unless($request->user()->purchasing_role === PurchasingRole::Admin, 403);
abort_unless($request->user()->hasFunctions([PurchasingRole::Admin]), 403);
Inertia::encryptHistory();
$response = Inertia::render('purchases/admission-policy', ['policies' => AdmissionPolicy::query()->latest('revision')->paginate(20)->withQueryString(), 'latest' => AdmissionPolicy::query()->latest('revision')->first()])->toResponse($request);
$response->headers->set('Cache-Control', 'private, no-store, max-age=0');
@@ -71,7 +71,7 @@ class FormalAdmissionController extends Controller
'canReview' => $open && $fresh && $workflow->status($decision, $selected) === 'pending' && $workflow->eligible($request->user(), $requisition, $selected, $decision) && ! AdmissionReview::query()->where('admission_decision_id', $decision->id)->where('reviewed_by', $request->user()->id)->exists()];
});
Inertia::encryptHistory();
$response = Inertia::render('purchases/admission', ['requisition' => $requisition->only(['id', 'number']), 'policy' => AdmissionPolicy::query()->latest('revision')->first(), 'adoptions' => AdmissionAdoption::query()->where('requisition_id', $requisition->id)->latest('revision')->get(), 'adoption' => $adoption, 'selected' => $selected, 'proposals' => $proposals, 'history' => $history, 'canAdopt' => $open && $request->user()->purchasing_role === PurchasingRole::Procurement, 'canPropose' => $open && $current && $request->user()->purchasing_role === PurchasingRole::Procurement && $request->user()->id !== $requisition->requester_id, 'closed' => $closed])->toResponse($request);
$response = Inertia::render('purchases/admission', ['requisition' => $requisition->only(['id', 'number']), 'policy' => AdmissionPolicy::query()->latest('revision')->first(), 'adoptions' => AdmissionAdoption::query()->where('requisition_id', $requisition->id)->latest('revision')->get(), 'adoption' => $adoption, 'selected' => $selected, 'proposals' => $proposals, 'history' => $history, 'canAdopt' => $open && $request->user()->hasFunctions([PurchasingRole::Procurement]), 'canPropose' => $open && $current && $request->user()->hasFunctions([PurchasingRole::Procurement]) && $request->user()->id !== $requisition->requester_id, 'closed' => $closed])->toResponse($request);
$response->headers->set('Cache-Control', 'private, no-store, max-age=0');
return $response;
@@ -23,14 +23,14 @@ class FormalRecommendationController extends Controller
{
private function access(Request $request, int $id): Requisition
{
abort_unless(in_array($request->user()->purchasing_role, [PurchasingRole::Admin, PurchasingRole::Procurement, PurchasingRole::Finance, PurchasingRole::DepartmentHead]), 403);
abort_unless($request->user()->hasFunctions([PurchasingRole::Admin, PurchasingRole::Procurement, PurchasingRole::Finance, PurchasingRole::DepartmentHead]), 403);
return Requisition::query()->visibleTo($request->user())->findOrFail($id);
}
public function policy(Request $request): Response
{
abort_unless($request->user()->purchasing_role === PurchasingRole::Admin, 403);
abort_unless($request->user()->hasFunctions([PurchasingRole::Admin]), 403);
Inertia::encryptHistory();
$response = Inertia::render('purchases/recommendation-policy', ['policies' => RecommendationPolicy::query()->latest('revision')->paginate(20)->withQueryString(), 'latest' => RecommendationPolicy::query()->latest('revision')->first()])->toResponse($request);
$response->headers->set('Cache-Control', 'private, no-store, max-age=0');
@@ -68,7 +68,7 @@ class FormalRecommendationController extends Controller
Inertia::encryptHistory();
$response = Inertia::render('purchases/recommendation', ['requisition' => $requisition->only(['id', 'number']), 'policy' => RecommendationPolicy::query()->latest('revision')->first(), 'adoption' => $adoption, 'selected' => $selected, 'adoptions' => RecommendationAdoption::query()->where('requisition_id', $requisition->id)->latest('revision')->get(), 'hasAdmission' => app(FormalAdmission::class)->adoption($requisition) !== null,
'basis' => ['hash' => $basis['hash'], 'ready' => $basis['ready'], 'top_ids' => $basis['top_ids'], 'rows' => collect($basis['payload']['rows'])->map(fn ($row) => array_intersect_key($row, array_flip(['id', 'reference', 'supplier_name', 'currency', 'total_minor', 'eligible', 'ineligibility_reasons', 'evaluation_count', 'score_minor'])))->all()], 'previousId' => $latest->id ?? 0,
'history' => $history, 'closed' => $closed, 'canAdopt' => $open && $request->user()->purchasing_role === PurchasingRole::Procurement, 'canPropose' => $open && $current && $basis['ready'] && $request->user()->purchasing_role === PurchasingRole::Procurement && $request->user()->id !== $requisition->requester_id])->toResponse($request);
'history' => $history, 'closed' => $closed, 'canAdopt' => $open && $request->user()->hasFunctions([PurchasingRole::Procurement]), 'canPropose' => $open && $current && $basis['ready'] && $request->user()->hasFunctions([PurchasingRole::Procurement]) && $request->user()->id !== $requisition->requester_id])->toResponse($request);
$response->headers->set('Cache-Control', 'private, no-store, max-age=0');
return $response;
@@ -2,7 +2,6 @@
namespace App\Http\Controllers;
use App\Enums\PurchasingRole;
use App\Http\Requests\ConfirmReceiptRequest;
use App\Models\GoodsReceipt;
use App\Models\PurchaseOrder;
@@ -26,7 +25,7 @@ class GoodsReceiptController extends Controller
{
Gate::authorize('viewAny', GoodsReceipt::class);
$orders = PurchaseOrder::query();
if ($request->user()->purchasing_role === PurchasingRole::DepartmentHead) {
if ($request->user()->departmentScoped()) {
$orders->whereIn('requisition_id', Requisition::query()->visibleTo($request->user())->select('id'));
}
@@ -15,15 +15,15 @@ class InvestmentPolicyController extends Controller
{
public function index(Request $request, InvestmentWorkflow $workflow): Response
{
abort_unless(in_array($request->user()->purchasing_role, [PurchasingRole::Admin, PurchasingRole::Finance, PurchasingRole::Procurement]), 403);
abort_unless($request->user()->hasFunctions([PurchasingRole::Admin, PurchasingRole::Finance, PurchasingRole::Procurement]), 403);
$current = $workflow->current();
return Inertia::render('investment/index', ['policies' => InvestmentPolicy::query()->latest('revision')->get(), 'reviews' => InvestmentPolicyReview::query()->orderBy('id')->get(), 'currentPolicyId' => $current && $current->valid_until >= today()->format('Y-m-d') ? $current->id : null, 'canManage' => $request->user()->purchasing_role === PurchasingRole::Admin]);
return Inertia::render('investment/index', ['policies' => InvestmentPolicy::query()->latest('revision')->get(), 'reviews' => InvestmentPolicyReview::query()->orderBy('id')->get(), 'currentPolicyId' => $current && $current->valid_until >= today()->format('Y-m-d') ? $current->id : null, 'canManage' => $request->user()->hasFunctions([PurchasingRole::Admin])]);
}
public function store(Request $request, InvestmentWorkflow $workflow): RedirectResponse
{
abort_unless($request->user()->purchasing_role === PurchasingRole::Admin, 403);
abort_unless($request->user()->hasFunctions([PurchasingRole::Admin]), 403);
$data = $request->validate(['previous_id' => 'required|integer|min:0', 'mode' => 'required|in:ceiling', 'scope' => 'required|in:process_total', 'currency' => 'required|in:MZN', 'ceiling' => ['required', 'regex:/^\d{1,12}(\.\d{1,2})?$/'], 'includes_tax' => 'required|boolean', 'valid_from' => 'required|date_format:Y-m-d', 'valid_until' => 'required|date_format:Y-m-d|after_or_equal:valid_from', 'reason' => 'required|string|max:4000']);
$workflow->propose($request->user(), $data);
+3 -3
View File
@@ -16,14 +16,14 @@ class InvoiceController extends Controller
{
public function index(Request $request): Response
{
abort_unless(in_array($request->user()->purchasing_role, [PurchasingRole::Admin, PurchasingRole::Finance]), 403);
abort_unless($request->user()->hasFunctions([PurchasingRole::Admin, PurchasingRole::Finance]), 403);
return Inertia::render('invoices/index', ['invoices' => Invoice::query()->with('items')->latest('id')->paginate(15), 'orders' => PurchaseOrder::query()->with('items')->latest('id')->get()->map(fn ($order) => ['id' => $order->id, 'number' => $order->number, 'supplier' => $order->snapshot['supplier']['name'], 'items' => $order->items]), 'payablesAvailable' => SupplierPayables::enabled() && app(SupplierPayables::class)->can($request->user(), 'view'), 'canManage' => $request->user()->purchasing_role === PurchasingRole::Finance]);
return Inertia::render('invoices/index', ['invoices' => Invoice::query()->with('items')->latest('id')->paginate(15), 'orders' => PurchaseOrder::query()->with('items')->latest('id')->get()->map(fn ($order) => ['id' => $order->id, 'number' => $order->number, 'supplier' => $order->snapshot['supplier']['name'], 'items' => $order->items]), 'payablesAvailable' => SupplierPayables::enabled() && app(SupplierPayables::class)->can($request->user(), 'view'), 'canManage' => $request->user()->hasFunctions([PurchasingRole::Finance])]);
}
public function store(Request $request, PurchaseOrder $order, InvoiceWorkflow $workflow): RedirectResponse
{
abort_unless($request->user()->purchasing_role === PurchasingRole::Finance, 403);
abort_unless($request->user()->hasFunctions([PurchasingRole::Finance]), 403);
$decimal = ['required', 'regex:/^\d{1,12}(\.\d{1,2})?$/'];
$data = $request->validate(['number' => 'required|string|max:100', 'issued_on' => 'required|date_format:Y-m-d', 'subtotal' => $decimal, 'tax' => $decimal, 'total' => $decimal, 'items' => 'required|array|min:1|max:50', 'items.*.purchase_order_item_id' => 'required|integer|distinct', 'items.*.quantity' => ['required', 'regex:/^\d{1,9}(\.\d{1,3})?$/'], 'items.*.unit' => 'required|string|max:30', 'items.*.unit_price' => $decimal, 'items.*.tax_rate' => ['required', 'regex:/^\d{1,3}(\.\d{1,2})?$/']]);
$workflow->register($request->user(), $order, $data);
@@ -0,0 +1,72 @@
<?php
namespace App\Http\Controllers;
use App\Models\Tenant;
use App\Services\OrganizationAccess;
use App\Services\OrganizationConfiguration;
use Brick\Math\BigDecimal;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Validation\Rule;
use Inertia\Inertia;
use Symfony\Component\HttpFoundation\Response;
class OrganizationConfigurationController extends Controller
{
public function index(Request $request, OrganizationConfiguration $service): Response
{
$actor = $service->administrator($request->user());
$ready = Schema::hasTable('organization_access');
$state = $ready ? DB::table('organization_access')->where('tenant_id', $actor->tenant_id)->first() : null;
Inertia::encryptHistory();
$response = Inertia::render('company/parameters', ['ready' => $ready, 'active' => (bool) ($state->active ?? false), 'version' => $state->version ?? 1,
'companyRules' => $this->companyRules((int) $actor->tenant_id),
'permissions' => OrganizationAccess::PERMISSIONS,
'profiles' => $ready ? DB::table('organization_profiles')->where('tenant_id', $actor->tenant_id)->orderBy('name')->get()->map(fn ($p) => ['id' => $p->id, 'name' => $p->name, 'permissions' => json_decode($p->permissions, true, 32, JSON_THROW_ON_ERROR)]) : [],
'memberships' => $ready ? DB::table('organization_memberships')->where('tenant_id', $actor->tenant_id)->get(['user_id', 'profile_id', 'maintenance', 'expires_at', 'reason']) : [],
'users' => DB::table('users')->where('tenant_id', $actor->tenant_id)->orderBy('name')->get(['id', 'name', 'email', 'department_id']),
'departments' => DB::table('departments')->where('tenant_id', $actor->tenant_id)->orderBy('name')->get(['id', 'name']),
'costCenters' => DB::table('cost_centers')->where('tenant_id', $actor->tenant_id)->orderBy('code')->get(['id', 'code', 'name']),
'notice' => $request->session()->get('organization_notice')])->toResponse($request);
$response->headers->set('Cache-Control', 'private, no-store, max-age=0');
return $response;
}
public function command(Request $request, string $command, OrganizationConfiguration $service): Response
{
$actor = $service->administrator($request->user());
abort_unless(Schema::hasTable('organization_access'), 503, 'A instalação da parametrização ainda não está concluída.');
$rules = ['version' => 'required|integer|min:1', 'tenant_id' => 'prohibited', 'purchasing_role' => 'prohibited'];
if ($command === 'profile') {
$rules += ['id' => 'nullable|integer|min:1', 'name' => ['required', 'string', 'max:120', Rule::unique('organization_profiles')->where('tenant_id', $actor->tenant_id)->ignore($request->integer('id'))],
'permissions' => 'present|array', 'permissions.*' => ['string', 'distinct', Rule::in(array_keys(OrganizationAccess::PERMISSIONS))]];
} elseif ($command === 'user') {
$rules += ['name' => 'required|string|max:100', 'email' => 'required|email|max:255|unique:users', 'password' => 'required|string|min:12|confirmed',
'department_id' => 'required|integer|min:1', 'profile_ids' => 'present|array', 'profile_ids.*' => 'integer|distinct|min:1', 'reason' => 'required|string|min:10|max:4000'];
} elseif ($command === 'assign') {
$rules += ['user_id' => 'required|integer|min:1', 'profile_ids' => 'present|array', 'profile_ids.*' => 'integer|distinct|min:1', 'maintenance' => 'required|boolean',
'expires_at' => 'nullable|date|after:now', 'reason' => 'required|string|min:10|max:4000'];
} elseif ($command === 'rules') {
$rules += ['first_limit' => ['required', 'string', 'regex:/^\d{1,12}(\.\d{1,2})?$/D'], 'second_limit' => ['required', 'string', 'regex:/^\d{1,12}(\.\d{1,2})?$/D'], 'includes_tax' => 'required|boolean', 'reason' => 'required|string|min:10|max:4000'];
} else {
abort_unless($command === 'activate', 404);
$rules += ['reason' => 'required|string|min:10|max:4000', 'confirm' => 'required|accepted'];
}
$service->execute($actor, $command, $request->validate($rules));
return to_route('organization.parameters')->with('organization_notice', 'Parametrização actualizada e registada na auditoria.');
}
/** @return array<string,mixed> */
private function companyRules(int $tenant): array
{
$company = Tenant::query()->findOrFail($tenant);
return ['first_limit' => (string) BigDecimal::of((string) $company->first_limit_minor)->dividedBy(100, 2),
'second_limit' => (string) BigDecimal::of((string) $company->second_limit_minor)->dividedBy(100, 2),
'includes_tax' => $company->approval_includes_tax, 'provisional' => $company->rules_provisional];
}
}
@@ -0,0 +1,146 @@
<?php
namespace App\Http\Controllers;
use App\Enums\PurchasingRole;
use App\Models\Department;
use App\Models\PurchaseOrder;
use App\Models\Quotation;
use App\Models\Requisition;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Symfony\Component\HttpFoundation\Response;
class ProcurementReportController extends Controller
{
private function snapshot(\Closure $read): mixed
{
if (DB::connection()->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::connection()->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 = User::query()->findOrFail($request->user()->id);
abort_unless($actor->tenant_id === $request->user()->tenant_id && $actor->hasFunctions([PurchasingRole::Admin, PurchasingRole::Procurement, PurchasingRole::Finance, PurchasingRole::DepartmentHead]), 403);
$request->setUserResolver(fn () => $actor);
$filters = $request->validate([
'search' => 'nullable|string|max:100',
'status' => 'nullable|in:draft,pending,approved,rejected,cancelled',
'from' => 'nullable|date_format:Y-m-d',
'to' => 'nullable|date_format:Y-m-d',
'department_id' => 'nullable|integer|min:1',
]);
if (! empty($filters['from']) && ! empty($filters['to']) && $filters['from'] > $filters['to']) {
throw ValidationException::withMessages(['to' => 'A data final deve ser igual ou posterior à data inicial.']);
}
return $filters;
}
/** @param array<string,mixed> $filters
* @return Builder<Requisition>
*/
private function query(User $actor, array $filters): Builder
{
$query = Requisition::query()->where('tenant_id', $actor->tenant_id)->visibleTo($actor);
if (isset($filters['search']) && $filters['search'] !== '') {
$query->where('number', 'like', '%'.$filters['search'].'%');
}
foreach (['status', 'department_id'] as $field) {
if (! empty($filters[$field])) {
$query->where($field, $filters[$field]);
}
}
if (! empty($filters['from'])) {
$query->where('created_at', '>=', $filters['from'].' 00:00:00');
}
if (! empty($filters['to'])) {
$query->where('created_at', '<', (new \DateTimeImmutable($filters['to']))->modify('+1 day')->format('Y-m-d').' 00:00:00');
}
return $query;
}
/** @param Builder<Requisition> $query
* @return Builder<Requisition>
*/
private function records(Builder $query): Builder
{
return $query->select('requisitions.*')->with(['department:id,name', 'requester:id,name'])->addSelect([
'quotations_count' => Quotation::query()->selectRaw('COUNT(*)')->whereColumn('requisition_id', 'requisitions.id'),
'order_number' => PurchaseOrder::query()->visibleTo(request()->user())->select('number')->whereColumn('requisition_id', 'requisitions.id')->orderBy('id')->limit(1),
]);
}
/** @return array<string,mixed> */
private function row(Requisition $record): array
{
return [
'id' => $record->id, 'number' => $record->number, 'status' => $record->status,
'created_at' => $record->created_at?->toIso8601String(),
'department' => $record->department?->name, 'requester' => $record->requester?->name,
'total_minor' => (string) $record->total_minor,
'quotations_count' => (int) $record->getAttribute('quotations_count'),
'order_number' => $record->getAttribute('order_number'),
];
}
public function index(Request $request): Response
{
$props = $this->snapshot(function () use ($request): array {
$filters = $this->filters($request);
$query = $this->query($request->user(), $filters);
$counts = (clone $query)->selectRaw('status, COUNT(*) AS count')->groupBy('status')->pluck('count', 'status')->map(fn ($value) => (int) $value);
$records = $this->records(clone $query)->orderByDesc('id')->paginate(25)->withQueryString()->through(fn (Requisition $record) => $this->row($record));
$departments = Department::query()->when($request->user()->departmentScoped(), fn ($q) => $q->whereKey($request->user()->department_id))->orderBy('name')->get(['id', 'name']);
return ['records' => $records, 'counts' => $counts, 'filters' => $filters, 'departments' => $departments, 'generatedAt' => now()->toIso8601String()];
});
Inertia::encryptHistory();
$response = Inertia::render('reports/procurement', $props)->toResponse($request);
$response->headers->set('Cache-Control', 'private, no-store, max-age=0');
return $response;
}
public function export(Request $request): Response
{
$filters = $this->filters($request);
$query = $this->records($this->query($request->user(), $filters));
$text = fn (?string $value) => preg_match('/^[\s]*[=+\-@]|^[\t\r\n]/u', $value ?? '') ? "'".$value : ($value ?? '');
return response()->streamDownload(fn () => $this->snapshot(function () use ($query, $text, $filters) {
$output = fopen('php://output', 'w');
if ($output === false) {
throw new \RuntimeException('Não foi possível gerar o ficheiro.');
}
fwrite($output, "\xEF\xBB\xBF");
fputcsv($output, ['Relatório de compras', 'Criado em', now()->toIso8601String()], ';', '"', '');
fputcsv($output, ['Filtros', $text(json_encode($filters, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR))], ';', '"', '');
fputcsv($output, ['Requisição', 'Criada em', 'Departamento', 'Solicitante', 'Estado da requisição', 'Estimativa total (centavos MZN)', 'Propostas registadas', 'Ordem de compra', 'Processo', 'Relatório de conferência'], ';', '"', '');
foreach ($query->lazyById(200) as $record) {
$row = $this->row($record);
$base = url('/requisicoes/'.$record->id);
fputcsv($output, [$text($record->number), $row['created_at'], $text($row['department']), $text($row['requester']), $record->status, $row['total_minor'], $row['quotations_count'], $text($row['order_number']), $base.'/processo', $base.'/relatorio'], ';', '"', '');
}
fclose($output);
}), 'relatorio-compras-'.now()->format('Ymd-His').'.csv', ['Content-Type' => 'text/csv; charset=UTF-8', 'Cache-Control' => 'private, no-store, max-age=0']);
}
}
@@ -19,7 +19,7 @@ class ProposalOpeningController extends Controller
{
private function access(Request $request, int $requisitionId): Requisition
{
abort_unless(in_array($request->user()->purchasing_role, [PurchasingRole::Admin, PurchasingRole::Procurement, PurchasingRole::Finance, PurchasingRole::DepartmentHead]), 403);
abort_unless($request->user()->hasFunctions([PurchasingRole::Admin, PurchasingRole::Procurement, PurchasingRole::Finance, PurchasingRole::DepartmentHead]), 403);
return Requisition::query()->visibleTo($request->user())->findOrFail($requisitionId);
}
@@ -36,7 +36,7 @@ class ProposalOpeningController extends Controller
'requisition' => $requisition->only(['id', 'number', 'justification']), 'proposals' => $proposals, 'basisHash' => $workflow->basis($proposals), 'openings' => $openings,
'sourceChanged' => $latest && $workflow->basis($proposals) !== $latest->basis_hash,
'latestId' => $latest->id ?? 0,
'canRecord' => $request->user()->purchasing_role === PurchasingRole::Procurement && $requisition->status === 'approved' && ! PurchaseAward::query()->where('requisition_id', $requisition->id)->exists() && ! app(ConflictGuard::class)->participationBlocked($request->user(), $requisition),
'canRecord' => $request->user()->hasFunctions([PurchasingRole::Procurement]) && $requisition->status === 'approved' && ! PurchaseAward::query()->where('requisition_id', $requisition->id)->exists() && ! app(ConflictGuard::class)->participationBlocked($request->user(), $requisition),
])->toResponse($request);
$response->headers->set('Cache-Control', 'private, no-store, max-age=0');
@@ -10,6 +10,7 @@ use App\Models\PurchaseControlReview;
use App\Models\PurchaseOrder;
use App\Models\PurchaseRevision;
use App\Models\Requisition;
use App\Services\OrganizationAccess;
use App\Services\PurchaseControlPolicyWorkflow;
use App\Services\PurchaseRevisionWorkflow;
use Illuminate\Http\RedirectResponse;
@@ -22,13 +23,13 @@ class PurchaseControlController extends Controller
public function index(Request $request): Response
{
$user = $request->user();
$operational = in_array($user->purchasing_role, [PurchasingRole::Admin, PurchasingRole::Procurement, PurchasingRole::Finance]);
$operational = $user->hasFunctions([PurchasingRole::Admin, PurchasingRole::Procurement, PurchasingRole::Finance]);
$sources = Requisition::query()->where('requester_id', $user->id)->select('id');
$ownAwards = PurchaseAward::query()->whereIn('requisition_id', $sources)->select('id');
$cancellations = AwardCancellation::query()->whereIn('purchase_award_id', $ownAwards)->where('status', 'approved')->get(['id', 'purchase_award_id']);
return Inertia::render('purchase-controls/index', [
'role' => $user->purchasing_role->value, 'actorId' => $user->id,
'role' => $user->purchasing_role->value ?? '', 'permissions' => app(OrganizationAccess::class)->permissions($user), 'actorId' => $user->id,
'policies' => $operational ? PurchaseControlPolicy::query()->latest('revision')->get() : [],
'reviews' => $operational ? PurchaseControlReview::query()->get() : [],
'awards' => $operational ? PurchaseAward::query()->whereNotIn('requisition_id', PurchaseOrder::query()->select('requisition_id'))->latest('id')->get(['id', 'requisition_id', 'status']) : [],
@@ -44,7 +45,7 @@ class PurchaseControlController extends Controller
public function propose(Request $request, PurchaseControlPolicyWorkflow $workflow): RedirectResponse
{
abort_unless($request->user()->purchasing_role === PurchasingRole::Admin, 403);
abort_unless($request->user()->hasFunctions([PurchasingRole::Admin]), 403);
$data = $request->validate(['previous_id' => 'required|integer|min:0', 'mode' => 'required|in:unissued_award', 'enabled' => 'required|boolean', 'valid_from' => 'required|date_format:Y-m-d', 'valid_until' => 'required|date_format:Y-m-d|after_or_equal:valid_from', 'reason' => 'required|string|min:10|max:4000']);
$workflow->propose($request->user(), $data);
+3 -3
View File
@@ -24,7 +24,7 @@ class PurchaseController extends Controller
{
private function access(Request $request): void
{
abort_unless(in_array($request->user()->purchasing_role, [PurchasingRole::Admin, PurchasingRole::Procurement, PurchasingRole::Finance, PurchasingRole::DepartmentHead]), 403);
abort_unless($request->user()->hasFunctions([PurchasingRole::Admin, PurchasingRole::Procurement, PurchasingRole::Finance, PurchasingRole::DepartmentHead]), 403);
}
public function award(Request $request, Requisition $requisition, ProposalComparison $comparison, PurchaseWorkflow $workflow): Response
@@ -50,10 +50,10 @@ class PurchaseController extends Controller
return Inertia::render('purchases/award', [
'requisition' => $requisition, 'comparison' => $comparison->inspect($requisition), 'award' => $award,
'canSelect' => $request->user()->purchasing_role === PurchasingRole::Procurement && $award === null && $request->user()->id !== $requisition->requester_id && (! $adoption || $candidate !== null),
'canSelect' => $request->user()->hasFunctions([PurchasingRole::Procurement]) && $award === null && $request->user()->id !== $requisition->requester_id && (! $adoption || $candidate !== null),
'recommendation' => ['adopted' => $adoption !== null, 'quotation_id' => $candidate],
'canDecide' => ! $frozen && $award !== null && $workflow->canDecide($request->user(), $award, $requisition),
'canIssue' => ! $frozen && $award?->status === 'approved' && $request->user()->purchasing_role === PurchasingRole::Procurement,
'canIssue' => ! $frozen && $award?->status === 'approved' && $request->user()->hasFunctions([PurchasingRole::Procurement]),
'order' => PurchaseOrder::query()->where('requisition_id', $requisition->id)->first(['id', 'number']),
]);
}
@@ -21,7 +21,7 @@ class PurchasePlanningController extends Controller
public function index(Request $request): Response
{
$actor = $request->user();
$canManage = in_array($actor->purchasing_role, [PurchasingRole::Admin, PurchasingRole::Procurement]);
$canManage = $actor->hasFunctions([PurchasingRole::Admin, PurchasingRole::Procurement]);
$filters = $request->validate(['item' => 'nullable|integer|min:1']);
$items = PurchasePlanItem::query()->visibleTo($actor)->when($filters['item'] ?? null, fn ($query, $id) => $query->whereKey($id))->latest('id')->paginate(15)->withQueryString();
$milestones = PurchasePlanMilestone::query()->whereIn('purchase_plan_item_id', $items->pluck('id'))->orderBy('planned_on')->orderBy('id')->get();
@@ -30,10 +30,9 @@ class PurchaseProcessController extends Controller
$actor = $request->user();
Requisition::query()->visibleTo($actor)->findOrFail($requisition->id);
$requisition->load(['requester:id,name', 'department:id,name', 'steps']);
$role = $actor->purchasing_role;
$purchases = in_array($role, [PurchasingRole::Admin, PurchasingRole::Procurement, PurchasingRole::Finance, PurchasingRole::DepartmentHead]);
$finance = in_array($role, [PurchasingRole::Admin, PurchasingRole::Finance]);
$inventory = $purchases || $role === PurchasingRole::Warehouse;
$purchases = $actor->hasFunctions([PurchasingRole::Admin, PurchasingRole::Procurement, PurchasingRole::Finance, PurchasingRole::DepartmentHead]);
$finance = $actor->hasFunctions([PurchasingRole::Admin, PurchasingRole::Finance]);
$inventory = $purchases || $actor->hasFunctions([PurchasingRole::Warehouse]);
$base = '/requisicoes/'.$requisition->id;
$stages = [];
$documents = [['label' => $requisition->number, 'kind' => 'Requisição', 'href' => $base]];
+5 -5
View File
@@ -24,7 +24,7 @@ class QuotationController extends Controller
{
private function access(Request $request): void
{
abort_unless(in_array($request->user()->purchasing_role, [PurchasingRole::Admin, PurchasingRole::Procurement, PurchasingRole::Finance, PurchasingRole::DepartmentHead]), 403);
abort_unless($request->user()->hasFunctions([PurchasingRole::Admin, PurchasingRole::Procurement, PurchasingRole::Finance, PurchasingRole::DepartmentHead]), 403);
}
public function index(Request $request): Response
@@ -46,15 +46,15 @@ class QuotationController extends Controller
return Inertia::render('quotations/show', [
'requisition' => $requisition->load('items'), 'quotations' => $scores,
'suppliers' => Supplier::query()->where('status', 'active')->where('blocked', false)->whereDate('next_review', '>=', today())->orderBy('name')->get(['id', 'name']),
'canCreate' => ! $closed && $request->user()->purchasing_role === PurchasingRole::Procurement && $requisition->status === 'approved' && ! $guard->participationBlocked($request->user(), $requisition),
'canEvaluate' => ! $closed && $request->user()->purchasing_role !== PurchasingRole::Admin && $request->user()->id !== $requisition->requester_id && $requisition->status === 'approved' && ! $guard->participationBlocked($request->user(), $requisition),
'canCreate' => ! $closed && $request->user()->hasFunctions([PurchasingRole::Procurement]) && $requisition->status === 'approved' && ! $guard->participationBlocked($request->user(), $requisition),
'canEvaluate' => ! $closed && $request->user()->hasFunctions([PurchasingRole::Procurement, PurchasingRole::Finance, PurchasingRole::DepartmentHead]) && $request->user()->id !== $requisition->requester_id && $requisition->status === 'approved' && ! $guard->participationBlocked($request->user(), $requisition),
'ready' => $result['ready'],
]);
}
public function store(Request $request, Requisition $requisition, RequisitionWorkflow $workflow): RedirectResponse
{
abort_unless($request->user()->purchasing_role === PurchasingRole::Procurement, 403);
abort_unless($request->user()->hasFunctions([PurchasingRole::Procurement]), 403);
$data = $request->validate([
'supplier_id' => 'required|integer', 'reference' => 'required|string|max:100',
'delivery_days' => 'required|integer|min:1|max:3650', 'valid_until' => 'required|date_format:Y-m-d|after_or_equal:today',
@@ -120,7 +120,7 @@ class QuotationController extends Controller
public function evaluate(Request $request, Quotation $quotation, RequisitionWorkflow $workflow): RedirectResponse
{
$this->access($request);
abort_if($request->user()->purchasing_role === PurchasingRole::Admin, 403);
abort_unless($request->user()->hasFunctions([PurchasingRole::Procurement, PurchasingRole::Finance, PurchasingRole::DepartmentHead]), 403);
$data = $request->validate(['quality' => 'required|numeric|decimal:0,2|between:0,10', 'history' => 'required|numeric|decimal:0,2|between:0,10', 'payment' => 'required|numeric|decimal:0,2|between:0,10', 'comment' => 'required|string|min:10|max:2000', 'no_conflict' => 'accepted']);
DB::transaction(function () use ($request, $quotation, $data, $workflow) {
$guard = app(ConflictGuard::class);
@@ -14,6 +14,7 @@ use App\Models\TechnicalReview;
use App\Models\Tenant;
use App\Services\ConflictGuard;
use App\Services\EquipmentWorkflow;
use App\Services\OrganizationAccess;
use App\Services\RequisitionWorkflow;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
@@ -51,6 +52,8 @@ class RequisitionController extends Controller
public function create(Request $request): Response
{
abort_unless(app(OrganizationAccess::class)->canRequest($request->user()), 403);
return Inertia::render('requisitions/form', [
'costCenters' => CostCenter::query()->where('department_id', $request->user()->department_id)->orderBy('code')->get(),
'requisition' => null,
@@ -72,7 +75,7 @@ class RequisitionController extends Controller
return Inertia::render('requisitions/show', [
'requisition' => $requisition,
'canEdit' => $requisition->requester_id === $request->user()->id && $requisition->status === 'draft',
'canEdit' => app(OrganizationAccess::class)->canRequest($request->user()) && $requisition->requester_id === $request->user()->id && $requisition->status === 'draft',
'canDecide' => $requisition->canDecide($request->user()),
'conflictBlocked' => app(ConflictGuard::class)->participationBlocked($request->user(), $requisition),
'audit' => AuditLog::query()->where('requisition_id', $requisition->id)->orderBy('id')->get(['id', 'action', 'user_id', 'created_at']),
@@ -86,6 +89,7 @@ class RequisitionController extends Controller
public function edit(Request $request, Requisition $requisition): Response
{
abort_unless(app(OrganizationAccess::class)->canRequest($request->user()), 403);
abort_unless($requisition->requester_id === $request->user()->id && $requisition->status === 'draft', 403);
return Inertia::render('requisitions/form', [
+2 -2
View File
@@ -21,7 +21,7 @@ class StockController extends Controller
private function access(Request $request, bool $manage = false): void
{
$roles = $manage ? [PurchasingRole::Admin, PurchasingRole::Warehouse] : [PurchasingRole::Admin, PurchasingRole::Warehouse, PurchasingRole::Procurement, PurchasingRole::Finance];
abort_unless(in_array($request->user()->purchasing_role, $roles), 403);
abort_unless($request->user()->hasFunctions($roles), 403);
}
public function index(Request $request): Response
@@ -49,7 +49,7 @@ class StockController extends Controller
return Inertia::render('stock/index', [
'items' => $items, 'warehouses' => $warehouses, 'balances' => $balances,
'movements' => $movements->latest('id')->paginate(20)->withQueryString(), 'filters' => $filters,
'canManage' => in_array($request->user()->purchasing_role, [PurchasingRole::Admin, PurchasingRole::Warehouse]),
'canManage' => $request->user()->hasFunctions([PurchasingRole::Admin, PurchasingRole::Warehouse]),
]);
}
@@ -22,7 +22,7 @@ class StockOperationController extends Controller
{
public function index(Request $request, StockWorkflow $workflow): Response
{
abort_unless(in_array($request->user()->purchasing_role, [PurchasingRole::Warehouse, PurchasingRole::Admin, PurchasingRole::Finance, PurchasingRole::Procurement]), 403);
abort_unless($request->user()->hasFunctions([PurchasingRole::Warehouse, PurchasingRole::Admin, PurchasingRole::Finance, PurchasingRole::Procurement]), 403);
$items = StockItem::query()->orderBy('code')->get(['id', 'code', 'name', 'unit']);
$warehouses = Warehouse::query()->orderBy('code')->get(['id', 'code', 'name']);
$balances = [];
@@ -37,14 +37,14 @@ class StockOperationController extends Controller
'users' => User::query()->where('tenant_id', $request->user()->tenant_id)->orderBy('name')->get(['id', 'name']),
'operations' => StockOperation::query()->latest('id')->paginate(20),
'counts' => StockOperation::query()->where('kind', 'count')->whereNotIn('id', StockOperation::query()->whereNotNull('count_id')->select('count_id'))->latest('id')->get(),
'canOperate' => $request->user()->purchasing_role === PurchasingRole::Warehouse,
'canOperate' => $request->user()->hasFunctions([PurchasingRole::Warehouse]),
'equipment' => Equipment::query()->orderBy('code')->get(['id', 'code', 'description']),
]);
}
public function store(Request $request, StockWorkflow $workflow): JsonResponse|RedirectResponse
{
abort_unless($request->user()->purchasing_role === PurchasingRole::Warehouse, 403);
abort_unless($request->user()->hasFunctions([PurchasingRole::Warehouse]), 403);
$request->merge(['reference' => Str::upper(Str::squish($request->string('reference')->toString())), 'reason' => trim($request->string('reason')->toString()), 'destination' => trim($request->string('destination')->toString())]);
$tenant = $request->user()->tenant_id;
$data = $request->validate([
@@ -18,10 +18,10 @@ class SupplierBankController extends Controller
{
public function show(Request $request, Supplier $supplier, SupplierBankWorkflow $workflow): Response
{
abort_unless(in_array($request->user()->purchasing_role, [PurchasingRole::Admin, PurchasingRole::Procurement, PurchasingRole::Finance]), 403);
abort_unless($request->user()->hasFunctions([PurchasingRole::Admin, PurchasingRole::Procurement, PurchasingRole::Finance]), 403);
$details = SupplierBankDetail::query()->where('supplier_id', $supplier->id)->latest('revision')->get();
$current = $details->first();
$canRead = in_array($request->user()->purchasing_role, [PurchasingRole::Procurement, PurchasingRole::Finance]);
$canRead = $request->user()->hasFunctions([PurchasingRole::Procurement, PurchasingRole::Finance]);
$data = null;
if ($current && $canRead) {
app(RequisitionWorkflow::class)->audit($request->user(), 'supplier.bank_viewed', null, null, ['supplier_id' => $supplier->id, 'bank_detail_id' => $current->id]);
@@ -29,7 +29,7 @@ class SupplierBankController extends Controller
}
Inertia::encryptHistory();
$response = Inertia::render('suppliers/bank', ['supplier' => $supplier->only(['id', 'name']), 'current' => $data, 'currentId' => $current->id ?? 0, 'state' => $workflow->state($supplier), 'history' => $details->map(fn ($detail) => $detail->only(['id', 'revision', 'created_by', 'created_at'])), 'reviews' => SupplierBankReview::query()->whereIn('supplier_bank_detail_id', $details->pluck('id'))->orderBy('id')->get(), 'canPropose' => $request->user()->purchasing_role === PurchasingRole::Procurement, 'canReview' => $current && $request->user()->purchasing_role === PurchasingRole::Finance && $current->created_by !== $request->user()->id && ! SupplierBankReview::query()->where('supplier_bank_detail_id', $current->id)->where('reviewed_by', $request->user()->id)->exists()])->toResponse($request);
$response = Inertia::render('suppliers/bank', ['supplier' => $supplier->only(['id', 'name']), 'current' => $data, 'currentId' => $current->id ?? 0, 'state' => $workflow->state($supplier), 'history' => $details->map(fn ($detail) => $detail->only(['id', 'revision', 'created_by', 'created_at'])), 'reviews' => SupplierBankReview::query()->whereIn('supplier_bank_detail_id', $details->pluck('id'))->orderBy('id')->get(), 'canPropose' => $request->user()->hasFunctions([PurchasingRole::Procurement]), 'canReview' => $current && $request->user()->hasFunctions([PurchasingRole::Finance]) && $current->created_by !== $request->user()->id && ! SupplierBankReview::query()->where('supplier_bank_detail_id', $current->id)->where('reviewed_by', $request->user()->id)->exists()])->toResponse($request);
$response->headers->set('Cache-Control', 'private, no-store, max-age=0');
return $response;
+4 -4
View File
@@ -19,14 +19,14 @@ class SupplierController extends Controller
{
public function index(Request $request): Response
{
abort_unless(in_array($request->user()->purchasing_role, [PurchasingRole::Admin, PurchasingRole::Procurement, PurchasingRole::Finance, PurchasingRole::DepartmentHead]), 403);
abort_unless($request->user()->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()->purchasing_role === PurchasingRole::Procurement, 'canReview' => $request->user()->purchasing_role === PurchasingRole::Finance]);
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])]);
}
public function store(SaveSupplierRequest $request, RequisitionWorkflow $workflow): RedirectResponse
@@ -50,7 +50,7 @@ class SupplierController extends Controller
'documents' => $canDocuments ? $supplier->documents()->latest('id')->get() : [],
'canEdit' => $request->user()->can('update', $supplier),
'canDocuments' => $canDocuments,
'canReview' => $request->user()->purchasing_role === PurchasingRole::Finance,
'canReview' => $request->user()->hasFunctions([PurchasingRole::Finance]),
]);
}
@@ -84,7 +84,7 @@ class SupplierController extends Controller
public function review(Request $request, Supplier $supplier, RequisitionWorkflow $workflow): RedirectResponse
{
abort_unless($request->user()->purchasing_role === PurchasingRole::Finance, 403);
abort_unless($request->user()->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) {
$supplier = Supplier::query()->lockForUpdate()->findOrFail($supplier->id);
@@ -0,0 +1,159 @@
<?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 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($request->user());
$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, '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($request->user());
$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 && $writable && in_array($evaluation->status, ['draft', 'returned'], true),
'canReview' => ! $report && $writable && $evaluation->status === 'review' && ! in_array($actor->id, $evaluation->contributors, true),
'canRevise' => ! $report && $writable && $evaluation->status === 'approved' && $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 && $writable && in_array($r->status, ['draft', 'returned'], true),
'canReview' => ! $report && $writable && $r->status === 'review' && ! in_array($actor->id, $r->contributors, true)])],
'improvements' => ['ready' => $measuresReady, 'canCreate' => ! $report && $measuresReady && $writable && $evaluation->status === 'approved',
'today' => today()->toDateString(),
'records' => $measures->map(fn (SupplierImprovement $m) => [...$improvementWorkflow->snapshot($m),
'canPlan' => ! $report && $writable && in_array($m->status, ['open', 'returned'], true),
'canExecute' => ! $report && $writable && $actor->id === $m->responsible_id && in_array($m->status, ['open', 'returned'], true),
'canReview' => ! $report && $writable && $m->status === 'review' && ! in_array($actor->id, $m->contributors, true)]),
'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($request->user(), (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($request->user(), $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($request->user(), $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($request->user(), $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.');
}
}
@@ -0,0 +1,44 @@
<?php
namespace App\Http\Controllers;
use App\Services\SupplierImprovementWorkflow;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Schema;
class SupplierImprovementController extends Controller
{
public function store(Request $request, int $evaluation, SupplierImprovementWorkflow $workflow): RedirectResponse
{
return $this->execute($request, $evaluation, null, 'create', $workflow);
}
public function update(Request $request, int $evaluation, int $measure, string $command, SupplierImprovementWorkflow $workflow): RedirectResponse
{
return $this->execute($request, $evaluation, $measure, $command, $workflow);
}
private function execute(Request $request, int $evaluation, ?int $measure, string $command, SupplierImprovementWorkflow $workflow): RedirectResponse
{
abort_unless(Schema::hasTable('supplier_improvements'), 503, 'A instalação das medidas de melhoria ainda não está concluída.');
$rules = ['tenant_id' => 'prohibited', 'supplier_evaluation_id' => 'prohibited', 'created_by' => 'prohibited', 'status' => 'prohibited', 'contributors' => 'prohibited', 'reviewed_by' => 'prohibited', 'reviewed_at' => 'prohibited'];
if ($command === 'create' || $command === 'plan') {
$rules += ['title' => 'required|string|min:3|max:200', 'description' => 'required|string|min:10|max:10000', 'responsible_id' => 'required|integer|min:1', 'due_on' => 'required|date_format:Y-m-d', 'result' => 'prohibited', 'evidence' => 'prohibited'];
} else {
$rules += ['title' => 'prohibited', 'description' => 'prohibited', 'responsible_id' => 'prohibited', 'due_on' => 'prohibited'];
}
$rules += $command === 'create' ? ['operation_key' => 'required|uuid', 'version' => 'prohibited', 'reason' => 'prohibited'] : ['operation_key' => 'prohibited', 'version' => 'required|integer|min:1'];
if (in_array($command, ['progress', 'submit'], true)) {
$rules += ['result' => 'nullable|string|max:10000', 'evidence' => 'nullable|string|max:10000', 'reason' => 'prohibited'];
} elseif ($command !== 'create') {
$rules += ['reason' => 'required|string|min:10|max:4000'];
if ($command !== 'plan') {
$rules += ['result' => 'prohibited', 'evidence' => 'prohibited'];
}
}
$workflow->execute($request->user(), $evaluation, $measure, $command, $request->validate($rules));
return to_route('supplier-evaluations.show', $evaluation)->with('supplier_evaluation_notice', 'Medida de melhoria e histórico actualizados.');
}
}
@@ -0,0 +1,146 @@
<?php
namespace App\Http\Controllers;
use App\Models\SupplierEvaluation;
use App\Models\SupplierImprovement;
use App\Models\User;
use App\Services\SupplierEvaluationWorkflow;
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 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($request->user());
$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']);
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($request->user());
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', $request->user()->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($request->user());
$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($request->user())->whereIn('id', (clone $all)->select('supplier_evaluation_id'))->orderByDesc('id')->get(['supplier_id', 'supplier_snapshot']);
return ['filters' => $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', $request->user()->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);
$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], ';', '"', '');
fputcsv($out, ['Filtros', $text(json_encode($filters, 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']);
}
}
@@ -0,0 +1,43 @@
<?php
namespace App\Http\Controllers;
use App\Services\SupplierResponseWorkflow;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Schema;
class SupplierResponseController extends Controller
{
public function store(Request $request, int $evaluation, SupplierResponseWorkflow $workflow): RedirectResponse
{
return $this->execute($request, $evaluation, null, 'create', $workflow);
}
public function update(Request $request, int $evaluation, int $record, string $command, SupplierResponseWorkflow $workflow): RedirectResponse
{
return $this->execute($request, $evaluation, $record, $command, $workflow);
}
private function execute(Request $request, int $evaluation, ?int $record, string $command, SupplierResponseWorkflow $workflow): RedirectResponse
{
abort_unless(Schema::hasTable('supplier_responses'), 503, 'A instalação do contraditório ainda não está concluída.');
$rules = ['tenant_id' => 'prohibited', 'supplier_evaluation_id' => 'prohibited', 'created_by' => 'prohibited', 'contributors' => 'prohibited', 'status' => 'prohibited', 'decision' => 'prohibited', 'reviewed_by' => 'prohibited', 'reviewed_at' => 'prohibited'];
$rules += $command === 'create' ? ['operation_key' => 'required|uuid', 'version' => 'prohibited'] : ['operation_key' => 'prohibited', 'version' => 'required|integer|min:1'];
if (in_array($command, ['create', 'save'], true)) {
$rules += ['payload' => 'required|array:title,received_on,source_reference,statement', 'payload.title' => 'required|string|min:3|max:200',
'payload.received_on' => 'nullable|date_format:Y-m-d', 'payload.source_reference' => 'nullable|string|max:4000', 'payload.statement' => 'nullable|string|max:20000'];
} else {
$rules['payload'] = 'prohibited';
}
$rules['reason'] = in_array($command, ['return', 'decide'], true) ? 'required|string|min:10|max:10000' : 'prohibited';
if ($command === 'decide') {
$rules += ['outcome' => 'required|in:accepted,partially_accepted,not_accepted', 'follow_up' => 'nullable|string|max:10000', 'response_reference' => 'nullable|string|max:4000'];
} else {
$rules += ['outcome' => 'prohibited', 'follow_up' => 'prohibited', 'response_reference' => 'prohibited'];
}
$workflow->execute($request->user(), $evaluation, $record, $command, $request->validate($rules));
return to_route('supplier-evaluations.show', $evaluation)->with('supplier_evaluation_notice', 'Contraditório e histórico actualizados.');
}
}
@@ -0,0 +1,28 @@
<?php
namespace App\Http\Middleware;
use App\Services\OrganizationAccess;
use App\Services\RequisitionWorkflow;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureOrganizationAccess
{
public function handle(Request $request, Closure $next): Response
{
$user = $request->user();
if ($user !== null && $user->tenant_id !== null) {
abort_unless(app(OrganizationAccess::class)->permissions($user) !== [], 403, 'Não existe uma atribuição de acesso válida. Contacte os administradores da empresa.');
abort_if(app(OrganizationAccess::class)->maintenanceOnly($user) && ! $request->isMethodSafe(), 403, 'A manutenção temporária tem acesso de diagnóstico e consulta.');
}
$response = $next($request);
if ($user !== null && app(OrganizationAccess::class)->maintenanceOnly($user)) {
app(RequisitionWorkflow::class)->audit($user, 'maintenance.consulted', null, null, ['path' => $request->path(), 'status' => $response->getStatusCode()]);
}
return $response;
}
}
@@ -2,6 +2,7 @@
namespace App\Http\Middleware;
use App\Services\OrganizationAccess;
use App\Services\SupplierPayables;
use Illuminate\Http\Request;
use Inertia\Middleware;
@@ -39,9 +40,15 @@ class HandleInertiaRequests extends Middleware
return [
...parent::share($request),
'name' => config('app.name'),
'canRequest' => $request->user() !== null && app(OrganizationAccess::class)->canRequest($request->user()),
'auth' => [
'user' => $request->user(),
],
'organizationAccess' => $request->user() === null ? null : [
'permissions' => app(OrganizationAccess::class)->permissions($request->user()),
'active' => app(OrganizationAccess::class)->active($request->user()->tenant_id),
'profileLabel' => app(OrganizationAccess::class)->profileLabel($request->user()),
],
'payablesAvailable' => $request->user() !== null && app(SupplierPayables::class)->can($request->user(), 'view'),
'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true',
];
+1 -1
View File
@@ -11,7 +11,7 @@ class SaveSupplierRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()?->purchasing_role === PurchasingRole::Procurement;
return $this->user()?->hasFunctions([PurchasingRole::Procurement]) ?? false;
}
/** @return array<string, mixed> */