WIP gettnig there

This commit is contained in:
2026-03-29 11:13:56 -04:00
parent ad1bb59f98
commit 664a3189bd
3 changed files with 362 additions and 194 deletions
+87
View File
@@ -211,6 +211,7 @@ class DrugVariantResponse(BaseModel):
unit: str
base_unit: str
low_stock_threshold: float
has_inventory_history: bool = False
packs: List[VariantPackResponse] = []
batches: List[BatchResponse] = []
@@ -318,6 +319,19 @@ def write_audit_log(
def enrich_variant_with_batches(db: Session, variant: DrugVariant) -> Dict[str, Any]:
"""Return variant data with active batch details for API responses."""
has_batch_history = (
db.query(Batch.id)
.filter(Batch.drug_variant_id == variant.id)
.first()
is not None
)
has_dispense_history = (
db.query(Dispensing.id)
.filter(Dispensing.drug_variant_id == variant.id)
.first()
is not None
)
variant_dict = {
"id": variant.id,
"drug_id": variant.drug_id,
@@ -326,6 +340,7 @@ def enrich_variant_with_batches(db: Session, variant: DrugVariant) -> Dict[str,
"unit": variant.unit,
"base_unit": variant.unit,
"low_stock_threshold": variant.low_stock_threshold,
"has_inventory_history": has_batch_history or has_dispense_history,
}
packs = (
db.query(VariantPack)
@@ -861,6 +876,26 @@ def delete_drug(drug_id: int, db: Session = Depends(get_db), current_user: User
raise HTTPException(status_code=404, detail="Drug not found")
variant_ids = [row[0] for row in db.query(DrugVariant.id).filter(DrugVariant.drug_id == drug_id).all()]
if variant_ids:
has_batch_history = (
db.query(Batch.id)
.filter(Batch.drug_variant_id.in_(variant_ids))
.first()
is not None
)
has_dispense_history = (
db.query(Dispensing.id)
.filter(Dispensing.drug_variant_id.in_(variant_ids))
.first()
is not None
)
if has_batch_history or has_dispense_history:
raise HTTPException(
status_code=400,
detail="Cannot delete drug with variants that have batch or dispensing history. Archive or manage records first.",
)
if variant_ids:
batch_ids = [row[0] for row in db.query(Batch.id).filter(Batch.drug_variant_id.in_(variant_ids)).all()]
if batch_ids:
@@ -972,6 +1007,39 @@ def update_drug_variant(variant_id: int, variant_update: DrugVariantUpdate, db:
payload["unit"] = cleaned_base_unit
payload.pop("base_unit", None)
has_batch_history = (
db.query(Batch.id)
.filter(Batch.drug_variant_id == variant_id)
.first()
is not None
)
has_dispense_history = (
db.query(Dispensing.id)
.filter(Dispensing.drug_variant_id == variant_id)
.first()
is not None
)
is_locked = has_batch_history or has_dispense_history
locked_field_changes = []
if is_locked:
if "strength" in payload and payload["strength"] != variant.strength:
locked_field_changes.append("strength")
if "unit" in payload and payload["unit"] != variant.unit:
locked_field_changes.append("base_unit")
if "quantity" in payload and payload["quantity"] != variant.quantity:
locked_field_changes.append("quantity")
if locked_field_changes:
raise HTTPException(
status_code=400,
detail=(
"Cannot change "
+ ", ".join(locked_field_changes)
+ " after batches or dispensing history exist for this variant"
),
)
for field, value in payload.items():
setattr(variant, field, value)
@@ -995,6 +1063,25 @@ def delete_drug_variant(variant_id: int, db: Session = Depends(get_db), current_
if not variant:
raise HTTPException(status_code=404, detail="Drug variant not found")
has_batch_history = (
db.query(Batch.id)
.filter(Batch.drug_variant_id == variant_id)
.first()
is not None
)
has_dispense_history = (
db.query(Dispensing.id)
.filter(Dispensing.drug_variant_id == variant_id)
.first()
is not None
)
if has_batch_history or has_dispense_history:
raise HTTPException(
status_code=400,
detail="Cannot delete variant with batch or dispensing history. Archive or manage records first.",
)
batch_ids = [row[0] for row in db.query(Batch.id).filter(Batch.drug_variant_id == variant_id).all()]
if batch_ids:
db.query(DispensingAllocation).filter(DispensingAllocation.batch_id.in_(batch_ids)).delete(synchronize_session=False)
+266 -165
View File
@@ -5,12 +5,12 @@ let showLowStockOnly = false;
let selectedLocationFilter = '';
let searchTerm = '';
let expandedDrugs = new Set();
let expandedVariants = 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) {
@@ -231,11 +231,11 @@ function setupEventListeners() {
const addDeliveryLineBtn = document.getElementById('addDeliveryLineBtn');
const addVariantFromDeliveryBtn = document.getElementById('addVariantFromDeliveryBtn');
const addVariantPackRowBtn = document.getElementById('addVariantPackRowBtn');
const addEditVariantPackRowBtn = document.getElementById('addEditVariantPackRowBtn');
const variantUnitSelect = document.getElementById('variantUnit');
const variantStrengthInput = document.getElementById('variantStrength');
const editVariantUnitSelect = document.getElementById('editVariantUnit');
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');
@@ -267,11 +267,17 @@ function setupEventListeners() {
if (addDeliveryLineBtn) addDeliveryLineBtn.addEventListener('click', () => appendDeliveryLine());
if (addVariantFromDeliveryBtn) addVariantFromDeliveryBtn.addEventListener('click', handleAddVariantFromDelivery);
if (addVariantPackRowBtn) addVariantPackRowBtn.addEventListener('click', () => appendVariantPackRow());
if (addEditVariantPackRowBtn) addEditVariantPackRowBtn.addEventListener('click', () => appendEditVariantPackRow());
if (variantUnitSelect) {
variantUnitSelect.addEventListener('change', () => {
refreshVariantPackRowLabels();
});
}
if (editVariantUnitSelect) {
editVariantUnitSelect.addEventListener('change', () => {
refreshEditVariantPackRowLabels();
});
}
if (variantStrengthInput && variantUnitSelect) {
variantStrengthInput.addEventListener('blur', () => {
variantUnitSelect.value = inferBaseUnitFromStrength(variantStrengthInput.value);
@@ -307,9 +313,6 @@ 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);
@@ -574,6 +577,63 @@ function formatDisplayDate(value) {
return parsed.toLocaleDateString();
}
function formatDisplayNumber(value) {
const numeric = Number(value);
if (Number.isNaN(numeric)) return '0';
return Number.isInteger(numeric) ? String(numeric) : String(Number(numeric.toFixed(3)));
}
function renderVariantInventoryDetails(variant) {
const activePacks = getActivePacksForVariant(variant);
const batches = [...(variant.batches || [])]
.filter(batch => Number(batch.quantity) > 0)
.sort((a, b) => new Date(a.expiry_date) - new Date(b.expiry_date));
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>
<span style="color: #4b5563;"> (${formatDisplayNumber(pack.pack_size_in_base_units)} ${escapeHtml(variant.unit)})</span>
</div>
`).join('')
: '<div style="padding: 6px 8px; background: #ffffff; border: 1px dashed #cfd8e3; border-radius: 5px; font-size: 0.9em; color: #6b7280;">No active packs configured</div>';
const batchesHtml = batches.length > 0
? batches.map(batch => {
const locationLabel = getBatchLocationLabel(batch);
const hasPackState = batch.current_full_pack_count != null && batch.current_loose_base_units != null && batch.received_pack_label;
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.quantity)} ${escapeHtml(variant.unit)}`;
return `
<div style="padding: 8px; background: #ffffff; border: 1px solid #d8e2ea; border-radius: 5px; font-size: 0.9em;">
<div style="display: flex; justify-content: space-between; gap: 8px; flex-wrap: wrap;">
<strong>${escapeHtml(batch.batch_number)}</strong>
<span style="color: #4b5563;">Expires ${formatDisplayDate(batch.expiry_date)}</span>
</div>
<div style="margin-top: 4px; color: #374151;">${escapeHtml(locationLabel)} | ${stocktakeLabel}</div>
</div>
`;
}).join('')
: '<div style="padding: 6px 8px; background: #ffffff; border: 1px dashed #cfd8e3; border-radius: 5px; font-size: 0.9em; color: #6b7280;">No active batches</div>';
return `
<div style="margin-top: 10px; background: #f2f6fa; border: 1px solid #d6e0ea; border-radius: 8px; padding: 10px;" onclick="event.stopPropagation()">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px;">
<div>
<div style="font-size: 0.8em; text-transform: uppercase; letter-spacing: 0.04em; color: #4b5563; margin-bottom: 6px;">Active Packs</div>
<div style="display: grid; gap: 6px;">${packsHtml}</div>
</div>
<div>
<div style="font-size: 0.8em; text-transform: uppercase; letter-spacing: 0.04em; color: #4b5563; margin-bottom: 6px;">Current Batches</div>
<div style="display: grid; gap: 6px;">${batchesHtml}</div>
</div>
</div>
</div>
`;
}
function getBatchLocationLabel(batch) {
return batch.location_name || batch.location?.name || `Location #${batch.location_id}`;
}
@@ -882,8 +942,9 @@ function renderDrugs() {
const variantsHtml = isExpanded ? `
${drug.variants.map(variant => {
const variantIsLowStock = variant.quantity <= variant.low_stock_threshold;
const variantExpanded = expandedVariants.has(variant.id);
return `
<div class="variant-item ${variantIsLowStock ? 'low-stock' : ''}">
<div class="variant-item ${variantIsLowStock ? 'low-stock' : ''}" onclick="toggleVariantExpansion(${variant.id}, event)">
<div class="variant-info">
<div class="variant-details">
<div class="variant-name">${escapeHtml(drug.name)} ${escapeHtml(variant.strength)}</div>
@@ -893,17 +954,18 @@ function renderDrugs() {
<span class="variant-badge ${variantIsLowStock ? 'badge-low' : 'badge-normal'}">
${variantIsLowStock ? 'Low Stock' : 'OK'}
</span>
<span style="margin-left: 8px; font-size: 0.85em; color: #475569;">Inventory ${variantExpanded ? '▼' : '▶'}</span>
</div>
</div>
<div class="variant-actions">
${!isReadOnly ? `
<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>
<button class="btn btn-danger btn-small" onclick="deleteVariant(${variant.id})">Delete</button>
<button class="btn btn-primary btn-small" onclick="event.stopPropagation(); 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="event.stopPropagation(); dispenseVariant(${variant.id})">💊 Dispense</button>
<button class="btn btn-warning btn-small" onclick="event.stopPropagation(); openEditVariantModal(${variant.id})">Edit</button>
<button class="btn btn-danger btn-small" onclick="event.stopPropagation(); deleteVariant(${variant.id})" title="${variant.has_inventory_history ? 'Variant has history and cannot be deleted' : ''}">Delete</button>
` : ''}
</div>
${variantExpanded ? renderVariantInventoryDetails(variant) : ''}
</div>
`;
}).join('')}` : '';
@@ -931,7 +993,7 @@ function renderDrugs() {
<button class="btn btn-info btn-small" onclick="event.stopPropagation(); showDrugHistory(${drug.id})">📋 History</button>
${!isReadOnly ? `
<button class="btn btn-warning btn-small" onclick="event.stopPropagation(); openEditModal(${drug.id})">Edit Drug</button>
<button class="btn btn-danger btn-small" onclick="event.stopPropagation(); deleteDrug(${drug.id})">Delete</button>
<button class="btn btn-danger btn-small" onclick="event.stopPropagation(); deleteDrug(${drug.id})" title="${drug.variants.some(v => v.has_inventory_history) ? 'Drug has variants with history and cannot be deleted' : ''}">Delete</button>
` : ''}
<span class="expand-icon">${isExpanded ? '▼' : '▶'}</span>
</div>
@@ -1099,12 +1161,29 @@ function closeEditModal() {
function toggleDrugExpansion(drugId) {
if (expandedDrugs.has(drugId)) {
expandedDrugs.delete(drugId);
const collapsedDrug = allDrugs.find(drug => drug.id === drugId);
if (collapsedDrug) {
(collapsedDrug.variants || []).forEach(variant => expandedVariants.delete(variant.id));
}
} else {
expandedDrugs.add(drugId);
}
renderDrugs();
}
function toggleVariantExpansion(variantId, event) {
if (event) {
event.stopPropagation();
}
if (expandedVariants.has(variantId)) {
expandedVariants.delete(variantId);
} else {
expandedVariants.add(variantId);
}
renderDrugs();
}
// Open add variant modal
function openAddVariantModal(drugId) {
const drug = allDrugs.find(d => d.id === drugId);
@@ -1130,6 +1209,10 @@ function getVariantPackRowsContainer() {
return document.getElementById('variantPackRows');
}
function getEditVariantPackRowsContainer() {
return document.getElementById('editVariantPackRows');
}
function refreshVariantPackRowLabels() {
const container = getVariantPackRowsContainer();
const baseUnit = document.getElementById('variantUnit')?.value || 'units';
@@ -1144,6 +1227,20 @@ function refreshVariantPackRowLabels() {
});
}
function refreshEditVariantPackRowLabels() {
const container = getEditVariantPackRowsContainer();
const baseUnit = document.getElementById('editVariantUnit')?.value || 'units';
if (!container) return;
container.querySelectorAll('.edit-variant-pack-row').forEach(row => {
const packUnit = row.querySelector('.edit-variant-pack-unit')?.value || 'pack';
const label = row.querySelector('.edit-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;
@@ -1209,6 +1306,71 @@ function initializeVariantPackRows() {
appendVariantPackRow({ packUnit: 'bottle' });
}
function appendEditVariantPackRow(prefill = {}) {
const container = getEditVariantPackRowsContainer();
if (!container) return;
const row = document.createElement('div');
row.className = 'delivery-line edit-variant-pack-row';
const selectedPackUnit = prefill.packUnit || 'bottle';
const selectedSize = prefill.packSize || '';
const baseUnit = document.getElementById('editVariantUnit')?.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="edit-variant-pack-unit">
<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="edit-variant-pack-size-label">Bottle Size (${baseUnit}) *</label>
<input type="number" class="edit-variant-pack-size" min="0.0001" step="0.0001" value="${selectedSize}">
</div>
<button type="button" class="btn btn-danger btn-small edit-variant-pack-remove-btn">Remove</button>
</div>
`;
const removeBtn = row.querySelector('.edit-variant-pack-remove-btn');
const unitSelect = row.querySelector('.edit-variant-pack-unit');
if (removeBtn) {
removeBtn.addEventListener('click', () => {
row.remove();
});
}
if (unitSelect) {
unitSelect.addEventListener('change', refreshEditVariantPackRowLabels);
}
container.appendChild(row);
refreshEditVariantPackRowLabels();
}
function initializeEditVariantPackRows() {
const container = getEditVariantPackRowsContainer();
if (!container) return;
container.innerHTML = '';
appendEditVariantPackRow({ packUnit: 'bottle' });
}
function setEditVariantFieldLockState(isLocked) {
const strengthInput = document.getElementById('editVariantStrength');
const quantityInput = document.getElementById('editVariantQuantity');
const unitSelect = document.getElementById('editVariantUnit');
const lockNotice = document.getElementById('editVariantLockNotice');
if (strengthInput) strengthInput.disabled = isLocked;
if (quantityInput) quantityInput.disabled = isLocked;
if (unitSelect) unitSelect.disabled = isLocked;
if (lockNotice) lockNotice.style.display = isLocked ? 'block' : 'none';
}
// Handle add variant form
async function handleAddVariant(e) {
e.preventDefault();
@@ -1319,14 +1481,11 @@ function openEditVariantModal(variantId) {
document.getElementById('editVariantUnit').value = variant.unit;
document.getElementById('editVariantThreshold').value = variant.low_stock_threshold;
document.getElementById('editVariantModal').classList.add('show');
}
const hasInventoryContext = Boolean(variant.has_inventory_history);
setEditVariantFieldLockState(hasInventoryContext);
initializeEditVariantPackRows();
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;
document.getElementById('editVariantModal').classList.add('show');
}
// Handle edit variant form
@@ -1334,165 +1493,89 @@ async function handleEditVariant(e) {
e.preventDefault();
const variantId = parseInt(document.getElementById('editVariantId').value);
const strengthInput = document.getElementById('editVariantStrength');
const quantityInput = document.getElementById('editVariantQuantity');
const unitSelect = document.getElementById('editVariantUnit');
const baseUnit = unitSelect.value;
const variantData = {
strength: document.getElementById('editVariantStrength').value,
quantity: parseFloat(document.getElementById('editVariantQuantity').value),
unit: document.getElementById('editVariantUnit').value,
low_stock_threshold: parseFloat(document.getElementById('editVariantThreshold').value)
};
if (!strengthInput.disabled && !quantityInput.disabled && !unitSelect.disabled) {
const quantityValue = parseFloat(quantityInput.value);
if (Number.isNaN(quantityValue) || quantityValue < 0) {
showToast('Please enter a valid quantity (0 or greater)', 'warning');
return;
}
variantData.strength = strengthInput.value;
variantData.quantity = quantityValue;
variantData.unit = baseUnit;
variantData.base_unit = baseUnit;
}
const packRows = Array.from(document.querySelectorAll('#editVariantPackRows .edit-variant-pack-row'));
const newPackPayloads = [];
for (let i = 0; i < packRows.length; i += 1) {
const row = packRows[i];
const packUnitRaw = row.querySelector('.edit-variant-pack-unit')?.value || '';
const packSizeRaw = row.querySelector('.edit-variant-pack-size')?.value || '';
if (!packUnitRaw && !packSizeRaw) {
continue;
}
const packSize = parseFloat(packSizeRaw);
if (!packUnitRaw || Number.isNaN(packSize) || packSize <= 0) {
showToast(`Pack row ${i + 1} is incomplete`, 'warning');
return;
}
const normalizedPackUnit = packUnitRaw.trim().toLowerCase();
const titleCasePack = normalizedPackUnit.charAt(0).toUpperCase() + normalizedPackUnit.slice(1);
newPackPayloads.push({
label: `${titleCasePack} ${packSize} ${baseUnit}`,
pack_unit_name: normalizedPackUnit,
pack_size_in_base_units: packSize,
is_active: true
});
}
try {
const response = await apiCall(`/variants/${variantId}`, {
method: 'PUT',
body: JSON.stringify(variantData)
});
if (!response.ok) throw new Error('Failed to update variant');
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to update variant');
}
for (const packPayload of newPackPayloads) {
const packResponse = await apiCall(`/variants/${variantId}/packs`, {
method: 'POST',
body: JSON.stringify(packPayload)
});
if (!packResponse.ok) {
const packError = await packResponse.json();
throw new Error(packError.detail || 'Variant updated but failed to add one or more pack sizes');
}
}
closeModal(document.getElementById('editVariantModal'));
await loadDrugs();
renderDrugs();
showToast('Variant updated successfully!', 'success');
const message = newPackPayloads.length > 0
? `Variant updated and ${newPackPayloads.length} pack size${newPackPayloads.length === 1 ? '' : 's'} added`
: 'Variant updated successfully!';
showToast(message, 'success');
} catch (error) {
console.error('Error updating variant:', error);
showToast('Failed to update variant. Check the console for details.', 'error');
showToast('Failed to update variant: ' + error.message, 'error');
}
}
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
@@ -1683,6 +1766,12 @@ async function handlePrintNotes(e) {
// Delete variant
async function deleteVariant(variantId) {
const variant = getVariantById(variantId);
if (variant && variant.has_inventory_history) {
showToast('Cannot delete variant with batch or dispensing history', 'warning');
return;
}
if (!confirm('Are you sure you want to delete this variant?')) return;
try {
@@ -1690,14 +1779,17 @@ async function deleteVariant(variantId) {
method: 'DELETE'
});
if (!response.ok) throw new Error('Failed to delete variant');
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to delete variant');
}
await loadDrugs();
renderDrugs();
showToast('Variant deleted successfully!', 'success');
} catch (error) {
console.error('Error deleting variant:', error);
showToast('Failed to delete variant. Check the console for details.', 'error');
showToast('Failed to delete variant: ' + error.message, 'error');
}
}
@@ -1803,6 +1895,12 @@ async function handleEditDrug(e) {
// Delete drug
async function deleteDrug(drugId) {
const drug = allDrugs.find(d => d.id === drugId);
if (drug && drug.variants.some(v => v.has_inventory_history)) {
showToast('Cannot delete drug with variants that have batch or dispensing history', 'warning');
return;
}
if (!confirm('Are you sure you want to delete this drug?')) return;
try {
@@ -1810,13 +1908,16 @@ async function deleteDrug(drugId) {
method: 'DELETE'
});
if (!response.ok) throw new Error('Failed to delete drug');
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to delete drug');
}
await loadDrugs();
showToast('Drug deleted successfully!', 'success');
} catch (error) {
console.error('Error deleting drug:', error);
showToast('Failed to delete drug. Check the console for details.', 'error');
showToast('Failed to delete drug: ' + error.message, 'error');
}
}
+9 -29
View File
@@ -366,6 +366,9 @@
<h2>Edit Variant</h2>
<form id="editVariantForm">
<input type="hidden" id="editVariantId">
<p id="editVariantLockNotice" style="display:none; margin: 0 0 12px; padding: 8px 10px; background: #fff8e1; border: 1px solid #f5c15d; border-radius: 6px; color: #7a4f01;">
Strength, quantity, and base unit are locked once this variant has stock/batch history.
</p>
<div class="form-group">
<label for="editVariantStrength">Strength *</label>
<input type="text" id="editVariantStrength" required>
@@ -389,6 +392,12 @@
</select>
</div>
</div>
<div class="form-group">
<label>Add Pack Sizes</label>
<div id="editVariantPackRows" class="delivery-lines"></div>
<button type="button" id="addEditVariantPackRowBtn" class="btn btn-secondary btn-small">+ Add Another Size</button>
</div>
<div class="form-group">
<label for="editVariantThreshold">Low Stock Threshold *</label>
@@ -616,35 +625,6 @@
</div>
</div>
<!-- Variant Pack Management Modal -->
<div id="variantPacksModal" class="modal">
<div class="modal-content modal-large">
<span class="close">&times;</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>