add pharmacy

This commit is contained in:
Flatlogic Bot 2026-03-21 07:52:14 +00:00
parent 01f56287c6
commit 641316f659
9 changed files with 1195 additions and 5 deletions

30
api/patients.php Normal file
View File

@ -0,0 +1,30 @@
<?php
require_once __DIR__ . '/../includes/common_data.php';
header('Content-Type: application/json');
$action = $_GET['action'] ?? '';
$pdo = db();
try {
switch ($action) {
case 'search':
$q = $_GET['q'] ?? '';
if (strlen($q) < 1) {
echo json_encode([]);
exit;
}
$stmt = $pdo->prepare("SELECT id, name, phone FROM patients WHERE name LIKE ? OR phone LIKE ? LIMIT 20");
$term = "%$q%";
$stmt->execute([$term, $term]);
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode($results);
break;
default:
http_response_code(400);
echo json_encode(['error' => 'Invalid action']);
}
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}

172
api/pharmacy.php Normal file
View File

@ -0,0 +1,172 @@
<?php
require_once __DIR__ . '/../includes/common_data.php'; // Includes db/config.php
header('Content-Type: application/json');
$action = $_GET['action'] ?? '';
$pdo = db();
try {
switch ($action) {
case 'get_stock':
// List all drugs with total stock quantity
$sql = "SELECT d.id, d.name_en, d.name_ar, d.min_stock_level, d.reorder_level, d.unit,
COALESCE(SUM(b.quantity), 0) as total_stock
FROM drugs d
LEFT JOIN pharmacy_batches b ON d.id = b.drug_id AND b.quantity > 0 AND b.expiry_date >= CURDATE()
GROUP BY d.id
ORDER BY d.name_en ASC";
$stmt = $pdo->query($sql);
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
break;
case 'get_batches':
$drug_id = $_GET['drug_id'] ?? 0;
if (!$drug_id) throw new Exception("Drug ID required");
$sql = "SELECT b.*, s.name_en as supplier_name
FROM pharmacy_batches b
LEFT JOIN suppliers s ON b.supplier_id = s.id
WHERE b.drug_id = ? AND b.quantity > 0
ORDER BY b.expiry_date ASC";
$stmt = $pdo->prepare($sql);
$stmt->execute([$drug_id]);
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
break;
case 'add_stock':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Invalid method");
$drug_id = $_POST['drug_id'] ?? 0;
$batch_number = $_POST['batch_number'] ?? '';
$expiry_date = $_POST['expiry_date'] ?? '';
$quantity = $_POST['quantity'] ?? 0;
$cost_price = $_POST['cost_price'] ?? 0;
$sale_price = $_POST['sale_price'] ?? 0;
$supplier_id = !empty($_POST['supplier_id']) ? $_POST['supplier_id'] : null;
if (!$drug_id || !$batch_number || !$expiry_date || !$quantity) {
throw new Exception("Missing required fields");
}
$stmt = $pdo->prepare("INSERT INTO pharmacy_batches
(drug_id, batch_number, expiry_date, quantity, cost_price, sale_price, supplier_id, received_date)
VALUES (?, ?, ?, ?, ?, ?, ?, CURDATE())");
$stmt->execute([$drug_id, $batch_number, $expiry_date, $quantity, $cost_price, $sale_price, $supplier_id]);
echo json_encode(['success' => true, 'message' => 'Stock added successfully']);
break;
case 'search_drugs':
$q = $_GET['q'] ?? '';
$sql = "SELECT d.id, d.name_en, d.name_ar, d.price as default_price,
COALESCE(SUM(b.quantity), 0) as stock
FROM drugs d
LEFT JOIN pharmacy_batches b ON d.id = b.drug_id AND b.quantity > 0 AND b.expiry_date >= CURDATE()
WHERE d.name_en LIKE ? OR d.name_ar LIKE ?
GROUP BY d.id
LIMIT 20";
$stmt = $pdo->prepare($sql);
$term = "%$q%";
$stmt->execute([$term, $term]);
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
break;
case 'create_sale':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Invalid method");
$input = json_decode(file_get_contents('php://input'), true);
if (empty($input['items'])) throw new Exception("No items in sale");
$pdo->beginTransaction();
try {
// Create Sale Record
$stmt = $pdo->prepare("INSERT INTO pharmacy_sales (patient_id, visit_id, total_amount, payment_method, status) VALUES (?, ?, ?, ?, 'completed')");
$stmt->execute([
$input['patient_id'] ?? null,
$input['visit_id'] ?? null,
$input['total_amount'] ?? 0,
$input['payment_method'] ?? 'cash'
]);
$sale_id = $pdo->lastInsertId();
// Process Items
foreach ($input['items'] as $item) {
$drug_id = $item['drug_id'];
$qty_needed = $item['quantity'];
$unit_price = $item['price']; // Or fetch from batch? Use provided price for now.
// Fetch available batches (FIFO)
$batch_stmt = $pdo->prepare("SELECT id, quantity FROM pharmacy_batches WHERE drug_id = ? AND quantity > 0 ORDER BY expiry_date ASC FOR UPDATE");
$batch_stmt->execute([$drug_id]);
$batches = $batch_stmt->fetchAll(PDO::FETCH_ASSOC);
$qty_remaining = $qty_needed;
foreach ($batches as $batch) {
if ($qty_remaining <= 0) break;
$take = min($batch['quantity'], $qty_remaining);
// Deduct from batch
$update = $pdo->prepare("UPDATE pharmacy_batches SET quantity = quantity - ? WHERE id = ?");
$update->execute([$take, $batch['id']]);
// Add to sale items
$item_stmt = $pdo->prepare("INSERT INTO pharmacy_sale_items (sale_id, drug_id, batch_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?, ?)");
$item_stmt->execute([$sale_id, $drug_id, $batch['id'], $take, $unit_price, $take * $unit_price]);
$qty_remaining -= $take;
}
if ($qty_remaining > 0) {
throw new Exception("Insufficient stock for drug ID: $drug_id");
}
}
$pdo->commit();
echo json_encode(['success' => true, 'sale_id' => $sale_id]);
} catch (Exception $e) {
$pdo->rollBack();
throw $e;
}
break;
case 'get_sales':
// List recent sales
$sql = "SELECT s.*, p.name_en as patient_name
FROM pharmacy_sales s
LEFT JOIN patients p ON s.patient_id = p.id
ORDER BY s.created_at DESC LIMIT 50";
$stmt = $pdo->query($sql);
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
break;
case 'get_sale_details':
$sale_id = $_GET['sale_id'] ?? 0;
if (!$sale_id) throw new Exception("Sale ID required");
$stmt = $pdo->prepare("SELECT s.*, p.name_en as patient_name FROM pharmacy_sales s LEFT JOIN patients p ON s.patient_id = p.id WHERE s.id = ?");
$stmt->execute([$sale_id]);
$sale = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$sale) throw new Exception("Sale not found");
$items_stmt = $pdo->prepare("SELECT i.*, d.name_en as drug_name
FROM pharmacy_sale_items i
JOIN drugs d ON i.drug_id = d.id
WHERE i.sale_id = ?");
$items_stmt->execute([$sale_id]);
$sale['items'] = $items_stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode($sale);
break;
default:
throw new Exception("Invalid action");
}
} catch (Exception $e) {
http_response_code(400);
echo json_encode(['error' => $e->getMessage()]);
}

24
check_pharmacy_schema.php Normal file
View File

@ -0,0 +1,24 @@
<?php
require_once 'db/config.php';
$pdo = db();
$tables = ['drugs', 'suppliers', 'visit_prescriptions'];
foreach ($tables as $table) {
echo "--- Table: $table ---
";
try {
$stmt = $pdo->query("DESCRIBE $table");
$columns = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($columns as $col) {
echo "{$col['Field']} ({$col['Type']})
";
}
} catch (PDOException $e) {
echo "Error describing $table: " . $e->getMessage() . "
";
}
echo "
";
}

View File

@ -0,0 +1,49 @@
-- Create pharmacy_batches table
CREATE TABLE IF NOT EXISTS pharmacy_batches (
id INT AUTO_INCREMENT PRIMARY KEY,
drug_id INT NOT NULL,
batch_number VARCHAR(50) NOT NULL,
expiry_date DATE NOT NULL,
quantity INT NOT NULL DEFAULT 0,
cost_price DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
sale_price DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
supplier_id INT NULL,
received_date DATE NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (drug_id) REFERENCES drugs(id) ON DELETE CASCADE,
FOREIGN KEY (supplier_id) REFERENCES suppliers(id) ON DELETE SET NULL
);
-- Create pharmacy_sales table
CREATE TABLE IF NOT EXISTS pharmacy_sales (
id INT AUTO_INCREMENT PRIMARY KEY,
patient_id INT NULL,
visit_id INT NULL,
total_amount DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
payment_method VARCHAR(50) DEFAULT 'cash',
status VARCHAR(20) DEFAULT 'completed', -- completed, refunded, cancelled
notes TEXT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (patient_id) REFERENCES patients(id) ON DELETE SET NULL,
FOREIGN KEY (visit_id) REFERENCES visits(id) ON DELETE SET NULL
);
-- Create pharmacy_sale_items table
CREATE TABLE IF NOT EXISTS pharmacy_sale_items (
id INT AUTO_INCREMENT PRIMARY KEY,
sale_id INT NOT NULL,
drug_id INT NOT NULL,
batch_id INT NULL, -- Can be null if we track sales without specific batch selection (though we should enforce it for stock deduction)
quantity INT NOT NULL DEFAULT 1,
unit_price DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
total_price DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (sale_id) REFERENCES pharmacy_sales(id) ON DELETE CASCADE,
FOREIGN KEY (drug_id) REFERENCES drugs(id),
FOREIGN KEY (batch_id) REFERENCES pharmacy_batches(id) ON DELETE SET NULL
);
-- Add stock management columns to drugs table
ALTER TABLE drugs ADD COLUMN IF NOT EXISTS min_stock_level INT DEFAULT 10;
ALTER TABLE drugs ADD COLUMN IF NOT EXISTS reorder_level INT DEFAULT 20;
ALTER TABLE drugs ADD COLUMN IF NOT EXISTS unit VARCHAR(50) DEFAULT 'pack';

View File

@ -129,13 +129,18 @@ $site_favicon = !empty($site_settings['company_favicon']) ? $site_settings['comp
<a href="xray_inquiries.php" class="sidebar-link py-2 <?php echo $section === 'xray_inquiries' ? 'active' : ''; ?>"><i class="bi bi-question-circle me-2"></i> <?php echo __('inquiries'); ?></a>
</div>
</div>
<!-- Drugs Module -->
<a href="#drugsSubmenu" data-bs-toggle="collapse" class="sidebar-link <?php echo in_array($section, ['drugs', 'drugs_groups', 'suppliers']) ? 'active' : ''; ?> d-flex justify-content-between align-items-center">
<span><i class="bi bi-capsule me-2"></i> <?php echo __('drugs'); ?></span>
<!-- Pharmacy Module -->
<a href="#pharmacySubmenu" data-bs-toggle="collapse" class="sidebar-link <?php echo in_array($section, ['pharmacy_inventory', 'pharmacy_pos', 'pharmacy_sales', 'drugs', 'drugs_groups', 'suppliers']) ? 'active' : ''; ?> d-flex justify-content-between align-items-center">
<span><i class="bi bi-capsule me-2"></i> <?php echo __('pharmacy'); ?></span>
<i class="bi bi-chevron-down small"></i>
</a>
<div class="collapse <?php echo in_array($section, ['drugs', 'drugs_groups', 'suppliers']) ? 'show' : ''; ?>" id="drugsSubmenu">
<div class="collapse <?php echo in_array($section, ['pharmacy_inventory', 'pharmacy_pos', 'pharmacy_sales', 'drugs', 'drugs_groups', 'suppliers']) ? 'show' : ''; ?>" id="pharmacySubmenu">
<div class="sidebar-submenu">
<a href="pharmacy_inventory.php" class="sidebar-link py-2 <?php echo $section === 'pharmacy_inventory' ? 'active' : ''; ?>"><i class="bi bi-boxes me-2"></i> <?php echo __('inventory'); ?></a>
<a href="pharmacy_pos.php" class="sidebar-link py-2 <?php echo $section === 'pharmacy_pos' ? 'active' : ''; ?>"><i class="bi bi-cart-check me-2"></i> <?php echo __('pos'); ?></a>
<a href="pharmacy_sales.php" class="sidebar-link py-2 <?php echo $section === 'pharmacy_sales' ? 'active' : ''; ?>"><i class="bi bi-receipt me-2"></i> <?php echo __('sales_history'); ?></a>
<div class="border-top my-1 border-secondary" style="border-color: #0d4680 !important;"></div>
<a href="drugs.php" class="sidebar-link py-2 <?php echo $section === 'drugs' ? 'active' : ''; ?>"><i class="bi bi-list-check me-2"></i> <?php echo __('drugs'); ?></a>
<a href="drugs_groups.php" class="sidebar-link py-2 <?php echo $section === 'drugs_groups' ? 'active' : ''; ?>"><i class="bi bi-collection me-2"></i> <?php echo __('groups'); ?></a>
<a href="suppliers.php" class="sidebar-link py-2 <?php echo $section === 'suppliers' ? 'active' : ''; ?>"><i class="bi bi-truck me-2"></i> <?php echo __('suppliers'); ?></a>

View File

@ -0,0 +1,316 @@
<?php
$section = 'pharmacy_inventory';
require_once __DIR__ . '/../layout/header.php';
$page = $_GET['page'] ?? 1;
$search = $_GET['search'] ?? '';
// Fetch drugs with stock info
// The API 'get_stock' returns all, but for pagination we might want to do it in PHP or adjust API.
// For now, let's use the API approach via JS or just fetch in PHP.
// Since we are in PHP page, fetching directly is better for SEO/Speed than pure AJAX sometimes, but standard here seems mixed.
// I'll fetch via PHP for initial render.
$limit = 10;
$offset = ($page - 1) * $limit;
$where = "WHERE 1=1";
$params = [];
if ($search) {
$where .= " AND (d.name_en LIKE ? OR d.name_ar LIKE ?)";
$params[] = "%$search%";
$params[] = "%$search%";
}
// Count
$count_stmt = $db->prepare("SELECT COUNT(*) FROM drugs d $where");
$count_stmt->execute($params);
$total_rows = $count_stmt->fetchColumn();
$total_pages = ceil($total_rows / $limit);
// Fetch
$sql = "SELECT d.id, d.name_en, d.name_ar, d.min_stock_level, d.reorder_level, d.unit,
COALESCE(SUM(b.quantity), 0) as total_stock
FROM drugs d
LEFT JOIN pharmacy_batches b ON d.id = b.drug_id AND b.quantity > 0 AND b.expiry_date >= CURDATE()
$where
GROUP BY d.id
ORDER BY d.name_en ASC
LIMIT $limit OFFSET $offset";
$stmt = $db->prepare($sql);
$stmt->execute($params);
$drugs = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<div class="container-fluid">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold text-primary"><i class="bi bi-boxes me-2"></i> <?php echo __('inventory'); ?></h2>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addStockModal">
<i class="bi bi-plus-lg me-1"></i> <?php echo __('add_stock'); ?>
</button>
</div>
<div class="card shadow-sm">
<div class="card-body">
<form method="GET" class="row g-3 mb-4">
<div class="col-md-4">
<div class="input-group">
<span class="input-group-text bg-light border-end-0"><i class="bi bi-search text-muted"></i></span>
<input type="text" name="search" class="form-control border-start-0 ps-0" placeholder="<?php echo __('search'); ?>..." value="<?php echo htmlspecialchars($search); ?>">
</div>
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary w-100"><?php echo __('search'); ?></button>
</div>
</form>
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead>
<tr>
<th><?php echo __('drug_name'); ?></th>
<th><?php echo __('stock'); ?></th>
<th><?php echo __('unit'); ?></th>
<th><?php echo __('status'); ?></th>
<th><?php echo __('actions'); ?></th>
</tr>
</thead>
<tbody>
<?php if (empty($drugs)): ?>
<tr>
<td colspan="5" class="text-center py-4 text-muted">
<i class="bi bi-inbox fs-1 d-block mb-2"></i>
<?php echo __('no_data_found'); ?>
</td>
</tr>
<?php else: ?>
<?php foreach ($drugs as $drug): ?>
<?php
$status_class = 'bg-success';
$status_text = 'OK';
if ($drug['total_stock'] <= 0) {
$status_class = 'bg-danger';
$status_text = 'Out of Stock';
} elseif ($drug['total_stock'] <= $drug['min_stock_level']) {
$status_class = 'bg-warning text-dark';
$status_text = 'Low Stock';
}
?>
<tr>
<td>
<div class="fw-bold"><?php echo htmlspecialchars($drug['name_en']); ?></div>
<small class="text-muted"><?php echo htmlspecialchars($drug['name_ar']); ?></small>
</td>
<td>
<span class="fs-5 fw-bold"><?php echo $drug['total_stock']; ?></span>
</td>
<td><?php echo htmlspecialchars($drug['unit'] ?? '-'); ?></td>
<td>
<span class="badge <?php echo $status_class; ?> rounded-pill">
<?php echo $status_text; ?>
</span>
</td>
<td>
<button class="btn btn-sm btn-outline-info" onclick="viewBatches(<?php echo $drug['id']; ?>, '<?php echo addslashes($drug['name_en']); ?>')">
<i class="bi bi-eye"></i> <?php echo __('view_batches'); ?>
</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- Pagination -->
<?php if ($total_pages > 1): ?>
<nav aria-label="Page navigation" class="mt-4">
<ul class="pagination justify-content-center">
<li class="page-item <?php echo $page <= 1 ? 'disabled' : ''; ?>">
<a class="page-link" href="?page=<?php echo $page - 1; ?>&search=<?php echo urlencode($search); ?>"><?php echo __('previous'); ?></a>
</li>
<?php for ($i = 1; $i <= $total_pages; $i++): ?>
<li class="page-item <?php echo $page == $i ? 'active' : ''; ?>">
<a class="page-link" href="?page=<?php echo $i; ?>&search=<?php echo urlencode($search); ?>"><?php echo $i; ?></a>
</li>
<?php endfor; ?>
<li class="page-item <?php echo $page >= $total_pages ? 'disabled' : ''; ?>">
<a class="page-link" href="?page=<?php echo $page + 1; ?>&search=<?php echo urlencode($search); ?>"><?php echo __('next'); ?></a>
</li>
</ul>
</nav>
<?php endif; ?>
</div>
</div>
</div>
<!-- Add Stock Modal -->
<div class="modal fade" id="addStockModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><?php echo __('add_stock'); ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="addStockForm">
<div class="mb-3">
<label class="form-label"><?php echo __('select_drug'); ?></label>
<select class="form-select select2" name="drug_id" required style="width: 100%;">
<option value=""><?php echo __('select_drug'); ?></option>
<?php
$all_drugs = $db->query("SELECT id, name_en FROM drugs ORDER BY name_en ASC")->fetchAll(PDO::FETCH_ASSOC);
foreach ($all_drugs as $d) {
echo "<option value='{$d['id']}'>" . htmlspecialchars($d['name_en']) . "</option>";
}
?>
</select>
</div>
<div class="mb-3">
<label class="form-label"><?php echo __('batch_number'); ?></label>
<input type="text" class="form-control" name="batch_number" required>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label"><?php echo __('expiry_date'); ?></label>
<input type="date" class="form-control" name="expiry_date" required>
</div>
<div class="col-md-6 mb-3">
<label class="form-label"><?php echo __('quantity'); ?></label>
<input type="number" class="form-control" name="quantity" min="1" required>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label"><?php echo __('cost_price'); ?></label>
<input type="number" class="form-control" name="cost_price" step="0.01" value="0">
</div>
<div class="col-md-6 mb-3">
<label class="form-label"><?php echo __('sale_price'); ?></label>
<input type="number" class="form-control" name="sale_price" step="0.01" value="0">
</div>
</div>
<div class="mb-3">
<label class="form-label"><?php echo __('supplier'); ?></label>
<select class="form-select" name="supplier_id">
<option value=""><?php echo __('select_supplier'); ?></option>
<?php
$suppliers = $db->query("SELECT id, name_en FROM suppliers ORDER BY name_en ASC")->fetchAll(PDO::FETCH_ASSOC);
foreach ($suppliers as $s) {
echo "<option value='{$s['id']}'>" . htmlspecialchars($s['name_en']) . "</option>";
}
?>
</select>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?php echo __('cancel'); ?></button>
<button type="button" class="btn btn-primary" onclick="submitStock()"><?php echo __('save'); ?></button>
</div>
</div>
</div>
</div>
<!-- View Batches Modal -->
<div class="modal fade" id="viewBatchesModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="batchesModalTitle"><?php echo __('view_batches'); ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="table-responsive">
<table class="table table-sm table-striped">
<thead>
<tr>
<th><?php echo __('batch_number'); ?></th>
<th><?php echo __('expiry_date'); ?></th>
<th><?php echo __('quantity'); ?></th>
<th><?php echo __('cost_price'); ?></th>
<th><?php echo __('supplier'); ?></th>
</tr>
</thead>
<tbody id="batchesTableBody">
<!-- Populated via JS -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<script>
function submitStock() {
const form = document.getElementById('addStockForm');
if (!form.checkValidity()) {
form.reportValidity();
return;
}
const formData = new FormData(form);
fetch('api/pharmacy.php?action=add_stock', {
method: 'POST',
body: formData
})
.then(r => r.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert(data.error || 'Error adding stock');
}
})
.catch(e => {
console.error(e);
alert('Network error');
});
}
function viewBatches(drugId, drugName) {
document.getElementById('batchesModalTitle').textContent = drugName + ' - Batches';
const tbody = document.getElementById('batchesTableBody');
tbody.innerHTML = '<tr><td colspan="5" class="text-center">Loading...</td></tr>';
const modal = new bootstrap.Modal(document.getElementById('viewBatchesModal'));
modal.show();
fetch('api/pharmacy.php?action=get_batches&drug_id=' + drugId)
.then(r => r.json())
.then(data => {
tbody.innerHTML = '';
if (data.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="text-center">No active batches found</td></tr>';
return;
}
data.forEach(batch => {
tbody.innerHTML += `
<tr>
<td>${batch.batch_number}</td>
<td>${batch.expiry_date}</td>
<td>${batch.quantity}</td>
<td>${batch.cost_price}</td>
<td>${batch.supplier_name || '-'}</td>
</tr>
`;
});
})
.catch(e => {
tbody.innerHTML = '<tr><td colspan="5" class="text-center text-danger">Error loading data</td></tr>';
});
}
$(document).ready(function() {
$('.select2').select2({
theme: 'bootstrap-5',
dropdownParent: $('#addStockModal')
});
});
</script>
<?php require_once __DIR__ . '/../layout/footer.php'; ?>

