diff --git a/application/controllers/Employees.php b/application/controllers/Employees.php index e16bfbf..bbf4c87 100644 --- a/application/controllers/Employees.php +++ b/application/controllers/Employees.php @@ -407,75 +407,64 @@ class Employees extends MY_Controller { // ========================================================= private function prepareData() { + $nullableText = function ($value) { + $value = trim((string) $value); + return $value === '' || in_array(strtolower($value), array('undefined', 'null'), true) + ? null + : $value; + }; + $nullableId = function ($value) { + $value = (int) $value; + return $value > 0 ? $value : null; + }; + $data = [ - 'nik' => trim($this->input->post('nik')), + 'nik' => $nullableText($this->input->post('nik')), 'full_name' => trim( $this->input->post('full_name') ), - 'gender' => $this->input->post('gender'), + 'gender' => $nullableText($this->input->post('gender')), - 'birth_place' => trim( - $this->input->post('birth_place') - ), + 'birth_place' => $nullableText($this->input->post('birth_place')), - 'birth_date' => $this->input->post('birth_date'), + 'birth_date' => $nullableText($this->input->post('birth_date')), - 'phone' => trim( - $this->input->post('phone') - ), + 'phone' => $nullableText($this->input->post('phone')), - 'email' => trim( - $this->input->post('email') - ), + 'email' => $nullableText($this->input->post('email')), - 'address' => trim( - $this->input->post('address') - ), + 'address' => $nullableText($this->input->post('address')), 'join_date' => $this->input->post('join_date'), - 'resign_date' => $this->input->post('resign_date'), + 'resign_date' => $nullableText($this->input->post('resign_date')), 'employment_status' => $this->input->post('employment_status'), 'marital_status' => $this->input->post('marital_status'), - 'religion' => trim( - $this->input->post('religion') - ), + 'religion' => $nullableText($this->input->post('religion')), - 'department_id' => $this->input->post('department_id'), + 'department_id' => $nullableId($this->input->post('department_id')), - 'position_id' => $this->input->post('position_id'), + 'position_id' => $nullableId($this->input->post('position_id')), - 'branch_id' => $this->input->post('branch_id'), + 'branch_id' => $nullableId($this->input->post('branch_id')), - 'bank_name' => trim( - $this->input->post('bank_name') - ), + 'bank_name' => $nullableText($this->input->post('bank_name')), - 'bank_account_number' => trim( - $this->input->post('bank_account_number') - ), + 'bank_account_number' => $nullableText($this->input->post('bank_account_number')), - 'bank_account_name' => trim( - $this->input->post('bank_account_name') - ), + 'bank_account_name' => $nullableText($this->input->post('bank_account_name')), - 'bpjs_kesehatan_number' => trim( - $this->input->post('bpjs_kesehatan_number') - ), + 'bpjs_kesehatan_number' => $nullableText($this->input->post('bpjs_kesehatan_number')), - 'bpjs_ketenagakerjaan_number' => trim( - $this->input->post('bpjs_ketenagakerjaan_number') - ), + 'bpjs_ketenagakerjaan_number' => $nullableText($this->input->post('bpjs_ketenagakerjaan_number')), - 'fingerprint_user_id' => trim( - $this->input->post('fingerprint_user_id') - ), + 'fingerprint_user_id' => $nullableText($this->input->post('fingerprint_user_id')), 'is_active' => $this->input->post('is_active') === '0' ? 0 : 1 ]; @@ -492,7 +481,10 @@ class Employees extends MY_Controller { private function canEmployee($action) { - return is_master_admin_user() || check_permission('hr_employees',$action) || check_permission('employees',$action); + return $this->session->userdata('role') === 'Admin' + || is_master_admin_user() + || check_permission('hr_employees',$action) + || check_permission('employees',$action); } private function maskRestrictedSalary($employee) diff --git a/application/controllers/Organizationmaster.php b/application/controllers/Organizationmaster.php index 437c163..a868b14 100644 --- a/application/controllers/Organizationmaster.php +++ b/application/controllers/Organizationmaster.php @@ -9,5 +9,5 @@ class Organizationmaster extends MY_Controller private function saveMaster($table,$nameField,$position=false){$id=(int)$this->input->post('id');$code=strtoupper(trim($this->input->post('code',true)));$name=trim($this->input->post('name',true));if($code===''||$name==='')return json_response(false,'Kode dan nama wajib diisi.',array(),422);$q=$this->db->where('code',$code);if($id)$q->where('id !=',$id);if($q->count_all_results($table))return json_response(false,'Kode sudah digunakan.',array(),422);$data=array('code'=>$code,$nameField=>$name,'description'=>trim($this->input->post('description',true))?:null,'is_active'=>$this->input->post('is_active')?1:0);if($position)$data['department_id']=$this->input->post('department_id')?:null;if($id)$this->db->where('id',$id)->update($table,$data);else{$this->db->insert($table,$data);$id=$this->db->insert_id();}return json_response(true,'Data berhasil disimpan.',array('id'=>$id));} public function delete_department($id){return$this->deactivate('k_departments',$id,'department_id');} public function delete_position($id){return$this->deactivate('k_positions',$id,'position_id');} - private function deactivate($table,$id,$employeeField){if($this->db->where($employeeField,(int)$id)->count_all_results('k_employees')){$this->db->where('id',(int)$id)->update($table,array('is_active'=>0));return json_response(true,'Data sudah digunakan karyawan, sehingga dinonaktifkan dan tidak dihapus.');}$this->db->where('id',(int)$id)->delete($table);return json_response(true,'Data berhasil dihapus.');} + private function deactivate($table,$id,$employeeField){$id=(int)$id;$used=$this->db->where($employeeField,$id)->count_all_results('k_employees')>0;if($table==='k_departments'&&$this->db->where('department_id',$id)->count_all_results('k_positions')>0)$used=true;if($used){$this->db->where('id',$id)->update($table,array('is_active'=>0));return json_response(true,'Data sudah digunakan, sehingga dinonaktifkan dan tidak dihapus.');}$this->db->where('id',$id)->delete($table);return json_response(true,'Data berhasil dihapus.');} } diff --git a/application/libraries/PayrollService.php b/application/libraries/PayrollService.php index a242700..3ab9296 100644 --- a/application/libraries/PayrollService.php +++ b/application/libraries/PayrollService.php @@ -26,7 +26,19 @@ class PayrollService if($period->workflow_status!=='draft')throw new BusinessException('Hanya payroll Draft yang dapat dihitung ulang.'); $start=$period->attendance_start_date?:$period->start_date;$end=$period->attendance_end_date?:$period->end_date; if(!$db->get_where('payroll_attendance_locks',array('company_id'=>$companyId,'payroll_period_id'=>$period->id,'status'=>'locked'))->row())throw new BusinessException('Kehadiran periode ini harus dikunci sebelum payroll dihitung.'); - $employees=$db->where(array('company_id'=>$companyId,'is_active'=>1))->order_by('full_name')->get('k_employees')->result(); + $employmentEmployees=$db + ->where(array('company_id'=>$companyId,'is_active'=>1)) + ->where('join_date <=',$end) + ->group_start() + ->where('resign_date IS NULL',null,false) + ->or_where('resign_date','0000-00-00') + ->or_where('resign_date >=',$start) + ->group_end() + ->order_by('full_name') + ->get('k_employees') + ->result(); + $this->removeIneligibleDraftPayrolls($period->id,$companyId,$employmentEmployees); + $employees=$employmentEmployees; if(!$canManageAll)$employees=array_values(array_filter($employees,function($employee){return empty($employee->salary_restricted);})); if(!$employees)throw new BusinessException('Tidak ada karyawan yang dapat dihitung dengan permission Anda.'); foreach($employees as$employee)$this->calculateEmployee($period,$employee,$start,$end,$companyId,$uid); @@ -37,6 +49,20 @@ class PayrollService }catch(Throwable$e){$db->trans_rollback();throw$e;} } + private function removeIneligibleDraftPayrolls($periodId,$companyId,$eligibleEmployees) + { + $eligibleIds=array_map(function($employee){return(int)$employee->id;},$eligibleEmployees); + $query=$this->CI->db + ->select('id') + ->where(array('company_id'=>(int)$companyId,'payroll_period_id'=>(int)$periodId)); + if($eligibleIds)$query->where_not_in('employee_id',$eligibleIds); + $stale=$query->get('k_payrolls')->result(); + foreach($stale as$row){ + $this->CI->db->where('payroll_id',(int)$row->id)->delete('payroll_calculation_lines'); + $this->CI->db->where('id',(int)$row->id)->delete('k_payrolls'); + } + } + private function calculateEmployee($period,$employee,$start,$end,$companyId,$uid) { $db=$this->CI->db; diff --git a/application/migrations/20260914000100_normalize_employee_resign_dates.php b/application/migrations/20260914000100_normalize_employee_resign_dates.php new file mode 100644 index 0000000..d34379b --- /dev/null +++ b/application/migrations/20260914000100_normalize_employee_resign_dates.php @@ -0,0 +1,22 @@ +db->table_exists('k_employees') || !$this->db->field_exists('resign_date', 'k_employees')) { + return; + } + + // Database lama memakai zero-date sebagai nilai kosong. NULL adalah + // representasi yang valid untuk karyawan yang belum/tidak resign. + $this->db->query("UPDATE k_employees SET resign_date = NULL WHERE CAST(resign_date AS CHAR) = '0000-00-00'"); + } + + public function down() + { + // Tidak dikembalikan ke zero-date karena nilai tersebut tidak valid + // pada MySQL strict mode. + } +} diff --git a/application/views/employees/attendance_monitoring.php b/application/views/employees/attendance_monitoring.php index bfe2217..e26ca3d 100644 --- a/application/views/employees/attendance_monitoring.php +++ b/application/views/employees/attendance_monitoring.php @@ -20,7 +20,7 @@ - + diff --git a/application/views/hr_payroll/organization_master.php b/application/views/hr_payroll/organization_master.php index 2d26b95..36726d1 100644 --- a/application/views/hr_payroll/organization_master.php +++ b/application/views/hr_payroll/organization_master.php @@ -2,11 +2,11 @@ load->view('hr_payroll/_header',array('active_menu'=>'organization_master','title'=>'Departemen & Posisi','subtitle'=>'Satu sumber data organisasi untuk karyawan, pembelian, payroll, jadwal, dan modul terkait.')); ?>
Departemen
-
+
KodeDepartemenStatus
Posisi / Jabatan
-
+
KodePosisiDepartemenStatus
@@ -28,10 +28,10 @@ $(function(){ positionDepartment.innerHTML=''+state.departments.filter(x=>x.is_active==1).map(x=>``).join(''); }catch(error){departmentRows.innerHTML=positionRows.innerHTML=`${esc(error.message)}`;} } - async function save(form,path){const button=form.querySelector('button[type="submit"]');button.disabled=true;try{const response=await fetch(base+path,{method:'POST',body:new FormData(form),headers:{Accept:'application/json','X-Requested-With':'XMLHttpRequest'}}),json=await response.json();if(!response.ok||!json.status)throw new Error(json.message||'Gagal menyimpan');form.reset();form.elements.id.value='';form.elements.is_active.checked=true;await load();}catch(error){alert(error.message)}finally{button.disabled=false}} + async function save(form,path){const button=form.querySelector('button[type="submit"]');button.disabled=true;try{const response=await fetch(base+path,{method:'POST',body:new FormData(form),headers:{Accept:'application/json','X-Requested-With':'XMLHttpRequest'}}),json=await response.json();if(!response.ok||!json.status)throw new Error(json.message||'Gagal menyimpan');form.reset();form.elements.id.value='';form.elements.is_active.checked=true;await load();if(window.AppAlert)AppAlert.toast(json.message||'Data berhasil disimpan.','success');}catch(error){if(window.AppAlert)AppAlert.toast(error.message,'error');else alert(error.message)}finally{button.disabled=false}} departmentForm.addEventListener('submit',event=>{event.preventDefault();save(departmentForm,'save_department')}); positionForm.addEventListener('submit',event=>{event.preventDefault();save(positionForm,'save_position')}); - $(document).on('click','.btn-reset',function(){const form=this.closest('form');form.reset();form.elements.id.value='';form.elements.is_active.checked=true}).on('click','.edit-dep',function(){const x=state.departments.find(v=>v.id==this.dataset.id),f=departmentForm;if(!x)return;f.elements.id.value=x.id;f.elements.code.value=x.code;f.elements.name.value=x.department_name;f.elements.description.value=x.description||'';f.elements.is_active.checked=x.is_active==1}).on('click','.edit-pos',function(){const x=state.positions.find(v=>v.id==this.dataset.id),f=positionForm;if(!x)return;f.elements.id.value=x.id;f.elements.code.value=x.code;f.elements.name.value=x.position_name;f.elements.department_id.value=x.department_id||'';f.elements.description.value=x.description||'';f.elements.is_active.checked=x.is_active==1}).on('click','.del-dep,.del-pos',async function(){if(!confirm('Hapus data ini? Data yang sudah digunakan akan dinonaktifkan.'))return;const path=this.classList.contains('del-dep')?'delete_department/':'delete_position/',response=await fetch(base+path+this.dataset.id,{method:'POST',body:new FormData(departmentForm),headers:{Accept:'application/json','X-Requested-With':'XMLHttpRequest'}}),json=await response.json();alert(json.message);load()}); + $(document).on('click','.btn-reset',function(){const form=this.closest('form');form.reset();form.elements.id.value='';form.elements.is_active.checked=true}).on('click','.edit-dep',function(){const x=state.departments.find(v=>v.id==this.dataset.id),f=departmentForm;if(!x)return;f.elements.id.value=x.id;f.elements.code.value=x.code;f.elements.name.value=x.department_name;f.elements.description.value=x.description||'';f.elements.is_active.checked=x.is_active==1}).on('click','.edit-pos',function(){const x=state.positions.find(v=>v.id==this.dataset.id),f=positionForm;if(!x)return;f.elements.id.value=x.id;f.elements.code.value=x.code;f.elements.name.value=x.position_name;f.elements.department_id.value=x.department_id||'';f.elements.description.value=x.description||'';f.elements.is_active.checked=x.is_active==1}).on('click','.del-dep,.del-pos',async function(){const confirmed=window.AppAlert?await AppAlert.confirm({title:'Hapus data ini?',text:'Data yang sudah digunakan akan dinonaktifkan.'}):{isConfirmed:confirm('Hapus data ini? Data yang sudah digunakan akan dinonaktifkan.')};if(!confirmed.isConfirmed)return;try{const path=this.classList.contains('del-dep')?'delete_department/':'delete_position/',response=await fetch(base+path+this.dataset.id,{method:'POST',body:new FormData(departmentForm),headers:{Accept:'application/json','X-Requested-With':'XMLHttpRequest'}}),json=await response.json();if(!response.ok||!json.status)throw new Error(json.message||'Gagal menghapus data.');await load();if(window.AppAlert)AppAlert.toast(json.message,'success');}catch(error){if(window.AppAlert)AppAlert.toast(error.message,'error');else alert(error.message)}}); load(); }); diff --git a/assets/js/app-ui.min.js b/assets/js/app-ui.min.js index d44ab25..86af271 100644 --- a/assets/js/app-ui.min.js +++ b/assets/js/app-ui.min.js @@ -1,4 +1,9 @@ (function(){'use strict';var d=document,q=function(s,c){return Array.prototype.slice.call((c||d).querySelectorAll(s));}; +function loadingElement(){return d.getElementById('globalLoading');} +function resetFormLoading(f){if(!f||f.dataset.globalLoading!=='1')return;delete f.dataset.submitting;delete f.dataset.globalLoading;q('[aria-busy="true"]',f).forEach(function(b){b.disabled=false;b.removeAttribute('aria-busy');});} +function hideGlobalLoading(f){var l=loadingElement();if(l)l.hidden=true;if(f)resetFormLoading(f);else q('form[data-global-loading="1"]').forEach(resetFormLoading);} +function showGlobalLoading(f){var l=loadingElement();if(l)l.hidden=false;if(f)f.dataset.globalLoading='1';} +window.AppLoading={show:showGlobalLoading,hide:hideGlobalLoading}; window.cleanNumber=function(v){if(v===null||v===undefined)return'';var s=String(v).trim().replace(/\s|Rp/gi,'');if(!s)return'';var neg=s.charAt(0)==='-';s=s.replace(/-/g,'');var comma=s.lastIndexOf(','),dot=s.lastIndexOf('.');if(comma>=0&&dot>=0){if(comma>dot)s=s.replace(/\./g,'').replace(',','.');else s=s.replace(/,/g,'');}else if(comma>=0){var ca=s.split(',');s=(ca.length===2&&ca[1].length>0&&ca[1].length<=2)?ca[0].replace(/,/g,'')+'.'+ca[1]:s.replace(/,/g,'');}else if(dot>=0){var da=s.split('.');s=(da.length===2&&da[1].length>0&&da[1].length<=2)?s:s.replace(/\./g,'');}s=s.replace(/[^0-9.]/g,'');return(neg?'-':'')+s;}; window.parseMoney=function(v){var n=Number(window.cleanNumber(v));return Number.isFinite(n)?n:0;}; window.formatNumber=function(v){if(v===null||v===undefined)return'';var raw=String(v).trim().replace(/\s|Rp/gi,''),neg=raw.charAt(0)==='-';raw=raw.replace(/-/g,'').replace(/[^0-9.,]/g,'');if(raw==='')return'';var comma=raw.lastIndexOf(','),dot=raw.lastIndexOf('.'),integer=raw,decimal='',hasDecimal=false;if(comma>=0&&dot>=0){var sep=comma>dot?',':'.',at=Math.max(comma,dot);integer=raw.substring(0,at).replace(/[.,]/g,'');decimal=raw.substring(at+1).replace(/\D/g,'').substring(0,2);hasDecimal=true;}else if(comma>=0){var ctail=raw.length-comma-1;if(ctail<=2){integer=raw.substring(0,comma).replace(/\D/g,'');decimal=raw.substring(comma+1).replace(/\D/g,'').substring(0,2);hasDecimal=true;}else integer=raw.replace(/,/g,'');}else if(dot>=0){var dtail=raw.length-dot-1;if(raw.split('.').length===2&&dtail>0&&dtail<=2){integer=raw.substring(0,dot).replace(/\D/g,'');decimal=raw.substring(dot+1).replace(/\D/g,'').substring(0,2);hasDecimal=true;}else integer=raw.replace(/\./g,'');}integer=(integer||'0').replace(/^0+(?=\d)/,'').replace(/\B(?=(\d{3})+(?!\d))/g,'.');return(neg?'-':'')+integer+(hasDecimal?','+decimal:'');}; @@ -10,8 +15,9 @@ function moneyFields(scope){q('input[name],input.format-rupiah',scope||d).forEac moneyFields(d);q('input[type=number],.format-rupiah').forEach(function(x){x.setAttribute('inputmode',x.step&&String(x.step).indexOf('.')>=0?'decimal':'numeric');}); q('img:not([loading])').forEach(function(x){x.loading='lazy';x.decoding='async';}); q('.table-responsive table').forEach(function(t){t.classList.add('table-hover','responsive-detail-table');var heads=q('thead th',t).map(function(h){return h.textContent.trim();});q('tbody tr',t).forEach(function(r){q('td',r).forEach(function(c,i){if(!c.dataset.label)c.dataset.label=heads[i]||'';});});}); -q('form').forEach(function(f){f.addEventListener('submit',function(e){if(e.defaultPrevented||f.dataset.ajax==='true')return;if(f.dataset.submitting==='1'){e.preventDefault();return;}if(!navigator.onLine&&String(f.method).toLowerCase()==='post'){e.preventDefault();alert('Posting transaksi offline tidak diizinkan. Sambungkan internet lalu coba kembali.');return;}f.dataset.submitting='1';q('button[type=submit],input[type=submit]',f).forEach(function(b){b.disabled=true;b.setAttribute('aria-busy','true');});var l=d.getElementById('globalLoading');if(l)l.hidden=false;});}); +q('form').forEach(function(f){f.addEventListener('submit',function(e){if(e.defaultPrevented||f.dataset.ajax==='true'||String(f.target).toLowerCase()==='_blank')return;if(f.dataset.submitting==='1'){e.preventDefault();return;}if(!navigator.onLine&&String(f.method).toLowerCase()==='post'){e.preventDefault();alert('Posting transaksi offline tidak diizinkan. Sambungkan internet lalu coba kembali.');return;}f.dataset.submitting='1';q('button[type=submit],input[type=submit]',f).forEach(function(b){b.disabled=true;b.setAttribute('aria-busy','true');});showGlobalLoading(f);window.setTimeout(function(){if(e.defaultPrevented)hideGlobalLoading(f);},0);});}); q('input[type=file][accept*="image"]:not([multiple])').forEach(function(input){input.addEventListener('change',function(){var file=input.files&&input.files[0];if(!file||file.size<350000)return;var img=new Image(),url=URL.createObjectURL(file);img.onload=function(){var scale=Math.min(1,1600/Math.max(img.width,img.height)),canvas=d.createElement('canvas');canvas.width=Math.round(img.width*scale);canvas.height=Math.round(img.height*scale);canvas.getContext('2d').drawImage(img,0,0,canvas.width,canvas.height);canvas.toBlob(function(blob){if(blob&&blob.size