const API_URL = '/api'; let accessToken = null; let currentUser = null; let allDrugs = []; let auditTrailRows = []; let dispensingRows = []; let globalInventoryRows = []; let batchAttentionRows = []; let activeReportType = 'dispensing'; const batchLookupById = new Map(); const loadedBatchVariants = new Set(); function openModal(modal) { if (!modal) return; modal.classList.add('show'); document.body.style.overflow = 'hidden'; } function closeModal(modal) { if (!modal) return; modal.classList.remove('show'); document.body.style.overflow = 'auto'; } function resetDisposeBatchModal() { const form = document.getElementById('disposeBatchForm'); if (form) { form.reset(); } const batchIdInput = document.getElementById('disposeBatchId'); const batchNameInput = document.getElementById('disposeBatchName'); if (batchIdInput) batchIdInput.value = ''; if (batchNameInput) batchNameInput.value = ''; } function closeDisposeBatchModal() { resetDisposeBatchModal(); closeModal(document.getElementById('disposeBatchModal')); } function showToast(message, type = 'info', duration = 3000) { const container = document.getElementById('toastContainer'); if (!container) return; const toast = document.createElement('div'); toast.className = `toast ${type}`; const icons = { success: '✓', error: '✕', warning: '⚠', info: 'ℹ' }; toast.innerHTML = ` `; container.appendChild(toast); setTimeout(() => { toast.classList.add('fade-out'); setTimeout(() => { if (container.contains(toast)) container.removeChild(toast); }, 300); }, duration); } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function getVariantInfoById(variantId) { for (const drug of allDrugs) { const variant = drug.variants.find(v => v.id === variantId); if (variant) { return { drugName: drug.name, strength: variant.strength, unit: variant.unit }; } } return { drugName: 'Unknown Drug', strength: '', unit: 'units' }; } function extractDrugLabelFromAuditRow(row) { const details = row.details || {}; if (details.drug_name) return details.drug_name; if (details.before?.name) return details.before.name; if (details.after?.name) return details.after.name; if (details.name && row.entity_type === 'drug') return details.name; if (details.drug_id) { const info = getVariantInfoById(details.drug_id); if (info.drugName !== 'Unknown Drug') return info.drugName; } if (details.variant_id) { const info = getVariantInfoById(details.variant_id); if (info.drugName) return `${info.drugName}${info.strength ? ` ${info.strength}` : ''}`; } if (row.entity_type === 'variant' && row.entity_id) { const info = getVariantInfoById(row.entity_id); if (info.drugName) return `${info.drugName}${info.strength ? ` ${info.strength}` : ''}`; } return 'N/A'; } function extractDrugLabelFromDispenseRow(row) { const info = getVariantInfoById(row.drug_variant_id); return `${info.drugName}${info.strength ? ` ${info.strength}` : ''}`; } function formatAuditSummary(row) { const details = row.details || {}; if (row.action === 'dispense.create') { const qty = details.quantity || details.dispensed_quantity || ''; const animal = details.animal_name ? ` for ${details.animal_name}` : ''; return `Dispensed ${qty}${animal}`.trim(); } if (row.action === 'batch.create' || row.action === 'batch.update') { const batch = details.batch_number || details.after?.batch_number || details.before?.batch_number || ''; const quantity = details.quantity || details.after?.quantity || ''; return `Batch ${batch}${quantity !== '' ? ` (qty ${quantity})` : ''}`.trim(); } if (row.action === 'drug.create' || row.action === 'drug.update') { const name = details.name || details.after?.name || details.before?.name || extractDrugLabelFromAuditRow(row); return `Drug ${name}`; } if (row.action === 'variant.create' || row.action === 'variant.update') { const variant = details.strength || details.after?.strength || details.before?.strength || ''; const drug = extractDrugLabelFromAuditRow(row); return `Variant ${variant}${drug !== 'N/A' ? ` (${drug})` : ''}`.trim(); } if (details.message) return String(details.message); return row.action || 'Event'; } function formatDispenseAllocation(row) { if (row.allocations && row.allocations.length > 0) { return row.allocations .map(a => { const batch = batchLookupById.get(a.batch_id); if (batch) { const expiry = batch.expiry_date ? new Date(batch.expiry_date).toLocaleDateString() : 'Unknown'; return `${batch.batch_number} (exp ${expiry}): ${a.quantity}`; } return `Batch ${a.batch_id}: ${a.quantity}`; }) .join(', '); } if (row.batch_id) { const batch = batchLookupById.get(row.batch_id); if (batch) { const expiry = batch.expiry_date ? new Date(batch.expiry_date).toLocaleDateString() : 'Unknown'; return `${batch.batch_number} (exp ${expiry})`; } return `Batch ${row.batch_id}`; } return 'N/A'; } async function ensureBatchLookupForDispensing(rows) { const variantIds = Array.from(new Set(rows.map(row => row.drug_variant_id).filter(Boolean))); for (const variantId of variantIds) { if (loadedBatchVariants.has(variantId)) { continue; } try { const response = await apiCall(`/variants/${variantId}/batches`); if (!response.ok) { continue; } const batches = await response.json(); batches.forEach(batch => { batchLookupById.set(batch.id, { batch_number: batch.batch_number, expiry_date: batch.expiry_date }); }); loadedBatchVariants.add(variantId); } catch (error) { console.error(`Failed to load batch lookup for variant ${variantId}:`, error); } } } function detailsContainsText(details, searchText) { if (!details) return false; try { return JSON.stringify(details).toLowerCase().includes(searchText); } catch { return false; } } function getActiveRows() { if (activeReportType === 'dispensing') return dispensingRows; if (activeReportType === 'global_inventory') return globalInventoryRows; if (activeReportType === 'batch_attention') return batchAttentionRows; return auditTrailRows; } function getRowUser(row) { if (activeReportType === 'dispensing') return row.user_name || 'unknown'; if (activeReportType === 'global_inventory') return ''; if (activeReportType === 'batch_attention') return ''; return row.actor_username || 'system'; } function getRowDrug(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}` : ''}`; return extractDrugLabelFromAuditRow(row); } function getRowDate(row) { 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); return new Date(row.created_at); } function populateCommonFilters(rows) { const userFilter = document.getElementById('reportUserFilter'); const drugFilter = document.getElementById('reportDrugFilter'); if (!userFilter || !drugFilter) return; const previousUser = userFilter.value; const previousDrug = drugFilter.value; const users = Array.from(new Set(rows.map(getRowUser).filter(Boolean))).sort((a, b) => a.localeCompare(b)); const drugs = Array.from(new Set(rows.map(getRowDrug).filter(label => label && label !== 'N/A'))).sort((a, b) => a.localeCompare(b)); userFilter.innerHTML = ''; users.forEach(user => { const option = document.createElement('option'); option.value = user; option.textContent = user; userFilter.appendChild(option); }); drugFilter.innerHTML = ''; drugs.forEach(drug => { const option = document.createElement('option'); option.value = drug; option.textContent = drug; drugFilter.appendChild(option); }); userFilter.value = users.includes(previousUser) ? previousUser : ''; drugFilter.value = drugs.includes(previousDrug) ? previousDrug : ''; } function renderAuditTable(rows) { const container = document.getElementById('reportsTableContainer'); if (!container) return; if (!rows.length) { container.innerHTML = '
No audit events match the selected filters.
'; return; } const rowsHtml = rows.map(row => { const dateText = new Date(row.created_at).toLocaleString(); const userText = row.actor_username || 'system'; const detailsText = row.details ? escapeHtml(JSON.stringify(row.details, null, 2)) : '-'; return `${detailsText}| Date | User | Action | Entity | Drug | Summary | Details |
|---|
No dispensing records match the selected filters.
'; return; } const rowsHtml = rows.map(row => { const dateText = new Date(row.dispensed_at).toLocaleString(); const info = getVariantInfoById(row.drug_variant_id); const quantityText = `${row.quantity} ${info.unit || 'units'}`; const animal = row.animal_name || '-'; const vet = row.prescribing_vet || '-'; const notes = row.notes || '-'; const allocations = formatDispenseAllocation(row); return `| Date | User | Drug | Strength | Quantity | Animal | Prescribing Vet | Batch Allocation | Notes |
|---|
No inventory lines match the selected filters.
'; 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 `| Drug | Variant | Batch | Quantity | Location | Expiry |
|---|
No expired batches match the selected filters.
'; return; } const rowsHtml = rows.map(row => { const expiryText = row.expiry_date ? new Date(row.expiry_date).toLocaleDateString() : 'Unknown'; const quantityText = `${row.quantity} ${row.unit || 'units'}`; const statusText = 'Expired'; const isExpired = true; const packState = row.current_loose_base_units > 0 ? `${row.current_full_pack_count || 0} full packs + ${row.current_loose_base_units} loose ${row.unit || 'units'}` : `${row.current_full_pack_count || 0} full packs`; return `| Drug | Strength | Batch | Quantity | Pack State | Location | Expiry | Status | Action |
|---|
${loadingText}
`; } if (reportsSummary) reportsSummary.textContent = ''; try { if (activeReportType === 'dispensing') { const response = await apiCall('/dispense/history?limit=1000'); if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'Failed to load dispensing history'); } dispensingRows = await response.json(); await ensureBatchLookupForDispensing(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') { const response = await apiCall('/reports/batch-attention'); if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'Failed to load batch attention report'); } batchAttentionRows = await response.json(); populateCommonFilters(batchAttentionRows); } else { const response = await apiCall('/reports/audit-trail'); if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'Failed to load audit trail report'); } auditTrailRows = await response.json(); populateCommonFilters(auditTrailRows); } applyCurrentFilters(); } catch (error) { console.error('Error loading report:', error); if (container) { container.innerHTML = `Failed to load report: ${escapeHtml(error.message)}
`; } showToast(`Failed to load report: ${error.message}`, 'error'); } } function showReportsPage() { document.getElementById('reportsApp').style.display = 'block'; document.getElementById('reportsErrorState').style.display = 'none'; const userDisplay = document.getElementById('reportsCurrentUser'); if (userDisplay && currentUser) { const roleLabel = currentUser.role.charAt(0).toUpperCase() + currentUser.role.slice(1); userDisplay.textContent = `👤 ${currentUser.username} [${roleLabel}]`; } } function showErrorState(message) { const errorMessage = document.getElementById('reportsErrorMessage'); if (errorMessage) errorMessage.textContent = message; document.getElementById('reportsApp').style.display = 'none'; document.getElementById('reportsErrorState').style.display = 'flex'; } function setupEventListeners() { const reportTypeSelect = document.getElementById('reportTypeSelect'); const applyBtn = document.getElementById('applyReportFiltersBtn'); const clearBtn = document.getElementById('clearReportFiltersBtn'); const refreshBtn = document.getElementById('refreshReportsBtn'); const backBtn = document.getElementById('backToInventoryBtn'); const logoutBtn = document.getElementById('reportsLogoutBtn'); const goToLoginBtn = document.getElementById('goToLoginBtn'); const disposeBatchForm = document.getElementById('disposeBatchForm'); const cancelDisposeBatchBtn = document.getElementById('cancelDisposeBatchBtn'); const closeButtons = document.querySelectorAll('.close'); const userFilter = document.getElementById('reportUserFilter'); const drugFilter = document.getElementById('reportDrugFilter'); const fromDate = document.getElementById('reportFromDate'); const toDate = document.getElementById('reportToDate'); const searchInput = document.getElementById('reportActionSearch'); if (reportTypeSelect) { reportTypeSelect.addEventListener('change', async (e) => { activeReportType = e.target.value; updateReportHeading(); await loadActiveReport(); }); } if (applyBtn) applyBtn.addEventListener('click', applyCurrentFilters); if (refreshBtn) refreshBtn.addEventListener('click', loadActiveReport); if (clearBtn) { clearBtn.addEventListener('click', () => { if (userFilter) userFilter.value = ''; if (drugFilter) drugFilter.value = ''; if (fromDate) fromDate.value = ''; if (toDate) toDate.value = ''; if (searchInput) searchInput.value = ''; applyCurrentFilters(); }); } if (userFilter) userFilter.addEventListener('change', applyCurrentFilters); if (drugFilter) drugFilter.addEventListener('change', applyCurrentFilters); if (fromDate) fromDate.addEventListener('change', applyCurrentFilters); if (toDate) toDate.addEventListener('change', applyCurrentFilters); if (searchInput) { let timeout; searchInput.addEventListener('input', () => { clearTimeout(timeout); timeout = setTimeout(applyCurrentFilters, 120); }); } if (backBtn) backBtn.addEventListener('click', () => { window.location.href = 'index.html'; }); if (logoutBtn) logoutBtn.addEventListener('click', () => { localStorage.removeItem('accessToken'); localStorage.removeItem('currentUser'); window.location.href = 'index.html'; }); if (goToLoginBtn) goToLoginBtn.addEventListener('click', () => { window.location.href = 'index.html'; }); if (disposeBatchForm) disposeBatchForm.addEventListener('submit', handleDisposeBatch); if (cancelDisposeBatchBtn) cancelDisposeBatchBtn.addEventListener('click', closeDisposeBatchModal); closeButtons.forEach(btn => btn.addEventListener('click', (e) => { const modal = e.target.closest('.modal'); if (modal?.id === 'disposeBatchModal') { closeDisposeBatchModal(); return; } closeModal(modal); })); window.addEventListener('click', (e) => { if (e.target.classList.contains('modal')) { if (e.target.id === 'disposeBatchModal') { closeDisposeBatchModal(); return; } closeModal(e.target); } }); } async function initializeReportsPage() { const token = localStorage.getItem('accessToken'); const userData = localStorage.getItem('currentUser'); if (!token || !userData) { showErrorState('You are not logged in. Please sign in first.'); return; } accessToken = token; try { currentUser = JSON.parse(userData); } catch { showErrorState('Invalid session data. Please sign in again.'); return; } if (!currentUser.role && currentUser.is_admin !== undefined) { currentUser.role = currentUser.is_admin ? 'admin' : 'user'; } if (currentUser.role !== 'admin') { showErrorState('Only admin users can access reports.'); return; } setupEventListeners(); showReportsPage(); updateReportHeading(); await loadReferenceData(); await loadActiveReport(); } document.addEventListener('DOMContentLoaded', initializeReportsPage);