Batch disposal

This commit is contained in:
2026-04-06 10:41:33 -04:00
parent 5b5e17ec3e
commit b958ca493b
7 changed files with 620 additions and 65 deletions
+110 -8
View File
@@ -10,6 +10,34 @@ 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;
@@ -325,16 +353,15 @@ function renderBatchAttentionTable(rows) {
if (!container) return;
if (!rows.length) {
container.innerHTML = '<p class="empty" style="padding: 14px;">No expired or partial batches match the selected filters.</p>';
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'}`;
let statusText = 'Partial';
if (row.status === 'expired') statusText = 'Expired';
if (row.status === 'expired_partial') statusText = 'Expired + Partial';
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'}`
@@ -350,6 +377,7 @@ function renderBatchAttentionTable(rows) {
<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('');
@@ -366,6 +394,7 @@ function renderBatchAttentionTable(rows) {
<th>Location</th>
<th>Expiry</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>${rowsHtml}</tbody>
@@ -373,6 +402,54 @@ function renderBatchAttentionTable(rows) {
`;
}
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');
@@ -436,7 +513,7 @@ function applyCurrentFilters() {
const reportName = activeReportType === 'dispensing'
? 'dispensing records'
: activeReportType === 'batch_attention'
? 'expired/partial batches'
? 'expired batches'
: 'audit events';
reportsSummary.textContent = `Showing ${filteredRows.length} of ${sourceRows.length} ${reportName}`;
}
@@ -461,8 +538,8 @@ function updateReportHeading() {
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...';
heading.textContent = 'Expired Batches';
searchInput.placeholder = 'Search drug, batch, location...';
if (userFilter) userFilter.style.display = 'none';
} else {
heading.textContent = 'Audit Trail (Raw)';
@@ -511,7 +588,7 @@ async function loadActiveReport() {
const loadingText = activeReportType === 'dispensing'
? 'Loading dispensing history...'
: activeReportType === 'batch_attention'
? 'Loading expired / partial batches...'
? 'Loading expired batches...'
: 'Loading audit trail...';
container.innerHTML = `<p class="loading" style="padding: 14px;">${loadingText}</p>`;
}
@@ -581,6 +658,9 @@ function setupEventListeners() {
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');
@@ -635,6 +715,28 @@ function setupEventListeners() {
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() {