diff --git a/api/patients.php b/api/patients.php new file mode 100644 index 0000000..52f8ad4 --- /dev/null +++ b/api/patients.php @@ -0,0 +1,30 @@ +prepare("SELECT id, name, phone FROM patients WHERE name LIKE ? OR phone LIKE ? LIMIT 20"); + $term = "%$q%"; + $stmt->execute([$term, $term]); + $results = $stmt->fetchAll(PDO::FETCH_ASSOC); + echo json_encode($results); + break; + + default: + http_response_code(400); + echo json_encode(['error' => 'Invalid action']); + } +} catch (Exception $e) { + http_response_code(500); + echo json_encode(['error' => $e->getMessage()]); +} diff --git a/api/pharmacy.php b/api/pharmacy.php new file mode 100644 index 0000000..8d96340 --- /dev/null +++ b/api/pharmacy.php @@ -0,0 +1,172 @@ + 0 AND b.expiry_date >= CURDATE() + GROUP BY d.id + ORDER BY d.name_en ASC"; + $stmt = $pdo->query($sql); + echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC)); + break; + + case 'get_batches': + $drug_id = $_GET['drug_id'] ?? 0; + if (!$drug_id) throw new Exception("Drug ID required"); + + $sql = "SELECT b.*, s.name_en as supplier_name + FROM pharmacy_batches b + LEFT JOIN suppliers s ON b.supplier_id = s.id + WHERE b.drug_id = ? AND b.quantity > 0 + ORDER BY b.expiry_date ASC"; + $stmt = $pdo->prepare($sql); + $stmt->execute([$drug_id]); + echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC)); + break; + + case 'add_stock': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Invalid method"); + + $drug_id = $_POST['drug_id'] ?? 0; + $batch_number = $_POST['batch_number'] ?? ''; + $expiry_date = $_POST['expiry_date'] ?? ''; + $quantity = $_POST['quantity'] ?? 0; + $cost_price = $_POST['cost_price'] ?? 0; + $sale_price = $_POST['sale_price'] ?? 0; + $supplier_id = !empty($_POST['supplier_id']) ? $_POST['supplier_id'] : null; + + if (!$drug_id || !$batch_number || !$expiry_date || !$quantity) { + throw new Exception("Missing required fields"); + } + + $stmt = $pdo->prepare("INSERT INTO pharmacy_batches + (drug_id, batch_number, expiry_date, quantity, cost_price, sale_price, supplier_id, received_date) + VALUES (?, ?, ?, ?, ?, ?, ?, CURDATE())"); + $stmt->execute([$drug_id, $batch_number, $expiry_date, $quantity, $cost_price, $sale_price, $supplier_id]); + + echo json_encode(['success' => true, 'message' => 'Stock added successfully']); + break; + + case 'search_drugs': + $q = $_GET['q'] ?? ''; + $sql = "SELECT d.id, d.name_en, d.name_ar, d.price as default_price, + COALESCE(SUM(b.quantity), 0) as stock + FROM drugs d + LEFT JOIN pharmacy_batches b ON d.id = b.drug_id AND b.quantity > 0 AND b.expiry_date >= CURDATE() + WHERE d.name_en LIKE ? OR d.name_ar LIKE ? + GROUP BY d.id + LIMIT 20"; + $stmt = $pdo->prepare($sql); + $term = "%$q%"; + $stmt->execute([$term, $term]); + echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC)); + break; + + case 'create_sale': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Invalid method"); + $input = json_decode(file_get_contents('php://input'), true); + + if (empty($input['items'])) throw new Exception("No items in sale"); + + $pdo->beginTransaction(); + + try { + // Create Sale Record + $stmt = $pdo->prepare("INSERT INTO pharmacy_sales (patient_id, visit_id, total_amount, payment_method, status) VALUES (?, ?, ?, ?, 'completed')"); + $stmt->execute([ + $input['patient_id'] ?? null, + $input['visit_id'] ?? null, + $input['total_amount'] ?? 0, + $input['payment_method'] ?? 'cash' + ]); + $sale_id = $pdo->lastInsertId(); + + // Process Items + foreach ($input['items'] as $item) { + $drug_id = $item['drug_id']; + $qty_needed = $item['quantity']; + $unit_price = $item['price']; // Or fetch from batch? Use provided price for now. + + // Fetch available batches (FIFO) + $batch_stmt = $pdo->prepare("SELECT id, quantity FROM pharmacy_batches WHERE drug_id = ? AND quantity > 0 ORDER BY expiry_date ASC FOR UPDATE"); + $batch_stmt->execute([$drug_id]); + $batches = $batch_stmt->fetchAll(PDO::FETCH_ASSOC); + + $qty_remaining = $qty_needed; + + foreach ($batches as $batch) { + if ($qty_remaining <= 0) break; + + $take = min($batch['quantity'], $qty_remaining); + + // Deduct from batch + $update = $pdo->prepare("UPDATE pharmacy_batches SET quantity = quantity - ? WHERE id = ?"); + $update->execute([$take, $batch['id']]); + + // Add to sale items + $item_stmt = $pdo->prepare("INSERT INTO pharmacy_sale_items (sale_id, drug_id, batch_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?, ?)"); + $item_stmt->execute([$sale_id, $drug_id, $batch['id'], $take, $unit_price, $take * $unit_price]); + + $qty_remaining -= $take; + } + + if ($qty_remaining > 0) { + throw new Exception("Insufficient stock for drug ID: $drug_id"); + } + } + + $pdo->commit(); + echo json_encode(['success' => true, 'sale_id' => $sale_id]); + + } catch (Exception $e) { + $pdo->rollBack(); + throw $e; + } + break; + + case 'get_sales': + // List recent sales + $sql = "SELECT s.*, p.name_en as patient_name + FROM pharmacy_sales s + LEFT JOIN patients p ON s.patient_id = p.id + ORDER BY s.created_at DESC LIMIT 50"; + $stmt = $pdo->query($sql); + echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC)); + break; + + case 'get_sale_details': + $sale_id = $_GET['sale_id'] ?? 0; + if (!$sale_id) throw new Exception("Sale ID required"); + + $stmt = $pdo->prepare("SELECT s.*, p.name_en as patient_name FROM pharmacy_sales s LEFT JOIN patients p ON s.patient_id = p.id WHERE s.id = ?"); + $stmt->execute([$sale_id]); + $sale = $stmt->fetch(PDO::FETCH_ASSOC); + + if (!$sale) throw new Exception("Sale not found"); + + $items_stmt = $pdo->prepare("SELECT i.*, d.name_en as drug_name + FROM pharmacy_sale_items i + JOIN drugs d ON i.drug_id = d.id + WHERE i.sale_id = ?"); + $items_stmt->execute([$sale_id]); + $sale['items'] = $items_stmt->fetchAll(PDO::FETCH_ASSOC); + + echo json_encode($sale); + break; + + default: + throw new Exception("Invalid action"); + } +} catch (Exception $e) { + http_response_code(400); + echo json_encode(['error' => $e->getMessage()]); +} \ No newline at end of file diff --git a/check_pharmacy_schema.php b/check_pharmacy_schema.php new file mode 100644 index 0000000..f317829 --- /dev/null +++ b/check_pharmacy_schema.php @@ -0,0 +1,24 @@ +query("DESCRIBE $table"); + $columns = $stmt->fetchAll(PDO::FETCH_ASSOC); + foreach ($columns as $col) { + echo "{$col['Field']} ({$col['Type']}) +"; + } + } catch (PDOException $e) { + echo "Error describing $table: " . $e->getMessage() . " +"; + } + echo " +"; +} + diff --git a/db/migrations/20260321_create_pharmacy_module.sql b/db/migrations/20260321_create_pharmacy_module.sql new file mode 100644 index 0000000..90aac09 --- /dev/null +++ b/db/migrations/20260321_create_pharmacy_module.sql @@ -0,0 +1,49 @@ +-- Create pharmacy_batches table +CREATE TABLE IF NOT EXISTS pharmacy_batches ( + id INT AUTO_INCREMENT PRIMARY KEY, + drug_id INT NOT NULL, + batch_number VARCHAR(50) NOT NULL, + expiry_date DATE NOT NULL, + quantity INT NOT NULL DEFAULT 0, + cost_price DECIMAL(10, 2) NOT NULL DEFAULT 0.00, + sale_price DECIMAL(10, 2) NOT NULL DEFAULT 0.00, + supplier_id INT NULL, + received_date DATE NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (drug_id) REFERENCES drugs(id) ON DELETE CASCADE, + FOREIGN KEY (supplier_id) REFERENCES suppliers(id) ON DELETE SET NULL +); + +-- Create pharmacy_sales table +CREATE TABLE IF NOT EXISTS pharmacy_sales ( + id INT AUTO_INCREMENT PRIMARY KEY, + patient_id INT NULL, + visit_id INT NULL, + total_amount DECIMAL(10, 2) NOT NULL DEFAULT 0.00, + payment_method VARCHAR(50) DEFAULT 'cash', + status VARCHAR(20) DEFAULT 'completed', -- completed, refunded, cancelled + notes TEXT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (patient_id) REFERENCES patients(id) ON DELETE SET NULL, + FOREIGN KEY (visit_id) REFERENCES visits(id) ON DELETE SET NULL +); + +-- Create pharmacy_sale_items table +CREATE TABLE IF NOT EXISTS pharmacy_sale_items ( + id INT AUTO_INCREMENT PRIMARY KEY, + sale_id INT NOT NULL, + drug_id INT NOT NULL, + batch_id INT NULL, -- Can be null if we track sales without specific batch selection (though we should enforce it for stock deduction) + quantity INT NOT NULL DEFAULT 1, + unit_price DECIMAL(10, 2) NOT NULL DEFAULT 0.00, + total_price DECIMAL(10, 2) NOT NULL DEFAULT 0.00, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (sale_id) REFERENCES pharmacy_sales(id) ON DELETE CASCADE, + FOREIGN KEY (drug_id) REFERENCES drugs(id), + FOREIGN KEY (batch_id) REFERENCES pharmacy_batches(id) ON DELETE SET NULL +); + +-- Add stock management columns to drugs table +ALTER TABLE drugs ADD COLUMN IF NOT EXISTS min_stock_level INT DEFAULT 10; +ALTER TABLE drugs ADD COLUMN IF NOT EXISTS reorder_level INT DEFAULT 20; +ALTER TABLE drugs ADD COLUMN IF NOT EXISTS unit VARCHAR(50) DEFAULT 'pack'; diff --git a/includes/layout/header.php b/includes/layout/header.php index 33bf2a8..699bc91 100644 --- a/includes/layout/header.php +++ b/includes/layout/header.php @@ -129,13 +129,18 @@ $site_favicon = !empty($site_settings['company_favicon']) ? $site_settings['comp - - - + + + + -
+