add purchase to pharmacy
This commit is contained in:
parent
fd41c8937a
commit
dad73767a1
296
api/pharmacy.php
296
api/pharmacy.php
@ -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");
|
||||
}
|
||||
|
||||
@ -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,23 +35,129 @@ 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);
|
||||
if (empty($data['id']) || empty($data['status'])) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
29
db/migrations/20260321_create_purchase_returns.sql
Normal file
29
db/migrations/20260321_create_purchase_returns.sql
Normal 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
|
||||
);
|
||||
@ -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>
|
||||
|
||||
308
includes/pages/pharmacy_alerts.php
Normal file
308
includes/pages/pharmacy_alerts.php
Normal 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">«</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">»</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>
|
||||
469
includes/pages/pharmacy_purchase_returns.php
Normal file
469
includes/pages/pharmacy_purchase_returns.php
Normal 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>
|
||||
599
includes/pages/pharmacy_purchases.php
Normal file
599
includes/pages/pharmacy_purchases.php
Normal 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>
|
||||
349
includes/pages/pharmacy_reports.php
Normal file
349
includes/pages/pharmacy_reports.php
Normal 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">«</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">»</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
102
lang.php
@ -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
6
pharmacy_alerts.php
Normal 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';
|
||||
?>
|
||||
10
pharmacy_purchase_returns.php
Normal file
10
pharmacy_purchase_returns.php
Normal 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
10
pharmacy_purchases.php
Normal 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
12
pharmacy_reports.php
Normal 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
277
print_pharmacy_report.php
Normal 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>
|
||||
Loading…
x
Reference in New Issue
Block a user