Files
SGF/app/Modules/Workshop/WorkshopEquipment.php
T
2026-11-11 09:00:17 +02:00

21 lines
7.7 KiB
PHP

<?php
namespace App\Modules\Workshop;
use App\Modules\Identity\Services\{Permissions,Audit};use App\Shared\Context\{CompanyContext,ModuleRecords};use App\Shared\Installation\CompositionRuntime;use Illuminate\Support\Facades\{DB,Crypt,Validator};
final class WorkshopEquipment
{
public function __construct(private CompanyContext $context,private Permissions $permissions){}
private function scope(int $actor,bool $write=false):array{abort_unless(config('workshop_catalogue.enabled')===true,404);CompositionRuntime::assert();$s=$this->context->identifiers()+['unit_id'=>$this->context->unitId()];abort_unless($s['unit_id']&&$actor===$this->context->userId()&&ModuleRecords::active()==='workshop',403);$this->context->selectFor($actor,$s['tenant_id'],$s['company_id'],$s['unit_id']);foreach(['view',...($write?['open']:[])] as $right)$this->permissions->require($actor,$s['tenant_id'],$s['company_id'],'workshop.'.$right,$s['unit_id']);return $s;}
private function manager(int $actor,array $s):bool{foreach(['open','assign','authorize','verify'] as $right)if($this->permissions->has($actor,$s['tenant_id'],$s['company_id'],'workshop.'.$right,$s['unit_id']))return true;return false;}
private function query(int $actor,array $s){$q=DB::table('wo_equipment as e')->where(['e.tenant_id'=>$s['tenant_id'],'e.company_id'=>$s['company_id']])->whereIn('e.client_id',ModuleRecords::query('a_clients')->select('id'));if(!$this->manager($actor,$s))$q->whereIn('e.id',DB::table('wo_orders as o')->where(['o.tenant_id'=>$s['tenant_id'],'o.company_id'=>$s['company_id'],'o.unit_id'=>$s['unit_id']])->whereRaw("(SELECT a.technician_id FROM wo_entries a WHERE a.order_id=o.id AND a.kind='assignment' ORDER BY a.id DESC LIMIT 1)=?",[$actor])->select('o.equipment_id'));return $q;}
private function hash(mixed $v):string{return hash('sha256',json_encode($v,JSON_THROW_ON_ERROR));}
public function listing(int $actor,array $input):array{$s=$this->scope($actor);$rules=['view'=>'sometimes|in:equipment,clients','search'=>'sometimes|nullable|string|max:100','client_id'=>'sometimes|integer|min:1','page'=>'sometimes|integer|min:1|max:100000','per_page'=>'sometimes|integer|in:25,100'];abort_if(array_diff(array_keys($input),array_keys($rules)),422);$d=Validator::make($input,$rules)->validate();$clients=($d['view']??'equipment')==='clients';if($clients){$this->scope($actor,true);$q=ModuleRecords::query('a_clients')->where(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id']]);$fields=['id','name','version'];}else{$q=$this->query($actor,$s)->join('a_clients as c','c.id','=','e.client_id');if(isset($d['client_id']))$q->where('e.client_id',$d['client_id']);$fields=['e.*','c.name as client_name'];}if(($d['search']??'')!==''){$needle='%'.str_replace(['!','%','_'],['!!','!%','!_'],$d['search']).'%';if($clients)$q->whereRaw("name ILIKE ? ESCAPE '!'",[$needle]);else$q->where(fn($x)=>$x->whereRaw("e.identifier ILIKE ? ESCAPE '!'",[$needle])->orWhereRaw("e.description ILIKE ? ESCAPE '!'",[$needle])->orWhereRaw("e.serial ILIKE ? ESCAPE '!'",[$needle]));}$p=$q->orderBy($clients?'id':'e.id','desc')->paginate((int)($d['per_page']??25),$fields,'page',(int)($d['page']??1));return ['records'=>$p,'can_create'=>$this->permissions->has($actor,$s['tenant_id'],$s['company_id'],'workshop.open',$s['unit_id']),'immutable_identification'=>true];}
public function visible(int $actor,int $id):array
{
$s=$this->scope($actor);$row=$this->query($actor,$s)->where('e.id',$id)->join('a_clients as client',fn($j)=>$j->on('client.id','=','e.client_id')->on('client.tenant_id','=','e.tenant_id')->on('client.company_id','=','e.company_id'))->first(['e.id','e.client_id','e.identifier','e.description','e.serial','client.name as client_name']);abort_unless($row,403,'Equipamento fora do catálogo autorizado.');return(array)$row;
}
private function data(array $input):array{foreach(['identifier','description','serial','reason'] as $key)if(isset($input[$key])&&is_string($input[$key]))$input[$key]=trim($input[$key]);$rules=['client_id'=>'required|integer|min:1','identifier'=>'required|string|min:2|max:100','description'=>'required|string|min:3|max:300','serial'=>'nullable|string|max:100','reason'=>'required|string|min:10|max:2000'];abort_if(array_diff(array_keys($input),array_keys($rules)),422);$d=Validator::make($input,$rules)->validate();return ['client_id'=>(int)$d['client_id'],'identifier'=>trim($d['identifier']),'description'=>trim($d['description']),'serial'=>isset($d['serial'])&&trim($d['serial'])!==''?trim($d['serial']):null,'reason'=>trim($d['reason'])];}
private function client(array $s,array $d):object{return ModuleRecords::query('a_clients')->where(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id'],'id'=>$d['client_id']])->first()??abort(403,'Cliente sem associação explícita ao módulo e à empresa.');}
public function preview(int $actor,array $input,string $key):array{$s=$this->scope($actor,true);abort_unless(preg_match('/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/D',$key),422);$d=$this->data($input);$c=$this->client($s,$d);abort_if(DB::table('wo_equipment')->where(['company_id'=>$s['company_id'],'identifier'=>$d['identifier']])->exists(),409,'Identificação existente. Reutilize o equipamento.');$scope=['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id'],'unit_id'=>$s['unit_id']];$cut=['actor'=>$actor,'scope'=>$scope,'key'=>$key,'data'=>$d,'client_version'=>(int)$c->version,'generation'=>$this->hash(request()->session()->get('company_context',[])),'observed'=>time()];return ['preview'=>Crypt::encryptString(json_encode($cut,JSON_THROW_ON_ERROR)),'impact'=>['client'=>$c->name,'identifier'=>$d['identifier'],'description'=>$d['description'],'serial'=>$d['serial'],'creates_order'=>false,'financial_effect'=>false]];}
public function confirm(int $actor,string $token):array{try{$cut=json_decode(Crypt::decryptString($token),true,64,JSON_THROW_ON_ERROR);}catch(\Throwable){abort(409,'Prévia inválida.');}$s=$this->scope($actor,true);return DB::transaction(function()use($actor,$cut,$s){DB::statement("SET LOCAL lock_timeout='5s'");DB::table('companies')->where('id',$s['company_id'])->lockForUpdate()->firstOrFail();DB::table('users')->where('id',$actor)->lockForUpdate()->firstOrFail();$this->scope($actor,true);DB::table('permission_grants')->where($s+['user_id'=>$actor])->lockForUpdate()->get();abort_unless(($cut['actor']??null)===$actor&&($cut['scope']??null)===$s&&($cut['generation']??null)===$this->hash(request()->session()->get('company_context',[]))&&($cut['observed']??0)>=time()-600&&$cut['observed']<=time(),409,'Contexto/prévia alterados.');$d=$this->data($cut['data']);$c=$this->client($s,$d);$old=DB::table('wo_equipment_commands')->where($s)->where(['actor_id'=>$actor,'command_key'=>$cut['key']])->first();if($old){abort_unless($old->request_hash===$this->hash($d),409);return ['id'=>$old->equipment_id,'replayed'=>true];}abort_unless((int)$c->version===$cut['client_version'],409,'Cliente alterado. Confira novamente.');abort_if(DB::table('wo_equipment')->where(['company_id'=>$s['company_id'],'identifier'=>$d['identifier']])->exists(),409,'Identificação já registada.');$id=DB::table('wo_equipment')->insertGetId(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id'],'client_id'=>$d['client_id'],'identifier'=>$d['identifier'],'description'=>$d['description'],'serial'=>$d['serial'],'actor_id'=>$actor,'created_at'=>now()]);DB::table('wo_equipment_commands')->insert($s+['actor_id'=>$actor,'command_key'=>$cut['key'],'request_hash'=>$this->hash($d),'equipment_id'=>$id,'reason'=>$d['reason'],'created_at'=>now()]);Audit::record('workshop.equipment.registered',$actor,$s['tenant_id'],$s['company_id'],$s['unit_id'],details:['record_id'=>$id,'reason'=>$d['reason']]);return ['id'=>$id,'replayed'=>false];});}
}