Files
accounting_dev_v2/application/controllers/Leaverequests.php
T
2026-09-14 11:14:25 +07:00

199 lines
15 KiB
PHP

<?php
defined('BASEPATH') OR exit('No direct script access allowed');
class Leaverequests extends MY_Controller
{
private $leaveTypes = array('annual','sick','permit','maternity','unpaid');
public function __construct()
{
parent::__construct();
$this->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;
}
}