64 lines
13 KiB
PHP
64 lines
13 KiB
PHP
<?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>
|