add purchase to pharmacy

This commit is contained in:
Flatlogic Bot 2026-03-21 12:43:58 +00:00
parent fd41c8937a
commit dad73767a1
14 changed files with 2634 additions and 18 deletions

View File

@ -6,6 +6,10 @@ $action = $_GET['action'] ?? '';
$pdo = db();
try {
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 10;
$offset = ($page - 1) * $limit;
switch ($action) {
case 'get_stock':
// List all drugs with total stock quantity
@ -19,6 +23,116 @@ try {
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
break;
case 'get_low_stock':
// Count total for pagination
$countSql = "SELECT COUNT(*) FROM (
SELECT d.id
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
HAVING COALESCE(SUM(b.quantity), 0) <= MAX(d.reorder_level)
) as total";
$total = $pdo->query($countSql)->fetchColumn();
// List drugs where total stock is <= reorder_level
$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
HAVING total_stock <= MAX(d.reorder_level)
ORDER BY total_stock ASC
LIMIT :limit OFFSET :offset";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
echo json_encode([
'data' => $stmt->fetchAll(PDO::FETCH_ASSOC),
'total' => $total,
'page' => $page,
'limit' => $limit
]);
break;
case 'get_expired':
// Count total
$countSql = "SELECT COUNT(*)
FROM pharmacy_batches b
JOIN drugs d ON b.drug_id = d.id
LEFT JOIN suppliers s ON b.supplier_id = s.id
WHERE b.expiry_date < CURDATE() AND b.quantity > 0";
$total = $pdo->query($countSql)->fetchColumn();
// List batches that have expired and still have quantity
$sql = "SELECT b.id, b.batch_number, b.expiry_date, b.quantity,
d.name_en as drug_name, d.name_ar as drug_name_ar,
s.name_en as supplier_name
FROM pharmacy_batches b
JOIN drugs d ON b.drug_id = d.id
LEFT JOIN suppliers s ON b.supplier_id = s.id
WHERE b.expiry_date < CURDATE() AND b.quantity > 0
ORDER BY b.expiry_date ASC
LIMIT :limit OFFSET :offset";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
echo json_encode([
'data' => $stmt->fetchAll(PDO::FETCH_ASSOC),
'total' => $total,
'page' => $page,
'limit' => $limit
]);
break;
case 'get_near_expiry':
$days = $_GET['days'] ?? 90;
// Count total
$countSql = "SELECT COUNT(*)
FROM pharmacy_batches b
JOIN drugs d ON b.drug_id = d.id
LEFT JOIN suppliers s ON b.supplier_id = s.id
WHERE b.expiry_date >= CURDATE()
AND b.expiry_date <= DATE_ADD(CURDATE(), INTERVAL ? DAY)
AND b.quantity > 0";
$countStmt = $pdo->prepare($countSql);
$countStmt->execute([$days]);
$total = $countStmt->fetchColumn();
// List batches expiring in the next X days
$sql = "SELECT b.id, b.batch_number, b.expiry_date, b.quantity,
d.name_en as drug_name, d.name_ar as drug_name_ar,
s.name_en as supplier_name,
DATEDIFF(b.expiry_date, CURDATE()) as days_remaining
FROM pharmacy_batches b
JOIN drugs d ON b.drug_id = d.id
LEFT JOIN suppliers s ON b.supplier_id = s.id
WHERE b.expiry_date >= CURDATE()
AND b.expiry_date <= DATE_ADD(CURDATE(), INTERVAL :days DAY)
AND b.quantity > 0
ORDER BY b.expiry_date ASC
LIMIT :limit OFFSET :offset";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':days', $days, PDO::PARAM_INT);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
echo json_encode([
'data' => $stmt->fetchAll(PDO::FETCH_ASSOC),
'total' => $total,
'page' => $page,
'limit' => $limit
]);
break;
case 'get_batches':
$drug_id = $_GET['drug_id'] ?? 0;
if (!$drug_id) throw new Exception("Drug ID required");
@ -164,6 +278,188 @@ try {
echo json_encode($sale);
break;
case 'get_report':
$type = $_GET['type'] ?? 'inventory_valuation';
$startDate = $_GET['start_date'] ?? date('Y-m-01');
$endDate = $_GET['end_date'] ?? date('Y-m-d');
if ($type === 'inventory_valuation') {
// Count distinct drugs in stock for pagination
$countSql = "SELECT COUNT(DISTINCT d.id)
FROM drugs d
JOIN pharmacy_batches b ON d.id = b.drug_id
WHERE b.quantity > 0";
$total = $pdo->query($countSql)->fetchColumn();
$sql = "SELECT d.name_en as drug_name, d.name_ar as drug_name_ar,
g.name_en as category_name,
SUM(b.quantity) as stock_quantity,
SUM(b.quantity * b.cost_price) / SUM(b.quantity) as avg_cost,
SUM(b.quantity * b.sale_price) / SUM(b.quantity) as selling_price,
SUM(b.quantity * b.cost_price) as total_cost_value,
SUM(b.quantity * b.sale_price) as total_sales_value
FROM drugs d
JOIN pharmacy_batches b ON d.id = b.drug_id
LEFT JOIN drugs_groups g ON d.group_id = g.id
WHERE b.quantity > 0
GROUP BY d.id
ORDER BY d.name_en ASC
LIMIT :limit OFFSET :offset";
// Calculate Grand Totals (entire stock)
$grandTotalSql = "SELECT SUM(b.quantity * b.cost_price) as total_cost,
SUM(b.quantity * b.sale_price) as total_sales
FROM pharmacy_batches b
WHERE b.quantity > 0";
$grandTotals = $pdo->query($grandTotalSql)->fetch(PDO::FETCH_ASSOC);
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
echo json_encode([
'data' => $stmt->fetchAll(PDO::FETCH_ASSOC),
'total' => $total,
'page' => $page,
'limit' => $limit,
'grand_total_cost' => $grandTotals['total_cost'] ?? 0,
'grand_total_sales' => $grandTotals['total_sales'] ?? 0
]);
} elseif ($type === 'sales') {
// Count
$countSql = "SELECT COUNT(*) FROM pharmacy_sales WHERE created_at BETWEEN ? AND ? + INTERVAL 1 DAY";
$countStmt = $pdo->prepare($countSql);
$countStmt->execute([$startDate, $endDate]);
$total = $countStmt->fetchColumn();
$sql = "SELECT s.id, s.created_at, s.total_amount, s.payment_method,
p.name as patient_name,
(SELECT COUNT(*) FROM pharmacy_sale_items i WHERE i.sale_id = s.id) as item_count
FROM pharmacy_sales s
LEFT JOIN patients p ON s.patient_id = p.id
WHERE s.created_at BETWEEN :start AND :end + INTERVAL 1 DAY
ORDER BY s.created_at DESC
LIMIT :limit OFFSET :offset";
$grandTotalSql = "SELECT SUM(total_amount) as total FROM pharmacy_sales WHERE created_at BETWEEN ? AND ? + INTERVAL 1 DAY";
$grandTotalStmt = $pdo->prepare($grandTotalSql);
$grandTotalStmt->execute([$startDate, $endDate]);
$grandTotal = $grandTotalStmt->fetchColumn();
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':start', $startDate);
$stmt->bindValue(':end', $endDate);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
echo json_encode([
'data' => $stmt->fetchAll(PDO::FETCH_ASSOC),
'total' => $total,
'page' => $page,
'limit' => $limit,
'grand_total_sales' => $grandTotal ?? 0
]);
} elseif ($type === 'expiry') {
$countSql = "SELECT COUNT(*) FROM pharmacy_batches WHERE expiry_date BETWEEN ? AND ? AND quantity > 0";
$countStmt = $pdo->prepare($countSql);
$countStmt->execute([$startDate, $endDate]);
$total = $countStmt->fetchColumn();
$sql = "SELECT b.id, b.batch_number, b.expiry_date, b.quantity,
d.name_en as drug_name, d.name_ar as drug_name_ar,
s.name_en as supplier_name,
DATEDIFF(b.expiry_date, CURDATE()) as days_remaining
FROM pharmacy_batches b
JOIN drugs d ON b.drug_id = d.id
LEFT JOIN suppliers s ON b.supplier_id = s.id
WHERE b.expiry_date BETWEEN :start AND :end AND b.quantity > 0
ORDER BY b.expiry_date ASC
LIMIT :limit OFFSET :offset";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':start', $startDate);
$stmt->bindValue(':end', $endDate);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
echo json_encode([
'data' => $stmt->fetchAll(PDO::FETCH_ASSOC),
'total' => $total,
'page' => $page,
'limit' => $limit
]);
} elseif ($type === 'purchase_report') {
$countSql = "SELECT COUNT(*) FROM pharmacy_lpos WHERE lpo_date BETWEEN ? AND ?";
$countStmt = $pdo->prepare($countSql);
$countStmt->execute([$startDate, $endDate]);
$total = $countStmt->fetchColumn();
$sql = "SELECT l.id, l.lpo_date, l.status, l.total_amount, s.name_en as supplier_name, s.name_ar as supplier_name_ar
FROM pharmacy_lpos l
LEFT JOIN suppliers s ON l.supplier_id = s.id
WHERE l.lpo_date BETWEEN :start AND :end
ORDER BY l.lpo_date DESC
LIMIT :limit OFFSET :offset";
$grandTotalSql = "SELECT SUM(total_amount) as total FROM pharmacy_lpos WHERE lpo_date BETWEEN ? AND ?";
$grandTotalStmt = $pdo->prepare($grandTotalSql);
$grandTotalStmt->execute([$startDate, $endDate]);
$grandTotal = $grandTotalStmt->fetchColumn();
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':start', $startDate);
$stmt->bindValue(':end', $endDate);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
echo json_encode([
'data' => $stmt->fetchAll(PDO::FETCH_ASSOC),
'total' => $total,
'page' => $page,
'limit' => $limit,
'grand_total_purchases' => $grandTotal ?? 0
]);
} elseif ($type === 'low_stock') {
// Reuse get_low_stock logic
$countSql = "SELECT COUNT(*) FROM (
SELECT d.id
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
HAVING COALESCE(SUM(b.quantity), 0) <= MAX(d.reorder_level)
) as total";
$total = $pdo->query($countSql)->fetchColumn();
$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
HAVING total_stock <= MAX(d.reorder_level)
ORDER BY total_stock ASC
LIMIT :limit OFFSET :offset";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
echo json_encode([
'data' => $stmt->fetchAll(PDO::FETCH_ASSOC),
'total' => $total,
'page' => $page,
'limit' => $limit
]);
}
break;
default:
throw new Exception("Invalid action");
}

View File

