Scan shortcut on main screen
This commit is contained in:
+162
-4
@@ -19,6 +19,9 @@ 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
|
||||
let _highlightedBatchLot = null; // lot number to highlight after a main-screen scan
|
||||
let _highlightedVariantId = null; // variant the highlighted batch belongs to
|
||||
let _highlightClearTimer = null; // timer to auto-remove the highlight
|
||||
|
||||
/** Build a human-readable pack label from pack fields, e.g. "Box of 28" */
|
||||
function packLabel(packOrUnitName, packSize) {
|
||||
@@ -521,6 +524,9 @@ function setupEventListeners() {
|
||||
closeModal(e.target);
|
||||
}
|
||||
});
|
||||
|
||||
// Main-screen barcode scan
|
||||
document.addEventListener('keydown', _onMainScreenKeydown);
|
||||
}
|
||||
|
||||
// Load drugs from API
|
||||
@@ -863,11 +869,22 @@ function populateDispensePackSelect(variant) {
|
||||
});
|
||||
|
||||
if (packCount) packCount.value = '';
|
||||
|
||||
// Auto-select the first available pack
|
||||
if (activePacks.length > 0) {
|
||||
packSelect.value = String(activePacks[0].id);
|
||||
}
|
||||
|
||||
if (packPreview) {
|
||||
packPreview.textContent = activePacks.length > 0
|
||||
? `Select a pack and whole-number count (${variant.unit} base unit).`
|
||||
: `No active packs for this variant.`;
|
||||
}
|
||||
|
||||
// Update preview to reflect the auto-selected pack
|
||||
if (activePacks.length > 0) {
|
||||
updateDispenseQuantityFromPack();
|
||||
}
|
||||
}
|
||||
|
||||
function updateDispenseModeUi() {
|
||||
@@ -999,13 +1016,16 @@ function renderVariantInventoryDetails(variant, batches) {
|
||||
const stocktakeLabel = hasPackState
|
||||
? `${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;'
|
||||
: 'padding: 8px; background: #ffffff; border: 1px solid #d8e2ea; border-radius: 5px; font-size: 0.9em;';
|
||||
const isHighlighted = _highlightedBatchLot === batch.batch_number && _highlightedVariantId === variant.id;
|
||||
const batchCardStyles = isHighlighted
|
||||
? 'padding: 8px; background: #fef9c3; border: 2px solid #f59e0b; border-radius: 5px; font-size: 0.9em;'
|
||||
: expired
|
||||
? 'padding: 8px; background: #fff1f2; border: 1px solid #f3a6ad; border-radius: 5px; font-size: 0.9em;'
|
||||
: 'padding: 8px; background: #ffffff; border: 1px solid #d8e2ea; border-radius: 5px; font-size: 0.9em;';
|
||||
const expiryStyles = expired ? 'color: #b91c1c; font-weight: 700;' : 'color: #4b5563;';
|
||||
|
||||
return `
|
||||
<div style="${batchCardStyles}">
|
||||
<div style="${batchCardStyles}" data-batch-number="${escapeHtml(batch.batch_number)}" data-variant-id="${variant.id}">
|
||||
<div style="display: flex; justify-content: space-between; gap: 8px; flex-wrap: wrap;">
|
||||
<div style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap;">
|
||||
<strong>${escapeHtml(batch.batch_number)}</strong>
|
||||
@@ -3341,6 +3361,68 @@ let _preScanFocusedValue = null; // its value before any scan chars were typed
|
||||
const SCAN_MAX_GAP_MS = 50;
|
||||
const SCAN_MIN_LENGTH = 20;
|
||||
|
||||
// Separate buffer for main-screen scan (does not interfere with delivery modal)
|
||||
let _mainScanBuffer = [];
|
||||
let _mainScanBufferTimer = null;
|
||||
let _mainPreScanFocusedInput = null;
|
||||
let _mainPreScanFocusedValue = null;
|
||||
|
||||
function _onMainScreenKeydown(e) {
|
||||
// Only act when no modal is open
|
||||
if (document.querySelector('.modal.show')) return;
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
if (e.key === 'Enter') {
|
||||
const raw = _mainScanBuffer.map(x => x.char).join('');
|
||||
_mainScanBuffer = [];
|
||||
if (_mainScanBufferTimer) { clearTimeout(_mainScanBufferTimer); _mainScanBufferTimer = null; }
|
||||
|
||||
if (raw.length >= SCAN_MIN_LENGTH) {
|
||||
e.preventDefault();
|
||||
if (_mainPreScanFocusedInput) {
|
||||
_mainPreScanFocusedInput.value = _mainPreScanFocusedValue || '';
|
||||
}
|
||||
_mainPreScanFocusedInput = null;
|
||||
_mainPreScanFocusedValue = null;
|
||||
handleMainScreenBarcodeScan(raw);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key.length === 1) {
|
||||
const gap = _mainScanBuffer.length > 0 ? now - _mainScanBuffer[_mainScanBuffer.length - 1].time : 0;
|
||||
|
||||
if (_mainScanBuffer.length > 0 && gap > SCAN_MAX_GAP_MS) {
|
||||
_mainScanBuffer = [];
|
||||
_mainPreScanFocusedInput = null;
|
||||
_mainPreScanFocusedValue = null;
|
||||
}
|
||||
|
||||
if (_mainScanBuffer.length === 0) {
|
||||
const active = document.activeElement;
|
||||
if (active && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA')) {
|
||||
_mainPreScanFocusedInput = active;
|
||||
_mainPreScanFocusedValue = active.value;
|
||||
} else {
|
||||
_mainPreScanFocusedInput = null;
|
||||
_mainPreScanFocusedValue = null;
|
||||
}
|
||||
} else {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
_mainScanBuffer.push({ char: e.key, time: now });
|
||||
|
||||
if (_mainScanBufferTimer) clearTimeout(_mainScanBufferTimer);
|
||||
_mainScanBufferTimer = setTimeout(() => {
|
||||
_mainScanBuffer = [];
|
||||
_mainPreScanFocusedInput = null;
|
||||
_mainPreScanFocusedValue = null;
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
function _onDeliveryModalKeydown(e) {
|
||||
// Only act when the receive delivery modal is open
|
||||
if (!document.getElementById('receiveDeliveryModal')?.classList.contains('show')) return;
|
||||
@@ -3412,6 +3494,82 @@ function _onDeliveryModalKeydown(e) {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleMainScreenBarcodeScan(raw) {
|
||||
const parsed = parseGS1(raw);
|
||||
if (!parsed) {
|
||||
showToast('Barcode not recognised as a GS1 code', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const { gtin, lot } = parsed;
|
||||
|
||||
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) {
|
||||
showToast('Unknown barcode — scan it in a delivery first to map it to a drug', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const drugId = mapping.drug_id;
|
||||
const variantId = mapping.drug_variant_id;
|
||||
|
||||
// Expand drug + variant
|
||||
expandedDrugs.add(drugId);
|
||||
expandedVariants.add(variantId);
|
||||
|
||||
// Ensure detail + batch data are loaded
|
||||
await ensureDrugDetailLoaded(drugId);
|
||||
if (!loadedVariantBatches.has(variantId)) {
|
||||
try {
|
||||
const resp = await apiCall(`/variants/${variantId}/batches`);
|
||||
if (resp.ok) loadedVariantBatches.set(variantId, await resp.json());
|
||||
} catch (_) { /* non-fatal */ }
|
||||
}
|
||||
|
||||
// Set highlight
|
||||
if (_highlightClearTimer) { clearTimeout(_highlightClearTimer); _highlightClearTimer = null; }
|
||||
_highlightedBatchLot = lot;
|
||||
_highlightedVariantId = variantId;
|
||||
|
||||
renderDrugs();
|
||||
|
||||
requestAnimationFrame(() => scrollToHighlightedBatch());
|
||||
|
||||
// Auto-clear highlight after 5 seconds
|
||||
_highlightClearTimer = setTimeout(() => {
|
||||
_highlightedBatchLot = null;
|
||||
_highlightedVariantId = null;
|
||||
_highlightClearTimer = null;
|
||||
renderDrugs();
|
||||
}, 5000);
|
||||
|
||||
// Confirm to user
|
||||
const batches = loadedVariantBatches.get(variantId) || [];
|
||||
const found = batches.some(b => b.batch_number === lot);
|
||||
if (found) {
|
||||
showToast(`${mapping.drug_name} ${mapping.variant_strength} — lot ${lot}`, 'success');
|
||||
} else {
|
||||
showToast(`${mapping.drug_name} ${mapping.variant_strength} — lot ${lot} (no active stock)`, 'warning');
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToHighlightedBatch() {
|
||||
if (!_highlightedBatchLot || _highlightedVariantId == null) return;
|
||||
const el = document.querySelector(`[data-batch-number="${CSS.escape(_highlightedBatchLot)}"][data-variant-id="${_highlightedVariantId}"]`);
|
||||
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
|
||||
async function handleBarcodeScan(raw) {
|
||||
const parsed = parseGS1(raw);
|
||||
if (!parsed) {
|
||||
|
||||
Reference in New Issue
Block a user