update reciept

This commit is contained in:
Flatlogic Bot 2026-03-22 17:51:27 +00:00
parent 329da3e5d5
commit 97ae4ba490
10 changed files with 873 additions and 440 deletions

View File

@ -5,15 +5,29 @@ require_once __DIR__ . '/../ai/LocalAIApi.php';
// Get JSON input // Get JSON input
$input = json_decode(file_get_contents('php://input'), true); $input = json_decode(file_get_contents('php://input'), true);
$target = $input['target'] ?? 'treatment_plan'; // symptoms, diagnosis, treatment_plan $target = $input['target'] ?? 'treatment_plan'; // symptoms, diagnosis, treatment_plan, translate
$symptoms = $input['symptoms'] ?? ''; $symptoms = $input['symptoms'] ?? '';
$diagnosis = $input['diagnosis'] ?? ''; $diagnosis = $input['diagnosis'] ?? '';
$currentValue = $input['current_value'] ?? ''; // For expanding symptoms $currentValue = $input['current_value'] ?? ''; // For expanding symptoms or translation text
$systemPrompt = 'You are a professional medical assistant.'; $systemPrompt = 'You are a professional medical assistant.';
$userPrompt = ""; $userPrompt = "";
switch ($target) { switch ($target) {
case 'translate':
$text = $input['text'] ?? '';
$from = $input['from'] ?? 'English';
$to = $input['to'] ?? 'Arabic';
if (empty($text)) {
echo json_encode(['success' => false, 'error' => 'No text provided for translation.']);
exit;
}
$systemPrompt = "You are a professional translator specializing in medical terminology.";
$userPrompt = "Translate the following text from $from to $to. Return only the translated text without any explanations or extra characters.\n\nText: $text";
break;
case 'symptoms': case 'symptoms':
if (empty($currentValue)) { if (empty($currentValue)) {
$userPrompt = "Generate a list of common clinical symptoms for a general checkup in a professional medical format (HTML lists)."; $userPrompt = "Generate a list of common clinical symptoms for a general checkup in a professional medical format (HTML lists).";
@ -57,7 +71,7 @@ try {
if (!empty($response['success'])) { if (!empty($response['success'])) {
$text = LocalAIApi::extractText($response); $text = LocalAIApi::extractText($response);
echo json_encode(['success' => true, 'report' => $text]); echo json_encode(['success' => true, 'report' => trim($text)]);
} else { } else {
echo json_encode(['success' => false, 'error' => $response['error'] ?? 'AI generation failed.']); echo json_encode(['success' => false, 'error' => $response['error'] ?? 'AI generation failed.']);
} }

View File

@ -192,6 +192,14 @@ try {
if (empty($input['items'])) throw new Exception("No items in sale"); if (empty($input['items'])) throw new Exception("No items in sale");
// Check Settings
$allow_negative = false;
try {
$settingStmt = $pdo->prepare("SELECT setting_value FROM settings WHERE setting_key = 'allow_negative_stock'");
$settingStmt->execute();
$allow_negative = (bool)$settingStmt->fetchColumn();
} catch (Exception $e) { /* ignore */ }
$pdo->beginTransaction(); $pdo->beginTransaction();
try { try {
@ -235,9 +243,15 @@ try {
} }
if ($qty_remaining > 0) { if ($qty_remaining > 0) {
if ($allow_negative) {
// Record sale for remaining quantity without batch
$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, null, $qty_remaining, $unit_price, $qty_remaining * $unit_price]);
} else {
throw new Exception("Insufficient stock for drug ID: $drug_id"); throw new Exception("Insufficient stock for drug ID: $drug_id");
} }
} }
}
$pdo->commit(); $pdo->commit();
echo json_encode(['success' => true, 'sale_id' => $sale_id]); echo json_encode(['success' => true, 'sale_id' => $sale_id]);

View File

@ -0,0 +1 @@
ALTER TABLE drugs ADD COLUMN image VARCHAR(255) NULL;

View File

@ -804,10 +804,11 @@ if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST')
$price = $_POST['price'] ?? 0; $price = $_POST['price'] ?? 0;
$expiry_date = $_POST['expiry_date'] ?: null; $expiry_date = $_POST['expiry_date'] ?: null;
$supplier_id = $_POST['supplier_id'] ?: null; $supplier_id = $_POST['supplier_id'] ?: null;
$image = upload_file($_FILES['image'] ?? [], 0, "assets/uploads/drugs/");
if ($name_en && $name_ar) { if ($name_en && $name_ar) {
$stmt = $db->prepare("INSERT INTO drugs (name_en, name_ar, group_id, description_en, description_ar, default_dosage, default_instructions, price, expiry_date, supplier_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); $stmt = $db->prepare("INSERT INTO drugs (name_en, name_ar, group_id, description_en, description_ar, default_dosage, default_instructions, price, expiry_date, supplier_id, image) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$name_en, $name_ar, $group_id, $desc_en, $desc_ar, $dosage, $instructions, $price, $expiry_date, $supplier_id]); $stmt->execute([$name_en, $name_ar, $group_id, $desc_en, $desc_ar, $dosage, $instructions, $price, $expiry_date, $supplier_id, $image]);
$_SESSION['flash_message'] = __('add_drug') . ' ' . __('successfully'); $_SESSION['flash_message'] = __('add_drug') . ' ' . __('successfully');
$redirect = true; $redirect = true;
} }
@ -823,10 +824,16 @@ if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST')
$price = $_POST['price'] ?? 0; $price = $_POST['price'] ?? 0;
$expiry_date = $_POST['expiry_date'] ?: null; $expiry_date = $_POST['expiry_date'] ?: null;
$supplier_id = $_POST['supplier_id'] ?: null; $supplier_id = $_POST['supplier_id'] ?: null;
$image = upload_file($_FILES['image'] ?? [], 0, "assets/uploads/drugs/");
if ($id && $name_en && $name_ar) { if ($id && $name_en && $name_ar) {
if ($image) {
$stmt = $db->prepare("UPDATE drugs SET name_en = ?, name_ar = ?, group_id = ?, description_en = ?, description_ar = ?, default_dosage = ?, default_instructions = ?, price = ?, expiry_date = ?, supplier_id = ?, image = ? WHERE id = ?");
$stmt->execute([$name_en, $name_ar, $group_id, $desc_en, $desc_ar, $dosage, $instructions, $price, $expiry_date, $supplier_id, $image, $id]);
} else {
$stmt = $db->prepare("UPDATE drugs SET name_en = ?, name_ar = ?, group_id = ?, description_en = ?, description_ar = ?, default_dosage = ?, default_instructions = ?, price = ?, expiry_date = ?, supplier_id = ? WHERE id = ?"); $stmt = $db->prepare("UPDATE drugs SET name_en = ?, name_ar = ?, group_id = ?, description_en = ?, description_ar = ?, default_dosage = ?, default_instructions = ?, price = ?, expiry_date = ?, supplier_id = ? WHERE id = ?");
$stmt->execute([$name_en, $name_ar, $group_id, $desc_en, $desc_ar, $dosage, $instructions, $price, $expiry_date, $supplier_id, $id]); $stmt->execute([$name_en, $name_ar, $group_id, $desc_en, $desc_ar, $dosage, $instructions, $price, $expiry_date, $supplier_id, $id]);
}
$_SESSION['flash_message'] = __('edit_drug') . ' ' . __('successfully'); $_SESSION['flash_message'] = __('edit_drug') . ' ' . __('successfully');
$redirect = true; $redirect = true;
} }

View File

@ -57,7 +57,7 @@ if (isset($_GET['ajax_search'])) {
if (empty($drugs)): if (empty($drugs)):
?> ?>
<tr> <tr>
<td colspan="7" class="text-center py-5 text-muted"> <td colspan="8" class="text-center py-5 text-muted">
<i class="bi bi-capsule display-4 d-block mb-3"></i> <i class="bi bi-capsule display-4 d-block mb-3"></i>
<?php echo __('no_drugs_found'); ?> <?php echo __('no_drugs_found'); ?>
</td> </td>
@ -67,10 +67,16 @@ if (isset($_GET['ajax_search'])) {
<tr> <tr>
<td class="px-4 fw-medium text-secondary"><?php echo $drug['id']; ?></td> <td class="px-4 fw-medium text-secondary"><?php echo $drug['id']; ?></td>
<td> <td>
<div class="d-flex align-items-center"> <?php if (!empty($drug['image'])): ?>
<div class="bg-primary bg-opacity-10 text-primary p-2 rounded-circle me-3 flex-shrink-0"> <img src="<?php echo htmlspecialchars($drug['image']); ?>" class="rounded" style="width: 40px; height: 40px; object-fit: cover;">
<i class="bi bi-capsule fs-5"></i> <?php else: ?>
<div class="bg-light rounded d-flex align-items-center justify-content-center text-muted" style="width: 40px; height: 40px;">
<i class="bi bi-image"></i>
</div> </div>
<?php endif; ?>
</td>
<td>
<div class="d-flex align-items-center">
<div class="text-wrap" style="word-break: break-word;"> <div class="text-wrap" style="word-break: break-word;">
<div class="fw-semibold text-dark"><?php echo htmlspecialchars($drug['name_'.$lang]); ?></div> <div class="fw-semibold text-dark"><?php echo htmlspecialchars($drug['name_'.$lang]); ?></div>
<small class="text-muted d-block"><?php echo htmlspecialchars($drug['name_'.($lang == 'en' ? 'ar' : 'en')]); ?></small> <small class="text-muted d-block"><?php echo htmlspecialchars($drug['name_'.($lang == 'en' ? 'ar' : 'en')]); ?></small>
@ -205,6 +211,7 @@ if (isset($_GET['ajax_search'])) {
<thead class="table-light text-secondary"> <thead class="table-light text-secondary">
<tr> <tr>
<th class="px-4 py-3">#</th> <th class="px-4 py-3">#</th>
<th class="py-3" style="width: 50px;"><?php echo __('image'); ?></th>
<th class="py-3" style="width: 30%; min-width: 200px;"><?php echo __('drug_name'); ?></th> <th class="py-3" style="width: 30%; min-width: 200px;"><?php echo __('drug_name'); ?></th>
<th class="py-3"><?php echo __('drug_group'); ?></th> <th class="py-3"><?php echo __('drug_group'); ?></th>
<th class="py-3"><?php echo __('expiry_date'); ?></th> <th class="py-3"><?php echo __('expiry_date'); ?></th>
@ -216,7 +223,7 @@ if (isset($_GET['ajax_search'])) {
<tbody id="drugsTableBody"> <tbody id="drugsTableBody">
<?php if (empty($drugs)): ?> <?php if (empty($drugs)): ?>
<tr> <tr>
<td colspan="7" class="text-center py-5 text-muted"> <td colspan="8" class="text-center py-5 text-muted">
<i class="bi bi-capsule display-4 d-block mb-3"></i> <i class="bi bi-capsule display-4 d-block mb-3"></i>
<?php echo __('no_drugs_found'); ?> <?php echo __('no_drugs_found'); ?>
</td> </td>
@ -226,10 +233,16 @@ if (isset($_GET['ajax_search'])) {
<tr> <tr>
<td class="px-4 fw-medium text-secondary"><?php echo $drug['id']; ?></td> <td class="px-4 fw-medium text-secondary"><?php echo $drug['id']; ?></td>
<td> <td>
<div class="d-flex align-items-center"> <?php if (!empty($drug['image'])): ?>
<div class="bg-primary bg-opacity-10 text-primary p-2 rounded-circle me-3 flex-shrink-0"> <img src="<?php echo htmlspecialchars($drug['image']); ?>" class="rounded" style="width: 40px; height: 40px; object-fit: cover;">
<i class="bi bi-capsule fs-5"></i> <?php else: ?>
<div class="bg-light rounded d-flex align-items-center justify-content-center text-muted" style="width: 40px; height: 40px;">
<i class="bi bi-image"></i>
</div> </div>
<?php endif; ?>
</td>
<td>
<div class="d-flex align-items-center">
<div class="text-wrap" style="word-break: break-word;"> <div class="text-wrap" style="word-break: break-word;">
<div class="fw-semibold text-dark"><?php echo htmlspecialchars($drug['name_'.$lang]); ?></div> <div class="fw-semibold text-dark"><?php echo htmlspecialchars($drug['name_'.$lang]); ?></div>
<small class="text-muted d-block"><?php echo htmlspecialchars($drug['name_'.($lang == 'en' ? 'ar' : 'en')]); ?></small> <small class="text-muted d-block"><?php echo htmlspecialchars($drug['name_'.($lang == 'en' ? 'ar' : 'en')]); ?></small>
@ -337,18 +350,28 @@ if (isset($_GET['ajax_search'])) {
<h5 class="modal-title" id="drugModalTitle"><?php echo __('add_drug'); ?></h5> <h5 class="modal-title" id="drugModalTitle"><?php echo __('add_drug'); ?></h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<form method="POST" action=""> <form method="POST" action="" enctype="multipart/form-data">
<input type="hidden" name="action" id="drugAction" value="add_drug"> <input type="hidden" name="action" id="drugAction" value="add_drug">
<input type="hidden" name="id" id="drugId"> <input type="hidden" name="id" id="drugId">
<div class="modal-body p-4"> <div class="modal-body p-4">
<div class="row g-3"> <div class="row g-3">
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label"><?php echo __('name_en'); ?> <span class="text-danger">*</span></label> <label class="form-label"><?php echo __('name_en'); ?> <span class="text-danger">*</span></label>
<div class="input-group">
<input type="text" class="form-control" name="name_en" id="drugNameEn" required> <input type="text" class="form-control" name="name_en" id="drugNameEn" required>
<button type="button" class="btn btn-outline-secondary" onclick="translateDrug('en', 'ar')" title="<?php echo __('auto_translate'); ?>">
<i class="bi bi-translate"></i>
</button>
</div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label"><?php echo __('name_ar'); ?> <span class="text-danger">*</span></label> <label class="form-label"><?php echo __('name_ar'); ?> <span class="text-danger">*</span></label>
<div class="input-group">
<input type="text" class="form-control" name="name_ar" id="drugNameAr" required> <input type="text" class="form-control" name="name_ar" id="drugNameAr" required>
<button type="button" class="btn btn-outline-secondary" onclick="translateDrug('ar', 'en')" title="<?php echo __('auto_translate'); ?>">
<i class="bi bi-translate"></i>
</button>
</div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
@ -395,6 +418,12 @@ if (isset($_GET['ajax_search'])) {
<input type="text" class="form-control" name="default_instructions" id="drugInstructions"> <input type="text" class="form-control" name="default_instructions" id="drugInstructions">
</div> </div>
<div class="col-md-12">
<label class="form-label"><?php echo __('image'); ?></label>
<input type="file" class="form-control" name="image" accept="image/*" onchange="previewImage(this)">
<div id="drugImagePreview" class="mt-2"></div>
</div>
<div class="col-12"> <div class="col-12">
<label class="form-label"><?php echo __('description_en'); ?></label> <label class="form-label"><?php echo __('description_en'); ?></label>
<textarea class="form-control" name="description_en" id="drugDescEn" rows="2"></textarea> <textarea class="form-control" name="description_en" id="drugDescEn" rows="2"></textarea>
@ -485,6 +514,8 @@ function resetDrugModal() {
document.getElementById('drugDescAr').value = ''; document.getElementById('drugDescAr').value = '';
document.getElementById('drugExpiry').value = ''; document.getElementById('drugExpiry').value = '';
document.getElementById('drugSupplierId').value = ''; document.getElementById('drugSupplierId').value = '';
document.getElementById('drugImagePreview').innerHTML = '';
document.querySelector('input[name="image"]').value = '';
} }
function showEditDrugModal(drug) { function showEditDrugModal(drug) {
@ -503,6 +534,13 @@ function showEditDrugModal(drug) {
document.getElementById('drugExpiry').value = drug.expiry_date || ''; document.getElementById('drugExpiry').value = drug.expiry_date || '';
document.getElementById('drugSupplierId').value = drug.supplier_id || ''; document.getElementById('drugSupplierId').value = drug.supplier_id || '';
const preview = document.getElementById('drugImagePreview');
if (drug.image) {
preview.innerHTML = `<img src="${drug.image}" class="rounded shadow-sm" style="max-height: 100px;">`;
} else {
preview.innerHTML = '';
}
var modal = new bootstrap.Modal(document.getElementById('addDrugModal')); var modal = new bootstrap.Modal(document.getElementById('addDrugModal'));
modal.show(); modal.show();
} }
@ -513,6 +551,52 @@ function showDeleteDrugModal(id) {
modal.show(); modal.show();
} }
function previewImage(input) {
const preview = document.getElementById('drugImagePreview');
if (input.files && input.files[0]) {
const reader = new FileReader();
reader.onload = function(e) {
preview.innerHTML = `<img src="${e.target.result}" class="rounded shadow-sm" style="max-height: 100px;">`;
}
reader.readAsDataURL(input.files[0]);
}
}
async function translateDrug(source, target) {
const text = document.getElementById(source === 'en' ? 'drugNameEn' : 'drugNameAr').value;
if (!text) return;
const btn = event.currentTarget;
const originalContent = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span>';
try {
const response = await fetch('api/ai_report.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
target: 'translate',
text: text,
from: source === 'en' ? 'English' : 'Arabic',
to: target === 'en' ? 'English' : 'Arabic'
})
});
const data = await response.json();
if (data.success) {
document.getElementById(target === 'en' ? 'drugNameEn' : 'drugNameAr').value = data.report;
} else {
alert('Translation failed: ' + (data.error || 'Unknown error'));
}
} catch (e) {
console.error(e);
alert('Network error');
} finally {
btn.disabled = false;
btn.innerHTML = originalContent;
}
}
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const searchInput = document.getElementById('drugSearchInput'); const searchInput = document.getElementById('drugSearchInput');
const tableBody = document.getElementById('drugsTableBody'); const tableBody = document.getElementById('drugsTableBody');

View File

@ -1,3 +1,16 @@
<?php
// Fetch Allow Negative Stock setting
$allowNegativeStock = 0;
try {
$stmt = $db->prepare("SELECT setting_value FROM settings WHERE setting_key = 'allow_negative_stock'");
$stmt->execute();
if ($row = $stmt->fetch()) {
$allowNegativeStock = (int)$row['setting_value'];
}
} catch (Exception $e) {
// defaults to 0
}
?>
<div class="container-fluid h-100"> <div class="container-fluid h-100">
<div class="row h-100"> <div class="row h-100">
<!-- Left Panel: Product Search --> <!-- Left Panel: Product Search -->
@ -10,9 +23,9 @@
</div> </div>
</div> </div>
<div class="card-body p-0 overflow-auto" style="max-height: calc(100vh - 200px);"> <div class="card-body p-0 overflow-auto" style="max-height: calc(100vh - 200px);">
<div id="drugList" class="list-group list-group-flush"> <div id="drugList" class="row row-cols-1 row-cols-md-3 row-cols-lg-4 g-3 p-3">
<!-- Populated by JS --> <!-- Populated by JS -->
<div class="text-center py-5 text-muted"> <div class="col-12 text-center py-5 text-muted">
<div class="spinner-border text-primary" role="status"> <div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span> <span class="visually-hidden">Loading...</span>
</div> </div>
@ -115,6 +128,7 @@
<script> <script>
let cart = []; let cart = [];
let debounceTimer; let debounceTimer;
const allowNegativeStock = <?php echo $allowNegativeStock; ?>;
const searchInput = document.getElementById('drugSearch'); const searchInput = document.getElementById('drugSearch');
const drugList = document.getElementById('drugList'); const drugList = document.getElementById('drugList');
@ -134,7 +148,7 @@ function fetchDrugs(query = '') {
.then(data => { .then(data => {
drugList.innerHTML = ''; drugList.innerHTML = '';
if (data.length === 0) { if (data.length === 0) {
drugList.innerHTML = '<div class="list-group-item text-center text-muted"><?php echo __('no_drugs_found'); ?></div>'; drugList.innerHTML = '<div class="col-12 text-center text-muted py-5"><?php echo __('no_drugs_found'); ?></div>';
return; return;
} }
@ -142,37 +156,44 @@ function fetchDrugs(query = '') {
const stock = parseFloat(drug.stock); const stock = parseFloat(drug.stock);
// Use batch price if available (current active price), otherwise default // Use batch price if available (current active price), otherwise default
const price = parseFloat(drug.batch_price || drug.default_price || 0); const price = parseFloat(drug.batch_price || drug.default_price || 0);
const isOutOfStock = stock <= 0; const isOutOfStock = stock <= 0 && !allowNegativeStock;
let skuHtml = drug.sku ? `<span class="badge bg-secondary me-1">${drug.sku}</span>` : '';
const item = document.createElement('a'); const col = document.createElement('div');
item.className = `list-group-item list-group-item-action d-flex justify-content-between align-items-center ${isOutOfStock ? 'disabled bg-light' : ''}`; col.className = 'col';
let skuHtml = drug.sku ? `<span class="badge bg-secondary me-2">${drug.sku}</span>` : ''; const card = document.createElement('div');
card.className = `card h-100 shadow-sm ${isOutOfStock ? 'bg-light opacity-75' : 'cursor-pointer border-0 hover-shadow'}`;
if (!isOutOfStock) {
card.style.cursor = 'pointer';
card.onclick = () => addToCart(drug);
}
item.innerHTML = ` const imgHtml = drug.image
<div> ? `<img src="${drug.image}" class="card-img-top" alt="${drug.name_en}" style="height: 120px; object-fit: cover;">`
<div class="fw-bold">${drug.name_en}</div> : `<div class="card-img-top bg-light d-flex align-items-center justify-content-center text-muted" style="height: 120px;"><i class="bi bi-capsule fs-1"></i></div>`;
<small class="text-muted">${skuHtml}${drug.name_ar || ''}</small>
</div> card.innerHTML = `
<div class="text-end"> ${imgHtml}
<div class="fw-bold text-primary">${formatCurrency(price)}</div> <div class="card-body p-2">
<small class="${isOutOfStock ? 'text-danger' : 'text-success'}"> <h6 class="card-title text-truncate mb-1 fw-bold" title="${drug.name_en}">${drug.name_en}</h6>
${isOutOfStock ? '<?php echo __('out_of_stock'); ?>' : '<?php echo __('stock'); ?>: ' + stock} <small class="text-muted d-block text-truncate mb-2">${skuHtml}${drug.name_ar || ''}</small>
<div class="d-flex justify-content-between align-items-center mt-auto">
<span class="fw-bold text-primary">${formatCurrency(price)}</span>
<small class="${isOutOfStock ? 'text-danger fw-bold' : 'text-success'}">
${stock <= 0 ? '<?php echo __('out_of_stock'); ?>' : '<?php echo __('stock'); ?>: ' + stock}
</small> </small>
</div> </div>
</div>
`; `;
if (!isOutOfStock) { col.appendChild(card);
item.onclick = () => addToCart(drug); drugList.appendChild(col);
item.style.cursor = 'pointer';
}
drugList.appendChild(item);
}); });
}) })
.catch(err => { .catch(err => {
console.error(err); console.error(err);
drugList.innerHTML = '<div class="list-group-item text-center text-danger">Error loading items</div>'; drugList.innerHTML = '<div class="col-12 text-center text-danger py-5">Error loading items</div>';
}); });
} }
@ -195,7 +216,7 @@ searchInput.addEventListener('input', function() {
function addToCart(drug) { function addToCart(drug) {
const existing = cart.find(i => i.id === drug.id); const existing = cart.find(i => i.id === drug.id);
if (existing) { if (existing) {
if (existing.quantity >= drug.stock) { if (!allowNegativeStock && existing.quantity >= drug.stock) {
alert('<?php echo __('insufficient_stock'); ?>'); alert('<?php echo __('insufficient_stock'); ?>');
return; return;
} }
@ -218,7 +239,7 @@ function updateQty(id, change) {
if (!item) return; if (!item) return;
const newQty = item.quantity + change; const newQty = item.quantity + change;
if (newQty > item.max_stock) { if (!allowNegativeStock && newQty > item.max_stock) {
alert('<?php echo __('insufficient_stock'); ?>'); alert('<?php echo __('insufficient_stock'); ?>');
return; return;
} }

View File

@ -11,13 +11,35 @@
<?php endif; ?> <?php endif; ?>
<div class="card shadow-sm border-0"> <div class="card shadow-sm border-0">
<div class="card-header py-3 d-flex align-items-center"> <div class="card-header bg-white">
<i class="bi bi-info-circle me-2"></i> <ul class="nav nav-tabs card-header-tabs" id="settingsTabs" role="tablist">
<h5 class="mb-0 fw-bold"><?php echo __('company_details'); ?></h5> <li class="nav-item" role="presentation">
<button class="nav-link active" id="general-tab" data-bs-toggle="tab" data-bs-target="#general" type="button" role="tab" aria-controls="general" aria-selected="true">
<i class="bi bi-info-circle me-2"></i><?php echo __('general_settings'); ?>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="system-tab" data-bs-toggle="tab" data-bs-target="#system" type="button" role="tab" aria-controls="system" aria-selected="false">
<i class="bi bi-gear me-2"></i><?php echo __('system_settings'); ?>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="email-tab" data-bs-toggle="tab" data-bs-target="#email" type="button" role="tab" aria-controls="email" aria-selected="false">
<i class="bi bi-envelope me-2"></i><?php echo __('email_settings'); ?>
</button>
</li>
</ul>
</div> </div>
<div class="card-body p-4"> <div class="card-body p-4">
<form action="settings.php" method="POST" enctype="multipart/form-data"> <form action="settings.php" method="POST" enctype="multipart/form-data">
<div class="tab-content" id="settingsTabsContent">
<!-- Tab 1: General Settings -->
<div class="tab-pane fade show active" id="general" role="tabpanel" aria-labelledby="general-tab">
<div class="row g-4"> <div class="row g-4">
<div class="col-12 mb-2">
<h6 class="fw-bold text-dark"><i class="bi bi-building me-2"></i> <?php echo __('company_details'); ?></h6>
</div>
<!-- Basic Information --> <!-- Basic Information -->
<div class="col-md-6"> <div class="col-md-6">
<label for="company_name" class="form-label fw-semibold text-muted small text-uppercase"> <?php echo __('company_name'); ?></label> <label for="company_name" class="form-label fw-semibold text-muted small text-uppercase"> <?php echo __('company_name'); ?></label>
@ -75,8 +97,33 @@
<input type="time" class="form-control" id="working_hours_end" name="working_hours_end" value="<?php echo htmlspecialchars($settings['working_hours_end'] ?? '17:00'); ?>"> <input type="time" class="form-control" id="working_hours_end" name="working_hours_end" value="<?php echo htmlspecialchars($settings['working_hours_end'] ?? '17:00'); ?>">
</div> </div>
<!-- Visit Settings --> <!-- Branding -->
<div class="col-12 mt-4"><hr></div> <div class="col-12 mt-4"><hr></div>
<div class="col-md-6 mt-2">
<label for="company_logo" class="form-label fw-semibold text-muted small text-uppercase"> <?php echo __('company_logo'); ?></label>
<?php if (!empty($settings['company_logo'])): ?>
<div class="mb-2">
<img src="<?php echo htmlspecialchars($settings['company_logo']); ?>" alt="Logo" class="img-thumbnail" style="max-height: 80px;">
</div>
<?php endif; ?>
<input type="file" class="form-control" id="company_logo" name="company_logo" accept="image/*">
</div>
<div class="col-md-6 mt-2">
<label for="company_favicon" class="form-label fw-semibold text-muted small text-uppercase"> <?php echo __('company_favicon'); ?></label>
<?php if (!empty($settings['company_favicon'])): ?>
<div class="mb-2">
<img src="<?php echo htmlspecialchars($settings['company_favicon']); ?>" alt="Favicon" class="img-thumbnail" style="max-height: 32px;">
</div>
<?php endif; ?>
<input type="file" class="form-control" id="company_favicon" name="company_favicon" accept="image/x-icon,image/png">
</div>
</div>
</div>
<!-- Tab 2: System Settings -->
<div class="tab-pane fade" id="system" role="tabpanel" aria-labelledby="system-tab">
<div class="row g-4">
<!-- Visit Settings -->
<div class="col-12 mb-2"> <div class="col-12 mb-2">
<h6 class="fw-bold text-dark"><i class="bi bi-gear me-2"></i> <?php echo __('visit_settings'); ?></h6> <h6 class="fw-bold text-dark"><i class="bi bi-gear me-2"></i> <?php echo __('visit_settings'); ?></h6>
</div> </div>
@ -103,25 +150,75 @@
<input type="number" class="form-control" id="decimal_digits" name="decimal_digits" value="<?php echo htmlspecialchars($settings['decimal_digits'] ?? '2'); ?>" min="0" max="4"> <input type="number" class="form-control" id="decimal_digits" name="decimal_digits" value="<?php echo htmlspecialchars($settings['decimal_digits'] ?? '2'); ?>" min="0" max="4">
</div> </div>
<!-- Branding --> <!-- Pharmacy Settings -->
<div class="col-12 mt-4"><hr></div> <div class="col-12 mt-4"><hr></div>
<div class="col-md-6 mt-2"> <div class="col-12 mb-2">
<label for="company_logo" class="form-label fw-semibold text-muted small text-uppercase"><?php echo __('company_logo'); ?></label> <h6 class="fw-bold text-dark"><i class="bi bi-capsule me-2"></i> <?php echo __('pharmacy_settings'); ?></h6>
<?php if (!empty($settings['company_logo'])): ?>
<div class="mb-2">
<img src="<?php echo htmlspecialchars($settings['company_logo']); ?>" alt="Logo" class="img-thumbnail" style="max-height: 80px;">
</div> </div>
<?php endif; ?> <div class="col-12">
<input type="file" class="form-control" id="company_logo" name="company_logo" accept="image/*"> <div class="form-check form-switch">
<input type="hidden" name="allow_negative_stock" value="0">
<input class="form-check-input" type="checkbox" id="allow_negative_stock" name="allow_negative_stock" value="1" <?php echo !empty($settings['allow_negative_stock']) ? 'checked' : ''; ?>>
<label class="form-check-label" for="allow_negative_stock"> <?php echo __('allow_negative_stock'); ?></label>
<div class="form-text text-muted"> <?php echo __('allow_negative_stock_desc'); ?></div>
</div>
</div>
</div>
</div>
<!-- Tab 3: Email Settings -->
<div class="tab-pane fade" id="email" role="tabpanel" aria-labelledby="email-tab">
<div class="row g-4">
<div class="col-12 mb-2">
<h6 class="fw-bold text-dark"><i class="bi bi-envelope me-2"></i> <?php echo __('email_settings'); ?></h6>
<div class="alert alert-info py-2 small">
<i class="bi bi-info-circle me-1"></i> Use these settings to configure outgoing emails.
</div>
</div>
<div class="col-md-8">
<label for="smtp_host" class="form-label fw-semibold text-muted small text-uppercase"> <?php echo __('smtp_host'); ?></label>
<input type="text" class="form-control" id="smtp_host" name="smtp_host" value="<?php echo htmlspecialchars($settings['smtp_host'] ?? ''); ?>" placeholder="smtp.example.com">
</div>
<div class="col-md-4">
<label for="smtp_port" class="form-label fw-semibold text-muted small text-uppercase"> <?php echo __('smtp_port'); ?></label>
<input type="number" class="form-control" id="smtp_port" name="smtp_port" value="<?php echo htmlspecialchars($settings['smtp_port'] ?? '587'); ?>" placeholder="587">
</div>
<div class="col-md-6">
<label for="smtp_user" class="form-label fw-semibold text-muted small text-uppercase"> <?php echo __('smtp_user'); ?></label>
<input type="text" class="form-control" id="smtp_user" name="smtp_user" value="<?php echo htmlspecialchars($settings['smtp_user'] ?? ''); ?>" placeholder="user@example.com">
</div>
<div class="col-md-6">
<label for="smtp_pass" class="form-label fw-semibold text-muted small text-uppercase"> <?php echo __('smtp_pass'); ?></label>
<input type="password" class="form-control" id="smtp_pass" name="smtp_pass" value="<?php echo htmlspecialchars($settings['smtp_pass'] ?? ''); ?>" placeholder="********">
</div>
<div class="col-md-4">
<label for="smtp_secure" class="form-label fw-semibold text-muted small text-uppercase"> <?php echo __('smtp_secure'); ?></label>
<select class="form-select" id="smtp_secure" name="smtp_secure">
<option value="tls" <?php echo ($settings['smtp_secure'] ?? '') === 'tls' ? 'selected' : ''; ?>><?php echo __('tls'); ?></option>
<option value="ssl" <?php echo ($settings['smtp_secure'] ?? '') === 'ssl' ? 'selected' : ''; ?>><?php echo __('ssl'); ?></option>
<option value="" <?php echo ($settings['smtp_secure'] ?? '') === '' ? 'selected' : ''; ?>><?php echo __('none'); ?></option>
</select>
</div>
<div class="col-12 mt-4"><hr></div>
<div class="col-12 mb-2">
<h6 class="fw-bold text-dark"><i class="bi bi-person-badge me-2"></i> <?php echo __('smtp_from_name'); ?> & <?php echo __('email'); ?></h6>
</div>
<div class="col-md-6">
<label for="smtp_from_email" class="form-label fw-semibold text-muted small text-uppercase"> <?php echo __('smtp_from_email'); ?></label>
<input type="email" class="form-control" id="smtp_from_email" name="smtp_from_email" value="<?php echo htmlspecialchars($settings['smtp_from_email'] ?? ''); ?>" placeholder="noreply@example.com">
</div>
<div class="col-md-6">
<label for="smtp_from_name" class="form-label fw-semibold text-muted small text-uppercase"> <?php echo __('smtp_from_name'); ?></label>
<input type="text" class="form-control" id="smtp_from_name" name="smtp_from_name" value="<?php echo htmlspecialchars($settings['smtp_from_name'] ?? ''); ?>" placeholder="My Hospital">
</div>
</div> </div>
<div class="col-md-6 mt-2">
<label for="company_favicon" class="form-label fw-semibold text-muted small text-uppercase"><?php echo __('company_favicon'); ?></label>
<?php if (!empty($settings['company_favicon'])): ?>
<div class="mb-2">
<img src="<?php echo htmlspecialchars($settings['company_favicon']); ?>" alt="Favicon" class="img-thumbnail" style="max-height: 32px;">
</div> </div>
<?php endif; ?>
<input type="file" class="form-control" id="company_favicon" name="company_favicon" accept="image/x-icon,image/png">
</div> </div>
<div class="col-12 mt-5 text-end"> <div class="col-12 mt-5 text-end">
@ -129,7 +226,6 @@
<i class="bi bi-check2-circle me-2"></i> <?php echo __('save_changes'); ?> <i class="bi bi-check2-circle me-2"></i> <?php echo __('save_changes'); ?>
</button> </button>
</div> </div>
</div>
</form> </form>
</div> </div>
</div> </div>

326
lang.php
View File

@ -382,6 +382,55 @@ $translations = array (
'wait_time' => 'Wait Time', 'wait_time' => 'Wait Time',
'call' => 'Call', 'call' => 'Call',
'finish' => 'Finish', 'finish' => 'Finish',
'drug_group' => 'Drug Group',
'select_group' => 'Select Group',
'select_supplier' => 'Select Supplier',
'import_drugs' => 'Import Drugs',
'upload_file' => 'Upload File',
'csv_format_drugs' => 'Format: Name (EN), Name (AR), Group, Price, Expiry, Supplier',
'delete_drug' => 'Delete Drug',
'auto_translate' => 'Auto Translate',
'translate' => 'Translate',
'image' => 'Image',
'all' => 'All',
'are_you_sure_delete' => 'Are you sure you want to delete?',
'no_drugs_found' => 'No drugs found',
'insufficient_stock' => 'Insufficient stock',
'cart_is_empty' => 'Cart is empty',
'payment_method' => 'Payment Method',
'cash' => 'Cash',
'card' => 'Card',
'total_amount' => 'Total Amount',
'confirm_payment' => 'Confirm Payment',
'cart' => 'Cart',
'checkout' => 'Checkout',
'search_patient' => 'Search Patient',
'cart_empty' => 'Cart is empty',
'stock' => 'Stock',
'default_dosage' => 'Default Dosage',
'default_instructions' => 'Default Instructions',
'import' => 'Import',
'description_en' => 'Description (English)',
'description_ar' => 'Description (Arabic)',
'general_settings' => 'General Settings',
'system_settings' => 'System Settings',
'email_settings' => 'Email Settings',
'pharmacy_settings' => 'Pharmacy Settings',
'allow_negative_stock' => 'Allow Negative Stock Sales',
'allow_negative_stock_desc' => 'If enabled, the POS will allow sales even if the system shows zero stock.',
'smtp_host' => 'SMTP Host',
'smtp_port' => 'SMTP Port',
'smtp_user' => 'SMTP User',
'smtp_pass' => 'SMTP Password',
'smtp_secure' => 'SMTP Encryption',
'smtp_from_email' => 'From Email',
'smtp_from_name' => 'From Name',
'smtp_test' => 'Test SMTP Connection',
'smtp_test_success' => 'SMTP Connection Successful',
'smtp_test_failed' => 'SMTP Connection Failed',
'none' => 'None',
'ssl' => 'SSL',
'tls' => 'TLS'
), ),
'ar' => 'ar' =>
array ( array (
@ -643,127 +692,176 @@ $translations = array (
'add_ad' => 'إضافة إعلان', 'add_ad' => 'إضافة إعلان',
'edit_ad' => 'تعديل إعلان', 'edit_ad' => 'تعديل إعلان',
'delete_ad' => 'حذف إعلان', 'delete_ad' => 'حذف إعلان',
'ad_text_en' => 'نص الإعلان (إنجليزي)', 'ad_text_en' => 'Ad Text (English)',
'ad_text_ar' => 'نص الإعلان (عربي)', 'ad_text_ar' => 'Ad Text (Arabic)',
'no_ads_found' => 'لا توجد إعلانات', 'no_ads_found' => 'No ads found',
'scheduled' => 'مجدولة', 'scheduled' => 'Scheduled',
'completed' => 'مكتملة', 'completed' => 'Completed',
'cancelled' => 'ملغاة', 'cancelled' => 'Cancelled',
'no_address_provided' => 'لم يتم تقديم عنوان', 'no_address_provided' => 'No address provided',
'provider' => 'المقدم', 'provider' => 'Provider',
'unassigned' => 'غير معين', 'unassigned' => 'Unassigned',
'reason_label' => 'السبب', 'reason_label' => 'Reason',
'complete' => 'إكمال', 'complete' => 'Complete',
'complete_home_visit' => 'إكمال الزيارة المنزلية', 'complete_home_visit' => 'Complete Home Visit',
'notes_treatment' => 'ملاحظات / العلاج', 'notes_treatment' => 'Notes / Treatment',
'create_bill' => 'إنشاء فاتورة', 'create_bill' => 'Create Bill',
'save_complete' => 'حفظ وإكمال', 'save_complete' => 'Save & Complete',
'no_appointments_found' => 'لا توجد مواعيد', 'no_appointments_found' => 'No appointments found',
'doctor_is_on_holiday_on_this_date' => 'الطبيب المحدد في إجازة في هذا التاريخ.', 'doctor_is_on_holiday_on_this_date' => 'Selected doctor is on holiday on this date.',
'edit_appointment' => 'تعديل موعد', 'edit_appointment' => 'Edit Appointment',
'appointment_details' => 'تفاصيل الموعد', 'appointment_details' => 'Appointment Details',
'print_daily_list' => 'طباعة القائمة اليومية', 'print_daily_list' => 'Print Daily List',
'appointments_list' => 'قائمة المواعيد', 'appointments_list' => 'Appointments List',
'back' => 'رجوع', 'back' => 'Back',
'no_appointments' => 'لم يتم العثور على مواعيد', 'no_appointments' => 'No appointments found',
'printed_on' => 'تم الطباعة في', 'printed_on' => 'Printed on',
'fill_all_fields' => 'يرجى ملء جميع الحقول', 'fill_all_fields' => 'Please fill in all fields',
'invalid_credentials' => 'البريد الإلكتروني أو كلمة المرور غير صحيحة', 'invalid_credentials' => 'Invalid email or password',
'login_to_continue' => 'قم بتسجيل الدخول للمتابعة', 'login_to_continue' => 'Login to continue to your account',
'forgot_password' => 'هل نسيت كلمة المرور؟', 'forgot_password' => 'Forgot Password?',
'user_profile' => 'الملف الشخصي', 'user_profile' => 'User Profile',
'profile_picture' => 'الصورة الشخصية', 'profile_picture' => 'Profile Picture',
'edit_profile' => 'تعديل الملف الشخصي', 'edit_profile' => 'Edit Profile',
'change_avatar' => 'تغيير الصورة', 'change_avatar' => 'Change Avatar',
'choose_file' => 'اختر ملف', 'choose_file' => 'Choose file',
'allowed_file_types' => 'أنواع الملفات المسموحة', 'allowed_file_types' => 'Allowed file types',
'change_password' => 'تغيير كلمة المرور', 'change_password' => 'Change Password',
'leave_blank_to_keep_current' => 'اتركه فارغاً للاحتفاظ بكلمة المرور الحالية', 'leave_blank_to_keep_current' => 'Leave blank to keep current password',
'new_password' => 'كلمة المرور الجديدة', 'new_password' => 'New Password',
'confirm_password' => 'تأكيد كلمة المرور', 'confirm_password' => 'Confirm Password',
'name_required' => 'الاسم مطلوب', 'name_required' => 'Name is required',
'email_required' => 'البريد الإلكتروني مطلوب', 'email_required' => 'Email is required',
'email_already_taken' => 'البريد الإلكتروني مسجل مسبقاً', 'email_already_taken' => 'Email is already taken',
'password_min_length' => 'يجب أن تتكون كلمة المرور من 6 أحرف على الأقل', 'password_min_length' => 'Password must be at least 6 characters',
'passwords_do_not_match' => 'كلمات المرور غير متطابقة', 'passwords_do_not_match' => 'Passwords do not match',
'invalid_file_type' => 'نوع الملف غير صالح. مسموح فقط بـ JPG, PNG, GIF.', 'invalid_file_type' => 'Invalid file type. Only JPG, PNG and GIF are allowed.',
'upload_failed' => 'فشل تحميل الملف', 'upload_failed' => 'File upload failed',
'profile_updated_successfully' => 'تم تحديث الملف الشخصي بنجاح', 'profile_updated_successfully' => 'Profile updated successfully',
'error_updating_profile' => 'خطأ في تحديث الملف الشخصي', 'error_updating_profile' => 'Error updating profile',
'settings_updated_successfully' => 'تم تحديث الإعدادات بنجاح.', 'settings_updated_successfully' => 'Settings updated successfully.',
'company_details' => 'تفاصيل الشركة', 'company_details' => 'Company Details',
'company_name' => 'اسم الشركة', 'company_name' => 'Company Name',
'company_email' => 'البريد الإلكتروني للشركة', 'company_email' => 'Company Email',
'company_phone' => 'هاتف الشركة', 'company_phone' => 'Company Phone',
'company_address' => 'عنوان الشركة', 'company_address' => 'Company Address',
'ctr_no' => 'رقم السجل التجاري', 'ctr_no' => 'CR No.',
'registration_no' => 'رقم التسجيل', 'registration_no' => 'Registration No.',
'vat_no' => 'الرقم الضريبي', 'vat_no' => 'VAT No.',
'timezone' => 'المنطقة الزمنية', 'timezone' => 'Timezone',
'working_hours_start' => 'بداية ساعات العمل', 'working_hours_start' => 'Working Hours Start',
'working_hours_end' => 'نهاية ساعات العمل', 'working_hours_end' => 'Working Hours End',
'currency_settings' => 'إعدادات العملة', 'currency_settings' => 'Currency Settings',
'currency_symbol' => 'رمز العملة', 'currency_symbol' => 'Currency Symbol',
'decimal_digits' => 'الخانات العشرية', 'decimal_digits' => 'Decimal Digits',
'company_logo' => 'شعار الشركة', 'company_logo' => 'Company Logo',
'company_favicon' => 'أيقونة الموقع', 'company_favicon' => 'Company Favicon',
'civil_id' => 'الرقم المدني', 'civil_id' => 'Civil ID',
'nationality' => 'الجنسية', 'nationality' => 'Nationality',
'dob' => 'تاريخ الميلاد', 'dob' => 'Date of Birth',
'gender' => 'الجنس', 'gender' => 'Gender',
'blood_group' => 'فصيلة الدم', 'blood_group' => 'Blood Group',
'insurance_company' => 'شركة التأمين', 'insurance_company' => 'Insurance Company',
'address' => 'العنوان', 'address' => 'Address',
'male' => 'ذكر', 'male' => 'Male',
'female' => 'أنثى', 'female' => 'Female',
'other' => 'آخر', 'other' => 'Other',
'edit_patient' => 'تعديل مريض', 'edit_patient' => 'Edit Patient',
'delete_patient' => 'حذف مريض', 'delete_patient' => 'Delete Patient',
'confirm_delete' => 'هل أنت متأكد من حذف', 'confirm_delete' => 'Are you sure you want to delete',
'add_doctor' => 'إضافة طبيب', 'add_doctor' => 'Add Doctor',
'edit_doctor' => 'تعديل طبيب', 'edit_doctor' => 'Edit Doctor',
'specialization_en' => 'التخصص (إنجليزي)', 'specialization_en' => 'Specialization (English)',
'specialization_ar' => 'التخصص (عربي)', 'specialization_ar' => 'Specialization (Arabic)',
'hr_management' => 'إدارة الموارد البشرية', 'hr_management' => 'HR Management',
'attendance' => 'الحضور والانصراف', 'attendance' => 'Attendance',
'leave_requests' => 'طلبات الإجازة', 'leave_requests' => 'Leave Requests',
'add_employee' => 'إضافة موظف', 'add_employee' => 'Add Employee',
'edit_employee' => 'تعديل موظف', 'edit_employee' => 'Edit Employee',
'delete_employee' => 'حذف موظف', 'delete_employee' => 'Delete Employee',
'select_position' => 'اختر المسمى الوظيفي', 'select_position' => 'Select Position',
'no_employees_found' => 'لم يتم العثور على موظفين', 'no_employees_found' => 'No employees found',
'visit_settings' => 'Visit Settings', 'visit_settings' => 'Visit Settings',
'disable_visit_edit_24h' => 'Disable editing visits after 24 hours', 'disable_visit_edit_24h' => 'Disable editing visits after 24 hours',
'disable_visit_edit_24h_desc' => 'If enabled, visits cannot be edited 24 hours after their creation.', 'disable_visit_edit_24h_desc' => 'If enabled, visits cannot be edited 24 hours after their creation.',
'details' => 'التفاصيل', 'details' => 'Details',
'vitals' => 'المؤشرات الحيوية', 'vitals' => 'Vitals',
'symptoms_diagnosis' => 'الأعراض والتشخيص', 'symptoms_diagnosis' => 'Symptoms & Diagnosis',
'treatment_plan' => 'خطة العلاج', 'treatment_plan' => 'Treatment Plan',
'prescriptions' => 'الوصفات الطبية', 'prescriptions' => 'Prescriptions',
'weight' => 'الوزن', 'weight' => 'Weight',
'blood_pressure' => 'ضغط الدم', 'blood_pressure' => 'Blood Pressure',
'heart_rate' => 'معدل ضربات القلب', 'heart_rate' => 'Heart Rate',
'temperature' => 'درجة الحرارة', 'temperature' => 'Temperature',
'symptoms' => 'الأعراض', 'symptoms' => 'Symptoms',
'diagnosis' => 'التشخيص', 'diagnosis' => 'Diagnosis',
'drug_name' => 'اسم الدواء', 'drug_name' => 'Drug Name',
'dosage' => 'الجرعة', 'dosage' => 'Dosage',
'instructions' => 'التعليمات', 'instructions' => 'Instructions',
'add_drug' => 'إضافة دواء', 'add_drug' => 'Add Drug',
'nursing_notes' => 'ملاحظات التمريض', 'nursing_notes' => 'Nursing Notes',
'issue_new_token' => 'إصدار تذكرة جديدة', 'issue_new_token' => 'Issue New Token',
'select_patient' => 'اختر المريض', 'select_patient' => 'Select Patient',
'showing_last_50_patients' => 'عرض آخر 50 مريض مسجل', 'showing_last_50_patients' => 'Showing last 50 registered patients',
'doctor_optional' => 'الطبيب (اختياري)', 'doctor_optional' => 'Doctor (Optional)',
'any_doctor' => 'أي طبيب', 'any_doctor' => 'Any Doctor',
'waiting' => 'انتظار', 'waiting' => 'Waiting',
'serving' => 'تحت الخدمة', 'serving' => 'Serving',
'all_statuses' => 'كل الحالات', 'all_statuses' => 'All Statuses',
'refresh' => 'تحديث', 'refresh' => 'تحديث',
'no_tokens_found' => 'لا توجد تذاكر', 'no_tokens_found' => 'لا توجد تذاكر',
'token' => 'التذكرة', 'token' => 'التذكرة',
'wait_time' => 'وقت الانتظار', 'wait_time' => 'وقت الانتظار',
'call' => 'نداء', 'call' => 'نداء',
'finish' => 'إنهاء', 'finish' => 'إنهاء',
), 'drug_group' => 'مجموعة الدواء',
'select_group' => 'اختر المجموعة',
'select_supplier' => 'اختر المورد',
'import_drugs' => 'استيراد الأدوية',
'upload_file' => 'رفع ملف',
'csv_format_drugs' => 'التنسيق: الاسم (إنجليزي), الاسم (عربي), المجموعة, السعر, انتهاء الصلاحية, المورد',
'delete_drug' => 'حذف الدواء',
'auto_translate' => 'ترجمة تلقائية',
'translate' => 'ترجمة',
'image' => 'صورة',
'all' => 'الكل',
'are_you_sure_delete' => 'هل أنت متأكد من الحذف؟',
'no_drugs_found' => 'لم يتم العثور على أدوية',
'insufficient_stock' => 'المخزون غير كاف',
'cart_is_empty' => 'السلة فارغة',
'payment_method' => 'طريقة الدفع',
'cash' => 'نقد',
'card' => 'بطاقة',
'total_amount' => 'المبلغ الإجمالي',
'confirm_payment' => 'تأكيد الدفع',
'cart' => 'السلة',
'checkout' => 'دفع',
'search_patient' => 'بحث عن مريض',
'cart_empty' => 'السلة فارغة',
'stock' => 'المخزون',
'default_dosage' => 'الجرعة الافتراضية',
'default_instructions' => 'التعليمات الافتراضية',
'import' => 'استيراد',
'description_en' => 'الوصف (إنجليزي)',
'description_ar' => 'الوصف (عربي)',
'general_settings' => 'الإعدادات العامة',
'system_settings' => 'إعدادات النظام',
'email_settings' => 'إعدادات البريد الإلكتروني',
'pharmacy_settings' => 'إعدادات الصيدلية',
'allow_negative_stock' => 'السماح بالبيع بالسالب',
'allow_negative_stock_desc' => 'في حالة التفعيل، سيسمح نظام نقاط البيع بإتمام عملية البيع حتى لو كان الرصيد صفراً.',
'smtp_host' => 'خادم SMTP',
'smtp_port' => 'منفذ SMTP',
'smtp_user' => 'اسم مستخدم SMTP',
'smtp_pass' => 'كلمة مرور SMTP',
'smtp_secure' => 'تشفير SMTP',
'smtp_from_email' => 'البريد الإلكتروني للمرسل',
'smtp_from_name' => 'اسم المرسل',
'smtp_test' => 'اختبار اتصال SMTP',
'smtp_test_success' => 'نجح الاتصال بخادم SMTP',
'smtp_test_failed' => 'فشل الاتصال بخادم SMTP',
'none' => 'بدون',
'ssl' => 'SSL',
'tls' => 'TLS'
)
); );

View File

@ -1,15 +1,15 @@
<?php <?php
// Mail configuration sourced from environment variables. // Mail configuration sourced from environment variables or Database Settings.
// No secrets are stored here; the file just maps env -> config array for MailService.
if (!function_exists('env_val')) {
function env_val(string $key, $default = null) { function env_val(string $key, $default = null) {
$v = getenv($key); $v = getenv($key);
return ($v === false || $v === null || $v === '') ? $default : $v; return ($v === false || $v === null || $v === '') ? $default : $v;
} }
}
// Fallback: if critical vars are missing from process env, try to parse executor/.env // Fallback: if critical vars are missing from process env, try to parse executor/.env
// This helps in web/Apache contexts where .env is not exported. if (!function_exists('load_dotenv_if_needed')) {
// Supports simple KEY=VALUE lines; ignores quotes and comments.
function load_dotenv_if_needed(array $keys): void { function load_dotenv_if_needed(array $keys): void {
$missing = array_filter($keys, fn($k) => getenv($k) === false || getenv($k) === ''); $missing = array_filter($keys, fn($k) => getenv($k) === false || getenv($k) === '');
if (empty($missing)) return; if (empty($missing)) return;
@ -22,9 +22,7 @@ function load_dotenv_if_needed(array $keys): void {
if ($line[0] === '#' || trim($line) === '') continue; if ($line[0] === '#' || trim($line) === '') continue;
if (!str_contains($line, '=')) continue; if (!str_contains($line, '=')) continue;
[$k, $v] = array_map('trim', explode('=', $line, 2)); [$k, $v] = array_map('trim', explode('=', $line, 2));
// Strip potential surrounding quotes $v = trim($v, "' ");
$v = trim($v, "\"' ");
// Do not override existing env
if ($k !== '' && (getenv($k) === false || getenv($k) === '')) { if ($k !== '' && (getenv($k) === false || getenv($k) === '')) {
putenv("{$k}={$v}"); putenv("{$k}={$v}");
} }
@ -32,6 +30,7 @@ function load_dotenv_if_needed(array $keys): void {
$loaded = true; $loaded = true;
} }
} }
}
load_dotenv_if_needed([ load_dotenv_if_needed([
'MAIL_TRANSPORT','SMTP_HOST','SMTP_PORT','SMTP_SECURE','SMTP_USER','SMTP_PASS', 'MAIL_TRANSPORT','SMTP_HOST','SMTP_PORT','SMTP_SECURE','SMTP_USER','SMTP_PASS',
@ -39,6 +38,7 @@ load_dotenv_if_needed([
'DKIM_DOMAIN','DKIM_SELECTOR','DKIM_PRIVATE_KEY_PATH' 'DKIM_DOMAIN','DKIM_SELECTOR','DKIM_PRIVATE_KEY_PATH'
]); ]);
// 1. Defaults from Environment
$transport = env_val('MAIL_TRANSPORT', 'smtp'); $transport = env_val('MAIL_TRANSPORT', 'smtp');
$smtp_host = env_val('SMTP_HOST'); $smtp_host = env_val('SMTP_HOST');
$smtp_port = (int) env_val('SMTP_PORT', 587); $smtp_port = (int) env_val('SMTP_PORT', 587);
@ -54,6 +54,35 @@ $dkim_domain = env_val('DKIM_DOMAIN');
$dkim_selector = env_val('DKIM_SELECTOR'); $dkim_selector = env_val('DKIM_SELECTOR');
$dkim_private_key_path = env_val('DKIM_PRIVATE_KEY_PATH'); $dkim_private_key_path = env_val('DKIM_PRIVATE_KEY_PATH');
// 2. Override from Database (if available)
if (file_exists(__DIR__ . '/../db/config.php')) {
require_once __DIR__ . '/../db/config.php';
if (function_exists('db')) {
try {
$pdo = db();
// Check if settings table exists to avoid errors during fresh install/migration
$checkTable = $pdo->query("SHOW TABLES LIKE 'settings'");
if ($checkTable->rowCount() > 0) {
$stmt = $pdo->query("SELECT setting_key, setting_value FROM settings WHERE setting_key LIKE 'smtp_%'");
$db_settings = [];
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$db_settings[$row['setting_key']] = $row['setting_value'];
}
if (!empty($db_settings['smtp_host'])) $smtp_host = $db_settings['smtp_host'];
if (!empty($db_settings['smtp_port'])) $smtp_port = (int)$db_settings['smtp_port'];
if (isset($db_settings['smtp_user'])) $smtp_user = $db_settings['smtp_user'];
if (isset($db_settings['smtp_pass'])) $smtp_pass = $db_settings['smtp_pass'];
if (isset($db_settings['smtp_secure'])) $smtp_secure = $db_settings['smtp_secure'];
if (!empty($db_settings['smtp_from_email'])) $from_email = $db_settings['smtp_from_email'];
if (!empty($db_settings['smtp_from_name'])) $from_name = $db_settings['smtp_from_name'];
}
} catch (Exception $e) {
// Ignore DB errors (e.g. connection issues or during installation)
}
}
}
return [ return [
'transport' => $transport, 'transport' => $transport,

View File

@ -62,169 +62,238 @@ try {
} }
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en" dir="ltr">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Receipt #<?php echo $sale_id; ?></title> <title>Receipt #<?php echo $sale_id; ?></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style> <style>
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; color: #333; }
.receipt-header { border-bottom: 2px solid #333; padding-bottom: 20px; margin-bottom: 20px; }
.receipt-footer { border-top: 1px dashed #333; padding-top: 20px; margin-top: 30px; }
.company-logo { max-height: 80px; }
.receipt-title { font-size: 1.8rem; font-weight: bold; text-transform: uppercase; }
.table thead th { border-bottom: 2px solid #333; background-color: #f8f9fa; font-size: 0.9rem; }
.table-bordered td, .table-bordered th { border-color: #dee2e6; }
.bilingual { display: flex; flex-direction: column; line-height: 1.2; }
.text-ar { font-family: Tahoma, sans-serif; font-size: 0.9em; direction: rtl; }
@media print { @media print {
.no-print { display: none !important; } @page {
body { padding: 0; margin: 0; background: white; } size: 80mm auto; /* Force 80mm width */
.container { max-width: 100%; width: 100%; padding: 0; } margin: 0; /* No margins from printer driver */
.card { border: none !important; box-shadow: none !important; } }
html, body {
width: 80mm;
margin: 0;
padding: 0;
}
.no-print {
display: none !important;
}
.container {
width: 100% !important;
padding: 1mm 2mm !important;
margin: 0 !important;
}
}
body {
font-family: 'Courier New', Courier, monospace; /* Monospace for alignment */
font-size: 11px;
color: #000;
background: #fff;
width: 78mm; /* Screen preview width */
margin: 0 auto;
padding: 2mm;
}
.container {
width: 100%;
box-sizing: border-box;
}
.header {
text-align: center;
margin-bottom: 5px;
}
.logo {
max-width: 40px;
max-height: 40px;
margin-bottom: 2px;
}
.company-name {
font-size: 13px;
font-weight: bold;
text-transform: uppercase;
}
.company-info {
font-size: 10px;
line-height: 1.2;
}
.divider {
border-top: 1px dashed #000;
margin: 4px 0;
}
.receipt-details {
display: flex;
justify-content: space-between;
font-size: 10px;
margin-bottom: 2px;
}
.items-table {
width: 100%;
border-collapse: collapse;
font-size: 10px;
table-layout: fixed;
}
.items-table th {
text-align: left;
border-bottom: 1px dashed #000;
padding: 2px 0;
font-weight: bold;
}
.items-table td {
padding: 2px 0;
vertical-align: top;
word-wrap: break-word;
}
.ar-text {
display: block;
direction: rtl;
font-family: Tahoma, sans-serif; /* Better for Arabic */
font-size: 9px;
color: #333;
}
.total-section {
margin-top: 5px;
border-top: 1px dashed #000;
padding-top: 5px;
font-size: 12px;
}
.footer {
text-align: center;
margin-top: 10px;
font-size: 10px;
}
.btn {
padding: 5px 10px;
cursor: pointer;
background: #007bff;
color: white;
border: none;
border-radius: 3px;
text-decoration: none;
display: inline-block;
font-family: sans-serif;
font-size: 12px;
margin: 2px;
}
.btn-secondary { background: #6c757d; }
.no-print {
text-align: center;
margin-bottom: 10px;
padding: 10px;
background: #f8f9fa;
border: 1px solid #ddd;
} }
</style> </style>
</head> </head>
<body onload="window.print()"> <body onload="window.print()">
<div class="container my-4" style="max-width: 800px;"> <div class="no-print">
<div class="no-print mb-4 text-end"> <button onclick="window.print()" class="btn">Print 80mm</button>
<button onclick="window.print()" class="btn btn-primary"><i class="bi bi-printer"></i> Print</button>
<button onclick="window.close()" class="btn btn-secondary">Close</button> <button onclick="window.close()" class="btn btn-secondary">Close</button>
</div> </div>
<div class="card p-4"> <div class="container">
<!-- Header --> <!-- Header -->
<div class="receipt-header"> <div class="header">
<div class="row align-items-center">
<div class="col-8">
<div class="d-flex align-items-center">
<?php if (!empty($settings['company_logo'])): ?> <?php if (!empty($settings['company_logo'])): ?>
<img src="<?php echo htmlspecialchars($settings['company_logo']); ?>" alt="Logo" class="company-logo me-3"> <img src="<?php echo htmlspecialchars($settings['company_logo']); ?>" alt="Logo" class="logo">
<?php endif; ?> <?php endif; ?>
<div> <div class="company-name"><?php echo htmlspecialchars($settings['company_name'] ?? 'Pharmacy'); ?></div>
<h4 class="fw-bold m-0"><?php echo htmlspecialchars($settings['company_name'] ?? 'Pharmacy Name'); ?></h4> <div class="company-info">
<div class="small text-muted mt-1">
<?php echo htmlspecialchars($settings['company_address'] ?? ''); ?><br> <?php echo htmlspecialchars($settings['company_address'] ?? ''); ?><br>
<?php echo htmlspecialchars($settings['company_phone'] ?? ''); ?> <?php echo htmlspecialchars($settings['company_phone'] ?? ''); ?>
</div> </div>
</div> </div>
<div class="divider"></div>
<!-- Receipt Meta -->
<div class="receipt-details">
<span>#<?php echo $sale_id; ?></span>
<span style="direction: ltr;"><?php echo date('Y-m-d H:i', strtotime($sale['created_at'])); ?></span>
</div> </div>
</div> <div class="receipt-details">
<div class="col-4 text-end"> <span>رقم الإيصال</span>
<div class="receipt-title">RECEIPT</div> <span>التاريخ</span>
<div class="text-ar">إيصال استلام</div>
<div class="mt-2">
<strong>#<?php echo str_pad($sale_id, 6, '0', STR_PAD_LEFT); ?></strong>
</div>
<div class="small text-muted">
<?php echo date('d/m/Y h:i A', strtotime($sale['created_at'])); ?>
</div>
</div>
</div>
</div> </div>
<!-- Patient Info (if applicable) -->
<?php if ($sale['patient_id']): ?> <?php if ($sale['patient_id']): ?>
<div class="row mb-4 border-bottom pb-3"> <div class="divider"></div>
<div class="col-12"> <div class="receipt-details">
<span class="fw-bold">Patient / المريض:</span> <span>Patient / المريض:</span>
<?php echo htmlspecialchars($sale['patient_name']); ?>
<?php if($sale['patient_phone']) echo ' (' . htmlspecialchars($sale['patient_phone']) . ')'; ?>
</div> </div>
<div style="font-weight: bold; font-size: 11px;">
<?php echo htmlspecialchars($sale['patient_name']); ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
<!-- Items Table --> <div class="divider"></div>
<table class="table table-bordered mb-4">
<!-- Items -->
<table class="items-table">
<thead> <thead>
<tr> <tr>
<th class="text-center" width="50">#</th> <th style="width: 45%;">Item / الصنف</th>
<th> <th style="width: 15%; text-align: center;">Qty<br>العدد</th>
<div class="d-flex justify-content-between"> <th style="width: 20%; text-align: right;">Price<br>السعر</th>
<span>Item Description</span> <th style="width: 20%; text-align: right;">Total<br>الإجمالي</th>
<span class="text-ar">الصنف</span>
</div>
</th>
<th class="text-center" width="80">
<div class="d-flex justify-content-between px-1">
<span>Qty</span>
<span class="text-ar">العدد</span>
</div>
</th>
<th class="text-end" width="120">
<div class="d-flex justify-content-between px-1">
<span>Price</span>
<span class="text-ar">السعر</span>
</div>
</th>
<th class="text-end" width="120">
<div class="d-flex justify-content-between px-1">
<span>Total</span>
<span class="text-ar">الإجمالي</span>
</div>
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php $i = 1; foreach ($items as $item): ?> <?php foreach ($items as $item): ?>
<tr> <tr>
<td class="text-center"><?php echo $i++; ?></td>
<td> <td>
<div class="fw-bold"><?php echo htmlspecialchars($item['drug_name_en']); ?></div> <?php echo htmlspecialchars($item['drug_name_en']); ?>
<?php if($item['drug_name_ar']): ?> <?php if(!empty($item['drug_name_ar'])): ?>
<div class="text-ar small text-muted"><?php echo htmlspecialchars($item['drug_name_ar']); ?></div> <br><span class="ar-text"><?php echo htmlspecialchars($item['drug_name_ar']); ?></span>
<?php endif; ?>
<?php if($item['sku']): ?>
<div class="small text-muted" style="font-size: 0.75rem;">SKU: <?php echo htmlspecialchars($item['sku']); ?></div>
<?php endif; ?> <?php endif; ?>
</td> </td>
<td class="text-center"><?php echo $item['quantity']; ?></td> <td style="text-align: center;"><?php echo $item['quantity']; ?></td>
<td class="text-end"><?php echo format_currency($item['unit_price']); ?></td> <td style="text-align: right;"><?php echo number_format($item['unit_price'], 2); ?></td>
<td class="text-end"><?php echo format_currency($item['total_price']); ?></td> <td style="text-align: right;"><?php echo number_format($item['total_price'], 2); ?></td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
</tbody> </tbody>
<tfoot>
<tr>
<td colspan="4" class="text-end fw-bold">
TOTAL / الإجمالي
</td>
<td class="text-end fw-bold fs-5">
<?php echo format_currency($sale['total_amount']); ?>
</td>
</tr>
</tfoot>
</table> </table>
<!-- Payment Info --> <!-- Totals -->
<div class="row"> <div class="total-section">
<div class="col-6"> <div style="display: flex; justify-content: space-between; font-weight: bold; font-size: 13px;">
<div class="small"> <div>TOTAL <span style="font-size: 11px; font-weight: normal;">الإجمالي</span></div>
<strong>Payment Method / طريقة الدفع:</strong><br> <div><?php echo format_currency($sale['total_amount']); ?></div>
<span class="text-uppercase"><?php echo htmlspecialchars($sale['payment_method']); ?></span>
</div> </div>
<div style="display: flex; justify-content: space-between; margin-top: 5px; font-size: 10px;">
<div>Paid Via <span style="font-size: 9px;">طريقة الدفع</span></div>
<div style="text-transform: uppercase;">
<?php echo htmlspecialchars($sale['payment_method']); ?>
<?php
$methods_ar = ['cash' => 'نقد', 'card' => 'بطاقة', 'insurance' => 'تأمين'];
if(isset($methods_ar[strtolower($sale['payment_method'])])) {
echo ' / ' . $methods_ar[strtolower($sale['payment_method'])];
}
?>
</div> </div>
<div class="col-6 text-end">
<!--
<br>
<div class="border-top border-dark d-inline-block pt-1" style="width: 200px; text-align: center;">
<span class="small">Signature / التوقيع</span>
</div>
-->
</div> </div>
</div> </div>
<div class="divider"></div>
<!-- Footer --> <!-- Footer -->
<div class="receipt-footer text-center small text-muted"> <div class="footer">
<p class="mb-1">Thank you for your visit! / !شكراً لزيارتكم</p> Thank You! / شكراً لكم<br>
<p>Get Well Soon / تمنياتنا بالشفاء العاجل</p> Get Well Soon / بالشفاء العاجل
</div>
</div> </div>
<!-- Cut space -->
<div style="height: 10mm;"></div>
</div> </div>
</body> </body>