@ -27,7 +27,7 @@ try {
]);
$lpoId = $pdo->lastInsertId();
$stmtItem = $pdo->prepare("INSERT INTO pharmacy_lpo_items (lpo_id, drug_id, quantity, cost_price, total_cost) VALUES (?, ?, ?, ?, ?)");
$stmtItem = $pdo->prepare("INSERT INTO pharmacy_lpo_items (lpo_id, drug_id, quantity, cost_price, total_cost, batch_number, expiry_date) VALUES (?, ?, ?, ?, ?, ?, ?)");
foreach ($data['items'] as $item) {
$stmtItem->execute([
@ -35,12 +35,14 @@ try {
$item['drug_id'],
$item['quantity'],
$item['cost_price'],
$item['total_cost']
$item['total_cost'],
!empty($item['batch_number']) ? $item['batch_number'] : null,
!empty($item['expiry_date']) ? $item['expiry_date'] : null
]);
}
$pdo->commit();
echo json_encode(['success' => true, 'message' => 'LPO created successfully']);
echo json_encode(['success' => true, 'message' => 'Purchase created successfully']);
} elseif ($action === 'update_status') {
$data = json_decode(file_get_contents('php://input'), true);
@ -48,10 +50,114 @@ try {
throw new Exception("ID and Status are required");
}
$pdo->beginTransaction();
// Check if items update is requested (specifically for Received status or corrections)
if (!empty($data['items']) && is_array($data['items'])) {
$updateItemStmt = $pdo->prepare("UPDATE pharmacy_lpo_items SET batch_number = ?, expiry_date = ? WHERE id = ?");
foreach ($data['items'] as $item) {
if (!empty($item['id'])) {
$updateItemStmt->execute([
!empty($item['batch_number']) ? $item['batch_number'] : null,
!empty($item['expiry_date']) ? $item['expiry_date'] : null,
$item['id']
]);
}
}
}
// If status is being changed to Received, we must update stock
if ($data['status'] === 'Received') {
// Fetch LPO items (re-fetch to get updated values)
$stmtItems = $pdo->prepare("SELECT * FROM pharmacy_lpo_items WHERE lpo_id = ?");
$stmtItems->execute([$data['id']]);
$items = $stmtItems->fetchAll(PDO::FETCH_ASSOC);
// Fetch LPO details for supplier
$stmtLPO = $pdo->prepare("SELECT supplier_id FROM pharmacy_lpos WHERE id = ?");
$stmtLPO->execute([$data['id']]);
$lpo = $stmtLPO->fetch(PDO::FETCH_ASSOC);
$batchStmt = $pdo->prepare("INSERT INTO pharmacy_batches (drug_id, batch_number, expiry_date, quantity, cost_price, sale_price, supplier_id, received_date) VALUES (?, ?, ?, ?, ?, ?, ?, CURDATE())");
// We need sale price for the batch. Ideally, LPO should have it or we fetch current from drugs table.
$drugPriceStmt = $pdo->prepare("SELECT price FROM drugs WHERE id = ?");
foreach ($items as $item) {
if (empty($item['batch_number']) || empty($item['expiry_date'])) {
// If still missing (should be caught by UI), generate defaults
$item['batch_number'] = $item['batch_number'] ?? 'BATCH-' . date('Ymd') . '-' . $item['id'];
$item['expiry_date'] = $item['expiry_date'] ?? date('Y-m-d', strtotime('+1 year'));
}
$drugPriceStmt->execute([$item['drug_id']]);
$drug = $drugPriceStmt->fetch(PDO::FETCH_ASSOC);
$salePrice = $drug['price'] ?? ($item['cost_price'] * 1.5); // Fallback margin
$batchStmt->execute([
$item['drug_id'],
$item['batch_number'],
$item['expiry_date'],
$item['quantity'],
$item['cost_price'],
$salePrice,
$lpo['supplier_id']
]);
}
}
$stmt = $pdo->prepare("UPDATE pharmacy_lpos SET status = ? WHERE id = ?");
$stmt->execute([$data['status'], $data['id']]);
$pdo->commit();
echo json_encode(['success' => true]);
} elseif ($action === 'create_return') {
$data = json_decode(file_get_contents('php://input'), true);
if (empty($data['supplier_id']) || empty($data['items'])) {
throw new Exception("Supplier and items are required.");
}
$pdo->beginTransaction();
$stmt = $pdo->prepare("INSERT INTO pharmacy_purchase_returns (supplier_id, return_date, total_amount, reason) VALUES (?, ?, ?, ?)");
$stmt->execute([
$data['supplier_id'],
$data['return_date'] ?? date('Y-m-d'),
$data['total_amount'] ?? 0,
$data['reason'] ?? ''
]);
$returnId = $pdo->lastInsertId();
$stmtItem = $pdo->prepare("INSERT INTO pharmacy_purchase_return_items (return_id, drug_id, batch_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?, ?)");
$updateBatch = $pdo->prepare("UPDATE pharmacy_batches SET quantity = quantity - ? WHERE id = ?");
foreach ($data['items'] as $item) {
// Check stock first
$checkBatch = $pdo->prepare("SELECT quantity FROM pharmacy_batches WHERE id = ?");
$checkBatch->execute([$item['batch_id']]);
$currentStock = $checkBatch->fetchColumn();
if ($currentStock < $item['quantity']) {
throw new Exception("Insufficient stock in batch for drug ID " . $item['drug_id']);
}
$stmtItem->execute([
$returnId,
$item['drug_id'],
$item['batch_id'],
$item['quantity'],
$item['unit_price'],
$item['total_price']
]);
// Deduct stock
$updateBatch->execute([$item['quantity'], $item['batch_id']]);
}
$pdo->commit();
echo json_encode(['success' => true, 'message' => 'Return created successfully']);
}
} elseif ($_SERVER['REQUEST_METHOD'] === 'GET') {
if ($action === 'get_lpos') {
@ -100,6 +206,70 @@ try {
} elseif ($action === 'get_drugs') {
$stmt = $pdo->query("SELECT id, name_en, name_ar, sku, price FROM drugs ORDER BY name_en ASC");
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
} elseif ($action === 'get_returns') {
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 20;
$offset = ($page - 1) * $limit;
$countStmt = $pdo->query("SELECT COUNT(*) FROM pharmacy_purchase_returns");
$total = $countStmt->fetchColumn();
$stmt = $pdo->prepare("
SELECT r.*, s.name_en as supplier_name
FROM pharmacy_purchase_returns r
LEFT JOIN suppliers s ON r.supplier_id = s.id
ORDER BY r.return_date DESC
LIMIT :limit OFFSET :offset
");
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
echo json_encode([
'data' => $stmt->fetchAll(PDO::FETCH_ASSOC),
'total' => $total,
'page' => $page,
'limit' => $limit,
'pages' => ceil($total / $limit)
]);
} elseif ($action === 'get_return_details') {
$id = $_GET['id'] ?? 0;
$stmt = $pdo->prepare("
SELECT i.*, d.name_en as drug_name, d.sku, b.batch_number
FROM pharmacy_purchase_return_items i
LEFT JOIN drugs d ON i.drug_id = d.id
LEFT JOIN pharmacy_batches b ON i.batch_id = b.id
WHERE i.return_id = ?
");
$stmt->execute([$id]);
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
} elseif ($action === 'get_supplier_batches') {
// Get batches for a specific supplier (or all if not specified, but usually filtered by drug)
// Ideally: We select a supplier for return, then we select items.
// Better: Select Drug -> Show Batches (maybe filter by supplier if we track supplier_id in batch)
$drug_id = $_GET['drug_id'] ?? 0;
$supplier_id = $_GET['supplier_id'] ?? 0;
$sql = "SELECT b.id, b.batch_number, b.expiry_date, b.quantity, b.cost_price
FROM pharmacy_batches b
WHERE b.drug_id = ? AND b.quantity > 0";
$params = [$drug_id];
if ($supplier_id) {
// If we want to strictly return only what we bought from this supplier:
// $sql .= " AND b.supplier_id = ?";
// $params[] = $supplier_id;
// BUT, sometimes we might return to a supplier what we bought elsewhere if they accept it,
// or `supplier_id` in batches might be null for old data.
// Let's NOT strictly enforce supplier_id match for now, just show all batches.
}
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
}
}

View File

@ -0,0 +1,29 @@
-- Add batch and expiry to LPO items for receiving stock
ALTER TABLE pharmacy_lpo_items ADD COLUMN IF NOT EXISTS batch_number VARCHAR(50) NULL;
ALTER TABLE pharmacy_lpo_items ADD COLUMN IF NOT EXISTS expiry_date DATE NULL;
-- Create Purchase Returns table
CREATE TABLE IF NOT EXISTS pharmacy_purchase_returns (
id INT AUTO_INCREMENT PRIMARY KEY,
supplier_id INT NOT NULL,
return_date DATE NOT NULL,
total_amount DECIMAL(10, 2) DEFAULT 0.00,
reason TEXT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (supplier_id) REFERENCES suppliers(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Create Purchase Return Items table
CREATE TABLE IF NOT EXISTS pharmacy_purchase_return_items (
id INT AUTO_INCREMENT PRIMARY KEY,
return_id INT NOT NULL,
drug_id INT NOT NULL,
batch_id INT NULL, -- Which batch we are returning from
quantity INT NOT NULL,
unit_price DECIMAL(10, 2) NOT NULL, -- Refund price
total_price DECIMAL(10, 2) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (return_id) REFERENCES pharmacy_purchase_returns(id) ON DELETE CASCADE,
FOREIGN KEY (drug_id) REFERENCES drugs(id),
FOREIGN KEY (batch_id) REFERENCES pharmacy_batches(id) ON DELETE SET NULL
);

View File

@ -131,16 +131,19 @@ $site_favicon = !empty($site_settings['company_favicon']) ? $site_settings['comp
</div>
<!-- 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', 'pharmacy_lpos']) ? 'active' : ''; ?> d-flex justify-content-between align-items-center">
<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', 'pharmacy_purchases', 'pharmacy_purchase_returns', 'pharmacy_alerts', 'pharmacy_reports']) ? '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, ['pharmacy_inventory', 'pharmacy_pos', 'pharmacy_sales', 'drugs', 'drugs_groups', 'suppliers', 'pharmacy_lpos']) ? 'show' : ''; ?>" id="pharmacySubmenu">
<div class="collapse <?php echo in_array($section, ['pharmacy_inventory', 'pharmacy_pos', 'pharmacy_sales', 'drugs', 'drugs_groups', 'suppliers', 'pharmacy_purchases', 'pharmacy_purchase_returns', 'pharmacy_alerts', 'pharmacy_reports']) ? '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>
<a href="pharmacy_lpos.php" class="sidebar-link py-2 <?php echo $section === 'pharmacy_lpos' ? 'active' : ''; ?>"><i class="bi bi-receipt-cutoff me-2"></i> <?php echo __('lpos'); ?></a>
<a href="pharmacy_purchases.php" class="sidebar-link py-2 <?php echo $section === 'pharmacy_purchases' ? 'active' : ''; ?>"><i class="bi bi-basket2 me-2"></i> <?php echo __('purchases'); ?></a>
<a href="pharmacy_purchase_returns.php" class="sidebar-link py-2 <?php echo $section === 'pharmacy_purchase_returns' ? 'active' : ''; ?>"><i class="bi bi-arrow-return-left me-2"></i> <?php echo __('purchase_returns'); ?></a>
<a href="pharmacy_alerts.php" class="sidebar-link py-2 <?php echo $section === 'pharmacy_alerts' ? 'active' : ''; ?>"><i class="bi bi-exclamation-triangle me-2"></i> <?php echo __('alerts'); ?></a>
<a href="pharmacy_reports.php" class="sidebar-link py-2 <?php echo $section === "pharmacy_reports" ? "active" : ""; ?>"><i class="bi bi-file-earmark-bar-graph me-2"></i> <?php echo __("reports"); ?></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>

View File

@ -0,0 +1,308 @@
<div class="container-fluid">
<h2 class="mt-4 mb-4"><?= __('pharmacy_alerts') ?></h2>
<ul class="nav nav-tabs" id="pharmacyAlertsTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="low-stock-tab" data-bs-toggle="tab" data-bs-target="#low-stock" type="button" role="tab" aria-controls="low-stock" aria-selected="true"><?= __('low_stock') ?></button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="expired-tab" data-bs-toggle="tab" data-bs-target="#expired" type="button" role="tab" aria-controls="expired" aria-selected="false"><?= __('expired_batches') ?></button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="near-expiry-tab" data-bs-toggle="tab" data-bs-target="#near-expiry" type="button" role="tab" aria-controls="near-expiry" aria-selected="false"><?= __('near_expiry') ?></button>
</li>
</ul>
<div class="tab-content" id="pharmacyAlertsTabContent">
<!-- Low Stock Tab -->
<div class="tab-pane fade show active" id="low-stock" role="tabpanel" aria-labelledby="low-stock-tab">
<div class="card mt-3 border-0 shadow-sm">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover" id="lowStockTable">
<thead>
<tr>
<th><?= __('drug_name') ?></th>
<th><?= __('stock') ?></th>
<th><?= __('reorder_level') ?></th>
<th><?= __('min_stock') ?></th>
<th><?= __('actions') ?></th>
</tr>
</thead>
<tbody>
<tr><td colspan="5" class="text-center"><?= __('loading') ?></td></tr>
</tbody>
</table>
</div>
<!-- Pagination Container -->
<div id="lowStockPagination" class="mt-3"></div>
</div>
</div>
</div>
<!-- Expired Tab -->
<div class="tab-pane fade" id="expired" role="tabpanel" aria-labelledby="expired-tab">
<div class="card mt-3 border-0 shadow-sm">
<div class="card-body">
<div class="alert alert-danger">
<i class="bi bi-exclamation-triangle-fill"></i> <?= __('these_batches_expired') ?>
</div>
<div class="table-responsive">
<table class="table table-hover" id="expiredTable">
<thead>
<tr>
<th><?= __('drug_name') ?></th>
<th><?= __('batch_number') ?></th>
<th><?= __('expiry_date') ?></th>
<th><?= __('quantity') ?></th>
<th><?= __('supplier') ?></th>
</tr>
</thead>
<tbody>
<tr><td colspan="5" class="text-center"><?= __('loading') ?></td></tr>
</tbody>
</table>
</div>
<!-- Pagination Container -->
<div id="expiredPagination" class="mt-3"></div>
</div>
</div>
</div>
<!-- Near Expiry Tab -->
<div class="tab-pane fade" id="near-expiry" role="tabpanel" aria-labelledby="near-expiry-tab">
<div class="card mt-3 border-0 shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<label for="daysFilter" class="form-label me-2"><?= __('show_expiring_within') ?></label>
<select id="daysFilter" class="form-select d-inline-block w-auto">
<option value="30">30 <?= __('days') ?></option>
<option value="60">60 <?= __('days') ?></option>
<option value="90" selected>90 <?= __('days') ?></option>
<option value="180">180 <?= __('days') ?></option>
</select>
</div>
</div>
<div class="table-responsive">
<table class="table table-hover" id="nearExpiryTable">
<thead>
<tr>
<th><?= __('drug_name') ?></th>
<th><?= __('batch_number') ?></th>
<th><?= __('expiry_date') ?></th>
<th><?= __('days_remaining') ?></th>
<th><?= __('quantity') ?></th>
<th><?= __('supplier') ?></th>
</tr>
</thead>
<tbody>
<tr><td colspan="6" class="text-center"><?= __('loading') ?></td></tr>
</tbody>
</table>
</div>
<!-- Pagination Container -->
<div id="nearExpiryPagination" class="mt-3"></div>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
loadLowStock();
// Load other tabs when clicked
document.getElementById('expired-tab').addEventListener('shown.bs.tab', function (event) {
loadExpired();
});
document.getElementById('near-expiry-tab').addEventListener('shown.bs.tab', function (event) {
loadNearExpiry();
});
document.getElementById('daysFilter').addEventListener('change', function() {
loadNearExpiry();
});
});
function loadLowStock(page = 1) {
fetch(`api/pharmacy.php?action=get_low_stock&page=${page}&limit=10`)
.then(response => response.json())
.then(response => {
const tbody = document.querySelector('#lowStockTable tbody');
tbody.innerHTML = '';
const data = response.data || []; // Handle case where API might not return data key if error or old format (safety)
if (data.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="text-center"><?= __('no_low_stock_found') ?></td></tr>';
document.getElementById('lowStockPagination').innerHTML = '';
return;
}
data.forEach(item => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${item.name_en} <br> <small class="text-muted">${item.name_ar || ''}</small></td>
<td><span class="badge bg-danger">${item.total_stock} ${item.unit || ''}</span></td>
<td>${item.reorder_level}</td>
<td>${item.min_stock_level}</td>
<td>
<a href="pharmacy_lpos.php" class="btn btn-sm btn-primary">
<i class="bi bi-cart-plus"></i> <?= __('order_stock') ?>
</a>
</td>
`;
tbody.appendChild(tr);
});
renderPagination(response.total, response.page, response.limit, 'lowStockPagination', loadLowStock);
})
.catch(error => console.error('Error loading low stock:', error));
}
function loadExpired(page = 1) {
fetch(`api/pharmacy.php?action=get_expired&page=${page}&limit=10`)
.then(response => response.json())
.then(response => {
const tbody = document.querySelector('#expiredTable tbody');
tbody.innerHTML = '';
const data = response.data || [];
if (data.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="text-center"><?= __('no_expired_batches_found') ?></td></tr>';
document.getElementById('expiredPagination').innerHTML = '';
return;
}
data.forEach(item => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${item.drug_name} <br> <small class="text-muted">${item.drug_name_ar || ''}</small></td>
<td>${item.batch_number}</td>
<td class="text-danger fw-bold">${item.expiry_date}</td>
<td>${item.quantity}</td>
<td>${item.supplier_name || '-'}</td>
`;
tbody.appendChild(tr);
});
renderPagination(response.total, response.page, response.limit, 'expiredPagination', loadExpired);
})
.catch(error => console.error('Error loading expired:', error));
}
function loadNearExpiry(page = 1) {
const days = document.getElementById('daysFilter').value;
fetch(`api/pharmacy.php?action=get_near_expiry&days=${days}&page=${page}&limit=10`)
.then(response => response.json())
.then(response => {
const tbody = document.querySelector('#nearExpiryTable tbody');
tbody.innerHTML = '';
const data = response.data || [];
if (data.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="text-center"><?= __('no_batches_expiring_soon') ?></td></tr>';
document.getElementById('nearExpiryPagination').innerHTML = '';
return;
}
data.forEach(item => {
const tr = document.createElement('tr');
let badgeClass = 'bg-warning text-dark';
if (item.days_remaining < 30) badgeClass = 'bg-danger';
tr.innerHTML = `
<td>${item.drug_name} <br> <small class="text-muted">${item.drug_name_ar || ''}</small></td>
<td>${item.batch_number}</td>
<td>${item.expiry_date}</td>
<td><span class="badge ${badgeClass}">${item.days_remaining} <?= __('days') ?></span></td>
<td>${item.quantity}</td>
<td>${item.supplier_name || '-'}</td>
`;
tbody.appendChild(tr);
});
renderPagination(response.total, response.page, response.limit, 'nearExpiryPagination', loadNearExpiry);
})
.catch(error => console.error('Error loading near expiry:', error));
}
function renderPagination(total, page, limit, containerId, callback) {
const totalPages = Math.ceil(total / limit);
const container = document.getElementById(containerId);
container.innerHTML = '';
if (totalPages <= 1) return;
const ul = document.createElement('ul');
ul.className = 'pagination justify-content-center';
// Previous
const prevLi = document.createElement('li');
prevLi.className = `page-item ${page === 1 ? 'disabled' : ''}`;
prevLi.innerHTML = `<a class="page-link" href="#" aria-label="Previous"><span aria-hidden="true">&laquo;</span></a>`;
prevLi.onclick = (e) => {
e.preventDefault();
if (page > 1) callback(page - 1);
};
ul.appendChild(prevLi);
// Page Numbers logic
let startPage = Math.max(1, page - 2);
let endPage = Math.min(totalPages, page + 2);
if (startPage > 1) {
ul.appendChild(createPageItem(1, page, callback));
if (startPage > 2) {
const ellipsis = document.createElement('li');
ellipsis.className = 'page-item disabled';
ellipsis.innerHTML = '<span class="page-link">...</span>';
ul.appendChild(ellipsis);
}
}
for (let i = startPage; i <= endPage; i++) {
ul.appendChild(createPageItem(i, page, callback));
}
if (endPage < totalPages) {
if (endPage < totalPages - 1) {
const ellipsis = document.createElement('li');
ellipsis.className = 'page-item disabled';
ellipsis.innerHTML = '<span class="page-link">...</span>';
ul.appendChild(ellipsis);
}
ul.appendChild(createPageItem(totalPages, page, callback));
}
// Next
const nextLi = document.createElement('li');
nextLi.className = `page-item ${page === totalPages ? 'disabled' : ''}`;
nextLi.innerHTML = `<a class="page-link" href="#" aria-label="Next"><span aria-hidden="true">&raquo;</span></a>`;
nextLi.onclick = (e) => {
e.preventDefault();
if (page < totalPages) callback(page + 1);
};
ul.appendChild(nextLi);
container.appendChild(ul);
}
function createPageItem(pageNum, currentPage, callback) {
const li = document.createElement('li');
li.className = `page-item ${pageNum === currentPage ? 'active' : ''}`;
li.innerHTML = `<a class="page-link" href="#">${pageNum}</a>`;
li.onclick = (e) => {
e.preventDefault();
callback(pageNum);
};
return li;
}
</script>

View File

@ -0,0 +1,469 @@
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="mb-0 text-primary fw-bold"><i class="bi bi-arrow-return-left me-2"></i> <?php echo __('purchase_returns'); ?></h4>
<button type="button" class="btn btn-primary" onclick="openCreateReturnModal()">
<i class="bi bi-plus-lg me-2"></i> <?php echo __('create_return'); ?>
</button>
</div>
<div class="card shadow-sm border-0">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover align-middle" id="returnsTable">
<thead class="table-light">
<tr>
<th>#</th>
<th><?php echo __('date'); ?></th>
<th><?php echo __('supplier'); ?></th>
<th><?php echo __('total_amount'); ?></th>
<th><?php echo __('reason'); ?></th>
<th><?php echo __('actions'); ?></th>
</tr>
</thead>
<tbody id="returnsTableBody">
<tr><td colspan="6" class="text-center py-4"><div class="spinner-border text-primary" role="status"></div></td></tr>
</tbody>
</table>
</div>
<!-- Pagination -->
<nav id="paginationContainer" class="mt-3" aria-label="Page navigation"></nav>
</div>
</div>
<!-- Create Return Modal -->
<div class="modal fade" id="createReturnModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-xl">
<form id="createReturnForm" onsubmit="submitReturn(event)">
<div class="modal-content border-0 shadow">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title fw-bold"><i class="bi bi-plus-circle me-2"></i> <?php echo __('create_return'); ?></h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body bg-light">
<div class="card mb-3 border-0 shadow-sm">
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-bold"><?php echo __('supplier'); ?></label>
<select name="supplier_id" id="return_supplier_id" class="form-select select2-modal" required>
<option value=""><?php echo __('select_supplier'); ?>...</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label fw-bold"><?php echo __('return_date'); ?></label>
<input type="date" name="return_date" id="return_date" class="form-control" value="<?php echo date('Y-m-d'); ?>" required>
</div>
<div class="col-md-12">
<label class="form-label"><?php echo __('reason'); ?></label>
<textarea name="reason" id="return_reason" class="form-control" rows="2"></textarea>
</div>
</div>
</div>
</div>
<div class="card border-0 shadow-sm">
<div class="card-header bg-white py-3">
<div class="d-flex justify-content-between align-items-center">
<h6 class="mb-0 fw-bold text-primary"><?php echo __('items'); ?></h6>
<button type="button" class="btn btn-sm btn-outline-primary" onclick="addReturnItemRow()">
<i class="bi bi-plus-lg me-1"></i> <?php echo __('add_item'); ?>
</button>
</div>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-bordered mb-0" id="returnItemsTable">
<thead class="table-light">
<tr>
<th style="width: 30%;"><?php echo __('drug_name'); ?></th>
<th style="width: 25%;"><?php echo __('batch_number'); ?></th>
<th style="width: 15%;"><?php echo __('quantity'); ?></th>
<th style="width: 15%;"><?php echo __('unit_price'); ?></th>
<th style="width: 15%;"><?php echo __('total_price'); ?></th>
<th style="width: 5%;"></th>
</tr>
</thead>
<tbody id="returnItemsBody">
<!-- Dynamic Rows -->
</tbody>
<tfoot class="table-light">
<tr>
<td colspan="4" class="text-end fw-bold"><?php echo __('total_amount'); ?>:</td>
<td class="fw-bold text-primary fs-5" id="returnTotalDisplay">0.00</td>
<td></td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
<div class="modal-footer bg-white">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?php echo __('cancel'); ?></button>
<button type="submit" class="btn btn-primary px-4"><i class="bi bi-save me-2"></i> <?php echo __('save'); ?></button>
</div>
</div>
</form>
</div>
</div>
<!-- View Return Modal -->
<div class="modal fade" id="viewReturnModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content border-0 shadow">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title fw-bold"><i class="bi bi-eye me-2"></i> <?php echo __('view_return'); ?> #<span id="view_return_id"></span></h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row mb-4">
<div class="col-md-6">
<p class="mb-1 text-muted small"><?php echo __('supplier'); ?></p>
<h6 class="fw-bold" id="view_return_supplier"></h6>
</div>
<div class="col-md-6 text-end">
<p class="mb-1 text-muted small"><?php echo __('date'); ?></p>
<h6 class="fw-bold" id="view_return_date"></h6>
</div>
</div>
<h6 class="border-bottom pb-2 mb-3 fw-bold text-primary"><?php echo __('items'); ?></h6>
<div class="table-responsive">
<table class="table table-bordered table-sm">
<thead class="table-light">
<tr>
<th><?php echo __('drug_name'); ?></th>
<th><?php echo __('batch_number'); ?></th>
<th class="text-center"><?php echo __('quantity'); ?></th>
<th class="text-end"><?php echo __('unit_price'); ?></th>
<th class="text-end"><?php echo __('total_price'); ?></th>
</tr>
</thead>
<tbody id="viewReturnItemsBody"></tbody>
<tfoot class="table-light">
<tr>
<td colspan="4" class="text-end fw-bold"><?php echo __('total_amount'); ?></td>
<td class="text-end fw-bold text-primary" id="view_return_total"></td>
</tr>
</tfoot>
</table>
</div>
<div class="mt-3">
<p class="mb-1 text-muted small"><?php echo __('reason'); ?></p>
<p class="bg-light p-2 rounded" id="view_return_reason"></p>
</div>
</div>
<div class="modal-footer bg-light">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?php echo __('close'); ?></button>
</div>
</div>
</div>
</div>
<script>
let allDrugs = [];
let allSuppliers = [];
let currentPage = 1;
document.addEventListener('DOMContentLoaded', function() {
loadReturns(1);
fetchSuppliers();
fetchDrugs();
// Calculate total when inputs change in the table
document.getElementById('returnItemsBody').addEventListener('input', function(e) {
if (e.target.classList.contains('return-qty') || e.target.classList.contains('return-price')) {
calculateRowTotal(e.target.closest('tr'));
calculateGrandTotal();
}
});
// Re-initialize Select2 when modal is shown
$('#createReturnModal').on('shown.bs.modal', function () {
$('.select2-modal').select2({
dropdownParent: $('#createReturnModal'),
width: '100%'
});
});
});
function loadReturns(page = 1) {
currentPage = page;
fetch('api/pharmacy_lpo.php?action=get_returns&page=' + page)
.then(response => response.json())
.then(data => {
const tbody = document.getElementById('returnsTableBody');
const paginationContainer = document.getElementById('paginationContainer');
tbody.innerHTML = '';
const returns = data.data || (Array.isArray(data) ? data : []);
const totalPages = data.pages || 1;
if (returns.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="text-center py-4 text-muted"><?php echo __('no_data_found'); ?></td></tr>';
paginationContainer.innerHTML = '';
return;
}
returns.forEach(r => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${r.id}</td>
<td>${r.return_date}</td>
<td>${r.supplier_name || '-'}</td>
<td class="fw-bold text-danger">$${parseFloat(r.total_amount).toFixed(2)}</td>
<td>${r.reason || '-'}</td>
<td>
<button class="btn btn-sm btn-outline-primary" onclick="viewReturn(${r.id}, '${r.supplier_name}', '${r.return_date}', '${r.total_amount}', '${r.reason || ''}')">
<i class="bi bi-eye"></i>
</button>
</td>
`;
tbody.appendChild(tr);
});
renderPagination(currentPage, totalPages);
})
.catch(err => {
console.error(err);
document.getElementById('returnsTableBody').innerHTML = '<tr><td colspan="6" class="text-center text-danger">Error loading data</td></tr>';
});
}
function renderPagination(current, total) {
const container = document.getElementById('paginationContainer');
if (total <= 1) {
container.innerHTML = '';
return;
}
let html = '<ul class="pagination justify-content-center">';
html += `<li class="page-item ${current <= 1 ? 'disabled' : ''}"><a class="page-link" href="#" onclick="event.preventDefault(); loadReturns(${current - 1})"><?php echo __('previous'); ?></a></li>`;
let start = Math.max(1, current - 2);
let end = Math.min(total, start + 4);
if (end - start < 4) start = Math.max(1, end - 4);
for (let i = start; i <= end; i++) {
html += `<li class="page-item ${current === i ? 'active' : ''}"><a class="page-link" href="#" onclick="event.preventDefault(); loadReturns(${i})">${i}</a></li>`;
}
html += `<li class="page-item ${current >= total ? 'disabled' : ''}"><a class="page-link" href="#" onclick="event.preventDefault(); loadReturns(${current + 1})"><?php echo __('next'); ?></a></li>`;
html += '</ul>';
container.innerHTML = html;
}
function fetchSuppliers() {
fetch('api/pharmacy_lpo.php?action=get_suppliers')
.then(response => response.json())
.then(data => {
allSuppliers = data;
const select = document.getElementById('return_supplier_id');
select.innerHTML = '<option value=""><?php echo __('select_supplier'); ?>...</option>';
data.forEach(s => {
select.innerHTML += `<option value="${s.id}">${s.name_en} / ${s.name_ar}</option>`;
});
});
}
function fetchDrugs() {
fetch('api/pharmacy_lpo.php?action=get_drugs')
.then(response => response.json())
.then(data => {
allDrugs = data;
});
}
function openCreateReturnModal() {
document.getElementById('createReturnForm').reset();
$('#return_supplier_id').val('').trigger('change');
document.getElementById('returnItemsBody').innerHTML = '';
document.getElementById('returnTotalDisplay').innerText = '0.00';
addReturnItemRow();
var modal = new bootstrap.Modal(document.getElementById('createReturnModal'));
modal.show();
}
function addReturnItemRow() {
const tbody = document.getElementById('returnItemsBody');
const tr = document.createElement('tr');
let drugOptions = '<option value="">Select Drug...</option>';
allDrugs.forEach(d => {
drugOptions += `<option value="${d.id}" data-price="${d.price}">${d.name_en} (${d.sku || '-'})</option>`;
});
tr.innerHTML = `
<td>
<select name="items[drug_id][]" class="form-select select2-modal-row return-drug" onchange="returnDrugSelected(this)" required>
${drugOptions}
</select>
</td>
<td>
<select name="items[batch_id][]" class="form-select select2-modal-row return-batch" onchange="returnBatchSelected(this)" required disabled>
<option value="">Select Batch...</option>
</select>
</td>
<td><input type="number" name="items[quantity][]" class="form-control return-qty" min="1" value="1" required></td>
<td><input type="number" name="items[unit_price][]" class="form-control return-price" step="0.01" min="0" value="0.00" required></td>
<td><input type="text" class="form-control return-total" readonly value="0.00"></td>
<td class="text-center">
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeReturnRow(this)"><i class="bi bi-trash"></i></button>
</td>
`;
tbody.appendChild(tr);
$(tr).find('.select2-modal-row').select2({
dropdownParent: $('#createReturnModal'),
width: '100%'
});
}
function removeReturnRow(btn) {
const tbody = document.getElementById('returnItemsBody');
if (tbody.children.length > 1) {
btn.closest('tr').remove();
calculateGrandTotal();
}
}
function returnDrugSelected(select) {
const drugId = select.value;
const row = select.closest('tr');
const batchSelect = row.querySelector('.return-batch');
// Clear and disable batch select
batchSelect.innerHTML = '<option value="">Select Batch...</option>';
batchSelect.disabled = true;
$(batchSelect).trigger('change'); // Notify Select2
if (!drugId) return;
// Fetch Batches
fetch('api/pharmacy.php?action=get_batches&drug_id=' + drugId)
.then(response => response.json())
.then(data => {
batchSelect.disabled = false;
data.forEach(b => {
// Show expiry and qty
const label = `${b.batch_number} (Exp: ${b.expiry_date}, Qty: ${b.quantity})`;
const option = new Option(label, b.id, false, false);
option.setAttribute('data-qty', b.quantity);
option.setAttribute('data-cost', b.cost_price);
batchSelect.add(option);
});
$(batchSelect).trigger('change'); // Update UI
});
}
function returnBatchSelected(select) {
const option = select.options[select.selectedIndex];
const row = select.closest('tr');
const qtyInput = row.querySelector('.return-qty');
const priceInput = row.querySelector('.return-price');
const maxQty = option.getAttribute('data-qty') || 1;
const costPrice = option.getAttribute('data-cost') || 0;
// Set max quantity
qtyInput.max = maxQty;
qtyInput.value = Math.min(qtyInput.value, maxQty);
// Suggest refund price as cost price
priceInput.value = costPrice;
calculateRowTotal(row);
calculateGrandTotal();
}
function calculateRowTotal(row) {
const qty = parseFloat(row.querySelector('.return-qty').value) || 0;
const price = parseFloat(row.querySelector('.return-price').value) || 0;
const total = qty * price;
row.querySelector('.return-total').value = total.toFixed(2);
}
function calculateGrandTotal() {
let total = 0;
document.querySelectorAll('.return-total').forEach(input => {
total += parseFloat(input.value) || 0;
});
document.getElementById('returnTotalDisplay').innerText = total.toFixed(2);
}
function submitReturn(e) {
e.preventDefault();
const supplierId = document.getElementById('return_supplier_id').value;
const date = document.getElementById('return_date').value;
const reason = document.getElementById('return_reason').value;
const totalAmount = document.getElementById('returnTotalDisplay').innerText;
const items = [];
document.querySelectorAll('#returnItemsBody tr').forEach(row => {
const drugId = $(row).find('.return-drug').val();
const batchId = $(row).find('.return-batch').val();
const quantity = row.querySelector('.return-qty').value;
const unitPrice = row.querySelector('.return-price').value;
const totalPrice = row.querySelector('.return-total').value;
if (drugId && batchId) {
items.push({
drug_id: drugId,
batch_id: batchId,
quantity: quantity,
unit_price: unitPrice,
total_price: totalPrice
});
}
});
if (items.length === 0) {
alert('Please add at least one item.');
return;
}
fetch('api/pharmacy_lpo.php?action=create_return', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ supplier_id: supplierId, return_date: date, reason: reason, total_amount: totalAmount, items: items })
})
.then(response => response.json())
.then(data => {
if (data.success) {
bootstrap.Modal.getInstance(document.getElementById('createReturnModal')).hide();
loadReturns();
} else {
alert('Error: ' + (data.error || 'Unknown error'));
}
});
}
function viewReturn(id, supplier, date, total, reason) {
document.getElementById('view_return_id').innerText = id;
document.getElementById('view_return_supplier').innerText = supplier;
document.getElementById('view_return_date').innerText = date;
document.getElementById('view_return_total').innerText = '$' + parseFloat(total).toFixed(2);
document.getElementById('view_return_reason').innerText = reason;
const tbody = document.getElementById('viewReturnItemsBody');
tbody.innerHTML = '<tr><td colspan="5" class="text-center">Loading...</td></tr>';
var modal = new bootstrap.Modal(document.getElementById('viewReturnModal'));
modal.show();
fetch('api/pharmacy_lpo.php?action=get_return_details&id=' + id)
.then(response => response.json())
.then(data => {
tbody.innerHTML = '';
data.forEach(item => {
tbody.innerHTML += `
<tr>
<td>${item.drug_name} <small class="text-muted">(${item.sku || '-'})</small></td>
<td>${item.batch_number || '-'}</td>
<td class="text-center">${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>
`;
});
});
}
</script>

View File

@ -0,0 +1,599 @@
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="mb-0 text-primary fw-bold"><i class="bi bi-basket2 me-2"></i> <?php echo __('purchases'); ?></h4>
<button type="button" class="btn btn-primary" onclick="openCreatePurchaseModal()">
<i class="bi bi-plus-lg me-2"></i> <?php echo __('create_purchase'); ?>
</button>
</div>
<div class="card shadow-sm border-0">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover align-middle" id="purchaseTable">
<thead class="table-light">
<tr>
<th>#</th>
<th><?php echo __('purchase_date'); ?></th>
<th><?php echo __('supplier'); ?></th>
<th><?php echo __('total_amount'); ?></th>
<th><?php echo __('status'); ?></th>
<th><?php echo __('actions'); ?></th>
</tr>
</thead>
<tbody id="purchaseTableBody">
<tr><td colspan="6" class="text-center py-4"><div class="spinner-border text-primary" role="status"></div></td></tr>
</tbody>
</table>
</div>
<!-- Pagination -->
<nav id="paginationContainer" class="mt-3" aria-label="Page navigation"></nav>
</div>
</div>
<!-- Create Purchase Modal -->
<div class="modal fade" id="createPurchaseModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-xl">
<form id="createPurchaseForm" onsubmit="submitPurchase(event)">
<div class="modal-content border-0 shadow">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title fw-bold"><i class="bi bi-plus-circle me-2"></i> <?php echo __('create_purchase'); ?></h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body bg-light">
<div class="card mb-3 border-0 shadow-sm">
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-bold"><?php echo __('supplier'); ?></label>
<select name="supplier_id" id="purchase_supplier_id" class="form-select select2-modal" required>
<option value=""><?php echo __('select_supplier'); ?>...</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label fw-bold"><?php echo __('purchase_date'); ?></label>
<input type="date" name="lpo_date" id="purchase_date" class="form-control" value="<?php echo date('Y-m-d'); ?>" required>
</div>
<div class="col-md-12">
<label class="form-label"><?php echo __('notes'); ?></label>
<textarea name="notes" id="purchase_notes" class="form-control" rows="2"></textarea>
</div>
</div>
</div>
</div>
<div class="card border-0 shadow-sm">
<div class="card-header bg-white py-3">
<div class="d-flex justify-content-between align-items-center">
<h6 class="mb-0 fw-bold text-primary"><?php echo __('items'); ?></h6>
<button type="button" class="btn btn-sm btn-outline-primary" onclick="addPurchaseItemRow()">
<i class="bi bi-plus-lg me-1"></i> <?php echo __('add_item'); ?>
</button>
</div>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-bordered mb-0" id="purchaseItemsTable">
<thead class="table-light">
<tr>
<th style="width: 25%;"><?php echo __('drug_name'); ?></th>
<th style="width: 15%;"><?php echo __('batch_number'); ?></th>
<th style="width: 15%;"><?php echo __('expiry_date'); ?></th>
<th style="width: 10%;"><?php echo __('quantity'); ?></th>
<th style="width: 15%;"><?php echo __('cost_price'); ?></th>
<th style="width: 15%;"><?php echo __('total_cost'); ?></th>
<th style="width: 5%;"></th>
</tr>
</thead>
<tbody id="purchaseItemsBody">
<!-- Dynamic Rows -->
</tbody>
<tfoot class="table-light">
<tr>
<td colspan="5" class="text-end fw-bold"><?php echo __('total_amount'); ?>:</td>
<td class="fw-bold text-primary fs-5" id="purchaseTotalDisplay">0.00</td>
<td></td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
<div class="modal-footer bg-white">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?php echo __('cancel'); ?></button>
<button type="submit" class="btn btn-primary px-4"><i class="bi bi-save me-2"></i> <?php echo __('save'); ?></button>
</div>
</div>
</form>
</div>
</div>
<!-- Receive Purchase Modal -->
<div class="modal fade" id="receivePurchaseModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content border-0 shadow">
<div class="modal-header bg-success text-white">
<h5 class="modal-title fw-bold"><i class="bi bi-check-circle me-2"></i> <?php echo __('receive_purchase'); ?></h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body bg-light">
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<?php echo __('receive_warning_msg'); ?>
<?php echo __('ensure_batch_expiry_msg'); ?>
</div>
<input type="hidden" id="receive_purchase_id">
<div class="card border-0 shadow-sm">
<div class="table-responsive">
<table class="table table-bordered mb-0">
<thead class="table-light">
<tr>
<th><?php echo __('drug_name'); ?></th>
<th><?php echo __('batch_number'); ?> <span class="text-danger">*</span></th>
<th><?php echo __('expiry_date'); ?> <span class="text-danger">*</span></th>
<th><?php echo __('quantity'); ?></th>
</tr>
</thead>
<tbody id="receivePurchaseItemsBody"></tbody>
</table>
</div>
</div>
</div>
<div class="modal-footer bg-white">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?php echo __('cancel'); ?></button>
<button type="button" class="btn btn-success px-4" onclick="confirmReceivePurchase()"><i class="bi bi-box-seam me-2"></i> <?php echo __('confirm_received'); ?></button>
</div>
</div>
</div>
</div>
<!-- View Purchase Modal -->
<div class="modal fade" id="viewPurchaseModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content border-0 shadow">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title fw-bold"><i class="bi bi-eye me-2"></i> <?php echo __('view_purchase'); ?> #<span id="view_purchase_id"></span></h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row mb-4">
<div class="col-md-6">
<p class="mb-1 text-muted small"><?php echo __('supplier'); ?></p>
<h6 class="fw-bold" id="view_purchase_supplier"></h6>
</div>
<div class="col-md-6 text-end">
<p class="mb-1 text-muted small"><?php echo __('date'); ?></p>
<h6 class="fw-bold" id="view_purchase_date"></h6>
<span class="badge bg-secondary" id="view_purchase_status"></span>
</div>
</div>
<h6 class="border-bottom pb-2 mb-3 fw-bold text-primary"><?php echo __('items'); ?></h6>
<div class="table-responsive">
<table class="table table-bordered table-sm">
<thead class="table-light">
<tr>
<th><?php echo __('drug_name'); ?></th>
<th><?php echo __('batch_number'); ?></th>
<th><?php echo __('expiry_date'); ?></th>
<th class="text-center"><?php echo __('quantity'); ?></th>
<th class="text-end"><?php echo __('cost_price'); ?></th>
<th class="text-end"><?php echo __('total_cost'); ?></th>
</tr>
</thead>
<tbody id="viewPurchaseItemsBody"></tbody>
<tfoot class="table-light">
<tr>
<td colspan="5" class="text-end fw-bold"><?php echo __('total_amount'); ?></td>
<td class="text-end fw-bold text-primary" id="view_purchase_total"></td>
</tr>
</tfoot>
</table>
</div>
<div class="mt-3">
<p class="mb-1 text-muted small"><?php echo __('notes'); ?></p>
<p class="bg-light p-2 rounded" id="view_purchase_notes"></p>
</div>
</div>
<div class="modal-footer bg-light">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?php echo __('close'); ?></button>
</div>
</div>
</div>
</div>
<script>
let allDrugs = [];
let allSuppliers = [];
let currentPage = 1;
document.addEventListener('DOMContentLoaded', function() {
loadPurchases(1);
fetchSuppliers();
fetchDrugs();
// Calculate total when inputs change in the table
document.getElementById('purchaseItemsBody').addEventListener('input', function(e) {
if (e.target.classList.contains('purchase-qty') || e.target.classList.contains('purchase-price')) {
calculateRowTotal(e.target.closest('tr'));
calculateGrandTotal();
}
});
// Re-initialize Select2 when modal is shown
$('#createPurchaseModal').on('shown.bs.modal', function () {
$('.select2-modal').select2({
dropdownParent: $('#createPurchaseModal'),
width: '100%'
});
});
});
function loadPurchases(page = 1) {
currentPage = page;
fetch('api/pharmacy_lpo.php?action=get_lpos&page=' + page)
.then(response => response.json())
.then(data => {
const tbody = document.getElementById('purchaseTableBody');
const paginationContainer = document.getElementById('paginationContainer');
tbody.innerHTML = '';
const purchases = data.data || (Array.isArray(data) ? data : []);
const totalPages = data.pages || 1;
if (purchases.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="text-center py-4 text-muted"><?php echo __('no_data_found'); ?></td></tr>';
paginationContainer.innerHTML = '';
return;
}
purchases.forEach(p => {
const tr = document.createElement('tr');
let statusBadge = '';
switch(p.status) {
case 'Draft': statusBadge = '<span class="badge bg-secondary">Draft</span>'; break;
case 'Sent': statusBadge = '<span class="badge bg-primary">Sent</span>'; break;
case 'Received': statusBadge = '<span class="badge bg-success">Received</span>'; break;
case 'Cancelled': statusBadge = '<span class="badge bg-danger">Cancelled</span>'; break;
default: statusBadge = `<span class="badge bg-info">${p.status}</span>`;
}
tr.innerHTML = `
<td>${p.id}</td>
<td>${p.lpo_date}</td>
<td>${p.supplier_name || '-'}</td>
<td class="fw-bold text-success">$${parseFloat(p.total_amount).toFixed(2)}</td>
<td>${statusBadge}</td>
<td>
<button class="btn btn-sm btn-outline-primary" onclick="viewPurchase(${p.id}, '${p.supplier_name}', '${p.lpo_date}', '${p.total_amount}', '${p.status}', '${p.notes || ''}')">
<i class="bi bi-eye"></i>
</button>
${p.status === 'Draft' ? `<button class="btn btn-sm btn-outline-success ms-1" onclick="updateStatus(${p.id}, 'Sent')" title="Mark as Sent"><i class="bi bi-send"></i></button>` : ''}
${p.status === 'Sent' ? `<button class="btn btn-sm btn-success ms-1" onclick="openReceiveModal(${p.id})" title="Receive Items"><i class="bi bi-box-seam"></i></button>` : ''}
</td>
`;
tbody.appendChild(tr);
});
renderPagination(currentPage, totalPages);
})
.catch(err => {
console.error(err);
document.getElementById('purchaseTableBody').innerHTML = '<tr><td colspan="6" class="text-center text-danger">Error loading data</td></tr>';
});
}
function renderPagination(current, total) {
const container = document.getElementById('paginationContainer');
if (total <= 1) {
container.innerHTML = '';
return;
}
let html = '<ul class="pagination justify-content-center">';
html += `<li class="page-item ${current <= 1 ? 'disabled' : ''}"><a class="page-link" href="#" onclick="event.preventDefault(); loadPurchases(${current - 1})"><?php echo __('previous'); ?></a></li>`;
let start = Math.max(1, current - 2);
let end = Math.min(total, start + 4);
if (end - start < 4) start = Math.max(1, end - 4);
for (let i = start; i <= end; i++) {
html += `<li class="page-item ${current === i ? 'active' : ''}"><a class="page-link" href="#" onclick="event.preventDefault(); loadPurchases(${i})">${i}</a></li>`;
}
html += `<li class="page-item ${current >= total ? 'disabled' : ''}"><a class="page-link" href="#" onclick="event.preventDefault(); loadPurchases(${current + 1})"><?php echo __('next'); ?></a></li>`;
html += '</ul>';
container.innerHTML = html;
}
function fetchSuppliers() {
fetch('api/pharmacy_lpo.php?action=get_suppliers')
.then(response => response.json())
.then(data => {
allSuppliers = data;
const select = document.getElementById('purchase_supplier_id');
select.innerHTML = '<option value=""><?php echo __('select_supplier'); ?>...</option>';
data.forEach(s => {
select.innerHTML += `<option value="${s.id}">${s.name_en} / ${s.name_ar}</option>`;
});
});
}
function fetchDrugs() {
fetch('api/pharmacy_lpo.php?action=get_drugs')
.then(response => response.json())
.then(data => {
allDrugs = data;
});
}
function openCreatePurchaseModal() {
document.getElementById('createPurchaseForm').reset();
$('#purchase_supplier_id').val('').trigger('change');
document.getElementById('purchaseItemsBody').innerHTML = '';
document.getElementById('purchaseTotalDisplay').innerText = '0.00';
addPurchaseItemRow();
var modal = new bootstrap.Modal(document.getElementById('createPurchaseModal'));
modal.show();
}
function addPurchaseItemRow() {
const tbody = document.getElementById('purchaseItemsBody');
const tr = document.createElement('tr');
let drugOptions = '<option value="">Select Drug...</option>';
allDrugs.forEach(d => {
drugOptions += `<option value="${d.id}" data-price="${d.price}">${d.name_en} (${d.sku || '-'})</option>`;
});
tr.innerHTML = `
<td>
<select name="items[drug_id][]" class="form-select select2-modal-row purchase-drug" onchange="drugSelected(this)" required>
${drugOptions}
</select>
</td>
<td><input type="text" name="items[batch_number][]" class="form-control" placeholder="Optional"></td>
<td><input type="date" name="items[expiry_date][]" class="form-control"></td>
<td><input type="number" name="items[quantity][]" class="form-control purchase-qty" min="1" value="1" required></td>
<td><input type="number" name="items[cost_price][]" class="form-control purchase-price" step="0.01" min="0" value="0.00" required></td>
<td><input type="text" class="form-control purchase-total" readonly value="0.00"></td>
<td class="text-center">
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removePurchaseRow(this)"><i class="bi bi-trash"></i></button>
</td>
`;
tbody.appendChild(tr);
$(tr).find('.select2-modal-row').select2({
dropdownParent: $('#createPurchaseModal'),
width: '100%'
});
}
function removePurchaseRow(btn) {
const tbody = document.getElementById('purchaseItemsBody');
if (tbody.children.length > 1) {
btn.closest('tr').remove();
calculateGrandTotal();
}
}
function drugSelected(select) {
// Optional: Pre-fill last cost price if available (requires API change to fetch history)
}
function calculateRowTotal(row) {
const qty = parseFloat(row.querySelector('.purchase-qty').value) || 0;
const price = parseFloat(row.querySelector('.purchase-price').value) || 0;
const total = qty * price;
row.querySelector('.purchase-total').value = total.toFixed(2);
}
function calculateGrandTotal() {
let total = 0;
document.querySelectorAll('.purchase-total').forEach(input => {
total += parseFloat(input.value) || 0;
});
document.getElementById('purchaseTotalDisplay').innerText = total.toFixed(2);
}
function submitPurchase(e) {
e.preventDefault();
const supplierId = document.getElementById('purchase_supplier_id').value;
const date = document.getElementById('purchase_date').value;
const notes = document.getElementById('purchase_notes').value;
const totalAmount = document.getElementById('purchaseTotalDisplay').innerText;
const items = [];
document.querySelectorAll('#purchaseItemsBody tr').forEach(row => {
const drugId = $(row).find('.purchase-drug').val();
const quantity = row.querySelector('.purchase-qty').value;
const costPrice = row.querySelector('.purchase-price').value;
const totalCost = row.querySelector('.purchase-total').value;
const batchNumber = row.querySelector('input[name="items[batch_number][]"]').value;
const expiryDate = row.querySelector('input[name="items[expiry_date][]"]').value;
if (drugId) {
items.push({
drug_id: drugId,
quantity: quantity,
cost_price: costPrice,
total_cost: totalCost,
batch_number: batchNumber,
expiry_date: expiryDate
});
}
});
if (items.length === 0) {
alert('Please add at least one item.');
return;
}
fetch('api/pharmacy_lpo.php?action=create_lpo', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ supplier_id: supplierId, lpo_date: date, notes: notes, total_amount: totalAmount, items: items })
})
.then(response => response.json())
.then(data => {
if (data.success) {
bootstrap.Modal.getInstance(document.getElementById('createPurchaseModal')).hide();
loadPurchases();
} else {
alert('Error: ' + (data.error || 'Unknown error'));
}
});
}
function viewPurchase(id, supplier, date, total, status, notes) {
document.getElementById('view_purchase_id').innerText = id;
document.getElementById('view_purchase_supplier').innerText = supplier;
document.getElementById('view_purchase_date').innerText = date;
document.getElementById('view_purchase_total').innerText = '$' + parseFloat(total).toFixed(2);
document.getElementById('view_purchase_status').innerText = status;
document.getElementById('view_purchase_notes').innerText = notes;
const tbody = document.getElementById('viewPurchaseItemsBody');
tbody.innerHTML = '<tr><td colspan="6" class="text-center">Loading...</td></tr>';
var modal = new bootstrap.Modal(document.getElementById('viewPurchaseModal'));
modal.show();
fetch('api/pharmacy_lpo.php?action=get_lpo_details&id=' + id)
.then(response => response.json())
.then(data => {
tbody.innerHTML = '';
data.forEach(item => {
tbody.innerHTML += `
<tr>
<td>${item.drug_name} <small class="text-muted">(${item.sku || '-'})</small></td>
<td>${item.batch_number || '-'}</td>
<td>${item.expiry_date || '-'}</td>
<td class="text-center">${item.quantity}</td>
<td class="text-end">$${parseFloat(item.cost_price).toFixed(2)}</td>
<td class="text-end">$${parseFloat(item.total_cost).toFixed(2)}</td>
</tr>
`;
});
});
}
function updateStatus(id, newStatus) {
if (!confirm('Are you sure?')) return;
fetch('api/pharmacy_lpo.php?action=update_status', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: id, status: newStatus })
})
.then(response => response.json())
.then(data => {
if (data.success) loadPurchases();
else alert('Error updating status');
});
}
// Receive Flow
let currentReceiveId = 0;
function openReceiveModal(id) {
currentReceiveId = id;
document.getElementById('receive_purchase_id').value = id;
const tbody = document.getElementById('receivePurchaseItemsBody');
tbody.innerHTML = '<tr><td colspan="4" class="text-center">Loading items...</td></tr>';
var modal = new bootstrap.Modal(document.getElementById('receivePurchaseModal'));
modal.show();
fetch('api/pharmacy_lpo.php?action=get_lpo_details&id=' + id)
.then(response => response.json())
.then(data => {
tbody.innerHTML = '';
data.forEach(item => {
const batchVal = item.batch_number || '';
const expiryVal = item.expiry_date || '';
// If missing, generate default suggestion (e.g. current date + 1 year) or empty?
// Let's leave empty to force user check, or prefill if available.
tbody.innerHTML += `
<tr data-item-id="${item.id}">
<td>
${item.drug_name}
<div class="small text-muted">SKU: ${item.sku || '-'}</div>
</td>
<td>
<input type="text" class="form-control receive-batch" value="${batchVal}" required>
</td>
<td>
<input type="date" class="form-control receive-expiry" value="${expiryVal}" required>
</td>
<td class="text-center align-middle">${item.quantity}</td>
</tr>
`;
});
});
}
function confirmReceivePurchase() {
const id = document.getElementById('receive_purchase_id').value;
const items = [];
let valid = true;
document.querySelectorAll('#receivePurchaseItemsBody tr').forEach(row => {
const itemId = row.getAttribute('data-item-id');
const batch = row.querySelector('.receive-batch').value.trim();
const expiry = row.querySelector('.receive-expiry').value;
if (!batch || !expiry) {
valid = false;
row.querySelector('.receive-batch').classList.add('is-invalid');
row.querySelector('.receive-expiry').classList.add('is-invalid');
} else {
row.querySelector('.receive-batch').classList.remove('is-invalid');
row.querySelector('.receive-expiry').classList.remove('is-invalid');
}
items.push({
id: itemId,
batch_number: batch,
expiry_date: expiry
});
});
if (!valid) {
alert('Please fill in Batch Number and Expiry Date for all items.');
return;
}
if (!confirm('Are you sure you want to mark this purchase as Received? Stock will be updated.')) return;
fetch('api/pharmacy_lpo.php?action=update_status', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: id,
status: 'Received',
items: items
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
bootstrap.Modal.getInstance(document.getElementById('receivePurchaseModal')).hide();
loadPurchases();
} else {
alert('Error: ' + (data.error || 'Unknown error'));
}
})
.catch(err => {
console.error(err);
alert('Failed to receive purchase');
});
}
</script>

View File

@ -0,0 +1,349 @@
<?php
$section = 'pharmacy_reports';
?>
<div class="container-fluid">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold text-dark mb-0">
<i class="bi bi-file-earmark-bar-graph me-2 text-primary"></i> <?php echo __('pharmacy_reports'); ?>
</h2>
</div>
<div class="card shadow-sm mb-4">
<div class="card-body">
<form id="reportForm" class="row g-3 align-items-end">
<div class="col-md-3">
<label for="reportType" class="form-label"><?php echo __('report_type'); ?></label>
<select class="form-select" id="reportType" name="type" required>
<option value="inventory_valuation"><?php echo __('inventory_valuation'); ?></option>
<option value="sales"><?php echo __('sales_report'); ?></option>
<option value="purchase_report"><?php echo __('purchase_report'); ?></option>
<option value="expiry"><?php echo __('expiry_report'); ?></option>
<option value="low_stock"><?php echo __('low_stock_report'); ?></option>
</select>
</div>
<div class="col-md-3 date-range-group" style="display: none;">
<label for="startDate" class="form-label"><?php echo __('start_date'); ?></label>
<input type="date" class="form-control" id="startDate" name="start_date" value="<?php echo date('Y-m-01'); ?>">
</div>
<div class="col-md-3 date-range-group" style="display: none;">
<label for="endDate" class="form-label"><?php echo __('end_date'); ?></label>
<input type="date" class="form-control" id="endDate" name="end_date" value="<?php echo date('Y-m-d'); ?>">
</div>
<div class="col-md-3">
<button type="button" class="btn btn-primary me-2" onclick="generateReport()">
<i class="bi bi-table"></i> <?php echo __('generate'); ?>
</button>
<button type="button" class="btn btn-secondary" onclick="printReport()">
<i class="bi bi-printer"></i> <?php echo __('print'); ?>
</button>
</div>
</form>
</div>
</div>
<div class="card shadow-sm" id="reportResultCard" style="display: none;">
<div class="card-header bg-white py-3">
<h5 class="card-title mb-0" id="reportTitle"></h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover table-bordered" id="reportTable">
<thead class="table-light">
<tr id="tableHeaders"></tr>
</thead>
<tbody id="tableBody"></tbody>
<tfoot id="tableFooter" class="table-light fw-bold"></tfoot>
</table>
</div>
<!-- Pagination Controls -->
<div id="paginationContainer" class="mt-3"></div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const reportType = document.getElementById('reportType');
const dateRangeGroups = document.querySelectorAll('.date-range-group');
function toggleDateInputs() {
const val = reportType.value;
if (val === 'sales' || val === 'expiry' || val === 'purchase_report') {
dateRangeGroups.forEach(el => el.style.display = 'block');
} else {
dateRangeGroups.forEach(el => el.style.display = 'none');
}
}
reportType.addEventListener('change', toggleDateInputs);
toggleDateInputs(); // Init
});
let currentPage = 1;
const limit = 20;
function generateReport(page = 1) {
currentPage = page;
const type = document.getElementById('reportType').value;
const startDate = document.getElementById('startDate').value;
const endDate = document.getElementById('endDate').value;
document.getElementById('reportResultCard').style.display = 'block';
document.getElementById('tableBody').innerHTML = '<tr><td colspan="10" class="text-center p-4"><div class="spinner-border text-primary" role="status"></div><p class="mt-2 mb-0"><?php echo __('loading'); ?>...</p></td></tr>';
const params = new URLSearchParams({
action: 'get_report',
type: type,
start_date: startDate,
end_date: endDate,
page: page,
limit: limit
});
fetch('api/pharmacy.php?' + params.toString())
.then(response => response.json())
.then(data => {
renderTable(type, data);
})
.catch(error => {
console.error('Error:', error);
document.getElementById('tableBody').innerHTML = `<tr><td colspan="10" class="text-center text-danger p-3">${error.message}</td></tr>`;
});
}
function renderTable(type, response) {
const headersRow = document.getElementById('tableHeaders');
const tbody = document.getElementById('tableBody');
const tfoot = document.getElementById('tableFooter');
headersRow.innerHTML = '';
tbody.innerHTML = '';
tfoot.innerHTML = '';
const data = response.data || [];
const total = response.total || 0;
if (data.length === 0) {
tbody.innerHTML = '<tr><td colspan="10" class="text-center text-muted py-4"><?php echo __('no_records_found'); ?></td></tr>';
document.getElementById('paginationContainer').innerHTML = '';
return;
}
if (type === 'inventory_valuation') {
headersRow.innerHTML = `
<th><?php echo __('drug_name'); ?></th>
<th><?php echo __('category'); ?></th>
<th><?php echo __('current_stock'); ?></th>
<th><?php echo __('avg_cost'); ?></th>
<th><?php echo __('selling_price'); ?></th>
<th><?php echo __('total_cost_value'); ?></th>
<th><?php echo __('total_sales_value'); ?></th>
`;
data.forEach(row => {
const costVal = parseFloat(row.total_cost_value || 0);
const salesVal = parseFloat(row.total_sales_value || 0);
tbody.innerHTML += `
<tr>
<td>${row.drug_name || ''} <small class="text-muted">(${row.drug_name_ar || ''})</small></td>
<td>${row.category_name || '-'}</td>
<td>${row.stock_quantity || 0}</td>
<td>${parseFloat(row.avg_cost || 0).toFixed(2)}</td>
<td>${parseFloat(row.selling_price || 0).toFixed(2)}</td>
<td>${costVal.toFixed(2)}</td>
<td>${salesVal.toFixed(2)}</td>
</tr>
`;
});
if (response.grand_total_cost) {
tfoot.innerHTML = `
<tr>
<td colspan="5" class="text-end"><?php echo __('grand_total'); ?>:</td>
<td>${parseFloat(response.grand_total_cost).toFixed(2)}</td>
<td>${parseFloat(response.grand_total_sales).toFixed(2)}</td>
</tr>
`;
}
} else if (type === 'sales') {
headersRow.innerHTML = `
<th><?php echo __('date'); ?></th>
<th><?php echo __('receipt_no'); ?></th>
<th><?php echo __('patient'); ?></th>
<th><?php echo __('items'); ?></th>
<th><?php echo __('total_amount'); ?></th>
<th><?php echo __('payment_method'); ?></th>
`;
data.forEach(row => {
tbody.innerHTML += `
<tr>
<td>${row.created_at}</td>
<td>#${row.id}</td>
<td>${row.patient_name || 'Guest'}</td>
<td>${row.item_count || 1}</td>
<td>${parseFloat(row.total_amount).toFixed(2)}</td>
<td>${row.payment_method || '-'}</td>
</tr>
`;
});
if (response.grand_total_sales) {
tfoot.innerHTML = `
<tr>
<td colspan="4" class="text-end"><?php echo __('grand_total'); ?>:</td>
<td colspan="2">${parseFloat(response.grand_total_sales).toFixed(2)}</td>
</tr>
`;
}
} else if (type === 'purchase_report') {
headersRow.innerHTML = `
<th><?php echo __('date'); ?></th>
<th><?php echo __('lpo'); ?> #</th>
<th><?php echo __('supplier'); ?></th>
<th><?php echo __('status'); ?></th>
<th><?php echo __('total_amount'); ?></th>
`;
data.forEach(row => {
let statusBadge = '';
switch(row.status) {
case 'Draft': statusBadge = '<span class="badge bg-secondary">Draft</span>'; break;
case 'Sent': statusBadge = '<span class="badge bg-primary">Sent</span>'; break;
case 'Received': statusBadge = '<span class="badge bg-success">Received</span>'; break;
case 'Cancelled': statusBadge = '<span class="badge bg-danger">Cancelled</span>'; break;
default: statusBadge = `<span class="badge bg-info">${row.status}</span>`;
}
tbody.innerHTML += `
<tr>
<td>${row.lpo_date}</td>
<td>#${row.id}</td>
<td>${row.supplier_name || '-'}</td>
<td>${statusBadge}</td>
<td>${parseFloat(row.total_amount).toFixed(2)}</td>
</tr>
`;
});
if (response.grand_total_purchases) {
tfoot.innerHTML = `
<tr>
<td colspan="4" class="text-end"><?php echo __('grand_total'); ?>:</td>
<td>${parseFloat(response.grand_total_purchases).toFixed(2)}</td>
</tr>
`;
}
} else if (type === 'expiry') {
headersRow.innerHTML = `
<th><?php echo __('drug_name'); ?></th>
<th><?php echo __('batch_number'); ?></th>
<th><?php echo __('expiry_date'); ?></th>
<th><?php echo __('quantity'); ?></th>
<th><?php echo __('supplier'); ?></th>
<th><?php echo __('days_remaining'); ?></th>
`;
data.forEach(row => {
const days = parseInt(row.days_remaining);
const statusClass = days < 0 ? 'text-danger fw-bold' : (days < 90 ? 'text-warning fw-bold' : '');
tbody.innerHTML += `
<tr>
<td>${row.drug_name}</td>
<td>${row.batch_number}</td>
<td>${row.expiry_date}</td>
<td>${row.quantity}</td>
<td>${row.supplier_name || '-'}</td>
<td class="${statusClass}">${days} <?php echo __('days'); ?></td>
</tr>
`;
});
} else if (type === 'low_stock') {
headersRow.innerHTML = `
<th><?php echo __('drug_name'); ?></th>
<th><?php echo __('current_stock'); ?></th>
<th><?php echo __('min_stock_level'); ?></th>
<th><?php echo __('reorder_level'); ?></th>
<th><?php echo __('unit'); ?></th>
<th><?php echo __('status'); ?></th>
`;
data.forEach(row => {
tbody.innerHTML += `
<tr>
<td>${row.name_en}</td>
<td>${row.total_stock}</td>
<td>${row.min_stock_level || 0}</td>
<td>${row.reorder_level || 0}</td>
<td>${row.unit || '-'}</td>
<td><span class="badge bg-danger"><?php echo __('low_stock'); ?></span></td>
</tr>
`;
});
}
renderPagination(total, limit, currentPage);
}
function renderPagination(totalItems, itemsPerPage, currentPage) {
const totalPages = Math.ceil(totalItems / itemsPerPage);
const paginationContainer = document.getElementById('paginationContainer');
paginationContainer.innerHTML = '';
if (totalPages <= 1) return;
let paginationHTML = '<nav aria-label="Page navigation"><ul class="pagination justify-content-center">';
// Previous Button
paginationHTML += `
<li class="page-item ${currentPage === 1 ? 'disabled' : ''}">
<a class="page-link" href="#" onclick="generateReport(${currentPage - 1}); return false;" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
`;
// Page Numbers
for (let i = 1; i <= totalPages; i++) {
if (i === 1 || i === totalPages || (i >= currentPage - 2 && i <= currentPage + 2)) {
paginationHTML += `
<li class="page-item ${i === currentPage ? 'active' : ''}">
<a class="page-link" href="#" onclick="generateReport(${i}); return false;">${i}</a>
</li>
`;
} else if (i === currentPage - 3 || i === currentPage + 3) {
paginationHTML += '<li class="page-item disabled"><span class="page-link">...</span></li>';
}
}
// Next Button
paginationHTML += `
<li class="page-item ${currentPage === totalPages ? 'disabled' : ''}">
<a class="page-link" href="#" onclick="generateReport(${currentPage + 1}); return false;" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
`;
paginationHTML += '</ul></nav>';
paginationContainer.innerHTML = paginationHTML;
}
function printReport() {
const type = document.getElementById('reportType').value;
const startDate = document.getElementById('startDate').value;
const endDate = document.getElementById('endDate').value;
const url = `print_pharmacy_report.php?type=${type}&start_date=${startDate}&end_date=${endDate}`;
// Open in new window/tab
window.open(url, '_blank', 'width=1000,height=800');
}
</script>

102
lang.php
View File

@ -206,16 +206,6 @@ $translations = [
'company_favicon' => 'Company Favicon',
'save_changes' => 'Save Changes',
'settings_updated_successfully' => 'Settings updated successfully',
'attachment' => 'Attachment',
'image' => 'Image',
'view_current' => 'View Current',
'view_image' => 'View Image',
'view_results' => 'View Results',
'no_data_found' => 'No data found',
'close' => 'Close',
'results' => 'Results',
'laboratory_inquiries' => 'Laboratory Inquiries',
'xray_inquiries' => 'X-Ray Inquiries',
'prescriptions' => 'Prescriptions',
'add_drug' => 'Add Drug',
'drug_name' => 'Drug Name',
@ -364,7 +354,51 @@ $translations = [
'total_cost' => 'Total Cost',
'draft' => 'Draft',
'sent' => 'Sent',
'received' => 'Received'
'received' => 'Received',
'pharmacy_alerts' => 'Pharmacy Alerts',
'alerts' => 'Alerts',
'expired_batches' => 'Expired Batches',
'near_expiry' => 'Near Expiry',
'days_remaining' => 'Days Remaining',
'show_expiring_within' => 'Show expiring within (days):',
'days' => 'Days',
'no_low_stock_found' => 'No low stock items found',
'no_expired_batches_found' => 'No expired batches found',
'no_batches_expiring_soon' => 'No batches expiring soon',
'order_stock' => 'Order Stock',
'these_batches_expired' => 'These batches have expired and should be removed from stock.',
'loading' => 'Loading...',
'pharmacy_reports' => 'Pharmacy Reports',
'reports' => 'Reports',
'inventory_valuation' => 'Inventory Valuation',
'sales_report' => 'Sales Report',
'expiry_report' => 'Expiry Report',
'low_stock_report' => 'Low Stock Report',
'current_stock' => 'Current Stock',
'avg_cost' => 'Avg Cost',
'selling_price' => 'Selling Price',
'total_cost_value' => 'Total Cost Value',
'total_sales_value' => 'Total Sales Value',
'grand_total' => 'Grand Total',
'category' => 'Category',
'generate' => 'Generate',
'receipt_no' => 'Receipt No',
'no_records_found' => 'No records found',
'purchases' => 'Purchases',
'create_purchase' => 'Create Purchase',
'view_purchase' => 'View Purchase',
'receive_purchase' => 'Receive Purchase',
'purchase_date' => 'Purchase Date',
'purchase_returns' => 'Purchase Returns',
'create_return' => 'Create Return',
'view_return' => 'View Return',
'return_date' => 'Return Date',
'unit_price' => 'Unit Price',
'total_price' => 'Total Price',
'receive_warning_msg' => 'Receiving this purchase will add items to stock.',
'ensure_batch_expiry_msg' => 'Ensure Batch Numbers and Expiry Dates are correct.',
'confirm_received' => 'Confirm Received',
'purchase_report' => 'Purchase Report'
],
'ar' => [
'attachment' => 'المرفق',
@ -735,6 +769,50 @@ $translations = [
'total_cost' => 'التكلفة الإجمالية',
'draft' => 'مسودة',
'sent' => 'تم الإرسال',
'received' => 'تم الاستلام'
'received' => 'تم الاستلام',
'pharmacy_alerts' => 'تنبيهات الصيدلية',
'alerts' => 'تنبيهات',
'expired_batches' => 'التشغيلات المنتهية',
'near_expiry' => 'قرب الانتهاء',
'days_remaining' => 'الأيام المتبقية',
'show_expiring_within' => 'عرض المنتهي خلال (يوم):',
'days' => 'أيام',
'no_low_stock_found' => 'لا توجد أصناف منخفضة المخزون',
'no_expired_batches_found' => 'لا توجد تشغيلات منتهية',
'no_batches_expiring_soon' => 'لا توجد تشغيلات ستنتهي قريباً',
'order_stock' => 'طلب مخزون',
'these_batches_expired' => 'هذه التشغيلات منتهية الصلاحية ويجب إزالتها من المخزون.',
'loading' => 'جاري التحميل...',
'pharmacy_reports' => 'تقارير الصيدلية',
'reports' => 'التقارير',
'inventory_valuation' => 'تقييم المخزون',
'sales_report' => 'تقرير المبيعات',
'expiry_report' => 'تقرير الصلاحية',
'low_stock_report' => 'تقرير المخزون المنخفض',
'current_stock' => 'المخزون الحالي',
'avg_cost' => 'متوسط التكلفة',
'selling_price' => 'سعر البيع',
'total_cost_value' => 'إجمالي قيمة التكلفة',
'total_sales_value' => 'إجمالي قيمة البيع',
'grand_total' => 'المجموع الكلي',
'category' => 'الفئة',
'generate' => 'إنشاء',
'receipt_no' => 'رقم الإيصال',
'no_records_found' => 'لم يتم العثور على سجلات',
'purchases' => 'المشتريات',
'create_purchase' => 'إضافة شراء',
'view_purchase' => 'عرض الشراء',
'receive_purchase' => 'استلام الشراء',
'purchase_date' => 'تاريخ الشراء',
'purchase_returns' => 'مرتجعات المشتريات',
'create_return' => 'إضافة مرتجع',
'view_return' => 'عرض المرتجع',
'return_date' => 'تاريخ الإرجاع',
'unit_price' => 'سعر الوحدة',
'total_price' => 'السعر الإجمالي',
'receive_warning_msg' => 'استلام هذا الشراء سيضيف العناصر إلى المخزون.',
'ensure_batch_expiry_msg' => 'تأكد من صحة أرقام التشغيلات وتواريخ الانتهاء.',
'confirm_received' => 'تأكيد الاستلام',
'purchase_report' => 'تقرير المشتريات'
]
];

6
pharmacy_alerts.php Normal file
View File

@ -0,0 +1,6 @@
<?php
$section = 'pharmacy_alerts';
require_once 'includes/layout/header.php';
require_once 'includes/pages/pharmacy_alerts.php';
require_once 'includes/layout/footer.php';
?>

View File

@ -0,0 +1,10 @@
<?php
require_once __DIR__ . '/includes/layout/header.php';
$section = 'pharmacy_purchase_returns';
?>
<div class="container-fluid">
<?php require_once __DIR__ . '/includes/pages/pharmacy_purchase_returns.php'; ?>
</div>
<?php require_once __DIR__ . '/includes/layout/footer.php'; ?>

10
pharmacy_purchases.php Normal file
View File

@ -0,0 +1,10 @@
<?php
require_once __DIR__ . '/includes/layout/header.php';
$section = 'pharmacy_purchases';
?>
<div class="container-fluid">
<?php require_once __DIR__ . '/includes/pages/pharmacy_purchases.php'; ?>
</div>
<?php require_once __DIR__ . '/includes/layout/footer.php'; ?>

12
pharmacy_reports.php Normal file
View File

@ -0,0 +1,12 @@
<?php
require_once __DIR__ . '/includes/layout/header.php';
// Check if user is logged in (if not already handled in header or config)
// Access control can be added here
$section = 'pharmacy_reports';
// Include the content page
require_once __DIR__ . '/includes/pages/pharmacy_reports.php';
require_once __DIR__ . '/includes/layout/footer.php';

277
print_pharmacy_report.php Normal file
View File

@ -0,0 +1,277 @@
<?php
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/helpers.php';
$pdo = db();
$lang = $_SESSION['lang'] ?? 'en';
$type = $_GET['type'] ?? 'inventory_valuation';
$startDate = $_GET['start_date'] ?? date('Y-m-01');
$endDate = $_GET['end_date'] ?? date('Y-m-d');
// Fetch company settings
$stmt = $pdo->query("SELECT setting_key, setting_value FROM settings WHERE setting_key IN ('company_name', 'company_logo')");
$site_settings = [];
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$site_settings[$row['setting_key']] = $row['setting_value'];
}
$companyName = $site_settings['company_name'] ?? __('hospital_management');
// Helper to translate report titles
function getReportTitle($type) {
switch ($type) {
case 'inventory_valuation': return __('inventory_valuation');
case 'sales': return __('sales_report');
case 'expiry': return __('expiry_report');
case 'low_stock': return __('low_stock_report');
case 'purchase_report': return __('purchase_report');
default: return __('report');
}
}
// Data Fetching Logic (No pagination for print, or very high limit)
$limit = 1000;
$data = [];
$grandTotals = [];
if ($type === 'inventory_valuation') {
$sql = "SELECT d.name_en as drug_name, d.name_ar as drug_name_ar,
g.name_en as category_name,
SUM(b.quantity) as stock_quantity,
SUM(b.quantity * b.cost_price) / SUM(b.quantity) as avg_cost,
SUM(b.quantity * b.sale_price) / SUM(b.quantity) as selling_price,
SUM(b.quantity * b.cost_price) as total_cost_value,
SUM(b.quantity * b.sale_price) as total_sales_value
FROM drugs d
JOIN pharmacy_batches b ON d.id = b.drug_id
LEFT JOIN drugs_groups g ON d.group_id = g.id
WHERE b.quantity > 0
GROUP BY d.id
ORDER BY d.name_en ASC
LIMIT $limit";
$stmt = $pdo->query($sql);
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);
$grandTotalSql = "SELECT SUM(b.quantity * b.cost_price) as total_cost,
SUM(b.quantity * b.sale_price) as total_sales
FROM pharmacy_batches b
WHERE b.quantity > 0";
$grandTotals = $pdo->query($grandTotalSql)->fetch(PDO::FETCH_ASSOC);
} elseif ($type === 'sales') {
$sql = "SELECT s.id, s.created_at, s.total_amount, s.payment_method,
p.name as patient_name,
(SELECT COUNT(*) FROM pharmacy_sale_items i WHERE i.sale_id = s.id) as item_count
FROM pharmacy_sales s
LEFT JOIN patients p ON s.patient_id = p.id
WHERE s.created_at BETWEEN ? AND ? + INTERVAL 1 DAY
ORDER BY s.created_at DESC
LIMIT $limit";
$stmt = $pdo->prepare($sql);
$stmt->execute([$startDate, $endDate]);
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);
$grandTotalSql = "SELECT SUM(total_amount) as total FROM pharmacy_sales WHERE created_at BETWEEN ? AND ? + INTERVAL 1 DAY";
$grandTotalStmt = $pdo->prepare($grandTotalSql);
$grandTotalStmt->execute([$startDate, $endDate]);
$grandTotals['total_sales'] = $grandTotalStmt->fetchColumn();
} elseif ($type === 'expiry') {
$sql = "SELECT b.id, b.batch_number, b.expiry_date, b.quantity,
d.name_en as drug_name, d.name_ar as drug_name_ar,
s.name_en as supplier_name,
DATEDIFF(b.expiry_date, CURDATE()) as days_remaining
FROM pharmacy_batches b
JOIN drugs d ON b.drug_id = d.id
LEFT JOIN suppliers s ON b.supplier_id = s.id
WHERE b.expiry_date BETWEEN ? AND ? AND b.quantity > 0
ORDER BY b.expiry_date ASC
LIMIT $limit";
$stmt = $pdo->prepare($sql);
$stmt->execute([$startDate, $endDate]);
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);
} elseif ($type === 'low_stock') {
$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
HAVING total_stock <= MAX(d.reorder_level)
ORDER BY total_stock ASC
LIMIT $limit";
$stmt = $pdo->query($sql);
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);
} elseif ($type === 'purchase_report') {
$sql = "SELECT l.id, l.lpo_date, l.status, l.total_amount, s.name_en as supplier_name
FROM pharmacy_lpos l
LEFT JOIN suppliers s ON l.supplier_id = s.id
WHERE l.lpo_date BETWEEN ? AND ?
ORDER BY l.lpo_date DESC
LIMIT $limit";
$stmt = $pdo->prepare($sql);
$stmt->execute([$startDate, $endDate]);
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);
$grandTotalSql = "SELECT SUM(total_amount) as total FROM pharmacy_lpos WHERE lpo_date BETWEEN ? AND ?";
$grandTotalStmt = $pdo->prepare($grandTotalSql);
$grandTotalStmt->execute([$startDate, $endDate]);
$grandTotals['total_purchases'] = $grandTotalStmt->fetchColumn();
}
?>
<!DOCTYPE html>
<html lang="<?php echo $lang; ?>" dir="<?php echo get_dir(); ?>">
<head>
<meta charset="UTF-8">
<title><?php echo getReportTitle($type); ?></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<?php if (is_rtl()): ?>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.rtl.min.css">
<?php endif; ?>
<style>
body { font-size: 14px; background: white; }
.table thead th { background-color: #f8f9fa !important; border-bottom: 2px solid #dee2e6; -webkit-print-color-adjust: exact; }
@media print {
.no-print { display: none !important; }
.card { border: none !important; shadow: none !important; }
body { padding: 0; }
}
</style>
</head>
<body onload="window.print()">
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h3 class="fw-bold mb-1"><?php echo htmlspecialchars($companyName); ?></h3>
<h5 class="text-muted"><?php echo getReportTitle($type); ?></h5>
</div>
<div class="text-end">
<p class="mb-0 text-muted"><?php echo __('printed_on'); ?>: <?php echo date('Y-m-d H:i'); ?></p>
<?php if (in_array($type, ['sales', 'expiry', 'purchase_report'])): ?>
<p class="mb-0 text-muted"><?php echo __('date'); ?>: <?php echo $startDate; ?> - <?php echo $endDate; ?></p>
<?php endif; ?>
</div>
</div>
<table class="table table-bordered table-striped">
<thead>
<?php if ($type === 'inventory_valuation'): ?>
<tr>
<th><?php echo __('drug_name'); ?></th>
<th><?php echo __('category'); ?></th>
<th><?php echo __('current_stock'); ?></th>
<th><?php echo __('avg_cost'); ?></th>
<th><?php echo __('selling_price'); ?></th>
<th><?php echo __('total_cost_value'); ?></th>
<th><?php echo __('total_sales_value'); ?></th>
</tr>
<?php elseif ($type === 'sales'): ?>
<tr>
<th><?php echo __('date'); ?></th>
<th><?php echo __('receipt_no'); ?></th>
<th><?php echo __('patient'); ?></th>
<th><?php echo __('items'); ?></th>
<th><?php echo __('total_amount'); ?></th>
<th><?php echo __('payment_method'); ?></th>
</tr>
<?php elseif ($type === 'expiry'): ?>
<tr>
<th><?php echo __('drug_name'); ?></th>
<th><?php echo __('batch_number'); ?></th>
<th><?php echo __('expiry_date'); ?></th>
<th><?php echo __('quantity'); ?></th>
<th><?php echo __('supplier'); ?></th>
<th><?php echo __('days_remaining'); ?></th>
</tr>
<?php elseif ($type === 'low_stock'): ?>
<tr>
<th><?php echo __('drug_name'); ?></th>
<th><?php echo __('current_stock'); ?></th>
<th><?php echo __('min_stock_level'); ?></th>
<th><?php echo __('reorder_level'); ?></th>
<th><?php echo __('unit'); ?></th>
</tr>
<?php elseif ($type === 'purchase_report'): ?>
<tr>
<th><?php echo __('date'); ?></th>
<th><?php echo __('lpo'); ?> #</th>
<th><?php echo __('supplier'); ?></th>
<th><?php echo __('status'); ?></th>
<th><?php echo __('total_amount'); ?></th>
</tr>
<?php endif; ?>
</thead>
<tbody>
<?php foreach ($data as $row): ?>
<tr>
<?php if ($type === 'inventory_valuation'): ?>
<td><?php echo htmlspecialchars($row['drug_name']); ?></td>
<td><?php echo htmlspecialchars($row['category_name'] ?? '-'); ?></td>
<td><?php echo $row['stock_quantity']; ?></td>
<td><?php echo number_format($row['avg_cost'], 2); ?></td>
<td><?php echo number_format($row['selling_price'], 2); ?></td>
<td><?php echo number_format($row['total_cost_value'], 2); ?></td>
<td><?php echo number_format($row['total_sales_value'], 2); ?></td>
<?php elseif ($type === 'sales'): ?>
<td><?php echo $row['created_at']; ?></td>
<td>#<?php echo $row['id']; ?></td>
<td><?php echo htmlspecialchars($row['patient_name'] ?? 'Guest'); ?></td>
<td><?php echo $row['item_count']; ?></td>
<td><?php echo number_format($row['total_amount'], 2); ?></td>
<td><?php echo htmlspecialchars($row['payment_method']); ?></td>
<?php elseif ($type === 'expiry'): ?>
<td><?php echo htmlspecialchars($row['drug_name']); ?></td>
<td><?php echo htmlspecialchars($row['batch_number']); ?></td>
<td><?php echo $row['expiry_date']; ?></td>
<td><?php echo $row['quantity']; ?></td>
<td><?php echo htmlspecialchars($row['supplier_name'] ?? '-'); ?></td>
<td><?php echo $row['days_remaining']; ?></td>
<?php elseif ($type === 'low_stock'): ?>
<td><?php echo htmlspecialchars($row['name_en']); ?></td>
<td><?php echo $row['total_stock']; ?></td>
<td><?php echo $row['min_stock_level']; ?></td>
<td><?php echo $row['reorder_level']; ?></td>
<td><?php echo $row['unit'] ?? '-'; ?></td>
<?php elseif ($type === 'purchase_report'): ?>
<td><?php echo $row['lpo_date']; ?></td>
<td>#<?php echo $row['id']; ?></td>
<td><?php echo htmlspecialchars($row['supplier_name'] ?? '-'); ?></td>
<td><?php echo htmlspecialchars($row['status']); ?></td>
<td><?php echo number_format($row['total_amount'], 2); ?></td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
<?php if (empty($data)): ?>
<tr><td colspan="7" class="text-center py-3"><?php echo __('no_records_found'); ?></td></tr>
<?php endif; ?>
</tbody>
<?php if (!empty($grandTotals) || isset($grandTotals['total_sales']) || isset($grandTotals['total_purchases'])): ?>
<tfoot>
<?php if ($type === 'inventory_valuation'): ?>
<tr class="fw-bold">
<td colspan="5" class="text-end"><?php echo __('grand_total'); ?>:</td>
<td><?php echo number_format($grandTotals['total_cost'] ?? 0, 2); ?></td>
<td><?php echo number_format($grandTotals['total_sales'] ?? 0, 2); ?></td>
</tr>
<?php elseif ($type === 'sales'): ?>
<tr class="fw-bold">
<td colspan="4" class="text-end"><?php echo __('grand_total'); ?>:</td>
<td colspan="2"><?php echo number_format($grandTotals['total_sales'] ?? 0, 2); ?></td>
</tr>
<?php elseif ($type === 'purchase_report'): ?>
<tr class="fw-bold">
<td colspan="4" class="text-end"><?php echo __('grand_total'); ?>:</td>
<td><?php echo number_format($grandTotals['total_purchases'] ?? 0, 2); ?></td>
</tr>
<?php endif; ?>
</tfoot>
<?php endif; ?>
</table>
</div>
</body>
</html>