Stock Take PDF
This commit is contained in:
@@ -31,7 +31,7 @@
|
||||
<label for="reportTypeSelect">Report</label>
|
||||
<select id="reportTypeSelect">
|
||||
<option value="dispensing" selected>Dispensing History</option>
|
||||
<option value="global_inventory">Global Inventory</option>
|
||||
<option value="global_inventory">Stock Check</option>
|
||||
<option value="batch_attention">Expired Batches</option>
|
||||
<option value="audit">Audit Trail (Raw)</option>
|
||||
</select>
|
||||
@@ -64,6 +64,7 @@
|
||||
<button id="applyReportFiltersBtn" type="button" class="btn btn-primary btn-small">Apply Filters</button>
|
||||
<button id="clearReportFiltersBtn" type="button" class="btn btn-secondary btn-small">Clear</button>
|
||||
<button id="refreshReportsBtn" type="button" class="btn btn-secondary btn-small">Refresh</button>
|
||||
<button id="stockCheckPdfBtn" type="button" class="btn btn-secondary btn-small" style="display: none;">Print Stock Check</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -120,4 +121,4 @@
|
||||
|
||||
<script src="reports.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
+3
-1
@@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user