GS1 scanning and workflow improvements
This commit is contained in:
+35
-1
@@ -1670,7 +1670,41 @@ def create_variant_batch(variant_id: int, payload: BatchCreate, db: Session = De
|
|||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
if existing:
|
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(
|
row = Batch(
|
||||||
drug_variant_id=variant_id,
|
drug_variant_id=variant_id,
|
||||||
|
|||||||
+28
-8
@@ -296,6 +296,23 @@ function setupEventListeners() {
|
|||||||
const closeButtons = document.querySelectorAll('.close');
|
const closeButtons = document.querySelectorAll('.close');
|
||||||
|
|
||||||
if (drugForm) drugForm.addEventListener('submit', handleAddDrug);
|
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 (variantForm) variantForm.addEventListener('submit', handleAddVariant);
|
||||||
if (editVariantForm) editVariantForm.addEventListener('submit', handleEditVariant);
|
if (editVariantForm) editVariantForm.addEventListener('submit', handleEditVariant);
|
||||||
if (dispenseForm) dispenseForm.addEventListener('submit', handleDispenseDrug);
|
if (dispenseForm) dispenseForm.addEventListener('submit', handleDispenseDrug);
|
||||||
@@ -403,6 +420,9 @@ function setupEventListeners() {
|
|||||||
const cancelAdminChangePasswordBtn = document.getElementById('cancelAdminChangePasswordBtn');
|
const cancelAdminChangePasswordBtn = document.getElementById('cancelAdminChangePasswordBtn');
|
||||||
if (cancelAdminChangePasswordBtn) cancelAdminChangePasswordBtn.addEventListener('click', () => closeModal(document.getElementById('adminChangePasswordModal')));
|
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) => {
|
closeButtons.forEach(btn => btn.addEventListener('click', (e) => {
|
||||||
const modal = e.target.closest('.modal');
|
const modal = e.target.closest('.modal');
|
||||||
if (modal?.id === 'disposeBatchModal') {
|
if (modal?.id === 'disposeBatchModal') {
|
||||||
@@ -2049,7 +2069,7 @@ function appendVariantPackRow(prefill = {}) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="variant-pack-size-label">Bottle Size (${baseUnit}) *</label>
|
<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>
|
</div>
|
||||||
<button type="button" class="btn btn-danger btn-small variant-pack-remove-btn">Remove</button>
|
<button type="button" class="btn btn-danger btn-small variant-pack-remove-btn">Remove</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -3173,6 +3193,10 @@ function parseGS1(raw) {
|
|||||||
const aimPrefix = raw.match(/^\][a-zA-Z]\d/);
|
const aimPrefix = raw.match(/^\][a-zA-Z]\d/);
|
||||||
let data = aimPrefix ? raw.substring(3) : raw;
|
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 GS = '\x1d'; // FNC1 separator
|
||||||
const hasGS = data.includes(GS);
|
const hasGS = data.includes(GS);
|
||||||
|
|
||||||
@@ -3246,6 +3270,9 @@ function _onDeliveryModalKeydown(e) {
|
|||||||
// Only act when the receive delivery modal is open
|
// Only act when the receive delivery modal is open
|
||||||
if (!document.getElementById('receiveDeliveryModal')?.classList.contains('show')) return;
|
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
|
// Track which delivery line last had focus
|
||||||
const focusedLine = document.activeElement?.closest('.delivery-line');
|
const focusedLine = document.activeElement?.closest('.delivery-line');
|
||||||
if (focusedLine) _activeScanLineEl = focusedLine;
|
if (focusedLine) _activeScanLineEl = focusedLine;
|
||||||
@@ -3254,7 +3281,6 @@ function _onDeliveryModalKeydown(e) {
|
|||||||
|
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
const raw = _scanBuffer.map(x => x.char).join('');
|
const raw = _scanBuffer.map(x => x.char).join('');
|
||||||
console.log('[barcode] Enter received. Buffer length:', raw.length, 'Content:', raw);
|
|
||||||
_scanBuffer = [];
|
_scanBuffer = [];
|
||||||
if (_scanBufferTimer) { clearTimeout(_scanBufferTimer); _scanBufferTimer = null; }
|
if (_scanBufferTimer) { clearTimeout(_scanBufferTimer); _scanBufferTimer = null; }
|
||||||
|
|
||||||
@@ -3267,10 +3293,8 @@ function _onDeliveryModalKeydown(e) {
|
|||||||
}
|
}
|
||||||
_preScanFocusedInput = null;
|
_preScanFocusedInput = null;
|
||||||
_preScanFocusedValue = null;
|
_preScanFocusedValue = null;
|
||||||
console.log('[barcode] Treating as scan, calling handleBarcodeScan');
|
|
||||||
handleBarcodeScan(raw);
|
handleBarcodeScan(raw);
|
||||||
} else {
|
} else {
|
||||||
console.log('[barcode] Buffer too short for scan, ignoring');
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -3281,7 +3305,6 @@ function _onDeliveryModalKeydown(e) {
|
|||||||
|
|
||||||
// If gap is too large, start fresh (human typed slowly)
|
// If gap is too large, start fresh (human typed slowly)
|
||||||
if (_scanBuffer.length > 0 && gap > SCAN_MAX_GAP_MS) {
|
if (_scanBuffer.length > 0 && gap > SCAN_MAX_GAP_MS) {
|
||||||
console.log('[barcode] Gap too large (' + gap + 'ms), resetting buffer');
|
|
||||||
_scanBuffer = [];
|
_scanBuffer = [];
|
||||||
_preScanFocusedInput = null;
|
_preScanFocusedInput = null;
|
||||||
_preScanFocusedValue = null;
|
_preScanFocusedValue = null;
|
||||||
@@ -3303,12 +3326,10 @@ function _onDeliveryModalKeydown(e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_scanBuffer.push({ char: e.key, time: now });
|
_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
|
// Auto-clear buffer if Enter never comes
|
||||||
if (_scanBufferTimer) clearTimeout(_scanBufferTimer);
|
if (_scanBufferTimer) clearTimeout(_scanBufferTimer);
|
||||||
_scanBufferTimer = setTimeout(() => {
|
_scanBufferTimer = setTimeout(() => {
|
||||||
console.log('[barcode] Buffer auto-cleared (no Enter)');
|
|
||||||
_scanBuffer = [];
|
_scanBuffer = [];
|
||||||
_preScanFocusedInput = null;
|
_preScanFocusedInput = null;
|
||||||
_preScanFocusedValue = null;
|
_preScanFocusedValue = null;
|
||||||
@@ -3767,7 +3788,6 @@ async function openReceiveDeliveryModal() {
|
|||||||
if (modalEl._barcodeListener) document.removeEventListener('keydown', modalEl._barcodeListener);
|
if (modalEl._barcodeListener) document.removeEventListener('keydown', modalEl._barcodeListener);
|
||||||
modalEl._barcodeListener = _onDeliveryModalKeydown;
|
modalEl._barcodeListener = _onDeliveryModalKeydown;
|
||||||
document.addEventListener('keydown', modalEl._barcodeListener);
|
document.addEventListener('keydown', modalEl._barcodeListener);
|
||||||
console.log('[barcode] Listener attached to receiveDeliveryModal');
|
|
||||||
|
|
||||||
openModal(modalEl);
|
openModal(modalEl);
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -268,7 +268,7 @@
|
|||||||
<input type="hidden" id="variantDrugId">
|
<input type="hidden" id="variantDrugId">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="variantStrength">Strength *</label>
|
<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>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -672,7 +672,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="button" class="btn btn-primary" onclick="handleSaveGtinMapping()">Save Mapping</button>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user