load->library(array('CompanyContext','ApprovalService','AuditService','TransactionService','NumberingService')); } private function company(){return (int)$this->companycontext->id();} private function uid(){return (int)$this->session->userdata('user_id');} private function roleId() { $role=(int)$this->session->userdata('role_id'); if($role)return $role; $user=$this->db->select('role_id')->get_where('users',array('id'=>$this->uid()))->row(); return $user?(int)$user->role_id:0; } private function can($action){return is_master_admin_user()||check_permission('employees',$action);} private function permissions(){return array('create'=>$this->can('can_create'),'update'=>$this->can('can_update'),'approve'=>$this->can('can_approve'),'export'=>$this->can('can_export'));} public function index() { $company=$this->company(); $employees=$this->db->select('id,employee_code,fingerprint_user_id,full_name')->where(array('company_id'=>$company,'is_active'=>1))->order_by('full_name')->get('k_employees')->result(); $counts=$this->workflowCounts(); $data=array('active_menu'=>'leave_requests','employees'=>$employees,'permissions'=>$this->permissions(),'counts'=>$counts); $this->load->view('partials/header',$data); $this->load->view('employees/leave_requests',$data); $this->load->view('partials/footer'); } public function get_data() { $status=trim((string)$this->input->get('status')); $type=trim((string)$this->input->get('type')); $employee=(int)$this->input->get('employee_id'); $this->db->select('lr.id,lr.request_no,lr.employee_id,lr.leave_type,lr.start_date,lr.end_date,lr.total_days,lr.reason,lr.approval_status,lr.workflow_status,lr.submitted_at,lr.approved_at,lr.rejected_at,lr.rejection_reason,lr.created_by,lr.submitted_by,e.full_name,e.employee_code,e.fingerprint_user_id,d.department_name,p.position_name,creator.nama creator_name,approver.nama approver_name,rejector.nama rejector_name') ->from('k_leave_requests lr')->join('k_employees e','e.id=lr.employee_id','left') ->join('k_departments d','d.id=e.department_id','left')->join('k_positions p','p.id=e.position_id','left') ->join('users creator','creator.id=lr.created_by','left')->join('users approver','approver.id=lr.approved_by','left')->join('users rejector','rejector.id=lr.rejected_by','left') ->where('lr.company_id',$this->company()); if(in_array($status,array('draft','submitted','approved','rejected','cancelled'),true))$this->db->where('lr.workflow_status',$status); if(in_array($type,$this->leaveTypes,true))$this->db->where('lr.leave_type',$type); if($employee>0)$this->db->where('lr.employee_id',$employee); $rows=$this->db->order_by('lr.id','DESC')->get()->result(); return json_response(true,'Daftar pengajuan cuti.',array('data'=>$rows,'summary'=>$this->workflowCounts())); } public function detail($id) { $leave=$this->find((int)$id); if(!$leave)return json_response(false,'Pengajuan cuti tidak ditemukan.',array(),404); $history=array(); $history=$this->db->select('aa.action,aa.notes,aa.created_at,aa.step_order,u.nama user_name,aws.name step_name,ar.request_no approval_no') ->from('approval_actions aa')->join('approval_requests ar','ar.id=aa.request_id') ->join('users u','u.id=aa.user_id','left')->join('approval_workflow_steps aws','aws.workflow_id=ar.workflow_id AND aws.step_order=aa.step_order','left') ->where(array('ar.entity_type'=>'leave_request','ar.entity_id'=>$leave->id))->order_by('aa.id')->get()->result(); return json_response(true,'Detail pengajuan cuti.',array('leave'=>$leave,'history'=>$history,'permissions'=>$this->permissions())); } public function save() { if(strtoupper($this->input->method())!=='POST')return json_response(false,'Metode tidak diizinkan.',array(),405); $id=(int)$this->input->post('id'); $this->requirePermission('employees',$id?'can_update':'can_create'); try{ $saved=$this->transactionservice->run(function()use($id){ $before=$id?$this->lock($id):null; if($id&&!in_array($before->workflow_status,array('draft','rejected'),true))throw new BusinessException('Hanya Draft atau pengajuan yang ditolak yang dapat diperbaiki.'); $employeeId=(int)$this->input->post('employee_id'); $employee=$this->db->get_where('k_employees',array('id'=>$employeeId,'company_id'=>$this->company(),'is_active'=>1))->row(); if(!$employee)throw new BusinessException('Karyawan aktif tidak ditemukan.'); $type=trim((string)$this->input->post('leave_type')); if(!in_array($type,$this->leaveTypes,true))throw new BusinessException('Jenis cuti atau izin tidak valid.'); $start=$this->validDate($this->input->post('start_date'),'Tanggal mulai'); $end=$this->validDate($this->input->post('end_date'),'Tanggal selesai'); if($end<$start)throw new BusinessException('Tanggal selesai tidak boleh sebelum tanggal mulai.'); $days=(int)((strtotime($end)-strtotime($start))/86400)+1; if($days<1||$days>366)throw new BusinessException('Rentang cuti harus antara 1 sampai 366 hari.'); $reason=trim((string)$this->input->post('reason')); if($reason==='')throw new BusinessException('Alasan pengajuan wajib diisi.'); if(mb_strlen($reason)>2000)throw new BusinessException('Alasan pengajuan maksimal 2.000 karakter.'); $overlap=$this->db->where(array('company_id'=>$this->company(),'employee_id'=>$employeeId))->where_in('workflow_status',array('submitted','approved'))->where('start_date <=',$end)->where('end_date >=',$start); if($id)$overlap->where('id !=',$id); if($overlap->count_all_results('k_leave_requests'))throw new BusinessException('Karyawan sudah mempunyai pengajuan aktif pada rentang tanggal tersebut.'); $row=array('employee_id'=>$employeeId,'leave_type'=>$type,'start_date'=>$start,'end_date'=>$end,'total_days'=>$days,'reason'=>$reason,'approval_status'=>'pending','workflow_status'=>'draft','rejection_reason'=>null,'rejected_by'=>null,'rejected_at'=>null,'updated_by'=>$this->uid(),'updated_at'=>date('Y-m-d H:i:s')); if($id){$row['version']=(int)$before->version+1;$this->db->where(array('id'=>$id,'company_id'=>$this->company()))->update('k_leave_requests',$row);$leaveId=$id;$auditAction='update';} else{$row['company_id']=$this->company();$row['request_no']=$this->numberingservice->next('leave_request',$start);$row['created_by']=$this->uid();$row['created_at']=date('Y-m-d H:i:s');$row['version']=1;$this->db->insert('k_leave_requests',$row);$leaveId=(int)$this->db->insert_id();$auditAction='create';} $after=$this->find($leaveId); $this->auditservice->record('hr','leave_request',$leaveId,$auditAction,$before,$after,$this->uid()); return $leaveId; }); return json_response(true,$id?'Draft pengajuan berhasil diperbarui.':'Draft pengajuan berhasil dibuat.',array('id'=>$saved)); }catch(Throwable$e){return business_exception_response($e);} } public function submit($id) { $this->requirePermission('employees','can_create'); return $this->action(function()use($id){ $before=$this->lock((int)$id); if($before->workflow_status!=='draft')throw new BusinessException('Hanya Draft yang dapat diajukan.'); $request=$this->approvalservice->create('hr','leave_request',$before->id,0,$this->uid(),array('company_id'=>$this->company(),'request_no'=>$before->request_no,'employee_id'=>$before->employee_id,'employee_name'=>$before->full_name,'leave_type'=>$before->leave_type,'start_date'=>$before->start_date,'end_date'=>$before->end_date,'total_days'=>$before->total_days)); $update=array('workflow_status'=>'submitted','approval_status'=>'pending','approval_request_id'=>(int)$request->id,'submitted_by'=>$this->uid(),'submitted_at'=>date('Y-m-d H:i:s'),'updated_by'=>$this->uid(),'updated_at'=>date('Y-m-d H:i:s'),'version'=>(int)$before->version+1); $this->db->where(array('id'=>$before->id,'company_id'=>$this->company()))->update('k_leave_requests',$update); $this->auditservice->record('hr','leave_request',$before->id,'submit',$before,$update,$this->uid()); return $before->id; },'Pengajuan cuti berhasil dikirim untuk persetujuan.'); } public function approve($id) { $this->requirePermission('employees','can_approve'); return $this->action(function()use($id){ $before=$this->lock((int)$id); if($before->workflow_status!=='submitted')throw new BusinessException('Pengajuan tidak sedang menunggu persetujuan.'); $request=$this->approvalservice->pendingForEntity('leave_request',$before->id); if(!$request)throw new BusinessException('Permintaan persetujuan tidak ditemukan.'); $result=$this->approvalservice->act($request->id,'approved',$this->uid(),$this->roleId(),trim((string)$this->input->post('notes'))?:null); $update=array('updated_by'=>$this->uid(),'updated_at'=>date('Y-m-d H:i:s'),'version'=>(int)$before->version+1); if($result->status==='approved'){$update['workflow_status']='approved';$update['approval_status']='approved';$update['approved_by']=$this->uid();$update['approved_at']=date('Y-m-d H:i:s');} $this->db->where(array('id'=>$before->id,'company_id'=>$this->company()))->update('k_leave_requests',$update); $this->auditservice->record('hr','leave_request',$before->id,'approve',$before,$update,$this->uid()); return $before->id; },'Pengajuan cuti berhasil disetujui.'); } public function reject($id) { $this->requirePermission('employees','can_approve'); return $this->action(function()use($id){ $reason=trim((string)$this->input->post('reason')); if($reason==='')throw new BusinessException('Alasan penolakan wajib diisi.'); $before=$this->lock((int)$id); if($before->workflow_status!=='submitted')throw new BusinessException('Pengajuan tidak sedang menunggu persetujuan.'); $request=$this->approvalservice->pendingForEntity('leave_request',$before->id); if(!$request)throw new BusinessException('Permintaan persetujuan tidak ditemukan.'); $this->approvalservice->act($request->id,'rejected',$this->uid(),$this->roleId(),$reason); $update=array('workflow_status'=>'rejected','approval_status'=>'rejected','rejected_by'=>$this->uid(),'rejected_at'=>date('Y-m-d H:i:s'),'rejection_reason'=>$reason,'updated_by'=>$this->uid(),'updated_at'=>date('Y-m-d H:i:s'),'version'=>(int)$before->version+1); $this->db->where(array('id'=>$before->id,'company_id'=>$this->company()))->update('k_leave_requests',$update); $this->auditservice->record('hr','leave_request',$before->id,'reject',$before,$update,$this->uid()); return $before->id; },'Pengajuan cuti ditolak dan dapat diperbaiki kembali.'); } public function cancel($id) { if(!$this->can('can_update')&&!$this->can('can_create'))return json_response(false,'Anda tidak memiliki izin untuk membatalkan Draft.',array(),403); return $this->action(function()use($id){ $before=$this->lock((int)$id); if(!in_array($before->workflow_status,array('draft','rejected'),true))throw new BusinessException('Hanya Draft atau pengajuan ditolak yang dapat dibatalkan.'); $update=array('workflow_status'=>'cancelled','cancelled_by'=>$this->uid(),'cancelled_at'=>date('Y-m-d H:i:s'),'updated_by'=>$this->uid(),'updated_at'=>date('Y-m-d H:i:s'),'version'=>(int)$before->version+1); $this->db->where(array('id'=>$before->id,'company_id'=>$this->company()))->update('k_leave_requests',$update); $this->auditservice->record('hr','leave_request',$before->id,'cancel',$before,$update,$this->uid()); return $before->id; },'Draft pengajuan berhasil dibatalkan.'); } private function action($callback,$message) { if(strtoupper($this->input->method())!=='POST')return json_response(false,'Metode tidak diizinkan.',array(),405); try{$result=$this->transactionservice->run($callback);return json_response(true,$message,array('result'=>$result));}catch(Throwable$e){return business_exception_response($e);} } private function lock($id) { $row=$this->db->query('SELECT lr.*,e.full_name FROM k_leave_requests lr LEFT JOIN k_employees e ON e.id=lr.employee_id WHERE lr.id=? AND lr.company_id=? FOR UPDATE',array((int)$id,$this->company()))->row(); if(!$row)throw new BusinessException('Pengajuan cuti tidak ditemukan.'); return $row; } private function find($id) { return $this->db->select('lr.*,e.full_name,e.employee_code,e.fingerprint_user_id,d.department_name,p.position_name,ar.workflow_id,ar.request_no approval_no,ar.current_step,ar.status approval_request_status,creator.nama creator_name,submitter.nama submitter_name,approver.nama approver_name,rejector.nama rejector_name') ->from('k_leave_requests lr')->join('k_employees e','e.id=lr.employee_id','left')->join('k_departments d','d.id=e.department_id','left')->join('k_positions p','p.id=e.position_id','left') ->join('approval_requests ar','ar.id=lr.approval_request_id','left')->join('users creator','creator.id=lr.created_by','left')->join('users submitter','submitter.id=lr.submitted_by','left')->join('users approver','approver.id=lr.approved_by','left')->join('users rejector','rejector.id=lr.rejected_by','left') ->where(array('lr.id'=>(int)$id,'lr.company_id'=>$this->company()))->get()->row(); } private function validDate($value,$label) { $value=trim((string)$value);$date=DateTime::createFromFormat('Y-m-d',$value); if(!$date||$date->format('Y-m-d')!==$value)throw new BusinessException($label.' tidak valid.'); return $value; } private function workflowCounts() { $counts=array('all'=>0,'draft'=>0,'submitted'=>0,'approved'=>0,'rejected'=>0,'cancelled'=>0); foreach($this->db->select('workflow_status,COUNT(*) total')->where('company_id',$this->company())->group_by('workflow_status')->get('k_leave_requests')->result() as $row){$counts[$row->workflow_status]=(int)$row->total;$counts['all']+=(int)$row->total;} return $counts; } }