Files
accounting_dev_v2/assets/js/technician-equipment-bulk.js
T
2026-09-11 16:03:00 +07:00

337 lines
22 KiB
JavaScript

(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 = '&times;'; 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);