Barcode scanning and GTIN mapping
This commit is contained in:
+659
-49
@@ -13,6 +13,26 @@ let deliveryLineCounter = 0;
|
||||
let deliveryLocations = [];
|
||||
let currentDispenseBatches = [];
|
||||
let currentDispenseLegacyQuantity = 0;
|
||||
let _gtinMappingPendingRefresh = false;
|
||||
let _gtinMappingPendingVariantId = null;
|
||||
let _gtinMappingPendingRestore = null; // { drugId, variantId, packId } — auto-select after reload
|
||||
let _gtinMappingWaitingForNewDrug = null; // Set of drug IDs before add-drug; resolved on first loadDrugs
|
||||
|
||||
/** Build a human-readable pack label from pack fields, e.g. "Box of 28" */
|
||||
function packLabel(packOrUnitName, packSize) {
|
||||
// Accept either (pack object) or (unit_name string, size number)
|
||||
let unitName, size;
|
||||
if (typeof packOrUnitName === 'object' && packOrUnitName !== null) {
|
||||
unitName = packOrUnitName.pack_unit_name;
|
||||
size = packOrUnitName.pack_size_in_base_units;
|
||||
} else {
|
||||
unitName = packOrUnitName;
|
||||
size = packSize;
|
||||
}
|
||||
const displaySize = size === Math.floor(size) ? Math.floor(size) : size;
|
||||
const unit = String(unitName || 'pack');
|
||||
return `${unit.charAt(0).toUpperCase()}${unit.slice(1)} of ${displaySize}`;
|
||||
}
|
||||
|
||||
function resetDisposeBatchModal() {
|
||||
const form = document.getElementById('disposeBatchForm');
|
||||
@@ -132,10 +152,12 @@ function showMainApp() {
|
||||
const addDrugBtn = document.getElementById('addDrugBtn');
|
||||
const dispenseBtn = document.getElementById('dispenseBtn');
|
||||
const printNotesBtn = document.getElementById('printNotesBtn');
|
||||
const receiveDeliveryBtn = document.getElementById('receiveDeliveryBtn');
|
||||
|
||||
if (addDrugBtn) addDrugBtn.style.display = isReadOnly ? 'none' : 'block';
|
||||
if (dispenseBtn) dispenseBtn.style.display = isReadOnly ? 'none' : 'block';
|
||||
if (printNotesBtn) printNotesBtn.style.display = isReadOnly ? 'none' : 'block';
|
||||
if (receiveDeliveryBtn) receiveDeliveryBtn.style.display = isReadOnly ? 'none' : 'block';
|
||||
|
||||
setupEventListeners();
|
||||
loadDrugs();
|
||||
@@ -287,7 +309,10 @@ function setupEventListeners() {
|
||||
|
||||
const receiveDeliveryForm = document.getElementById('receiveDeliveryForm');
|
||||
if (receiveDeliveryForm) receiveDeliveryForm.addEventListener('submit', handleReceiveDelivery);
|
||||
if (cancelReceiveDeliveryBtn) cancelReceiveDeliveryBtn.addEventListener('click', () => closeModal(receiveDeliveryModal));
|
||||
if (cancelReceiveDeliveryBtn) cancelReceiveDeliveryBtn.addEventListener('click', () => {
|
||||
_detachDeliveryBarcodeListener();
|
||||
closeModal(receiveDeliveryModal);
|
||||
});
|
||||
if (addDeliveryLineBtn) addDeliveryLineBtn.addEventListener('click', () => appendDeliveryLine());
|
||||
if (addVariantFromDeliveryBtn) addVariantFromDeliveryBtn.addEventListener('click', handleAddVariantFromDelivery);
|
||||
if (addPackSizeFromDeliveryBtn) addPackSizeFromDeliveryBtn.addEventListener('click', openAddPackSizeFromDeliveryModal);
|
||||
@@ -337,6 +362,7 @@ function setupEventListeners() {
|
||||
|
||||
if (addDrugBtn) addDrugBtn.addEventListener('click', () => openModal(addModal));
|
||||
if (printNotesBtn) printNotesBtn.addEventListener('click', () => openModal(printNotesModal));
|
||||
if (receiveDeliveryBtn) receiveDeliveryBtn.addEventListener('click', openReceiveDeliveryModal);
|
||||
if (dispenseBtn) dispenseBtn.addEventListener('click', () => {
|
||||
updateDispenseDrugSelect();
|
||||
updateDispenseModeUi();
|
||||
@@ -382,6 +408,9 @@ function setupEventListeners() {
|
||||
if (modal?.id === 'disposeBatchModal') {
|
||||
resetDisposeBatchModal();
|
||||
}
|
||||
if (modal?.id === 'receiveDeliveryModal') {
|
||||
_detachDeliveryBarcodeListener();
|
||||
}
|
||||
closeModal(modal);
|
||||
}));
|
||||
|
||||
@@ -470,6 +499,54 @@ async function loadDrugs() {
|
||||
updateLocationFilterOptions();
|
||||
renderDrugs();
|
||||
updateDispenseDrugSelect();
|
||||
|
||||
if (_gtinMappingPendingRefresh) {
|
||||
_gtinMappingPendingRefresh = false;
|
||||
const restore = _gtinMappingPendingRestore || {};
|
||||
_gtinMappingPendingRestore = null;
|
||||
_gtinMappingPendingVariantId = null;
|
||||
|
||||
// Resolve new variant by diffing (add-variant flow)
|
||||
if (restore._existingVariantIds && restore.drugId) {
|
||||
const drug = allDrugs.find(d => d.id === restore.drugId);
|
||||
const newVariant = drug?.variants?.find(v => !restore._existingVariantIds.has(v.id));
|
||||
if (newVariant) {
|
||||
restore.variantId = newVariant.id;
|
||||
// If no pack snapshot, all packs are new — pick the first active one
|
||||
if (!restore._existingPackIds) {
|
||||
const firstPack = getActivePacksForVariant(newVariant)?.[0];
|
||||
if (firstPack) restore.packId = firstPack.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Resolve new pack by diffing (add-pack flow)
|
||||
if (restore._existingPackIds && restore.drugId && restore.variantId) {
|
||||
const drug = allDrugs.find(d => d.id === restore.drugId);
|
||||
const variant = drug?.variants?.find(v => v.id === restore.variantId);
|
||||
const newPack = getActivePacksForVariant(variant)?.find(p => !restore._existingPackIds.has(p.id));
|
||||
if (newPack) restore.packId = newPack.id;
|
||||
}
|
||||
|
||||
_reinitGtinMappingModal(restore);
|
||||
}
|
||||
|
||||
// After handleAddDrug's loadDrugs fires: find the newly created drug and set up
|
||||
// _gtinMappingPendingRefresh so that when handleAddVariant calls loadDrugs next,
|
||||
// we auto-select drug + new variant in the GTIN modal.
|
||||
if (_gtinMappingWaitingForNewDrug) {
|
||||
const newDrug = allDrugs.find(d => !_gtinMappingWaitingForNewDrug.has(d.id));
|
||||
_gtinMappingWaitingForNewDrug = null;
|
||||
if (newDrug) {
|
||||
// handleAddDrug will now open addVariantModal — prepare to catch that save
|
||||
_gtinMappingPendingRestore = {
|
||||
drugId: newDrug.id,
|
||||
variantId: null,
|
||||
packId: null,
|
||||
_existingVariantIds: new Set((newDrug.variants || []).map(v => v.id))
|
||||
};
|
||||
_gtinMappingPendingRefresh = true;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading drugs:', error);
|
||||
document.getElementById('drugsList').innerHTML =
|
||||
@@ -745,7 +822,7 @@ function populateDispensePackSelect(variant) {
|
||||
activePacks.forEach(pack => {
|
||||
const option = document.createElement('option');
|
||||
option.value = String(pack.id);
|
||||
option.textContent = `${pack.label} (${pack.pack_size_in_base_units} ${variant.unit})`;
|
||||
option.textContent = `${packLabel(pack)} (${pack.pack_size_in_base_units} ${variant.unit})`;
|
||||
packSelect.appendChild(option);
|
||||
});
|
||||
|
||||
@@ -872,7 +949,7 @@ function renderVariantInventoryDetails(variant) {
|
||||
const packsHtml = activePacks.length > 0
|
||||
? activePacks.map(pack => `
|
||||
<div style="padding: 6px 8px; background: #ffffff; border: 1px solid #d8e2ea; border-radius: 5px; font-size: 0.9em;">
|
||||
<strong>${escapeHtml(pack.label)}</strong>
|
||||
<strong>${escapeHtml(packLabel(pack))}</strong>
|
||||
<span style="color: #4b5563;"> (${formatDisplayNumber(pack.pack_size_in_base_units)} ${escapeHtml(variant.unit)})</span>
|
||||
</div>
|
||||
`).join('')
|
||||
@@ -882,9 +959,9 @@ function renderVariantInventoryDetails(variant) {
|
||||
? batches.map(batch => {
|
||||
const locationLabel = getBatchLocationLabel(batch);
|
||||
const expired = isBatchExpired(batch);
|
||||
const hasPackState = batch.current_full_pack_count != null && batch.current_loose_base_units != null && batch.received_pack_label;
|
||||
const hasPackState = batch.current_full_pack_count != null && batch.current_loose_base_units != null && batch.received_pack_unit_name;
|
||||
const stocktakeLabel = hasPackState
|
||||
? `${formatDisplayNumber(batch.current_full_pack_count)} full ${escapeHtml(batch.received_pack_label)} + ${formatDisplayNumber(batch.current_loose_base_units)} ${escapeHtml(variant.unit)} loose`
|
||||
? `${formatDisplayNumber(batch.current_full_pack_count)} full ${escapeHtml(packLabel(batch.received_pack_unit_name, batch.received_pack_size_snapshot))} + ${formatDisplayNumber(batch.current_loose_base_units)} ${escapeHtml(variant.unit)} loose`
|
||||
: `${formatDisplayNumber(batch.quantity)} ${escapeHtml(variant.unit)}`;
|
||||
const batchCardStyles = expired
|
||||
? 'padding: 8px; background: #fff1f2; border: 1px solid #f3a6ad; border-radius: 5px; font-size: 0.9em;'
|
||||
@@ -1043,8 +1120,8 @@ function batchMatchesSelectedPack(batch, selectedPack) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const batchPackLabel = String(batch.received_pack_label || '').trim().toLowerCase();
|
||||
const selectedPackLabel = String(selectedPack.label || '').trim().toLowerCase();
|
||||
const batchPackLabel = String(batch.received_pack_unit_name || '').trim().toLowerCase();
|
||||
const selectedPackLabel = String(selectedPack.pack_unit_name || '').trim().toLowerCase();
|
||||
if (batchPackLabel && selectedPackLabel && batchPackLabel === selectedPackLabel) {
|
||||
return true;
|
||||
}
|
||||
@@ -1152,8 +1229,8 @@ function renderDispenseBatchAllocationRows(activeBatches) {
|
||||
</div>
|
||||
</div>
|
||||
<div style="font-size: 0.9em; color: #374151;">
|
||||
${batch.current_full_pack_count != null && batch.current_loose_base_units != null && batch.received_pack_label
|
||||
? `Stock: ${formatDisplayNumber(batch.current_full_pack_count)} full ${escapeHtml(batch.received_pack_label)} + ${formatDisplayNumber(batch.current_loose_base_units)} ${escapeHtml(unitLabel)} loose`
|
||||
${batch.current_full_pack_count != null && batch.current_loose_base_units != null && batch.received_pack_unit_name
|
||||
? `Stock: ${formatDisplayNumber(batch.current_full_pack_count)} full ${escapeHtml(packLabel(batch.received_pack_unit_name, batch.received_pack_size_snapshot))} + ${formatDisplayNumber(batch.current_loose_base_units)} ${escapeHtml(unitLabel)} loose`
|
||||
: ''}
|
||||
${batchAvailabilityNote ? `<div style="margin-top: 4px; color: #d32f2f;">${batchAvailabilityNote}</div>` : ''}
|
||||
</div>
|
||||
@@ -1204,8 +1281,8 @@ function renderExpiredDispenseBatches(expiredBatches) {
|
||||
expiredContent.innerHTML = expiredBatches.map(batch => {
|
||||
const locationLabel = getBatchLocationLabel(batch);
|
||||
const expiryLabel = formatDisplayDate(batch.expiry_date);
|
||||
const stocktakeLabel = batch.current_full_pack_count != null && batch.current_loose_base_units != null && batch.received_pack_label
|
||||
? `${formatDisplayNumber(batch.current_full_pack_count)} full ${escapeHtml(batch.received_pack_label)} + ${formatDisplayNumber(batch.current_loose_base_units)} ${escapeHtml(unitLabel)} loose`
|
||||
const stocktakeLabel = batch.current_full_pack_count != null && batch.current_loose_base_units != null && batch.received_pack_unit_name
|
||||
? `${formatDisplayNumber(batch.current_full_pack_count)} full ${escapeHtml(packLabel(batch.received_pack_unit_name, batch.received_pack_size_snapshot))} + ${formatDisplayNumber(batch.current_loose_base_units)} ${escapeHtml(unitLabel)} loose`
|
||||
: `${formatDisplayNumber(batch.quantity)} ${escapeHtml(unitLabel)}`;
|
||||
|
||||
return `
|
||||
@@ -1608,7 +1685,6 @@ function renderDrugs() {
|
||||
</div>
|
||||
<div class="drug-actions">
|
||||
${!isReadOnly ? `
|
||||
<button class="btn btn-success btn-small" onclick="event.stopPropagation(); openReceiveDeliveryModal(${drug.id})">📦 Receive Delivery</button>
|
||||
<button class="btn btn-primary btn-small" onclick="event.stopPropagation(); openAddVariantModal(${drug.id})">➕ Add</button>
|
||||
` : ''}
|
||||
<button class="btn btn-info btn-small" onclick="event.stopPropagation(); showDrugHistory(${drug.id})">📋 History</button>
|
||||
@@ -2987,7 +3063,7 @@ function buildDeliveryPackOptions(variant, selectedPackId = '') {
|
||||
|
||||
return [`<option value="">-- Select pack --</option>`, ...packs.map(pack => {
|
||||
const selected = String(pack.id) === String(selectedPackId) ? ' selected' : '';
|
||||
const label = `${pack.label} (${pack.pack_size_in_base_units} ${variant.unit})`;
|
||||
const label = `${packLabel(pack)} (${pack.pack_size_in_base_units} ${variant.unit})`;
|
||||
return `<option value="${pack.id}"${selected}>${escapeHtml(label)}</option>`;
|
||||
})].join('');
|
||||
}
|
||||
@@ -3014,10 +3090,22 @@ function updateDeliveryLineQuantityDisplay(line) {
|
||||
}
|
||||
|
||||
function wireDeliveryLineEvents(line) {
|
||||
const drugSelect = line.querySelector('.delivery-drug-select');
|
||||
const variantSelect = line.querySelector('.delivery-variant-select');
|
||||
const packSelect = line.querySelector('.delivery-pack-select');
|
||||
const packCountInput = line.querySelector('.delivery-pack-count');
|
||||
|
||||
if (drugSelect && variantSelect) {
|
||||
drugSelect.addEventListener('change', () => {
|
||||
const drugId = parseInt(drugSelect.value || '', 10);
|
||||
const drug = allDrugs.find(d => d.id === drugId) || null;
|
||||
variantSelect.innerHTML = buildDeliveryVariantOptions(drug, '');
|
||||
if (packSelect) packSelect.innerHTML = buildDeliveryPackOptions(null, '');
|
||||
if (packCountInput) packCountInput.value = '';
|
||||
updateDeliveryLineQuantityDisplay(line);
|
||||
});
|
||||
}
|
||||
|
||||
if (variantSelect && packSelect) {
|
||||
variantSelect.addEventListener('change', () => {
|
||||
const variantId = parseInt(variantSelect.value || '', 10);
|
||||
@@ -3043,10 +3131,507 @@ function wireDeliveryLineEvents(line) {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── GS1 barcode scanning ──────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Parse a GS1-128 / DataMatrix scan string.
|
||||
* Handles fixed-length AIs: 01 (GTIN-14), 17 (expiry YYMMDD), then 10 (lot).
|
||||
* Returns { gtin, expiry (Date), lot } or null if the string doesn't match.
|
||||
*/
|
||||
// GS1 AI fixed-length lookup (number of data digits after the AI prefix).
|
||||
// AIs not listed here are treated as variable-length (terminated by GS/FNC1 or end of string).
|
||||
const GS1_FIXED_LENGTHS = {
|
||||
'00': 18, '01': 14, '02': 14,
|
||||
'11': 6, '12': 6, '13': 6, '14': 6, '15': 6, '16': 6, '17': 6, '18': 6, '19': 6,
|
||||
'20': 2,
|
||||
'31': 6, '32': 6, '33': 6, '34': 6, '35': 6, '36': 6,
|
||||
'41': 13,
|
||||
};
|
||||
|
||||
// 2-digit AI prefixes we recognise enough to skip over.
|
||||
const GS1_KNOWN_AI_PREFIXES = new Set([
|
||||
'00','01','02','10','11','12','13','14','15','16','17','18','19',
|
||||
'20','21','22','23','24','25','26',
|
||||
'30','31','32','33','34','35','36','37',
|
||||
'40','41','42','43',
|
||||
'70','71','72','73','74','75','76','77','78','79',
|
||||
'80','81','82','83','84','85','86','87','88','89',
|
||||
'90','91','92','93','94','95','96','97','98','99',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Parse a GS1-128 / DataMatrix scan string.
|
||||
* Fixed-length AIs are consumed exactly. Variable-length AIs are terminated
|
||||
* by a GS (FNC1, \x1d) character — if no GS is present they run to end of string
|
||||
* (per GS1 spec: variable-length fields must be FNC1-terminated unless last).
|
||||
* Returns { gtin, expiry (Date), lot } or null if required fields not found.
|
||||
*/
|
||||
function parseGS1(raw) {
|
||||
if (!raw || raw.length < 16) return null;
|
||||
|
||||
// Strip any leading AIM symbology identifier e.g. "]d2", "]Q3"
|
||||
const aimPrefix = raw.match(/^\][a-zA-Z]\d/);
|
||||
let data = aimPrefix ? raw.substring(3) : raw;
|
||||
|
||||
const GS = '\x1d'; // FNC1 separator
|
||||
const hasGS = data.includes(GS);
|
||||
|
||||
let pos = 0;
|
||||
let gtin = null, expiry = null, lot = null;
|
||||
|
||||
while (pos < data.length) {
|
||||
if (data[pos] === GS) { pos++; continue; }
|
||||
if (pos + 2 > data.length) break;
|
||||
|
||||
const ai = data.substring(pos, pos + 2);
|
||||
if (!GS1_KNOWN_AI_PREFIXES.has(ai)) break;
|
||||
|
||||
pos += 2; // consume AI
|
||||
|
||||
if (GS1_FIXED_LENGTHS[ai] !== undefined) {
|
||||
// Fixed-length: consume exactly N chars
|
||||
const len = GS1_FIXED_LENGTHS[ai];
|
||||
const value = data.substring(pos, pos + len);
|
||||
pos += len;
|
||||
|
||||
if (ai === '01') {
|
||||
if (value.length === 14 && /^\d{14}$/.test(value)) gtin = value;
|
||||
} else if (ai === '17') {
|
||||
const yy = parseInt(value.substring(0, 2), 10);
|
||||
const mm = parseInt(value.substring(2, 4), 10);
|
||||
const dd = parseInt(value.substring(4, 6), 10);
|
||||
expiry = dd === 0
|
||||
? new Date(yy + 2000, mm, 0) // last day of month
|
||||
: new Date(yy + 2000, mm - 1, dd);
|
||||
}
|
||||
} else {
|
||||
// Variable-length: terminated by GS if present, otherwise end of string
|
||||
let end;
|
||||
if (hasGS) {
|
||||
const gsIdx = data.indexOf(GS, pos);
|
||||
end = gsIdx !== -1 ? gsIdx : data.length;
|
||||
} else {
|
||||
end = data.length;
|
||||
}
|
||||
const value = data.substring(pos, end);
|
||||
pos = end;
|
||||
|
||||
if (ai === '10') lot = value;
|
||||
// ai 21 (serial), 22, etc. ignored
|
||||
}
|
||||
}
|
||||
|
||||
if (!gtin || !expiry || !lot) return null;
|
||||
return { gtin, expiry, lot };
|
||||
}
|
||||
|
||||
/** Format a Date as YYYY-MM-DD for use in <input type="date"> */
|
||||
function formatDateForInput(d) {
|
||||
const yyyy = d.getFullYear();
|
||||
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const dd = String(d.getDate()).padStart(2, '0');
|
||||
return `${yyyy}-${mm}-${dd}`;
|
||||
}
|
||||
|
||||
// Buffer for keyboard-wedge barcode detection
|
||||
let _scanBuffer = [];
|
||||
let _scanBufferTimer = null;
|
||||
let _activeScanLineEl = null; // last delivery line that received focus
|
||||
let _preScanFocusedInput = null; // input that had focus when scan started
|
||||
let _preScanFocusedValue = null; // its value before any scan chars were typed
|
||||
const SCAN_MAX_GAP_MS = 50;
|
||||
const SCAN_MIN_LENGTH = 20;
|
||||
|
||||
function _onDeliveryModalKeydown(e) {
|
||||
// Only act when the receive delivery modal is open
|
||||
if (!document.getElementById('receiveDeliveryModal')?.classList.contains('show')) return;
|
||||
|
||||
// Track which delivery line last had focus
|
||||
const focusedLine = document.activeElement?.closest('.delivery-line');
|
||||
if (focusedLine) _activeScanLineEl = focusedLine;
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
if (e.key === 'Enter') {
|
||||
const raw = _scanBuffer.map(x => x.char).join('');
|
||||
console.log('[barcode] Enter received. Buffer length:', raw.length, 'Content:', raw);
|
||||
_scanBuffer = [];
|
||||
if (_scanBufferTimer) { clearTimeout(_scanBufferTimer); _scanBufferTimer = null; }
|
||||
|
||||
// Only treat as a scan if it arrived very fast
|
||||
if (raw.length >= SCAN_MIN_LENGTH) {
|
||||
e.preventDefault();
|
||||
// Restore the focused input to its pre-scan value (remove the 1 char that slipped in)
|
||||
if (_preScanFocusedInput) {
|
||||
_preScanFocusedInput.value = _preScanFocusedValue || '';
|
||||
}
|
||||
_preScanFocusedInput = null;
|
||||
_preScanFocusedValue = null;
|
||||
console.log('[barcode] Treating as scan, calling handleBarcodeScan');
|
||||
handleBarcodeScan(raw);
|
||||
} else {
|
||||
console.log('[barcode] Buffer too short for scan, ignoring');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Single printable character
|
||||
if (e.key.length === 1) {
|
||||
const gap = _scanBuffer.length > 0 ? now - _scanBuffer[_scanBuffer.length - 1].time : 0;
|
||||
|
||||
// If gap is too large, start fresh (human typed slowly)
|
||||
if (_scanBuffer.length > 0 && gap > SCAN_MAX_GAP_MS) {
|
||||
console.log('[barcode] Gap too large (' + gap + 'ms), resetting buffer');
|
||||
_scanBuffer = [];
|
||||
_preScanFocusedInput = null;
|
||||
_preScanFocusedValue = null;
|
||||
}
|
||||
|
||||
// Save the focused input + its value before the first scan char lands
|
||||
if (_scanBuffer.length === 0) {
|
||||
const active = document.activeElement;
|
||||
if (active && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA')) {
|
||||
_preScanFocusedInput = active;
|
||||
_preScanFocusedValue = active.value;
|
||||
} else {
|
||||
_preScanFocusedInput = null;
|
||||
_preScanFocusedValue = null;
|
||||
}
|
||||
} else {
|
||||
// Subsequent rapid chars — suppress them from going into the focused input
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
_scanBuffer.push({ char: e.key, time: now });
|
||||
console.log('[barcode] Buffered char:', e.key, '| gap:', gap + 'ms | buffer length:', _scanBuffer.length);
|
||||
|
||||
// Auto-clear buffer if Enter never comes
|
||||
if (_scanBufferTimer) clearTimeout(_scanBufferTimer);
|
||||
_scanBufferTimer = setTimeout(() => {
|
||||
console.log('[barcode] Buffer auto-cleared (no Enter)');
|
||||
_scanBuffer = [];
|
||||
_preScanFocusedInput = null;
|
||||
_preScanFocusedValue = null;
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleBarcodeScan(raw) {
|
||||
const parsed = parseGS1(raw);
|
||||
if (!parsed) {
|
||||
showToast('Barcode not recognised as a GS1 code', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const { gtin, expiry, lot } = parsed;
|
||||
const expiryStr = formatDateForInput(expiry);
|
||||
|
||||
// Look up GTIN mapping
|
||||
let mapping = null;
|
||||
try {
|
||||
const resp = await apiCall(`/gtin/${encodeURIComponent(gtin)}`);
|
||||
if (resp.ok) {
|
||||
mapping = await resp.json();
|
||||
} else if (resp.status !== 404) {
|
||||
throw new Error(`Server error ${resp.status}`);
|
||||
}
|
||||
} catch (err) {
|
||||
showToast('Failed to look up barcode: ' + err.message, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mapping) {
|
||||
// Unknown GTIN — open mapping modal then re-process
|
||||
openGtinMappingModal(gtin, expiryStr, lot);
|
||||
return;
|
||||
}
|
||||
|
||||
_applyBarcodeScanToLines(mapping, lot, expiryStr);
|
||||
}
|
||||
|
||||
function _applyBarcodeScanToLines(mapping, lot, expiryStr) {
|
||||
const container = document.getElementById('deliveryLinesContainer');
|
||||
if (!container) return;
|
||||
|
||||
const lines = Array.from(container.querySelectorAll('.delivery-line'));
|
||||
|
||||
// 1. Find an existing line with the same variant + lot + expiry → increment pack count
|
||||
for (const line of lines) {
|
||||
const variantId = line.querySelector('.delivery-variant-select')?.value;
|
||||
const batchVal = line.querySelector('.delivery-batch-number')?.value?.trim();
|
||||
const expiryVal = line.querySelector('.delivery-expiry-date')?.value;
|
||||
|
||||
if (
|
||||
String(variantId) === String(mapping.drug_variant_id) &&
|
||||
batchVal === lot &&
|
||||
expiryVal === expiryStr
|
||||
) {
|
||||
const countInput = line.querySelector('.delivery-pack-count');
|
||||
const current = parseFloat(countInput.value) || 0;
|
||||
countInput.value = current + 1;
|
||||
updateDeliveryLineQuantityDisplay(line);
|
||||
showToast(`Pack count incremented to ${current + 1} for lot ${lot}`, 'success');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Find any existing empty line (lot and expiry both blank) — never overwrite a filled line
|
||||
const emptyLine = lines.find(l => {
|
||||
const batch = l.querySelector('.delivery-batch-number')?.value?.trim();
|
||||
const expiry = l.querySelector('.delivery-expiry-date')?.value;
|
||||
return !batch && !expiry;
|
||||
}) || null;
|
||||
|
||||
if (emptyLine) {
|
||||
_populateDeliveryLine(emptyLine, mapping, lot, expiryStr);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Append a new line
|
||||
appendDeliveryLine();
|
||||
const newLine = container.querySelector('.delivery-line:last-child');
|
||||
if (newLine) _populateDeliveryLine(newLine, mapping, lot, expiryStr);
|
||||
}
|
||||
|
||||
function _populateDeliveryLine(line, mapping, lot, expiryStr) {
|
||||
const drugSelect = line.querySelector('.delivery-drug-select');
|
||||
const variantSelect = line.querySelector('.delivery-variant-select');
|
||||
const packSelect = line.querySelector('.delivery-pack-select');
|
||||
const batchInput = line.querySelector('.delivery-batch-number');
|
||||
const expiryInput = line.querySelector('.delivery-expiry-date');
|
||||
const packCountInput = line.querySelector('.delivery-pack-count');
|
||||
|
||||
if (drugSelect) {
|
||||
drugSelect.innerHTML = buildDeliveryDrugOptions(mapping.drug_id);
|
||||
drugSelect.value = String(mapping.drug_id);
|
||||
}
|
||||
|
||||
if (variantSelect) {
|
||||
const drug = allDrugs.find(d => d.id === mapping.drug_id) || null;
|
||||
variantSelect.innerHTML = buildDeliveryVariantOptions(drug, mapping.drug_variant_id);
|
||||
variantSelect.value = String(mapping.drug_variant_id);
|
||||
const variant = getVariantById(mapping.drug_variant_id);
|
||||
if (packSelect) {
|
||||
packSelect.innerHTML = buildDeliveryPackOptions(variant, mapping.variant_pack_id);
|
||||
packSelect.value = String(mapping.variant_pack_id);
|
||||
}
|
||||
}
|
||||
|
||||
if (batchInput) batchInput.value = lot;
|
||||
if (expiryInput) expiryInput.value = expiryStr;
|
||||
if (packCountInput && !packCountInput.value) packCountInput.value = 1;
|
||||
|
||||
updateDeliveryLineQuantityDisplay(line);
|
||||
showToast(`Populated: ${mapping.drug_name} ${mapping.variant_strength} — lot ${lot}`, 'success');
|
||||
}
|
||||
|
||||
function _detachDeliveryBarcodeListener() {
|
||||
const modalEl = document.getElementById('receiveDeliveryModal');
|
||||
if (modalEl?._barcodeListener) {
|
||||
document.removeEventListener('keydown', modalEl._barcodeListener);
|
||||
modalEl._barcodeListener = null;
|
||||
}
|
||||
_scanBuffer = [];
|
||||
_activeScanLineEl = null;
|
||||
}
|
||||
|
||||
function _refreshGtinMappingSelects() {
|
||||
// Kept for compatibility — delegates to reinit with current selections
|
||||
const drugId = parseInt(document.getElementById('gtinMappingDrugSelect')?.value || '', 10) || null;
|
||||
const variantId = parseInt(document.getElementById('gtinMappingVariantSelect')?.value || '', 10) || null;
|
||||
const packId = parseInt(document.getElementById('gtinMappingPackSelect')?.value || '', 10) || null;
|
||||
_reinitGtinMappingModal({ drugId, variantId, packId });
|
||||
}
|
||||
|
||||
// Reinitialise the GTIN mapping modal dropdowns from fresh allDrugs data,
|
||||
// optionally pre-selecting specific drug/variant/pack IDs.
|
||||
function _reinitGtinMappingModal(restore) {
|
||||
const drugSelect = document.getElementById('gtinMappingDrugSelect');
|
||||
const variantSelect = document.getElementById('gtinMappingVariantSelect');
|
||||
const packSelect = document.getElementById('gtinMappingPackSelect');
|
||||
if (!drugSelect) return;
|
||||
|
||||
// Rebuild drug list
|
||||
drugSelect.innerHTML = '<option value="">-- Select drug --</option>' +
|
||||
allDrugs.map(d => `<option value="${d.id}">${escapeHtml(d.name)}</option>`).join('');
|
||||
|
||||
const drugId = restore?.drugId || null;
|
||||
const variantId = restore?.variantId || null;
|
||||
const packId = restore?.packId || null;
|
||||
|
||||
// If no drug to restore, clear cascades and stop
|
||||
if (!drugId) {
|
||||
variantSelect.innerHTML = '<option value="">-- Select variant --</option>';
|
||||
packSelect.innerHTML = '<option value="">-- Select pack --</option>';
|
||||
return;
|
||||
}
|
||||
|
||||
drugSelect.value = String(drugId);
|
||||
const drug = allDrugs.find(d => d.id === drugId);
|
||||
|
||||
// Rebuild variant list
|
||||
variantSelect.innerHTML = '<option value="">-- Select variant --</option>';
|
||||
if (drug) {
|
||||
variantSelect.innerHTML += drug.variants.map(v =>
|
||||
`<option value="${v.id}">${escapeHtml(v.strength)} (${escapeHtml(v.unit)})</option>`
|
||||
).join('');
|
||||
}
|
||||
|
||||
if (!variantId) {
|
||||
packSelect.innerHTML = '<option value="">-- Select pack --</option>';
|
||||
return;
|
||||
}
|
||||
|
||||
variantSelect.value = String(variantId);
|
||||
const variant = drug?.variants?.find(v => v.id === variantId);
|
||||
|
||||
// Rebuild pack list
|
||||
packSelect.innerHTML = '<option value="">-- Select pack --</option>';
|
||||
if (variant) {
|
||||
const packs = getActivePacksForVariant(variant);
|
||||
packSelect.innerHTML += packs.map(p =>
|
||||
`<option value="${p.id}">${escapeHtml(packLabel(p))}</option>`
|
||||
).join('');
|
||||
if (packId) packSelect.value = String(packId);
|
||||
}
|
||||
}
|
||||
|
||||
function gtinMappingAddDrug() {
|
||||
// Snapshot current drug IDs. handleAddDrug will call loadDrugs() once (we intercept it
|
||||
// to find the new drug ID), then open addVariantModal. We hook the *subsequent* loadDrugs
|
||||
// call (from handleAddVariant) to reinit the GTIN modal with drug+variant selected.
|
||||
_gtinMappingWaitingForNewDrug = new Set(allDrugs.map(d => d.id));
|
||||
openModal(document.getElementById('addModal'));
|
||||
}
|
||||
|
||||
function gtinMappingAddVariant() {
|
||||
const drugId = parseInt(document.getElementById('gtinMappingDrugSelect').value || '', 10);
|
||||
if (!drugId) { showToast('Select a drug first', 'warning'); return; }
|
||||
const drug = allDrugs.find(d => d.id === drugId);
|
||||
const existingVariantIds = new Set((drug?.variants || []).map(v => v.id));
|
||||
// After reload, find the new variant by diffing
|
||||
_gtinMappingPendingRestore = { drugId, variantId: null, packId: null, _existingVariantIds: existingVariantIds };
|
||||
_gtinMappingPendingRefresh = true;
|
||||
openAddVariantModal(drugId);
|
||||
}
|
||||
|
||||
function gtinMappingAddPack() {
|
||||
const variantId = parseInt(document.getElementById('gtinMappingVariantSelect').value || '', 10);
|
||||
if (!variantId) { showToast('Select a variant first', 'warning'); return; }
|
||||
const drugId = parseInt(document.getElementById('gtinMappingDrugSelect').value || '', 10);
|
||||
const drug = allDrugs.find(d => d.id === drugId);
|
||||
if (!drug) return;
|
||||
const variant = drug.variants?.find(v => v.id === variantId);
|
||||
const existingPackIds = new Set((getActivePacksForVariant(variant) || []).map(p => p.id));
|
||||
_gtinMappingPendingRestore = { drugId, variantId, packId: null, _existingPackIds: existingPackIds };
|
||||
_gtinMappingPendingRefresh = true;
|
||||
deliveryDrugId = drugId;
|
||||
_gtinMappingPendingVariantId = variantId;
|
||||
openAddPackSizeFromDeliveryModal();
|
||||
}
|
||||
|
||||
// ─── GTIN mapping modal logic ──────────────────────────────────────────────
|
||||
|
||||
let _pendingGtinScan = null; // { gtin, expiryStr, lot } while mapping modal is open
|
||||
|
||||
function openGtinMappingModal(gtin, expiryStr, lot) {
|
||||
_pendingGtinScan = { gtin, expiryStr, lot };
|
||||
|
||||
document.getElementById('gtinMappingGtin').value = gtin;
|
||||
|
||||
// Populate drug selector from allDrugs
|
||||
const drugSelect = document.getElementById('gtinMappingDrugSelect');
|
||||
drugSelect.innerHTML = '<option value="">-- Select drug --</option>' +
|
||||
allDrugs.map(d => `<option value="${d.id}">${escapeHtml(d.name)}</option>`).join('');
|
||||
|
||||
document.getElementById('gtinMappingVariantSelect').innerHTML = '<option value="">-- Select variant --</option>';
|
||||
document.getElementById('gtinMappingPackSelect').innerHTML = '<option value="">-- Select pack --</option>';
|
||||
|
||||
openModal(document.getElementById('gtinMappingModal'));
|
||||
}
|
||||
|
||||
function onGtinMappingDrugChange() {
|
||||
const drugId = parseInt(document.getElementById('gtinMappingDrugSelect').value || '', 10);
|
||||
const drug = allDrugs.find(d => d.id === drugId);
|
||||
const variantSelect = document.getElementById('gtinMappingVariantSelect');
|
||||
const packSelect = document.getElementById('gtinMappingPackSelect');
|
||||
|
||||
variantSelect.innerHTML = '<option value="">-- Select variant --</option>';
|
||||
packSelect.innerHTML = '<option value="">-- Select pack --</option>';
|
||||
|
||||
if (!drug) return;
|
||||
variantSelect.innerHTML += drug.variants.map(v =>
|
||||
`<option value="${v.id}">${escapeHtml(v.strength)} (${escapeHtml(v.unit)})</option>`
|
||||
).join('');
|
||||
}
|
||||
|
||||
function onGtinMappingVariantChange() {
|
||||
const drugId = parseInt(document.getElementById('gtinMappingDrugSelect').value || '', 10);
|
||||
const variantId = parseInt(document.getElementById('gtinMappingVariantSelect').value || '', 10);
|
||||
const drug = allDrugs.find(d => d.id === drugId);
|
||||
const variant = drug?.variants?.find(v => v.id === variantId);
|
||||
const packSelect = document.getElementById('gtinMappingPackSelect');
|
||||
|
||||
packSelect.innerHTML = '<option value="">-- Select pack --</option>';
|
||||
if (!variant) return;
|
||||
|
||||
const packs = getActivePacksForVariant(variant);
|
||||
packSelect.innerHTML += packs.map(p =>
|
||||
`<option value="${p.id}">${escapeHtml(packLabel(p))}</option>`
|
||||
).join('');
|
||||
}
|
||||
|
||||
async function handleSaveGtinMapping() {
|
||||
if (!_pendingGtinScan) return;
|
||||
|
||||
const variantId = parseInt(document.getElementById('gtinMappingVariantSelect').value || '', 10);
|
||||
const packId = parseInt(document.getElementById('gtinMappingPackSelect').value || '', 10);
|
||||
|
||||
if (!variantId || !packId) {
|
||||
showToast('Please select a variant and pack', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await apiCall('/gtin', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
gtin: _pendingGtinScan.gtin,
|
||||
drug_variant_id: variantId,
|
||||
variant_pack_id: packId,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json();
|
||||
throw new Error(err.detail || 'Failed to save GTIN mapping');
|
||||
}
|
||||
|
||||
const mapping = await resp.json();
|
||||
closeModal(document.getElementById('gtinMappingModal'));
|
||||
showToast(`GTIN mapped to ${mapping.drug_name} ${mapping.variant_strength}`, 'success');
|
||||
|
||||
// Now apply the scan that triggered this
|
||||
_applyBarcodeScanToLines(mapping, _pendingGtinScan.lot, _pendingGtinScan.expiryStr);
|
||||
_pendingGtinScan = null;
|
||||
} catch (err) {
|
||||
showToast('Error saving GTIN: ' + err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function buildDeliveryDrugOptions(selectedDrugId = '') {
|
||||
return [
|
||||
'<option value="">-- Select drug --</option>',
|
||||
...allDrugs.map(d => {
|
||||
const sel = String(d.id) === String(selectedDrugId) ? ' selected' : '';
|
||||
return `<option value="${d.id}"${sel}>${escapeHtml(d.name)}</option>`;
|
||||
})
|
||||
].join('');
|
||||
}
|
||||
|
||||
function appendDeliveryLine(prefill = {}) {
|
||||
const container = document.getElementById('deliveryLinesContainer');
|
||||
const drug = getActiveDeliveryDrug();
|
||||
if (!container || !drug) return;
|
||||
if (!container) return;
|
||||
|
||||
deliveryLineCounter += 1;
|
||||
const lineId = `delivery-line-${deliveryLineCounter}`;
|
||||
@@ -3055,19 +3640,27 @@ function appendDeliveryLine(prefill = {}) {
|
||||
line.className = 'delivery-line';
|
||||
line.dataset.lineId = lineId;
|
||||
|
||||
const initialVariant = prefill.variantId
|
||||
? drug.variants.find(v => String(v.id) === String(prefill.variantId)) || null
|
||||
: drug.variants.length === 1 ? drug.variants[0] : null;
|
||||
const initialVariantId = prefill.variantId || (initialVariant ? initialVariant.id : '');
|
||||
const initialPackId = prefill.packId || (getActivePacksForVariant(initialVariant).length === 1 ? getActivePacksForVariant(initialVariant)[0].id : '');
|
||||
const initialDrug = prefill.drugId ? allDrugs.find(d => String(d.id) === String(prefill.drugId)) || null : null;
|
||||
const initialDrugId = prefill.drugId || '';
|
||||
const initialVariant = prefill.variantId && initialDrug
|
||||
? initialDrug.variants.find(v => String(v.id) === String(prefill.variantId)) || null
|
||||
: null;
|
||||
const initialVariantId = prefill.variantId || '';
|
||||
const initialPackId = prefill.packId || (initialVariant && getActivePacksForVariant(initialVariant).length === 1 ? getActivePacksForVariant(initialVariant)[0].id : '');
|
||||
const initialPackCount = prefill.packCount || '';
|
||||
|
||||
line.innerHTML = `
|
||||
<div class="delivery-line-grid">
|
||||
<div class="form-group">
|
||||
<label>Drug</label>
|
||||
<select class="delivery-drug-select" required>
|
||||
${buildDeliveryDrugOptions(initialDrugId)}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Variant</label>
|
||||
<select class="delivery-variant-select" required>
|
||||
${buildDeliveryVariantOptions(drug, initialVariantId)}
|
||||
${buildDeliveryVariantOptions(initialDrug, initialVariantId)}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -3116,25 +3709,24 @@ function appendDeliveryLine(prefill = {}) {
|
||||
}
|
||||
|
||||
function refreshDeliveryVariantSelects() {
|
||||
const drug = getActiveDeliveryDrug();
|
||||
const container = document.getElementById('deliveryLinesContainer');
|
||||
if (!drug || !container) return;
|
||||
if (!container) return;
|
||||
|
||||
container.querySelectorAll('.delivery-line').forEach(line => {
|
||||
const select = line.querySelector('.delivery-variant-select');
|
||||
const drugSelect = line.querySelector('.delivery-drug-select');
|
||||
const variantSelect = line.querySelector('.delivery-variant-select');
|
||||
const packSelect = line.querySelector('.delivery-pack-select');
|
||||
if (!select) return;
|
||||
if (!variantSelect) return;
|
||||
|
||||
const currentVariantId = select.value;
|
||||
const nextVariantId = currentVariantId || (drug.variants.length === 1 ? String(drug.variants[0].id) : '');
|
||||
select.innerHTML = buildDeliveryVariantOptions(drug, nextVariantId);
|
||||
const drugId = parseInt(drugSelect?.value || '', 10);
|
||||
const drug = allDrugs.find(d => d.id === drugId) || null;
|
||||
const currentVariantId = variantSelect.value;
|
||||
variantSelect.innerHTML = buildDeliveryVariantOptions(drug, currentVariantId);
|
||||
|
||||
const variant = getVariantById(parseInt(select.value || '', 10));
|
||||
const variant = getVariantById(parseInt(variantSelect.value || '', 10));
|
||||
if (packSelect) {
|
||||
const currentPackId = packSelect.value;
|
||||
const activePacks = getActivePacksForVariant(variant);
|
||||
const nextPackId = currentPackId || (activePacks.length === 1 ? String(activePacks[0].id) : '');
|
||||
packSelect.innerHTML = buildDeliveryPackOptions(variant, nextPackId);
|
||||
packSelect.innerHTML = buildDeliveryPackOptions(variant, currentPackId);
|
||||
}
|
||||
|
||||
updateDeliveryLineQuantityDisplay(line);
|
||||
@@ -3154,13 +3746,8 @@ async function initializeDeliveryLocations() {
|
||||
}
|
||||
}
|
||||
|
||||
async function openReceiveDeliveryModal(drugId) {
|
||||
deliveryDrugId = drugId;
|
||||
const drug = getActiveDeliveryDrug();
|
||||
if (!drug) {
|
||||
showToast('Drug not found', 'error');
|
||||
return;
|
||||
}
|
||||
async function openReceiveDeliveryModal() {
|
||||
deliveryDrugId = null;
|
||||
|
||||
const form = document.getElementById('receiveDeliveryForm');
|
||||
const container = document.getElementById('deliveryLinesContainer');
|
||||
@@ -3168,17 +3755,33 @@ async function openReceiveDeliveryModal(drugId) {
|
||||
|
||||
if (form) form.reset();
|
||||
if (container) container.innerHTML = '';
|
||||
if (label) label.textContent = `Drug: ${drug.name}`;
|
||||
if (label) label.textContent = 'Scan items or add lines manually';
|
||||
|
||||
await initializeDeliveryLocations();
|
||||
appendDeliveryLine();
|
||||
|
||||
openModal(document.getElementById('receiveDeliveryModal'));
|
||||
// Attach barcode scanner listener
|
||||
_activeScanLineEl = null;
|
||||
_scanBuffer = [];
|
||||
const modalEl = document.getElementById('receiveDeliveryModal');
|
||||
if (modalEl._barcodeListener) document.removeEventListener('keydown', modalEl._barcodeListener);
|
||||
modalEl._barcodeListener = _onDeliveryModalKeydown;
|
||||
document.addEventListener('keydown', modalEl._barcodeListener);
|
||||
console.log('[barcode] Listener attached to receiveDeliveryModal');
|
||||
|
||||
openModal(modalEl);
|
||||
}
|
||||
|
||||
function handleAddVariantFromDelivery() {
|
||||
if (!deliveryDrugId) {
|
||||
showToast('Select a drug first', 'warning');
|
||||
const deliveryContainer = document.getElementById('deliveryLinesContainer');
|
||||
const firstDrugIdStr = deliveryContainer
|
||||
? Array.from(deliveryContainer.querySelectorAll('.delivery-drug-select')).map(s => s.value).find(v => v)
|
||||
: null;
|
||||
deliveryDrugId = firstDrugIdStr ? parseInt(firstDrugIdStr, 10) : null;
|
||||
}
|
||||
if (!deliveryDrugId) {
|
||||
showToast('Select a drug on a delivery line first', 'warning');
|
||||
return;
|
||||
}
|
||||
openAddVariantModal(deliveryDrugId);
|
||||
@@ -3186,10 +3789,19 @@ function handleAddVariantFromDelivery() {
|
||||
|
||||
function openAddPackSizeFromDeliveryModal() {
|
||||
if (!deliveryDrugId) {
|
||||
showToast('Select a drug first', 'warning');
|
||||
// In multi-drug mode, get drug from the first line that has one selected
|
||||
const deliveryContainer = document.getElementById('deliveryLinesContainer');
|
||||
const firstDrugIdStr = deliveryContainer
|
||||
? Array.from(deliveryContainer.querySelectorAll('.delivery-drug-select')).map(s => s.value).find(v => v)
|
||||
: null;
|
||||
deliveryDrugId = firstDrugIdStr ? parseInt(firstDrugIdStr, 10) : null;
|
||||
}
|
||||
|
||||
if (!deliveryDrugId) {
|
||||
showToast('Select a drug on a delivery line first', 'warning');
|
||||
return;
|
||||
}
|
||||
const drug = getActiveDeliveryDrug();
|
||||
const drug = allDrugs.find(d => d.id === deliveryDrugId);
|
||||
if (!drug) {
|
||||
showToast('Drug not found', 'error');
|
||||
return;
|
||||
@@ -3243,7 +3855,6 @@ async function handleAddPackSize(e) {
|
||||
const variantId = parseInt(document.getElementById('addPackSizeVariantSelect')?.value || '', 10);
|
||||
const packType = (document.getElementById('addPackSizeType')?.value || 'box').trim();
|
||||
const packSize = parseFloat(document.getElementById('addPackSizeCount')?.value || '');
|
||||
const packLabel = `${packType.charAt(0).toUpperCase() + packType.slice(1)} of ${packSize}`;
|
||||
|
||||
if (!variantId) {
|
||||
showToast('Please select a variant', 'warning');
|
||||
@@ -3258,7 +3869,6 @@ async function handleAddPackSize(e) {
|
||||
const response = await apiCall(`/variants/${variantId}/packs`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
label: packLabel,
|
||||
pack_unit_name: packType,
|
||||
pack_size_in_base_units: packSize,
|
||||
is_active: true
|
||||
@@ -3284,9 +3894,8 @@ async function handleAddPackSize(e) {
|
||||
async function handleReceiveDelivery(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const drug = getActiveDeliveryDrug();
|
||||
const container = document.getElementById('deliveryLinesContainer');
|
||||
if (!drug || !container) {
|
||||
if (!container) {
|
||||
showToast('Delivery context unavailable', 'error');
|
||||
return;
|
||||
}
|
||||
@@ -3320,7 +3929,7 @@ async function handleReceiveDelivery(e) {
|
||||
return;
|
||||
}
|
||||
|
||||
const variant = drug.variants.find(v => v.id === variantId);
|
||||
const variant = getVariantById(variantId);
|
||||
const selectedPack = variant ? getActivePacksForVariant(variant).find(pack => pack.id === packId) : null;
|
||||
if (!selectedPack) {
|
||||
showToast(`Delivery line ${i + 1} has an invalid pack selection`, 'warning');
|
||||
@@ -3356,6 +3965,7 @@ async function handleReceiveDelivery(e) {
|
||||
}
|
||||
|
||||
closeModal(document.getElementById('receiveDeliveryModal'));
|
||||
_detachDeliveryBarcodeListener();
|
||||
await loadDrugs();
|
||||
showToast(`Delivery received successfully (${payloads.length} line${payloads.length === 1 ? '' : 's'})`, 'success');
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user