update HR dan Payroll

This commit is contained in:
Wian Drs
2026-09-14 11:14:25 +07:00
parent ec94783db4
commit 63993ee76e
50 changed files with 2432 additions and 1473 deletions
@@ -1,75 +1,40 @@
<div class="container mt-4">
<?php $this->load->view('hr_payroll/_header',array('active_menu'=>'attendance_monitoring','title'=>'Monitoring Absensi','subtitle'=>'Pantau karyawan yang dijadwalkan hadir dan kelola absensi manual melalui persetujuan.','action_html'=>$permissions['create']?'<button class="btn btn-warning" id="addManual"><i class="bi bi-plus-lg"></i> Absen Manual</button>':'')); ?>
<div class="card border-0 shadow-sm rounded-4">
<section class="card hr-panel border-0">
<div class="card-body p-0">
<ul class="nav nav-pills p-3 gap-2 border-bottom" role="tablist">
<li class="nav-item"><button class="nav-link active" data-bs-toggle="tab" data-bs-target="#dailyPane">Kehadiran Terjadwal</button></li>
<li class="nav-item"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#manualPane">Pengajuan Absen Manual</button></li>
</ul>
<div class="tab-content">
<div class="tab-pane fade show active p-3" id="dailyPane">
<div class="d-flex flex-wrap justify-content-between align-items-end gap-2 mb-3"><div><h5 class="mb-1">Daftar Kehadiran Harian</h5><small class="text-muted">Menampilkan semua karyawan berdasarkan jadwal yang telah dipublikasikan.</small></div><div><label class="form-label">Tanggal</label><input type="date" id="filterDate" class="form-control" value="<?=date('Y-m-d')?>"></div></div>
<div class="table-responsive"><table id="tableAttendance" class="table modern-table align-middle w-100"><thead><tr><th>No</th><th>Kode</th><th>Karyawan</th><th>Shift</th><th>Masuk</th><th>Pulang</th><th>Status</th></tr></thead></table></div>
</div>
<div class="tab-pane fade p-3" id="manualPane">
<div class="d-flex flex-wrap justify-content-between align-items-end gap-2 mb-3"><div><h5 class="mb-1">Alur Absensi Manual</h5><small class="text-muted">Draft dibuat dahulu, diajukan, kemudian baru masuk ke daftar kehadiran setelah disetujui.</small></div><select id="manualStatus" class="form-select" style="max-width:220px"><option value="">Semua status</option><option value="draft">Draft</option><option value="submitted">Menunggu persetujuan</option><option value="approved">Disetujui</option><option value="rejected">Ditolak</option></select></div>
<div class="hr-workflow-strip"><span><b>1</b>Buat Draft</span><span><b>2</b>Ajukan</span><span><b>3</b>Approve/Reject</span><span><b>4</b>Masuk Absensi</span></div>
<div class="table-responsive"><table id="tableManual" class="table modern-table align-middle w-100"><thead><tr><th>Dokumen</th><th>Karyawan</th><th>Tanggal/Jam</th><th>Jenis</th><th>Alasan</th><th>Status</th><th>Pembuat</th><th>Aksi</th></tr></thead></table></div>
</div>
</div>
</div>
</section>
<div class="card-body p-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="mb-0">
Monitoring Absensi
</h5>
<input type="date"
id="filterDate"
class="form-control"
style="max-width:220px"
value="<?= date('Y-m-d') ?>">
</div>
<div class="table-responsive">
<table id="tableAttendance"
class="table modern-table align-middle w-100">
<thead class="table-light">
<tr>
<th>No</th>
<th>Kode</th>
<th>Nama</th>
<th>Shift</th>
<th>Checkin</th>
<th>Checkout</th>
<th>Status</th>
</tr>
</thead>
</table>
</div>
</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"><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>
<script>
$(function(){
let table = $('#tableAttendance').DataTable({
ajax:{
url:`<?= base_url(); ?>attendancemonitoring/get_data`,
data:function(d){
d.date = $('#filterDate').val();
},
dataSrc:'data'
},
columns:[
{data:0},
{data:1},
{data:2},
{data:3},
{data:4},
{data:5},
{data:6}
]
});
$('#filterDate').change(function(){
table.ajax.reload();
});
const base=window.APP_BASE_URL||'<?=base_url()?>',modal=new bootstrap.Modal('#manualModal');
const daily=$('#tableAttendance').DataTable({processing:true,ajax:{url:base+'attendancemonitoring/get_data',data:d=>d.date=$('#filterDate').val(),dataSrc:'data'},order:[],columns:[0,1,2,3,4,5,6].map(i=>({data:i}))});
const statusLabel={draft:'Draft',submitted:'Menunggu Persetujuan',approved:'Disetujui',rejected:'Ditolak',cancelled:'Dibatalkan'};
const manual=$('#tableManual').DataTable({processing:true,ajax:{url:base+'attendancemonitoring/manual_requests',data:d=>d.status=$('#manualStatus').val(),dataSrc:'data'},order:[],columns:['request','employee','datetime','action','reason','status','creator','actions'].map(x=>({data:x})),columnDefs:[{targets:5,render:v=>`<span class="badge bg-${v==='approved'?'success':(v==='rejected'?'danger':(v==='submitted'?'warning':'secondary'))}">${statusLabel[v]||v}</span>`},{targets:7,orderable:false,searchable:false}]});
$('#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()});});
$(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})});});
$(document).on('click','.manual-detail',function(){const id=this.dataset.id;$.getJSON(base+'attendancemonitoring/manual_detail/'+id,r=>{if(!r.status)return AppAlert.toast(r.message,'error');const x=r.request;Swal.fire({title:x.request_no,html:`<div class="text-start"><b>${$('<div>').text(x.employee_code+' - '+x.full_name).html()}</b><hr><div>Tanggal: ${x.attendance_date} ${x.attendance_time.slice(0,5)}</div><div>Jenis: ${x.attendance_action==='check_in'?'Masuk':'Pulang'}</div><div>Status: ${x.status}</div><div class="mt-2"><small>Alasan</small><br>${$('<div>').text(x.reason).html()}</div>${x.rejection_reason?`<div class="alert alert-danger mt-2 mb-0">${$('<div>').text(x.rejection_reason).html()}</div>`:''}</div>`,showConfirmButton:true});});});
});
</script>
</script>
@@ -1,3 +1,4 @@
<?php $this->load->view('hr_payroll/_header',array('active_menu'=>'attendance_monthly','title'=>'Absensi Bulanan','subtitle'=>'Lihat rekap kehadiran bulanan dan rincian absensi setiap karyawan.')); ?>
<style>
.calendar-grid{
display:grid;
@@ -428,4 +429,4 @@
});
</script>
</script>
+24 -23
View File
@@ -1,3 +1,4 @@
<?php $this->load->view('hr_payroll/_header',array('active_menu'=>'employees','title'=>'Data Karyawan','subtitle'=>'Kelola identitas, penempatan kerja, rekening, dan pengaturan kerahasiaan gaji.')); ?>
<div class="container mt-4">
<div class="card border-0 shadow-lg rounded-4">
@@ -16,10 +17,10 @@
</small>
</div>
<button class="btn btn-warning rounded-pill px-4 btn-add">
<?php if($employee_permissions['create']):?><button class="btn btn-warning rounded-pill px-4 btn-add">
<i class="fa fa-plus me-2"></i>
Tambah Karyawan
</button>
</button><?php endif?>
</div>
@@ -400,6 +401,15 @@
</div>
<?php if($can_manage_all_salary):?>
<div class="col-12 mb-3">
<div class="form-check salary-restricted-note">
<input class="form-check-input" type="checkbox" id="salary_restricted" value="1">
<label class="form-check-label" for="salary_restricted"><b>Gaji terbatas</b> — hanya pengguna dengan permission <i>Manage All Gaji</i> yang dapat melihat dan memproses payroll karyawan ini.</label>
</div>
</div>
<?php endif?>
<div class="col-md-4 mb-3">
<label>Status Active</label>
@@ -419,16 +429,6 @@
</div>
<div class="col-md-4 mb-3">
<label>Basic Salary</label>
<input type="number"
id="basic_salary"
class="form-control">
</div>
<div class="col-md-4 mb-3">
<label>Bank Name</label>
@@ -636,22 +636,22 @@ $(function(){
data:null,
render:function(data){
return `
let actions = `
<button class="btn btn-info btn-sm btn-detail"
data-id="${data.id}">
Detail
</button>
<?php if($employee_permissions['update']):?>${data.salary_hidden ? '' : `
<button class="btn btn-warning btn-sm btn-edit"
data-id="${data.id}">
Edit
</button>
</button>`}<?php endif?>
<?php if($employee_permissions['delete']):?>${data.salary_hidden ? '' : `
<button class="btn btn-danger btn-sm btn-delete"
data-id="${data.id}">
Delete
</button>
`;
</button>`}<?php endif?>
`; return actions;
}
}
@@ -676,6 +676,7 @@ $(function(){
$('#employment_status').val('permanent');
$('#marital_status').val('single');
$('#is_active').val('1');
$('#salary_restricted').prop('checked',false);
$('#photoPreview').attr(
'src',
@@ -777,7 +778,7 @@ $(function(){
$('#position_id').val(d.position_id);
$('#branch_id').val(d.branch_id);
$('#basic_salary').val(d.basic_salary);
$('#salary_restricted').prop('checked',Number(d.salary_restricted)===1);
$('#bank_name').val(d.bank_name);
$('#bank_account_number').val(d.bank_account_number);
@@ -840,7 +841,7 @@ $(function(){
formData.append('position_id', $('#position_id').val());
formData.append('branch_id', $('#branch_id').val());
formData.append('basic_salary', $('#basic_salary').val());
<?php if($can_manage_all_salary):?>formData.append('salary_restricted', $('#salary_restricted').is(':checked') ? '1' : '0');<?php endif?>
formData.append('bank_name', $('#bank_name').val());
formData.append('bank_account_number', $('#bank_account_number').val());
@@ -1237,11 +1238,11 @@ $(function(){
<div class="col-md-6 mb-3">
<label class="text-muted small">
Basic Salary
Struktur Gaji
</label>
<div class="fw-semibold">
Rp ${parseInt(d.basic_salary || 0).toLocaleString('id-ID')}
${d.salary_hidden ? '<span class="text-danger"><i class="bi bi-lock"></i> Gaji terbatas</span>' : '<span class="text-muted">Dikelola melalui Master Gaji Karyawan</span>'}
</div>
</div>
@@ -1338,4 +1339,4 @@ $(function(){
});
});
</script>
</script>
@@ -1,3 +1,4 @@
<?php $this->load->view('hr_payroll/_header',array('active_menu'=>'generate_schedule','title'=>'Jadwal Kerja','subtitle'=>'Susun dan publikasikan jadwal karyawan tanpa mengubah aturan shift yang sudah berjalan.')); ?>
<!-- application/views/employees/generate_schedule.php -->
<style>
@@ -1056,4 +1057,4 @@ $('#btnAddEmployeeSchedule').click(function(){
});
});
</script>
</script>
+2 -1
View File
@@ -1,3 +1,4 @@
<?php $this->load->view('hr_payroll/_header',array('active_menu'=>'holidays','title'=>'Hari Libur','subtitle'=>'Kelola kalender hari libur nasional dan hari libur perusahaan.')); ?>
<div class="container mt-4">
<div class="card modern-card shadow-sm border-0 rounded-4">
@@ -331,4 +332,4 @@ $(function(){
});
});
</script>
</script>
+83 -406
View File
@@ -1,418 +1,95 @@
<div class="container mt-4">
<div class="card modern-card shadow-sm border-0 rounded-4">
<div class="card-body p-4">
<div class="d-flex justify-content-between mb-3">
<h5 class="mb-0">
Leave Requests
</h5>
<button class="btn btn-warning btn-add">
Tambah Leave
</button>
</div>
<div class="table-responsive">
<table id="tableLeave"
class="table modern-table align-middle w-100">
<thead class="table-light">
<tr>
<th width="5%">No</th>
<th>Finger ID</th>
<th>Employee</th>
<th>Jenis Leave</th>
<th>Tanggal Mulai</th>
<th>Tanggal Selesai</th>
<th>Total</th>
<th>Status</th>
<th width="15%">Aksi</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
<?php $this->load->view('hr_payroll/_header',array('active_menu'=>'leave_requests','title'=>'Cuti & Izin','subtitle'=>'Kelola pengajuan secara tertib dari draft, persetujuan HR, hingga masuk ke perhitungan kehadiran.','action_html'=>$permissions['create']?'<button type="button" class="btn btn-warning btn-add-leave"><i class="bi bi-plus-lg"></i> Buat Pengajuan</button>':'')); ?>
<div class="leave-flow" aria-label="Alur pengajuan cuti">
<span><b>1</b><small>Buat Draft</small></span><i class="bi bi-chevron-right"></i>
<span><b>2</b><small>Ajukan</small></span><i class="bi bi-chevron-right"></i>
<span><b>3</b><small>Review HR</small></span><i class="bi bi-chevron-right"></i>
<span><b>4</b><small>Disetujui / Ditolak</small></span>
</div>
<!-- MODAL -->
<div class="modal fade" id="modalLeave">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content rounded-4">
<div class="modal-header bg-warning text-white">
<h5 class="modal-title">
Tambah Leave
</h5>
<button class="btn-close btn-close-white"
data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="inputId">
<label class="fw-semibold">
Employee
</label>
<select class="form-control select-search"
id="employee_id">
<option value="">
Pilih Employee
</option>
<?php foreach($employees as $e): ?>
<option value="<?= $e->id ?>">
<?= $e->fingerprint_user_id ?>
-
<?= $e->full_name ?>
</option>
<?php endforeach; ?>
</select>
<label class="fw-semibold mt-2">
Jenis Cuti / Izin
</label>
<select class="form-control"
id="leave_type">
<option value="annual">
Cuti Tahunan
</option>
<option value="sick">
Sakit
</option>
<option value="permit">
Izin
</option>
<option value="maternity">
Cuti Melahirkan
</option>
<option value="unpaid">
Cuti Tidak Dibayar
</option>
</select>
<div class="row">
<div class="col-md-6">
<label class="fw-semibold mt-2">
Start Date
</label>
<input type="date"
class="form-control"
id="start_date">
</div>
<div class="col-md-6">
<label class="fw-semibold mt-2">
End Date
</label>
<input type="date"
class="form-control"
id="end_date">
</div>
</div>
<label class="fw-semibold mt-2">
Approval Status
</label>
<select class="form-control"
id="approval_status">
<option value="pending">
Pending
</option>
<option value="approved">
Approved
</option>
<option value="rejected">
Rejected
</option>
</select>
<label class="fw-semibold mt-2">
Reason
</label>
<textarea class="form-control"
id="reason"
rows="4"></textarea>
</div>
<div class="modal-footer">
<button class="btn btn-secondary"
data-bs-dismiss="modal">
Batal
</button>
<button class="btn btn-warning"
id="btnSimpan">
Simpan
</button>
</div>
<div class="leave-metrics">
<article><small>Semua Pengajuan</small><strong data-leave-count="all"><?=number_format($counts['all'],0,',','.')?></strong><span>Seluruh dokumen</span></article>
<article><small>Draft</small><strong data-leave-count="draft"><?=number_format($counts['draft'],0,',','.')?></strong><span>Belum diajukan</span></article>
<article class="metric-warning"><small>Menunggu Persetujuan</small><strong data-leave-count="submitted"><?=number_format($counts['submitted'],0,',','.')?></strong><span>Perlu tindakan HR</span></article>
<article class="metric-success"><small>Disetujui</small><strong data-leave-count="approved"><?=number_format($counts['approved'],0,',','.')?></strong><span>Aktif pada kehadiran</span></article>
</div>
<section class="card border-0 leave-card">
<div class="card-body p-0">
<nav class="leave-tabs" aria-label="Status pengajuan">
<button type="button" class="active" data-status="">Semua <b data-tab-count="all"><?=$counts['all']?></b></button>
<button type="button" data-status="draft">Draft <b data-tab-count="draft"><?=$counts['draft']?></b></button>
<button type="button" data-status="submitted">Menunggu <b data-tab-count="submitted"><?=$counts['submitted']?></b></button>
<button type="button" data-status="approved">Disetujui <b data-tab-count="approved"><?=$counts['approved']?></b></button>
<button type="button" data-status="rejected">Ditolak <b data-tab-count="rejected"><?=$counts['rejected']?></b></button>
</nav>
<div class="leave-toolbar">
<select id="filterEmployee" class="form-select select-search" data-placeholder="Semua karyawan"><option value="">Semua karyawan</option><?php foreach($employees as$employee):?><option value="<?=$employee->id?>"><?=html_escape(($employee->employee_code?:$employee->fingerprint_user_id).' - '.$employee->full_name)?></option><?php endforeach?></select>
<select id="filterType" class="form-select"><option value="">Semua jenis</option><option value="annual">Cuti Tahunan</option><option value="sick">Sakit</option><option value="permit">Izin</option><option value="maternity">Cuti Melahirkan</option><option value="unpaid">Cuti Tidak Dibayar</option></select>
<button type="button" class="btn btn-outline-secondary" id="refreshLeave"><i class="bi bi-arrow-clockwise"></i> Muat Ulang</button>
</div>
<div class="table-responsive">
<table id="tableLeave" class="table align-middle w-100 mb-0">
<thead><tr><th>Pengajuan</th><th>Karyawan</th><th>Jenis</th><th>Periode</th><th>Status</th><th>Diproses Oleh</th><th>Aksi</th></tr></thead>
</table>
</div>
</div>
</section>
<div class="modal fade" id="leaveModal" tabindex="-1" data-bs-backdrop="static">
<div class="modal-dialog modal-dialog-centered modal-lg"><div class="modal-content">
<div class="modal-header"><div><small class="text-muted">FORM PENGAJUAN</small><h5 class="modal-title">Buat Pengajuan Cuti / Izin</h5></div><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Tutup"></button></div>
<form id="leaveForm" data-ajax="true">
<div class="modal-body">
<input type="hidden" name="id" id="leaveId">
<div id="rejectionNotice" class="alert alert-danger d-none"></div>
<div class="row g-3">
<div class="col-12"><label class="form-label">Karyawan <span class="text-danger">*</span></label><select class="form-select select-search" name="employee_id" id="employeeId" data-placeholder="Cari karyawan" required><option value=""></option><?php foreach($employees as$employee):?><option value="<?=$employee->id?>"><?=html_escape(($employee->employee_code?:$employee->fingerprint_user_id).' - '.$employee->full_name)?></option><?php endforeach?></select></div>
<div class="col-md-6"><label class="form-label">Jenis Cuti / Izin <span class="text-danger">*</span></label><select class="form-select" name="leave_type" id="leaveType" required><option value="annual">Cuti Tahunan</option><option value="sick">Sakit</option><option value="permit">Izin</option><option value="maternity">Cuti Melahirkan</option><option value="unpaid">Cuti Tidak Dibayar</option></select></div>
<div class="col-md-3"><label class="form-label">Tanggal Mulai <span class="text-danger">*</span></label><input type="date" class="form-control" name="start_date" id="startDate" required></div>
<div class="col-md-3"><label class="form-label">Tanggal Selesai <span class="text-danger">*</span></label><input type="date" class="form-control" name="end_date" id="endDate" required></div>
<div class="col-12"><label class="form-label">Alasan Pengajuan <span class="text-danger">*</span></label><textarea class="form-control" name="reason" id="leaveReason" rows="4" maxlength="2000" placeholder="Jelaskan kebutuhan cuti atau izin secara ringkas dan jelas" required></textarea><div class="form-text">Status persetujuan ditentukan melalui proses Ajukan dan tindakan approver, bukan dari form ini.</div></div>
</div>
</div>
<div class="modal-footer"><button type="button" class="btn btn-light" data-bs-dismiss="modal">Tutup</button><button type="submit" class="btn btn-warning" id="saveLeave"><i class="bi bi-save"></i> Simpan Draft</button></div>
</form>
</div></div>
</div>
<div class="modal fade" id="leaveDetailModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-lg"><div class="modal-content">
<div class="modal-header"><div><small class="text-muted">DETAIL & RIWAYAT</small><h5 class="modal-title" id="detailTitle">Pengajuan Cuti</h5></div><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Tutup"></button></div>
<div class="modal-body" id="leaveDetailBody"></div>
<div class="modal-footer"><button type="button" class="btn btn-light" data-bs-dismiss="modal">Tutup</button></div>
</div></div>
</div>
<script>
$(function(){
let table = $('#tableLeave').DataTable({
processing:true,
responsive:true,
autoWidth:false,
ajax:{
url:`<?= base_url(); ?>leaverequests/get_data`,
dataSrc:'data'
},
order:[]
});
// =====================================
// RESET
// =====================================
function resetForm(){
$('#inputId').val('');
$('#employee_id').val('');
$('#leave_type').val('annual');
$('#start_date').val('');
$('#end_date').val('');
$('#approval_status').val('pending');
$('#reason').val('');
$('#modalLeave .modal-title')
.text('Tambah Leave');
}
// =====================================
// ADD
// =====================================
$('.btn-add').click(function(){
resetForm();
$('#btnSimpan')
.data('action','add');
$('#modalLeave').modal('show');
});
// =====================================
// SAVE UPDATE
// =====================================
$('#btnSimpan').click(function(){
let action = $(this).data('action');
let data = {
id: $('#inputId').val(),
employee_id: $('#employee_id').val(),
leave_type: $('#leave_type').val(),
start_date: $('#start_date').val(),
end_date: $('#end_date').val(),
approval_status: $('#approval_status').val(),
reason: $('#reason').val()
};
if(
!data.employee_id ||
!data.start_date ||
!data.end_date
){
Swal.fire(
'Warning',
'Data wajib diisi',
'warning'
);
return;
}
let url = action == 'add'
? `<?= base_url(); ?>leaverequests/save`
: `<?= base_url(); ?>leaverequests/update`;
$.ajax({
url:url,
type:'POST',
data:data,
dataType:'json',
success:function(res){
if(res.status){
$('#modalLeave').modal('hide');
table.ajax.reload(null,false);
Swal.fire(
'Sukses',
res.message,
'success'
);
} else {
Swal.fire(
'Error',
res.message,
'error'
);
}
}
});
});
// =====================================
// EDIT
// =====================================
$(document).on('click','.btn-edit',function(){
let id = $(this).data('id');
$.get(
`<?= base_url(); ?>leaverequests/detail/` + id,
function(res){
$('#inputId').val(res.id);
$('#employee_id').val(res.employee_id);
$('#leave_type').val(res.leave_type);
$('#start_date').val(res.start_date);
$('#end_date').val(res.end_date);
$('#approval_status')
.val(res.approval_status);
$('#reason').val(res.reason);
$('#modalLeave .modal-title')
.text('Edit Leave');
$('#btnSimpan')
.data('action','edit');
$('#modalLeave').modal('show');
},
'json'
);
});
// =====================================
// DELETE
// =====================================
$(document).on('click','.btn-delete',function(){
let id = $(this).data('id');
Swal.fire({
title:'Yakin hapus?',
text:'Data tidak bisa dikembalikan',
icon:'warning',
showCancelButton:true
}).then((result)=>{
if(result.isConfirmed){
$.get(
`<?= base_url(); ?>leaverequests/delete/` + id,
function(res){
if(res.status){
table.ajax.reload(null,false);
Swal.fire(
'Sukses',
res.message,
'success'
);
}
},
'json'
);
}
});
});
const permissions=<?=json_encode($permissions)?>,base='<?=base_url('leaverequests/')?>',csrf=()=>({[APP_CSRF.name]:APP_CSRF.value});
const types={annual:'Cuti Tahunan',sick:'Sakit',permit:'Izin',maternity:'Cuti Melahirkan',unpaid:'Cuti Tidak Dibayar'};
const statuses={draft:'Draft',submitted:'Menunggu Persetujuan',approved:'Disetujui',rejected:'Ditolak',cancelled:'Dibatalkan'};
const esc=value=>$('<div>').text(value==null?'-':value).html();
const date=value=>value?String(value).slice(0,10).split('-').reverse().join('/'):'-';
const badge=status=>'<span class="leave-status leave-status-'+esc(status)+'">'+esc(statuses[status]||status)+'</span>';
const toast=(message,icon)=>AppAlert.toast(message,icon||'success');
function fail(xhr){let message=xhr.responseJSON&&xhr.responseJSON.message;if(!message&&xhr.statusText==='timeout')message='Waktu proses habis. Silakan coba kembali.';toast(message||'Proses tidak berhasil.','error')}
function post(url,data){return $.ajax({url:url,type:'POST',data:Object.assign({},data||{},csrf()),dataType:'json',timeout:30000})}
function actions(row){let out='<div class="d-flex flex-wrap gap-1"><button type="button" class="btn btn-sm btn-outline-primary leave-detail" data-id="'+row.id+'"><i class="bi bi-eye"></i> Detail</button>';if((row.workflow_status==='draft'||row.workflow_status==='rejected')&&permissions.update)out+='<button type="button" class="btn btn-sm btn-outline-secondary leave-edit" data-id="'+row.id+'"><i class="bi bi-pencil"></i> Edit</button>';if(row.workflow_status==='draft'&&permissions.create)out+='<button type="button" class="btn btn-sm btn-warning leave-submit" data-id="'+row.id+'"><i class="bi bi-send"></i> Ajukan</button>';if(row.workflow_status==='submitted'&&permissions.approve)out+='<button type="button" class="btn btn-sm btn-success leave-approve" data-id="'+row.id+'"><i class="bi bi-check-lg"></i> Setujui</button><button type="button" class="btn btn-sm btn-outline-danger leave-reject" data-id="'+row.id+'"><i class="bi bi-x-lg"></i> Tolak</button>';if((row.workflow_status==='draft'||row.workflow_status==='rejected')&&(permissions.update||permissions.create))out+='<button type="button" class="btn btn-sm btn-outline-danger leave-cancel" data-id="'+row.id+'" title="Batalkan"><i class="bi bi-trash"></i></button>';return out+'</div>'}
const table=$('#tableLeave').DataTable({processing:true,responsive:true,autoWidth:false,pageLength:25,order:[],ajax:{url:base+'get_data',data:d=>{d.status=$('.leave-tabs button.active').data('status')||'';d.type=$('#filterType').val();d.employee_id=$('#filterEmployee').val()},dataSrc:function(response){updateCounts(response.summary||{});return response.data||[]}},columns:[{data:'request_no',render:(value,type,row)=>'<button type="button" class="btn btn-link p-0 fw-semibold leave-detail" data-id="'+row.id+'">'+esc(value)+'</button><small class="d-block text-muted">Dibuat '+esc(row.creator_name||'-')+'</small>'},{data:'full_name',render:(value,type,row)=>'<strong>'+esc(value)+'</strong><small class="d-block text-muted">'+esc(row.employee_code||row.fingerprint_user_id)+' · '+esc(row.department_name||'-')+'</small>'},{data:'leave_type',render:value=>types[value]||value},{data:null,render:row=>date(row.start_date)+' – '+date(row.end_date)+'<small class="d-block text-muted">'+Number(row.total_days||0).toLocaleString('id-ID')+' hari</small>'},{data:'workflow_status',render:(value,type,row)=>badge(value)+(value==='rejected'&&row.rejection_reason?'<small class="d-block text-danger mt-1">'+esc(row.rejection_reason)+'</small>':'')},{data:null,render:row=>row.workflow_status==='approved'?esc(row.approver_name||'-'):row.workflow_status==='rejected'?esc(row.rejector_name||'-'):row.workflow_status==='submitted'?'<span class="text-warning-emphasis">Menunggu HR</span>':'-'},{data:null,orderable:false,searchable:false,render:actions}],language:{search:'Cari:',lengthMenu:'Tampilkan _MENU_',info:'_START_–_END_ dari _TOTAL_',infoEmpty:'Belum ada pengajuan',zeroRecords:'Pengajuan tidak ditemukan',processing:'Memuat data...',paginate:{previous:'Sebelumnya',next:'Berikutnya'}}});
function updateCounts(summary){Object.keys(summary).forEach(key=>{$('[data-leave-count="'+key+'"]').text(Number(summary[key]||0).toLocaleString('id-ID'));$('[data-tab-count="'+key+'"]').text(Number(summary[key]||0).toLocaleString('id-ID'))})}
function reload(){table.ajax.reload(null,false)}
$('.leave-tabs button').on('click',function(){$('.leave-tabs button').removeClass('active');$(this).addClass('active');reload()});
$('#filterEmployee,#filterType').on('change',reload);$('#refreshLeave').on('click',reload);
function resetForm(){$('#leaveForm')[0].reset();$('#leaveId').val('');$('#employeeId').val(null).trigger('change');$('#leaveType').val('annual');$('#rejectionNotice').addClass('d-none').empty();$('#leaveModal .modal-title').text('Buat Pengajuan Cuti / Izin')}
$('.btn-add-leave').on('click',function(){resetForm();bootstrap.Modal.getOrCreateInstance(document.getElementById('leaveModal')).show()});
$('#leaveForm').on('submit',function(event){event.preventDefault();let button=$('#saveLeave').prop('disabled',true).html('<span class="spinner-border spinner-border-sm"></span> Menyimpan');$.ajax({url:base+'save',type:'POST',data:$(this).serialize(),dataType:'json',timeout:30000}).done(response=>{bootstrap.Modal.getInstance(document.getElementById('leaveModal')).hide();reload();toast(response.message,'success')}).fail(fail).always(()=>button.prop('disabled',false).html('<i class="bi bi-save"></i> Simpan Draft'))});
$(document).on('click','.leave-edit',function(){let id=$(this).data('id');$.get(base+'detail/'+id).done(response=>{let row=response.leave;resetForm();$('#leaveId').val(row.id);$('#employeeId').val(String(row.employee_id)).trigger('change');$('#leaveType').val(row.leave_type);$('#startDate').val(row.start_date);$('#endDate').val(row.end_date);$('#leaveReason').val(row.reason);if(row.rejection_reason)$('#rejectionNotice').removeClass('d-none').html('<strong>Alasan penolakan:</strong> '+esc(row.rejection_reason));$('#leaveModal .modal-title').text('Perbaiki '+row.request_no);bootstrap.Modal.getOrCreateInstance(document.getElementById('leaveModal')).show()}).fail(fail)});
$(document).on('click','.leave-detail',function(){let id=$(this).data('id');$('#leaveDetailBody').html('<div class="text-center py-5"><span class="spinner-border text-warning"></span><p class="mt-2 mb-0">Memuat detail...</p></div>');bootstrap.Modal.getOrCreateInstance(document.getElementById('leaveDetailModal')).show();$.get(base+'detail/'+id).done(response=>{let row=response.leave,history=(response.history||[]).map(item=>'<li><span class="leave-history-dot"></span><div><strong>'+esc({submitted:'Diajukan',approved:'Disetujui',rejected:'Ditolak',cancelled:'Dibatalkan'}[item.action]||item.action)+'</strong><small>'+esc(item.user_name||'-')+' · '+esc(item.created_at)+'</small>'+(item.notes?'<p>'+esc(item.notes)+'</p>':'')+'</div></li>').join('');$('#detailTitle').text(row.request_no);$('#leaveDetailBody').html('<div class="leave-detail-grid"><span>Karyawan<strong>'+esc(row.full_name)+'</strong><small>'+esc(row.employee_code||row.fingerprint_user_id)+'</small></span><span>Jenis<strong>'+esc(types[row.leave_type]||row.leave_type)+'</strong></span><span>Periode<strong>'+date(row.start_date)+' – '+date(row.end_date)+'</strong><small>'+esc(row.total_days)+' hari</small></span><span>Status<strong>'+badge(row.workflow_status)+'</strong></span><span>Diajukan oleh<strong>'+esc(row.submitter_name||'-')+'</strong><small>'+esc(row.submitted_at||'-')+'</small></span><span>Disetujui oleh<strong>'+esc(row.approver_name||'-')+'</strong><small>'+esc(row.approved_at||'-')+'</small></span></div><div class="leave-reason"><small>ALASAN PENGAJUAN</small><p>'+esc(row.reason)+'</p></div>'+(row.rejection_reason?'<div class="alert alert-danger"><strong>Alasan penolakan</strong><br>'+esc(row.rejection_reason)+'</div>':'')+'<h6 class="mt-4">Riwayat Persetujuan</h6><ul class="leave-history">'+(history||'<li><span class="leave-history-dot"></span><div><strong>Draft dibuat</strong><small>'+esc(row.creator_name||'-')+' · '+esc(row.created_at)+'</small></div></li>')+'</ul>')}).fail(fail)});
$(document).on('click','.leave-submit',function(){let id=$(this).data('id'),button=$(this),original=button.html();AppAlert.confirm({title:'Ajukan cuti / izin?',text:'Setelah diajukan, data tidak dapat diedit sampai diputuskan oleh approver.',confirmButtonText:'Ya, Ajukan'}).then(result=>{if(!result.isConfirmed)return;button.prop('disabled',true).html('<span class="spinner-border spinner-border-sm"></span> Memproses');post(base+'submit/'+id).done(response=>{reload();toast(response.message,'success')}).fail(fail).always(()=>button.prop('disabled',false).html(original))})});
$(document).on('click','.leave-approve',function(){let id=$(this).data('id');AppAlert.prompt({title:'Setujui pengajuan?',input:'textarea',inputLabel:'Catatan approver (opsional)',confirmButtonText:'Setujui',icon:'question'}).then(result=>{if(result.isConfirmed)post(base+'approve/'+id,{notes:result.value||''}).done(response=>{reload();toast(response.message,'success')}).fail(fail)})});
$(document).on('click','.leave-reject',function(){let id=$(this).data('id');AppAlert.prompt({title:'Tolak pengajuan?',input:'textarea',inputLabel:'Alasan penolakan',inputPlaceholder:'Jelaskan alasan agar pengajuan dapat diperbaiki',inputValidator:value=>!String(value||'').trim()?'Alasan penolakan wajib diisi.':undefined,confirmButtonText:'Tolak Pengajuan',icon:'warning'}).then(result=>{if(result.isConfirmed)post(base+'reject/'+id,{reason:result.value}).done(response=>{reload();toast(response.message,'success')}).fail(fail)})});
$(document).on('click','.leave-cancel',function(){let id=$(this).data('id');AppAlert.confirm({title:'Batalkan Draft?',text:'Dokumen tetap disimpan sebagai histori dan tidak memengaruhi kehadiran.',confirmButtonText:'Ya, Batalkan'}).then(result=>{if(result.isConfirmed)post(base+'cancel/'+id).done(response=>{reload();toast(response.message,'success')}).fail(fail)})});
const focus=new URLSearchParams(location.search).get('focus');if(focus)table.one('draw.dt',()=>setTimeout(()=>$('.leave-detail[data-id="'+String(focus).replace(/[^0-9]/g,'')+'"]').first().trigger('click'),100));
});
</script>
</script>
+2 -1
View File
@@ -1,3 +1,4 @@
<?php $this->load->view('hr_payroll/_header',array('active_menu'=>'shift','title'=>'Master Shift','subtitle'=>'Atur jam kerja, toleransi keterlambatan, dan pola shift karyawan.')); ?>
<div class="container mt-4">
<div class="card modern-card shadow-sm border-0 rounded-4">
@@ -408,4 +409,4 @@ $(function(){
});
});
</script>
</script>