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 -1
View File
@@ -4,7 +4,7 @@ defined('BASEPATH') OR exit('No direct script access allowed');
$loginError = isset($error) && trim((string) $error) !== ''
? trim((string) $error)
: trim((string) $this->session->flashdata('error'));
$authNotice = trim((string) $this->session->flashdata('auth_notice'));
$authNotice = isset($auth_notice)&&trim((string)$auth_notice)!==''?trim((string)$auth_notice):trim((string) $this->session->flashdata('auth_notice'));
?>
<!DOCTYPE html>
<html lang="id">
@@ -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>
+39
View File
@@ -0,0 +1,39 @@
<?php
$hrActive = isset($active_menu) ? $active_menu : '';
$hrTitle = isset($title) ? $title : 'HR & Payroll';
$hrSubtitle = isset($subtitle) ? $subtitle : 'Kelola administrasi karyawan secara tertib dan mudah dipahami.';
$employeePages = array('employees','organization_master');
$attendancePages = array('generate_schedule','attendance_monitoring','attendance_monthly','shift','holidays','leave_requests');
if (in_array($hrActive, $employeePages, true)) {
$hrLinks = array(
array('employees','Data Karyawan','employees','hr_employees','bi-person-vcard'),
array('organization_master','Departemen & Posisi','organizationmaster','hr_organization','bi-diagram-3')
);
} elseif (in_array($hrActive, $attendancePages, true)) {
$hrLinks = array(
array('generate_schedule','Jadwal Kerja','generateschedule','hr_schedules','bi-calendar3'),
array('attendance_monitoring','Monitoring Absen','attendancemonitoring','hr_attendance','bi-person-check'),
array('attendance_monthly','Absen Bulanan','attendancemonthly','hr_attendance','bi-calendar-check'),
array('holidays','Hari Libur','holidays','hr_schedules','bi-calendar-event'),
array('shift','Shift','shifts','hr_schedules','bi-clock'),
array('leave_requests','Cuti & Izin','leaverequests','hr_leave','bi-calendar2-minus')
);
} else {
$hrLinks = array(
array('hr_payroll','Daftar Periode Gaji','hrpayroll?section=periods','payroll','bi-calendar2-range'),
array('hr_payroll_salary','Master Gaji Karyawan','hrpayroll?section=salaries','payroll','bi-person-lines-fill'),
array('hr_payroll_settings','Setting Payroll','hrpayroll?section=settings','payroll','bi-sliders'),
array('hr_payroll_adjustments','Penyesuaian Periode','hrpayroll?section=adjustments','payroll','bi-plus-minus')
);
}
?>
<section class="hr-module-hero">
<div><span>HR & PAYROLL WORKSPACE</span><h2 class="text-light"><?=html_escape($hrTitle)?></h2><p><?=html_escape($hrSubtitle)?></p></div>
<?php if (!empty($action_html)) echo $action_html; ?>
</section>
<nav class="hr-module-nav" aria-label="Navigasi HR & Payroll">
<?php foreach ($hrLinks as $link): if (!is_master_admin_user() && !check_permission($link[3],'can_view') && !check_permission('employees','can_view')) continue; ?>
<?php $isActive = $hrActive === $link[0] || ($hrActive === 'hr_payroll' && strpos($link[2], 'section='.((string)($payroll_section ?? 'periods'))) !== false); ?>
<a class="<?=$isActive?'active':''?>" href="<?=base_url($link[2])?>"><i class="bi <?=$link[4]?>"></i><span><?=html_escape($link[1])?></span></a>
<?php endforeach; ?>
</nav>
+57 -3
View File
@@ -1,3 +1,57 @@
<div class="container-fluid py-3"><h4>HR & Payroll Control</h4><p class="text-muted">Lock absensi → Draft → Review HR → Approval → Final → Jurnal → Pembayaran.</p><?php foreach(array('success','error')as$f)if($this->session->flashdata($f)):?><div class="alert alert-<?=$f==='success'?'success':'danger'?>"><?=html_escape($this->session->flashdata($f))?></div><?php endif?><div class="row g-3"><div class="col-lg-4"><div class="card h-100"><div class="card-header">1. Lock Absensi Bulanan</div><div class="card-body"><form method="post" action="<?=base_url('hrpayroll/lock_attendance')?>"><input class="form-control mb-2" type="month" name="period" value="<?=date('Y-m')?>" required><input class="form-control mb-2" name="notes" required placeholder="Catatan validasi absensi"><button class="btn btn-warning">Validasi & Lock</button></form></div></div></div><div class="col-lg-4"><div class="card h-100"><div class="card-header">2. Hitung Draft Payroll</div><div class="card-body"><form method="post" action="<?=base_url('hrpayroll/calculate')?>"><input class="form-control mb-2" type="month" name="month" value="<?=date('Y-m')?>" required><p class="small">Menggunakan snapshot absensi terkunci dan formula komponen aktif.</p><button class="btn btn-primary">Generate/Hitung Ulang Draft</button></form></div></div></div><div class="col-lg-4"><div class="card h-100"><div class="card-header">Formula Configurable</div><div class="card-body"><small>Variabel: basic_salary, work_days, present_days, unpaid_days, overtime_hours, overtime_rate, daily_rate.</small><form class="mt-2" method="post" action="<?=base_url('hrpayroll/component')?>"><div class="d-flex gap-1"><input class="form-control" name="code" placeholder="Kode" required><input class="form-control" name="name" placeholder="Nama" required></div><div class="d-flex gap-1 my-1"><select class="form-select" name="component_type"><option value="earning">Pendapatan</option><option value="deduction">Potongan</option><option value="employer_cost">Beban perusahaan</option></select><select class="form-select" name="category"><option>allowance</option><option>overtime</option><option>unpaid_leave</option><option>loan</option><option>bpjs</option><option>tax</option><option>thr</option><option>bonus</option><option>other</option></select></div><input class="form-control mb-1" name="formula_expression" placeholder="Contoh: overtime_hours * overtime_rate"><input class="form-control mb-1" type="number" name="fixed_amount" placeholder="Atau nominal tetap"><label class="me-2"><input type="checkbox" name="taxable" value="1"> Taxable</label><label><input type="checkbox" name="prorated" value="1"> Prorata</label><br><button class="btn btn-sm btn-outline-primary mt-2">Simpan Komponen</button></form></div></div></div></div>
<div class="card mt-3"><div class="card-header">Payroll Register</div><div class="table-responsive"><table class="table mb-0"><tr><th>Periode</th><th>Karyawan</th><th>Bruto</th><th>Potongan</th><th>Neto</th><th>Status</th><th>Workflow</th></tr><?php foreach($periods as$p):?><tr><td><?=html_escape($p->period_name)?></td><td><?=$p->employee_count?></td><td class="text-end"><?=number_format($p->gross,0,',','.')?></td><td class="text-end"><?=number_format($p->deductions,0,',','.')?></td><td class="text-end"><?=number_format($p->net,0,',','.')?></td><td><span class="badge bg-secondary"><?=$p->workflow_status?></span></td><td><div class="d-flex gap-1"><?php if($p->workflow_status==='draft'):?><form method="post" action="<?=base_url('hrpayroll/workflow/'.$p->id.'/review')?>"><button class="btn btn-sm btn-info">Review HR</button></form><?php elseif($p->workflow_status==='reviewed'):?><form method="post" action="<?=base_url('hrpayroll/workflow/'.$p->id.'/approve')?>"><button class="btn btn-sm btn-warning">Approve</button></form><?php elseif($p->workflow_status==='approved'):?><form method="post" action="<?=base_url('hrpayroll/workflow/'.$p->id.'/finalize')?>"><button class="btn btn-sm btn-danger">Final & Lock</button></form><?php elseif($p->workflow_status==='final'):?><form class="d-flex" method="post" action="<?=base_url('hrpayroll/post/'.$p->id)?>"><input class="form-control form-control-sm" type="date" name="date" value="<?=$p->end_date?>"><button class="btn btn-sm btn-success">Post Jurnal</button></form><?php elseif($p->workflow_status==='posted'):?><form class="d-flex" method="post" action="<?=base_url('hrpayroll/pay/'.$p->id)?>"><input class="form-control form-control-sm" type="date" name="date" value="<?=date('Y-m-d')?>"><select class="form-select form-select-sm" name="cash_account_id"><?php foreach($cash_accounts as$c):?><option value="<?=$c->id?>"><?=html_escape($c->name)?></option><?php endforeach?></select><button class="btn btn-sm btn-primary">Bayar</button></form><?php endif?></div></td></tr><?php endforeach?></table></div></div>
<div class="card mt-3"><div class="card-header">Komponen Aktif</div><div class="table-responsive"><table class="table table-sm"><tr><th>Kode</th><th>Nama</th><th>Jenis</th><th>Kategori</th><th>Formula/Nominal</th><th>Pajak</th></tr><?php foreach($components as$c):?><tr><td><?=$c->code?></td><td><?=html_escape($c->name)?></td><td><?=$c->component_type?></td><td><?=$c->category?></td><td><?=html_escape($c->formula_expression?:number_format($c->fixed_amount,0,',','.'))?></td><td><?=$c->taxable?'Ya':'Tidak'?></td></tr><?php endforeach?></table></div></div></div>
<?php
$sectionLabels=array('periods'=>array('hr_payroll','Daftar Periode Gaji','Kelola rentang kehadiran, proses payroll, jurnal, dan pembayaran dalam satu alur.'),'salaries'=>array('hr_payroll_salary','Master Gaji Karyawan','Atur pendapatan dan potongan berulang dengan histori tanggal efektif.'),'settings'=>array('hr_payroll_settings','Setting Payroll','Kelola komponen, template posisi, dan aturan kehadiran tanpa input berulang setiap bulan.'),'adjustments'=>array('hr_payroll_adjustments','Penyesuaian Periode','Catat bonus, rapel, insentif, atau potongan yang hanya berlaku pada satu periode.'));
$meta=$sectionLabels[$payroll_section];
$this->load->view('hr_payroll/_header',array('active_menu'=>$meta[0],'payroll_section'=>$payroll_section,'title'=>$meta[1],'subtitle'=>$meta[2]));
$csrf='<input type="hidden" name="'.$this->security->get_csrf_token_name().'" value="'.$this->security->get_csrf_hash().'">';
$methodLabels=array('fixed'=>'Nominal tetap','per_present_day'=>'Per hari hadir','per_absent_day'=>'Per hari tidak hadir','per_minute'=>'Per menit','per_hour'=>'Per jam','percentage_basic'=>'Persentase gaji pokok','formula'=>'Formula tervalidasi');
$statusLabels=array('draft'=>'Draft','reviewed'=>'Sudah Direview','approved'=>'Disetujui','final'=>'Final','posted'=>'Terposting','paid'=>'Dibayar','cancelled'=>'Dibatalkan');
?>
<?php foreach(array('success','error')as$flash):if($this->session->flashdata($flash)):?><div data-app-toast data-icon="<?=$flash==='success'?'success':'error'?>" data-message="<?=html_escape($this->session->flashdata($flash))?>"></div><?php endif;endforeach?>
<?php if(!$can_manage_all_salary):?><div class="alert alert-warning"><i class="bi bi-lock-fill me-1"></i> Tampilan ini tidak memuat nilai maupun total gaji terbatas. Akses tersebut memerlukan permission <b>Manage All Gaji</b>.</div><?php endif?>
<?php if($payroll_section==='periods'):?>
<div class="hr-workflow-strip" aria-label="Tahapan payroll"><span><b>1</b>Buat Periode</span><span><b>2</b>Lock Kehadiran</span><span><b>3</b>Hitung Draft</span><span><b>4</b>Review HR</span><span><b>5</b>Approve & Final</span><span><b>6</b>Posting Jurnal</span><span><b>7</b>Pembayaran</span></div>
<?php if($permissions['create']):?><section class="card hr-panel mb-3"><div class="card-header bg-white"><b>Buat Periode Gaji</b><small class="d-block text-muted">Rentang kehadiran bebas melintasi bulan; periode pada perusahaan yang sama tidak boleh tumpang tindih.</small></div><div class="card-body"><form method="post" action="<?=base_url('hrpayroll/create_period')?>" class="row g-3"><?=$csrf?><div class="col-lg-3"><label class="form-label">Nama Periode</label><input class="form-control" name="period_name" placeholder="Contoh: Gaji September 2026" required></div><div class="col-md-3 col-lg-2"><label class="form-label">Awal Kehadiran</label><input type="date" class="form-control" name="attendance_start_date" required></div><div class="col-md-3 col-lg-2"><label class="form-label">Akhir Kehadiran</label><input type="date" class="form-control" name="attendance_end_date" required></div><div class="col-md-3 col-lg-2"><label class="form-label">Tanggal Bayar</label><input type="date" class="form-control" name="payment_date" required></div><div class="col-lg-3"><label class="form-label">Keterangan</label><div class="input-group"><input class="form-control" name="notes" placeholder="Opsional"><button class="btn btn-primary"><i class="bi bi-plus-lg"></i> Buat</button></div></div></form></div></section><?php endif?>
<section class="card hr-panel"><div class="card-header bg-white"><b>Daftar Periode Payroll</b><small class="d-block text-muted">Aksi hanya muncul sesuai status dan permission. Finalisasi tidak membuat jurnal; jurnal dibuat pada tahap Posting.</small></div><div class="table-responsive"><table class="table align-middle mb-0 payroll-table"><thead><tr><th>Periode</th><th>Lock Kehadiran</th><th>Status</th><th class="text-end">Karyawan</th><th class="text-end">Bruto</th><th class="text-end">Potongan</th><th class="text-end">Neto</th><th>Aksi Berikutnya</th></tr></thead><tbody>
<?php if(!$periods):?><tr><td colspan="8" class="text-center text-muted py-5">Belum ada periode payroll. Buat satu periode untuk memulai.</td></tr><?php endif;foreach($periods as$period):$start=$period->attendance_start_date?:$period->start_date;$end=$period->attendance_end_date?:$period->end_date;$locked=$period->attendance_lock_status==='locked';?>
<tr><td><b><?=html_escape($period->period_name)?></b><small class="d-block text-muted"><?=html_escape($start)?> s.d. <?=html_escape($end)?></small><small class="text-muted">Bayar: <?=html_escape($period->payment_date?:'-')?></small></td><td><span class="badge <?=$locked?'bg-success':'bg-secondary'?>"><?=$locked?'Terkunci':'Belum dikunci'?></span></td><td><span class="badge bg-<?=in_array($period->workflow_status,array('posted','paid'),true)?'success':($period->workflow_status==='draft'?'secondary':'warning')?>"><?=html_escape($statusLabels[$period->workflow_status]??$period->workflow_status)?></span></td><td class="text-end"><?=number_format($period->employee_count)?></td><td class="text-end">Rp <?=number_format($period->gross,2,',','.')?></td><td class="text-end">Rp <?=number_format($period->deductions,2,',','.')?></td><td class="text-end fw-bold">Rp <?=number_format($period->net,2,',','.')?></td><td><div class="payroll-actions"><button class="btn btn-sm btn-outline-primary payroll-detail" data-id="<?=$period->id?>"><i class="bi bi-eye"></i> Detail</button>
<?php if($permissions['post']&&$period->workflow_status==='draft'&&!$locked):?><form method="post" action="<?=base_url('hrpayroll/lock_attendance')?>" class="js-confirm" data-confirm-title="Kunci rentang kehadiran?" data-confirm-text="Absensi dalam rentang ini tidak dapat diubah setelah dikunci."><?=$csrf?><input type="hidden" name="period_id" value="<?=$period->id?>"><input type="hidden" name="notes" value="Validasi payroll <?=html_escape($period->period_name)?>"><button class="btn btn-sm btn-warning">Lock Kehadiran</button></form><?php endif?>
<?php if($permissions['create']&&$period->workflow_status==='draft'&&$locked):?><form method="post" action="<?=base_url('hrpayroll/calculate')?>" class="js-confirm" data-confirm-title="Hitung draft payroll?" data-confirm-text="Hasil Draft periode ini akan dibuat ulang tanpa menggandakan rincian."><?=$csrf?><input type="hidden" name="period_id" value="<?=$period->id?>"><button class="btn btn-sm btn-primary"><?=empty($period->calculated_at)?'Hitung Draft':'Hitung Ulang'?></button></form><?php endif?>
<?php if($permissions['approve']&&$period->workflow_status==='draft'&&!empty($period->calculated_at)):?><form method="post" action="<?=base_url('hrpayroll/workflow/'.$period->id.'/review')?>" class="js-confirm"><?=$csrf?><button class="btn btn-sm btn-warning">Review HR</button></form><?php endif?>
<?php if($permissions['approve']&&$period->workflow_status==='reviewed'):?><form method="post" action="<?=base_url('hrpayroll/workflow/'.$period->id.'/approve')?>" class="js-confirm"><?=$csrf?><button class="btn btn-sm btn-success">Approve</button></form><form method="post" action="<?=base_url('hrpayroll/workflow/'.$period->id.'/revert')?>" class="js-confirm"><?=$csrf?><button class="btn btn-sm btn-outline-secondary">Kembali ke Draft</button></form><?php endif?>
<?php if($permissions['approve']&&$period->workflow_status==='approved'):?><form method="post" action="<?=base_url('hrpayroll/workflow/'.$period->id.'/finalize')?>" class="js-confirm"><?=$csrf?><button class="btn btn-sm btn-dark">Finalisasi</button></form><form method="post" action="<?=base_url('hrpayroll/workflow/'.$period->id.'/revert')?>" class="js-confirm"><?=$csrf?><button class="btn btn-sm btn-outline-secondary">Kembali ke Draft</button></form><?php endif?>
<?php if($permissions['post']&&$period->workflow_status==='final'):?><form method="post" action="<?=base_url('hrpayroll/post/'.$period->id)?>" class="d-flex gap-1 js-confirm" data-confirm-title="Posting jurnal payroll?"><?=$csrf?><input class="form-control form-control-sm" type="date" name="date" value="<?=$period->payment_date?:date('Y-m-d')?>" required><button class="btn btn-sm btn-success">Posting</button></form><?php endif?>
<?php if($permissions['post']&&$period->workflow_status==='posted'):?><form method="post" action="<?=base_url('hrpayroll/pay/'.$period->id)?>" class="d-flex gap-1 js-confirm" data-confirm-title="Bayar payroll?"><?=$csrf?><input class="form-control form-control-sm" type="date" name="date" value="<?=$period->payment_date?:date('Y-m-d')?>" required><select class="form-select form-select-sm" name="cash_account_id" required><option value="">Kas/Bank</option><?php foreach($cash_accounts as$account):?><option value="<?=$account->id?>"><?=html_escape($account->code.' - '.$account->name)?></option><?php endforeach?></select><button class="btn btn-sm btn-success">Bayar</button></form><?php endif?>
<?php if($permissions['export']&&in_array($period->workflow_status,array('final','posted','paid'),true)):?><a class="btn btn-sm btn-outline-danger" href="<?=base_url('hrpayroll/export_pdf/'.$period->id)?>">PDF</a><a class="btn btn-sm btn-outline-success" href="<?=base_url('hrpayroll/export_excel/'.$period->id.'/general')?>">Excel</a><div class="dropdown"><button class="btn btn-sm btn-outline-primary dropdown-toggle" data-bs-toggle="dropdown">Sumber Bank</button><div class="dropdown-menu"><a class="dropdown-item" href="<?=base_url('hrpayroll/export_excel/'.$period->id.'/bri')?>">BRI / QLola</a><a class="dropdown-item" href="<?=base_url('hrpayroll/export_excel/'.$period->id.'/bca')?>">BCA</a></div></div><?php endif?></div></td></tr><?php endforeach?>
</tbody></table></div></section>
<?php elseif($payroll_section==='salaries'):?>
<section class="card hr-panel"><div class="card-header bg-white"><b>Master Gaji Karyawan</b><small class="d-block text-muted">Estimasi hanya menjumlahkan komponen nominal tetap yang aktif saat ini; nilai final dihitung pada periode payroll.</small></div><div class="card-body border-bottom"><div class="row g-2"><div class="col-md-4"><label class="form-label">Departemen</label><select id="salaryDepartmentFilter" class="form-select"><option value="">Semua departemen</option><?php foreach($departments as$d):?><option><?=html_escape($d->department_name)?></option><?php endforeach?></select></div><div class="col-md-4"><label class="form-label">Posisi</label><select id="salaryPositionFilter" class="form-select"><option value="">Semua posisi</option><?php foreach($positions as$p):?><option><?=html_escape($p->position_name)?></option><?php endforeach?></select></div><div class="col-md-4"><label class="form-label">Status</label><select id="salaryStatusFilter" class="form-select"><option value="">Semua status</option><option>Aktif</option><option>Nonaktif</option></select></div></div></div><div class="table-responsive"><table id="salaryTable" class="table align-middle mb-0"><thead><tr><th>Karyawan</th><th>Departemen</th><th>Posisi</th><th>Status</th><th class="text-end">Pendapatan Tetap</th><th class="text-end">Potongan Tetap</th><th class="text-end">Estimasi Neto</th><th>Efektif</th><th>Aksi</th></tr></thead><tbody><?php foreach($salary_rows as$row):?><tr><td><b><?=html_escape($row->full_name)?></b><small class="d-block text-muted"><?=html_escape($row->employee_code)?></small></td><td><?=html_escape($row->department_name?:'-')?></td><td><?=html_escape($row->position_name?:'-')?></td><td><span class="badge <?=$row->is_active?'bg-success':'bg-secondary'?>"><?=$row->is_active?'Aktif':'Nonaktif'?></span><?php if($row->salary_restricted):?><span class="badge bg-danger">Gaji terbatas</span><?php endif?></td><td class="text-end">Rp <?=number_format($row->fixed_earnings,2,',','.')?></td><td class="text-end">Rp <?=number_format($row->fixed_deductions,2,',','.')?></td><td class="text-end fw-bold">Rp <?=number_format($row->estimated_net,2,',','.')?></td><td><?=html_escape($row->effective_from?:'-')?></td><td><?php if($permissions['update']):?><button class="btn btn-sm btn-outline-primary salary-manage" data-id="<?=$row->id?>"><i class="bi bi-sliders"></i> Atur Gaji</button><?php else:?><button class="btn btn-sm btn-outline-primary salary-manage" data-id="<?=$row->id?>">Detail</button><?php endif?></td></tr><?php endforeach?></tbody></table></div></section>
<?php elseif($payroll_section==='settings'):?>
<ul class="nav nav-pills payroll-setting-tabs mb-3" role="tablist"><li class="nav-item"><button class="nav-link active" data-bs-toggle="pill" data-bs-target="#componentsTab">Master Komponen</button></li><li class="nav-item"><button class="nav-link" data-bs-toggle="pill" data-bs-target="#positionsTab">Template Posisi</button></li><li class="nav-item"><button class="nav-link" data-bs-toggle="pill" data-bs-target="#attendanceRulesTab">Aturan Kehadiran</button></li><li class="nav-item"><button class="nav-link" data-bs-toggle="pill" data-bs-target="#generalTab">Pengaturan Umum</button></li></ul>
<div class="tab-content">
<div class="tab-pane fade show active" id="componentsTab"><section class="card hr-panel"><div class="card-header bg-white"><b>Master Komponen</b><small class="d-block text-muted">Komponen tidak otomatis berlaku untuk semua karyawan.</small></div><?php if($permissions['update']):?><div class="card-body border-bottom"><form method="post" action="<?=base_url('hrpayroll/component')?>" class="row g-2"><?=$csrf?><div class="col-md-2"><label class="form-label">Kode</label><input class="form-control" name="code" required></div><div class="col-md-3"><label class="form-label">Nama</label><input class="form-control" name="name" required></div><div class="col-md-2"><label class="form-label">Jenis</label><select class="form-select" name="component_type"><option value="earning">Pendapatan</option><option value="deduction">Potongan</option><option value="employer_cost">Beban perusahaan</option></select></div><div class="col-md-2"><label class="form-label">Kategori</label><select class="form-select" name="category"><option value="basic">Gaji Pokok</option><option value="allowance">Tunjangan</option><option value="overtime">Lembur</option><option value="unpaid_leave">Cuti tak dibayar</option><option value="loan">Pinjaman</option><option value="bpjs">BPJS</option><option value="tax">Pajak</option><option value="thr">THR</option><option value="bonus">Bonus</option><option value="other">Lainnya</option></select></div><div class="col-md-3"><label class="form-label">Metode</label><select class="form-select" name="calculation_method"><?php foreach($methodLabels as$key=>$label):?><option value="<?=$key?>"><?=html_escape($label)?></option><?php endforeach?></select></div><div class="col-md-2"><label class="form-label">Nilai Default</label><input type="number" min="0" step="0.01" class="form-control" name="default_value" value="0"></div><div class="col-md-3"><label class="form-label">Formula</label><input class="form-control" name="formula_expression" placeholder="present_days * 25000"></div><div class="col-md-2"><label class="form-label">Akun Beban</label><select class="form-select searchable" name="expense_account_id"><option value="">Mapping umum</option><?php foreach($accounts as$a):?><option value="<?=$a->id?>"><?=html_escape($a->kode_akun.' - '.$a->nama_akun)?></option><?php endforeach?></select></div><div class="col-md-2"><label class="form-label">Akun Kewajiban</label><select class="form-select searchable" name="liability_account_id"><option value="">Mapping umum</option><?php foreach($accounts as$a):?><option value="<?=$a->id?>"><?=html_escape($a->kode_akun.' - '.$a->nama_akun)?></option><?php endforeach?></select></div><div class="col-md-2"><label class="form-label">Mulai Berlaku</label><input type="date" class="form-control" name="effective_from" value="<?=date('Y-m-d')?>"></div><div class="col-md-1 d-flex align-items-end"><label class="form-check mb-2"><input class="form-check-input" type="checkbox" name="is_active" value="1" checked> Aktif</label></div><div class="col-md-2 d-flex align-items-end justify-content-end"><button class="btn btn-primary">Simpan Komponen</button></div></form></div><?php endif?><div class="table-responsive"><table class="table align-middle mb-0"><thead><tr><th>Kode</th><th>Komponen</th><th>Jenis</th><th>Metode</th><th class="text-end">Default</th><th>Mapping</th><th>Status</th></tr></thead><tbody><?php foreach($components as$c):?><tr><td><b><?=html_escape($c->code)?></b></td><td><?=html_escape($c->name)?></td><td><?=html_escape($c->component_type)?></td><td><?=html_escape($methodLabels[$c->calculation_method]??$c->calculation_method)?></td><td class="text-end">Rp <?=number_format($c->default_value??$c->fixed_amount,2,',','.')?></td><td><?=html_escape(($c->expense_account_id?:'mapping umum').' / '.($c->liability_account_id?:'mapping umum'))?></td><td><span class="badge <?=$c->is_active?'bg-success':'bg-secondary'?>"><?=$c->is_active?'Aktif':'Nonaktif'?></span></td></tr><?php endforeach?></tbody></table></div></section></div>
<div class="tab-pane fade" id="positionsTab"><section class="card hr-panel"><div class="card-header bg-white"><b>Template Posisi</b><small class="d-block text-muted">Override karyawan tetap memiliki prioritas di atas template posisi.</small></div><?php if($permissions['update']):?><div class="card-body border-bottom"><form method="post" action="<?=base_url('hrpayroll/save_position_line')?>" class="row g-2"><?=$csrf?><div class="col-md-3"><label class="form-label">Posisi</label><select class="form-select searchable" name="position_id" required><option value="">Pilih posisi</option><?php foreach($positions as$p):?><option value="<?=$p->id?>"><?=html_escape($p->position_name)?></option><?php endforeach?></select></div><div class="col-md-3"><label class="form-label">Komponen</label><select class="form-select searchable" name="component_id" required><option value="">Pilih komponen</option><?php foreach($components as$c):?><option value="<?=$c->id?>"><?=html_escape($c->code.' - '.$c->name)?></option><?php endforeach?></select></div><div class="col-md-2"><label class="form-label">Metode</label><select class="form-select" name="calculation_method"><?php foreach($methodLabels as$key=>$label):?><option value="<?=$key?>"><?=html_escape($label)?></option><?php endforeach?></select></div><div class="col-md-2"><label class="form-label">Nilai/Tarif</label><input type="number" min="0" step="0.01" class="form-control" name="amount_override" value="0"></div><div class="col-md-2"><label class="form-label">Mulai Berlaku</label><input type="date" class="form-control" name="effective_from" value="<?=date('Y-m-d')?>" required></div><div class="col-md-10"><input class="form-control" name="notes" placeholder="Keterangan template, opsional"></div><div class="col-md-2"><button class="btn btn-primary w-100">Tambah Template</button></div></form></div><?php endif?><div class="table-responsive"><table class="table align-middle mb-0"><thead><tr><th>Posisi</th><th>Komponen</th><th>Metode</th><th class="text-end">Nilai</th><th>Efektif</th><th>Status</th></tr></thead><tbody><?php if(!$position_assignments):?><tr><td colspan="6" class="text-center text-muted py-4">Belum ada template posisi.</td></tr><?php endif;foreach($position_assignments as$x):?><tr><td><?=html_escape($x->position_name)?></td><td><?=html_escape($x->component_code.' - '.$x->component_name)?></td><td><?=html_escape($methodLabels[$x->calculation_method]??$x->calculation_method)?></td><td class="text-end">Rp <?=number_format($x->amount_override,2,',','.')?></td><td><?=html_escape($x->effective_from)?><?=!empty($x->effective_to)?' s.d. '.html_escape($x->effective_to):''?></td><td><span class="badge <?=$x->is_active?'bg-success':'bg-secondary'?>"><?=$x->is_active?'Aktif':'Nonaktif'?></span></td></tr><?php endforeach?></tbody></table></div></section></div>
<div class="tab-pane fade" id="attendanceRulesTab"><section class="card hr-panel"><div class="card-header bg-white"><b>Aturan Kehadiran</b><small class="d-block text-muted">Terlambat secara default tidak dipotong. Cuti tahunan, sakit, izin, dan hari libur tidak dianggap alpa.</small></div><?php if($permissions['update']):?><div class="card-body border-bottom"><form method="post" action="<?=base_url('hrpayroll/save_attendance_rule')?>" class="row g-2"><?=$csrf?><div class="col-md-3"><label class="form-label">Nama Aturan</label><input class="form-control" name="rule_name" required></div><div class="col-md-2"><label class="form-label">Kondisi</label><select class="form-select" name="condition_type"><option value="alpha">Alpa</option><option value="late">Terlambat</option><option value="early_leave">Pulang cepat</option><option value="unpaid">Cuti tidak dibayar</option><option value="missing_checkin">Tidak check-in</option><option value="missing_checkout">Tidak check-out</option></select></div><div class="col-md-2"><label class="form-label">Metode Potongan</label><select class="form-select" name="calculation_method"><option value="none">Tidak dipotong</option><option value="fixed">Nominal tetap</option><option value="per_minute">Per menit</option><option value="per_hour">Per jam</option><option value="per_day">Per hari</option><option value="percentage_daily">Persentase tarif harian</option><option value="formula">Formula</option></select></div><div class="col-md-2"><label class="form-label">Komponen Potongan</label><select class="form-select searchable" name="component_id"><option value="">Tidak ada</option><?php foreach($components as$c):if($c->component_type!=='deduction')continue;?><option value="<?=$c->id?>"><?=html_escape($c->name)?></option><?php endforeach?></select></div><div class="col-md-1"><label class="form-label">Toleransi</label><input type="number" min="0" class="form-control" name="tolerance_minutes" value="0"></div><div class="col-md-2"><label class="form-label">Tarif</label><input type="number" min="0" step="0.01" class="form-control" name="rate_value" value="0"></div><div class="col-md-2"><label class="form-label">Maksimum</label><input type="number" min="0" step="0.01" class="form-control" name="maximum_amount"></div><div class="col-md-3"><label class="form-label">Mulai Berlaku</label><input type="date" class="form-control" name="effective_from" value="<?=date('Y-m-d')?>" required></div><div class="col-md-4"><label class="form-label">Formula / Keterangan</label><input class="form-control" name="formula_expression" placeholder="condition_quantity * rule_rate"></div><div class="col-md-1 d-flex align-items-end"><label class="form-check mb-2"><input class="form-check-input" type="checkbox" name="is_active" value="1" checked> Aktif</label></div><div class="col-md-2 d-flex align-items-end"><button class="btn btn-primary w-100">Simpan Versi</button></div></form></div><?php endif?><div class="table-responsive"><table class="table align-middle mb-0"><thead><tr><th>Aturan</th><th>Kondisi</th><th>Metode</th><th>Toleransi</th><th class="text-end">Tarif</th><th>Komponen</th><th>Efektif</th></tr></thead><tbody><?php foreach($attendance_rules as$r):?><tr><td><?=html_escape($r->rule_name)?></td><td><?=html_escape($r->condition_type)?></td><td><?=html_escape($r->calculation_method)?></td><td><?=number_format($r->tolerance_minutes)?> menit</td><td class="text-end"><?=number_format($r->rate_value,2,',','.')?></td><td><?=html_escape($r->component_name?:'Tidak memotong')?></td><td><?=html_escape($r->effective_from)?></td></tr><?php endforeach?></tbody></table></div></section></div>
<div class="tab-pane fade" id="generalTab"><section class="card hr-panel"><div class="card-header bg-white"><b>Pengaturan Umum Payroll</b><small class="d-block text-muted">Berlaku untuk perusahaan aktif dan tidak mengubah histori payroll lama.</small></div><div class="card-body"><div class="row g-3 mb-4"><div class="col-md-4"><div class="payroll-info-box"><b>Sumber Kehadiran</b><span>Rentang tanggal pada periode payroll</span></div></div><div class="col-md-4"><div class="payroll-info-box"><b>Prioritas Komponen</b><span>Override karyawan → template posisi → nilai default</span></div></div><div class="col-md-4"><div class="payroll-info-box"><b>Pengamanan</b><span>Snapshot, separation of duties, idempotency jurnal</span></div></div></div><?php if($permissions['update']):?><form method="post" action="<?=base_url('hrpayroll/save_settings')?>" class="row g-3"><?=$csrf?><div class="col-md-4"><label class="form-label">Pemisahan Tugas</label><label class="form-check form-switch"><input class="form-check-input" type="checkbox" name="separation_of_duties" value="1" <?=!$payroll_settings||$payroll_settings->separation_of_duties?'checked':''?>> Reviewer, approver, dan finalizer harus berbeda</label></div><div class="col-md-3"><label class="form-label">Tanggal Bayar Default</label><input type="number" min="1" max="31" class="form-control" name="default_payment_day" value="<?=html_escape($payroll_settings->default_payment_day??'')?>" placeholder="1–31"></div><div class="col-md-5"><label class="form-label">Catatan Export Bank</label><div class="input-group"><input class="form-control" name="bank_export_note" value="<?=html_escape($payroll_settings->bank_export_note??'')?>" placeholder="Instruksi internal sebelum upload"><button class="btn btn-primary">Simpan</button></div></div></form><?php endif?><p class="text-muted small mt-3 mb-0">Akun jurnal utama dikelola melalui System Account Mapping. Akun khusus dapat dipilih pada masing-masing komponen.</p></div></section></div>
</div>
<?php else:?>
<?php if($permissions['create']):?><section class="card hr-panel mb-3"><div class="card-header bg-white"><b>Tambah Penyesuaian Satu Periode</b><small class="d-block text-muted">Tidak membuat jurnal saat disimpan dan tidak berulang ke periode berikutnya.</small></div><div class="card-body"><form method="post" action="<?=base_url('hrpayroll/save_adjustment')?>" class="row g-2"><?=$csrf?><div class="col-md-3"><label class="form-label">Periode Draft</label><select class="form-select searchable" name="payroll_period_id" required><option value="">Pilih periode</option><?php foreach($periods as$p):if($p->workflow_status!=='draft')continue;?><option value="<?=$p->id?>"><?=html_escape($p->period_name)?></option><?php endforeach?></select></div><div class="col-md-3"><label class="form-label">Karyawan</label><select class="form-select searchable" 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="col-md-2"><label class="form-label">Jenis</label><select class="form-select" name="adjustment_type"><option value="earning">Pendapatan</option><option value="deduction">Potongan</option></select></div><div class="col-md-2"><label class="form-label">Komponen</label><select class="form-select searchable" name="component_id" required><option value="">Pilih komponen</option><?php foreach($components as$c):if($c->component_type==='employer_cost')continue;?><option value="<?=$c->id?>"><?=html_escape($c->code.' - '.$c->name)?></option><?php endforeach?></select></div><div class="col-md-2"><label class="form-label">Nominal</label><input type="number" min="0.01" step="0.01" class="form-control" name="amount" required></div><div class="col-md-10"><label class="form-label">Keterangan</label><input class="form-control" name="notes" placeholder="Alasan bonus, rapel, koreksi, atau potongan" required></div><div class="col-md-2 d-flex align-items-end"><button class="btn btn-primary w-100">Simpan</button></div></form></div></section><?php endif?>
<section class="card hr-panel"><div class="card-header bg-white"><b>Daftar Penyesuaian</b></div><div class="table-responsive"><table class="table align-middle mb-0"><thead><tr><th>Periode</th><th>Karyawan</th><th>Komponen</th><th>Jenis</th><th class="text-end">Nominal</th><th>Keterangan</th><th>Status</th><th>Aksi</th></tr></thead><tbody><?php if(!$adjustments):?><tr><td colspan="8" class="text-center text-muted py-5">Belum ada penyesuaian periode.</td></tr><?php endif;foreach($adjustments as$a):?><tr><td><?=html_escape($a->period_name)?></td><td><b><?=html_escape($a->full_name)?></b><small class="d-block text-muted"><?=html_escape($a->employee_code)?></small></td><td><?=html_escape($a->component_name)?></td><td><span class="badge <?=$a->adjustment_type==='earning'?'bg-success':'bg-warning text-dark'?>"><?=html_escape($a->adjustment_type==='earning'?'Pendapatan':'Potongan')?></span></td><td class="text-end">Rp <?=number_format($a->amount,2,',','.')?></td><td><?=html_escape($a->notes)?></td><td><?=html_escape($statusLabels[$a->workflow_status]??$a->workflow_status)?></td><td><?php if($permissions['update']&&$a->workflow_status==='draft'):?><form method="post" action="<?=base_url('hrpayroll/deactivate_adjustment/'.$a->id)?>" class="js-confirm"><?=$csrf?><button class="btn btn-sm btn-outline-danger">Nonaktifkan</button></form><?php else:?>-<?php endif?></td></tr><?php endforeach?></tbody></table></div></section>
<?php endif?>
<div class="modal fade" id="payrollDetailModal" tabindex="-1"><div class="modal-dialog modal-xl modal-dialog-scrollable"><div class="modal-content"><div class="modal-header"><div><small class="text-muted">RINCIAN PERIODE</small><h5 class="modal-title" id="payrollDetailTitle">Payroll</h5></div><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><div class="table-responsive"><table class="table align-middle"><thead><tr><th>Karyawan</th><th class="text-end">Hadir</th><th class="text-end">Alpa</th><th class="text-end">Lembur</th><th class="text-end">Bruto</th><th class="text-end">Potongan</th><th class="text-end">Neto</th><th>Slip</th></tr></thead><tbody id="payrollDetailRows"></tbody></table></div></div></div></div></div>
<div class="modal fade" id="salaryModal" tabindex="-1" data-bs-backdrop="static"><div class="modal-dialog modal-xl modal-dialog-scrollable"><div class="modal-content"><div class="modal-header"><div><small class="text-muted">STRUKTUR GAJI EFEKTIF</small><h5 class="modal-title" id="salaryModalTitle">Karyawan</h5><small id="salaryModalMeta" class="text-muted"></small></div><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><div class="table-responsive mb-4"><table class="table align-middle"><thead><tr><th>Komponen</th><th>Jenis</th><th>Metode</th><th class="text-end">Nilai/Tarif</th><th>Efektif</th><th>Status</th><th>Aksi</th></tr></thead><tbody id="salaryLineRows"></tbody></table></div><?php if($permissions['update']):?><form id="salaryLineForm" data-ajax="true" class="row g-2"><?=$csrf?><input type="hidden" name="employee_id" id="salaryEmployeeId"><div class="col-md-3"><label class="form-label">Komponen</label><select class="form-select" name="component_id" required><option value="">Pilih komponen</option><?php foreach($components as$c):?><option value="<?=$c->id?>"><?=html_escape($c->code.' - '.$c->name)?></option><?php endforeach?></select></div><div class="col-md-2"><label class="form-label">Metode</label><select class="form-select" name="calculation_method"><?php foreach($methodLabels as$key=>$label):?><option value="<?=$key?>"><?=html_escape($label)?></option><?php endforeach?></select></div><div class="col-md-2"><label class="form-label">Nilai/Tarif</label><input type="number" min="0" step="0.01" class="form-control" name="amount_override" value="0" required></div><div class="col-md-2"><label class="form-label">Mulai Berlaku</label><input type="date" class="form-control" name="effective_from" value="<?=date('Y-m-d')?>" required></div><div class="col-md-2"><label class="form-label">Selesai (opsional)</label><input type="date" class="form-control" name="effective_to"></div><div class="col-md-1 d-flex align-items-end"><button class="btn btn-primary w-100">Tambah</button></div><div class="col-md-6"><input class="form-control" name="formula_expression" placeholder="Formula bila metode Formula"></div><div class="col-md-6"><input class="form-control" name="notes" placeholder="Keterangan perubahan"></div></form><?php endif?></div></div></div></div>
<script>
$(function(){
const esc=v=>$('<div>').text(v??'').html(),money=v=>'Rp '+Number(v||0).toLocaleString('id-ID',{minimumFractionDigits:2,maximumFractionDigits:2});
$('.searchable').select2({width:'100%'});
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'))});
});
</script>
@@ -1,5 +1,5 @@
<div class="container-fluid py-3">
<div class="mb-3"><h4 class="mb-1">Master Departemen & Posisi</h4><p class="text-muted mb-0">Satu sumber data untuk karyawan, Purchase Request, payroll, jadwal, dan modul lainnya.</p></div>
<?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>
@@ -0,0 +1,4 @@
<!doctype html><html><head><meta charset="utf-8"><style>@page{margin:24px 28px}body{font:10px DejaVu Sans;color:#1f2937}h1{font-size:18px;margin:0}p{margin:3px 0}.head{display:table;width:100%;border-bottom:2px solid #ef7d00;padding-bottom:10px;margin-bottom:14px}.head>div{display:table-cell;vertical-align:top}.right{text-align:right}table{width:100%;border-collapse:collapse}th{background:#17284a;color:#fff;text-align:left}th,td{padding:7px;border:1px solid #dce3ed}.num{text-align:right}.total{font-weight:bold;background:#fff4e8}.foot{margin-top:14px;color:#64748b}</style></head><body>
<div class="head"><div><h1><?=html_escape($company->name??$company->company_name??'Perusahaan')?></h1><p>Register Payroll</p></div><div class="right"><b><?=html_escape($period->period_name)?></b><p>Kehadiran: <?=html_escape($period->attendance_start_date?:$period->start_date)?> s.d. <?=html_escape($period->attendance_end_date?:$period->end_date)?></p><p>Tanggal bayar: <?=html_escape($period->payment_date?:'-')?> · Status: <?=strtoupper(html_escape($period->workflow_status))?></p></div></div>
<table><thead><tr><th>No</th><th>Kode</th><th>Karyawan</th><th>Bank / Rekening</th><th class="num">Gaji Pokok</th><th class="num">Bruto</th><th class="num">Potongan</th><th class="num">Neto</th></tr></thead><tbody><?php $basic=$gross=$deduction=$net=0;foreach($rows as$i=>$r):$basic+=(float)$r->basic_salary;$gross+=(float)$r->gross_salary;$deduction+=(float)$r->total_deductions;$net+=(float)$r->total_salary;?><tr><td><?=$i+1?></td><td><?=html_escape($r->employee_code)?></td><td><?=html_escape($r->full_name)?></td><td><?=html_escape(trim(($r->bank_name?:'-').' / '.($r->bank_account_number?:'-'),' /'))?></td><td class="num"><?=number_format($r->basic_salary,2,',','.')?></td><td class="num"><?=number_format($r->gross_salary,2,',','.')?></td><td class="num"><?=number_format($r->total_deductions,2,',','.')?></td><td class="num"><?=number_format($r->total_salary,2,',','.')?></td></tr><?php endforeach?><tr class="total"><td colspan="4">TOTAL</td><td class="num"><?=number_format($basic,2,',','.')?></td><td class="num"><?=number_format($gross,2,',','.')?></td><td class="num"><?=number_format($deduction,2,',','.')?></td><td class="num"><?=number_format($net,2,',','.')?></td></tr></tbody></table>
<p class="foot">Dokumen payroll bersifat rahasia. Dibuat pada <?=date('d/m/Y H:i')?>.</p></body></html>
+1 -1
View File
@@ -1 +1 @@
<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Slip Gaji</title><style>body{font:14px Arial;max-width:760px;margin:30px auto}table{width:100%;border-collapse:collapse}td,th{padding:8px;border-bottom:1px solid #ddd}.r{text-align:right}@media print{button{display:none}}</style></head><body><button onclick="print()">Cetak</button><h2>SLIP GAJI</h2><p><b><?=html_escape($payroll->full_name)?></b> — <?=html_escape($payroll->employee_code)?><br>Periode <?=html_escape($payroll->period_name)?></p><table><tr><th>Komponen</th><th>Jenis</th><th class="r">Nilai</th></tr><?php foreach($lines as$l):?><tr><td><?=html_escape($l->component_name)?></td><td><?=$l->component_type?></td><td class="r"><?=number_format($l->amount,0,',','.')?></td></tr><?php endforeach?><tr><th colspan="2">Gaji Neto</th><th class="r"><?=number_format($payroll->total_salary,0,',','.')?></th></tr></table><small>Dokumen rahasia. Status payroll: <?=$payroll->workflow_status?></small></body></html>
<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Slip Gaji</title><style>body{font:14px Arial;color:#1f2937;max-width:760px;margin:30px auto}.head{display:flex;justify-content:space-between;border-bottom:3px solid #ed7900;padding-bottom:12px}table{width:100%;border-collapse:collapse;margin-top:20px}td,th{padding:9px;border-bottom:1px solid #dce3ed}th{background:#f1f5f9;text-align:left}.r{text-align:right}.net{background:#17284a;color:#fff}.muted{color:#64748b;font-size:12px}@media print{button{display:none}}</style></head><body><button onclick="print()">Cetak</button><div class="head"><div><h2>SLIP GAJI</h2><b><?=html_escape($payroll->full_name)?></b> — <?=html_escape($payroll->employee_code)?></div><div class="r"><b><?=html_escape($payroll->period_name)?></b><div class="muted"><?=html_escape($payroll->attendance_start_date?:$payroll->period_start_date)?> s.d. <?=html_escape($payroll->attendance_end_date?:$payroll->period_end_date)?></div><div class="muted">Tanggal bayar: <?=html_escape($payroll->payment_date?:'-')?></div></div></div><table><tr><th>Komponen</th><th>Jenis</th><th class="r">Nilai</th></tr><?php foreach($lines as$line):?><tr><td><?=html_escape($line->component_name)?></td><td><?=html_escape($line->component_type)?></td><td class="r"><?=number_format($line->amount,2,',','.')?></td></tr><?php endforeach?><tr class="net"><th colspan="2">Gaji Neto</th><th class="r">Rp <?=number_format($payroll->total_salary,2,',','.')?></th></tr></table><p class="muted">Dokumen rahasia. Status payroll: <?=html_escape(strtoupper($payroll->workflow_status))?></p></body></html>
@@ -40,11 +40,12 @@
$(function(){
const permissions=<?=json_encode($inventory_permissions)?>,csrf=()=>({[APP_CSRF.name]:APP_CSRF.value}),esc=v=>$('<div>').text(v==null?'-':v).html(),fmt=v=>Number(v||0).toLocaleString('id-ID',{maximumFractionDigits:4}),money=v=>'Rp '+Number(v||0).toLocaleString('id-ID',{minimumFractionDigits:2,maximumFractionDigits:2});
const labels={opening:'Saldo Awal',in:'Masuk Manual',out:'Keluar Manual',transfer:'Transfer Gudang',adjustment:'Penyesuaian',opname:'Stock Opname',damaged:'Barang Rusak',lost:'Barang Hilang',draft:'Draft',submitted:'Diajukan',approved:'Disetujui',posted:'Diposting',rejected:'Ditolak',reversed:'Dibalik'};
function api(url,data){return $.ajax({url:url,type:'POST',data:Object.assign({},data||{},csrf()),dataType:'json'})}
function fail(xhr){AppAlert.toast(xhr.responseJSON&&xhr.responseJSON.message||'Proses tidak berhasil.','error')}
function api(url,data){return $.ajax({url:url,type:'POST',data:Object.assign({},data||{},csrf()),dataType:'json',timeout:30000})}
function hideGlobalLoading(){let loading=document.getElementById('globalLoading');if(loading)loading.hidden=true}
function fail(xhr){hideGlobalLoading();let message=xhr.responseJSON&&xhr.responseJSON.message;if(!message&&xhr.statusText==='timeout')message='Waktu proses habis. Silakan coba kembali.';AppAlert.toast(message||'Proses tidak berhasil.','error')}
const table=$('#documentTable').DataTable({processing:true,serverSide:true,responsive:true,searchDelay:350,pageLength:10,order:[],ajax:{url:'<?=base_url('inventoryprofessional/document_data')?>',data:d=>{d.type=$('#filterType').val();d.status=$('#filterStatus').val()}},columns:[{data:'document_no',render:(v,t,r)=>'<button class="btn btn-link p-0 fw-semibold btn-detail" data-id="'+r.id+'">'+esc(v)+'</button>'},{data:'document_date',render:v=>v?v.split('-').reverse().join('/'):'-'},{data:'document_type',render:v=>labels[v]||v},{data:null,render:r=>esc((r.from_warehouse||'-')+' → '+(r.to_warehouse||'-'))},{data:'line_count',className:'text-center'},{data:'status',render:v=>'<span class="status-chip status-'+v+'">'+esc(labels[v]||v)+'</span>'},{data:'creator_name',defaultContent:'-'},{data:null,orderable:false,searchable:false,render:r=>actions(r)}],language:{search:'Cari:',lengthMenu:'Tampilkan _MENU_',info:'_START_–_END_ dari _TOTAL_',processing:'Memuat data...',paginate:{previous:'Sebelumnya',next:'Berikutnya'}}});
const focusDocument=new URLSearchParams(window.location.search).get('focus');if(focusDocument)table.one('draw.dt',function(){setTimeout(function(){$('<button type="button" class="d-none btn-detail" data-id="'+String(focusDocument).replace(/[^0-9]/g,'')+'"></button>').appendTo(document.body).trigger('click').remove()},100)});
function actions(r){let a='<div class="d-flex flex-wrap gap-1"><button class="btn btn-sm btn-outline-primary btn-detail" data-id="'+r.id+'"><i class="bi bi-eye"></i></button>';if(permissions.export)a+='<button class="btn btn-sm btn-outline-danger btn-pdf" data-id="'+r.id+'"><i class="bi bi-file-earmark-pdf"></i></button>';if(r.status==='draft'&&permissions.create)a+='<button class="btn btn-sm btn-primary btn-state" data-action="submit" data-id="'+r.id+'">Ajukan</button>';if(r.status==='submitted'&&permissions.approve)a+='<button class="btn btn-sm btn-success btn-state" data-action="approve" data-id="'+r.id+'">Setujui</button><button class="btn btn-sm btn-outline-danger btn-reject" data-id="'+r.id+'">Tolak</button>';if(r.status==='approved'&&permissions.post)a+='<button class="btn btn-sm btn-success btn-state" data-action="post" data-id="'+r.id+'">Posting</button>';if(r.status==='posted'&&permissions.post)a+='<button class="btn btn-sm btn-outline-danger btn-reverse" data-id="'+r.id+'">Reversal</button>';return a+'</div>'}
function actions(r){let a='<div class="d-flex flex-wrap gap-1"><button type="button" class="btn btn-sm btn-outline-primary btn-detail" data-id="'+r.id+'"><i class="bi bi-eye"></i></button>';if(permissions.export)a+='<button type="button" class="btn btn-sm btn-outline-danger btn-pdf" data-id="'+r.id+'"><i class="bi bi-file-earmark-pdf"></i></button>';if(r.status==='draft'&&permissions.create)a+='<button type="button" class="btn btn-sm btn-primary btn-state" data-action="submit" data-id="'+r.id+'">Ajukan</button>';if(r.status==='submitted'&&permissions.approve)a+='<button type="button" class="btn btn-sm btn-success btn-state" data-action="approve" data-id="'+r.id+'">Setujui</button><button type="button" class="btn btn-sm btn-outline-danger btn-reject" data-id="'+r.id+'">Tolak</button>';if(r.status==='approved'&&permissions.post)a+='<button type="button" class="btn btn-sm btn-success btn-state" data-action="post" data-id="'+r.id+'">Posting</button>';if(r.status==='posted'&&permissions.post)a+='<button type="button" class="btn btn-sm btn-outline-danger btn-reverse" data-id="'+r.id+'">Reversal</button>';return a+'</div>'}
$('#filterType,#filterStatus').on('change',()=>table.ajax.reload());$('#refreshDocuments').on('click',()=>table.ajax.reload(null,false));
$('[data-inventory-tab]').on('click',function(){let tab=$(this).data('inventory-tab');$('[data-inventory-tab]').removeClass('active');$(this).addClass('active');if(tab==='reservation'){$('#documentSection').addClass('d-none');$('#reservationSection').removeClass('d-none');loadReservations();return}$('#reservationSection').addClass('d-none');$('#documentSection').removeClass('d-none');$('#filterType').val(tab);table.ajax.reload()});
function addLine(){let id=Date.now()+Math.random().toString(16).slice(2),tr=$('<tr class="document-line"><td><select class="form-select line-item"></select></td><td><select class="form-select line-barcode"><option value="">Tanpa barcode</option></select></td><td><select class="form-select line-batch"><option value="">Tanpa batch</option></select></td><td class="direction-column"><select class="form-select line-direction"><option value="in">Tambah</option><option value="out">Kurangi</option></select></td><td><input class="form-control line-qty" inputmode="decimal" placeholder="0" required></td><td><input class="form-control line-cost format-rupiah" inputmode="decimal" placeholder="0"></td><td><input class="form-control line-reason" placeholder="Catatan baris"></td><td><button type="button" class="btn btn-sm btn-outline-danger remove-line"><i class="bi bi-x"></i></button></td></tr>');tr.attr('data-key',id);$('#documentLines').append(tr);initLine(tr);toggleType()}
@@ -53,7 +54,7 @@ $(function(){
$('#newDocument').on('click',function(){$('#documentForm')[0].reset();selectedFiles=[];renderAttachments();$('#documentDate').val('<?=date('Y-m-d')?>');$('#idempotencyKey').val('WEB-INV-'+Date.now()+'-'+Math.random().toString(36).slice(2));$('#documentLines').empty();addLine();toggleType();bootstrap.Modal.getOrCreateInstance('#documentModal').show()});$('#addLine').on('click',addLine);$('#documentType').on('change',toggleType);$(document).on('click','.remove-line',function(){if($('.document-line').length>1)$(this).closest('tr').remove()});
let selectedFiles=[];$(document).on('click','#chooseAttachments',()=>$('#documentAttachments').click());$('#documentAttachments').on('change',function(){selectedFiles=selectedFiles.concat(Array.from(this.files));this.value='';renderAttachments()});function renderAttachments(){let h=selectedFiles.map((f,i)=>{let preview=f.type.indexOf('image/')===0?'<img src="'+URL.createObjectURL(f)+'" alt="">':'<i class="bi bi-file-earmark-pdf attachment-file-icon"></i>';return'<div class="attachment-tile">'+preview+'<button type="button" class="remove-attachment" data-index="'+i+'" aria-label="Hapus"><i class="bi bi-x"></i></button><small>'+esc(f.name)+'</small></div>'}).join('');$('#attachmentNames').html(h+'<button type="button" class="attachment-add" id="chooseAttachments"><i class="bi bi-plus-lg"></i><span>Tambah</span></button>')}$(document).on('click','.remove-attachment',function(){selectedFiles.splice(Number($(this).data('index')),1);renderAttachments()});
$('#documentForm').on('submit',function(e){e.preventDefault();let lines=[];$('.document-line').each(function(){let r=$(this);lines.push({item_id:r.find('.line-item').val(),barcode_id:r.find('.line-barcode').val(),batch_id:r.find('.line-batch').val(),movement_direction:r.find('.line-direction').val(),qty:r.find('.line-qty').val().replace(/\./g,'').replace(',','.'),unit_cost:r.find('.line-cost').val().replace(/\./g,'').replace(',','.'),reason:r.find('.line-reason').val()})});if(lines.some(x=>!x.item_id||!x.qty))return AppAlert.toast('Barang dan qty setiap baris wajib diisi.','warning');let btn=$('#saveDocument').prop('disabled',true);api('<?=base_url('inventoryprofessional/create')?>',{type:$('#documentType').val(),date:$('#documentDate').val(),from_warehouse_id:$('#fromWarehouse').val(),to_warehouse_id:$('#toWarehouse').val(),source_type:$('#sourceType').val(),source_id:$('#sourceId').val(),reason:$('#documentReason').val(),idempotency_key:$('#idempotencyKey').val(),lines:JSON.stringify(lines)}).done(async function(r){let files=selectedFiles.slice();try{for(let f of files){let fd=new FormData();fd.append(APP_CSRF.name,APP_CSRF.value);fd.append('entity_type','stock_document');fd.append('entity_id',r.document_id);fd.append('attachment',f);await $.ajax({url:'<?=base_url('accountingattachments/upload')?>',type:'POST',data:fd,processData:false,contentType:false})}bootstrap.Modal.getInstance(document.getElementById('documentModal')).hide();table.ajax.reload(null,false);AppAlert.toast(r.message,'success')}catch(x){AppAlert.toast('Draft tersimpan, tetapi ada lampiran yang gagal diunggah. Buka detail untuk memeriksa.','warning')}}).fail(fail).always(()=>btn.prop('disabled',false))});
$(document).on('click','.btn-state',function(){let b=$(this),action=b.data('action'),word=action==='submit'?'mengajukan':action==='approve'?'menyetujui':'memposting';AppAlert.confirm({title:'Konfirmasi '+word+' dokumen',text:'Pastikan detail barang dan gudang sudah benar.'}).then(x=>{if(!x.isConfirmed)return;b.prop('disabled',true);api('<?=base_url('inventoryprofessional')?>/'+action+'/'+b.data('id')).done(r=>{table.ajax.reload(null,false);AppAlert.toast(r.message,'success')}).fail(fail).always(()=>b.prop('disabled',false))})});
$(document).on('click','.btn-state',function(){let b=$(this),action=b.data('action'),word=action==='submit'?'mengajukan':action==='approve'?'menyetujui':'memposting',original=b.html();hideGlobalLoading();AppAlert.confirm({title:'Konfirmasi '+word+' dokumen',text:'Pastikan detail barang dan gudang sudah benar.'}).then(x=>{if(!x.isConfirmed){hideGlobalLoading();return}b.prop('disabled',true).html('<span class="spinner-border spinner-border-sm me-1" aria-hidden="true"></span> Memproses');api('<?=base_url('inventoryprofessional')?>/'+action+'/'+b.data('id')).done(r=>{table.ajax.reload(null,false);AppAlert.toast(r.message,'success')}).fail(fail).always(()=>{hideGlobalLoading();b.prop('disabled',false).html(original)})}).catch(()=>{hideGlobalLoading();b.prop('disabled',false).html(original)})});
$(document).on('click','.btn-reject',function(){let id=$(this).data('id');AppAlert.prompt({title:'Tolak dokumen stok',input:'textarea',inputLabel:'Alasan penolakan',inputValidator:v=>!v.trim()?'Alasan wajib diisi.':undefined,confirmButtonText:'Tolak Dokumen'}).then(x=>{if(x.isConfirmed)api('<?=base_url('inventoryprofessional/reject')?>/'+id,{reason:x.value}).done(r=>{table.ajax.reload(null,false);AppAlert.toast(r.message,'success')}).fail(fail)})});
$(document).on('click','.btn-reverse',function(){let id=$(this).data('id');AppAlert.prompt({title:'Reversal dokumen stok',html:'<label class="form-label d-block text-start">Tanggal reversal</label><input id="reverseDate" type="date" class="form-control mb-3" value="<?=date('Y-m-d')?>"><label class="form-label d-block text-start">Alasan</label><textarea id="reverseReason" class="form-control"></textarea>',preConfirm:()=>{let d=$('#reverseDate').val(),r=$('#reverseReason').val().trim();if(!d||!r){Swal.showValidationMessage('Tanggal dan alasan wajib diisi.');return false}return{date:d,reason:r}},confirmButtonText:'Posting Reversal'}).then(x=>{if(x.isConfirmed)api('<?=base_url('inventoryprofessional/reverse')?>/'+id,x.value).done(r=>{table.ajax.reload(null,false);AppAlert.toast(r.message,'success')}).fail(fail)})});
$(document).on('click','.btn-detail',function(){let id=$(this).data('id');$('#detailBody').html('<div class="inventory-loading"><span class="spinner-border spinner-border-sm"></span> Memuat detail…</div>');bootstrap.Modal.getOrCreateInstance('#detailModal').show();$.get('<?=base_url('inventoryprofessional/document_detail')?>/'+id).done(function(r){let d=r.document,lines=d.lines.map(l=>'<tr><td>'+esc(l.kode_detail+' - '+l.nama_barang)+'</td><td>'+esc(l.barcode)+(l.serial_number?'<small class="d-block">SN '+esc(l.serial_number)+'</small>':'')+'</td><td>'+esc(l.batch_no)+'</td><td class="text-end">'+fmt(d.document_type==='opname'?l.counted_qty:l.qty)+' '+esc(l.unit)+'</td><td class="text-end">'+money(l.unit_cost)+'</td><td>'+esc(l.reason)+'</td></tr>').join(''),hist=d.history.map(h=>'<li><strong>'+esc(labels[h.to_status]||h.to_status)+'</strong><span>'+esc(h.user_name)+' · '+esc(h.created_at)+'</span><span>'+esc(h.notes)+'</span></li>').join(''),atts=d.attachments.map(a=>'<button class="btn btn-sm btn-outline-secondary attachment-preview" data-url="'+a.preview_url+'"><i class="bi bi-paperclip"></i> '+esc(a.original_name)+'</button>').join(' ');$('#detailTitle').text(d.document_no);$('#detailBody').html('<div class="document-meta"><span>Jenis<strong>'+esc(labels[d.document_type]||d.document_type)+'</strong></span><span>Tanggal<strong>'+esc(d.document_date)+'</strong></span><span>Status<strong><span class="status-chip status-'+d.status+'">'+esc(labels[d.status]||d.status)+'</span></strong></span><span>Gudang Asal<strong>'+esc(d.from_warehouse)+'</strong></span><span>Gudang Tujuan<strong>'+esc(d.to_warehouse)+'</strong></span><span>Dokumen Sumber<strong>'+esc(d.source_type)+' #'+esc(d.source_id)+'</strong></span><span>Pembuat<strong>'+esc(d.creator_name)+'</strong></span><span>Pengaju<strong>'+esc(d.submitter_name)+'</strong></span><span>Approver / Poster<strong>'+esc(d.approver_name)+' / '+esc(d.poster_name)+'</strong></span></div><div class="alert alert-light border mt-3"><strong>Alasan:</strong> '+esc(d.reason)+'</div><div class="table-responsive"><table class="table inventory-table"><thead><tr><th>Barang</th><th>Barcode/SN</th><th>Batch</th><th class="text-end">Qty</th><th class="text-end">Biaya</th><th>Catatan</th></tr></thead><tbody>'+lines+'</tbody></table></div><h6>Lampiran</h6><div class="mb-3 d-flex flex-wrap gap-2">'+(atts||'<span class="text-muted">Tidak ada lampiran.</span>')+'</div><h6>Riwayat Status</h6><ul class="timeline-list">'+hist+'</ul>');$('#detailActions').html('<button class="btn btn-outline-danger btn-pdf" data-id="'+d.id+'"><i class="bi bi-file-earmark-pdf"></i> PDF</button><button class="btn btn-light" data-bs-dismiss="modal">Tutup</button>')}).fail(fail)});
+4 -3
View File
@@ -12,6 +12,7 @@ $dataPages=isset($uiAssets['ui_data_pages'])?$uiAssets['ui_data_pages']:array();
$selectPages=isset($uiAssets['ui_select_pages'])?$uiAssets['ui_select_pages']:array();
$userName=$this->session->userdata('nama')?:$this->session->userdata('username')?:'Pengguna';
$userRole=$this->session->userdata('role')?:'User';
$userPhoto=$this->session->userdata('profile_photo');
?><!doctype html><html lang="id"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><meta name="theme-color" content="#ee7800">
<meta name="csrf-name" content="<?=$this->security->get_csrf_token_name()?>"><meta name="csrf-token" content="<?=$this->security->get_csrf_hash()?>">
@@ -20,7 +21,7 @@ $userRole=$this->session->userdata('role')?:'User';
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" onerror="this.onerror=null;this.href='<?=base_url('assets/vendor/bootstrap.min.css')?>'"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css">
<?php if(in_array($active_menu,$dataPages,true)):?><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/datatables.net-bs5@1.13.8/css/dataTables.bootstrap5.min.css"><?php endif?>
<?php if(in_array($active_menu,$selectPages,true)):?><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/select2@4.0.13/dist/css/select2.min.css"><?php endif?>
<link rel="stylesheet" href="<?=base_url('assets/css/style-custom.css').'?v='.filemtime(FCPATH.'assets/css/style-custom.css')?>"><link rel="stylesheet" href="<?=base_url('assets/css/app-final.css').'?v='.filemtime(FCPATH.'assets/css/app-final.css')?>">
<link rel="stylesheet" href="<?=base_url('assets/css/style-custom.css').'?v='.filemtime(FCPATH.'assets/css/style-custom.css')?>"><link rel="stylesheet" href="<?=base_url('assets/css/app-final.css').'?v='.filemtime(FCPATH.'assets/css/app-final.css')?>"><?php if($active_menu==='profile'):?><link rel="stylesheet" href="<?=base_url('assets/css/profile.css').'?v='.filemtime(FCPATH.'assets/css/profile.css')?>"><?php endif?><?php if($active_menu==='leave_requests'):?><link rel="stylesheet" href="<?=base_url('assets/css/leave-requests.css').'?v='.filemtime(FCPATH.'assets/css/leave-requests.css')?>"><?php endif?><?php if(in_array($active_menu,array('organization_master','employees','generate_schedule','attendance_monitoring','attendance_monthly','shift','holidays','leave_requests','hr_payroll'),true)):?><link rel="stylesheet" href="<?=base_url('assets/css/hr-payroll.css').'?v='.filemtime(FCPATH.'assets/css/hr-payroll.css')?>"><?php endif?>
<script src="<?=base_url('assets/vendor/jquery.min.js')?>"></script>
<?php if(in_array($active_menu,$dataPages,true)):?><script src="https://cdn.jsdelivr.net/npm/datatables.net@1.13.8/js/jquery.dataTables.min.js"></script><script src="https://cdn.jsdelivr.net/npm/datatables.net-bs5@1.13.8/js/dataTables.bootstrap5.min.js"></script><script src="https://cdn.jsdelivr.net/npm/datatables.net-responsive@2.5.0/js/dataTables.responsive.min.js"></script><?php endif?>
<?php if(in_array($active_menu,$selectPages,true)):?><script src="https://cdn.jsdelivr.net/npm/select2@4.0.13/dist/js/select2.min.js"></script><?php endif?>
@@ -31,12 +32,12 @@ $userRole=$this->session->userdata('role')?:'User';
<a class="sidebar-brand" href="<?=base_url()?>"><span class="brand-mark"><img src="<?=base_url('assets/img/accounting-white.png')?>" alt=""></span><span><strong>Accounting</strong><small>Business Suite</small></span></a>
<div class="sidebar-search"><i class="bi bi-search"></i><input class="menu-filter" type="search" placeholder="Cari menu..." aria-label="Cari menu"></div>
<nav class="sidebar-nav" aria-label="Navigasi utama"><?php $this->load->view('partials/navigation_items',compact('menu_items','active_menu'))?></nav>
<div class="sidebar-user"><span class="user-avatar"><?=html_escape(strtoupper(substr($userName,0,1)))?></span><span class="user-copy"><strong><?=html_escape($userName)?></strong><small><?=html_escape($userRole)?></small></span><a href="<?=base_url('auth/logout')?>" title="Keluar"><i class="bi bi-box-arrow-right"></i></a></div>
<div class="sidebar-user"><span class="user-avatar"><?php if($userPhoto&&is_file(FCPATH.$userPhoto)):?><img src="<?=base_url($userPhoto)?>" alt=""><?php else:?><?=html_escape(strtoupper(substr($userName,0,1)))?><?php endif?></span><span class="user-copy"><a href="<?=base_url('profile')?>"><strong><?=html_escape($userName)?></strong><small><?=html_escape($userRole)?></small></a></span><a href="<?=base_url('auth/logout')?>" title="Keluar"><i class="bi bi-box-arrow-right"></i></a></div>
</aside>
<section class="app-workspace">
<header class="app-topbar">
<div class="topbar-leading"><button class="icon-button d-lg-none" data-bs-toggle="offcanvas" data-bs-target="#mobileMenu" aria-label="Buka menu"><i class="bi bi-list"></i></button><div><span class="topbar-kicker">Accounting Workspace</span><h1><?=html_escape($page_title)?></h1></div></div>
<div class="topbar-actions"><button class="icon-button" data-bs-toggle="modal" data-bs-target="#menuSearchModal" aria-label="Cari menu"><i class="bi bi-search"></i></button><button class="icon-button quick-button" data-bs-toggle="modal" data-bs-target="#quickActions" aria-label="Aksi cepat"><i class="bi bi-lightning-charge"></i></button><a class="icon-button d-lg-none" href="<?=base_url('auth/logout')?>" aria-label="Keluar"><i class="bi bi-box-arrow-right"></i></a></div>
<div class="topbar-actions"><button class="icon-button" data-bs-toggle="modal" data-bs-target="#menuSearchModal" aria-label="Cari menu"><i class="bi bi-search"></i></button><button class="icon-button quick-button" data-bs-toggle="modal" data-bs-target="#quickActions" aria-label="Aksi cepat"><i class="bi bi-lightning-charge"></i></button><a class="icon-button d-lg-none" href="<?=base_url('profile')?>" aria-label="Profil"><i class="bi bi-person"></i></a><a class="icon-button d-lg-none" href="<?=base_url('auth/logout')?>" aria-label="Keluar"><i class="bi bi-box-arrow-right"></i></a></div>
</header>
<div class="offcanvas offcanvas-start app-mobile-drawer" id="mobileMenu" tabindex="-1"><div class="offcanvas-header"><a class="drawer-brand" href="<?=base_url()?>"><span class="brand-mark"><img src="<?=base_url('assets/img/accounting-white.png')?>" alt=""></span><span><strong>Accounting</strong><small>Business Suite</small></span></a><button class="btn-close" data-bs-dismiss="offcanvas" aria-label="Tutup"></button></div><div class="offcanvas-body"><div class="sidebar-search"><i class="bi bi-search"></i><input class="menu-filter" type="search" placeholder="Cari menu..."></div><nav class="sidebar-nav mobile-nav"><?php $this->load->view('partials/navigation_items',compact('menu_items','active_menu'))?></nav></div></div>
<div class="modal fade" id="menuSearchModal" tabindex="-1"><div class="modal-dialog modal-dialog-centered"><div class="modal-content"><div class="modal-header"><div><small class="text-muted">NAVIGASI</small><h5 class="modal-title">Cari menu</h5></div><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><div class="command-search"><i class="bi bi-search"></i><input class="form-control form-control-lg menu-filter" type="search" placeholder="Ketik nama menu..." autofocus></div><div class="search-menu-results mt-3"></div></div></div></div></div>
@@ -1,13 +1,13 @@
<?php foreach($menu_items as$item):
$children=isset($item['children'])?$item['children']:array();
$active=$active_menu===$item['key'];
foreach($children as$child)$active=$active||$active_menu===$child['key'];
foreach($children as$child)$active=$active||$active_menu===$child['key']||in_array($active_menu,isset($child['active_keys'])?$child['active_keys']:array(),true);
$searchLabel=strtolower($item['label'].' '.implode(' ',array_column($children,'label')));
?>
<?php if($children):?>
<details class="app-menu-item nav-group <?=$active?'is-active':''?>" data-menu-label="<?=html_escape($searchLabel)?>" <?=$active?'open':''?>>
<summary class="nav-entry"><span class="nav-entry-icon"><i class="bi <?=$item['icon']?>"></i></span><span class="nav-entry-label"><?=html_escape($item['label'])?></span><?php if(!empty($item['badge'])):?><span class="nav-badge <?=html_escape($item['badge_class']??'')?>" title="<?=html_escape($item['badge_title']??'')?>"><?=(int)$item['badge']?></span><?php endif?><i class="bi bi-chevron-down nav-chevron"></i></summary>
<div class="nav-children"><?php foreach($children as$child):?><a class="nav-child <?=$active_menu===$child['key']?'active':''?>" href="<?=base_url($child['url'])?>"><span></span><?=html_escape($child['label'])?><?php if(!empty($child['badge'])):?><span class="nav-badge <?=html_escape($child['badge_class']??'')?> ms-auto" title="<?=html_escape($child['badge_title']??'')?>"><?=(int)$child['badge']?></span><?php endif?></a><?php endforeach?></div>
<div class="nav-children"><?php foreach($children as$child):$childActive=$active_menu===$child['key']||in_array($active_menu,isset($child['active_keys'])?$child['active_keys']:array(),true);?><a class="nav-child <?=$childActive?'active':''?>" href="<?=base_url($child['url'])?>"><span></span><?=html_escape($child['label'])?><?php if(!empty($child['badge'])):?><span class="nav-badge <?=html_escape($child['badge_class']??'')?> ms-auto" title="<?=html_escape($child['badge_title']??'')?>"><?=(int)$child['badge']?></span><?php endif?></a><?php endforeach?></div>
</details>
<?php else:?>
<div class="app-menu-item" data-menu-label="<?=html_escape($searchLabel)?>"><a class="nav-entry <?=$active?'active':''?>" href="<?=base_url($item['url'])?>"><span class="nav-entry-icon"><i class="bi <?=$item['icon']?>"></i></span><span class="nav-entry-label"><?=html_escape($item['label'])?></span><?php if(!empty($item['badge'])):?><span class="nav-badge <?=html_escape($item['badge_class']??'')?>" title="<?=html_escape($item['badge_title']??'')?>"><?=(int)$item['badge']?></span><?php endif?></a></div>
+34
View File
@@ -0,0 +1,34 @@
<?php
$csrfName=$this->security->get_csrf_token_name();$csrfHash=$this->security->get_csrf_hash();
$initial=strtoupper(substr(trim((string)($user->nama?:$user->username)),0,1));
$activeSessions=array_values(array_filter($sessions,function($row){return$row->status==='active';}));
$historySessions=array_values(array_filter($sessions,function($row){return$row->status!=='active';}));
$resultLabels=array('success'=>'Login berhasil','invalid_credentials'=>'Kredensial salah','device_limit'=>'Ditolak: batas perangkat');
$resultClasses=array('success'=>'success','invalid_credentials'=>'danger','device_limit'=>'warning');
$statusLabels=array('active'=>'Aktif','logged_out'=>'Logout','expired'=>'Kedaluwarsa','revoked'=>'Diakhiri');
$statusClasses=array('active'=>'success','logged_out'=>'secondary','expired'=>'warning','revoked'=>'danger');
function profileDateTime($value){return$value?date('d/m/Y H:i',strtotime($value)):'-';}
?>
<div class="profile-page">
<section class="profile-hero"><div class="profile-identity"><div class="profile-avatar-large"><?php if(!empty($user->profile_photo)&&is_file(FCPATH.$user->profile_photo)):?><img src="<?=base_url($user->profile_photo)?>" alt="Foto profil"><?php else:?><?=html_escape($initial)?><?php endif?></div><div><span>AKUN PENGGUNA</span><h2 class="text-light"><?=html_escape($user->nama?:$user->username)?></h2><p>@<?=html_escape($user->username)?> &bull; <?=html_escape($user->email?:'Email belum diatur')?></p></div></div><div class="profile-security-summary"><small>Keamanan Login</small><b><?=$active_count?> / <?=(int)($user->max_active_devices?:1)?> perangkat aktif</b><span>Durasi maksimum <?=(int)($user->session_duration_hours?:24)?> jam dari login pertama</span></div></section>
<?php foreach(array('success'=>'success','error'=>'danger')as$key=>$class)if($this->session->flashdata($key)):?><div class="alert alert-<?=$class?>"><?=html_escape($this->session->flashdata($key))?></div><?php endif?>
<nav class="profile-tabs"><a class="<?=$profile_tab==='settings'?'active':''?>" href="<?=base_url('profile')?>"><i class="bi bi-person-gear"></i><span>Profil & Keamanan</span></a><a class="<?=$profile_tab==='devices'?'active':''?>" href="<?=base_url('profile?tab=devices')?>"><i class="bi bi-laptop"></i><span>Perangkat & Aktivitas</span><?php if($active_count):?><b><?=$active_count?></b><?php endif?></a></nav>
<?php if($profile_tab==='settings'):?>
<div class="profile-grid">
<section class="profile-card"><div class="profile-card-head"><div><h5>Informasi Profil</h5><p>Identitas yang tampil di dalam aplikasi.</p></div><i class="bi bi-person-vcard"></i></div><form method="post" enctype="multipart/form-data" action="<?=base_url('profile/update-profile')?>"><input type="hidden" name="<?=$csrfName?>" value="<?=$csrfHash?>"><div class="profile-photo-field"><div class="profile-avatar-preview"><?php if(!empty($user->profile_photo)&&is_file(FCPATH.$user->profile_photo)):?><img src="<?=base_url($user->profile_photo)?>" alt="Foto profil"><?php else:?><?=html_escape($initial)?><?php endif?></div><div><label class="form-label">Foto Profil</label><input class="form-control" type="file" name="profile_photo" accept="image/jpeg,image/png,image/webp"><small>JPG, PNG, atau WEBP; maksimal 2 MB.</small></div></div><div class="mb-3"><label class="form-label">Nama Lengkap</label><input class="form-control" name="nama" value="<?=html_escape($user->nama)?>" maxlength="255" required></div><div class="mb-3"><label class="form-label">Email</label><input class="form-control" type="email" name="email" value="<?=html_escape($user->email)?>" maxlength="150"></div><div class="mb-3"><label class="form-label">Username</label><input class="form-control" value="<?=html_escape($user->username)?>" disabled><small>Username dikelola oleh administrator.</small></div><button class="btn btn-primary"><i class="bi bi-check2-circle"></i> Simpan Profil</button></form></section>
<div class="profile-stack">
<section class="profile-card"><div class="profile-card-head"><div><h5>Pengaturan Session Login</h5><p>Batas waktu tetap dihitung sejak login pertama.</p></div><i class="bi bi-shield-check"></i></div><form method="post" action="<?=base_url('profile/update-security')?>"><input type="hidden" name="<?=$csrfName?>" value="<?=$csrfHash?>"><div class="row g-3"><div class="col-md-7"><label class="form-label">Durasi Maksimum Login</label><select class="form-select" name="session_duration_hours"><?php foreach(array(24=>'24 Jam',72=>'3 Hari',168=>'7 Hari')as$value=>$label):?><option value="<?=$value?>" <?=(int)$user->session_duration_hours===$value?'selected':''?>><?=$label?></option><?php endforeach?></select></div><div class="col-md-5"><label class="form-label">Batas Perangkat</label><input class="form-control" type="number" name="max_active_devices" min="1" max="10" value="<?=(int)($user->max_active_devices?:1)?>" required></div></div><div class="profile-notice"><i class="bi bi-info-circle"></i><span>Pengaturan baru berlaku saat login berikutnya. Rotasi ID session setiap 5 menit tidak memperpanjang batas ini.</span></div><button class="btn btn-primary"><i class="bi bi-save"></i> Simpan Pengaturan</button></form></section>
<section class="profile-card"><div class="profile-card-head"><div><h5>Ganti Password</h5><p>Perangkat lain otomatis dikeluarkan setelah password diubah.</p></div><i class="bi bi-key"></i></div><form method="post" action="<?=base_url('profile/update-password')?>"><input type="hidden" name="<?=$csrfName?>" value="<?=$csrfHash?>"><div class="row g-3"><div class="col-12"><label class="form-label">Password Saat Ini</label><input class="form-control" type="password" name="current_password" autocomplete="current-password" required></div><div class="col-md-6"><label class="form-label">Password Baru</label><input class="form-control" type="password" name="new_password" autocomplete="new-password" minlength="8" required></div><div class="col-md-6"><label class="form-label">Konfirmasi Password</label><input class="form-control" type="password" name="confirm_password" autocomplete="new-password" minlength="8" required></div></div><small class="d-block mt-2">Minimal 8 karakter serta mengandung huruf dan angka.</small><button class="btn btn-outline-primary mt-3"><i class="bi bi-key"></i> Ubah Password</button></form></section>
</div>
</div>
<?php else:?>
<section class="profile-card mb-3"><div class="profile-card-head"><div><h5>Perangkat Aktif</h5><p>Session aktif menggunakan satu slot perangkat sampai logout, diakhiri, atau mencapai batas waktunya.</p></div><span class="profile-count"><?=count($activeSessions)?> aktif</span></div><div class="device-list"><?php foreach($activeSessions as$session):?><article class="device-row"><div class="device-icon"><i class="bi <?=$session->device_type==='Mobile'?'bi-phone':($session->device_type==='Tablet'?'bi-tablet':'bi-laptop')?>"></i></div><div class="device-main"><div><b><?=html_escape($session->browser.' di '.$session->platform)?></b><?php if($session->is_current):?><span class="badge bg-success">Perangkat ini</span><?php endif?></div><small><?=html_escape($session->device_type)?> &bull; IP <?=html_escape($session->ip_address?:'-')?></small><div class="device-times"><span>Login <b><?=profileDateTime($session->login_at)?></b></span><span>Aktif terakhir <b><?=profileDateTime($session->last_seen_at)?></b></span><span>Berakhir <b><?=profileDateTime($session->expires_at)?></b></span></div><details><summary>Detail perangkat</summary><div class="user-agent"><?=html_escape($session->user_agent?:'-')?></div></details></div><form method="post" action="<?=base_url('profile/revoke-session/'.$session->id)?>" class="revoke-session-form"><input type="hidden" name="<?=$csrfName?>" value="<?=$csrfHash?>"><button class="btn btn-sm btn-outline-danger"><?=$session->is_current?'Logout perangkat ini':'Akhiri session'?></button></form></article><?php endforeach?><?php if(!$activeSessions):?><div class="profile-empty">Tidak ada perangkat aktif.</div><?php endif?></div></section>
<section class="profile-card mb-3"><div class="profile-card-head"><div><h5>Aktivitas & Percobaan Login</h5><p>Termasuk login berhasil, kredensial salah, dan permintaan yang ditolak karena batas perangkat.</p></div><i class="bi bi-clock-history"></i></div><div class="table-responsive"><table class="table align-middle profile-table"><thead><tr><th>Waktu</th><th>Perangkat</th><th>Alamat IP</th><th>Hasil</th><th>Keterangan</th></tr></thead><tbody><?php foreach($attempts as$attempt):?><tr><td><?=profileDateTime($attempt->attempted_at)?></td><td><b><?=html_escape($attempt->browser)?></b><small><?=html_escape($attempt->device_type)?> &bull; <?=html_escape($attempt->platform)?></small></td><td><?=html_escape($attempt->ip_address?:'-')?></td><td><span class="badge bg-<?=$resultClasses[$attempt->result]??'secondary'?>"><?=html_escape($resultLabels[$attempt->result]??$attempt->result)?></span></td><td><?=html_escape($attempt->reason?:'-')?><?php if($attempt->result==='device_limit'):?><small><?=$attempt->active_session_count?> dari <?=$attempt->device_limit?> slot sedang aktif</small><?php endif?></td></tr><?php endforeach?><?php if(!$attempts):?><tr><td colspan="5" class="text-center text-muted py-4">Belum ada aktivitas login yang tercatat.</td></tr><?php endif?></tbody></table></div></section>
<?php if($historySessions):?><section class="profile-card"><div class="profile-card-head"><div><h5>Riwayat Session Terakhir</h5><p>Session yang telah logout, kedaluwarsa, atau diakhiri.</p></div></div><div class="table-responsive"><table class="table align-middle profile-table"><thead><tr><th>Perangkat</th><th>Login</th><th>Terakhir Aktif</th><th>Status</th><th>Alasan</th></tr></thead><tbody><?php foreach($historySessions as$session):?><tr><td><b><?=html_escape($session->browser.' di '.$session->platform)?></b><small><?=html_escape($session->device_type)?> &bull; <?=html_escape($session->ip_address?:'-')?></small></td><td><?=profileDateTime($session->login_at)?></td><td><?=profileDateTime($session->last_seen_at)?></td><td><span class="badge bg-<?=$statusClasses[$session->status]??'secondary'?>"><?=html_escape($statusLabels[$session->status]??$session->status)?></span></td><td><?=html_escape($session->end_reason?:'-')?></td></tr><?php endforeach?></tbody></table></div></section><?php endif?>
<?php endif?>
</div>
<script>document.querySelectorAll('.revoke-session-form').forEach(function(form){form.addEventListener('submit',function(event){event.preventDefault();var current=this.querySelector('button').textContent.indexOf('Logout')!==-1;AppAlert.confirm({title:current?'Logout perangkat ini?':'Akhiri session perangkat?',text:current?'Anda harus login kembali pada perangkat ini.':'Perangkat tersebut akan diminta login kembali.',icon:'warning'}).then(result=>{if(result.isConfirmed)form.submit()});});});</script>
+1 -1
View File
@@ -6,7 +6,7 @@
<?php foreach($roles as$r):?><button type="button" class="list-group-item list-group-item-action role-item" data-id="<?=$r->id?>"><div class="d-flex justify-content-between"><b><?=html_escape($r->nama_role)?></b><span><?php if($r->is_super_admin):?><span class="badge bg-danger">Master Admin</span><?php elseif($r->is_system):?><span class="badge bg-secondary">Bawaan</span><?php else:?><span class="badge bg-info text-dark">Custom</span><?php endif?></span></div><small class="text-muted"><?=html_escape($r->description?:'Belum ada uraian tanggung jawab')?></small></button><?php endforeach?>
</div></div></div>
<div class="col-lg-8"><div class="card"><div class="card-header" id="formTitle">Pilih role untuk melihat permission</div><div class="card-body"><form id="roleForm"><input type="hidden" name="id" id="roleId"><div class="row g-2 mb-3"><div class="col-md-4"><label class="form-label">Nama Role</label><input class="form-control" name="name" id="roleName" required></div><div class="col-md-6"><label class="form-label">Tanggung Jawab Role</label><input class="form-control" name="description" id="roleDescription" required placeholder="Jelaskan pekerjaan yang ditangani"></div><div class="col-md-2 d-flex align-items-end"><label class="form-check mb-2"><input class="form-check-input" type="checkbox" name="is_active" id="roleActive" value="1"> Aktif</label></div></div>
<div class="table-responsive"><table class="table table-sm align-middle"><thead><tr><th>Modul</th><?php foreach($actions as$label):?><th class="text-center"><?=$label?></th><?php endforeach?></tr></thead><tbody><?php foreach($features as$key=>$label):?><tr><td><b><?=$label?></b></td><?php foreach($actions as$action=>$actionLabel):?><td class="text-center"><input class="form-check-input permission-box" type="checkbox" name="permissions[<?=$key?>][]" value="<?=$action?>" data-feature="<?=$key?>" data-action="<?=$action?>"></td><?php endforeach?></tr><?php endforeach?></tbody></table></div>
<div class="table-responsive"><table class="table table-sm align-middle"><thead><tr><th>Modul</th><?php foreach($actions as$label):?><th class="text-center"><?=$label?></th><?php endforeach?></tr></thead><tbody><?php foreach($features as$key=>$label):?><tr><td><b><?=$label?></b><?php if($key==='payroll'):?><div class="form-check mt-2"><input class="form-check-input permission-box" type="checkbox" name="permissions[payroll][]" value="can_manage_all_salary" id="manageAllSalary" data-feature="payroll" data-action="can_manage_all_salary"><label class="form-check-label small text-danger" for="manageAllSalary">Manage All Gaji (termasuk gaji terbatas)</label></div><?php endif?></td><?php foreach($actions as$action=>$actionLabel):?><td class="text-center"><input class="form-check-input permission-box" type="checkbox" name="permissions[<?=$key?>][]" value="<?=$action?>" data-feature="<?=$key?>" data-action="<?=$action?>"></td><?php endforeach?></tr><?php endforeach?></tbody></table></div>
<div class="d-flex justify-content-between mt-3"><button type="button" class="btn btn-outline-danger" id="deleteRole" hidden>Hapus Role Custom</button><button class="btn btn-success ms-auto" id="saveRole">Simpan Permission</button></div>
</form></div></div></div>
</div>