load->model('PeralatanTeknisi_model', 'ptm'); $this->load->library('TechnicianEquipmentService'); if (!$this->session->userdata('logged_in')) { redirect('auth'); } } public function index() { $this->renderEquipmentWorkbench('overview'); } public function get_data() { $user_id = $this->input->get('user_id'); $status = $this->input->get('status') ?: 'active'; log_message('debug', '[get_data] user_id: ' . var_export($user_id, true) . ', status: ' . $status); $data = $this->ptm->get_barang_dibawa($user_id, $status); log_message('debug', '[get_data] query returned ' . count($data) . ' rows'); $result = []; $no = 1; foreach ($data as $row) { $result[] = [ $no++, htmlspecialchars($row->barcode, ENT_QUOTES, 'UTF-8'), htmlspecialchars($row->nama_barang, ENT_QUOTES, 'UTF-8'), htmlspecialchars($row->serial_number, ENT_QUOTES, 'UTF-8'), ]; } log_message('debug', '[get_data] response: ' . json_encode(['data' => $result])); echo json_encode(['data' => $result]); } public function scan_item() { $barcode = trim($this->input->get('barcode')); if (empty($barcode)) { echo json_encode(['status' => false, 'message' => 'QR Code tidak boleh kosong']); return; } $item = $this->ptm->get_item_by_barcode($barcode); if (!$item) { echo json_encode(['status' => false, 'message' => 'Barcode tidak ditemukan']); return; } $teknisi = $this->ptm->get_current_teknisi($item->barcode); echo json_encode([ 'status' => true, 'message' => 'Barang ditemukan', 'data' => [ 'barcode' => $item->barcode, 'nama_barang' => $item->nama_barang, 'serial_number' => $item->serial_number, 'barcode_id' => $item->barcode_id, 'item_id' => $item->item_id, 'barcode_status' => $item->barcode_status, 'teknisi_id' => $teknisi ? $teknisi->user_id : null, 'teknisi_name' => $teknisi ? $teknisi->full_name : null, 'teknisi_code' => $teknisi ? $teknisi->employee_code : null ] ]); } function get_date_by_employee_id() { $user_id = (int)$this->input->get('user_id'); if (!$user_id) { echo json_encode(['status' => false, 'message' => 'ID teknisi tidak valid']); return; } $dates = $this->ptm->get_dates_by_employee_id($user_id); echo json_encode(['status' => true, 'data' => $dates]); } public function batch_kembalikan() { if(strtoupper($this->input->method())==='POST')return json_response(false,'Alur lama dinonaktifkan. Gunakan Scan Peralatan agar pengembalian masuk pemeriksaan gudang.',array(),409); if (strtoupper($this->input->method()) !== 'POST') show_404(); $barcodes = $this->input->post('barcodes'); if (empty($barcodes) || !is_array($barcodes)) { echo json_encode(['status' => false, 'message' => 'Tidak ada barcode yang dipilih']); return; } $result = $this->ptm->batch_kembalikan($barcodes); echo json_encode($result); } public function batch_dibawa() { if(strtoupper($this->input->method())==='POST')return json_response(false,'Alur lama dinonaktifkan. Gunakan Scan Peralatan dan Serahkan ke Teknisi.',array(),409); if (strtoupper($this->input->method()) !== 'POST') show_404(); $barcodes = $this->input->post('barcodes'); $user_id = (int)$this->input->post('user_id'); if (empty($barcodes) || !is_array($barcodes)) { echo json_encode(['status' => false, 'message' => 'Tidak ada barcode yang dipilih']); return; } if (empty($user_id)) { echo json_encode(['status' => false, 'message' => 'Pilih teknisi terlebih dahulu']); return; } $result = $this->ptm->batch_dibawa($barcodes, $user_id); echo json_encode($result); } public function get_teknisi_table() { $draw = (int)$this->input->get('draw'); $start = (int)$this->input->get('start'); $length = (int)$this->input->get('length'); $search = $this->input->get('search')['value'] ?? ''; $order_col = (int)$this->input->get('order')[0]['column'] ?? 1; $order_dir = $this->input->get('order')[0]['dir'] ?? 'asc'; $data = $this->ptm->get_teknisi_table_data($start, $length, $search, $order_col, $order_dir); $total_filtered = $this->ptm->count_filtered_teknisi($search); $total_all = $this->ptm->count_all_teknisi(); $result = []; $no = $start + 1; foreach ($data as $row) { $item_count = (int)$row->item_count; $item_text = $item_count . ' Item'; $aksi = ' '; $result[] = [ $no++, htmlspecialchars($row->full_name, ENT_QUOTES, 'UTF-8'), htmlspecialchars($row->employee_code, ENT_QUOTES, 'UTF-8'), htmlspecialchars($row->department_name, ENT_QUOTES, 'UTF-8') ?: '-', $item_text, $aksi ]; } echo json_encode([ 'draw' => $draw, 'recordsTotal' => $total_all, 'recordsFiltered' => $total_filtered, 'data' => $result ]); } public function teknisi_detail() { $user_id = (int)$this->input->get('user_id'); if (!$user_id) { echo json_encode(null); return; } $info = $this->ptm->get_teknisi_info($user_id); echo json_encode($info); } public function get_teknisi_items() { $user_id=(int)$this->input->get('user_id');$draw=(int)$this->input->get('draw');$start=(int)$this->input->get('start');$length=(int)$this->input->get('length');$search=$this->input->get('search')['value']??'';$order_col=(int)($this->input->get('order')[0]['column']??2);$order_dir=$this->input->get('order')[0]['dir']??'asc'; if(!$user_id){echo json_encode(array('draw'=>$draw,'recordsTotal'=>0,'recordsFiltered'=>0,'data'=>array()));return;} $data=$this->ptm->get_teknisi_items_data($user_id,$start,$length,$search,$order_col,$order_dir);$result=array();$no=$start+1; foreach($data as$row){ $carried=(float)$row->carried_qty;$installed=(float)$row->installed_qty;$tracking=strtoupper((string)$row->tracking_type)==='UNIT'?'UNIT':'QTY'; if($carried>0&&$installed>0)$badge='Sebagian DipasangDibawa '.number_format($carried,0,',','.').' · Dipasang '.number_format($installed,0,',','.').''; elseif($installed>0)$badge='Dipasang'.htmlspecialchars(($row->customer_name?:'-').' · '.($row->usage_date?:'-'),ENT_QUOTES,'UTF-8').''; else$badge='Dibawa'; $action='-'; if(is_master_admin_user()||check_permission('items','can_update')){ $action=''; if(!empty($row->proof_id))$action.=' '; }elseif(!empty($row->proof_id))$action='Bukti'; $result[]=array($no++,$row->tanggal??date('d/m/Y'),htmlspecialchars($row->barcode,ENT_QUOTES,'UTF-8'),htmlspecialchars($row->nama_barang,ENT_QUOTES,'UTF-8'),htmlspecialchars($row->serial_number,ENT_QUOTES,'UTF-8')?:'-',number_format((float)$row->assigned_qty,0,',','.'),$badge,$action); } echo json_encode(array('draw'=>$draw,'recordsTotal'=>$this->ptm->count_all_teknisi_items($user_id),'recordsFiltered'=>$this->ptm->count_filtered_teknisi_items($user_id,$search),'data'=>$result)); } public function get_form_barang_bawaan_data() { $user_id = (int)$this->input->get('user_id'); if (!$user_id) { echo json_encode(['status' => false, 'message' => 'ID teknisi tidak valid']); return; } $info = $this->ptm->get_teknisi_info($user_id); if (!$info) { echo json_encode(['status' => false, 'message' => 'Teknisi tidak ditemukan']); return; } $tanggal_param = $this->input->get('tanggal'); $items = []; if ($tanggal_param) { $items = $this->ptm->get_teknisi_items_full($user_id, $tanggal_param); } if ($tanggal_param) { if ($tanggal_param === 'all') { $noFaktur = 'GRJN/K/ALL' . date('/m/Y'); $tanggal = date('d F Y'); } else { $ts = strtotime($tanggal_param); $noFaktur = 'GRJN/K/' . date('d/m/Y', $ts); $tanggal = date('d F Y', $ts); } } else { $noFaktur = 'GRJN/K/' . date('d/m/Y'); $tanggal = date('d F Y'); } $itemsData = []; $no = 1; foreach ($items as $item) { $itemsData[] = [ 'no' => $no++, 'tanggal' => $item->tanggal ?? date('d/m/Y'), 'barcode' => $item->barcode ?? '-', 'nama_barang' => $item->nama_barang ?? '-', 'serial_number' => $item->serial_number ?? '-', 'jumlah' => isset($item->qty_sisa) ? (int)$item->qty_sisa : 1, 'keterangan' => '' ]; } echo json_encode([ 'status' => true, 'data' => [ 'no_faktur' => $noFaktur, 'tanggal' => $tanggal, 'teknisi' => $info->full_name, 'teknisi_id' => $info->id, 'employee_code' => $info->employee_code, 'position_name' => $info->position_name, 'department_name' => $info->department_name, 'total_item' => count($itemsData), 'items' => $itemsData ] ]); } public function generate_pdf_barang_bawaan($user_id = null) { return $this->technician_inventory_pdf($user_id); $user_id = (int)$user_id; if (!$user_id) { show_error('ID teknisi tidak valid', 400); } if (ob_get_length()) { ob_end_clean(); } $info = $this->ptm->get_teknisi_info($user_id); if (!$info) { show_error('Teknisi tidak ditemukan', 404); } $tanggal_param = $this->input->get('tanggal'); if ($tanggal_param === 'all') { $items = $this->ptm->get_teknisi_items_full($user_id); $noFaktur = 'GRJN/K/ALL' . date('/m/Y'); $tanggal = 'Semua Tanggal'; } elseif ($tanggal_param) { $ts = strtotime($tanggal_param); $items = $this->ptm->get_teknisi_items_full($user_id, $tanggal_param); $noFaktur = 'GRJN/K/' . date('d/m/Y', $ts); $tanggal = date('d/m/Y', $ts); } else { $items = $this->ptm->get_teknisi_items_full($user_id); $noFaktur = 'GRJN/K/' . date('d/m/Y'); $tanggal = date('d/m/Y'); } require_once(APPPATH . 'third_party/fpdf/fpdf.php'); require_once(APPPATH . 'libraries/PDF_BarangBawaan.php'); $pdf = new PDF_BarangBawaan('L', 'mm', 'A5'); $pdf->AliasNbPages(); $pdf->SetMargins(5, 5, 5); $pdf->SetAutoPageBreak(true, 10); $pdf->setFormInfo($noFaktur, $tanggal, $info->full_name); $pdf->AddPage('L', 'A5'); $pdf->BarangBawaanTable($items); $pdf->SignatureArea(); $namaFile = 'FORM_BARANG_BAWAAN_' . str_replace(' ', '_', $info->full_name) . '_' . date('Ymd') . '.pdf'; $pdf->Output('I', $namaFile); exit; } public function get_transfer_data() { $data = $this->ptm->get_transfer_data(); $result = []; $no = 1; foreach ($data as $row) { $result[] = [ $no++, htmlspecialchars($row->barcode, ENT_QUOTES, 'UTF-8'), htmlspecialchars($row->nama_barang, ENT_QUOTES, 'UTF-8'), htmlspecialchars(($row->from_code ?? '') . ' - ' . ($row->from_name ?? ''), ENT_QUOTES, 'UTF-8'), htmlspecialchars(($row->to_code ?? '') . ' - ' . ($row->to_name ?? ''), ENT_QUOTES, 'UTF-8'), htmlspecialchars($row->created_at, ENT_QUOTES, 'UTF-8'), 'Selesai' ]; } echo json_encode(['data' => $result]); } public function batch_transfer() { if(strtoupper($this->input->method())==='POST')return json_response(false,'Alur lama dinonaktifkan. Gunakan tindakan Transfer Teknisi pada hasil scan.',array(),409); if (strtoupper($this->input->method()) !== 'POST') { show_404(); } $barcodes = $this->input->post('barcodes'); $from_user_id = (int)$this->input->post('from_user_id'); $to_user_id = (int)$this->input->post('to_user_id'); $technician = $this->ptm->get_teknisi_info($to_user_id); log_message('debug', 'batch_transfer POST: ' . json_encode([ 'barcodes' => $barcodes, 'from_user_id' => $from_user_id, 'to_user_id' => $to_user_id ])); if (empty($barcodes) || !is_array($barcodes)) { echo json_encode(['status' => false, 'message' => 'Tidak ada barcode yang dipilih']); return; } if (empty($from_user_id) || empty($to_user_id)) { echo json_encode(['status' => false, 'message' => 'Pilih teknisi asal dan tujuan']); return; } if ($from_user_id == $to_user_id) { echo json_encode(['status' => false, 'message' => 'Teknisi tujuan tidak boleh sama dengan teknisi asal']); return; } if (!$technician || !$this->ptm->get_teknisi_info($from_user_id)) { echo json_encode(['status' => false, 'message' => 'Teknisi asal atau tujuan tidak ditemukan pada perusahaan aktif']); return; } $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.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' AND COALESCE(it.usage_status,'carried') = 'carried' AND it.user_id = ? FOR UPDATE", [(int)$this->companycontext->id(), $barcode, $from_user_id] )->row(); if (!$item) { $errors[] = "Barcode $barcode tidak ditemukan pada teknisi asal"; continue; } log_message('debug', 'batch_transfer item: ' . json_encode($item)); $this->db ->where('barcode', $item->barcode) ->where('user_id', $from_user_id) ->where('status', 'active') ->update('item_technician', [ 'user_id' => $to_user_id, 'usage_status' => 'carried', 'usage_date' => null, 'customer_id' => null, 'usage_notes' => null, 'updated_by' => (int)$this->session->userdata('user_id'), 'updated_at' => $now ]); $this->db->insert('item_movements', [ 'item_id' => $item->item_id, 'barcode_id' => $item->barcode_id, 'qty' => (float)($item->assigned_qty?:$item->qty_sisa), 'from_type' => 'technician', 'from_id' => $from_user_id, 'to_type' => 'technician', 'to_id' => $to_user_id, 'movement_type' => 'transfer', 'notes' => 'Transfer Barang dari: ' . $item->teknisi_name . ' ke ' . $technician->full_name, 'created_at' => $now ]); } $this->db->trans_complete(); if ($this->db->trans_status() === FALSE) { $db_error = $this->db->error(); log_message('error', 'batch_transfer gagal: ' . json_encode($db_error)); echo json_encode([ 'status' => false, 'message' => 'Gagal memproses transfer barang teknisi.' ]); return; } $success_count = count($barcodes) - count($errors); $msg = "$success_count barang berhasil ditransfer"; if (count($errors) > 0) { $msg .= '. ' . implode(', ', $errors); } echo json_encode(['status' => true, 'message' => $msg, 'errors' => $errors]); } public function get_log_data() { $this->equipmentPermission('technician_equipment','can_view');$rows=array();$labels=array('company'=>'Perusahaan','customer'=>'Customer','transferred_non_inventory'=>'Non-Persediaan Diserahkan','unknown'=>'Belum Diketahui','carried'=>'Dibawa','installed'=>'Dipasang','removed'=>'Dilepas','returned'=>'Dikembalikan','good'=>'Baik','inspection_required'=>'Perlu Diperiksa','repair_required'=>'Perlu Perbaikan','under_repair'=>'Dalam Perbaikan','damaged'=>'Rusak','lost'=>'Hilang','scrapped'=>'Dihapuskan'); foreach($this->technicianequipmentservice->history($this->logFilters())as$index=>$row){$rows[]=array($index+1,html_escape($row['document_no']).'
'.html_escape(date('d/m/Y',strtotime($row['document_date']))).'',html_escape($row['technician_name']?:'-'),html_escape($row['display_name']),html_escape($row['barcode_snapshot']).'
'.html_escape($row['serial_number']?:'-').'',html_escape($labels[$row['ownership_type']]??$row['ownership_type']),number_format((float)$row['qty'],abs((float)$row['qty']-round((float)$row['qty']))<.0001?0:4,',','.'),html_escape($row['customer_name']?:'-'),html_escape(($row['from_custodian_type']?:'-').' → '.($row['to_custodian_type']?:'-')),html_escape($labels[$row['to_usage_status']]??$row['to_usage_status']),html_escape($labels[$row['to_condition_status']]??$row['to_condition_status']),html_escape($row['document_notes']?:$row['notes']?:'-'),'');} return json_response(true,'Riwayat peralatan.',array('data'=>$rows)); } public function log_barang() { $this->equipmentPermission('technician_equipment','can_view'); $data = [ "active_menu" => "peralatan_teknisi", "teknisi_list" => $this->ptm->get_teknisi_list(), "customers" => $this->db->select('id,nama')->where('company_id',(int)$this->companycontext->id())->order_by('nama')->get('customers')->result(), "can_export" => is_master_admin_user() || check_permission('technician_equipment','can_export') || check_permission('items','can_export'), "equipment_permissions" => $this->equipmentPermissionFlags(), "equipment_dashboard" => $this->scopedEquipmentDashboard() ]; $this->load->view('partials/header', $data); $this->load->view('peralatan_teknisi/log_barang', $data); $this->load->view('partials/footer'); } public function detail($userId=0) { $this->equipmentPermission('technician_equipment','can_view');$userId=(int)$userId;if($this->restrictedTechnicianEmployee()&&$userId!==$this->restrictedTechnicianEmployee())show_error('Teknisi hanya dapat melihat barang dalam penguasaannya.',403,'Akses ditolak');$technician=$this->ptm->get_teknisi_info($userId);if(!$technician)show_404(); $data=array('active_menu'=>'peralatan_teknisi','technician'=>$technician,'allocations'=>$this->technicianequipmentservice->allocations(null,null,$userId),'documents'=>$this->technicianequipmentservice->documents(array('technician_id'=>$userId)),'equipment_permissions'=>$this->equipmentPermissionFlags(),'equipment_dashboard'=>$this->scopedEquipmentDashboard()); $this->load->view('partials/header',$data);$this->load->view('peralatan_teknisi/detail_professional',$data);$this->load->view('partials/footer'); } public function get_status_history_data($userId=0) { $this->requirePermission('items','can_view');$userId=(int)$userId;if(!$this->ptm->get_teknisi_info($userId))return json_response(false,'Teknisi tidak ditemukan.',array('data'=>array()),404); $labels=array('initial'=>'Saldo Awal','carried'=>'Dibawa Teknisi','installed'=>'Dipasang','returned'=>'Kembali ke Gudang','warehouse'=>'Gudang');$rows=array(); foreach($this->ptm->get_status_history($userId)as$index=>$row){$proof=$row->attachment_id?'':'-';$rows[]=array($index+1,date('d/m/Y',strtotime($row->event_date)),htmlspecialchars(($row->kode_detail?:'-').' - '.($row->nama_barang?:'-'),ENT_QUOTES,'UTF-8'),htmlspecialchars($row->barcode,ENT_QUOTES,'UTF-8'),htmlspecialchars($labels[$row->from_status]??ucfirst($row->from_status),ENT_QUOTES,'UTF-8').' → '.htmlspecialchars($labels[$row->to_status]??ucfirst($row->to_status),ENT_QUOTES,'UTF-8'),number_format((float)$row->qty,0,',','.'),htmlspecialchars($row->customer_name?:'-',ENT_QUOTES,'UTF-8'),htmlspecialchars($row->notes?:'-',ENT_QUOTES,'UTF-8'),htmlspecialchars($row->actor_name?:'-',ENT_QUOTES,'UTF-8'),$proof);} return json_response(true,'Riwayat status peralatan.',array('data'=>$rows)); } public function update_item_status() { if(strtoupper($this->input->method())==='POST')return json_response(false,'Alur status lama dinonaktifkan. Gunakan workbench Peralatan Teknisi agar histori dan saldo tetap sinkron.',array(),409); if(strtoupper($this->input->method())!=='POST')show_404(); $this->requirePermission('items','can_update'); $barcode=trim((string)$this->input->post('barcode',true));$userId=(int)$this->input->post('user_id');$status=trim((string)$this->input->post('status',true));$date=trim((string)$this->input->post('date',true));$customer=(int)$this->input->post('customer_id');$notes=trim((string)$this->input->post('notes',true));$qty=(float)$this->input->post('qty');$source=trim((string)$this->input->post('source_status',true));$key=trim((string)$this->input->post('idempotency_key',true)); if($barcode===''||$userId<1)return json_response(false,'Barang teknisi tidak valid.',array(),422); if(!in_array($status,array('installed','carried','warehouse'),true))return json_response(false,'Status tujuan tidak valid.',array(),422); $parsed=DateTime::createFromFormat('Y-m-d',$date);if(!$parsed||$parsed->format('Y-m-d')!==$date)return json_response(false,'Tanggal perubahan status wajib diisi dengan benar.',array(),422); if($status==='installed'&&$customer<1)return json_response(false,'Customer wajib dipilih untuk status Dipasang.',array(),422); if($notes==='')return json_response(false,'Keterangan perubahan status wajib diisi.',array(),422); if(isset($_FILES['proof'])&&!empty($_FILES['proof']['tmp_name'])){$mime=$this->validInstallationImage($_FILES['proof']);if(!$mime)return json_response(false,'Bukti harus berupa gambar JPG, PNG, WEBP, GIF, BMP, HEIC, atau AVIF maksimal 5 MB.',array(),422);}else$mime=null; $storedPath=null;$this->db->trans_begin(); try{ $result=$this->ptm->change_item_status($barcode,$userId,array('status'=>$status,'date'=>$date,'customer_id'=>$customer,'notes'=>$notes,'qty'=>$qty,'source_status'=>$source,'idempotency_key'=>$key),(int)$this->session->userdata('user_id')); if($mime&&!empty($result['log_id'])){$attachment=$this->saveInstallationImage((int)$result['log_id'],$_FILES['proof'],$mime);$storedPath=$attachment['path'];$this->db->where('id',(int)$result['log_id'])->update('technician_item_status_logs',array('attachment_id'=>(int)$attachment['id']));} if($this->db->trans_status()===false)throw new RuntimeException('Perubahan status barang gagal disimpan.');$this->db->trans_commit(); $labels=array('installed'=>'Dipasang','carried'=>'Dibawa Teknisi','warehouse'=>'Dikembalikan ke Gudang');log_activity('items','update','Mengubah status barcode '.$barcode.' menjadi '.$labels[$status].' sebanyak '.($result['qty']??0),'success'); return json_response(true,'Status barang berhasil diperbarui menjadi '.$labels[$status].'.',array('result'=>$result)); }catch(RuntimeException$e){$this->db->trans_rollback();if($storedPath&&is_file($storedPath))unlink($storedPath);return json_response(false,$e->getMessage(),array(),422);}catch(Throwable$e){$this->db->trans_rollback();if($storedPath&&is_file($storedPath))unlink($storedPath);return business_exception_response($e);} } public function log_pdf() { $this->equipmentPermission('technician_equipment','can_export');$filters=$this->logFilters();$rows=$this->technicianequipmentservice->history($filters);$company=$this->db->get_where('companies',array('id'=>(int)$this->companycontext->id()))->row(); require_once FCPATH.'vendor/autoload.php';$html=$this->load->view('peralatan_teknisi/log_pdf',array('rows'=>$rows,'filters'=>$filters,'company'=>$company),true);$pdf=new Dompdf\Dompdf();$pdf->loadHtml($html);$pdf->setPaper('A4','landscape');$pdf->render();$pdf->stream('Log-Peralatan-Teknisi-'.date('Ymd-His').'.pdf',array('Attachment'=>false)); } private function logFilters() { $status=trim((string)$this->input->get('status',true)); if(!in_array($status,array('','carried','installed','removed','returned'),true))$status='';$condition=trim((string)$this->input->get('condition',true));$ownership=trim((string)$this->input->get('ownership',true)); $technicianId=(int)$this->input->get('user_id');if($this->restrictedTechnicianEmployee())$technicianId=$this->restrictedTechnicianEmployee();return array('date_from'=>trim((string)$this->input->get('date_from',true)),'date_to'=>trim((string)$this->input->get('date_to',true)),'customer_id'=>(int)$this->input->get('customer_id'),'technician_id'=>$technicianId,'status'=>$status,'condition'=>$condition,'ownership'=>$ownership,'query'=>trim((string)$this->input->get('query',true))); } private function validInstallationImage(array$file) { if(($file['error']??UPLOAD_ERR_OK)!==UPLOAD_ERR_OK||($file['size']??0)>5242880||!is_uploaded_file($file['tmp_name']))return null;$mime=(new finfo(FILEINFO_MIME_TYPE))->file($file['tmp_name']);$allowed=array('image/jpeg'=>'jpg','image/png'=>'png','image/webp'=>'webp','image/gif'=>'gif','image/bmp'=>'bmp','image/heic'=>'heic','image/heif'=>'heif','image/avif'=>'avif');return isset($allowed[$mime])?$mime:null; } private function saveInstallationImage($statusLogId,array$file,$mime) { $extensions=array('image/jpeg'=>'jpg','image/png'=>'png','image/webp'=>'webp','image/gif'=>'gif','image/bmp'=>'bmp','image/heic'=>'heic','image/heif'=>'heif','image/avif'=>'avif');$dir=FCPATH.'uploads/accounting/';if(!is_dir($dir)&&!mkdir($dir,0750,true))throw new RuntimeException('Folder bukti perubahan status tidak tersedia.');$stored=bin2hex(random_bytes(20)).'.'.$extensions[$mime];$path=$dir.$stored;if(!move_uploaded_file($file['tmp_name'],$path))throw new RuntimeException('Bukti perubahan status gagal disimpan.');$row=array('module'=>'inventory','entity_type'=>'technician_status_change','entity_id'=>$statusLogId,'original_name'=>basename($file['name']),'stored_name'=>$stored,'mime_type'=>$mime,'file_size'=>filesize($path),'sha256'=>hash_file('sha256',$path),'uploaded_by'=>(int)$this->session->userdata('user_id'),'created_at'=>date('Y-m-d H:i:s'));if($this->db->field_exists('company_id','transaction_attachments'))$row['company_id']=(int)$this->companycontext->id();$this->db->insert('transaction_attachments',$row);return array('id'=>(int)$this->db->insert_id(),'path'=>$path); } /* Final operational custody API. Kept separate from the legacy endpoints so old documents remain readable while new movements use the auditable flow. */ public function replacement_workflow(){ $this->equipmentPermission('technician_installation','can_create');$this->renderEquipmentWorkbench('replacement'); } public function inspection_queue(){ $this->equipmentPermission('technician_inspection','can_view');$this->renderEquipmentWorkbench('inspection'); } public function customer_register(){ $this->renderEquipmentWorkbench('customer'); } public function bulk_workflow() { $this->equipmentPermission('technician_equipment','can_view');$restricted=$this->restrictedTechnicianEmployee();$allTechnicians=$this->ptm->get_teknisi_list();$technicians=$allTechnicians;if($restricted)$technicians=array_values(array_filter($technicians,function($row)use($restricted){return(int)$row->id===$restricted;}));$data=array('active_menu'=>'peralatan_teknisi','teknisi_list'=>$technicians,'target_teknisi_list'=>$allTechnicians,'warehouses'=>$this->db->select('id,nama')->where(array('company_id'=>(int)$this->companycontext->id(),'is_active'=>1))->order_by('nama')->get('warehouses')->result(),'equipment_permissions'=>$this->equipmentPermissionFlags(),'equipment_dashboard'=>$this->scopedEquipmentDashboard());$this->load->view('partials/header',$data);$this->load->view('peralatan_teknisi/bulk_workflow',$data);$this->load->view('partials/footer'); } private function renderEquipmentWorkbench($panel='overview') { $this->equipmentPermission('technician_equipment','can_view'); $dashboard=$this->scopedEquipmentDashboard();$restricted=$this->restrictedTechnicianEmployee();$allTechnicians=$this->ptm->get_teknisi_list();$technicians=$allTechnicians;if($restricted)$technicians=array_values(array_filter($technicians,function($row)use($restricted){return(int)$row->id===$restricted;})); $data=array( 'active_menu'=>'peralatan_teknisi','active_panel'=>$panel,'equipment_dashboard'=>$dashboard, 'teknisi_list'=>$technicians,'target_teknisi_list'=>$allTechnicians, 'customers'=>$this->db->select('id,nama')->where('company_id',(int)$this->companycontext->id())->order_by('nama')->get('customers')->result(), 'warehouses'=>$this->db->select('id,nama')->where(array('company_id'=>(int)$this->companycontext->id(),'is_active'=>1))->order_by('nama')->get('warehouses')->result(), 'equipment_permissions'=>$this->equipmentPermissionFlags() ); $this->load->view('partials/header',$data);$this->load->view('peralatan_teknisi/workbench',$data);$this->load->view('partials/footer'); } public function equipment_dashboard() { $this->equipmentPermission('technician_equipment','can_view'); return json_response(true,'Ringkasan peralatan.',array('result'=>$this->scopedEquipmentDashboard())); } public function equipment_scan() { $this->equipmentPermission('technician_equipment','can_view'); try{$result=$this->technicianequipmentservice->scan($this->input->get('code',true));$employee=$this->restrictedTechnicianEmployee();if($employee){$this->assertTechnicianSubjectScope($result['subject_type'],(int)$result['subject_id'],$employee);$result['warehouse_available']=0;$result['allocations']=$this->technicianequipmentservice->allocations($result['subject_type'],(int)$result['subject_id'],$employee);}return json_response(true,'Barang ditemukan.',array('result'=>$result));} catch(Throwable $e){return business_exception_response($e);} } public function equipment_search() { $this->equipmentPermission('technician_equipment','can_view');$mode=trim((string)$this->input->get('mode',true));$technicianId=(int)$this->input->get('technician_id');$restricted=$this->restrictedTechnicianEmployee();if($restricted)$technicianId=$restricted;try{return json_response(true,'Hasil pencarian peralatan.',array('data'=>$this->technicianequipmentservice->search($this->input->get('q',true),$mode?:'all',$technicianId,20)));}catch(Throwable$e){return business_exception_response($e);} } public function equipment_technician_items() { $this->equipmentPermission('technician_equipment','can_view');$technicianId=(int)$this->input->get('technician_id');$restricted=$this->restrictedTechnicianEmployee();if($restricted){if($technicianId&&$technicianId!==$restricted)show_error('Teknisi hanya dapat melihat barang dalam penguasaannya.',403,'Akses ditolak');$technicianId=$restricted;}if($technicianId<1)return json_response(false,'Pilih teknisi asal terlebih dahulu.',array('data'=>array()),422); try{$rows=$this->technicianequipmentservice->technicianHoldings($technicianId);return json_response(true,'Daftar barang teknisi.',array('data'=>$rows,'total'=>count($rows)));}catch(Throwable$e){return business_exception_response($e);} } public function equipment_allocations() { $this->equipmentPermission('technician_equipment','can_view'); $technicianId=(int)$this->input->get('technician_id');if($this->restrictedTechnicianEmployee())$technicianId=$this->restrictedTechnicianEmployee();try{return json_response(true,'Posisi peralatan.',array('data'=>$this->technicianequipmentservice->allocations(null,null,$technicianId)));} catch(Throwable $e){return business_exception_response($e);} } public function equipment_documents() { $this->equipmentPermission('technician_equipment','can_view'); $technicianId=(int)$this->input->get('technician_id');if($this->restrictedTechnicianEmployee())$technicianId=$this->restrictedTechnicianEmployee();$filters=array('date_from'=>$this->input->get('date_from',true),'date_to'=>$this->input->get('date_to',true),'customer_id'=>(int)$this->input->get('customer_id'),'technician_id'=>$technicianId,'document_type'=>$this->input->get('document_type',true),'status'=>$this->input->get('status',true)); try{return json_response(true,'Riwayat peralatan.',array('data'=>$this->technicianequipmentservice->documents($filters)));} catch(Throwable $e){return business_exception_response($e);} } public function equipment_document($id=0) { $this->equipmentPermission('technician_equipment','can_view');$this->assertTechnicianDocumentScope((int)$id); try{$result=$this->technicianequipmentservice->document((int)$id);$flags=$this->equipmentPermissionFlags();if(empty($flags['attachment']))$result['attachments']=array();return json_response(true,'Detail dokumen.',array('result'=>$result));} catch(Throwable $e){return business_exception_response($e);} } public function customer_equipment_data() { $this->equipmentPermission('technician_equipment','can_view'); try{$rows=$this->technicianequipmentservice->customerEquipmentSummary($this->restrictedTechnicianEmployee());return json_response(true,'Ringkasan barang per customer.',array('data'=>$rows));} catch(Throwable $e){return business_exception_response($e);} } public function customer_equipment_customer_data($customerId=0) { $this->equipmentPermission('technician_equipment','can_view');$customerId=(int)$customerId;$customer=$this->db->select('id,nama')->get_where('customers',array('id'=>$customerId,'company_id'=>(int)$this->companycontext->id()))->row();if(!$customer)return json_response(false,'Customer tidak ditemukan.',array('data'=>array()),404);$status=trim((string)$this->input->get('status',true)); try{$rows=$this->technicianequipmentservice->customerEquipmentRows($customerId,$status,$this->restrictedTechnicianEmployee());return json_response(true,'Detail barang customer.',array('data'=>$rows,'customer'=>$customer));}catch(Throwable$e){return business_exception_response($e);} } public function customer_equipment_customer_pdf($customerId=0) { $this->equipmentPermission('technician_equipment','can_export');$customerId=(int)$customerId;$customer=$this->db->select('id,nama')->get_where('customers',array('id'=>$customerId,'company_id'=>(int)$this->companycontext->id()))->row();if(!$customer)show_404();$status=trim((string)$this->input->get('status',true));$labels=array('all'=>'Semua Status','installed'=>'Terpasang','stored'=>'Tersimpan di Customer','technician'=>'Dibawa Teknisi','inspection'=>'Perlu Pemeriksaan/Perbaikan','good'=>'Kondisi Baik','damaged'=>'Rusak/Hilang');if(!isset($labels[$status]))$status='all';$rows=$this->technicianequipmentservice->customerEquipmentRows($customerId,$status,$this->restrictedTechnicianEmployee());$company=$this->db->get_where('companies',array('id'=>(int)$this->companycontext->id()))->row();require_once FCPATH.'vendor/autoload.php';$html=$this->load->view('peralatan_teknisi/customer_equipment_pdf',array('rows'=>$rows,'customer'=>$customer,'company'=>$company,'status_label'=>$labels[$status],'printed_by'=>$this->session->userdata('nama')),true);$pdf=new Dompdf\Dompdf();$pdf->loadHtml($html);$pdf->setPaper('A4','landscape');$pdf->render();$pdf->stream('Barang-Customer-'.preg_replace('/[^A-Za-z0-9_-]+/','-',(string)$customer->nama).'-'.$status.'-'.date('Ymd').'.pdf',array('Attachment'=>false)); } public function customer_equipment_notes_data($customerId=0) { $this->equipmentPermission('technician_equipment','can_view');$customerId=(int)$customerId;$customer=$this->db->select('id,nama')->get_where('customers',array('id'=>$customerId,'company_id'=>(int)$this->companycontext->id()))->row();if(!$customer)return json_response(false,'Customer tidak ditemukan.',array('data'=>array()),404); try{return json_response(true,'Log keterangan barang customer.',array('data'=>$this->technicianequipmentservice->customerEquipmentNotes($customerId,$this->restrictedTechnicianEmployee()),'customer'=>$customer));}catch(Throwable$e){return business_exception_response($e);} } public function customer_equipment_notes_pdf($customerId=0) { $this->equipmentPermission('technician_equipment','can_export');$customerId=(int)$customerId;$customer=$this->db->select('id,nama')->get_where('customers',array('id'=>$customerId,'company_id'=>(int)$this->companycontext->id()))->row();if(!$customer)show_404();$rows=$this->technicianequipmentservice->customerEquipmentNotes($customerId,$this->restrictedTechnicianEmployee());$company=$this->db->get_where('companies',array('id'=>(int)$this->companycontext->id()))->row();require_once FCPATH.'vendor/autoload.php';$html=$this->load->view('peralatan_teknisi/customer_equipment_notes_pdf',array('rows'=>$rows,'customer'=>$customer,'company'=>$company,'printed_by'=>$this->session->userdata('nama')),true);$pdf=new Dompdf\Dompdf();$pdf->loadHtml($html);$pdf->setPaper('A4','landscape');$pdf->render();$pdf->stream('Log-Keterangan-Barang-'.preg_replace('/[^A-Za-z0-9_-]+/','-',(string)$customer->nama).'-'.date('Ymd').'.pdf',array('Attachment'=>true)); } public function customer_equipment_detail($id=0) { $this->equipmentPermission('technician_equipment','can_view');try{$employee=$this->restrictedTechnicianEmployee();if($employee)$this->assertTechnicianSubjectScope('customer_equipment',(int)$id,$employee);$result=$this->technicianequipmentservice->external((int)$id);$flags=$this->equipmentPermissionFlags();if(empty($flags['attachment']))$result['attachments']=array();return json_response(true,'Detail barang customer.',array('result'=>$result));}catch(Throwable$e){return business_exception_response($e);} } public function register_customer_equipment() { if(strtoupper($this->input->method())!=='POST')show_404(); $this->equipmentPermission('technician_equipment','can_create'); $saved=array();try{$files=$this->validatedEquipmentFiles();$post=$this->input->post(NULL,true);if((($post['ownership_type']??'')==='transferred_non_inventory'||!empty($post['deployment_allowed']))&&!$files)throw new BusinessException('Bukti penyerahan/otorisasi wajib dilampirkan untuk status yang dipilih.');$this->db->trans_begin();$result=$this->technicianequipmentservice->registerExternal($post,(int)$this->session->userdata('user_id'));foreach($files as$file)$saved[]=$this->storeEquipmentFile((int)$result['id'],$file,'customer_equipment_registry');if($this->db->trans_status()===false)throw new RuntimeException('Register barang customer gagal disimpan.');$this->db->trans_commit();log_activity('items','equipment_register','Mendaftarkan barang customer non-accounting '.$result['external_barcode'],'success');return json_response(true,'Barang customer berhasil dicatat tanpa masuk persediaan accounting.',array('result'=>$result));} catch(Throwable $e){if($this->db->trans_active())$this->db->trans_rollback();foreach($saved as$path)if(is_file($path))@unlink($path);return business_exception_response($e);} } public function process_equipment() { if(strtoupper($this->input->method())!=='POST')show_404(); $data=$this->input->post(NULL,true);$action=(string)($data['action']??'');$map=array('handover'=>array('technician_handover','can_create'),'install'=>array('technician_installation','can_create'),'remove'=>array('technician_installation','can_update'),'transfer'=>array('technician_transfer','can_update'),'return'=>array('technician_return','can_update'),'return_customer'=>array('technician_return','can_update')); if(!isset($map[$action]))return json_response(false,'Tindakan tidak valid.',array(),422);if($action==='install'&&trim((string)($data['installation_location']??''))==='')return json_response(false,'Lokasi pemasangan wajib diisi.',array(),422);if(in_array($action,array('remove','return','transfer'),true)&&trim((string)($data['reason']??''))==='')return json_response(false,'Alasan perpindahan wajib diisi.',array(),422);$this->equipmentPermission($map[$action][0],$map[$action][1]);$this->assertTechnicianActionScope($data,$action); return $this->runEquipmentPosting(function()use($data){return$this->technicianequipmentservice->process($data,(int)$this->session->userdata('user_id'));},$action); } public function process_equipment_batch() { if(strtoupper($this->input->method())!=='POST')show_404();$data=$this->input->post(NULL,true);$action=(string)($data['action']??'');$map=array('handover'=>array('technician_handover','can_create'),'transfer'=>array('technician_transfer','can_update'),'return'=>array('technician_return','can_update'));if(!isset($map[$action]))return json_response(false,'Jenis perpindahan batch tidak valid.',array(),422);$items=json_decode((string)$this->input->post('items_json',false),true);if(!is_array($items)||!$items)return json_response(false,'Tambahkan minimal satu barang.',array(),422);$data['items']=$items;$this->equipmentPermission($map[$action][0],$map[$action][1]);$restricted=$this->restrictedTechnicianEmployee();if($restricted&&(int)($data['technician_id']??0)!==$restricted)show_error('Teknisi hanya dapat memproses barang untuk dirinya sendiri.',403,'Akses ditolak');if($restricted&&$action!=='handover')foreach($items as$item)$this->assertTechnicianActionScope(array('allocation_id'=>(int)($item['allocation_id']??0)),$action);return$this->runEquipmentPosting(function()use($data){return$this->technicianequipmentservice->processBatch($data,(int)$this->session->userdata('user_id'));},$action); } public function process_replacement() { if(strtoupper($this->input->method())!=='POST')show_404();$this->equipmentPermission('technician_installation','can_create');$data=$this->input->post(NULL,true);if(trim((string)($data['installation_location']??''))===''||trim((string)($data['reason']??''))==='')return json_response(false,'Lokasi pemasangan dan alasan penggantian wajib diisi.',array(),422);$this->assertTechnicianActionScope($data,'replacement'); return $this->runEquipmentPosting(function()use($data){return$this->technicianequipmentservice->replacement($data,(int)$this->session->userdata('user_id'));},'replacement'); } public function process_inspection() { if(strtoupper($this->input->method())!=='POST')show_404();$this->equipmentPermission('technician_inspection','can_post');$data=$this->input->post(NULL,true);if(in_array(($data['result_status']??''),array('damaged','lost','scrapped'),true))$this->equipmentPermission('technician_condition','can_update');$this->assertTechnicianActionScope($data,'inspection'); return $this->runEquipmentPosting(function()use($data){return$this->technicianequipmentservice->inspect($data,(int)$this->session->userdata('user_id'));},'inspection'); } public function process_correction() { if(strtoupper($this->input->method())!=='POST')show_404();$this->equipmentPermission('technician_correction','can_update');$data=$this->input->post(NULL,true);$this->assertTechnicianDocumentScope((int)($data['document_id']??0)); return $this->runEquipmentPosting(function()use($data){return$this->technicianequipmentservice->reverse($data,(int)$this->session->userdata('user_id'));},'correction'); } public function inspection_data() { $this->equipmentPermission('technician_inspection','can_view'); $technicianId=(int)$this->input->get('technician_id');if($this->restrictedTechnicianEmployee())$technicianId=$this->restrictedTechnicianEmployee();$filters=array('date_from'=>$this->input->get('date_from',true),'date_to'=>$this->input->get('date_to',true),'technician_id'=>$technicianId,'customer_id'=>(int)$this->input->get('customer_id'),'ownership'=>$this->input->get('ownership',true),'condition'=>$this->input->get('condition',true),'status'=>$this->input->get('status',true));try{return json_response(true,'Antrean pemeriksaan.',array('data'=>$this->technicianequipmentservice->inspectionQueue($filters)));} catch(Throwable $e){return business_exception_response($e);} } private function runEquipmentPosting(Closure $callback,$action) { $saved=array();$files=array(); try{ $files=$this->validatedEquipmentFiles();if(in_array($action,array('remove','return','inspection','replacement'),true)&&!$files)throw new BusinessException('Bukti foto atau PDF wajib dilampirkan untuk proses ini.');$this->db->trans_begin();$result=$callback(); if(empty($result['duplicate']))foreach($files as$file)$saved[]=$this->storeEquipmentFile((int)$result['document_id'],$file); if($this->db->trans_status()===false)throw new RuntimeException('Transaksi peralatan gagal disimpan.');$this->db->trans_commit(); log_activity('items','equipment_'.$action,'Memproses dokumen peralatan '.$result['document_no'].' tanpa jurnal accounting','success'); return json_response(true,(!empty($result['duplicate'])?'Permintaan ini sudah pernah diproses.':'Dokumen '.$result['document_no'].' berhasil diposting.'),array('result'=>$result)); }catch(Throwable $e){if($this->db->trans_active())$this->db->trans_rollback();foreach($saved as$path)if(is_file($path))@unlink($path);return business_exception_response($e);} } private function validatedEquipmentFiles() { if(empty($_FILES['attachments']))return array();$input=$_FILES['attachments'];$files=array();$count=is_array($input['name'])?count($input['name']):1;if($count>10)throw new BusinessException('Maksimal 10 lampiran per dokumen.');$allowed=array('application/pdf'=>'pdf','image/jpeg'=>'jpg','image/png'=>'png','image/webp'=>'webp','image/gif'=>'gif','image/bmp'=>'bmp','image/heic'=>'heic','image/heif'=>'heif','image/avif'=>'avif'); for($i=0;$i<$count;$i++){$file=array('name'=>is_array($input['name'])?$input['name'][$i]:$input['name'],'tmp_name'=>is_array($input['tmp_name'])?$input['tmp_name'][$i]:$input['tmp_name'],'error'=>is_array($input['error'])?$input['error'][$i]:$input['error'],'size'=>is_array($input['size'])?$input['size'][$i]:$input['size']);if($file['error']===UPLOAD_ERR_NO_FILE)continue;if($file['error']!==UPLOAD_ERR_OK||!is_uploaded_file($file['tmp_name']))throw new BusinessException('Salah satu lampiran gagal diterima server.');if((int)$file['size']>5242880)throw new BusinessException('Ukuran tiap lampiran maksimal 5 MB.');$mime=(new finfo(FILEINFO_MIME_TYPE))->file($file['tmp_name']);if(!isset($allowed[$mime]))throw new BusinessException('Lampiran harus berupa gambar atau PDF yang valid.');$file['mime']=$mime;$file['extension']=$allowed[$mime];$files[]=$file;} return$files; } private function storeEquipmentFile($documentId,array$file,$entityType='technician_equipment_document') { $dir=FCPATH.'uploads/accounting/';if(!is_dir($dir)&&!mkdir($dir,0750,true))throw new RuntimeException('Folder lampiran tidak tersedia.');$stored=bin2hex(random_bytes(20)).'.'.$file['extension'];$path=$dir.$stored;if(!move_uploaded_file($file['tmp_name'],$path))throw new RuntimeException('Lampiran gagal disimpan.');$row=array('module'=>'inventory','entity_type'=>$entityType,'entity_id'=>$documentId,'original_name'=>basename($file['name']),'stored_name'=>$stored,'mime_type'=>$file['mime'],'file_size'=>filesize($path),'sha256'=>hash_file('sha256',$path),'uploaded_by'=>(int)$this->session->userdata('user_id'),'created_at'=>date('Y-m-d H:i:s'));if($this->db->field_exists('company_id','transaction_attachments'))$row['company_id']=(int)$this->companycontext->id();$this->db->insert('transaction_attachments',$row);if(!$this->db->insert_id())throw new RuntimeException('Metadata lampiran gagal disimpan.');return$path; } public function equipment_document_pdf($id=0) { $this->equipmentPermission('technician_equipment','can_export');$this->assertTechnicianDocumentScope((int)$id); try{$document=$this->technicianequipmentservice->document((int)$id);$company=$this->db->get_where('companies',array('id'=>(int)$this->companycontext->id()))->row();require_once FCPATH.'vendor/autoload.php';$html=$this->load->view('peralatan_teknisi/document_pdf',array('document'=>$document,'company'=>$company),true);$pdf=new Dompdf\Dompdf();$pdf->loadHtml($html);$pdf->setPaper('A4','portrait');$pdf->render();$pdf->stream($document['document_no'].'.pdf',array('Attachment'=>false));} catch(Throwable $e){show_error($e->getMessage(),422,'Dokumen tidak dapat dibuka');} } public function technician_inventory_pdf($id=0) { $this->equipmentPermission('technician_equipment','can_export');$id=(int)$id;$restricted=$this->restrictedTechnicianEmployee();if($restricted&&$id!==$restricted)show_error('PDF bukan milik teknisi yang sedang login.',403,'Akses ditolak');$technician=$this->ptm->get_teknisi_info($id);if(!$technician)show_404(); $status=trim((string)$this->input->get('status',true));$labels=array('carried'=>'Sedang Dibawa','installed'=>'Terpasang di Customer','removed'=>'Hasil Pelepasan','inspection'=>'Menunggu Pemeriksaan','all'=>'Semua Posisi');if(!isset($labels[$status]))$status='carried'; $rows=$this->technicianequipmentservice->allocations(null,null,$id);if($status!=='all')$rows=array_values(array_filter($rows,function($row)use($status){if($status==='carried')return$row['custodian_type']==='technician'&&$row['usage_status']==='carried';if($status==='installed')return$row['custodian_type']==='customer'&&$row['usage_status']==='installed';if($status==='removed')return$row['usage_status']==='removed';return$status==='inspection'&&$row['condition_status']==='inspection_required';})); $company=$this->db->get_where('companies',array('id'=>(int)$this->companycontext->id()))->row();require_once FCPATH.'vendor/autoload.php';$html=$this->load->view('peralatan_teknisi/technician_inventory_pdf',array('technician'=>$technician,'rows'=>$rows,'company'=>$company,'printed_by'=>$this->session->userdata('nama'),'status_label'=>$labels[$status]),true);$pdf=new Dompdf\Dompdf();$pdf->loadHtml($html);$pdf->setPaper('A4','landscape');$pdf->render();$pdf->stream('Daftar-Peralatan-'.preg_replace('/[^A-Za-z0-9_-]+/','-',(string)$technician->full_name).'-'.$status.'-'.date('Ymd').'.pdf',array('Attachment'=>false)); } private function equipmentPermission($feature,$action) { if(is_master_admin_user())return true;if(check_permission($feature,$action))return true;$permissions=$this->session->userdata('permissions');if(is_array($permissions)&&array_key_exists($feature,$permissions))return$this->requirePermission($feature,$action); // Hanya session lama yang belum mengenal feature baru yang memakai fallback. if(check_permission('items',$action))return true; return$this->requirePermission($feature,$action); } private function equipmentPermissionFlags() { $pairs=array('view'=>array('technician_equipment','can_view'),'attachment'=>array('technician_attachment','can_view'),'register'=>array('technician_equipment','can_create'),'handover'=>array('technician_handover','can_create'),'install'=>array('technician_installation','can_create'),'remove'=>array('technician_installation','can_update'),'transfer'=>array('technician_transfer','can_update'),'return'=>array('technician_return','can_update'),'inspect_view'=>array('technician_inspection','can_view'),'inspect'=>array('technician_inspection','can_post'),'condition'=>array('technician_condition','can_update'),'correct'=>array('technician_correction','can_update'),'export'=>array('technician_equipment','can_export'));$flags=array();$permissions=$this->session->userdata('permissions');foreach($pairs as$key=>$pair){$known=is_array($permissions)&&array_key_exists($pair[0],$permissions);$flags[$key]=is_master_admin_user()||check_permission($pair[0],$pair[1])||(!$known&&check_permission('items',$pair[1]));}return$flags; } private function restrictedTechnicianEmployee() { if(is_master_admin_user()||check_permission('technician_equipment_all','can_view'))return 0;$role=strtolower((string)$this->session->userdata('role'));if(strpos($role,'teknisi')===false)return 0;if(!$this->db->field_exists('employee_id','users'))show_error('Migration tautan pengguna-karyawan belum dijalankan.',500,'Konfigurasi belum lengkap');$user=$this->db->select('employee_id')->get_where('users',array('id'=>(int)$this->session->userdata('user_id')))->row();if(!$user||(int)$user->employee_id<1)show_error('Akun teknisi belum ditautkan ke Data Karyawan. Hubungi administrator.',403,'Akses ditolak');return(int)$user->employee_id; } private function scopedEquipmentDashboard() { $dashboard=$this->technicianequipmentservice->dashboard();$employee=$this->restrictedTechnicianEmployee();if(!$employee)return$dashboard;$allocations=$this->technicianequipmentservice->allocations(null,null,$employee);$summary=array('carried'=>0,'installed'=>0,'inspection'=>0,'repair'=>0,'damaged'=>0,'lost'=>0);foreach($allocations as$a){$qty=(float)$a['qty'];if($a['custodian_type']==='technician')$summary['carried']+=$qty;if($a['usage_status']==='installed')$summary['installed']+=$qty;if(in_array($a['condition_status'],array('repair_required','under_repair'),true))$summary['repair']+=$qty;if($a['condition_status']==='damaged')$summary['damaged']+=$qty;if($a['condition_status']==='lost')$summary['lost']+=$qty;}$inspectionStats=$this->technicianequipmentservice->inspectionQueueStats($employee);$summary['inspection']=(float)$inspectionStats['qty'];$dashboard['summary']=$summary;$dashboard['inspection_count']=(int)$inspectionStats['row_count'];$dashboard['technicians']=array_values(array_filter($dashboard['technicians'],function($row)use($employee){return(int)$row->id===$employee;}));return$dashboard; } private function assertTechnicianActionScope(array$data,$action) { $employee=$this->restrictedTechnicianEmployee();if(!$employee)return true;if(in_array($action,array('handover','remove','replacement'),true)&&(int)($data['technician_id']??0)!==$employee)show_error('Teknisi hanya dapat memproses barang untuk dirinya sendiri.',403,'Akses ditolak');$allocationId=(int)($action==='replacement'?($data['outgoing_allocation_id']??0):($data['allocation_id']??0));if(!$allocationId)return true;$allocation=$this->db->select('subject_type,subject_id,custodian_type,custodian_id')->get_where('technician_custody_allocations',array('id'=>$allocationId,'company_id'=>(int)$this->companycontext->id()))->row();if(!$allocation)show_error('Posisi barang tidak ditemukan.',404,'Data tidak ditemukan');if(in_array($action,array('remove','inspection'),true))return$this->assertTechnicianSubjectScope($allocation->subject_type,(int)$allocation->subject_id,$employee);if($allocation->custodian_type!=='technician'||(int)$allocation->custodian_id!==$employee)show_error('Barang bukan berada dalam penguasaan teknisi yang sedang login.',403,'Akses ditolak');return true; } private function assertTechnicianSubjectScope($subjectType,$subjectId,$employee) { $company=(int)$this->companycontext->id();$subjectId=(int)$subjectId;$employee=(int)$employee; $current=$this->db->get_where('technician_custody_allocations',array('company_id'=>$company,'subject_type'=>$subjectType,'subject_id'=>$subjectId,'custodian_type'=>'technician','custodian_id'=>$employee))->row();if($current)return true; $related=$this->db->select('l.id')->from('technician_equipment_document_lines l')->join('technician_equipment_documents d','d.id=l.document_id')->where(array('l.company_id'=>$company,'l.subject_type'=>$subjectType,'l.subject_id'=>$subjectId))->group_start()->where('d.technician_id',$employee)->or_where('d.target_technician_id',$employee)->group_end()->limit(1)->get()->row();if(!$related)show_error('Barang bukan bagian dari penguasaan atau riwayat teknisi yang sedang login.',403,'Akses ditolak');return true; } private function assertTechnicianDocumentScope($id) { $employee=$this->restrictedTechnicianEmployee();if(!$employee)return true;$owned=$this->db->group_start()->where('technician_id',$employee)->or_where('target_technician_id',$employee)->group_end()->get_where('technician_equipment_documents',array('id'=>(int)$id,'company_id'=>(int)$this->companycontext->id()))->row();if(!$owned)show_error('Dokumen bukan bagian dari penguasaan teknisi yang sedang login.',403,'Akses ditolak');return true; } }