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

46 lines
5.9 KiB
PHP

<?php
namespace App\Modules\Workshop;
use App\Modules\Identity\Services\Permissions;
use App\Shared\Context\{CompanyContext,ModuleRecords};
use App\Shared\Installation\CompositionRuntime;
use Illuminate\Support\Facades\{DB,Validator};
/** Paged observation of original orders. Never prepares, locks or confirms work. */
final class WorkshopOrderRead
{
public function __construct(private CompanyContext $context,private Permissions $permissions){}
public function listing(int $actor,array $input):array
{
CompositionRuntime::assert();WorkshopWorkflow::guard();
$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']);
$this->permissions->require($actor,$s['tenant_id'],$s['company_id'],'workshop.view',$s['unit_id']);
$rules=['equipment_id'=>'sometimes|integer|min:1','page'=>'sometimes|integer|min:1|max:100000','per_page'=>'sometimes|integer|in:25,100','search'=>'sometimes|nullable|string|max:100','state'=>'sometimes|in:open,in_review,exception_review,records_verified','from'=>'required_with:to|date_format:Y-m-d|after_or_equal:2000-01-01|before_or_equal:2100-12-31','to'=>'required_with:from|date_format:Y-m-d|after_or_equal:from|before_or_equal:2100-12-31'];
abort_if(array_diff(array_keys($input),array_keys($rules)),422);$d=Validator::make($input,$rules)->validate();
$manager=false;foreach(['open','assign','authorize','verify']as$right)$manager=$manager||$this->permissions->has($actor,$s['tenant_id'],$s['company_id'],'workshop.'.$right,$s['unit_id']);
return DB::transaction(function()use($s,$actor,$manager,$d){
if(DB::transactionLevel()===1)DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');
$equipment=isset($d['equipment_id'])?app(WorkshopEquipment::class)->visible($actor,(int)$d['equipment_id']):null;
$current="NOT EXISTS(SELECT 1 FROM wo_entries newer WHERE newer.previous_id=e.id)";
$points="SELECT 1 FROM wo_entries e LEFT JOIN wo_decisions decision ON decision.entry_id=e.id WHERE e.order_id=o.id AND e.kind IN ('task','time','material') AND $current";
$state="CASE WHEN NOT EXISTS($points) THEN 'open' WHEN EXISTS($points AND e.outside_authority=true) THEN 'exception_review' WHEN EXISTS($points AND (decision.id IS NULL OR decision.decision<>'verified')) THEN 'in_review' ELSE 'records_verified' END";
$assignment="(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)";
$q=DB::table('wo_orders as o')->where(array_combine(array_map(fn($k)=>'o.'.$k,array_keys($s)),array_values($s)))
->leftJoin('wo_equipment as equipment',fn($j)=>$j->on('equipment.id','=','o.equipment_id')->on('equipment.company_id','=','o.company_id')->on('equipment.tenant_id','=','o.tenant_id'))
->leftJoin('a_clients as client',fn($j)=>$j->on('client.id','=','o.client_id')->on('client.company_id','=','o.company_id')->on('client.tenant_id','=','o.tenant_id'))
->leftJoin('users as technician',fn($j)=>$j->whereRaw('technician.id='.$assignment));
$period=null;if(isset($d['from'])){$start=\Carbon\CarbonImmutable::createFromFormat('!Y-m-d',$d['from'],WorkshopWorkflow::ZONE);$end=\Carbon\CarbonImmutable::createFromFormat('!Y-m-d',$d['to'],WorkshopWorkflow::ZONE)->addDay();$q->where('o.created_at','>=',$start->utc()->format('Y-m-d H:i:sP'))->where('o.created_at','<',$end->utc()->format('Y-m-d H:i:sP'));$period=['from'=>$d['from'],'to'=>$d['to'],'zone'=>WorkshopWorkflow::ZONE,'basis'=>'order_created_at_current_records','start_utc'=>$start->utc()->toIso8601String(),'end_utc_exclusive'=>$end->utc()->toIso8601String()];}
if($equipment)$q->where('o.equipment_id',$equipment['id']);
if(!$manager)$q->whereRaw($assignment.'=?',[$actor]);
if(isset($d['state']))$q->whereRaw("($state)=?",[$d['state']]);
if(($d['search']??'')!==''){$needle='%'.str_replace(['!','%','_'],['!!','!%','!_'],$d['search']).'%';$q->where(function($x)use($needle){foreach(['o.reference','equipment.identifier','equipment.description','equipment.serial','client.name','technician.name']as$i=>$column)$x->{$i?'orWhereRaw':'whereRaw'}("$column ILIKE ? ESCAPE '!'",[$needle]);});}
$p=$q->orderByDesc('o.id')->paginate((int)($d['per_page']??25),['o.id','o.reference','o.created_at','o.client_id','o.equipment_id','client.name as client','equipment.identifier as equipment','equipment.description','equipment.serial','technician.name as technician',DB::raw("$assignment as technician_id"),DB::raw("$state as state")],'page',(int)($d['page']??1));
$stats=DB::table('wo_entries as e')->leftJoin('wo_decisions as decision','decision.entry_id','=','e.id')->whereIn('e.order_id',$p->pluck('id')->all())->whereRaw($current)->groupBy('e.order_id')->selectRaw("e.order_id,count(*) FILTER(WHERE e.kind IN ('task','time','material')) points,count(*) FILTER(WHERE e.kind IN ('task','time','material') AND (decision.id IS NULL OR decision.decision<>'verified')) points_not_verified,coalesce(sum(e.minutes) FILTER(WHERE e.kind='time'),0) minutes_reported,coalesce(sum(e.minutes) FILTER(WHERE e.kind='time' AND decision.decision='verified'),0) minutes_verified")->get()->keyBy('order_id');
foreach($p->items()as$row){$statsRow=$stats->get($row->id);foreach(['points','points_not_verified','minutes_reported','minutes_verified']as$key)$row->$key=(int)($statsRow?->$key??0);}
return ['equipment'=>$equipment,'orders'=>$p,'observed_at'=>now()->utc()->toIso8601String(),'states_are'=>'current_resource_records_not_delivery_or_payment','read_only'=>true,'opening_period'=>$period,'notice'=>'Estado dos apontamentos actuais; não equivale a conclusão, entrega, facturação ou pagamento.'];
});
}
}