Stock Take PDF
This commit is contained in:
@@ -31,7 +31,7 @@
|
|||||||
<label for="reportTypeSelect">Report</label>
|
<label for="reportTypeSelect">Report</label>
|
||||||
<select id="reportTypeSelect">
|
<select id="reportTypeSelect">
|
||||||
<option value="dispensing" selected>Dispensing History</option>
|
<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="batch_attention">Expired Batches</option>
|
||||||
<option value="audit">Audit Trail (Raw)</option>
|
<option value="audit">Audit Trail (Raw)</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -64,6 +64,7 @@
|
|||||||
<button id="applyReportFiltersBtn" type="button" class="btn btn-primary btn-small">Apply Filters</button>
|
<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="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="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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -592,33 +592,288 @@ function applyCurrentFilters() {
|
|||||||
} else {
|
} else {
|
||||||
renderAuditTable(filteredRows);
|
renderAuditTable(filteredRows);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return filteredRows;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateReportHeading() {
|
function updateReportHeading() {
|
||||||
const heading = document.getElementById('reportsHeading');
|
const heading = document.getElementById('reportsHeading');
|
||||||
const searchInput = document.getElementById('reportActionSearch');
|
const searchInput = document.getElementById('reportActionSearch');
|
||||||
const userFilter = document.getElementById('reportUserFilter')?.closest('.report-control');
|
const userFilter = document.getElementById('reportUserFilter')?.closest('.report-control');
|
||||||
|
const stockCheckPdfBtn = document.getElementById('stockCheckPdfBtn');
|
||||||
if (!heading || !searchInput) return;
|
if (!heading || !searchInput) return;
|
||||||
|
|
||||||
if (activeReportType === 'dispensing') {
|
if (activeReportType === 'dispensing') {
|
||||||
heading.textContent = 'Dispensing History';
|
heading.textContent = 'Dispensing History';
|
||||||
searchInput.placeholder = 'Search user, drug, animal, notes, batch allocation...';
|
searchInput.placeholder = 'Search user, drug, animal, notes, batch allocation...';
|
||||||
if (userFilter) userFilter.style.display = '';
|
if (userFilter) userFilter.style.display = '';
|
||||||
|
if (stockCheckPdfBtn) stockCheckPdfBtn.style.display = 'none';
|
||||||
} else if (activeReportType === 'global_inventory') {
|
} else if (activeReportType === 'global_inventory') {
|
||||||
heading.textContent = 'Global Inventory';
|
heading.textContent = 'Global Inventory';
|
||||||
searchInput.placeholder = 'Search drug, variant, batch, location...';
|
searchInput.placeholder = 'Search drug, variant, batch, location...';
|
||||||
if (userFilter) userFilter.style.display = 'none';
|
if (userFilter) userFilter.style.display = 'none';
|
||||||
|
if (stockCheckPdfBtn) stockCheckPdfBtn.style.display = '';
|
||||||
} else if (activeReportType === 'batch_attention') {
|
} else if (activeReportType === 'batch_attention') {
|
||||||
heading.textContent = 'Expired Batches';
|
heading.textContent = 'Expired Batches';
|
||||||
searchInput.placeholder = 'Search drug, batch, location...';
|
searchInput.placeholder = 'Search drug, batch, location...';
|
||||||
if (userFilter) userFilter.style.display = 'none';
|
if (userFilter) userFilter.style.display = 'none';
|
||||||
|
if (stockCheckPdfBtn) stockCheckPdfBtn.style.display = 'none';
|
||||||
} else {
|
} else {
|
||||||
heading.textContent = 'Audit Trail (Raw)';
|
heading.textContent = 'Audit Trail (Raw)';
|
||||||
searchInput.placeholder = 'Search action, entity, details...';
|
searchInput.placeholder = 'Search action, entity, details...';
|
||||||
if (userFilter) userFilter.style.display = '';
|
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 = {}) {
|
async function apiCall(endpoint, options = {}) {
|
||||||
const headers = {
|
const headers = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -736,6 +991,7 @@ function setupEventListeners() {
|
|||||||
const applyBtn = document.getElementById('applyReportFiltersBtn');
|
const applyBtn = document.getElementById('applyReportFiltersBtn');
|
||||||
const clearBtn = document.getElementById('clearReportFiltersBtn');
|
const clearBtn = document.getElementById('clearReportFiltersBtn');
|
||||||
const refreshBtn = document.getElementById('refreshReportsBtn');
|
const refreshBtn = document.getElementById('refreshReportsBtn');
|
||||||
|
const stockCheckPdfBtn = document.getElementById('stockCheckPdfBtn');
|
||||||
const backBtn = document.getElementById('backToInventoryBtn');
|
const backBtn = document.getElementById('backToInventoryBtn');
|
||||||
const logoutBtn = document.getElementById('reportsLogoutBtn');
|
const logoutBtn = document.getElementById('reportsLogoutBtn');
|
||||||
const goToLoginBtn = document.getElementById('goToLoginBtn');
|
const goToLoginBtn = document.getElementById('goToLoginBtn');
|
||||||
@@ -759,6 +1015,7 @@ function setupEventListeners() {
|
|||||||
|
|
||||||
if (applyBtn) applyBtn.addEventListener('click', applyCurrentFilters);
|
if (applyBtn) applyBtn.addEventListener('click', applyCurrentFilters);
|
||||||
if (refreshBtn) refreshBtn.addEventListener('click', loadActiveReport);
|
if (refreshBtn) refreshBtn.addEventListener('click', loadActiveReport);
|
||||||
|
if (stockCheckPdfBtn) stockCheckPdfBtn.addEventListener('click', generateStockCheckPdf);
|
||||||
|
|
||||||
if (clearBtn) {
|
if (clearBtn) {
|
||||||
clearBtn.addEventListener('click', () => {
|
clearBtn.addEventListener('click', () => {
|
||||||
|
|||||||
@@ -842,6 +842,8 @@ footer {
|
|||||||
.report-actions {
|
.report-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reports-summary {
|
.reports-summary {
|
||||||
|
|||||||
Reference in New Issue
Block a user