CI =& get_instance(); } public function create($module, $entityType, $entityId, $amount, $userId, $snapshot) { $existing = $this->CI->db->where(array('entity_type'=>$entityType,'entity_id'=>(int)$entityId,'status'=>'pending'))->get('approval_requests')->row(); if ($existing) return $existing; $workflow = $this->CI->db->where(array('module'=>$module,'entity_type'=>$entityType,'is_active'=>1)) ->group_start()->where('min_amount IS NULL', null, false)->or_where('min_amount <=', $amount)->group_end() ->group_start()->where('max_amount IS NULL', null, false)->or_where('max_amount >=', $amount)->group_end() ->order_by('min_amount','DESC')->get('approval_workflows')->row(); if (!$workflow) throw new BusinessException('Workflow persetujuan belum dikonfigurasi.'); $requestNo = 'APR-'.date('Ymd-His').'-'.$entityId.'-'.random_int(100,999); $this->CI->db->insert('approval_requests', array('request_no'=>$requestNo,'workflow_id'=>$workflow->id,'entity_type'=>$entityType,'entity_id'=>(int)$entityId,'amount'=>$amount,'requested_by'=>(int)$userId,'requested_at'=>date('Y-m-d H:i:s'),'current_step'=>1,'status'=>'pending','snapshot'=>json_encode($snapshot, JSON_UNESCAPED_UNICODE))); $id = $this->CI->db->insert_id(); $this->CI->db->insert('approval_actions', array('request_id'=>$id,'step_order'=>0,'action'=>'submitted','user_id'=>(int)$userId,'created_at'=>date('Y-m-d H:i:s'))); return $this->find($id); } public function act($requestId, $action, $userId, $roleId, $notes = null) { $request = $this->CI->db->query('SELECT * FROM approval_requests WHERE id=? FOR UPDATE', array((int)$requestId))->row(); if (!$request || $request->status !== 'pending') throw new BusinessException('Permintaan approval tidak aktif.'); $step = $this->CI->db->get_where('approval_workflow_steps', array('workflow_id'=>$request->workflow_id,'step_order'=>$request->current_step))->row(); $role = $this->CI->db->get_where('roles', array('id'=>(int)$roleId))->row(); $masterAdmin = $role && (!empty($role->is_super_admin) || $role->nama_role === 'Admin'); if (!$step || (!$masterAdmin && (($step->approver_user_id && (int)$step->approver_user_id !== (int)$userId) || (!$step->approver_user_id && (int)$step->approver_role_id !== (int)$roleId)))) throw new BusinessException('Anda bukan approver pada tahap ini.', array(), 403); $workflow = $this->CI->db->get_where('approval_workflows', array('id'=>$request->workflow_id))->row(); if (!$masterAdmin && $workflow->require_separation && (int)$request->requested_by === (int)$userId) throw new BusinessException('Pembuat transaksi tidak boleh menyetujui transaksinya sendiri.'); if (!in_array($action,array('approved','rejected'),true)) throw new BusinessException('Tindakan approval tidak valid.'); if ($action === 'rejected' && trim((string)$notes) === '') throw new BusinessException('Alasan penolakan wajib diisi.'); $this->CI->db->insert('approval_actions', array('request_id'=>$request->id,'step_order'=>$request->current_step,'action'=>$action,'user_id'=>(int)$userId,'notes'=>$notes,'created_at'=>date('Y-m-d H:i:s'))); $next = $this->CI->db->get_where('approval_workflow_steps',array('workflow_id'=>$request->workflow_id,'step_order'=>$request->current_step+1))->row(); $final = $action === 'rejected' ? 'rejected' : ($next ? 'pending' : 'approved'); $update = array('status'=>$final); if ($next) $update['current_step'] = $request->current_step+1; if ($final !== 'pending') $update['completed_at'] = date('Y-m-d H:i:s'); $this->CI->db->where('id',$request->id)->update('approval_requests',$update); return $this->find($request->id); } public function pendingFor($userId, $roleId) { $role=$this->CI->db->get_where('roles',array('id'=>(int)$roleId))->row(); $masterAdmin=$role&&(!empty($role->is_super_admin)||$role->nama_role==='Admin'); if($masterAdmin){$rows=$this->CI->db->select('approval_requests.*, approval_workflows.name workflow_name, approval_workflows.module workflow_module, users.nama requester_name')->from('approval_requests')->join('approval_workflows','approval_workflows.id=approval_requests.workflow_id')->join('users','users.id=approval_requests.requested_by','left')->where('approval_requests.status','pending')->order_by('approval_requests.requested_at','ASC')->get()->result();return $this->withDestinations($rows);} $rows=$this->CI->db->select('approval_requests.*, approval_workflows.name workflow_name, approval_workflows.module workflow_module, users.nama requester_name') ->from('approval_requests')->join('approval_workflows','approval_workflows.id=approval_requests.workflow_id') ->join('approval_workflow_steps','approval_workflow_steps.workflow_id=approval_requests.workflow_id AND approval_workflow_steps.step_order=approval_requests.current_step') ->join('users','users.id=approval_requests.requested_by','left')->where('approval_requests.status','pending') ->group_start()->where('approval_workflow_steps.approver_user_id',(int)$userId)->or_group_start()->where('approval_workflow_steps.approver_user_id IS NULL',null,false)->where('approval_workflow_steps.approver_role_id',(int)$roleId)->group_end()->group_end() ->order_by('approval_requests.requested_at','ASC')->get()->result(); return $this->withDestinations($rows); } private function withDestinations(array $rows) { foreach($rows as $row){$destination=$this->destinationFor($row->entity_type,$row->entity_id,$row->workflow_module);$row->destination_url=base_url($destination[0]);$row->destination_label=$destination[1];} return $rows; } private function destinationFor($type,$id,$module) { $id=(int)$id; $map=array( 'journal'=>array('jurnal?focus='.$id,'Buka Jurnal'), 'invoice'=>array('invoices/detail/'.$id,'Buka Invoice'), 'purchase_request'=>array('purchases?step=request&focus='.$id,'Buka Purchase Request'), 'purchase_order'=>array('purchases?step=order&focus='.$id,'Buka Purchase Order'), 'purchase_return'=>array('purchases/returns?section=return&focus='.$id,'Buka Pengajuan Retur'), 'supplier_refund'=>array('purchases/returns?section=refund&focus='.$id,'Buka Pengajuan Refund'), 'supplier_invoice'=>array('purchases?step=invoice&focus='.$id,'Buka Tagihan Supplier'), 'stock_document'=>array('inventoryprofessional?focus='.$id,'Buka Dokumen Persediaan'), 'cash_transaction'=>array('cashbank?focus='.$id,'Buka Transaksi Kas/Bank'), 'payroll_period'=>array('hrpayroll?focus='.$id,'Buka Payroll'), 'manual_attendance'=>array('attendancemonitoring?focus='.$id,'Buka Absensi Manual'), 'attendance_correction'=>array('attendancemonitoring?focus='.$id,'Buka Koreksi Absensi'), 'leave_request'=>array('leaverequests?focus='.$id,'Buka Pengajuan Cuti'), 'budget'=>array('budgets?focus='.$id,'Buka Budget') ); if(isset($map[$type]))return $map[$type]; $fallback=array('purchase'=>'purchases','payable'=>'purchases?step=invoice','inventory'=>'inventoryprofessional','invoice'=>'invoices','journal'=>'jurnal','cash_bank'=>'cashbank','payroll'=>'hrpayroll','hr'=>'attendancemonitoring','budget'=>'budgets'); return array(isset($fallback[$module])?$fallback[$module]:'dashboard','Buka Dokumen'); } public function find($id) { return $this->CI->db->get_where('approval_requests',array('id'=>(int)$id))->row(); } public function pendingForEntity($type,$id) { return $this->CI->db->where(array('entity_type'=>$type,'entity_id'=>(int)$id,'status'=>'pending'))->get('approval_requests')->row(); } }