View File

@ -0,0 +1,328 @@
<?php
$section = 'pharmacy_pos';
require_once __DIR__ . '/../layout/header.php';
?>
<div class="container-fluid h-100">
<div class="row h-100">
<!-- Left Panel: Product Search -->
<div class="col-md-7 d-flex flex-column h-100">
<div class="card shadow-sm flex-grow-1">
<div class="card-header bg-white py-3">
<div class="input-group input-group-lg">
<span class="input-group-text bg-light border-end-0"><i class="bi bi-search"></i></span>
<input type="text" id="drugSearch" class="form-control border-start-0" placeholder="<?php echo __('search_by_name'); ?>..." autocomplete="off">
</div>
</div>
<div class="card-body p-0 overflow-auto" style="max-height: calc(100vh - 200px);">
<div id="drugList" class="list-group list-group-flush">
<!-- Populated by JS -->
<div class="text-center py-5 text-muted">
<i class="bi bi-search fs-1"></i>
<p><?php echo __('search_to_add_items'); ?></p>
</div>
</div>
</div>
</div>
</div>
<!-- Right Panel: Cart -->
<div class="col-md-5 d-flex flex-column h-100">
<div class="card shadow-sm flex-grow-1 border-primary">
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="bi bi-cart3 me-2"></i> <?php echo __('cart'); ?></h5>
<span id="cartCount" class="badge bg-white text-primary rounded-pill">0</span>
</div>
<div class="card-body p-0 d-flex flex-column">
<div class="table-responsive flex-grow-1" style="max-height: calc(100vh - 350px); overflow-y: auto;">
<table class="table table-striped mb-0">
<thead class="sticky-top bg-light">
<tr>
<th><?php echo __('item'); ?></th>
<th width="80"><?php echo __('qty'); ?></th>
<th width="100" class="text-end"><?php echo __('total'); ?></th>
<th width="50"></th>
</tr>
</thead>
<tbody id="cartTableBody">
<!-- Cart Items -->
</tbody>
</table>
</div>
<div class="p-3 bg-light border-top">
<div class="d-flex justify-content-between mb-2">
<span class="fs-5"><?php echo __('total'); ?>:</span>
<span class="fs-4 fw-bold text-primary" id="cartTotal">0.00</span>
</div>
<button class="btn btn-success w-100 btn-lg" onclick="showCheckout()">
<i class="bi bi-credit-card me-2"></i> <?php echo __('checkout'); ?>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Checkout Modal -->
<div class="modal fade" id="checkoutModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><?php echo __('checkout'); ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="checkoutForm">
<div class="mb-3">
<label class="form-label"><?php echo __('patient'); ?> (<?php echo __('optional'); ?>)</label>
<select class="form-select select2-patient" id="patientSelect" style="width: 100%;">
<option value=""><?php echo __('select_patient'); ?></option>
<!-- Populated via AJAX or PHP if small list -->
</select>
</div>
<!-- Visit ID (Optional linkage) -->
<input type="hidden" id="visitSelect" value="">
<div class="mb-3">
<label class="form-label"><?php echo __('payment_method'); ?></label>
<div class="btn-group w-100" role="group">
<input type="radio" class="btn-check" name="payment_method" id="pay_cash" value="cash" checked>
<label class="btn btn-outline-primary" for="pay_cash"><i class="bi bi-cash me-1"></i> <?php echo __('cash'); ?></label>
<input type="radio" class="btn-check" name="payment_method" id="pay_card" value="card">
<label class="btn btn-outline-primary" for="pay_card"><i class="bi bi-credit-card me-1"></i> <?php echo __('card'); ?></label>
<input type="radio" class="btn-check" name="payment_method" id="pay_insurance" value="insurance">
<label class="btn btn-outline-primary" for="pay_insurance"><i class="bi bi-shield-check me-1"></i> <?php echo __('insurance'); ?></label>
</div>
</div>
<div class="alert alert-info d-flex justify-content-between">
<span><?php echo __('total_amount'); ?>:</span>
<strong id="checkoutTotal">0.00</strong>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?php echo __('cancel'); ?></button>
<button type="button" class="btn btn-primary" onclick="processSale()">
<i class="bi bi-check-lg me-1"></i> <?php echo __('confirm_payment'); ?>
</button>
</div>
</div>
</div>
</div>
<script>
let cart = [];
let debounceTimer;
const searchInput = document.getElementById('drugSearch');
const drugList = document.getElementById('drugList');
const cartTableBody = document.getElementById('cartTableBody');
const cartTotalEl = document.getElementById('cartTotal');
const cartCountEl = document.getElementById('cartCount');
const checkoutTotalEl = document.getElementById('checkoutTotal');
// Search Logic
searchInput.addEventListener('input', function() {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
const query = this.value.trim();
if (query.length < 2) {
drugList.innerHTML = '<div class="text-center py-5 text-muted"><i class="bi bi-search fs-1"></i><p><?php echo __('search_to_add_items'); ?></p></div>';
return;
}
fetch('api/pharmacy.php?action=search_drugs&q=' + encodeURIComponent(query))
.then(r => r.json())
.then(data => {
drugList.innerHTML = '';
if (data.length === 0) {
drugList.innerHTML = '<div class="list-group-item text-center text-muted"><?php echo __('no_drugs_found'); ?></div>';
return;
}
data.forEach(drug => {
const stock = parseFloat(drug.stock);
const price = parseFloat(drug.default_price || 0);
const isOutOfStock = stock <= 0;
const item = document.createElement('a');
item.className = `list-group-item list-group-item-action d-flex justify-content-between align-items-center ${isOutOfStock ? 'disabled bg-light' : ''}`;
item.innerHTML = `
<div>
<div class="fw-bold">${drug.name_en}</div>
<small class="text-muted">${drug.name_ar || ''}</small>
</div>
<div class="text-end">
<div class="fw-bold text-primary">${price.toFixed(2)}</div>
<small class="${isOutOfStock ? 'text-danger' : 'text-success'}">
${isOutOfStock ? '<?php echo __('out_of_stock'); ?>' : '<?php echo __('stock'); ?>: ' + stock}
</small>
</div>
`;
if (!isOutOfStock) {
item.onclick = () => addToCart(drug);
item.style.cursor = 'pointer';
}
drugList.appendChild(item);
});
});
}, 300);
});
// Cart Logic
function addToCart(drug) {
const existing = cart.find(i => i.id === drug.id);
if (existing) {
if (existing.quantity >= drug.stock) {
alert('<?php echo __('insufficient_stock'); ?>');
return;
}
existing.quantity++;
} else {
cart.push({
id: drug.id,
name: drug.name_en,
price: parseFloat(drug.default_price || 0),
quantity: 1,
max_stock: parseFloat(drug.stock)
});
}
renderCart();
}
function updateQty(id, change) {
const item = cart.find(i => i.id === id);
if (!item) return;
const newQty = item.quantity + change;
if (newQty > item.max_stock) {
alert('<?php echo __('insufficient_stock'); ?>');
return;
}
if (newQty <= 0) {
cart = cart.filter(i => i.id !== id);
} else {
item.quantity = newQty;
}
renderCart();
}
function renderCart() {
cartTableBody.innerHTML = '';
let total = 0;
cart.forEach(item => {
const itemTotal = item.quantity * item.price;
total += itemTotal;
cartTableBody.innerHTML += `
<tr>
<td>
<div class="fw-bold text-truncate" style="max-width: 180px;">${item.name}</div>
<small class="text-muted">${item.price.toFixed(2)}</small>
</td>
<td>
<div class="input-group input-group-sm">
<button class="btn btn-outline-secondary" onclick="updateQty(${item.id}, -1)">-</button>
<input type="text" class="form-control text-center p-0" value="${item.quantity}" readonly>
<button class="btn btn-outline-secondary" onclick="updateQty(${item.id}, 1)">+</button>
</div>
</td>
<td class="text-end fw-bold">${itemTotal.toFixed(2)}</td>
<td class="text-end">
<button class="btn btn-sm btn-link text-danger p-0" onclick="updateQty(${item.id}, -1000)"><i class="bi bi-trash"></i></button>
</td>
</tr>
`;
});
cartTotalEl.textContent = total.toFixed(2);
cartCountEl.textContent = cart.length;
checkoutTotalEl.textContent = total.toFixed(2);
if (cart.length === 0) {
cartTableBody.innerHTML = '<tr><td colspan="4" class="text-center text-muted py-4"><?php echo __('cart_empty'); ?></td></tr>';
}
}
// Checkout Logic
function showCheckout() {
if (cart.length === 0) {
alert('<?php echo __('cart_is_empty'); ?>');
return;
}
const modal = new bootstrap.Modal(document.getElementById('checkoutModal'));
modal.show();
}
function processSale() {
const patientId = $('#patientSelect').val();
const paymentMethod = document.querySelector('input[name="payment_method"]:checked').value;
const payload = {
patient_id: patientId || null,
visit_id: null,
payment_method: paymentMethod,
total_amount: parseFloat(cartTotalEl.textContent),
items: cart.map(i => ({
drug_id: i.id,
quantity: i.quantity,
price: i.price
}))
};
fetch('api/pharmacy.php?action=create_sale', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
})
.then(r => r.json())
.then(data => {
if (data.success) {
alert('Sale completed!');
cart = [];
renderCart();
bootstrap.Modal.getInstance(document.getElementById('checkoutModal')).hide();
// TODO: Open receipt
} else {
alert(data.error || 'Transaction failed');
}
})
.catch(e => {
console.error(e);
alert('Network error');
});
}
// Initialize Patient Select2
$(document).ready(function() {
$('.select2-patient').select2({
theme: 'bootstrap-5',
dropdownParent: $('#checkoutModal'),
ajax: {
url: 'api/patients.php?action=search', // Need to check if this endpoint exists or similar
dataType: 'json',
delay: 250,
processResults: function (data) {
return {
results: data.map(p => ({id: p.id, text: p.name + ' (' + p.phone + ')'}))
};
},
cache: true
},
placeholder: '<?php echo __('search_patient'); ?>',
minimumInputLength: 1
});
});
</script>
<?php require_once __DIR__ . '/../layout/footer.php'; ?>

