perbaikan bug global loading HR dan Payroll

This commit is contained in:
Wian Drs
2026-09-14 12:05:53 +07:00
parent 63993ee76e
commit 5e1fd11410
9 changed files with 153 additions and 66 deletions
+33 -41
View File
@@ -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)
@@ -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.');}
}
+27 -1
View File
@@ -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;
@@ -0,0 +1,22 @@
<?php
defined('BASEPATH') OR exit('No direct script access allowed');
class Migration_Normalize_employee_resign_dates extends CI_Migration
{
public function up()
{
if (!$this->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.
}
}
@@ -20,7 +20,7 @@
</div>
</section>
<div class="modal fade" id="manualModal" tabindex="-1" data-bs-backdrop="static"><div class="modal-dialog modal-dialog-centered"><div class="modal-content"><div class="modal-header"><div><small class="text-muted">DRAFT ABSENSI</small><h5 class="modal-title">Tambah Absen Manual</h5></div><button class="btn-close" data-bs-dismiss="modal"></button></div><form id="manualForm"><div class="modal-body"><div class="mb-3"><label class="form-label">Karyawan <span class="text-danger">*</span></label><select class="form-select select-search" name="employee_id" required><option value="">Pilih karyawan</option><?php foreach($employees as$e):?><option value="<?=$e->id?>"><?=html_escape($e->employee_code.' - '.$e->full_name)?></option><?php endforeach?></select></div><div class="row g-3"><div class="col-6"><label class="form-label">Tanggal <span class="text-danger">*</span></label><input class="form-control" type="date" name="attendance_date" value="<?=date('Y-m-d')?>" required></div><div class="col-6"><label class="form-label">Jam <span class="text-danger">*</span></label><input class="form-control" type="time" name="attendance_time" value="<?=date('H:i')?>" required></div><div class="col-12"><label class="form-label">Jenis Absen <span class="text-danger">*</span></label><select class="form-select" name="attendance_action" required><option value="check_in">Absen Masuk</option><option value="check_out">Absen Pulang</option></select></div><div class="col-12"><label class="form-label">Alasan / Keterangan <span class="text-danger">*</span></label><textarea class="form-control" name="reason" rows="3" maxlength="1000" placeholder="Jelaskan alasan pencatatan manual agar approver dapat memeriksa." required></textarea></div></div><div class="alert alert-warning mt-3 mb-0 small"><i class="bi bi-shield-check"></i> Data belum mengubah absensi sampai pengajuan disetujui.</div></div><div class="modal-footer"><button type="button" class="btn btn-light" data-bs-dismiss="modal">Batal</button><button class="btn btn-warning"><i class="bi bi-save"></i> Simpan Draft</button></div></form></div></div></div>
<div class="modal fade" id="manualModal" tabindex="-1" data-bs-backdrop="static"><div class="modal-dialog modal-dialog-centered"><div class="modal-content"><div class="modal-header"><div><small class="text-muted">DRAFT ABSENSI</small><h5 class="modal-title">Tambah Absen Manual</h5></div><button class="btn-close" data-bs-dismiss="modal"></button></div><form id="manualForm" data-ajax="true"><div class="modal-body"><div class="mb-3"><label class="form-label">Karyawan <span class="text-danger">*</span></label><select class="form-select select-search" name="employee_id" required><option value="">Pilih karyawan</option><?php foreach($employees as$e):?><option value="<?=$e->id?>"><?=html_escape($e->employee_code.' - '.$e->full_name)?></option><?php endforeach?></select></div><div class="row g-3"><div class="col-6"><label class="form-label">Tanggal <span class="text-danger">*</span></label><input class="form-control" type="date" name="attendance_date" value="<?=date('Y-m-d')?>" required></div><div class="col-6"><label class="form-label">Jam <span class="text-danger">*</span></label><input class="form-control" type="time" name="attendance_time" value="<?=date('H:i')?>" required></div><div class="col-12"><label class="form-label">Jenis Absen <span class="text-danger">*</span></label><select class="form-select" name="attendance_action" required><option value="check_in">Absen Masuk</option><option value="check_out">Absen Pulang</option></select></div><div class="col-12"><label class="form-label">Alasan / Keterangan <span class="text-danger">*</span></label><textarea class="form-control" name="reason" rows="3" maxlength="1000" placeholder="Jelaskan alasan pencatatan manual agar approver dapat memeriksa." required></textarea></div></div><div class="alert alert-warning mt-3 mb-0 small"><i class="bi bi-shield-check"></i> Data belum mengubah absensi sampai pengajuan disetujui.</div></div><div class="modal-footer"><button type="button" class="btn btn-light" data-bs-dismiss="modal">Batal</button><button type="submit" class="btn btn-warning"><i class="bi bi-save"></i> Simpan Draft</button></div></form></div></div></div>
<script>
$(function(){
@@ -31,7 +31,7 @@ $(function(){
$('#filterDate').on('change',()=>daily.ajax.reload());$('#manualStatus').on('change',()=>manual.ajax.reload());$('#addManual').on('click',()=>{document.getElementById('manualForm').reset();modal.show();});
function refresh(){manual.ajax.reload(null,false);daily.ajax.reload(null,false)}
function request(url,data){return $.ajax({url:base+url,type:'POST',data:data||{}}).done(r=>{if(r.status){AppAlert.toast(r.message,'success');refresh()}else AppAlert.toast(r.message,'error')}).fail(x=>AppAlert.toast(x.responseJSON?.message||'Proses gagal.','error'));}
$('#manualForm').on('submit',function(e){e.preventDefault();request('attendancemonitoring/save_manual',$(this).serialize()).done(r=>{if(r.status)modal.hide()});});
$('#manualForm').on('submit',function(e){e.preventDefault();const form=this,button=form.querySelector('button[type="submit"]'),original=button.innerHTML;button.disabled=true;button.innerHTML='<span class="spinner-border spinner-border-sm"></span> Menyimpan';request('attendancemonitoring/save_manual',$(form).serialize()).done(r=>{if(r.status){modal.hide();form.reset();}}).always(()=>{button.disabled=false;button.innerHTML=original;if(window.AppLoading)AppLoading.hide();});});
$(document).on('click','.manual-submit',function(){const id=this.dataset.id;AppAlert.confirm({title:'Ajukan absensi manual?',text:'Dokumen akan dikirim kepada approver HR.'}).then(x=>{if(x.isConfirmed)request('attendancemonitoring/submit_manual/'+id)});});
$(document).on('click','.manual-approve',function(){const id=this.dataset.id;AppAlert.confirm({title:'Setujui absensi manual?',text:'Setelah disetujui, waktu akan langsung diterapkan ke daftar absensi.',confirmButtonText:'Approve'}).then(x=>{if(x.isConfirmed)request('attendancemonitoring/approve_manual/'+id)});});
$(document).on('click','.manual-reject',function(){const id=this.dataset.id;AppAlert.prompt({title:'Tolak pengajuan',input:'textarea',inputLabel:'Alasan penolakan',inputValidator:v=>!v?.trim()?'Alasan wajib diisi.':undefined,confirmButtonText:'Reject'}).then(x=>{if(x.isConfirmed)request('attendancemonitoring/reject_manual/'+id,{reason:x.value})});});
+55 -14
View File
@@ -401,6 +401,16 @@
</div>
<div class="col-md-4 mb-3">
<label>Tanggal Resign <span class="text-muted">(opsional)</span></label>
<input type="date"
id="resign_date"
class="form-control">
</div>
<?php if($can_manage_all_salary):?>
<div class="col-12 mb-3">
<div class="form-check salary-restricted-note">
@@ -566,6 +576,23 @@ const base_url = "<?= base_url(); ?>";
$(function(){
let action = 'add';
const employeeModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('modalEmployee'));
const employeeDetailModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('modalDetail'));
function addCsrf(formData){
if(window.APP_CSRF && window.APP_CSRF.name && !formData.has(window.APP_CSRF.name)){
formData.append(window.APP_CSRF.name, window.APP_CSRF.value);
}
return formData;
}
function ajaxError(xhr){
let message = 'Permintaan tidak dapat diproses.';
if(xhr && xhr.responseJSON && xhr.responseJSON.message) message = xhr.responseJSON.message;
else if(xhr && xhr.status === 403) message = 'Anda tidak memiliki izin untuk tindakan ini.';
else if(xhr && xhr.status === 401) message = 'Sesi telah berakhir. Silakan login kembali.';
Swal.fire('Error', message, 'error');
}
// =========================================
// DATATABLE
@@ -723,7 +750,7 @@ $(function(){
action = 'add';
$('#modalEmployee').modal('show');
employeeModal.show();
});
@@ -801,11 +828,11 @@ $(function(){
action = 'edit';
$('#modalEmployee').modal('show');
employeeModal.show();
},
'json'
);
).fail(ajaxError);
});
@@ -830,7 +857,7 @@ $(function(){
formData.append('address', $('#address').val());
formData.append('join_date', $('#join_date').val());
formData.append('resign_date', $('#resign_date').val());
formData.append('resign_date', $('#resign_date').val() || '');
formData.append('employment_status', $('#employment_status').val());
formData.append('marital_status', $('#marital_status').val());
@@ -860,6 +887,8 @@ $(function(){
formData.append('photo_file', photo);
}
addCsrf(formData);
if(
!$('#full_name').val() ||
!$('#join_date').val()
@@ -878,6 +907,9 @@ $(function(){
? base_url + 'employees/store'
: base_url + 'employees/update/' + $('#id').val();
const saveButton = $('#btnSimpan');
saveButton.prop('disabled', true);
$.ajax({
url:url,
@@ -891,7 +923,7 @@ $(function(){
if(res.status){
$('#modalEmployee').modal('hide');
employeeModal.hide();
table.ajax.reload(null,false);
@@ -910,7 +942,9 @@ $(function(){
);
}
}
},
error:ajaxError,
complete:function(){ saveButton.prop('disabled', false); }
});
@@ -1279,11 +1313,11 @@ $(function(){
`);
$('#modalDetail').modal('show');
employeeDetailModal.show();
},
'json'
);
).fail(ajaxError);
});
@@ -1305,9 +1339,16 @@ $(function(){
if(r.isConfirmed){
$.get(
base_url + 'employees/delete/' + id,
function(res){
const deleteData = new FormData();
addCsrf(deleteData);
$.ajax({
url:base_url + 'employees/delete/' + id,
type:'POST',
data:deleteData,
processData:false,
contentType:false,
dataType:'json',
success:function(res){
if(res.status){
@@ -1329,8 +1370,8 @@ $(function(){
}
},
'json'
);
error:ajaxError
});
}
+1 -1
View File
@@ -52,6 +52,6 @@ $(function(){
if($('#salaryTable').length){const table=$('#salaryTable').DataTable({pageLength:25,order:[[0,'asc']],language:{search:'Cari:',lengthMenu:'Tampilkan _MENU_ data',info:'_START_–_END_ dari _TOTAL_',paginate:{previous:'Sebelumnya',next:'Berikutnya'}}});$('#salaryDepartmentFilter').on('change',function(){table.column(1).search(this.value).draw()});$('#salaryPositionFilter').on('change',function(){table.column(2).search(this.value).draw()});$('#salaryStatusFilter').on('change',function(){table.column(3).search(this.value).draw()})}
const detailModal=bootstrap.Modal.getOrCreateInstance('#payrollDetailModal');$('.payroll-detail').on('click',function(){$.getJSON('<?=base_url('hrpayroll/period_detail/')?>'+this.dataset.id).done(r=>{if(!r.status)return AppAlert.toast(r.message,'error');$('#payrollDetailTitle').text(r.period.period_name);$('#payrollDetailRows').html((r.rows||[]).map(x=>`<tr><td><b>${esc(x.full_name)}</b><small class="d-block text-muted">${esc(x.employee_code)}${x.salary_restricted==1?' · Gaji terbatas':''}</small></td><td class="text-end">${x.total_present}</td><td class="text-end">${x.total_alpha}</td><td class="text-end">${x.total_overtime_hours}</td><td class="text-end">${money(x.gross_salary)}</td><td class="text-end">${money(x.total_deductions)}</td><td class="text-end fw-bold">${money(x.total_salary)}</td><td>${['final','posted','paid'].includes(r.period.workflow_status)?`<a class="btn btn-sm btn-outline-primary" target="_blank" href="<?=base_url('hrpayroll/slip/')?>${x.id}">Slip</a>`:'-'}</td></tr>`).join('')||'<tr><td colspan="8" class="text-center text-muted">Belum ada rincian.</td></tr>');detailModal.show()}).fail(x=>AppAlert.toast(x.responseJSON?.message||'Rincian gagal dimuat.','error'))});
const salaryModal=bootstrap.Modal.getOrCreateInstance('#salaryModal');let currentEmployee=0;function loadSalary(id){currentEmployee=id;$('#salaryLineRows').html('<tr><td colspan="7" class="text-center py-4"><span class="spinner-border spinner-border-sm"></span> Memuat…</td></tr>');salaryModal.show();$.getJSON('<?=base_url('hrpayroll/salary_detail/')?>'+id).done(r=>{if(!r.status){salaryModal.hide();return AppAlert.toast(r.message,'error')}$('#salaryEmployeeId').val(id);$('#salaryModalTitle').text(r.data?.employee?.full_name||r.employee?.full_name||'Karyawan');const employee=r.data?.employee||r.employee,lines=r.data?.lines||r.lines||[];$('#salaryModalMeta').text((employee?.employee_code||'')+' · '+(employee?.department_name||'-')+' · '+(employee?.position_name||'-'));$('#salaryLineRows').html(lines.map(x=>`<tr><td><b>${esc(x.component_code)}</b><small class="d-block text-muted">${esc(x.component_name)}</small></td><td>${esc(x.component_type)}</td><td>${esc(x.calculation_method)}</td><td class="text-end">${money(x.amount_override)}</td><td>${esc(x.effective_from)}${x.effective_to?' s.d. '+esc(x.effective_to):''}</td><td><span class="badge ${x.is_active==1?'bg-success':'bg-secondary'}">${x.is_active==1?'Aktif':'Selesai'}</span></td><td>${x.is_active==1?`<button class="btn btn-sm btn-outline-danger deactivate-salary" data-id="${x.id}">Nonaktifkan</button>`:'-'}</td></tr>`).join('')||'<tr><td colspan="7" class="text-center text-muted py-4">Belum ada komponen gaji.</td></tr>')}).fail(x=>{salaryModal.hide();AppAlert.toast(x.responseJSON?.message||'Struktur gaji gagal dimuat.','error')})}
$('.salary-manage').on('click',function(){loadSalary(this.dataset.id)});$('#salaryLineForm').on('submit',function(e){e.preventDefault();const button=this.querySelector('button[type=submit]');button.disabled=true;$.ajax({url:'<?=base_url('hrpayroll/save_salary_line')?>',method:'POST',data:new FormData(this),processData:false,contentType:false,dataType:'json'}).done(r=>{if(!r.status)return AppAlert.toast(r.message,'error');AppAlert.toast(r.message,'success');this.reset();this.elements.employee_id.value=currentEmployee;loadSalary(currentEmployee)}).fail(x=>AppAlert.toast(x.responseJSON?.message||'Komponen gagal disimpan.','error')).always(()=>button.disabled=false)});$(document).on('click','.deactivate-salary',function(){if(!confirm('Nonaktifkan komponen tanpa menghapus historinya?'))return;const form=new FormData();form.append('<?=$this->security->get_csrf_token_name()?>','<?=$this->security->get_csrf_hash()?>');$.ajax({url:'<?=base_url('hrpayroll/deactivate_salary_line/')?>'+this.dataset.id,method:'POST',data:form,processData:false,contentType:false,dataType:'json'}).done(r=>{AppAlert.toast(r.message,r.status?'success':'error');if(r.status)loadSalary(currentEmployee)}).fail(x=>AppAlert.toast(x.responseJSON?.message||'Komponen gagal dinonaktifkan.','error'))});
$('.salary-manage').on('click',function(){loadSalary(this.dataset.id)});$('#salaryLineForm').on('submit',function(e){e.preventDefault();const button=this.querySelector('button[type=submit]');button.disabled=true;$.ajax({url:'<?=base_url('hrpayroll/save_salary_line')?>',method:'POST',data:new FormData(this),processData:false,contentType:false,dataType:'json'}).done(r=>{if(!r.status)return AppAlert.toast(r.message,'error');AppAlert.toast(r.message,'success');this.reset();this.elements.employee_id.value=currentEmployee;loadSalary(currentEmployee)}).fail(x=>AppAlert.toast(x.responseJSON?.message||'Komponen gagal disimpan.','error')).always(()=>{button.disabled=false;if(window.AppLoading)AppLoading.hide()})});$(document).on('click','.deactivate-salary',function(){if(!confirm('Nonaktifkan komponen tanpa menghapus historinya?'))return;const form=new FormData();form.append('<?=$this->security->get_csrf_token_name()?>','<?=$this->security->get_csrf_hash()?>');$.ajax({url:'<?=base_url('hrpayroll/deactivate_salary_line/')?>'+this.dataset.id,method:'POST',data:form,processData:false,contentType:false,dataType:'json'}).done(r=>{AppAlert.toast(r.message,r.status?'success':'error');if(r.status)loadSalary(currentEmployee)}).fail(x=>AppAlert.toast(x.responseJSON?.message||'Komponen gagal dinonaktifkan.','error')).always(()=>{if(window.AppLoading)AppLoading.hide()})});
});
</script>
@@ -2,11 +2,11 @@
<?php $this->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.')); ?>
<div class="row g-3">
<div class="col-lg-6"><div class="card h-100"><div class="card-header"><b>Departemen</b></div><div class="card-body">
<form id="departmentForm" class="row g-2 mb-3"><input type="hidden" name="<?=$this->security->get_csrf_token_name()?>" value="<?=$this->security->get_csrf_hash()?>"><input type="hidden" name="id"><div class="col-md-3"><input class="form-control" name="code" placeholder="Kode" required></div><div class="col-md-4"><input class="form-control" name="name" placeholder="Nama departemen" required></div><div class="col-md-5"><input class="form-control" name="description" placeholder="Keterangan"></div><div class="col-8"><label><input type="checkbox" name="is_active" value="1" checked> Aktif</label></div><div class="col-4 text-end"><button type="button" class="btn btn-light btn-reset">Reset</button> <button class="btn btn-primary">Simpan</button></div></form>
<form id="departmentForm" class="row g-2 mb-3" data-ajax="true"><input type="hidden" name="<?=$this->security->get_csrf_token_name()?>" value="<?=$this->security->get_csrf_hash()?>"><input type="hidden" name="id"><div class="col-md-3"><input class="form-control" name="code" placeholder="Kode" required></div><div class="col-md-4"><input class="form-control" name="name" placeholder="Nama departemen" required></div><div class="col-md-5"><input class="form-control" name="description" placeholder="Keterangan"></div><div class="col-8"><label><input type="checkbox" name="is_active" value="1" checked> Aktif</label></div><div class="col-4 text-end"><button type="button" class="btn btn-light btn-reset">Reset</button> <button type="submit" class="btn btn-primary">Simpan</button></div></form>
<div class="table-responsive"><table class="table table-sm align-middle"><thead><tr><th>Kode</th><th>Departemen</th><th>Status</th><th></th></tr></thead><tbody id="departmentRows"></tbody></table></div>
</div></div></div>
<div class="col-lg-6"><div class="card h-100"><div class="card-header"><b>Posisi / Jabatan</b></div><div class="card-body">
<form id="positionForm" class="row g-2 mb-3"><input type="hidden" name="<?=$this->security->get_csrf_token_name()?>" value="<?=$this->security->get_csrf_hash()?>"><input type="hidden" name="id"><div class="col-md-3"><input class="form-control" name="code" placeholder="Kode" required></div><div class="col-md-4"><input class="form-control" name="name" placeholder="Nama posisi" required></div><div class="col-md-5"><select class="form-select" name="department_id" id="positionDepartment"><option value="">Semua departemen</option></select></div><div class="col-12"><input class="form-control" name="description" placeholder="Keterangan/tanggung jawab"></div><div class="col-8"><label><input type="checkbox" name="is_active" value="1" checked> Aktif</label></div><div class="col-4 text-end"><button type="button" class="btn btn-light btn-reset">Reset</button> <button class="btn btn-primary">Simpan</button></div></form>
<form id="positionForm" class="row g-2 mb-3" data-ajax="true"><input type="hidden" name="<?=$this->security->get_csrf_token_name()?>" value="<?=$this->security->get_csrf_hash()?>"><input type="hidden" name="id"><div class="col-md-3"><input class="form-control" name="code" placeholder="Kode" required></div><div class="col-md-4"><input class="form-control" name="name" placeholder="Nama posisi" required></div><div class="col-md-5"><select class="form-select" name="department_id" id="positionDepartment"><option value="">Semua departemen</option></select></div><div class="col-12"><input class="form-control" name="description" placeholder="Keterangan/tanggung jawab"></div><div class="col-8"><label><input type="checkbox" name="is_active" value="1" checked> Aktif</label></div><div class="col-4 text-end"><button type="button" class="btn btn-light btn-reset">Reset</button> <button type="submit" class="btn btn-primary">Simpan</button></div></form>
<div class="table-responsive"><table class="table table-sm align-middle"><thead><tr><th>Kode</th><th>Posisi</th><th>Departemen</th><th>Status</th><th></th></tr></thead><tbody id="positionRows"></tbody></table></div>
</div></div></div>
</div>
@@ -28,10 +28,10 @@ $(function(){
positionDepartment.innerHTML='<option value="">Semua departemen</option>'+state.departments.filter(x=>x.is_active==1).map(x=>`<option value="${x.id}">${esc(x.code+' - '+x.department_name)}</option>`).join('');
}catch(error){departmentRows.innerHTML=positionRows.innerHTML=`<tr><td colspan="5" class="text-danger">${esc(error.message)}</td></tr>`;}
}
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();
});
</script>
+8 -2
View File
@@ -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<file.size&&window.DataTransfer){var dt=new DataTransfer();dt.items.add(new File([blob],file.name.replace(/\.[^.]+$/,'.jpg'),{type:'image/jpeg'}));input.files=dt.files;}URL.revokeObjectURL(url);},'image/jpeg',.82);};img.src=url;});});
if(window.jQuery){jQuery(d).on('input','.format-rupiah',function(){this.value=window.formatNumber(this.value);}).on('submit','form',function(){jQuery(this).find('.format-rupiah').each(function(){this.value=window.cleanNumber(this.value);});});var initSelect=function(scope){if(!jQuery.fn.select2)return;jQuery(scope||d).find('select.select-search').each(function(){var x=jQuery(this);if(x.hasClass('select2-hidden-accessible'))return;var p=x.closest('.modal'),lookup=x.data('lookup'),options={width:'100%',placeholder:x.data('placeholder')||x.attr('placeholder')||'-- Pilih --',allowClear:true,dropdownParent:p.length?p:jQuery(document.body)};if(lookup)options.ajax={url:(window.APP_BASE_URL||'/')+'lookups/search/'+lookup,dataType:'json',delay:300,cache:true,data:function(params){return{q:params.term||'',page:params.page||1,limit:20}},processResults:function(data){return data}};x.select2(options);});};initSelect(d);if(window.MutationObserver)new MutationObserver(function(ms){ms.forEach(function(m){Array.prototype.forEach.call(m.addedNodes,function(n){if(n.nodeType===1){moneyFields(n);initSelect(n);}});});}).observe(d.body,{childList:true,subtree:true});}
window.addEventListener('pageshow',function(){var l=d.getElementById('globalLoading');if(l)l.hidden=true;q('form').forEach(function(f){delete f.dataset.submitting;q('[aria-busy]',f).forEach(function(b){b.disabled=false;b.removeAttribute('aria-busy');});});});
window.addEventListener('pageshow',function(){hideGlobalLoading();q('form').forEach(function(f){delete f.dataset.submitting;delete f.dataset.globalLoading;q('[aria-busy]',f).forEach(function(b){b.disabled=false;b.removeAttribute('aria-busy');});});});
window.addEventListener('error',function(){hideGlobalLoading();});window.addEventListener('unhandledrejection',function(){hideGlobalLoading();});
if('serviceWorker'in navigator)window.addEventListener('load',function(){navigator.serviceWorker.register((window.APP_BASE_URL||'/')+'service-worker.js');});}());