const API_URL = '/api'; let accessToken = null; let currentUser = null; let allDrugs = []; let auditTrailRows = []; let dispensingRows = []; let activeReportType = 'dispensing'; const batchLookupById = new Map(); const loadedBatchVariants = new Set(); 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 = ` ${icons[type] || icons.info} ${message} `; 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() { return activeReportType === 'dispensing' ? dispensingRows : auditTrailRows; } function getRowUser(row) { return activeReportType === 'dispensing' ? (row.user_name || 'unknown') : (row.actor_username || 'system'); } function getRowDrug(row) { return activeReportType === 'dispensing' ? extractDrugLabelFromDispenseRow(row) : extractDrugLabelFromAuditRow(row); } function getRowDate(row) { return new Date(activeReportType === 'dispensing' ? row.dispensed_at : 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))).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 ` ${escapeHtml(dateText)} ${escapeHtml(userText)} ${escapeHtml(row.action || '')} ${escapeHtml(row.entity_type || '')} ${escapeHtml(extractDrugLabelFromAuditRow(row))} ${escapeHtml(formatAuditSummary(row))} ${detailsText} `; }).join(''); container.innerHTML = ` ${rowsHtml}
Date User Action Entity Drug Summary Details
`; } function renderDispensingTable(rows) { const container = document.getElementById('reportsTableContainer'); if (!container) return; if (!rows.length) { container.innerHTML = '

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 notes = row.notes || '-'; const allocations = formatDispenseAllocation(row); return ` ${escapeHtml(dateText)} ${escapeHtml(row.user_name || 'unknown')} ${escapeHtml(info.drugName)} ${escapeHtml(info.strength || '-')} ${escapeHtml(quantityText)} ${escapeHtml(animal)} ${escapeHtml(allocations)} ${escapeHtml(notes)} `; }).join(''); container.innerHTML = ` ${rowsHtml}
Date User Drug Strength Quantity Animal Batch Allocation Notes
`; } function applyCurrentFilters() { const userFilter = document.getElementById('reportUserFilter'); const drugFilter = document.getElementById('reportDrugFilter'); const fromDateInput = document.getElementById('reportFromDate'); const toDateInput = document.getElementById('reportToDate'); const searchInput = document.getElementById('reportActionSearch'); const reportsSummary = document.getElementById('reportsSummary'); const selectedUser = userFilter ? userFilter.value : ''; const selectedDrug = drugFilter ? drugFilter.value : ''; const fromDate = fromDateInput && fromDateInput.value ? new Date(`${fromDateInput.value}T00:00:00`) : null; const toDate = toDateInput && toDateInput.value ? new Date(`${toDateInput.value}T23:59:59`) : null; const searchText = searchInput ? searchInput.value.trim().toLowerCase() : ''; const sourceRows = getActiveRows(); const filteredRows = sourceRows.filter(row => { const userMatch = !selectedUser || getRowUser(row) === selectedUser; const drugMatch = !selectedDrug || getRowDrug(row) === selectedDrug; const rowDate = getRowDate(row); const fromMatch = !fromDate || rowDate >= fromDate; const toMatch = !toDate || rowDate <= toDate; let textMatch = true; if (searchText) { if (activeReportType === 'dispensing') { const info = getVariantInfoById(row.drug_variant_id); const haystack = [ row.user_name || '', info.drugName || '', info.strength || '', row.animal_name || '', row.notes || '', formatDispenseAllocation(row) ].join(' ').toLowerCase(); textMatch = haystack.includes(searchText); } else { const actionText = (row.action || '').toLowerCase(); const entityText = (row.entity_type || '').toLowerCase(); const summaryText = formatAuditSummary(row).toLowerCase(); textMatch = actionText.includes(searchText) || entityText.includes(searchText) || summaryText.includes(searchText) || detailsContainsText(row.details, searchText); } } return userMatch && drugMatch && fromMatch && toMatch && textMatch; }); if (reportsSummary) { const reportName = activeReportType === 'dispensing' ? 'dispensing records' : 'audit events'; reportsSummary.textContent = `Showing ${filteredRows.length} of ${sourceRows.length} ${reportName}`; } if (activeReportType === 'dispensing') { renderDispensingTable(filteredRows); } else { renderAuditTable(filteredRows); } } function updateReportHeading() { const heading = document.getElementById('reportsHeading'); const searchInput = document.getElementById('reportActionSearch'); if (!heading || !searchInput) return; if (activeReportType === 'dispensing') { heading.textContent = 'Dispensing History'; searchInput.placeholder = 'Search user, drug, animal, notes, batch allocation...'; } else { heading.textContent = 'Audit Trail (Raw)'; searchInput.placeholder = 'Search action, entity, details...'; } } async function apiCall(endpoint, options = {}) { const headers = { 'Content-Type': 'application/json', ...options.headers }; if (accessToken) headers.Authorization = `Bearer ${accessToken}`; const response = await fetch(`${API_URL}${endpoint}`, { ...options, headers }); if (response.status === 401) { localStorage.removeItem('accessToken'); localStorage.removeItem('currentUser'); window.location.href = 'index.html'; throw new Error('Authentication expired'); } return response; } async function loadReferenceData() { try { const drugResponse = await apiCall('/drugs'); if (drugResponse.ok) { allDrugs = await drugResponse.json(); } } catch (error) { console.error('Failed to load drug reference data:', error); } } async function loadActiveReport() { const container = document.getElementById('reportsTableContainer'); const reportsSummary = document.getElementById('reportsSummary'); if (container) { const loadingText = activeReportType === 'dispensing' ? 'Loading dispensing history...' : 'Loading audit trail...'; container.innerHTML = `

${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 { 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 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'; }); } 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);