Reporting and batch management

This commit is contained in:
2026-04-06 11:04:06 -04:00
parent b958ca493b
commit 36f0a5b07e
5 changed files with 392 additions and 36 deletions
+79 -2
View File
@@ -5,6 +5,7 @@ let currentUser = null;
let allDrugs = [];
let auditTrailRows = [];
let dispensingRows = [];
let globalInventoryRows = [];
let batchAttentionRows = [];
let activeReportType = 'dispensing';
const batchLookupById = new Map();
@@ -200,24 +201,28 @@ function detailsContainsText(details, searchText) {
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);
}
@@ -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) {
const container = document.getElementById('reportsTableContainer');
if (!container) return;
@@ -469,8 +518,8 @@ function applyCurrentFilters() {
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;
const fromMatch = !fromDate || !rowDate || rowDate >= fromDate;
const toMatch = !toDate || !rowDate || rowDate <= toDate;
let textMatch = true;
if (searchText) {
@@ -485,6 +534,16 @@ function applyCurrentFilters() {
formatDispenseAllocation(row)
].join(' ').toLowerCase();
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') {
const haystack = [
row.drug_name || '',
@@ -512,6 +571,8 @@ function applyCurrentFilters() {
if (reportsSummary) {
const reportName = activeReportType === 'dispensing'
? 'dispensing records'
: activeReportType === 'global_inventory'
? 'inventory lines'
: activeReportType === 'batch_attention'
? 'expired batches'
: 'audit events';
@@ -520,6 +581,8 @@ function applyCurrentFilters() {
if (activeReportType === 'dispensing') {
renderDispensingTable(filteredRows);
} else if (activeReportType === 'global_inventory') {
renderGlobalInventoryTable(filteredRows);
} else if (activeReportType === 'batch_attention') {
renderBatchAttentionTable(filteredRows);
} else {
@@ -537,6 +600,10 @@ function updateReportHeading() {
heading.textContent = 'Dispensing History';
searchInput.placeholder = 'Search user, drug, animal, notes, batch allocation...';
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') {
heading.textContent = 'Expired Batches';
searchInput.placeholder = 'Search drug, batch, location...';
@@ -587,6 +654,8 @@ async function loadActiveReport() {
if (container) {
const loadingText = activeReportType === 'dispensing'
? 'Loading dispensing history...'
: activeReportType === 'global_inventory'
? 'Loading global inventory...'
: activeReportType === 'batch_attention'
? 'Loading expired batches...'
: 'Loading audit trail...';
@@ -604,6 +673,14 @@ async function loadActiveReport() {
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) {