diff --git a/application/config/permissions.php b/application/config/permissions.php index 05628f2..44bbe38 100644 --- a/application/config/permissions.php +++ b/application/config/permissions.php @@ -5,16 +5,17 @@ $config['permission_features'] = array( 'dashboard' => 'Dashboard', 'approvals' => 'Daftar Approval', 'invoices' => 'Penjualan, Invoice & Piutang', 'purchases' => 'Pembelian, Supplier & Hutang', 'items' => 'Persediaan & Gudang', - 'technician_equipment' => 'Peralatan Teknisi - Akses', - 'technician_equipment_all' => 'Peralatan Teknisi - Lihat Seluruh Teknisi', - 'technician_attachment' => 'Peralatan Teknisi - Lihat Lampiran', - 'technician_handover' => 'Peralatan Teknisi - Serah Terima', - 'technician_installation' => 'Peralatan Teknisi - Pasang & Lepas', - 'technician_transfer' => 'Peralatan Teknisi - Transfer', - 'technician_return' => 'Peralatan Teknisi - Pengembalian', - 'technician_inspection' => 'Peralatan Teknisi - Pemeriksaan', - 'technician_condition' => 'Peralatan Teknisi - Rusak/Hilang/Nonaktif', - 'technician_correction' => 'Peralatan Teknisi - Koreksi', + 'technician_equipment' => 'Operasional Perangkat - Akses & Export', + 'technician_equipment_all' => 'Operasional Perangkat - Lihat Seluruh Teknisi', + 'technician_attachment' => 'Operasional Perangkat - Lihat Lampiran', + 'technician_handover' => 'Operasional Perangkat - Serah Terima', + 'technician_installation' => 'Operasional Perangkat - Pasang & Lepas', + 'technician_transfer' => 'Operasional Perangkat - Transfer', + 'technician_return' => 'Operasional Perangkat - Pengembalian', + 'technician_inspection' => 'Operasional Perangkat - Pemeriksaan', + 'technician_customer_item' => 'Operasional Perangkat - Barang Customer', + 'technician_condition' => 'Operasional Perangkat - Rusak/Hilang/Nonaktif', + 'technician_correction' => 'Operasional Perangkat - Koreksi', 'asset' => 'Aset Tetap', 'jurnal' => 'Jurnal & Periode Accounting', 'reports' => 'Laporan Profesional', 'budgets' => 'Budget', diff --git a/application/controllers/Accountingattachments.php b/application/controllers/Accountingattachments.php index 4de0ec6..7400875 100644 --- a/application/controllers/Accountingattachments.php +++ b/application/controllers/Accountingattachments.php @@ -23,6 +23,6 @@ class Accountingattachments extends MY_Controller } public function download($id){$f=$this->db->get_where('transaction_attachments',array('id'=>(int)$id))->row();if(!$f)show_404();$this->assertEntityAccess($f);$path=FCPATH.'uploads/accounting/'.$f->stored_name;if(!$this->safePath($path))show_404();$this->load->helper('download');force_download($f->original_name,file_get_contents($path));} public function preview($id){$f=$this->db->get_where('transaction_attachments',array('id'=>(int)$id))->row();if(!$f)show_404();$this->assertEntityAccess($f);$path=FCPATH.'uploads/accounting/'.$f->stored_name;if(!$this->safePath($path))show_404();$mime=function_exists('finfo_open')?(new finfo(FILEINFO_MIME_TYPE))->file($path):$f->mime_type;if($mime!=='application/pdf'&&strpos((string)$mime,'image/')!==0)show_404();$name=str_replace(array('"',"\r","\n"),'',basename($f->original_name));$this->output->set_header('Content-Type: '.$mime)->set_header('Content-Disposition: inline; filename="'.$name.'"')->set_header('Content-Length: '.filesize($path))->set_header('X-Content-Type-Options: nosniff')->set_output(file_get_contents($path));} - private function assertEntityAccess($file){$company=(int)$this->session->userdata('company_id');if(in_array($file->entity_type,array('technician_equipment_document','customer_equipment_registry'),true)){$table=$file->entity_type==='technician_equipment_document'?'technician_equipment_documents':'customer_equipment_registry';$owned=$this->db->get_where($table,array('id'=>(int)$file->entity_id,'company_id'=>$company))->row();if(!$owned)show_error('Akses lampiran ditolak.',403,'Akses ditolak');$permissions=$this->session->userdata('permissions');$known=is_array($permissions)&&array_key_exists('technician_attachment',$permissions);$fallback=!$known&&(check_permission('technician_equipment','can_view')||check_permission('items','can_view'));if(!is_master_admin_user()&&!check_permission('technician_attachment','can_view')&&!$fallback)show_error('Akses lampiran ditolak.',403,'Akses ditolak');return;}if($file->entity_type==='technician_installation'){$owned=$this->db->select('b.id')->from('item_barcodes b')->join('items i','i.id=b.item_id')->where(array('b.id'=>(int)$file->entity_id,'i.company_id'=>$company))->get()->row();if(!$owned)show_error('Akses lampiran ditolak.',403,'Akses ditolak');$this->requirePermission('items','can_view');return;}if($file->entity_type==='technician_status_change'){$owned=$this->db->get_where('technician_item_status_logs',array('id'=>(int)$file->entity_id,'company_id'=>$company))->row();if(!$owned)show_error('Akses lampiran ditolak.',403,'Akses ditolak');$this->requirePermission('items','can_view');return;}$map=array('stock_document'=>array('stock_documents','items'),'asset'=>array('assets','asset'),'asset_maintenance'=>array('asset_maintenance','asset'),'asset_opname'=>array('asset_opnames','asset'));if(!isset($map[$file->entity_type]))return;list($table,$feature)=$map[$file->entity_type];if(!$this->db->get_where($table,array('id'=>(int)$file->entity_id,'company_id'=>$company))->row())show_error('Akses lampiran ditolak.',403,'Akses ditolak');$this->requirePermission($feature,'can_view');} + private function assertEntityAccess($file){$company=(int)$this->session->userdata('company_id');if(in_array($file->entity_type,array('technician_equipment_document','customer_equipment_registry'),true)){$table=$file->entity_type==='technician_equipment_document'?'technician_equipment_documents':'customer_equipment_registry';$owned=$this->db->get_where($table,array('id'=>(int)$file->entity_id,'company_id'=>$company))->row();if(!$owned)show_error('Akses lampiran ditolak.',403,'Akses ditolak');$this->requirePermission('technician_attachment','can_view');return;}if($file->entity_type==='technician_installation'){$owned=$this->db->select('b.id')->from('item_barcodes b')->join('items i','i.id=b.item_id')->where(array('b.id'=>(int)$file->entity_id,'i.company_id'=>$company))->get()->row();if(!$owned)show_error('Akses lampiran ditolak.',403,'Akses ditolak');$this->requirePermission('technician_attachment','can_view');return;}if($file->entity_type==='technician_status_change'){$owned=$this->db->get_where('technician_item_status_logs',array('id'=>(int)$file->entity_id,'company_id'=>$company))->row();if(!$owned)show_error('Akses lampiran ditolak.',403,'Akses ditolak');$this->requirePermission('technician_attachment','can_view');return;}$map=array('stock_document'=>array('stock_documents','items'),'asset'=>array('assets','asset'),'asset_maintenance'=>array('asset_maintenance','asset'),'asset_opname'=>array('asset_opnames','asset'));if(!isset($map[$file->entity_type]))return;list($table,$feature)=$map[$file->entity_type];if(!$this->db->get_where($table,array('id'=>(int)$file->entity_id,'company_id'=>$company))->row())show_error('Akses lampiran ditolak.',403,'Akses ditolak');$this->requirePermission($feature,'can_view');} private function safePath($path){$root=realpath(FCPATH.'uploads/accounting');$real=realpath($path);return$root&&$real&&strpos($real,$root.DIRECTORY_SEPARATOR)===0&&is_file($real);} } diff --git a/application/controllers/Roles.php b/application/controllers/Roles.php index dff5cf8..7df843d 100644 --- a/application/controllers/Roles.php +++ b/application/controllers/Roles.php @@ -30,6 +30,8 @@ class Roles extends MY_Admin_Controller $duplicate=$this->db->where('nama_role',$name)->where('id !=',$id)->count_all_results('roles');if($duplicate)return json_response(false,'Nama role sudah digunakan.',array(),422); $posted=(array)$this->input->post('permissions');$permissions=array(); foreach($this->features as$key=>$label){$allowedActions=array_keys($this->actions);if($key==='payroll')$allowedActions=array_merge($allowedActions,array('can_manage_all_salary','can_manage_allowances'));$selected=array_values(array_intersect((array)(isset($posted[$key])?$posted[$key]:array()),$allowedActions));if($key==='payroll'&&in_array('can_manage_allowances',$selected,true)&&!in_array('can_update',$selected,true))$selected[]='can_update';if($selected){if(!in_array('can_view',$selected,true))array_unshift($selected,'can_view');$permissions[$key]=$selected;}} + $hasDevicePermission=false;foreach(array_keys($permissions)as$feature){if(strpos($feature,'technician_')===0){$hasDevicePermission=true;break;}} + if($hasDevicePermission){if(empty($permissions['technician_equipment']))$permissions['technician_equipment']=array('can_view');elseif(!in_array('can_view',$permissions['technician_equipment'],true))array_unshift($permissions['technician_equipment'],'can_view');} if(!$permissions)return json_response(false,'Role wajib mempunyai minimal satu permission.',array(),422); $data=array('nama_role'=>$name,'description'=>$description,'permissions'=>json_encode($permissions),'is_active'=>$this->input->post('is_active')?1:0,'updated_at'=>date('Y-m-d H:i:s')); if($id)$this->db->where('id',$id)->update('roles',$data);else{$data['is_system']=0;$data['is_super_admin']=0;$data['created_at']=date('Y-m-d H:i:s');$this->db->insert('roles',$data);$id=$this->db->insert_id();} diff --git a/application/controllers/Teknisi.php b/application/controllers/Teknisi.php index 033be6f..85b33a7 100644 --- a/application/controllers/Teknisi.php +++ b/application/controllers/Teknisi.php @@ -200,7 +200,7 @@ class Teknisi extends MY_Controller { 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')){ + if(is_master_admin_user()||check_permission('technician_installation','can_create')||check_permission('technician_installation','can_update')||check_permission('technician_return','can_update')){ $action=''; if(!empty($row->proof_id))$action.=' '; }elseif(!empty($row->proof_id))$action='Bukti'; @@ -478,7 +478,7 @@ class Teknisi extends MY_Controller { "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'), + "can_export" => is_master_admin_user() || check_permission('technician_equipment','can_export'), "equipment_permissions" => $this->equipmentPermissionFlags(), "equipment_dashboard" => $this->scopedEquipmentDashboard(), "initial_filters" => $this->logFilters(), @@ -499,7 +499,7 @@ class Teknisi extends MY_Controller { 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); + $this->equipmentPermission('technician_equipment','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)); @@ -509,7 +509,7 @@ class Teknisi extends MY_Controller { { 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'); + $this->equipmentPermission('technician_correction','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); @@ -657,7 +657,7 @@ class Teknisi extends MY_Controller { public function register_customer_equipment() { if(strtoupper($this->input->method())!=='POST')show_404(); - $this->equipmentPermission('technician_equipment','can_create'); + $this->equipmentPermission('technician_customer_item','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);} } @@ -741,15 +741,14 @@ class Teknisi extends MY_Controller { 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; + if(is_master_admin_user())return true; + if(check_permission($feature,$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; + $pairs=array('view'=>array('technician_equipment','can_view'),'attachment'=>array('technician_attachment','can_view'),'register'=>array('technician_customer_item','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();foreach($pairs as$key=>$pair)$flags[$key]=is_master_admin_user()||check_permission($pair[0],$pair[1]);return$flags; } private function restrictedTechnicianEmployee() diff --git a/application/core/MY_Controller.php b/application/core/MY_Controller.php index 1337431..36dbdc8 100644 --- a/application/core/MY_Controller.php +++ b/application/core/MY_Controller.php @@ -162,9 +162,9 @@ class MY_Controller extends CI_Controller $action = $targetState === 'sent' ? 'can_create' : 'can_approve'; } if ($class === 'items' && in_array($method, array('save_serial_number','delete_serial_number'), true)) $action = 'can_update'; - // Teknisi melakukan validasi granular per endpoint di controllernya. - // Fallback items menjaga session lama tetap dapat dipakai sampai login ulang. - if ($class === 'teknisi' && (is_master_admin_user() || check_permission('technician_equipment','can_view') || check_permission('items','can_view'))) return; + // Operasional Perangkat melakukan validasi granular per endpoint di controllernya. + // Akses Persediaan tidak otomatis memberikan akses ke modul terpisah ini. + if ($class === 'teknisi' && (is_master_admin_user() || check_permission('technician_equipment','can_view'))) return; $this->requirePermission($features[$class],$action); } diff --git a/application/libraries/NavigationService.php b/application/libraries/NavigationService.php index 59ed89c..45702ab 100644 --- a/application/libraries/NavigationService.php +++ b/application/libraries/NavigationService.php @@ -12,7 +12,8 @@ class NavigationService array('key'=>'approvals','label'=>'Approval','icon'=>'bi-inbox','url'=>'approvals','feature'=>'approvals','badge'=>$this->approvalBadge()), array('key'=>'sales_menu','label'=>'Penjualan','icon'=>'bi-receipt','feature'=>'invoices','badge'=>$this->salesBadge('all'),'badge_class'=>$this->salesBadge('overdue')?'nav-badge-danger':'nav-badge-warning','badge_title'=>'Invoice, piutang, pengiriman, atau retur membutuhkan tindakan','children'=>array(array('key'=>'sales','label'=>'Ringkasan Penjualan','url'=>'sales'),array('key'=>'sales_invoice','label'=>'Invoice','url'=>'invoices','badge'=>$this->salesBadge('draft')+$this->salesBadge('approval'),'badge_class'=>'nav-badge-warning'),array('key'=>'sales_delivery','label'=>'Pengiriman / Surat Jalan','url'=>'sales/deliveries','badge'=>$this->salesBadge('delivery'),'badge_class'=>'nav-badge-warning'),array('key'=>'receivables','label'=>'Piutang & Pembayaran','url'=>'receivables','badge'=>$this->salesBadge('receivable'),'badge_class'=>$this->salesBadge('overdue')?'nav-badge-danger':'nav-badge-warning'),array('key'=>'sales_return','label'=>'Retur Penjualan & Refund','url'=>'sales/returns','badge'=>$this->salesBadge('return'),'badge_class'=>'nav-badge-warning'),array('key'=>'customers','label'=>'Customer','url'=>'customers'))), array('key'=>'purchase','label'=>'Pembelian','icon'=>'bi-cart','feature'=>array('purchases','purchase_returns','purchase_refunds','supplier_debit_notes'),'badge'=>$this->purchaseBadge(),'badge_class'=>$this->purchaseBadgeClass(),'badge_title'=>'Tagihan dan dokumen Retur/Refund yang membutuhkan tindakan','children'=>array(array('key'=>'suppliers','label'=>'Master Supplier','url'=>'suppliers','feature'=>'purchases'),array('key'=>'purchases','label'=>'Purchase Workflow','url'=>'purchases','feature'=>'purchases'),array('key'=>'purchase_returns','label'=>'Retur & Refund','url'=>'purchases/returns','feature'=>array('purchase_returns','purchase_refunds','supplier_debit_notes'),'badge'=>$this->returnBadge(),'badge_class'=>$this->returnBadgeClass(),'badge_title'=>'Dokumen Retur/Refund yang membutuhkan tindakan'),array('key'=>'payables','label'=>'Hutang & Pembayaran','url'=>'payables','feature'=>'purchases','badge'=>$this->payableBadge(),'badge_class'=>$this->payableBadgeClass(),'badge_title'=>$this->payableBadgeTitle()))), - array('key'=>'inventory','label'=>'Persediaan','icon'=>'bi-boxes','feature'=>array('items','technician_equipment'),'badge'=>$this->inventoryBadge(),'badge_class'=>($this->inventoryDocumentBadge()+$this->technicianActionBadge())>0?'nav-badge-danger':'nav-badge-warning','badge_title'=>'Barang pending, dokumen stok, atau peralatan teknisi membutuhkan tindakan','children'=>array(array('key'=>'inventory_overview','label'=>'Ringkasan Persediaan','url'=>'inventoryprofessional','feature'=>'items'),array('key'=>'draft_items','label'=>'Barang Pending','url'=>'items/draft_items','feature'=>'items','badge'=>$this->pendingItemBadge(),'badge_class'=>'nav-badge-warning'),array('key'=>'items','label'=>'Daftar Barang','url'=>'items','feature'=>'items'),array('key'=>'inventory_operations','label'=>'Operasional Stok','url'=>'inventoryprofessional/operations','feature'=>'items','badge'=>$this->inventoryDocumentBadge(),'badge_class'=>'nav-badge-danger','badge_title'=>'Dokumen stok menunggu persetujuan atau posting'),array('key'=>'inventory_locations','label'=>'Gudang & Lokasi Rak','url'=>'inventoryprofessional/locations','feature'=>'items'),array('key'=>'kode_barang','label'=>'Jenis / Kode Barang','url'=>'kodebarang','feature'=>'items'),array('key'=>'peralatan_teknisi','label'=>'Peralatan Teknisi','url'=>'teknisi','feature'=>array('technician_equipment','items'),'badge'=>$this->technicianActionBadge(),'badge_class'=>'nav-badge-danger','badge_title'=>'Barang menunggu pemeriksaan atau tindak lanjut'),array('key'=>'inventory_reports','label'=>'Laporan Persediaan','url'=>'inventoryprofessional/reports','feature'=>'items'))), + array('key'=>'inventory','label'=>'Persediaan','icon'=>'bi-boxes','feature'=>'items','badge'=>$this->inventoryBadge(),'badge_class'=>$this->inventoryDocumentBadge()>0?'nav-badge-danger':'nav-badge-warning','badge_title'=>'Barang pending atau dokumen stok membutuhkan tindakan','children'=>array(array('key'=>'inventory_overview','label'=>'Ringkasan Persediaan','url'=>'inventoryprofessional','feature'=>'items'),array('key'=>'draft_items','label'=>'Barang Pending','url'=>'items/draft_items','feature'=>'items','badge'=>$this->pendingItemBadge(),'badge_class'=>'nav-badge-warning'),array('key'=>'items','label'=>'Daftar Barang','url'=>'items','feature'=>'items'),array('key'=>'inventory_operations','label'=>'Operasional Stok','url'=>'inventoryprofessional/operations','feature'=>'items','badge'=>$this->inventoryDocumentBadge(),'badge_class'=>'nav-badge-danger','badge_title'=>'Dokumen stok menunggu persetujuan atau posting'),array('key'=>'inventory_locations','label'=>'Gudang & Lokasi Rak','url'=>'inventoryprofessional/locations','feature'=>'items'),array('key'=>'kode_barang','label'=>'Jenis / Kode Barang','url'=>'kodebarang','feature'=>'items'),array('key'=>'inventory_reports','label'=>'Laporan Persediaan','url'=>'inventoryprofessional/reports','feature'=>'items'))), + array('key'=>'peralatan_teknisi','label'=>'Operasional Perangkat','icon'=>'bi-router','url'=>'teknisi','feature'=>'technician_equipment','badge'=>$this->technicianActionBadge(),'badge_class'=>'nav-badge-danger','badge_title'=>'Perangkat menunggu pemeriksaan atau tindak lanjut'), array('key'=>'fixed_assets','label'=>'Aset Tetap','icon'=>'bi-building-gear','url'=>'fixedassets','feature'=>'asset'), array('key'=>'accounting','label'=>'Accounting','icon'=>'bi-journal-text','feature'=>'jurnal','children'=>array(array('key'=>'jurnal','label'=>'Jurnal Umum','url'=>'jurnal'),array('key'=>'accounts','label'=>'Daftar Akun','url'=>'accounts'),array('key'=>'basejurnal','label'=>'Base Jurnal','url'=>'basejurnal'),array('key'=>'accounting_periods','label'=>'Periode Accounting','url'=>'accountingperiods'))), array('key'=>'professional_reports','label'=>'Laporan Profesional','icon'=>'bi-bar-chart','url'=>'professionalreports','feature'=>'reports'), @@ -29,8 +30,14 @@ class NavigationService private function budgetApprovalBadge(){if($this->budgetBadge!==null)return$this->budgetBadge;if(!$this->allowed('budgets'))return$this->budgetBadge=0;$company=(int)$this->CI->session->userdata('company_id');$total=0;if($this->CI->db->table_exists('budgets')&&(is_master_admin_user()||check_permission('budgets','can_approve'))){$this->CI->db->where('status','submitted');if($this->CI->db->field_exists('company_id','budgets'))$this->CI->db->where('company_id',$company);$total+=(int)$this->CI->db->count_all_results('budgets');}if($this->CI->db->table_exists('account_budget_entries')&&$this->CI->db->table_exists('account_budget_reminder_completions')){$period=date('Y-m');$row=$this->CI->db->query("SELECT COUNT(*) total FROM account_budget_entries e WHERE e.company_id=? AND e.is_active=1 AND e.reminder_enabled=1 AND ((e.entry_type='recurring' AND e.period<=?) OR (e.entry_type='override' AND e.period=?)) AND LEAST(e.reminder_day,DAY(LAST_DAY(CURDATE())))<=LEAST(DAY(LAST_DAY(CURDATE())),DAY(CURDATE())+7) AND NOT EXISTS(SELECT 1 FROM account_budget_reminder_completions c WHERE c.company_id=e.company_id AND c.budget_entry_id=e.id AND c.period=?)",array($company,$period,$period,$period))->row();$total+=(int)($row?$row->total:0);}return$this->budgetBadge=$total;} private function pendingItemBadge(){if($this->pendingBadge!==null)return$this->pendingBadge;if(!$this->CI->db->table_exists('items'))return$this->pendingBadge=0;$company=(int)$this->CI->session->userdata('company_id');if($this->CI->db->table_exists('item_barcodes'))return$this->pendingBadge=(int)$this->CI->db->query("SELECT COUNT(DISTINCT i.id) total FROM items i LEFT JOIN item_barcodes ib ON ib.item_id=i.id AND ib.status='pending' WHERE i.company_id=? AND EXISTS(SELECT 1 FROM goods_receipt_lines grl WHERE grl.item_id=i.id) AND(i.status='draft' OR ib.id IS NOT NULL)",array($company))->row()->total;return$this->pendingBadge=(int)$this->CI->db->where(array('status'=>'draft','company_id'=>$company))->count_all_results('items');} private function inventoryDocumentBadge(){if($this->inventoryDocumentBadge!==null)return$this->inventoryDocumentBadge;if(!$this->CI->db->table_exists('stock_documents'))return$this->inventoryDocumentBadge=0;$this->CI->db->where_in('status',array('submitted','approved'));if($this->CI->db->field_exists('company_id','stock_documents'))$this->CI->db->where('company_id',(int)$this->CI->session->userdata('company_id'));return$this->inventoryDocumentBadge=(int)$this->CI->db->count_all_results('stock_documents');} - private function technicianActionBadge(){if($this->technicianBadge!==null)return$this->technicianBadge;if(!$this->CI->db->table_exists('technician_custody_allocations'))return$this->technicianBadge=0;$this->CI->db->where('company_id',(int)$this->CI->session->userdata('company_id'))->where_in('condition_status',array('inspection_required','repair_required','damaged','lost'));return$this->technicianBadge=(int)$this->CI->db->count_all_results('technician_custody_allocations');} - private function inventoryBadge(){return$this->pendingItemBadge()+$this->inventoryDocumentBadge()+$this->technicianActionBadge();} + private function technicianActionBadge(){ + if($this->technicianBadge!==null)return$this->technicianBadge; + if(!$this->CI->db->table_exists('technician_custody_allocations')||(!is_master_admin_user()&&!check_permission('technician_inspection','can_view')))return$this->technicianBadge=0; + $technicianId=0;$role=strtolower((string)$this->CI->session->userdata('role')); + if(!is_master_admin_user()&&!check_permission('technician_equipment_all','can_view')&&strpos($role,'teknisi')!==false&&$this->CI->db->field_exists('employee_id','users')){$user=$this->CI->db->select('employee_id')->get_where('users',array('id'=>(int)$this->CI->session->userdata('user_id')))->row();$technicianId=$user?(int)$user->employee_id:0;} + $this->CI->load->library('TechnicianEquipmentService');$stats=$this->CI->technicianequipmentservice->inspectionQueueStats($technicianId);return$this->technicianBadge=(int)$stats['row_count']; + } + private function inventoryBadge(){return$this->pendingItemBadge()+$this->inventoryDocumentBadge();} private function payableBadge(){if($this->payableBadge!==null)return$this->payableBadge;if(!$this->CI->db->table_exists('supplier_invoices'))return$this->payableBadge=0;$this->CI->db->select("COUNT(DISTINCT id) total,SUM(CASE WHEN due_date<=CURDATE() THEN 1 ELSE 0 END) overdue",false)->where('status','partial')->where('balance >',0);if($this->CI->db->field_exists('company_id','supplier_invoices'))$this->CI->db->where('company_id',(int)$this->CI->session->userdata('company_id'));$row=$this->CI->db->get('supplier_invoices')->row();$this->payableBadge=$row?(int)$row->total:0;$this->payableBadgeClass=$row&&(int)$row->overdue>0?'nav-badge-danger':($this->payableBadge>0?'nav-badge-warning':'');return$this->payableBadge;} private function payableBadgeClass(){$this->payableBadge();return$this->payableBadgeClass;} private function payableBadgeTitle(){$this->payableBadge();return$this->payableBadgeClass==='nav-badge-danger'?'Ada tagihan yang sudah jatuh tempo':'Tagihan belum lunas dan belum jatuh tempo';} @@ -38,6 +45,13 @@ class NavigationService private function returnBadgeClass(){$this->returnBadge();return$this->returnBadgeClass;} private function purchaseBadge(){return($this->allowed('purchases')?$this->payableBadge():0)+$this->returnBadge();} private function purchaseBadgeClass(){return($this->allowed('purchases')&&$this->payableBadgeClass()==='nav-badge-danger')||$this->returnBadgeClass()==='nav-badge-danger'?'nav-badge-danger':($this->purchaseBadge()>0?'nav-badge-warning':'');} - public function breadcrumb($active){foreach($this->items()as$i){if($i['key']===$active)return array($i['label']);foreach(isset($i['children'])?$i['children']:array()as$c)if($c['key']===$active)return array($i['label'],$c['label']);}return array(ucwords(str_replace('_',' ',$active)));} + public function breadcrumb($active){ + if($active==='peralatan_teknisi'){ + $pages=array('index'=>'Ringkasan','bulk_workflow'=>'Pergerakan Batch','detail'=>'Detail Teknisi','replacement_workflow'=>'Pemasangan & Penggantian','inspection_queue'=>'Pemeriksaan','customer_register'=>'Barang Customer','log_barang'=>'Riwayat & PDF'); + $method=strtolower((string)$this->CI->router->fetch_method()); + return array('Operasional Perangkat',$pages[$method]??'Ringkasan'); + } + foreach($this->items()as$i){if($i['key']===$active)return array($i['label']);foreach(isset($i['children'])?$i['children']:array()as$c)if($c['key']===$active)return array($i['label'],$c['label']);}return array(ucwords(str_replace('_',' ',$active))); + } public function quickActions(){$defs=array(array('label'=>'Jurnal','icon'=>'bi-journal-plus','url'=>'jurnal','feature'=>'jurnal'),array('label'=>'Terima Pembayaran','icon'=>'bi-cash-coin','url'=>'receivables','feature'=>'invoices'),array('label'=>'Bayar Supplier','icon'=>'bi-wallet2','url'=>'payables','feature'=>'purchases'),array('label'=>'Scan QR','icon'=>'bi-qr-code-scan','url'=>'inventoryprofessional#scanner','feature'=>'items'));$out=array();foreach($defs as$d)if($this->allowed($d['feature']))$out[]=$d;return$out;} } diff --git a/application/migrations/20260915000100_separate_device_operations_permissions.php b/application/migrations/20260915000100_separate_device_operations_permissions.php new file mode 100644 index 0000000..01d4d99 --- /dev/null +++ b/application/migrations/20260915000100_separate_device_operations_permissions.php @@ -0,0 +1,83 @@ +db->table_exists('roles')) return; + + $deviceFeatures = array( + 'technician_equipment', 'technician_equipment_all', + 'technician_attachment', 'technician_handover', + 'technician_installation', 'technician_transfer', + 'technician_return', 'technician_inspection', + 'technician_customer_item', 'technician_condition', + 'technician_correction' + ); + + foreach ($this->db->select('id,permissions,is_super_admin')->get('roles')->result() as $role) { + if ((int)$role->is_super_admin === 1) continue; + $permissions = json_decode((string)$role->permissions, true); + if (!is_array($permissions)) $permissions = array(); + $changed = false; + + // Migrasi lama sudah membuat permission operasional dari Persediaan. + // Bagian ini tetap idempotent untuk database yang melewati migrasi lama. + if (!isset($permissions['technician_equipment']) && isset($permissions['items']) && in_array('can_view', (array)$permissions['items'], true)) { + $permissions['technician_equipment'] = array_values(array_unique(array_intersect( + (array)$permissions['items'], + array('can_view','can_create','can_update','can_delete','can_submit','can_approve','can_post','can_export','can_import') + ))); + $changed = true; + } + + if (!isset($permissions['technician_customer_item'])) { + $source = isset($permissions['technician_equipment']) ? (array)$permissions['technician_equipment'] : array(); + $customerRights = array_values(array_intersect($source, array('can_view','can_create','can_update'))); + if ($customerRights) { + $permissions['technician_customer_item'] = $customerRights; + $changed = true; + } + } + + $hasDevicePermission = false; + foreach ($deviceFeatures as $feature) { + if (!empty($permissions[$feature])) { $hasDevicePermission = true; break; } + } + if ($hasDevicePermission && (empty($permissions['technician_equipment']) || !in_array('can_view', (array)$permissions['technician_equipment'], true))) { + $permissions['technician_equipment'] = array_values(array_unique(array_merge( + array('can_view'), + isset($permissions['technician_equipment']) ? (array)$permissions['technician_equipment'] : array() + ))); + $changed = true; + } + + if ($changed) { + $this->db->where('id', (int)$role->id)->update('roles', array( + 'permissions' => json_encode($permissions), + 'updated_at' => date('Y-m-d H:i:s') + )); + } + } + } + + public function down() + { + if (!$this->db->table_exists('roles')) return; + foreach ($this->db->select('id,permissions,is_super_admin')->get('roles')->result() as $role) { + if ((int)$role->is_super_admin === 1) continue; + $permissions = json_decode((string)$role->permissions, true); + if (!is_array($permissions) || !isset($permissions['technician_customer_item'])) continue; + unset($permissions['technician_customer_item']); + $this->db->where('id', (int)$role->id)->update('roles', array( + 'permissions' => json_encode($permissions), + 'updated_at' => date('Y-m-d H:i:s') + )); + } + } +} diff --git a/application/views/inventory_professional/_module_nav.php b/application/views/inventory_professional/_module_nav.php index de407f1..f267896 100644 --- a/application/views/inventory_professional/_module_nav.php +++ b/application/views/inventory_professional/_module_nav.php @@ -10,6 +10,5 @@ Operasional Stok Gudang & Rak Jenis & Kode Barang - Peralatan Teknisi Laporan diff --git a/application/views/peralatan_teknisi/_module_header.php b/application/views/peralatan_teknisi/_module_header.php new file mode 100644 index 0000000..4f237d1 --- /dev/null +++ b/application/views/peralatan_teknisi/_module_header.php @@ -0,0 +1,11 @@ + + +
+
OPERASIONAL PERANGKAT

+ Scan Perangkat +
diff --git a/application/views/peralatan_teknisi/_module_nav.php b/application/views/peralatan_teknisi/_module_nav.php index 5e5d242..cf39948 100644 --- a/application/views/peralatan_teknisi/_module_nav.php +++ b/application/views/peralatan_teknisi/_module_nav.php @@ -5,7 +5,7 @@ $techNavDashboard = isset($equipment_dashboard) && is_array($equipment_dashboard $techNavInspectionCount = (int)($techNavDashboard['inspection_count'] ?? 0); $techNavCanBatch = !empty($techNavPermissions['handover']) || !empty($techNavPermissions['transfer']) || !empty($techNavPermissions['return']); ?> -