Files
accounting_dev_v2/application/views/employees/leave_requests.php
T
2026-09-14 11:14:25 +07:00

96 lines
17 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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>
<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(){
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>