616 lines
20 KiB
PHP
616 lines
20 KiB
PHP
<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>Jurnal Umum</h5>
|
|
<button class="btn btn-warning btn-add">Tambah Jurnal</button>
|
|
</div>
|
|
|
|
<div class="row mb-3">
|
|
<div class="col-sm-6 col-md-4 col-lg-3">
|
|
<label for="journalStatusFilter" class="form-label">Status</label>
|
|
<select id="journalStatusFilter" class="form-select">
|
|
<option value="">Semua Status</option>
|
|
<option value="draft">Draft</option>
|
|
<option value="submitted">Diajukan</option>
|
|
<option value="approved">Disetujui</option>
|
|
<option value="posted">Terposting</option>
|
|
<option value="rejected">Ditolak</option>
|
|
<option value="reversed">Reversal</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<!--<form id="filterForm" class="row mb-3">-->
|
|
|
|
<!-- <div class="col-md-4">-->
|
|
<!-- <label>Dari Tanggal</label>-->
|
|
<!-- <input type="date" id="tanggal_dari" class="form-control">-->
|
|
<!-- </div>-->
|
|
|
|
<!-- <div class="col-md-4">-->
|
|
<!-- <label>Sampai Tanggal</label>-->
|
|
<!-- <input type="date" id="tanggal_sampai" class="form-control">-->
|
|
<!-- </div>-->
|
|
|
|
<!-- <div class="col-md-4 d-flex align-items-end">-->
|
|
<!-- <button type="submit" class="btn btn-warning w-100">-->
|
|
<!-- Tampilkan-->
|
|
<!-- </button>-->
|
|
<!-- </div>-->
|
|
|
|
<!--</form>-->
|
|
|
|
<div class="table-responsive">
|
|
<table id="tableJurnal" class="table modern-table align-middle w-100">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>No</th>
|
|
<th>Tanggal</th>
|
|
<th>No Ref</th>
|
|
<th style="max-width: 600px">Keterangan</th>
|
|
<th class="text-end">Debit</th>
|
|
<th class="text-end">Kredit</th>
|
|
<th>Status</th>
|
|
<th>Aksi</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody></tbody>
|
|
<tfoot>
|
|
<tr class="fw-bold">
|
|
<td colspan="4" class="text-end">TOTAL</td>
|
|
<td class="text-end" id="totalDebitTable">0</td>
|
|
<td class="text-end" id="totalKreditTable">0</td>
|
|
<td></td><td></td>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- MODAL INPUT -->
|
|
<div class="modal fade" id="modalJurnal">
|
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
|
<div class="modal-content">
|
|
|
|
<div class="modal-header bg-warning text-white">
|
|
<h5 id="journalModalTitle">Tambah Jurnal</h5>
|
|
<button class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
|
|
<div class="modal-body">
|
|
|
|
<!-- MODE -->
|
|
<div class="mb-3">
|
|
<div class="btn-group w-100">
|
|
<button type="button" class="btn btn-warning active" id="modeManual">Jurnal Manual</button>
|
|
<button type="button" class="btn btn-outline-secondary" id="modeAuto">Template Jurnal</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- TEMPLATE -->
|
|
<div id="autoSection" class="mb-3">
|
|
<label>Template Jurnal</label>
|
|
<select id="base_jurnal_id" class="form-control select-search">
|
|
<option value="">-- Pilih Template --</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- FORM -->
|
|
<input type="date" id="tanggal" class="form-control mb-2">
|
|
<textarea id="keterangan" class="form-control mb-3" placeholder="Keterangan"></textarea>
|
|
|
|
<!-- TABLE -->
|
|
<table class="table" id="tableDetail">
|
|
<thead>
|
|
<tr>
|
|
<th>Akun</th>
|
|
<th>Debit</th>
|
|
<th>Kredit</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody></tbody>
|
|
|
|
<tfoot>
|
|
<tr class="fw-bold">
|
|
<td class="text-end">TOTAL</td>
|
|
<td id="formTotalDebit">0</td>
|
|
<td id="formTotalKredit">0</td>
|
|
<td></td>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
|
|
<button class="btn btn-warning btn-add-row">Tambah Baris</button>
|
|
|
|
</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>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- MODAL DETAIL -->
|
|
<div class="modal fade" id="modalDetail">
|
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
|
<div class="modal-content">
|
|
|
|
<div class="modal-header bg-warning text-white">
|
|
<h5>Detail Jurnal</h5>
|
|
<button class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
|
|
<div class="modal-body">
|
|
|
|
<div class="mb-3">
|
|
<strong>Tanggal:</strong> <span id="d_tanggal"></span><br>
|
|
<strong>No Ref:</strong> <span id="d_noref"></span><br>
|
|
<strong>Keterangan:</strong> <span id="d_keterangan"></span><br>
|
|
<strong>Dibuat oleh:</strong> <span id="d_createdby"></span><br>
|
|
<strong>Status:</strong> <span id="d_status"></span>
|
|
<div id="d_rejection" class="alert alert-danger mt-3 d-none"></div>
|
|
</div>
|
|
|
|
<table class="table table-bordered">
|
|
<thead>
|
|
<tr>
|
|
<th>Akun</th>
|
|
<th>Debit</th>
|
|
<th>Kredit</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="detailBody"></tbody>
|
|
|
|
<tfoot>
|
|
<tr>
|
|
<th>Total</th>
|
|
<th id="totalDebitDetail">0</th>
|
|
<th id="totalKreditDetail">0</th>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
|
|
<h6 class="mt-3">Riwayat Status</h6>
|
|
<div id="statusHistory" class="small text-muted"></div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
#tableJurnal tbody tr:hover {
|
|
background-color: #fff3cd;
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
|
|
$(function(){
|
|
|
|
let accounts = [];
|
|
let mode = 'manual';
|
|
let editJournalId = 0;
|
|
|
|
// ================= LOAD ACCOUNTS
|
|
function loadAccounts(){
|
|
return $.get("<?= base_url('jurnal/get_accounts'); ?>", function(res){
|
|
accounts = res;
|
|
},'json');
|
|
}
|
|
|
|
// ================= LOAD BASE JURNAL
|
|
function loadBaseJurnal(){
|
|
return $.get("<?= base_url('jurnal/get_base_jurnal'); ?>", function(res){
|
|
|
|
let opt = '<option value="">-- Pilih Template --</option>';
|
|
res.forEach(b=>{
|
|
opt += `<option value="${b.id}">${b.kode} - ${b.nama}</option>`;
|
|
});
|
|
|
|
$('#base_jurnal_id').html(opt);
|
|
|
|
},'json');
|
|
}
|
|
|
|
function accountOptions(){
|
|
let opt = '<option value="">Pilih Akun</option>';
|
|
accounts.forEach(a=>{
|
|
opt += `<option value="${a.id}">${a.kode_akun} - ${a.nama_akun}</option>`;
|
|
});
|
|
return opt;
|
|
}
|
|
|
|
// ================= ADD ROW
|
|
function addRow(data=null){
|
|
|
|
let isAuto = (mode === 'auto');
|
|
|
|
let debitVal = data?.debit || '';
|
|
let kreditVal = data?.kredit || '';
|
|
|
|
let hideDebit = isAuto && data?.posisi === 'kredit';
|
|
let hideKredit = isAuto && data?.posisi === 'debit';
|
|
|
|
let row = `
|
|
<tr>
|
|
<td>
|
|
<select class="form-control account_id" ${isAuto ? 'disabled' : ''}>
|
|
${accountOptions()}
|
|
</select>
|
|
</td>
|
|
|
|
<td>
|
|
<input type="text" class="form-control debit format-rupiah"
|
|
value="${debitVal}"
|
|
${hideDebit ? 'style="display:none"' : ''}
|
|
>
|
|
</td>
|
|
|
|
<td>
|
|
<input type="text" class="form-control kredit format-rupiah"
|
|
value="${kreditVal}"
|
|
${hideKredit ? 'style="display:none"' : ''}
|
|
>
|
|
</td>
|
|
|
|
<td class="text-center">
|
|
${isAuto ? '' : '<button class="btn btn-danger btn-remove">X</button>'}
|
|
</td>
|
|
</tr>
|
|
`;
|
|
|
|
$('#tableDetail tbody').append(row);
|
|
|
|
if(data){
|
|
$('#tableDetail tbody tr:last .account_id').val(data.account_id);
|
|
}
|
|
}
|
|
|
|
// ================= LOAD TEMPLATE DETAIL
|
|
function loadTemplateDetail(id){
|
|
|
|
if(!id) return;
|
|
|
|
$.get("<?= base_url('jurnal/get_base_jurnal_detail/'); ?>" + id, function(res){
|
|
|
|
$('#tableDetail tbody').html('');
|
|
|
|
res.forEach(r=>{
|
|
addRow({
|
|
account_id: r.account_id,
|
|
posisi: r.posisi, // penting!
|
|
debit: r.posisi === 'debit' ? 0 : '',
|
|
kredit: r.posisi === 'kredit' ? 0 : ''
|
|
});
|
|
});
|
|
|
|
hitungTotalForm();
|
|
|
|
},'json');
|
|
}
|
|
|
|
|
|
$('#modeAuto').click(function(){
|
|
|
|
mode = 'auto';
|
|
|
|
$(this).addClass('btn-warning active').removeClass('btn-outline-secondary');
|
|
$('#modeManual').removeClass('btn-warning active').addClass('btn-outline-secondary');
|
|
|
|
$('#autoSection').show();
|
|
|
|
// ✅ RESET TOTAL FORM
|
|
$('#tableDetail tbody').html('');
|
|
$('#keterangan').val(''); // ✅ FIX
|
|
$('#tanggal').val(''); // ✅ FIX
|
|
$('#base_jurnal_id').val('');
|
|
|
|
// reset total juga
|
|
$('#formTotalDebit').text('0');
|
|
$('#formTotalKredit').text('0');
|
|
|
|
// sembunyikan tombol tambah
|
|
$('.btn-add-row').hide();
|
|
});
|
|
|
|
$('#modeManual').click(function(){
|
|
mode = 'manual';
|
|
|
|
$(this).addClass('btn-warning active').removeClass('btn-outline-secondary');
|
|
$('#modeAuto').removeClass('btn-warning active').addClass('btn-outline-secondary');
|
|
|
|
$('#autoSection').hide();
|
|
$('#tableDetail tbody').html('');
|
|
|
|
$('.btn-add-row').show(); // ⬅️ tampilkan lagi
|
|
|
|
addRow();
|
|
addRow();
|
|
});
|
|
|
|
// ================= PILIH TEMPLATE
|
|
$('#base_jurnal_id').change(function(){
|
|
loadTemplateDetail($(this).val());
|
|
});
|
|
|
|
// ================= ADD ROW BUTTON
|
|
$('.btn-add-row').click(function(){
|
|
|
|
if(mode === 'manual'){
|
|
addRow();
|
|
}else{
|
|
Swal.fire('Info','Mode otomatis pakai template','info');
|
|
}
|
|
|
|
});
|
|
|
|
// ================= REMOVE ROW
|
|
$(document).on('click','.btn-remove',function(){
|
|
$(this).closest('tr').remove();
|
|
hitungTotalForm();
|
|
});
|
|
|
|
// ================= HITUNG TOTAL
|
|
function hitungTotalForm(){
|
|
let td=0, tk=0;
|
|
|
|
$('#tableDetail tbody tr').each(function(){
|
|
td += window.parseMoney($(this).find('.debit').val());
|
|
tk += window.parseMoney($(this).find('.kredit').val());
|
|
});
|
|
|
|
$('#formTotalDebit').text(td.toLocaleString('id-ID'));
|
|
$('#formTotalKredit').text(tk.toLocaleString('id-ID'));
|
|
}
|
|
|
|
$(document).on('keyup change','.debit, .kredit',hitungTotalForm);
|
|
|
|
// ================= OPEN MODAL
|
|
$('.btn-add').click(async function(){
|
|
|
|
await loadAccounts();
|
|
await loadBaseJurnal();
|
|
|
|
$('#modalJurnal').modal('show');
|
|
|
|
$('#tableDetail tbody').html('');
|
|
$('#base_jurnal_id').val('');
|
|
|
|
// reset total juga
|
|
$('#formTotalDebit').text('0');
|
|
$('#formTotalKredit').text('0');
|
|
|
|
$('#modeManual').click(); // default jurnal manual dengan dua akun
|
|
editJournalId = 0;
|
|
$('#journalModalTitle').text('Tambah Jurnal');
|
|
$('#btnSimpan').text('Simpan');
|
|
});
|
|
|
|
$(document).on('click','.btn-edit-rejected',async function(){
|
|
const id=$(this).data('id');
|
|
await loadAccounts();
|
|
await loadBaseJurnal();
|
|
$('#modeManual').click();
|
|
$.get("<?= base_url('jurnal/detail/'); ?>"+id,function(res){
|
|
editJournalId=id;
|
|
$('#journalModalTitle').text('Koreksi Jurnal Ditolak');
|
|
$('#btnSimpan').text('Simpan Koreksi');
|
|
$('#tanggal').val(res.header.tanggal);
|
|
$('#keterangan').val(res.header.keterangan);
|
|
$('#tableDetail tbody').empty();
|
|
(res.detail||[]).forEach(function(row){addRow({account_id:row.account_id,debit:Number(row.debit)>0?row.debit:'',kredit:Number(row.kredit)>0?row.kredit:''});});
|
|
hitungTotalForm();
|
|
$('#modalJurnal').modal('show');
|
|
},'json').fail(function(xhr){Swal.fire('Error',xhr.responseJSON?.message||'Jurnal tidak dapat dimuat.','error');});
|
|
});
|
|
|
|
$('#btnSimpan').click(function(){
|
|
|
|
let account_id = [];
|
|
let debit = [];
|
|
let kredit = [];
|
|
|
|
let totalDebit = 0;
|
|
let totalKredit = 0;
|
|
|
|
$('#tableDetail tbody tr').each(function(){
|
|
|
|
let d = window.parseMoney($(this).find('.debit').val());
|
|
let k = window.parseMoney($(this).find('.kredit').val());
|
|
|
|
if(d <= 0 && k <= 0) return;
|
|
|
|
account_id.push($(this).find('.account_id').val());
|
|
debit.push(d);
|
|
kredit.push(k);
|
|
|
|
totalDebit += d;
|
|
totalKredit += k;
|
|
});
|
|
|
|
// ✅ VALIDASI DI SINI
|
|
if(Math.abs(totalDebit - totalKredit) > 0.009){
|
|
Swal.fire('Warning','Total Debit dan Kredit harus sama!','warning');
|
|
return;
|
|
}
|
|
|
|
if(totalDebit === 0 && totalKredit === 0){
|
|
Swal.fire('Warning','Minimal satu pasangan debit dan kredit harus diisi.','warning');
|
|
return;
|
|
}
|
|
|
|
if(account_id.length < 2 || account_id.some(id => !id)){
|
|
Swal.fire('Warning','Setiap baris yang memiliki nominal wajib memilih akun.','warning');
|
|
return;
|
|
}
|
|
|
|
if($('#keterangan').val().trim() === ''){
|
|
Swal.fire('Warning','Keterangan tidak boleh kosong!','warning');
|
|
return;
|
|
}
|
|
|
|
if(!$('#tanggal').val()){
|
|
Swal.fire('Warning','Tanggal tidak boleh kosong!','warning');
|
|
return;
|
|
}
|
|
// lanjut simpan
|
|
const saveUrl=editJournalId
|
|
? "<?= base_url('jurnal/update_rejected/'); ?>"+editJournalId
|
|
: "<?= base_url('jurnal/save'); ?>";
|
|
$.post(saveUrl, {
|
|
tanggal: $('#tanggal').val(),
|
|
keterangan: $('#keterangan').val(),
|
|
account_id: account_id,
|
|
debit: debit,
|
|
kredit: kredit
|
|
}, function(res){
|
|
|
|
if(res.status){
|
|
$('#modalJurnal').modal('hide');
|
|
editJournalId=0;
|
|
$('#tableJurnal').DataTable().ajax.reload(null,false);
|
|
Swal.fire('Sukses',res.message,'success');
|
|
} else {
|
|
Swal.fire('Error',res.message,'error');
|
|
}
|
|
|
|
},'json');
|
|
});
|
|
|
|
// DATATABLE
|
|
let table = $('#tableJurnal').DataTable({
|
|
processing:true,
|
|
serverSide:true,
|
|
pageLength:10, // default 10
|
|
lengthMenu:[ [10,25,50,100], [10,25,50,100] ],
|
|
order:[[1,'desc']],
|
|
ajax:{
|
|
url:"<?= base_url('jurnal/get_data'); ?>",
|
|
type:"POST",
|
|
data:function(d){
|
|
d.status = $('#journalStatusFilter').val();
|
|
}
|
|
},
|
|
columnDefs:[
|
|
{ targets:[4,5], className:'text-end' }
|
|
],
|
|
drawCallback:function(){
|
|
|
|
let api = this.api();
|
|
let td = 0, tk = 0;
|
|
|
|
api.rows().every(function(){
|
|
let d = this.data();
|
|
|
|
td += parseFloat(d[4].replace(/\./g,'')) || 0;
|
|
tk += parseFloat(d[5].replace(/\./g,'')) || 0;
|
|
});
|
|
|
|
$('#totalDebitTable').text(td.toLocaleString('id-ID'));
|
|
$('#totalKreditTable').text(tk.toLocaleString('id-ID'));
|
|
}
|
|
});
|
|
|
|
$('#journalStatusFilter').on('change', function(){
|
|
table.ajax.reload(null, true);
|
|
});
|
|
|
|
// DELETE
|
|
$(document).on('click','.btn-delete',function(){
|
|
let id = $(this).data('id');
|
|
|
|
Swal.fire({
|
|
title:'Hapus?',
|
|
icon:'warning',
|
|
showCancelButton:true
|
|
}).then(r=>{
|
|
if(r.isConfirmed){
|
|
$.post("<?= base_url('jurnal/delete/'); ?>"+id,{},function(res){
|
|
if(res.status){ table.ajax.reload(null,false); Swal.fire('Sukses',res.message,'success'); }
|
|
else Swal.fire('Error',res.message,'error');
|
|
},'json');
|
|
}
|
|
});
|
|
});
|
|
|
|
function workflowPost(endpoint, id, data, confirmation){
|
|
Swal.fire({title:confirmation,icon:'question',showCancelButton:true}).then(function(result){
|
|
if(!result.isConfirmed) return;
|
|
$.post("<?= base_url('jurnal/'); ?>"+endpoint+'/'+id, data || {}, function(res){
|
|
if(res.status){ table.ajax.reload(null,false); Swal.fire('Sukses',res.message,'success'); }
|
|
else Swal.fire('Error',res.message,'error');
|
|
},'json').fail(function(xhr){
|
|
Swal.fire('Error', xhr.responseJSON?.message || 'Proses workflow gagal.','error');
|
|
});
|
|
});
|
|
}
|
|
|
|
$(document).on('click','.btn-submit',function(){ workflowPost('submit',$(this).data('id'),{},'Ajukan jurnal ini?'); });
|
|
$(document).on('click','.btn-approve',function(){ workflowPost('approve',$(this).data('id'),{},'Setujui jurnal ini?'); });
|
|
$(document).on('click','.btn-reject',async function(){
|
|
const result=await Swal.fire({title:'Tolak jurnal',input:'textarea',inputLabel:'Alasan',showCancelButton:true,inputValidator:v=>!v?'Alasan wajib diisi':undefined});
|
|
if(result.isConfirmed) workflowPost('reject',$(this).data('id'),{reason:result.value},'Konfirmasi penolakan jurnal?');
|
|
});
|
|
$(document).on('click','.btn-reverse',async function(){
|
|
const result=await Swal.fire({title:'Reversal jurnal',html:'<input type="date" id="reverseDate" class="swal2-input" value="<?= date('Y-m-d') ?>"><textarea id="reverseReason" class="swal2-textarea" placeholder="Alasan reversal"></textarea>',showCancelButton:true,preConfirm:()=>{const date=$('#reverseDate').val(),reason=$('#reverseReason').val().trim();if(!date||!reason){Swal.showValidationMessage('Tanggal dan alasan wajib diisi');return false;}return {date,reason};}});
|
|
if(result.isConfirmed) workflowPost('reverse',$(this).data('id'),result.value,'Buat jurnal reversal?');
|
|
});
|
|
|
|
});
|
|
|
|
// DETAIL
|
|
function openJournalDetail(id){
|
|
$.get("<?= base_url('jurnal/detail/'); ?>" + id, function(res){
|
|
|
|
$('#d_tanggal').text(res.header.tanggal);
|
|
$('#d_noref').text(res.header.no_ref);
|
|
$('#d_keterangan').text(res.header.keterangan);
|
|
$('#d_createdby').text(res.user ? res.user.nama : '-');
|
|
$('#d_status').text(String(res.header.status || '').toUpperCase());
|
|
if(res.header.rejection_reason){$('#d_rejection').removeClass('d-none').html('<strong>Alasan penolakan sebelumnya</strong><br>'+String(res.header.rejection_reason).replace(/[&<>'"]/g,function(c){return{'&':'&','<':'<','>':'>',"'":''','"':'"'}[c];}));}else{$('#d_rejection').addClass('d-none').empty();}
|
|
let history=(res.history||[]).map(h=>`${h.from_status||'-'} → ${h.to_status} oleh ${h.changed_by_name||'-'} (${h.changed_at})${h.notes?' — '+h.notes:''}`).join('<br>');
|
|
$('#statusHistory').html(history || 'Belum ada perubahan status.');
|
|
|
|
let html = '';
|
|
let totalDebit = 0;
|
|
let totalKredit = 0;
|
|
|
|
res.detail.forEach(row => {
|
|
|
|
totalDebit += parseFloat(row.debit);
|
|
totalKredit += parseFloat(row.kredit);
|
|
|
|
html += `
|
|
<tr>
|
|
<td>${row.kode_akun} - ${row.nama_akun}</td>
|
|
<td>${parseFloat(row.debit).toLocaleString('id-ID')}</td>
|
|
<td>${parseFloat(row.kredit).toLocaleString('id-ID')}</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
|
|
$('#detailBody').html(html);
|
|
$('#totalDebitDetail').text(totalDebit.toLocaleString('id-ID'));
|
|
$('#totalKreditDetail').text(totalKredit.toLocaleString('id-ID'));
|
|
|
|
$('#modalDetail').modal('show');
|
|
|
|
},'json').fail(function(xhr){Swal.fire('Error',xhr.responseJSON?.message||'Detail jurnal tidak dapat dimuat.','error');});
|
|
}
|
|
|
|
$(document).on('click','.btn-detail',function(){openJournalDetail($(this).data('id'));});
|
|
$(function(){const focus=Number(new URLSearchParams(window.location.search).get('focus')||0);if(focus>0)openJournalDetail(focus);});
|
|
|
|
|
|
</script>
|