Better dispensing

This commit is contained in:
2026-04-06 09:15:38 -04:00
parent 664a3189bd
commit 5b5e17ec3e
5 changed files with 773 additions and 269 deletions
+104 -7
View File
@@ -5,6 +5,7 @@ let currentUser = null;
let allDrugs = [];
let auditTrailRows = [];
let dispensingRows = [];
let batchAttentionRows = [];
let activeReportType = 'dispensing';
const batchLookupById = new Map();
const loadedBatchVariants = new Set();
@@ -170,19 +171,27 @@ function detailsContainsText(details, searchText) {
}
function getActiveRows() {
return activeReportType === 'dispensing' ? dispensingRows : auditTrailRows;
if (activeReportType === 'dispensing') return dispensingRows;
if (activeReportType === 'batch_attention') return batchAttentionRows;
return auditTrailRows;
}
function getRowUser(row) {
return activeReportType === 'dispensing' ? (row.user_name || 'unknown') : (row.actor_username || 'system');
if (activeReportType === 'dispensing') return row.user_name || 'unknown';
if (activeReportType === 'batch_attention') return '';
return row.actor_username || 'system';
}
function getRowDrug(row) {
return activeReportType === 'dispensing' ? extractDrugLabelFromDispenseRow(row) : extractDrugLabelFromAuditRow(row);
if (activeReportType === 'dispensing') return extractDrugLabelFromDispenseRow(row);
if (activeReportType === 'batch_attention') return `${row.drug_name || 'Unknown Drug'}${row.strength ? ` ${row.strength}` : ''}`;
return extractDrugLabelFromAuditRow(row);
}
function getRowDate(row) {
return new Date(activeReportType === 'dispensing' ? row.dispensed_at : row.created_at);
if (activeReportType === 'dispensing') return new Date(row.dispensed_at);
if (activeReportType === 'batch_attention') return new Date(row.expiry_date);
return new Date(row.created_at);
}
function populateCommonFilters(rows) {
@@ -193,7 +202,7 @@ function populateCommonFilters(rows) {
const previousUser = userFilter.value;
const previousDrug = drugFilter.value;
const users = Array.from(new Set(rows.map(getRowUser))).sort((a, b) => a.localeCompare(b));
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 = '<option value="">All Users</option>';
@@ -311,6 +320,59 @@ function renderDispensingTable(rows) {
`;
}
function renderBatchAttentionTable(rows) {
const container = document.getElementById('reportsTableContainer');
if (!container) return;
if (!rows.length) {
container.innerHTML = '<p class="empty" style="padding: 14px;">No expired or partial batches match the selected filters.</p>';
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'}`;
let statusText = 'Partial';
if (row.status === 'expired') statusText = 'Expired';
if (row.status === 'expired_partial') statusText = 'Expired + Partial';
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 `
<tr>
<td>${escapeHtml(row.drug_name || '')}</td>
<td>${escapeHtml(row.strength || '-')}</td>
<td>${escapeHtml(row.batch_number || '')}</td>
<td>${escapeHtml(quantityText)}</td>
<td>${escapeHtml(packState)}</td>
<td>${escapeHtml(row.location || '-')}</td>
<td>${escapeHtml(expiryText)}</td>
<td>${escapeHtml(statusText)}</td>
</tr>
`;
}).join('');
container.innerHTML = `
<table class="reports-table">
<thead>
<tr>
<th>Drug</th>
<th>Strength</th>
<th>Batch</th>
<th>Quantity</th>
<th>Pack State</th>
<th>Location</th>
<th>Expiry</th>
<th>Status</th>
</tr>
</thead>
<tbody>${rowsHtml}</tbody>
</table>
`;
}
function applyCurrentFilters() {
const userFilter = document.getElementById('reportUserFilter');
const drugFilter = document.getElementById('reportDrugFilter');
@@ -346,6 +408,16 @@ function applyCurrentFilters() {
formatDispenseAllocation(row)
].join(' ').toLowerCase();
textMatch = haystack.includes(searchText);
} else if (activeReportType === 'batch_attention') {
const haystack = [
row.drug_name || '',
row.strength || '',
row.batch_number || '',
row.location || '',
row.status || '',
row.unit || ''
].join(' ').toLowerCase();
textMatch = haystack.includes(searchText);
} else {
const actionText = (row.action || '').toLowerCase();
const entityText = (row.entity_type || '').toLowerCase();
@@ -361,12 +433,18 @@ function applyCurrentFilters() {
});
if (reportsSummary) {
const reportName = activeReportType === 'dispensing' ? 'dispensing records' : 'audit events';
const reportName = activeReportType === 'dispensing'
? 'dispensing records'
: activeReportType === 'batch_attention'
? 'expired/partial batches'
: 'audit events';
reportsSummary.textContent = `Showing ${filteredRows.length} of ${sourceRows.length} ${reportName}`;
}
if (activeReportType === 'dispensing') {
renderDispensingTable(filteredRows);
} else if (activeReportType === 'batch_attention') {
renderBatchAttentionTable(filteredRows);
} else {
renderAuditTable(filteredRows);
}
@@ -375,14 +453,21 @@ function applyCurrentFilters() {
function updateReportHeading() {
const heading = document.getElementById('reportsHeading');
const searchInput = document.getElementById('reportActionSearch');
const userFilter = document.getElementById('reportUserFilter')?.closest('.report-control');
if (!heading || !searchInput) return;
if (activeReportType === 'dispensing') {
heading.textContent = 'Dispensing History';
searchInput.placeholder = 'Search user, drug, animal, notes, batch allocation...';
if (userFilter) userFilter.style.display = '';
} else if (activeReportType === 'batch_attention') {
heading.textContent = 'Expired / Partial Batches';
searchInput.placeholder = 'Search drug, batch, location, status...';
if (userFilter) userFilter.style.display = 'none';
} else {
heading.textContent = 'Audit Trail (Raw)';
searchInput.placeholder = 'Search action, entity, details...';
if (userFilter) userFilter.style.display = '';
}
}
@@ -423,7 +508,11 @@ 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...';
const loadingText = activeReportType === 'dispensing'
? 'Loading dispensing history...'
: activeReportType === 'batch_attention'
? 'Loading expired / partial batches...'
: 'Loading audit trail...';
container.innerHTML = `<p class="loading" style="padding: 14px;">${loadingText}</p>`;
}
if (reportsSummary) reportsSummary.textContent = '';
@@ -438,6 +527,14 @@ async function loadActiveReport() {
dispensingRows = await response.json();
await ensureBatchLookupForDispensing(dispensingRows);
populateCommonFilters(dispensingRows);
} 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) {