View File

@ -0,0 +1,217 @@
<?php
$section = 'pharmacy_sales';
require_once __DIR__ . '/../layout/header.php';
$page = $_GET['page'] ?? 1;
$limit = 20;
$offset = ($page - 1) * $limit;
// Count total
$count_stmt = $db->query("SELECT COUNT(*) FROM pharmacy_sales");
$total_rows = $count_stmt->fetchColumn();
$total_pages = ceil($total_rows / $limit);
// Fetch Sales
$sql = "SELECT s.*, p.name_en as patient_name
FROM pharmacy_sales s
LEFT JOIN patients p ON s.patient_id = p.id
ORDER BY s.created_at DESC
LIMIT $limit OFFSET $offset";
$stmt = $db->query($sql);
$sales = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<div class="container-fluid">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold text-primary"><i class="bi bi-receipt me-2"></i> <?php echo __('sales_history'); ?></h2>
<a href="pharmacy_pos.php" class="btn btn-primary">
<i class="bi bi-cart-plus me-2"></i> <?php echo __('new_sale'); ?>
</a>
</div>
<div class="card shadow-sm">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead>
<tr>
<th>#</th>
<th><?php echo __('date'); ?></th>
<th><?php echo __('patient'); ?></th>
<th><?php echo __('items'); ?></th>
<th><?php echo __('total_amount'); ?></th>
<th><?php echo __('payment_method'); ?></th>
<th><?php echo __('status'); ?></th>
<th><?php echo __('actions'); ?></th>
</tr>
</thead>
<tbody>
<?php if (empty($sales)): ?>
<tr>
<td colspan="8" class="text-center py-4 text-muted">
<i class="bi bi-inbox fs-1 d-block mb-2"></i>
<?php echo __('no_data_found'); ?>
</td>
</tr>
<?php else: ?>
<?php foreach ($sales as $sale): ?>
<tr>
<td><?php echo $sale['id']; ?></td>
<td><?php echo date('Y-m-d H:i', strtotime($sale['created_at'])); ?></td>
<td>
<?php if ($sale['patient_name']): ?>
<a href="patients.php?action=view&id=<?php echo $sale['patient_id']; ?>" class="text-decoration-none">
<?php echo htmlspecialchars($sale['patient_name']); ?>
</a>
<?php else: ?>
<span class="text-muted"><?php echo __('walk_in'); ?></span>
<?php endif; ?>
</td>
<td>
<?php
// Fetch item count or summary (quick query)
$item_count_stmt = $db->prepare("SELECT COUNT(*) FROM pharmacy_sale_items WHERE sale_id = ?");
$item_count_stmt->execute([$sale['id']]);
echo $item_count_stmt->fetchColumn() . ' ' . __('items');
?>
</td>
<td class="fw-bold"><?php echo number_format($sale['total_amount'], 2); ?></td>
<td>
<span class="badge bg-light text-dark border">
<?php echo ucfirst($sale['payment_method']); ?>
</span>
</td>
<td>
<span class="badge bg-success"><?php echo ucfirst($sale['status']); ?></span>
</td>
<td>
<button class="btn btn-sm btn-outline-primary" onclick="viewReceipt(<?php echo $sale['id']; ?>)">
<i class="bi bi-receipt"></i> <?php echo __('receipt'); ?>
</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- Pagination -->
<?php if ($total_pages > 1): ?>
<nav aria-label="Page navigation" class="mt-4">
<ul class="pagination justify-content-center">
<li class="page-item <?php echo $page <= 1 ? 'disabled' : ''; ?>">
<a class="page-link" href="?page=<?php echo $page - 1; ?>"><?php echo __('previous'); ?></a>
</li>
<?php for ($i = 1; $i <= $total_pages; $i++): ?>
<li class="page-item <?php echo $page == $i ? 'active' : ''; ?>">
<a class="page-link" href="?page=<?php echo $i; ?>"><?php echo $i; ?></a>
</li>
<?php endfor; ?>
<li class="page-item <?php echo $page >= $total_pages ? 'disabled' : ''; ?>">
<a class="page-link" href="?page=<?php echo $page + 1; ?>"><?php echo __('next'); ?></a>
</li>
</ul>
</nav>
<?php endif; ?>
</div>
</div>
</div>
<!-- Receipt Modal -->
<div class="modal fade" id="receiptModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><?php echo __('receipt'); ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="receiptBody">
<!-- Content via JS -->
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?php echo __('close'); ?></button>
<button type="button" class="btn btn-primary" onclick="printReceiptContent()"><i class="bi bi-printer"></i> <?php echo __('print'); ?></button>
</div>
</div>
</div>
</div>
<script>
function viewReceipt(saleId) {
const modal = new bootstrap.Modal(document.getElementById('receiptModal'));
const body = document.getElementById('receiptBody');
body.innerHTML = '<div class="text-center py-4"><div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div></div>';
modal.show();
fetch('api/pharmacy.php?action=get_sale_details&sale_id=' + saleId)
.then(r => r.json())
.then(data => {
if (data.error) {
body.innerHTML = '<div class="text-danger">' + data.error + '</div>';
return;
}
let html = `
<div class="text-center mb-4">
<h5><?php echo htmlspecialchars($site_name); ?></h5>
<p class="mb-0 text-muted">Receipt #${data.id}</p>
<small class="text-muted">${data.created_at}</small>
</div>
<div class="mb-3 d-flex justify-content-between">
<span><strong>Patient:</strong> ${data.patient_name || 'Walk-in'}</span>
<span><strong>Method:</strong> ${data.payment_method}</span>
</div>
<table class="table table-sm border-top border-bottom">
<thead><tr><th>Item</th><th class="text-end">Qty</th><th class="text-end">Price</th><th class="text-end">Total</th></tr></thead>
<tbody>
`;
data.items.forEach(item => {
html += `
<tr>
<td>${item.drug_name}</td>
<td class="text-end">${item.quantity}</td>
<td class="text-end">${parseFloat(item.unit_price).toFixed(2)}</td>
<td class="text-end">${parseFloat(item.total_price).toFixed(2)}</td>
</tr>
`;
});
html += `
</tbody>
<tfoot>
<tr class="fw-bold">
<td colspan="3" class="text-end pt-3">Total Amount</td>
<td class="text-end pt-3 fs-5">${parseFloat(data.total_amount).toFixed(2)}</td>
</tr>
</tfoot>
</table>
<div class="text-center mt-4 text-muted small">
<p>Thank you for your visit!</p>
</div>
`;
body.innerHTML = html;
})
.catch(e => {
console.error(e);
body.innerHTML = '<div class="text-danger">Error loading receipt</div>';
});
}
function printReceiptContent() {
const content = document.getElementById('receiptBody').innerHTML;
const win = window.open('', '', 'height=600,width=800');
win.document.write('<html><head><title>Print Receipt</title>');
win.document.write('<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">');
win.document.write('</head><body><div class="p-4" style="max-width: 400px; margin: 0 auto;">');
win.document.write(content);
win.document.write('</div></body></html>');
win.document.close();
win.focus();
setTimeout(() => { win.print(); win.close(); }, 500);
}
</script>
<?php require_once __DIR__ . '/../layout/footer.php'; ?>

