Update besar apk finance
This commit is contained in:
@@ -0,0 +1,336 @@
|
||||
(function ($) {
|
||||
'use strict';
|
||||
|
||||
const cfg = window.TECH_EQUIPMENT_BULK || {};
|
||||
const base = cfg.base || '/';
|
||||
const form = document.getElementById('techBulkForm');
|
||||
if (!form) return;
|
||||
|
||||
let action = '';
|
||||
let selected = [];
|
||||
let searchResults = [];
|
||||
let technicianHoldings = [];
|
||||
let visibleHoldings = [];
|
||||
let holdingsRequest = null;
|
||||
const fileSelections = new WeakMap();
|
||||
|
||||
const esc = value => $('<div>').text(value == null ? '' : String(value)).html();
|
||||
const number = value => {
|
||||
const n = Number(value || 0);
|
||||
return Number.isInteger(n) ? n.toLocaleString('id-ID') : n.toLocaleString('id-ID', {maximumFractionDigits: 4});
|
||||
};
|
||||
const toast = (ok, message) => {
|
||||
if (window.Swal) Swal.fire({toast: true, position: 'top-end', icon: ok ? 'success' : 'error', title: message, showConfirmButton: false, timer: ok ? 2600 : 4500, timerProgressBar: true});
|
||||
else alert(message);
|
||||
};
|
||||
const key = () => 'TEQ-BATCH-' + Date.now() + '-' + Math.random().toString(36).slice(2);
|
||||
|
||||
function modal(id, show) {
|
||||
const element = document.getElementById(id);
|
||||
if (!element) return;
|
||||
const instance = bootstrap.Modal.getOrCreateInstance(element, {backdrop: 'static', keyboard: false});
|
||||
show ? instance.show() : instance.hide();
|
||||
}
|
||||
|
||||
function initEmployeeSearch() {
|
||||
if (!$.fn.select2) return;
|
||||
$(form).find('select[name="technician_id"],select[name="target_technician_id"]').each(function () {
|
||||
const select = $(this);
|
||||
if (select.hasClass('select2-hidden-accessible')) return;
|
||||
select.select2({width: '100%', placeholder: select.find('option:first').text() || 'Cari karyawan', allowClear: true});
|
||||
});
|
||||
}
|
||||
|
||||
function setAction(next, resetItems) {
|
||||
action = next;
|
||||
form.elements.action.value = action;
|
||||
form.elements.idempotency_key.value = key();
|
||||
document.querySelectorAll('.tech-bulk-action').forEach(button => button.classList.toggle('active', button.dataset.action === action));
|
||||
const isTransfer = action === 'transfer';
|
||||
const isReturn = action === 'return';
|
||||
$('[data-bulk-field="target"]').toggleClass('d-none', !isTransfer).find('select').prop('required', isTransfer);
|
||||
$('[data-bulk-field="warehouse"]').toggleClass('d-none', !isReturn).find('select').prop('required', false);
|
||||
$('[data-bulk-field="proof"]').toggleClass('d-none', !isReturn);
|
||||
$('[data-tech-label]').text(action === 'handover' ? 'Karyawan Tujuan' : 'Karyawan Asal');
|
||||
form.elements.technician_id.required = true;
|
||||
if (resetItems) { clearItems(); clearFileBox(form.querySelector('[data-filebox]')); }
|
||||
$('#techBulkSearch').attr('placeholder', action === 'handover' ? 'Scan barang gudang atau cari nama barang' : 'Scan barcode/SN atau cari dari daftar barang teknisi');
|
||||
$('#techTechnicianStock').toggleClass('d-none', !(isTransfer || isReturn));
|
||||
if (isTransfer || isReturn) {
|
||||
if (form.elements.technician_id.value) loadTechnicianHoldings();
|
||||
else renderTechnicianHoldings();
|
||||
} else {
|
||||
technicianHoldings = [];
|
||||
visibleHoldings = [];
|
||||
}
|
||||
}
|
||||
|
||||
function clearItems() {
|
||||
selected = [];
|
||||
renderItems();
|
||||
renderTechnicianHoldings();
|
||||
$('#techBulkSearchResults').addClass('d-none').empty();
|
||||
}
|
||||
|
||||
function itemKey(item) {
|
||||
return action === 'handover' ? 'S' + item.subject_id : 'A' + item.allocation_id;
|
||||
}
|
||||
|
||||
function addItem(item, silent) {
|
||||
if (selected.some(row => itemKey(row) === itemKey(item))) {
|
||||
if (!silent) toast(false, 'Barang tersebut sudah ada dalam daftar. Ubah jumlah pada baris yang tersedia.');
|
||||
return false;
|
||||
}
|
||||
if (selected.length >= 200) {
|
||||
if (!silent) toast(false, 'Maksimal 200 baris dalam satu dokumen batch.');
|
||||
return false;
|
||||
}
|
||||
item.qty = String(item.tracking_type).toUpperCase() === 'UNIT' ? 1 : Number(item.available_qty);
|
||||
item.notes = '';
|
||||
selected.push(item);
|
||||
renderItems();
|
||||
$('#techBulkSearch').val('').trigger('focus');
|
||||
renderTechnicianHoldings('');
|
||||
$('#techBulkSearchResults').addClass('d-none').empty();
|
||||
return true;
|
||||
}
|
||||
|
||||
function renderItems() {
|
||||
const body = $('#techBulkItems tbody');
|
||||
if (!selected.length) {
|
||||
body.html('<tr class="tech-bulk-empty"><td colspan="7" class="tech-empty">Belum ada barang yang dipilih.</td></tr>');
|
||||
} else {
|
||||
body.html(selected.map((item, index) => {
|
||||
const unit = String(item.tracking_type).toUpperCase() === 'UNIT';
|
||||
return '<tr data-index="' + index + '">' +
|
||||
'<td><strong>' + esc(item.display_name) + '</strong><br><small>' + esc(item.ownership_type === 'company' ? 'Milik Perusahaan' : 'Barang Customer / Non-Persediaan') + '</small></td>' +
|
||||
'<td>' + esc(item.barcode) + '<br><small>' + esc(item.serial_number || '-') + '</small></td>' +
|
||||
'<td><span class="tech-status">' + esc(item.tracking_type) + '</span></td>' +
|
||||
'<td>' + number(item.available_qty) + '</td>' +
|
||||
'<td>' + (unit ? '<strong>1</strong>' : '<input class="form-control tech-bulk-qty" inputmode="decimal" value="' + esc(item.qty) + '" min="0.0001" max="' + esc(item.available_qty) + '">') + '</td>' +
|
||||
'<td><input class="form-control tech-bulk-note" value="' + esc(item.notes) + '" placeholder="Opsional"></td>' +
|
||||
'<td><button type="button" class="btn btn-outline-danger tech-bulk-remove" title="Hapus baris"><i class="bi bi-x-lg"></i></button></td>' +
|
||||
'</tr>';
|
||||
}).join(''));
|
||||
}
|
||||
$('#techBulkCount').text(selected.length);
|
||||
$('#techBulkSummary').text(selected.length ? selected.length + ' barang siap diposting dalam satu dokumen' : 'Belum ada barang dipilih');
|
||||
$(form).find('[type=submit]').prop('disabled', !selected.length);
|
||||
const needsCompanyWarehouse = action === 'return' && selected.some(item => item.subject_type === 'company_barcode' || item.ownership_type === 'transferred_non_inventory');
|
||||
$('[data-bulk-field="warehouse"]').find('select').prop('required', needsCompanyWarehouse);
|
||||
$('#techWarehouseHint').text(needsCompanyWarehouse ? 'Wajib karena terdapat barang milik perusahaan.' : 'Tidak diperlukan jika seluruh barang adalah milik customer.');
|
||||
}
|
||||
|
||||
const holdingStatus = value => ({carried: 'Sedang Dibawa', removed: 'Hasil Pelepasan', returned: 'Dikembalikan'}[value] || value || '-');
|
||||
|
||||
function renderTechnicianHoldings(query) {
|
||||
const panel = $('#techTechnicianStock');
|
||||
if (!panel.length || action === 'handover') return;
|
||||
panel.removeClass('d-none');
|
||||
const normalized = String(query == null ? $('#techBulkSearch').val() : query).trim().toLowerCase();
|
||||
visibleHoldings = technicianHoldings.filter(item => !normalized || [item.display_name, item.barcode, item.serial_number, item.customer_name, item.usage_status].some(value => String(value || '').toLowerCase().includes(normalized)));
|
||||
const body = $('#techTechnicianStockBody');
|
||||
if (!form.elements.technician_id.value) body.html('<tr><td colspan="6" class="tech-empty">Pilih teknisi asal untuk menampilkan barang.</td></tr>');
|
||||
else if (!technicianHoldings.length) body.html('<tr><td colspan="6" class="tech-empty">Teknisi ini tidak mempunyai barang dalam penguasaan.</td></tr>');
|
||||
else if (!visibleHoldings.length) body.html('<tr><td colspan="6" class="tech-empty">Tidak ada barang yang cocok dengan pencarian.</td></tr>');
|
||||
else body.html(visibleHoldings.map(item => {
|
||||
const checked = selected.some(row => Number(row.allocation_id) === Number(item.allocation_id));
|
||||
return '<tr class="' + (checked ? 'tech-stock-row-selected' : '') + '"><td><input type="checkbox" class="form-check-input tech-stock-check" data-id="' + Number(item.allocation_id) + '" ' + (checked ? 'checked' : '') + '></td><td><strong>' + esc(item.display_name) + '</strong><br><small>' + esc(item.ownership_type === 'company' ? 'Milik Perusahaan' : 'Barang Customer / Non-Persediaan') + (item.customer_name ? ' · ' + esc(item.customer_name) : '') + '</small></td><td>' + esc(item.barcode) + '<br><small>' + esc(item.serial_number || '-') + '</small></td><td><span class="tech-status">' + esc(holdingStatus(item.usage_status)) + '</span><br><small>' + esc(item.condition_status || '-') + '</small></td><td>' + esc(item.tracking_type) + '</td><td><strong>' + number(item.available_qty) + '</strong></td></tr>';
|
||||
}).join(''));
|
||||
const selectedVisible = visibleHoldings.filter(item => selected.some(row => Number(row.allocation_id) === Number(item.allocation_id))).length;
|
||||
const toggle = document.getElementById('techToggleVisible');
|
||||
if (toggle) { toggle.checked = visibleHoldings.length > 0 && selectedVisible === visibleHoldings.length; toggle.indeterminate = selectedVisible > 0 && selectedVisible < visibleHoldings.length; }
|
||||
$('#techTechnicianStockInfo').text((normalized ? visibleHoldings.length + ' dari ' : '') + technicianHoldings.length + ' barang · ' + selected.length + ' dipilih');
|
||||
}
|
||||
|
||||
function loadTechnicianHoldings() {
|
||||
if (action === 'handover') return;
|
||||
const technicianId = Number(form.elements.technician_id.value || 0);
|
||||
technicianHoldings = [];
|
||||
visibleHoldings = [];
|
||||
$('#techTechnicianStock').removeClass('d-none');
|
||||
$('#techTechnicianStockBody').html('<tr><td colspan="6" class="tech-empty"><span class="spinner-border spinner-border-sm me-2"></span>Memuat barang teknisi...</td></tr>');
|
||||
$('#techTechnicianStockInfo').text('Memuat daftar...');
|
||||
if (!technicianId) return renderTechnicianHoldings();
|
||||
if (holdingsRequest && holdingsRequest.readyState !== 4) holdingsRequest.abort();
|
||||
holdingsRequest = $.getJSON(base + 'teknisi/equipment_technician_items', {technician_id: technicianId})
|
||||
.done(response => {
|
||||
if (Number(form.elements.technician_id.value || 0) !== technicianId) return;
|
||||
technicianHoldings = response.data || [];
|
||||
renderTechnicianHoldings();
|
||||
const initialQuery = $('#techBulkSearch').val().trim();
|
||||
if (initialQuery) performSearch();
|
||||
})
|
||||
.fail(xhr => {
|
||||
if (xhr.statusText === 'abort') return;
|
||||
$('#techTechnicianStockBody').html('<tr><td colspan="6" class="tech-empty text-danger">Daftar barang teknisi gagal dimuat.</td></tr>');
|
||||
toast(false, (xhr.responseJSON && xhr.responseJSON.message) || 'Daftar barang teknisi gagal dimuat.');
|
||||
});
|
||||
}
|
||||
|
||||
function renderSearch(rows) {
|
||||
const target = $('#techBulkSearchResults');
|
||||
if (!rows.length) {
|
||||
target.removeClass('d-none').html('<div class="tech-empty py-3">Barang tidak ditemukan atau tidak tersedia pada posisi yang dipilih.</div>');
|
||||
return;
|
||||
}
|
||||
target.removeClass('d-none').html(rows.map((item, index) => '<button type="button" class="tech-search-option" data-index="' + index + '"><span><strong>' + esc(item.display_name) + '</strong><small>' + esc(item.barcode) + (item.serial_number ? ' · SN ' + esc(item.serial_number) : '') + (item.technician_name ? ' · ' + esc(item.technician_name) : '') + '</small></span><span><b>' + number(item.available_qty) + '</b><small>' + esc(item.tracking_type) + '</small></span></button>').join(''));
|
||||
}
|
||||
|
||||
function performSearch() {
|
||||
const query = $('#techBulkSearch').val().trim();
|
||||
const technicianId = Number(form.elements.technician_id.value || 0);
|
||||
if ((action === 'transfer' || action === 'return') && !technicianId) return toast(false, 'Pilih teknisi asal terlebih dahulu.');
|
||||
if (action === 'transfer' || action === 'return') {
|
||||
if (!query) { renderTechnicianHoldings(''); return; }
|
||||
const exact = technicianHoldings.filter(item => [item.barcode, item.serial_number].some(value => String(value || '').toLowerCase() === query.toLowerCase()));
|
||||
if (exact.length === 1) addItem(exact[0]);
|
||||
else renderTechnicianHoldings(query);
|
||||
return;
|
||||
}
|
||||
if (!query) return toast(false, 'Scan barcode atau isi kata pencarian.');
|
||||
const button = $('#techBulkSearchButton').prop('disabled', true);
|
||||
$.getJSON(base + 'teknisi/equipment_search', {q: query, mode: action, technician_id: technicianId})
|
||||
.done(response => {
|
||||
searchResults = response.data || [];
|
||||
const exact = searchResults.filter(item => [item.barcode, item.serial_number].some(value => String(value || '').toLowerCase() === query.toLowerCase()));
|
||||
if (exact.length === 1) addItem(exact[0]);
|
||||
else if (searchResults.length === 1) addItem(searchResults[0]);
|
||||
else renderSearch(searchResults);
|
||||
})
|
||||
.fail(xhr => toast(false, (xhr.responseJSON && xhr.responseJSON.message) || 'Pencarian barang gagal.'))
|
||||
.always(() => button.prop('disabled', false));
|
||||
}
|
||||
|
||||
$('.tech-bulk-action').on('click', function () {
|
||||
if (this.dataset.action === action) return;
|
||||
setAction(this.dataset.action, true);
|
||||
});
|
||||
$('#techBulkSearchButton').on('click', performSearch);
|
||||
$('#techBulkSearch').on('keydown', function (event) {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
performSearch();
|
||||
}
|
||||
});
|
||||
$('#techBulkSearch').on('input', function () {
|
||||
if (action === 'transfer' || action === 'return') renderTechnicianHoldings(this.value);
|
||||
});
|
||||
$(document).on('click', '.tech-search-option', function () { addItem(searchResults[Number(this.dataset.index)]); });
|
||||
$(document).on('change', '.tech-stock-check', function () {
|
||||
const allocationId = Number(this.dataset.id);
|
||||
const item = technicianHoldings.find(row => Number(row.allocation_id) === allocationId);
|
||||
if (!item) return;
|
||||
if (this.checked) { if (!addItem(item, true)) { this.checked = false; toast(false, 'Maksimal 200 baris dalam satu dokumen batch.'); renderTechnicianHoldings(); } }
|
||||
else { selected = selected.filter(row => Number(row.allocation_id) !== allocationId); renderItems(); renderTechnicianHoldings(); }
|
||||
});
|
||||
$('#techSelectVisible').on('click', function () {
|
||||
let added = 0;
|
||||
for (const item of visibleHoldings) {
|
||||
if (selected.length >= 200) break;
|
||||
if (!selected.some(row => Number(row.allocation_id) === Number(item.allocation_id))) { item.qty = String(item.tracking_type).toUpperCase() === 'UNIT' ? 1 : Number(item.available_qty); item.notes = ''; selected.push(item); added++; }
|
||||
}
|
||||
renderItems();renderTechnicianHoldings();
|
||||
if (visibleHoldings.length > 200 || selected.length >= 200 && visibleHoldings.some(item => !selected.some(row => Number(row.allocation_id) === Number(item.allocation_id)))) toast(false, 'Maksimal 200 baris per dokumen. Sisa barang dapat diproses pada batch berikutnya.');
|
||||
else if (added) toast(true, added + ' barang ditambahkan.');
|
||||
});
|
||||
$('#techToggleVisible').on('change', function () { if (this.checked) $('#techSelectVisible').trigger('click'); else { const visibleIds = new Set(visibleHoldings.map(item => Number(item.allocation_id))); selected = selected.filter(item => !visibleIds.has(Number(item.allocation_id))); renderItems();renderTechnicianHoldings(); } });
|
||||
$('#techClearSelection').on('click', function () { selected = [];renderItems();renderTechnicianHoldings(); });
|
||||
$(document).on('click', '.tech-bulk-remove', function () {
|
||||
selected.splice(Number($(this).closest('tr').data('index')), 1);
|
||||
renderItems();
|
||||
renderTechnicianHoldings();
|
||||
});
|
||||
$(document).on('input', '.tech-bulk-qty', function () { selected[Number($(this).closest('tr').data('index'))].qty = this.value; });
|
||||
$(document).on('input', '.tech-bulk-note', function () { selected[Number($(this).closest('tr').data('index'))].notes = this.value; });
|
||||
$(form.elements.technician_id).on('change', function () {
|
||||
if (action !== 'transfer' && action !== 'return') return;
|
||||
const hadSelection = selected.length > 0;
|
||||
selected = [];renderItems();
|
||||
$('#techBulkSearch').val('');
|
||||
loadTechnicianHoldings();
|
||||
if (hadSelection) toast(true, 'Pilihan barang dikosongkan karena teknisi asal berubah.');
|
||||
});
|
||||
|
||||
form.addEventListener('submit', async function (event) {
|
||||
event.preventDefault();
|
||||
if (!form.reportValidity()) return;
|
||||
if (!selected.length) return toast(false, 'Tambahkan minimal satu barang.');
|
||||
for (const item of selected) {
|
||||
const amount = Number(item.qty);
|
||||
if (!(amount > 0) || amount > Number(item.available_qty) + 0.0001) return toast(false, 'Jumlah untuk ' + item.display_name + ' tidak valid.');
|
||||
}
|
||||
if (action === 'return' && !form.querySelector('input[type=file]').files.length) return toast(false, 'Bukti pengembalian wajib dilampirkan satu kali untuk transaksi batch.');
|
||||
const confirmed = !window.Swal || (await Swal.fire({icon: 'question', title: 'Posting ' + selected.length + ' barang?', text: 'Seluruh barang akan diproses dalam satu dokumen.', showCancelButton: true, confirmButtonText: 'Ya, posting', cancelButtonText: 'Periksa lagi', allowOutsideClick: false})).isConfirmed;
|
||||
if (!confirmed) return;
|
||||
const submit = $(form).find('[type=submit]').prop('disabled', true);
|
||||
const payload = new FormData(form);
|
||||
payload.set('idempotency_key', form.elements.idempotency_key.value || key());
|
||||
payload.set('items_json', JSON.stringify(selected.map(item => ({subject_id: item.subject_id || null, allocation_id: item.allocation_id || null, qty: Number(item.qty), notes: item.notes || ''}))));
|
||||
$.ajax({url: base + 'teknisi/process_equipment_batch', method: 'POST', data: payload, processData: false, contentType: false, dataType: 'json'})
|
||||
.done(response => {
|
||||
if (!response.status) return toast(false, response.message || 'Transaksi batch gagal.');
|
||||
toast(true, response.message);
|
||||
clearItems();
|
||||
form.elements.idempotency_key.value = key();
|
||||
clearFileBox(form.querySelector('[data-filebox]'));
|
||||
if (action === 'transfer' || action === 'return') loadTechnicianHoldings();
|
||||
})
|
||||
.fail(xhr => toast(false, (xhr.responseJSON && xhr.responseJSON.message) || 'Transaksi batch gagal diproses.'))
|
||||
.always(() => submit.prop('disabled', !selected.length));
|
||||
});
|
||||
|
||||
function setupFileBox(box) {
|
||||
if (!box) return;
|
||||
const input = box.querySelector('input[type=file]');
|
||||
fileSelections.set(box, []);
|
||||
input.addEventListener('change', () => {
|
||||
const files = (fileSelections.get(box) || []).concat(Array.from(input.files)).slice(0, 10);
|
||||
fileSelections.set(box, files);
|
||||
syncFiles(box);
|
||||
});
|
||||
}
|
||||
function syncFiles(box) {
|
||||
if (!box) return;
|
||||
const input = box.querySelector('input[type=file]');
|
||||
const add = box.querySelector('.tech-add-file');
|
||||
const files = fileSelections.get(box) || [];
|
||||
box.querySelectorAll('.tech-thumb').forEach(element => element.remove());
|
||||
files.forEach((file, index) => {
|
||||
const thumb = document.createElement('div');
|
||||
thumb.className = 'tech-thumb';
|
||||
if (file.type === 'application/pdf') thumb.innerHTML = '<div class="tech-pdf"><i class="bi bi-file-pdf fs-2 text-danger"></i></div>';
|
||||
else { const image = document.createElement('img'); image.src = URL.createObjectURL(file); image.alt = file.name; thumb.appendChild(image); }
|
||||
const remove = document.createElement('button');
|
||||
remove.type = 'button'; remove.innerHTML = '×'; remove.title = 'Hapus';
|
||||
remove.addEventListener('click', event => { event.stopPropagation(); fileSelections.set(box, (fileSelections.get(box) || []).filter((_, itemIndex) => itemIndex !== index)); syncFiles(box); });
|
||||
thumb.appendChild(remove);
|
||||
thumb.addEventListener('click', event => { if (event.target === remove) return; preview(URL.createObjectURL(file), file.type); });
|
||||
box.insertBefore(thumb, add);
|
||||
});
|
||||
const transfer = new DataTransfer();
|
||||
files.forEach(file => transfer.items.add(file));
|
||||
input.files = transfer.files;
|
||||
}
|
||||
function clearFileBox(box) { if (!box) return; fileSelections.set(box, []); syncFiles(box); }
|
||||
function preview(url, mime) {
|
||||
const body = document.querySelector('#techBulkPreviewModal [data-preview-body]');
|
||||
body.innerHTML = mime === 'application/pdf' ? '<iframe title="Preview PDF" src="' + esc(url) + '" style="width:100%;height:100%;border:0"></iframe>' : '<div class="h-100 d-grid align-items-center text-center"><img src="' + esc(url) + '" alt="Preview" style="max-width:100%;max-height:100%;margin:auto;object-fit:contain"></div>';
|
||||
modal('techBulkPreviewModal', true);
|
||||
}
|
||||
|
||||
document.querySelectorAll('[data-filebox]').forEach(setupFileBox);
|
||||
const firstAction = document.querySelector('.tech-bulk-action');
|
||||
const initial = new URLSearchParams(location.search);
|
||||
const requestedAction = initial.get('action');
|
||||
const requestedButton = requestedAction ? document.querySelector('.tech-bulk-action[data-action="' + requestedAction + '"]') : null;
|
||||
if (requestedButton || firstAction) setAction((requestedButton || firstAction).dataset.action, false);
|
||||
if (initial.get('technician_id')) form.elements.technician_id.value = initial.get('technician_id');
|
||||
else if (form.elements.technician_id.options.length === 2) form.elements.technician_id.selectedIndex = 1;
|
||||
initEmployeeSearch();
|
||||
$(form).find('select[name="technician_id"],select[name="target_technician_id"]').trigger('change.select2');
|
||||
if (initial.get('scan')) $('#techBulkSearch').val(initial.get('scan'));
|
||||
if ((action === 'transfer' || action === 'return') && form.elements.technician_id.value) loadTechnicianHoldings();
|
||||
else if (initial.get('scan')) performSearch();
|
||||
renderItems();
|
||||
})(jQuery);
|
||||
Reference in New Issue
Block a user