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

709 lines
32 KiB
PHP

<?php
defined('BASEPATH') OR exit('No direct script access allowed');
class PeralatanTeknisi_model extends CI_Model {
public function __construct()
{
parent::__construct();
}
private function companyId()
{
$companyId = (int) $this->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();
}
}