(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 => $('
').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('Belum ada barang yang dipilih.'); } else { body.html(selected.map((item, index) => { const unit = String(item.tracking_type).toUpperCase() === 'UNIT'; return '' + '' + esc(item.display_name) + '
' + esc(item.ownership_type === 'company' ? 'Milik Perusahaan' : 'Barang Customer / Non-Persediaan') + '' + '' + esc(item.barcode) + '
' + esc(item.serial_number || '-') + '' + '' + esc(item.tracking_type) + '' + '' + number(item.available_qty) + '' + '' + (unit ? '1' : '') + '' + '' + '' + ''; }).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('Pilih teknisi asal untuk menampilkan barang.'); else if (!technicianHoldings.length) body.html('Teknisi ini tidak mempunyai barang dalam penguasaan.'); else if (!visibleHoldings.length) body.html('Tidak ada barang yang cocok dengan pencarian.'); else body.html(visibleHoldings.map(item => { const checked = selected.some(row => Number(row.allocation_id) === Number(item.allocation_id)); return '' + esc(item.display_name) + '
' + esc(item.ownership_type === 'company' ? 'Milik Perusahaan' : 'Barang Customer / Non-Persediaan') + (item.customer_name ? ' · ' + esc(item.customer_name) : '') + '' + esc(item.barcode) + '
' + esc(item.serial_number || '-') + '' + esc(holdingStatus(item.usage_status)) + '
' + esc(item.condition_status || '-') + '' + esc(item.tracking_type) + '' + number(item.available_qty) + ''; }).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('Memuat barang teknisi...'); $('#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('Daftar barang teknisi gagal dimuat.'); 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('
Barang tidak ditemukan atau tidak tersedia pada posisi yang dipilih.
'); return; } target.removeClass('d-none').html(rows.map((item, index) => '').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 = '
'; 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' ? '' : '
Preview
'; 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);