From 3f230bb0d760c792b6160de79772255026d3bf03 Mon Sep 17 00:00:00 2001 From: James Pattinson Date: Thu, 18 Jun 2026 20:15:24 +0100 Subject: [PATCH] Stock Take PDF --- frontend/reports.html | 5 +- frontend/reports.js | 257 ++++++++++++++++++++++++++++++++++++++++++ frontend/styles.css | 4 +- 3 files changed, 263 insertions(+), 3 deletions(-) diff --git a/frontend/reports.html b/frontend/reports.html index c7e90bf..a12054e 100644 --- a/frontend/reports.html +++ b/frontend/reports.html @@ -31,7 +31,7 @@ @@ -64,6 +64,7 @@ + @@ -120,4 +121,4 @@ - \ No newline at end of file + diff --git a/frontend/reports.js b/frontend/reports.js index 46c9190..3d4267e 100644 --- a/frontend/reports.js +++ b/frontend/reports.js @@ -592,33 +592,288 @@ function applyCurrentFilters() { } else { renderAuditTable(filteredRows); } + + return filteredRows; } function updateReportHeading() { const heading = document.getElementById('reportsHeading'); const searchInput = document.getElementById('reportActionSearch'); const userFilter = document.getElementById('reportUserFilter')?.closest('.report-control'); + const stockCheckPdfBtn = document.getElementById('stockCheckPdfBtn'); 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 = ''; + if (stockCheckPdfBtn) stockCheckPdfBtn.style.display = 'none'; } else if (activeReportType === 'global_inventory') { heading.textContent = 'Global Inventory'; searchInput.placeholder = 'Search drug, variant, batch, location...'; if (userFilter) userFilter.style.display = 'none'; + if (stockCheckPdfBtn) stockCheckPdfBtn.style.display = ''; } else if (activeReportType === 'batch_attention') { heading.textContent = 'Expired Batches'; searchInput.placeholder = 'Search drug, batch, location...'; if (userFilter) userFilter.style.display = 'none'; + if (stockCheckPdfBtn) stockCheckPdfBtn.style.display = 'none'; } else { heading.textContent = 'Audit Trail (Raw)'; searchInput.placeholder = 'Search action, entity, details...'; if (userFilter) userFilter.style.display = ''; + if (stockCheckPdfBtn) stockCheckPdfBtn.style.display = 'none'; } } +function formatDisplayDate(value) { + if (!value) return '-'; + const date = typeof value === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(value) + ? new Date(`${value}T00:00:00`) + : new Date(value); + return Number.isNaN(date.getTime()) ? '-' : date.toLocaleDateString(); +} + +function getReportFilterSummary() { + const drugFilter = document.getElementById('reportDrugFilter')?.value || ''; + const fromDate = document.getElementById('reportFromDate')?.value || ''; + const toDate = document.getElementById('reportToDate')?.value || ''; + const searchText = document.getElementById('reportActionSearch')?.value.trim() || ''; + const parts = []; + + if (drugFilter) parts.push(`Drug: ${drugFilter}`); + if (fromDate) parts.push(`From: ${formatDisplayDate(fromDate)}`); + if (toDate) parts.push(`To: ${formatDisplayDate(toDate)}`); + if (searchText) parts.push(`Search: ${searchText}`); + + return parts.length ? parts.join(' | ') : 'All inventory lines'; +} + +function getStockCheckRows() { + const previousReportType = activeReportType; + activeReportType = 'global_inventory'; + const rows = applyCurrentFilters() || []; + activeReportType = previousReportType; + return rows; +} + +function buildStockCheckPdfHtml(rows) { + const groupedRows = new Map(); + const sortedRows = [...rows].sort((a, b) => { + const drugCompare = String(a.drug_name || '').localeCompare(String(b.drug_name || ''), undefined, { sensitivity: 'base' }); + if (drugCompare !== 0) return drugCompare; + const strengthCompare = String(a.strength || '').localeCompare(String(b.strength || ''), undefined, { numeric: true, sensitivity: 'base' }); + if (strengthCompare !== 0) return strengthCompare; + const expiryCompare = String(a.expiry_date || '9999-12-31').localeCompare(String(b.expiry_date || '9999-12-31')); + if (expiryCompare !== 0) return expiryCompare; + return String(a.batch_number || '').localeCompare(String(b.batch_number || ''), undefined, { numeric: true, sensitivity: 'base' }); + }); + + sortedRows.forEach(row => { + const drugName = row.drug_name || 'Unknown Drug'; + if (!groupedRows.has(drugName)) groupedRows.set(drugName, []); + groupedRows.get(drugName).push(row); + }); + + const generatedAt = new Date().toLocaleString(); + const filterSummary = getReportFilterSummary(); + const totalLines = rows.length; + + const sectionsHtml = Array.from(groupedRows.entries()).map(([drugName, drugRows]) => { + const bodyHtml = drugRows.map(row => { + const batchText = row.inventory_source === 'legacy' ? 'Legacy stock' : (row.batch_number || '-'); + const quantityText = `${row.quantity ?? 0} ${row.unit || 'units'}`; + const controlledText = row.is_controlled ? 'Yes' : 'No'; + + return ` + + ${escapeHtml(row.strength || '-')} + ${escapeHtml(batchText)} + ${escapeHtml(quantityText)} + ${escapeHtml(row.location_name || '-')} + ${escapeHtml(formatDisplayDate(row.expiry_date))} + ${escapeHtml(controlledText)} + + + + `; + }).join(''); + + return ` +
+

${escapeHtml(drugName)}

+ + + + + + + + + + + + + + ${bodyHtml} +
VariantBatchSystem QtyLocationExpiryCDActualNotes
+
+ `; + }).join(''); + + return ` + + + + + Manual Stock Check + + + +
+
+

Manual Stock Check

+
Generated: ${escapeHtml(generatedAt)}
+
Filters: ${escapeHtml(filterSummary)}
+
Inventory lines: ${escapeHtml(String(totalLines))}
+
+
+
Checked by
+
Date
+
+
+ ${sectionsHtml || '
No inventory lines match the selected filters.
'} + + + + `; +} + +function generateStockCheckPdf() { + if (activeReportType !== 'global_inventory') { + showToast('Select Global Inventory to generate a stock check PDF.', 'warning'); + return; + } + + const rows = getStockCheckRows(); + if (!rows.length) { + showToast('No inventory lines match the selected filters.', 'warning'); + return; + } + + const printWindow = window.open('', '_blank'); + if (!printWindow) { + showToast('Allow pop-ups to generate the stock check PDF.', 'warning'); + return; + } + + printWindow.document.open(); + printWindow.document.write(buildStockCheckPdfHtml(rows)); + printWindow.document.close(); +} + async function apiCall(endpoint, options = {}) { const headers = { 'Content-Type': 'application/json', @@ -736,6 +991,7 @@ function setupEventListeners() { const applyBtn = document.getElementById('applyReportFiltersBtn'); const clearBtn = document.getElementById('clearReportFiltersBtn'); const refreshBtn = document.getElementById('refreshReportsBtn'); + const stockCheckPdfBtn = document.getElementById('stockCheckPdfBtn'); const backBtn = document.getElementById('backToInventoryBtn'); const logoutBtn = document.getElementById('reportsLogoutBtn'); const goToLoginBtn = document.getElementById('goToLoginBtn'); @@ -759,6 +1015,7 @@ function setupEventListeners() { if (applyBtn) applyBtn.addEventListener('click', applyCurrentFilters); if (refreshBtn) refreshBtn.addEventListener('click', loadActiveReport); + if (stockCheckPdfBtn) stockCheckPdfBtn.addEventListener('click', generateStockCheckPdf); if (clearBtn) { clearBtn.addEventListener('click', () => { diff --git a/frontend/styles.css b/frontend/styles.css index 1c68c3a..35679fe 100644 --- a/frontend/styles.css +++ b/frontend/styles.css @@ -842,6 +842,8 @@ footer { .report-actions { display: flex; align-items: center; + gap: 8px; + flex-wrap: wrap; } .reports-summary { @@ -1384,4 +1386,4 @@ footer { min-width: auto; width: 100%; } -} \ No newline at end of file +}