GS1 scanning and workflow improvements

This commit is contained in:
2026-04-20 12:43:29 -04:00
parent cfb08bd288
commit 6be571a48c
3 changed files with 66 additions and 12 deletions
+35 -1
View File
@@ -1670,7 +1670,41 @@ def create_variant_batch(variant_id: int, payload: BatchCreate, db: Session = De
.first()
)
if existing:
raise HTTPException(status_code=400, detail="Batch number already exists for this variant")
if existing.expiry_date != payload.expiry_date:
raise HTTPException(
status_code=400,
detail="Batch number already exists for this variant with a different expiry date",
)
# Same batch number and expiry — restock the existing batch
existing.quantity += batch_quantity
if existing.received_pack_id == resolved["pack_id"]:
existing.received_pack_count = (existing.received_pack_count or 0) + resolved["pack_count"]
if payload.notes:
existing.notes = (existing.notes + "\n" + payload.notes) if existing.notes else payload.notes
recompute_batch_pack_state(existing)
variant.quantity += batch_quantity
write_audit_log(
db,
action="batch.restock",
entity_type="batch",
entity_id=existing.id,
actor=current_user,
details={
"variant_id": variant_id,
"batch_number": batch_number,
"quantity_added": batch_quantity,
"new_total_quantity": existing.quantity,
"received_pack_id": resolved["pack_id"],
"received_pack_count": resolved["pack_count"],
"expiry_date": str(payload.expiry_date),
"location_id": existing.location_id,
},
)
db.commit()
db.refresh(existing)
return serialize_batch_response(db, existing)
row = Batch(
drug_variant_id=variant_id,
+28 -8
View File
@@ -296,6 +296,23 @@ function setupEventListeners() {
const closeButtons = document.querySelectorAll('.close');
if (drugForm) drugForm.addEventListener('submit', handleAddDrug);
// Auto-capitalise the first letter typed in each Add Drug text field
['drugName', 'drugDescription'].forEach(id => {
const el = document.getElementById(id);
if (el) el.addEventListener('input', () => {
if (el.value.length === 1) el.value = el.value.toUpperCase();
});
});
// Auto-capitalise the first letter typed in each Dispense text field
['dispenseAnimal', 'dispenseDosage', 'dispenseNotes'].forEach(id => {
const el = document.getElementById(id);
if (el) el.addEventListener('input', () => {
if (el.value.length === 1) el.value = el.value.toUpperCase();
});
});
if (variantForm) variantForm.addEventListener('submit', handleAddVariant);
if (editVariantForm) editVariantForm.addEventListener('submit', handleEditVariant);
if (dispenseForm) dispenseForm.addEventListener('submit', handleDispenseDrug);
@@ -403,6 +420,9 @@ function setupEventListeners() {
const cancelAdminChangePasswordBtn = document.getElementById('cancelAdminChangePasswordBtn');
if (cancelAdminChangePasswordBtn) cancelAdminChangePasswordBtn.addEventListener('click', () => closeModal(document.getElementById('adminChangePasswordModal')));
const cancelGtinMappingBtn = document.getElementById('cancelGtinMappingBtn');
if (cancelGtinMappingBtn) cancelGtinMappingBtn.addEventListener('click', () => closeModal(document.getElementById('gtinMappingModal')));
closeButtons.forEach(btn => btn.addEventListener('click', (e) => {
const modal = e.target.closest('.modal');
if (modal?.id === 'disposeBatchModal') {
@@ -2049,7 +2069,7 @@ function appendVariantPackRow(prefill = {}) {
</div>
<div class="form-group">
<label class="variant-pack-size-label">Bottle Size (${baseUnit}) *</label>
<input type="number" class="variant-pack-size" min="0.0001" step="0.0001" value="${selectedSize}" required>
<input type="number" class="variant-pack-size" min="1" step="1" value="${selectedSize}" required>
</div>
<button type="button" class="btn btn-danger btn-small variant-pack-remove-btn">Remove</button>
</div>
@@ -3173,6 +3193,10 @@ function parseGS1(raw) {
const aimPrefix = raw.match(/^\][a-zA-Z]\d/);
let data = aimPrefix ? raw.substring(3) : raw;
// Normalise semicolons to the standard GS/FNC1 character — some scanners
// emit ';' as the group separator for variable-length field termination.
data = data.replace(/;/g, '\x1d');
const GS = '\x1d'; // FNC1 separator
const hasGS = data.includes(GS);
@@ -3246,6 +3270,9 @@ function _onDeliveryModalKeydown(e) {
// Only act when the receive delivery modal is open
if (!document.getElementById('receiveDeliveryModal')?.classList.contains('show')) return;
// Stop intercepting if the GTIN mapping modal is open on top
if (document.getElementById('gtinMappingModal')?.classList.contains('show')) return;
// Track which delivery line last had focus
const focusedLine = document.activeElement?.closest('.delivery-line');
if (focusedLine) _activeScanLineEl = focusedLine;
@@ -3254,7 +3281,6 @@ function _onDeliveryModalKeydown(e) {
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; }
@@ -3267,10 +3293,8 @@ function _onDeliveryModalKeydown(e) {
}
_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;
}
@@ -3281,7 +3305,6 @@ function _onDeliveryModalKeydown(e) {
// 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;
@@ -3303,12 +3326,10 @@ function _onDeliveryModalKeydown(e) {
}
_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;
@@ -3767,7 +3788,6 @@ async function openReceiveDeliveryModal() {
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);
}
+2 -2
View File
@@ -268,7 +268,7 @@
<input type="hidden" id="variantDrugId">
<div class="form-group">
<label for="variantStrength">Strength *</label>
<input type="text" id="variantStrength" placeholder="e.g., 5.4mg, 10.8mg, 100ml" required>
<input type="text" id="variantStrength" placeholder="e.g., 5.4mg, 0.5mg/ml" required>
</div>
<div class="form-group">
@@ -672,7 +672,7 @@
</div>
<div class="form-actions">
<button type="button" class="btn btn-primary" onclick="handleSaveGtinMapping()">Save Mapping</button>
<button type="button" class="btn btn-secondary close">Cancel</button>
<button type="button" class="btn btn-secondary" id="cancelGtinMappingBtn">Cancel</button>
</div>
</div>
</div>