855 lines
32 KiB
JavaScript
855 lines
32 KiB
JavaScript
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 = `
|
||
<span class="toast-icon">${icons[type] || icons.info}</span>
|
||
<span class="toast-message">${message}</span>
|
||
`;
|
||
|
||
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 = '<option value="">All Users</option>';
|
||
users.forEach(user => {
|
||
const option = document.createElement('option');
|
||
option.value = user;
|
||
option.textContent = user;
|
||
userFilter.appendChild(option);
|
||
});
|
||
|
||
drugFilter.innerHTML = '<option value="">All Drugs</option>';
|
||
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 = '<p class="empty" style="padding: 14px;">No audit events match the selected filters.</p>';
|
||
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 `
|
||
<tr>
|
||
<td>${escapeHtml(dateText)}</td>
|
||
<td>${escapeHtml(userText)}</td>
|
||
<td>${escapeHtml(row.action || '')}</td>
|
||
<td>${escapeHtml(row.entity_type || '')}</td>
|
||
<td>${escapeHtml(extractDrugLabelFromAuditRow(row))}</td>
|
||
<td>${escapeHtml(formatAuditSummary(row))}</td>
|
||
<td><code>${detailsText}</code></td>
|
||
</tr>
|
||
`;
|
||
}).join('');
|
||
|
||
container.innerHTML = `
|
||
<table class="reports-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Date</th>
|
||
<th>User</th>
|
||
<th>Action</th>
|
||
<th>Entity</th>
|
||
<th>Drug</th>
|
||
<th>Summary</th>
|
||
<th>Details</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>${rowsHtml}</tbody>
|
||
</table>
|
||
`;
|
||
}
|
||
|
||
function renderDispensingTable(rows) {
|
||
const container = document.getElementById('reportsTableContainer');
|
||
if (!container) return;
|
||
|
||
if (!rows.length) {
|
||
container.innerHTML = '<p class="empty" style="padding: 14px;">No dispensing records match the selected filters.</p>';
|
||
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 `
|
||
<tr>
|
||
<td>${escapeHtml(dateText)}</td>
|
||
<td>${escapeHtml(row.user_name || 'unknown')}</td>
|
||
<td>${escapeHtml(info.drugName)}</td>
|
||
<td>${escapeHtml(info.strength || '-')}</td>
|
||
<td>${escapeHtml(quantityText)}</td>
|
||
<td>${escapeHtml(animal)}</td>
|
||
<td>${escapeHtml(allocations)}</td>
|
||
<td>${escapeHtml(notes)}</td>
|
||
</tr>
|
||
`;
|
||
}).join('');
|
||
|
||
container.innerHTML = `
|
||
<table class="reports-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Date</th>
|
||
<th>User</th>
|
||
<th>Drug</th>
|
||
<th>Strength</th>
|
||
<th>Quantity</th>
|
||
<th>Animal</th>
|
||
<th>Batch Allocation</th>
|
||
<th>Notes</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>${rowsHtml}</tbody>
|
||
</table>
|
||
`;
|
||
}
|
||
|
||
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;
|
||
|
||
if (!rows.length) {
|
||
container.innerHTML = '<p class="empty" style="padding: 14px;">No expired 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'}`;
|
||
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 `
|
||
<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>
|
||
<td>${isExpired ? `<button type="button" class="btn btn-danger btn-small" onclick="disposeBatchFromReport(${row.batch_id}, '${String(row.batch_number || '').replace(/'/g, "\\'")}')">Dispose Expired Batch</button>` : '-'}</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>
|
||
<th>Action</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>${rowsHtml}</tbody>
|
||
</table>
|
||
`;
|
||
}
|
||
|
||
function disposeBatchFromReport(batchId, batchNumber) {
|
||
const modal = document.getElementById('disposeBatchModal');
|
||
const batchIdInput = document.getElementById('disposeBatchId');
|
||
const batchNameInput = document.getElementById('disposeBatchName');
|
||
const notesInput = document.getElementById('disposeBatchNotes');
|
||
|
||
if (!modal || !batchIdInput || !batchNameInput || !notesInput) {
|
||
showToast('Dispose batch modal is unavailable.', 'error');
|
||
return;
|
||
}
|
||
|
||
batchIdInput.value = String(batchId);
|
||
batchNameInput.value = batchNumber;
|
||
notesInput.value = '';
|
||
openModal(modal);
|
||
}
|
||
|
||
async function handleDisposeBatch(e) {
|
||
e.preventDefault();
|
||
|
||
const batchId = parseInt(document.getElementById('disposeBatchId')?.value || '', 10);
|
||
const notes = document.getElementById('disposeBatchNotes')?.value.trim() || '';
|
||
|
||
if (!batchId) {
|
||
showToast('Batch disposal context is unavailable.', 'error');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await apiCall(`/batches/${batchId}/dispose`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({ notes: notes || null })
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.detail || 'Failed to dispose batch');
|
||
}
|
||
|
||
closeDisposeBatchModal();
|
||
await loadActiveReport();
|
||
showToast('Expired batch marked as disposed.', 'success');
|
||
} catch (error) {
|
||
console.error('Error disposing batch from report:', error);
|
||
showToast('Failed to dispose batch: ' + error.message, 'error');
|
||
}
|
||
}
|
||
|
||
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 || rowDate >= fromDate;
|
||
const toMatch = !toDate || !rowDate || 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 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 || '',
|
||
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();
|
||
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'
|
||
: activeReportType === 'global_inventory'
|
||
? 'inventory lines'
|
||
: activeReportType === 'batch_attention'
|
||
? 'expired batches'
|
||
: 'audit events';
|
||
reportsSummary.textContent = `Showing ${filteredRows.length} of ${sourceRows.length} ${reportName}`;
|
||
}
|
||
|
||
if (activeReportType === 'dispensing') {
|
||
renderDispensingTable(filteredRows);
|
||
} else if (activeReportType === 'global_inventory') {
|
||
renderGlobalInventoryTable(filteredRows);
|
||
} else if (activeReportType === 'batch_attention') {
|
||
renderBatchAttentionTable(filteredRows);
|
||
} else {
|
||
renderAuditTable(filteredRows);
|
||
}
|
||
}
|
||
|
||
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 === '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...';
|
||
if (userFilter) userFilter.style.display = 'none';
|
||
} else {
|
||
heading.textContent = 'Audit Trail (Raw)';
|
||
searchInput.placeholder = 'Search action, entity, details...';
|
||
if (userFilter) userFilter.style.display = '';
|
||
}
|
||
}
|
||
|
||
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...'
|
||
: activeReportType === 'global_inventory'
|
||
? 'Loading global inventory...'
|
||
: activeReportType === 'batch_attention'
|
||
? 'Loading expired batches...'
|
||
: 'Loading audit trail...';
|
||
container.innerHTML = `<p class="loading" style="padding: 14px;">${loadingText}</p>`;
|
||
}
|
||
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 = `<p class="error" style="padding: 14px;">Failed to load report: ${escapeHtml(error.message)}</p>`;
|
||
}
|
||
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);
|