feat(inventaris): stok keluar/retur/opname + detail unit, dan edit slip payroll (port upstream)

- inventoryService: port penuh dari upstream (issueStock FIFO/SN, returnStock, adjustStock opname, serial tersedia, statistik, detail barang) + kompatibilitas fork
- schema: kolom price/supplier (stock) dan remaining_stock/recipient/reference_no (log) via migrasi aman
- admin: route stok keluar/retur/opname + API detail & serial; modal + tombol aksi di halaman inventaris; viewStock yang tadinya undefined
- payroll: updateSlip() + route /payroll/slip/:id/update
This commit is contained in:
oxpoe
2026-11-05 01:26:18 +07:00
parent d24cabf818
commit 95fa69e968
5 changed files with 800 additions and 83 deletions

View File

@@ -93,6 +93,9 @@
<div class="act-menu">
<button type="button" class="act-item" <%- `onclick="viewStock(${item.id}, '${item.name}')"` %>><i class="bi bi-eye"></i> <%= t('admin.inventory.detail_stock', 'Detail Stok') %></button>
<button type="button" class="act-item success" <%- `onclick="openAddStock(${item.id}, '${item.name}')"` %>><i class="bi bi-plus-circle"></i> <%= t('admin.inventory.add_stock', 'Tambah Stok') %></button>
<button type="button" class="act-item" <%- `onclick="openIssueStock(${item.id}, '${item.name}')"` %>><i class="bi bi-box-arrow-up"></i> Stok Keluar</button>
<button type="button" class="act-item" <%- `onclick="openReturnStock(${item.id}, '${item.name}')"` %>><i class="bi bi-arrow-return-left"></i> Retur</button>
<button type="button" class="act-item" <%- `onclick="openAdjustStock(${item.id}, '${item.name}')"` %>><i class="bi bi-clipboard-check"></i> Opname</button>
<button type="button" class="act-item" <%- `onclick='openEditItem(${JSON.stringify(item)})'` %>><i class="bi bi-pencil"></i> Edit Barang</button>
</div>
</div>
@@ -250,6 +253,76 @@
</div>
</div>
<!-- MODAL: STOK KELUAR -->
<div class="mo" id="modal-issue">
<div class="mb">
<div class="mh"><div class="mt">Stok Keluar</div><button class="mc" onclick="closeModal('modal-issue')">&times;</button></div>
<form action="/admin/inventory/stock/out" method="POST">
<input type="hidden" name="item_id" id="o_item_id">
<div class="mbody">
<div class="fg"><label class="fl">Nama Barang</label><input class="fc" id="o_item_name" readonly></div>
<div class="fg"><label class="fl">Serial Number (kosongkan untuk barang tanpa SN)</label><input class="fc" name="serial_number" list="o_serials" placeholder="Pilih / scan SN"><datalist id="o_serials"></datalist></div>
<div class="form-row">
<div class="fg"><label class="fl">Jumlah</label><input class="fc" type="number" name="quantity" value="1" min="1"></div>
<div class="fg"><label class="fl">Tipe</label>
<select class="fs" name="type"><option value="out">Keluar / Operasional</option><option value="assigned">Terpasang ke Pelanggan</option><option value="broken">Rusak</option><option value="loss">Hilang</option></select>
</div>
</div>
<div class="fg"><label class="fl">Penerima / Teknisi</label><input class="fc" name="recipient" placeholder="Nama teknisi / pelanggan"></div>
<div class="fg"><label class="fl">Catatan</label><textarea class="fc" name="note"></textarea></div>
</div>
<div class="mf"><button type="button" class="btn btn-g" onclick="closeModal('modal-issue')">Batal</button><button class="btn btn-p">Simpan</button></div>
</form>
</div>
</div>
<!-- MODAL: RETUR -->
<div class="mo" id="modal-return">
<div class="mb">
<div class="mh"><div class="mt">Retur Barang</div><button class="mc" onclick="closeModal('modal-return')">&times;</button></div>
<form action="/admin/inventory/stock/return" method="POST">
<input type="hidden" name="item_id" id="r_item_id">
<div class="mbody">
<div class="fg"><label class="fl">Nama Barang</label><input class="fc" id="r_item_name" readonly></div>
<div class="fg"><label class="fl">Serial Number (untuk unit ber-SN)</label><input class="fc" name="serial_number" placeholder="Kosongkan untuk barang tanpa SN"></div>
<div class="form-row">
<div class="fg"><label class="fl">Jumlah</label><input class="fc" type="number" name="quantity" value="1" min="1"></div>
<div class="fg"><label class="fl">Kondisi</label>
<select class="fs" name="condition"><option value="used">Bekas / Copotan</option><option value="new">Baru</option><option value="broken">Rusak</option></select>
</div>
</div>
<div class="fg"><label class="fl">Catatan</label><textarea class="fc" name="note"></textarea></div>
</div>
<div class="mf"><button type="button" class="btn btn-g" onclick="closeModal('modal-return')">Batal</button><button class="btn btn-p">Simpan</button></div>
</form>
</div>
</div>
<!-- MODAL: OPNAME -->
<div class="mo" id="modal-adjust">
<div class="mb">
<div class="mh"><div class="mt">Stock Opname</div><button class="mc" onclick="closeModal('modal-adjust')">&times;</button></div>
<form action="/admin/inventory/stock/adjust" method="POST">
<input type="hidden" name="item_id" id="a_item_id">
<div class="mbody">
<div class="fg"><label class="fl">Nama Barang</label><input class="fc" id="a_item_name" readonly></div>
<div class="fg"><label class="fl">Jumlah Fisik di Gudang</label><input class="fc" type="number" name="actual_quantity" min="0" required></div>
<div class="fg"><label class="fl">Catatan</label><textarea class="fc" name="note"></textarea></div>
</div>
<div class="mf"><button type="button" class="btn btn-g" onclick="closeModal('modal-adjust')">Batal</button><button class="btn btn-p">Simpan</button></div>
</form>
</div>
</div>
<!-- MODAL: DETAIL STOK -->
<div class="mo" id="modal-detail">
<div class="mb">
<div class="mh"><div class="mt" id="d_title">Detail Stok</div><button class="mc" onclick="closeModal('modal-detail')">&times;</button></div>
<div class="mbody" id="d_body"></div>
</div>
</div>
<script>
var I18N_INV = {
addTitle: "<%= t('admin.inventory.modal_add', 'Tambah Barang Baru') %>",
@@ -288,6 +361,48 @@
document.getElementById('modal-stock').classList.add('show');
}
function openIssueStock(itemId, itemName) {
document.getElementById('o_item_id').value = itemId;
document.getElementById('o_item_name').value = itemName;
var dl = document.getElementById('o_serials');
dl.innerHTML = '';
fetch('/admin/api/inventory/serials/' + itemId).then(r => r.json()).then(d => {
(d.serials || []).forEach(function (s) {
var opt = document.createElement('option');
opt.value = s.serial_number;
dl.appendChild(opt);
});
}).catch(function () {});
document.getElementById('modal-issue').classList.add('show');
}
function openReturnStock(itemId, itemName) {
document.getElementById('r_item_id').value = itemId;
document.getElementById('r_item_name').value = itemName;
document.getElementById('modal-return').classList.add('show');
}
function openAdjustStock(itemId, itemName) {
document.getElementById('a_item_id').value = itemId;
document.getElementById('a_item_name').value = itemName;
document.getElementById('modal-adjust').classList.add('show');
}
function viewStock(itemId, itemName) {
document.getElementById('d_title').innerText = 'Detail Stok — ' + itemName;
var body = document.getElementById('d_body');
body.innerHTML = 'Memuat…';
fetch('/admin/api/inventory/item/' + itemId).then(r => r.json()).then(function (d) {
if (!d.success) { body.innerText = d.message || 'Gagal memuat'; return; }
var rows = (d.stockList || []).map(function (s) {
return '<tr><td>' + (s.serial_number || '-') + '</td><td>' + s.status + '</td><td class="num">' + s.quantity + '</td><td>' + (s.customer_name || '-') + '</td></tr>';
}).join('');
body.innerHTML = '<table class="dtbl"><thead><tr><th>SN</th><th>Status</th><th class="num">Qty</th><th>Pelanggan</th></tr></thead><tbody>' + (rows || '<tr><td colspan="4" class="text-muted">Belum ada unit stok</td></tr>') + '</tbody></table>';
}).catch(function () { body.innerText = 'Gagal memuat'; });
document.getElementById('modal-detail').classList.add('show');
}
function closeModal(id) { document.getElementById(id).classList.remove('show'); }
function filterItems(q) {