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

780 lines
60 KiB
PHP

<?php
defined('BASEPATH') OR exit('No direct script access allowed');
class Teknisi extends MY_Controller {
public function __construct()
{
parent::__construct();
$this->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 = '<a class="btn btn-info btn-sm" href="'.base_url('teknisi/detail/'.(int)$row->id).'" title="Detail dan Riwayat">
<i class="bi bi-eye"></i>
</a>
<button class="btn btn-warning btn-sm btn-form-barang-bawaan" data-id="'.$row->id.'" data-nama="'.htmlspecialchars($row->full_name, ENT_QUOTES, 'UTF-8').'" title="Form Barang Bawaan">
<i class="bi bi-file-earmark-text"></i>
</button>';
$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='<span class="badge bg-warning text-dark">Sebagian Dipasang</span><small class="d-block text-muted mt-1">Dibawa '.number_format($carried,0,',','.').' &middot; Dipasang '.number_format($installed,0,',','.').'</small>';
elseif($installed>0)$badge='<span class="badge bg-primary">Dipasang</span><small class="d-block text-muted mt-1">'.htmlspecialchars(($row->customer_name?:'-').' · '.($row->usage_date?:'-'),ENT_QUOTES,'UTF-8').'</small>';
else$badge='<span class="badge bg-success">Dibawa</span>';
$action='-';
if(is_master_admin_user()||check_permission('items','can_update')){
$action='<button type="button" class="btn btn-sm btn-outline-primary btn-change-item-status" data-user="'.(int)$row->user_id.'" data-barcode="'.htmlspecialchars($row->barcode,ENT_QUOTES,'UTF-8').'" data-item="'.htmlspecialchars($row->nama_barang,ENT_QUOTES,'UTF-8').'" data-tracking="'.$tracking.'" data-carried="'.$carried.'" data-installed="'.$installed.'" data-customer="'.(int)$row->customer_id.'"><i class="bi bi-arrow-left-right me-1"></i>Ubah Status</button>';
if(!empty($row->proof_id))$action.=' <a class="btn btn-sm btn-outline-info" target="_blank" href="'.base_url('accountingattachments/preview/'.(int)$row->proof_id).'" title="Bukti terakhir"><i class="bi bi-image"></i></a>';
}elseif(!empty($row->proof_id))$action='<a class="btn btn-sm btn-outline-info" target="_blank" href="'.base_url('accountingattachments/preview/'.(int)$row->proof_id).'"><i class="bi bi-image me-1"></i>Bukti</a>';
$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'),
'<span class="badge bg-success">Selesai</span>'
];
}
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']).'<br><small>'.html_escape(date('d/m/Y',strtotime($row['document_date']))).'</small>',html_escape($row['technician_name']?:'-'),html_escape($row['display_name']),html_escape($row['barcode_snapshot']).'<br><small>'.html_escape($row['serial_number']?:'-').'</small>',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']?:'-'),'<button class="btn btn-outline-primary btn-sm tech-log-detail" data-id="'.(int)$row['document_id'].'"><i class="bi bi-eye"></i></button>');}
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?'<a class="btn btn-sm btn-outline-info" target="_blank" href="'.base_url('accountingattachments/preview/'.(int)$row->attachment_id).'" title="Lihat bukti"><i class="bi bi-image"></i></a>':'-';$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').' &rarr; '.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;
}
}