first upload

This commit is contained in:
2026-01-16 12:48:44 -05:00
parent b7a13e9c39
commit f83d672d28
12 changed files with 1941 additions and 0 deletions

567
frontend/app.js Normal file
View File

@@ -0,0 +1,567 @@
const API_URL = '/api';
let allDrugs = [];
let currentDrug = null;
let showLowStockOnly = false;
let searchTerm = '';
let expandedDrugs = new Set();
// Initialize
document.addEventListener('DOMContentLoaded', () => {
const drugForm = document.getElementById('drugForm');
const variantForm = document.getElementById('variantForm');
const editVariantForm = document.getElementById('editVariantForm');
const dispenseForm = document.getElementById('dispenseForm');
const editForm = document.getElementById('editForm');
const addModal = document.getElementById('addModal');
const addVariantModal = document.getElementById('addVariantModal');
const editVariantModal = document.getElementById('editVariantModal');
const dispenseModal = document.getElementById('dispenseModal');
const editModal = document.getElementById('editModal');
const addDrugBtn = document.getElementById('addDrugBtn');
const cancelAddBtn = document.getElementById('cancelAddBtn');
const cancelVariantBtn = document.getElementById('cancelVariantBtn');
const cancelEditVariantBtn = document.getElementById('cancelEditVariantBtn');
const cancelDispenseBtn = document.getElementById('cancelDispenseBtn');
const cancelEditBtn = document.getElementById('cancelEditBtn');
const showAllBtn = document.getElementById('showAllBtn');
const showLowStockBtn = document.getElementById('showLowStockBtn');
// Modal close buttons
const closeButtons = document.querySelectorAll('.close');
drugForm.addEventListener('submit', handleAddDrug);
variantForm.addEventListener('submit', handleAddVariant);
editVariantForm.addEventListener('submit', handleEditVariant);
dispenseForm.addEventListener('submit', handleDispenseDrug);
editForm.addEventListener('submit', handleEditDrug);
addDrugBtn.addEventListener('click', () => openModal(addModal));
cancelAddBtn.addEventListener('click', () => closeModal(addModal));
cancelVariantBtn.addEventListener('click', () => closeModal(addVariantModal));
cancelEditVariantBtn.addEventListener('click', () => closeModal(editVariantModal));
cancelDispenseBtn.addEventListener('click', () => closeModal(dispenseModal));
cancelEditBtn.addEventListener('click', closeEditModal);
const closeHistoryBtn = document.getElementById('closeHistoryBtn');
closeHistoryBtn.addEventListener('click', () => closeModal(document.getElementById('historyModal')));
closeButtons.forEach(btn => btn.addEventListener('click', (e) => {
const modal = e.target.closest('.modal');
closeModal(modal);
}));
showAllBtn.addEventListener('click', () => {
showLowStockOnly = false;
updateFilterButtons();
renderDrugs();
});
showLowStockBtn.addEventListener('click', () => {
showLowStockOnly = true;
updateFilterButtons();
renderDrugs();
});
// Search functionality
const drugSearch = document.getElementById('drugSearch');
if (drugSearch) {
let searchTimeout;
drugSearch.addEventListener('input', (e) => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
searchTerm = e.target.value.toLowerCase().trim();
renderDrugs();
}, 150);
});
}
// Close modal when clicking outside
window.addEventListener('click', (e) => {
if (e.target.classList.contains('modal')) {
closeModal(e.target);
}
});
loadDrugs();
});
// Load drugs from API
async function loadDrugs() {
try {
const response = await fetch(`${API_URL}/drugs`);
if (!response.ok) throw new Error('Failed to load drugs');
allDrugs = await response.json();
renderDrugs();
updateDispenseDrugSelect();
} catch (error) {
console.error('Error loading drugs:', error);
document.getElementById('drugsList').innerHTML =
'<p class="empty">Error loading drugs. Make sure the backend is running on http://localhost:8000</p>';
}
}
// Modal utility functions
function openModal(modal) {
modal.classList.add('show');
document.body.style.overflow = 'hidden';
}
function closeModal(modal) {
modal.classList.remove('show');
document.body.style.overflow = 'auto';
}
function closeEditModal() {
closeModal(document.getElementById('editModal'));
}
function updateDispenseDrugSelect() {
const select = document.getElementById('dispenseDrugSelect');
select.innerHTML = '<option value="">-- Select a drug variant --</option>';
allDrugs.forEach(drug => {
drug.variants.forEach(variant => {
const option = document.createElement('option');
option.value = variant.id;
option.textContent = `${drug.name} ${variant.strength} (${variant.quantity} ${variant.unit})`;
select.appendChild(option);
});
});
}
// Render drugs list
function renderDrugs() {
const drugsList = document.getElementById('drugsList');
let drugsToShow = allDrugs;
// Apply search filter
if (searchTerm) {
drugsToShow = drugsToShow.filter(drug =>
drug.name.toLowerCase().includes(searchTerm) ||
(drug.description && drug.description.toLowerCase().includes(searchTerm)) ||
drug.variants.some(variant => variant.strength.toLowerCase().includes(searchTerm))
);
}
// Apply stock filter
if (showLowStockOnly) {
drugsToShow = drugsToShow.filter(drug =>
drug.variants.some(variant => variant.quantity <= variant.low_stock_threshold)
);
}
if (drugsToShow.length === 0) {
drugsList.innerHTML = '<p class="empty">No drugs found matching your criteria</p>';
return;
}
drugsList.innerHTML = drugsToShow.map(drug => {
const totalVariants = drug.variants.length;
const lowStockVariants = drug.variants.filter(v => v.quantity <= v.low_stock_threshold).length;
const totalQuantity = drug.variants.reduce((sum, v) => sum + v.quantity, 0);
const isLowStock = lowStockVariants > 0;
const isExpanded = expandedDrugs.has(drug.id);
const variantsHtml = isExpanded ? `
${drug.variants.map(variant => {
const variantIsLowStock = variant.quantity <= variant.low_stock_threshold;
return `
<div class="variant-item ${variantIsLowStock ? 'low-stock' : ''}">
<div class="variant-info">
<div class="variant-details">
<div class="variant-name">${escapeHtml(drug.name)} ${escapeHtml(variant.strength)}</div>
<div class="variant-quantity">${variant.quantity} ${escapeHtml(variant.unit)}</div>
</div>
<div class="variant-status">
<span class="variant-badge ${variantIsLowStock ? 'badge-low' : 'badge-normal'}">
${variantIsLowStock ? 'Low Stock' : 'OK'}
</span>
</div>
</div>
<div class="variant-actions">
<button class="btn btn-success btn-small" onclick="dispenseVariant(${variant.id})">💊 Dispense</button>
<button class="btn btn-warning btn-small" onclick="openEditVariantModal(${variant.id})">Edit</button>
<button class="btn btn-danger btn-small" onclick="deleteVariant(${variant.id})">Delete</button>
</div>
</div>
`;
}).join('')}` : '';
return `
<div class="drug-item ${isLowStock ? 'low-stock' : ''} ${isExpanded ? 'expanded' : ''}" onclick="toggleDrugExpansion(${drug.id})">
<div class="drug-info">
<div class="drug-name">${escapeHtml(drug.name)}</div>
<div class="drug-description">${drug.description ? escapeHtml(drug.description) : 'No description'}</div>
<div class="drug-quantity">${totalVariants} variant${totalVariants !== 1 ? 's' : ''} (${totalQuantity} total units)</div>
<div class="drug-status">
<span class="drug-badge ${isLowStock ? 'badge-low' : 'badge-normal'}">
${isLowStock ? `${lowStockVariants} low` : 'All OK'}
</span>
</div>
</div>
<div class="drug-actions">
<button class="btn btn-primary btn-small" onclick="event.stopPropagation(); openAddVariantModal(${drug.id})"> Add</button>
<button class="btn btn-info btn-small" onclick="event.stopPropagation(); showDrugHistory(${drug.id})">📋 History</button>
<button class="btn btn-warning btn-small" onclick="event.stopPropagation(); openEditModal(${drug.id})">Edit Drug</button>
<button class="btn btn-danger btn-small" onclick="event.stopPropagation(); deleteDrug(${drug.id})">Delete</button>
<span class="expand-icon">${isExpanded ? '▼' : '▶'}</span>
</div>
</div>
<div class="drug-variants ${isExpanded ? 'expanded' : ''}" id="variants-${drug.id}">
${variantsHtml}
</div>
`;
}).join('');
}
// Handle add drug form
async function handleAddDrug(e) {
e.preventDefault();
const drugData = {
name: document.getElementById('drugName').value,
description: document.getElementById('drugDescription').value
};
try {
const response = await fetch(`${API_URL}/drugs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(drugData)
});
if (!response.ok) throw new Error('Failed to add drug');
document.getElementById('drugForm').reset();
closeModal(document.getElementById('addModal'));
await loadDrugs();
alert('Drug added successfully! Now add variants for this drug.');
} catch (error) {
console.error('Error adding drug:', error);
alert('Failed to add drug. Check the console for details.');
}
}
// Handle dispense drug form
async function handleDispenseDrug(e) {
e.preventDefault();
const variantId = parseInt(document.getElementById('dispenseDrugSelect').value);
const quantity = parseFloat(document.getElementById('dispenseQuantity').value);
const animalName = document.getElementById('dispenseAnimal').value;
const userName = document.getElementById('dispenseUser').value;
const notes = document.getElementById('dispenseNotes').value;
if (!variantId || !quantity || !animalName || !userName) {
alert('Please fill in all required fields');
return;
}
const dispensingData = {
drug_variant_id: variantId,
quantity: quantity,
animal_name: animalName,
user_name: userName,
notes: notes || null
};
try {
const response = await fetch(`${API_URL}/dispense`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(dispensingData)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to dispense drug');
}
document.getElementById('dispenseForm').reset();
closeModal(document.getElementById('dispenseModal'));
await loadDrugs();
alert('Drug dispensed successfully!');
} catch (error) {
console.error('Error dispensing drug:', error);
alert('Failed to dispense drug: ' + error.message);
}
}
// Open edit modal
function openEditModal(drugId) {
const drug = allDrugs.find(d => d.id === drugId);
if (!drug) return;
document.getElementById('editDrugId').value = drug.id;
document.getElementById('editDrugName').value = drug.name;
document.getElementById('editDrugDescription').value = drug.description || '';
document.getElementById('editModal').classList.add('show');
}
// Close edit modal
function closeEditModal() {
document.getElementById('editModal').classList.remove('show');
document.getElementById('editForm').reset();
}
// Show variants for a drug
function toggleDrugExpansion(drugId) {
if (expandedDrugs.has(drugId)) {
expandedDrugs.delete(drugId);
} else {
expandedDrugs.add(drugId);
}
renderDrugs();
}
// Open add variant modal
function openAddVariantModal(drugId) {
const drug = allDrugs.find(d => d.id === drugId);
if (!drug) return;
currentDrug = drug;
document.getElementById('variantDrugId').value = drug.id;
document.getElementById('addVariantModal').classList.add('show');
}
// Handle add variant form
async function handleAddVariant(e) {
e.preventDefault();
const drugId = parseInt(document.getElementById('variantDrugId').value);
const variantData = {
strength: document.getElementById('variantStrength').value,
quantity: parseFloat(document.getElementById('variantQuantity').value),
unit: document.getElementById('variantUnit').value,
low_stock_threshold: parseFloat(document.getElementById('variantThreshold').value)
};
try {
const response = await fetch(`${API_URL}/drugs/${drugId}/variants`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(variantData)
});
if (!response.ok) throw new Error('Failed to add variant');
document.getElementById('variantForm').reset();
closeModal(document.getElementById('addVariantModal'));
await loadDrugs();
renderDrugs();
alert('Variant added successfully!');
} catch (error) {
console.error('Error adding variant:', error);
alert('Failed to add variant. Check the console for details.');
}
}
// Open edit variant modal
function openEditVariantModal(variantId) {
const variant = currentDrug.variants.find(v => v.id === variantId);
if (!variant) return;
document.getElementById('editVariantId').value = variant.id;
document.getElementById('editVariantStrength').value = variant.strength;
document.getElementById('editVariantQuantity').value = variant.quantity;
document.getElementById('editVariantUnit').value = variant.unit;
document.getElementById('editVariantThreshold').value = variant.low_stock_threshold;
document.getElementById('editVariantModal').classList.add('show');
}
// Handle edit variant form
async function handleEditVariant(e) {
e.preventDefault();
const variantId = parseInt(document.getElementById('editVariantId').value);
const variantData = {
strength: document.getElementById('editVariantStrength').value,
quantity: parseFloat(document.getElementById('editVariantQuantity').value),
unit: document.getElementById('editVariantUnit').value,
low_stock_threshold: parseFloat(document.getElementById('editVariantThreshold').value)
};
try {
const response = await fetch(`${API_URL}/variants/${variantId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(variantData)
});
if (!response.ok) throw new Error('Failed to update variant');
closeModal(document.getElementById('editVariantModal'));
await loadDrugs();
renderDrugs();
alert('Variant updated successfully!');
} catch (error) {
console.error('Error updating variant:', error);
alert('Failed to update variant. Check the console for details.');
}
}
// Dispense from variant
function dispenseVariant(variantId) {
// Update the dropdown display with all variants
updateDispenseDrugSelect();
// Pre-select the variant in the dispense modal
const drugSelect = document.getElementById('dispenseDrugSelect');
drugSelect.value = variantId;
// Open dispense modal
openModal(document.getElementById('dispenseModal'));
}
// Delete variant
async function deleteVariant(variantId) {
if (!confirm('Are you sure you want to delete this variant?')) return;
try {
const response = await fetch(`${API_URL}/variants/${variantId}`, {
method: 'DELETE'
});
if (!response.ok) throw new Error('Failed to delete variant');
await loadDrugs();
renderDrugs();
alert('Variant deleted successfully!');
} catch (error) {
console.error('Error deleting variant:', error);
alert('Failed to delete variant. Check the console for details.');
}
}
// Show dispensing history for a drug
async function showDrugHistory(drugId) {
const drug = allDrugs.find(d => d.id === drugId);
if (!drug) return;
const historyModal = document.getElementById('historyModal');
const historyContent = document.getElementById('historyContent');
document.getElementById('historyDrugName').textContent = drug.name;
historyContent.innerHTML = '<p class="loading">Loading history...</p>';
openModal(historyModal);
try {
const response = await fetch(`${API_URL}/dispense/history`);
if (!response.ok) throw new Error('Failed to fetch history');
const allHistory = await response.json();
// Filter history for this drug's variants
const variantIds = drug.variants.map(v => v.id);
const drugHistory = allHistory.filter(item => variantIds.includes(item.drug_variant_id));
if (drugHistory.length === 0) {
historyContent.innerHTML = '<p class="empty">No dispensing history for this drug.</p>';
return;
}
// Sort by dispensed_at descending (most recent first)
drugHistory.sort((a, b) => new Date(b.dispensed_at) - new Date(a.dispensed_at));
const historyHtml = drugHistory.map(item => {
const variant = drug.variants.find(v => v.id === item.drug_variant_id);
const date = new Date(item.dispensed_at).toLocaleDateString();
const time = new Date(item.dispensed_at).toLocaleTimeString();
return `
<div class="history-item">
<div class="history-header">
<div class="history-variant">${drug.name} ${variant.strength}</div>
<div class="history-datetime">${date} ${time}</div>
</div>
<div class="history-details">
<div class="history-row">
<span class="history-label">Quantity:</span>
<span class="history-value">${item.quantity} ${variant.unit}</span>
</div>
<div class="history-row">
<span class="history-label">Animal:</span>
<span class="history-value">${escapeHtml(item.animal_name)}</span>
</div>
<div class="history-row">
<span class="history-label">User:</span>
<span class="history-value">${escapeHtml(item.user_name)}</span>
</div>
${item.notes ? `
<div class="history-row">
<span class="history-label">Notes:</span>
<span class="history-value">${escapeHtml(item.notes)}</span>
</div>
` : ''}
</div>
</div>
`;
}).join('');
historyContent.innerHTML = historyHtml;
} catch (error) {
console.error('Error fetching history:', error);
historyContent.innerHTML = '<p class="error">Failed to load history. Check the console for details.</p>';
}
}
// Handle edit drug form
async function handleEditDrug(e) {
e.preventDefault();
const drugId = parseInt(document.getElementById('editDrugId').value);
const drugData = {
name: document.getElementById('editDrugName').value,
description: document.getElementById('editDrugDescription').value
};
try {
const response = await fetch(`${API_URL}/drugs/${drugId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(drugData)
});
if (!response.ok) throw new Error('Failed to update drug');
closeEditModal();
await loadDrugs();
alert('Drug updated successfully!');
} catch (error) {
console.error('Error updating drug:', error);
alert('Failed to update drug. Check the console for details.');
}
}
// Delete drug
async function deleteDrug(drugId) {
if (!confirm('Are you sure you want to delete this drug?')) return;
try {
const response = await fetch(`${API_URL}/drugs/${drugId}`, {
method: 'DELETE'
});
if (!response.ok) throw new Error('Failed to delete drug');
await loadDrugs();
alert('Drug deleted successfully!');
} catch (error) {
console.error('Error deleting drug:', error);
alert('Failed to delete drug. Check the console for details.');
}
}
// Update filter button states
function updateFilterButtons() {
document.getElementById('showAllBtn').classList.toggle('active', !showLowStockOnly);
document.getElementById('showLowStockBtn').classList.toggle('active', showLowStockOnly);
}
// Escape HTML to prevent XSS
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}

239
frontend/index.html Normal file
View File

@@ -0,0 +1,239 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Drug Inventory System</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<header>
<h1>🐶 MTAR Drug Inventory System 🐶</h1>
</header>
<main>
<!-- Drug List Section -->
<section id="listSection" class="list-section">
<div class="section-header">
<h2>Current Inventory</h2>
<div class="header-actions">
<button id="addDrugBtn" class="btn btn-primary btn-small"> Add Drug</button>
</div>
<div class="filters">
<button id="showAllBtn" class="filter-btn active">All</button>
<button id="showLowStockBtn" class="filter-btn">Low Stock Only</button>
</div>
</div>
<!-- Search Section -->
<div class="search-section">
<input type="text" id="drugSearch" placeholder="🔍 Search drugs by name..." class="search-input">
</div>
<div id="drugsList" class="drugs-list">
<p class="loading">Loading drugs...</p>
</div>
</section>
</main>
<footer>
<p>Many Tears Confidential</p>
</footer>
</div>
<!-- Edit Drug Modal -->
<div id="editModal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h2>Edit Drug</h2>
<form id="editForm">
<input type="hidden" id="editDrugId">
<div class="form-group">
<label for="editDrugName">Drug Name *</label>
<input type="text" id="editDrugName" required>
</div>
<div class="form-group">
<label for="editDrugDescription">Description</label>
<input type="text" id="editDrugDescription">
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Save Changes</button>
<button type="button" class="btn btn-secondary" id="cancelEditBtn">Cancel</button>
</div>
</form>
</div>
</div>
<!-- Add Drug Modal -->
<div id="addModal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h2>Add New Drug</h2>
<form id="drugForm">
<div class="form-group">
<label for="drugName">Drug Name *</label>
<input type="text" id="drugName" required>
</div>
<div class="form-group">
<label for="drugDescription">Description</label>
<input type="text" id="drugDescription">
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Add Drug</button>
<button type="button" class="btn btn-secondary" id="cancelAddBtn">Cancel</button>
</div>
</form>
</div>
</div>
<!-- Dispense Drug Modal -->
<div id="dispenseModal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h2>Dispense Drug</h2>
<form id="dispenseForm">
<div class="form-group">
<label for="dispenseDrugSelect">Drug Variant *</label>
<select id="dispenseDrugSelect" required>
<option value="">-- Select a drug variant --</option>
</select>
</div>
<div class="form-group">
<label for="dispenseQuantity">Quantity *</label>
<input type="number" id="dispenseQuantity" step="0.1" required>
</div>
<div class="form-group">
<label for="dispenseAnimal">Animal Name/ID *</label>
<input type="text" id="dispenseAnimal" required>
</div>
<div class="form-group">
<label for="dispenseUser">User Name *</label>
<input type="text" id="dispenseUser" required>
</div>
<div class="form-group">
<label for="dispenseNotes">Notes</label>
<input type="text" id="dispenseNotes" placeholder="Optional">
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Dispense</button>
<button type="button" class="btn btn-secondary" id="cancelDispenseBtn">Cancel</button>
</div>
</form>
</div>
</div>
<!-- Add Variant Modal -->
<div id="addVariantModal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h2>Add Variant</h2>
<form id="variantForm">
<input type="hidden" id="variantDrugId">
<div class="form-group">
<label for="variantStrength">Strength *</label>
<input type="text" id="variantStrength" placeholder="e.g., 5.4mg, 10.8mg, 100ml" required>
</div>
<div class="form-row">
<div class="form-group">
<label for="variantQuantity">Quantity *</label>
<input type="number" id="variantQuantity" step="0.1" required>
</div>
<div class="form-group">
<label for="variantUnit">Unit *</label>
<select id="variantUnit">
<option value="tablets">Tablets</option>
<option value="bottles">Bottles</option>
<option value="ml">ml</option>
<option value="vials">Vials</option>
<option value="units">Units</option>
<option value="packets">Packets</option>
</select>
</div>
</div>
<div class="form-group">
<label for="variantThreshold">Low Stock Threshold *</label>
<input type="number" id="variantThreshold" step="0.1" value="10" required>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Add Variant</button>
<button type="button" class="btn btn-secondary" id="cancelVariantBtn">Cancel</button>
</div>
</form>
</div>
</div>
<!-- Edit Variant Modal -->
<div id="editVariantModal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h2>Edit Variant</h2>
<form id="editVariantForm">
<input type="hidden" id="editVariantId">
<div class="form-group">
<label for="editVariantStrength">Strength *</label>
<input type="text" id="editVariantStrength" required>
</div>
<div class="form-row">
<div class="form-group">
<label for="editVariantQuantity">Quantity *</label>
<input type="number" id="editVariantQuantity" step="0.1" required>
</div>
<div class="form-group">
<label for="editVariantUnit">Unit *</label>
<select id="editVariantUnit">
<option value="tablets">Tablets</option>
<option value="bottles">Bottles</option>
<option value="ml">ml</option>
<option value="vials">Vials</option>
<option value="units">Units</option>
<option value="packets">Packets</option>
</select>
</div>
</div>
<div class="form-group">
<label for="editVariantThreshold">Low Stock Threshold *</label>
<input type="number" id="editVariantThreshold" step="0.1" required>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Save Changes</button>
<button type="button" class="btn btn-secondary" id="cancelEditVariantBtn">Cancel</button>
</div>
</form>
</div>
</div>
<!-- Dispensing History Modal -->
<div id="historyModal" class="modal">
<div class="modal-content modal-large">
<span class="close">&times;</span>
<h2>Dispensing History - <span id="historyDrugName"></span></h2>
<div id="historyContent" class="history-content">
<p class="loading">Loading history...</p>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" id="closeHistoryBtn">Close</button>
</div>
</div>
</div>
<script src="app.js"></script>
</body>
</html>

672
frontend/styles.css Normal file
View File

@@ -0,0 +1,672 @@
:root {
--primary-color: #2c3e50;
--secondary-color: #3498db;
--success-color: #27ae60;
--warning-color: #f39c12;
--danger-color: #e74c3c;
--light-bg: #ecf0f1;
--white: #ffffff;
--text-dark: #2c3e50;
--text-light: #7f8c8d;
--border-color: #bdc3c7;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background-color: var(--light-bg);
color: var(--text-dark);
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
header {
background: var(--primary-color);
color: var(--white);
padding: 40px 20px;
border-radius: 8px;
margin-bottom: 30px;
text-align: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
header h1 {
font-size: 2.5em;
margin-bottom: 10px;
}
.subtitle {
font-size: 1.1em;
opacity: 0.9;
}
main {
margin-bottom: 40px;
}
section {
background: var(--white);
padding: 25px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
h2 {
color: var(--primary-color);
margin-bottom: 20px;
font-size: 1.5em;
border-bottom: 2px solid var(--secondary-color);
padding-bottom: 10px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: var(--text-dark);
}
input[type="text"],
input[type="number"],
select {
width: 100%;
padding: 12px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 1em;
font-family: inherit;
transition: border-color 0.2s;
}
input[type="text"]:focus,
input[type="number"]:focus,
select:focus {
outline: none;
border-color: var(--secondary-color);
box-shadow: 0 0 5px rgba(52, 152, 219, 0.3);
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 4px;
font-size: 1em;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
text-align: center;
}
.btn-primary {
background-color: var(--secondary-color);
color: var(--white);
width: 100%;
}
.btn-primary:hover {
background-color: #2980b9;
box-shadow: 0 2px 8px rgba(52, 152, 219, 0.4);
}
.btn-secondary {
background-color: var(--text-light);
color: var(--white);
}
.btn-secondary:hover {
background-color: #5a686f;
}
.btn-success {
background-color: var(--success-color);
color: var(--white);
padding: 8px 16px;
font-size: 0.9em;
}
.btn-success:hover {
background-color: #229954;
}
.btn-warning {
background-color: var(--warning-color);
color: var(--white);
padding: 8px 16px;
font-size: 0.9em;
}
.btn-warning:hover {
background-color: #d68910;
}
.btn-danger {
background-color: var(--danger-color);
color: var(--white);
padding: 8px 16px;
font-size: 0.9em;
}
.btn-danger:hover {
background-color: #c0392b;
}
.btn-info {
background-color: #17a2b8;
color: var(--white);
padding: 8px 16px;
font-size: 0.9em;
}
.btn-info:hover {
background-color: #138496;
}
.section-header {
margin-bottom: 20px;
}
.section-header h2 {
margin-bottom: 15px;
}
.header-actions {
margin-bottom: 15px;
}
.filters {
display: flex;
gap: 10px;
}
.filter-btn {
padding: 8px 16px;
border: 1px solid var(--border-color);
background: var(--white);
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
color: var(--text-dark);
}
.filter-btn:hover {
border-color: var(--secondary-color);
color: var(--secondary-color);
}
.filter-btn.active {
background-color: var(--secondary-color);
color: var(--white);
border-color: var(--secondary-color);
}
.drugs-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.drug-item {
display: flex;
align-items: center;
padding: 12px 16px;
background: var(--white);
border: 1px solid var(--border-color);
border-radius: 6px;
transition: all 0.2s;
font-size: 0.95em;
cursor: pointer;
}
.drug-item:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transform: translateY(-1px);
}
.drug-item.low-stock {
border-left: 4px solid var(--warning-color);
background: #fffdf0;
}
.drug-info {
flex: 1;
display: flex;
align-items: center;
gap: 16px;
}
.drug-name {
font-weight: 600;
color: var(--primary-color);
min-width: 200px;
}
.drug-description {
color: var(--text-light);
font-size: 0.9em;
flex: 1;
}
.drug-quantity {
font-weight: 600;
color: var(--primary-color);
min-width: 100px;
text-align: center;
}
.drug-unit {
color: var(--text-light);
min-width: 80px;
}
.drug-status {
min-width: 100px;
text-align: center;
}
.drug-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.8em;
font-weight: 600;
}
.badge-normal {
background-color: #d5f4e6;
color: var(--success-color);
}
.badge-low {
background-color: #fdeef0;
color: var(--danger-color);
}
.drug-actions {
display: flex;
gap: 8px;
margin-left: 16px;
align-items: center;
}
.expand-icon {
font-size: 1.2em;
color: var(--text-light);
cursor: pointer;
margin-left: 8px;
transition: transform 0.2s;
}
.drug-item.expanded .expand-icon {
transform: rotate(90deg);
}
.drug-variants {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-in-out;
margin-top: 12px;
padding: 0 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.drug-variants.expanded {
max-height: 1000px; /* Adjust based on content */
}
.btn-small {
padding: 6px 12px;
font-size: 0.85em;
border-radius: 4px;
}
.loading {
text-align: center;
color: var(--text-light);
padding: 40px;
font-style: italic;
}
.empty {
text-align: center;
color: var(--text-light);
padding: 40px;
}
footer {
background: var(--primary-color);
color: var(--white);
text-align: center;
padding: 20px;
border-radius: 8px;
}
/* Actions Section */
.actions-section {
margin-bottom: 30px;
}
.action-buttons {
display: flex;
gap: 15px;
justify-content: center;
margin-bottom: 20px;
}
.action-buttons .btn {
padding: 12px 24px;
font-size: 1em;
border-radius: 6px;
}
/* Search Section */
.search-section {
margin-bottom: 15px;
padding: 0 20px;
}
.search-input {
width: 100%;
max-width: none;
padding: 12px 16px;
border: 2px solid var(--border-color);
border-radius: 8px;
font-size: 1em;
transition: border-color 0.2s;
}
.search-input:focus {
outline: none;
border-color: var(--secondary-color);
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
}
/* List Section */
.list-section {
background: var(--white);
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
/* Modal Styles */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
animation: fadeIn 0.2s;
}
.modal.show {
display: flex;
justify-content: center;
align-items: center;
}
.modal-content {
background-color: var(--white);
padding: 30px;
border-radius: 8px;
width: 90%;
max-width: 500px;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
animation: slideIn 0.3s;
}
.modal-content.modal-large {
max-width: 700px;
max-height: 80vh;
overflow-y: auto;
}
.close {
color: var(--text-light);
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
margin-top: -10px;
}
.close:hover {
color: var(--text-dark);
}
.form-actions {
display: flex;
gap: 10px;
margin-top: 20px;
}
.form-actions .btn {
flex: 1;
}
/* History Styles */
.history-content {
display: flex;
flex-direction: column;
gap: 15px;
margin: 20px 0;
}
.history-item {
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 12px;
background: #f8f9fa;
}
.history-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
border-bottom: 1px solid var(--border-color);
padding-bottom: 8px;
}
.history-variant {
font-weight: 600;
color: var(--primary-color);
}
.history-datetime {
font-size: 0.9em;
color: var(--text-light);
}
.history-details {
display: flex;
flex-direction: column;
gap: 6px;
}
.history-row {
display: flex;
gap: 10px;
font-size: 0.95em;
}
.history-label {
font-weight: 600;
color: var(--text-dark);
min-width: 70px;
}
.history-value {
color: var(--text-dark);
}
/* Responsive Design */
@media (max-width: 768px) {
main {
grid-template-columns: 1fr;
}
header h1 {
font-size: 1.8em;
}
.section-header {
flex-direction: column;
align-items: flex-start;
gap: 15px;
}
.filters {
width: 100%;
}
.filter-btn {
flex: 1;
}
.drug-details {
grid-template-columns: 1fr;
}
.drug-actions {
flex-wrap: wrap;
}
.modal-content {
width: 95%;
}
}
/* Variants Section */
.variants-list {
display: grid;
gap: 15px;
}
.variant-item {
background: var(--white);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 12px 16px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: transform 0.2s, box-shadow 0.2s;
}
.variant-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.variant-item.low-stock {
border-left: 4px solid var(--danger-color);
}
.variant-info {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 15px;
}
.variant-details {
flex: 1;
}
.variant-name {
font-size: 1.2em;
font-weight: bold;
color: var(--primary-color);
margin-bottom: 5px;
}
.variant-strength {
color: var(--text-light);
margin-bottom: 5px;
}
.variant-quantity {
font-size: 1.1em;
font-weight: 500;
color: var(--primary-color);
}
.variant-unit {
color: var(--text-light);
}
.variant-status {
text-align: right;
}
.variant-badge {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.9em;
font-weight: bold;
}
.badge-normal {
background-color: var(--success-color);
color: var(--white);
}
.badge-low {
background-color: var(--danger-color);
color: var(--white);
}
.variant-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.btn-small {
padding: 6px 12px;
font-size: 0.9em;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideIn {
from {
transform: translateY(-50px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}