diff --git a/docker-compose.yml b/docker-compose.yml
index 2512013..9294fce 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -19,6 +19,7 @@ services:
- MQTT_LABEL_TOPIC=${MQTT_LABEL_TOPIC:-vet/labels/print}
- MQTT_STATUS_TOPIC=${MQTT_STATUS_TOPIC:-vet/labels/status}
- LABEL_TEMPLATE_ID=${LABEL_TEMPLATE_ID:-vet_label}
+ - NOTES_TEMPLATE_ID=${NOTES_TEMPLATE_ID:-notes_1}
- LABEL_SIZE=${LABEL_SIZE:-29x90}
- LABEL_TEST=${LABEL_TEST:-false}
@@ -35,7 +36,7 @@ services:
frontend:
image: nginx:alpine
- container_name: drugsprod
+ container_name: drugsdev
volumes:
- ./frontend:/usr/share/nginx/html:ro
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
diff --git a/frontend/app.js b/frontend/app.js
index 3d6d977..9c86a0c 100644
--- a/frontend/app.js
+++ b/frontend/app.js
@@ -96,6 +96,11 @@ function showMainApp() {
if (locationsBtn) {
locationsBtn.style.display = currentUser.role === 'admin' ? 'block' : 'none';
}
+
+ const reportsBtn = document.getElementById('reportsBtn');
+ if (reportsBtn) {
+ reportsBtn.style.display = currentUser.role === 'admin' ? 'block' : 'none';
+ }
// Hide action buttons for read-only users
const isReadOnly = currentUser.role === 'readonly';
@@ -223,6 +228,7 @@ function setupEventListeners() {
const userMenuBtn = document.getElementById('userMenuBtn');
const adminBtn = document.getElementById('adminBtn');
const locationsBtn = document.getElementById('locationsBtn');
+ const reportsBtn = document.getElementById('reportsBtn');
const logoutBtn = document.getElementById('logoutBtn');
const changePasswordBtn = document.getElementById('changePasswordBtn');
@@ -260,7 +266,7 @@ function setupEventListeners() {
const closeHistoryBtn = document.getElementById('closeHistoryBtn');
if (closeHistoryBtn) closeHistoryBtn.addEventListener('click', () => closeModal(document.getElementById('historyModal')));
-
+
const closeUserManagementBtn = document.getElementById('closeUserManagementBtn');
if (closeUserManagementBtn) closeUserManagementBtn.addEventListener('click', () => closeModal(document.getElementById('userManagementModal')));
@@ -311,6 +317,7 @@ function setupEventListeners() {
if (changePasswordBtn) changePasswordBtn.addEventListener('click', openChangePasswordModal);
if (adminBtn) adminBtn.addEventListener('click', openUserManagement);
if (locationsBtn) locationsBtn.addEventListener('click', openLocationManagement);
+ if (reportsBtn) reportsBtn.addEventListener('click', openReportsPage);
if (logoutBtn) logoutBtn.addEventListener('click', handleLogout);
// Search functionality
@@ -1419,6 +1426,13 @@ function escapeHtml(text) {
return div.innerHTML;
}
+async function openReportsPage() {
+ const dropdown = document.getElementById('userDropdown');
+ if (dropdown) dropdown.style.display = 'none';
+
+ window.location.href = 'reports.html';
+}
+
// User Management
async function openUserManagement() {
const modal = document.getElementById('userManagementModal');
diff --git a/frontend/index.html b/frontend/index.html
index 888ac3a..ef9dd18 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -44,6 +44,7 @@
+
diff --git a/frontend/reports.html b/frontend/reports.html
new file mode 100644
index 0000000..56d704c
--- /dev/null
+++ b/frontend/reports.html
@@ -0,0 +1,92 @@
+
+
+
+
+
+ Reports - Drug Inventory System
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Loading audit trail...
+
+
+
+
+
+
+
+
+
+
+
Audit Reports
+
Access denied.
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/reports.js b/frontend/reports.js
new file mode 100644
index 0000000..4c22b5a
--- /dev/null
+++ b/frontend/reports.js
@@ -0,0 +1,578 @@
+const API_URL = '/api';
+
+let accessToken = null;
+let currentUser = null;
+let allDrugs = [];
+let auditTrailRows = [];
+let dispensingRows = [];
+let activeReportType = 'dispensing';
+const batchLookupById = new Map();
+const loadedBatchVariants = new Set();
+
+function showToast(message, type = 'info', duration = 3000) {
+ const container = document.getElementById('toastContainer');
+ if (!container) return;
+
+ const toast = document.createElement('div');
+ toast.className = `toast ${type}`;
+
+ const icons = { success: '✓', error: '✕', warning: '⚠', info: 'ℹ' };
+ toast.innerHTML = `
+ ${icons[type] || icons.info}
+ ${message}
+ `;
+
+ container.appendChild(toast);
+ setTimeout(() => {
+ toast.classList.add('fade-out');
+ setTimeout(() => {
+ if (container.contains(toast)) container.removeChild(toast);
+ }, 300);
+ }, duration);
+}
+
+function escapeHtml(text) {
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+}
+
+function getVariantInfoById(variantId) {
+ for (const drug of allDrugs) {
+ const variant = drug.variants.find(v => v.id === variantId);
+ if (variant) {
+ return {
+ drugName: drug.name,
+ strength: variant.strength,
+ unit: variant.unit
+ };
+ }
+ }
+ return { drugName: 'Unknown Drug', strength: '', unit: 'units' };
+}
+
+function extractDrugLabelFromAuditRow(row) {
+ const details = row.details || {};
+
+ if (details.drug_name) return details.drug_name;
+ if (details.before?.name) return details.before.name;
+ if (details.after?.name) return details.after.name;
+ if (details.name && row.entity_type === 'drug') return details.name;
+
+ if (details.drug_id) {
+ const info = getVariantInfoById(details.drug_id);
+ if (info.drugName !== 'Unknown Drug') return info.drugName;
+ }
+
+ if (details.variant_id) {
+ const info = getVariantInfoById(details.variant_id);
+ if (info.drugName) return `${info.drugName}${info.strength ? ` ${info.strength}` : ''}`;
+ }
+
+ if (row.entity_type === 'variant' && row.entity_id) {
+ const info = getVariantInfoById(row.entity_id);
+ if (info.drugName) return `${info.drugName}${info.strength ? ` ${info.strength}` : ''}`;
+ }
+
+ return 'N/A';
+}
+
+function extractDrugLabelFromDispenseRow(row) {
+ const info = getVariantInfoById(row.drug_variant_id);
+ return `${info.drugName}${info.strength ? ` ${info.strength}` : ''}`;
+}
+
+function formatAuditSummary(row) {
+ const details = row.details || {};
+ if (row.action === 'dispense.create') {
+ const qty = details.quantity || details.dispensed_quantity || '';
+ const animal = details.animal_name ? ` for ${details.animal_name}` : '';
+ return `Dispensed ${qty}${animal}`.trim();
+ }
+ if (row.action === 'batch.create' || row.action === 'batch.update') {
+ const batch = details.batch_number || details.after?.batch_number || details.before?.batch_number || '';
+ const quantity = details.quantity || details.after?.quantity || '';
+ return `Batch ${batch}${quantity !== '' ? ` (qty ${quantity})` : ''}`.trim();
+ }
+ if (row.action === 'drug.create' || row.action === 'drug.update') {
+ const name = details.name || details.after?.name || details.before?.name || extractDrugLabelFromAuditRow(row);
+ return `Drug ${name}`;
+ }
+ if (row.action === 'variant.create' || row.action === 'variant.update') {
+ const variant = details.strength || details.after?.strength || details.before?.strength || '';
+ const drug = extractDrugLabelFromAuditRow(row);
+ return `Variant ${variant}${drug !== 'N/A' ? ` (${drug})` : ''}`.trim();
+ }
+ if (details.message) return String(details.message);
+ return row.action || 'Event';
+}
+
+function formatDispenseAllocation(row) {
+ if (row.allocations && row.allocations.length > 0) {
+ return row.allocations
+ .map(a => {
+ const batch = batchLookupById.get(a.batch_id);
+ if (batch) {
+ const expiry = batch.expiry_date ? new Date(batch.expiry_date).toLocaleDateString() : 'Unknown';
+ return `${batch.batch_number} (exp ${expiry}): ${a.quantity}`;
+ }
+ return `Batch ${a.batch_id}: ${a.quantity}`;
+ })
+ .join(', ');
+ }
+ if (row.batch_id) {
+ const batch = batchLookupById.get(row.batch_id);
+ if (batch) {
+ const expiry = batch.expiry_date ? new Date(batch.expiry_date).toLocaleDateString() : 'Unknown';
+ return `${batch.batch_number} (exp ${expiry})`;
+ }
+ return `Batch ${row.batch_id}`;
+ }
+ return 'N/A';
+}
+
+async function ensureBatchLookupForDispensing(rows) {
+ const variantIds = Array.from(new Set(rows.map(row => row.drug_variant_id).filter(Boolean)));
+
+ for (const variantId of variantIds) {
+ if (loadedBatchVariants.has(variantId)) {
+ continue;
+ }
+
+ try {
+ const response = await apiCall(`/variants/${variantId}/batches`);
+ if (!response.ok) {
+ continue;
+ }
+
+ const batches = await response.json();
+ batches.forEach(batch => {
+ batchLookupById.set(batch.id, {
+ batch_number: batch.batch_number,
+ expiry_date: batch.expiry_date
+ });
+ });
+
+ loadedBatchVariants.add(variantId);
+ } catch (error) {
+ console.error(`Failed to load batch lookup for variant ${variantId}:`, error);
+ }
+ }
+}
+
+function detailsContainsText(details, searchText) {
+ if (!details) return false;
+ try {
+ return JSON.stringify(details).toLowerCase().includes(searchText);
+ } catch {
+ return false;
+ }
+}
+
+function getActiveRows() {
+ return activeReportType === 'dispensing' ? dispensingRows : auditTrailRows;
+}
+
+function getRowUser(row) {
+ return activeReportType === 'dispensing' ? (row.user_name || 'unknown') : (row.actor_username || 'system');
+}
+
+function getRowDrug(row) {
+ return activeReportType === 'dispensing' ? extractDrugLabelFromDispenseRow(row) : extractDrugLabelFromAuditRow(row);
+}
+
+function getRowDate(row) {
+ return new Date(activeReportType === 'dispensing' ? row.dispensed_at : row.created_at);
+}
+
+function populateCommonFilters(rows) {
+ const userFilter = document.getElementById('reportUserFilter');
+ const drugFilter = document.getElementById('reportDrugFilter');
+ if (!userFilter || !drugFilter) return;
+
+ const previousUser = userFilter.value;
+ const previousDrug = drugFilter.value;
+
+ const users = Array.from(new Set(rows.map(getRowUser))).sort((a, b) => a.localeCompare(b));
+ const drugs = Array.from(new Set(rows.map(getRowDrug).filter(label => label && label !== 'N/A'))).sort((a, b) => a.localeCompare(b));
+
+ userFilter.innerHTML = '';
+ users.forEach(user => {
+ const option = document.createElement('option');
+ option.value = user;
+ option.textContent = user;
+ userFilter.appendChild(option);
+ });
+
+ drugFilter.innerHTML = '';
+ drugs.forEach(drug => {
+ const option = document.createElement('option');
+ option.value = drug;
+ option.textContent = drug;
+ drugFilter.appendChild(option);
+ });
+
+ userFilter.value = users.includes(previousUser) ? previousUser : '';
+ drugFilter.value = drugs.includes(previousDrug) ? previousDrug : '';
+}
+
+function renderAuditTable(rows) {
+ const container = document.getElementById('reportsTableContainer');
+ if (!container) return;
+
+ if (!rows.length) {
+ container.innerHTML = 'No audit events match the selected filters.
';
+ return;
+ }
+
+ const rowsHtml = rows.map(row => {
+ const dateText = new Date(row.created_at).toLocaleString();
+ const userText = row.actor_username || 'system';
+ const detailsText = row.details ? escapeHtml(JSON.stringify(row.details, null, 2)) : '-';
+
+ return `
+
+ | ${escapeHtml(dateText)} |
+ ${escapeHtml(userText)} |
+ ${escapeHtml(row.action || '')} |
+ ${escapeHtml(row.entity_type || '')} |
+ ${escapeHtml(extractDrugLabelFromAuditRow(row))} |
+ ${escapeHtml(formatAuditSummary(row))} |
+ ${detailsText} |
+
+ `;
+ }).join('');
+
+ container.innerHTML = `
+
+
+
+ | Date |
+ User |
+ Action |
+ Entity |
+ Drug |
+ Summary |
+ Details |
+
+
+ ${rowsHtml}
+
+ `;
+}
+
+function renderDispensingTable(rows) {
+ const container = document.getElementById('reportsTableContainer');
+ if (!container) return;
+
+ if (!rows.length) {
+ container.innerHTML = 'No dispensing records match the selected filters.
';
+ return;
+ }
+
+ const rowsHtml = rows.map(row => {
+ const dateText = new Date(row.dispensed_at).toLocaleString();
+ const info = getVariantInfoById(row.drug_variant_id);
+ const quantityText = `${row.quantity} ${info.unit || 'units'}`;
+ const animal = row.animal_name || '-';
+ const notes = row.notes || '-';
+ const allocations = formatDispenseAllocation(row);
+
+ return `
+
+ | ${escapeHtml(dateText)} |
+ ${escapeHtml(row.user_name || 'unknown')} |
+ ${escapeHtml(info.drugName)} |
+ ${escapeHtml(info.strength || '-')} |
+ ${escapeHtml(quantityText)} |
+ ${escapeHtml(animal)} |
+ ${escapeHtml(allocations)} |
+ ${escapeHtml(notes)} |
+
+ `;
+ }).join('');
+
+ container.innerHTML = `
+
+
+
+ | Date |
+ User |
+ Drug |
+ Strength |
+ Quantity |
+ Animal |
+ Batch Allocation |
+ Notes |
+
+
+ ${rowsHtml}
+
+ `;
+}
+
+function applyCurrentFilters() {
+ const userFilter = document.getElementById('reportUserFilter');
+ const drugFilter = document.getElementById('reportDrugFilter');
+ const fromDateInput = document.getElementById('reportFromDate');
+ const toDateInput = document.getElementById('reportToDate');
+ const searchInput = document.getElementById('reportActionSearch');
+ const reportsSummary = document.getElementById('reportsSummary');
+
+ const selectedUser = userFilter ? userFilter.value : '';
+ const selectedDrug = drugFilter ? drugFilter.value : '';
+ const fromDate = fromDateInput && fromDateInput.value ? new Date(`${fromDateInput.value}T00:00:00`) : null;
+ const toDate = toDateInput && toDateInput.value ? new Date(`${toDateInput.value}T23:59:59`) : null;
+ const searchText = searchInput ? searchInput.value.trim().toLowerCase() : '';
+
+ const sourceRows = getActiveRows();
+ const filteredRows = sourceRows.filter(row => {
+ const userMatch = !selectedUser || getRowUser(row) === selectedUser;
+ const drugMatch = !selectedDrug || getRowDrug(row) === selectedDrug;
+ const rowDate = getRowDate(row);
+ const fromMatch = !fromDate || rowDate >= fromDate;
+ const toMatch = !toDate || rowDate <= toDate;
+
+ let textMatch = true;
+ if (searchText) {
+ if (activeReportType === 'dispensing') {
+ const info = getVariantInfoById(row.drug_variant_id);
+ const haystack = [
+ row.user_name || '',
+ info.drugName || '',
+ info.strength || '',
+ row.animal_name || '',
+ row.notes || '',
+ formatDispenseAllocation(row)
+ ].join(' ').toLowerCase();
+ textMatch = haystack.includes(searchText);
+ } else {
+ const actionText = (row.action || '').toLowerCase();
+ const entityText = (row.entity_type || '').toLowerCase();
+ const summaryText = formatAuditSummary(row).toLowerCase();
+ textMatch = actionText.includes(searchText)
+ || entityText.includes(searchText)
+ || summaryText.includes(searchText)
+ || detailsContainsText(row.details, searchText);
+ }
+ }
+
+ return userMatch && drugMatch && fromMatch && toMatch && textMatch;
+ });
+
+ if (reportsSummary) {
+ const reportName = activeReportType === 'dispensing' ? 'dispensing records' : 'audit events';
+ reportsSummary.textContent = `Showing ${filteredRows.length} of ${sourceRows.length} ${reportName}`;
+ }
+
+ if (activeReportType === 'dispensing') {
+ renderDispensingTable(filteredRows);
+ } else {
+ renderAuditTable(filteredRows);
+ }
+}
+
+function updateReportHeading() {
+ const heading = document.getElementById('reportsHeading');
+ const searchInput = document.getElementById('reportActionSearch');
+ if (!heading || !searchInput) return;
+
+ if (activeReportType === 'dispensing') {
+ heading.textContent = 'Dispensing History';
+ searchInput.placeholder = 'Search user, drug, animal, notes, batch allocation...';
+ } else {
+ heading.textContent = 'Audit Trail (Raw)';
+ searchInput.placeholder = 'Search action, entity, details...';
+ }
+}
+
+async function apiCall(endpoint, options = {}) {
+ const headers = {
+ 'Content-Type': 'application/json',
+ ...options.headers
+ };
+
+ if (accessToken) headers.Authorization = `Bearer ${accessToken}`;
+
+ const response = await fetch(`${API_URL}${endpoint}`, {
+ ...options,
+ headers
+ });
+
+ if (response.status === 401) {
+ localStorage.removeItem('accessToken');
+ localStorage.removeItem('currentUser');
+ window.location.href = 'index.html';
+ throw new Error('Authentication expired');
+ }
+ return response;
+}
+
+async function loadReferenceData() {
+ try {
+ const drugResponse = await apiCall('/drugs');
+ if (drugResponse.ok) {
+ allDrugs = await drugResponse.json();
+ }
+ } catch (error) {
+ console.error('Failed to load drug reference data:', error);
+ }
+}
+
+async function loadActiveReport() {
+ const container = document.getElementById('reportsTableContainer');
+ const reportsSummary = document.getElementById('reportsSummary');
+ if (container) {
+ const loadingText = activeReportType === 'dispensing' ? 'Loading dispensing history...' : 'Loading audit trail...';
+ container.innerHTML = `${loadingText}
`;
+ }
+ if (reportsSummary) reportsSummary.textContent = '';
+
+ try {
+ if (activeReportType === 'dispensing') {
+ const response = await apiCall('/dispense/history?limit=1000');
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.detail || 'Failed to load dispensing history');
+ }
+ dispensingRows = await response.json();
+ await ensureBatchLookupForDispensing(dispensingRows);
+ populateCommonFilters(dispensingRows);
+ } else {
+ const response = await apiCall('/reports/audit-trail');
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.detail || 'Failed to load audit trail report');
+ }
+ auditTrailRows = await response.json();
+ populateCommonFilters(auditTrailRows);
+ }
+
+ applyCurrentFilters();
+ } catch (error) {
+ console.error('Error loading report:', error);
+ if (container) {
+ container.innerHTML = `Failed to load report: ${escapeHtml(error.message)}
`;
+ }
+ showToast(`Failed to load report: ${error.message}`, 'error');
+ }
+}
+
+function showReportsPage() {
+ document.getElementById('reportsApp').style.display = 'block';
+ document.getElementById('reportsErrorState').style.display = 'none';
+
+ const userDisplay = document.getElementById('reportsCurrentUser');
+ if (userDisplay && currentUser) {
+ const roleLabel = currentUser.role.charAt(0).toUpperCase() + currentUser.role.slice(1);
+ userDisplay.textContent = `👤 ${currentUser.username} [${roleLabel}]`;
+ }
+}
+
+function showErrorState(message) {
+ const errorMessage = document.getElementById('reportsErrorMessage');
+ if (errorMessage) errorMessage.textContent = message;
+ document.getElementById('reportsApp').style.display = 'none';
+ document.getElementById('reportsErrorState').style.display = 'flex';
+}
+
+function setupEventListeners() {
+ const reportTypeSelect = document.getElementById('reportTypeSelect');
+ const applyBtn = document.getElementById('applyReportFiltersBtn');
+ const clearBtn = document.getElementById('clearReportFiltersBtn');
+ const refreshBtn = document.getElementById('refreshReportsBtn');
+ const backBtn = document.getElementById('backToInventoryBtn');
+ const logoutBtn = document.getElementById('reportsLogoutBtn');
+ const goToLoginBtn = document.getElementById('goToLoginBtn');
+
+ const userFilter = document.getElementById('reportUserFilter');
+ const drugFilter = document.getElementById('reportDrugFilter');
+ const fromDate = document.getElementById('reportFromDate');
+ const toDate = document.getElementById('reportToDate');
+ const searchInput = document.getElementById('reportActionSearch');
+
+ if (reportTypeSelect) {
+ reportTypeSelect.addEventListener('change', async (e) => {
+ activeReportType = e.target.value;
+ updateReportHeading();
+ await loadActiveReport();
+ });
+ }
+
+ if (applyBtn) applyBtn.addEventListener('click', applyCurrentFilters);
+ if (refreshBtn) refreshBtn.addEventListener('click', loadActiveReport);
+
+ if (clearBtn) {
+ clearBtn.addEventListener('click', () => {
+ if (userFilter) userFilter.value = '';
+ if (drugFilter) drugFilter.value = '';
+ if (fromDate) fromDate.value = '';
+ if (toDate) toDate.value = '';
+ if (searchInput) searchInput.value = '';
+ applyCurrentFilters();
+ });
+ }
+
+ if (userFilter) userFilter.addEventListener('change', applyCurrentFilters);
+ if (drugFilter) drugFilter.addEventListener('change', applyCurrentFilters);
+ if (fromDate) fromDate.addEventListener('change', applyCurrentFilters);
+ if (toDate) toDate.addEventListener('change', applyCurrentFilters);
+ if (searchInput) {
+ let timeout;
+ searchInput.addEventListener('input', () => {
+ clearTimeout(timeout);
+ timeout = setTimeout(applyCurrentFilters, 120);
+ });
+ }
+
+ if (backBtn) backBtn.addEventListener('click', () => {
+ window.location.href = 'index.html';
+ });
+
+ if (logoutBtn) logoutBtn.addEventListener('click', () => {
+ localStorage.removeItem('accessToken');
+ localStorage.removeItem('currentUser');
+ window.location.href = 'index.html';
+ });
+
+ if (goToLoginBtn) goToLoginBtn.addEventListener('click', () => {
+ window.location.href = 'index.html';
+ });
+}
+
+async function initializeReportsPage() {
+ const token = localStorage.getItem('accessToken');
+ const userData = localStorage.getItem('currentUser');
+
+ if (!token || !userData) {
+ showErrorState('You are not logged in. Please sign in first.');
+ return;
+ }
+
+ accessToken = token;
+
+ try {
+ currentUser = JSON.parse(userData);
+ } catch {
+ showErrorState('Invalid session data. Please sign in again.');
+ return;
+ }
+
+ if (!currentUser.role && currentUser.is_admin !== undefined) {
+ currentUser.role = currentUser.is_admin ? 'admin' : 'user';
+ }
+
+ if (currentUser.role !== 'admin') {
+ showErrorState('Only admin users can access reports.');
+ return;
+ }
+
+ setupEventListeners();
+ showReportsPage();
+ updateReportHeading();
+
+ await loadReferenceData();
+ await loadActiveReport();
+}
+
+document.addEventListener('DOMContentLoaded', initializeReportsPage);
diff --git a/frontend/styles.css b/frontend/styles.css
index 5339422..fca6877 100644
--- a/frontend/styles.css
+++ b/frontend/styles.css
@@ -785,6 +785,98 @@ footer {
color: var(--text-dark);
}
+.reports-modal-content {
+ max-width: 1000px;
+}
+
+.reports-controls {
+ display: flex;
+ align-items: end;
+ gap: 12px;
+ margin: 16px 0 10px;
+ flex-wrap: wrap;
+}
+
+.report-control {
+ flex: 1;
+ min-width: 220px;
+}
+
+.report-control label {
+ display: block;
+ font-weight: 600;
+ margin-bottom: 6px;
+}
+
+.report-control select {
+ width: 100%;
+ padding: 8px 10px;
+ border: 1px solid var(--border-color);
+ border-radius: 6px;
+}
+
+.report-control input {
+ width: 100%;
+ padding: 8px 10px;
+ border: 1px solid var(--border-color);
+ border-radius: 6px;
+}
+
+.report-control input:focus,
+.report-control select:focus {
+ outline: none;
+ border-color: var(--secondary-color);
+ box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.12);
+}
+
+.report-actions {
+ display: flex;
+ align-items: center;
+}
+
+.reports-summary {
+ font-size: 0.95em;
+ color: var(--text-light);
+ margin-bottom: 10px;
+}
+
+.reports-table-container {
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ max-height: 52vh;
+ overflow: auto;
+ background: var(--white);
+}
+
+.reports-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 0.92em;
+}
+
+.reports-table th,
+.reports-table td {
+ border-bottom: 1px solid #e9ecef;
+ padding: 8px 10px;
+ text-align: left;
+ vertical-align: top;
+}
+
+.reports-table th {
+ position: sticky;
+ top: 0;
+ background: #f8fafc;
+ z-index: 1;
+ font-weight: 700;
+}
+
+.reports-table code {
+ white-space: pre-wrap;
+ word-break: break-word;
+ font-size: 0.85em;
+ color: #1f2937;
+}
+
/* Responsive Design */
@media (max-width: 768px) {
main {
@@ -834,6 +926,15 @@ footer {
flex: 1;
}
+ .reports-controls {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .report-control {
+ min-width: 0;
+ }
+
.drug-details {
grid-template-columns: 1fr;
}