From be707683bacc3591797f2e745a8287efaf071290 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Tue, 21 Apr 2026 02:42:34 +0000 Subject: [PATCH] update whatsapp and payments --- api/customers.php | 7 +- api/place_order.php | 57 ++- api/sales_payment.php | 46 +++ api/settings.php | 17 +- api/wablas_test.php | 59 +++ cookies_wablas_test.txt | 5 + customers.php | 33 +- .../2026-04-21_existing_install_patch.sql | 133 ++++++ db/schema.sql | 12 + debts.php | 114 ++++-- edit_online_order.php | 28 +- edit_sale.php | 92 ++++- includes/app.php | 382 +++++++++++++++++- includes/footer_settings.php | 281 +++++++++---- includes/sale_form.php | 108 ++++- online_orders.php | 28 +- pos.php | 104 ++++- print_receipt.php | 15 +- reports.php | 4 +- sale.php | 13 +- sales.php | 108 +++-- shop.php | 6 +- 22 files changed, 1424 insertions(+), 228 deletions(-) create mode 100644 api/sales_payment.php create mode 100644 api/wablas_test.php create mode 100644 cookies_wablas_test.txt create mode 100644 db/migrations/2026-04-21_existing_install_patch.sql diff --git a/api/customers.php b/api/customers.php index c1255d2..4d5371f 100644 --- a/api/customers.php +++ b/api/customers.php @@ -5,12 +5,17 @@ $user = require_permission('customers', 'add'); if ($_SERVER['REQUEST_METHOD'] === 'POST') { header('Content-Type: application/json'); $name = trim($_POST['name'] ?? ''); - $phone = trim($_POST['phone'] ?? ''); + $phoneInput = trim($_POST['phone'] ?? ''); + $phone = $phoneInput === '' ? '' : normalize_oman_phone($phoneInput); if (!$name) { echo json_encode(['success' => false, 'error' => tr('الاسم مطلوب', 'Name is required')]); exit; } + if ($phoneInput !== '' && $phone === '') { + echo json_encode(['success' => false, 'error' => tr('رقم الهاتف يجب أن يكون عمانياً من 8 خانات.', 'Phone must be an 8-digit Oman number.')]); + exit; + } try { $pdo = db(); diff --git a/api/place_order.php b/api/place_order.php index 7a56c3d..a7ed2a8 100644 --- a/api/place_order.php +++ b/api/place_order.php @@ -14,14 +14,20 @@ if (!$input || empty($input['items'])) { } $name = trim($input['name'] ?? ''); -$phone = trim($input['phone'] ?? ''); +$phoneInput = trim($input['phone'] ?? ''); +$phone = normalize_oman_phone($phoneInput); $address = trim($input['address'] ?? ''); -if ($name === '' || $phone === '' || $address === '') { +if ($name === '' || $phoneInput === '' || $address === '') { echo json_encode(['success' => false, 'error' => 'Missing customer details']); exit; } +if ($phone === '') { + echo json_encode(['success' => false, 'error' => 'Phone must be an 8-digit Oman number']); + exit; +} + $items = $input['items']; $subtotal = 0; $totalVat = 0; @@ -78,15 +84,37 @@ try { $totalAmount ]); - // Optional: send telegram notification if configured + // Optional: send telegram and WhatsApp notifications if configured try { - $orderId = $db->lastInsertId(); - $msg = "🛒 *New Online Order #{$orderId}*\n\n"; - $msg .= "👤 {$name}\n📞 {$phone}\n📍 {$address}\n\n"; - $msg .= "💰 Subtotal: " . currency($subtotal) . "\n"; - $msg .= "🧾 VAT: " . currency($totalVat) . "\n"; - $msg .= "💵 Total: " . currency($totalAmount) . "\n"; - + $orderId = (int) $db->lastInsertId(); + $orderData = [ + 'id' => $orderId, + 'customer_name' => $name, + 'customer_phone' => $phone, + 'customer_address' => $address, + 'items' => $processedItems, + 'subtotal' => $subtotal, + 'vat_amount' => $totalVat, + 'total_amount' => $totalAmount, + 'status' => 'pending', + 'created_at' => date('Y-m-d H:i:s'), + ]; + + $msg = "🛒 *New Online Order #{$orderId}* + +"; + $msg .= "👤 {$name} +📞 " . phone_display($phone) . " +📍 {$address} + +"; + $msg .= "💰 Subtotal: " . currency($subtotal) . " +"; + $msg .= "🧾 VAT: " . currency($totalVat) . " +"; + $msg .= "💵 Total: " . currency($totalAmount) . " +"; + $botToken = getenv('TELEGRAM_BOT_TOKEN') ?: get_setting('telegram_bot_token'); $chatId = getenv('TELEGRAM_CHAT_ID') ?: get_setting('telegram_chat_id'); if ($botToken && $chatId) { @@ -94,7 +122,8 @@ try { $data = ['chat_id' => $chatId, 'text' => $msg, 'parse_mode' => 'Markdown']; $options = [ 'http' => [ - 'header' => "Content-type: application/x-www-form-urlencoded\r\n", + 'header' => "Content-type: application/x-www-form-urlencoded +", 'method' => 'POST', 'content' => http_build_query($data) ] @@ -102,10 +131,14 @@ try { $context = stream_context_create($options); @file_get_contents($url, false, $context); } + + if (wablas_is_configured()) { + wablas_notify_online_order($orderData, 'created'); + } } catch (Exception $e) { // ignore notification errors } - + echo json_encode(['success' => true]); } catch (Exception $e) { echo json_encode(['success' => false, 'error' => 'Database error: ' . $e->getMessage()]); diff --git a/api/sales_payment.php b/api/sales_payment.php new file mode 100644 index 0000000..f3f6ba4 --- /dev/null +++ b/api/sales_payment.php @@ -0,0 +1,46 @@ + false, 'error' => tr('طريقة الطلب غير مدعومة.', 'Method not allowed.')]); + exit; +} + +$saleId = (int) ($_POST['sale_id'] ?? 0); +$paymentAmount = (string) ($_POST['payment_amount'] ?? ''); +$completeOrder = !empty($_POST['complete_order']); + +if ($saleId <= 0) { + echo json_encode(['success' => false, 'error' => tr('الفاتورة غير صالحة.', 'Invalid invoice.')]); + exit; +} +if ($paymentAmount === '' || !is_numeric($paymentAmount) || (float) $paymentAmount <= 0) { + echo json_encode(['success' => false, 'error' => tr('أدخل مبلغ دفعة صحيحاً.', 'Enter a valid payment amount.')]); + exit; +} + +try { + $sale = fetch_sale($saleId); + if (!$sale) { + echo json_encode(['success' => false, 'error' => tr('الفاتورة غير موجودة أو غير مصرح بها.', 'Invoice not found or not allowed.')]); + exit; + } + + $result = apply_sale_payment($saleId, (float) $paymentAmount, $completeOrder); + + echo json_encode([ + 'success' => true, + 'message' => $result['due_amount'] <= 0.0005 + ? tr('تم سداد الفاتورة بالكامل.', 'Invoice paid in full.') + : tr('تم تسجيل الدفعة الجزئية بنجاح.', 'Partial payment recorded successfully.'), + 'payment_status' => $result['payment_status'], + 'paid_amount' => $result['paid_amount'], + 'due_amount' => $result['due_amount'], + 'applied_amount' => $result['applied_amount'], + ]); +} catch (Throwable $e) { + echo json_encode(['success' => false, 'error' => tr('تعذر تسجيل الدفعة.', 'Could not record the payment.')]); +} diff --git a/api/settings.php b/api/settings.php index e3c5c9a..45ead73 100644 --- a/api/settings.php +++ b/api/settings.php @@ -14,14 +14,29 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $keys = [ 'timezone', 'company_name_ar', 'company_name_en', 'vat_percentage', 'company_vat_number', 'company_phone', 'company_email', 'company_address', + 'wablas_enabled', 'wablas_token', 'wablas_secret_key', + 'wablas_template_created', 'wablas_template_pending', 'wablas_template_accepted', 'wablas_template_completed', 'wablas_template_rejected', 'smtp_host', 'smtp_port', 'smtp_user', 'smtp_pass', 'smtp_secure', 'mail_from', 'mail_from_name' ]; $stmt = $pdo->prepare("INSERT INTO settings (setting_key, setting_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)"); + $companyPhone = trim((string) ($_POST['company_phone'] ?? '')); + if ($companyPhone !== '') { + $companyPhone = normalize_oman_phone($companyPhone); + if ($companyPhone === '') { + set_flash('danger', tr('رقم هاتف الشركة يجب أن يكون عمانياً من 8 خانات.', 'Company phone must be an 8-digit Oman number.')); + $referer = $_SERVER['HTTP_REFERER'] ?? '../index.php'; + header('Location: ' . $referer); + exit; + } + $_POST['company_phone'] = $companyPhone; + } + foreach ($keys as $key) { if (isset($_POST[$key])) { - $stmt->execute([$key, $_POST[$key]]); + $value = is_string($_POST[$key]) ? trim($_POST[$key]) : $_POST[$key]; + $stmt->execute([$key, $value]); } } diff --git a/api/wablas_test.php b/api/wablas_test.php new file mode 100644 index 0000000..23eaf4d --- /dev/null +++ b/api/wablas_test.php @@ -0,0 +1,59 @@ + $token, + 'secret_key' => $secretKey, + 'ignore_enabled' => true, +]); + +if (!empty($result['success'])) { + set_flash('success', tr('تم إرسال رسالة الاختبار إلى 968 ', 'Test message sent to 968 ') . $phone . '.'); +} else { + $status = isset($result['status']) ? (' (' . (int) $result['status'] . ')') : ''; + set_flash('danger', tr('فشل إرسال رسالة الاختبار.', 'Failed to send the test message.') . ' ' . (string) ($result['error'] ?? ('Wablas error' . $status))); +} + +header('Location: ' . ($_SERVER['HTTP_REFERER'] ?? '../index.php')); +exit; diff --git a/cookies_wablas_test.txt b/cookies_wablas_test.txt new file mode 100644 index 0000000..efe55ff --- /dev/null +++ b/cookies_wablas_test.txt @@ -0,0 +1,5 @@ +# Netscape HTTP Cookie File +# https://curl.se/docs/http-cookies.html +# This file was generated by libcurl! Edit at your own risk. + +127.0.0.1 FALSE / FALSE 0 PHPSESSID 1snhg6de9qetq9ilb98ihn8qnv diff --git a/customers.php b/customers.php index ebf9950..c488862 100644 --- a/customers.php +++ b/customers.php @@ -11,13 +11,27 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $action = $_POST['action'] ?? ''; if ($action === 'create') { + $phoneInput = trim((string) ($_POST['phone'] ?? '')); + $phone = $phoneInput === '' ? '' : normalize_oman_phone($phoneInput); + if ($phoneInput !== '' && $phone === '') { + set_flash('danger', tr('رقم الهاتف يجب أن يكون عمانياً من 8 خانات.', 'Phone must be an 8-digit Oman number.')); + redirect_to('customers.php'); + } + $stmt = $pdo->prepare('INSERT INTO customers (name, phone, email, address) VALUES (?, ?, ?, ?)'); - $stmt->execute([$_POST['name'], $_POST['phone'] ?? '', $_POST['email'] ?? '', $_POST['address'] ?? '']); + $stmt->execute([$_POST['name'], $phone, $_POST['email'] ?? '', $_POST['address'] ?? '']); set_flash('success', tr('تمت إضافة العميل بنجاح', 'Customer added successfully')); redirect_to('customers.php'); } elseif ($action === 'edit') { + $phoneInput = trim((string) ($_POST['phone'] ?? '')); + $phone = $phoneInput === '' ? '' : normalize_oman_phone($phoneInput); + if ($phoneInput !== '' && $phone === '') { + set_flash('danger', tr('رقم الهاتف يجب أن يكون عمانياً من 8 خانات.', 'Phone must be an 8-digit Oman number.')); + redirect_to('customers.php'); + } + $stmt = $pdo->prepare('UPDATE customers SET name = ?, phone = ?, email = ?, address = ? WHERE id = ?'); - $stmt->execute([$_POST['name'], $_POST['phone'] ?? '', $_POST['email'] ?? '', $_POST['address'] ?? '', $_POST['id']]); + $stmt->execute([$_POST['name'], $phone, $_POST['email'] ?? '', $_POST['address'] ?? '', $_POST['id']]); set_flash('success', tr('تم التحديث بنجاح', 'Updated successfully')); redirect_to('customers.php'); } elseif ($action === 'delete') { @@ -37,7 +51,8 @@ $search = $_GET['q'] ?? ''; $where = '1=1'; $params = []; if ($search) { - $where .= ' AND (name LIKE ? OR phone LIKE ? OR email LIKE ?)'; + $where .= " AND (name LIKE ? OR phone LIKE ? OR CONCAT('968', phone) LIKE ? OR email LIKE ?)"; + $params[] = "%$search%"; $params[] = "%$search%"; $params[] = "%$search%"; $params[] = "%$search%"; @@ -95,7 +110,7 @@ require __DIR__ . '/includes/header.php'; - + @@ -143,7 +158,10 @@ require __DIR__ . '/includes/header.php';
- +
+ 968 + +
@@ -183,7 +201,10 @@ require __DIR__ . '/includes/header.php';
- +
+ 968 + +
diff --git a/db/migrations/2026-04-21_existing_install_patch.sql b/db/migrations/2026-04-21_existing_install_patch.sql new file mode 100644 index 0000000..8027093 --- /dev/null +++ b/db/migrations/2026-04-21_existing_install_patch.sql @@ -0,0 +1,133 @@ +-- Existing installation patch for partial payments + WhatsApp toggle/test settings support +-- Safe to import from phpMyAdmin on an already-installed database. +-- It only adds missing columns/settings and backfills payment amounts/status. + +SET @OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS; +SET FOREIGN_KEY_CHECKS = 0; + +DROP PROCEDURE IF EXISTS apply_existing_install_patch; +DELIMITER $$ +CREATE PROCEDURE apply_existing_install_patch() +BEGIN + -- users.avatar + IF NOT EXISTS ( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'users' + AND COLUMN_NAME = 'avatar' + ) THEN + ALTER TABLE users + ADD COLUMN avatar VARCHAR(255) DEFAULT NULL; + END IF; + + -- branches.avatar + IF NOT EXISTS ( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'branches' + AND COLUMN_NAME = 'avatar' + ) THEN + ALTER TABLE branches + ADD COLUMN avatar VARCHAR(255) DEFAULT NULL; + END IF; + + -- sales_orders.customer_id + IF NOT EXISTS ( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'sales_orders' + AND COLUMN_NAME = 'customer_id' + ) THEN + ALTER TABLE sales_orders + ADD COLUMN customer_id INT(10) UNSIGNED DEFAULT NULL; + END IF; + + -- sales_orders.payment_status + IF NOT EXISTS ( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'sales_orders' + AND COLUMN_NAME = 'payment_status' + ) THEN + ALTER TABLE sales_orders + ADD COLUMN payment_status VARCHAR(20) NOT NULL DEFAULT 'paid'; + END IF; + + -- sales_orders.vat_amount + IF NOT EXISTS ( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'sales_orders' + AND COLUMN_NAME = 'vat_amount' + ) THEN + ALTER TABLE sales_orders + ADD COLUMN vat_amount DECIMAL(10,3) NOT NULL DEFAULT 0.000 AFTER subtotal; + END IF; + + -- sales_orders.paid_amount + IF NOT EXISTS ( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'sales_orders' + AND COLUMN_NAME = 'paid_amount' + ) THEN + ALTER TABLE sales_orders + ADD COLUMN paid_amount DECIMAL(10,3) NOT NULL DEFAULT 0.000 AFTER total_amount; + END IF; + + -- sales_orders.due_amount + IF NOT EXISTS ( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'sales_orders' + AND COLUMN_NAME = 'due_amount' + ) THEN + ALTER TABLE sales_orders + ADD COLUMN due_amount DECIMAL(10,3) NOT NULL DEFAULT 0.000 AFTER paid_amount; + END IF; + + -- Backfill / normalize payment values for existing invoices. + UPDATE sales_orders + SET paid_amount = CASE + WHEN payment_status = 'unpaid' THEN 0 + ELSE total_amount + END + WHERE paid_amount IS NULL OR paid_amount = 0; + + UPDATE sales_orders + SET due_amount = GREATEST(total_amount - paid_amount, 0); + + UPDATE sales_orders + SET payment_status = CASE + WHEN due_amount <= 0.0005 THEN 'paid' + WHEN paid_amount > 0 THEN 'partial' + ELSE 'unpaid' + END; + + -- Settings keys used by the new WhatsApp tab and toggle. + INSERT IGNORE INTO settings (setting_key, setting_value) VALUES ('wablas_enabled', '1'); + INSERT IGNORE INTO settings (setting_key, setting_value) VALUES ('wablas_token', ''); + INSERT IGNORE INTO settings (setting_key, setting_value) VALUES ('wablas_secret_key', ''); + INSERT IGNORE INTO settings (setting_key, setting_value) VALUES ('wablas_template_created', ''); + INSERT IGNORE INTO settings (setting_key, setting_value) VALUES ('wablas_template_pending', ''); + INSERT IGNORE INTO settings (setting_key, setting_value) VALUES ('wablas_template_accepted', ''); + INSERT IGNORE INTO settings (setting_key, setting_value) VALUES ('wablas_template_completed', ''); + INSERT IGNORE INTO settings (setting_key, setting_value) VALUES ('wablas_template_rejected', ''); +END $$ +DELIMITER ; + +CALL apply_existing_install_patch(); +DROP PROCEDURE IF EXISTS apply_existing_install_patch; + +SET FOREIGN_KEY_CHECKS = @OLD_FOREIGN_KEY_CHECKS; + +-- Optional verification after import: +-- SHOW COLUMNS FROM sales_orders; +-- SELECT setting_key, setting_value FROM settings WHERE setting_key LIKE 'wablas%'; diff --git a/db/schema.sql b/db/schema.sql index 9b55a0b..8b955c4 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -130,13 +130,17 @@ CREATE TABLE IF NOT EXISTS `sales_orders` ( `cashier_username` varchar(60) NOT NULL, `cashier_name` varchar(120) NOT NULL, `role_name` varchar(40) NOT NULL, + `customer_id` int(10) unsigned DEFAULT NULL, `customer_name` varchar(120) DEFAULT NULL, `payment_method` varchar(30) NOT NULL, + `payment_status` varchar(20) NOT NULL DEFAULT 'paid', `items_json` longtext NOT NULL, `item_count` int(10) unsigned NOT NULL DEFAULT 0, `subtotal` decimal(10,2) NOT NULL DEFAULT 0.00, `vat_amount` decimal(10,3) NOT NULL DEFAULT 0.000, `total_amount` decimal(10,2) NOT NULL DEFAULT 0.00, + `paid_amount` decimal(10,3) NOT NULL DEFAULT 0.000, + `due_amount` decimal(10,3) NOT NULL DEFAULT 0.000, `notes` text DEFAULT NULL, `sale_date` datetime NOT NULL DEFAULT current_timestamp(), `created_at` datetime NOT NULL DEFAULT current_timestamp(), @@ -220,6 +224,14 @@ INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES ('company_ INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES ('company_vat_number', ''); INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES ('mail_from', ''); INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES ('mail_from_name', ''); +INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES ('wablas_enabled', '1'); +INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES ('wablas_token', ''); +INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES ('wablas_secret_key', ''); +INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES ('wablas_template_created', ''); +INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES ('wablas_template_pending', ''); +INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES ('wablas_template_accepted', ''); +INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES ('wablas_template_completed', ''); +INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES ('wablas_template_rejected', ''); INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES ('smtp_host', ''); INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES ('smtp_pass', ''); INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES ('smtp_port', '587'); diff --git a/debts.php b/debts.php index 12e1306..d4b5c29 100644 --- a/debts.php +++ b/debts.php @@ -9,11 +9,17 @@ $debtsLoadError = ''; $unpaidSales = []; $debtsByCustomer = []; -// Handle Mark as Paid +// Handle legacy mark-as-paid shortcut if (isset($_GET['mark_paid'])) { - $id = (int)$_GET['mark_paid']; + $id = (int) $_GET['mark_paid']; try { - $pdo->prepare("UPDATE sales_orders SET payment_status = 'paid', status = 'completed' WHERE id = ?")->execute([$id]); + $sale = fetch_sale($id); + if ($sale) { + $summary = sale_payment_summary($sale); + if ($summary['due_amount'] > 0.0005) { + apply_sale_payment($id, $summary['due_amount'], true); + } + } set_flash('success', tr('تم استلام المبلغ بنجاح.', 'Payment received successfully.')); } catch (Throwable $e) { set_flash('danger', tr('خطأ أثناء التحديث.', 'Error updating.')); @@ -42,6 +48,9 @@ try { 's.receipt_no', 's.sale_date', 's.total_amount', + 's.payment_status', + 'COALESCE(s.paid_amount, 0) AS paid_amount', + 'COALESCE(s.due_amount, s.total_amount) AS due_amount', isset($salesColumns['customer_id']) ? 's.customer_id' : 'NULL AS customer_id', isset($salesColumns['customer_name']) ? 's.customer_name' : 'NULL AS customer_name', 'NULL AS c_name', @@ -52,15 +61,15 @@ try { if ($hasCustomersTable && isset($salesColumns['customer_id']) && isset($customerColumns['id'])) { $joinSql = ' LEFT JOIN customers c ON s.customer_id = c.id '; if (isset($customerColumns['name'])) { - $selectParts[6] = 'c.name AS c_name'; + $selectParts[9] = 'c.name AS c_name'; } if (isset($customerColumns['phone'])) { - $selectParts[7] = 'c.phone AS c_phone'; + $selectParts[10] = 'c.phone AS c_phone'; } } if (isset($salesColumns['payment_status'])) { - $whereSql = " WHERE s.payment_status = 'unpaid'"; + $whereSql = " WHERE s.payment_status IN ('unpaid', 'partial')"; } elseif (isset($salesColumns['payment_method'])) { $whereSql = " WHERE s.payment_method = 'pay_later'"; } else { @@ -89,11 +98,16 @@ foreach ($unpaidSales as $sale) { 'name' => $sale['c_name'] ?: $sale['customer_name'] ?: tr('عميل غير معروف', 'Unknown Customer'), 'phone' => $sale['c_phone'] ?: '', 'total' => 0.0, - 'count' => 0 + 'open_invoices' => 0, + 'partial_invoices' => 0 ]; } - $debtsByCustomer[$cId]['total'] += (float)$sale['total_amount']; - $debtsByCustomer[$cId]['count'] += 1; + $saleSummary = sale_payment_summary($sale); + $debtsByCustomer[$cId]['total'] += (float) $saleSummary['due_amount']; + $debtsByCustomer[$cId]['open_invoices'] += 1; + if ($saleSummary['payment_status'] === 'partial') { + $debtsByCustomer[$cId]['partial_invoices'] += 1; + } } // Sort by highest debt @@ -129,9 +143,12 @@ require_once 'includes/header.php';
-
+
+ +
+ 0): ?> +
-
@@ -146,7 +163,7 @@ require_once 'includes/header.php';
-
+
@@ -156,31 +173,38 @@ require_once 'includes/header.php'; - + + + + - + + - - - + - + + + - @@ -195,20 +219,48 @@ require_once 'includes/header.php';
diff --git a/edit_online_order.php b/edit_online_order.php index a1f4db4..c61ad42 100644 --- a/edit_online_order.php +++ b/edit_online_order.php @@ -20,14 +20,17 @@ $catalog = catalog(); if ($_SERVER['REQUEST_METHOD'] === 'POST') { $customerName = trim((string) ($_POST['customer_name'] ?? '')); - $customerPhone = trim((string) ($_POST['customer_phone'] ?? '')); + $customerPhoneInput = trim((string) ($_POST['customer_phone'] ?? '')); + $customerPhone = normalize_oman_phone($customerPhoneInput); $customerAddress = trim((string) ($_POST['customer_address'] ?? '')); $saleStatus = trim((string) ($_POST['sale_status'] ?? 'pending')); $cartJson = (string) ($_POST['cart_json'] ?? '[]'); $items = json_decode($cartJson, true); - if ($customerName === '' || $customerPhone === '' || $customerAddress === '') { + if ($customerName === '' || $customerPhoneInput === '' || $customerAddress === '') { $error = tr('الرجاء تعبئة بيانات العميل الأساسية.', 'Please fill the main customer details.'); + } elseif ($customerPhone === '') { + $error = tr('رقم الهاتف يجب أن يكون عمانياً من 8 خانات.', 'Phone must be an 8-digit Oman number.'); } elseif (!is_array($items) || $items === []) { $error = tr('أضف صنفاً واحداً على الأقل إلى الطلب.', 'Add at least one item to the order.'); } else { @@ -66,6 +69,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($normalized === []) { $error = tr('الطلب غير صالح بعد التحقق من الأصناف.', 'The order is invalid after product validation.'); } else { + $statusChanged = ($editOrder['status'] ?? 'pending') !== $saleStatus; $stmt = db()->prepare('UPDATE online_orders SET customer_name = :customer_name, customer_phone = :customer_phone, @@ -88,6 +92,21 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { ':id' => $editOrderId, ]); + if ($statusChanged && wablas_is_configured()) { + wablas_notify_online_order([ + 'id' => $editOrderId, + 'customer_name' => $customerName, + 'customer_phone' => $customerPhone, + 'customer_address' => $customerAddress, + 'items' => $normalized, + 'subtotal' => $subtotal, + 'vat_amount' => $totalVat, + 'total_amount' => $subtotal + $totalVat, + 'status' => $saleStatus, + 'created_at' => $editOrder['created_at'] ?? date('Y-m-d H:i:s'), + ], $saleStatus); + } + set_flash('success', tr('تم تحديث الطلب بنجاح.', 'Order updated successfully.')); redirect_to('online_orders.php'); } @@ -333,7 +352,10 @@ require __DIR__ . '/includes/header.php';
- +
+ 968 + +
diff --git a/edit_sale.php b/edit_sale.php index 7396512..0155786 100644 --- a/edit_sale.php +++ b/edit_sale.php @@ -19,6 +19,8 @@ if ($user['role'] !== 'owner' && $editSale['branch_code'] !== $user['branch_code $pageTitle = tr('تعديل فاتورة', 'Edit Invoice') . ' #' . h($editSale['receipt_no']); $activeNav = 'sales'; $error = ''; +$editPaymentSummary = sale_payment_summary($editSale); +$paymentAmountInput = (string) ($_POST['payment_amount'] ?? ($editPaymentSummary['paid_amount'] > 0 ? number_format((float) $editPaymentSummary['paid_amount'], 3, '.', '') : '')); $catalog = catalog(); $allowedBranches = get_user_branches($user); @@ -33,7 +35,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $customerId = isset($_POST['customer_id']) && $_POST['customer_id'] !== '' ? (int)$_POST['customer_id'] : null; $customerName = trim((string) ($_POST['customer_name'] ?? '')); $paymentMethod = trim((string) ($_POST['payment_method'] ?? 'cash')); - $paymentStatus = ($paymentMethod === 'pay_later') ? 'unpaid' : 'paid'; + $paymentAmountInput = trim((string) ($_POST['payment_amount'] ?? '')); $saleStatus = trim((string) ($_POST['sale_status'] ?? 'completed')); $notes = trim((string) ($_POST['notes'] ?? '')); $cartJson = (string) ($_POST['cart_json'] ?? '[]'); @@ -43,8 +45,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $error = tr('اختر فرعاً صالحاً لهذه الصلاحية.', 'Choose a valid branch for this role.'); } elseif (!in_array($paymentMethod, ['cash', 'card', 'transfer', 'pay_later'], true)) { $error = tr('اختر طريقة دفع صحيحة.', 'Choose a valid payment method.'); - } elseif ($paymentMethod === 'pay_later' && !$customerId) { - $error = tr('يجب اختيار عميل مسجل للدفع الآجل.', 'You must select a registered customer for pay later.'); } elseif (!is_array($items) || $items === []) { $error = tr('أضف صنفاً واحداً على الأقل إلى الفاتورة.', 'Add at least one item to the invoice.'); } else { @@ -83,13 +83,27 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($normalized === []) { $error = tr('الفاتورة غير صالحة بعد التحقق من الأصناف.', 'The invoice is invalid after product validation.'); } else { + $totalAmount = $subtotal + $totalVat; + if ($paymentAmountInput !== '' && !is_numeric($paymentAmountInput)) { + $error = tr('أدخل مبلغاً مدفوعاً صحيحاً.', 'Enter a valid paid amount.'); + } else { + $paymentMeta = sale_payment_breakdown($totalAmount, $paymentMethod, $paymentAmountInput); + if ($paymentMeta['due_amount'] > 0.0005 && !$customerId) { + $error = tr('يجب اختيار عميل مسجل عند وجود مبلغ متبقٍ أو دفعة جزئية.', 'Select a registered customer when there is a remaining balance or partial payment.'); + } + } + } + + if ($error === '') { $cashierName = current_lang() === 'ar' ? $user['name_ar'] : $user['name_en']; - $stmt = db()->prepare('UPDATE sales_orders SET + $stmt = db()->prepare('UPDATE sales_orders SET branch_code = :branch_code, customer_id = :customer_id, customer_name = :customer_name, payment_method = :payment_method, payment_status = :payment_status, + paid_amount = :paid_amount, + due_amount = :due_amount, items_json = :items_json, item_count = :item_count, subtotal = :subtotal, @@ -103,11 +117,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { ':customer_id' => $customerId, ':customer_name' => $customerName !== '' ? $customerName : null, ':payment_method' => $paymentMethod, - ':payment_status' => $paymentStatus, + ':payment_status' => $paymentMeta['payment_status'], + ':paid_amount' => $paymentMeta['paid_amount'], + ':due_amount' => $paymentMeta['due_amount'], ':items_json' => json_encode($normalized, JSON_UNESCAPED_UNICODE), ':item_count' => $itemCount, ':subtotal' => $subtotal, - ':total_amount' => $subtotal, + ':vat_amount' => $totalVat, + ':total_amount' => $totalAmount, ':status' => $saleStatus, ':notes' => $notes !== '' ? $notes : null, ':id' => $editSaleId, @@ -381,12 +398,17 @@ require __DIR__ . '/includes/header.php';
+
+ + +
+
@@ -447,7 +469,9 @@ require __DIR__ . '/includes/header.php'; diff --git a/includes/app.php b/includes/app.php index 9ed6092..bbc57f1 100644 --- a/includes/app.php +++ b/includes/app.php @@ -9,7 +9,7 @@ require_once __DIR__ . '/../db/config.php'; // Auto-migrate newly added columns try { - $flagFile = sys_get_temp_dir() . '/.schema_migrated_v2_' . md5(__DIR__); + $flagFile = sys_get_temp_dir() . '/.schema_migrated_v3_' . md5(__DIR__); if (!file_exists($flagFile)) { $pdo = db(); $stmt = $pdo->query("SHOW COLUMNS FROM users LIKE 'avatar'"); @@ -32,6 +32,17 @@ try { if ($stmt5->rowCount() === 0) { $pdo->exec("ALTER TABLE sales_orders ADD COLUMN vat_amount decimal(10,3) NOT NULL DEFAULT 0.000 AFTER subtotal"); } + $stmt6 = $pdo->query("SHOW COLUMNS FROM sales_orders LIKE 'paid_amount'"); + if ($stmt6->rowCount() === 0) { + $pdo->exec("ALTER TABLE sales_orders ADD COLUMN paid_amount decimal(10,3) NOT NULL DEFAULT 0.000 AFTER total_amount"); + } + $stmt7 = $pdo->query("SHOW COLUMNS FROM sales_orders LIKE 'due_amount'"); + if ($stmt7->rowCount() === 0) { + $pdo->exec("ALTER TABLE sales_orders ADD COLUMN due_amount decimal(10,3) NOT NULL DEFAULT 0.000 AFTER paid_amount"); + } + $pdo->exec("UPDATE sales_orders SET paid_amount = CASE WHEN payment_status = 'unpaid' THEN 0 ELSE total_amount END WHERE paid_amount IS NULL OR paid_amount = 0"); + $pdo->exec("UPDATE sales_orders SET due_amount = GREATEST(total_amount - paid_amount, 0)"); + $pdo->exec("UPDATE sales_orders SET payment_status = CASE WHEN due_amount <= 0.0005 THEN 'paid' WHEN paid_amount > 0 THEN 'partial' ELSE 'unpaid' END"); @file_put_contents($flagFile, '1'); } } catch (\Throwable $e) {} @@ -102,6 +113,42 @@ function h($value): string return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8'); } +function phone_digits(string $value): string +{ + return preg_replace('/\D+/', '', $value) ?? ''; +} + +function normalize_oman_phone(string $value): string +{ + $digits = phone_digits($value); + if ($digits === '') { + return ''; + } + + if (str_starts_with($digits, '00968')) { + $digits = substr($digits, 5); + } elseif (str_starts_with($digits, '968')) { + $digits = substr($digits, 3); + } + + if (strlen($digits) === 9 && $digits[0] === '0') { + $digits = substr($digits, 1); + } + + return strlen($digits) === 8 ? $digits : ''; +} + +function phone_display(?string $value): string +{ + $raw = trim((string) $value); + if ($raw === '') { + return ''; + } + + $local = normalize_oman_phone($raw); + return $local !== '' ? ('968 ' . $local) : $raw; +} + function qs_with_lang(array $params = []): string { if (!isset($params['lang']) || !in_array($params['lang'], ['ar', 'en'], true)) { @@ -327,6 +374,331 @@ function sale_mode_label(string $mode): string return $mode === 'normal' ? tr('بيع عادي', 'Normal Sale') : tr('بيع نقاط البيع', 'POS Sale'); } +function round_money(float $amount): float +{ + return round($amount, 3); +} + +function sale_payment_breakdown(float $totalAmount, ?string $paymentMethod = null, $paidInput = null): array +{ + $totalAmount = max(0.0, round_money($totalAmount)); + $defaultPaid = $paymentMethod === 'pay_later' ? 0.0 : $totalAmount; + if ($paidInput === null || $paidInput === '') { + $paidAmount = $defaultPaid; + } elseif (is_numeric($paidInput)) { + $paidAmount = (float) $paidInput; + } else { + $paidAmount = $defaultPaid; + } + + $paidAmount = min($totalAmount, max(0.0, round_money($paidAmount))); + $dueAmount = max(0.0, round_money($totalAmount - $paidAmount)); + $paymentStatus = $dueAmount <= 0.0005 ? 'paid' : ($paidAmount > 0 ? 'partial' : 'unpaid'); + + return [ + 'paid_amount' => $paidAmount, + 'due_amount' => $dueAmount, + 'payment_status' => $paymentStatus, + ]; +} + +function sale_payment_summary(array $sale): array +{ + $totalAmount = round_money((float) ($sale['total_amount'] ?? 0)); + $storedPaid = $sale['paid_amount'] ?? null; + $storedDue = $sale['due_amount'] ?? null; + $paymentStatus = (string) ($sale['payment_status'] ?? ''); + + if ($storedPaid === null && $storedDue === null) { + return sale_payment_breakdown($totalAmount, (string) ($sale['payment_method'] ?? ''), $paymentStatus === 'unpaid' ? 0 : $totalAmount); + } + + $paidAmount = $storedPaid !== null ? max(0.0, round_money((float) $storedPaid)) : max(0.0, round_money($totalAmount - (float) $storedDue)); + $dueAmount = $storedDue !== null ? max(0.0, round_money((float) $storedDue)) : max(0.0, round_money($totalAmount - $paidAmount)); + + if ($dueAmount > $totalAmount) { + $dueAmount = $totalAmount; + } + if ($paidAmount > $totalAmount) { + $paidAmount = $totalAmount; + } + + if ($paymentStatus === '' || !in_array($paymentStatus, ['paid', 'partial', 'unpaid'], true)) { + $paymentStatus = $dueAmount <= 0.0005 ? 'paid' : ($paidAmount > 0 ? 'partial' : 'unpaid'); + } + + return [ + 'paid_amount' => $paidAmount, + 'due_amount' => $dueAmount, + 'payment_status' => $paymentStatus, + ]; +} + +function payment_status_label(string $status): string +{ + return match ($status) { + 'partial' => tr('مدفوعة جزئياً', 'Partially Paid'), + 'unpaid' => tr('غير مدفوعة', 'Unpaid'), + default => tr('مدفوعة', 'Paid'), + }; +} + +function payment_status_badge_class(string $status): string +{ + return match ($status) { + 'partial' => 'bg-warning text-dark', + 'unpaid' => 'bg-danger text-white', + default => 'bg-success text-white', + }; +} + +function apply_sale_payment(int $saleId, float $paymentAmount, bool $completeOrderWhenPaid = false): array +{ + $sale = fetch_sale($saleId); + if (!$sale) { + throw new RuntimeException('Sale not found.'); + } + + $summary = sale_payment_summary($sale); + $paymentAmount = max(0.0, round_money($paymentAmount)); + if ($paymentAmount <= 0) { + throw new RuntimeException('Invalid payment amount.'); + } + if ($summary['due_amount'] <= 0.0005) { + throw new RuntimeException('Invoice already paid.'); + } + + $appliedAmount = min($paymentAmount, $summary['due_amount']); + $newPaidAmount = round_money($summary['paid_amount'] + $appliedAmount); + $newDueAmount = max(0.0, round_money((float) $sale['total_amount'] - $newPaidAmount)); + $newPaymentStatus = $newDueAmount <= 0.0005 ? 'paid' : 'partial'; + $newSaleStatus = (string) ($sale['status'] ?? 'completed'); + if ($completeOrderWhenPaid && $newDueAmount <= 0.0005 && $newSaleStatus === 'order') { + $newSaleStatus = 'completed'; + } + + $stmt = db()->prepare('UPDATE sales_orders SET paid_amount = :paid_amount, due_amount = :due_amount, payment_status = :payment_status, status = :status WHERE id = :id'); + $stmt->execute([ + ':paid_amount' => $newPaidAmount, + ':due_amount' => $newDueAmount, + ':payment_status' => $newPaymentStatus, + ':status' => $newSaleStatus, + ':id' => $saleId, + ]); + + return [ + 'applied_amount' => $appliedAmount, + 'paid_amount' => $newPaidAmount, + 'due_amount' => $newDueAmount, + 'payment_status' => $newPaymentStatus, + 'status' => $newSaleStatus, + ]; +} + +function wablas_is_enabled(): bool +{ + return (string) get_setting('wablas_enabled', '1') !== '0'; +} + +function wablas_has_credentials(?string $token = null, ?string $secretKey = null): bool +{ + $token = $token ?? trim((string) get_setting('wablas_token', '')); + $secretKey = $secretKey ?? trim((string) get_setting('wablas_secret_key', '')); + + return $token !== '' && $secretKey !== ''; +} + +function wablas_is_configured(bool $requireEnabled = true): bool +{ + return wablas_has_credentials() + && (!$requireEnabled || wablas_is_enabled()); +} + +function wablas_order_status_label(string $status): string +{ + return match ($status) { + 'pending' => tr('قيد الانتظار', 'Pending'), + 'accepted' => tr('مقبول', 'Accepted'), + 'completed' => tr('مكتمل', 'Completed'), + 'rejected' => tr('مرفوض', 'Rejected'), + 'order' => tr('طلب مسبق', 'Pre-order'), + default => $status, + }; +} + +function wablas_default_order_template(string $event): string +{ + return match ($event) { + 'created' => "مرحباً {customer_name}، تم استلام طلبك رقم #{order_id}. +الحالة: {status_label} +الإجمالي: {total_amount} +العنوان: {customer_address} +شكراً لتسوقك معنا.", + 'pending' => "مرحباً {customer_name}، طلبك رقم #{order_id} ما زال {status_label}. +الإجمالي: {total_amount} +سنوافيك بأي تحديث جديد.", + 'accepted' => "مرحباً {customer_name}، تم قبول طلبك رقم #{order_id}. +الإجمالي: {total_amount} +سنبدأ التجهيز الآن.", + 'completed' => "مرحباً {customer_name}، طلبك رقم #{order_id} أصبح {status_label}. +الإجمالي: {total_amount} +شكراً لك.", + 'rejected' => "مرحباً {customer_name}، نعتذر، تم تحديث طلبك رقم #{order_id} إلى {status_label}. +إذا رغبت بالمساعدة تواصل معنا.", + default => "مرحباً {customer_name}، تم تحديث طلبك رقم #{order_id} إلى {status_label}.", + }; +} + +function wablas_render_template(string $template, array $vars): string +{ + $message = $template; + foreach ($vars as $key => $value) { + $message = str_replace('{' . $key . '}', (string) $value, $message); + } + + return preg_replace("/ +{3,}/", " + +", trim($message)) ?? trim($message); +} + +function wablas_order_items_summary(array $order): string +{ + $items = $order['items'] ?? null; + if (!is_array($items)) { + $items = json_decode((string) ($order['items_json'] ?? '[]'), true); + } + if (!is_array($items) || $items === []) { + return ''; + } + + $parts = []; + foreach ($items as $item) { + $name = (string) ($item['name'] ?? $item['name_ar'] ?? $item['sku'] ?? ''); + $qty = (int) ($item['qty'] ?? 0); + if ($name === '') { + continue; + } + $parts[] = '- ' . $name . ($qty > 0 ? (' x' . $qty) : ''); + } + + return implode(" +", $parts); +} + +function wablas_order_template_vars(array $order): array +{ + $status = (string) ($order['status'] ?? 'pending'); + return [ + 'order_id' => (string) ($order['id'] ?? ''), + 'customer_name' => (string) ($order['customer_name'] ?? ''), + 'customer_phone' => phone_display((string) ($order['customer_phone'] ?? '')), + 'customer_address' => (string) ($order['customer_address'] ?? ''), + 'status' => $status, + 'status_label' => wablas_order_status_label($status), + 'subtotal' => currency((float) ($order['subtotal'] ?? 0)), + 'vat_amount' => currency((float) ($order['vat_amount'] ?? 0)), + 'total_amount' => currency((float) ($order['total_amount'] ?? 0)), + 'created_at' => (string) ($order['created_at'] ?? ''), + 'items_summary' => wablas_order_items_summary($order), + ]; +} + +function wablas_send_message(string $phone, string $message, array $options = []): array +{ + $localPhone = normalize_oman_phone($phone); + $token = trim((string) ($options['token'] ?? get_setting('wablas_token', ''))); + $secretKey = trim((string) ($options['secret_key'] ?? get_setting('wablas_secret_key', ''))); + $ignoreEnabled = !empty($options['ignore_enabled']); + $message = trim($message); + + if ($localPhone === '') { + return ['success' => false, 'error' => 'Invalid Oman phone number']; + } + if ($message === '') { + return ['success' => false, 'error' => 'Message is empty']; + } + if (!$ignoreEnabled && !wablas_is_enabled()) { + return ['success' => false, 'error' => 'WhatsApp sending is disabled']; + } + if (!wablas_has_credentials($token, $secretKey)) { + return ['success' => false, 'error' => 'Wablas is not configured']; + } + + $endpoint = 'https://wablas.com/api/send-message'; + $payload = http_build_query([ + 'phone' => '968' . $localPhone, + 'message' => $message, + ]); + $headers = [ + 'Authorization: ' . $token . '.' . $secretKey, + 'Content-Type: application/x-www-form-urlencoded', + ]; + + $responseBody = false; + $httpCode = 0; + + if (function_exists('curl_init')) { + $ch = curl_init($endpoint); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $payload, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_TIMEOUT => 20, + ]); + $responseBody = curl_exec($ch); + $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + $curlError = curl_error($ch); + curl_close($ch); + if ($responseBody === false) { + return ['success' => false, 'error' => $curlError ?: 'Failed to contact Wablas']; + } + } else { + $context = stream_context_create([ + 'http' => [ + 'method' => 'POST', + 'header' => implode(" +", $headers), + 'content' => $payload, + 'timeout' => 20, + 'ignore_errors' => true, + ], + ]); + $responseBody = @file_get_contents($endpoint, false, $context); + $statusLine = $http_response_header[0] ?? ''; + if (preg_match('/\s(\d{3})\s/', $statusLine, $m)) { + $httpCode = (int) $m[1]; + } + } + + $decoded = json_decode((string) $responseBody, true); + $success = $httpCode >= 200 && $httpCode < 300; + + return [ + 'success' => $success, + 'status' => $httpCode, + 'data' => $decoded, + 'raw' => $responseBody, + 'phone' => '968' . $localPhone, + ]; +} + +function wablas_notify_online_order(array $order, string $event): array +{ + $template = trim((string) get_setting('wablas_template_' . $event, '')); + if ($template === '') { + $template = wablas_default_order_template($event); + } + + $message = wablas_render_template($template, wablas_order_template_vars($order)); + $result = wablas_send_message((string) ($order['customer_phone'] ?? ''), $message); + if (empty($result['success'])) { + error_log('Wablas notify failed for order #' . (string) ($order['id'] ?? '') . ': ' . (string) ($result['error'] ?? ('HTTP ' . ($result['status'] ?? '0')))); + } + return $result; +} + function ensure_sales_table(): void { $sql = "CREATE TABLE IF NOT EXISTS sales_orders ( @@ -346,6 +718,8 @@ function ensure_sales_table(): void subtotal DECIMAL(10,2) NOT NULL DEFAULT 0, vat_amount DECIMAL(10,3) NOT NULL DEFAULT 0.000, total_amount DECIMAL(10,2) NOT NULL DEFAULT 0, + paid_amount DECIMAL(10,3) NOT NULL DEFAULT 0.000, + due_amount DECIMAL(10,3) NOT NULL DEFAULT 0.000, status VARCHAR(20) NOT NULL DEFAULT 'completed', notes TEXT DEFAULT NULL, sale_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, @@ -363,9 +737,9 @@ function create_sale(array $data): int ensure_sales_table(); $stmt = db()->prepare('INSERT INTO sales_orders - (receipt_no, sale_mode, branch_code, cashier_username, cashier_name, role_name, customer_id, customer_name, payment_method, payment_status, items_json, item_count, subtotal, vat_amount, total_amount, status, notes, sale_date) + (receipt_no, sale_mode, branch_code, cashier_username, cashier_name, role_name, customer_id, customer_name, payment_method, payment_status, items_json, item_count, subtotal, vat_amount, total_amount, paid_amount, due_amount, status, notes, sale_date) VALUES - (:receipt_no, :sale_mode, :branch_code, :cashier_username, :cashier_name, :role_name, :customer_id, :customer_name, :payment_method, :payment_status, :items_json, :item_count, :subtotal, :vat_amount, :total_amount, :status, :notes, NOW())'); + (:receipt_no, :sale_mode, :branch_code, :cashier_username, :cashier_name, :role_name, :customer_id, :customer_name, :payment_method, :payment_status, :items_json, :item_count, :subtotal, :vat_amount, :total_amount, :paid_amount, :due_amount, :status, :notes, NOW())'); $stmt->bindValue(':receipt_no', $data['receipt_no']); $stmt->bindValue(':sale_mode', $data['sale_mode']); @@ -382,6 +756,8 @@ function create_sale(array $data): int $stmt->bindValue(':subtotal', $data['subtotal']); $stmt->bindValue(':vat_amount', $data['vat_amount'] ?? 0.0); $stmt->bindValue(':total_amount', $data['total_amount']); + $stmt->bindValue(':paid_amount', $data['paid_amount'] ?? $data['total_amount']); + $stmt->bindValue(':due_amount', $data['due_amount'] ?? 0.0); $stmt->bindValue(':status', $data['status'] ?? 'completed'); $stmt->bindValue(':notes', $data['notes']); $stmt->execute(); diff --git a/includes/footer_settings.php b/includes/footer_settings.php index 059ed01..7e41ba3 100644 --- a/includes/footer_settings.php +++ b/includes/footer_settings.php @@ -1,7 +1,7 @@