diff --git a/db/migrations/20260218_modern_loyalty_system.sql b/db/migrations/20260218_modern_loyalty_system.sql new file mode 100644 index 0000000..739e180 --- /dev/null +++ b/db/migrations/20260218_modern_loyalty_system.sql @@ -0,0 +1,31 @@ +-- Modern Loyalty System Migration +ALTER TABLE customers +ADD COLUMN IF NOT EXISTS loyalty_tier ENUM('bronze', 'silver', 'gold') DEFAULT 'bronze', +ADD COLUMN IF NOT EXISTS total_spent DECIMAL(15, 3) DEFAULT 0.000; + +CREATE TABLE IF NOT EXISTS loyalty_transactions ( + id INT AUTO_INCREMENT PRIMARY KEY, + customer_id INT NOT NULL, + transaction_id INT NULL, + points_change DECIMAL(15, 3) NOT NULL, + transaction_type ENUM('earned', 'redeemed', 'adjustment') NOT NULL, + description TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE CASCADE +); + +-- Update existing total_spent based on previous transactions if possible +UPDATE customers c +SET c.total_spent = ( + SELECT COALESCE(SUM(total_with_vat), 0) + FROM invoices + WHERE customer_id = c.id AND type = 'sale' +); + +-- Initial tier update based on existing spent amount +UPDATE customers +SET loyalty_tier = CASE + WHEN total_spent >= 1500 THEN 'gold' + WHEN total_spent >= 500 THEN 'silver' + ELSE 'bronze' +END; diff --git a/index.php b/index.php index 33f58c6..9e11d48 100644 --- a/index.php +++ b/index.php @@ -39,6 +39,58 @@ function getPromotionalPrice($item) { return $price; } +function getLoyaltyMultiplier($tier) { + return match($tier) { + 'silver' => 1.2, + 'gold' => 1.5, + default => 1.0, + }; +} + +function updateCustomerLoyalty($customer_id, $spent_amount, $points_earned, $loyalty_redeemed_value, $invoice_id = null) { + $db = db(); + + // Fetch settings for dynamic rates + $settings_res = $db->query("SELECT * FROM settings WHERE `key` IN ('loyalty_enabled', 'loyalty_redeem_points_per_unit')")->fetchAll(PDO::FETCH_ASSOC); + $settings = []; + foreach ($settings_res as $s) $settings[$s['key']] = $s['value']; + + if (($settings['loyalty_enabled'] ?? '0') !== '1') return; // System disabled + + $redeem_rate = (float)($settings['loyalty_redeem_points_per_unit'] ?? 100); + $points_redeemed = (float)$loyalty_redeemed_value * $redeem_rate; + + // Update points and total_spent + $stmt = $db->prepare("UPDATE customers SET loyalty_points = loyalty_points - ? + ?, total_spent = total_spent + ? WHERE id = ?"); + $stmt->execute([(float)$points_redeemed, (float)$points_earned, (float)$spent_amount, $customer_id]); + + // Fetch updated total_spent to check for tier upgrade + $stmt = $db->prepare("SELECT total_spent, loyalty_tier FROM customers WHERE id = ?"); + $stmt->execute([$customer_id]); + $customer = $stmt->fetch(); + + $new_tier = 'bronze'; + if ($customer['total_spent'] >= 1500) $new_tier = 'gold'; + elseif ($customer['total_spent'] >= 500) $new_tier = 'silver'; + + if ($new_tier !== $customer['loyalty_tier']) { + $stmt = $db->prepare("UPDATE customers SET loyalty_tier = ? WHERE id = ?"); + $stmt->execute([$new_tier, $customer_id]); + } + + // Log Earned Points + if ($points_earned > 0) { + $stmt = $db->prepare("INSERT INTO loyalty_transactions (customer_id, transaction_id, points_change, transaction_type, description) VALUES (?, ?, ?, 'earned', ?)"); + $stmt->execute([$customer_id, $invoice_id, $points_earned, "Earned from transaction #$invoice_id (Tier: " . strtoupper($customer['loyalty_tier']) . ")"]); + } + + // Log Redeemed Points + if ($points_redeemed > 0) { + $stmt = $db->prepare("INSERT INTO loyalty_transactions (customer_id, transaction_id, points_change, transaction_type, description) VALUES (?, ?, ?, 'redeemed', ?)"); + $stmt->execute([$customer_id, $invoice_id, -$points_redeemed, "Redeemed $points_redeemed pts (Value: " . number_format((float)$loyalty_redeemed_value, 3) . " OMR) in transaction #$invoice_id"]); + } +} + function numberToWords($num) { $num = (int)$num; if ($num === 0) return "Zero"; @@ -275,6 +327,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $loyalty_redeemed = (float)($_POST['loyalty_redeemed'] ?? 0); $items = json_decode($_POST['items'] ?? '[]', true); + // Fetch settings + $settings_res = $db->query("SELECT * FROM settings WHERE `key` IN ('loyalty_enabled', 'loyalty_points_per_unit')")->fetchAll(PDO::FETCH_ASSOC); + $app_settings = []; + foreach ($settings_res as $s) $app_settings[$s['key']] = $s['value']; + $loyalty_enabled = ($app_settings['loyalty_enabled'] ?? '0') === '1'; + $points_per_unit = (float)($app_settings['loyalty_points_per_unit'] ?? 1); + if (empty($items)) { throw new Exception("Cart is empty"); } @@ -282,8 +341,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $net_amount = (float)($total_amount - $discount_amount - $loyalty_redeemed); if ($net_amount < 0) $net_amount = 0; - // Loyalty Calculation: 1 point per 1 OMR spent on net amount - $loyalty_earned = floor($net_amount); + // Loyalty Calculation: Based on Tier Multiplier + $loyalty_multiplier = 1.0; + if ($customer_id && $loyalty_enabled) { + $stmtTier = $db->prepare("SELECT loyalty_tier FROM customers WHERE id = ?"); + $stmtTier->execute([$customer_id]); + $tier = $stmtTier->fetchColumn() ?: 'bronze'; + $loyalty_multiplier = getLoyaltyMultiplier($tier); + } + $loyalty_earned = $loyalty_enabled ? floor($net_amount * $points_per_unit * $loyalty_multiplier) : 0; // Check if credit is used for walk-in or exceeds limit $credit_total = 0; @@ -371,8 +437,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } } - $stmt = $db->prepare("UPDATE customers SET loyalty_points = loyalty_points - ? + ?, balance = balance - ? WHERE id = ?"); - $stmt->execute([(float)$loyalty_redeemed, (float)$loyalty_earned, (float)$credit_total, $customer_id]); + // New Modern Loyalty Logic + updateCustomerLoyalty($customer_id, (float)$net_amount, (float)$loyalty_earned, (float)$loyalty_redeemed, (int)$invoice_id); + + // Update Balance separately if credit used + if ($credit_total > 0) { + $stmt = $db->prepare("UPDATE customers SET balance = balance - ? WHERE id = ?"); + $stmt->execute([(float)$credit_total, $customer_id]); + } } // Add Payments @@ -2129,6 +2201,26 @@ switch ($page) { $data['payroll'] = db()->query("SELECT p.*, e.name as emp_name FROM hr_payroll p JOIN hr_employees e ON p.employee_id = e.id WHERE p.payroll_month = $month AND p.payroll_year = $year ORDER BY p.id DESC")->fetchAll(); $data['employees'] = db()->query("SELECT id, name, salary FROM hr_employees WHERE status = 'active' ORDER BY name ASC")->fetchAll(); break; + case 'loyalty_history': + $where = ["1=1"]; + $params = []; + if (!empty($_GET['customer_id'])) { + $where[] = "lt.customer_id = ?"; + $params[] = (int)$_GET['customer_id']; + } + if (!empty($_GET['type'])) { + $where[] = "lt.transaction_type = ?"; + $params[] = $_GET['type']; + } + $whereSql = implode(" AND ", $where); + $stmt = db()->prepare("SELECT lt.*, c.name as customer_name, c.loyalty_tier, c.loyalty_points + FROM loyalty_transactions lt + JOIN customers c ON lt.customer_id = c.id + WHERE $whereSql + ORDER BY lt.created_at DESC"); + $stmt->execute($params); + $data['loyalty_transactions'] = $stmt->fetchAll(); + break; case 'devices': $data['devices'] = db()->query("SELECT * FROM hr_biometric_devices ORDER BY id DESC")->fetchAll(); break; @@ -2312,6 +2404,9 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; Low Stock Report + + Loyalty History + @@ -2381,6 +2476,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; 'hr_attendance' => ['en' => 'HR Attendance', 'ar' => 'حضور الموارد البشرية'], 'hr_payroll' => ['en' => 'HR Payroll', 'ar' => 'رواتب الموارد البشرية'], 'cashflow_report' => ['en' => 'Cashflow Statement', 'ar' => 'قائمة التدفقات النقدية'], + 'loyalty_history' => ['en' => 'Loyalty History', 'ar' => 'سجل الولاء'], ]; $currTitle = $titles[$page] ?? $titles['dashboard']; ?> @@ -3200,6 +3296,90 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; + +
| Date | +Customer | +Tier | +Type | +Points | +Description | +
|---|---|---|---|---|---|
| No transactions found. | |||||
| = date('Y-m-d H:i', strtotime($lt['created_at'])) ?> | +
+ = htmlspecialchars($lt['customer_name']) ?>
+ Current Balance: = number_format($lt['loyalty_points'], 0) ?> pts
+ |
+ + + = $tier ?> + | ++ + = ucfirst($type) ?> + | ++ = (float)$lt['points_change'] > 0 ? '+' : '' ?>= number_format($lt['points_change'], 0) ?> + | += htmlspecialchars($lt['description']) ?> | +