new updates
This commit is contained in:
parent
b26eab2ba0
commit
110b26742e
@ -47,9 +47,94 @@ foreach ($items as $item) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$vat_or_discount = (float)$order['discount'];
|
$vat_or_discount = (float)$order['discount'];
|
||||||
|
$company_settings = get_company_settings();
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<style>
|
||||||
|
/* Print Styles */
|
||||||
|
@media print {
|
||||||
|
.sidebar, .navbar, header, .no-print, .btn, .breadcrumb, .no-print * {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
.main-content {
|
||||||
|
margin-left: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin-top: 0 !important;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background-color: #fff !important;
|
||||||
|
color: #000 !important;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
border: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
.container-fluid {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
.print-only {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
.table {
|
||||||
|
width: 100% !important;
|
||||||
|
border-collapse: collapse !important;
|
||||||
|
}
|
||||||
|
.table th, .table td {
|
||||||
|
border: 1px solid #dee2e6 !important;
|
||||||
|
padding: 8px !important;
|
||||||
|
}
|
||||||
|
.status-badge {
|
||||||
|
border: 1px solid #000 !important;
|
||||||
|
color: #000 !important;
|
||||||
|
background: none !important;
|
||||||
|
}
|
||||||
|
.card-body {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.print-only {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!-- Formal Print Header -->
|
||||||
|
<div class="print-only mb-4">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col-6">
|
||||||
|
<?php if (!empty($company_settings['logo_url'])): ?>
|
||||||
|
<img src="../<?= htmlspecialchars($company_settings['logo_url']) ?>" alt="Logo" style="max-height: 80px;">
|
||||||
|
<?php else: ?>
|
||||||
|
<h2 class="fw-bold"><?= htmlspecialchars($company_settings['company_name']) ?></h2>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 text-end">
|
||||||
|
<h1 class="fw-bold mb-0">INVOICE</h1>
|
||||||
|
<p class="mb-0">#<?= $order['id'] ?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-6">
|
||||||
|
<h6 class="fw-bold text-uppercase text-muted small mb-2">Company Details</h6>
|
||||||
|
<div class="fw-bold"><?= htmlspecialchars($company_settings['company_name']) ?></div>
|
||||||
|
<div><?= nl2br(htmlspecialchars($company_settings['address'] ?? '')) ?></div>
|
||||||
|
<div>Phone: <?= htmlspecialchars($company_settings['phone'] ?? '') ?></div>
|
||||||
|
<?php if (!empty($company_settings['vat_number'])): ?>
|
||||||
|
<div>VAT No: <?= htmlspecialchars($company_settings['vat_number']) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 text-end">
|
||||||
|
<h6 class="fw-bold text-uppercase text-muted small mb-2">Order Info</h6>
|
||||||
|
<div><strong>Date:</strong> <?= date('M d, Y H:i', strtotime($order['created_at'])) ?></div>
|
||||||
|
<div><strong>Status:</strong> <?= ucfirst($order['status']) ?></div>
|
||||||
|
<div><strong>Payment:</strong> <?= htmlspecialchars($order['payment_type_name'] ?? 'N/A') ?></div>
|
||||||
|
<div><strong>Type:</strong> <?= ucfirst($order['order_type']) ?></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4 no-print">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="fw-bold mb-0">Order #<?= $order['id'] ?></h2>
|
<h2 class="fw-bold mb-0">Order #<?= $order['id'] ?></h2>
|
||||||
<p class="text-muted mb-0">Placed on <?= date('M d, Y H:i', strtotime($order['created_at'])) ?></p>
|
<p class="text-muted mb-0">Placed on <?= date('M d, Y H:i', strtotime($order['created_at'])) ?></p>
|
||||||
@ -74,7 +159,7 @@ $vat_or_discount = (float)$order['discount'];
|
|||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<!-- Order Items -->
|
<!-- Order Items -->
|
||||||
<div class="card border-0 shadow-sm mb-4">
|
<div class="card border-0 shadow-sm mb-4">
|
||||||
<div class="card-header bg-white py-3">
|
<div class="card-header bg-white py-3 no-print">
|
||||||
<h5 class="card-title mb-0 fw-bold">Order Items</h5>
|
<h5 class="card-title mb-0 fw-bold">Order Items</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
@ -129,17 +214,17 @@ $vat_or_discount = (float)$order['discount'];
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Additional Info -->
|
<!-- Additional Info -->
|
||||||
<div class="card border-0 shadow-sm">
|
<div class="card border-0 shadow-sm mb-4 no-print">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="fw-bold mb-3">Internal Notes</h5>
|
<h5 class="fw-bold mb-3">Internal Notes</h5>
|
||||||
<p class="text-muted"><?= htmlspecialchars($order['notes'] ?? 'No notes provided for this order.') ?></p>
|
<p class="text-muted mb-0"><?= htmlspecialchars($order['notes'] ?? 'No notes provided for this order.') ?></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<!-- Status & Payment -->
|
<!-- Status & Payment -->
|
||||||
<div class="card border-0 shadow-sm mb-4">
|
<div class="card border-0 shadow-sm mb-4 no-print">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h6 class="text-muted small text-uppercase fw-bold mb-3">Order Status</h6>
|
<h6 class="text-muted small text-uppercase fw-bold mb-3">Order Status</h6>
|
||||||
<div class="d-flex align-items-center mb-4">
|
<div class="d-flex align-items-center mb-4">
|
||||||
@ -194,7 +279,7 @@ $vat_or_discount = (float)$order['discount'];
|
|||||||
<h6 class="text-muted small text-uppercase fw-bold mb-3">Customer Information</h6>
|
<h6 class="text-muted small text-uppercase fw-bold mb-3">Customer Information</h6>
|
||||||
<?php if ($order['customer_name']): ?>
|
<?php if ($order['customer_name']): ?>
|
||||||
<div class="d-flex align-items-center mb-3">
|
<div class="d-flex align-items-center mb-3">
|
||||||
<div class="bg-primary bg-opacity-10 text-primary p-2 rounded-circle me-3">
|
<div class="bg-primary bg-opacity-10 text-primary p-2 rounded-circle me-3 no-print">
|
||||||
<i class="bi bi-person fs-4"></i>
|
<i class="bi bi-person fs-4"></i>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -204,19 +289,28 @@ $vat_or_discount = (float)$order['discount'];
|
|||||||
</div>
|
</div>
|
||||||
<?php if ($order['customer_phone']): ?>
|
<?php if ($order['customer_phone']): ?>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<i class="bi bi-telephone text-muted me-2"></i>
|
<i class="bi bi-telephone text-muted me-2 no-print"></i>
|
||||||
|
<span class="print-only">Phone: </span>
|
||||||
<a href="tel:<?= $order['customer_phone'] ?>" class="text-decoration-none text-dark"><?= htmlspecialchars($order['customer_phone'] ?? '') ?></a>
|
<a href="tel:<?= $order['customer_phone'] ?>" class="text-decoration-none text-dark"><?= htmlspecialchars($order['customer_phone'] ?? '') ?></a>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if ($order['customer_email']): ?>
|
<?php if ($order['customer_email']): ?>
|
||||||
<div class="mb-0">
|
<div class="mb-2">
|
||||||
<i class="bi bi-envelope text-muted me-2"></i>
|
<i class="bi bi-envelope text-muted me-2 no-print"></i>
|
||||||
|
<span class="print-only">Email: </span>
|
||||||
<a href="mailto:<?= $order['customer_email'] ?>" class="text-decoration-none text-dark"><?= htmlspecialchars($order['customer_email'] ?? '') ?></a>
|
<a href="mailto:<?= $order['customer_email'] ?>" class="text-decoration-none text-dark"><?= htmlspecialchars($order['customer_email'] ?? '') ?></a>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($order['customer_address'])): ?>
|
||||||
|
<div class="mb-0">
|
||||||
|
<i class="bi bi-geo-alt text-muted me-2 no-print"></i>
|
||||||
|
<span class="print-only">Address: </span>
|
||||||
|
<span class="text-dark"><?= htmlspecialchars($order['customer_address']) ?></span>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div class="text-center py-3">
|
<div class="text-center py-3">
|
||||||
<i class="bi bi-person-x fs-1 text-muted opacity-25"></i>
|
<i class="bi bi-person-x fs-1 text-muted opacity-25 no-print"></i>
|
||||||
<p class="text-muted small mb-0 mt-2">No customer attached to this order (Guest)</p>
|
<p class="text-muted small mb-0 mt-2">No customer attached to this order (Guest)</p>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
@ -267,6 +361,11 @@ function printThermalReceipt() {
|
|||||||
|
|
||||||
const win = window.open('', 'Receipt', `width=${width},height=${height},top=${top},left=${left}`);
|
const win = window.open('', 'Receipt', `width=${width},height=${height},top=${top},left=${left}`);
|
||||||
|
|
||||||
|
if (!win) {
|
||||||
|
alert('Please allow popups for this website to print thermal receipts.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const tr = {
|
const tr = {
|
||||||
'Order': 'الطلب',
|
'Order': 'الطلب',
|
||||||
'Type': 'النوع',
|
'Type': 'النوع',
|
||||||
@ -278,6 +377,7 @@ function printThermalReceipt() {
|
|||||||
'TOTAL': 'المجموع',
|
'TOTAL': 'المجموع',
|
||||||
'Subtotal': 'المجموع الفرعي',
|
'Subtotal': 'المجموع الفرعي',
|
||||||
'VAT': 'ضريبة القيمة المضافة',
|
'VAT': 'ضريبة القيمة المضافة',
|
||||||
|
'Tax Included': 'شامل الضريبة',
|
||||||
'THANK YOU FOR YOUR VISIT!': 'شكراً لزيارتكم!',
|
'THANK YOU FOR YOUR VISIT!': 'شكراً لزيارتكم!',
|
||||||
'Please come again.': 'يرجى زيارتنا مرة أخرى.',
|
'Please come again.': 'يرجى زيارتنا مرة أخرى.',
|
||||||
'Customer Details': 'تفاصيل العميل',
|
'Customer Details': 'تفاصيل العميل',
|
||||||
|
|||||||
@ -312,6 +312,7 @@ include 'includes/header.php';
|
|||||||
'cash' => 'bg-success',
|
'cash' => 'bg-success',
|
||||||
'credit card' => 'bg-primary',
|
'credit card' => 'bg-primary',
|
||||||
'loyalty redeem' => 'bg-warning',
|
'loyalty redeem' => 'bg-warning',
|
||||||
|
'bank transfer' => 'bg-info',
|
||||||
'unpaid' => 'bg-secondary',
|
'unpaid' => 'bg-secondary',
|
||||||
default => 'bg-secondary'
|
default => 'bg-secondary'
|
||||||
};
|
};
|
||||||
|
|||||||
@ -11,60 +11,76 @@ $message = '';
|
|||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||||
$action = $_POST['action'];
|
$action = $_POST['action'];
|
||||||
$id = isset($_POST['id']) ? (int)$_POST['id'] : null;
|
$id = isset($_POST['id']) ? (int)$_POST['id'] : null;
|
||||||
$name = $_POST['name'];
|
|
||||||
$category_id = $_POST['category_id'];
|
|
||||||
$price = $_POST['price'];
|
|
||||||
$cost_price = $_POST['cost_price'] ?: 0;
|
|
||||||
$stock_quantity = $_POST['stock_quantity'] ?: 0;
|
|
||||||
$description = $_POST['description'];
|
|
||||||
|
|
||||||
$promo_discount_percent = $_POST['promo_discount_percent'] !== '' ? $_POST['promo_discount_percent'] : null;
|
|
||||||
$promo_date_from = $_POST['promo_date_from'] !== '' ? $_POST['promo_date_from'] : null;
|
|
||||||
$promo_date_to = $_POST['promo_date_to'] !== '' ? $_POST['promo_date_to'] : null;
|
|
||||||
|
|
||||||
$image_url = null;
|
if ($action === 'cancel_promotion' && $id) {
|
||||||
if ($id) {
|
if (!has_permission('products_edit')) {
|
||||||
$stmt = $pdo->prepare("SELECT image_url FROM products WHERE id = ?");
|
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to edit products.</div>';
|
||||||
$stmt->execute([$id]);
|
} else {
|
||||||
$image_url = $stmt->fetchColumn();
|
try {
|
||||||
|
$stmt = $pdo->prepare("UPDATE products SET promo_discount_percent = NULL, promo_date_from = NULL, promo_date_to = NULL WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$message = '<div class="alert alert-success">Promotion cancelled successfully!</div>';
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
$message = '<div class="alert alert-danger">Database error: ' . $e->getMessage() . '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
$image_url = 'https://placehold.co/400x300?text=' . urlencode($name);
|
$name = $_POST['name'];
|
||||||
}
|
$name_ar = $_POST['name_ar'] ?? '';
|
||||||
|
$category_id = $_POST['category_id'];
|
||||||
if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
|
$price = $_POST['price'];
|
||||||
$uploadDir = __DIR__ . '/../assets/images/products/';
|
$cost_price = $_POST['cost_price'] ?: 0;
|
||||||
if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);
|
$stock_quantity = $_POST['stock_quantity'] ?: 0;
|
||||||
|
$description = $_POST['description'];
|
||||||
|
|
||||||
$file_ext = strtolower(pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION));
|
$promo_discount_percent = $_POST['promo_discount_percent'] !== '' ? $_POST['promo_discount_percent'] : null;
|
||||||
if (in_array($file_ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'])) {
|
$promo_date_from = $_POST['promo_date_from'] !== '' ? $_POST['promo_date_from'] : null;
|
||||||
$fileName = uniqid('prod_') . '.' . $file_ext;
|
$promo_date_to = $_POST['promo_date_to'] !== '' ? $_POST['promo_date_to'] : null;
|
||||||
if (move_uploaded_file($_FILES['image']['tmp_name'], $uploadDir . $fileName)) {
|
|
||||||
$image_url = 'assets/images/products/' . $fileName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
$image_url = null;
|
||||||
if ($action === 'edit_product' && $id) {
|
if ($id) {
|
||||||
// Check for edit OR add (for backward compatibility)
|
$stmt = $pdo->prepare("SELECT image_url FROM products WHERE id = ?");
|
||||||
if (!has_permission('products_edit') && !has_permission('products_add')) {
|
$stmt->execute([$id]);
|
||||||
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to edit products.</div>';
|
$image_url = $stmt->fetchColumn();
|
||||||
} else {
|
} else {
|
||||||
$stmt = $pdo->prepare("UPDATE products SET name = ?, category_id = ?, price = ?, cost_price = ?, stock_quantity = ?, description = ?, image_url = ?, promo_discount_percent = ?, promo_date_from = ?, promo_date_to = ? WHERE id = ?");
|
$image_url = 'https://placehold.co/400x300?text=' . urlencode($name);
|
||||||
$stmt->execute([$name, $category_id, $price, $cost_price, $stock_quantity, $description, $image_url, $promo_discount_percent, $promo_date_from, $promo_date_to, $id]);
|
}
|
||||||
$message = '<div class="alert alert-success">Product updated successfully!</div>';
|
|
||||||
}
|
if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
|
||||||
} elseif ($action === 'add_product') {
|
$uploadDir = __DIR__ . '/../assets/images/products/';
|
||||||
if (!has_permission('products_add')) {
|
if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);
|
||||||
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to add products.</div>';
|
|
||||||
} else {
|
$file_ext = strtolower(pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION));
|
||||||
$stmt = $pdo->prepare("INSERT INTO products (name, category_id, price, cost_price, stock_quantity, description, image_url, promo_discount_percent, promo_date_from, promo_date_to) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
if (in_array($file_ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'])) {
|
||||||
$stmt->execute([$name, $category_id, $price, $cost_price, $stock_quantity, $description, $image_url, $promo_discount_percent, $promo_date_from, $promo_date_to]);
|
$fileName = uniqid('prod_') . '.' . $file_ext;
|
||||||
$message = '<div class="alert alert-success">Product created successfully!</div>';
|
if (move_uploaded_file($_FILES['image']['tmp_name'], $uploadDir . $fileName)) {
|
||||||
|
$image_url = 'assets/images/products/' . $fileName;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (PDOException $e) {
|
|
||||||
$message = '<div class="alert alert-danger">Database error: ' . $e->getMessage() . '</div>';
|
try {
|
||||||
|
if ($action === 'edit_product' && $id) {
|
||||||
|
// Check for edit OR add (for backward compatibility)
|
||||||
|
if (!has_permission('products_edit') && !has_permission('products_add')) {
|
||||||
|
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to edit products.</div>';
|
||||||
|
} else {
|
||||||
|
$stmt = $pdo->prepare("UPDATE products SET name = ?, name_ar = ?, category_id = ?, price = ?, cost_price = ?, stock_quantity = ?, description = ?, image_url = ?, promo_discount_percent = ?, promo_date_from = ?, promo_date_to = ? WHERE id = ?");
|
||||||
|
$stmt->execute([$name, $name_ar, $category_id, $price, $cost_price, $stock_quantity, $description, $image_url, $promo_discount_percent, $promo_date_from, $promo_date_to, $id]);
|
||||||
|
$message = '<div class="alert alert-success">Product updated successfully!</div>';
|
||||||
|
}
|
||||||
|
} elseif ($action === 'add_product') {
|
||||||
|
if (!has_permission('products_add')) {
|
||||||
|
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to add products.</div>';
|
||||||
|
} else {
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO products (name, name_ar, category_id, price, cost_price, stock_quantity, description, image_url, promo_discount_percent, promo_date_from, promo_date_to) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||||
|
$stmt->execute([$name, $name_ar, $category_id, $price, $cost_price, $stock_quantity, $description, $image_url, $promo_discount_percent, $promo_date_from, $promo_date_to]);
|
||||||
|
$message = '<div class="alert alert-success">Product created successfully!</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
$message = '<div class="alert alert-danger">Database error: ' . $e->getMessage() . '</div>';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,7 +108,8 @@ $query = "SELECT p.*, c.name as category_name
|
|||||||
LEFT JOIN categories c ON p.category_id = c.id";
|
LEFT JOIN categories c ON p.category_id = c.id";
|
||||||
|
|
||||||
if ($search) {
|
if ($search) {
|
||||||
$where[] = "(p.name LIKE ? OR p.description LIKE ?)";
|
$where[] = "(p.name LIKE ? OR p.name_ar LIKE ? OR p.description LIKE ?)";
|
||||||
|
$params[] = "%$search%";
|
||||||
$params[] = "%$search%";
|
$params[] = "%$search%";
|
||||||
$params[] = "%$search%";
|
$params[] = "%$search%";
|
||||||
}
|
}
|
||||||
@ -173,6 +190,7 @@ include 'includes/header.php';
|
|||||||
!empty($product['promo_date_to']) &&
|
!empty($product['promo_date_to']) &&
|
||||||
$today >= $product['promo_date_from'] &&
|
$today >= $product['promo_date_from'] &&
|
||||||
$today <= $product['promo_date_to'];
|
$today <= $product['promo_date_to'];
|
||||||
|
$has_promo_data = !empty($product['promo_discount_percent']);
|
||||||
?>
|
?>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="ps-4">
|
<td class="ps-4">
|
||||||
@ -180,6 +198,9 @@ include 'includes/header.php';
|
|||||||
<img src="<?= htmlspecialchars(strpos($product['image_url'], 'http') === 0 ? $product['image_url'] : '../' . $product['image_url']) ?>" alt="" class="rounded-3 me-3 border shadow-sm" style="width: 48px; height: 48px; object-fit: cover;">
|
<img src="<?= htmlspecialchars(strpos($product['image_url'], 'http') === 0 ? $product['image_url'] : '../' . $product['image_url']) ?>" alt="" class="rounded-3 me-3 border shadow-sm" style="width: 48px; height: 48px; object-fit: cover;">
|
||||||
<div>
|
<div>
|
||||||
<div class="fw-bold text-dark"><?= htmlspecialchars($product['name']) ?></div>
|
<div class="fw-bold text-dark"><?= htmlspecialchars($product['name']) ?></div>
|
||||||
|
<?php if (!empty($product['name_ar'])): ?>
|
||||||
|
<div class="text-primary small fw-semibold" dir="rtl"><?= htmlspecialchars($product['name_ar']) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
<div class="text-muted small text-truncate" style="max-width: 180px;"><?= htmlspecialchars($product['description'] ?? '') ?></div>
|
<div class="text-muted small text-truncate" style="max-width: 180px;"><?= htmlspecialchars($product['description'] ?? '') ?></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -200,13 +221,25 @@ include 'includes/header.php';
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<?php if ($is_promo_active): ?>
|
<div class="d-flex align-items-center gap-2">
|
||||||
<span class="badge bg-success rounded-pill px-2">-<?= floatval($product['promo_discount_percent']) ?>%</span>
|
<?php if ($is_promo_active): ?>
|
||||||
<?php elseif (!empty($product['promo_discount_percent'])): ?>
|
<span class="badge bg-success rounded-pill px-2">-<?= floatval($product['promo_discount_percent']) ?>%</span>
|
||||||
<span class="badge bg-secondary rounded-pill px-2">Inactive</span>
|
<?php elseif (!empty($product['promo_discount_percent'])): ?>
|
||||||
<?php else: ?>
|
<span class="badge bg-secondary rounded-pill px-2">Inactive</span>
|
||||||
<span class="text-muted small">-</span>
|
<?php else: ?>
|
||||||
<?php endif; ?>
|
<span class="text-muted small">-</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($has_promo_data && has_permission('products_edit')): ?>
|
||||||
|
<form method="POST" class="d-inline" onsubmit="return confirm('Cancel this promotion?')">
|
||||||
|
<input type="hidden" name="action" value="cancel_promotion">
|
||||||
|
<input type="hidden" name="id" value="<?= $product['id'] ?>">
|
||||||
|
<button type="submit" class="btn btn-link p-0 text-danger" title="Cancel Promotion">
|
||||||
|
<i class="bi bi-x-circle-fill"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end pe-4">
|
<td class="text-end pe-4">
|
||||||
<div class="d-inline-flex gap-2">
|
<div class="d-inline-flex gap-2">
|
||||||
@ -249,9 +282,20 @@ include 'includes/header.php';
|
|||||||
<div class="row g-3 mb-4">
|
<div class="row g-3 mb-4">
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label small fw-bold text-muted">PRODUCT NAME <span class="text-danger">*</span></label>
|
<label class="form-label small fw-bold text-muted">PRODUCT NAME (EN) <span class="text-danger">*</span></label>
|
||||||
<input type="text" name="name" id="productName" class="form-control rounded-3" required>
|
<input type="text" name="name" id="productName" class="form-control rounded-3" required>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label small fw-bold text-muted d-flex justify-content-between">
|
||||||
|
<span>PRODUCT NAME (ARABIC)</span>
|
||||||
|
<a href="javascript:void(0)" onclick="translateName()" class="text-decoration-none small text-primary fw-bold" id="translateBtn">
|
||||||
|
<i class="bi bi-translate me-1"></i> Auto-translate
|
||||||
|
</a>
|
||||||
|
</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" name="name_ar" id="productNameAr" class="form-control rounded-3 text-end" dir="rtl" placeholder="الاسم بالعربية">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<label class="form-label small fw-bold text-muted">CATEGORY <span class="text-danger">*</span></label>
|
<label class="form-label small fw-bold text-muted">CATEGORY <span class="text-danger">*</span></label>
|
||||||
@ -341,6 +385,7 @@ function prepareEditForm(prod) {
|
|||||||
document.getElementById('productAction').value = 'edit_product';
|
document.getElementById('productAction').value = 'edit_product';
|
||||||
document.getElementById('productId').value = prod.id;
|
document.getElementById('productId').value = prod.id;
|
||||||
document.getElementById('productName').value = prod.name;
|
document.getElementById('productName').value = prod.name;
|
||||||
|
document.getElementById('productNameAr').value = prod.name_ar || '';
|
||||||
document.getElementById('productCategoryId').value = prod.category_id;
|
document.getElementById('productCategoryId').value = prod.category_id;
|
||||||
document.getElementById('productPrice').value = prod.price;
|
document.getElementById('productPrice').value = prod.price;
|
||||||
document.getElementById('productCostPrice').value = prod.cost_price || '';
|
document.getElementById('productCostPrice').value = prod.cost_price || '';
|
||||||
@ -358,6 +403,39 @@ function prepareEditForm(prod) {
|
|||||||
document.getElementById('productImagePreview').style.display = 'none';
|
document.getElementById('productImagePreview').style.display = 'none';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function translateName() {
|
||||||
|
const enName = document.getElementById('productName').value;
|
||||||
|
if (!enName) {
|
||||||
|
alert('Please enter an English name first.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const btn = document.getElementById('translateBtn');
|
||||||
|
const originalContent = btn.innerHTML;
|
||||||
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Translating...';
|
||||||
|
btn.classList.add('disabled');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('../api/translate.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ text: enName, target_lang: 'Arabic' })
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success) {
|
||||||
|
document.getElementById('productNameAr').value = data.translated_text;
|
||||||
|
} else {
|
||||||
|
alert('Translation error: ' + data.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Translation error:', error);
|
||||||
|
alert('An error occurred during translation.');
|
||||||
|
} finally {
|
||||||
|
btn.innerHTML = originalContent;
|
||||||
|
btn.classList.remove('disabled');
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
|||||||
@ -18,8 +18,9 @@ if (empty($name)) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!preg_match('/^\d{8}$/', $phone)) {
|
// Relaxed phone validation: 8 to 15 digits
|
||||||
echo json_encode(['error' => 'Phone number must be exactly 8 digits']);
|
if (!preg_match('/^\d{8,15}$/', $phone)) {
|
||||||
|
echo json_encode(['error' => 'Phone number must be between 8 and 15 digits']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
26
api/customer_loyalty_history.php
Normal file
26
api/customer_loyalty_history.php
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
require_once __DIR__ . '/../includes/functions.php';
|
||||||
|
|
||||||
|
$customer_id = isset($_GET['customer_id']) ? intval($_GET['customer_id']) : null;
|
||||||
|
|
||||||
|
if (!$customer_id) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Customer ID required']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
$stmt = $pdo->prepare("SELECT id, points_change, reason, order_id, created_at
|
||||||
|
FROM loyalty_points_history
|
||||||
|
WHERE customer_id = ?
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 50");
|
||||||
|
$stmt->execute([$customer_id]);
|
||||||
|
$history = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'history' => $history]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
@ -80,6 +80,8 @@ try {
|
|||||||
$current_points = 0;
|
$current_points = 0;
|
||||||
$points_deducted = 0;
|
$points_deducted = 0;
|
||||||
$points_awarded = 0;
|
$points_awarded = 0;
|
||||||
|
$award_history_id = null;
|
||||||
|
$redeem_history_id = null;
|
||||||
|
|
||||||
if ($customer_id) {
|
if ($customer_id) {
|
||||||
$stmt = $pdo->prepare("SELECT name, phone, points FROM customers WHERE id = ?");
|
$stmt = $pdo->prepare("SELECT name, phone, points FROM customers WHERE id = ?");
|
||||||
@ -105,6 +107,11 @@ try {
|
|||||||
$pdo->prepare("UPDATE customers SET loyalty_redemptions_count = loyalty_redemptions_count + 1 WHERE id = ?")->execute([$customer_id]);
|
$pdo->prepare("UPDATE customers SET loyalty_redemptions_count = loyalty_redemptions_count + 1 WHERE id = ?")->execute([$customer_id]);
|
||||||
$deductStmt->execute([$points_threshold, $customer_id]);
|
$deductStmt->execute([$points_threshold, $customer_id]);
|
||||||
$points_deducted = $points_threshold;
|
$points_deducted = $points_threshold;
|
||||||
|
|
||||||
|
// Record Loyalty History (Deduction)
|
||||||
|
$historyStmt = $pdo->prepare("INSERT INTO loyalty_points_history (customer_id, points_change, reason) VALUES (?, ?, 'Redeemed Free Meal')");
|
||||||
|
$historyStmt->execute([$customer_id, -$points_threshold]);
|
||||||
|
$redeem_history_id = $pdo->lastInsertId();
|
||||||
|
|
||||||
// --- OVERRIDE PAYMENT TYPE ---
|
// --- OVERRIDE PAYMENT TYPE ---
|
||||||
$ptStmt = $pdo->prepare("SELECT id FROM payment_types WHERE name = 'Loyalty Redeem' LIMIT 1");
|
$ptStmt = $pdo->prepare("SELECT id FROM payment_types WHERE name = 'Loyalty Redeem' LIMIT 1");
|
||||||
@ -115,11 +122,16 @@ try {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Award Points
|
// Award Points (ONLY IF NOT REDEEMING)
|
||||||
if ($customer_id && $loyalty_enabled) {
|
if ($customer_id && $loyalty_enabled && !$redeem_loyalty) {
|
||||||
$awardStmt = $pdo->prepare("UPDATE customers SET points = points + ? WHERE id = ?");
|
$awardStmt = $pdo->prepare("UPDATE customers SET points = points + ? WHERE id = ?");
|
||||||
$awardStmt->execute([$points_per_order, $customer_id]);
|
$awardStmt->execute([$points_per_order, $customer_id]);
|
||||||
$points_awarded = $points_per_order;
|
$points_awarded = $points_per_order;
|
||||||
|
|
||||||
|
// Record Loyalty History (Award)
|
||||||
|
$historyStmt = $pdo->prepare("INSERT INTO loyalty_points_history (customer_id, points_change, reason) VALUES (?, ?, 'Earned from Order')");
|
||||||
|
$historyStmt->execute([$customer_id, $points_per_order]);
|
||||||
|
$award_history_id = $pdo->lastInsertId();
|
||||||
}
|
}
|
||||||
|
|
||||||
// User/Payment info
|
// User/Payment info
|
||||||
@ -214,6 +226,16 @@ try {
|
|||||||
$order_id = $pdo->lastInsertId();
|
$order_id = $pdo->lastInsertId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update loyalty history with order_id
|
||||||
|
if ($order_id) {
|
||||||
|
if ($award_history_id) {
|
||||||
|
$pdo->prepare("UPDATE loyalty_points_history SET order_id = ? WHERE id = ?")->execute([$order_id, $award_history_id]);
|
||||||
|
}
|
||||||
|
if ($redeem_history_id) {
|
||||||
|
$pdo->prepare("UPDATE loyalty_points_history SET order_id = ? WHERE id = ?")->execute([$order_id, $redeem_history_id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Insert Items and Update Stock
|
// Insert Items and Update Stock
|
||||||
$item_stmt = $pdo->prepare("INSERT INTO order_items (order_id, product_id, variant_id, quantity, unit_price) VALUES (?, ?, ?, ?, ?)");
|
$item_stmt = $pdo->prepare("INSERT INTO order_items (order_id, product_id, variant_id, quantity, unit_price) VALUES (?, ?, ?, ?, ?)");
|
||||||
$stock_stmt = $pdo->prepare("UPDATE products SET stock_quantity = stock_quantity - ? WHERE id = ?");
|
$stock_stmt = $pdo->prepare("UPDATE products SET stock_quantity = stock_quantity - ? WHERE id = ?");
|
||||||
|
|||||||
29
api/translate.php
Normal file
29
api/translate.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once __DIR__ . '/../ai/LocalAIApi.php';
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$text = $data['text'] ?? '';
|
||||||
|
$target_lang = $data['target_lang'] ?? 'Arabic';
|
||||||
|
|
||||||
|
if (empty($text)) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'No text provided']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$prompt = "Translate the following product name or description to $target_lang. Return ONLY the translated text, nothing else.\n\nText: $text";
|
||||||
|
|
||||||
|
$resp = LocalAIApi::createResponse([
|
||||||
|
'input' => [
|
||||||
|
['role' => 'system', 'content' => 'You are a helpful translation assistant.'],
|
||||||
|
['role' => 'user', 'content' => $prompt],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!empty($resp['success'])) {
|
||||||
|
$translatedText = LocalAIApi::extractText($resp);
|
||||||
|
echo json_encode(['success' => true, 'translated_text' => trim($translatedText)]);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'error' => $resp['error'] ?? 'AI error']);
|
||||||
|
}
|
||||||
|
|
||||||
@ -125,6 +125,20 @@ body {
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Force group elements to always be black and ignore theme active/hover colors */
|
||||||
|
.sidebar .collapse .nav-link,
|
||||||
|
.sidebar .collapse .nav-link.active,
|
||||||
|
.sidebar .collapse .nav-link:hover,
|
||||||
|
.sidebar .collapse .nav-link:focus {
|
||||||
|
color: #000000 !important;
|
||||||
|
}
|
||||||
|
.sidebar .collapse .nav-link i,
|
||||||
|
.sidebar .collapse .nav-link.active i,
|
||||||
|
.sidebar .collapse .nav-link:hover i,
|
||||||
|
.sidebar .collapse .nav-link:focus i {
|
||||||
|
color: #000000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-heading i {
|
.sidebar-heading i {
|
||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
@ -415,4 +429,4 @@ body {
|
|||||||
/* Ensure Dropdowns are always on top */
|
/* Ensure Dropdowns are always on top */
|
||||||
.dropdown-menu {
|
.dropdown-menu {
|
||||||
z-index: 1050 !important;
|
z-index: 1050 !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,6 +36,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const loyaltyPointsDisplay = document.getElementById('loyalty-points-display');
|
const loyaltyPointsDisplay = document.getElementById('loyalty-points-display');
|
||||||
const loyaltyMessage = document.getElementById('loyalty-message');
|
const loyaltyMessage = document.getElementById('loyalty-message');
|
||||||
const redeemLoyaltyBtn = document.getElementById('redeem-loyalty-btn');
|
const redeemLoyaltyBtn = document.getElementById('redeem-loyalty-btn');
|
||||||
|
const viewPointsHistoryBtn = document.getElementById('view-points-history-btn');
|
||||||
|
|
||||||
|
// Points History Modal
|
||||||
|
const pointsHistoryModalEl = document.getElementById('pointsHistoryModal');
|
||||||
|
const pointsHistoryModal = pointsHistoryModalEl ? new bootstrap.Modal(pointsHistoryModalEl) : null;
|
||||||
|
const pointsHistoryBody = document.getElementById('points-history-body');
|
||||||
|
const pointsHistoryEmpty = document.getElementById('points-history-empty');
|
||||||
|
|
||||||
// Table Management
|
// Table Management
|
||||||
let currentTableId = null;
|
let currentTableId = null;
|
||||||
@ -188,7 +195,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
// Populate Cart
|
// Populate Cart
|
||||||
cart = data.items; // Assuming format matches
|
cart = data.items; // Assuming format matches
|
||||||
cartVatInput.value = data.order.discount || 0; // We still use the discount field for VAT value
|
|
||||||
|
// Note: In auto-VAT mode, we don't load data.order.discount into cartVatInput
|
||||||
|
// as it will be re-calculated based on subtotal.
|
||||||
|
|
||||||
updateCart();
|
updateCart();
|
||||||
if (recallModal) recallModal.hide();
|
if (recallModal) recallModal.hide();
|
||||||
@ -287,7 +296,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
// Hide Loyalty
|
// Hide Loyalty
|
||||||
if (loyaltySection) loyaltySection.classList.add('d-none');
|
if (loyaltySection) loyaltySection.classList.add('d-none');
|
||||||
isLoyaltyRedemption = false;
|
isLoyaltyRedemption = false;
|
||||||
cartVatInput.value = 0;
|
|
||||||
updateCart();
|
updateCart();
|
||||||
|
|
||||||
customerSearchInput.focus();
|
customerSearchInput.focus();
|
||||||
@ -304,9 +312,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- NEW RESTRICTION ---
|
// --- STRICT ONE ITEM RESTRICTION ---
|
||||||
if (cart.length > 1) {
|
if (cart.length > 1) {
|
||||||
showToast("Can only redeem a free meal with a single item in cart!", "warning");
|
showToast("Can only redeem a free meal with exactly one item in cart!", "warning");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -314,29 +322,71 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
title: 'Redeem Loyalty?',
|
title: 'Redeem Loyalty?',
|
||||||
text: "Redeem 70 points for a free meal? This will apply a full discount to the current order.",
|
text: "Redeem points for a free meal? This will finalize the order immediately.",
|
||||||
icon: 'question',
|
icon: 'question',
|
||||||
showCancelButton: true,
|
showCancelButton: true,
|
||||||
confirmButtonColor: '#198754',
|
confirmButtonColor: '#198754',
|
||||||
cancelButtonColor: '#6c757d',
|
cancelButtonColor: '#6c757d',
|
||||||
confirmButtonText: 'Yes, redeem it!'
|
confirmButtonText: 'Yes, redeem and finish!'
|
||||||
}).then((result) => {
|
}).then((result) => {
|
||||||
if (result.isConfirmed) {
|
if (result.isConfirmed) {
|
||||||
isLoyaltyRedemption = true;
|
isLoyaltyRedemption = true;
|
||||||
|
|
||||||
// Calculate total and apply as discount (which is now negative VAT internally)
|
|
||||||
const subtotal = cart.reduce((acc, item) => acc + (item.price * item.quantity), 0);
|
|
||||||
cartVatInput.value = -subtotal.toFixed(2);
|
|
||||||
updateCart();
|
updateCart();
|
||||||
|
|
||||||
showToast("Loyalty Redemption Applied!", "success");
|
// Directly process order with Loyalty payment type
|
||||||
redeemLoyaltyBtn.disabled = true; // Prevent double click
|
processOrder(null, 'Loyalty Redeem');
|
||||||
loyaltyMessage.innerHTML = '<span class="text-success fw-bold"><i class="bi bi-check-circle"></i> Redeemed! Place order to finalize.</span>';
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Points History View
|
||||||
|
if (viewPointsHistoryBtn) {
|
||||||
|
viewPointsHistoryBtn.addEventListener('click', () => {
|
||||||
|
if (!currentCustomer) return;
|
||||||
|
fetchPointsHistory(currentCustomer.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchPointsHistory(customerId) {
|
||||||
|
if (!pointsHistoryBody || !pointsHistoryModal) return;
|
||||||
|
pointsHistoryBody.innerHTML = '<tr><td colspan="3" class="text-center py-4"><div class="spinner-border spinner-border-sm text-primary"></div> Loading...</td></tr>';
|
||||||
|
pointsHistoryEmpty.classList.add('d-none');
|
||||||
|
pointsHistoryModal.show();
|
||||||
|
|
||||||
|
fetch(`api/customer_loyalty_history.php?customer_id=${customerId}`)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
pointsHistoryBody.innerHTML = '';
|
||||||
|
if (data.success && data.history.length > 0) {
|
||||||
|
data.history.forEach(item => {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
const dateObj = new Date(item.created_at);
|
||||||
|
const date = dateObj.toLocaleDateString('en-GB', { day: '2-digit', month: 'short' }) + ' ' +
|
||||||
|
dateObj.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
|
||||||
|
|
||||||
|
const pointsClass = item.points_change > 0 ? 'text-success' : 'text-danger';
|
||||||
|
const pointsPrefix = item.points_change > 0 ? '+' : '';
|
||||||
|
|
||||||
|
tr.innerHTML = `
|
||||||
|
<td class="ps-3 small align-middle" style="font-size: 0.75rem; white-space: nowrap;">${date}</td>
|
||||||
|
<td class="align-middle">
|
||||||
|
<div class="small fw-bold">${item.reason}</div>
|
||||||
|
${item.order_id ? `<div class="x-small text-muted" style="font-size: 0.65rem;">Order #${item.order_id}</div>` : ''}
|
||||||
|
</td>
|
||||||
|
<td class="text-end pe-3 fw-bold ${pointsClass} align-middle">${pointsPrefix}${item.points_change}</td>
|
||||||
|
`;
|
||||||
|
pointsHistoryBody.appendChild(tr);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
pointsHistoryEmpty.classList.remove('d-none');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
pointsHistoryBody.innerHTML = '<tr><td colspan="3" class="text-center text-danger">Error loading history</td></tr>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// --- Table & Order Type Logic ---
|
// --- Table & Order Type Logic ---
|
||||||
const orderTypeInputs = document.querySelectorAll('input[name="order_type"]');
|
const orderTypeInputs = document.querySelectorAll('input[name="order_type"]');
|
||||||
|
|
||||||
@ -434,6 +484,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const product = {
|
const product = {
|
||||||
id: target.dataset.id,
|
id: target.dataset.id,
|
||||||
name: target.dataset.name,
|
name: target.dataset.name,
|
||||||
|
name_ar: target.dataset.nameAr || '',
|
||||||
price: parseFloat(target.dataset.price),
|
price: parseFloat(target.dataset.price),
|
||||||
base_price: parseFloat(target.dataset.price),
|
base_price: parseFloat(target.dataset.price),
|
||||||
hasVariants: target.dataset.hasVariants === 'true',
|
hasVariants: target.dataset.hasVariants === 'true',
|
||||||
@ -517,6 +568,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
<p class="mt-2">Cart is empty</p>
|
<p class="mt-2">Cart is empty</p>
|
||||||
</div>`;
|
</div>`;
|
||||||
cartSubtotal.innerText = formatCurrency(0);
|
cartSubtotal.innerText = formatCurrency(0);
|
||||||
|
cartVatInput.value = 0;
|
||||||
cartTotalPrice.innerText = formatCurrency(0);
|
cartTotalPrice.innerText = formatCurrency(0);
|
||||||
if (quickOrderBtn) quickOrderBtn.disabled = true;
|
if (quickOrderBtn) quickOrderBtn.disabled = true;
|
||||||
if (placeOrderBtn) placeOrderBtn.disabled = true;
|
if (placeOrderBtn) placeOrderBtn.disabled = true;
|
||||||
@ -534,10 +586,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
row.className = 'd-flex justify-content-between align-items-center mb-3 border-bottom pb-2';
|
row.className = 'd-flex justify-content-between align-items-center mb-3 border-bottom pb-2';
|
||||||
|
|
||||||
const variantLabel = item.variant_name ? `<span class="badge bg-light text-dark border ms-1">${item.variant_name}</span>` : '';
|
const variantLabel = item.variant_name ? `<span class="badge bg-light text-dark border ms-1">${item.variant_name}</span>` : '';
|
||||||
|
const arabicNameDisplay = item.name_ar ? `<div class="text-primary small fw-semibold" dir="rtl" style="font-size: 0.75rem;">${item.name_ar}</div>` : '';
|
||||||
|
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<div class="flex-grow-1 me-2">
|
<div class="flex-grow-1 me-2">
|
||||||
<div class="fw-bold text-truncate" style="max-width: 140px;">${item.name}</div>
|
<div class="fw-bold text-truncate" style="max-width: 140px;">${item.name}</div>
|
||||||
|
${arabicNameDisplay}
|
||||||
<div class="small text-muted">${formatCurrency(item.price)} ${variantLabel}</div>
|
<div class="small text-muted">${formatCurrency(item.price)} ${variantLabel}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex align-items-center bg-light rounded px-1">
|
<div class="d-flex align-items-center bg-light rounded px-1">
|
||||||
@ -555,13 +609,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
cartSubtotal.innerText = formatCurrency(subtotal);
|
cartSubtotal.innerText = formatCurrency(subtotal);
|
||||||
|
|
||||||
let vat = parseFloat(cartVatInput.value) || 0;
|
let vat = 0;
|
||||||
if (isLoyaltyRedemption) {
|
if (isLoyaltyRedemption) {
|
||||||
// Internal trick: send negative VAT to represent discount for loyalty
|
// Internal trick: send negative VAT to represent discount for loyalty
|
||||||
vat = -subtotal;
|
vat = -subtotal;
|
||||||
cartVatInput.value = (-subtotal).toFixed(2);
|
} else {
|
||||||
|
// Automatic VAT calculation from system settings
|
||||||
|
const vatRate = parseFloat(COMPANY_SETTINGS.vat_rate) || 0;
|
||||||
|
vat = subtotal * (vatRate / 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cartVatInput.value = vat.toFixed(2);
|
||||||
|
|
||||||
let total = subtotal + vat;
|
let total = subtotal + vat;
|
||||||
if (total < 0) total = 0;
|
if (total < 0) total = 0;
|
||||||
|
|
||||||
@ -570,12 +629,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (placeOrderBtn) placeOrderBtn.disabled = false;
|
if (placeOrderBtn) placeOrderBtn.disabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cartVatInput) {
|
|
||||||
cartVatInput.addEventListener('input', () => {
|
|
||||||
updateCart();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
window.removeFromCart = function(index) {
|
window.removeFromCart = function(index) {
|
||||||
cart.splice(index, 1);
|
cart.splice(index, 1);
|
||||||
updateCart();
|
updateCart();
|
||||||
@ -631,6 +684,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (n.includes('cash')) return 'bi-cash-coin';
|
if (n.includes('cash')) return 'bi-cash-coin';
|
||||||
if (n.includes('card') || n.includes('visa') || n.includes('master')) return 'bi-credit-card';
|
if (n.includes('card') || n.includes('visa') || n.includes('master')) return 'bi-credit-card';
|
||||||
if (n.includes('qr') || n.includes('scan')) return 'bi-qr-code';
|
if (n.includes('qr') || n.includes('scan')) return 'bi-qr-code';
|
||||||
|
if (n.includes('bank') || n.includes('transfer')) return 'bi-building-columns';
|
||||||
return 'bi-wallet2';
|
return 'bi-wallet2';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -812,6 +866,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
const win = window.open('', 'Receipt', `width=${width},height=${height},top=${top},left=${left}`);
|
const win = window.open('', 'Receipt', `width=${width},height=${height},top=${top},left=${left}`);
|
||||||
|
|
||||||
|
if (!win) {
|
||||||
|
alert('Please allow popups for this website to print receipts.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const tr = {
|
const tr = {
|
||||||
'Order': 'الطلب',
|
'Order': 'الطلب',
|
||||||
'Type': 'النوع',
|
'Type': 'النوع',
|
||||||
@ -839,6 +898,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
<tr>
|
<tr>
|
||||||
<td style="padding: 5px 0; border-bottom: 1px solid #eee;">
|
<td style="padding: 5px 0; border-bottom: 1px solid #eee;">
|
||||||
<div style="font-weight: bold;">${item.name}</div>
|
<div style="font-weight: bold;">${item.name}</div>
|
||||||
|
${item.name_ar ? `<div style="font-size: 11px; color: #0d6efd; font-weight: 600;" dir="rtl">${item.name_ar}</div>` : ''}
|
||||||
${item.variant_name ? `<div style="font-size: 10px; color: #555;">(${item.variant_name})</div>` : ''}
|
${item.variant_name ? `<div style="font-size: 10px; color: #555;">(${item.variant_name})</div>` : ''}
|
||||||
<div style="font-size: 11px;">${item.quantity} x ${formatCurrency(item.price)}</div>
|
<div style="font-size: 11px;">${item.quantity} x ${formatCurrency(item.price)}</div>
|
||||||
</td>
|
</td>
|
||||||
@ -874,18 +934,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const loyaltyHtml = data.loyaltyRedeemed ? `<div style="color: #d63384; font-weight: bold; margin: 5px 0; text-align: center;">* Loyalty Reward Applied *</div>` : '';
|
const loyaltyHtml = data.loyaltyRedeemed ? `<div style="color: #d63384; font-weight: bold; margin: 5px 0; text-align: center;">* Loyalty Reward Applied *</div>` : '';
|
||||||
|
|
||||||
const subtotal = data.total - data.vat;
|
const subtotal = data.total - data.vat;
|
||||||
const vatRateSettings = parseFloat(settings.vat_rate) || 0;
|
|
||||||
const vatAmount = data.vat; // User manually entered VAT
|
|
||||||
|
|
||||||
const logoHtml = settings.logo_url ? `<img src="${BASE_URL}${settings.logo_url}" style="max-height: 80px; max-width: 150px; margin-bottom: 10px;">` : '';
|
const logoHtml = settings.logo_url ? `<img src="${BASE_URL}${settings.logo_url}" style="max-height: 80px; max-width: 150px; margin-bottom: 10px;">` : '';
|
||||||
|
|
||||||
const html = `
|
const html = `
|
||||||
<html dir="ltr">
|
<html dir="ltr">
|
||||||
<head>
|
<head>
|
||||||
<title>Receipt #${data.orderId}</title>
|
<title>Receipt #${data.orderId}</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+Arabic:wght@400;600;700&display=swap" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: 'Courier New', Courier, monospace;
|
font-family: 'Courier New', 'Noto Sans Arabic', Courier, monospace;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
@ -1027,12 +1085,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const saveCustomerBtn = document.getElementById('save-new-customer');
|
const saveCustomerBtn = document.getElementById('save-new-customer');
|
||||||
const newCustomerName = document.getElementById('new-customer-name');
|
const newCustomerName = document.getElementById('new-customer-name');
|
||||||
const newCustomerPhone = document.getElementById('new-customer-phone');
|
const newCustomerPhone = document.getElementById('new-customer-phone');
|
||||||
const phoneError = document.getElementById('phone-error');
|
|
||||||
|
|
||||||
addCustomerBtn.addEventListener('click', () => {
|
addCustomerBtn.addEventListener('click', () => {
|
||||||
newCustomerName.value = '';
|
newCustomerName.value = '';
|
||||||
newCustomerPhone.value = '';
|
newCustomerPhone.value = '';
|
||||||
phoneError.classList.add('d-none');
|
|
||||||
addCustomerModal.show();
|
addCustomerModal.show();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1045,14 +1101,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8 digits validation
|
|
||||||
if (!/^\d{8}$/.test(phone)) {
|
|
||||||
phoneError.classList.remove('d-none');
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
phoneError.classList.add('d-none');
|
|
||||||
}
|
|
||||||
|
|
||||||
saveCustomerBtn.disabled = true;
|
saveCustomerBtn.disabled = true;
|
||||||
saveCustomerBtn.textContent = 'Saving...';
|
saveCustomerBtn.textContent = 'Saving...';
|
||||||
|
|
||||||
@ -1081,4 +1129,4 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
11
db/migrations/027_loyalty_history.sql
Normal file
11
db/migrations/027_loyalty_history.sql
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
-- Create loyalty_points_history table
|
||||||
|
CREATE TABLE IF NOT EXISTS loyalty_points_history (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
customer_id INT NOT NULL,
|
||||||
|
order_id INT DEFAULT NULL,
|
||||||
|
points_change INT NOT NULL,
|
||||||
|
reason VARCHAR(255) DEFAULT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
2
db/migrations/028_add_name_ar_to_products.sql
Normal file
2
db/migrations/028_add_name_ar_to_products.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
-- Add Arabic name to products table
|
||||||
|
ALTER TABLE products ADD COLUMN name_ar VARCHAR(255) AFTER name;
|
||||||
62
pos.php
62
pos.php
@ -81,11 +81,11 @@ if (!$loyalty_settings) {
|
|||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css" rel="stylesheet">
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Noto+Sans+Arabic:wght@400;600;700&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?= time() ?>">
|
<link rel="stylesheet" href="assets/css/custom.css?v=<?= time() ?>">
|
||||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
<style>
|
<style>
|
||||||
body { height: 100vh; overflow: hidden; font-family: 'Inter', sans-serif; } /* Fix body for scrolling areas */
|
body { height: 100vh; overflow: hidden; font-family: 'Inter', 'Noto Sans Arabic', sans-serif; } /* Fix body for scrolling areas */
|
||||||
.scrollable-y { overflow-y: auto; height: 100%; scrollbar-width: thin; }
|
.scrollable-y { overflow-y: auto; height: 100%; scrollbar-width: thin; }
|
||||||
.category-sidebar { height: calc(100vh - 60px); background: #f8f9fa; }
|
.category-sidebar { height: calc(100vh - 60px); background: #f8f9fa; }
|
||||||
.product-area { height: calc(100vh - 60px); background: #fff; }
|
.product-area { height: calc(100vh - 60px); background: #fff; }
|
||||||
@ -212,6 +212,7 @@ if (!$loyalty_settings) {
|
|||||||
<div class="card h-100 border-0 shadow-sm product-card add-to-cart"
|
<div class="card h-100 border-0 shadow-sm product-card add-to-cart"
|
||||||
data-id="<?= $product['id'] ?>"
|
data-id="<?= $product['id'] ?>"
|
||||||
data-name="<?= htmlspecialchars($product['name']) ?>"
|
data-name="<?= htmlspecialchars($product['name']) ?>"
|
||||||
|
data-name-ar="<?= htmlspecialchars($product['name_ar'] ?? '') ?>"
|
||||||
data-price="<?= $effective_price ?>"
|
data-price="<?= $effective_price ?>"
|
||||||
data-has-variants="<?= $has_variants ? 'true' : 'false' ?>">
|
data-has-variants="<?= $has_variants ? 'true' : 'false' ?>">
|
||||||
<div class="position-relative">
|
<div class="position-relative">
|
||||||
@ -239,6 +240,9 @@ if (!$loyalty_settings) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body p-2">
|
<div class="card-body p-2">
|
||||||
<h6 class="card-title fw-bold small mb-1 text-truncate"><?= htmlspecialchars($product['name']) ?></h6>
|
<h6 class="card-title fw-bold small mb-1 text-truncate"><?= htmlspecialchars($product['name']) ?></h6>
|
||||||
|
<?php if (!empty($product['name_ar'])): ?>
|
||||||
|
<div class="text-primary small fw-semibold text-truncate mb-1" dir="rtl" style="font-size: 0.8rem;"><?= htmlspecialchars($product['name_ar']) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
<p class="card-text small text-muted text-truncate mb-0"><?= htmlspecialchars($product['category_name']) ?></p>
|
<p class="card-text small text-muted text-truncate mb-0"><?= htmlspecialchars($product['category_name']) ?></p>
|
||||||
<?php if ($has_variants): ?>
|
<?php if ($has_variants): ?>
|
||||||
<span class="badge bg-light text-secondary border mt-1">Options</span>
|
<span class="badge bg-light text-secondary border mt-1">Options</span>
|
||||||
@ -292,9 +296,14 @@ if (!$loyalty_settings) {
|
|||||||
<span class="d-block fw-bold small text-warning-emphasis">Loyalty Points</span>
|
<span class="d-block fw-bold small text-warning-emphasis">Loyalty Points</span>
|
||||||
<span id="loyalty-points-display" class="fw-bold fs-5">0</span>
|
<span id="loyalty-points-display" class="fw-bold fs-5">0</span>
|
||||||
</div>
|
</div>
|
||||||
<button id="redeem-loyalty-btn" class="btn btn-sm btn-success shadow-sm" disabled>
|
<div class="d-flex flex-column gap-1">
|
||||||
<i class="bi bi-gift-fill me-1"></i> Redeem Meal
|
<button id="redeem-loyalty-btn" class="btn btn-sm btn-success shadow-sm" disabled>
|
||||||
</button>
|
<i class="bi bi-gift-fill me-1"></i> Redeem
|
||||||
|
</button>
|
||||||
|
<button id="view-points-history-btn" class="btn btn-sm btn-outline-primary border-0 shadow-none p-0 text-decoration-underline" style="font-size: 0.7rem;">
|
||||||
|
<i class="bi bi-clock-history"></i> View History
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="loyalty-message" class="small text-muted mt-1 fst-italic" style="font-size: 0.75rem;"></div>
|
<div id="loyalty-message" class="small text-muted mt-1 fst-italic" style="font-size: 0.75rem;"></div>
|
||||||
</div>
|
</div>
|
||||||
@ -319,10 +328,10 @@ if (!$loyalty_settings) {
|
|||||||
|
|
||||||
<!-- VAT Field -->
|
<!-- VAT Field -->
|
||||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
<span class="text-muted">VAT</span>
|
<span class="text-muted">VAT (<?= htmlspecialchars((string)($settings['vat_rate'] ?? 0)) ?>%)</span>
|
||||||
<div class="input-group input-group-sm w-50">
|
<div class="input-group input-group-sm w-50">
|
||||||
<span class="input-group-text bg-white border-end-0 text-muted">+</span>
|
<span class="input-group-text bg-white border-end-0 text-muted">+</span>
|
||||||
<input type="number" id="cart-vat-input" class="form-control border-start-0 text-end" value="0" step="0.01">
|
<input type="number" id="cart-vat-input" class="form-control border-start-0 text-end bg-white" value="0" step="0.01" readonly>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -427,9 +436,8 @@ if (!$loyalty_settings) {
|
|||||||
<input type="text" class="form-control form-control-sm" id="new-customer-name" placeholder="Enter name">
|
<input type="text" class="form-control form-control-sm" id="new-customer-name" placeholder="Enter name">
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="new-customer-phone" class="form-label small text-muted">Phone Number (8 Digits)</label>
|
<label for="new-customer-phone" class="form-label small text-muted">Phone Number</label>
|
||||||
<input type="text" class="form-control form-control-sm" id="new-customer-phone" placeholder="12345678" maxlength="8">
|
<input type="text" class="form-control form-control-sm" id="new-customer-phone" placeholder="Enter phone">
|
||||||
<div class="form-text text-danger d-none" id="phone-error">Must be exactly 8 digits.</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="d-grid">
|
<div class="d-grid">
|
||||||
<button type="button" class="btn btn-primary btn-sm" id="save-new-customer">Save Customer</button>
|
<button type="button" class="btn btn-primary btn-sm" id="save-new-customer">Save Customer</button>
|
||||||
@ -439,6 +447,38 @@ if (!$loyalty_settings) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Points History Modal -->
|
||||||
|
<div class="modal fade" id="pointsHistoryModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title fw-bold">Points History</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body p-0">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm table-hover mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th class="ps-3">Date</th>
|
||||||
|
<th>Action</th>
|
||||||
|
<th class="text-end pe-3">Points</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="points-history-body">
|
||||||
|
<!-- History rows injected via JS -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div id="points-history-empty" class="text-center py-4 text-muted d-none">
|
||||||
|
<i class="bi bi-calendar-x fs-2"></i>
|
||||||
|
<p class="mt-2">No history found</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Payment Selection Modal -->
|
<!-- Payment Selection Modal -->
|
||||||
<div class="modal fade" id="paymentSelectionModal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static">
|
<div class="modal fade" id="paymentSelectionModal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static">
|
||||||
<div class="modal-dialog modal-dialog-centered">
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
@ -498,4 +538,4 @@ if (!$loyalty_settings) {
|
|||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<script src="assets/js/main.js?v=<?= time() ?>"></script>
|
<script src="assets/js/main.js?v=<?= time() ?>"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Loading…
x
Reference in New Issue
Block a user