Autosave: 20260303-131517

This commit is contained in:
Flatlogic Bot 2026-03-03 13:15:17 +00:00
parent 538704f9d0
commit e135bdd9e0
12 changed files with 658 additions and 645 deletions

View File

@ -16,36 +16,26 @@ $items = $input['items'] ?? [];
$vat_total = (float)($input['vat_total'] ?? 0);
$total_price = (float)($input['total_price'] ?? 0);
$payment_method = $input['payment_method'] ?? null;
$points_to_redeem = (float)($input['points_to_redeem'] ?? 0);
$branch_id = $_SESSION['branch_id'];
$user_id = $_SESSION['user_id'];
// Check if branch_id is 'all' (happens when super_admin views all branches)
// Check if branch_id is 'all'
if ($branch_id === 'all') {
// We need a specific branch_id to save an order.
// If we're in "all" mode, we'll try to get the user's primary branch or just the first available branch.
try {
$pdo = db();
$stmt_u = $pdo->prepare("SELECT branch_id FROM users WHERE id = ?");
$stmt_u->execute([$user_id]);
$u_branch = $stmt_u->fetchColumn();
if ($u_branch && $u_branch != 'all') {
$branch_id = $u_branch;
} else {
// Fallback to first branch in user_branches
$stmt_ub = $pdo->prepare("SELECT branch_id FROM user_branches WHERE user_id = ? LIMIT 1");
$stmt_ub->execute([$user_id]);
$ub_branch = $stmt_ub->fetchColumn();
if ($ub_branch) {
$branch_id = $ub_branch;
} else {
// Last resort: first branch in system
$branch_id = $pdo->query("SELECT id FROM branches LIMIT 1")->fetchColumn();
}
$branch_id = $ub_branch ?: $pdo->query("SELECT id FROM branches LIMIT 1")->fetchColumn();
}
} catch (Exception $e) {
// Silently continue, might fail at insert
}
} catch (Exception $e) {}
}
if (empty($items)) {
@ -60,72 +50,92 @@ try {
$payment_status = ($payment_method && $payment_method !== 'pay_later') ? 'paid' : 'unpaid';
if ($order_id) {
// Update existing order
$stmt = $pdo->prepare("UPDATE orders SET customer_id = ?, total_price = ?, vat_total = ?, payment_status = ? WHERE id = ? AND branch_id = ?");
$stmt->execute([$customer_id, $total_price, $vat_total, $payment_status, $order_id, $branch_id]);
// Remove existing items
$stmt = $pdo->prepare("DELETE FROM order_items WHERE order_id = ?");
$stmt->execute([$order_id]);
} else {
// Create new order
$stmt = $pdo->prepare("INSERT INTO orders (branch_id, customer_id, user_id, order_number, total_price, vat_total, status, payment_status)
VALUES (?, ?, ?, NULL, ?, ?, 'received', ?)");
$stmt->execute([$branch_id, $customer_id, $user_id, $total_price, $vat_total, $payment_status]);
$order_id = $pdo->lastInsertId();
// Get branch prefix
// Order Number Generation
$stmt_prefix = $pdo->prepare("SELECT prefix FROM branches WHERE id = ?");
$stmt_prefix->execute([$branch_id]);
$prefix = $stmt_prefix->fetchColumn() ?: 'ORD';
if (strlen($prefix) > 3) $prefix = substr($prefix, 0, 3);
$prefix = strtoupper(str_pad($prefix, 3, 'X'));
$prefix = strtoupper(str_pad(substr($stmt_prefix->fetchColumn() ?: 'ORD', 0, 3), 3, 'X'));
// Determine the next serial number for this branch
$stmt_max = $pdo->prepare("SELECT order_number FROM orders WHERE branch_id = ? AND order_number LIKE ? ORDER BY id DESC LIMIT 100");
$stmt_max->execute([$branch_id, "$prefix-%"]);
$existing_orders = $stmt_max->fetchAll(PDO::FETCH_COLUMN);
$existing = $stmt_max->fetchAll(PDO::FETCH_COLUMN);
$max_serial = 0;
foreach ($existing_orders as $onum) {
foreach ($existing as $onum) {
if (preg_match('/' . preg_quote($prefix) . '-(\d{6})/', $onum, $matches)) {
$serial = (int)$matches[1];
if ($serial > $max_serial) $max_serial = $serial;
if ((int)$matches[1] > $max_serial) $max_serial = (int)$matches[1];
}
}
$next_serial = $max_serial + 1;
// Format order_number as XXX-###### (6 digits)
$order_number = $prefix . '-' . str_pad($next_serial, 6, '0', STR_PAD_LEFT);
// Update the order with the generated order_number
$stmt_update = $pdo->prepare("UPDATE orders SET order_number = ? WHERE id = ?");
$stmt_update->execute([$order_number, $order_id]);
$order_number = $prefix . '-' . str_pad($max_serial + 1, 6, '0', STR_PAD_LEFT);
$pdo->prepare("UPDATE orders SET order_number = ? WHERE id = ?")->execute([$order_number, $order_id]);
}
$stmt_item = $pdo->prepare("INSERT INTO order_items (order_id, item_id, variant_id, service_id, quantity, unit_price, vat_amount, subtotal)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
$stmt_item = $pdo->prepare("INSERT INTO order_items (order_id, item_id, variant_id, service_id, quantity, unit_price, vat_amount, subtotal) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
foreach ($items as $item) {
$subtotal = ($item['price'] * $item['quantity']);
$stmt_item->execute([
$order_id,
$item['itemId'],
$item['variantId'] ?: null,
$item['serviceId'],
$item['quantity'],
$item['price'],
$item['vatAmount'] ?: 0,
$subtotal
]);
$stmt_item->execute([$order_id, $item['itemId'], $item['variantId'] ?: null, $item['serviceId'], $item['quantity'], $item['price'], $item['vatAmount'] ?: 0, ($item['price'] * $item['quantity'])]);
}
// Handle Payment recording
if ($payment_method && $payment_method !== 'pay_later') {
$stmt_payment = $pdo->prepare("INSERT INTO payments (order_id, amount, payment_method) VALUES (?, ?, ?)");
$stmt_payment->execute([$order_id, $total_price, $payment_method]);
// Loyalty Redemption Logic
$redeem_amount = 0;
if ($customer_id && $points_to_redeem > 0 && get_setting('loyalty_enabled') === '1') {
$stmt_c = $pdo->prepare("SELECT loyalty_points FROM customers WHERE id = ? FOR UPDATE");
$stmt_c->execute([$customer_id]);
$curr_points = (float)$stmt_c->fetchColumn();
if ($curr_points >= $points_to_redeem) {
$point_val = (float)get_setting('loyalty_currency_per_point', 0.05);
$redeem_amount = $points_to_redeem * $point_val;
// Cap redemption at order total
if ($redeem_amount > $total_price) {
$redeem_amount = $total_price;
$points_to_redeem = $redeem_amount / $point_val;
}
// Deduct points
$pdo->prepare("UPDATE customers SET loyalty_points = loyalty_points - ? WHERE id = ?")->execute([$points_to_redeem, $customer_id]);
// Record loyalty transaction
$pdo->prepare("INSERT INTO loyalty_transactions (customer_id, order_id, points, type, description) VALUES (?, ?, ?, 'redeemed', ?)")
->execute([$customer_id, $order_id, -$points_to_redeem, "Redeemed for order $order_number"]);
// Record loyalty payment
$pdo->prepare("INSERT INTO payments (order_id, amount, payment_method) VALUES (?, ?, 'loyalty')")
->execute([$order_id, $redeem_amount]);
// Save loyalty discount to order
$pdo->prepare("UPDATE orders SET loyalty_discount = ? WHERE id = ?")
->execute([$redeem_amount, $order_id]);
}
}
// Handle Remaining Payment
$remaining_to_pay = $total_price - $redeem_amount;
if ($payment_method && $payment_method !== 'pay_later' && $remaining_to_pay > 0) {
$pdo->prepare("INSERT INTO payments (order_id, amount, payment_method) VALUES (?, ?, ?)")
->execute([$order_id, $remaining_to_pay, $payment_method]);
}
// Loyalty Earning Logic
if ($customer_id && get_setting('loyalty_enabled') === '1' && $payment_status === 'paid') {
$pts_per_curr = (float)get_setting('loyalty_points_per_currency', 1);
$points_earned = $remaining_to_pay * $pts_per_curr;
if ($points_earned > 0) {
$pdo->prepare("UPDATE customers SET loyalty_points = loyalty_points + ? WHERE id = ?")->execute([$points_earned, $customer_id]);
$pdo->prepare("INSERT INTO loyalty_transactions (customer_id, order_id, points, type, description) VALUES (?, ?, ?, 'earned', ?)")
->execute([$customer_id, $order_id, $points_earned, "Earned from order $order_number"]);
}
}
// Fetch customer details for notifications
$customer = null;
if ($customer_id) {
$stmt_cust = $pdo->prepare('SELECT name_ar, phone FROM customers WHERE id = ?');
@ -135,10 +145,9 @@ try {
$pdo->commit();
// WhatsApp Notifications
// WhatsApp
try {
if (get_setting('whatsapp_enabled') === '1' && $customer && !empty($customer['phone'])) {
// 1. Order Created Notification
$template = get_setting('msg_order_created_ar');
if (!empty($template)) {
$details_parts = [];
@ -148,39 +157,11 @@ try {
$names = $stmt_names->fetch();
$details_parts[] = ($names['item_name'] ?? 'صنف') . ' (' . ($names['service_name'] ?? 'خدمة') . ') x' . $item['quantity'];
}
$order_details = implode(', ', $details_parts);
// Get order number if not already available
if (!isset($order_number)) {
$stmt_onum = $pdo->prepare('SELECT order_number FROM orders WHERE id = ?');
$stmt_onum->execute([$order_id]);
$order_number = $stmt_onum->fetchColumn();
}
$message = str_replace(
['{customer_name}', '{order_number}', '{order_details}', '{total_price}'],
[$customer['name_ar'], $order_number, $order_details, $total_price],
$template
);
$message = str_replace(['{customer_name}', '{order_number}', '{order_details}', '{total_price}'], [$customer['name_ar'], $order_number, implode(', ', $details_parts), $total_price], $template);
send_whatsapp_message($customer['phone'], $message);
}
// 2. Payment Received Notification (if paid during checkout)
if ($payment_method && $payment_method !== 'pay_later') {
$template_pay = get_setting('msg_payment_ar');
if (!empty($template_pay)) {
$message_pay = str_replace(
['{customer_name}', '{order_number}', '{amount}', '{remaining_balance}'],
[$customer['name_ar'], $order_number, $total_price, 0],
$template_pay
);
send_whatsapp_message($customer['phone'], $message_pay);
}
}
}
} catch (Exception $e) {
// Silently fail for notifications
}
} catch (Exception $e) {}
echo json_encode(['success' => true, 'order_id' => $order_id]);
} catch (Exception $e) {

View File

@ -16,10 +16,10 @@ if (strlen($query) < 2) {
}
try {
$stmt = db()->prepare("SELECT id, name_en, name_ar, phone FROM customers WHERE name_en LIKE ? OR name_ar LIKE ? OR phone LIKE ? LIMIT 10");
$stmt = db()->prepare("SELECT id, name_en, name_ar, phone, loyalty_points FROM customers WHERE name_en LIKE ? OR name_ar LIKE ? OR phone LIKE ? LIMIT 10");
$stmt->execute(["%$query%", "%$query%", "%$query%"]);
$customers = $stmt->fetchAll();
echo json_encode($customers);
} catch (Exception $e) {
echo json_encode([]);
}
}

View File

@ -3,7 +3,6 @@ $title = 'company_profile';
require_once __DIR__ . '/includes/header.php';
if ($current_role === 'limited_viewer') { header('Location: admin.php'); exit; }
// Only super_admin can access company profile
if ($current_role !== 'super_admin') {
echo '<div class="alert alert-danger">' . __('Access Denied') . '</div>';
require_once __DIR__ . '/includes/footer.php';
@ -14,7 +13,6 @@ $success = '';
$error = '';
$is_super = ($current_role === 'super_admin');
// Get company data
$stmt = db()->query("SELECT * FROM companies LIMIT 1");
$company = $stmt->fetch();
@ -26,45 +24,34 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$phone = $_POST['phone'] ?? '';
$address_en = $_POST['address_en'] ?? '';
$address_ar = $_POST['address_ar'] ?? '';
$vat_number = $_POST['vat_no'] ?? ''; // Keep for compatibility
$ctr_no = $_POST['ctr_no'] ?? '';
$vat_no = $_POST['vat_no'] ?? '';
$ctr_no = $_POST['ctr_no'] ?? '';
// Handle Logo Upload
$logo = $company['logo'];
if (isset($_FILES['logo']) && $_FILES['logo']['error'] === UPLOAD_ERR_OK) {
$ext = pathinfo($_FILES['logo']['name'], PATHINFO_EXTENSION);
$filename = 'logo_' . time() . '.' . $ext;
$target = 'assets/images/company/' . $filename;
if (move_uploaded_file($_FILES['logo']['tmp_name'], $target)) {
$logo = $target;
}
if (move_uploaded_file($_FILES['logo']['tmp_name'], $target)) $logo = $target;
}
// Handle Favicon Upload
$favicon = $company['favicon'];
if (isset($_FILES['favicon']) && $_FILES['favicon']['error'] === UPLOAD_ERR_OK) {
$ext = pathinfo($_FILES['favicon']['name'], PATHINFO_EXTENSION);
$filename = 'favicon_' . time() . '.' . $ext;
$target = 'assets/images/company/' . $filename;
if (move_uploaded_file($_FILES['favicon']['tmp_name'], $target)) {
$favicon = $target;
}
if (move_uploaded_file($_FILES['favicon']['tmp_name'], $target)) $favicon = $target;
}
try {
$stmt = db()->prepare("UPDATE companies SET name_en = ?, name_ar = ?, logo = ?, favicon = ?, email = ?, phone = ?, address_en = ?, address_ar = ?, vat_number = ?, ctr_no = ?, vat_no = ? WHERE id = ?");
$stmt->execute([$name_en, $name_ar, $logo, $favicon, $email, $phone, $address_en, $address_ar, $vat_number, $ctr_no, $vat_no, $company['id']]);
$stmt->execute([$name_en, $name_ar, $logo, $favicon, $email, $phone, $address_en, $address_ar, $vat_no, $ctr_no, $vat_no, $company['id']]);
$success = __('success_update');
// Refresh data
$stmt = db()->query("SELECT * FROM companies LIMIT 1");
$company = $stmt->fetch();
} catch (Exception $e) {
$error = __('error_update') . ' ' . $e->getMessage();
}
} catch (Exception $e) { $error = __('error_update') . ' ' . $e->getMessage(); }
}
// Handle WhatsApp Settings Save
if ($is_super && isset($_POST['save_whatsapp_settings'])) {
set_setting('whatsapp_enabled', $_POST['whatsapp_enabled'] ?? '0');
set_setting('wablas_token', $_POST['wablas_token'] ?? '');
@ -73,10 +60,16 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
set_setting('msg_order_created_ar', $_POST['msg_order_created_ar'] ?? '');
set_setting('msg_order_ready_ar', $_POST['msg_order_ready_ar'] ?? '');
set_setting('msg_payment_ar', $_POST['msg_payment_ar'] ?? '');
$success = is_arabic() ? 'تم حفظ إعدادات الواتساب بنجاح' : 'WhatsApp settings saved successfully';
$success = __('success_update');
}
if ($is_super && isset($_POST['save_loyalty_settings'])) {
set_setting('loyalty_enabled', $_POST['loyalty_enabled'] ?? '0');
set_setting('loyalty_points_per_currency', $_POST['loyalty_points_per_currency'] ?? '1');
set_setting('loyalty_currency_per_point', $_POST['loyalty_currency_per_point'] ?? '0.05');
$success = __('success_update');
}
}
?>
<div class="d-flex justify-content-between align-items-center mb-4">
@ -84,122 +77,105 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
</div>
<?php if ($success): ?>
<div class="alert alert-success"><?= $success ?></div>
<?php endif; ?>
<?php if ($error): ?>
<div class="alert alert-danger"><?= $error ?></div>
<?php endif; ?>
<div class="card p-4 mb-4 shadow-sm" style="border-radius: 20px;">
<h5 class="fw-bold mb-4"><i class="bi bi-building me-2"></i> <?= is_arabic() ? 'معلومات الشركة' : 'Company Information' ?></h5>
<form action="company_profile.php" method="POST" enctype="multipart/form-data">
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label"><?= __('name_en') ?></label>
<input type="text" name="name_en" class="form-control" value="<?= htmlspecialchars($company['name_en'] ?? '') ?>" required>
</div>
<div class="col-md-6 mb-3">
<label class="form-label"><?= __('name_ar') ?></label>
<input type="text" name="name_ar" class="form-control" value="<?= htmlspecialchars($company['name_ar'] ?? '') ?>" required>
</div>
<div class="col-md-6 mb-3">
<label class="form-label"><?= __('email') ?></label>
<input type="email" name="email" class="form-control" value="<?= htmlspecialchars($company['email'] ?? '') ?>">
</div>
<div class="col-md-6 mb-3">
<label class="form-label"><?= __('phone') ?></label>
<input type="text" name="phone" class="form-control" value="<?= htmlspecialchars($company['phone'] ?? '') ?>">
</div>
<div class="col-md-6 mb-3">
<label class="form-label"><?= __('ctr_no') ?></label>
<input type="text" name="ctr_no" class="form-control" value="<?= htmlspecialchars($company['ctr_no'] ?? '') ?>">
</div>
<div class="col-md-6 mb-3">
<label class="form-label"><?= __('vat_no') ?></label>
<input type="text" name="vat_no" class="form-control" value="<?= htmlspecialchars($company['vat_no'] ?? '') ?>">
</div>
<div class="col-md-6 mb-3">
<div class="row">
<div class="col-md-6">
<label class="form-label"><?= __('logo') ?></label>
<input type="file" name="logo" class="form-control" accept="image/*">
<?php if ($company['logo']): ?>
<img src="<?= $company['logo'] ?>" alt="Logo" class="mt-2 rounded border" style="max-height: 50px;">
<?php endif; ?>
</div>
<div class="col-md-6">
<label class="form-label"><?= __('favicon') ?></label>
<input type="file" name="favicon" class="form-control" accept="image/*">
<?php if ($company['favicon']): ?>
<img src="<?= $company['favicon'] ?>" alt="Favicon" class="mt-2 rounded border" style="max-height: 32px;">
<?php endif; ?>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label"><?= __('address_en') ?></label>
<textarea name="address_en" class="form-control" rows="3"><?= htmlspecialchars($company['address_en'] ?? '') ?></textarea>
</div>
<div class="col-md-6 mb-3">
<label class="form-label"><?= __('address_ar') ?></label>
<textarea name="address_ar" class="form-control" rows="3"><?= htmlspecialchars($company['address_ar'] ?? '') ?></textarea>
</div>
</div>
<div class="text-end">
<button type="submit" name="save_company_profile" class="btn btn-primary px-5" style="border-radius: 12px;">
<i class="bi bi-save me-2"></i> <?= __('save_changes') ?>
</button>
</div>
</form>
</div>
<?php if ($is_super): ?>
<div class="card p-4 shadow-sm" style="border-radius: 20px;">
<h5 class="fw-bold mb-4"><i class="bi bi-whatsapp text-success me-2"></i> <?= __('whatsapp_settings') ?></h5>
<form method="POST">
<div class="row g-3">
<div class="col-md-12">
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" id="whatsapp_enabled" name="whatsapp_enabled" value="1" <?= get_setting('whatsapp_enabled') == '1' ? 'checked' : '' ?>>
<label class="form-check-label" for="whatsapp_enabled"><?= __('whatsapp_enabled') ?></label>
</div>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold"><?= __('wablas_token') ?></label>
<input type="text" class="form-control" name="wablas_token" value="<?= htmlspecialchars(get_setting('wablas_token', '')) ?>" placeholder="Wablas API Token">
</div>
<div class="col-md-6">
<label class="form-label fw-semibold"><?= __('wablas_server') ?></label>
<input type="text" class="form-control" name="wablas_server" value="<?= htmlspecialchars(get_setting('wablas_server', 'https://console.wablas.com')) ?>" placeholder="https://console.wablas.com">
</div>
<div class="col-md-12">
<label class="form-label fw-semibold"><?= __('wablas_security_key') ?></label>
<input type="text" class="form-control" name="wablas_security_key" value="<?= htmlspecialchars(get_setting('wablas_security_key', '')) ?>" placeholder="Wablas Security Key">
</div>
<div class="col-md-12">
<label class="form-label fw-semibold"><?= __('msg_order_created_ar') ?></label>
<textarea class="form-control mb-2" name="msg_order_created_ar" rows="2"><?= htmlspecialchars(get_setting('msg_order_created_ar', '')) ?></textarea>
<small class="text-muted">Tags: {customer_name}, {order_number}, {order_details}, {total_price}</small>
</div>
<div class="col-md-12">
<label class="form-label fw-semibold"><?= __('msg_order_ready_ar') ?></label>
<textarea class="form-control mb-2" name="msg_order_ready_ar" rows="2"><?= htmlspecialchars(get_setting('msg_order_ready_ar', '')) ?></textarea>
<small class="text-muted">Tags: {customer_name}, {order_number}</small>
</div>
<div class="col-md-12">
<label class="form-label fw-semibold"><?= __('msg_payment_ar') ?></label>
<textarea class="form-control mb-2" name="msg_payment_ar" rows="2"><?= htmlspecialchars(get_setting('msg_payment_ar', '')) ?></textarea>
<small class="text-muted">Tags: {customer_name}, {order_number}, {amount}, {remaining_balance}</small>
</div>
<div class="col-12 mt-4">
<button type="submit" name="save_whatsapp_settings" class="btn btn-primary px-4 py-2" style="border-radius: 12px;">
<i class="bi bi-save me-2"></i> <?= is_arabic() ? 'حفظ' : 'Save' ?>
</button>
</div>
</div>
</form>
<div class="alert alert-success alert-dismissible fade show rounded-4 border-0 shadow-sm" role="alert">
<i class="bi bi-check-circle-fill me-2"></i> <?= $success ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<?php require_once __DIR__ . '/includes/footer.php'; ?>
<?php if ($error): ?>
<div class="alert alert-danger alert-dismissible fade show rounded-4 border-0 shadow-sm" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i> <?= $error ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<div class="card p-4 mb-4 shadow-sm border-0" style="border-radius: 20px;">
<h5 class="fw-bold mb-4"><i class="bi bi-building me-2"></i> <?= is_arabic() ? 'معلومات الشركة' : 'Company Information' ?></h5>
<form action="company_profile.php" method="POST" enctype="multipart/form-data">
<div class="row">
<div class="col-md-6 mb-3"><label class="form-label"><?= __('name_en') ?></label><input type="text" name="name_en" class="form-control rounded-3" value="<?= htmlspecialchars($company['name_en'] ?? '') ?>" required></div>
<div class="col-md-6 mb-3"><label class="form-label"><?= __('name_ar') ?></label><input type="text" name="name_ar" class="form-control rounded-3" value="<?= htmlspecialchars($company['name_ar'] ?? '') ?>" required></div>
<div class="col-md-6 mb-3"><label class="form-label"><?= __('email') ?></label><input type="email" name="email" class="form-control rounded-3" value="<?= htmlspecialchars($company['email'] ?? '') ?>"></div>
<div class="col-md-6 mb-3"><label class="form-label"><?= __('phone') ?></label><input type="text" name="phone" class="form-control rounded-3" value="<?= htmlspecialchars($company['phone'] ?? '') ?>"></div>
<div class="col-md-6 mb-3"><label class="form-label"><?= __('ctr_no') ?></label><input type="text" name="ctr_no" class="form-control rounded-3" value="<?= htmlspecialchars($company['ctr_no'] ?? '') ?>"></div>
<div class="col-md-6 mb-3"><label class="form-label"><?= __('vat_no') ?></label><input type="text" name="vat_no" class="form-control rounded-3" value="<?= htmlspecialchars($company['vat_no'] ?? '') ?>"></div>
<div class="col-md-6 mb-3">
<div class="row">
<div class="col-md-6"><label class="form-label"><?= __('logo') ?></label><input type="file" name="logo" class="form-control rounded-3" accept="image/*"><?php if ($company['logo']): ?><img src="<?= $company['logo'] ?>" alt="Logo" class="mt-2 rounded border" style="max-height: 50px;"><?php endif; ?></div>
<div class="col-md-6"><label class="form-label"><?= __('favicon') ?></label><input type="file" name="favicon" class="form-control rounded-3" accept="image/*"><?php if ($company['favicon']): ?><img src="<?= $company['favicon'] ?>" alt="Favicon" class="mt-2 rounded border" style="max-height: 32px;"><?php endif; ?></div>
</div>
</div>
<div class="col-md-6 mb-3"><label class="form-label"><?= __('address_en') ?></label><textarea name="address_en" class="form-control rounded-3" rows="3"><?= htmlspecialchars($company['address_en'] ?? '') ?></textarea></div>
<div class="col-md-6 mb-3"><label class="form-label"><?= __('address_ar') ?></label><textarea name="address_ar" class="form-control rounded-3" rows="3"><?= htmlspecialchars($company['address_ar'] ?? '') ?></textarea></div>
</div>
<div class="text-end"><button type="submit" name="save_company_profile" class="btn btn-primary px-5 rounded-4 shadow-sm"><i class="bi bi-save me-2"></i> <?= __('save_changes') ?></button></div>
</form>
</div>
<div class="row g-4 mb-4">
<?php if ($is_super): ?>
<div class="col-md-6">
<div class="card p-4 shadow-sm border-0 h-100" style="border-radius: 20px;">
<h5 class="fw-bold mb-4"><i class="bi bi-whatsapp text-success me-2"></i> <?= __('whatsapp_settings') ?></h5>
<form method="POST">
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" id="whatsapp_enabled" name="whatsapp_enabled" value="1" <?= get_setting('whatsapp_enabled') == '1' ? 'checked' : '' ?>>
<label class="form-check-label fw-semibold" for="whatsapp_enabled"><?= __('whatsapp_enabled') ?></label>
</div>
<div class="mb-3"><label class="form-label small fw-bold"><?= __('wablas_token') ?></label><input type="text" class="form-control rounded-3" name="wablas_token" value="<?= htmlspecialchars(get_setting('wablas_token', '')) ?>"></div>
<div class="mb-3"><label class="form-label small fw-bold"><?= __('wablas_server') ?></label><input type="text" class="form-control rounded-3" name="wablas_server" value="<?= htmlspecialchars(get_setting('wablas_server', 'https://console.wablas.com')) ?>"></div>
<div class="mb-4"><label class="form-label small fw-bold"><?= __('wablas_security_key') ?></label><input type="text" class="form-control rounded-3" name="wablas_security_key" value="<?= htmlspecialchars(get_setting('wablas_security_key', '')) ?>"></div>
<div class="text-end"><button type="submit" name="save_whatsapp_settings" class="btn btn-primary px-4 rounded-3 shadow-sm"><?= __('save') ?></button></div>
</form>
</div>
</div>
<div class="col-md-6">
<div class="card p-4 shadow-sm border-0 h-100" style="border-radius: 20px;">
<h5 class="fw-bold mb-4"><i class="bi bi-star-fill text-warning me-2"></i> <?= __('loyalty_settings') ?></h5>
<form method="POST">
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" id="loyalty_enabled" name="loyalty_enabled" value="1" <?= get_setting('loyalty_enabled') == '1' ? 'checked' : '' ?>>
<label class="form-check-label fw-semibold" for="loyalty_enabled"><?= __('loyalty_enabled') ?></label>
</div>
<div class="mb-3">
<label class="form-label small fw-bold"><?= __('loyalty_points_per_currency') ?></label>
<input type="number" step="0.01" class="form-control rounded-3" name="loyalty_points_per_currency" value="<?= htmlspecialchars(get_setting('loyalty_points_per_currency', '1')) ?>">
</div>
<div class="mb-4">
<label class="form-label small fw-bold"><?= __('loyalty_currency_per_point') ?></label>
<input type="number" step="0.001" class="form-control rounded-3" name="loyalty_currency_per_point" value="<?= htmlspecialchars(get_setting('loyalty_currency_per_point', '0.05')) ?>">
</div>
<div class="text-end"><button type="submit" name="save_loyalty_settings" class="btn btn-primary px-4 rounded-3 shadow-sm"><?= __('save') ?></button></div>
</form>
</div>
</div>
<?php endif; ?>
</div>
<?php if ($is_super): ?>
<div class="card p-4 shadow-sm border-0" style="border-radius: 20px;">
<h5 class="fw-bold mb-4"><i class="bi bi-chat-dots me-2"></i> <?= is_arabic() ? 'قوالب الرسائل' : 'Message Templates' ?></h5>
<form method="POST">
<div class="mb-3">
<label class="form-label small fw-bold"><?= __('msg_order_created_ar') ?></label>
<textarea class="form-control rounded-3 mb-1" name="msg_order_created_ar" rows="2"><?= htmlspecialchars(get_setting('msg_order_created_ar', '')) ?></textarea>
<small class="text-muted">Tags: {customer_name}, {order_number}, {order_details}, {total_price}</small>
</div>
<div class="mb-3">
<label class="form-label small fw-bold"><?= __('msg_order_ready_ar') ?></label>
<textarea class="form-control rounded-3 mb-1" name="msg_order_ready_ar" rows="2"><?= htmlspecialchars(get_setting('msg_order_ready_ar', '')) ?></textarea>
<small class="text-muted">Tags: {customer_name}, {order_number}</small>
</div>
<div class="mb-4">
<label class="form-label small fw-bold"><?= __('msg_payment_ar') ?></label>
<textarea class="form-control rounded-3 mb-1" name="msg_payment_ar" rows="2"><?= htmlspecialchars(get_setting('msg_payment_ar', '')) ?></textarea>
<small class="text-muted">Tags: {customer_name}, {order_number}, {amount}, {remaining_balance}</small>
</div>
<div class="text-end"><button type="submit" name="save_whatsapp_settings" class="btn btn-primary px-4 rounded-3 shadow-sm"><?= __('save') ?></button></div>
</form>
</div>
<?php endif; ?>
<?php require_once __DIR__ . '/includes/footer.php'; ?>

View File

@ -3,45 +3,24 @@ $title = 'customer_statement';
require_once __DIR__ . '/includes/header.php';
$customer_id = $_GET['id'] ?? null;
if (!$customer_id) {
header('Location: customers.php');
exit;
}
if (!$customer_id) { header('Location: customers.php'); exit; }
// Fetch customer details
$stmt = db()->prepare("SELECT * FROM customers WHERE id = ?");
$stmt->execute([$customer_id]);
$customer = $stmt->fetch();
if (!$customer) {
header('Location: customers.php');
exit;
}
if (!$customer) { header('Location: customers.php'); exit; }
$from_date = $_GET['from_date'] ?? '';
$to_date = $_GET['to_date'] ?? '';
$active_tab = $_GET['tab'] ?? 'financial';
// Base queries
// Financial Logic
$orders_sql = "SELECT id, order_number, total_price, created_at FROM orders WHERE customer_id = ?";
$payments_sql = "SELECT p.id, p.amount, p.payment_method, p.created_at, o.order_number
FROM payments p
JOIN orders o ON p.order_id = o.id
WHERE o.customer_id = ?";
$params = [$customer_id];
if ($from_date) {
$orders_sql .= " AND DATE(created_at) >= ?";
$payments_sql .= " AND DATE(p.created_at) >= ?";
$params[] = $from_date;
}
if ($to_date) {
$orders_sql .= " AND DATE(created_at) <= ?";
$payments_sql .= " AND DATE(p.created_at) <= ?";
$params[] = $to_date;
}
$payments_sql = "SELECT p.id, p.amount, p.payment_method, p.created_at, o.order_number FROM payments p JOIN orders o ON p.order_id = o.id WHERE o.customer_id = ?";
if ($from_date) { $orders_sql .= " AND DATE(created_at) >= ?"; $payments_sql .= " AND DATE(p.created_at) >= ?"; }
if ($to_date) { $orders_sql .= " AND DATE(created_at) <= ?"; $payments_sql .= " AND DATE(p.created_at) <= ?"; }
$stmt_orders = db()->prepare($orders_sql);
// We need to be careful with params if both filters are set
$orders_params = [$customer_id];
if ($from_date) $orders_params[] = $from_date;
if ($to_date) $orders_params[] = $to_date;
@ -55,292 +34,161 @@ if ($to_date) $payments_params[] = $to_date;
$stmt_payments->execute($payments_params);
$payments = $stmt_payments->fetchAll();
// Combine and sort
$transactions = [];
foreach ($orders as $o) {
$transactions[] = [
'date' => $o['created_at'],
'type' => 'order',
'ref' => $o['order_number'],
'debit' => $o['total_price'],
'credit' => 0,
'description' => __('order') . ' #' . $o['order_number']
];
}
foreach ($payments as $p) {
$transactions[] = [
'date' => $p['created_at'],
'type' => 'payment',
'ref' => $p['order_number'],
'debit' => 0,
'credit' => $p['amount'],
'description' => __('payment') . ' (' . __($p['payment_method']) . ') - ' . __('order') . ' #' . $p['order_number']
];
foreach ($orders as $o) { $transactions[] = ['date' => $o['created_at'], 'type' => 'order', 'ref' => $o['order_number'], 'debit' => $o['total_price'], 'credit' => 0, 'description' => __('order') . ' #' . $o['order_number']]; }
foreach ($payments as $p) { $transactions[] = ['date' => $p['created_at'], 'type' => 'payment', 'ref' => $p['order_number'], 'debit' => 0, 'credit' => $p['amount'], 'description' => __('payment') . ' (' . __($p['payment_method']) . ') - ' . __('order') . ' #' . $p['order_number']]; }
usort($transactions, function($a, $b) { return strtotime($a['date']) - strtotime($b['date']); });
$total_debit = 0; $total_credit = 0; $balance = 0;
foreach ($transactions as &$t) { $total_debit += $t['debit']; $total_credit += $t['credit']; $balance += ($t['debit'] - $t['credit']); $t['running_balance'] = $balance; }
// Loyalty Logic
$loyalty_sql = "SELECT lt.*, o.order_number FROM loyalty_transactions lt LEFT JOIN orders o ON lt.order_id = o.id WHERE lt.customer_id = ?";
if ($from_date) $loyalty_sql .= " AND DATE(lt.created_at) >= ?";
if ($to_date) $loyalty_sql .= " AND DATE(lt.created_at) <= ?";
$loyalty_sql .= " ORDER BY lt.created_at ASC";
$stmt_loyalty = db()->prepare($loyalty_sql);
$loyalty_params = [$customer_id];
if ($from_date) $loyalty_params[] = $from_date;
if ($to_date) $loyalty_params[] = $to_date;
$stmt_loyalty->execute($loyalty_params);
$loyalty_transactions = $stmt_loyalty->fetchAll();
$loyalty_balance = 0;
foreach ($loyalty_transactions as &$lt) {
$loyalty_balance += $lt['points'];
$lt['running_balance'] = $loyalty_balance;
}
usort($transactions, function($a, $b) {
return strtotime($a['date']) - strtotime($b['date']);
});
$total_debit = 0;
$total_credit = 0;
$balance = 0;
foreach ($transactions as &$t) {
$total_debit += $t['debit'];
$total_credit += $t['credit'];
$balance += ($t['debit'] - $t['credit']);
$t['running_balance'] = $balance;
}
// Fallback values if database is empty
$display_company_name = ($lang === 'ar' ? ($company_info['name_ar'] ?: $company_info['name_en']) : $company_info['name_en']) ?: 'Laundry Brand';
$display_address = ($lang === 'ar' ? ($company_info['address_ar'] ?: $company_info['address_en']) : $company_info['address_en']);
$display_phone = $company_info['phone'];
$display_email = $company_info['email'];
$display_vat = $company_info['vat_no'] ?: $company_info['vat_number'];
$display_ctr = $company_info['ctr_no'];
?>
<div class="d-print-none mb-4">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="customers.php"><?= __('customers') ?></a></li>
<li class="breadcrumb-item active"><?= __('customer_statement') ?></li>
</ol>
<ol class="breadcrumb"><li class="breadcrumb-item"><a href="customers.php"><?= __('customers') ?></a></li><li class="breadcrumb-item active"><?= __('customer_statement') ?></li></ol>
</nav>
</div>
<div class="card p-4 border-0 shadow-sm mb-4 d-print-none" style="border-radius: 20px;">
<div class="card p-4 border-0 shadow-sm mb-4 d-print-none rounded-4">
<form action="" method="GET" class="row g-3 align-items-end">
<input type="hidden" name="id" value="<?= $customer_id ?>">
<div class="col-md-4">
<label class="form-label small fw-bold"><?= __('from_date') ?></label>
<input type="date" name="from_date" class="form-control" value="<?= $from_date ?>" style="border-radius: 12px;">
</div>
<div class="col-md-4">
<label class="form-label small fw-bold"><?= __('to_date') ?></label>
<input type="date" name="to_date" class="form-control" value="<?= $to_date ?>" style="border-radius: 12px;">
</div>
<input type="hidden" name="tab" value="<?= $active_tab ?>">
<div class="col-md-4"><label class="form-label small fw-bold"><?= __('from_date') ?></label><input type="date" name="from_date" class="form-control rounded-3" value="<?= $from_date ?>"></div>
<div class="col-md-4"><label class="form-label small fw-bold"><?= __('to_date') ?></label><input type="date" name="to_date" class="form-control rounded-3" value="<?= $to_date ?>"></div>
<div class="col-md-4 d-flex gap-2">
<button type="submit" class="btn btn-primary px-4 w-100" style="border-radius: 12px;">
<i class="bi bi-filter me-1"></i> <?= __('filter') ?? 'Filter' ?>
</button>
<button type="button" class="btn btn-outline-dark px-4 w-100" style="border-radius: 12px;" onclick="window.print()">
<i class="bi bi-printer me-1"></i> <?= __('print') ?>
</button>
<button type="submit" class="btn btn-primary px-4 w-100 rounded-3"><i class="bi bi-filter me-1"></i> <?= __('filter') ?></button>
<button type="button" class="btn btn-outline-dark px-4 w-100 rounded-3" onclick="window.print()"><i class="bi bi-printer me-1"></i> <?= __('print') ?></button>
</div>
</form>
</div>
<div id="printableStatement" class="card p-5 border-0 shadow-sm" style="border-radius: 20px;">
<!-- Formal Header for Printing -->
<!-- Tabs -->
<ul class="nav nav-tabs d-print-none mb-4 border-0 gap-2">
<li class="nav-item">
<a class="nav-link rounded-3 fw-bold <?= $active_tab === 'financial' ? 'active bg-primary text-white border-primary' : 'bg-white text-muted border' ?>" href="?id=<?= $customer_id ?>&tab=financial&from_date=<?= $from_date ?>&to_date=<?= $to_date ?>">
<i class="bi bi-cash-stack me-2"></i><?= __('financial_statement') ?? 'Financial Statement' ?>
</a>
</li>
<li class="nav-item">
<a class="nav-link rounded-3 fw-bold <?= $active_tab === 'loyalty' ? 'active bg-primary text-white border-primary' : 'bg-white text-muted border' ?>" href="?id=<?= $customer_id ?>&tab=loyalty&from_date=<?= $from_date ?>&to_date=<?= $to_date ?>">
<i class="bi bi-star-fill me-2"></i><?= __('loyalty_statement') ?? 'Loyalty Statement' ?>
</a>
</li>
</ul>
<div id="printableStatement" class="card p-4 p-md-5 border-0 shadow-sm rounded-4">
<!-- Formal Header -->
<div class="d-none d-print-block mb-5">
<div class="row align-items-center mb-4">
<div class="col-7">
<?php if (!empty($company_info['logo'])): ?>
<img src="<?= $company_info['logo'] ?>" alt="Logo" style="max-height: 100px;" class="mb-3">
<?php else: ?>
<div class="mb-3 d-flex align-items-center text-primary">
<i class="bi bi-tsunami fs-1 me-2"></i>
<span class="fs-4 fw-bold">Laundry Management</span>
</div>
<?php endif; ?>
<?php if (!empty($company_info['logo'])): ?><img src="<?= $company_info['logo'] ?>" alt="Logo" style="max-height: 100px;" class="mb-3"><?php endif; ?>
<h2 class="fw-bold mb-1"><?= htmlspecialchars($display_company_name) ?></h2>
<div class="mb-0 text-muted small">
<?php if ($display_address): ?>
<?= htmlspecialchars($display_address) ?><br>
<?php endif; ?>
<?php if ($display_phone): ?>
<?= __('phone') ?>: <?= htmlspecialchars($display_phone) ?>
<?php endif; ?>
<?php if ($display_email): ?>
<?= $display_phone ? ' | ' : '' ?><?= __('email') ?>: <?= htmlspecialchars($display_email) ?><br>
<?php elseif($display_phone): ?>
<br>
<?php endif; ?>
<?php if ($display_vat): ?>
<?= __('vat_no') ?>: <?= htmlspecialchars($display_vat) ?>
<?php endif; ?>
<?php if ($display_ctr): ?>
<?= $display_vat ? ' | ' : '' ?><?= __('ctr_no') ?>: <?= htmlspecialchars($display_ctr) ?>
<?php endif; ?>
</div>
<div class="text-muted small"><?= htmlspecialchars($display_address) ?><br><?= __('phone') ?>: <?= htmlspecialchars($company_info['phone']) ?></div>
</div>
<div class="col-5 text-end">
<h1 class="fw-bold text-uppercase mb-2" style="color: #0d6efd;"><?= __('statement') ?></h1>
<div class="mt-3">
<h1 class="fw-bold text-uppercase mb-2 text-primary"><?= $active_tab === 'loyalty' ? (__('loyalty_statement') ?? 'Loyalty Statement') : __('statement') ?></h1>
<div class="mt-3 small">
<p class="mb-0 fw-bold"><?= __('date') ?>: <?= date('d/m/Y') ?></p>
<p class="mb-0 text-muted small"><?= __('cashier') ?>: <?= htmlspecialchars($_SESSION['full_name'] ?? 'System Admin') ?></p>
<?php if ($from_date || $to_date): ?>
<p class="mb-0 text-muted small mt-1">
<?= $from_date ? __('from_date') . ': ' . date('d/m/Y', strtotime($from_date)) : '' ?>
<?= $to_date ? ' ' . __('to_date') . ': ' . date('d/m/Y', strtotime($to_date)) : '' ?>
</p>
<?php endif; ?>
<?php if ($from_date || $to_date): ?><p class="mb-0 text-muted mt-1"><?= $from_date ? __('from_date').': '.date('d/m/Y',strtotime($from_date)) : '' ?> <?= $to_date ? __('to_date').': '.date('d/m/Y',strtotime($to_date)) : '' ?></p><?php endif; ?>
</div>
</div>
</div>
<div class="row p-3 bg-light rounded-3 mb-4 mx-0 border">
<div class="col-12">
<h6 class="fw-bold text-muted text-uppercase mb-2 small"><?= __('customer_details') ?></h6>
<h4 class="fw-bold mb-1"><?= $lang === 'ar' ? ($customer['name_ar'] ?: $customer['name_en']) : $customer['name_en'] ?></h4>
<p class="mb-0 text-muted small"><?= $customer['phone'] ?> <?= $customer['email'] ? ' | ' . $customer['email'] : '' ?></p>
</div>
<div class="col-12"><h6 class="fw-bold text-muted text-uppercase mb-2 small"><?= __('customer_details') ?></h6><h4 class="fw-bold mb-1"><?= $lang === 'ar' ? ($customer['name_ar'] ?: $customer['name_en']) : $customer['name_en'] ?></h4><p class="mb-0 text-muted small"><?= $customer['phone'] ?> <?= $customer['email'] ? ' | '.$customer['email'] : '' ?></p></div>
</div>
</div>
<!-- Screen Header -->
<div class="d-print-none mb-4">
<h4 class="fw-bold mb-1"><?= $lang === 'ar' ? ($customer['name_ar'] ?: $customer['name_en']) : $customer['name_en'] ?></h4>
<p class="text-muted mb-0"><?= $customer['phone'] ?> <?= $customer['email'] ? ' | ' . $customer['email'] : '' ?></p>
<p class="text-muted mb-0"><?= $customer['phone'] ?> <?= $customer['email'] ? ' | '.$customer['email'] : '' ?></p>
</div>
<div class="table-responsive">
<table class="table table-striped align-middle mt-4">
<thead class="table-dark">
<tr>
<th><?= __('date') ?></th>
<th><?= __('description') ?></th>
<th class="text-center"><?= __('ref') ?? 'Ref' ?></th>
<th class="text-end"><?= __('debit') ?></th>
<th class="text-end"><?= __('credit') ?></th>
<th class="text-end"><?= __('balance') ?></th>
</tr>
</thead>
<tbody>
<?php if (empty($transactions)): ?>
<tr>
<td colspan="6" class="text-center py-5 text-muted">
<?= __('no_transactions_found') ?? 'No transactions found' ?>
</td>
</tr>
<?php else: ?>
<?php foreach ($transactions as $t): ?>
<tr>
<td class="small"><?= date('d/m/Y H:i', strtotime($t['date'])) ?></td>
<td><?= $t['description'] ?></td>
<td class="text-center"><span class="badge bg-light text-dark border"><?= $t['ref'] ?></span></td>
<td class="text-end"><?= $t['debit'] > 0 ? format_amount($t['debit']) : '-' ?></td>
<td class="text-end"><?= $t['credit'] > 0 ? format_amount($t['credit']) : '-' ?></td>
<td class="text-end fw-bold"><?= format_amount($t['running_balance']) ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
<tfoot class="table-light fw-bold border-top">
<tr>
<td colspan="3" class="text-end"><?= __('total') ?></td>
<td class="text-end"><?= format_amount($total_debit) ?></td>
<td class="text-end"><?= format_amount($total_credit) ?></td>
<td class="text-end text-primary"><?= format_amount($balance) ?></td>
</tr>
</tfoot>
</table>
</div>
<div class="row mt-5">
<div class="col-md-5 ms-auto">
<div class="card bg-white border" style="border-radius: 15px;">
<div class="card-body p-4">
<h5 class="fw-bold mb-4 border-bottom pb-2 small text-uppercase text-muted"><?= __('summary') ?? 'Summary' ?></h5>
<div class="d-flex justify-content-between mb-2">
<span class="text-muted small"><?= __('total_debit') ?>:</span>
<span class="fw-bold small"><?= format_amount($total_debit) ?></span>
</div>
<div class="d-flex justify-content-between mb-2">
<span class="text-muted small"><?= __('total_credit') ?>:</span>
<span class="fw-bold small"><?= format_amount($total_credit) ?></span>
</div>
<div class="d-flex justify-content-between pt-2 mt-2 border-top">
<span class="fw-bold fs-6"><?= __('closing_balance') ?>:</span>
<span class="fw-bold text-primary fs-6"><?= format_amount($balance) ?></span>
</div>
</div>
</div>
<?php if ($active_tab === 'financial'): ?>
<div class="table-responsive">
<table class="table table-striped align-middle mt-2">
<thead class="table-dark">
<tr><th><?= __('date') ?></th><th><?= __('description') ?></th><th class="text-center"><?= __('ref') ?></th><th class="text-end"><?= __('debit') ?></th><th class="text-end"><?= __('credit') ?></th><th class="text-end"><?= __('balance') ?></th></tr>
</thead>
<tbody>
<?php if (empty($transactions)): ?><tr><td colspan="6" class="text-center py-5 text-muted"><?= __('no_transactions_found') ?></td></tr>
<?php else: foreach ($transactions as $t): ?>
<tr><td class="small"><?= date('d/m/Y H:i', strtotime($t['date'])) ?></td><td><?= $t['description'] ?></td><td class="text-center"><span class="badge bg-light text-dark border"><?= $t['ref'] ?></span></td><td class="text-end"><?= $t['debit'] > 0 ? format_amount($t['debit']) : '-' ?></td><td class="text-end"><?= $t['credit'] > 0 ? format_amount($t['credit']) : '-' ?></td><td class="text-end fw-bold"><?= format_amount($t['running_balance']) ?></td></tr>
<?php endforeach; endif; ?>
</tbody>
<tfoot class="table-light fw-bold"><tr><td colspan="3" class="text-end"><?= __('total') ?></td><td class="text-end"><?= format_amount($total_debit) ?></td><td class="text-end"><?= format_amount($total_credit) ?></td><td class="text-end text-primary"><?= format_amount($balance) ?></td></tr></tfoot>
</table>
</div>
</div>
<!-- Print Footer -->
<div class="d-none d-print-block mt-5 pt-4 border-top">
<div class="row text-center small text-muted">
<div class="col-4">
<div class="mb-4">_______________________</div>
<div><?= __('customer_signature') ?? 'Customer Signature' ?></div>
</div>
<div class="col-4">
<div class="mb-4"><?= date('d/m/Y H:i') ?></div>
<div><?= __('print_date') ?? 'Print Date' ?></div>
</div>
<div class="col-4">
<div class="mb-4">_______________________</div>
<div><?= __('authorized_signature') ?? 'Authorized Signature' ?></div>
<?php else: ?>
<div class="table-responsive">
<table class="table table-striped align-middle mt-2">
<thead class="table-dark">
<tr><th><?= __('date') ?></th><th><?= __('description') ?></th><th class="text-center"><?= __('ref') ?></th><th class="text-end"><?= __('earned') ?? 'Earned' ?></th><th class="text-end"><?= __('redeemed') ?? 'Redeemed' ?></th><th class="text-end"><?= __('balance') ?></th></tr>
</thead>
<tbody>
<?php if (empty($loyalty_transactions)): ?><tr><td colspan="6" class="text-center py-5 text-muted"><?= __('no_transactions_found') ?></td></tr>
<?php else: foreach ($loyalty_transactions as $lt): ?>
<tr>
<td class="small"><?= date('d/m/Y H:i', strtotime($lt['created_at'])) ?></td>
<td><?= $lt['description'] ?> <span class="badge bg-soft-primary text-primary small ms-2"><?= __($lt['type']) ?></span></td>
<td class="text-center"><?= $lt['order_number'] ? '<span class="badge bg-light text-dark border">'.$lt['order_number'].'</span>' : '-' ?></td>
<td class="text-end text-success"><?= $lt['points'] > 0 ? '+'.number_format($lt['points'], 2) : '-' ?></td>
<td class="text-end text-danger"><?= $lt['points'] < 0 ? number_format($lt['points'], 2) : '-' ?></td>
<td class="text-end fw-bold"><?= number_format($lt['running_balance'], 2) ?></td>
</tr>
<?php endforeach; endif; ?>
</tbody>
<tfoot class="table-light fw-bold"><tr><td colspan="5" class="text-end"><?= __('current_balance') ?? 'Current Balance' ?></td><td class="text-end text-primary"><?= number_format($customer['loyalty_points'], 2) ?></td></tr></tfoot>
</table>
</div>
<?php endif; ?>
<!-- Summary Box -->
<div class="row mt-5 d-print-none">
<div class="col-md-5 ms-auto">
<div class="card bg-light border-0 p-4 rounded-4">
<h6 class="fw-bold mb-3 text-muted text-uppercase small"><?= __('summary') ?></h6>
<?php if ($active_tab === 'financial'): ?>
<div class="d-flex justify-content-between mb-2"><span><?= __('total_debit') ?>:</span><span class="fw-bold"><?= format_amount($total_debit) ?></span></div>
<div class="d-flex justify-content-between mb-2"><span><?= __('total_credit') ?>:</span><span class="fw-bold"><?= format_amount($total_credit) ?></span></div>
<div class="d-flex justify-content-between pt-2 mt-2 border-top"><span class="fw-bold"><?= __('closing_balance') ?>:</span><span class="fw-bold text-primary"><?= format_amount($balance) ?></span></div>
<?php else: ?>
<div class="d-flex justify-content-between pt-2 mt-2 border-top"><span class="fw-bold"><?= __('loyalty_points') ?>:</span><span class="fw-bold text-primary fs-4"><?= number_format($customer['loyalty_points'], 2) ?></span></div>
<?php endif; ?>
</div>
</div>
</div>
</div>
<style>
.bg-soft-primary { background-color: rgba(13, 110, 253, 0.1); }
@media print {
@page {
size: auto;
margin: 10mm 15mm;
}
body {
background-color: white !important;
font-size: 11px;
-webkit-print-color-adjust: exact;
}
.main-content {
padding: 0 !important;
margin: 0 !important;
width: 100% !important;
max-width: 100% !important;
}
header, .sidebar, .d-print-none, .breadcrumb, nav {
display: none !important;
}
.container-fluid, .row, .col-lg-10 {
padding: 0 !important;
margin: 0 !important;
width: 100% !important;
max-width: 100% !important;
flex: 0 0 100% !important;
}
.card {
box-shadow: none !important;
border: none !important;
padding: 0 !important;
margin: 0 !important;
}
#printableStatement {
padding: 0 !important;
}
table {
width: 100% !important;
border-collapse: collapse !important;
}
th, td {
border: 1px solid #dee2e6 !important;
padding: 6px !important;
}
.badge {
border: none !important;
padding: 0 !important;
}
.bg-light {
background-color: #f8f9fa !important;
}
.table-dark {
background-color: #212529 !important;
color: white !important;
}
body { background-color: white !important; font-size: 10px; }
.main-content { padding: 0 !important; margin: 0 !important; }
header, .sidebar, .d-print-none, .breadcrumb, nav { display: none !important; }
.container-fluid, .row, .col-lg-10 { padding: 0 !important; margin: 0 !important; width: 100% !important; max-width: 100% !important; flex: 0 0 100% !important; }
.card { box-shadow: none !important; border: none !important; padding: 0 !important; }
}
</style>
<?php require_once __DIR__ . '/includes/footer.php'; ?>
<?php require_once __DIR__ . '/includes/footer.php'; ?>

View File

@ -1,5 +1,4 @@
<?php
// ACTION HANDLING FIRST (to allow redirects)
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/lang.php';
@ -8,23 +7,22 @@ if (!isset($_SESSION['user_id'])) {
exit;
}
// Initial view check
if (!has_permission('view')) {
header('Location: admin.php');
exit;
}
$branch_id = $_SESSION['branch_id'];
$loyalty_enabled = get_setting('loyalty_enabled', '0');
// Handle Actions
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
$action = $_POST['action'];
// Permission mapping for actions
$required_permission = 'view';
if ($action === 'add_customer') $required_permission = 'add';
if ($action === 'edit_customer') $required_permission = 'edit';
if ($action === 'delete_customer') $required_permission = 'delete';
if ($action === 'adjust_points') $required_permission = 'edit';
if (!has_permission($required_permission)) {
header('Location: customers.php?error=no_permission');
@ -60,42 +58,54 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
exit;
} elseif ($action === 'delete_customer') {
$id = $_POST['id'];
// Check if customer has orders
$stmt = db()->prepare("SELECT COUNT(*) FROM orders WHERE customer_id = ?");
$stmt->execute([$id]);
if ($stmt->fetchColumn() > 0) {
header('Location: customers.php?error=customer_has_orders');
exit;
}
$stmt = db()->prepare("DELETE FROM customers WHERE id = ?");
$stmt->execute([$id]);
header('Location: customers.php?success=customer_deleted');
exit;
} elseif ($action === 'adjust_points') {
$id = $_POST['id'];
$points = (float)$_POST['points'];
$type = $_POST['type']; // 'add' or 'subtract'
$desc = $_POST['description'] ?? 'Manual adjustment';
$pdo = db();
$pdo->beginTransaction();
try {
$val = ($type === 'add') ? $points : -$points;
$pdo->prepare("UPDATE customers SET loyalty_points = loyalty_points + ? WHERE id = ?")->execute([$val, $id]);
$pdo->prepare("INSERT INTO loyalty_transactions (customer_id, points, type, description) VALUES (?, ?, 'adjusted', ?)")
->execute([$id, $val, $desc]);
$pdo->commit();
header('Location: customers.php?success=points_adjusted');
exit;
} catch (Exception $e) {
$pdo->rollBack();
header('Location: customers.php?error=' . urlencode($e->getMessage()));
exit;
}
}
}
$title = 'customers';
require_once __DIR__ . '/includes/header.php';
$search = $_GET['search'] ?? '';
$sql = "SELECT * FROM customers WHERE 1=1";
$params = [];
if ($search) {
$sql .= " AND (name_en LIKE ? OR name_ar LIKE ? OR phone LIKE ?)";
$params[] = "%$search%";
$params[] = "%$search%";
$params[] = "%$search%";
$params[] = "%$search%"; $params[] = "%$search%"; $params[] = "%$search%";
}
$sql .= " ORDER BY created_at DESC";
$stmt = db()->prepare($sql);
$stmt->execute($params);
$customers = $stmt->fetchAll();
?>
<div class="card p-4 border-0 shadow-sm" style="border-radius: 20px;">
@ -104,31 +114,27 @@ $customers = $stmt->fetchAll();
<div class="d-flex gap-2">
<form action="" method="GET" class="d-flex gap-2">
<div class="input-group shadow-sm rounded-4 overflow-hidden border">
<input type="text" name="search" class="form-control border-0" placeholder="<?= __('search') ?>" value="<?= htmlspecialchars($search) ?>">
<button class="btn btn-white border-0" type="submit">
<i class="bi bi-search"></i>
</button>
<input type="text" name="search" class="form-control border-0 py-2 shadow-none" placeholder="<?= __('search') ?>" value="<?= htmlspecialchars($search) ?>">
<button class="btn btn-white border-0" type="submit"><i class="bi bi-search"></i></button>
</div>
</form>
<?php if (has_permission('add')):
?><button class="btn btn-primary px-4 shadow-sm" style="border-radius: 12px;" onclick="openCustomerModal()">
<?php if (has_permission('add')): ?>
<button class="btn btn-primary px-4 shadow-sm rounded-4" onclick="openCustomerModal()">
<i class="bi bi-person-plus-fill me-1"></i> <?= __('add_new') ?>
</button>
<?php endif; ?>
</div>
</div>
<?php if(isset($_GET['success'])):
?><div class="alert alert-success alert-dismissible fade show rounded-4 shadow-sm mb-4 border-0" role="alert">
<i class="bi bi-check-circle-fill me-2"></i>
<?= __($_GET['success']) ?? 'Action completed successfully' ?>
<?php if(isset($_GET['success'])): ?>
<div class="alert alert-success alert-dismissible fade show rounded-4 shadow-sm mb-4 border-0" role="alert">
<i class="bi bi-check-circle-fill me-2"></i> <?= __($_GET['success']) ?? 'Action completed successfully' ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<?php if(isset($_GET['error'])):
?><div class="alert alert-danger alert-dismissible fade show rounded-4 shadow-sm mb-4 border-0" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<?= __($_GET['error']) ?? 'Permission Denied' ?>
<?php if(isset($_GET['error'])): ?>
<div class="alert alert-danger alert-dismissible fade show rounded-4 shadow-sm mb-4 border-0" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i> <?= __($_GET['error']) ?? 'An error occurred' ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
@ -140,33 +146,41 @@ $customers = $stmt->fetchAll();
<th>#</th>
<th><?= __('name') ?></th>
<th><?= __('phone') ?></th>
<?php if ($loyalty_enabled == '1'): ?>
<th><?= __('loyalty_points') ?? 'Loyalty Points' ?></th>
<?php endif; ?>
<th><?= __('email') ?></th>
<th><?= __('date') ?></th>
<th class="text-end"><?= __('actions') ?></th>
</tr>
</thead>
<tbody>
<?php foreach($customers as $c):
?><tr >
<?php foreach($customers as $c): ?>
<tr>
<td><?= $c['id'] ?></td>
<td>
<div class="fw-bold"><?= $lang === 'ar' ? ($c['name_ar'] ?: $c['name_en']) : $c['name_en'] ?></div>
</td>
<td><div class="fw-bold"><?= $lang === 'ar' ? ($c['name_ar'] ?: $c['name_en']) : $c['name_en'] ?></div></td>
<td><?= $c['phone'] ?></td>
<?php if ($loyalty_enabled == '1'): ?>
<td>
<span class="badge bg-primary rounded-pill px-3 py-2 pointer" onclick="openLoyaltyModal(<?= $c['id'] ?>, '<?= addslashes($lang === 'ar' ? ($c['name_ar'] ?: $c['name_en']) : $c['name_en']) ?>', <?= $c['loyalty_points'] ?>)">
<?= number_format($c['loyalty_points'], 2) ?>
</span>
</td>
<?php endif; ?>
<td><?= $c['email'] ?: '-' ?></td>
<td class="small text-muted"><?= date('d/m/Y', strtotime($c['created_at'])) ?></td>
<td class="text-end">
<div class="d-flex gap-1 justify-content-end">
<a href="customer_statement.php?id=<?= $c['id'] ?>" class="btn btn-sm btn-light border-0 p-2 text-info" title="<?= __('statement') ?>" style="border-radius: 8px;">
<a href="customer_statement.php?id=<?= $c['id'] ?>" class="btn btn-sm btn-light border-0 p-2 text-info rounded-3" title="<?= __('statement') ?>">
<i class="bi bi-file-earmark-text-fill"></i>
</a>
<?php if (has_permission('edit')):
?><button class="btn btn-sm btn-light border-0 p-2 text-primary" style="border-radius: 8px;" onclick="openCustomerModal(<?= htmlspecialchars(json_encode($c)) ?>)">
<?php if (has_permission('edit')): ?>
<button class="btn btn-sm btn-light border-0 p-2 text-primary rounded-3" onclick="openCustomerModal(<?= htmlspecialchars(json_encode($c)) ?>)">
<i class="bi bi-pencil-fill"></i>
</button>
<?php endif; ?>
<?php if (has_permission('delete')):
?><button class="btn btn-sm btn-light border-0 p-2 text-danger" style="border-radius: 8px;" onclick="confirmDelete('customer', <?= $c['id'] ?>)">
<?php if (has_permission('delete')): ?>
<button class="btn btn-sm btn-light border-0 p-2 text-danger rounded-3" onclick="confirmDelete('customer', <?= $c['id'] ?>)">
<i class="bi bi-trash-fill"></i>
</button>
<?php endif; ?>
@ -174,13 +188,8 @@ $customers = $stmt->fetchAll();
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($customers)):
?><tr >
<td colspan="6" class="text-center py-5 text-muted">
<i class="bi bi-people fs-1 d-block mb-3 opacity-25"></i>
<?= __('no_customers_found') ?? 'No customers found' ?>
</td>
</tr>
<?php if (empty($customers)): ?>
<tr><td colspan="7" class="text-center py-5 text-muted"><i class="bi bi-people fs-1 d-block mb-3 opacity-25"></i><?= __('no_customers_found') ?? 'No customers found' ?></td></tr>
<?php endif; ?>
</tbody>
</table>
@ -190,66 +199,94 @@ $customers = $stmt->fetchAll();
<!-- Customer Modal (Add/Edit) -->
<div class="modal fade" id="customerModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow-lg" style="border-radius: 20px;">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold" id="customerModalLabel"><?= __('add_new_customer') ?? 'Add New Customer' ?></h5>
<div class="modal-content border-0 shadow-lg rounded-4">
<div class="modal-header border-0 pb-0 p-4">
<h5 class="modal-title fw-bold" id="customerModalLabel"><?= __('add_new_customer') ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-4">
<form id="customerForm" method="POST">
<input type="hidden" name="action" id="customerAction" value="add_customer">
<input type="hidden" name="id" id="customerId">
<div class="mb-3">
<label class="form-label small fw-bold"><?= __('phone') ?></label>
<input type="text" name="phone" id="customerPhone" class="form-control" required style="border-radius: 12px;">
<input type="text" name="phone" id="customerPhone" class="form-control rounded-3" required>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label small fw-bold"><?= __('name_en') ?? 'Name (English)' ?></label>
<div class="input-group">
<input type="text" name="name_en" id="customerNameEn" class="form-control" required style="border-top-left-radius: 12px; border-bottom-left-radius: 12px;">
<button type="button" class="btn btn-outline-secondary" style="border-top-right-radius: 12px; border-bottom-right-radius: 12px;" onclick="translateField('customerNameEn', 'customerNameAr', 'en-ar')">
<i class="bi bi-translate"></i>
</button>
<input type="text" name="name_en" id="customerNameEn" class="form-control rounded-start-3" required>
<button type="button" class="btn btn-outline-secondary rounded-end-3" onclick="translateField('customerNameEn', 'customerNameAr', 'en-ar')"><i class="bi bi-translate"></i></button>
</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label small fw-bold"><?= __('name_ar') ?? 'Name (Arabic)' ?></label>
<div class="input-group">
<input type="text" name="name_ar" id="customerNameAr" class="form-control" style="border-top-left-radius: 12px; border-bottom-left-radius: 12px;">
<button type="button" class="btn btn-outline-secondary" style="border-top-right-radius: 12px; border-bottom-right-radius: 12px;" onclick="translateField('customerNameAr', 'customerNameEn', 'ar-en')">
<i class="bi bi-translate"></i>
</button>
<input type="text" name="name_ar" id="customerNameAr" class="form-control rounded-start-3">
<button type="button" class="btn btn-outline-secondary rounded-end-3" onclick="translateField('customerNameAr', 'customerNameEn', 'ar-en')"><i class="bi bi-translate"></i></button>
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label small fw-bold"><?= __('email') ?></label>
<input type="email" name="email" id="customerEmail" class="form-control" style="border-radius: 12px;">
<input type="email" name="email" id="customerEmail" class="form-control rounded-3">
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label small fw-bold"><?= __('address_en') ?? 'Address (English)' ?></label>
<textarea name="address_en" id="customerAddressEn" class="form-control" style="border-radius: 12px;" rows="2"></textarea>
<textarea name="address_en" id="customerAddressEn" class="form-control rounded-3" rows="2"></textarea>
</div>
<div class="col-md-6 mb-3">
<label class="form-label small fw-bold"><?= __('address_ar') ?? 'Address (Arabic)' ?></label>
<textarea name="address_ar" id="customerAddressAr" class="form-control" style="border-radius: 12px;" rows="2"></textarea>
<textarea name="address_ar" id="customerAddressAr" class="form-control rounded-3" rows="2"></textarea>
</div>
</div>
<button type="submit" class="btn btn-primary w-100 py-3 mt-2 fw-bold shadow-sm" style="border-radius: 15px;"><?= __('save') ?></button>
<button type="submit" class="btn btn-primary w-100 py-3 mt-2 fw-bold shadow-sm rounded-4"><?= __('save') ?></button>
</form>
</div>
</div>
</div>
</div>
<!-- Loyalty Adjustment Modal -->
<div class="modal fade" id="loyaltyModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow-lg rounded-4">
<div class="modal-header border-0 pb-0 p-4">
<h5 class="modal-title fw-bold"><?= __('loyalty_points') ?? 'Loyalty Points' ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-4 text-center">
<h6 id="loyaltyCustomerName" class="fw-bold mb-1"></h6>
<div class="display-6 fw-bold text-primary mb-4" id="loyaltyCurrentPoints">0.00</div>
<form method="POST">
<input type="hidden" name="action" value="adjust_points">
<input type="hidden" name="id" id="loyaltyCustomerId">
<div class="row g-2 mb-3">
<div class="col-6">
<input type="radio" class="btn-check" name="type" id="typeAdd" value="add" checked>
<label class="btn btn-outline-success w-100 py-2 rounded-3" for="typeAdd"><?= __('add') ?></label>
</div>
<div class="col-6">
<input type="radio" class="btn-check" name="type" id="typeSubtract" value="subtract">
<label class="btn btn-outline-danger w-100 py-2 rounded-3" for="typeSubtract"><?= __('subtract') ?></label>
</div>
</div>
<div class="mb-3">
<input type="number" name="points" class="form-control form-control-lg text-center rounded-3" placeholder="0.00" step="0.01" required>
</div>
<div class="mb-4">
<input type="text" name="description" class="form-control rounded-3" placeholder="<?= __('description') ?>">
</div>
<button type="submit" class="btn btn-primary w-100 py-3 fw-bold rounded-4 shadow-sm"><?= __('confirm') ?></button>
</form>
</div>
</div>
</div>
</div>
<!-- Delete Confirmation Form -->
<form id="deleteForm" method="POST" style="display: none;">
<input type="hidden" name="action" id="deleteAction">
<input type="hidden" name="id" id="deleteId">
@ -263,15 +300,11 @@ async function translateField(sourceId, targetId, direction) {
if (!text) return;
const btn = event.currentTarget;
const originalHtml = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = "<span class=\"spinner-border spinner-border-sm\"></span>";
btn.disabled = true; btn.innerHTML = "<span class=\"spinner-border spinner-border-sm\"></span>";
try {
const formData = new FormData();
formData.append("text", text);
formData.append("direction", direction);
const formData = new FormData(); formData.append("text", text); formData.append("direction", direction);
const resp = await fetch("api/translate.php", { method: "POST", body: formData });
const data = await resp.json();
if (data.success) targetEl.value = data.translation;
const data = await resp.json(); if (data.success) targetEl.value = data.translation;
} catch (e) { console.error(e); }
finally { btn.disabled = false; btn.innerHTML = originalHtml; }
}
@ -280,12 +313,10 @@ function openCustomerModal(customer = null) {
const modal = new bootstrap.Modal(document.getElementById('customerModal'));
const form = document.getElementById('customerForm');
const label = document.getElementById('customerModalLabel');
const actionInput = document.getElementById('customerAction');
const idInput = document.getElementById('customerId');
if (customer) {
label.innerText = "<?= __('edit') ?> <?= __('customer') ?>";
actionInput.value = 'edit_customer';
document.getElementById('customerAction').value = 'edit_customer';
idInput.value = customer.id;
document.getElementById('customerPhone').value = customer.phone;
document.getElementById('customerNameEn').value = customer.name_en;
@ -295,11 +326,17 @@ function openCustomerModal(customer = null) {
document.getElementById('customerAddressAr').value = customer.address_ar || '';
} else {
label.innerText = "<?= __('add_new_customer') ?>";
actionInput.value = 'add_customer';
form.reset();
idInput.value = '';
document.getElementById('customerAction').value = 'add_customer';
form.reset(); idInput.value = '';
}
modal.show();
}
function openLoyaltyModal(id, name, points) {
const modal = new bootstrap.Modal(document.getElementById('loyaltyModal'));
document.getElementById('loyaltyCustomerId').value = id;
document.getElementById('loyaltyCustomerName').innerText = name;
document.getElementById('loyaltyCurrentPoints').innerText = parseFloat(points).toFixed(2);
modal.show();
}
@ -312,4 +349,5 @@ function confirmDelete(type, id) {
}
</script>
<?php require_once __DIR__ . '/includes/footer.php'; ?>
<style>.pointer { cursor: pointer; }</style>
<?php require_once __DIR__ . '/includes/footer.php'; ?>

View File

@ -0,0 +1,22 @@
-- Add loyalty points to customers table
ALTER TABLE customers ADD COLUMN IF NOT EXISTS loyalty_points DECIMAL(10, 2) DEFAULT 0.00;
-- Create loyalty transactions table
CREATE TABLE IF NOT EXISTS loyalty_transactions (
id INT AUTO_INCREMENT PRIMARY KEY,
customer_id INT NOT NULL,
order_id INT NULL,
points DECIMAL(10, 2) NOT NULL,
type ENUM('earned', 'redeemed', 'adjusted') NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX (customer_id),
FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE CASCADE,
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Initial settings for loyalty system
INSERT IGNORE INTO settings (setting_key, setting_value) VALUES
('loyalty_enabled', '1'),
('loyalty_points_per_currency', '1'), -- 1 point per 1 unit spent
('loyalty_currency_per_point', '0.05'); -- 1 point = 0.05 unit discount (20 points = 1 unit)

View File

@ -0,0 +1,2 @@
-- Add loyalty_discount column to orders table
ALTER TABLE orders ADD COLUMN IF NOT EXISTS loyalty_discount DECIMAL(12, 3) DEFAULT 0.000;

View File

@ -18,7 +18,14 @@ $translations = [
'branches' => 'Branches',
'users' => 'Users',
'settings' => 'Settings',
'whatsapp_settings' => 'WhatsApp Settings', 'whatsapp_enabled' => 'WhatsApp Enabled', 'wablas_token' => 'Wablas API Token', 'wablas_server' => 'Wablas Server URL', 'wablas_security_key' => 'Wablas Security Key', 'msg_order_created_ar' => 'Order Created Message (Arabic)', 'msg_order_ready_ar' => 'Order Ready Message (Arabic)', 'msg_payment_ar' => 'Payment Received Message (Arabic)',
'whatsapp_settings' => 'WhatsApp Settings',
'whatsapp_enabled' => 'WhatsApp Enabled',
'wablas_token' => 'Wablas API Token',
'wablas_server' => 'Wablas Server URL',
'wablas_security_key' => 'Wablas Security Key',
'msg_order_created_ar' => 'Order Created Message (Arabic)',
'msg_order_ready_ar' => 'Order Ready Message (Arabic)',
'msg_payment_ar' => 'Payment Received Message (Arabic)',
'messages_enabled' => 'Messages Enabled',
'messages_disabled' => 'Messages Disabled',
'logout' => 'Logout',
@ -213,6 +220,15 @@ $translations = [
'customer_updated' => 'Customer updated successfully',
'customer_deleted' => 'Customer deleted successfully',
'customer_has_orders' => 'Cannot delete customer because they have existing orders',
'loyalty_settings' => 'Loyalty System Settings',
'loyalty_enabled' => 'Loyalty System Enabled',
'loyalty_points_per_currency' => 'Points earned per 1 unit spent',
'loyalty_currency_per_point' => 'Value of 1 point (discount amount)',
'loyalty_points' => 'Loyalty Points',
'points_adjusted' => 'Points adjusted successfully',
'loyalty' => 'Loyalty Points',
'loyalty_discount' => 'Loyalty Discount',
'final_total' => 'Final Total',
],
'ar' => [
'dashboard' => 'لوحة القيادة',
@ -224,7 +240,14 @@ $translations = [
'branches' => 'الفروع',
'users' => 'المستخدمين',
'settings' => 'الإعدادات',
'whatsapp_settings' => 'إعدادات الواتساب', 'whatsapp_enabled' => 'تفعيل الواتساب', 'wablas_token' => 'رمز API Wablas', 'wablas_server' => 'رابط خادم Wablas', 'wablas_security_key' => 'مفتاح الأمان Wablas', 'msg_order_created_ar' => 'رسالة إنشاء الطلب (بالعربية)', 'msg_order_ready_ar' => 'رسالة الطلب جاهز (بالعربية)', 'msg_payment_ar' => 'رسالة استلام الدفعة (بالعربية)',
'whatsapp_settings' => 'إعدادات الواتساب',
'whatsapp_enabled' => 'تفعيل الواتساب',
'wablas_token' => 'رمز API Wablas',
'wablas_server' => 'رابط خادم Wablas',
'wablas_security_key' => 'مفتاح الأمان Wablas',
'msg_order_created_ar' => 'رسالة إنشاء الطلب (بالعربية)',
'msg_order_ready_ar' => 'رسالة الطلب جاهز (بالعربية)',
'msg_payment_ar' => 'رسالة استلام الدفعة (بالعربية)',
'messages_enabled' => 'الرسائل مفعلة',
'messages_disabled' => 'الرسائل معطلة',
'logout' => 'تسجيل الخروج',
@ -419,6 +442,15 @@ $translations = [
'customer_updated' => 'تم تحديث العميل بنجاح',
'customer_deleted' => 'تم حذف العميل بنجاح',
'customer_has_orders' => 'لا يمكن حذف العميل لوجود طلبات مرتبطة به',
'loyalty_settings' => 'إعدادات نظام الولاء',
'loyalty_enabled' => 'تفعيل نظام الولاء',
'loyalty_points_per_currency' => 'النقاط المكتسبة لكل وحدة عملة يتم إنفاقها',
'loyalty_currency_per_point' => 'قيمة النقطة الواحدة (مبلغ الخصم)',
'loyalty_points' => 'نقاط الولاء',
'points_adjusted' => 'تم تعديل النقاط بنجاح',
'loyalty' => 'نقاط الولاء',
'loyalty_discount' => 'خصم الولاء',
'final_total' => 'الإجمالي النهائي',
]
];
@ -459,4 +491,4 @@ function branch_url($new_branch_id) {
$params = $_GET;
$params['switch_branch'] = $new_branch_id;
return '?' . http_build_query($params);
}
}

View File

@ -56,14 +56,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
$stmt->execute([$order_id]);
$total_paid = $stmt->fetch()['total_paid'] ?? 0;
$payment_status = $total_paid >= $order['total_price'] ? 'paid' : ($total_paid > 0 ? 'partially_paid' : 'unpaid');
$payment_status = $total_paid >= ($order['total_price'] - $order['loyalty_discount']) ? 'paid' : ($total_paid > 0 ? 'partially_paid' : 'unpaid');
$stmt = db()->prepare("UPDATE orders SET payment_status = ? WHERE id = ?");
$stmt->execute([$payment_status, $order_id]);
// WhatsApp Notification for Payment Received
try {
if (get_setting('whatsapp_enabled') === '1' && !empty($order['customer_phone'])) {
$remaining_now = (float)$order['total_price'] - (float)$total_paid;
$remaining_now = ((float)$order['total_price'] - (float)$order['loyalty_discount']) - (float)$total_paid;
$template = get_setting('msg_payment_ar');
if (!empty($template)) {
$message = str_replace(
@ -151,9 +151,15 @@ $payments = $stmt->fetchAll();
<th colspan="5" class="text-end text-muted small"><?= __('vat_total') ?? 'VAT Total' ?></th>
<th class="text-end"><?= format_amount($order['vat_total']) ?></th>
</tr>
<?php if ($order['loyalty_discount'] > 0): ?>
<tr>
<th colspan="5" class="text-end text-success small"><?= __('loyalty_discount') ?? 'Loyalty Discount' ?></th>
<th class="text-end text-success fw-bold">-<?= format_amount($order['loyalty_discount']) ?></th>
</tr>
<?php endif; ?>
<tr>
<th colspan="5" class="text-end fs-5 fw-bold"><?= __('total') ?></th>
<th class="text-end fs-5 fw-bold text-primary"><?= format_amount($order['total_price']) ?></th>
<th class="text-end fs-5 fw-bold text-primary"><?= format_amount($order['total_price'] - $order['loyalty_discount']) ?></th>
</tr>
</tfoot>
</table>
@ -226,21 +232,21 @@ $payments = $stmt->fetchAll();
$stmt = db()->prepare("SELECT SUM(amount) as total_paid FROM payments WHERE order_id = ?");
$stmt->execute([$order_id]);
$total_paid = $stmt->fetch()['total_paid'] ?? 0;
$remaining = (float)$order['total_price'] - (float)$total_paid;
$remaining = ((float)$order['total_price'] - (float)$order['loyalty_discount']) - (float)$total_paid;
?>
<div class="d-flex justify-content-between align-items-center mb-3">
<span class="text-muted fw-bold small"><?= __('remaining_amount') ?></span>
<span class="fs-5 fw-bold <?= $remaining <= 0 ? 'text-success' : 'text-danger' ?>">
<span class="fs-5 fw-bold <?= $remaining <= 0.001 ? 'text-success' : 'text-danger' ?>">
<?= format_amount(max(0, $remaining)) ?>
</span>
</div>
<?php if ($remaining > 0): ?>
<?php if ($remaining > 0.001): ?>
<form method="POST">
<input type="hidden" name="action" value="add_payment">
<div class="mb-3">
<label class="form-label small fw-bold text-muted"><?= __('amount') ?></label>
<input type="number" step="0.001" name="amount" class="form-control bg-light border-0" value="<?= $remaining ?>" required style="border-radius: 12px;">
<input type="number" step="0.001" name="amount" class="form-control bg-light border-0" value="<?= round($remaining, 3) ?>" required style="border-radius: 12px;">
</div>
<div class="mb-3">
<label class="form-label small fw-bold text-muted"><?= __('payment_method') ?></label>
@ -288,4 +294,4 @@ function getPaymentStatusColor($status) {
][$status] ?? 'info';
}
require_once __DIR__ . '/includes/footer.php';
?>
?>

View File

@ -141,6 +141,8 @@ if ($current_role === 'super_admin') {
<?php endif; ?>
<th><?= __('customer') ?></th>
<th><?= __('total') ?></th>
<th><?= __('loyalty_discount') ?? 'Loyalty Discount' ?></th>
<th><?= __('final_total') ?? 'Final Total' ?></th>
<th><?= __('status') ?></th>
<th><?= __('payment_status') ?></th>
<th><?= __('date') ?></th>
@ -159,7 +161,9 @@ if ($current_role === 'super_admin') {
<div class="fw-bold"><?= $lang === 'ar' ? ($order['customer_name_ar'] ?: $order['customer_name_en']) : $order['customer_name_en'] ?></div>
<div class="small text-muted"><?= $order['customer_phone'] ?></div>
</td>
<td class="fw-bold text-primary"><?= format_amount($order['total_price']) ?></td>
<td class="small text-muted"><?= format_amount($order['total_price']) ?></td>
<td class="text-success small fw-bold"><?= $order['loyalty_discount'] > 0 ? '-' . format_amount($order['loyalty_discount']) : '-' ?></td>
<td class="fw-bold text-primary"><?= format_amount($order['total_price'] - $order['loyalty_discount']) ?></td>
<td><span class="badge bg-<?= getStatusColor($order['status']) ?> rounded-pill px-3 py-2"><?= __($order['status']) ?></span></td>
<td><span class="badge badge-soft-<?= getPaymentStatusColor($order['payment_status']) ?> rounded-pill px-3 py-2"><?= __($order['payment_status']) ?></span></td>
<td class="small"><?= date('d/m/Y H:i', strtotime($order['created_at'])) ?></td>
@ -187,7 +191,7 @@ if ($current_role === 'super_admin') {
<?php endforeach; ?>
<?php if (empty($orders)): ?>
<tr>
<td colspan="<?= $current_role === 'super_admin' ? 9 : 8 ?>" class="text-center py-5 text-muted">
<td colspan="<?= $current_role === 'super_admin' ? 11 : 10 ?>" class="text-center py-5 text-muted">
<i class="bi bi-inbox fs-1 d-block mb-3 opacity-25"></i>
<?= __('no_orders_found') ?? 'No orders found' ?>
</td>
@ -349,4 +353,4 @@ function getPaymentStatusColor($status) {
][$status] ?? 'info';
}
require_once __DIR__ . '/includes/footer.php';
?>
?>

204
pos.php
View File

@ -3,7 +3,6 @@ $title = 'pos';
require_once __DIR__ . '/includes/header.php';
// Global check in header.php handles 'view' permission.
// We should check 'edit' permission if we are editing an order.
$edit_order_id = $_GET['edit_order_id'] ?? null;
if ($edit_order_id && !has_permission('edit')) {
header('Location: pos.php?error=no_edit_permission');
@ -12,6 +11,11 @@ if ($edit_order_id && !has_permission('edit')) {
$branch_id = $_SESSION['branch_id'] ?? 1;
// Loyalty Settings
$loyalty_enabled = get_setting('loyalty_enabled', '0');
$loyalty_points_per_currency = get_setting('loyalty_points_per_currency', '1');
$loyalty_currency_per_point = get_setting('loyalty_currency_per_point', '0.05');
// Handle Edit Order
$edit_order = null;
$edit_items = [];
@ -81,7 +85,6 @@ foreach ($items_raw as $i) {
}
}
// Only include if has services
if (!empty($item_services)) {
$items[$i['id']] = [
'id' => $i['id'],
@ -96,7 +99,7 @@ foreach ($items_raw as $i) {
}
// Get all customers for this branch
$stmt = db()->prepare("SELECT * FROM customers ORDER BY name_en ASC");
$stmt = db()->prepare("SELECT id, name_en, name_ar, phone, loyalty_points FROM customers ORDER BY name_en ASC");
$stmt->execute();
$customers = $stmt->fetchAll();
@ -111,7 +114,7 @@ $pageTitle = $edit_order ? ($lang == 'en' ? 'Edit Order #' . $edit_order['order_
<h4 class="fw-bold mb-0"><?= $pageTitle ?></h4>
<div class="input-group w-50 shadow-sm rounded-4 overflow-hidden">
<span class="input-group-text bg-white border-0"><i class="bi bi-search"></i></span>
<input type="text" id="itemSearch" class="form-control border-0 py-2" placeholder="<?= $lang == 'en' ? 'Search items...' : 'بحث عن المنتجات...' ?>">
<input type="text" id="itemSearch" class="form-control border-0 py-2 shadow-none" placeholder="<?= $lang == 'en' ? 'Search items...' : 'بحث عن المنتجات...' ?>">
</div>
</div>
@ -171,7 +174,7 @@ $pageTitle = $edit_order ? ($lang == 'en' ? 'Edit Order #' . $edit_order['order_
</div>
<div class="px-3 pb-3">
<div class="d-flex gap-2">
<div class="d-flex gap-2 mb-2">
<!-- Persistent Customer Search -->
<div class="position-relative flex-grow-1" id="customerSearchWrapper">
<div class="input-group shadow-sm rounded-3 overflow-hidden border">
@ -183,7 +186,7 @@ $pageTitle = $edit_order ? ($lang == 'en' ? 'Edit Order #' . $edit_order['order_
</div>
<div class="position-absolute w-100 shadow-lg bg-white rounded-3 mt-1 d-none" id="customerResults" style="z-index: 1050; max-height: 300px; overflow-y: auto; border: 1px solid #eee;">
<div class="p-2 border-bottom">
<button class="btn btn-light btn-sm w-100 text-start rounded-2 customer-result-item" data-id="" data-name="<?= $lang == 'en' ? 'Walk-in Customer' : 'عميل عابر' ?>" data-search="walk-in">
<button class="btn btn-light btn-sm w-100 text-start rounded-2 customer-result-item" data-id="" data-name="<?= $lang == 'en' ? 'Walk-in Customer' : 'عميل عابر' ?>" data-search="walk-in" data-points="0">
<?= $lang == 'en' ? 'Walk-in Customer' : 'عميل عابر' ?>
</button>
</div>
@ -193,7 +196,7 @@ $pageTitle = $edit_order ? ($lang == 'en' ? 'Edit Order #' . $edit_order['order_
$searchStr = strtolower($c['name_en'] . ' ' . ($c['name_ar'] ?? '') . ' ' . $c['phone']);
?>
<div class="p-1">
<button class="btn btn-white btn-sm w-100 text-start rounded-2 customer-result-item p-2" type="button" data-id="<?= $c['id'] ?>" data-name="<?= $cname ?>" data-phone="<?= $c['phone'] ?>" data-search="<?= $searchStr ?>">
<button class="btn btn-white btn-sm w-100 text-start rounded-2 customer-result-item p-2" type="button" data-id="<?= $c['id'] ?>" data-name="<?= $cname ?>" data-phone="<?= $c['phone'] ?>" data-search="<?= $searchStr ?>" data-points="<?= $c['loyalty_points'] ?>">
<div class="fw-bold small text-dark"><?= $cname ?></div>
<div class="text-muted small" style="font-size: 0.7rem;"><?= $c['phone'] ?></div>
</button>
@ -210,6 +213,15 @@ $pageTitle = $edit_order ? ($lang == 'en' ? 'Edit Order #' . $edit_order['order_
</button>
<?php endif; ?>
</div>
<!-- Loyalty Info Display -->
<div id="loyaltyDisplay" class="d-none bg-primary bg-opacity-10 rounded-3 p-2 mb-2 d-flex align-items-center justify-content-between border border-primary border-opacity-25">
<div class="d-flex align-items-center">
<i class="bi bi-star-fill text-primary me-2"></i>
<span class="small fw-bold text-primary"><?= $lang == 'en' ? 'Loyalty Points' : 'نقاط الولاء' ?></span>
</div>
<span class="badge bg-primary rounded-pill" id="customerPoints">0</span>
</div>
</div>
<div class="card-body p-0 flex-grow-1 overflow-auto" id="cartContainer">
@ -230,11 +242,33 @@ $pageTitle = $edit_order ? ($lang == 'en' ? 'Edit Order #' . $edit_order['order_
<span><?= $lang == 'en' ? 'VAT' : 'الضريبة' ?></span>
<span id="cartVat">0.000 <?= currency() ?></span>
</div>
<!-- Loyalty Discount Display -->
<div id="loyaltyDiscountRow" class="d-none d-flex justify-content-between mb-2 small text-success fw-bold">
<span><?= $lang == 'en' ? 'Loyalty Discount' : 'خصم الولاء' ?></span>
<span id="cartLoyaltyDiscount">-0.000 <?= currency() ?></span>
</div>
<div class="d-flex justify-content-between pt-2 border-top border-secondary border-opacity-10 mt-2">
<h5 class="mb-0 fw-bold"><?= $lang == 'en' ? 'Total' : 'الإجمالي' ?></h5>
<h5 id="cartTotal" class="mb-0 fw-bold text-primary">0.000 <?= currency() ?></h5>
</div>
</div>
<!-- Loyalty Redemption UI -->
<?php if ($loyalty_enabled == '1'): ?>
<div id="loyaltyRedeemUI" class="d-none mb-3">
<div class="input-group shadow-sm rounded-3 overflow-hidden border">
<span class="input-group-text bg-white border-0 small"><i class="bi bi-star text-primary"></i></span>
<input type="number" id="pointsToRedeem" class="form-control border-0 py-2 shadow-none small" placeholder="<?= $lang == 'en' ? 'Redeem Points' : 'استبدال النقاط' ?>" min="0">
<button class="btn btn-primary btn-sm px-3" onclick="applyPoints()">
<?= $lang == 'en' ? 'Apply' : 'تطبيق' ?>
</button>
</div>
<div id="redeemHint" class="small text-muted mt-1" style="font-size: 0.7rem;"></div>
</div>
<?php endif; ?>
<?php if (($edit_order && has_permission('edit')) || (!$edit_order && has_permission('add'))): ?>
<button class="btn btn-primary w-100 py-3 rounded-4 fw-bold shadow-sm" onclick="checkout()">
<i class="bi bi-credit-card me-2"></i> <?= $edit_order ? ($lang == 'en' ? 'Update Order' : 'تحديث الطلب') : ($lang == 'en' ? 'Complete Order' : 'إتمام الطلب') ?>
@ -355,9 +389,15 @@ const lang = '<?= $lang ?>';
const currencyLabel = '<?= currency() ?>';
const decimalPrecision = <?= decimals() ?>;
const editOrderId = <?= $edit_order_id ?: 'null' ?>;
const loyaltyEnabled = <?= $loyalty_enabled ?>;
const pointsPerCurrency = <?= $loyalty_points_per_currency ?>;
const currencyPerPoint = <?= $loyalty_currency_per_point ?>;
let cart = <?= json_encode($edit_items) ?>;
let selectionModal;
let paymentModal;
let customerLoyaltyPoints = 0;
let pointsToRedeem = 0;
// If not editing, try to load from local storage
if (!editOrderId) {
@ -420,8 +460,7 @@ document.addEventListener('DOMContentLoaded', () => {
const initialCustId = "<?= $edit_order['customer_id'] ?>";
const initialCustBtn = document.querySelector(`.customer-result-item[data-id="${initialCustId}"]`);
if (initialCustBtn) {
custSearchInput.value = initialCustBtn.getAttribute('data-name');
clearCustBtn.classList.remove('d-none');
selectCustomer(initialCustId, initialCustBtn.getAttribute('data-name'), initialCustBtn.getAttribute('data-points'));
}
<?php endif; ?>
@ -443,7 +482,6 @@ document.addEventListener('DOMContentLoaded', () => {
});
});
// Close results when clicking outside
document.addEventListener('click', function(e) {
if (!document.getElementById('customerSearchWrapper').contains(e.target)) {
custResults.classList.add('d-none');
@ -453,30 +491,17 @@ document.addEventListener('DOMContentLoaded', () => {
if (clearCustBtn) {
clearCustBtn.addEventListener('click', () => {
custIdInput.value = '';
custSearchInput.value = '';
custSearchInput.placeholder = lang === 'en' ? 'Walk-in Customer' : 'عميل عابر';
clearCustBtn.classList.add('d-none');
resetCustomerSelection();
});
}
// Customer selection delegation
custResults.addEventListener('click', function(e) {
const btn = e.target.closest('.customer-result-item');
if (btn) {
const id = btn.getAttribute('data-id');
const name = btn.getAttribute('data-name');
custIdInput.value = id;
if (id) {
custSearchInput.value = name;
clearCustBtn.classList.remove('d-none');
} else {
custSearchInput.value = '';
custSearchInput.placeholder = name;
clearCustBtn.classList.add('d-none');
}
const points = btn.getAttribute('data-points') || 0;
selectCustomer(id, name, points);
custResults.classList.add('d-none');
}
});
@ -484,6 +509,68 @@ document.addEventListener('DOMContentLoaded', () => {
updateCart();
});
function selectCustomer(id, name, points) {
const custIdInput = document.getElementById('customerId');
const custSearchInput = document.getElementById('customerSearchInput');
const clearCustBtn = document.getElementById('clearCustomerBtn');
const loyaltyDisplay = document.getElementById('loyaltyDisplay');
const loyaltyRedeemUI = document.getElementById('loyaltyRedeemUI');
const customerPointsEl = document.getElementById('customerPoints');
const redeemHint = document.getElementById('redeemHint');
custIdInput.value = id;
customerLoyaltyPoints = parseFloat(points);
if (id) {
custSearchInput.value = name;
clearCustBtn.classList.remove('d-none');
if (loyaltyEnabled) {
loyaltyDisplay.classList.remove('d-none');
loyaltyRedeemUI.classList.remove('d-none');
customerPointsEl.innerText = customerLoyaltyPoints.toFixed(2);
if (redeemHint) {
redeemHint.innerText = lang === 'en'
? `1 Point = ${currencyPerPoint} ${currencyLabel}`
: `1 نقطة = ${currencyPerPoint} ${currencyLabel}`;
}
}
} else {
resetCustomerSelection();
}
}
function resetCustomerSelection() {
const custIdInput = document.getElementById('customerId');
const custSearchInput = document.getElementById('customerSearchInput');
const clearCustBtn = document.getElementById('clearCustomerBtn');
const loyaltyDisplay = document.getElementById('loyaltyDisplay');
const loyaltyRedeemUI = document.getElementById('loyaltyRedeemUI');
custIdInput.value = '';
custSearchInput.value = '';
custSearchInput.placeholder = lang === 'en' ? 'Walk-in Customer' : 'عميل عابر';
clearCustBtn.classList.add('d-none');
loyaltyDisplay.classList.add('d-none');
loyaltyRedeemUI.classList.add('d-none');
customerLoyaltyPoints = 0;
pointsToRedeem = 0;
const ptsInput = document.getElementById('pointsToRedeem');
if (ptsInput) ptsInput.value = '';
updateCart();
}
function applyPoints() {
const ptsInput = document.getElementById('pointsToRedeem');
let val = parseFloat(ptsInput.value) || 0;
if (val > customerLoyaltyPoints) {
alert(lang === 'en' ? 'Insufficient points' : 'نقاط غير كافية');
val = customerLoyaltyPoints;
ptsInput.value = val;
}
pointsToRedeem = val;
updateCart();
}
function showOptions(itemId) {
const item = itemsData[itemId];
if (!item) return;
@ -504,7 +591,6 @@ function showOptions(itemId) {
list.appendChild(col);
});
}
if (selectionModal) selectionModal.show();
}
@ -579,15 +665,37 @@ function updateCart() {
const subtotalEl = document.getElementById('cartSubtotal');
const vatEl = document.getElementById('cartVat');
const totalEl = document.getElementById('cartTotal');
const discountRow = document.getElementById('loyaltyDiscountRow');
const discountEl = document.getElementById('cartLoyaltyDiscount');
let totalBeforeDiscount = subtotal + totalVat;
let loyaltyDiscount = pointsToRedeem * currencyPerPoint;
if (loyaltyDiscount > totalBeforeDiscount) {
loyaltyDiscount = totalBeforeDiscount;
pointsToRedeem = loyaltyDiscount / currencyPerPoint;
}
if (loyaltyDiscount > 0) {
discountRow.classList.remove('d-none');
discountEl.innerText = '-' + loyaltyDiscount.toFixed(decimalPrecision) + ' ' + currencyLabel;
} else {
discountRow.classList.add('d-none');
}
const finalTotal = totalBeforeDiscount - loyaltyDiscount;
if (subtotalEl) subtotalEl.innerText = subtotal.toFixed(decimalPrecision) + ' ' + currencyLabel;
if (vatEl) vatEl.innerText = totalVat.toFixed(decimalPrecision) + ' ' + currencyLabel;
if (totalEl) totalEl.innerText = (subtotal + totalVat).toFixed(decimalPrecision) + ' ' + currencyLabel;
if (totalEl) totalEl.innerText = finalTotal.toFixed(decimalPrecision) + ' ' + currencyLabel;
}
function clearCart() {
if (confirm(lang === 'en' ? 'Clear cart?' : 'مسح السلة؟')) {
cart = [];
pointsToRedeem = 0;
const ptsInput = document.getElementById('pointsToRedeem');
if (ptsInput) ptsInput.value = '';
updateCart();
}
}
@ -595,13 +703,18 @@ function clearCart() {
function checkout() {
if (cart.length === 0) return;
let total = 0;
let subtotal = 0;
let totalVat = 0;
cart.forEach(item => {
const itemSubtotal = item.price * item.qty;
total += itemSubtotal + (itemSubtotal * ((item.vat_percent || 15) / 100));
subtotal += itemSubtotal;
totalVat += itemSubtotal * ((item.vat_percent || 15) / 100);
});
document.getElementById('paymentTotalAmount').innerText = total.toFixed(decimalPrecision) + ' ' + currencyLabel;
let finalTotal = (subtotal + totalVat) - (pointsToRedeem * currencyPerPoint);
if (finalTotal < 0) finalTotal = 0;
document.getElementById('paymentTotalAmount').innerText = finalTotal.toFixed(decimalPrecision) + ' ' + currencyLabel;
if (paymentModal) paymentModal.show();
}
@ -610,14 +723,11 @@ async function completeCheckout(paymentMethod) {
let subtotal = 0;
let totalVat = 0;
const itemsToSubmit = cart.map(item => {
const itemSubtotal = item.price * item.qty;
const itemVat = itemSubtotal * ((item.vat_percent || 15) / 100);
subtotal += itemSubtotal;
totalVat += itemVat;
return {
itemId: item.item_id,
serviceId: item.service_id,
@ -640,28 +750,21 @@ async function completeCheckout(paymentMethod) {
items: itemsToSubmit,
vat_total: totalVat,
total_price: totalPrice,
payment_method: paymentMethod
payment_method: paymentMethod,
points_to_redeem: pointsToRedeem
})
});
const res = await response.json();
if (res.success) {
const orderId = res.order_id || editOrderId;
// Direct Print
printReceipt(orderId);
if (!editOrderId) {
cart = [];
updateCart();
localStorage.removeItem('pos_cart');
}
if (paymentModal) paymentModal.hide();
// Wait a bit for print dialog to start then redirect
setTimeout(() => {
window.location.href = 'pos.php';
}, 2000);
setTimeout(() => { window.location.href = 'pos.php'; }, 2000);
} else alert(res.error);
} catch (e) { alert('Error'); }
}
@ -669,7 +772,6 @@ async function completeCheckout(paymentMethod) {
function printReceipt(orderId) {
const iframe = document.getElementById('printFrame');
iframe.src = 'receipt.php?id=' + orderId;
iframe.onload = function() {
iframe.contentWindow.focus();
iframe.contentWindow.print();
@ -690,21 +792,15 @@ async function saveCustomer() {
const phone = res.customer.phone;
const displayName = lang === 'en' ? nameEn : nameAr;
// Update hidden input
document.getElementById('customerId').value = id;
// Update search input
const custSearchInput = document.getElementById('customerSearchInput');
custSearchInput.value = displayName;
document.getElementById('clearCustomerBtn').classList.remove('d-none');
selectCustomer(id, displayName, 0);
// Add to results list
const list = document.getElementById('customerResultsList');
if (list) {
const searchStr = `${nameEn} ${nameAr} ${phone}`.toLowerCase();
const div = document.createElement('div');
div.className = 'p-1';
div.innerHTML = `
<button class="btn btn-white btn-sm w-100 text-start rounded-2 customer-result-item p-2" type="button" data-id="${id}" data-name="${displayName}" data-phone="${phone}" data-search="${searchStr}">
<button class="btn btn-white btn-sm w-100 text-start rounded-2 customer-result-item p-2" type="button" data-id="${id}" data-name="${displayName}" data-phone="${phone}" data-search="${searchStr}" data-points="0">
<div class="fw-bold small text-dark">${displayName}</div>
<div class="text-muted small" style="font-size: 0.7rem;">${phone}</div>
</button>
@ -735,4 +831,4 @@ async function saveCustomer() {
.customer-result-item:hover { background-color: #f8f9fa; }
</style>
<?php require_once __DIR__ . '/includes/footer.php'; ?>
<?php require_once __DIR__ . '/includes/footer.php'; ?>

View File

@ -41,7 +41,7 @@ $order_items = $stmt->fetchAll();
$stmt = db()->prepare("SELECT SUM(amount) as total_paid FROM payments WHERE order_id = ?");
$stmt->execute([$order_id]);
$total_paid = $stmt->fetch()['total_paid'] ?? 0;
$remaining = $order['total_price'] - $total_paid;
$remaining = ($order['total_price'] - $order['loyalty_discount']) - $total_paid;
// Bilingual Helper for Labels
function b_label($key) {
@ -301,9 +301,17 @@ $display_ctr = $company['ctr_no'];
<div class="info-label"><?= b_label('vat') ?></div>
<div class="info-value"><?= format_amount($order['vat_total']) ?></div>
</div>
<?php if ($order['loyalty_discount'] > 0): ?>
<div class="total-row text-success fw-bold">
<div class="info-label"><?= b_label('loyalty_discount') ?></div>
<div class="info-value">-<?= format_amount($order['loyalty_discount']) ?></div>
</div>
<?php endif; ?>
<div class="total-row fw-bold grand-total">
<div class="info-label"><?= b_label('total') ?></div>
<div class="info-value" style="font-size: 16px;"><?= format_amount($order['total_price']) ?></div>
<div class="info-value" style="font-size: 16px;"><?= format_amount($order['total_price'] - $order['loyalty_discount']) ?></div>
</div>
<div class="total-row mt-2">
@ -334,4 +342,4 @@ $display_ctr = $company['ctr_no'];
}
</script>
</body>
</html>
</html>