user('web'); abort_unless($actor instanceof User, 403); return $actor; } public function show(Request $request, int $equipment): \Symfony\Component\HttpFoundation\Response { $actor = $request->user('web'); abort_unless($actor instanceof User, 403); $actor = User::query()->where('tenant_id', $actor->tenant_id)->findOrFail($actor->id); abort_unless($actor->hasFunctions([PurchasingRole::Admin, PurchasingRole::Warehouse, PurchasingRole::Procurement, PurchasingRole::Finance, PurchasingRole::DepartmentHead]), 403); $request->validate(['purchases_page' => 'nullable|integer|min:1|max:1000000', 'movements_page' => 'nullable|integer|min:1|max:1000000', 'requests_page' => 'nullable|integer|min:1|max:1000000']); return DB::transaction(function () use ($request, $actor, $equipment) { if (DB::getDriverName() === 'pgsql' && DB::transactionLevel() === 1) { DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY'); } $asset = Equipment::query()->when($actor->departmentScoped(), fn ($q) => $q->where('department_id', $actor->department_id))->findOrFail($equipment); $purchases = Requisition::query()->visibleTo($actor)->whereHas('items', fn ($q) => $q->where('equipment_id', $asset->id)) ->with(['items' => fn ($q) => $q->where('equipment_id', $asset->id)->select(['id', 'requisition_id', 'description', 'quantity', 'unit'])]) ->orderBy('id')->paginate(15, ['id', 'number', 'status', 'needed_by'], 'purchases_page')->withQueryString(); $movements = StockOperation::query()->where('equipment_id', $asset->id)->orderBy('id') ->paginate(15, ['id', 'kind', 'reference', 'quantity_milli', 'recorded_at', 'snapshot'], 'movements_page')->withQueryString() ->through(fn (StockOperation $operation) => [ 'id' => $operation->id, 'kind' => $operation->kind, 'reference' => $operation->reference, 'quantity_milli' => $operation->quantity_milli, 'recorded_at' => $operation->recorded_at, 'item' => $operation->snapshot['stock_item'] ?? null, 'warehouse' => $operation->snapshot['warehouse'] ?? null, ]); Inertia::encryptHistory(); $response = Inertia::render('equipment/show', ['equipment' => $asset->only(['id', 'code', 'description', 'area', 'status']), 'geography' => app(AdministrativeGeography::class)->describe($asset->geography_reference), 'warehouseRequests' => app(EquipmentWarehouseRequests::class)->page($actor, $asset), 'purchases' => $purchases, 'movements' => $movements])->toResponse($request); $response->headers->set('Cache-Control', 'private, no-store, max-age=0'); return $response; }); } public function index(Request $request): Response { abort_unless($this->actor($request)->hasFunctions([PurchasingRole::Admin, PurchasingRole::Warehouse, PurchasingRole::Procurement, PurchasingRole::Finance, PurchasingRole::DepartmentHead]), 403); $filters = $request->validate(['location' => 'nullable|string|regex:/^[0-9]+$/D|max:20']); $geography = app(AdministrativeGeography::class); $assets = Equipment::query(); if (! empty($filters['location'])) { abort_unless($geography->availableFor('equipment'), 503); $assets->where('geography_reference->catalogue', config('geography.source_sha256'))->whereIn('geography_reference->record_id', $geography->descendants($filters['location'])); } if ($this->actor($request)->departmentScoped()) { $assets->where('department_id', $this->actor($request)->department_id); } $equipment = $assets->orderBy('code')->get(); $links = EquipmentSparePart::query()->whereIn('equipment_id', $equipment->pluck('id'))->get(); $parts = SparePart::query(); if ($this->actor($request)->departmentScoped() || ! empty($filters['location'])) { $parts->whereIn('id', $links->pluck('spare_part_id')); } $parts = $parts->get()->map(function (SparePart $part) { $sum = StockMovement::query()->where('stock_item_id', $part->stock_item_id)->sum('quantity_milli'); $balance = BigDecimal::of((string) $sum)->toInt(); return [...$part->toArray(), 'balance_milli' => $balance, 'below_minimum' => $balance < $part->minimum_milli]; }); $items = StockItem::query(); $departments = Department::query(); if ($this->actor($request)->departmentScoped() || ! empty($filters['location'])) { $items->whereIn('id', $parts->pluck('stock_item_id')->merge($equipment->pluck('stock_item_id')->filter())); if ($this->actor($request)->departmentScoped()) { $departments->whereKey($this->actor($request)->department_id); } } return Inertia::render('equipment/index', [ 'geographyEnabled' => $geography->availableFor('equipment'), 'equipmentGeography' => $equipment->mapWithKeys(fn (Equipment $asset) => [$asset->id => $geography->describe($asset->geography_reference)])->all(), 'filters' => $filters, 'geographyFilter' => empty($filters['location']) ? null : $geography->describe(['catalogue' => config('geography.source_sha256'), 'record_id' => $filters['location'], 'country' => 'MZ']), 'equipment' => $equipment, 'parts' => $parts, 'links' => $links, 'levels' => CriticalityLevel::query()->orderBy('code')->get(), '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' => $this->actor($request)->hasFunctions([PurchasingRole::Admin, PurchasingRole::Warehouse, PurchasingRole::Procurement]), 'canConfigure' => $this->actor($request)->hasFunctions([PurchasingRole::Admin]), ]); } public function store(Request $request, string $kind): RedirectResponse { $user = $this->actor($request); abort_unless(in_array($kind, ['categorias', 'activos', 'pecas', 'associacoes']), 404); 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()))]); $rules = match ($kind) { 'categorias' => ['code' => ['required', 'regex:/^[A-Z0-9][A-Z0-9._-]{0,29}$/', Rule::unique('criticality_levels')->where('tenant_id', $tenant)], 'name' => 'required|string|max:150', 'criteria' => 'required|string|max:4000', 'is_critical' => 'required|boolean'], 'activos' => ['id' => ['nullable', 'integer', $exists('equipment')], 'version' => 'nullable|required_with:id|integer|min:1', 'code' => ['required', 'regex:/^[A-Z0-9][A-Z0-9._-]{0,29}$/', Rule::unique('equipment')->where('tenant_id', $tenant)->ignore($request->input('id'))], 'description' => 'required|string|max:255', 'area' => 'required|string|max:200', 'serial_number' => 'nullable|string|max:100', 'status' => ['required', Rule::in(['active', 'stopped', 'retired'])], 'department_id' => ['required', 'integer', $exists('departments')], 'stock_item_id' => ['nullable', 'integer', $exists('stock_items')], 'criticality_level_id' => ['required', 'integer', $exists('criticality_levels')]], 'pecas' => ['id' => ['nullable', 'integer', $exists('spare_parts')], 'version' => 'nullable|required_with:id|integer|min:1', 'stock_item_id' => ['required', 'integer', $exists('stock_items'), Rule::unique('spare_parts')->ignore($request->input('id'))], 'criticality_level_id' => ['required', 'integer', $exists('criticality_levels')], 'minimum_quantity' => ['required', 'regex:/^\d{1,9}(\.\d{1,3})?$/']], 'associacoes' => ['equipment_id' => ['required', 'integer', $exists('equipment')], 'spare_part_id' => ['required', 'integer', $exists('spare_parts')]], }; if ($kind === 'activos') { $rules += app(AdministrativeGeography::class)->referenceRules('equipment'); } $data = $request->validate($rules); DB::transaction(function () use ($data, $kind, $user) { app(ConflictGuard::class)->lockCompany($user); $user = app(CurrentActor::class)->lock($user, $kind === 'categorias' ? [PurchasingRole::Admin] : [PurchasingRole::Admin, PurchasingRole::Warehouse, PurchasingRole::Procurement]); abort_if(app(OrganizationAccess::class)->maintenanceOnly($user), 403); if ($kind === 'activos' && array_key_exists('geography_reference', $data)) { $data['geography_reference'] = app(AdministrativeGeography::class)->canonicalFor('equipment', $data['geography_reference']); } $model = match ($kind) { 'categorias' => new CriticalityLevel,'activos' => new Equipment,'pecas' => new SparePart,'associacoes' => new EquipmentSparePart }; $id = $data['id'] ?? null; $before = null; $input = $data; unset($input['id'],$input['version']); if ($id) { $model = $model->newQuery()->whereKey($id)->lockForUpdate()->firstOrFail(); if (! $model instanceof Equipment && ! $model instanceof SparePart) { throw new \LogicException('Este cadastro é apenas de acréscimo.'); } $before = $model->toArray(); if ((int) $data['version'] !== $model->version) { throw ValidationException::withMessages(['version' => 'Ficha alterada. Actualize antes de guardar.']); }if ($kind === 'pecas' && (int) $input['stock_item_id'] !== $model->stock_item_id) { throw ValidationException::withMessages(['stock_item_id' => 'O artigo de uma peça não pode ser substituído; preserve a rastreabilidade.']); }$input['version'] = $model->version + 1; } if ($kind === 'pecas') { $input['minimum_milli'] = ReceiptWorkflow::milli($input['minimum_quantity']); unset($input['minimum_quantity']); } if ($kind === 'associacoes' && EquipmentSparePart::query()->where('equipment_id', $input['equipment_id'])->where('spare_part_id', $input['spare_part_id'])->exists()) { throw ValidationException::withMessages(['spare_part_id' => 'Esta associação já existe e permanece no histórico.']); } if (! $id) { $input['tenant_id'] = $user->tenant_id; $input['created_by'] = $user->id; } if (in_array($kind, ['categorias', 'associacoes'])) { $input['created_at'] = now(); } $model->fill($input)->save(); app(RequisitionWorkflow::class)->audit($user, 'equipment.'.$kind.($id ? '_updated' : '_created'), null, $before, $model->toArray()); }); return back(); } }