validated(); $tenantId = $request->attributes->get('tenant_id'); $sortBy = $filters['sort_by'] ?? 'created_at'; $sortDirection = $filters['sort_direction'] ?? 'desc'; $tickets = Ticket::query()->with(['ticketType:id,code,name,need_approval,require_photo,require_material','customer:id,customer_code,name','tenant:id,tenant_name','creator:id,name','assignments.user:id,name']) ->when(! $request->user()->isMasterAdmin(), fn ($query) => $query->where('tenant_id', $tenantId)) ->when($request->user()->isMasterAdmin() && $tenantId, fn ($query) => $query->where('tenant_id', $tenantId)) ->when(! empty($filters['assigned_to_me']), fn ($query) => $query->whereHas('assignments', fn ($assignment) => $assignment->where('user_id', $request->user()->id))) ->when( ! empty($filters['search']), fn ($query) => $query->where(function ($searchQuery) use ($filters) { $searchQuery->where('ticket_no', 'ILIKE', "%{$filters['search']}%") ->orWhere('title', 'ILIKE', "%{$filters['search']}%") ->orWhere('description', 'ILIKE', "%{$filters['search']}%"); }) ) ->when( ! empty($filters['status']), fn ($query) => $query->where('status', $filters['status']) ) ->when( ! empty($filters['priority']), fn ($query) => $query->where('priority', $filters['priority']) ) ->when( ! empty($filters['ticket_type_id']), fn ($query) => $query->where('ticket_type_id', $filters['ticket_type_id']) ) ->when( ! empty($filters['customer_id']), fn ($query) => $query->where('customer_id', $filters['customer_id']) ) ->orderBy($sortBy, $sortDirection) ->paginate($filters['per_page'] ?? 10); return $this->successPaginated( $tickets, TicketResource::class, 'Data Ticket berhasil diambil' ); } public function store(StoreTicketRequest $request) { $ticket = $this->service->create( $request->validated(), $request->user(), $request->attributes->get('tenant_id') ); return $this->created( new TicketResource($ticket), 'Ticket berhasil dibuat' ); } public function show(Request $request, Ticket $ticket) { $this->authorizeTenant($request, $ticket); return $this->success( new TicketResource($ticket->load(['ticketType','customer','tenant','creator','assignments.user','logs','materials','attachments'])) ); } public function update( UpdateTicketRequest $request, Ticket $ticket ) { $this->authorizeTenant($request, $ticket); $ticket = $this->service->update( $ticket, $request->validated() ); return $this->updated( new TicketResource($ticket), 'Ticket berhasil diperbarui' ); } public function destroy(Request $request, Ticket $ticket) { $this->authorizeTenant($request, $ticket); $this->service->delete($ticket); return $this->deleted( 'Ticket berhasil dibatalkan' ); } public function options(Request $request) { $tenantId = $request->attributes->get('tenant_id'); return $this->success([ 'ticket_types' => TicketType::orderBy('name')->get(['id','code','name','need_approval','require_photo','require_material']), 'customers' => Customer::when(! $request->user()->isMasterAdmin() || $tenantId, fn ($q) => $q->where('tenant_id', $tenantId))->orderBy('name')->limit(500)->get(['id','customer_code','name','tenant_id']), 'technicians' => User::whereHas('userTenants', fn ($q) => $q->where('tenant_id', $tenantId))->orderBy('name')->get(['id','name','email']), 'installation_setting' => $tenantId ? \App\Models\TenantInstallationSetting::firstOrCreate(['tenant_id' => $tenantId], ['activation_mode' => 'list_order']) : null, 'package_profiles'=>\App\Models\NasPackageProfile::where('tenant_id',$tenantId)->where('status','active')->orderBy('name')->get(['id','name']), 'nas_mikrotiks'=>\App\Models\NasMikrotik::where('tenant_id',$tenantId)->where('status','active')->orderBy('name')->get(['id','name']), 'billing_profiles'=>\App\Models\BillingProfile::where('tenant_id',$tenantId)->where('status','active')->orderBy('name')->get(['id','name']), 'topology_odps'=>\App\Models\TopologyNode::where('tenant_id',$tenantId)->whereHas('deviceType',fn($q)=>$q->whereRaw('UPPER(code) = ?',['ODP']))->orderBy('name')->get(['id','code','name']), ], 'Pilihan ticket berhasil diambil'); } public function storeMaterial(Request $request, Ticket $ticket) { $this->authorizeTenant($request,$ticket); $data=$request->validate(['technician_material_id'=>['nullable','required_without:technician_consumable_id','integer','exists:technician_materials,id'],'technician_consumable_id'=>['nullable','required_without:technician_material_id','integer','exists:technician_consumables,id'],'qty'=>['required','numeric','gt:0'],'notes'=>['nullable','string','max:1000']]); abort_if(!empty($data['technician_material_id'])&&!empty($data['technician_consumable_id']),422,'Pilih satu material saja.'); $material=\Illuminate\Support\Facades\DB::transaction(function()use($data,$ticket,$request){ if(!empty($data['technician_material_id'])){$source=\App\Models\TechnicianMaterial::whereKey($data['technician_material_id'])->where('user_id',$request->user()->id)->where('status','assigned')->lockForUpdate()->firstOrFail();$qty=1;$source->update(['status'=>'used']);$name=$source->material_name;} else{$source=\App\Models\TechnicianConsumable::whereKey($data['technician_consumable_id'])->where('user_id',$request->user()->id)->where('status','assigned')->lockForUpdate()->firstOrFail();$qty=(float)$data['qty'];abort_if((float)$source->qty_remaining<$qty,422,'Jumlah material yang dibawa tidak mencukupi.');$source->update(['qty_used'=>(float)$source->qty_used+$qty,'qty_remaining'=>(float)$source->qty_remaining-$qty]);$name=$source->material_name;} \App\Models\MaterialMovement::create(['barcode_id'=>$source->barcode_id,'user_id'=>$request->user()->id,'from_user_id'=>$request->user()->id,'movement_type'=>'use','ticket_id'=>$ticket->id,'qty'=>$qty,'description'=>$data['notes']??null,'created_by'=>$request->user()->id,'created_at'=>now()]); $warehouseBarcode=\App\Models\ItemBarcode::where('barcode',$source->barcode_id)->first(); if($warehouseBarcode)\App\Models\ItemMovement::create(['tenant_id'=>$ticket->tenant_id,'warehouse_item_id'=>$warehouseBarcode->warehouse_item_id,'item_barcode_id'=>$warehouseBarcode->id,'movement_type'=>'used_on_ticket','qty'=>$qty,'reference_type'=>'ticket','reference_id'=>$ticket->id,'from_user_id'=>$request->user()->id,'notes'=>$data['notes']??null,'created_by'=>$request->user()->id]); return $ticket->materials()->create([...$data,'material_name'=>$name,'qty'=>$qty,'used_by'=>$request->user()->id,'created_at'=>now()]); }); \App\Models\TicketLog::create(['ticket_id'=>$ticket->id,'user_id'=>$request->user()->id,'activity'=>'Material Digunakan','notes'=>$material->material_name.' x '.$material->qty,'created_at'=>now()]); return $this->created($material->load('user:id,name'),'Material ticket berhasil ditambahkan'); } public function materialOptions(Request $request, Ticket $ticket) { $this->authorizeTenant($request,$ticket);$userId=$request->user()->id; return $this->success(['serialized'=>\App\Models\TechnicianMaterial::where('user_id',$userId)->where('status','assigned')->get(['id','barcode_id','material_name','serial_number']),'consumables'=>\App\Models\TechnicianConsumable::where('user_id',$userId)->where('status','assigned')->where('qty_remaining','>',0)->get(['id','barcode_id','material_name','unit','qty_remaining'])],'Material yang dibawa berhasil diambil'); } public function installationPorts(Request $request,Ticket $ticket,int $node,\App\Services\Customer\CustomerService $customers){$this->authorizeTenant($request,$ticket);return $this->success($customers->availableOdpPorts($node,$request->user(),$ticket->tenant_id,$ticket->customer_id),'Port ODP berhasil diambil');} public function destroyMaterial(Request $request, Ticket $ticket, TicketMaterial $material) { $this->authorizeTenant($request,$ticket);abort_unless((int)$material->ticket_id===(int)$ticket->id,404); abort_if(in_array($ticket->status,['resolved','closed'],true),422,'Material ticket yang sudah selesai tidak dapat diubah.'); \Illuminate\Support\Facades\DB::transaction(function()use($request,$ticket,$material){ if($material->technician_material_id){$source=\App\Models\TechnicianMaterial::lockForUpdate()->find($material->technician_material_id);if($source)$source->update(['status'=>'assigned']);} else{$source=\App\Models\TechnicianConsumable::lockForUpdate()->find($material->technician_consumable_id);if($source)$source->update(['qty_used'=>max(0,(float)$source->qty_used-(float)$material->qty),'qty_remaining'=>(float)$source->qty_remaining+(float)$material->qty,'status'=>'assigned']);} if(isset($source)&&$source){\App\Models\MaterialMovement::create(['barcode_id'=>$source->barcode_id,'user_id'=>$request->user()->id,'to_user_id'=>$request->user()->id,'movement_type'=>'return','ticket_id'=>$ticket->id,'qty'=>$material->qty,'description'=>'Pembatalan pemakaian material pada ticket','created_by'=>$request->user()->id,'created_at'=>now()]);$warehouseBarcode=\App\Models\ItemBarcode::where('barcode',$source->barcode_id)->first();if($warehouseBarcode)\App\Models\ItemMovement::create(['tenant_id'=>$ticket->tenant_id,'warehouse_item_id'=>$warehouseBarcode->warehouse_item_id,'item_barcode_id'=>$warehouseBarcode->id,'movement_type'=>'ticket_usage_cancelled','qty'=>$material->qty,'reference_type'=>'ticket','reference_id'=>$ticket->id,'to_user_id'=>$request->user()->id,'notes'=>'Material dikembalikan ke saldo teknisi','created_by'=>$request->user()->id]);} $material->delete(); }); return $this->deleted('Material ticket berhasil dihapus dan dikembalikan ke daftar bawaan user'); } private function authorizeTenant(Request $request, Ticket $ticket): void { abort_unless($request->user()->isMasterAdmin() || (int) $ticket->tenant_id === (int) $request->attributes->get('tenant_id'), 403); } }