122 lines
7.6 KiB
PHP
122 lines
7.6 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Enums\PurchasingRole;
|
|
use App\Models\CriticalityLevel;
|
|
use App\Models\Department;
|
|
use App\Models\Equipment;
|
|
use App\Models\EquipmentSparePart;
|
|
use App\Models\SparePart;
|
|
use App\Models\StockItem;
|
|
use App\Models\StockMovement;
|
|
use App\Models\StockOperation;
|
|
use App\Services\ConflictGuard;
|
|
use App\Services\ReceiptWorkflow;
|
|
use App\Services\RequisitionWorkflow;
|
|
use Brick\Math\BigDecimal;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Validation\Rule;
|
|
use Illuminate\Validation\ValidationException;
|
|
use Inertia\Inertia;
|
|
use Inertia\Response;
|
|
|
|
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);
|
|
$assets = Equipment::query();
|
|
if ($request->user()->purchasing_role === PurchasingRole::DepartmentHead) {
|
|
$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) {
|
|
$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 ($request->user()->purchasing_role === PurchasingRole::DepartmentHead) {
|
|
$items->whereIn('id', $parts->pluck('stock_item_id')->merge($equipment->pluck('stock_item_id')->filter()));
|
|
$departments->whereKey($request->user()->department_id);
|
|
}
|
|
|
|
return Inertia::render('equipment/index', [
|
|
'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' => in_array($request->user()->purchasing_role, [PurchasingRole::Admin, PurchasingRole::Warehouse, PurchasingRole::Procurement]),
|
|
'canConfigure' => $request->user()->purchasing_role === PurchasingRole::Admin,
|
|
]);
|
|
}
|
|
|
|
public function store(Request $request, string $kind): RedirectResponse
|
|
{
|
|
$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);
|
|
$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')]],
|
|
};
|
|
$data = $request->validate($rules);
|
|
DB::transaction(function () use ($data, $kind, $user) {
|
|
app(ConflictGuard::class)->lockCompany($user);
|
|
$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();
|
|
}
|
|
}
|