updating session and pos

This commit is contained in:
Flatlogic Bot 2026-02-25 02:42:12 +00:00
parent ad06129a7a
commit 9999efc72b
4 changed files with 204 additions and 31 deletions

View File

@ -1,8 +1,4 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// Ensure functions are available
if (file_exists(__DIR__ . '/../../db/config.php')) {
require_once __DIR__ . '/../../db/config.php';
@ -10,6 +6,13 @@ if (file_exists(__DIR__ . '/../../db/config.php')) {
if (file_exists(__DIR__ . '/../../includes/functions.php')) {
require_once __DIR__ . '/../../includes/functions.php';
}
if (function_exists('init_session')) {
init_session();
} elseif (session_status() === PHP_SESSION_NONE) {
session_start();
}
if (file_exists(__DIR__ . '/../../includes/lang.php')) {
require_once __DIR__ . '/../../includes/lang.php';
}

View File

@ -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 = `<div class=\"p-4 text-center text-muted w-100">${_t("none")}</div>`;
grid.innerHTML = `<div class=\"p-4 text-center text-muted w-100\">${_t("none")}</div>`;
}
})
.catch(() => {
grid.innerHTML = `<div class=\"alert alert-danger w-100">${_t("error")}</div>`;
grid.innerHTML = `<div class=\"alert alert-danger w-100\">${_t("error")}</div>`;
});
}
@ -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 = `<h6 class=\"fw-bold text-muted text-uppercase small border-bottom pb-2">${area}</h6>`;
areaHeader.innerHTML = `<h6 class=\"fw-bold text-muted text-uppercase small border-bottom pb-2\">${area}</h6>`;
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 = `
<button class=\"btn ${statusClass} w-100 py-3 rounded-3 position-relative\" onclick=\"selectTable(${table.id}, '${table.name}')\">
<div class=\"fw-bold">${table.name}</div>
<div class=\"fw-bold\">${table.name}</div>
<div class=\"small\" style=\"font-size: 0.7rem;\">Cap: ${table.capacity}</div>
${table.is_occupied ? '<span class="position-absolute top-0 start-100 translate-middle p-1 bg-danger border border-light rounded-circle" title="Occupied"></span>' : ''}
</button>
@ -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 => `
<tr>
<td style="padding: 5px 0; border-bottom: 1px solid #eee;">
<div style="font-weight: bold;">${item.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>
</td>
<td style="text-align: right; vertical-align: middle; font-weight: bold;">${formatCurrency(item.quantity * item.price)}</td>
</tr>
`).join('');
const customerHtml = data.customer ? `
<div style="margin-bottom: 10px; border: 1px solid #eee; padding: 8px; border-radius: 4px;">
<div style="font-weight: bold; text-transform: uppercase; font-size: 10px; color: #666; margin-bottom: 3px;">
<span style="float: left;">Customer Details</span>
<span style="float: right;">${tr['Customer Details']}</span>
<div style="clear: both;"></div>
</div>
<div style="font-weight: bold;">${data.customer.name}</div>
${data.customer.phone ? `<div>Tel: ${data.customer.phone}</div>` : ''}
${data.customer.address ? `<div style="font-size: 11px;">${data.customer.address}</div>` : ''}
</div>
` : '';
const tableHtml = data.tableNumber && data.orderType === 'dine-in' ? `
<div style="display: flex; justify-content: space-between;">
<span><strong>Table:</strong> ${data.tableNumber}</span>
<span><strong>${tr['Table']}:</strong> ${data.tableNumber}</span>
</div>` : '';
const paymentHtml = data.paymentMethod ? `
<div style="display: flex; justify-content: space-between;">
<span><strong>Payment:</strong> ${data.paymentMethod}</span>
<span><strong>${tr['Payment']}:</strong> ${data.paymentMethod}</span>
</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 logoHtml = settings.logo_url ? `<img src="${BASE_URL}${settings.logo_url}" style="max-height: 80px; max-width: 150px; margin-bottom: 10px;">` : '';
const html = `
<html dir="ltr">
<head>
<title>Receipt #${data.orderId}</title>
<style>
body { font-family: 'Courier New', Courier, monospace; font-size: 13px; margin: 0; padding: 15px; color: #000; line-height: 1.4; }
.header { text-align: center; margin-bottom: 15px; }
.header h2 { margin: 0 0 5px 0; font-size: 20px; font-weight: bold; }
.header div { font-size: 12px; }
table { width: 100%; border-collapse: collapse; }
.divider { border-bottom: 2px dashed #000; margin: 10px 0; }
.thick-divider { border-bottom: 2px solid #000; margin: 10px 0; }
.totals td { padding: 3px 0; }
.footer { text-align: center; margin-top: 25px; font-size: 12px; }
.order-info { font-size: 11px; margin-bottom: 10px; }
.order-info-row { display: flex; justify-content: space-between; margin-bottom: 2px; }
.rtl { direction: rtl; unicode-bidi: embed; }
@media print { body { width: 80mm; padding: 5mm; } @page { size: 80mm auto; margin: 0; } }
</style>
</head>
<body>
<div class="header">
${logoHtml}
<h2>${settings.company_name}</h2>
<div style="font-weight: bold;">${CURRENT_OUTLET.name}</div>
<div>${settings.address || ''}</div>
<div>Tel: ${settings.phone || ''}</div>
${settings.vat_number ? `<div style="margin-top: 4px;">VAT No / الرقم الضريبي: ${settings.vat_number}</div>` : ''}
${settings.ctr_number ? `<div>CTR No / رقم السجل: ${settings.ctr_number}</div>` : ''}
</div>
<div class="divider"></div>
<div class="order-info">
<div class="order-info-row"><span><strong>Order:</strong> #${data.orderId}</span><span><strong>${tr['Order']}:</strong> #${data.orderId}</span></div>
<div class="order-info-row"><span><strong>Type:</strong> ${data.orderType.toUpperCase()}</span><span><strong>${tr['Type']}:</strong> ${tr[data.orderType] || data.orderType}</span></div>
<div class="order-info-row"><span><strong>Date:</strong> ${data.date}</span><span><strong>${tr['Date']}:</strong> ${data.date}</span></div>
<div class="order-info-row"><span><strong>Staff:</strong> ${CURRENT_USER.name}</span><span><strong>${tr['Staff']}:</strong> ${CURRENT_USER.name}</span></div>
</div>
${tableHtml}${paymentHtml}${loyaltyHtml}
<div class="thick-divider"></div>
${customerHtml}
<table>
<thead><tr><th style="text-align: left; padding-bottom: 5px;">ITEM / الصنف</th><th style="text-align: right; padding-bottom: 5px;">TOTAL / المجموع</th></tr></thead>
<tbody>${itemsHtml}</tbody>
</table>
<div class="divider"></div>
<div class="totals">
<table style="width: 100%">
<tr><td>Subtotal / ${tr['Subtotal']}</td><td style="text-align: right">${formatCurrency(subtotal)}</td></tr>
${Math.abs(data.vat) > 0 ? `<tr><td>${data.vat < 0 ? 'Discount' : 'VAT'} / ${tr['VAT']}</td><td style="text-align: right">${data.vat < 0 ? '-' : '+'}${formatCurrency(Math.abs(data.vat))}</td></tr>` : ''}
<tr style="font-weight: bold; font-size: 18px;"><td style="padding-top: 10px;">TOTAL / ${tr['TOTAL']}</td><td style="text-align: right; padding-top: 10px;">${formatCurrency(data.total)}</td></tr>
</table>
</div>
<div class="thick-divider"></div>
<div class="footer">
<div style="font-weight: bold; font-size: 14px; margin-bottom: 2px;">THANK YOU FOR YOUR VISIT!</div>
<div style="font-weight: bold; font-size: 14px; margin-bottom: 5px;" class="rtl">${tr['THANK YOU FOR YOUR VISIT!']}</div>
<div>Please come again.</div><div class="rtl">${tr['Please come again.']}</div>
${settings.email ? `<div style="margin-top: 5px; font-size: 10px;">${settings.email}</div>` : ''}
<div style="margin-top: 15px; font-size: 10px; color: #888;">Powered by Abidarcafe</div>
</div>
<script>window.onload = function() { window.print(); setTimeout(function() { window.close(); }, 1500); }</script>
</body></html>
`;
win.document.write(html);
win.document.close();
};
window.openRatingQRModal = function() {
const qrContainer = document.getElementById('rating-qr-container');
const ratingUrl = BASE_URL + '/rate.php';

View File

@ -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) {

29
pos.php
View File

@ -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) {
<img src="<?= get_base_url() . htmlspecialchars($settings['logo_url']) ?>?v=<?= time() ?>" alt="Logo" height="28" class="me-2">
<?php endif; ?>
<span class="d-none d-sm-inline" style="letter-spacing: -0.5px;"><?= htmlspecialchars($settings['company_name']) ?></span>
<span class="badge bg-primary-subtle text-primary ms-2 fs-7 fw-semibold rounded-pill px-2 border border-primary-subtle"><?= htmlspecialchars($current_outlet_name) ?></span>
</a>
<div class="ms-auto d-flex align-items-center gap-1 gap-sm-2">
@ -200,7 +191,7 @@ if (!$loyalty_settings) {
<div class="row g-0 pos-layout">
<!-- Left Sidebar: Categories -->
<div class="col-md-2 col-auto pos-categories scrollable-y p-2" style="max-width: 170px;">
<div class="col-md-2 col-auto pos-categories scrollable-y p-2" style="max-width: 220px;">
<button class="category-btn active mb-2" data-category="all">
<i class="bi bi-grid fs-5 text-inherit"></i>
<span style="font-size: 0.85rem; font-weight: 700;">All</span>
@ -222,7 +213,7 @@ if (!$loyalty_settings) {
<!-- Search Bar -->
<div class="px-3 py-2 border-bottom bg-white">
<div class="row g-2 align-items-center">
<div class="col">
<div class="col-md-6 col-lg-5">
<div class="position-relative">
<span class="position-absolute top-50 start-0 translate-middle-y ms-3 text-muted">
<i class="bi bi-search small"></i>
@ -232,7 +223,7 @@ if (!$loyalty_settings) {
</div>
<?php if (count($outlets) > 1): ?>
<div class="col-auto">
<select class="form-select form-select-sm border-0 bg-light rounded-3 fw-bold text-primary" style="min-width: 120px;" onchange="location.href='?outlet_id=' + this.value + '&order_type=<?= $order_type ?>'">
<select class="form-select form-select-sm border-0 bg-light rounded-3 fw-bold text-primary" style="min-width: 200px;" onchange="location.href='?outlet_id=' + this.value + '&order_type=<?= $order_type ?>'">
<?php foreach ($outlets as $o): ?>
<option value="<?= $o['id'] ?>" <?= $o['id'] == $outlet_id ? 'selected' : '' ?>><?= htmlspecialchars($o['name']) ?></option>
<?php endforeach; ?>
@ -244,7 +235,7 @@ if (!$loyalty_settings) {
<!-- Grid Container -->
<div class="flex-grow-1 scrollable-y p-2">
<div class="row g-2 row-cols-3 row-cols-sm-4 row-cols-md-5 row-cols-lg-6 row-cols-xl-7 row-cols-xxl-7" id="product-grid">
<div class="row g-2 row-cols-3 row-cols-sm-4 row-cols-md-5 row-cols-lg-6 row-cols-xl-6 row-cols-xxl-6" id="product-grid">
<?php foreach ($all_products as $product): ?>
<?php $has_variants = !empty($variants_by_product[$product['id']]); ?>
<div class="col product-item"
@ -267,7 +258,7 @@ if (!$loyalty_settings) {
</div>
<?php endif; ?>
</div>
<div class="card-body p-1 text-center d-flex flex-column justify-content-between">
<div class="card-body p-1 text-center d-flex flex-column justify-content-between flex-grow-0">
<div>
<h6 class="card-title product-title mb-0"><?= htmlspecialchars($product['name']) ?></h6>
</div>