WIP
This commit is contained in:
+816
-8
@@ -7,6 +7,10 @@ let searchTerm = '';
|
||||
let expandedDrugs = new Set();
|
||||
let currentUser = null;
|
||||
let accessToken = null;
|
||||
let deliveryDrugId = null;
|
||||
let deliveryLineCounter = 0;
|
||||
let deliveryLocations = [];
|
||||
let activeVariantPacksVariantId = null;
|
||||
|
||||
// Toast notification system
|
||||
function showToast(message, type = 'info', duration = 3000) {
|
||||
@@ -212,6 +216,7 @@ function setupEventListeners() {
|
||||
const editModal = document.getElementById('editModal');
|
||||
const printNotesModal = document.getElementById('printNotesModal');
|
||||
const batchReceiveModal = document.getElementById('batchReceiveModal');
|
||||
const receiveDeliveryModal = document.getElementById('receiveDeliveryModal');
|
||||
const addDrugBtn = document.getElementById('addDrugBtn');
|
||||
const dispenseBtn = document.getElementById('dispenseBtn');
|
||||
const printNotesBtn = document.getElementById('printNotesBtn');
|
||||
@@ -222,6 +227,15 @@ function setupEventListeners() {
|
||||
const cancelPrescribeBtn = document.getElementById('cancelPrescribeBtn');
|
||||
const cancelEditBtn = document.getElementById('cancelEditBtn');
|
||||
const cancelBatchReceiveBtn = document.getElementById('cancelBatchReceiveBtn');
|
||||
const cancelReceiveDeliveryBtn = document.getElementById('cancelReceiveDeliveryBtn');
|
||||
const addDeliveryLineBtn = document.getElementById('addDeliveryLineBtn');
|
||||
const addVariantFromDeliveryBtn = document.getElementById('addVariantFromDeliveryBtn');
|
||||
const addVariantPackRowBtn = document.getElementById('addVariantPackRowBtn');
|
||||
const variantUnitSelect = document.getElementById('variantUnit');
|
||||
const variantStrengthInput = document.getElementById('variantStrength');
|
||||
const dispenseModeSelect = document.getElementById('dispenseMode');
|
||||
const variantPacksForm = document.getElementById('variantPacksForm');
|
||||
const closeVariantPacksBtn = document.getElementById('closeVariantPacksBtn');
|
||||
const showAllBtn = document.getElementById('showAllBtn');
|
||||
const showLowStockBtn = document.getElementById('showLowStockBtn');
|
||||
const locationFilterSelect = document.getElementById('locationFilterSelect');
|
||||
@@ -246,11 +260,31 @@ function setupEventListeners() {
|
||||
const batchReceiveForm = document.getElementById('batchReceiveForm');
|
||||
if (batchReceiveForm) batchReceiveForm.addEventListener('submit', handleBatchReceive);
|
||||
if (cancelBatchReceiveBtn) cancelBatchReceiveBtn.addEventListener('click', () => closeModal(batchReceiveModal));
|
||||
|
||||
const receiveDeliveryForm = document.getElementById('receiveDeliveryForm');
|
||||
if (receiveDeliveryForm) receiveDeliveryForm.addEventListener('submit', handleReceiveDelivery);
|
||||
if (cancelReceiveDeliveryBtn) cancelReceiveDeliveryBtn.addEventListener('click', () => closeModal(receiveDeliveryModal));
|
||||
if (addDeliveryLineBtn) addDeliveryLineBtn.addEventListener('click', () => appendDeliveryLine());
|
||||
if (addVariantFromDeliveryBtn) addVariantFromDeliveryBtn.addEventListener('click', handleAddVariantFromDelivery);
|
||||
if (addVariantPackRowBtn) addVariantPackRowBtn.addEventListener('click', () => appendVariantPackRow());
|
||||
if (variantUnitSelect) {
|
||||
variantUnitSelect.addEventListener('change', () => {
|
||||
refreshVariantPackRowLabels();
|
||||
});
|
||||
}
|
||||
if (variantStrengthInput && variantUnitSelect) {
|
||||
variantStrengthInput.addEventListener('blur', () => {
|
||||
variantUnitSelect.value = inferBaseUnitFromStrength(variantStrengthInput.value);
|
||||
refreshVariantPackRowLabels();
|
||||
});
|
||||
}
|
||||
if (dispenseModeSelect) dispenseModeSelect.addEventListener('change', updateDispenseModeUi);
|
||||
|
||||
if (addDrugBtn) addDrugBtn.addEventListener('click', () => openModal(addModal));
|
||||
if (printNotesBtn) printNotesBtn.addEventListener('click', () => openModal(printNotesModal));
|
||||
if (dispenseBtn) dispenseBtn.addEventListener('click', () => {
|
||||
updateDispenseDrugSelect();
|
||||
updateDispenseModeUi();
|
||||
openModal(dispenseModal);
|
||||
});
|
||||
|
||||
@@ -272,6 +306,9 @@ function setupEventListeners() {
|
||||
|
||||
const closeLocationManagementBtn = document.getElementById('closeLocationManagementBtn');
|
||||
if (closeLocationManagementBtn) closeLocationManagementBtn.addEventListener('click', () => closeModal(document.getElementById('locationManagementModal')));
|
||||
|
||||
if (variantPacksForm) variantPacksForm.addEventListener('submit', handleCreateVariantPack);
|
||||
if (closeVariantPacksBtn) closeVariantPacksBtn.addEventListener('click', () => closeModal(document.getElementById('variantPacksModal')));
|
||||
|
||||
const createLocationForm = document.getElementById('createLocationForm');
|
||||
if (createLocationForm) createLocationForm.addEventListener('submit', createLocation);
|
||||
@@ -333,6 +370,28 @@ function setupEventListeners() {
|
||||
});
|
||||
}
|
||||
|
||||
const dispenseQuantityInput = document.getElementById('dispenseQuantity');
|
||||
if (dispenseQuantityInput) {
|
||||
dispenseQuantityInput.addEventListener('input', () => {
|
||||
const mode = document.getElementById('dispenseMode')?.value || 'subunit';
|
||||
if (mode !== 'subunit') {
|
||||
return;
|
||||
}
|
||||
|
||||
const packSelect = document.getElementById('dispensePackSelect');
|
||||
const packCount = document.getElementById('dispensePackCount');
|
||||
const packPreview = document.getElementById('dispensePackPreview');
|
||||
const variantId = parseInt(document.getElementById('dispenseDrugSelect')?.value || '', 10);
|
||||
const variant = getVariantById(variantId);
|
||||
|
||||
if (packSelect) packSelect.value = '';
|
||||
if (packCount) packCount.value = '';
|
||||
if (packPreview && variant) {
|
||||
packPreview.textContent = `Enter direct quantity in ${variant.unit}.`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Close modal when clicking outside
|
||||
window.addEventListener('click', (e) => {
|
||||
if (e.target.classList.contains('modal')) {
|
||||
@@ -398,6 +457,108 @@ function updateDispenseDrugSelect() {
|
||||
select.appendChild(option);
|
||||
});
|
||||
});
|
||||
|
||||
const packSelect = document.getElementById('dispensePackSelect');
|
||||
const packCount = document.getElementById('dispensePackCount');
|
||||
const packPreview = document.getElementById('dispensePackPreview');
|
||||
const modeSelect = document.getElementById('dispenseMode');
|
||||
if (packSelect) {
|
||||
packSelect.innerHTML = '<option value="">-- Select pack --</option>';
|
||||
}
|
||||
if (packCount) {
|
||||
packCount.value = '';
|
||||
}
|
||||
if (modeSelect) {
|
||||
modeSelect.value = 'subunit';
|
||||
}
|
||||
if (packPreview) {
|
||||
packPreview.textContent = 'Select a pack and whole-number count.';
|
||||
}
|
||||
|
||||
updateDispenseModeUi();
|
||||
}
|
||||
|
||||
function populateDispensePackSelect(variant) {
|
||||
const packSelect = document.getElementById('dispensePackSelect');
|
||||
const packCount = document.getElementById('dispensePackCount');
|
||||
const packPreview = document.getElementById('dispensePackPreview');
|
||||
if (!packSelect) return;
|
||||
|
||||
const activePacks = getActivePacksForVariant(variant);
|
||||
packSelect.innerHTML = '<option value="">-- Select pack --</option>';
|
||||
|
||||
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})`;
|
||||
packSelect.appendChild(option);
|
||||
});
|
||||
|
||||
if (packCount) packCount.value = '';
|
||||
if (packPreview) {
|
||||
packPreview.textContent = activePacks.length > 0
|
||||
? `Select a pack and whole-number count (${variant.unit} base unit).`
|
||||
: `No active packs for this variant.`;
|
||||
}
|
||||
}
|
||||
|
||||
function updateDispenseModeUi() {
|
||||
const mode = document.getElementById('dispenseMode')?.value || 'subunit';
|
||||
const quantityGroup = document.getElementById('dispenseQuantityGroup');
|
||||
const packRow = document.getElementById('dispensePackRow');
|
||||
const quantityInput = document.getElementById('dispenseQuantity');
|
||||
const packSelect = document.getElementById('dispensePackSelect');
|
||||
const packCount = document.getElementById('dispensePackCount');
|
||||
|
||||
if (quantityGroup) {
|
||||
quantityGroup.style.display = mode === 'subunit' ? '' : 'none';
|
||||
}
|
||||
if (packRow) {
|
||||
packRow.style.display = mode === 'pack' ? '' : 'none';
|
||||
}
|
||||
|
||||
if (quantityInput) {
|
||||
quantityInput.required = mode === 'subunit';
|
||||
}
|
||||
if (packSelect) {
|
||||
packSelect.required = mode === 'pack';
|
||||
}
|
||||
if (packCount) {
|
||||
packCount.required = mode === 'pack';
|
||||
}
|
||||
|
||||
updateAllocationPreview();
|
||||
}
|
||||
|
||||
function updateDispenseQuantityFromPack() {
|
||||
const mode = document.getElementById('dispenseMode')?.value || 'subunit';
|
||||
if (mode !== 'pack') return;
|
||||
|
||||
const variantId = parseInt(document.getElementById('dispenseDrugSelect')?.value || '', 10);
|
||||
const packId = parseInt(document.getElementById('dispensePackSelect')?.value || '', 10);
|
||||
const packCount = parseFloat(document.getElementById('dispensePackCount')?.value || '');
|
||||
const quantityInput = document.getElementById('dispenseQuantity');
|
||||
const preview = document.getElementById('dispensePackPreview');
|
||||
|
||||
const variant = getVariantById(variantId);
|
||||
if (!quantityInput || !preview || !variant) return;
|
||||
|
||||
const selectedPack = getActivePacksForVariant(variant).find(pack => pack.id === packId);
|
||||
if (selectedPack && !Number.isNaN(packCount) && packCount > 0) {
|
||||
if (Math.abs(packCount - Math.round(packCount)) > 1e-6) {
|
||||
preview.textContent = 'Whole-pack mode requires a whole-number pack count.';
|
||||
return;
|
||||
}
|
||||
const quantity = packCount * selectedPack.pack_size_in_base_units;
|
||||
quantityInput.value = String(quantity);
|
||||
preview.textContent = `${packCount} × ${selectedPack.pack_size_in_base_units} = ${quantity} ${variant.unit}`;
|
||||
updateAllocationPreview();
|
||||
return;
|
||||
}
|
||||
|
||||
preview.textContent = selectedPack
|
||||
? `1 ${selectedPack.pack_unit_name} = ${selectedPack.pack_size_in_base_units} ${variant.unit}`
|
||||
: `Select a pack to calculate quantity.`;
|
||||
}
|
||||
|
||||
function formatDisplayDate(value) {
|
||||
@@ -455,6 +616,8 @@ function updateLocationFilterOptions() {
|
||||
|
||||
function populateDispenseBatchSelect(activeBatches) {
|
||||
const batchSelect = document.getElementById('dispenseBatchSelect');
|
||||
const selectedVariantId = parseInt(document.getElementById('dispenseDrugSelect').value || '', 10);
|
||||
const unitLabel = getVariantById(selectedVariantId)?.unit || 'units';
|
||||
const previousValue = batchSelect.value;
|
||||
|
||||
batchSelect.innerHTML = '<option value="">Automatic FEFO Selection</option>';
|
||||
@@ -465,7 +628,7 @@ function populateDispenseBatchSelect(activeBatches) {
|
||||
const locationLabel = getBatchLocationLabel(batch);
|
||||
const fefoLabel = index === 0 ? ' [FEFO default]' : '';
|
||||
option.value = batch.id;
|
||||
option.textContent = `${batch.batch_number} | ${batch.quantity} units | ${locationLabel} | Expires ${expiryLabel}${fefoLabel}`;
|
||||
option.textContent = `${batch.batch_number} | ${batch.quantity} ${unitLabel} | ${locationLabel} | Expires ${expiryLabel}${fefoLabel}`;
|
||||
batchSelect.appendChild(option);
|
||||
});
|
||||
|
||||
@@ -484,8 +647,16 @@ async function updateBatchInfo() {
|
||||
if (!variantId) {
|
||||
batchInfoSection.style.display = 'none';
|
||||
batchSelect.innerHTML = '<option value="">Automatic FEFO Selection</option>';
|
||||
const packSelect = document.getElementById('dispensePackSelect');
|
||||
if (packSelect) packSelect.innerHTML = '<option value="">-- Select pack --</option>';
|
||||
return;
|
||||
}
|
||||
|
||||
const variant = getVariantById(variantId);
|
||||
if (variant) {
|
||||
populateDispensePackSelect(variant);
|
||||
}
|
||||
updateDispenseModeUi();
|
||||
|
||||
batchInfoSection.style.display = 'block';
|
||||
batchInfoContent.innerHTML = '<p class="loading">Loading batches...</p>';
|
||||
@@ -558,6 +729,7 @@ async function updateBatchInfo() {
|
||||
// Update allocation preview based on quantity and allow_split flag
|
||||
async function updateAllocationPreview() {
|
||||
const variantId = parseInt(document.getElementById('dispenseDrugSelect').value);
|
||||
const unitLabel = getVariantById(variantId)?.unit || 'units';
|
||||
const quantity = parseFloat(document.getElementById('dispenseQuantity').value);
|
||||
const allowSplit = document.getElementById('dispenseAllowSplit').checked;
|
||||
const preferredBatchId = parseInt(document.getElementById('dispenseBatchSelect').value);
|
||||
@@ -626,10 +798,10 @@ async function updateAllocationPreview() {
|
||||
|
||||
if (remainingQty > 0 && allowSplit) {
|
||||
allocationPreviewContent.innerHTML = `
|
||||
<p style="color: #d32f2f; margin: 0 0 10px 0;">✕ Warning: Only ${quantity - remainingQty} units available across all batches (${remainingQty} short)</p>
|
||||
<p style="color: #d32f2f; margin: 0 0 10px 0;">✕ Warning: Only ${quantity - remainingQty} ${escapeHtml(unitLabel)} available across all batches (${remainingQty} short)</p>
|
||||
<div>${allocations.map(a => `
|
||||
<div style="padding: 5px; margin: 3px 0; background: white; border-radius: 2px; font-size: 0.9em;">
|
||||
<strong>${a.batchNumber}</strong>${a.preferred ? ' <span style="color: #1565c0;">(preferred)</span>' : ''}: ${a.quantity} units (${escapeHtml(a.location)}) - Expires ${formatDisplayDate(a.expiryDate)}
|
||||
<strong>${a.batchNumber}</strong>${a.preferred ? ' <span style="color: #1565c0;">(preferred)</span>' : ''}: ${a.quantity} ${escapeHtml(unitLabel)} (${escapeHtml(a.location)}) - Expires ${formatDisplayDate(a.expiryDate)}
|
||||
</div>
|
||||
`).join('')}</div>
|
||||
`;
|
||||
@@ -638,7 +810,7 @@ async function updateAllocationPreview() {
|
||||
|
||||
const allocationHtml = allocations.map(a => `
|
||||
<div style="padding: 5px; margin: 3px 0; background: white; border-radius: 2px; font-size: 0.9em;">
|
||||
<strong>${a.batchNumber}</strong>${a.preferred ? ' <span style="color: #1565c0;">(preferred)</span>' : ''}: ${a.quantity} units (${escapeHtml(a.location)}) - Expires ${formatDisplayDate(a.expiryDate)}
|
||||
<strong>${a.batchNumber}</strong>${a.preferred ? ' <span style="color: #1565c0;">(preferred)</span>' : ''}: ${a.quantity} ${escapeHtml(unitLabel)} (${escapeHtml(a.location)}) - Expires ${formatDisplayDate(a.expiryDate)}
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
@@ -725,7 +897,7 @@ function renderDrugs() {
|
||||
</div>
|
||||
<div class="variant-actions">
|
||||
${!isReadOnly ? `
|
||||
<button class="btn btn-success btn-small" onclick="openBatchReceiveModal(${variant.id})">📦 Receive Batch</button>
|
||||
<button class="btn btn-info btn-small" onclick="event.stopPropagation(); openVariantPacksModal(${variant.id})">📦 Packs</button>
|
||||
<button class="btn btn-primary btn-small" onclick="prescribeVariant(${variant.id}, '${drug.name.replace(/'/g, "\\'")}', '${variant.strength.replace(/'/g, "\\'")}', '${variant.unit.replace(/'/g, "\\'")}')">🏷️ Prescribe & Print</button>
|
||||
<button class="btn btn-success btn-small" onclick="dispenseVariant(${variant.id})">💊 Dispense</button>
|
||||
<button class="btn btn-warning btn-small" onclick="openEditVariantModal(${variant.id})">Edit</button>
|
||||
@@ -753,6 +925,7 @@ 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>
|
||||
@@ -825,13 +998,45 @@ async function handleDispenseDrug(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const variantId = parseInt(document.getElementById('dispenseDrugSelect').value);
|
||||
const quantity = parseFloat(document.getElementById('dispenseQuantity').value);
|
||||
let quantity = parseFloat(document.getElementById('dispenseQuantity').value);
|
||||
const dispenseMode = (document.getElementById('dispenseMode').value || 'subunit').toLowerCase();
|
||||
const preferredBatchIdValue = document.getElementById('dispenseBatchSelect').value;
|
||||
const requestedPackIdValue = document.getElementById('dispensePackSelect').value;
|
||||
const requestedPackCountValue = document.getElementById('dispensePackCount').value;
|
||||
const animalName = document.getElementById('dispenseAnimal').value;
|
||||
const userName = document.getElementById('dispenseUser').value;
|
||||
const notes = document.getElementById('dispenseNotes').value;
|
||||
const allowSplit = document.getElementById('dispenseAllowSplit').checked;
|
||||
|
||||
const selectedPackId = requestedPackIdValue ? parseInt(requestedPackIdValue, 10) : null;
|
||||
const selectedPackCount = requestedPackCountValue ? parseFloat(requestedPackCountValue) : null;
|
||||
const variant = getVariantById(variantId);
|
||||
const selectedPack = variant && selectedPackId
|
||||
? getActivePacksForVariant(variant).find(pack => pack.id === selectedPackId)
|
||||
: null;
|
||||
|
||||
if (!['subunit', 'pack'].includes(dispenseMode)) {
|
||||
showToast('Please select a valid dispense mode.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
if (dispenseMode === 'pack') {
|
||||
if (!selectedPack) {
|
||||
showToast('Please select a pack type for whole-pack dispensing.', 'warning');
|
||||
return;
|
||||
}
|
||||
if (selectedPackCount == null || Number.isNaN(selectedPackCount) || selectedPackCount <= 0) {
|
||||
showToast('Please enter a valid pack count greater than zero.', 'warning');
|
||||
return;
|
||||
}
|
||||
if (Math.abs(selectedPackCount - Math.round(selectedPackCount)) > 1e-6) {
|
||||
showToast('Whole-pack dispensing requires a whole-number pack count.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
quantity = selectedPackCount * selectedPack.pack_size_in_base_units;
|
||||
}
|
||||
|
||||
if (!variantId || isNaN(quantity) || quantity <= 0 || !userName) {
|
||||
showToast('Please fill in all required fields (Drug Variant, Quantity > 0, Dispensed by)', 'warning');
|
||||
return;
|
||||
@@ -840,7 +1045,10 @@ async function handleDispenseDrug(e) {
|
||||
const dispensingData = {
|
||||
drug_variant_id: variantId,
|
||||
quantity: quantity,
|
||||
dispense_mode: dispenseMode,
|
||||
batch_id: preferredBatchIdValue ? parseInt(preferredBatchIdValue) : null,
|
||||
requested_pack_id: dispenseMode === 'pack' ? selectedPackId : null,
|
||||
requested_pack_count: dispenseMode === 'pack' ? selectedPackCount : null,
|
||||
animal_name: animalName || null,
|
||||
user_name: userName,
|
||||
notes: notes || null,
|
||||
@@ -903,19 +1111,143 @@ function openAddVariantModal(drugId) {
|
||||
if (!drug) return;
|
||||
|
||||
currentDrug = drug;
|
||||
const form = document.getElementById('variantForm');
|
||||
if (form) form.reset();
|
||||
document.getElementById('variantDrugId').value = drug.id;
|
||||
initializeVariantPackRows();
|
||||
document.getElementById('addVariantModal').classList.add('show');
|
||||
}
|
||||
|
||||
function inferBaseUnitFromStrength(strength) {
|
||||
const value = String(strength || '').toLowerCase();
|
||||
if (value.includes('/ml') || value.includes('ml')) return 'ml';
|
||||
if (value.includes('tablet')) return 'tablets';
|
||||
if (value.includes('capsule')) return 'capsules';
|
||||
return 'units';
|
||||
}
|
||||
|
||||
function getVariantPackRowsContainer() {
|
||||
return document.getElementById('variantPackRows');
|
||||
}
|
||||
|
||||
function refreshVariantPackRowLabels() {
|
||||
const container = getVariantPackRowsContainer();
|
||||
const baseUnit = document.getElementById('variantUnit')?.value || 'units';
|
||||
if (!container) return;
|
||||
|
||||
container.querySelectorAll('.variant-pack-row').forEach(row => {
|
||||
const packUnit = row.querySelector('.variant-pack-unit')?.value || 'pack';
|
||||
const label = row.querySelector('.variant-pack-size-label');
|
||||
if (!label) return;
|
||||
const titleCasePack = packUnit.charAt(0).toUpperCase() + packUnit.slice(1);
|
||||
label.textContent = `${titleCasePack} Size (${baseUnit}) *`;
|
||||
});
|
||||
}
|
||||
|
||||
function appendVariantPackRow(prefill = {}) {
|
||||
const container = getVariantPackRowsContainer();
|
||||
if (!container) return;
|
||||
|
||||
const row = document.createElement('div');
|
||||
row.className = 'delivery-line variant-pack-row';
|
||||
|
||||
const selectedPackUnit = prefill.packUnit || 'bottle';
|
||||
const selectedSize = prefill.packSize || '';
|
||||
const baseUnit = document.getElementById('variantUnit')?.value || 'units';
|
||||
|
||||
row.innerHTML = `
|
||||
<div class="delivery-line-grid" style="grid-template-columns: 1.2fr 1.2fr auto;">
|
||||
<div class="form-group">
|
||||
<label>Pack Type *</label>
|
||||
<select class="variant-pack-unit" required>
|
||||
<option value="bottle" ${selectedPackUnit === 'bottle' ? 'selected' : ''}>Bottle</option>
|
||||
<option value="box" ${selectedPackUnit === 'box' ? 'selected' : ''}>Box</option>
|
||||
<option value="vial" ${selectedPackUnit === 'vial' ? 'selected' : ''}>Vial</option>
|
||||
<option value="packet" ${selectedPackUnit === 'packet' ? 'selected' : ''}>Packet</option>
|
||||
</select>
|
||||
</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>
|
||||
</div>
|
||||
<button type="button" class="btn btn-danger btn-small variant-pack-remove-btn">Remove</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const removeBtn = row.querySelector('.variant-pack-remove-btn');
|
||||
const unitSelect = row.querySelector('.variant-pack-unit');
|
||||
if (removeBtn) {
|
||||
removeBtn.addEventListener('click', () => {
|
||||
if (container.querySelectorAll('.variant-pack-row').length <= 1) {
|
||||
showToast('At least one pack size is required', 'warning');
|
||||
return;
|
||||
}
|
||||
row.remove();
|
||||
});
|
||||
}
|
||||
|
||||
if (unitSelect) {
|
||||
unitSelect.addEventListener('change', refreshVariantPackRowLabels);
|
||||
}
|
||||
|
||||
container.appendChild(row);
|
||||
refreshVariantPackRowLabels();
|
||||
}
|
||||
|
||||
function initializeVariantPackRows() {
|
||||
const container = getVariantPackRowsContainer();
|
||||
if (!container) return;
|
||||
container.innerHTML = '';
|
||||
|
||||
const strengthValue = document.getElementById('variantStrength')?.value || '';
|
||||
const inferredBaseUnit = inferBaseUnitFromStrength(strengthValue);
|
||||
const variantUnitSelect = document.getElementById('variantUnit');
|
||||
if (variantUnitSelect) {
|
||||
variantUnitSelect.value = inferredBaseUnit;
|
||||
}
|
||||
|
||||
appendVariantPackRow({ packUnit: 'bottle' });
|
||||
}
|
||||
|
||||
// Handle add variant form
|
||||
async function handleAddVariant(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const drugId = parseInt(document.getElementById('variantDrugId').value);
|
||||
const baseUnit = document.getElementById('variantUnit').value;
|
||||
const rows = Array.from(document.querySelectorAll('#variantPackRows .variant-pack-row'));
|
||||
|
||||
if (rows.length === 0) {
|
||||
showToast('Please add at least one pack size', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const packPayloads = [];
|
||||
for (let i = 0; i < rows.length; i += 1) {
|
||||
const row = rows[i];
|
||||
const packUnit = row.querySelector('.variant-pack-unit')?.value;
|
||||
const packSize = parseFloat(row.querySelector('.variant-pack-size')?.value || '');
|
||||
|
||||
if (!packUnit || Number.isNaN(packSize) || packSize <= 0) {
|
||||
showToast(`Pack row ${i + 1} is incomplete`, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedPackUnit = packUnit.trim().toLowerCase();
|
||||
const titleCasePack = normalizedPackUnit.charAt(0).toUpperCase() + normalizedPackUnit.slice(1);
|
||||
packPayloads.push({
|
||||
label: `${titleCasePack} ${packSize} ${baseUnit}`,
|
||||
pack_unit_name: normalizedPackUnit,
|
||||
pack_size_in_base_units: packSize,
|
||||
is_active: true
|
||||
});
|
||||
}
|
||||
|
||||
const variantData = {
|
||||
strength: document.getElementById('variantStrength').value,
|
||||
quantity: parseFloat(document.getElementById('variantQuantity').value),
|
||||
unit: document.getElementById('variantUnit').value,
|
||||
quantity: 0,
|
||||
unit: baseUnit,
|
||||
base_unit: baseUnit,
|
||||
low_stock_threshold: parseFloat(document.getElementById('variantThreshold').value)
|
||||
};
|
||||
|
||||
@@ -927,9 +1259,41 @@ async function handleAddVariant(e) {
|
||||
|
||||
if (!response.ok) throw new Error('Failed to add variant');
|
||||
|
||||
const createdVariant = await response.json();
|
||||
|
||||
for (const packPayload of packPayloads) {
|
||||
const packResponse = await apiCall(`/variants/${createdVariant.id}/packs`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(packPayload)
|
||||
});
|
||||
|
||||
if (!packResponse.ok) {
|
||||
const packError = await packResponse.json();
|
||||
throw new Error(packError.detail || 'Variant created but pack size creation failed');
|
||||
}
|
||||
}
|
||||
|
||||
// Archive the auto-created default 1:1 pack when custom pack sizes are configured.
|
||||
const packsResponse = await apiCall(`/variants/${createdVariant.id}/packs`);
|
||||
if (packsResponse.ok) {
|
||||
const packs = await packsResponse.json();
|
||||
const defaultPack = packs.find(
|
||||
p => p.is_active && Number(p.pack_size_in_base_units) === 1 && (p.pack_unit_name || '').toLowerCase() === baseUnit.toLowerCase()
|
||||
);
|
||||
if (defaultPack && packs.filter(p => p.is_active).length > 1) {
|
||||
await apiCall(`/variant-packs/${defaultPack.id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ is_active: false })
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('variantForm').reset();
|
||||
closeModal(document.getElementById('addVariantModal'));
|
||||
await loadDrugs();
|
||||
if (document.getElementById('receiveDeliveryModal')?.classList.contains('show')) {
|
||||
refreshDeliveryVariantSelects();
|
||||
}
|
||||
renderDrugs();
|
||||
showToast('Variant added successfully!', 'success');
|
||||
} catch (error) {
|
||||
@@ -958,6 +1322,13 @@ function openEditVariantModal(variantId) {
|
||||
document.getElementById('editVariantModal').classList.add('show');
|
||||
}
|
||||
|
||||
function inferPackUnitName(baseUnit) {
|
||||
const value = String(baseUnit || 'pack').trim().toLowerCase();
|
||||
if (!value) return 'pack';
|
||||
if (value.endsWith('s') && value.length > 1) return value.slice(0, -1);
|
||||
return value;
|
||||
}
|
||||
|
||||
// Handle edit variant form
|
||||
async function handleEditVariant(e) {
|
||||
e.preventDefault();
|
||||
@@ -988,6 +1359,140 @@ async function handleEditVariant(e) {
|
||||
}
|
||||
}
|
||||
|
||||
async function openVariantPacksModal(variantId) {
|
||||
const variant = getVariantById(variantId);
|
||||
if (!variant) {
|
||||
showToast('Variant not found', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const parentDrug = allDrugs.find(d => (d.variants || []).some(v => v.id === variantId));
|
||||
activeVariantPacksVariantId = variantId;
|
||||
|
||||
const label = document.getElementById('variantPacksLabel');
|
||||
if (label) {
|
||||
const drugName = parentDrug ? parentDrug.name : 'Drug';
|
||||
label.textContent = `${drugName} ${variant.strength} | Base Unit: ${variant.unit}`;
|
||||
}
|
||||
|
||||
const form = document.getElementById('variantPacksForm');
|
||||
if (form) form.reset();
|
||||
document.getElementById('variantPacksVariantId').value = String(variantId);
|
||||
document.getElementById('variantPacksNewUnit').value = inferPackUnitName(variant.unit);
|
||||
|
||||
await refreshVariantPacksList();
|
||||
openModal(document.getElementById('variantPacksModal'));
|
||||
}
|
||||
|
||||
async function refreshVariantPacksList() {
|
||||
const variantId = parseInt(document.getElementById('variantPacksVariantId')?.value || '', 10);
|
||||
const list = document.getElementById('variantPacksList');
|
||||
if (!variantId || !list) return;
|
||||
|
||||
list.innerHTML = '<p class="loading">Loading packs...</p>';
|
||||
|
||||
try {
|
||||
const response = await apiCall(`/variants/${variantId}/packs`);
|
||||
if (!response.ok) throw new Error('Failed to load pack presentations');
|
||||
const packs = await response.json();
|
||||
|
||||
if (!Array.isArray(packs) || packs.length === 0) {
|
||||
list.innerHTML = '<p class="empty">No pack presentations defined.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = `
|
||||
<div class="locations-table">
|
||||
${packs.map(pack => `
|
||||
<div class="location-item" style="${pack.is_active ? '' : 'opacity: 0.6;'}">
|
||||
<div style="flex: 1;">
|
||||
<strong>${escapeHtml(pack.label)}</strong>
|
||||
<div style="font-size: 0.88em; color: #666;">
|
||||
${escapeHtml(pack.pack_unit_name)} | ${pack.pack_size_in_base_units} base units
|
||||
${pack.is_active ? '' : ' | archived'}
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn ${pack.is_active ? 'btn-danger' : 'btn-secondary'} btn-small" onclick="toggleVariantPackActive(${pack.id}, ${pack.is_active ? 'false' : 'true'})">
|
||||
${pack.is_active ? 'Archive' : 'Restore'}
|
||||
</button>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
} catch (error) {
|
||||
console.error('Error loading variant packs:', error);
|
||||
list.innerHTML = '<p class="empty">Failed to load pack presentations</p>';
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCreateVariantPack(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const variantId = parseInt(document.getElementById('variantPacksVariantId').value, 10);
|
||||
const label = document.getElementById('variantPacksNewLabel').value.trim();
|
||||
const packUnitName = document.getElementById('variantPacksNewUnit').value.trim();
|
||||
const size = parseFloat(document.getElementById('variantPacksNewSize').value);
|
||||
|
||||
if (!variantId || !label || !packUnitName || Number.isNaN(size) || size <= 0) {
|
||||
showToast('Please complete all pack fields', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiCall(`/variants/${variantId}/packs`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
label,
|
||||
pack_unit_name: packUnitName,
|
||||
pack_size_in_base_units: size,
|
||||
is_active: true
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.detail || 'Failed to add pack presentation');
|
||||
}
|
||||
|
||||
document.getElementById('variantPacksForm').reset();
|
||||
const variant = getVariantById(variantId);
|
||||
document.getElementById('variantPacksNewUnit').value = inferPackUnitName(variant?.unit || 'pack');
|
||||
await loadDrugs();
|
||||
await refreshVariantPacksList();
|
||||
if (document.getElementById('receiveDeliveryModal')?.classList.contains('show')) {
|
||||
refreshDeliveryVariantSelects();
|
||||
}
|
||||
showToast('Pack presentation added', 'success');
|
||||
} catch (error) {
|
||||
console.error('Error creating pack presentation:', error);
|
||||
showToast('Failed to add pack presentation: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleVariantPackActive(packId, nextActiveState) {
|
||||
try {
|
||||
const response = await apiCall(`/variant-packs/${packId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ is_active: Boolean(nextActiveState) })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.detail || 'Failed to update pack state');
|
||||
}
|
||||
|
||||
await loadDrugs();
|
||||
await refreshVariantPacksList();
|
||||
if (document.getElementById('receiveDeliveryModal')?.classList.contains('show')) {
|
||||
refreshDeliveryVariantSelects();
|
||||
}
|
||||
showToast('Pack updated', 'success');
|
||||
} catch (error) {
|
||||
console.error('Error updating pack state:', error);
|
||||
showToast('Failed to update pack: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Dispense from variant
|
||||
function dispenseVariant(variantId) {
|
||||
// Update the dropdown display with all variants
|
||||
@@ -1751,3 +2256,306 @@ async function handleBatchReceive(e) {
|
||||
showToast('Failed to receive batch: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function getActiveDeliveryDrug() {
|
||||
return allDrugs.find(d => d.id === deliveryDrugId);
|
||||
}
|
||||
|
||||
function getVariantById(variantId) {
|
||||
for (const drug of allDrugs) {
|
||||
const found = (drug.variants || []).find(v => v.id === variantId);
|
||||
if (found) return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildDeliveryVariantOptions(drug, selectedVariantId = '') {
|
||||
if (!drug || !drug.variants || drug.variants.length === 0) {
|
||||
return '<option value="">-- No variants available --</option>';
|
||||
}
|
||||
|
||||
return [`<option value="">-- Select variant --</option>`, ...drug.variants.map(v => {
|
||||
const selected = String(v.id) === String(selectedVariantId) ? ' selected' : '';
|
||||
return `<option value="${v.id}"${selected}>${escapeHtml(v.strength)} (${escapeHtml(v.unit)})</option>`;
|
||||
})].join('');
|
||||
}
|
||||
|
||||
function getActivePacksForVariant(variant) {
|
||||
if (!variant || !Array.isArray(variant.packs)) return [];
|
||||
return variant.packs.filter(pack => pack.is_active);
|
||||
}
|
||||
|
||||
function buildDeliveryPackOptions(variant, selectedPackId = '') {
|
||||
const packs = getActivePacksForVariant(variant);
|
||||
if (packs.length === 0) {
|
||||
return '<option value="">-- No active packs --</option>';
|
||||
}
|
||||
|
||||
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})`;
|
||||
return `<option value="${pack.id}"${selected}>${escapeHtml(label)}</option>`;
|
||||
})].join('');
|
||||
}
|
||||
|
||||
function buildDeliveryLocationOptions(selectedLocationId = '') {
|
||||
return [`<option value="">-- Select location --</option>`, ...deliveryLocations.map(location => {
|
||||
const selected = String(location.id) === String(selectedLocationId) ? ' selected' : '';
|
||||
return `<option value="${location.id}"${selected}>${escapeHtml(location.name)}</option>`;
|
||||
})].join('');
|
||||
}
|
||||
|
||||
function updateDeliveryLineQuantityDisplay(line) {
|
||||
const variantId = parseInt(line.querySelector('.delivery-variant-select')?.value || '', 10);
|
||||
const packSelect = line.querySelector('.delivery-pack-select');
|
||||
|
||||
const variant = getVariantById(variantId);
|
||||
if (!variant || !packSelect) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentPackId = packSelect.value;
|
||||
packSelect.innerHTML = buildDeliveryPackOptions(variant, currentPackId);
|
||||
}
|
||||
|
||||
function wireDeliveryLineEvents(line) {
|
||||
const variantSelect = line.querySelector('.delivery-variant-select');
|
||||
const packSelect = line.querySelector('.delivery-pack-select');
|
||||
const packCountInput = line.querySelector('.delivery-pack-count');
|
||||
|
||||
if (variantSelect && packSelect) {
|
||||
variantSelect.addEventListener('change', () => {
|
||||
const variantId = parseInt(variantSelect.value || '', 10);
|
||||
const variant = getVariantById(variantId);
|
||||
packSelect.innerHTML = buildDeliveryPackOptions(variant, '');
|
||||
if (packCountInput) packCountInput.value = '';
|
||||
updateDeliveryLineQuantityDisplay(line);
|
||||
});
|
||||
}
|
||||
|
||||
if (packSelect) {
|
||||
packSelect.addEventListener('change', () => {
|
||||
updateDeliveryLineQuantityDisplay(line);
|
||||
});
|
||||
}
|
||||
|
||||
if (packCountInput) {
|
||||
packCountInput.addEventListener('input', () => {
|
||||
updateDeliveryLineQuantityDisplay(line);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function appendDeliveryLine(prefill = {}) {
|
||||
const container = document.getElementById('deliveryLinesContainer');
|
||||
const drug = getActiveDeliveryDrug();
|
||||
if (!container || !drug) return;
|
||||
|
||||
deliveryLineCounter += 1;
|
||||
const lineId = `delivery-line-${deliveryLineCounter}`;
|
||||
|
||||
const line = document.createElement('div');
|
||||
line.className = 'delivery-line';
|
||||
line.dataset.lineId = lineId;
|
||||
|
||||
const initialVariant = drug.variants.find(v => String(v.id) === String(prefill.variantId)) || drug.variants[0] || null;
|
||||
const initialVariantId = prefill.variantId || (initialVariant ? initialVariant.id : '');
|
||||
const initialPackId = prefill.packId || '';
|
||||
const initialPackCount = prefill.packCount || '';
|
||||
|
||||
line.innerHTML = `
|
||||
<div class="delivery-line-grid">
|
||||
<div class="form-group">
|
||||
<label>Variant</label>
|
||||
<select class="delivery-variant-select" required>
|
||||
${buildDeliveryVariantOptions(drug, initialVariantId)}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Pack Type</label>
|
||||
<select class="delivery-pack-select" required>
|
||||
${buildDeliveryPackOptions(initialVariant, initialPackId)}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Pack Count</label>
|
||||
<input type="number" class="delivery-pack-count" min="0.0001" step="0.0001" value="${initialPackCount}" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Batch Number</label>
|
||||
<input type="text" class="delivery-batch-number" value="${prefill.batchNumber || ''}" placeholder="e.g. ABC123" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Expiry</label>
|
||||
<input type="date" class="delivery-expiry-date" value="${prefill.expiryDate || ''}" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Location</label>
|
||||
<select class="delivery-location-select" required>
|
||||
${buildDeliveryLocationOptions(prefill.locationId || '')}
|
||||
</select>
|
||||
</div>
|
||||
<button type="button" class="btn btn-danger btn-small delivery-remove-btn">Remove</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const removeBtn = line.querySelector('.delivery-remove-btn');
|
||||
if (removeBtn) {
|
||||
removeBtn.addEventListener('click', () => {
|
||||
if (container.children.length <= 1) {
|
||||
showToast('At least one delivery line is required', 'warning');
|
||||
return;
|
||||
}
|
||||
line.remove();
|
||||
});
|
||||
}
|
||||
|
||||
wireDeliveryLineEvents(line);
|
||||
updateDeliveryLineQuantityDisplay(line);
|
||||
|
||||
container.appendChild(line);
|
||||
}
|
||||
|
||||
function refreshDeliveryVariantSelects() {
|
||||
const drug = getActiveDeliveryDrug();
|
||||
const container = document.getElementById('deliveryLinesContainer');
|
||||
if (!drug || !container) return;
|
||||
|
||||
container.querySelectorAll('.delivery-line').forEach(line => {
|
||||
const select = line.querySelector('.delivery-variant-select');
|
||||
const packSelect = line.querySelector('.delivery-pack-select');
|
||||
if (!select) return;
|
||||
|
||||
const currentVariantId = select.value;
|
||||
select.innerHTML = buildDeliveryVariantOptions(drug, currentVariantId);
|
||||
|
||||
const variant = getVariantById(parseInt(select.value || '', 10));
|
||||
if (packSelect) {
|
||||
const currentPackId = packSelect.value;
|
||||
packSelect.innerHTML = buildDeliveryPackOptions(variant, currentPackId);
|
||||
}
|
||||
|
||||
updateDeliveryLineQuantityDisplay(line);
|
||||
});
|
||||
}
|
||||
|
||||
async function initializeDeliveryLocations() {
|
||||
try {
|
||||
const response = await apiCall('/locations');
|
||||
if (!response.ok) throw new Error('Failed to load locations');
|
||||
const locations = await response.json();
|
||||
deliveryLocations = locations.filter(location => location.is_active);
|
||||
} catch (error) {
|
||||
console.error('Error loading delivery locations:', error);
|
||||
showToast('Failed to load storage locations', 'error');
|
||||
deliveryLocations = [];
|
||||
}
|
||||
}
|
||||
|
||||
async function openReceiveDeliveryModal(drugId) {
|
||||
deliveryDrugId = drugId;
|
||||
const drug = getActiveDeliveryDrug();
|
||||
if (!drug) {
|
||||
showToast('Drug not found', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const form = document.getElementById('receiveDeliveryForm');
|
||||
const container = document.getElementById('deliveryLinesContainer');
|
||||
const label = document.getElementById('receiveDeliveryDrugLabel');
|
||||
|
||||
if (form) form.reset();
|
||||
if (container) container.innerHTML = '';
|
||||
if (label) label.textContent = `Drug: ${drug.name}`;
|
||||
|
||||
await initializeDeliveryLocations();
|
||||
appendDeliveryLine();
|
||||
|
||||
openModal(document.getElementById('receiveDeliveryModal'));
|
||||
}
|
||||
|
||||
function handleAddVariantFromDelivery() {
|
||||
if (!deliveryDrugId) {
|
||||
showToast('Select a drug first', 'warning');
|
||||
return;
|
||||
}
|
||||
openAddVariantModal(deliveryDrugId);
|
||||
}
|
||||
|
||||
async function handleReceiveDelivery(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const drug = getActiveDeliveryDrug();
|
||||
const container = document.getElementById('deliveryLinesContainer');
|
||||
if (!drug || !container) {
|
||||
showToast('Delivery context unavailable', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = Array.from(container.querySelectorAll('.delivery-line'));
|
||||
if (lines.length === 0) {
|
||||
showToast('Add at least one delivery line', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const payloads = [];
|
||||
|
||||
for (let i = 0; i < lines.length; i += 1) {
|
||||
const line = lines[i];
|
||||
const variantId = parseInt(line.querySelector('.delivery-variant-select')?.value || '', 10);
|
||||
const packIdRaw = line.querySelector('.delivery-pack-select')?.value || '';
|
||||
const packId = packIdRaw ? parseInt(packIdRaw, 10) : null;
|
||||
const packCountRaw = line.querySelector('.delivery-pack-count')?.value || '';
|
||||
const packCount = packCountRaw ? parseFloat(packCountRaw) : null;
|
||||
const batchNumber = (line.querySelector('.delivery-batch-number')?.value || '').trim();
|
||||
const expiryDate = line.querySelector('.delivery-expiry-date')?.value || '';
|
||||
const locationId = parseInt(line.querySelector('.delivery-location-select')?.value || '', 10);
|
||||
|
||||
if (!variantId || !packId || packCount === null || Number.isNaN(packCount) || packCount <= 0 || !batchNumber || !expiryDate || !locationId) {
|
||||
showToast(`Delivery line ${i + 1} is incomplete`, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const variant = drug.variants.find(v => v.id === 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');
|
||||
return;
|
||||
}
|
||||
const computedQuantity = packCount * selectedPack.pack_size_in_base_units;
|
||||
|
||||
payloads.push({
|
||||
variantId,
|
||||
payload: {
|
||||
batch_number: batchNumber,
|
||||
received_pack_id: packId,
|
||||
received_pack_count: packCount,
|
||||
expiry_date: expiryDate,
|
||||
location_id: locationId,
|
||||
notes: `Received ${packCount} ${selectedPack.pack_unit_name}(s), total ${computedQuantity} ${variant ? variant.unit : 'units'}`
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
for (let i = 0; i < payloads.length; i += 1) {
|
||||
const entry = payloads[i];
|
||||
const response = await apiCall(`/variants/${entry.variantId}/batches`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(entry.payload)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(`Line ${i + 1}: ${error.detail || 'Failed to receive delivery line'}`);
|
||||
}
|
||||
}
|
||||
|
||||
closeModal(document.getElementById('receiveDeliveryModal'));
|
||||
await loadDrugs();
|
||||
showToast(`Delivery received successfully (${payloads.length} line${payloads.length === 1 ? '' : 's'})`, 'success');
|
||||
} catch (error) {
|
||||
console.error('Error receiving delivery:', error);
|
||||
showToast('Failed to receive delivery: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
+89
-19
@@ -203,11 +203,33 @@
|
||||
</select>
|
||||
<small style="display: block; margin-top: 6px; color: #666;">Leave on automatic to use the earliest-expiry batch first. Choose a batch here to consume that batch first instead.</small>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<label for="dispenseMode">Dispense Mode *</label>
|
||||
<select id="dispenseMode" onchange="updateDispenseModeUi()">
|
||||
<option value="subunit">Subunit Quantity</option>
|
||||
<option value="pack">Whole Packs</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="dispenseQuantityGroup">
|
||||
<label for="dispenseQuantity">Quantity *</label>
|
||||
<input type="number" id="dispenseQuantity" step="0.1" onchange="updateAllocationPreview()">
|
||||
</div>
|
||||
|
||||
<div class="form-row" id="dispensePackRow" style="display: none;">
|
||||
<div class="form-group">
|
||||
<label for="dispensePackSelect">Pack Type *</label>
|
||||
<select id="dispensePackSelect" onchange="updateDispenseQuantityFromPack()">
|
||||
<option value="">-- Select pack --</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="dispensePackCount">Pack Count *</label>
|
||||
<input type="number" id="dispensePackCount" min="0.0001" step="0.0001" onchange="updateDispenseQuantityFromPack()">
|
||||
<small id="dispensePackPreview" style="display: block; margin-top: 6px; color: #666;">Select a pack and whole-number count.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
@@ -307,23 +329,21 @@
|
||||
<input type="text" id="variantStrength" placeholder="e.g., 5.4mg, 10.8mg, 100ml" required>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="variantQuantity">Quantity *</label>
|
||||
<input type="number" id="variantQuantity" step="0.1" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="variantUnit">Unit *</label>
|
||||
<select id="variantUnit">
|
||||
<option value="tablets">Tablets</option>
|
||||
<option value="bottles">Bottles</option>
|
||||
<option value="boxes">boxes</option>
|
||||
<option value="vials">Vials</option>
|
||||
<option value="units">Units</option>
|
||||
<option value="packets">Packets</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="variantUnit">Base Unit *</label>
|
||||
<select id="variantUnit">
|
||||
<option value="ml">ml</option>
|
||||
<option value="tablets">tablets</option>
|
||||
<option value="capsules">capsules</option>
|
||||
<option value="units">units</option>
|
||||
<option value="vials">vials</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Pack Sizes *</label>
|
||||
<div id="variantPackRows" class="delivery-lines"></div>
|
||||
<button type="button" id="addVariantPackRowBtn" class="btn btn-secondary btn-small">+ Add Another Size</button>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@@ -358,7 +378,7 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="editVariantUnit">Unit *</label>
|
||||
<label for="editVariantUnit">Base Unit *</label>
|
||||
<select id="editVariantUnit">
|
||||
<option value="tablets">Tablets</option>
|
||||
<option value="bottles">Bottles</option>
|
||||
@@ -575,6 +595,56 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Receive Delivery Modal -->
|
||||
<div id="receiveDeliveryModal" class="modal">
|
||||
<div class="modal-content modal-large receive-delivery-modal-content">
|
||||
<span class="close">×</span>
|
||||
<h2>Receive Delivery</h2>
|
||||
<p id="receiveDeliveryDrugLabel" style="margin: 6px 0 16px; color: #666; font-weight: 600;"></p>
|
||||
<form id="receiveDeliveryForm" novalidate>
|
||||
<div id="deliveryLinesContainer" class="delivery-lines"></div>
|
||||
<div class="delivery-toolbar">
|
||||
<button type="button" id="addDeliveryLineBtn" class="btn btn-secondary">+ Add Delivery Line</button>
|
||||
<button type="button" id="addVariantFromDeliveryBtn" class="btn btn-info">+ Add Variant</button>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Receive Delivery</button>
|
||||
<button type="button" class="btn btn-secondary" id="cancelReceiveDeliveryBtn">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Variant Pack Management Modal -->
|
||||
<div id="variantPacksModal" class="modal">
|
||||
<div class="modal-content modal-large">
|
||||
<span class="close">×</span>
|
||||
<h2>Pack Presentations</h2>
|
||||
<p id="variantPacksLabel" style="margin: 6px 0 16px; color: #666; font-weight: 600;"></p>
|
||||
|
||||
<div class="form-group">
|
||||
<h3 style="margin-bottom: 8px;">Add Pack Presentation</h3>
|
||||
<form id="variantPacksForm">
|
||||
<input type="hidden" id="variantPacksVariantId">
|
||||
<div class="form-row">
|
||||
<input type="text" id="variantPacksNewLabel" placeholder="Label (e.g., Bottle 300 ml)" required>
|
||||
<input type="text" id="variantPacksNewUnit" placeholder="Pack Unit Name (e.g., bottle, box)" required>
|
||||
<input type="number" id="variantPacksNewSize" min="0.0001" step="0.0001" placeholder="Size in base units" required>
|
||||
<button type="submit" class="btn btn-primary btn-small">Add Pack</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="variantPacksList" class="locations-list">
|
||||
<p class="loading">Loading packs...</p>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-secondary" id="closeVariantPacksBtn">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
|
||||
@@ -673,6 +673,18 @@ footer {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#receiveDeliveryModal.show {
|
||||
align-items: flex-start;
|
||||
overflow-y: auto;
|
||||
padding: 24px 0;
|
||||
}
|
||||
|
||||
#receiveDeliveryModal .modal-content {
|
||||
width: min(1280px, 96vw) !important;
|
||||
max-width: min(1280px, 96vw) !important;
|
||||
max-height: calc(100vh - 48px) !important;
|
||||
}
|
||||
|
||||
#dispenseModal.show {
|
||||
align-items: flex-start;
|
||||
overflow-y: auto;
|
||||
@@ -877,6 +889,103 @@ footer {
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.delivery-lines {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin: 8px 0 16px;
|
||||
}
|
||||
|
||||
.delivery-line {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
background: #f9fbfd;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.receive-delivery-modal-content {
|
||||
width: min(1320px, 98vw);
|
||||
max-width: 1320px;
|
||||
max-height: 88vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.delivery-line-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.9fr 1.8fr 0.9fr 1.4fr 1.2fr 1.3fr auto;
|
||||
gap: 12px;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.delivery-line-grid > * {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.delivery-line-grid .form-group {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.delivery-line-grid label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-weight: 600;
|
||||
font-size: 0.88em;
|
||||
}
|
||||
|
||||
.delivery-line-grid input,
|
||||
.delivery-line-grid select {
|
||||
width: 100%;
|
||||
min-height: 40px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.delivery-toolbar {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin: 6px 0 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.delivery-remove-btn {
|
||||
align-self: end;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.receive-delivery-modal-content {
|
||||
width: min(1120px, 97vw);
|
||||
max-width: 1120px;
|
||||
}
|
||||
|
||||
.delivery-line-grid {
|
||||
grid-template-columns: 1.7fr 1.4fr 0.95fr 1.2fr 1.1fr 1.15fr;
|
||||
}
|
||||
|
||||
.delivery-remove-btn {
|
||||
grid-column: 1 / -1;
|
||||
justify-self: end;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
#receiveDeliveryModal .modal-content {
|
||||
width: 94vw !important;
|
||||
max-width: 94vw !important;
|
||||
}
|
||||
|
||||
.delivery-line-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.delivery-remove-btn {
|
||||
grid-column: 1 / -1;
|
||||
justify-self: end;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
main {
|
||||
@@ -935,6 +1044,14 @@ footer {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.delivery-line-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.delivery-toolbar {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.drug-details {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user