Files
2026-09-11 16:03:00 +07:00

64 lines
13 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
$inventory_title='Laporan Persediaan';
$inventory_subtitle='Laporan operasional, penelusuran barcode, nilai stok, dan rekonsiliasi accounting dalam satu tempat.';
$this->load->view('inventory_professional/_module_nav',get_defined_vars());
?>
<div class="row g-3">
<div class="col-xl-9">
<section class="inventory-card">
<div class="inventory-card-head">
<div><h5 id="reportTitle">Nilai Persediaan</h5><p>Gunakan filter untuk menelusuri saldo dan dokumen sumber.</p></div>
<?php if($inventory_permissions['export']):?><div class="d-flex gap-2"><button class="btn btn-sm btn-outline-success" id="exportExcel"><i class="bi bi-file-earmark-spreadsheet"></i> Excel</button><button class="btn btn-sm btn-outline-danger" id="exportPdf"><i class="bi bi-file-earmark-pdf"></i> PDF</button></div><?php endif?>
</div>
<div class="p-3 border-bottom inventory-toolbar">
<div class="filter-field"><label class="form-label">Jenis Laporan</label><select id="reportType" class="form-select"><option value="valuation">Nilai Persediaan</option><option value="stock_card">Kartu Stok</option><option value="location">Stok per Gudang & Rak</option><option value="barcode">Barcode & Serial</option><option value="batch">Batch & Kedaluwarsa</option><option value="reservation">Reservasi Stok</option><option value="reorder">Stok Minimum / Reorder</option><option value="slow">Slow-moving / Dead Stock</option><option value="damage">Barang Rusak / Hilang</option><option value="opname">Stock Opname</option><option value="reconciliation">Riwayat Rekonsiliasi</option></select></div>
<div class="filter-field"><label class="form-label">Gudang</label><select id="reportWarehouse" class="form-select"><option value="">Semua Gudang</option><?php foreach($warehouses as$w):?><option value="<?=$w->id?>"><?=html_escape($w->nama)?></option><?php endforeach?></select></div>
<div class="filter-field item-filter"><label class="form-label">Barang</label><select id="reportItem" class="form-select"></select></div>
<div class="filter-field date-filter"><label class="form-label">Dari Tanggal</label><input id="dateFrom" type="date" class="form-control" value="<?=date('Y-m-01')?>"></div>
<div class="filter-field date-filter"><label class="form-label">Sampai Tanggal</label><input id="dateTo" type="date" class="form-control" value="<?=date('Y-m-d')?>"></div>
<div class="filter-field slow-filter d-none"><label class="form-label">Tidak Bergerak</label><select id="reportDays" class="form-select"><option value="30">Minimal 30 hari</option><option value="60">Minimal 60 hari</option><option value="90" selected>Minimal 90 hari</option><option value="180">Minimal 180 hari</option></select></div>
<button class="btn btn-primary align-self-end" id="loadReport"><i class="bi bi-search"></i> Tampilkan</button>
</div>
<div class="table-responsive"><table class="table inventory-table align-middle mb-0 w-100" id="reportTable"><thead></thead><tbody></tbody></table></div>
</section>
</div>
<div class="col-xl-3">
<section class="inventory-card">
<div class="inventory-card-head"><div><h5>Rekonsiliasi</h5><p>Ledger dibandingkan dengan stock log, barcode, cache, dan GL.</p></div></div>
<div class="p-3"><label class="form-label">Posisi per Tanggal</label><input id="reconcileDate" type="date" class="form-control" value="<?=date('Y-m-d')?>"><div id="reconcileResult" class="inventory-balance-box my-3"><div><span>Belum diperiksa.</span></div></div><?php if($inventory_permissions['post']):?><button id="runReconciliation" class="btn btn-outline-primary w-100"><i class="bi bi-arrow-repeat"></i> Jalankan Rekonsiliasi</button><?php endif?><div id="reconcileIssues" class="mt-3"></div><small class="d-block text-muted mt-3">Selisih tidak membuat jurnal koreksi otomatis. Anomali ditampilkan agar dapat ditelusuri ke dokumen sumber.</small></div>
</section>
</div>
</div>
<div class="modal fade inventory-modal" id="reportPdfModal" tabindex="-1"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Laporan Persediaan</h5><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body p-0" style="height:min(82vh,900px)"><iframe title="Laporan persediaan" style="width:100%;height:100%;border:0"></iframe></div></div></div></div>
<script>
$(function(){
const csrf=()=>({[APP_CSRF.name]:APP_CSRF.value}),esc=v=>$('<div>').text(v==null||v===''?'-':v).html(),qty=v=>Number(v||0).toLocaleString('id-ID',{maximumFractionDigits:4}),money=v=>'Rp '+Number(v||0).toLocaleString('id-ID',{minimumFractionDigits:2,maximumFractionDigits:2});
const reports={
valuation:{title:'Nilai Persediaan',cols:[['kode_detail','Kode'],['nama_barang','Barang'],['warehouse_name','Gudang'],['costing_method','Metode'],['qty','Qty','qty'],['inventory_value','Nilai','money']]},
stock_card:{title:'Kartu Stok',cols:[['movement_date','Tanggal'],['document_no','Dokumen'],['warehouse_name','Gudang'],['direction','Arah'],['qty','Qty','qty'],['unit_cost','Biaya Satuan','money'],['value','Nilai','money'],['reason','Keterangan']]},
location:{title:'Stok per Gudang dan Rak',cols:[['kode_detail','Kode'],['nama_barang','Barang'],['warehouse_name','Gudang'],['bin_code','Rak/Bin'],['qty','Qty','qty']]},
barcode:{title:'Barcode dan Serial',cols:[['kode_detail','Kode'],['nama_barang','Barang'],['barcode','Barcode'],['serial_number','Serial'],['warehouse_name','Gudang'],['bin_code','Rak'],['batch_no','Batch'],['qty_sisa','Qty','qty'],['reserved_qty','Reservasi','qty'],['status','Status','status']]},
batch:{title:'Batch dan Kedaluwarsa',cols:[['kode_detail','Kode'],['nama_barang','Barang'],['batch_no','Batch'],['warehouse_name','Gudang'],['manufactured_date','Produksi'],['expiry_date','Kedaluwarsa'],['qty','Qty','qty']]},
reservation:{title:'Reservasi Stok',cols:[['reservation_no','Reservasi'],['kode_detail','Kode'],['nama_barang','Barang'],['warehouse_name','Gudang'],['source_type','Jenis Sumber'],['source_id','ID Sumber'],['qty','Qty','qty'],['released_qty','Diselesaikan','qty'],['expires_at','Berlaku Sampai'],['status','Status','status']]},
reorder:{title:'Stok Minimum dan Reorder',cols:[['kode_detail','Kode'],['nama_barang','Barang'],['warehouse_name','Gudang'],['qty','Qty','qty'],['reorder_point','Reorder Point','qty'],['minimum_stock','Minimum','qty']]},
slow:{title:'Slow-moving dan Dead Stock',cols:[['kode_detail','Kode'],['nama_barang','Barang'],['stok','Stok','qty'],['last_movement_at','Terakhir Bergerak'],['idle_days','Tidak Bergerak (hari)','qty']]},
damage:{title:'Barang Rusak dan Hilang',cols:[['document_no','Dokumen'],['document_date','Tanggal'],['document_type','Jenis'],['kode_detail','Kode'],['nama_barang','Barang'],['warehouse_name','Gudang'],['qty','Qty','qty'],['value','Nilai','money'],['status','Status','status'],['reason','Alasan']]},
opname:{title:'Stock Opname',cols:[['document_no','Dokumen'],['document_date','Tanggal'],['kode_detail','Kode'],['nama_barang','Barang'],['warehouse_name','Gudang'],['system_qty','Stok Sistem','qty'],['counted_qty','Fisik','qty'],['difference','Selisih','qty'],['status','Status','status']]},
reconciliation:{title:'Riwayat Rekonsiliasi',cols:[['as_of_date','Tanggal'],['ledger_value','Ledger','money'],['stock_log_value','Stock Log','money'],['gl_value','General Ledger','money'],['difference','Selisih','money'],['barcode_variance_count','Selisih Barcode','qty'],['cache_variance_count','Selisih Cache','qty'],['status','Status','status']]}
};
let current=[],dataTable=null;
$('#reportItem').select2({width:'100%',placeholder:'Semua / cari barang',allowClear:true,ajax:{url:'<?=base_url('inventoryprofessional/lookup_items')?>',dataType:'json',delay:250,data:p=>({q:p.term||'',warehouse_id:$('#reportWarehouse').val()}),processResults:r=>({results:r.results})}});
function params(){return{type:$('#reportType').val(),warehouse_id:$('#reportWarehouse').val(),item_id:$('#reportItem').val(),date_from:$('#dateFrom').val(),date_to:$('#dateTo').val(),days:$('#reportDays').val()}}
function fail(x){AppAlert.toast(x.responseJSON&&x.responseJSON.message||'Laporan tidak dapat dimuat.','error')}
function cell(value,kind,key,row){if(key==='document_no'&&row.document_id&&['opening','in','out','transfer','adjustment','opname','damaged','lost'].includes(row.document_type))return'<a class="fw-semibold" href="<?=base_url('inventoryprofessional/operations')?>?focus='+encodeURIComponent(row.document_id)+'">'+esc(value)+'</a>';if(kind==='money')return money(value);if(kind==='qty')return qty(value);if(kind==='status')return'<span class="status-chip status-'+esc(value)+'">'+esc(String(value||'-').replaceAll('_',' '))+'</span>';return esc(value)}
function render(type,rows){current=rows;const def=reports[type];if(dataTable){dataTable.destroy();dataTable=null}$('#reportTable thead').html('<tr>'+def.cols.map(c=>'<th>'+esc(c[1])+'</th>').join('')+'</tr>');$('#reportTable tbody').html(rows.map(r=>'<tr>'+def.cols.map(c=>'<td class="'+(c[2]==='money'||c[2]==='qty'?'text-end':'')+'">'+cell(r[c[0]],c[2],c[0],r)+'</td>').join('')+'</tr>').join('')||'<tr><td colspan="'+def.cols.length+'" class="text-center text-muted py-5">Tidak ada data pada filter yang dipilih.</td></tr>');if(rows.length)dataTable=$('#reportTable').DataTable({responsive:true,pageLength:25,order:[],searchDelay:250,language:{search:'Cari:',lengthMenu:'Tampilkan _MENU_',info:'_START_–_END_ dari _TOTAL_',paginate:{previous:'Sebelumnya',next:'Berikutnya'}}})}
function load(){let type=$('#reportType').val(),b=$('#loadReport').prop('disabled',true);$('#reportTitle').text(reports[type].title);$('#reportTable tbody').html('<tr><td class="text-center py-5"><span class="spinner-border spinner-border-sm"></span> Memuat laporan...</td></tr>');$.get('<?=base_url('inventoryprofessional/report_data')?>',params()).done(r=>render(type,r.data)).fail(fail).always(()=>b.prop('disabled',false))}
$('#reportType').on('change',function(){let type=this.value;$('.slow-filter').toggleClass('d-none',type!=='slow');$('.item-filter').toggleClass('d-none',type==='reconciliation');$('.date-filter').toggleClass('d-none',['valuation','location','barcode','batch','reorder','slow'].includes(type));load()});$('#loadReport').on('click',load);
$('#exportPdf').on('click',function(){let q=$.param(params());$('#reportPdfModal .modal-title').text(reports[$('#reportType').val()].title);$('#reportPdfModal iframe').attr('src','<?=base_url('inventoryprofessional/report_pdf')?>?'+q);bootstrap.Modal.getOrCreateInstance(document.getElementById('reportPdfModal')).show()});
$('#exportExcel').on('click',function(){window.location.href='<?=base_url('inventoryprofessional/report_excel')?>?'+$.param(params())});
function issueList(details){let rows=[];(details.ledger_vs_stock_log||[]).forEach(x=>rows.push('<li><strong>'+esc(x.kode_detail+' - '+x.nama_barang)+'</strong><span>'+esc(x.warehouse_name)+' · Selisih qty '+qty(x.qty_difference)+' · Selisih nilai '+money(x.value_difference)+'</span></li>'));(details.barcode||[]).forEach(x=>rows.push('<li><strong>'+esc(x.kode_detail+' - '+x.nama_barang)+'</strong><span>'+esc(x.warehouse_name)+' · Ledger '+qty(x.ledger_qty)+' · Barcode '+qty(x.barcode_qty)+' · Selisih '+qty(x.difference)+'</span></li>'));(details.cache||[]).forEach(x=>rows.push('<li><strong>'+esc(x.kode_detail+' - '+x.nama_barang)+'</strong><span>Cache '+qty(x.cache_qty)+' · Ledger '+qty(x.ledger_qty)+'</span></li>'));return rows.length?'<div class="alert alert-warning py-2"><strong>Sumber anomali</strong><ul class="timeline-list mt-2 mb-0">'+rows.slice(0,12).join('')+'</ul></div>':'<div class="alert alert-success py-2">Tidak ada rincian anomali operasional.</div>'}
$('#runReconciliation').on('click',function(){let b=$(this).prop('disabled',true);$.post('<?=base_url('inventoryprofessional/reconciliation')?>',Object.assign({date:$('#reconcileDate').val()},csrf())).done(r=>{let x=r.result;$('#reconcileResult').html('<div><span>Inventory Ledger</span><strong>'+money(x.ledger_value)+'</strong></div><div><span>Stock Log</span><strong>'+money(x.stock_log_value)+'</strong></div><div><span>General Ledger</span><strong>'+money(x.gl_value)+'</strong></div><div><span>Selisih Ledger–GL</span><strong>'+money(x.difference)+'</strong></div><div><span>Selisih Cache</span><strong>'+qty(x.cache_variance_count)+' barang</strong></div><div><span>Selisih Barcode</span><strong>'+qty(x.barcode_variance_count)+' posisi</strong></div><div><span>Status</span><strong><span class="status-chip status-'+(x.status==='matched'?'approved':'rejected')+'">'+esc(x.status)+'</span></strong></div>');$('#reconcileIssues').html(issueList(x.details||{}));AppAlert.toast(r.message,x.status==='matched'?'success':'warning')}).fail(fail).always(()=>b.prop('disabled',false))});
load();
});
</script>