View File

@ -324,6 +324,30 @@ $translations = [
'checkout' => 'Checkout',
'print_bill' => 'Print Bill',
'invoice' => 'Invoice',
'pharmacy' => 'Pharmacy',
'inventory' => 'Inventory',
'pos' => 'POS',
'sales_history' => 'Sales History',
'batch_number' => 'Batch Number',
'stock' => 'Stock',
'add_stock' => 'Add Stock',
'cost_price' => 'Cost Price',
'sale_price' => 'Sale Price',
'min_stock' => 'Min Stock',
'reorder_level' => 'Reorder Level',
'unit' => 'Unit',
'view_batches' => 'View Batches',
'received_date' => 'Received Date',
'payment_method' => 'Payment Method',
'cash' => 'Cash',
'card' => 'Card',
'receipt' => 'Receipt',
'total_amount' => 'Total Amount',
'quantity' => 'Quantity',
'new_sale' => 'New Sale',
'add_to_cart' => 'Add to Cart',
'cart' => 'Cart',
'insufficient_stock' => 'Insufficient Stock'
],
'ar' => [
'attachment' => 'المرفق',
@ -651,5 +675,30 @@ $translations = [
'checkout' => 'الدفع',
'print_bill' => 'طباعة الفاتورة',
'invoice' => 'فاتورة',
'pharmacy' => 'الصيدلية',
'inventory' => 'المخزون',
'pos' => 'نقطة البيع',
'sales_history' => 'سجل المبيعات',
'batch_number' => 'رقم التشغيلة',
'stock' => 'المخزون',
'add_stock' => 'إضافة مخزون',
'cost_price' => 'سعر التكلفة',
'sale_price' => 'سعر البيع',
'min_stock' => 'الحد الأدنى للمخزون',
'reorder_level' => 'حد إعادة الطلب',
'unit' => 'الوحدة',
'view_batches' => 'عرض التشغيلات',
'received_date' => 'تاريخ الاستلام',
'payment_method' => 'طريقة الدفع',
'cash' => 'نقدي',
'card' => 'بطاقة',
'insurance' => 'تأمين',
'receipt' => 'إيصال',
'total_amount' => 'المبلغ الإجمالي',
'quantity' => 'الكمية',
'new_sale' => 'بيع جديد',
'add_to_cart' => 'إضافة إلى السلة',
'cart' => 'السلة',
'insufficient_stock' => 'المخزون غير كاف'
]
];