Files
accounting_dev_v2/application/libraries/TechnicianEquipmentService.php
T
2026-09-11 16:03:00 +07:00

251 lines
70 KiB
PHP

<?php
defined('BASEPATH') OR exit('No direct script access allowed');
require_once APPPATH.'exceptions/BusinessException.php';
/** Operational custody subledger only. This service never writes journals,
* stock_logs, inventory_ledger, prices, costs, or item master records. */
class TechnicianEquipmentService
{
private $CI;
public function __construct(){ $this->CI=&get_instance();$this->CI->load->library(array('CompanyContext','NumberingService')); }
private function company(){return(int)$this->CI->companycontext->id();}
private function now(){return date('Y-m-d H:i:s');}
private function qty($value){$qty=round((float)$value,4);if($qty<=0)throw new BusinessException('Jumlah wajib lebih dari 0.');return$qty;}
private function date($value){$date=trim((string)$value);$d=DateTime::createFromFormat('Y-m-d',$date);if(!$d||$d->format('Y-m-d')!==$date)throw new BusinessException('Tanggal transaksi tidak valid.');return$date;}
private function key(array$row){return hash('sha256',implode('|',array($this->company(),$row['subject_type'],$row['subject_id'],$row['custodian_type'],(int)($row['custodian_id']??0),(int)($row['customer_id']??0),$row['usage_status'],$row['condition_status'])));}
private function user($id){$r=$this->CI->db->where(array('id'=>(int)$id,'company_id'=>$this->company(),'is_active'=>1))->get('k_employees')->row();if(!$r)throw new BusinessException('Karyawan/teknisi tidak ditemukan pada perusahaan aktif.');return$r;}
private function customer($id){$r=$this->CI->db->where(array('id'=>(int)$id,'company_id'=>$this->company()))->get('customers')->row();if(!$r)throw new BusinessException('Customer tidak ditemukan pada perusahaan aktif.');return$r;}
private function warehouse($id){$r=$this->CI->db->where(array('id'=>(int)$id,'company_id'=>$this->company(),'is_active'=>1))->get('warehouses')->row();if(!$r)throw new BusinessException('Gudang tidak ditemukan pada perusahaan aktif.');return$r;}
public function dashboard()
{
$company=$this->company();$summary=array('carried'=>0,'installed'=>0,'inspection'=>0,'repair'=>0,'damaged'=>0,'lost'=>0);
$rows=$this->CI->db->select("SUM(CASE WHEN custodian_type='technician' THEN qty ELSE 0 END) carried,SUM(CASE WHEN usage_status='installed' THEN qty ELSE 0 END) installed,SUM(CASE WHEN condition_status='inspection_required' THEN qty ELSE 0 END) inspection,SUM(CASE WHEN condition_status IN('repair_required','under_repair') THEN qty ELSE 0 END) repair,SUM(CASE WHEN condition_status='damaged' THEN qty ELSE 0 END) damaged,SUM(CASE WHEN condition_status='lost' THEN qty ELSE 0 END) lost",false)->where('company_id',$company)->get('technician_custody_allocations')->row();
if($rows)foreach($summary as$key=>$value)$summary[$key]=(float)($rows->$key??0);$inspectionStats=$this->inspectionQueueStats();$summary['inspection']=(float)$inspectionStats['qty'];
$technicians=$this->CI->db->select("e.id,e.employee_code,e.full_name,d.department_name,COALESCE(SUM(CASE WHEN a.custodian_type='technician' THEN a.qty ELSE 0 END),0) carried_qty,COALESCE(SUM(CASE WHEN a.usage_status='installed' THEN a.qty ELSE 0 END),0) installed_qty,COUNT(DISTINCT a.subject_type,a.subject_id) subject_count",false)->from('k_employees e')->join('k_departments d','d.id=e.department_id','left')->join('technician_custody_allocations a',"a.custodian_type='technician' AND a.custodian_id=e.id AND a.company_id=e.company_id",'left')->where(array('e.company_id'=>$company,'e.is_active'=>1))->group_start()->where_in('LOWER(d.department_name)',array('teknisi','it','admin','gudang'))->or_where('a.id IS NOT NULL',null,false)->group_end()->group_by('e.id')->order_by('e.full_name')->get()->result();
return array('summary'=>$summary,'technicians'=>$technicians,'inspection_count'=>(int)$inspectionStats['row_count']);
}
public function search($query,$mode='all',$technicianId=0,$limit=20)
{
$query=trim((string)$query);if($query==='')return array();$mode=in_array($mode,array('all','handover','transfer','return','customer'),true)?$mode:'all';$limit=max(1,min(30,(int)$limit));$like='%'.$this->CI->db->escape_like_str($query).'%';$company=$this->company();$rows=array();
if(in_array($mode,array('all','handover'),true)){
$sql="SELECT b.id subject_id,NULL allocation_id,'company_barcode' subject_type,b.barcode lookup_code,b.barcode,b.serial_number,i.kode_detail,i.nama_barang,COALESCE(k.tracking_type,'QTY') tracking_type,(b.qty_sisa-COALESCE(b.reserved_qty,0)-COALESCE(b.return_reserved_qty,0)) available_qty,'company' ownership_type,b.warehouse_id,w.nama warehouse_name,NULL technician_id,NULL technician_name,NULL customer_name FROM item_barcodes b JOIN items i ON i.id=b.item_id LEFT JOIN kode_barang k ON k.id=i.kode_id LEFT JOIN warehouses w ON w.id=b.warehouse_id WHERE i.company_id=? AND (b.barcode LIKE ? OR b.serial_number LIKE ? OR i.kode_detail LIKE ? OR i.nama_barang LIKE ?)";
$params=array($company,$like,$like,$like,$like);if($mode==='handover')$sql.=" AND b.status='available' AND b.qty_sisa-COALESCE(b.reserved_qty,0)-COALESCE(b.return_reserved_qty,0)>.0001";if($mode==='all'&&(int)$technicianId>0){$employee=(int)$technicianId;$sql.=" AND (EXISTS(SELECT 1 FROM technician_custody_allocations sa WHERE sa.company_id=i.company_id AND sa.subject_type='company_barcode' AND sa.subject_id=b.id AND sa.custodian_type='technician' AND sa.custodian_id={$employee}) OR EXISTS(SELECT 1 FROM technician_equipment_document_lines sl JOIN technician_equipment_documents sd ON sd.id=sl.document_id WHERE sl.company_id=i.company_id AND sl.subject_type='company_barcode' AND sl.subject_id=b.id AND (sd.technician_id={$employee} OR sd.target_technician_id={$employee})))";}$sql.=" ORDER BY CASE WHEN b.barcode=? OR b.serial_number=? THEN 0 ELSE 1 END,i.nama_barang,b.id LIMIT ".$limit;$params[]=$query;$params[]=$query;$rows=$this->CI->db->query($sql,$params)->result_array();
}
if(in_array($mode,array('transfer','return'),true)){
$sql="SELECT a.subject_id,a.id allocation_id,a.subject_type,a.barcode_snapshot lookup_code,a.barcode_snapshot barcode,COALESCE(b.serial_number,r.serial_number) serial_number,i.kode_detail,CASE WHEN a.subject_type='company_barcode' THEN i.nama_barang ELSE r.equipment_name END nama_barang,CASE WHEN a.subject_type='company_barcode' THEN COALESCE(k.tracking_type,'QTY') ELSE COALESCE(r.tracking_type,'UNIT') END tracking_type,a.qty available_qty,a.ownership_type,NULL warehouse_id,NULL warehouse_name,a.custodian_id technician_id,e.full_name technician_name,c.nama customer_name FROM technician_custody_allocations a LEFT JOIN items i ON i.id=a.item_id LEFT JOIN kode_barang k ON k.id=i.kode_id LEFT JOIN item_barcodes b ON b.id=a.barcode_id LEFT JOIN customer_equipment_registry r ON r.id=a.customer_equipment_id LEFT JOIN k_employees e ON e.id=a.custodian_id LEFT JOIN customers c ON c.id=a.customer_id WHERE a.company_id=? AND a.custodian_type='technician' AND a.qty>.0001";$params=array($company);if((int)$technicianId>0){$sql.=' AND a.custodian_id=?';$params[]=(int)$technicianId;}$sql.=" AND (a.barcode_snapshot LIKE ? OR b.serial_number LIKE ? OR r.serial_number LIKE ? OR i.kode_detail LIKE ? OR i.nama_barang LIKE ? OR r.equipment_name LIKE ?) ORDER BY CASE WHEN a.barcode_snapshot=? OR b.serial_number=? OR r.serial_number=? THEN 0 ELSE 1 END,nama_barang,a.id LIMIT ".$limit;array_push($params,$like,$like,$like,$like,$like,$like,$query,$query,$query);$rows=$this->CI->db->query($sql,$params)->result_array();
}
if(in_array($mode,array('all','customer'),true)){
$left=max(1,$limit-count($rows));$sql="SELECT r.id subject_id,NULL allocation_id,'customer_equipment' subject_type,r.external_barcode lookup_code,r.external_barcode barcode,r.serial_number,NULL kode_detail,r.equipment_name nama_barang,r.tracking_type,r.total_qty available_qty,r.ownership_type,NULL warehouse_id,NULL warehouse_name,NULL technician_id,NULL technician_name,c.nama customer_name FROM customer_equipment_registry r LEFT JOIN customers c ON c.id=r.owner_customer_id WHERE r.company_id=? AND (r.external_barcode LIKE ? OR r.serial_number LIKE ? OR r.equipment_name LIKE ? OR r.brand LIKE ? OR r.model LIKE ?)";if((int)$technicianId>0){$employee=(int)$technicianId;$sql.=" AND (EXISTS(SELECT 1 FROM technician_custody_allocations sa WHERE sa.company_id=r.company_id AND sa.subject_type='customer_equipment' AND sa.subject_id=r.id AND sa.custodian_type='technician' AND sa.custodian_id={$employee}) OR EXISTS(SELECT 1 FROM technician_equipment_document_lines sl JOIN technician_equipment_documents sd ON sd.id=sl.document_id WHERE sl.company_id=r.company_id AND sl.subject_type='customer_equipment' AND sl.subject_id=r.id AND (sd.technician_id={$employee} OR sd.target_technician_id={$employee})))";}$sql.=" ORDER BY CASE WHEN r.external_barcode=? OR r.serial_number=? THEN 0 ELSE 1 END,r.equipment_name,r.id LIMIT ".$left;$external=$this->CI->db->query($sql,array($company,$like,$like,$like,$like,$like,$query,$query))->result_array();$rows=array_merge($rows,$external);
}
foreach($rows as&$row){$row['tracking_type']=strtoupper((string)$row['tracking_type'])==='UNIT'?'UNIT':'QTY';$row['display_name']=trim((string)($row['kode_detail']??'')).(($row['kode_detail']??'')!==null&&($row['kode_detail']??'')!==''?' - ':'').($row['nama_barang']??'-');$row['available_qty']=(float)$row['available_qty'];}unset($row);usort($rows,function($a,$b)use($query){$aExact=strcasecmp((string)$a['barcode'],$query)===0||strcasecmp((string)($a['serial_number']??''),$query)===0;$bExact=strcasecmp((string)$b['barcode'],$query)===0||strcasecmp((string)($b['serial_number']??''),$query)===0;return$aExact===$bExact?strcasecmp((string)$a['display_name'],(string)$b['display_name']):($aExact?-1:1);});return array_slice($rows,0,$limit);
}
public function technicianHoldings($technicianId)
{
$technician=$this->user($technicianId);
$rows=$this->CI->db->query("SELECT a.subject_id,a.id allocation_id,a.subject_type,a.barcode_snapshot lookup_code,a.barcode_snapshot barcode,COALESCE(b.serial_number,r.serial_number) serial_number,i.kode_detail,CASE WHEN a.subject_type='company_barcode' THEN i.nama_barang ELSE r.equipment_name END nama_barang,CASE WHEN a.subject_type='company_barcode' THEN COALESCE(k.tracking_type,'QTY') ELSE COALESCE(r.tracking_type,'UNIT') END tracking_type,a.qty available_qty,a.ownership_type,a.usage_status,a.condition_status,a.customer_id,c.nama customer_name,a.custodian_id technician_id,e.full_name technician_name FROM technician_custody_allocations a LEFT JOIN items i ON i.id=a.item_id LEFT JOIN kode_barang k ON k.id=i.kode_id LEFT JOIN item_barcodes b ON b.id=a.barcode_id LEFT JOIN customer_equipment_registry r ON r.id=a.customer_equipment_id LEFT JOIN k_employees e ON e.id=a.custodian_id LEFT JOIN customers c ON c.id=a.customer_id WHERE a.company_id=? AND a.custodian_type='technician' AND a.custodian_id=? AND a.qty>.0001 ORDER BY CASE a.usage_status WHEN 'carried' THEN 0 WHEN 'removed' THEN 1 ELSE 2 END,nama_barang,a.barcode_snapshot,a.id",array($this->company(),(int)$technician->id))->result_array();
foreach($rows as&$row){$row['tracking_type']=strtoupper((string)$row['tracking_type'])==='UNIT'?'UNIT':'QTY';$row['display_name']=trim((string)($row['kode_detail']??'')).(($row['kode_detail']??'')!==null&&($row['kode_detail']??'')!==''?' - ':'').($row['nama_barang']??'-');$row['available_qty']=(float)$row['available_qty'];}unset($row);
return$rows;
}
public function scan($code)
{
$code=trim((string)$code);if($code==='')throw new BusinessException('Barcode atau serial wajib diisi.');$company=$this->company();
$internal=$this->CI->db->select("b.id subject_id,b.id barcode_id,b.item_id,b.barcode,b.serial_number,b.warehouse_id,b.qty_sisa,b.reserved_qty,b.return_reserved_qty,b.status barcode_status,b.version,i.kode_detail,i.nama_barang,k.tracking_type,k.unit,w.nama warehouse_name,'company_barcode' subject_type,'company' ownership_type",false)->from('item_barcodes b')->join('items i','i.id=b.item_id')->join('kode_barang k','k.id=i.kode_id','left')->join('warehouses w','w.id=b.warehouse_id','left')->where('i.company_id',$company)->group_start()->where('b.barcode',$code)->or_where('b.serial_number',$code)->group_end()->limit(1)->get()->row_array();
if($internal){$internal['tracking_type']=strtoupper((string)$internal['tracking_type'])==='UNIT'?'UNIT':'QTY';$internal['warehouse_available']=max(0,(float)$internal['qty_sisa']-(float)$internal['reserved_qty']-(float)$internal['return_reserved_qty']);$internal['display_name']=$internal['kode_detail'].' - '.$internal['nama_barang'];$internal['allocations']=$this->allocations('company_barcode',(int)$internal['subject_id']);$internal['actions']=$this->actions($internal);return$internal;}
$external=$this->CI->db->select("r.*,r.id subject_id,r.external_barcode barcode,'customer_equipment' subject_type")->from('customer_equipment_registry r')->where('r.company_id',$company)->group_start()->where('r.external_barcode',$code)->or_where('r.serial_number',$code)->group_end()->limit(1)->get()->row_array();
if(!$external)throw new BusinessException('Barcode atau serial tidak ditemukan.');$external['display_name']=$external['equipment_name'].($external['brand']?' - '.$external['brand']:'');$external['warehouse_available']=0;$external['allocations']=$this->allocations('customer_equipment',(int)$external['subject_id']);$external['actions']=$this->actions($external);return$external;
}
public function allocations($subjectType=null,$subjectId=null,$technicianId=null)
{
$this->CI->db->select("a.*,i.kode_detail,i.nama_barang,b.serial_number,r.equipment_name,r.brand,r.model,r.serial_number external_serial,c.nama customer_name,e.full_name technician_name,w.nama warehouse_name,CASE WHEN a.subject_type='company_barcode' THEN COALESCE(k.tracking_type,'QTY') ELSE COALESCE(r.tracking_type,'UNIT') END tracking_type,CASE WHEN a.subject_type='company_barcode' THEN CONCAT(COALESCE(i.kode_detail,'-'),' - ',COALESCE(i.nama_barang,'-')) ELSE CONCAT(COALESCE(r.equipment_name,'-'),IF(r.brand IS NULL OR r.brand='','',CONCAT(' - ',r.brand))) END display_name",false)->from('technician_custody_allocations a')->join('items i','i.id=a.item_id','left')->join('kode_barang k','k.id=i.kode_id','left')->join('item_barcodes b','b.id=a.barcode_id','left')->join('customer_equipment_registry r','r.id=a.customer_equipment_id','left')->join('customers c','c.id=a.customer_id','left')->join('k_employees e',"e.id=a.custodian_id AND a.custodian_type='technician'",'left')->join('warehouses w',"w.id=a.custodian_id AND a.custodian_type IN('warehouse','quarantine','service_center')",'left')->where('a.company_id',$this->company())->where('a.qty >',0);
if($subjectType)$this->CI->db->where(array('a.subject_type'=>$subjectType,'a.subject_id'=>(int)$subjectId));if($technicianId)$this->CI->db->group_start()->where(array('a.custodian_type'=>'technician','a.custodian_id'=>(int)$technicianId))->or_where(array('a.usage_status'=>'installed','a.custodian_type'=>'customer'))->where("EXISTS(SELECT 1 FROM technician_equipment_document_lines dl JOIN technician_equipment_documents dh ON dh.id=dl.document_id WHERE dl.subject_type=a.subject_type AND dl.subject_id=a.subject_id AND dh.technician_id=".(int)$technicianId.")",null,false)->group_end();
return$this->CI->db->order_by('a.updated_at','DESC')->get()->result_array();
}
private function actions(array$subject)
{
$actions=array();if($subject['subject_type']==='company_barcode'&&(float)$subject['warehouse_available']>0)$actions[]='handover';
foreach($subject['allocations']as$a){if(in_array($a['custodian_type'],array('quarantine','service_center'),true)&&in_array($a['condition_status'],array('inspection_required','repair_required','under_repair','damaged'),true)){$actions[]='inspect';continue;}if($a['custodian_type']==='technician'){if(in_array($a['usage_status'],array('carried','removed','returned'),true)){$actions[]='install';$actions[]='transfer';$actions[]='return';if($subject['subject_type']==='customer_equipment')$actions[]='return_customer';}}elseif($a['custodian_type']==='customer'&&$a['usage_status']==='installed'){$actions[]='remove';$actions[]='replacement';}}
return array_values(array_unique($actions));
}
public function registerExternal(array$data,$userId)
{
$this->CI->db->trans_begin();try{$row=$this->createExternal($data,$userId);if($this->CI->db->trans_status()===false)throw new RuntimeException('Register barang customer gagal disimpan.');$this->CI->db->trans_commit();return$row;}catch(Throwable$e){$this->CI->db->trans_rollback();throw$e;}
}
private function createExternal(array$data,$userId)
{
$customer=$this->customer($data['owner_customer_id']??0);$barcode=strtoupper(trim((string)($data['external_barcode']??'')));$name=trim((string)($data['equipment_name']??''));if($barcode===''||$name==='')throw new BusinessException('Nama barang dan barcode eksternal wajib diisi.');$tracking=strtoupper((string)($data['tracking_type']??'UNIT'))==='QTY'?'QTY':'UNIT';$qty=$tracking==='UNIT'?1:$this->qty($data['qty']??0);$ownership=in_array(($data['ownership_type']??''),array('customer','transferred_non_inventory','unknown'),true)?$data['ownership_type']:'customer';
$duplicate=$this->CI->db->where(array('company_id'=>$this->company(),'external_barcode'=>$barcode))->get('customer_equipment_registry')->row();if($duplicate)throw new BusinessException('Barcode barang customer sudah terdaftar.');$sourceDate=trim((string)($data['source_date']??date('Y-m-d')));$sourceDate=$this->date($sourceDate);$row=array('company_id'=>$this->company(),'owner_customer_id'=>$customer->id,'current_customer_id'=>$customer->id,'equipment_name'=>$name,'category'=>trim((string)($data['category']??''))?:null,'brand'=>trim((string)($data['brand']??''))?:null,'model'=>trim((string)($data['model']??''))?:null,'external_barcode'=>$barcode,'serial_number'=>trim((string)($data['serial_number']??''))?:null,'description'=>trim((string)($data['description']??''))?:null,'source_date'=>$sourceDate,'source_document_no'=>trim((string)($data['source_document_no']??''))?:null,'source_reason'=>trim((string)($data['source_reason']??''))?:null,'ownership_type'=>$ownership,'deployment_allowed'=>!empty($data['deployment_allowed'])?1:0,'authorization_reference'=>trim((string)($data['authorization_reference']??''))?:null,'tracking_type'=>$tracking,'total_qty'=>$qty,'custody_type'=>'customer','custody_id'=>$customer->id,'usage_status'=>'installed','condition_status'=>'good','lifecycle_status'=>'active','version'=>1,'registered_by'=>(int)$userId,'registered_at'=>$this->now());if($ownership==='customer'&&!empty($data['deployment_allowed'])&&empty($row['authorization_reference']))throw new BusinessException('Referensi otorisasi wajib diisi jika barang customer boleh digunakan kembali.');if($ownership==='transferred_non_inventory'&&(!$row['source_document_no']||!$row['source_reason']))throw new BusinessException('Dokumen sumber dan alasan penyerahan wajib diisi.');$this->CI->db->insert('customer_equipment_registry',$row);$id=(int)$this->CI->db->insert_id();$subject=array('subject_type'=>'customer_equipment','subject_id'=>$id,'item_id'=>null,'barcode_id'=>null,'customer_equipment_id'=>$id,'barcode_snapshot'=>$barcode,'ownership_type'=>$ownership);$this->addBalance($subject,'customer',$customer->id,$customer->id,'installed','good',$qty,null);return$this->CI->db->get_where('customer_equipment_registry',array('id'=>$id))->row_array();
}
public function process(array$data,$userId)
{
$action=(string)($data['action']??'');if(!in_array($action,array('handover','install','remove','transfer','return','return_customer'),true))throw new BusinessException('Tindakan peralatan tidak valid.');$date=$this->date($data['date']??'');$notes=trim((string)($data['notes']??''));if($notes==='')throw new BusinessException('Keterangan transaksi wajib diisi.');$key=trim((string)($data['idempotency_key']??''));if($key==='')throw new BusinessException('Kunci transaksi tidak tersedia. Muat ulang halaman.');
$this->CI->db->trans_begin();
try{
if($existing=$this->CI->db->get_where('technician_equipment_documents',array('company_id'=>$this->company(),'idempotency_key'=>$key))->row()){$this->CI->db->trans_commit();return array('document_id'=>(int)$existing->id,'document_no'=>$existing->document_no,'duplicate'=>true);}
$doc=$this->createDocument($action,$date,$data,$userId,$key);
if($action==='handover')$this->handover($doc,$data,$userId);
else{
$source=$this->lockAllocation($data['allocation_id']??0);$qty=$this->validatedQty($source,$data['qty']??0);
if($action==='install'){if($source['custodian_type']!=='technician'||!in_array($source['usage_status'],array('carried','removed','returned'),true))throw new BusinessException('Hanya barang yang sedang dibawa teknisi yang dapat dipasang.');$customer=$this->customer($data['customer_id']??0);$this->assertReusableExternal($source,$customer->id);$this->move($doc,$source,'customer',$customer->id,$customer->id,'installed','good',$qty,'installation',$notes);}
elseif($action==='remove'){if($source['custodian_type']!=='customer'||$source['usage_status']!=='installed')throw new BusinessException('Hanya barang terpasang yang dapat dilepas.');$technician=$this->user($data['technician_id']??$doc['technician_id']);$condition=in_array(($data['condition_status']??''),array('good','inspection_required','repair_required','damaged'),true)?$data['condition_status']:'inspection_required';$this->move($doc,$source,'technician',$technician->id,$source['customer_id'],'removed',$condition,$qty,'uninstallation',$notes);}
elseif($action==='transfer'){$target=$this->user($data['target_technician_id']??0);if($source['custodian_type']!=='technician'||(int)$source['custodian_id']===(int)$target->id)throw new BusinessException('Teknisi tujuan transfer tidak valid.');$this->move($doc,$source,'technician',$target->id,$source['customer_id'],'carried',$source['condition_status'],$qty,'transfer',$notes);}
elseif($action==='return')$this->returnFromTechnician($doc,$source,$qty,$data,$notes);
elseif($action==='return_customer'){if($source['subject_type']!=='customer_equipment'||$source['custodian_type']!=='technician')throw new BusinessException('Tindakan ini hanya untuk barang customer yang sedang dibawa teknisi.');$customer=$this->customer($data['customer_id']??$source['customer_id']);$this->move($doc,$source,'customer',$customer->id,$customer->id,'returned',$source['condition_status'],$qty,'transfer',$notes);}
}
if($this->CI->db->trans_status()===false)throw new RuntimeException('Transaksi peralatan gagal disimpan.');$this->CI->db->trans_commit();return array('document_id'=>$doc['id'],'document_no'=>$doc['document_no']);
}catch(Throwable$e){$this->CI->db->trans_rollback();throw$e;}
}
public function processBatch(array$data,$userId)
{
$action=(string)($data['action']??'');if(!in_array($action,array('handover','transfer','return'),true))throw new BusinessException('Jenis perpindahan batch tidak valid.');$date=$this->date($data['date']??'');$notes=trim((string)($data['notes']??''));if($notes==='')$notes=$action==='handover'?'Serah terima barang batch':($action==='transfer'?'Transfer barang antar teknisi':'Pengembalian barang batch ke gudang');$key=trim((string)($data['idempotency_key']??''));$items=$data['items']??array();if($key===''||!is_array($items)||count($items)<1)throw new BusinessException('Daftar barang dan kunci transaksi wajib tersedia.');if(count($items)>200)throw new BusinessException('Maksimal 200 baris dalam satu transaksi batch.');
$seen=array();foreach($items as$row){$identity=$action==='handover'?'S'.(int)($row['subject_id']??0):'A'.(int)($row['allocation_id']??0);if(isset($seen[$identity]))throw new BusinessException('Barang yang sama tercantum lebih dari satu kali. Gabungkan jumlahnya pada satu baris.');$seen[$identity]=true;}
$technician=null;$target=null;$warehouse=null;if($action==='handover')$technician=$this->user($data['technician_id']??0);elseif($action==='transfer'){$technician=$this->user($data['technician_id']??0);$target=$this->user($data['target_technician_id']??0);if((int)$technician->id===(int)$target->id)throw new BusinessException('Teknisi asal dan tujuan harus berbeda.');}else{$technician=$this->user($data['technician_id']??0);if(!empty($data['warehouse_id']))$warehouse=$this->warehouse($data['warehouse_id']);}
$this->CI->db->trans_begin();try{if($existing=$this->CI->db->get_where('technician_equipment_documents',array('company_id'=>$this->company(),'idempotency_key'=>$key))->row()){$this->CI->db->trans_commit();return array('document_id'=>(int)$existing->id,'document_no'=>$existing->document_no,'line_count'=>$this->CI->db->where('document_id',$existing->id)->count_all_results('technician_equipment_document_lines'),'duplicate'=>true);}$header=$data;$header['technician_id']=$technician->id;if($target)$header['target_technician_id']=$target->id;if($warehouse)$header['warehouse_id']=$warehouse->id;$header['notes']=$notes;$doc=$this->createDocument($action,$date,$header,$userId,$key);
foreach($items as$index=>$row){$rowNotes=trim((string)($row['notes']??''));$lineNotes=$notes.($rowNotes!==''?' | '.$rowNotes:'');if($action==='handover'){$payload=array_merge($header,array('subject_id'=>(int)($row['subject_id']??0),'qty'=>$row['qty']??0,'notes'=>$lineNotes));$this->handover($doc,$payload,$userId);continue;}$source=$this->lockAllocation($row['allocation_id']??0);if($source['custodian_type']!=='technician'||(int)$source['custodian_id']!==(int)$technician->id)throw new BusinessException('Baris '.($index+1).' tidak lagi berada pada teknisi asal.');$qty=$this->validatedQty($source,$row['qty']??0);if($action==='transfer')$this->move($doc,$source,'technician',$target->id,$source['customer_id'],'carried',$source['condition_status'],$qty,'transfer',$lineNotes);else$this->returnFromTechnician($doc,$source,$qty,$data,$lineNotes,$warehouse);}
if($this->CI->db->trans_status()===false)throw new RuntimeException('Transaksi batch gagal disimpan.');$this->CI->db->trans_commit();return array('document_id'=>$doc['id'],'document_no'=>$doc['document_no'],'line_count'=>count($items));}catch(Throwable$e){$this->CI->db->trans_rollback();throw$e;}
}
private function returnFromTechnician(array$doc,array$source,$qty,array$data,$notes,$warehouse=null)
{
if($source['custodian_type']!=='technician')throw new BusinessException('Barang harus berada pada teknisi sebelum dikembalikan.');
if($source['subject_type']==='company_barcode'||$source['ownership_type']==='transferred_non_inventory'){
if(!$warehouse)$warehouse=$this->warehouse($data['warehouse_id']??0);
if($source['subject_type']==='company_barcode')$this->move($doc,$source,'quarantine',$warehouse->id,$source['customer_id'],'returned','inspection_required',$qty,'transfer',$notes);
else$this->move($doc,$source,'warehouse',$warehouse->id,$source['customer_id'],'returned',$source['condition_status'],$qty,'transfer',$notes,false);
return;
}
$customerId=(int)($source['customer_id']??0);if($customerId<1&&$source['subject_type']==='customer_equipment'){$registry=$this->CI->db->select('owner_customer_id')->get_where('customer_equipment_registry',array('id'=>(int)$source['subject_id'],'company_id'=>$this->company()))->row();$customerId=(int)($registry->owner_customer_id??0);}
$customer=$this->customer($customerId);$this->move($doc,$source,'customer',$customer->id,$customer->id,'returned',$source['condition_status'],$qty,'transfer',$notes);
}
private function handover(array$doc,array$data,$userId)
{
$barcodeId=(int)($data['subject_id']??0);$barcode=$this->CI->db->query("SELECT b.*,i.company_id,i.nama_barang,COALESCE(k.tracking_type,'QTY') tracking_type FROM item_barcodes b JOIN items i ON i.id=b.item_id LEFT JOIN kode_barang k ON k.id=i.kode_id WHERE b.id=? AND i.company_id=? FOR UPDATE",array($barcodeId,$this->company()))->row();if(!$barcode)throw new BusinessException('Barcode gudang tidak ditemukan.');if((string)$barcode->status!=='available')throw new BusinessException('Barcode belum tersedia untuk diserahkan dari gudang.');$tracking=strtoupper((string)$barcode->tracking_type)==='UNIT'?'UNIT':'QTY';$qty=$tracking==='UNIT'?1:$this->qty($data['qty']??0);$available=(float)$barcode->qty_sisa-(float)$barcode->reserved_qty-(float)$barcode->return_reserved_qty;if($qty>$available+.0001)throw new BusinessException('Jumlah melebihi saldo barcode yang tersedia di gudang.');$technician=$this->user($data['technician_id']??0);$subject=array('subject_type'=>'company_barcode','subject_id'=>$barcode->id,'item_id'=>$barcode->item_id,'barcode_id'=>$barcode->id,'customer_equipment_id'=>null,'barcode_snapshot'=>$barcode->barcode,'ownership_type'=>'company');$this->addBalance($subject,'technician',$technician->id,null,'carried','good',$qty,$doc['id']);$reserved=round((float)$barcode->reserved_qty+$qty,4);$left=(float)$barcode->qty_sisa-$reserved-(float)$barcode->return_reserved_qty;$status=$left>.0001?'available':'installed';$this->CI->db->where(array('id'=>$barcode->id,'version'=>$barcode->version))->update('item_barcodes',array('reserved_qty'=>$reserved,'status'=>$status,'version'=>(int)$barcode->version+1));if($this->CI->db->affected_rows()!==1)throw new BusinessException('Barcode sedang diproses pengguna lain.');$notes=trim((string)$data['notes']);$movement=$this->movement($subject,$qty,'warehouse',$barcode->warehouse_id,'technician',$technician->id,'transfer',$doc['id'],'[AUTO] Serah terima '.$doc['document_no'].' | '.$notes);$this->line($doc,$subject,$tracking,'handover','warehouse',$barcode->warehouse_id,'technician',$technician->id,null,'warehouse','carried','good','good',$qty,$movement,$notes);
}
public function replacement(array$data,$userId)
{
$date=$this->date($data['date']??'');$notes=trim((string)($data['notes']??''));if($notes===''||empty($data['customer_id'])||empty($data['technician_id']))throw new BusinessException('Customer, teknisi, tanggal, dan keterangan wajib diisi.');$customer=$this->customer($data['customer_id']);$this->user($data['technician_id']);$key=trim((string)($data['idempotency_key']??''));if($key==='')throw new BusinessException('Kunci transaksi tidak tersedia.');
$this->CI->db->trans_begin();try{if($existing=$this->CI->db->get_where('technician_equipment_documents',array('company_id'=>$this->company(),'idempotency_key'=>$key))->row()){$this->CI->db->trans_commit();return array('document_id'=>(int)$existing->id,'document_no'=>$existing->document_no,'duplicate'=>true);}$doc=$this->createDocument('replacement',$date,$data,$userId,$key);$out=$this->lockAllocation($data['outgoing_allocation_id']??0);$outQty=$this->validatedQty($out,$data['outgoing_qty']??0);if($out['custodian_type']!=='technician')throw new BusinessException('Barang pengganti harus berada pada teknisi.');$this->move($doc,$out,'customer',$customer->id,$customer->id,'installed','good',$outQty,'installation','Barang pengganti: '.$notes);
if(!empty($data['incoming_allocation_id'])){$old=$this->lockAllocation($data['incoming_allocation_id']);if($old['custodian_type']!=='customer'||(int)$old['customer_id']!==(int)$customer->id)throw new BusinessException('Barang lama tidak terpasang pada customer yang dipilih.');$inQty=$this->validatedQty($old,$data['incoming_qty']??0);}
else{$external=$this->createExternal(array('owner_customer_id'=>$customer->id,'equipment_name'=>$data['old_equipment_name']??'','category'=>$data['old_category']??'','brand'=>$data['old_brand']??'','model'=>$data['old_model']??'','external_barcode'=>$data['old_barcode']??'','serial_number'=>$data['old_serial_number']??'','description'=>$data['old_description']??'','source_date'=>$date,'source_document_no'=>$data['work_order_no']??$doc['document_no'],'source_reason'=>$data['reason']??$notes,'ownership_type'=>$data['old_ownership_type']??'customer','deployment_allowed'=>!empty($data['old_deployment_allowed']),'authorization_reference'=>$data['old_authorization_reference']??'','tracking_type'=>$data['old_tracking_type']??'UNIT','qty'=>$data['incoming_qty']??1),$userId);$old=$this->lockAllocationBySubject('customer_equipment',$external['id'],'customer',$customer->id);$inQty=$this->validatedQty($old,$data['incoming_qty']??0);}
$this->assertReusableExternal($out,$customer->id);$condition=in_array(($data['old_condition']??''),array('good','inspection_required','repair_required','damaged'),true)?$data['old_condition']:'inspection_required';$this->move($doc,$old,'technician',(int)$data['technician_id'],$customer->id,'removed',$condition,$inQty,'uninstallation','Barang lama dilepas: '.$notes);
if($this->CI->db->trans_status()===false)throw new RuntimeException('Dokumen penggantian gagal disimpan.');$this->CI->db->trans_commit();return array('document_id'=>$doc['id'],'document_no'=>$doc['document_no']);}catch(Throwable$e){$this->CI->db->trans_rollback();throw$e;}
}
public function inspect(array$data,$userId)
{
$date=$this->date($data['date']??'');$notes=trim((string)($data['notes']??''));if($notes==='')throw new BusinessException('Catatan hasil pemeriksaan wajib diisi.');$result=(string)($data['result_status']??'');if(!in_array($result,array('good','repair_required','under_repair','damaged','lost','return_customer','scrapped'),true))throw new BusinessException('Hasil pemeriksaan tidak valid.');$key=trim((string)($data['idempotency_key']??''));if($key==='')throw new BusinessException('Kunci transaksi tidak tersedia. Muat ulang halaman.');
$this->CI->db->trans_begin();try{if($existing=$this->CI->db->get_where('technician_equipment_documents',array('company_id'=>$this->company(),'idempotency_key'=>$key))->row()){$this->CI->db->trans_commit();return array('document_id'=>$existing->id,'document_no'=>$existing->document_no,'duplicate'=>true);}$source=$this->lockAllocation($data['allocation_id']??0);$externalStorage=$source['subject_type']==='customer_equipment'&&in_array($source['custodian_type'],array('customer','warehouse'),true)&&$source['usage_status']==='returned';if(!in_array($source['condition_status'],array('inspection_required','repair_required','under_repair','damaged'),true)||(!in_array($source['custodian_type'],array('quarantine','service_center'),true)&&!$externalStorage))throw new BusinessException('Barang tidak berada pada antrean pemeriksaan/perbaikan.');$qty=$this->validatedQty($source,$data['qty']??0);$doc=$this->createDocument('inspection',$date,$data,$userId,$key);$this->CI->db->insert('technician_equipment_inspections',array('company_id'=>$this->company(),'document_id'=>$doc['id'],'source_allocation_id'=>$source['id'],'subject_type'=>$source['subject_type'],'subject_id'=>$source['subject_id'],'result_status'=>$result,'qty'=>$qty,'inspection_date'=>$date,'notes'=>$notes,'inspected_by'=>(int)$userId,'created_at'=>$this->now()));
if($externalStorage){$condition=in_array($result,array('return_customer','good'),true)?'good':$result;$this->move($doc,$source,$source['custodian_type'],$source['custodian_id'],$source['customer_id'],'returned',$condition,$qty,'adjustment',$notes,false);if($result==='scrapped')$this->CI->db->where(array('id'=>$source['subject_id'],'company_id'=>$this->company()))->update('customer_equipment_registry',array('lifecycle_status'=>'disposed','updated_at'=>$this->now()));}
elseif($result==='good'&&$source['subject_type']==='company_barcode'){$this->consume($source,$qty);$this->releaseInternal($source['barcode_id'],$qty,'available');$movement=$this->movement($this->subject($source),$qty,'warehouse',$source['custodian_id'],'warehouse',$source['custodian_id'],'adjustment',$doc['id'],'[AUTO] Lulus pemeriksaan '.$doc['document_no'].' | '.$notes);$this->line($doc,$source,$this->tracking($source),'inspection',$source['custodian_type'],$source['custodian_id'],'warehouse',$source['custodian_id'],$source['customer_id'],$source['usage_status'],'available',$source['condition_status'],'good',$qty,$movement,$notes);}
elseif($result==='return_customer'||($result==='good'&&$source['subject_type']==='customer_equipment')){$customer=$this->customer($source['customer_id']);$this->move($doc,$source,'customer',$customer->id,$customer->id,'returned','good',$qty,'transfer',$notes);}
else{$toType=$result==='under_repair'?'service_center':'quarantine';$condition=$result;$this->move($doc,$source,$toType,$source['custodian_id'],$source['customer_id'],'returned',$condition,$qty,'adjustment',$notes,false);if($source['subject_type']==='company_barcode')$this->refreshBarcodeStatus($source['barcode_id'],$result==='damaged'?'damaged':'quarantine');if($source['subject_type']==='customer_equipment'&&$result==='scrapped')$this->CI->db->where(array('id'=>$source['subject_id'],'company_id'=>$this->company()))->update('customer_equipment_registry',array('lifecycle_status'=>'disposed','updated_at'=>$this->now()));}
if($source['subject_type']==='customer_equipment')$this->syncExternal($source['subject_id']);if($this->CI->db->trans_status()===false)throw new RuntimeException('Pemeriksaan gagal disimpan.');$this->CI->db->trans_commit();return array('document_id'=>$doc['id'],'document_no'=>$doc['document_no']);}catch(Throwable$e){$this->CI->db->trans_rollback();throw$e;}
}
public function reverse(array$data,$userId)
{
$sourceId=(int)($data['document_id']??0);$reason=trim((string)($data['reason']??''));$date=$this->date($data['date']??date('Y-m-d'));$key=trim((string)($data['idempotency_key']??''));if($sourceId<1||$reason===''||$key==='')throw new BusinessException('Dokumen, tanggal, alasan koreksi, dan kunci transaksi wajib diisi.');
$this->CI->db->trans_begin();try{
if($existing=$this->CI->db->get_where('technician_equipment_documents',array('company_id'=>$this->company(),'idempotency_key'=>$key))->row()){$this->CI->db->trans_commit();return array('document_id'=>(int)$existing->id,'document_no'=>$existing->document_no,'duplicate'=>true);}
$original=$this->CI->db->query('SELECT * FROM technician_equipment_documents WHERE id=? AND company_id=? FOR UPDATE',array($sourceId,$this->company()))->row_array();if(!$original)throw new BusinessException('Dokumen yang akan dikoreksi tidak ditemukan.');if($original['status']!=='posted')throw new BusinessException('Dokumen sudah pernah dikoreksi atau tidak dapat dibalik.');if($original['document_type']==='opening')throw new BusinessException('Saldo awal migrasi tidak dapat dikoreksi dari transaksi operasional.');
$payload=array('customer_id'=>$original['customer_id'],'technician_id'=>$original['technician_id'],'target_technician_id'=>$original['target_technician_id'],'warehouse_id'=>$original['warehouse_id'],'reason'=>$reason,'notes'=>'[REVERSAL] '.$original['document_no'].' | '.$reason);$doc=$this->createDocument('correction',$date,$payload,$userId,$key);$this->CI->db->where('id',$doc['id'])->update('technician_equipment_documents',array('reversal_of_id'=>$sourceId));
$lines=$this->CI->db->where(array('document_id'=>$sourceId,'company_id'=>$this->company()))->order_by('id','DESC')->get('technician_equipment_document_lines')->result_array();if(!$lines)throw new BusinessException('Detail dokumen sumber tidak ditemukan.');
foreach($lines as$line){$subject=array('subject_type'=>$line['subject_type'],'subject_id'=>$line['subject_id'],'item_id'=>$line['item_id'],'barcode_id'=>$line['barcode_id'],'customer_equipment_id'=>$line['customer_equipment_id'],'barcode_snapshot'=>$line['barcode_snapshot'],'ownership_type'=>$line['ownership_type']);
if($line['subject_type']==='company_barcode'&&$line['to_custodian_type']==='warehouse'){$this->reserveInternal($line['barcode_id'],$line['qty']);}
else{$target=$this->lockMatchingAllocation($line['subject_type'],$line['subject_id'],$line['to_custodian_type'],$line['to_custodian_id'],$line['customer_id'],$line['to_usage_status'],$line['to_condition_status']);$this->consume($target,(float)$line['qty']);}
if($line['subject_type']==='company_barcode'&&$line['from_custodian_type']==='warehouse')$this->releaseInternal($line['barcode_id'],(float)$line['qty'],'available');
else$this->addBalance($subject,$line['from_custodian_type'],$line['from_custodian_id'],$line['customer_id'],$line['from_usage_status'],$line['from_condition_status'],(float)$line['qty'],$doc['id']);
$movement=null;if($line['subject_type']==='company_barcode'){$from=in_array($line['to_custodian_type'],array('quarantine','service_center'),true)?'warehouse':$line['to_custodian_type'];$to=in_array($line['from_custodian_type'],array('quarantine','service_center'),true)?'warehouse':$line['from_custodian_type'];$movement=$this->movement($subject,(float)$line['qty'],$from,$line['to_custodian_id'],$to,$line['from_custodian_id'],'adjustment',$doc['id'],'[AUTO][REVERSAL] '.$original['document_no'].' | '.$reason);}
$this->line($doc,$subject,$line['tracking_type'],'correction',$line['to_custodian_type'],$line['to_custodian_id'],$line['from_custodian_type'],$line['from_custodian_id'],$line['customer_id'],$line['to_usage_status'],$line['from_usage_status'],$line['to_condition_status'],$line['from_condition_status'],(float)$line['qty'],$movement,$reason);if($line['subject_type']==='customer_equipment')$this->syncExternal($line['subject_id']);
}
$this->CI->db->where(array('id'=>$sourceId,'status'=>'posted'))->update('technician_equipment_documents',array('status'=>'reversed','reversed_by'=>(int)$userId,'reversed_at'=>$this->now()));if($this->CI->db->affected_rows()!==1)throw new BusinessException('Status dokumen sumber telah berubah.');if($this->CI->db->trans_status()===false)throw new RuntimeException('Koreksi gagal disimpan.');$this->CI->db->trans_commit();return array('document_id'=>$doc['id'],'document_no'=>$doc['document_no']);
}catch(Throwable$e){$this->CI->db->trans_rollback();throw$e;}
}
public function inspectionQueueStats($technicianId=0)
{
$this->CI->db->select('COUNT(*) row_count,COALESCE(SUM(a.qty),0) qty',false)->from('technician_custody_allocations a')->join('technician_equipment_documents d','d.id=a.last_document_id','left')->where('a.company_id',$this->company())->group_start()->where_in('a.custodian_type',array('technician','quarantine','service_center'))->or_group_start()->where('a.subject_type','customer_equipment')->where_in('a.custodian_type',array('customer','warehouse'))->where('a.usage_status','returned')->group_end()->group_end()->where("(d.document_type IS NULL OR d.document_type <> 'inspection')",null,false)->where_in('a.condition_status',array('inspection_required','repair_required','under_repair','damaged'))->where('a.qty >',0);
if((int)$technicianId>0){$employee=(int)$technicianId;$this->CI->db->where("COALESCE(CASE WHEN a.custodian_type='technician' THEN a.custodian_id END,d.technician_id)={$employee}",null,false);}$row=$this->CI->db->get()->row_array();return array('row_count'=>(int)($row['row_count']??0),'qty'=>(float)($row['qty']??0));
}
public function inspectionQueue(array$filters=array())
{
$this->CI->db
->select("a.*,d.document_date,COALESCE(CASE WHEN a.custodian_type='technician' THEN a.custodian_id END,d.technician_id) technician_id,e.full_name technician_name,CASE WHEN a.subject_type='company_barcode' THEN CONCAT(i.kode_detail,' - ',i.nama_barang) ELSE r.equipment_name END display_name,CASE WHEN a.subject_type='company_barcode' THEN COALESCE(k.tracking_type,'QTY') ELSE COALESCE(r.tracking_type,'UNIT') END tracking_type,COALESCE(b.serial_number,r.serial_number) serial_number,w.nama warehouse_name,c.nama customer_name,(a.custodian_type='technician') requires_return",false)
->from('technician_custody_allocations a')
->join('technician_equipment_documents d','d.id=a.last_document_id','left')
->join('k_employees e',"e.id=COALESCE(CASE WHEN a.custodian_type='technician' THEN a.custodian_id END,d.technician_id)",'left')
->join('items i','i.id=a.item_id','left')->join('kode_barang k','k.id=i.kode_id','left')
->join('item_barcodes b','b.id=a.barcode_id','left')->join('customer_equipment_registry r','r.id=a.customer_equipment_id','left')
->join('warehouses w',"w.id=a.custodian_id AND a.custodian_type IN('warehouse','quarantine','service_center')",'left')
->join('customers c','c.id=a.customer_id','left')->where('a.company_id',$this->company())
->group_start()->where_in('a.custodian_type',array('technician','quarantine','service_center'))->or_group_start()->where('a.subject_type','customer_equipment')->where_in('a.custodian_type',array('customer','warehouse'))->where('a.usage_status','returned')->group_end()->group_end()
->where("(d.document_type IS NULL OR d.document_type <> 'inspection')",null,false)
->where_in('a.condition_status',array('inspection_required','repair_required','under_repair','damaged'))->where('a.qty >',0);
if(!empty($filters['date_from']))$this->CI->db->where('d.document_date >=',$filters['date_from']);
if(!empty($filters['date_to']))$this->CI->db->where('d.document_date <=',$filters['date_to']);
if(!empty($filters['technician_id'])){$technicianId=(int)$filters['technician_id'];$this->CI->db->where("COALESCE(CASE WHEN a.custodian_type='technician' THEN a.custodian_id END,d.technician_id)={$technicianId}",null,false);}
if(!empty($filters['customer_id']))$this->CI->db->where('a.customer_id',(int)$filters['customer_id']);
if(!empty($filters['ownership']))$this->CI->db->where('a.ownership_type',$filters['ownership']);
if(!empty($filters['condition']))$this->CI->db->where('a.condition_status',$filters['condition']);
if(!empty($filters['status']))$this->CI->db->where('a.usage_status',$filters['status']);
return$this->CI->db->order_by('requires_return','DESC',false)->order_by('a.updated_at')->get()->result_array();
}
public function documents(array$filters=array()){$this->CI->db->select('d.*,c.nama customer_name,e.full_name technician_name,t.full_name target_technician_name,w.nama warehouse_name,u.nama creator_name,(SELECT COUNT(*) FROM technician_equipment_document_lines l WHERE l.document_id=d.id) line_count')->from('technician_equipment_documents d')->join('customers c','c.id=d.customer_id','left')->join('k_employees e','e.id=d.technician_id','left')->join('k_employees t','t.id=d.target_technician_id','left')->join('warehouses w','w.id=d.warehouse_id','left')->join('users u','u.id=d.created_by','left')->where('d.company_id',$this->company());if(!empty($filters['date_from']))$this->CI->db->where('d.document_date >=',$filters['date_from']);if(!empty($filters['date_to']))$this->CI->db->where('d.document_date <=',$filters['date_to']);foreach(array('customer_id','technician_id')as$key)if(!empty($filters[$key]))$this->CI->db->where('d.'.$key,(int)$filters[$key]);if(!empty($filters['document_type']))$this->CI->db->where('d.document_type',$filters['document_type']);if(!empty($filters['status']))$this->CI->db->where('d.status',$filters['status']);return$this->CI->db->order_by('d.document_date','DESC')->order_by('d.id','DESC')->limit(1000)->get()->result_array();}
public function history(array$filters=array()){$this->CI->db->select("d.id document_id,d.document_no,d.document_type,d.document_date,d.status document_status,d.notes document_notes,d.created_at,d.idempotency_key,e.full_name technician_name,t.full_name target_technician_name,c.nama customer_name,u.nama creator_name,l.*,CASE WHEN l.subject_type='company_barcode' THEN CONCAT(COALESCE(i.kode_detail,'-'),' - ',COALESCE(i.nama_barang,'-')) ELSE COALESCE(r.equipment_name,'-') END display_name,COALESCE(b.serial_number,r.serial_number) serial_number",false)->from('technician_equipment_document_lines l')->join('technician_equipment_documents d','d.id=l.document_id')->join('k_employees e','e.id=d.technician_id','left')->join('k_employees t','t.id=d.target_technician_id','left')->join('customers c','c.id=l.customer_id','left')->join('users u','u.id=d.created_by','left')->join('items i','i.id=l.item_id','left')->join('item_barcodes b','b.id=l.barcode_id','left')->join('customer_equipment_registry r','r.id=l.customer_equipment_id','left')->where('d.company_id',$this->company());if(!empty($filters['date_from']))$this->CI->db->where('d.document_date >=',$filters['date_from']);if(!empty($filters['date_to']))$this->CI->db->where('d.document_date <=',$filters['date_to']);if(!empty($filters['technician_id']))$this->CI->db->group_start()->where('d.technician_id',(int)$filters['technician_id'])->or_where('d.target_technician_id',(int)$filters['technician_id'])->group_end();if(!empty($filters['customer_id']))$this->CI->db->where('l.customer_id',(int)$filters['customer_id']);if(!empty($filters['status']))$this->CI->db->where('l.to_usage_status',$filters['status']);if(!empty($filters['condition']))$this->CI->db->where('l.to_condition_status',$filters['condition']);if(!empty($filters['ownership']))$this->CI->db->where('l.ownership_type',$filters['ownership']);if(!empty($filters['query'])){$q=$this->CI->db->escape_like_str($filters['query']);$this->CI->db->where("(l.barcode_snapshot LIKE '%$q%' OR b.serial_number LIKE '%$q%' OR r.serial_number LIKE '%$q%' OR i.nama_barang LIKE '%$q%' OR r.equipment_name LIKE '%$q%')",null,false);}return$this->CI->db->order_by('d.document_date','DESC')->order_by('l.id','DESC')->limit(5000)->get()->result_array();}
public function document($id){$header=$this->CI->db->select('d.*,c.nama customer_name,e.full_name technician_name,t.full_name target_technician_name,w.nama warehouse_name,u.nama creator_name')->from('technician_equipment_documents d')->join('customers c','c.id=d.customer_id','left')->join('k_employees e','e.id=d.technician_id','left')->join('k_employees t','t.id=d.target_technician_id','left')->join('warehouses w','w.id=d.warehouse_id','left')->join('users u','u.id=d.created_by','left')->where(array('d.id'=>(int)$id,'d.company_id'=>$this->company()))->get()->row_array();if(!$header)throw new BusinessException('Dokumen peralatan tidak ditemukan.');$header['lines']=$this->CI->db->select("l.*,CASE WHEN l.subject_type='company_barcode' THEN CONCAT(i.kode_detail,' - ',i.nama_barang) ELSE r.equipment_name END display_name,COALESCE(b.serial_number,r.serial_number) serial_number,c.nama customer_name",false)->from('technician_equipment_document_lines l')->join('items i','i.id=l.item_id','left')->join('item_barcodes b','b.id=l.barcode_id','left')->join('customer_equipment_registry r','r.id=l.customer_equipment_id','left')->join('customers c','c.id=l.customer_id','left')->where('l.document_id',(int)$id)->order_by('l.id')->get()->result_array();$header['attachments']=$this->CI->db->where(array('company_id'=>$this->company(),'entity_type'=>'technician_equipment_document','entity_id'=>(int)$id))->get('transaction_attachments')->result_array();return$header;}
public function externalList($technicianId=null){$this->CI->db->select('r.*,c.nama owner_name,cc.nama current_customer_name')->from('customer_equipment_registry r')->join('customers c','c.id=r.owner_customer_id','left')->join('customers cc','cc.id=r.current_customer_id','left')->where('r.company_id',$this->company());if($technicianId){$technicianId=(int)$technicianId;$this->CI->db->where("(EXISTS(SELECT 1 FROM technician_custody_allocations a WHERE a.company_id=r.company_id AND a.subject_type='customer_equipment' AND a.subject_id=r.id AND a.custodian_type='technician' AND a.custodian_id={$technicianId}) OR EXISTS(SELECT 1 FROM technician_equipment_document_lines dl JOIN technician_equipment_documents dh ON dh.id=dl.document_id WHERE dl.company_id=r.company_id AND dl.subject_type='customer_equipment' AND dl.subject_id=r.id AND (dh.technician_id={$technicianId} OR dh.target_technician_id={$technicianId})))",null,false);}return$this->CI->db->order_by('r.id','DESC')->limit(1000)->get()->result_array();}
public function customerEquipmentSummary($technicianId=0)
{
$this->CI->db->select("c.id customer_id,c.nama customer_name,COUNT(DISTINCT CONCAT(a.subject_type,':',a.subject_id)) item_count,SUM(a.qty) total_qty,SUM(CASE WHEN a.ownership_type IN('company','transferred_non_inventory') THEN a.qty ELSE 0 END) company_qty,SUM(CASE WHEN a.ownership_type NOT IN('company','transferred_non_inventory') THEN a.qty ELSE 0 END) customer_qty,SUM(CASE WHEN a.usage_status='installed' THEN a.qty ELSE 0 END) installed_qty,SUM(CASE WHEN a.condition_status='good' THEN a.qty ELSE 0 END) good_qty",false)->from('technician_custody_allocations a')->join('customers c','c.id=a.customer_id')->where('a.company_id',$this->company())->where('a.customer_id IS NOT NULL',null,false)->where('a.qty >',0);
if((int)$technicianId>0){$employee=(int)$technicianId;$this->CI->db->where("(a.custodian_type='technician' AND a.custodian_id={$employee} OR EXISTS(SELECT 1 FROM technician_equipment_document_lines sl JOIN technician_equipment_documents sd ON sd.id=sl.document_id WHERE sl.company_id=a.company_id AND sl.subject_type=a.subject_type AND sl.subject_id=a.subject_id AND sl.customer_id=a.customer_id AND (sd.technician_id={$employee} OR sd.target_technician_id={$employee})))",null,false);}
return$this->CI->db->group_by(array('c.id','c.nama'))->order_by('c.nama')->get()->result_array();
}
public function customerEquipmentRows($customerId,$status='all',$technicianId=0)
{
$status=in_array($status,array('all','installed','stored','technician','inspection','good','damaged'),true)?$status:'all';
$this->CI->db->select("a.*,CASE WHEN a.subject_type='company_barcode' THEN CONCAT(COALESCE(i.kode_detail,'-'),' - ',COALESCE(i.nama_barang,'-')) ELSE COALESCE(r.equipment_name,'-') END display_name,CASE WHEN a.subject_type='company_barcode' THEN COALESCE(k.tracking_type,'QTY') ELSE COALESCE(r.tracking_type,'UNIT') END tracking_type,COALESCE(b.serial_number,r.serial_number) serial_number,c.nama customer_name,e.full_name technician_name,w.nama warehouse_name",false)->from('technician_custody_allocations a')->join('items i','i.id=a.item_id','left')->join('kode_barang k','k.id=i.kode_id','left')->join('item_barcodes b','b.id=a.barcode_id','left')->join('customer_equipment_registry r','r.id=a.customer_equipment_id','left')->join('customers c','c.id=a.customer_id','left')->join('k_employees e',"e.id=a.custodian_id AND a.custodian_type='technician'",'left')->join('warehouses w',"w.id=a.custodian_id AND a.custodian_type IN('warehouse','quarantine','service_center')",'left')->where(array('a.company_id'=>$this->company(),'a.customer_id'=>(int)$customerId))->where('a.qty >',0);
if($status==='installed')$this->CI->db->where('a.usage_status','installed');elseif($status==='stored')$this->CI->db->where(array('a.custodian_type'=>'customer','a.usage_status'=>'returned'));elseif($status==='technician')$this->CI->db->where('a.custodian_type','technician');elseif($status==='inspection')$this->CI->db->where_in('a.condition_status',array('inspection_required','repair_required','under_repair'));elseif($status==='good')$this->CI->db->where('a.condition_status','good');elseif($status==='damaged')$this->CI->db->where_in('a.condition_status',array('damaged','lost','scrapped'));
if((int)$technicianId>0){$employee=(int)$technicianId;$this->CI->db->where("(a.custodian_type='technician' AND a.custodian_id={$employee} OR EXISTS(SELECT 1 FROM technician_equipment_document_lines sl JOIN technician_equipment_documents sd ON sd.id=sl.document_id WHERE sl.company_id=a.company_id AND sl.subject_type=a.subject_type AND sl.subject_id=a.subject_id AND sl.customer_id=a.customer_id AND (sd.technician_id={$employee} OR sd.target_technician_id={$employee})))",null,false);}
return$this->CI->db->order_by('display_name')->order_by('a.barcode_snapshot')->get()->result_array();
}
public function customerEquipmentNotes($customerId,$technicianId=0)
{
$this->CI->db->select("d.id document_id,d.document_no,d.document_type,d.document_date,d.created_at,COALESCE(NULLIF(TRIM(l.notes),''),NULLIF(TRIM(d.notes),''),'-') note_text,CASE WHEN l.subject_type='company_barcode' THEN CONCAT(COALESCE(i.kode_detail,'-'),' - ',COALESCE(i.nama_barang,'-')) ELSE COALESCE(r.equipment_name,'-') END display_name,l.barcode_snapshot,COALESCE(b.serial_number,r.serial_number) serial_number,l.ownership_type,l.qty,u.nama actor_name,e.full_name technician_name",false)->from('technician_equipment_document_lines l')->join('technician_equipment_documents d','d.id=l.document_id')->join('items i','i.id=l.item_id','left')->join('item_barcodes b','b.id=l.barcode_id','left')->join('customer_equipment_registry r','r.id=l.customer_equipment_id','left')->join('users u','u.id=d.created_by','left')->join('k_employees e','e.id=d.technician_id','left')->where(array('l.company_id'=>$this->company(),'l.customer_id'=>(int)$customerId));
if((int)$technicianId>0){$employee=(int)$technicianId;$this->CI->db->group_start()->where('d.technician_id',$employee)->or_where('d.target_technician_id',$employee)->group_end();}
return$this->CI->db->order_by('d.document_date','DESC')->order_by('l.id','DESC')->limit(5000)->get()->result_array();
}
public function external($id){$row=$this->CI->db->select('r.*,c.nama owner_name,cc.nama current_customer_name,u.nama registered_name')->from('customer_equipment_registry r')->join('customers c','c.id=r.owner_customer_id','left')->join('customers cc','cc.id=r.current_customer_id','left')->join('users u','u.id=r.registered_by','left')->where(array('r.id'=>(int)$id,'r.company_id'=>$this->company()))->get()->row_array();if(!$row)throw new BusinessException('Barang customer tidak ditemukan.');$row['allocations']=$this->allocations('customer_equipment',(int)$id);$row['attachments']=$this->CI->db->where(array('company_id'=>$this->company(),'entity_type'=>'customer_equipment_registry','entity_id'=>(int)$id))->get('transaction_attachments')->result_array();return$row;}
private function createDocument($type,$date,array$data,$userId,$key){$number=$this->CI->numberingservice->next('technician_equipment',$date);$row=array('company_id'=>$this->company(),'document_no'=>$number,'document_type'=>$type,'document_date'=>$date,'status'=>'posted','customer_id'=>(int)($data['customer_id']??0)?:null,'technician_id'=>(int)($data['technician_id']??0)?:null,'target_technician_id'=>(int)($data['target_technician_id']??0)?:null,'warehouse_id'=>(int)($data['warehouse_id']??0)?:null,'work_order_no'=>trim((string)($data['work_order_no']??''))?:null,'installation_location'=>trim((string)($data['installation_location']??''))?:null,'reason'=>trim((string)($data['reason']??''))?:null,'notes'=>trim((string)($data['notes']??''))?:null,'idempotency_key'=>$key,'created_by'=>(int)$userId,'created_at'=>$this->now());$this->CI->db->insert('technician_equipment_documents',$row);$row['id']=(int)$this->CI->db->insert_id();return$row;}
private function lockAllocation($id){$row=$this->CI->db->query('SELECT * FROM technician_custody_allocations WHERE id=? AND company_id=? FOR UPDATE',array((int)$id,$this->company()))->row_array();if(!$row||(float)$row['qty']<=0)throw new BusinessException('Saldo posisi barang tidak ditemukan atau sudah berubah.');return$row;}
private function lockAllocationBySubject($type,$id,$custodian,$custodianId){$row=$this->CI->db->query('SELECT * FROM technician_custody_allocations WHERE company_id=? AND subject_type=? AND subject_id=? AND custodian_type=? AND custodian_id=? AND qty>0 ORDER BY id LIMIT 1 FOR UPDATE',array($this->company(),$type,(int)$id,$custodian,(int)$custodianId))->row_array();if(!$row)throw new BusinessException('Posisi barang tidak ditemukan.');return$row;}
private function lockMatchingAllocation($type,$id,$custodian,$custodianId,$customer,$usage,$condition){$sql='SELECT * FROM technician_custody_allocations WHERE company_id=? AND subject_type=? AND subject_id=? AND custodian_type=? AND COALESCE(custodian_id,0)=? AND COALESCE(customer_id,0)=? AND usage_status=? AND condition_status=? AND qty>0 ORDER BY id LIMIT 1 FOR UPDATE';$row=$this->CI->db->query($sql,array($this->company(),$type,(int)$id,$custodian,(int)$custodianId,(int)$customer,$usage,$condition))->row_array();if(!$row)throw new BusinessException('Koreksi ditolak karena barang hasil transaksi sudah dipindahkan atau statusnya telah berubah.');return$row;}
private function validatedQty(array$allocation,$input){$tracking=$this->tracking($allocation);$qty=$tracking==='UNIT'?1:$this->qty($input);if($qty>(float)$allocation['qty']+.0001)throw new BusinessException('Jumlah melebihi saldo pada posisi asal.');if($tracking==='UNIT'&&abs((float)$allocation['qty']-1)>.0001)throw new BusinessException('Saldo barang UNIT tidak valid.');return$qty;}
private function tracking(array$subject){if($subject['subject_type']==='company_barcode'){$r=$this->CI->db->select("COALESCE(k.tracking_type,'QTY') tracking_type",false)->from('item_barcodes b')->join('items i','i.id=b.item_id')->join('kode_barang k','k.id=i.kode_id','left')->where('b.id',(int)$subject['barcode_id'])->get()->row();return$r&&strtoupper($r->tracking_type)==='UNIT'?'UNIT':'QTY';}$r=$this->CI->db->get_where('customer_equipment_registry',array('id'=>(int)$subject['subject_id'],'company_id'=>$this->company()))->row();return$r&&strtoupper($r->tracking_type)==='QTY'?'QTY':'UNIT';}
private function subject(array$a){return array('subject_type'=>$a['subject_type'],'subject_id'=>(int)$a['subject_id'],'item_id'=>$a['item_id']?:null,'barcode_id'=>$a['barcode_id']?:null,'customer_equipment_id'=>$a['customer_equipment_id']?:null,'barcode_snapshot'=>$a['barcode_snapshot'],'ownership_type'=>$a['ownership_type']);}
private function assertReusableExternal(array$source,$customerId){if($source['subject_type']!=='customer_equipment')return;$item=$this->CI->db->get_where('customer_equipment_registry',array('id'=>(int)$source['subject_id'],'company_id'=>$this->company()))->row();if(!$item)throw new BusinessException('Register barang customer tidak ditemukan.');if((int)$item->owner_customer_id!==$customerId&&(!(int)$item->deployment_allowed||trim((string)$item->authorization_reference)===''))throw new BusinessException('Barang milik customer tidak memiliki otorisasi untuk dipasang pada customer lain.');}
private function addBalance(array$subject,$type,$id,$customer,$usage,$condition,$qty,$doc){$row=array_merge($subject,array('company_id'=>$this->company(),'custodian_type'=>$type,'custodian_id'=>$id?:null,'customer_id'=>$customer?:null,'usage_status'=>$usage,'condition_status'=>$condition,'qty'=>$qty,'version'=>1,'last_document_id'=>$doc?:null,'created_at'=>$this->now(),'updated_at'=>$this->now()));$row['balance_key']=$this->key($row);$existing=$this->CI->db->query('SELECT * FROM technician_custody_allocations WHERE balance_key=? FOR UPDATE',array($row['balance_key']))->row_array();if($existing){$this->CI->db->where(array('id'=>$existing['id'],'version'=>$existing['version']))->update('technician_custody_allocations',array('qty'=>(float)$existing['qty']+$qty,'version'=>(int)$existing['version']+1,'last_document_id'=>$doc?:$existing['last_document_id'],'updated_at'=>$this->now()));if($this->CI->db->affected_rows()!==1)throw new BusinessException('Saldo tujuan berubah saat transaksi. Silakan ulangi.');$row['id']=$existing['id'];}else{$this->CI->db->insert('technician_custody_allocations',$row);$row['id']=(int)$this->CI->db->insert_id();}return$row;}
private function consume(array$a,$qty){$left=round((float)$a['qty']-$qty,4);if($left<-.0001)throw new BusinessException('Saldo posisi barang tidak mencukupi.');if($left<=.0001)$this->CI->db->where(array('id'=>$a['id'],'version'=>$a['version']))->delete('technician_custody_allocations');else$this->CI->db->where(array('id'=>$a['id'],'version'=>$a['version']))->update('technician_custody_allocations',array('qty'=>$left,'version'=>(int)$a['version']+1,'updated_at'=>$this->now()));if($this->CI->db->affected_rows()!==1)throw new BusinessException('Saldo barang berubah saat transaksi. Silakan ulangi.');}
private function move(array$doc,array$source,$toType,$toId,$customer,$usage,$condition,$qty,$movementType,$notes,$writeMovement=true){$this->consume($source,$qty);$subject=$this->subject($source);$target=$this->addBalance($subject,$toType,$toId,$customer,$usage,$condition,$qty,$doc['id']);$movement=null;if($writeMovement&&$source['subject_type']==='company_barcode'){$fromType=in_array($source['custodian_type'],array('quarantine','service_center'),true)?'warehouse':$source['custodian_type'];$targetType=in_array($toType,array('quarantine','service_center'),true)?'warehouse':$toType;$movement=$this->movement($subject,$qty,$fromType,$source['custodian_id'],$targetType,$toId,$movementType,$doc['id'],'[AUTO] '.$doc['document_no'].' | '.$notes);}$this->line($doc,$subject,$this->tracking($subject),$doc['document_type'],$source['custodian_type'],$source['custodian_id'],$toType,$toId,$customer,$source['usage_status'],$usage,$source['condition_status'],$condition,$qty,$movement,$notes);if($source['subject_type']==='customer_equipment')$this->syncExternal($source['subject_id']);return$target;}
private function movement(array$subject,$qty,$fromType,$fromId,$toType,$toId,$movementType,$docId,$notes){$allowed=array('warehouse','technician','customer','supplier','invoice','asset');if(!in_array($fromType,$allowed,true)||!in_array($toType,$allowed,true))return null;$row=array('item_id'=>$subject['item_id'],'barcode_id'=>$subject['barcode_id'],'qty'=>$qty,'from_type'=>$fromType,'from_id'=>$fromId?:null,'to_type'=>$toType,'to_id'=>$toId?:null,'movement_type'=>$movementType,'reference_type'=>'technician_equipment','reference_id'=>$docId,'notes'=>mb_substr($notes,0,255),'created_at'=>$this->now());if($this->CI->db->field_exists('company_id','item_movements'))$row['company_id']=$this->company();$this->CI->db->insert('item_movements',$row);return(int)$this->CI->db->insert_id();}
private function line(array$doc,array$subject,$tracking,$action,$fromType,$fromId,$toType,$toId,$customer,$fromUsage,$toUsage,$fromCondition,$toCondition,$qty,$movement,$notes){$this->CI->db->insert('technician_equipment_document_lines',array('document_id'=>$doc['id'],'company_id'=>$this->company(),'subject_type'=>$subject['subject_type'],'subject_id'=>$subject['subject_id'],'item_id'=>$subject['item_id'],'barcode_id'=>$subject['barcode_id'],'customer_equipment_id'=>$subject['customer_equipment_id'],'barcode_snapshot'=>$subject['barcode_snapshot'],'ownership_type'=>$subject['ownership_type'],'tracking_type'=>$tracking,'action_type'=>$action,'from_custodian_type'=>$fromType,'from_custodian_id'=>$fromId?:null,'to_custodian_type'=>$toType,'to_custodian_id'=>$toId?:null,'customer_id'=>$customer?:null,'from_usage_status'=>$fromUsage,'to_usage_status'=>$toUsage,'from_condition_status'=>$fromCondition,'to_condition_status'=>$toCondition,'qty'=>$qty,'item_movement_id'=>$movement,'notes'=>$notes,'created_at'=>$this->now()));}
private function releaseInternal($barcodeId,$qty,$preferred){$b=$this->CI->db->query('SELECT * FROM item_barcodes WHERE id=? FOR UPDATE',array((int)$barcodeId))->row();if(!$b)throw new BusinessException('Barcode perusahaan tidak ditemukan.');$reserved=max(0,round((float)$b->reserved_qty-$qty,4));$available=(float)$b->qty_sisa-$reserved-(float)$b->return_reserved_qty;$status=$available>.0001?$preferred:'installed';$this->CI->db->where(array('id'=>$b->id,'version'=>$b->version))->update('item_barcodes',array('reserved_qty'=>$reserved,'status'=>$status,'version'=>(int)$b->version+1));if($this->CI->db->affected_rows()!==1)throw new BusinessException('Barcode berubah saat pemeriksaan.');}
private function reserveInternal($barcodeId,$qty){$b=$this->CI->db->query('SELECT * FROM item_barcodes WHERE id=? FOR UPDATE',array((int)$barcodeId))->row();if(!$b)throw new BusinessException('Barcode perusahaan tidak ditemukan.');$available=(float)$b->qty_sisa-(float)$b->reserved_qty-(float)$b->return_reserved_qty;if((float)$qty>$available+.0001)throw new BusinessException('Koreksi ditolak karena saldo gudang telah digunakan transaksi lain.');$reserved=round((float)$b->reserved_qty+(float)$qty,4);$status=((float)$b->qty_sisa-$reserved-(float)$b->return_reserved_qty)>.0001?'available':'installed';$this->CI->db->where(array('id'=>$b->id,'version'=>$b->version))->update('item_barcodes',array('reserved_qty'=>$reserved,'status'=>$status,'version'=>(int)$b->version+1));if($this->CI->db->affected_rows()!==1)throw new BusinessException('Barcode berubah saat koreksi.');}
private function refreshBarcodeStatus($barcodeId,$fallback){$b=$this->CI->db->query('SELECT * FROM item_barcodes WHERE id=? FOR UPDATE',array((int)$barcodeId))->row();if(!$b)return;$available=(float)$b->qty_sisa-(float)$b->reserved_qty-(float)$b->return_reserved_qty;$this->CI->db->where(array('id'=>$b->id,'version'=>$b->version))->update('item_barcodes',array('status'=>$available>.0001?'available':$fallback,'version'=>(int)$b->version+1));}
private function syncExternal($id){$alloc=$this->CI->db->where(array('company_id'=>$this->company(),'subject_type'=>'customer_equipment','subject_id'=>(int)$id))->where('qty >',0)->order_by('updated_at','DESC')->get('technician_custody_allocations')->result();if(!$alloc){$this->CI->db->where(array('id'=>(int)$id,'company_id'=>$this->company()))->set('lifecycle_status','inactive')->set('updated_at',$this->now())->set('version','version+1',false)->update('customer_equipment_registry');return;}$types=array_unique(array_map(function($a){return$a->custodian_type;},$alloc));$conditions=array_unique(array_map(function($a){return$a->condition_status;},$alloc));$first=$alloc[0];$condition=count($conditions)>1?'mixed':$first->condition_status;$lifecycle=$condition==='inspection_required'?'quarantined':(in_array($condition,array('repair_required','under_repair'),true)?'under_repair':(in_array($condition,array('scrapped'),true)?'disposed':(in_array($condition,array('damaged','lost'),true)?'inactive':'active')));$this->CI->db->where(array('id'=>(int)$id,'company_id'=>$this->company()))->set(array('custody_type'=>count($types)>1?'mixed':$first->custodian_type,'custody_id'=>count($types)>1?null:$first->custodian_id,'current_customer_id'=>$first->customer_id,'usage_status'=>count($types)>1?'mixed':$first->usage_status,'condition_status'=>$condition,'lifecycle_status'=>$lifecycle,'updated_at'=>$this->now()))->set('version','version+1',false)->update('customer_equipment_registry');}
}