Stock Take PDF

This commit is contained in:
2026-06-18 20:15:24 +01:00
parent 0fea301af1
commit 3f230bb0d7
3 changed files with 263 additions and 3 deletions
+257
View File
@@ -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 `
<tr>
<td>${escapeHtml(row.strength || '-')}</td>
<td>${escapeHtml(batchText)}</td>
<td>${escapeHtml(quantityText)}</td>
<td>${escapeHtml(row.location_name || '-')}</td>
<td>${escapeHtml(formatDisplayDate(row.expiry_date))}</td>
<td>${escapeHtml(controlledText)}</td>
<td class="manual-entry"></td>
<td class="manual-entry notes-cell"></td>
</tr>
`;
}).join('');
return `
<section class="drug-section">
<h2>${escapeHtml(drugName)}</h2>
<table>
<thead>
<tr>
<th>Variant</th>
<th>Batch</th>
<th>System Qty</th>
<th>Location</th>
<th>Expiry</th>
<th>CD</th>
<th>Actual</th>
<th>Notes</th>
</tr>
</thead>
<tbody>${bodyHtml}</tbody>
</table>
</section>
`;
}).join('');
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Manual Stock Check</title>
<style>
@page { size: A4 portrait; margin: 8mm; }
* { box-sizing: border-box; }
body {
margin: 0;
color: #111827;
font-family: Arial, Helvetica, sans-serif;
font-size: 8.5pt;
}
header {
display: flex;
justify-content: space-between;
gap: 10px;
align-items: flex-start;
margin-bottom: 5mm;
border-bottom: 1.5px solid #111827;
padding-bottom: 3mm;
}
h1 {
margin: 0 0 1.5mm;
font-size: 14pt;
line-height: 1.1;
}
.meta {
color: #374151;
font-size: 7.5pt;
line-height: 1.35;
}
.signoff {
min-width: 52mm;
display: grid;
gap: 2mm;
font-size: 7.5pt;
}
.signoff-line {
border-bottom: 1px solid #6b7280;
height: 5mm;
}
.empty {
border: 1px solid #d1d5db;
padding: 8mm;
color: #4b5563;
}
.drug-section {
break-inside: avoid;
page-break-inside: avoid;
margin-bottom: 4mm;
}
h2 {
margin: 0;
padding: 1mm 1.5mm;
background: #e5e7eb;
border: 1px solid #9ca3af;
border-bottom: 0;
font-size: 9.5pt;
line-height: 1.15;
}
table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
th,
td {
border: 1px solid #9ca3af;
padding: 1.2mm 1.4mm;
text-align: left;
vertical-align: top;
overflow-wrap: anywhere;
line-height: 1.2;
}
th {
background: #f3f4f6;
font-size: 6.8pt;
text-transform: uppercase;
letter-spacing: 0;
padding-top: 1mm;
padding-bottom: 1mm;
}
th:nth-child(1) { width: 13%; }
th:nth-child(2) { width: 15%; }
th:nth-child(3) { width: 12%; }
th:nth-child(4) { width: 13%; }
th:nth-child(5) { width: 10%; }
th:nth-child(6) { width: 5%; }
th:nth-child(7) { width: 12%; }
th:nth-child(8) { width: 20%; }
td.manual-entry {
height: 7mm;
background: #ffffff;
}
.notes-cell {
min-height: 7mm;
}
@media print {
body { print-color-adjust: exact; -webkit-print-color-adjust: exact; }
}
</style>
</head>
<body>
<header>
<div>
<h1>Manual Stock Check</h1>
<div class="meta">Generated: ${escapeHtml(generatedAt)}</div>
<div class="meta">Filters: ${escapeHtml(filterSummary)}</div>
<div class="meta">Inventory lines: ${escapeHtml(String(totalLines))}</div>
</div>
<div class="signoff">
<div>Checked by<div class="signoff-line"></div></div>
<div>Date<div class="signoff-line"></div></div>
</div>
</header>
${sectionsHtml || '<div class="empty">No inventory lines match the selected filters.</div>'}
<script>
window.addEventListener('load', () => {
window.focus();
window.print();
});
</script>
</body>
</html>
`;
}
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', () => {