add pharmacy
This commit is contained in:
parent
01f56287c6
commit
641316f659
30
api/patients.php
Normal file
30
api/patients.php
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../includes/common_data.php';
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$action = $_GET['action'] ?? '';
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch ($action) {
|
||||||
|
case 'search':
|
||||||
|
$q = $_GET['q'] ?? '';
|
||||||
|
if (strlen($q) < 1) {
|
||||||
|
echo json_encode([]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$stmt = $pdo->prepare("SELECT id, name, phone FROM patients WHERE name LIKE ? OR phone LIKE ? LIMIT 20");
|
||||||
|
$term = "%$q%";
|
||||||
|
$stmt->execute([$term, $term]);
|
||||||
|
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
echo json_encode($results);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'Invalid action']);
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
172
api/pharmacy.php
Normal file
172
api/pharmacy.php
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../includes/common_data.php'; // Includes db/config.php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$action = $_GET['action'] ?? '';
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch ($action) {
|
||||||
|
case 'get_stock':
|
||||||
|
// List all drugs with total stock quantity
|
||||||
|
$sql = "SELECT d.id, d.name_en, d.name_ar, d.min_stock_level, d.reorder_level, d.unit,
|
||||||
|
COALESCE(SUM(b.quantity), 0) as total_stock
|
||||||
|
FROM drugs d
|
||||||
|
LEFT JOIN pharmacy_batches b ON d.id = b.drug_id AND b.quantity > 0 AND b.expiry_date >= CURDATE()
|
||||||
|
GROUP BY d.id
|
||||||
|
ORDER BY d.name_en ASC";
|
||||||
|
$stmt = $pdo->query($sql);
|
||||||
|
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'get_batches':
|
||||||
|
$drug_id = $_GET['drug_id'] ?? 0;
|
||||||
|
if (!$drug_id) throw new Exception("Drug ID required");
|
||||||
|
|
||||||
|
$sql = "SELECT b.*, s.name_en as supplier_name
|
||||||
|
FROM pharmacy_batches b
|
||||||
|
LEFT JOIN suppliers s ON b.supplier_id = s.id
|
||||||
|
WHERE b.drug_id = ? AND b.quantity > 0
|
||||||
|
ORDER BY b.expiry_date ASC";
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute([$drug_id]);
|
||||||
|
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'add_stock':
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Invalid method");
|
||||||
|
|
||||||
|
$drug_id = $_POST['drug_id'] ?? 0;
|
||||||
|
$batch_number = $_POST['batch_number'] ?? '';
|
||||||
|
$expiry_date = $_POST['expiry_date'] ?? '';
|
||||||
|
$quantity = $_POST['quantity'] ?? 0;
|
||||||
|
$cost_price = $_POST['cost_price'] ?? 0;
|
||||||
|
$sale_price = $_POST['sale_price'] ?? 0;
|
||||||
|
$supplier_id = !empty($_POST['supplier_id']) ? $_POST['supplier_id'] : null;
|
||||||
|
|
||||||
|
if (!$drug_id || !$batch_number || !$expiry_date || !$quantity) {
|
||||||
|
throw new Exception("Missing required fields");
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO pharmacy_batches
|
||||||
|
(drug_id, batch_number, expiry_date, quantity, cost_price, sale_price, supplier_id, received_date)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, CURDATE())");
|
||||||
|
$stmt->execute([$drug_id, $batch_number, $expiry_date, $quantity, $cost_price, $sale_price, $supplier_id]);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Stock added successfully']);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'search_drugs':
|
||||||
|
$q = $_GET['q'] ?? '';
|
||||||
|
$sql = "SELECT d.id, d.name_en, d.name_ar, d.price as default_price,
|
||||||
|
COALESCE(SUM(b.quantity), 0) as stock
|
||||||
|
FROM drugs d
|
||||||
|
LEFT JOIN pharmacy_batches b ON d.id = b.drug_id AND b.quantity > 0 AND b.expiry_date >= CURDATE()
|
||||||
|
WHERE d.name_en LIKE ? OR d.name_ar LIKE ?
|
||||||
|
GROUP BY d.id
|
||||||
|
LIMIT 20";
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$term = "%$q%";
|
||||||
|
$stmt->execute([$term, $term]);
|
||||||
|
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'create_sale':
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Invalid method");
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if (empty($input['items'])) throw new Exception("No items in sale");
|
||||||
|
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create Sale Record
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO pharmacy_sales (patient_id, visit_id, total_amount, payment_method, status) VALUES (?, ?, ?, ?, 'completed')");
|
||||||
|
$stmt->execute([
|
||||||
|
$input['patient_id'] ?? null,
|
||||||
|
$input['visit_id'] ?? null,
|
||||||
|
$input['total_amount'] ?? 0,
|
||||||
|
$input['payment_method'] ?? 'cash'
|
||||||
|
]);
|
||||||
|
$sale_id = $pdo->lastInsertId();
|
||||||
|
|
||||||
|
// Process Items
|
||||||
|
foreach ($input['items'] as $item) {
|
||||||
|
$drug_id = $item['drug_id'];
|
||||||
|
$qty_needed = $item['quantity'];
|
||||||
|
$unit_price = $item['price']; // Or fetch from batch? Use provided price for now.
|
||||||
|
|
||||||
|
// Fetch available batches (FIFO)
|
||||||
|
$batch_stmt = $pdo->prepare("SELECT id, quantity FROM pharmacy_batches WHERE drug_id = ? AND quantity > 0 ORDER BY expiry_date ASC FOR UPDATE");
|
||||||
|
$batch_stmt->execute([$drug_id]);
|
||||||
|
$batches = $batch_stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$qty_remaining = $qty_needed;
|
||||||
|
|
||||||
|
foreach ($batches as $batch) {
|
||||||
|
if ($qty_remaining <= 0) break;
|
||||||
|
|
||||||
|
$take = min($batch['quantity'], $qty_remaining);
|
||||||
|
|
||||||
|
// Deduct from batch
|
||||||
|
$update = $pdo->prepare("UPDATE pharmacy_batches SET quantity = quantity - ? WHERE id = ?");
|
||||||
|
$update->execute([$take, $batch['id']]);
|
||||||
|
|
||||||
|
// Add to sale items
|
||||||
|
$item_stmt = $pdo->prepare("INSERT INTO pharmacy_sale_items (sale_id, drug_id, batch_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?, ?)");
|
||||||
|
$item_stmt->execute([$sale_id, $drug_id, $batch['id'], $take, $unit_price, $take * $unit_price]);
|
||||||
|
|
||||||
|
$qty_remaining -= $take;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($qty_remaining > 0) {
|
||||||
|
throw new Exception("Insufficient stock for drug ID: $drug_id");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
echo json_encode(['success' => true, 'sale_id' => $sale_id]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'get_sales':
|
||||||
|
// List recent sales
|
||||||
|
$sql = "SELECT s.*, p.name_en as patient_name
|
||||||
|
FROM pharmacy_sales s
|
||||||
|
LEFT JOIN patients p ON s.patient_id = p.id
|
||||||
|
ORDER BY s.created_at DESC LIMIT 50";
|
||||||
|
$stmt = $pdo->query($sql);
|
||||||
|
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'get_sale_details':
|
||||||
|
$sale_id = $_GET['sale_id'] ?? 0;
|
||||||
|
if (!$sale_id) throw new Exception("Sale ID required");
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT s.*, p.name_en as patient_name FROM pharmacy_sales s LEFT JOIN patients p ON s.patient_id = p.id WHERE s.id = ?");
|
||||||
|
$stmt->execute([$sale_id]);
|
||||||
|
$sale = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$sale) throw new Exception("Sale not found");
|
||||||
|
|
||||||
|
$items_stmt = $pdo->prepare("SELECT i.*, d.name_en as drug_name
|
||||||
|
FROM pharmacy_sale_items i
|
||||||
|
JOIN drugs d ON i.drug_id = d.id
|
||||||
|
WHERE i.sale_id = ?");
|
||||||
|
$items_stmt->execute([$sale_id]);
|
||||||
|
$sale['items'] = $items_stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
echo json_encode($sale);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Exception("Invalid action");
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
24
check_pharmacy_schema.php
Normal file
24
check_pharmacy_schema.php
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'db/config.php';
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
$tables = ['drugs', 'suppliers', 'visit_prescriptions'];
|
||||||
|
|
||||||
|
foreach ($tables as $table) {
|
||||||
|
echo "--- Table: $table ---
|
||||||
|
";
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->query("DESCRIBE $table");
|
||||||
|
$columns = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
foreach ($columns as $col) {
|
||||||
|
echo "{$col['Field']} ({$col['Type']})
|
||||||
|
";
|
||||||
|
}
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
echo "Error describing $table: " . $e->getMessage() . "
|
||||||
|
";
|
||||||
|
}
|
||||||
|
echo "
|
||||||
|
";
|
||||||
|
}
|
||||||
|
|
||||||
49
db/migrations/20260321_create_pharmacy_module.sql
Normal file
49
db/migrations/20260321_create_pharmacy_module.sql
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
-- Create pharmacy_batches table
|
||||||
|
CREATE TABLE IF NOT EXISTS pharmacy_batches (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
drug_id INT NOT NULL,
|
||||||
|
batch_number VARCHAR(50) NOT NULL,
|
||||||
|
expiry_date DATE NOT NULL,
|
||||||
|
quantity INT NOT NULL DEFAULT 0,
|
||||||
|
cost_price DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
|
||||||
|
sale_price DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
|
||||||
|
supplier_id INT NULL,
|
||||||
|
received_date DATE NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (drug_id) REFERENCES drugs(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (supplier_id) REFERENCES suppliers(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create pharmacy_sales table
|
||||||
|
CREATE TABLE IF NOT EXISTS pharmacy_sales (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
patient_id INT NULL,
|
||||||
|
visit_id INT NULL,
|
||||||
|
total_amount DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
|
||||||
|
payment_method VARCHAR(50) DEFAULT 'cash',
|
||||||
|
status VARCHAR(20) DEFAULT 'completed', -- completed, refunded, cancelled
|
||||||
|
notes TEXT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (patient_id) REFERENCES patients(id) ON DELETE SET NULL,
|
||||||
|
FOREIGN KEY (visit_id) REFERENCES visits(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create pharmacy_sale_items table
|
||||||
|
CREATE TABLE IF NOT EXISTS pharmacy_sale_items (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
sale_id INT NOT NULL,
|
||||||
|
drug_id INT NOT NULL,
|
||||||
|
batch_id INT NULL, -- Can be null if we track sales without specific batch selection (though we should enforce it for stock deduction)
|
||||||
|
quantity INT NOT NULL DEFAULT 1,
|
||||||
|
unit_price DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
|
||||||
|
total_price DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (sale_id) REFERENCES pharmacy_sales(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (drug_id) REFERENCES drugs(id),
|
||||||
|
FOREIGN KEY (batch_id) REFERENCES pharmacy_batches(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Add stock management columns to drugs table
|
||||||
|
ALTER TABLE drugs ADD COLUMN IF NOT EXISTS min_stock_level INT DEFAULT 10;
|
||||||
|
ALTER TABLE drugs ADD COLUMN IF NOT EXISTS reorder_level INT DEFAULT 20;
|
||||||
|
ALTER TABLE drugs ADD COLUMN IF NOT EXISTS unit VARCHAR(50) DEFAULT 'pack';
|
||||||
@ -129,13 +129,18 @@ $site_favicon = !empty($site_settings['company_favicon']) ? $site_settings['comp
|
|||||||
<a href="xray_inquiries.php" class="sidebar-link py-2 <?php echo $section === 'xray_inquiries' ? 'active' : ''; ?>"><i class="bi bi-question-circle me-2"></i> <?php echo __('inquiries'); ?></a>
|
<a href="xray_inquiries.php" class="sidebar-link py-2 <?php echo $section === 'xray_inquiries' ? 'active' : ''; ?>"><i class="bi bi-question-circle me-2"></i> <?php echo __('inquiries'); ?></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Drugs Module -->
|
|
||||||
<a href="#drugsSubmenu" data-bs-toggle="collapse" class="sidebar-link <?php echo in_array($section, ['drugs', 'drugs_groups', 'suppliers']) ? 'active' : ''; ?> d-flex justify-content-between align-items-center">
|
<!-- Pharmacy Module -->
|
||||||
<span><i class="bi bi-capsule me-2"></i> <?php echo __('drugs'); ?></span>
|
<a href="#pharmacySubmenu" data-bs-toggle="collapse" class="sidebar-link <?php echo in_array($section, ['pharmacy_inventory', 'pharmacy_pos', 'pharmacy_sales', 'drugs', 'drugs_groups', 'suppliers']) ? 'active' : ''; ?> d-flex justify-content-between align-items-center">
|
||||||
|
<span><i class="bi bi-capsule me-2"></i> <?php echo __('pharmacy'); ?></span>
|
||||||
<i class="bi bi-chevron-down small"></i>
|
<i class="bi bi-chevron-down small"></i>
|
||||||
</a>
|
</a>
|
||||||
<div class="collapse <?php echo in_array($section, ['drugs', 'drugs_groups', 'suppliers']) ? 'show' : ''; ?>" id="drugsSubmenu">
|
<div class="collapse <?php echo in_array($section, ['pharmacy_inventory', 'pharmacy_pos', 'pharmacy_sales', 'drugs', 'drugs_groups', 'suppliers']) ? 'show' : ''; ?>" id="pharmacySubmenu">
|
||||||
<div class="sidebar-submenu">
|
<div class="sidebar-submenu">
|
||||||
|
<a href="pharmacy_inventory.php" class="sidebar-link py-2 <?php echo $section === 'pharmacy_inventory' ? 'active' : ''; ?>"><i class="bi bi-boxes me-2"></i> <?php echo __('inventory'); ?></a>
|
||||||
|
<a href="pharmacy_pos.php" class="sidebar-link py-2 <?php echo $section === 'pharmacy_pos' ? 'active' : ''; ?>"><i class="bi bi-cart-check me-2"></i> <?php echo __('pos'); ?></a>
|
||||||
|
<a href="pharmacy_sales.php" class="sidebar-link py-2 <?php echo $section === 'pharmacy_sales' ? 'active' : ''; ?>"><i class="bi bi-receipt me-2"></i> <?php echo __('sales_history'); ?></a>
|
||||||
|
<div class="border-top my-1 border-secondary" style="border-color: #0d4680 !important;"></div>
|
||||||
<a href="drugs.php" class="sidebar-link py-2 <?php echo $section === 'drugs' ? 'active' : ''; ?>"><i class="bi bi-list-check me-2"></i> <?php echo __('drugs'); ?></a>
|
<a href="drugs.php" class="sidebar-link py-2 <?php echo $section === 'drugs' ? 'active' : ''; ?>"><i class="bi bi-list-check me-2"></i> <?php echo __('drugs'); ?></a>
|
||||||
<a href="drugs_groups.php" class="sidebar-link py-2 <?php echo $section === 'drugs_groups' ? 'active' : ''; ?>"><i class="bi bi-collection me-2"></i> <?php echo __('groups'); ?></a>
|
<a href="drugs_groups.php" class="sidebar-link py-2 <?php echo $section === 'drugs_groups' ? 'active' : ''; ?>"><i class="bi bi-collection me-2"></i> <?php echo __('groups'); ?></a>
|
||||||
<a href="suppliers.php" class="sidebar-link py-2 <?php echo $section === 'suppliers' ? 'active' : ''; ?>"><i class="bi bi-truck me-2"></i> <?php echo __('suppliers'); ?></a>
|
<a href="suppliers.php" class="sidebar-link py-2 <?php echo $section === 'suppliers' ? 'active' : ''; ?>"><i class="bi bi-truck me-2"></i> <?php echo __('suppliers'); ?></a>
|
||||||
|
|||||||
316
includes/pages/pharmacy_inventory.php
Normal file
316
includes/pages/pharmacy_inventory.php
Normal file
@ -0,0 +1,316 @@
|
|||||||
|
<?php
|
||||||
|
$section = 'pharmacy_inventory';
|
||||||
|
require_once __DIR__ . '/../layout/header.php';
|
||||||
|
|
||||||
|
$page = $_GET['page'] ?? 1;
|
||||||
|
$search = $_GET['search'] ?? '';
|
||||||
|
|
||||||
|
// Fetch drugs with stock info
|
||||||
|
// The API 'get_stock' returns all, but for pagination we might want to do it in PHP or adjust API.
|
||||||
|
// For now, let's use the API approach via JS or just fetch in PHP.
|
||||||
|
// Since we are in PHP page, fetching directly is better for SEO/Speed than pure AJAX sometimes, but standard here seems mixed.
|
||||||
|
// I'll fetch via PHP for initial render.
|
||||||
|
|
||||||
|
$limit = 10;
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
|
||||||
|
$where = "WHERE 1=1";
|
||||||
|
$params = [];
|
||||||
|
if ($search) {
|
||||||
|
$where .= " AND (d.name_en LIKE ? OR d.name_ar LIKE ?)";
|
||||||
|
$params[] = "%$search%";
|
||||||
|
$params[] = "%$search%";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count
|
||||||
|
$count_stmt = $db->prepare("SELECT COUNT(*) FROM drugs d $where");
|
||||||
|
$count_stmt->execute($params);
|
||||||
|
$total_rows = $count_stmt->fetchColumn();
|
||||||
|
$total_pages = ceil($total_rows / $limit);
|
||||||
|
|
||||||
|
// Fetch
|
||||||
|
$sql = "SELECT d.id, d.name_en, d.name_ar, d.min_stock_level, d.reorder_level, d.unit,
|
||||||
|
COALESCE(SUM(b.quantity), 0) as total_stock
|
||||||
|
FROM drugs d
|
||||||
|
LEFT JOIN pharmacy_batches b ON d.id = b.drug_id AND b.quantity > 0 AND b.expiry_date >= CURDATE()
|
||||||
|
$where
|
||||||
|
GROUP BY d.id
|
||||||
|
ORDER BY d.name_en ASC
|
||||||
|
LIMIT $limit OFFSET $offset";
|
||||||
|
$stmt = $db->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$drugs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2 class="fw-bold text-primary"><i class="bi bi-boxes me-2"></i> <?php echo __('inventory'); ?></h2>
|
||||||
|
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addStockModal">
|
||||||
|
<i class="bi bi-plus-lg me-1"></i> <?php echo __('add_stock'); ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="GET" class="row g-3 mb-4">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text bg-light border-end-0"><i class="bi bi-search text-muted"></i></span>
|
||||||
|
<input type="text" name="search" class="form-control border-start-0 ps-0" placeholder="<?php echo __('search'); ?>..." value="<?php echo htmlspecialchars($search); ?>">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<button type="submit" class="btn btn-primary w-100"><?php echo __('search'); ?></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><?php echo __('drug_name'); ?></th>
|
||||||
|
<th><?php echo __('stock'); ?></th>
|
||||||
|
<th><?php echo __('unit'); ?></th>
|
||||||
|
<th><?php echo __('status'); ?></th>
|
||||||
|
<th><?php echo __('actions'); ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php if (empty($drugs)): ?>
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="text-center py-4 text-muted">
|
||||||
|
<i class="bi bi-inbox fs-1 d-block mb-2"></i>
|
||||||
|
<?php echo __('no_data_found'); ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($drugs as $drug): ?>
|
||||||
|
<?php
|
||||||
|
$status_class = 'bg-success';
|
||||||
|
$status_text = 'OK';
|
||||||
|
if ($drug['total_stock'] <= 0) {
|
||||||
|
$status_class = 'bg-danger';
|
||||||
|
$status_text = 'Out of Stock';
|
||||||
|
} elseif ($drug['total_stock'] <= $drug['min_stock_level']) {
|
||||||
|
$status_class = 'bg-warning text-dark';
|
||||||
|
$status_text = 'Low Stock';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="fw-bold"><?php echo htmlspecialchars($drug['name_en']); ?></div>
|
||||||
|
<small class="text-muted"><?php echo htmlspecialchars($drug['name_ar']); ?></small>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="fs-5 fw-bold"><?php echo $drug['total_stock']; ?></span>
|
||||||
|
</td>
|
||||||
|
<td><?php echo htmlspecialchars($drug['unit'] ?? '-'); ?></td>
|
||||||
|
<td>
|
||||||
|
<span class="badge <?php echo $status_class; ?> rounded-pill">
|
||||||
|
<?php echo $status_text; ?>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-outline-info" onclick="viewBatches(<?php echo $drug['id']; ?>, '<?php echo addslashes($drug['name_en']); ?>')">
|
||||||
|
<i class="bi bi-eye"></i> <?php echo __('view_batches'); ?>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
<?php if ($total_pages > 1): ?>
|
||||||
|
<nav aria-label="Page navigation" class="mt-4">
|
||||||
|
<ul class="pagination justify-content-center">
|
||||||
|
<li class="page-item <?php echo $page <= 1 ? 'disabled' : ''; ?>">
|
||||||
|
<a class="page-link" href="?page=<?php echo $page - 1; ?>&search=<?php echo urlencode($search); ?>"><?php echo __('previous'); ?></a>
|
||||||
|
</li>
|
||||||
|
<?php for ($i = 1; $i <= $total_pages; $i++): ?>
|
||||||
|
<li class="page-item <?php echo $page == $i ? 'active' : ''; ?>">
|
||||||
|
<a class="page-link" href="?page=<?php echo $i; ?>&search=<?php echo urlencode($search); ?>"><?php echo $i; ?></a>
|
||||||
|
</li>
|
||||||
|
<?php endfor; ?>
|
||||||
|
<li class="page-item <?php echo $page >= $total_pages ? 'disabled' : ''; ?>">
|
||||||
|
<a class="page-link" href="?page=<?php echo $page + 1; ?>&search=<?php echo urlencode($search); ?>"><?php echo __('next'); ?></a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Stock Modal -->
|
||||||
|
<div class="modal fade" id="addStockModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title"><?php echo __('add_stock'); ?></h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="addStockForm">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label"><?php echo __('select_drug'); ?></label>
|
||||||
|
<select class="form-select select2" name="drug_id" required style="width: 100%;">
|
||||||
|
<option value=""><?php echo __('select_drug'); ?></option>
|
||||||
|
<?php
|
||||||
|
$all_drugs = $db->query("SELECT id, name_en FROM drugs ORDER BY name_en ASC")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
foreach ($all_drugs as $d) {
|
||||||
|
echo "<option value='{$d['id']}'>" . htmlspecialchars($d['name_en']) . "</option>";
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label"><?php echo __('batch_number'); ?></label>
|
||||||
|
<input type="text" class="form-control" name="batch_number" required>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label"><?php echo __('expiry_date'); ?></label>
|
||||||
|
<input type="date" class="form-control" name="expiry_date" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label"><?php echo __('quantity'); ?></label>
|
||||||
|
<input type="number" class="form-control" name="quantity" min="1" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label"><?php echo __('cost_price'); ?></label>
|
||||||
|
<input type="number" class="form-control" name="cost_price" step="0.01" value="0">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label"><?php echo __('sale_price'); ?></label>
|
||||||
|
<input type="number" class="form-control" name="sale_price" step="0.01" value="0">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label"><?php echo __('supplier'); ?></label>
|
||||||
|
<select class="form-select" name="supplier_id">
|
||||||
|
<option value=""><?php echo __('select_supplier'); ?></option>
|
||||||
|
<?php
|
||||||
|
$suppliers = $db->query("SELECT id, name_en FROM suppliers ORDER BY name_en ASC")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
foreach ($suppliers as $s) {
|
||||||
|
echo "<option value='{$s['id']}'>" . htmlspecialchars($s['name_en']) . "</option>";
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?php echo __('cancel'); ?></button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="submitStock()"><?php echo __('save'); ?></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- View Batches Modal -->
|
||||||
|
<div class="modal fade" id="viewBatchesModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="batchesModalTitle"><?php echo __('view_batches'); ?></h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><?php echo __('batch_number'); ?></th>
|
||||||
|
<th><?php echo __('expiry_date'); ?></th>
|
||||||
|
<th><?php echo __('quantity'); ?></th>
|
||||||
|
<th><?php echo __('cost_price'); ?></th>
|
||||||
|
<th><?php echo __('supplier'); ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="batchesTableBody">
|
||||||
|
<!-- Populated via JS -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function submitStock() {
|
||||||
|
const form = document.getElementById('addStockForm');
|
||||||
|
if (!form.checkValidity()) {
|
||||||
|
form.reportValidity();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData(form);
|
||||||
|
|
||||||
|
fetch('api/pharmacy.php?action=add_stock', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert(data.error || 'Error adding stock');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
console.error(e);
|
||||||
|
alert('Network error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function viewBatches(drugId, drugName) {
|
||||||
|
document.getElementById('batchesModalTitle').textContent = drugName + ' - Batches';
|
||||||
|
const tbody = document.getElementById('batchesTableBody');
|
||||||
|
tbody.innerHTML = '<tr><td colspan="5" class="text-center">Loading...</td></tr>';
|
||||||
|
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('viewBatchesModal'));
|
||||||
|
modal.show();
|
||||||
|
|
||||||
|
fetch('api/pharmacy.php?action=get_batches&drug_id=' + drugId)
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
if (data.length === 0) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="5" class="text-center">No active batches found</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.forEach(batch => {
|
||||||
|
tbody.innerHTML += `
|
||||||
|
<tr>
|
||||||
|
<td>${batch.batch_number}</td>
|
||||||
|
<td>${batch.expiry_date}</td>
|
||||||
|
<td>${batch.quantity}</td>
|
||||||
|
<td>${batch.cost_price}</td>
|
||||||
|
<td>${batch.supplier_name || '-'}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="5" class="text-center text-danger">Error loading data</td></tr>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('.select2').select2({
|
||||||
|
theme: 'bootstrap-5',
|
||||||
|
dropdownParent: $('#addStockModal')
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<?php require_once __DIR__ . '/../layout/footer.php'; ?>
|
||||||
328
includes/pages/pharmacy_pos.php
Normal file
328
includes/pages/pharmacy_pos.php
Normal file
@ -0,0 +1,328 @@
|
|||||||
|
<?php
|
||||||
|
$section = 'pharmacy_pos';
|
||||||
|
require_once __DIR__ . '/../layout/header.php';
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="container-fluid h-100">
|
||||||
|
<div class="row h-100">
|
||||||
|
<!-- Left Panel: Product Search -->
|
||||||
|
<div class="col-md-7 d-flex flex-column h-100">
|
||||||
|
<div class="card shadow-sm flex-grow-1">
|
||||||
|
<div class="card-header bg-white py-3">
|
||||||
|
<div class="input-group input-group-lg">
|
||||||
|
<span class="input-group-text bg-light border-end-0"><i class="bi bi-search"></i></span>
|
||||||
|
<input type="text" id="drugSearch" class="form-control border-start-0" placeholder="<?php echo __('search_by_name'); ?>..." autocomplete="off">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0 overflow-auto" style="max-height: calc(100vh - 200px);">
|
||||||
|
<div id="drugList" class="list-group list-group-flush">
|
||||||
|
<!-- Populated by JS -->
|
||||||
|
<div class="text-center py-5 text-muted">
|
||||||
|
<i class="bi bi-search fs-1"></i>
|
||||||
|
<p><?php echo __('search_to_add_items'); ?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Panel: Cart -->
|
||||||
|
<div class="col-md-5 d-flex flex-column h-100">
|
||||||
|
<div class="card shadow-sm flex-grow-1 border-primary">
|
||||||
|
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-cart3 me-2"></i> <?php echo __('cart'); ?></h5>
|
||||||
|
<span id="cartCount" class="badge bg-white text-primary rounded-pill">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0 d-flex flex-column">
|
||||||
|
<div class="table-responsive flex-grow-1" style="max-height: calc(100vh - 350px); overflow-y: auto;">
|
||||||
|
<table class="table table-striped mb-0">
|
||||||
|
<thead class="sticky-top bg-light">
|
||||||
|
<tr>
|
||||||
|
<th><?php echo __('item'); ?></th>
|
||||||
|
<th width="80"><?php echo __('qty'); ?></th>
|
||||||
|
<th width="100" class="text-end"><?php echo __('total'); ?></th>
|
||||||
|
<th width="50"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="cartTableBody">
|
||||||
|
<!-- Cart Items -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-3 bg-light border-top">
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span class="fs-5"><?php echo __('total'); ?>:</span>
|
||||||
|
<span class="fs-4 fw-bold text-primary" id="cartTotal">0.00</span>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-success w-100 btn-lg" onclick="showCheckout()">
|
||||||
|
<i class="bi bi-credit-card me-2"></i> <?php echo __('checkout'); ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Checkout Modal -->
|
||||||
|
<div class="modal fade" id="checkoutModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title"><?php echo __('checkout'); ?></h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="checkoutForm">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label"><?php echo __('patient'); ?> (<?php echo __('optional'); ?>)</label>
|
||||||
|
<select class="form-select select2-patient" id="patientSelect" style="width: 100%;">
|
||||||
|
<option value=""><?php echo __('select_patient'); ?></option>
|
||||||
|
<!-- Populated via AJAX or PHP if small list -->
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Visit ID (Optional linkage) -->
|
||||||
|
<input type="hidden" id="visitSelect" value="">
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label"><?php echo __('payment_method'); ?></label>
|
||||||
|
<div class="btn-group w-100" role="group">
|
||||||
|
<input type="radio" class="btn-check" name="payment_method" id="pay_cash" value="cash" checked>
|
||||||
|
<label class="btn btn-outline-primary" for="pay_cash"><i class="bi bi-cash me-1"></i> <?php echo __('cash'); ?></label>
|
||||||
|
|
||||||
|
<input type="radio" class="btn-check" name="payment_method" id="pay_card" value="card">
|
||||||
|
<label class="btn btn-outline-primary" for="pay_card"><i class="bi bi-credit-card me-1"></i> <?php echo __('card'); ?></label>
|
||||||
|
|
||||||
|
<input type="radio" class="btn-check" name="payment_method" id="pay_insurance" value="insurance">
|
||||||
|
<label class="btn btn-outline-primary" for="pay_insurance"><i class="bi bi-shield-check me-1"></i> <?php echo __('insurance'); ?></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-info d-flex justify-content-between">
|
||||||
|
<span><?php echo __('total_amount'); ?>:</span>
|
||||||
|
<strong id="checkoutTotal">0.00</strong>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?php echo __('cancel'); ?></button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="processSale()">
|
||||||
|
<i class="bi bi-check-lg me-1"></i> <?php echo __('confirm_payment'); ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let cart = [];
|
||||||
|
let debounceTimer;
|
||||||
|
|
||||||
|
const searchInput = document.getElementById('drugSearch');
|
||||||
|
const drugList = document.getElementById('drugList');
|
||||||
|
const cartTableBody = document.getElementById('cartTableBody');
|
||||||
|
const cartTotalEl = document.getElementById('cartTotal');
|
||||||
|
const cartCountEl = document.getElementById('cartCount');
|
||||||
|
const checkoutTotalEl = document.getElementById('checkoutTotal');
|
||||||
|
|
||||||
|
// Search Logic
|
||||||
|
searchInput.addEventListener('input', function() {
|
||||||
|
clearTimeout(debounceTimer);
|
||||||
|
debounceTimer = setTimeout(() => {
|
||||||
|
const query = this.value.trim();
|
||||||
|
if (query.length < 2) {
|
||||||
|
drugList.innerHTML = '<div class="text-center py-5 text-muted"><i class="bi bi-search fs-1"></i><p><?php echo __('search_to_add_items'); ?></p></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('api/pharmacy.php?action=search_drugs&q=' + encodeURIComponent(query))
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
drugList.innerHTML = '';
|
||||||
|
if (data.length === 0) {
|
||||||
|
drugList.innerHTML = '<div class="list-group-item text-center text-muted"><?php echo __('no_drugs_found'); ?></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.forEach(drug => {
|
||||||
|
const stock = parseFloat(drug.stock);
|
||||||
|
const price = parseFloat(drug.default_price || 0);
|
||||||
|
const isOutOfStock = stock <= 0;
|
||||||
|
|
||||||
|
const item = document.createElement('a');
|
||||||
|
item.className = `list-group-item list-group-item-action d-flex justify-content-between align-items-center ${isOutOfStock ? 'disabled bg-light' : ''}`;
|
||||||
|
item.innerHTML = `
|
||||||
|
<div>
|
||||||
|
<div class="fw-bold">${drug.name_en}</div>
|
||||||
|
<small class="text-muted">${drug.name_ar || ''}</small>
|
||||||
|
</div>
|
||||||
|
<div class="text-end">
|
||||||
|
<div class="fw-bold text-primary">${price.toFixed(2)}</div>
|
||||||
|
<small class="${isOutOfStock ? 'text-danger' : 'text-success'}">
|
||||||
|
${isOutOfStock ? '<?php echo __('out_of_stock'); ?>' : '<?php echo __('stock'); ?>: ' + stock}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (!isOutOfStock) {
|
||||||
|
item.onclick = () => addToCart(drug);
|
||||||
|
item.style.cursor = 'pointer';
|
||||||
|
}
|
||||||
|
|
||||||
|
drugList.appendChild(item);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cart Logic
|
||||||
|
function addToCart(drug) {
|
||||||
|
const existing = cart.find(i => i.id === drug.id);
|
||||||
|
if (existing) {
|
||||||
|
if (existing.quantity >= drug.stock) {
|
||||||
|
alert('<?php echo __('insufficient_stock'); ?>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
existing.quantity++;
|
||||||
|
} else {
|
||||||
|
cart.push({
|
||||||
|
id: drug.id,
|
||||||
|
name: drug.name_en,
|
||||||
|
price: parseFloat(drug.default_price || 0),
|
||||||
|
quantity: 1,
|
||||||
|
max_stock: parseFloat(drug.stock)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
renderCart();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateQty(id, change) {
|
||||||
|
const item = cart.find(i => i.id === id);
|
||||||
|
if (!item) return;
|
||||||
|
|
||||||
|
const newQty = item.quantity + change;
|
||||||
|
if (newQty > item.max_stock) {
|
||||||
|
alert('<?php echo __('insufficient_stock'); ?>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newQty <= 0) {
|
||||||
|
cart = cart.filter(i => i.id !== id);
|
||||||
|
} else {
|
||||||
|
item.quantity = newQty;
|
||||||
|
}
|
||||||
|
renderCart();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCart() {
|
||||||
|
cartTableBody.innerHTML = '';
|
||||||
|
let total = 0;
|
||||||
|
|
||||||
|
cart.forEach(item => {
|
||||||
|
const itemTotal = item.quantity * item.price;
|
||||||
|
total += itemTotal;
|
||||||
|
|
||||||
|
cartTableBody.innerHTML += `
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="fw-bold text-truncate" style="max-width: 180px;">${item.name}</div>
|
||||||
|
<small class="text-muted">${item.price.toFixed(2)}</small>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<button class="btn btn-outline-secondary" onclick="updateQty(${item.id}, -1)">-</button>
|
||||||
|
<input type="text" class="form-control text-center p-0" value="${item.quantity}" readonly>
|
||||||
|
<button class="btn btn-outline-secondary" onclick="updateQty(${item.id}, 1)">+</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="text-end fw-bold">${itemTotal.toFixed(2)}</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<button class="btn btn-sm btn-link text-danger p-0" onclick="updateQty(${item.id}, -1000)"><i class="bi bi-trash"></i></button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
cartTotalEl.textContent = total.toFixed(2);
|
||||||
|
cartCountEl.textContent = cart.length;
|
||||||
|
checkoutTotalEl.textContent = total.toFixed(2);
|
||||||
|
|
||||||
|
if (cart.length === 0) {
|
||||||
|
cartTableBody.innerHTML = '<tr><td colspan="4" class="text-center text-muted py-4"><?php echo __('cart_empty'); ?></td></tr>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checkout Logic
|
||||||
|
function showCheckout() {
|
||||||
|
if (cart.length === 0) {
|
||||||
|
alert('<?php echo __('cart_is_empty'); ?>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('checkoutModal'));
|
||||||
|
modal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function processSale() {
|
||||||
|
const patientId = $('#patientSelect').val();
|
||||||
|
const paymentMethod = document.querySelector('input[name="payment_method"]:checked').value;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
patient_id: patientId || null,
|
||||||
|
visit_id: null,
|
||||||
|
payment_method: paymentMethod,
|
||||||
|
total_amount: parseFloat(cartTotalEl.textContent),
|
||||||
|
items: cart.map(i => ({
|
||||||
|
drug_id: i.id,
|
||||||
|
quantity: i.quantity,
|
||||||
|
price: i.price
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch('api/pharmacy.php?action=create_sale', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
alert('Sale completed!');
|
||||||
|
cart = [];
|
||||||
|
renderCart();
|
||||||
|
bootstrap.Modal.getInstance(document.getElementById('checkoutModal')).hide();
|
||||||
|
// TODO: Open receipt
|
||||||
|
} else {
|
||||||
|
alert(data.error || 'Transaction failed');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
console.error(e);
|
||||||
|
alert('Network error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize Patient Select2
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('.select2-patient').select2({
|
||||||
|
theme: 'bootstrap-5',
|
||||||
|
dropdownParent: $('#checkoutModal'),
|
||||||
|
ajax: {
|
||||||
|
url: 'api/patients.php?action=search', // Need to check if this endpoint exists or similar
|
||||||
|
dataType: 'json',
|
||||||
|
delay: 250,
|
||||||
|
processResults: function (data) {
|
||||||
|
return {
|
||||||
|
results: data.map(p => ({id: p.id, text: p.name + ' (' + p.phone + ')'}))
|
||||||
|
};
|
||||||
|
},
|
||||||
|
cache: true
|
||||||
|
},
|
||||||
|
placeholder: '<?php echo __('search_patient'); ?>',
|
||||||
|
minimumInputLength: 1
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<?php require_once __DIR__ . '/../layout/footer.php'; ?>
|
||||||
217
includes/pages/pharmacy_sales.php
Normal file
217
includes/pages/pharmacy_sales.php
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
<?php
|
||||||
|
$section = 'pharmacy_sales';
|
||||||
|
require_once __DIR__ . '/../layout/header.php';
|
||||||
|
|
||||||
|
$page = $_GET['page'] ?? 1;
|
||||||
|
$limit = 20;
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
|
||||||
|
// Count total
|
||||||
|
$count_stmt = $db->query("SELECT COUNT(*) FROM pharmacy_sales");
|
||||||
|
$total_rows = $count_stmt->fetchColumn();
|
||||||
|
$total_pages = ceil($total_rows / $limit);
|
||||||
|
|
||||||
|
// Fetch Sales
|
||||||
|
$sql = "SELECT s.*, p.name_en as patient_name
|
||||||
|
FROM pharmacy_sales s
|
||||||
|
LEFT JOIN patients p ON s.patient_id = p.id
|
||||||
|
ORDER BY s.created_at DESC
|
||||||
|
LIMIT $limit OFFSET $offset";
|
||||||
|
$stmt = $db->query($sql);
|
||||||
|
$sales = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2 class="fw-bold text-primary"><i class="bi bi-receipt me-2"></i> <?php echo __('sales_history'); ?></h2>
|
||||||
|
<a href="pharmacy_pos.php" class="btn btn-primary">
|
||||||
|
<i class="bi bi-cart-plus me-2"></i> <?php echo __('new_sale'); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th><?php echo __('date'); ?></th>
|
||||||
|
<th><?php echo __('patient'); ?></th>
|
||||||
|
<th><?php echo __('items'); ?></th>
|
||||||
|
<th><?php echo __('total_amount'); ?></th>
|
||||||
|
<th><?php echo __('payment_method'); ?></th>
|
||||||
|
<th><?php echo __('status'); ?></th>
|
||||||
|
<th><?php echo __('actions'); ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php if (empty($sales)): ?>
|
||||||
|
<tr>
|
||||||
|
<td colspan="8" class="text-center py-4 text-muted">
|
||||||
|
<i class="bi bi-inbox fs-1 d-block mb-2"></i>
|
||||||
|
<?php echo __('no_data_found'); ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($sales as $sale): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?php echo $sale['id']; ?></td>
|
||||||
|
<td><?php echo date('Y-m-d H:i', strtotime($sale['created_at'])); ?></td>
|
||||||
|
<td>
|
||||||
|
<?php if ($sale['patient_name']): ?>
|
||||||
|
<a href="patients.php?action=view&id=<?php echo $sale['patient_id']; ?>" class="text-decoration-none">
|
||||||
|
<?php echo htmlspecialchars($sale['patient_name']); ?>
|
||||||
|
</a>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="text-muted"><?php echo __('walk_in'); ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?php
|
||||||
|
// Fetch item count or summary (quick query)
|
||||||
|
$item_count_stmt = $db->prepare("SELECT COUNT(*) FROM pharmacy_sale_items WHERE sale_id = ?");
|
||||||
|
$item_count_stmt->execute([$sale['id']]);
|
||||||
|
echo $item_count_stmt->fetchColumn() . ' ' . __('items');
|
||||||
|
?>
|
||||||
|
</td>
|
||||||
|
<td class="fw-bold"><?php echo number_format($sale['total_amount'], 2); ?></td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-light text-dark border">
|
||||||
|
<?php echo ucfirst($sale['payment_method']); ?>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-success"><?php echo ucfirst($sale['status']); ?></span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-outline-primary" onclick="viewReceipt(<?php echo $sale['id']; ?>)">
|
||||||
|
<i class="bi bi-receipt"></i> <?php echo __('receipt'); ?>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
<?php if ($total_pages > 1): ?>
|
||||||
|
<nav aria-label="Page navigation" class="mt-4">
|
||||||
|
<ul class="pagination justify-content-center">
|
||||||
|
<li class="page-item <?php echo $page <= 1 ? 'disabled' : ''; ?>">
|
||||||
|
<a class="page-link" href="?page=<?php echo $page - 1; ?>"><?php echo __('previous'); ?></a>
|
||||||
|
</li>
|
||||||
|
<?php for ($i = 1; $i <= $total_pages; $i++): ?>
|
||||||
|
<li class="page-item <?php echo $page == $i ? 'active' : ''; ?>">
|
||||||
|
<a class="page-link" href="?page=<?php echo $i; ?>"><?php echo $i; ?></a>
|
||||||
|
</li>
|
||||||
|
<?php endfor; ?>
|
||||||
|
<li class="page-item <?php echo $page >= $total_pages ? 'disabled' : ''; ?>">
|
||||||
|
<a class="page-link" href="?page=<?php echo $page + 1; ?>"><?php echo __('next'); ?></a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Receipt Modal -->
|
||||||
|
<div class="modal fade" id="receiptModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title"><?php echo __('receipt'); ?></h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" id="receiptBody">
|
||||||
|
<!-- Content via JS -->
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?php echo __('close'); ?></button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="printReceiptContent()"><i class="bi bi-printer"></i> <?php echo __('print'); ?></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function viewReceipt(saleId) {
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('receiptModal'));
|
||||||
|
const body = document.getElementById('receiptBody');
|
||||||
|
body.innerHTML = '<div class="text-center py-4"><div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div></div>';
|
||||||
|
modal.show();
|
||||||
|
|
||||||
|
fetch('api/pharmacy.php?action=get_sale_details&sale_id=' + saleId)
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.error) {
|
||||||
|
body.innerHTML = '<div class="text-danger">' + data.error + '</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = `
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<h5><?php echo htmlspecialchars($site_name); ?></h5>
|
||||||
|
<p class="mb-0 text-muted">Receipt #${data.id}</p>
|
||||||
|
<small class="text-muted">${data.created_at}</small>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3 d-flex justify-content-between">
|
||||||
|
<span><strong>Patient:</strong> ${data.patient_name || 'Walk-in'}</span>
|
||||||
|
<span><strong>Method:</strong> ${data.payment_method}</span>
|
||||||
|
</div>
|
||||||
|
<table class="table table-sm border-top border-bottom">
|
||||||
|
<thead><tr><th>Item</th><th class="text-end">Qty</th><th class="text-end">Price</th><th class="text-end">Total</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
`;
|
||||||
|
|
||||||
|
data.items.forEach(item => {
|
||||||
|
html += `
|
||||||
|
<tr>
|
||||||
|
<td>${item.drug_name}</td>
|
||||||
|
<td class="text-end">${item.quantity}</td>
|
||||||
|
<td class="text-end">${parseFloat(item.unit_price).toFixed(2)}</td>
|
||||||
|
<td class="text-end">${parseFloat(item.total_price).toFixed(2)}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
html += `
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr class="fw-bold">
|
||||||
|
<td colspan="3" class="text-end pt-3">Total Amount</td>
|
||||||
|
<td class="text-end pt-3 fs-5">${parseFloat(data.total_amount).toFixed(2)}</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
<div class="text-center mt-4 text-muted small">
|
||||||
|
<p>Thank you for your visit!</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
body.innerHTML = html;
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
console.error(e);
|
||||||
|
body.innerHTML = '<div class="text-danger">Error loading receipt</div>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function printReceiptContent() {
|
||||||
|
const content = document.getElementById('receiptBody').innerHTML;
|
||||||
|
const win = window.open('', '', 'height=600,width=800');
|
||||||
|
win.document.write('<html><head><title>Print Receipt</title>');
|
||||||
|
win.document.write('<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">');
|
||||||
|
win.document.write('</head><body><div class="p-4" style="max-width: 400px; margin: 0 auto;">');
|
||||||
|
win.document.write(content);
|
||||||
|
win.document.write('</div></body></html>');
|
||||||
|
win.document.close();
|
||||||
|
win.focus();
|
||||||
|
setTimeout(() => { win.print(); win.close(); }, 500);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<?php require_once __DIR__ . '/../layout/footer.php'; ?>
|
||||||
49
lang.php
49
lang.php
@ -324,6 +324,30 @@ $translations = [
|
|||||||
'checkout' => 'Checkout',
|
'checkout' => 'Checkout',
|
||||||
'print_bill' => 'Print Bill',
|
'print_bill' => 'Print Bill',
|
||||||
'invoice' => 'Invoice',
|
'invoice' => 'Invoice',
|
||||||
|
'pharmacy' => 'Pharmacy',
|
||||||
|
'inventory' => 'Inventory',
|
||||||
|
'pos' => 'POS',
|
||||||
|
'sales_history' => 'Sales History',
|
||||||
|
'batch_number' => 'Batch Number',
|
||||||
|
'stock' => 'Stock',
|
||||||
|
'add_stock' => 'Add Stock',
|
||||||
|
'cost_price' => 'Cost Price',
|
||||||
|
'sale_price' => 'Sale Price',
|
||||||
|
'min_stock' => 'Min Stock',
|
||||||
|
'reorder_level' => 'Reorder Level',
|
||||||
|
'unit' => 'Unit',
|
||||||
|
'view_batches' => 'View Batches',
|
||||||
|
'received_date' => 'Received Date',
|
||||||
|
'payment_method' => 'Payment Method',
|
||||||
|
'cash' => 'Cash',
|
||||||
|
'card' => 'Card',
|
||||||
|
'receipt' => 'Receipt',
|
||||||
|
'total_amount' => 'Total Amount',
|
||||||
|
'quantity' => 'Quantity',
|
||||||
|
'new_sale' => 'New Sale',
|
||||||
|
'add_to_cart' => 'Add to Cart',
|
||||||
|
'cart' => 'Cart',
|
||||||
|
'insufficient_stock' => 'Insufficient Stock'
|
||||||
],
|
],
|
||||||
'ar' => [
|
'ar' => [
|
||||||
'attachment' => 'المرفق',
|
'attachment' => 'المرفق',
|
||||||
@ -651,5 +675,30 @@ $translations = [
|
|||||||
'checkout' => 'الدفع',
|
'checkout' => 'الدفع',
|
||||||
'print_bill' => 'طباعة الفاتورة',
|
'print_bill' => 'طباعة الفاتورة',
|
||||||
'invoice' => 'فاتورة',
|
'invoice' => 'فاتورة',
|
||||||
|
'pharmacy' => 'الصيدلية',
|
||||||
|
'inventory' => 'المخزون',
|
||||||
|
'pos' => 'نقطة البيع',
|
||||||
|
'sales_history' => 'سجل المبيعات',
|
||||||
|
'batch_number' => 'رقم التشغيلة',
|
||||||
|
'stock' => 'المخزون',
|
||||||
|
'add_stock' => 'إضافة مخزون',
|
||||||
|
'cost_price' => 'سعر التكلفة',
|
||||||
|
'sale_price' => 'سعر البيع',
|
||||||
|
'min_stock' => 'الحد الأدنى للمخزون',
|
||||||
|
'reorder_level' => 'حد إعادة الطلب',
|
||||||
|
'unit' => 'الوحدة',
|
||||||
|
'view_batches' => 'عرض التشغيلات',
|
||||||
|
'received_date' => 'تاريخ الاستلام',
|
||||||
|
'payment_method' => 'طريقة الدفع',
|
||||||
|
'cash' => 'نقدي',
|
||||||
|
'card' => 'بطاقة',
|
||||||
|
'insurance' => 'تأمين',
|
||||||
|
'receipt' => 'إيصال',
|
||||||
|
'total_amount' => 'المبلغ الإجمالي',
|
||||||
|
'quantity' => 'الكمية',
|
||||||
|
'new_sale' => 'بيع جديد',
|
||||||
|
'add_to_cart' => 'إضافة إلى السلة',
|
||||||
|
'cart' => 'السلة',
|
||||||
|
'insufficient_stock' => 'المخزون غير كاف'
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
Loading…
x
Reference in New Issue
Block a user