Reporting and batch management
This commit is contained in:
@@ -245,6 +245,7 @@ class DispensingCreate(BaseModel):
|
|||||||
drug_variant_id: int
|
drug_variant_id: int
|
||||||
quantity: Optional[float] = None
|
quantity: Optional[float] = None
|
||||||
dispense_mode: str = "subunit"
|
dispense_mode: str = "subunit"
|
||||||
|
dispense_source: str = "batch"
|
||||||
requested_pack_id: Optional[int] = None
|
requested_pack_id: Optional[int] = None
|
||||||
requested_pack_count: Optional[float] = None
|
requested_pack_count: Optional[float] = None
|
||||||
animal_name: Optional[str] = None
|
animal_name: Optional[str] = None
|
||||||
@@ -496,12 +497,16 @@ def resolve_requested_allocations(
|
|||||||
requested_quantity: float,
|
requested_quantity: float,
|
||||||
requested_allocations: List[DispensingAllocationCreate],
|
requested_allocations: List[DispensingAllocationCreate],
|
||||||
dispense_mode: str,
|
dispense_mode: str,
|
||||||
|
dispense_source: str,
|
||||||
requested_pack_id: Optional[int],
|
requested_pack_id: Optional[int],
|
||||||
) -> List[Dict[str, Any]]:
|
) -> List[Dict[str, Any]]:
|
||||||
"""Validate explicit batch allocations against in-date stock for the variant."""
|
"""Validate explicit batch allocations against in-date stock for the variant."""
|
||||||
today = date.today()
|
today = date.today()
|
||||||
total_batched_quantity = sum(float(batch.quantity or 0) for batch in db.query(Batch).filter(Batch.drug_variant_id == variant_id).all())
|
total_batched_quantity = sum(float(batch.quantity or 0) for batch in db.query(Batch).filter(Batch.drug_variant_id == variant_id).all())
|
||||||
legacy_unbatched_quantity = max(0.0, float(variant_quantity or 0) - total_batched_quantity)
|
legacy_unbatched_quantity = max(0.0, float(variant_quantity or 0) - total_batched_quantity)
|
||||||
|
selected_source = (dispense_source or "batch").strip().lower()
|
||||||
|
if selected_source not in {"batch", "legacy"}:
|
||||||
|
raise HTTPException(status_code=400, detail="dispense_source must be either 'batch' or 'legacy'")
|
||||||
eligible_batches = (
|
eligible_batches = (
|
||||||
db.query(Batch)
|
db.query(Batch)
|
||||||
.filter(
|
.filter(
|
||||||
@@ -513,6 +518,20 @@ def resolve_requested_allocations(
|
|||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if selected_source == "legacy":
|
||||||
|
if dispense_mode == "pack":
|
||||||
|
raise HTTPException(status_code=400, detail="Whole-pack dispensing requires batched stock with pack information")
|
||||||
|
if requested_allocations:
|
||||||
|
raise HTTPException(status_code=400, detail="Batch allocations cannot be supplied when dispensing legacy stock")
|
||||||
|
if legacy_unbatched_quantity <= 0:
|
||||||
|
raise HTTPException(status_code=400, detail="No legacy loose stock is available for this variant")
|
||||||
|
if requested_quantity - legacy_unbatched_quantity > 1e-6:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Insufficient unbatched stock. Available: {legacy_unbatched_quantity}, Requested: {requested_quantity}",
|
||||||
|
)
|
||||||
|
return []
|
||||||
|
|
||||||
if not eligible_batches:
|
if not eligible_batches:
|
||||||
if dispense_mode == "pack":
|
if dispense_mode == "pack":
|
||||||
raise HTTPException(status_code=400, detail="Whole-pack dispensing requires batched stock with pack information")
|
raise HTTPException(status_code=400, detail="Whole-pack dispensing requires batched stock with pack information")
|
||||||
@@ -1359,9 +1378,12 @@ def dispense_drug(dispensing: DispensingCreate, db: Session = Depends(get_db), c
|
|||||||
requested_quantity=dispense_qty,
|
requested_quantity=dispense_qty,
|
||||||
requested_allocations=dispensing.allocations,
|
requested_allocations=dispensing.allocations,
|
||||||
dispense_mode=dispense_mode,
|
dispense_mode=dispense_mode,
|
||||||
|
dispense_source=dispensing.dispense_source,
|
||||||
requested_pack_id=resolved["pack_id"],
|
requested_pack_id=resolved["pack_id"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
selected_source = (dispensing.dispense_source or ("legacy" if not allocations else "batch")).strip().lower()
|
||||||
|
|
||||||
user_name = dispensing.user_name or current_user.username
|
user_name = dispensing.user_name or current_user.username
|
||||||
primary_batch_id = allocations[0]["batch"].id if allocations else None
|
primary_batch_id = allocations[0]["batch"].id if allocations else None
|
||||||
|
|
||||||
@@ -1402,6 +1424,7 @@ def dispense_drug(dispensing: DispensingCreate, db: Session = Depends(get_db), c
|
|||||||
"drug_variant_id": dispensing.drug_variant_id,
|
"drug_variant_id": dispensing.drug_variant_id,
|
||||||
"requested_quantity": dispense_qty,
|
"requested_quantity": dispense_qty,
|
||||||
"dispense_mode": dispense_mode,
|
"dispense_mode": dispense_mode,
|
||||||
|
"dispense_source": selected_source,
|
||||||
"requested_pack_id": resolved["pack_id"],
|
"requested_pack_id": resolved["pack_id"],
|
||||||
"requested_pack_count": resolved["pack_count"],
|
"requested_pack_count": resolved["pack_count"],
|
||||||
"allocations": allocation_payload,
|
"allocations": allocation_payload,
|
||||||
@@ -2054,6 +2077,98 @@ def report_stock_by_location(
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/reports/global-inventory")
|
||||||
|
def report_global_inventory(
|
||||||
|
format: str = "json",
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
variant_rows = (
|
||||||
|
db.query(DrugVariant, Drug)
|
||||||
|
.join(Drug, DrugVariant.drug_id == Drug.id)
|
||||||
|
.order_by(Drug.name.asc(), DrugVariant.strength.asc())
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
result: List[Dict[str, Any]] = []
|
||||||
|
for variant, drug in variant_rows:
|
||||||
|
batch_rows = (
|
||||||
|
db.query(Batch, Location)
|
||||||
|
.join(Location, Batch.location_id == Location.id)
|
||||||
|
.filter(Batch.drug_variant_id == variant.id, Batch.quantity > 0)
|
||||||
|
.order_by(Batch.expiry_date.asc(), Location.name.asc(), Batch.batch_number.asc())
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
total_batch_quantity = 0.0
|
||||||
|
for batch, location in batch_rows:
|
||||||
|
total_batch_quantity += float(batch.quantity or 0)
|
||||||
|
result.append(
|
||||||
|
{
|
||||||
|
"batch_id": batch.id,
|
||||||
|
"batch_number": batch.batch_number,
|
||||||
|
"drug_name": drug.name,
|
||||||
|
"strength": variant.strength,
|
||||||
|
"quantity": batch.quantity,
|
||||||
|
"unit": variant.unit,
|
||||||
|
"location_name": location.name,
|
||||||
|
"expiry_date": batch.expiry_date,
|
||||||
|
"inventory_source": "batch",
|
||||||
|
"is_controlled": bool(drug.is_controlled),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
legacy_quantity = max(0.0, float(variant.quantity or 0) - total_batch_quantity)
|
||||||
|
if legacy_quantity > 1e-6:
|
||||||
|
result.append(
|
||||||
|
{
|
||||||
|
"batch_id": None,
|
||||||
|
"batch_number": "Legacy stock",
|
||||||
|
"drug_name": drug.name,
|
||||||
|
"strength": variant.strength,
|
||||||
|
"quantity": legacy_quantity,
|
||||||
|
"unit": variant.unit,
|
||||||
|
"location_name": None,
|
||||||
|
"expiry_date": None,
|
||||||
|
"inventory_source": "legacy",
|
||||||
|
"is_controlled": bool(drug.is_controlled),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if format.lower() == "csv":
|
||||||
|
csv_rows = [
|
||||||
|
[
|
||||||
|
item["drug_name"],
|
||||||
|
item["strength"],
|
||||||
|
item["batch_number"],
|
||||||
|
item["quantity"],
|
||||||
|
item["unit"],
|
||||||
|
item["location_name"],
|
||||||
|
item["expiry_date"],
|
||||||
|
item["inventory_source"],
|
||||||
|
item["is_controlled"],
|
||||||
|
]
|
||||||
|
for item in result
|
||||||
|
]
|
||||||
|
return _csv_response(
|
||||||
|
"global_inventory.csv",
|
||||||
|
[
|
||||||
|
"drug_name",
|
||||||
|
"strength",
|
||||||
|
"batch_number",
|
||||||
|
"quantity",
|
||||||
|
"unit",
|
||||||
|
"location_name",
|
||||||
|
"expiry_date",
|
||||||
|
"inventory_source",
|
||||||
|
"is_controlled",
|
||||||
|
],
|
||||||
|
csv_rows,
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.get("/reports/batch-attention")
|
@router.get("/reports/batch-attention")
|
||||||
def report_batch_attention(
|
def report_batch_attention(
|
||||||
format: str = "json",
|
format: str = "json",
|
||||||
|
|||||||
+179
-31
@@ -260,6 +260,7 @@ function setupEventListeners() {
|
|||||||
const variantStrengthInput = document.getElementById('variantStrength');
|
const variantStrengthInput = document.getElementById('variantStrength');
|
||||||
const editVariantUnitSelect = document.getElementById('editVariantUnit');
|
const editVariantUnitSelect = document.getElementById('editVariantUnit');
|
||||||
const dispenseModeInputs = document.querySelectorAll('input[name="dispenseMode"]');
|
const dispenseModeInputs = document.querySelectorAll('input[name="dispenseMode"]');
|
||||||
|
const dispenseSourceInputs = document.querySelectorAll('input[name="dispenseSource"]');
|
||||||
const dispensePrintEnabled = document.getElementById('dispensePrintEnabled');
|
const dispensePrintEnabled = document.getElementById('dispensePrintEnabled');
|
||||||
const showAllBtn = document.getElementById('showAllBtn');
|
const showAllBtn = document.getElementById('showAllBtn');
|
||||||
const showLowStockBtn = document.getElementById('showLowStockBtn');
|
const showLowStockBtn = document.getElementById('showLowStockBtn');
|
||||||
@@ -311,6 +312,11 @@ function setupEventListeners() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
dispenseModeInputs.forEach(input => input.addEventListener('change', updateDispenseModeUi));
|
dispenseModeInputs.forEach(input => input.addEventListener('change', updateDispenseModeUi));
|
||||||
|
dispenseSourceInputs.forEach(input => input.addEventListener('change', () => {
|
||||||
|
renderDispenseInventorySourceView();
|
||||||
|
toggleDispensePrintFields();
|
||||||
|
updateDispenseAllocationSummary();
|
||||||
|
}));
|
||||||
if (dispensePrintEnabled) {
|
if (dispensePrintEnabled) {
|
||||||
dispensePrintEnabled.addEventListener('change', toggleDispensePrintFields);
|
dispensePrintEnabled.addEventListener('change', toggleDispensePrintFields);
|
||||||
}
|
}
|
||||||
@@ -503,6 +509,8 @@ function updateDispenseDrugSelect() {
|
|||||||
const packCount = document.getElementById('dispensePackCount');
|
const packCount = document.getElementById('dispensePackCount');
|
||||||
const packPreview = document.getElementById('dispensePackPreview');
|
const packPreview = document.getElementById('dispensePackPreview');
|
||||||
const quantityModeRadio = document.getElementById('dispenseModeQuantity');
|
const quantityModeRadio = document.getElementById('dispenseModeQuantity');
|
||||||
|
const batchSourceRadio = document.getElementById('dispenseSourceBatch');
|
||||||
|
const legacySourceRadio = document.getElementById('dispenseSourceLegacy');
|
||||||
if (packSelect) {
|
if (packSelect) {
|
||||||
packSelect.innerHTML = '<option value="">-- Select pack --</option>';
|
packSelect.innerHTML = '<option value="">-- Select pack --</option>';
|
||||||
}
|
}
|
||||||
@@ -515,6 +523,12 @@ function updateDispenseDrugSelect() {
|
|||||||
if (packPreview) {
|
if (packPreview) {
|
||||||
packPreview.textContent = 'Select a pack and whole-number count.';
|
packPreview.textContent = 'Select a pack and whole-number count.';
|
||||||
}
|
}
|
||||||
|
if (batchSourceRadio) {
|
||||||
|
batchSourceRadio.checked = true;
|
||||||
|
}
|
||||||
|
if (legacySourceRadio) {
|
||||||
|
legacySourceRadio.checked = false;
|
||||||
|
}
|
||||||
|
|
||||||
resetDispensePrintFields();
|
resetDispensePrintFields();
|
||||||
|
|
||||||
@@ -528,8 +542,78 @@ function getSelectedDispenseMode() {
|
|||||||
return document.querySelector('input[name="dispenseMode"]:checked')?.value || 'subunit';
|
return document.querySelector('input[name="dispenseMode"]:checked')?.value || 'subunit';
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasLegacyDispenseStock() {
|
function hasLegacyDispenseQuantity() {
|
||||||
return currentDispenseBatches.length === 0 && currentDispenseLegacyQuantity > 0;
|
return currentDispenseLegacyQuantity > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasBatchDispenseStock() {
|
||||||
|
return currentDispenseBatches.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectedDispenseSource() {
|
||||||
|
if (getSelectedDispenseMode() === 'pack') {
|
||||||
|
return 'batch';
|
||||||
|
}
|
||||||
|
|
||||||
|
const selected = document.querySelector('input[name="dispenseSource"]:checked')?.value;
|
||||||
|
if (selected) {
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasLegacyDispenseQuantity() && !hasBatchDispenseStock()) {
|
||||||
|
return 'legacy';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'batch';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLegacyDispenseSelected() {
|
||||||
|
return getSelectedDispenseMode() === 'subunit' && getSelectedDispenseSource() === 'legacy' && hasLegacyDispenseQuantity();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDispenseSourceUi() {
|
||||||
|
const sourceGroup = document.getElementById('dispenseSourceGroup');
|
||||||
|
const sourceHelp = document.getElementById('dispenseSourceHelp');
|
||||||
|
const batchRadio = document.getElementById('dispenseSourceBatch');
|
||||||
|
const legacyRadio = document.getElementById('dispenseSourceLegacy');
|
||||||
|
const hasBatches = hasBatchDispenseStock();
|
||||||
|
const hasLegacy = hasLegacyDispenseQuantity();
|
||||||
|
|
||||||
|
if (!sourceGroup || !batchRadio || !legacyRadio) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getSelectedDispenseMode() === 'pack' || (!hasBatches && !hasLegacy)) {
|
||||||
|
sourceGroup.style.display = 'none';
|
||||||
|
batchRadio.checked = true;
|
||||||
|
batchRadio.disabled = !hasBatches;
|
||||||
|
legacyRadio.checked = false;
|
||||||
|
legacyRadio.disabled = true;
|
||||||
|
if (sourceHelp) sourceHelp.textContent = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
batchRadio.disabled = !hasBatches;
|
||||||
|
legacyRadio.disabled = !hasLegacy;
|
||||||
|
|
||||||
|
if (hasLegacy && !hasBatches) {
|
||||||
|
legacyRadio.checked = true;
|
||||||
|
} else if (!hasLegacy && hasBatches) {
|
||||||
|
batchRadio.checked = true;
|
||||||
|
} else if (!batchRadio.checked && !legacyRadio.checked) {
|
||||||
|
batchRadio.checked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceGroup.style.display = hasLegacy ? '' : 'none';
|
||||||
|
if (sourceHelp) {
|
||||||
|
if (hasLegacy && hasBatches) {
|
||||||
|
sourceHelp.textContent = `Batch stock available alongside ${formatDisplayNumber(currentDispenseLegacyQuantity)} loose legacy units.`;
|
||||||
|
} else if (hasLegacy) {
|
||||||
|
sourceHelp.textContent = `Legacy loose stock available: ${formatDisplayNumber(currentDispenseLegacyQuantity)}.`;
|
||||||
|
} else {
|
||||||
|
sourceHelp.textContent = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDefaultLabelExpiryDate() {
|
function getDefaultLabelExpiryDate() {
|
||||||
@@ -546,7 +630,7 @@ function toggleDispensePrintFields() {
|
|||||||
const legacyExpiryGroup = document.getElementById('dispenseLegacyExpiryGroup');
|
const legacyExpiryGroup = document.getElementById('dispenseLegacyExpiryGroup');
|
||||||
const legacyExpiryInput = document.getElementById('dispenseLegacyExpiry');
|
const legacyExpiryInput = document.getElementById('dispenseLegacyExpiry');
|
||||||
const isEnabled = Boolean(printEnabled?.checked);
|
const isEnabled = Boolean(printEnabled?.checked);
|
||||||
const legacyStockOnly = hasLegacyDispenseStock();
|
const legacyStockOnly = isLegacyDispenseSelected();
|
||||||
|
|
||||||
if (printFields) {
|
if (printFields) {
|
||||||
printFields.style.display = isEnabled ? '' : 'none';
|
printFields.style.display = isEnabled ? '' : 'none';
|
||||||
@@ -691,10 +775,9 @@ function updateDispenseModeUi() {
|
|||||||
packCount.required = mode === 'pack';
|
packCount.required = mode === 'pack';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentDispenseBatches.length > 0) {
|
updateDispenseSourceUi();
|
||||||
renderDispenseBatchAllocationRows(currentDispenseBatches);
|
renderDispenseInventorySourceView();
|
||||||
}
|
toggleDispensePrintFields();
|
||||||
autoAllocateDispenseBatches();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateDispenseQuantityFromPack() {
|
function updateDispenseQuantityFromPack() {
|
||||||
@@ -982,14 +1065,14 @@ function getBatchAvailableDispenseQuantity(batch, mode = getSelectedDispenseMode
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getTotalAvailableDispenseQuantity(mode = getSelectedDispenseMode(), selectedPack = getSelectedDispensePack()) {
|
function getTotalAvailableDispenseQuantity(mode = getSelectedDispenseMode(), selectedPack = getSelectedDispensePack()) {
|
||||||
if (hasLegacyDispenseStock()) {
|
if (getSelectedDispenseSource() === 'legacy') {
|
||||||
return mode === 'pack' ? 0 : currentDispenseLegacyQuantity;
|
return mode === 'pack' ? 0 : currentDispenseLegacyQuantity;
|
||||||
}
|
}
|
||||||
return currentDispenseBatches.reduce((sum, batch) => sum + getBatchAvailableDispenseQuantity(batch, mode, selectedPack), 0);
|
return currentDispenseBatches.reduce((sum, batch) => sum + getBatchAvailableDispenseQuantity(batch, mode, selectedPack), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTotalAvailableDispensePackCount(selectedPack = getSelectedDispensePack()) {
|
function getTotalAvailableDispensePackCount(selectedPack = getSelectedDispensePack()) {
|
||||||
if (hasLegacyDispenseStock()) {
|
if (getSelectedDispenseSource() === 'legacy') {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (!selectedPack) {
|
if (!selectedPack) {
|
||||||
@@ -1134,6 +1217,52 @@ function renderExpiredDispenseBatches(expiredBatches) {
|
|||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderDispenseInventorySourceView() {
|
||||||
|
const batchInfoContent = document.getElementById('batchInfoContent');
|
||||||
|
const variantId = parseInt(document.getElementById('dispenseDrugSelect')?.value || '', 10);
|
||||||
|
const variant = getVariantById(variantId);
|
||||||
|
|
||||||
|
if (!batchInfoContent || !variant) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getSelectedDispenseMode() === 'pack') {
|
||||||
|
if (hasBatchDispenseStock()) {
|
||||||
|
renderDispenseBatchAllocationRows(currentDispenseBatches);
|
||||||
|
autoAllocateDispenseBatches();
|
||||||
|
} else if (hasLegacyDispenseQuantity()) {
|
||||||
|
batchInfoContent.innerHTML = `<div style="padding: 10px; background: #fff8e8; border: 1px solid #f3d18d; border-radius: 4px; color: #7a4f01;"><strong>Legacy stock only.</strong> ${formatDisplayNumber(currentDispenseLegacyQuantity)} ${escapeHtml(variant.unit || 'units')} available outside the batch system. Whole-pack dispensing is unavailable.</div>`;
|
||||||
|
updateDispenseAllocationSummary();
|
||||||
|
} else {
|
||||||
|
batchInfoContent.innerHTML = '<p style="color: #d32f2f; margin: 0;">⚠️ No active batches available for this variant</p>';
|
||||||
|
updateDispenseAllocationSummary();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLegacyDispenseSelected()) {
|
||||||
|
const extraText = hasBatchDispenseStock() ? ' Batch stock is also available; switch source to allocate from batches.' : ' Dispense by quantity only.';
|
||||||
|
batchInfoContent.innerHTML = `<div style="padding: 10px; background: #fff8e8; border: 1px solid #f3d18d; border-radius: 4px; color: #7a4f01;"><strong>Legacy loose stock selected.</strong> ${formatDisplayNumber(currentDispenseLegacyQuantity)} ${escapeHtml(variant.unit || 'units')} available outside the batch system.${extraText}</div>`;
|
||||||
|
updateDispenseAllocationSummary();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasBatchDispenseStock()) {
|
||||||
|
renderDispenseBatchAllocationRows(currentDispenseBatches);
|
||||||
|
autoAllocateDispenseBatches();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasLegacyDispenseQuantity()) {
|
||||||
|
batchInfoContent.innerHTML = `<div style="padding: 10px; background: #fff8e8; border: 1px solid #f3d18d; border-radius: 4px; color: #7a4f01;"><strong>Legacy stock only.</strong> ${formatDisplayNumber(currentDispenseLegacyQuantity)} ${escapeHtml(variant.unit || 'units')} available outside the batch system. Dispense by quantity only.</div>`;
|
||||||
|
updateDispenseAllocationSummary();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
batchInfoContent.innerHTML = '<p style="color: #d32f2f; margin: 0;">⚠️ No active batches available for this variant</p>';
|
||||||
|
updateDispenseAllocationSummary();
|
||||||
|
}
|
||||||
|
|
||||||
// Update batch info display when variant is selected
|
// Update batch info display when variant is selected
|
||||||
async function updateBatchInfo() {
|
async function updateBatchInfo() {
|
||||||
const variantId = parseInt(document.getElementById('dispenseDrugSelect').value);
|
const variantId = parseInt(document.getElementById('dispenseDrugSelect').value);
|
||||||
@@ -1147,6 +1276,7 @@ async function updateBatchInfo() {
|
|||||||
currentDispenseBatches = [];
|
currentDispenseBatches = [];
|
||||||
currentDispenseLegacyQuantity = 0;
|
currentDispenseLegacyQuantity = 0;
|
||||||
renderExpiredDispenseBatches([]);
|
renderExpiredDispenseBatches([]);
|
||||||
|
updateDispenseSourceUi();
|
||||||
toggleDispensePrintFields();
|
toggleDispensePrintFields();
|
||||||
updateDispenseAllocationSummary();
|
updateDispenseAllocationSummary();
|
||||||
return;
|
return;
|
||||||
@@ -1176,14 +1306,9 @@ async function updateBatchInfo() {
|
|||||||
currentDispenseBatches = activeBatches;
|
currentDispenseBatches = activeBatches;
|
||||||
renderExpiredDispenseBatches(expiredBatches);
|
renderExpiredDispenseBatches(expiredBatches);
|
||||||
|
|
||||||
if (activeBatches.length === 0) {
|
if (!activeBatches.length && currentDispenseLegacyQuantity <= 0 && expiredBatches.length > 0) {
|
||||||
if (currentDispenseLegacyQuantity > 0) {
|
updateDispenseSourceUi();
|
||||||
batchInfoContent.innerHTML = `<div style="padding: 10px; background: #fff8e8; border: 1px solid #f3d18d; border-radius: 4px; color: #7a4f01;"><strong>Legacy stock only.</strong> ${formatDisplayNumber(currentDispenseLegacyQuantity)} ${escapeHtml(variant?.unit || 'units')} available outside the batch system. Dispense by quantity only; whole-pack allocation is unavailable.</div>`;
|
batchInfoContent.innerHTML = '<p style="color: #d32f2f; margin: 0;">⚠️ No in-date batches available for this variant. Expired batches are hidden from selection.</p>';
|
||||||
} else {
|
|
||||||
batchInfoContent.innerHTML = expiredBatches.length > 0
|
|
||||||
? '<p style="color: #d32f2f; margin: 0;">⚠️ No in-date batches available for this variant. Expired batches are hidden from selection.</p>'
|
|
||||||
: '<p style="color: #d32f2f; margin: 0;">⚠️ No active batches available for this variant</p>';
|
|
||||||
}
|
|
||||||
toggleDispensePrintFields();
|
toggleDispensePrintFields();
|
||||||
updateDispenseAllocationSummary();
|
updateDispenseAllocationSummary();
|
||||||
return;
|
return;
|
||||||
@@ -1192,15 +1317,16 @@ async function updateBatchInfo() {
|
|||||||
// Sort by expiry date (FEFO order)
|
// Sort by expiry date (FEFO order)
|
||||||
activeBatches.sort((a, b) => new Date(a.expiry_date) - new Date(b.expiry_date));
|
activeBatches.sort((a, b) => new Date(a.expiry_date) - new Date(b.expiry_date));
|
||||||
currentDispenseBatches = activeBatches;
|
currentDispenseBatches = activeBatches;
|
||||||
renderDispenseBatchAllocationRows(activeBatches);
|
updateDispenseSourceUi();
|
||||||
|
renderDispenseInventorySourceView();
|
||||||
toggleDispensePrintFields();
|
toggleDispensePrintFields();
|
||||||
autoAllocateDispenseBatches();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading batches:', error);
|
console.error('Error loading batches:', error);
|
||||||
batchInfoContent.innerHTML = '<p style="color: #d32f2f; margin: 0;">Error loading batches</p>';
|
batchInfoContent.innerHTML = '<p style="color: #d32f2f; margin: 0;">Error loading batches</p>';
|
||||||
currentDispenseBatches = [];
|
currentDispenseBatches = [];
|
||||||
currentDispenseLegacyQuantity = 0;
|
currentDispenseLegacyQuantity = 0;
|
||||||
renderExpiredDispenseBatches([]);
|
renderExpiredDispenseBatches([]);
|
||||||
|
updateDispenseSourceUi();
|
||||||
toggleDispensePrintFields();
|
toggleDispensePrintFields();
|
||||||
updateDispenseAllocationSummary();
|
updateDispenseAllocationSummary();
|
||||||
}
|
}
|
||||||
@@ -1217,6 +1343,14 @@ function autoAllocateDispenseBatches() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isLegacyDispenseSelected()) {
|
||||||
|
allocationInputs.forEach(input => {
|
||||||
|
input.value = '0';
|
||||||
|
});
|
||||||
|
updateDispenseAllocationSummary();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let remaining = mode === 'pack'
|
let remaining = mode === 'pack'
|
||||||
? Math.max(0, Math.round(parseFloat(document.getElementById('dispensePackCount')?.value || '0')) || 0)
|
? Math.max(0, Math.round(parseFloat(document.getElementById('dispensePackCount')?.value || '0')) || 0)
|
||||||
: requestedQuantity;
|
: requestedQuantity;
|
||||||
@@ -1254,7 +1388,7 @@ function updateDispenseAllocationSummary() {
|
|||||||
const inputs = Array.from(document.querySelectorAll('.dispense-batch-allocation'));
|
const inputs = Array.from(document.querySelectorAll('.dispense-batch-allocation'));
|
||||||
const mode = getSelectedDispenseMode();
|
const mode = getSelectedDispenseMode();
|
||||||
const selectedPack = getSelectedDispensePack();
|
const selectedPack = getSelectedDispensePack();
|
||||||
const legacyStockOnly = hasLegacyDispenseStock();
|
const legacyStockOnly = isLegacyDispenseSelected();
|
||||||
const totalAvailableQuantity = getTotalAvailableDispenseQuantity(mode, selectedPack);
|
const totalAvailableQuantity = getTotalAvailableDispenseQuantity(mode, selectedPack);
|
||||||
const totalAvailablePacks = mode === 'pack' ? getTotalAvailableDispensePackCount(selectedPack) : 0;
|
const totalAvailablePacks = mode === 'pack' ? getTotalAvailableDispensePackCount(selectedPack) : 0;
|
||||||
|
|
||||||
@@ -1536,6 +1670,7 @@ async function handleDispenseDrug(e) {
|
|||||||
const variantId = parseInt(document.getElementById('dispenseDrugSelect').value);
|
const variantId = parseInt(document.getElementById('dispenseDrugSelect').value);
|
||||||
let quantity = parseFloat(document.getElementById('dispenseQuantity').value);
|
let quantity = parseFloat(document.getElementById('dispenseQuantity').value);
|
||||||
const dispenseMode = getSelectedDispenseMode();
|
const dispenseMode = getSelectedDispenseMode();
|
||||||
|
const dispenseSource = getSelectedDispenseSource();
|
||||||
const requestedPackIdValue = document.getElementById('dispensePackSelect').value;
|
const requestedPackIdValue = document.getElementById('dispensePackSelect').value;
|
||||||
const requestedPackCountValue = document.getElementById('dispensePackCount').value;
|
const requestedPackCountValue = document.getElementById('dispensePackCount').value;
|
||||||
const animalName = document.getElementById('dispenseAnimal').value;
|
const animalName = document.getElementById('dispenseAnimal').value;
|
||||||
@@ -1547,7 +1682,7 @@ async function handleDispenseDrug(e) {
|
|||||||
const selectedPackId = requestedPackIdValue ? parseInt(requestedPackIdValue, 10) : null;
|
const selectedPackId = requestedPackIdValue ? parseInt(requestedPackIdValue, 10) : null;
|
||||||
const selectedPackCount = requestedPackCountValue ? parseFloat(requestedPackCountValue) : null;
|
const selectedPackCount = requestedPackCountValue ? parseFloat(requestedPackCountValue) : null;
|
||||||
const variant = getVariantById(variantId);
|
const variant = getVariantById(variantId);
|
||||||
const legacyStockOnly = hasLegacyDispenseStock();
|
const legacyStockOnly = isLegacyDispenseSelected();
|
||||||
const selectedPack = variant && selectedPackId
|
const selectedPack = variant && selectedPackId
|
||||||
? getActivePacksForVariant(variant).find(pack => pack.id === selectedPackId)
|
? getActivePacksForVariant(variant).find(pack => pack.id === selectedPackId)
|
||||||
: null;
|
: null;
|
||||||
@@ -1660,6 +1795,7 @@ async function handleDispenseDrug(e) {
|
|||||||
dispense_mode: dispenseMode,
|
dispense_mode: dispenseMode,
|
||||||
requested_pack_id: dispenseMode === 'pack' ? selectedPackId : null,
|
requested_pack_id: dispenseMode === 'pack' ? selectedPackId : null,
|
||||||
requested_pack_count: dispenseMode === 'pack' ? selectedPackCount : null,
|
requested_pack_count: dispenseMode === 'pack' ? selectedPackCount : null,
|
||||||
|
dispense_source: dispenseSource,
|
||||||
animal_name: animalName || null,
|
animal_name: animalName || null,
|
||||||
notes: notes || null,
|
notes: notes || null,
|
||||||
allocations
|
allocations
|
||||||
@@ -1726,12 +1862,12 @@ function openEditModal(drugId) {
|
|||||||
document.getElementById('editDrugDescription').value = drug.description || '';
|
document.getElementById('editDrugDescription').value = drug.description || '';
|
||||||
document.getElementById('editDrugIsControlled').checked = drug.is_controlled || false;
|
document.getElementById('editDrugIsControlled').checked = drug.is_controlled || false;
|
||||||
|
|
||||||
document.getElementById('editModal').classList.add('show');
|
openModal(document.getElementById('editModal'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close edit modal
|
// Close edit modal
|
||||||
function closeEditModal() {
|
function closeEditModal() {
|
||||||
document.getElementById('editModal').classList.remove('show');
|
closeModal(document.getElementById('editModal'));
|
||||||
document.getElementById('editForm').reset();
|
document.getElementById('editForm').reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1772,7 +1908,7 @@ function openAddVariantModal(drugId) {
|
|||||||
if (form) form.reset();
|
if (form) form.reset();
|
||||||
document.getElementById('variantDrugId').value = drug.id;
|
document.getElementById('variantDrugId').value = drug.id;
|
||||||
initializeVariantPackRows();
|
initializeVariantPackRows();
|
||||||
document.getElementById('addVariantModal').classList.add('show');
|
openModal(document.getElementById('addVariantModal'));
|
||||||
}
|
}
|
||||||
|
|
||||||
function inferBaseUnitFromStrength(strength) {
|
function inferBaseUnitFromStrength(strength) {
|
||||||
@@ -2063,7 +2199,7 @@ function openEditVariantModal(variantId) {
|
|||||||
setEditVariantFieldLockState(hasInventoryContext);
|
setEditVariantFieldLockState(hasInventoryContext);
|
||||||
initializeEditVariantPackRows();
|
initializeEditVariantPackRows();
|
||||||
|
|
||||||
document.getElementById('editVariantModal').classList.add('show');
|
openModal(document.getElementById('editVariantModal'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle edit variant form
|
// Handle edit variant form
|
||||||
@@ -2987,7 +3123,9 @@ function wireDeliveryLineEvents(line) {
|
|||||||
variantSelect.addEventListener('change', () => {
|
variantSelect.addEventListener('change', () => {
|
||||||
const variantId = parseInt(variantSelect.value || '', 10);
|
const variantId = parseInt(variantSelect.value || '', 10);
|
||||||
const variant = getVariantById(variantId);
|
const variant = getVariantById(variantId);
|
||||||
packSelect.innerHTML = buildDeliveryPackOptions(variant, '');
|
const activePacks = getActivePacksForVariant(variant);
|
||||||
|
const nextPackId = activePacks.length === 1 ? activePacks[0].id : '';
|
||||||
|
packSelect.innerHTML = buildDeliveryPackOptions(variant, nextPackId);
|
||||||
if (packCountInput) packCountInput.value = '';
|
if (packCountInput) packCountInput.value = '';
|
||||||
updateDeliveryLineQuantityDisplay(line);
|
updateDeliveryLineQuantityDisplay(line);
|
||||||
});
|
});
|
||||||
@@ -3018,9 +3156,11 @@ function appendDeliveryLine(prefill = {}) {
|
|||||||
line.className = 'delivery-line';
|
line.className = 'delivery-line';
|
||||||
line.dataset.lineId = lineId;
|
line.dataset.lineId = lineId;
|
||||||
|
|
||||||
const initialVariant = drug.variants.find(v => String(v.id) === String(prefill.variantId)) || drug.variants[0] || null;
|
const initialVariant = prefill.variantId
|
||||||
|
? drug.variants.find(v => String(v.id) === String(prefill.variantId)) || null
|
||||||
|
: drug.variants.length === 1 ? drug.variants[0] : null;
|
||||||
const initialVariantId = prefill.variantId || (initialVariant ? initialVariant.id : '');
|
const initialVariantId = prefill.variantId || (initialVariant ? initialVariant.id : '');
|
||||||
const initialPackId = prefill.packId || '';
|
const initialPackId = prefill.packId || (getActivePacksForVariant(initialVariant).length === 1 ? getActivePacksForVariant(initialVariant)[0].id : '');
|
||||||
const initialPackCount = prefill.packCount || '';
|
const initialPackCount = prefill.packCount || '';
|
||||||
|
|
||||||
line.innerHTML = `
|
line.innerHTML = `
|
||||||
@@ -3039,7 +3179,7 @@ function appendDeliveryLine(prefill = {}) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Pack Count</label>
|
<label>Pack Count</label>
|
||||||
<input type="number" class="delivery-pack-count" min="0.0001" step="0.0001" value="${initialPackCount}" required>
|
<input type="number" class="delivery-pack-count" min="1" step="1" value="${initialPackCount}" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Batch Number</label>
|
<label>Batch Number</label>
|
||||||
@@ -3087,12 +3227,15 @@ function refreshDeliveryVariantSelects() {
|
|||||||
if (!select) return;
|
if (!select) return;
|
||||||
|
|
||||||
const currentVariantId = select.value;
|
const currentVariantId = select.value;
|
||||||
select.innerHTML = buildDeliveryVariantOptions(drug, currentVariantId);
|
const nextVariantId = currentVariantId || (drug.variants.length === 1 ? String(drug.variants[0].id) : '');
|
||||||
|
select.innerHTML = buildDeliveryVariantOptions(drug, nextVariantId);
|
||||||
|
|
||||||
const variant = getVariantById(parseInt(select.value || '', 10));
|
const variant = getVariantById(parseInt(select.value || '', 10));
|
||||||
if (packSelect) {
|
if (packSelect) {
|
||||||
const currentPackId = packSelect.value;
|
const currentPackId = packSelect.value;
|
||||||
packSelect.innerHTML = buildDeliveryPackOptions(variant, currentPackId);
|
const activePacks = getActivePacksForVariant(variant);
|
||||||
|
const nextPackId = currentPackId || (activePacks.length === 1 ? String(activePacks[0].id) : '');
|
||||||
|
packSelect.innerHTML = buildDeliveryPackOptions(variant, nextPackId);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDeliveryLineQuantityDisplay(line);
|
updateDeliveryLineQuantityDisplay(line);
|
||||||
@@ -3176,6 +3319,11 @@ async function handleReceiveDelivery(e) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Math.abs(packCount - Math.round(packCount)) > 1e-6) {
|
||||||
|
showToast(`Delivery line ${i + 1} pack count must be a whole number`, 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const variant = drug.variants.find(v => v.id === variantId);
|
const variant = drug.variants.find(v => v.id === variantId);
|
||||||
const selectedPack = variant ? getActivePacksForVariant(variant).find(pack => pack.id === packId) : null;
|
const selectedPack = variant ? getActivePacksForVariant(variant).find(pack => pack.id === packId) : null;
|
||||||
if (!selectedPack) {
|
if (!selectedPack) {
|
||||||
|
|||||||
@@ -222,6 +222,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" id="dispenseSourceGroup" style="display: none;">
|
||||||
|
<label>Stock Source *</label>
|
||||||
|
<div style="display: flex; gap: 18px; align-items: center; flex-wrap: wrap; margin-top: 6px;">
|
||||||
|
<label style="display: inline-flex; align-items: center; gap: 8px; margin: 0; font-weight: 500;">
|
||||||
|
<input type="radio" name="dispenseSource" id="dispenseSourceBatch" value="batch" checked>
|
||||||
|
Batch stock
|
||||||
|
</label>
|
||||||
|
<label style="display: inline-flex; align-items: center; gap: 8px; margin: 0; font-weight: 500;">
|
||||||
|
<input type="radio" name="dispenseSource" id="dispenseSourceLegacy" value="legacy">
|
||||||
|
Legacy loose stock
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<small id="dispenseSourceHelp" style="display: block; margin-top: 6px; color: #666;"></small>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="batchInfoSection" style="display: none; margin: 15px 0; padding: 12px; background: #f5f5f5; border-radius: 4px;">
|
<div id="batchInfoSection" style="display: none; margin: 15px 0; padding: 12px; background: #f5f5f5; border-radius: 4px;">
|
||||||
<h4 style="margin-top: 0; margin-bottom: 4px;">Batch Allocation</h4>
|
<h4 style="margin-top: 0; margin-bottom: 4px;">Batch Allocation</h4>
|
||||||
<p style="margin: 0 0 10px; color: #666;">Batches are shown in FEFO order. Adjust the allocation against each batch so the total matches the requested dispense amount.</p>
|
<p style="margin: 0 0 10px; color: #666;">Batches are shown in FEFO order. Adjust the allocation against each batch so the total matches the requested dispense amount.</p>
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
<label for="reportTypeSelect">Report</label>
|
<label for="reportTypeSelect">Report</label>
|
||||||
<select id="reportTypeSelect">
|
<select id="reportTypeSelect">
|
||||||
<option value="dispensing" selected>Dispensing History</option>
|
<option value="dispensing" selected>Dispensing History</option>
|
||||||
|
<option value="global_inventory">Global Inventory</option>
|
||||||
<option value="batch_attention">Expired Batches</option>
|
<option value="batch_attention">Expired Batches</option>
|
||||||
<option value="audit">Audit Trail (Raw)</option>
|
<option value="audit">Audit Trail (Raw)</option>
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
+79
-2
@@ -5,6 +5,7 @@ let currentUser = null;
|
|||||||
let allDrugs = [];
|
let allDrugs = [];
|
||||||
let auditTrailRows = [];
|
let auditTrailRows = [];
|
||||||
let dispensingRows = [];
|
let dispensingRows = [];
|
||||||
|
let globalInventoryRows = [];
|
||||||
let batchAttentionRows = [];
|
let batchAttentionRows = [];
|
||||||
let activeReportType = 'dispensing';
|
let activeReportType = 'dispensing';
|
||||||
const batchLookupById = new Map();
|
const batchLookupById = new Map();
|
||||||
@@ -200,24 +201,28 @@ function detailsContainsText(details, searchText) {
|
|||||||
|
|
||||||
function getActiveRows() {
|
function getActiveRows() {
|
||||||
if (activeReportType === 'dispensing') return dispensingRows;
|
if (activeReportType === 'dispensing') return dispensingRows;
|
||||||
|
if (activeReportType === 'global_inventory') return globalInventoryRows;
|
||||||
if (activeReportType === 'batch_attention') return batchAttentionRows;
|
if (activeReportType === 'batch_attention') return batchAttentionRows;
|
||||||
return auditTrailRows;
|
return auditTrailRows;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRowUser(row) {
|
function getRowUser(row) {
|
||||||
if (activeReportType === 'dispensing') return row.user_name || 'unknown';
|
if (activeReportType === 'dispensing') return row.user_name || 'unknown';
|
||||||
|
if (activeReportType === 'global_inventory') return '';
|
||||||
if (activeReportType === 'batch_attention') return '';
|
if (activeReportType === 'batch_attention') return '';
|
||||||
return row.actor_username || 'system';
|
return row.actor_username || 'system';
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRowDrug(row) {
|
function getRowDrug(row) {
|
||||||
if (activeReportType === 'dispensing') return extractDrugLabelFromDispenseRow(row);
|
if (activeReportType === 'dispensing') return extractDrugLabelFromDispenseRow(row);
|
||||||
|
if (activeReportType === 'global_inventory') return `${row.drug_name || 'Unknown Drug'}${row.strength ? ` ${row.strength}` : ''}`;
|
||||||
if (activeReportType === 'batch_attention') return `${row.drug_name || 'Unknown Drug'}${row.strength ? ` ${row.strength}` : ''}`;
|
if (activeReportType === 'batch_attention') return `${row.drug_name || 'Unknown Drug'}${row.strength ? ` ${row.strength}` : ''}`;
|
||||||
return extractDrugLabelFromAuditRow(row);
|
return extractDrugLabelFromAuditRow(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRowDate(row) {
|
function getRowDate(row) {
|
||||||
if (activeReportType === 'dispensing') return new Date(row.dispensed_at);
|
if (activeReportType === 'dispensing') return new Date(row.dispensed_at);
|
||||||
|
if (activeReportType === 'global_inventory') return row.expiry_date ? new Date(row.expiry_date) : null;
|
||||||
if (activeReportType === 'batch_attention') return new Date(row.expiry_date);
|
if (activeReportType === 'batch_attention') return new Date(row.expiry_date);
|
||||||
return new Date(row.created_at);
|
return new Date(row.created_at);
|
||||||
}
|
}
|
||||||
@@ -348,6 +353,50 @@ function renderDispensingTable(rows) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderGlobalInventoryTable(rows) {
|
||||||
|
const container = document.getElementById('reportsTableContainer');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
if (!rows.length) {
|
||||||
|
container.innerHTML = '<p class="empty" style="padding: 14px;">No inventory lines match the selected filters.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowsHtml = rows.map(row => {
|
||||||
|
const expiryText = row.expiry_date ? new Date(row.expiry_date).toLocaleDateString() : '-';
|
||||||
|
const quantityText = `${row.quantity} ${row.unit || 'units'}`;
|
||||||
|
const batchText = row.inventory_source === 'legacy' ? 'Legacy stock' : (row.batch_number || '');
|
||||||
|
const locationText = row.location_name || '-';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td>${escapeHtml(row.drug_name || '')}</td>
|
||||||
|
<td>${escapeHtml(row.strength || '-')}</td>
|
||||||
|
<td>${escapeHtml(batchText)}</td>
|
||||||
|
<td>${escapeHtml(quantityText)}</td>
|
||||||
|
<td>${escapeHtml(locationText)}</td>
|
||||||
|
<td>${escapeHtml(expiryText)}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
container.innerHTML = `
|
||||||
|
<table class="reports-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Drug</th>
|
||||||
|
<th>Variant</th>
|
||||||
|
<th>Batch</th>
|
||||||
|
<th>Quantity</th>
|
||||||
|
<th>Location</th>
|
||||||
|
<th>Expiry</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>${rowsHtml}</tbody>
|
||||||
|
</table>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
function renderBatchAttentionTable(rows) {
|
function renderBatchAttentionTable(rows) {
|
||||||
const container = document.getElementById('reportsTableContainer');
|
const container = document.getElementById('reportsTableContainer');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
@@ -469,8 +518,8 @@ function applyCurrentFilters() {
|
|||||||
const userMatch = !selectedUser || getRowUser(row) === selectedUser;
|
const userMatch = !selectedUser || getRowUser(row) === selectedUser;
|
||||||
const drugMatch = !selectedDrug || getRowDrug(row) === selectedDrug;
|
const drugMatch = !selectedDrug || getRowDrug(row) === selectedDrug;
|
||||||
const rowDate = getRowDate(row);
|
const rowDate = getRowDate(row);
|
||||||
const fromMatch = !fromDate || rowDate >= fromDate;
|
const fromMatch = !fromDate || !rowDate || rowDate >= fromDate;
|
||||||
const toMatch = !toDate || rowDate <= toDate;
|
const toMatch = !toDate || !rowDate || rowDate <= toDate;
|
||||||
|
|
||||||
let textMatch = true;
|
let textMatch = true;
|
||||||
if (searchText) {
|
if (searchText) {
|
||||||
@@ -485,6 +534,16 @@ function applyCurrentFilters() {
|
|||||||
formatDispenseAllocation(row)
|
formatDispenseAllocation(row)
|
||||||
].join(' ').toLowerCase();
|
].join(' ').toLowerCase();
|
||||||
textMatch = haystack.includes(searchText);
|
textMatch = haystack.includes(searchText);
|
||||||
|
} else if (activeReportType === 'global_inventory') {
|
||||||
|
const haystack = [
|
||||||
|
row.drug_name || '',
|
||||||
|
row.strength || '',
|
||||||
|
row.batch_number || '',
|
||||||
|
row.inventory_source || '',
|
||||||
|
row.location_name || '',
|
||||||
|
row.unit || ''
|
||||||
|
].join(' ').toLowerCase();
|
||||||
|
textMatch = haystack.includes(searchText);
|
||||||
} else if (activeReportType === 'batch_attention') {
|
} else if (activeReportType === 'batch_attention') {
|
||||||
const haystack = [
|
const haystack = [
|
||||||
row.drug_name || '',
|
row.drug_name || '',
|
||||||
@@ -512,6 +571,8 @@ function applyCurrentFilters() {
|
|||||||
if (reportsSummary) {
|
if (reportsSummary) {
|
||||||
const reportName = activeReportType === 'dispensing'
|
const reportName = activeReportType === 'dispensing'
|
||||||
? 'dispensing records'
|
? 'dispensing records'
|
||||||
|
: activeReportType === 'global_inventory'
|
||||||
|
? 'inventory lines'
|
||||||
: activeReportType === 'batch_attention'
|
: activeReportType === 'batch_attention'
|
||||||
? 'expired batches'
|
? 'expired batches'
|
||||||
: 'audit events';
|
: 'audit events';
|
||||||
@@ -520,6 +581,8 @@ function applyCurrentFilters() {
|
|||||||
|
|
||||||
if (activeReportType === 'dispensing') {
|
if (activeReportType === 'dispensing') {
|
||||||
renderDispensingTable(filteredRows);
|
renderDispensingTable(filteredRows);
|
||||||
|
} else if (activeReportType === 'global_inventory') {
|
||||||
|
renderGlobalInventoryTable(filteredRows);
|
||||||
} else if (activeReportType === 'batch_attention') {
|
} else if (activeReportType === 'batch_attention') {
|
||||||
renderBatchAttentionTable(filteredRows);
|
renderBatchAttentionTable(filteredRows);
|
||||||
} else {
|
} else {
|
||||||
@@ -537,6 +600,10 @@ function updateReportHeading() {
|
|||||||
heading.textContent = 'Dispensing History';
|
heading.textContent = 'Dispensing History';
|
||||||
searchInput.placeholder = 'Search user, drug, animal, notes, batch allocation...';
|
searchInput.placeholder = 'Search user, drug, animal, notes, batch allocation...';
|
||||||
if (userFilter) userFilter.style.display = '';
|
if (userFilter) userFilter.style.display = '';
|
||||||
|
} else if (activeReportType === 'global_inventory') {
|
||||||
|
heading.textContent = 'Global Inventory';
|
||||||
|
searchInput.placeholder = 'Search drug, variant, batch, location...';
|
||||||
|
if (userFilter) userFilter.style.display = 'none';
|
||||||
} else if (activeReportType === 'batch_attention') {
|
} else if (activeReportType === 'batch_attention') {
|
||||||
heading.textContent = 'Expired Batches';
|
heading.textContent = 'Expired Batches';
|
||||||
searchInput.placeholder = 'Search drug, batch, location...';
|
searchInput.placeholder = 'Search drug, batch, location...';
|
||||||
@@ -587,6 +654,8 @@ async function loadActiveReport() {
|
|||||||
if (container) {
|
if (container) {
|
||||||
const loadingText = activeReportType === 'dispensing'
|
const loadingText = activeReportType === 'dispensing'
|
||||||
? 'Loading dispensing history...'
|
? 'Loading dispensing history...'
|
||||||
|
: activeReportType === 'global_inventory'
|
||||||
|
? 'Loading global inventory...'
|
||||||
: activeReportType === 'batch_attention'
|
: activeReportType === 'batch_attention'
|
||||||
? 'Loading expired batches...'
|
? 'Loading expired batches...'
|
||||||
: 'Loading audit trail...';
|
: 'Loading audit trail...';
|
||||||
@@ -604,6 +673,14 @@ async function loadActiveReport() {
|
|||||||
dispensingRows = await response.json();
|
dispensingRows = await response.json();
|
||||||
await ensureBatchLookupForDispensing(dispensingRows);
|
await ensureBatchLookupForDispensing(dispensingRows);
|
||||||
populateCommonFilters(dispensingRows);
|
populateCommonFilters(dispensingRows);
|
||||||
|
} else if (activeReportType === 'global_inventory') {
|
||||||
|
const response = await apiCall('/reports/global-inventory');
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json();
|
||||||
|
throw new Error(error.detail || 'Failed to load global inventory');
|
||||||
|
}
|
||||||
|
globalInventoryRows = await response.json();
|
||||||
|
populateCommonFilters(globalInventoryRows);
|
||||||
} else if (activeReportType === 'batch_attention') {
|
} else if (activeReportType === 'batch_attention') {
|
||||||
const response = await apiCall('/reports/batch-attention');
|
const response = await apiCall('/reports/batch-attention');
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|||||||
Reference in New Issue
Block a user