From 664a3189bd499b90c7a18323db3fedf0b4acb5a7 Mon Sep 17 00:00:00 2001 From: James Pattinson Date: Sun, 29 Mar 2026 11:13:56 -0400 Subject: [PATCH] WIP gettnig there --- backend/app/main.py | 87 +++++++++ frontend/app.js | 431 +++++++++++++++++++++++++++----------------- frontend/index.html | 38 +--- 3 files changed, 362 insertions(+), 194 deletions(-) diff --git a/backend/app/main.py b/backend/app/main.py index 9cef6a9..a9d6ce1 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -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) diff --git a/frontend/app.js b/frontend/app.js index 6f458df..08010fa 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -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 => ` +
+ ${escapeHtml(pack.label)} + (${formatDisplayNumber(pack.pack_size_in_base_units)} ${escapeHtml(variant.unit)}) +
+ `).join('') + : '
No active packs configured
'; + + 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 ` +
+
+ ${escapeHtml(batch.batch_number)} + Expires ${formatDisplayDate(batch.expiry_date)} +
+
${escapeHtml(locationLabel)} | ${stocktakeLabel}
+
+ `; + }).join('') + : '
No active batches
'; + + return ` +
+
+
+
Active Packs
+
${packsHtml}
+
+
+
Current Batches
+
${batchesHtml}
+
+
+
+ `; +} + 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 ` -
+
${escapeHtml(drug.name)} ${escapeHtml(variant.strength)}
@@ -893,17 +954,18 @@ function renderDrugs() { ${variantIsLowStock ? 'Low Stock' : 'OK'} + Inventory ${variantExpanded ? '▼' : '▶'}
${!isReadOnly ? ` - - - - - + + + + ` : ''}
+ ${variantExpanded ? renderVariantInventoryDetails(variant) : ''}
`; }).join('')}` : ''; @@ -931,7 +993,7 @@ function renderDrugs() { ${!isReadOnly ? ` - + ` : ''} ${isExpanded ? '▼' : '▶'}
@@ -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 = ` +
+
+ + +
+
+ + +
+ +
+ `; + + 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 = '

Loading packs...

'; - - 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 = '

No pack presentations defined.

'; - return; - } - - list.innerHTML = ` -
- ${packs.map(pack => ` -
-
- ${escapeHtml(pack.label)} -
- ${escapeHtml(pack.pack_unit_name)} | ${pack.pack_size_in_base_units} base units - ${pack.is_active ? '' : ' | archived'} -
-
- -
- `).join('')} -
- `; - } catch (error) { - console.error('Error loading variant packs:', error); - list.innerHTML = '

Failed to load pack presentations

'; - } -} - -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'); } } diff --git a/frontend/index.html b/frontend/index.html index 33563f5..ea85bd9 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -366,6 +366,9 @@

Edit Variant

+
@@ -389,6 +392,12 @@
+ +
+ +
+ +
@@ -616,35 +625,6 @@
- -