session->userdata('company_id'); return $companyId > 0 ? $companyId : 1; } public function get_teknisi_list() { return $this->db ->select('e.id, e.employee_code, e.full_name, p.position_name, d.department_name') ->from('k_employees e') ->join('k_positions p', 'p.id = e.position_id', 'left') ->join('k_departments d', 'd.id = e.department_id', 'left') ->where('e.company_id', $this->companyId()) ->where('e.is_active', 1) ->where('(e.resign_date IS NULL OR e.resign_date = \'0000-00-00\')') ->order_by('e.full_name', 'ASC') ->get() ->result(); } public function get_barang_dibawa($user_id = null, $status = 'active') { $this->db ->select(" it.barcode, it.user_id, it.item_id, it.status, it.created_at, ib.serial_number, i.nama_barang, i.kode_detail, ke.full_name as teknisi_name, ke.employee_code ") ->from('item_technician it') ->join('item_barcodes ib', 'ib.barcode = it.barcode', 'left') ->join('items i', 'i.id = it.item_id', 'left') ->join('k_employees ke', 'ke.id = it.user_id', 'left'); $this->db->where('i.company_id', $this->companyId()); if ($user_id) { $this->db->where('it.user_id', $user_id); } if ($status && in_array($status, ['active', 'inactive'])) { $this->db->where('it.status', $status); } $this->db->order_by('it.created_at', 'DESC'); return $this->db->get()->result(); } public function get_item_by_barcode($barcode) { $barcode = trim($barcode); if ($barcode == '') return null; $row = $this->db ->select('ib.id as barcode_id, ib.item_id, ib.barcode, ib.serial_number, ib.qty_sisa, ib.status as barcode_status, i.nama_barang, i.kode_detail') ->from('item_barcodes ib') ->join('items i', 'i.id = ib.item_id', 'left') ->where('ib.barcode', $barcode) ->where('i.company_id', $this->companyId()) ->where('ib.qty_sisa >', 0) ->get() ->row(); if (!$row) { $row = $this->db ->select('ib.id as barcode_id, ib.item_id, ib.barcode, ib.serial_number, ib.qty_sisa, ib.status as barcode_status, i.nama_barang, i.kode_detail') ->from('item_barcodes ib') ->join('items i', 'i.id = ib.item_id', 'left') ->where('ib.serial_number', $barcode) ->where('i.company_id', $this->companyId()) ->where('ib.qty_sisa >', 0) ->get() ->row(); } return $row; } public function get_current_teknisi($barcode) { return $this->db ->select('it.user_id, ke.full_name, ke.employee_code') ->from('item_technician it') ->join('k_employees ke', 'ke.id = it.user_id', 'left') ->join('items i', 'i.id = it.item_id', 'inner') ->where('it.barcode', $barcode) ->where('it.status', 'active') ->where('i.company_id', $this->companyId()) ->get() ->row(); } public function batch_kembalikan(array $barcodes) { $now = date('Y-m-d H:i:s'); $errors = []; $this->db->trans_start(); foreach ($barcodes as $barcode) { $barcode = trim($barcode); if ($barcode == '') continue; $item = $this->db->query( "SELECT it.*, ib.id AS barcode_id, ib.warehouse_id, ib.qty_sisa, tec.full_name AS teknisi_name FROM item_technician it JOIN item_barcodes ib ON ib.barcode = it.barcode JOIN items i ON i.id = it.item_id AND i.company_id = ? LEFT JOIN k_employees tec ON tec.id = it.user_id WHERE it.barcode = ? AND it.status = 'active' FOR UPDATE", [$this->companyId(), $barcode] )->row(); if (!$item) { $errors[] = "Barcode $barcode tidak ditemukan atau sudah dikembalikan"; continue; } $assigned=(float)($item->assigned_qty?:$item->qty_sisa);$carried=(float)$item->carried_qty;$installed=(float)$item->installed_qty; if($carried>0&&$installed>0){$errors[]="Barcode $barcode memiliki status sebagian dibawa dan sebagian dipasang; gunakan Detail Teknisi untuk menentukan jumlah dan asal pengembalian";continue;} $source=$installed>0?'installed':'carried';$qty=$assigned; $this->db->insert('item_movements', [ 'item_id' => $item->item_id, 'barcode_id' => $item->barcode_id, 'qty' => $qty, 'from_type' => $source === 'installed' ? 'customer' : 'technician', 'from_id' => $source === 'installed' ? $item->customer_id : $item->user_id, 'to_type' => 'warehouse', 'to_id' => $item->warehouse_id, 'movement_type' => 'transfer', 'notes' => (($item->usage_status??'carried') === 'installed' ? 'Pengembalian dari customer melalui teknisi: ' : 'Pengembalian dari teknisi: ') . $item->teknisi_name . ' ke gudang', 'created_at' => $now ]); $this->db ->where('barcode', $barcode) ->where('status', 'active') ->update('item_technician', [ 'status' => 'inactive', 'assigned_qty' => 0, 'carried_qty' => 0, 'installed_qty' => 0, 'deleted_at' => $now ]); $this->db->insert('technician_item_status_logs',array('company_id'=>$this->companyId(),'user_id'=>$item->user_id,'item_id'=>$item->item_id,'barcode_id'=>$item->barcode_id,'barcode'=>$item->barcode,'from_status'=>$source,'to_status'=>'returned','qty'=>$qty,'customer_id'=>$source==='installed'?$item->customer_id:null,'event_date'=>date('Y-m-d'),'notes'=>'Pengembalian batch ke gudang','created_by'=>(int)$this->session->userdata('user_id'),'created_at'=>$now)); if ($item->barcode_id) { $this->db ->where('id', $item->barcode_id) ->update('item_barcodes', [ 'status' => 'available' ]); } } $this->db->trans_complete(); if ($this->db->trans_status() === FALSE) { return ['status' => false, 'message' => 'Gagal memproses pengembalian']; } $success_count = count($barcodes) - count($errors); $msg = "$success_count barang berhasil dikembalikan"; if (count($errors) > 0) { $msg .= '. ' . implode(', ', $errors); } return ['status' => true, 'message' => $msg, 'errors' => $errors]; } public function get_dates_by_employee_id($user_id) { return $this->db ->select("DATE(it.created_at) as tanggal") ->from('item_technician it') ->join('items i', 'i.id = it.item_id', 'inner') ->where('it.user_id', $user_id) ->where('it.status', 'active') ->where('i.company_id', $this->companyId()) ->group_by("DATE(it.created_at)") ->order_by("DATE(it.created_at)", "DESC") ->get() ->result(); } public function batch_dibawa(array $barcodes, $user_id) { $now = date('Y-m-d H:i:s'); $errors = []; $skipped = 0; $technician = $this->db ->select('id, full_name, employee_code') ->from('k_employees') ->where('id', $user_id) ->where('company_id', $this->companyId()) ->get() ->row(); if (!$technician) { return ['status' => false, 'message' => 'Teknisi tidak ditemukan pada perusahaan aktif.']; } $this->db->trans_start(); foreach ($barcodes as $barcode) { $barcode = trim($barcode); if ($barcode == '') continue; $barcode_row = $this->db->query( "SELECT ib.id AS barcode_id, ib.item_id, ib.barcode, ib.warehouse_id,ib.qty_sisa, COALESCE(k.tracking_type,'QTY') tracking_type FROM item_barcodes ib JOIN items i ON i.id = ib.item_id AND i.company_id = ? LEFT JOIN kode_barang k ON k.id=i.kode_id WHERE ib.barcode = ? AND ib.status = 'available' AND ib.qty_sisa > 0 AND COALESCE(ib.reserved_qty, 0) = 0 AND COALESCE(ib.return_reserved_qty, 0) = 0 FOR UPDATE", [$this->companyId(), $barcode] )->row(); if (!$barcode_row) { $errors[] = "Barcode $barcode tidak valid atau tidak tersedia"; continue; } $already = $this->db ->where('barcode', $barcode_row->barcode) ->where('status', 'active') ->get('item_technician') ->row(); if ($already) { if ($already->user_id == $user_id) { $skipped++; continue; } $errors[] = "Barang $barcode sedang dibawa teknisi lain"; continue; } $this->db->insert('item_technician', [ 'user_id' => $user_id, 'company_id' => $this->companyId(), 'barcode' => $barcode_row->barcode, 'item_id' => $barcode_row->item_id, 'status' => 'active', 'usage_status' => 'carried', 'assigned_qty' => (float)$barcode_row->qty_sisa, 'carried_qty' => (float)$barcode_row->qty_sisa, 'installed_qty' => 0, 'created_at' => $now ]); $last_movement = $this->db ->where('barcode_id', $barcode_row->barcode_id) ->order_by('id', 'DESC') ->limit(1) ->get('item_movements') ->row(); $from_type = 'warehouse'; $from_id = $barcode_row->warehouse_id; if ($last_movement) { $from_type = $last_movement->to_type; $from_id = $last_movement->to_id; } $this->db->insert('item_movements', [ 'item_id' => $barcode_row->item_id, 'barcode_id' => $barcode_row->barcode_id, 'qty' => (float)$barcode_row->qty_sisa, 'from_type' => $from_type, 'from_id' => $from_id, 'to_type' => 'technician', 'to_id' => $user_id, 'movement_type' => 'transfer', 'notes' => 'Dibawa oleh: ' . $technician->full_name . ' dari gudang', 'created_at' => $now ]); $this->db ->where('id', $barcode_row->barcode_id) ->update('item_barcodes', [ 'status' => 'installed' ]); } $this->db->trans_complete(); if ($this->db->trans_status() === FALSE) { return ['status' => false, 'message' => 'Gagal memproses penugasan barang']; } $success_count = count($barcodes) - count($errors) - $skipped; $msg = "$success_count barang berhasil ditugaskan"; if ($skipped > 0) { $msg .= ". $skipped barang sudah ditugaskan ke teknisi yang sama"; } if (count($errors) > 0) { $msg .= '. ' . implode(', ', $errors); } return ['status' => true, 'message' => $msg, 'errors' => $errors]; } public function get_teknisi_table_data($start, $length, $search, $order_col, $order_dir) { $sort_cols = [ 1 => 'e.full_name', 2 => 'e.employee_code', 3 => 'd.department_name', 4 => 'item_count' ]; $this->db ->select(" e.id, e.full_name, e.employee_code, p.position_name, d.department_name, (SELECT COUNT(*) FROM item_technician it WHERE it.user_id = e.id AND it.status = 'active') as item_count ") ->from('k_employees e') ->join('k_positions p', 'p.id = e.position_id', 'left') ->join('k_departments d', 'd.id = e.department_id', 'left') ->where('e.company_id', $this->companyId()) ->where('(e.resign_date IS NULL OR e.resign_date = \'0000-00-00\')') ->where_in('LOWER(p.position_name)', ['teknisi', 'noc']); if (!empty($search)) { $this->db->group_start() ->like('LOWER(e.full_name)', strtolower($search)) ->or_like('LOWER(e.employee_code)', strtolower($search)) ->or_like('LOWER(d.department_name)', strtolower($search)) ->group_end(); } if (isset($sort_cols[$order_col])) { $this->db->order_by($sort_cols[$order_col], $order_dir); } else { $this->db->order_by('e.full_name', 'ASC'); } if ($length > 0) { $this->db->limit($length, $start); } return $this->db->get()->result(); } public function count_all_teknisi() { $this->db ->from('k_employees e') ->join('k_positions p', 'p.id = e.position_id', 'left') ->where('e.company_id', $this->companyId()) ->where('(e.resign_date IS NULL OR e.resign_date = \'0000-00-00\')') ->where_in('LOWER(p.position_name)', ['teknisi', 'noc']); return $this->db->count_all_results(); } public function count_filtered_teknisi($search) { $this->db ->from('k_employees e') ->join('k_positions p', 'p.id = e.position_id', 'left') ->join('k_departments d', 'd.id = e.department_id', 'left') ->where('e.company_id', $this->companyId()) ->where('(e.resign_date IS NULL OR e.resign_date = \'0000-00-00\')') ->where_in('LOWER(p.position_name)', ['teknisi', 'noc']); if (!empty($search)) { $this->db->group_start() ->like('LOWER(e.full_name)', strtolower($search)) ->or_like('LOWER(e.employee_code)', strtolower($search)) ->or_like('LOWER(d.department_name)', strtolower($search)) ->group_end(); } return $this->db->count_all_results(); } public function get_teknisi_info($user_id) { return $this->db ->select('e.id, e.full_name, e.employee_code, p.position_name, d.department_name') ->from('k_employees e') ->join('k_positions p', 'p.id = e.position_id', 'left') ->join('k_departments d', 'd.id = e.department_id', 'left') ->where('e.id', $user_id) ->where('e.company_id', $this->companyId()) ->get() ->row(); } public function get_teknisi_items_data($user_id, $start, $length, $search, $order_col, $order_dir) { $sort_cols = [ 1 => 'it.created_at', 2 => 'it.barcode', 3 => 'i.nama_barang', 4 => 'ib.serial_number', 5 => 'it.status' ]; $this->db ->select(" it.barcode, it.user_id, i.nama_barang, ib.serial_number, ib.qty_sisa, COALESCE(k.tracking_type, 'QTY') tracking_type, COALESCE(NULLIF(it.assigned_qty,0),ib.qty_sisa) assigned_qty, COALESCE(it.carried_qty,CASE WHEN COALESCE(it.usage_status,'carried')='carried' THEN ib.qty_sisa ELSE 0 END) carried_qty, COALESCE(it.installed_qty,CASE WHEN it.usage_status='installed' THEN ib.qty_sisa ELSE 0 END) installed_qty, it.status, COALESCE(it.usage_status, 'carried') usage_status, it.usage_date, it.customer_id, c.nama customer_name, ib.id barcode_id, COALESCE((SELECT MAX(l.attachment_id) FROM technician_item_status_logs l WHERE l.company_id=it.company_id AND l.barcode_id=ib.id), (SELECT MAX(ta.id) FROM transaction_attachments ta WHERE ta.entity_type='technician_installation' AND ta.entity_id=ib.id)) proof_id, DATE_FORMAT(it.created_at, '%d/%m/%Y') as tanggal ") ->from('item_technician it') ->join('items i', 'i.id = it.item_id', 'left') ->join('kode_barang k', 'k.id = i.kode_id', 'left') ->join('item_barcodes ib', 'ib.barcode = it.barcode', 'left') ->join('customers c', 'c.id = it.customer_id', 'left') ->where('it.user_id', $user_id) ->where('it.status', 'active') ->where('i.company_id', $this->companyId()); if (!empty($search)) { $this->db->group_start() ->like('it.barcode', $search) ->or_like('LOWER(i.nama_barang)', strtolower($search)) ->or_like('ib.serial_number', $search) ->group_end(); } if (isset($sort_cols[$order_col])) { $this->db->order_by($sort_cols[$order_col], $order_dir); } else { $this->db->order_by('it.created_at', 'DESC'); } if ($length > 0) { $this->db->limit($length, $start); } return $this->db->get()->result(); } public function count_all_teknisi_items($user_id) { $this->db ->from('item_technician it') ->join('items i', 'i.id = it.item_id', 'inner') ->where('it.user_id', $user_id) ->where('it.status', 'active') ->where('i.company_id', $this->companyId()); return $this->db->count_all_results(); } public function count_filtered_teknisi_items($user_id, $search) { $this->db ->from('item_technician it') ->join('items i', 'i.id = it.item_id', 'left') ->join('item_barcodes ib', 'ib.barcode = it.barcode', 'left') ->where('it.user_id', $user_id) ->where('it.status', 'active') ->where('i.company_id', $this->companyId()); if (!empty($search)) { $this->db->group_start() ->like('it.barcode', $search) ->or_like('LOWER(i.nama_barang)', strtolower($search)) ->or_like('ib.serial_number', $search) ->group_end(); } return $this->db->count_all_results(); } public function get_transfer_data() { return $this->db ->select(" it.barcode, i.nama_barang, ib.serial_number, from_ke.full_name as from_name, from_ke.employee_code as from_code, to_ke.full_name as to_name, to_ke.employee_code as to_code, im.created_at, im.movement_type ") ->from('item_movements im') ->join('items i', 'i.id = im.item_id', 'left') ->join('item_barcodes ib', 'ib.id = im.barcode_id', 'left') ->join('k_employees from_ke', 'from_ke.id = im.from_id', 'left') ->join('k_employees to_ke', 'to_ke.id = im.to_id', 'left') ->where_in('im.movement_type', ['installation', 'transfer']) ->where('im.from_type', 'technician') ->where('im.to_type', 'technician') ->where('i.company_id', $this->companyId()) ->order_by('im.created_at', 'DESC') ->limit(200) ->get() ->result(); } public function detail_by_barcode($barcode) { return $this->db ->select(" it.*, ib.serial_number, ib.qty_sisa, ib.status as barcode_status, i.nama_barang, i.kode_detail, ke.full_name as teknisi_name, ke.employee_code ") ->from('item_technician it') ->join('item_barcodes ib', 'ib.barcode = it.barcode', 'left') ->join('items i', 'i.id = it.item_id', 'left') ->join('k_employees ke', 'ke.id = it.user_id', 'left') ->where('it.barcode', $barcode) ->where('i.company_id', $this->companyId()) ->get() ->row(); } public function get_teknisi_items_full($user_id, $tanggal = null) { $this->db ->select(" it.barcode, i.nama_barang, ib.serial_number, ib.qty_sisa, it.status, DATE_FORMAT(it.created_at, '%d/%m/%Y') as tanggal ") ->from('item_technician it') ->join('items i', 'i.id = it.item_id', 'left') ->join('item_barcodes ib', 'ib.barcode = it.barcode', 'left') ->where('it.user_id', $user_id) ->where('it.status', 'active') ->where('i.company_id', $this->companyId()); if ($tanggal AND $tanggal != 'all') { $this->db->where('DATE(it.created_at)', $tanggal); } return $this->db ->order_by('it.created_at', 'ASC') ->get() ->result(); } public function generate_no_faktur() { return 'GRJN/K/' . date('d/m/Y'); } public function get_log_barang() { return $this->get_log_barang_filtered(array()); } public function get_log_barang_filtered(array $filters) { $this->db ->select(" it.barcode, it.item_id, it.user_id, it.status, COALESCE(it.usage_status, 'carried') usage_status, COALESCE(it.assigned_qty,0) assigned_qty, COALESCE(it.carried_qty,0) carried_qty, COALESCE(it.installed_qty,0) installed_qty, it.usage_date, it.customer_id, it.usage_notes, it.created_at, it.deleted_at, ke.full_name as teknisi_name, ke.employee_code, c.nama customer_name, i.kode_detail as item_code, i.nama_barang, ib.serial_number, ib.qty_sisa, (SELECT COUNT(*) FROM transaction_attachments ta WHERE ta.entity_type='technician_installation' AND ta.entity_id=ib.id) attachment_count ") ->from('item_technician it') ->join('k_employees ke', 'ke.id = it.user_id', 'left') ->join('items i', 'i.id = it.item_id', 'left') ->join('item_barcodes ib', 'ib.barcode = it.barcode', 'left') ->join('customers c', 'c.id = it.customer_id', 'left') ->where('i.company_id', $this->companyId()); if(!empty($filters['user_id']))$this->db->where('it.user_id',(int)$filters['user_id']); if(!empty($filters['customer_id']))$this->db->where('it.customer_id',(int)$filters['customer_id']); $status=trim((string)($filters['status']??'')); if($status==='returned'){ $this->db->where('it.status','inactive'); }elseif($status==='installed'){ $this->db->where('it.status','active')->where('it.installed_qty >',0); }elseif($status==='carried'){ $this->db->where('it.status','active')->where('it.carried_qty >',0); } $from=trim((string)($filters['date_from']??''));$to=trim((string)($filters['date_to']??'')); if($from!==''||$to!==''){ $from=$from!==''?$from:'1900-01-01';$to=$to!==''?$to:'2999-12-31'; $this->db->group_start()->where("DATE(it.created_at) BETWEEN ".$this->db->escape($from).' AND '.$this->db->escape($to),null,false)->or_where("it.usage_date BETWEEN ".$this->db->escape($from).' AND '.$this->db->escape($to),null,false)->or_where("DATE(it.deleted_at) BETWEEN ".$this->db->escape($from).' AND '.$this->db->escape($to),null,false)->group_end(); } return $this->db ->order_by('it.created_at', 'DESC') ->get() ->result(); } public function change_item_status($barcode,$userId,array $data,$actorId) { $barcode=trim((string)$barcode);$company=$this->companyId();$now=date('Y-m-d H:i:s'); $this->db->trans_begin(); try{ $key=trim((string)($data['idempotency_key']??'')); if($key!==''){$duplicate=$this->db->where(array('company_id'=>$company,'idempotency_key'=>$key))->get('technician_item_status_logs')->row();if($duplicate){$this->db->trans_commit();return array('duplicate'=>true,'log_id'=>(int)$duplicate->id,'qty'=>(float)$duplicate->qty,'status'=>$duplicate->to_status);}} $row=$this->db->query("SELECT it.*,ib.id barcode_id,ib.qty_sisa,ib.warehouse_id,i.nama_barang,e.full_name technician_name,COALESCE(k.tracking_type,'QTY') tracking_type FROM item_technician it JOIN item_barcodes ib ON ib.barcode=it.barcode JOIN items i ON i.id=it.item_id JOIN k_employees e ON e.id=it.user_id LEFT JOIN kode_barang k ON k.id=i.kode_id WHERE it.company_id=? AND it.user_id=? AND it.barcode=? AND it.status='active' FOR UPDATE",array($company,(int)$userId,$barcode))->row(); if(!$row)throw new RuntimeException('Barang aktif pada teknisi tidak ditemukan.'); $target=(string)$data['status'];$tracking=strtoupper((string)$row->tracking_type)==='UNIT'?'UNIT':'QTY'; $assigned=(float)$row->assigned_qty;$carried=(float)$row->carried_qty;$installed=(float)$row->installed_qty; if($assigned<=0){$assigned=(float)$row->qty_sisa;if(($row->usage_status??'carried')==='installed')$installed=$assigned;else$carried=$assigned;} $qty=$tracking==='UNIT'?1:(float)($data['qty']??0); if($qty<=0)throw new RuntimeException('Jumlah yang diubah wajib lebih dari 0.'); $source='';$customerId=null;$customerName='-'; if($target==='installed'){$source='carried';$available=$carried;$customerId=(int)($data['customer_id']??0);if($customerId<1)throw new RuntimeException('Customer wajib dipilih untuk status Dipasang.');$customer=$this->db->select('id,nama')->where(array('id'=>$customerId,'company_id'=>$company))->get('customers')->row();if(!$customer)throw new RuntimeException('Customer tidak ditemukan pada perusahaan aktif.');$customerName=$customer->nama;if($installed>0&&!empty($row->customer_id)&&(int)$row->customer_id!==$customerId)throw new RuntimeException('Sebagian barcode ini sudah dipasang pada customer lain. Kembalikan dahulu ke teknisi sebelum memilih customer berbeda.');} elseif($target==='carried'){$source='installed';$available=$installed;$customerId=(int)$row->customer_id;if($customerId<1)throw new RuntimeException('Customer asal pemasangan tidak ditemukan.');$customer=$this->db->select('nama')->where(array('id'=>$customerId,'company_id'=>$company))->get('customers')->row();$customerName=$customer?$customer->nama:'-';} elseif($target==='warehouse'){$source=(string)($data['source_status']??'');if(!in_array($source,array('carried','installed'),true)){if($carried>0&&$installed<=0)$source='carried';elseif($installed>0&&$carried<=0)$source='installed';else throw new RuntimeException('Pilih asal barang yang akan dikembalikan ke gudang.');}$available=$source==='installed'?$installed:$carried;if($source==='installed'){$customerId=(int)$row->customer_id;$customer=$customerId?$this->db->select('nama')->where(array('id'=>$customerId,'company_id'=>$company))->get('customers')->row():null;$customerName=$customer?$customer->nama:'-';}} else throw new RuntimeException('Status tujuan tidak valid.'); if($qty>$available+0.00001)throw new RuntimeException('Jumlah melebihi saldo status asal ('.number_format($available,4,',','.').').'); if($source==='carried')$carried-=$qty;else$installed-=$qty; if($target==='installed')$installed+=$qty;elseif($target==='carried')$carried+=$qty;else$assigned-=$qty; $carried=max(0,$carried);$installed=max(0,$installed);$assigned=max(0,$assigned); $usage=$installed>0&&$carried>0?'mixed':($installed>0?'installed':'carried');$inactive=$assigned<=0.00001; $update=array('assigned_qty'=>$assigned,'carried_qty'=>$carried,'installed_qty'=>$installed,'usage_status'=>$usage,'usage_date'=>$installed>0?($target==='installed'?$data['date']:$row->usage_date):null,'customer_id'=>$installed>0?$customerId:null,'usage_notes'=>$data['notes'],'updated_by'=>(int)$actorId,'updated_at'=>$now); if($inactive){$update['status']='inactive';$update['deleted_at']=$now;} $this->db->where(array('company_id'=>$company,'user_id'=>(int)$userId,'barcode'=>$barcode,'status'=>'active'))->update('item_technician',$update); $labels=array('carried'=>'Dibawa Teknisi','installed'=>'Dipasang di Customer','warehouse'=>'Dikembalikan ke Gudang'); $notes='[AUTO] Perubahan status peralatan | Barang '.$row->nama_barang.' | Barcode '.$row->barcode.' | Teknisi '.$row->technician_name.' | Dari '.$labels[$source].' ke '.$labels[$target].' | Qty '.number_format($qty,4,',','.').' | Tanggal '.$data['date'].' | Customer '.$customerName.' | Keterangan '.$data['notes']; $movement=array('item_id'=>$row->item_id,'barcode_id'=>$row->barcode_id,'qty'=>$qty,'from_type'=>$source==='installed'?'customer':'technician','from_id'=>$source==='installed'?$customerId:$row->user_id,'to_type'=>$target==='installed'?'customer':($target==='carried'?'technician':'warehouse'),'to_id'=>$target==='installed'?$customerId:($target==='carried'?$row->user_id:$row->warehouse_id),'movement_type'=>$target==='installed'?'installation':($target==='carried'?'uninstallation':'transfer'),'notes'=>mb_substr($notes,0,250),'created_at'=>$now); $this->db->insert('item_movements',$movement); $this->db->insert('technician_item_status_logs',array('company_id'=>$company,'user_id'=>(int)$userId,'item_id'=>$row->item_id,'barcode_id'=>$row->barcode_id,'barcode'=>$row->barcode,'from_status'=>$source,'to_status'=>$target==='warehouse'?'returned':$target,'qty'=>$qty,'customer_id'=>$customerId?:null,'event_date'=>$data['date'],'notes'=>$data['notes'],'idempotency_key'=>$key?:null,'created_by'=>(int)$actorId,'created_at'=>$now)); $logId=(int)$this->db->insert_id(); $this->db->where('id',$row->barcode_id)->update('item_barcodes',array('status'=>$inactive?'available':'installed')); if($this->db->trans_status()===false)throw new RuntimeException('Perubahan status barang gagal disimpan.'); $this->db->trans_commit(); return array('log_id'=>$logId,'barcode_id'=>(int)$row->barcode_id,'status'=>$target,'qty'=>$qty,'customer_name'=>$customerName,'technician_name'=>$row->technician_name,'remaining_qty'=>$assigned); }catch(Throwable$e){$this->db->trans_rollback();throw$e;} } public function get_status_history($userId) { return $this->db->select('l.*,i.kode_detail,i.nama_barang,c.nama customer_name,u.nama actor_name')->from('technician_item_status_logs l')->join('items i','i.id=l.item_id','left')->join('customers c','c.id=l.customer_id','left')->join('users u','u.id=l.created_by','left')->where(array('l.company_id'=>$this->companyId(),'l.user_id'=>(int)$userId))->order_by('l.event_date','DESC')->order_by('l.id','DESC')->get()->result(); } }