@@ -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);
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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]];
|
||||
|
||||
@@ -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', [
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
];
|
||||
|
||||
@@ -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> */
|
||||
|
||||
Reference in New Issue
Block a user