diff --git a/admin/includes/header.php b/admin/includes/header.php
index 989a2d6..40d42f0 100644
--- a/admin/includes/header.php
+++ b/admin/includes/header.php
@@ -1,8 +1,4 @@
-
+
\ No newline at end of file
diff --git a/assets/js/main.js b/assets/js/main.js
index 3c548aa..e24e61f 100644
--- a/assets/js/main.js
+++ b/assets/js/main.js
@@ -99,7 +99,7 @@ document.addEventListener('DOMContentLoaded', () => {
function formatCurrency(amount) {
const symbol = settings.currency_symbol || '$';
const decimals = parseInt(settings.currency_decimals || 2);
- return symbol + parseFloat(amount).toFixed(decimals);
+ return symbol + parseFloat(Math.abs(amount)).toFixed(decimals);
}
function filterProducts() {
@@ -397,11 +397,11 @@ document.addEventListener('DOMContentLoaded', () => {
if (data.success && data.tables.length > 0) {
renderTables(data.tables);
} else {
- grid.innerHTML = `
${_t("none")}
`;
+ grid.innerHTML = `${_t("none")}
`;
}
})
.catch(() => {
- grid.innerHTML = `${_t("error")}
`;
+ grid.innerHTML = `${_t("error")}
`;
});
}
@@ -420,7 +420,7 @@ document.addEventListener('DOMContentLoaded', () => {
for (const area in areas) {
const areaHeader = document.createElement("div");
areaHeader.className = "col-12 mt-3";
- areaHeader.innerHTML = `${area}
`;
+ areaHeader.innerHTML = `${area}
`;
grid.appendChild(areaHeader);
areas[area].forEach(table => {
@@ -429,7 +429,7 @@ document.addEventListener('DOMContentLoaded', () => {
const statusClass = table.is_occupied ? "btn-outline-danger" : "btn-outline-success";
col.innerHTML = `
@@ -634,10 +634,36 @@ document.addEventListener('DOMContentLoaded', () => {
redeem_loyalty: isLoyaltyRedemption
};
+ // Prepare receipt data before clearing cart
+ const receiptData = {
+ orderId: null,
+ customer: currentCustomer ? { name: currentCustomer.name, phone: currentCustomer.phone, address: currentCustomer.address || '' } : null,
+ items: cart.map(item => ({
+ name: item.name,
+ variant_name: item.variant_name,
+ quantity: item.quantity,
+ price: item.price
+ })),
+ total: subtotal + vat,
+ vat: vat,
+ orderType: orderType,
+ tableNumber: (orderType === 'dine-in') ? currentTableName : null,
+ date: new Date().toLocaleString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }),
+ paymentMethod: paymentTypeName || 'Unpaid',
+ loyaltyRedeemed: isLoyaltyRedemption
+ };
+
fetch('api/order.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(orderData) })
.then(res => res.json())
.then(data => {
if (data.success) {
+ receiptData.orderId = data.order_id;
+
+ // Show receipt for Quick Pay (where paymentTypeId is provided)
+ if (paymentTypeId !== null) {
+ printThermalReceipt(receiptData);
+ }
+
showToast(`${_t('order_placed')} #${data.order_id}`, 'success');
clearCart();
if (paymentSelectionModal) paymentSelectionModal.hide();
@@ -646,6 +672,136 @@ document.addEventListener('DOMContentLoaded', () => {
});
};
+ window.printThermalReceipt = function(data) {
+ const width = 400;
+ const height = 800;
+ const left = (screen.width - width) / 2;
+ const top = (screen.height - height) / 2;
+
+ const win = window.open('', '_blank', `width=${width},height=${height},top=${top},left=${left}`);
+
+ if (!win) {
+ alert('Please allow popups for this website to print thermal receipts.');
+ return;
+ }
+
+ const tr = {
+ 'Order': 'الطلب', 'Type': 'النوع', 'Date': 'التاريخ', 'Staff': 'الموظف',
+ 'Table': 'طاولة', 'Payment': 'الدفع', 'ITEM': 'الصنف', 'TOTAL': 'المجموع',
+ 'Subtotal': 'المجموع الفرعي', 'VAT': 'ضريبة القيمة المضافة', 'Tax Included': 'شامل الضريبة',
+ 'THANK YOU FOR YOUR VISIT!': 'شكراً لزيارتكم!', 'Please come again.': 'يرجى زيارتنا مرة أخرى.',
+ 'Customer Details': 'تفاصيل العميل', 'Tel': 'هاتف', 'takeaway': 'سفري',
+ 'dine-in': 'محلي', 'delivery': 'توصيل', 'VAT No': 'الرقم الضريبي', 'CTR No': 'رقم السجل التجاري'
+ };
+
+ const itemsHtml = data.items.map(item => `
+
+ |
+ ${item.name}
+ ${item.variant_name ? `(${item.variant_name}) ` : ''}
+ ${item.quantity} x ${formatCurrency(item.price)}
+ |
+ ${formatCurrency(item.quantity * item.price)} |
+
+ `).join('');
+
+ const customerHtml = data.customer ? `
+
+
+
Customer Details
+
${tr['Customer Details']}
+
+
+
${data.customer.name}
+ ${data.customer.phone ? `
Tel: ${data.customer.phone}
` : ''}
+ ${data.customer.address ? `
${data.customer.address}
` : ''}
+
+ ` : '';
+
+ const tableHtml = data.tableNumber && data.orderType === 'dine-in' ? `
+
+ Table: ${data.tableNumber}
+ ${tr['Table']}: ${data.tableNumber}
+
` : '';
+
+ const paymentHtml = data.paymentMethod ? `
+
+ Payment: ${data.paymentMethod}
+ ${tr['Payment']}: ${data.paymentMethod}
+
` : '';
+
+ const loyaltyHtml = data.loyaltyRedeemed ? `* Loyalty Reward Applied *
` : '';
+
+ const subtotal = data.total - data.vat;
+ const logoHtml = settings.logo_url ? `
` : '';
+
+ const html = `
+
+
+ Receipt #${data.orderId}
+
+
+
+
+
+
+
Order: #${data.orderId}${tr['Order']}: #${data.orderId}
+
Type: ${data.orderType.toUpperCase()}${tr['Type']}: ${tr[data.orderType] || data.orderType}
+
Date: ${data.date}${tr['Date']}: ${data.date}
+
Staff: ${CURRENT_USER.name}${tr['Staff']}: ${CURRENT_USER.name}
+
+ ${tableHtml}${paymentHtml}${loyaltyHtml}
+
+ ${customerHtml}
+
+ | ITEM / الصنف | TOTAL / المجموع |
+ ${itemsHtml}
+
+
+
+
+ | Subtotal / ${tr['Subtotal']} | ${formatCurrency(subtotal)} |
+ ${Math.abs(data.vat) > 0 ? `| ${data.vat < 0 ? 'Discount' : 'VAT'} / ${tr['VAT']} | ${data.vat < 0 ? '-' : '+'}${formatCurrency(Math.abs(data.vat))} |
` : ''}
+ | TOTAL / ${tr['TOTAL']} | ${formatCurrency(data.total)} |
+
+
+
+
+
+
+ `;
+ win.document.write(html);
+ win.document.close();
+ };
+
window.openRatingQRModal = function() {
const qrContainer = document.getElementById('rating-qr-container');
const ratingUrl = BASE_URL + '/rate.php';
@@ -656,4 +812,4 @@ document.addEventListener('DOMContentLoaded', () => {
const modal = new bootstrap.Modal(document.getElementById('qrRatingModal'));
modal.show();
};
-});
+});
\ No newline at end of file
diff --git a/includes/functions.php b/includes/functions.php
index 074f1f8..cfbdfb2 100644
--- a/includes/functions.php
+++ b/includes/functions.php
@@ -228,8 +228,31 @@ function render_pagination_controls($pagination, $extra_params = []) {
function init_session() {
if (session_status() === PHP_SESSION_NONE) {
+ // Set session lifetime to 1 week (604800 seconds)
+ $lifetime = 604800;
+
+ // Ensure gc_maxlifetime is at least as long as cookie lifetime
+ ini_set('session.gc_maxlifetime', (string)$lifetime);
+
+ // Set cookie parameters before session_start
+ $isSecure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || $_SERVER['SERVER_PORT'] == 443;
+
+ session_set_cookie_params([
+ 'lifetime' => $lifetime,
+ 'path' => '/',
+ 'domain' => '',
+ 'secure' => $isSecure,
+ 'httponly' => true,
+ 'samesite' => 'Lax'
+ ]);
+
session_start();
}
+
+ // Refresh session expiration on each load if user is logged in
+ if (isset($_SESSION['user'])) {
+ // Optional: you could implement a last_activity check here
+ }
}
function login_user($username, $password) {
diff --git a/pos.php b/pos.php
index c77acdd..96fefa8 100644
--- a/pos.php
+++ b/pos.php
@@ -4,9 +4,7 @@ require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/functions.php';
require_once __DIR__ . '/includes/lang.php';
-if (session_status() === PHP_SESSION_NONE) {
- session_start();
-}
+init_session();
// User requested no translations in all app except for QR order and rating.
// Force English for POS.
@@ -102,7 +100,7 @@ if (!$loyalty_settings) {
.pos-products { background: #f8fafc; height: 100%; display: flex; flex-direction: column; }
.pos-cart { background: #fff; height: 100%; border-left: 1px solid #e0e0e0; display: flex; flex-direction: column; }
- .product-card { transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); cursor: pointer; border: 1px solid transparent !important; background: #fff; }
+ .product-card { transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); cursor: pointer; border: 1px solid transparent !important; background: #fff; aspect-ratio: 1/1; display: flex; flex-direction: column; }
.product-card:active { transform: scale(0.95); }
.product-card:hover { border-color: #0d6efd !important; box-shadow: 0 4px 12px rgba(0,0,0,0.08) !important; }
@@ -112,19 +110,13 @@ if (!$loyalty_settings) {
.search-dropdown { position: absolute; width: 100%; z-index: 1000; max-height: 200px; overflow-y: auto; display: none; }
/* Compact Card adjustments */
- .card-img-container { height: 75px; position: relative; background: #f1f5f9; }
+ .card-img-container { flex: 1; position: relative; background: #f1f5f9; overflow: hidden; }
.card-img-container img { height: 100%; width: 100%; object-fit: cover; transition: transform 0.3s; }
- .product-title { font-size: 0.75rem; line-height: 1.2; height: 1.8rem; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; font-weight: 700; color: #1e293b; }
- .product-price-tag { font-size: 0.85rem; color: #0d6efd; font-weight: 700; }
+ .product-title { font-size: 0.75rem; line-height: 1.1; height: 2.2rem; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; font-weight: 700; color: #1e293b; }
+ .product-price-tag { font-size: 0.8rem; color: #0d6efd; font-weight: 700; }
.product-cat-name { font-size: 0.65rem; color: #94a3b8; }
- /* Custom Grid for 7 columns */
- @media (min-width: 1200px) {
- .row-cols-xl-7 > *, .row-cols-xxl-7 > * { flex: 0 0 auto; width: 14.285714%; }
- }
-
@media (max-width: 576px) {
- .card-img-container { height: 65px; }
.pos-categories { width: 65px !important; flex: 0 0 65px !important; }
.category-btn span { display: none; }
.category-btn i { font-size: 1.4rem !important; margin-bottom: 0 !important; }
@@ -169,7 +161,6 @@ if (!$loyalty_settings) {
= htmlspecialchars($settings['company_name']) ?>
- = htmlspecialchars($current_outlet_name) ?>
@@ -200,7 +191,7 @@ if (!$loyalty_settings) {