WIP gettnig there
This commit is contained in:
@@ -211,6 +211,7 @@ class DrugVariantResponse(BaseModel):
|
|||||||
unit: str
|
unit: str
|
||||||
base_unit: str
|
base_unit: str
|
||||||
low_stock_threshold: float
|
low_stock_threshold: float
|
||||||
|
has_inventory_history: bool = False
|
||||||
packs: List[VariantPackResponse] = []
|
packs: List[VariantPackResponse] = []
|
||||||
batches: List[BatchResponse] = []
|
batches: List[BatchResponse] = []
|
||||||
|
|
||||||
@@ -318,6 +319,19 @@ def write_audit_log(
|
|||||||
|
|
||||||
def enrich_variant_with_batches(db: Session, variant: DrugVariant) -> Dict[str, Any]:
|
def enrich_variant_with_batches(db: Session, variant: DrugVariant) -> Dict[str, Any]:
|
||||||
"""Return variant data with active batch details for API responses."""
|
"""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 = {
|
variant_dict = {
|
||||||
"id": variant.id,
|
"id": variant.id,
|
||||||
"drug_id": variant.drug_id,
|
"drug_id": variant.drug_id,
|
||||||
@@ -326,6 +340,7 @@ def enrich_variant_with_batches(db: Session, variant: DrugVariant) -> Dict[str,
|
|||||||
"unit": variant.unit,
|
"unit": variant.unit,
|
||||||
"base_unit": variant.unit,
|
"base_unit": variant.unit,
|
||||||
"low_stock_threshold": variant.low_stock_threshold,
|
"low_stock_threshold": variant.low_stock_threshold,
|
||||||
|
"has_inventory_history": has_batch_history or has_dispense_history,
|
||||||
}
|
}
|
||||||
packs = (
|
packs = (
|
||||||
db.query(VariantPack)
|
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")
|
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()]
|
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:
|
if variant_ids:
|
||||||
batch_ids = [row[0] for row in db.query(Batch.id).filter(Batch.drug_variant_id.in_(variant_ids)).all()]
|
batch_ids = [row[0] for row in db.query(Batch.id).filter(Batch.drug_variant_id.in_(variant_ids)).all()]
|
||||||
if batch_ids:
|
if batch_ids:
|
||||||
@@ -972,6 +1007,39 @@ def update_drug_variant(variant_id: int, variant_update: DrugVariantUpdate, db:
|
|||||||
payload["unit"] = cleaned_base_unit
|
payload["unit"] = cleaned_base_unit
|
||||||
payload.pop("base_unit", None)
|
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():
|
for field, value in payload.items():
|
||||||
setattr(variant, field, value)
|
setattr(variant, field, value)
|
||||||
|
|
||||||
@@ -995,6 +1063,25 @@ def delete_drug_variant(variant_id: int, db: Session = Depends(get_db), current_
|
|||||||
if not variant:
|
if not variant:
|
||||||
raise HTTPException(status_code=404, detail="Drug variant not found")
|
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()]
|
batch_ids = [row[0] for row in db.query(Batch.id).filter(Batch.drug_variant_id == variant_id).all()]
|
||||||
if batch_ids:
|
if batch_ids:
|
||||||
db.query(DispensingAllocation).filter(DispensingAllocation.batch_id.in_(batch_ids)).delete(synchronize_session=False)
|
db.query(DispensingAllocation).filter(DispensingAllocation.batch_id.in_(batch_ids)).delete(synchronize_session=False)
|
||||||
|
|||||||
+266
-165
@@ -5,12 +5,12 @@ let showLowStockOnly = false;
|
|||||||
let selectedLocationFilter = '';
|
let selectedLocationFilter = '';
|
||||||
let searchTerm = '';
|
let searchTerm = '';
|
||||||
let expandedDrugs = new Set();
|
let expandedDrugs = new Set();
|
||||||
|
let expandedVariants = new Set();
|
||||||
let currentUser = null;
|
let currentUser = null;
|
||||||
let accessToken = null;
|
let accessToken = null;
|
||||||
let deliveryDrugId = null;
|
let deliveryDrugId = null;
|
||||||
let deliveryLineCounter = 0;
|
let deliveryLineCounter = 0;
|
||||||
let deliveryLocations = [];
|
let deliveryLocations = [];
|
||||||
let activeVariantPacksVariantId = null;
|
|
||||||
|
|
||||||
// Toast notification system
|
// Toast notification system
|
||||||
function showToast(message, type = 'info', duration = 3000) {
|
function showToast(message, type = 'info', duration = 3000) {
|
||||||
@@ -231,11 +231,11 @@ function setupEventListeners() {
|
|||||||
const addDeliveryLineBtn = document.getElementById('addDeliveryLineBtn');
|
const addDeliveryLineBtn = document.getElementById('addDeliveryLineBtn');
|
||||||
const addVariantFromDeliveryBtn = document.getElementById('addVariantFromDeliveryBtn');
|
const addVariantFromDeliveryBtn = document.getElementById('addVariantFromDeliveryBtn');
|
||||||
const addVariantPackRowBtn = document.getElementById('addVariantPackRowBtn');
|
const addVariantPackRowBtn = document.getElementById('addVariantPackRowBtn');
|
||||||
|
const addEditVariantPackRowBtn = document.getElementById('addEditVariantPackRowBtn');
|
||||||
const variantUnitSelect = document.getElementById('variantUnit');
|
const variantUnitSelect = document.getElementById('variantUnit');
|
||||||
const variantStrengthInput = document.getElementById('variantStrength');
|
const variantStrengthInput = document.getElementById('variantStrength');
|
||||||
|
const editVariantUnitSelect = document.getElementById('editVariantUnit');
|
||||||
const dispenseModeSelect = document.getElementById('dispenseMode');
|
const dispenseModeSelect = document.getElementById('dispenseMode');
|
||||||
const variantPacksForm = document.getElementById('variantPacksForm');
|
|
||||||
const closeVariantPacksBtn = document.getElementById('closeVariantPacksBtn');
|
|
||||||
const showAllBtn = document.getElementById('showAllBtn');
|
const showAllBtn = document.getElementById('showAllBtn');
|
||||||
const showLowStockBtn = document.getElementById('showLowStockBtn');
|
const showLowStockBtn = document.getElementById('showLowStockBtn');
|
||||||
const locationFilterSelect = document.getElementById('locationFilterSelect');
|
const locationFilterSelect = document.getElementById('locationFilterSelect');
|
||||||
@@ -267,11 +267,17 @@ function setupEventListeners() {
|
|||||||
if (addDeliveryLineBtn) addDeliveryLineBtn.addEventListener('click', () => appendDeliveryLine());
|
if (addDeliveryLineBtn) addDeliveryLineBtn.addEventListener('click', () => appendDeliveryLine());
|
||||||
if (addVariantFromDeliveryBtn) addVariantFromDeliveryBtn.addEventListener('click', handleAddVariantFromDelivery);
|
if (addVariantFromDeliveryBtn) addVariantFromDeliveryBtn.addEventListener('click', handleAddVariantFromDelivery);
|
||||||
if (addVariantPackRowBtn) addVariantPackRowBtn.addEventListener('click', () => appendVariantPackRow());
|
if (addVariantPackRowBtn) addVariantPackRowBtn.addEventListener('click', () => appendVariantPackRow());
|
||||||
|
if (addEditVariantPackRowBtn) addEditVariantPackRowBtn.addEventListener('click', () => appendEditVariantPackRow());
|
||||||
if (variantUnitSelect) {
|
if (variantUnitSelect) {
|
||||||
variantUnitSelect.addEventListener('change', () => {
|
variantUnitSelect.addEventListener('change', () => {
|
||||||
refreshVariantPackRowLabels();
|
refreshVariantPackRowLabels();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (editVariantUnitSelect) {
|
||||||
|
editVariantUnitSelect.addEventListener('change', () => {
|
||||||
|
refreshEditVariantPackRowLabels();
|
||||||
|
});
|
||||||
|
}
|
||||||
if (variantStrengthInput && variantUnitSelect) {
|
if (variantStrengthInput && variantUnitSelect) {
|
||||||
variantStrengthInput.addEventListener('blur', () => {
|
variantStrengthInput.addEventListener('blur', () => {
|
||||||
variantUnitSelect.value = inferBaseUnitFromStrength(variantStrengthInput.value);
|
variantUnitSelect.value = inferBaseUnitFromStrength(variantStrengthInput.value);
|
||||||
@@ -307,9 +313,6 @@ function setupEventListeners() {
|
|||||||
const closeLocationManagementBtn = document.getElementById('closeLocationManagementBtn');
|
const closeLocationManagementBtn = document.getElementById('closeLocationManagementBtn');
|
||||||
if (closeLocationManagementBtn) closeLocationManagementBtn.addEventListener('click', () => closeModal(document.getElementById('locationManagementModal')));
|
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');
|
const createLocationForm = document.getElementById('createLocationForm');
|
||||||
if (createLocationForm) createLocationForm.addEventListener('submit', createLocation);
|
if (createLocationForm) createLocationForm.addEventListener('submit', createLocation);
|
||||||
|
|
||||||
@@ -574,6 +577,63 @@ function formatDisplayDate(value) {
|
|||||||
return parsed.toLocaleDateString();
|
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) {
|
function getBatchLocationLabel(batch) {
|
||||||
return batch.location_name || batch.location?.name || `Location #${batch.location_id}`;
|
return batch.location_name || batch.location?.name || `Location #${batch.location_id}`;
|
||||||
}
|
}
|
||||||
@@ -882,8 +942,9 @@ function renderDrugs() {
|
|||||||
const variantsHtml = isExpanded ? `
|
const variantsHtml = isExpanded ? `
|
||||||
${drug.variants.map(variant => {
|
${drug.variants.map(variant => {
|
||||||
const variantIsLowStock = variant.quantity <= variant.low_stock_threshold;
|
const variantIsLowStock = variant.quantity <= variant.low_stock_threshold;
|
||||||
|
const variantExpanded = expandedVariants.has(variant.id);
|
||||||
return `
|
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-info">
|
||||||
<div class="variant-details">
|
<div class="variant-details">
|
||||||
<div class="variant-name">${escapeHtml(drug.name)} ${escapeHtml(variant.strength)}</div>
|
<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'}">
|
<span class="variant-badge ${variantIsLowStock ? 'badge-low' : 'badge-normal'}">
|
||||||
${variantIsLowStock ? 'Low Stock' : 'OK'}
|
${variantIsLowStock ? 'Low Stock' : 'OK'}
|
||||||
</span>
|
</span>
|
||||||
|
<span style="margin-left: 8px; font-size: 0.85em; color: #475569;">Inventory ${variantExpanded ? '▼' : '▶'}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="variant-actions">
|
<div class="variant-actions">
|
||||||
${!isReadOnly ? `
|
${!isReadOnly ? `
|
||||||
<button class="btn btn-info btn-small" onclick="event.stopPropagation(); openVariantPacksModal(${variant.id})">📦 Packs</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-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="event.stopPropagation(); dispenseVariant(${variant.id})">💊 Dispense</button>
|
||||||
<button class="btn btn-success btn-small" onclick="dispenseVariant(${variant.id})">💊 Dispense</button>
|
<button class="btn btn-warning btn-small" onclick="event.stopPropagation(); openEditVariantModal(${variant.id})">Edit</button>
|
||||||
<button class="btn btn-warning btn-small" onclick="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>
|
||||||
<button class="btn btn-danger btn-small" onclick="deleteVariant(${variant.id})">Delete</button>
|
|
||||||
` : ''}
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
|
${variantExpanded ? renderVariantInventoryDetails(variant) : ''}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}).join('')}` : '';
|
}).join('')}` : '';
|
||||||
@@ -931,7 +993,7 @@ function renderDrugs() {
|
|||||||
<button class="btn btn-info btn-small" onclick="event.stopPropagation(); showDrugHistory(${drug.id})">📋 History</button>
|
<button class="btn btn-info btn-small" onclick="event.stopPropagation(); showDrugHistory(${drug.id})">📋 History</button>
|
||||||
${!isReadOnly ? `
|
${!isReadOnly ? `
|
||||||
<button class="btn btn-warning btn-small" onclick="event.stopPropagation(); openEditModal(${drug.id})">Edit Drug</button>
|
<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>
|
<span class="expand-icon">${isExpanded ? '▼' : '▶'}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -1099,12 +1161,29 @@ function closeEditModal() {
|
|||||||
function toggleDrugExpansion(drugId) {
|
function toggleDrugExpansion(drugId) {
|
||||||
if (expandedDrugs.has(drugId)) {
|
if (expandedDrugs.has(drugId)) {
|
||||||
expandedDrugs.delete(drugId);
|
expandedDrugs.delete(drugId);
|
||||||
|
const collapsedDrug = allDrugs.find(drug => drug.id === drugId);
|
||||||
|
if (collapsedDrug) {
|
||||||
|
(collapsedDrug.variants || []).forEach(variant => expandedVariants.delete(variant.id));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
expandedDrugs.add(drugId);
|
expandedDrugs.add(drugId);
|
||||||
}
|
}
|
||||||
renderDrugs();
|
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
|
// Open add variant modal
|
||||||
function openAddVariantModal(drugId) {
|
function openAddVariantModal(drugId) {
|
||||||
const drug = allDrugs.find(d => d.id === drugId);
|
const drug = allDrugs.find(d => d.id === drugId);
|
||||||
@@ -1130,6 +1209,10 @@ function getVariantPackRowsContainer() {
|
|||||||
return document.getElementById('variantPackRows');
|
return document.getElementById('variantPackRows');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getEditVariantPackRowsContainer() {
|
||||||
|
return document.getElementById('editVariantPackRows');
|
||||||
|
}
|
||||||
|
|
||||||
function refreshVariantPackRowLabels() {
|
function refreshVariantPackRowLabels() {
|
||||||
const container = getVariantPackRowsContainer();
|
const container = getVariantPackRowsContainer();
|
||||||
const baseUnit = document.getElementById('variantUnit')?.value || 'units';
|
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 = {}) {
|
function appendVariantPackRow(prefill = {}) {
|
||||||
const container = getVariantPackRowsContainer();
|
const container = getVariantPackRowsContainer();
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
@@ -1209,6 +1306,71 @@ function initializeVariantPackRows() {
|
|||||||
appendVariantPackRow({ packUnit: 'bottle' });
|
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
|
// Handle add variant form
|
||||||
async function handleAddVariant(e) {
|
async function handleAddVariant(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -1319,14 +1481,11 @@ function openEditVariantModal(variantId) {
|
|||||||
document.getElementById('editVariantUnit').value = variant.unit;
|
document.getElementById('editVariantUnit').value = variant.unit;
|
||||||
document.getElementById('editVariantThreshold').value = variant.low_stock_threshold;
|
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) {
|
document.getElementById('editVariantModal').classList.add('show');
|
||||||
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
|
// Handle edit variant form
|
||||||
@@ -1334,165 +1493,89 @@ async function handleEditVariant(e) {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const variantId = parseInt(document.getElementById('editVariantId').value);
|
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 = {
|
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)
|
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 {
|
try {
|
||||||
const response = await apiCall(`/variants/${variantId}`, {
|
const response = await apiCall(`/variants/${variantId}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify(variantData)
|
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'));
|
closeModal(document.getElementById('editVariantModal'));
|
||||||
await loadDrugs();
|
await loadDrugs();
|
||||||
renderDrugs();
|
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) {
|
} catch (error) {
|
||||||
console.error('Error updating variant:', 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
|
// Dispense from variant
|
||||||
function dispenseVariant(variantId) {
|
function dispenseVariant(variantId) {
|
||||||
// Update the dropdown display with all variants
|
// Update the dropdown display with all variants
|
||||||
@@ -1683,6 +1766,12 @@ async function handlePrintNotes(e) {
|
|||||||
|
|
||||||
// Delete variant
|
// Delete variant
|
||||||
async function deleteVariant(variantId) {
|
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;
|
if (!confirm('Are you sure you want to delete this variant?')) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1690,14 +1779,17 @@ async function deleteVariant(variantId) {
|
|||||||
method: 'DELETE'
|
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();
|
await loadDrugs();
|
||||||
renderDrugs();
|
renderDrugs();
|
||||||
showToast('Variant deleted successfully!', 'success');
|
showToast('Variant deleted successfully!', 'success');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting variant:', 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
|
// Delete drug
|
||||||
async function deleteDrug(drugId) {
|
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;
|
if (!confirm('Are you sure you want to delete this drug?')) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1810,13 +1908,16 @@ async function deleteDrug(drugId) {
|
|||||||
method: 'DELETE'
|
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();
|
await loadDrugs();
|
||||||
showToast('Drug deleted successfully!', 'success');
|
showToast('Drug deleted successfully!', 'success');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting drug:', 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
@@ -366,6 +366,9 @@
|
|||||||
<h2>Edit Variant</h2>
|
<h2>Edit Variant</h2>
|
||||||
<form id="editVariantForm">
|
<form id="editVariantForm">
|
||||||
<input type="hidden" id="editVariantId">
|
<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">
|
<div class="form-group">
|
||||||
<label for="editVariantStrength">Strength *</label>
|
<label for="editVariantStrength">Strength *</label>
|
||||||
<input type="text" id="editVariantStrength" required>
|
<input type="text" id="editVariantStrength" required>
|
||||||
@@ -389,6 +392,12 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="form-group">
|
||||||
<label for="editVariantThreshold">Low Stock Threshold *</label>
|
<label for="editVariantThreshold">Low Stock Threshold *</label>
|
||||||
@@ -616,35 +625,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
<script src="app.js"></script>
|
<script src="app.js"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user