update whatsapp and payments

This commit is contained in:
Flatlogic Bot 2026-04-21 02:42:34 +00:00
parent 9d38c126a5
commit be707683ba
22 changed files with 1424 additions and 228 deletions

View File

@ -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();

View File

@ -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()]);

46
api/sales_payment.php Normal file
View File

@ -0,0 +1,46 @@
<?php
require_once __DIR__ . '/../includes/app.php';
require_auth();
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => 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.')]);
}

View File

@ -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]);
}
}

59
api/wablas_test.php Normal file
View File

@ -0,0 +1,59 @@
<?php
require_once __DIR__ . '/../includes/app.php';
require_permission('settings', 'edit');
$user = current_user();
if (!in_array($user['role'], ['owner', 'manager'], true)) {
set_flash('danger', tr('غير مصرح لك.', 'Unauthorized.'));
redirect_to('../index.php');
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
redirect_to('../index.php');
}
$phoneInput = trim((string) ($_POST['wablas_test_phone'] ?? ''));
$message = trim((string) ($_POST['wablas_test_message'] ?? ''));
$token = trim((string) ($_POST['wablas_token'] ?? get_setting('wablas_token', '')));
$secretKey = trim((string) ($_POST['wablas_secret_key'] ?? get_setting('wablas_secret_key', '')));
if ($phoneInput === '') {
set_flash('danger', tr('أدخل رقم واتساب تجريبي صالحاً من 8 خانات.', 'Enter a valid 8-digit test WhatsApp number.'));
header('Location: ' . ($_SERVER['HTTP_REFERER'] ?? '../index.php'));
exit;
}
$phone = normalize_oman_phone($phoneInput);
if ($phone === '') {
set_flash('danger', tr('رقم الاختبار يجب أن يكون عمانياً من 8 خانات.', 'The test phone must be a valid 8-digit Oman number.'));
header('Location: ' . ($_SERVER['HTTP_REFERER'] ?? '../index.php'));
exit;
}
if ($message === '') {
set_flash('danger', tr('اكتب رسالة الاختبار أولاً.', 'Write the test message first.'));
header('Location: ' . ($_SERVER['HTTP_REFERER'] ?? '../index.php'));
exit;
}
if (!wablas_has_credentials($token, $secretKey)) {
set_flash('danger', tr('أدخل Wablas Token و Secret Key قبل إرسال الاختبار.', 'Enter the Wablas token and secret key before sending a test message.'));
header('Location: ' . ($_SERVER['HTTP_REFERER'] ?? '../index.php'));
exit;
}
$result = wablas_send_message($phone, $message, [
'token' => $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;

5
cookies_wablas_test.txt Normal file
View File

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

View File

@ -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';
<tr>
<td><?= h($item['id']) ?></td>
<td class="fw-semibold"><?= h($item['name']) ?></td>
<td><?= h($item['phone']) ?></td>
<td><?= h(phone_display($item['phone'])) ?></td>
<td><?= h($item['email']) ?></td>
<td><?= h($item['address']) ?></td>
<td>
@ -143,7 +158,10 @@ require __DIR__ . '/includes/header.php';
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label"><?= h(tr('الهاتف', 'Phone')) ?></label>
<input type="text" name="phone" class="form-control">
<div class="input-group" dir="ltr">
<span class="input-group-text">968</span>
<input type="text" name="phone" class="form-control" inputmode="numeric" maxlength="8" pattern="\d{8}" placeholder="91234567">
</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label"><?= h(tr('البريد الإلكتروني', 'Email')) ?></label>
@ -183,7 +201,10 @@ require __DIR__ . '/includes/header.php';
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label"><?= h(tr('الهاتف', 'Phone')) ?></label>
<input type="text" name="phone" id="edit_phone" class="form-control">
<div class="input-group" dir="ltr">
<span class="input-group-text">968</span>
<input type="text" name="phone" id="edit_phone" class="form-control" inputmode="numeric" maxlength="8" pattern="\d{8}" placeholder="91234567">
</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label"><?= h(tr('البريد الإلكتروني', 'Email')) ?></label>

View File

@ -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%';

View File

@ -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');

114
debts.php
View File

@ -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';
<div>
<strong><?= h($debt['name']) ?></strong>
<?php if ($debt['phone']): ?>
<div class="small text-muted"><?= h($debt['phone']) ?></div>
<div class="small text-muted" dir="ltr"><?= h(phone_display($debt['phone'])) ?></div>
<?php endif; ?>
<div class="small text-muted"><?= h($debt['open_invoices']) ?> <?= h(tr('فواتير مفتوحة', 'open invoices')) ?></div>
<?php if ($debt['partial_invoices'] > 0): ?>
<div class="small text-warning"><?= h($debt['partial_invoices']) ?> <?= h(tr('منها دفعات جزئية', 'partially paid')) ?></div>
<?php endif; ?>
<div class="small text-muted"><?= h($debt['count']) ?> <?= h(tr('فواتير', 'bills')) ?></div>
</div>
<span class="badge bg-danger rounded-pill fs-6"><?= h(currency($debt['total'])) ?></span>
</li>
@ -146,7 +163,7 @@ require_once 'includes/header.php';
<div class="col-lg-8 mb-4">
<div class="card shadow-sm border-0 h-100">
<div class="card-header bg-white border-bottom-0 pt-4 pb-0">
<h6 class="m-0 font-weight-bold text-primary"><i class="bi bi-receipt"></i> <?= h(tr('الفواتير غير المدفوعة', 'Unpaid Bills')) ?></h6>
<h6 class="m-0 font-weight-bold text-primary"><i class="bi bi-receipt"></i> <?= h(tr('الفواتير غير المدفوعة والجزئية', 'Unpaid & Partial Invoices')) ?></h6>
</div>
<div class="card-body p-0">
<div class="table-responsive">
@ -156,31 +173,38 @@ require_once 'includes/header.php';
<th><?= h(tr('رقم الفاتورة', 'Receipt No')) ?></th>
<th><?= h(tr('العميل', 'Customer')) ?></th>
<th><?= h(tr('التاريخ', 'Date')) ?></th>
<th><?= h(tr('المبلغ', 'Amount')) ?></th>
<th><?= h(tr('الإجمالي', 'Total')) ?></th>
<th><?= h(tr('المدفوع', 'Paid')) ?></th>
<th><?= h(tr('المتبقي', 'Due')) ?></th>
<th><?= h(tr('الحالة', 'Status')) ?></th>
<th><?= h(tr('الإجراء', 'Action')) ?></th>
</tr>
</thead>
<tbody>
<?php if (empty($unpaidSales)): ?>
<tr>
<td colspan="5" class="text-center py-4 text-muted"><?= h(tr('لا توجد فواتير غير مدفوعة.', 'No unpaid bills.')) ?></td>
<td colspan="8" class="text-center py-4 text-muted"><?= h(tr('لا توجد فواتير غير مدفوعة أو جزئية.', 'No unpaid or partial invoices.')) ?></td>
</tr>
<?php else: ?>
<?php foreach ($unpaidSales as $sale): ?>
<?php $paymentSummary = sale_payment_summary($sale); ?>
<tr>
<td>
<a href="<?= h(url_for('sale.php', ['id' => $sale['id']])) ?>" class="fw-bold text-decoration-none">
<?= h($sale['receipt_no']) ?>
</a>
</td>
<td>
<?= h($sale['c_name'] ?: $sale['customer_name'] ?: '-') ?>
</td>
<td><?= h($sale['c_name'] ?: $sale['customer_name'] ?: '-') ?></td>
<td><?= h(date('Y-m-d', strtotime((string)$sale['sale_date']))) ?></td>
<td class="fw-bold text-danger"><?= h(currency((float)$sale['total_amount'])) ?></td>
<td class="fw-semibold"><?= h(currency((float)$sale['total_amount'])) ?></td>
<td class="text-primary"><?= h(currency((float)$paymentSummary['paid_amount'])) ?></td>
<td class="fw-bold text-danger"><?= h(currency((float)$paymentSummary['due_amount'])) ?></td>
<td>
<button class="btn btn-sm btn-outline-success rounded-pill px-3" onclick="markPaid(<?= $sale['id'] ?>)">
<i class="bi bi-cash-coin"></i> <?= h(tr('استلام', 'Receive')) ?>
<span class="badge <?= h(payment_status_badge_class($paymentSummary['payment_status'])) ?>"><?= h(payment_status_label($paymentSummary['payment_status'])) ?></span>
</td>
<td>
<button class="btn btn-sm btn-outline-success rounded-pill px-3" onclick="receivePayment(<?= (int) $sale['id'] ?>, <?= json_encode((float) $paymentSummary['due_amount']) ?>, <?= ($sale['status'] ?? 'completed') === 'order' ? 'true' : 'false' ?>)">
<i class="bi bi-cash-coin"></i> <?= h(tr('استلام دفعة', 'Receive Payment')) ?>
</button>
</td>
</tr>
@ -195,20 +219,48 @@ require_once 'includes/header.php';
</div>
<script>
function markPaid(id) {
Swal.fire({
title: "<?= h(tr('تأكيد استلام المبلغ؟', 'Confirm payment receipt?')) ?>",
text: "<?= h(tr('سيتم تحويل هذه الفاتورة إلى مدفوعة.', 'This bill will be marked as paid.')) ?>",
icon: "question",
async function receivePayment(id, dueAmount, completeOrder = false) {
const { value: paymentAmount } = await Swal.fire({
title: '<?= h(tr('استلام دفعة', 'Receive Payment')) ?>',
text: '<?= h(tr('أدخل المبلغ المستلم لهذه الفاتورة.', 'Enter the amount received for this invoice.')) ?>',
input: 'number',
inputAttributes: { min: '0.001', step: '0.001', max: String(dueAmount) },
inputValue: Number(dueAmount).toFixed(3),
showCancelButton: true,
confirmButtonColor: "#198754",
confirmButtonText: "<?= h(tr('نعم، تم الاستلام', 'Yes, Received')) ?>",
cancelButtonText: "<?= h(tr('إلغاء', 'Cancel')) ?>"
}).then((result) => {
if (result.isConfirmed) {
window.location.href = "debts.php?mark_paid=" + id;
confirmButtonColor: '#198754',
confirmButtonText: '<?= h(tr('حفظ الدفعة', 'Save Payment')) ?>',
cancelButtonText: '<?= h(tr('إلغاء', 'Cancel')) ?>',
inputValidator: (value) => {
const amount = parseFloat(value || '0');
if (!amount || amount <= 0) {
return '<?= h(tr('أدخل مبلغاً صحيحاً.', 'Enter a valid amount.')) ?>';
}
if (amount - dueAmount > 0.0005) {
return '<?= h(tr('المبلغ لا يمكن أن يتجاوز المتبقي.', 'Amount cannot exceed the due balance.')) ?>';
}
return null;
}
});
if (!paymentAmount) {
return;
}
const formData = new FormData();
formData.append('sale_id', String(id));
formData.append('payment_amount', String(paymentAmount));
if (completeOrder) {
formData.append('complete_order', '1');
}
const response = await fetch('api/sales_payment.php', { method: 'POST', body: formData });
const data = await response.json();
if (data.success) {
await Swal.fire({ icon: 'success', text: data.message, confirmButtonText: 'OK' });
window.location.reload();
} else {
Swal.fire({ icon: 'error', text: data.error || '<?= h(tr('تعذر تسجيل الدفعة.', 'Could not record the payment.')) ?>' });
}
}
</script>

View File

@ -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';
</div>
<div class="mb-3">
<label class="form-label"><?= h(tr('رقم الهاتف', 'Phone Number')) ?></label>
<input type="text" name="customer_phone" class="form-control custom-input" required value="<?= h($editOrder['customer_phone'] ?? '' ) ?>">
<div class="input-group" dir="ltr">
<span class="input-group-text">968</span>
<input type="text" name="customer_phone" class="form-control custom-input" required inputmode="numeric" maxlength="8" pattern="\d{8}" placeholder="91234567" value="<?= h(normalize_oman_phone((string) ($editOrder['customer_phone'] ?? ''))) ?>">
</div>
</div>
<div class="mb-3">
<label class="form-label"><?= h(tr('العنوان', 'Address')) ?></label>

View File

@ -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';
<div class="mb-3">
<label class="form-label"><?= h(tr('طريقة الدفع', 'Payment Method')) ?></label>
<select class="form-select custom-input" name="payment_method">
<option value="cash" <?= $editSale['payment_method'] === 'cash' ? 'selected' : '' ?>><?= h(tr('نقداً', 'Cash')) ?></option>
<option value="card" <?= $editSale['payment_method'] === 'card' ? 'selected' : '' ?>><?= h(tr('بطاقة ائتمان', 'Credit Card')) ?></option>
<option value="transfer" <?= $editSale['payment_method'] === 'transfer' ? 'selected' : '' ?>><?= h(tr('تحويل بنكي', 'Bank Transfer')) ?></option>
<option value="pay_later" <?= $editSale['payment_method'] === 'pay_later' ? 'selected' : '' ?>><?= h(tr('آجل (Pay Later)', 'Pay Later')) ?></option>
<option value="cash" <?= ($paymentMethod ?? $editSale['payment_method']) === 'cash' ? 'selected' : '' ?>><?= h(tr('نقداً', 'Cash')) ?></option>
<option value="card" <?= ($paymentMethod ?? $editSale['payment_method']) === 'card' ? 'selected' : '' ?>><?= h(tr('بطاقة ائتمان', 'Credit Card')) ?></option>
<option value="transfer" <?= ($paymentMethod ?? $editSale['payment_method']) === 'transfer' ? 'selected' : '' ?>><?= h(tr('تحويل بنكي', 'Bank Transfer')) ?></option>
<option value="pay_later" <?= ($paymentMethod ?? $editSale['payment_method']) === 'pay_later' ? 'selected' : '' ?>><?= h(tr('آجل (Pay Later)', 'Pay Later')) ?></option>
</select>
</div>
<div class="mb-3">
<label class="form-label" for="payment_amount"><?= h(tr('المبلغ المدفوع الآن', 'Paid Now')) ?></label>
<input type="number" class="form-control custom-input" id="payment_amount" name="payment_amount" min="0" step="0.001" value="<?= h($paymentAmountInput) ?>" placeholder="0.000">
<div class="form-text" id="paymentAmountHint"><?= h(tr('يمكنك تعديل الدفعة الجزئية وسيتم إعادة احتساب المتبقي تلقائياً.', 'Adjust the paid amount and the remaining balance will be recalculated automatically.')) ?></div>
</div>
<div class="mb-4">
<label class="form-label"><?= h(tr('ملاحظات (اختياري)', 'Notes (Optional)')) ?></label>
<textarea class="form-control custom-input" name="notes" rows="2" placeholder="<?= h(tr('أي ملاحظات إضافية...', 'Any additional notes...')) ?>"><?= h($editSale['notes'] ?? '' ) ?></textarea>
@ -447,7 +469,9 @@ require __DIR__ . '/includes/header.php';
<script>
const catalogData = <?= json_encode($catalog, JSON_UNESCAPED_UNICODE) ?>;
const catalogArray = Object.values(catalogData);
const partialPaymentText = '<?= h(tr('متبقٍ بعد الحفظ:', 'Due after save:')) ?>';
let invoiceItems = {};
let currentInvoiceTotal = 0;
// Prepopulate from editSale
const initialItemsJson = <?= empty($editSale['items_json']) ? '[]' : $editSale['items_json'] ?>;
@ -698,26 +722,68 @@ function renderInvoice() {
cartJson.value = JSON.stringify(cartData);
}
function updatePaymentAmountHint() {
const paymentAmountField = document.getElementById('payment_amount');
const paymentAmountHint = document.getElementById('paymentAmountHint');
if (!paymentAmountField || !paymentAmountHint) {
return;
}
const entered = Math.max(0, parseFloat(paymentAmountField.value || '0') || 0);
const due = Math.max(0, currentInvoiceTotal - Math.min(entered, currentInvoiceTotal));
paymentAmountHint.innerText = partialPaymentText + ' ' + due.toFixed(3) + currencySuffix;
}
function syncPaymentAmount(force = false) {
const paymentMethodField = document.querySelector('select[name="payment_method"]');
const paymentAmountField = document.getElementById('payment_amount');
if (!paymentMethodField || !paymentAmountField) {
return;
}
const isManual = paymentAmountField.dataset.manual === '1';
if (force || !isManual) {
const defaultAmount = paymentMethodField.value === 'pay_later' ? 0 : currentInvoiceTotal;
paymentAmountField.value = defaultAmount.toFixed(3);
paymentAmountField.dataset.manual = '0';
}
updatePaymentAmountHint();
}
function updateTotals(total, vat) {
const subtotal = total;
const finalTotal = subtotal + vat;
currentInvoiceTotal = finalTotal;
document.getElementById('displaySubtotal').innerText = subtotal.toFixed(3);
document.getElementById('displayVat').innerText = vat.toFixed(3);
document.getElementById('displayTotal').innerText = finalTotal.toFixed(3) + currencySuffix;
updatePaymentAmountHint();
}
const paymentMethodField = document.querySelector('select[name="payment_method"]');
const paymentAmountField = document.getElementById('payment_amount');
if (paymentMethodField && paymentAmountField) {
paymentAmountField.dataset.manual = paymentAmountField.value !== '' ? '1' : '0';
paymentMethodField.addEventListener('change', () => syncPaymentAmount());
paymentAmountField.addEventListener('input', () => {
paymentAmountField.dataset.manual = '1';
updatePaymentAmountHint();
});
}
renderInvoice();
// Intercept form submission to check if items exist
document.getElementById('smart-sale-form').addEventListener('submit', function(e) {
const paymentMethod = document.querySelector('select[name="payment_method"]').value;
const customerId = document.getElementById('formCustomerId').value;
const paymentAmount = Math.max(0, parseFloat(document.getElementById('payment_amount').value || '0') || 0);
if (Object.keys(invoiceItems).length === 0) {
e.preventDefault();
Swal.fire({icon: 'warning', text: '<?= h(tr('الرجاء إضافة أصناف للفاتورة أولاً.', 'Please add items to the invoice first.')) ?>'});
} else if (paymentMethod === 'pay_later' && !customerId) {
} else if (paymentAmount > currentInvoiceTotal + 0.0005) {
e.preventDefault();
Swal.fire({icon: 'warning', text: '<?= h(tr('يجب اختيار عميل مسجل للدفع الآجل.', 'You must select a registered customer for pay later.')) ?>'});
Swal.fire({icon: 'warning', text: '<?= h(tr('المبلغ المدفوع لا يمكن أن يتجاوز إجمالي الفاتورة.', 'Paid amount cannot exceed the invoice total.')) ?>'});
} else if (paymentAmount < currentInvoiceTotal && !customerId) {
e.preventDefault();
Swal.fire({icon: 'warning', text: '<?= h(tr('يجب اختيار عميل مسجل عند وجود مبلغ متبقٍ.', 'Select a registered customer when there is a remaining balance.')) ?>'});
}
});
</script>

View File

@ -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();

View File

@ -1,7 +1,7 @@
<?php if (isset($user) && $user && in_array($user['role'], ['owner', 'manager'])): ?>
<!-- Settings Modal -->
<div class="modal fade" id="settingsModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content">
<form action="api/settings.php" method="POST" enctype="multipart/form-data">
<div class="modal-header">
@ -9,95 +9,200 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row g-3">
<div class="col-md-12">
<label class="form-label"><?= h(tr('المنطقة الزمنية (Timezone)', 'Timezone')) ?></label>
<select class="form-select" name="timezone" required>
<?php
$zones = timezone_identifiers_list();
$current_tz = get_setting('timezone', 'UTC');
foreach($zones as $z): ?>
<option value="<?= h($z) ?>" <?= $z === $current_tz ? 'selected' : '' ?>><?= h($z) ?></option>
<?php endforeach; ?>
</select>
<ul class="nav nav-tabs mb-4" id="settingsTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="settings-company-tab" data-bs-toggle="tab" data-bs-target="#settings-company-pane" type="button" role="tab" aria-controls="settings-company-pane" aria-selected="true">
<?= h(tr('بيانات الشركة', 'Company')) ?>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="settings-wablas-tab" data-bs-toggle="tab" data-bs-target="#settings-wablas-pane" type="button" role="tab" aria-controls="settings-wablas-pane" aria-selected="false">
<?= h(tr('واتساب', 'WhatsApp')) ?>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="settings-email-tab" data-bs-toggle="tab" data-bs-target="#settings-email-pane" type="button" role="tab" aria-controls="settings-email-pane" aria-selected="false">
<?= h(tr('البريد الإلكتروني', 'Email')) ?>
</button>
</li>
</ul>
<div class="tab-content" id="settingsTabsContent">
<div class="tab-pane fade show active" id="settings-company-pane" role="tabpanel" aria-labelledby="settings-company-tab" tabindex="0">
<div class="row g-3">
<div class="col-md-12">
<label class="form-label"><?= h(tr('المنطقة الزمنية (Timezone)', 'Timezone')) ?></label>
<select class="form-select" name="timezone" required>
<?php
$zones = timezone_identifiers_list();
$current_tz = get_setting('timezone', 'UTC');
foreach($zones as $z): ?>
<option value="<?= h($z) ?>" <?= $z === $current_tz ? 'selected' : '' ?>><?= h($z) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr('اسم الشركة (عربي)', 'Company Name (AR)')) ?></label>
<input type="text" class="form-control" name="company_name_ar" value="<?= h(get_setting('company_name_ar')) ?>" required>
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr('اسم الشركة (إنجليزي)', 'Company Name (EN)')) ?></label>
<input type="text" class="form-control" name="company_name_en" value="<?= h(get_setting('company_name_en')) ?>" required>
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr('النسبة الضريبية %', 'VAT Percentage %')) ?></label>
<input type="number" step="0.01" class="form-control" name="vat_percentage" value="<?= h(get_setting('vat_percentage', 5)) ?>" required>
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr('الرقم الضريبي', 'VAT Number')) ?></label>
<input type="text" class="form-control" name="company_vat_number" value="<?= h(get_setting('company_vat_number')) ?>">
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr('رقم الهاتف', 'Phone Number')) ?></label>
<div class="input-group" dir="ltr">
<span class="input-group-text">968</span>
<input type="text" class="form-control" name="company_phone" value="<?= h(normalize_oman_phone((string) get_setting('company_phone'))) ?>" inputmode="numeric" maxlength="8" pattern="\d{8}" placeholder="91234567">
</div>
<div class="form-text"><?= h(tr('يُحفظ الرقم محلياً من 8 خانات ويُرسل مع المقدمة 968.', 'Phone is stored locally as 8 digits and sent with the 968 prefix.')) ?></div>
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr('البريد الإلكتروني', 'Email')) ?></label>
<input type="email" class="form-control" name="company_email" value="<?= h(get_setting('company_email')) ?>">
</div>
<div class="col-md-12">
<label class="form-label"><?= h(tr('العنوان', 'Address')) ?></label>
<textarea class="form-control" name="company_address" rows="2"><?= h(get_setting('company_address')) ?></textarea>
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr('الشعار (Logo)', 'Logo')) ?></label>
<input type="file" class="form-control" name="company_logo" accept="image/*">
<?php if (get_setting('company_logo')): ?>
<div class="mt-2"><img src="<?= h(get_setting('company_logo')) ?>" height="50" style="background: #f8f9fa; padding: 5px; border-radius: 4px;"></div>
<?php endif; ?>
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr('الأيقونة (Favicon)', 'Favicon')) ?></label>
<input type="file" class="form-control" name="company_favicon" accept="image/x-icon,image/png,image/jpeg">
<?php if (get_setting('company_favicon')): ?>
<div class="mt-2"><img src="<?= h(get_setting('company_favicon')) ?>" height="32" style="background: #f8f9fa; padding: 2px; border-radius: 4px;"></div>
<?php endif; ?>
</div>
</div>
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr('اسم الشركة (عربي)', 'Company Name (AR)')) ?></label>
<input type="text" class="form-control" name="company_name_ar" value="<?= h(get_setting('company_name_ar')) ?>" required>
<div class="tab-pane fade" id="settings-wablas-pane" role="tabpanel" aria-labelledby="settings-wablas-tab" tabindex="0">
<div class="row g-3">
<div class="col-md-12">
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-3 border rounded-4 px-4 py-3 bg-light">
<div>
<h6 class="mb-1 fw-bold"><?= h(tr('إعدادات واتساب Wablas', 'Wablas WhatsApp Settings')) ?></h6>
<div class="form-text mb-0"><?= h(tr('المتغيرات المتاحة داخل القوالب: {customer_name}, {order_id}, {status_label}, {total_amount}, {customer_address}, {items_summary}', 'Available template placeholders: {customer_name}, {order_id}, {status_label}, {total_amount}, {customer_address}, {items_summary}')) ?></div>
</div>
<div class="form-check form-switch fs-6 m-0">
<input type="hidden" name="wablas_enabled" value="0">
<input class="form-check-input" type="checkbox" role="switch" id="wablasEnabledSwitch" name="wablas_enabled" value="1" <?= wablas_is_enabled() ? 'checked' : '' ?>>
<label class="form-check-label fw-semibold" for="wablasEnabledSwitch"><?= h(tr('تفعيل الإرسال التلقائي', 'Enable automatic sending')) ?></label>
<div class="form-text"><?= h(tr('عند الإيقاف لن تُرسل رسائل واتساب تلقائياً من الطلبات أو تغييرات الحالة.', 'When disabled, order and status-change WhatsApp messages will not be sent automatically.')) ?></div>
</div>
</div>
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr('Wablas Token', 'Wablas Token')) ?></label>
<input type="text" class="form-control" name="wablas_token" value="<?= h(get_setting('wablas_token')) ?>">
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr('Wablas Secret Key', 'Wablas Secret Key')) ?></label>
<input type="password" class="form-control" name="wablas_secret_key" value="<?= h(get_setting('wablas_secret_key')) ?>">
</div>
<div class="col-md-12">
<div class="card border-0 shadow-sm bg-light-subtle">
<div class="card-body">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-2 mb-3">
<div>
<h6 class="fw-bold mb-1"><?= h(tr('رسالة اختبار', 'Test Message')) ?></h6>
<p class="text-muted small mb-0"><?= h(tr('يمكنك إرسال رسالة اختبار فورية باستخدام البيانات الموجودة حالياً داخل هذا النموذج قبل الحفظ.', 'Send an instant test message using the values currently filled in this form before saving.')) ?></p>
</div>
<button type="submit" class="btn btn-outline-success align-self-start align-self-md-center" formaction="api/wablas_test.php" formmethod="POST">
<i class="bi bi-whatsapp me-1"></i><?= h(tr('إرسال اختبار', 'Send Test')) ?>
</button>
</div>
<div class="row g-3">
<div class="col-md-4">
<label class="form-label"><?= h(tr('رقم الاختبار', 'Test Phone')) ?></label>
<div class="input-group" dir="ltr">
<span class="input-group-text">968</span>
<input type="text" class="form-control" name="wablas_test_phone" value="<?= h(normalize_oman_phone((string) get_setting('company_phone'))) ?>" inputmode="numeric" maxlength="8" pattern="\d{8}" placeholder="91234567">
</div>
</div>
<div class="col-md-8">
<label class="form-label"><?= h(tr('نص الرسالة التجريبية', 'Test Message Text')) ?></label>
<textarea class="form-control" name="wablas_test_message" rows="3" placeholder="<?= h(tr('اكتب رسالة الاختبار هنا', 'Write the test message here')) ?>"><?= h(tr('هذه رسالة واتساب تجريبية من النظام.', 'This is a WhatsApp test message from the system.')) ?></textarea>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-12">
<label class="form-label"><?= h(tr('قالب رسالة إنشاء الطلب', 'Order Created Template')) ?></label>
<textarea class="form-control" name="wablas_template_created" rows="3"><?= h(get_setting('wablas_template_created', wablas_default_order_template('created'))) ?></textarea>
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr('قالب قيد الانتظار', 'Pending Template')) ?></label>
<textarea class="form-control" name="wablas_template_pending" rows="3"><?= h(get_setting('wablas_template_pending', wablas_default_order_template('pending'))) ?></textarea>
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr('قالب مقبول', 'Accepted Template')) ?></label>
<textarea class="form-control" name="wablas_template_accepted" rows="3"><?= h(get_setting('wablas_template_accepted', wablas_default_order_template('accepted'))) ?></textarea>
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr('قالب مكتمل', 'Completed Template')) ?></label>
<textarea class="form-control" name="wablas_template_completed" rows="3"><?= h(get_setting('wablas_template_completed', wablas_default_order_template('completed'))) ?></textarea>
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr('قالب مرفوض', 'Rejected Template')) ?></label>
<textarea class="form-control" name="wablas_template_rejected" rows="3"><?= h(get_setting('wablas_template_rejected', wablas_default_order_template('rejected'))) ?></textarea>
</div>
</div>
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr('اسم الشركة (إنجليزي)', 'Company Name (EN)')) ?></label>
<input type="text" class="form-control" name="company_name_en" value="<?= h(get_setting('company_name_en')) ?>" required>
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr('النسبة الضريبية %', 'VAT Percentage %')) ?></label>
<input type="number" step="0.01" class="form-control" name="vat_percentage" value="<?= h(get_setting('vat_percentage', 5)) ?>" required>
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr('الرقم الضريبي', 'VAT Number')) ?></label>
<input type="text" class="form-control" name="company_vat_number" value="<?= h(get_setting('company_vat_number')) ?>">
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr('رقم الهاتف', 'Phone Number')) ?></label>
<input type="text" class="form-control" name="company_phone" value="<?= h(get_setting('company_phone')) ?>">
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr('البريد الإلكتروني', 'Email')) ?></label>
<input type="email" class="form-control" name="company_email" value="<?= h(get_setting('company_email')) ?>">
</div>
<div class="col-md-12">
<label class="form-label"><?= h(tr('العنوان', 'Address')) ?></label>
<textarea class="form-control" name="company_address" rows="2"><?= h(get_setting('company_address')) ?></textarea>
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr('الشعار (Logo)', 'Logo')) ?></label>
<input type="file" class="form-control" name="company_logo" accept="image/*">
<?php if (get_setting('company_logo')): ?>
<div class="mt-2"><img src="<?= h(get_setting('company_logo')) ?>" height="50" style="background: #f8f9fa; padding: 5px; border-radius: 4px;"></div>
<?php endif; ?>
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr('الأيقونة (Favicon)', 'Favicon')) ?></label>
<input type="file" class="form-control" name="company_favicon" accept="image/x-icon,image/png,image/jpeg">
<?php if (get_setting('company_favicon')): ?>
<div class="mt-2"><img src="<?= h(get_setting('company_favicon')) ?>" height="32" style="background: #f8f9fa; padding: 2px; border-radius: 4px;"></div>
<?php endif; ?>
</div>
<hr>
<div class="col-md-12">
<h6 class="mb-0 fw-bold"><?= h(tr("إعدادات البريد الإلكتروني (SMTP)", "SMTP Email Settings")) ?></h6>
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr("خادم SMTP (Host)", "SMTP Host")) ?></label>
<input type="text" class="form-control" name="smtp_host" value="<?= h(get_setting("smtp_host")) ?>">
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr("منفذ SMTP (Port)", "SMTP Port")) ?></label>
<input type="number" class="form-control" name="smtp_port" value="<?= h(get_setting("smtp_port", 587)) ?>">
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr("مستخدم SMTP (User)", "SMTP User")) ?></label>
<input type="text" class="form-control" name="smtp_user" value="<?= h(get_setting("smtp_user")) ?>">
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr("كلمة مرور SMTP (Pass)", "SMTP Password")) ?></label>
<input type="password" class="form-control" name="smtp_pass" value="<?= h(get_setting("smtp_pass")) ?>">
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr("تشفير SMTP (Secure)", "SMTP Secure (tls/ssl)")) ?></label>
<select class="form-select" name="smtp_secure">
<option value="tls" <?= get_setting("smtp_secure", "tls") === "tls" ? "selected" : "" ?>>TLS</option>
<option value="ssl" <?= get_setting("smtp_secure") === "ssl" ? "selected" : "" ?>>SSL</option>
<option value="" <?= get_setting("smtp_secure") === "" ? "selected" : "" ?>>None</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr("البريد المرسل (From Email)", "From Email")) ?></label>
<input type="email" class="form-control" name="mail_from" value="<?= h(get_setting("mail_from")) ?>">
</div>
<div class="col-md-12">
<label class="form-label"><?= h(tr("اسم المرسل (From Name)", "From Name")) ?></label>
<input type="text" class="form-control" name="mail_from_name" value="<?= h(get_setting("mail_from_name")) ?>">
<div class="tab-pane fade" id="settings-email-pane" role="tabpanel" aria-labelledby="settings-email-tab" tabindex="0">
<div class="row g-3">
<div class="col-md-12">
<h6 class="mb-0 fw-bold"><?= h(tr("إعدادات البريد الإلكتروني (SMTP)", "SMTP Email Settings")) ?></h6>
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr("خادم SMTP (Host)", "SMTP Host")) ?></label>
<input type="text" class="form-control" name="smtp_host" value="<?= h(get_setting("smtp_host")) ?>">
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr("منفذ SMTP (Port)", "SMTP Port")) ?></label>
<input type="number" class="form-control" name="smtp_port" value="<?= h(get_setting("smtp_port", 587)) ?>">
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr("مستخدم SMTP (User)", "SMTP User")) ?></label>
<input type="text" class="form-control" name="smtp_user" value="<?= h(get_setting("smtp_user")) ?>">
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr("كلمة مرور SMTP (Pass)", "SMTP Password")) ?></label>
<input type="password" class="form-control" name="smtp_pass" value="<?= h(get_setting("smtp_pass")) ?>">
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr("تشفير SMTP (Secure)", "SMTP Secure (tls/ssl)")) ?></label>
<select class="form-select" name="smtp_secure">
<option value="tls" <?= get_setting("smtp_secure", "tls") === "tls" ? "selected" : "" ?>>TLS</option>
<option value="ssl" <?= get_setting("smtp_secure") === "ssl" ? "selected" : "" ?>>SSL</option>
<option value="" <?= get_setting("smtp_secure") === "" ? "selected" : "" ?>>None</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr("البريد المرسل (From Email)", "From Email")) ?></label>
<input type="email" class="form-control" name="mail_from" value="<?= h(get_setting("mail_from")) ?>">
</div>
<div class="col-md-12">
<label class="form-label"><?= h(tr("اسم المرسل (From Name)", "From Name")) ?></label>
<input type="text" class="form-control" name="mail_from_name" value="<?= h(get_setting("mail_from_name")) ?>">
</div>
</div>
</div>
</div>
</div>

View File

@ -4,6 +4,7 @@ $user = require_roles(['owner', 'manager', 'cashier']);
$pageTitle = $saleMode === 'normal' ? tr('إنشاء فاتورة ضريبية', 'Create Tax Invoice') : tr('نقاط البيع', 'POS Sale');
$activeNav = $saleMode === 'normal' ? 'normal' : 'pos';
$error = '';
$paymentAmountInput = (string) ($_POST['payment_amount'] ?? '');
$catalog = catalog();
$allowedBranches = get_user_branches($user);
@ -18,7 +19,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'] ?? '[]');
@ -30,8 +31,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$error = tr('اختر طريقة دفع صحيحة.', 'Choose a valid payment method.');
} elseif ($saleStatus === 'order' && !$customerId) {
$error = tr('يجب اختيار عميل للطلب المسبق.', 'You must select a customer for a pre-order.');
} 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 {
@ -87,6 +86,18 @@ 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'];
$saleId = create_sale([
'receipt_no' => receipt_code(),
@ -98,12 +109,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' => $normalized,
'item_count' => $itemCount,
'subtotal' => $subtotal,
'vat_amount' => $totalVat,
'total_amount' => $subtotal + $totalVat,
'total_amount' => $totalAmount,
'status' => $saleStatus,
'notes' => $notes !== '' ? $notes : null,
]);
@ -384,12 +397,17 @@ require __DIR__ . '/header.php';
<div class="mb-3">
<label class="form-label"><?= h(tr('طريقة الدفع', 'Payment Method')) ?></label>
<select class="form-select custom-input" name="payment_method">
<option value="cash"><?= h(tr('نقداً', 'Cash')) ?></option>
<option value="card"><?= h(tr('بطاقة ائتمان', 'Credit Card')) ?></option>
<option value="transfer"><?= h(tr('تحويل بنكي', 'Bank Transfer')) ?></option>
<option value="pay_later"><?= h(tr('آجل (Pay Later)', 'Pay Later')) ?></option>
<option value="cash" <?= ($paymentMethod ?? 'cash') === 'cash' ? 'selected' : '' ?>><?= h(tr('نقداً', 'Cash')) ?></option>
<option value="card" <?= ($paymentMethod ?? '') === 'card' ? 'selected' : '' ?>><?= h(tr('بطاقة ائتمان', 'Credit Card')) ?></option>
<option value="transfer" <?= ($paymentMethod ?? '') === 'transfer' ? 'selected' : '' ?>><?= h(tr('تحويل بنكي', 'Bank Transfer')) ?></option>
<option value="pay_later" <?= ($paymentMethod ?? '') === 'pay_later' ? 'selected' : '' ?>><?= h(tr('آجل (Pay Later)', 'Pay Later')) ?></option>
</select>
</div>
<div class="mb-3">
<label class="form-label" for="payment_amount"><?= h(tr('المبلغ المدفوع الآن', 'Paid Now')) ?></label>
<input type="number" class="form-control custom-input" id="payment_amount" name="payment_amount" min="0" step="0.001" value="<?= h($paymentAmountInput) ?>" placeholder="0.000">
<div class="form-text" id="paymentAmountHint"><?= h(tr('يمكنك إدخال دفعة جزئية وسيتم تتبع الباقي تلقائياً.', 'You can enter a partial payment and the remaining balance will be tracked automatically.')) ?></div>
</div>
<div class="mb-4">
<label class="form-label"><?= h(tr('ملاحظات (اختياري)', 'Notes (Optional)')) ?></label>
<textarea class="form-control custom-input" name="notes" rows="2" placeholder="<?= h(tr('أي ملاحظات إضافية...', 'Any additional notes...')) ?>"></textarea>
@ -436,7 +454,10 @@ require __DIR__ . '/header.php';
</div>
<div class="mb-3">
<label class="form-label text-muted small mb-1"><?= h(tr('رقم الهاتف', 'Phone')) ?></label>
<input type="text" id="ncPhone" class="form-control rounded-3" dir="ltr">
<div class="input-group" dir="ltr">
<span class="input-group-text">968</span>
<input type="text" id="ncPhone" class="form-control rounded-end-3" inputmode="numeric" maxlength="8" pattern="\d{8}" placeholder="91234567">
</div>
</div>
<div class="d-grid mt-4">
<button class="btn btn-primary rounded-pill fw-semibold shadow-sm" onclick="saveNewCustomer()"><?= h(tr('حفظ العميل', 'Save Customer')) ?></button>
@ -450,7 +471,9 @@ require __DIR__ . '/header.php';
<script>
const catalogData = <?= json_encode($catalog, JSON_UNESCAPED_UNICODE) ?>;
const catalogArray = Object.values(catalogData);
const partialPaymentText = '<?= h(tr('متبقٍ بعد الحفظ:', 'Due after save:')) ?>';
let invoiceItems = {};
let currentInvoiceTotal = 0;
const searchInput = document.getElementById('itemSearchInput');
const dropdown = document.getElementById('itemDropdown');
@ -468,7 +491,8 @@ custInput.addEventListener('input', function() {
const q = this.value.toLowerCase().trim();
custDropdown.innerHTML = '';
if (q.length < 2) {
document.getElementById('formCustomerId').value = c.id; custDropdown.classList.remove('show');
document.getElementById('formCustomerId').value = '';
custDropdown.classList.remove('show');
return;
}
@ -481,22 +505,23 @@ custInput.addEventListener('input', function() {
matches.forEach(c => {
const div = document.createElement('div');
div.className = 'search-item-row';
div.innerHTML = `<strong>${c.name}</strong> ${c.phone ? '<small class="text-muted ms-2">'+c.phone+'</small>' : ''}`;
div.innerHTML = `<strong>${c.name}</strong> ${c.phone ? '<small class="text-muted ms-2">968 '+c.phone+'</small>' : ''}`;
div.onclick = function() {
custInput.value = c.name + (c.phone ? ' - ' + c.phone : '');
custInput.value = c.name + (c.phone ? ' - 968 ' + c.phone : '');
document.getElementById('formCustomerId').value = c.id; custDropdown.classList.remove('show');
};
custDropdown.appendChild(div);
});
custDropdown.classList.add('show');
} else {
document.getElementById('formCustomerId').value = c.id; custDropdown.classList.remove('show');
document.getElementById('formCustomerId').value = '';
custDropdown.classList.remove('show');
}
});
document.addEventListener('click', function(e) {
if (!custInput.contains(e.target) && !custDropdown.contains(e.target)) {
document.getElementById('formCustomerId').value = c.id; custDropdown.classList.remove('show');
custDropdown.classList.remove('show');
}
});
@ -530,7 +555,7 @@ async function saveNewCustomer() {
const data = await res.json();
if (data.success) {
customersData.push(data.customer);
custInput.value = data.customer.name + (data.customer.phone ? ' - ' + data.customer.phone : '');
custInput.value = data.customer.name + (data.customer.phone ? ' - 968 ' + data.customer.phone : '');
document.getElementById('formCustomerId').value = data.customer.id;
newCustomerModalObj.hide();
const Toast = Swal.mixin({ toast: true, position: 'top-end', showConfirmButton: false, timer: 2000 });
@ -723,12 +748,54 @@ function renderInvoice() {
cartJson.value = JSON.stringify(cartData);
}
function updatePaymentAmountHint() {
const paymentAmountField = document.getElementById('payment_amount');
const paymentAmountHint = document.getElementById('paymentAmountHint');
if (!paymentAmountField || !paymentAmountHint) {
return;
}
const entered = Math.max(0, parseFloat(paymentAmountField.value || '0') || 0);
const due = Math.max(0, currentInvoiceTotal - Math.min(entered, currentInvoiceTotal));
paymentAmountHint.innerText = partialPaymentText + ' ' + due.toFixed(3) + currencySuffix;
}
function syncPaymentAmount(force = false) {
const paymentMethodField = document.querySelector('select[name="payment_method"]');
const paymentAmountField = document.getElementById('payment_amount');
if (!paymentMethodField || !paymentAmountField) {
return;
}
const isManual = paymentAmountField.dataset.manual === '1';
if (force || !isManual) {
const defaultAmount = paymentMethodField.value === 'pay_later' ? 0 : currentInvoiceTotal;
paymentAmountField.value = defaultAmount.toFixed(3);
paymentAmountField.dataset.manual = '0';
}
updatePaymentAmountHint();
}
function updateTotals(total, vat) {
const subtotal = total;
const finalTotal = subtotal + vat;
currentInvoiceTotal = finalTotal;
document.getElementById('displaySubtotal').innerText = subtotal.toFixed(3);
document.getElementById('displayVat').innerText = vat.toFixed(3);
document.getElementById('displayTotal').innerText = finalTotal.toFixed(3) + currencySuffix;
syncPaymentAmount();
}
const paymentMethodField = document.querySelector('select[name="payment_method"]');
const paymentAmountField = document.getElementById('payment_amount');
if (paymentMethodField && paymentAmountField) {
if (paymentAmountField.value !== '') {
paymentAmountField.dataset.manual = '1';
updatePaymentAmountHint();
}
paymentMethodField.addEventListener('change', () => syncPaymentAmount());
paymentAmountField.addEventListener('input', () => {
paymentAmountField.dataset.manual = '1';
updatePaymentAmountHint();
});
}
// Intercept form submission to check if items exist
@ -736,15 +803,22 @@ document.getElementById('smart-sale-form').addEventListener('submit', function(e
const paymentMethod = document.querySelector('select[name="payment_method"]').value;
const saleStatus = document.querySelector('select[name="sale_status"]').value;
const customerId = document.getElementById('formCustomerId').value;
const paymentAmount = Math.max(0, parseFloat(document.getElementById('payment_amount').value || '0') || 0);
if (Object.keys(invoiceItems).length === 0) {
e.preventDefault();
Swal.fire({icon: 'warning', text: '<?= h(tr('الرجاء إضافة أصناف للفاتورة أولاً.', 'Please add items to the invoice first.')) ?>'});
} else if (saleStatus === 'order' && !customerId) {
e.preventDefault();
Swal.fire({icon: 'warning', text: '<?= h(tr('يجب اختيار عميل للطلب المسبق.', 'You must select a customer for a pre-order.')) ?>'});
} else if (paymentMethod === 'pay_later' && !customerId) {
} else if (paymentAmount > currentInvoiceTotal + 0.0005) {
e.preventDefault();
Swal.fire({icon: 'warning', text: '<?= h(tr('المبلغ المدفوع لا يمكن أن يتجاوز إجمالي الفاتورة.', 'Paid amount cannot exceed the invoice total.')) ?>'});
} else if (paymentMethod === 'pay_later' && !customerId && paymentAmount < currentInvoiceTotal) {
e.preventDefault();
Swal.fire({icon: 'warning', text: '<?= h(tr('يجب اختيار عميل مسجل للدفع الآجل.', 'You must select a registered customer for pay later.')) ?>'});
} else if (paymentAmount < currentInvoiceTotal && !customerId) {
e.preventDefault();
Swal.fire({icon: 'warning', text: '<?= h(tr('يجب اختيار عميل مسجل عند وجود مبلغ متبقٍ.', 'Select a registered customer when there is a remaining balance.')) ?>'});
}
});
</script>

View File

@ -9,10 +9,27 @@ $db = db();
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
if ($_POST['action'] === 'update_status') {
$id = (int)$_POST['id'];
$status = $_POST['status']; // pending, accepted, completed, rejected
$status = trim((string) ($_POST['status'] ?? 'pending')); // pending, accepted, completed, rejected
$allowedStatuses = ['pending', 'accepted', 'completed', 'rejected'];
if (!in_array($status, $allowedStatuses, true)) {
set_flash('danger', tr('حالة الطلب غير صالحة.', 'Invalid order status.'));
redirect_to('online_orders.php');
}
$beforeStmt = $db->prepare("SELECT * FROM online_orders WHERE id = ?");
$beforeStmt->execute([$id]);
$order = $beforeStmt->fetch(PDO::FETCH_ASSOC);
$stmt = $db->prepare("UPDATE online_orders SET status = ? WHERE id = ?");
$stmt->execute([$status, $id]);
if ($order && ($order['status'] ?? 'pending') !== $status) {
$order['status'] = $status;
if (($order['customer_phone'] ?? '') !== '' && wablas_is_configured()) {
wablas_notify_online_order($order, $status);
}
}
set_flash('success', tr('تم تحديث حالة الطلب', 'Order status updated'));
redirect_to('online_orders.php');
} elseif ($_POST['action'] === 'delete') {
@ -33,7 +50,8 @@ $query = "SELECT * FROM online_orders WHERE DATE(created_at) >= ? AND DATE(creat
$params = [$date_from, $date_to];
if ($search !== '') {
$query .= " AND (customer_name LIKE ? OR customer_phone LIKE ?)";
$query .= " AND (customer_name LIKE ? OR customer_phone LIKE ? OR CONCAT('968', customer_phone) LIKE ?)";
$params[] = "%$search%";
$params[] = "%$search%";
$params[] = "%$search%";
}
@ -124,7 +142,7 @@ require __DIR__ . '/includes/header.php';
<td class="ps-4 fw-bold">#<?= h($o['id']) ?></td>
<td><?= h(date('Y-m-d H:i', strtotime($o['created_at']))) ?></td>
<td class="fw-bold"><?= h($o['customer_name']) ?></td>
<td><?= h($o['customer_phone']) ?></td>
<td dir="ltr"><?= h(phone_display($o['customer_phone'])) ?></td>
<td style="max-width: 200px;" class="text-truncate" title="<?= h($o['customer_address']) ?>"><?= h($o['customer_address']) ?></td>
<td class="fw-bold text-primary"><?= h(currency($o['total_amount'])) ?></td>
<td><span class="badge <?= $statusClass ?> px-2 py-1"><?= h($statusText) ?></span></td>
@ -220,7 +238,7 @@ require __DIR__ . '/includes/header.php';
function viewOrder(order) {
const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('orderModal'));
document.getElementById('vName').innerText = order.name;
document.getElementById('vPhone').innerText = order.phone;
document.getElementById('vPhone').innerText = order.phone ? ('968 ' + String(order.phone).replace(/^((00968)|(968))/, '')) : '';
document.getElementById('vAddress').innerText = order.address;
document.getElementById('vSubtotal').innerText = Number(order.subtotal).toFixed(2);
document.getElementById('vVat').innerText = Number(order.vat).toFixed(2);

104
pos.php
View File

@ -5,6 +5,7 @@ $user = require_permission('pos', 'show');
$pageTitle = tr('نقاط البيع', 'Smart POS');
$activeNav = 'pos';
$error = '';
$paymentAmountInput = (string) ($_POST['payment_amount'] ?? '');
$catalog = catalog();
$allowedBranches = get_user_branches($user);
@ -22,7 +23,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'] ?? ''));
$notes = trim((string) ($_POST['notes'] ?? ''));
$cartJson = (string) ($_POST['cart_json'] ?? '[]');
$items = json_decode($cartJson, true);
@ -31,8 +32,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 cart.');
} else {
@ -67,6 +66,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($normalized === []) {
$error = tr('السلة غير صالحة بعد التحقق من الأصناف.', 'The cart 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'];
$saleId = create_sale([
'receipt_no' => receipt_code(),
@ -78,12 +89,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' => $normalized,
'item_count' => $itemCount,
'subtotal' => $subtotal,
'vat_amount' => $totalVat,
'total_amount' => $subtotal + $totalVat,
'total_amount' => $totalAmount,
'notes' => $notes !== '' ? $notes : null,
]);
@ -578,6 +591,7 @@ require __DIR__ . '/includes/header.php';
<input type="hidden" name="customer_id" id="inputCustomerId">
<input type="hidden" name="customer_name" id="inputCustomer">
<input type="hidden" name="payment_method" id="inputPayment">
<input type="hidden" name="payment_amount" id="inputPaymentAmount">
<input type="hidden" name="notes" id="inputNotes">
<input type="hidden" name="cart_json" id="inputCart">
</form>
@ -591,8 +605,12 @@ require __DIR__ . '/includes/header.php';
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body text-center pt-2">
<h2 class="text-primary fw-bold mb-4" id="modalTotalAmount">0.000</h2>
<h2 class="text-primary fw-bold mb-3" id="modalTotalAmount">0.000</h2>
<div class="mb-3 text-start">
<label for="modalPaidAmount" class="form-label small text-muted"><?= h(tr('المبلغ المدفوع الآن', 'Paid Now')) ?></label>
<input type="number" id="modalPaidAmount" class="form-control rounded-3" min="0" step="0.001" value="<?= h($paymentAmountInput) ?>" placeholder="0.000">
<div class="form-text" id="modalDueHint"><?= h(tr('سيتم تتبع المبلغ المتبقي تلقائياً.', 'The remaining balance will be tracked automatically.')) ?></div>
</div>
<div class="d-grid gap-3">
<button class="btn btn-lg btn-outline-success rounded-pill fw-semibold" onclick="submitSale('cash')">
<i class="bi bi-cash-coin me-2"></i> <?= h(tr('نقداً', 'Cash')) ?>
@ -627,7 +645,10 @@ require __DIR__ . '/includes/header.php';
</div>
<div class="mb-3">
<label class="form-label text-muted small mb-1"><?= h(tr('رقم الهاتف', 'Phone')) ?></label>
<input type="text" id="ncPhone" class="form-control rounded-3" dir="ltr">
<div class="input-group" dir="ltr">
<span class="input-group-text">968</span>
<input type="text" id="ncPhone" class="form-control rounded-end-3" inputmode="numeric" maxlength="8" pattern="\d{8}" placeholder="91234567">
</div>
</div>
<div class="d-grid mt-4">
<button class="btn btn-primary rounded-pill fw-semibold shadow-sm" onclick="saveNewCustomer()"><?= h(tr('حفظ العميل', 'Save Customer')) ?></button>
@ -659,6 +680,7 @@ let cart = {};
let catalogData = <?= json_encode($catalog, JSON_UNESCAPED_UNICODE) ?>;
let customersData = <?= json_encode($customers, JSON_UNESCAPED_UNICODE) ?>;
let currencyLabel = '<?= h(tr('ر.ع', 'OMR')) ?>';
const partialPaymentText = '<?= h(tr('متبقٍ بعد الحفظ:', 'Due after save:')) ?>';
// Customer Autocomplete & Add Logic
const custInput = document.getElementById('posCustomer');
@ -681,9 +703,9 @@ custInput.addEventListener('input', function() {
matches.forEach(c => {
const a = document.createElement('a');
a.className = 'list-group-item list-group-item-action cursor-pointer border-0 border-bottom';
a.innerHTML = `<strong>${c.name}</strong> ${c.phone ? '<small class="text-muted ms-2">'+c.phone+'</small>' : ''}`;
a.innerHTML = `<strong>${c.name}</strong> ${c.phone ? '<small class="text-muted ms-2">968 '+c.phone+'</small>' : ''}`;
a.onclick = function() {
custInput.value = c.name + (c.phone ? ' - ' + c.phone : '');
custInput.value = c.name + (c.phone ? ' - 968 ' + c.phone : '');
custInput.dataset.id = c.id;
custDropdown.classList.add('d-none');
};
@ -731,7 +753,7 @@ async function saveNewCustomer() {
const data = await res.json();
if (data.success) {
customersData.push(data.customer);
custInput.value = data.customer.name + (data.customer.phone ? ' - ' + data.customer.phone : '');
custInput.value = data.customer.name + (data.customer.phone ? ' - 968 ' + data.customer.phone : '');
custInput.dataset.id = data.customer.id;
newCustomerModalObj.hide();
const Toast = Swal.mixin({ toast: true, position: 'top-end', showConfirmButton: false, timer: 2000 });
@ -907,11 +929,52 @@ function clearCart() {
let paymentModalObj = null;
document.addEventListener('DOMContentLoaded', () => {
paymentModalObj = new bootstrap.Modal(document.getElementById('paymentModal'));
const paidField = document.getElementById('modalPaidAmount');
if (paidField) {
paidField.addEventListener('input', () => {
paidField.dataset.manual = '1';
updateModalDueHint();
});
}
updateHeldOrdersCount();
});
function getCartTotals() {
let subtotal = 0;
let vat = 0;
Object.values(cart).forEach(item => {
const lineTotal = item.qty * item.price;
subtotal += lineTotal;
const vatPercent = parseFloat(catalogData[item.sku]?.vat || 0) || 0;
vat += lineTotal * (vatPercent / 100);
});
return { total: subtotal + vat, vat, subtotal };
}
function updateModalDueHint() {
const total = getCartTotals().total;
const paidField = document.getElementById('modalPaidAmount');
const dueHint = document.getElementById('modalDueHint');
if (!paidField || !dueHint) {
return;
}
const paid = Math.max(0, parseFloat(paidField.value || '0') || 0);
const due = Math.max(0, total - Math.min(paid, total));
dueHint.innerText = partialPaymentText + ' ' + due.toFixed(3) + ' ' + currencyLabel;
}
function openPaymentModal() {
if (Object.keys(cart).length === 0) return;
const total = getCartTotals().total;
const paidField = document.getElementById('modalPaidAmount');
document.getElementById('modalTotalAmount').innerText = total.toFixed(3) + ' ' + currencyLabel;
if (paidField) {
if (paidField.value === '') {
paidField.value = total.toFixed(3);
}
paidField.dataset.manual = paidField.value !== '' ? '1' : '0';
}
updateModalDueHint();
paymentModalObj.show();
}
@ -919,9 +982,23 @@ function submitSale(method) {
const branch = document.getElementById('posBranch').value || '<?= h($allowedBranches[0] ?? '') ?>';
const customer = document.getElementById('posCustomer').value;
const customerId = document.getElementById('posCustomer').dataset.id || '';
const totals = getCartTotals();
const paidField = document.getElementById('modalPaidAmount');
let paidAmount = Math.max(0, parseFloat(paidField?.value || '0') || 0);
const manual = paidField?.dataset.manual === '1';
if (method === 'pay_later' && !customerId) {
Swal.fire({icon: 'warning', text: '<?= h(tr('يجب اختيار عميل مسجل للدفع الآجل.', 'You must select a registered customer for pay later.')) ?>'});
if (method === 'pay_later' && !manual) {
paidAmount = 0;
} else if (method !== 'pay_later' && !manual) {
paidAmount = totals.total;
}
if (paidAmount > totals.total + 0.0005) {
Swal.fire({icon: 'warning', text: '<?= h(tr('المبلغ المدفوع لا يمكن أن يتجاوز إجمالي الفاتورة.', 'Paid amount cannot exceed the invoice total.')) ?>'});
return;
}
if (paidAmount < totals.total && !customerId) {
Swal.fire({icon: 'warning', text: '<?= h(tr('يجب اختيار عميل مسجل عند وجود مبلغ متبقٍ.', 'Select a registered customer when there is a remaining balance.')) ?>'});
return;
}
@ -934,6 +1011,7 @@ function submitSale(method) {
document.getElementById('inputCustomerId').value = customerId;
document.getElementById('inputCustomer').value = customer;
document.getElementById('inputPayment').value = method;
document.getElementById('inputPaymentAmount').value = paidAmount.toFixed(3);
document.getElementById('inputCart').value = JSON.stringify(itemsArr);
document.getElementById('checkoutForm').submit();

View File

@ -15,6 +15,7 @@ if ($id > 0) {
if (!$sale) {
die("Sale not found.");
}
$paymentSummary = sale_payment_summary($sale);
// Receipt Configuration
$storeName = current_lang() === 'ar' ? get_setting('company_name_ar', 'حلوى الريامي') : get_setting('company_name_en', 'Al Riyami Sweets');
@ -188,7 +189,7 @@ $registerNo = 'REG-01';
<div class="font-bold" style="font-size: 16px;"><?= h($storeName) ?></div>
<div><?= h($storeAddress) ?></div>
<div>VAT: <?= h($vatNo) ?></div>
<div><?= h(tr('هاتف', 'Tel')) ?>: <?= h(get_setting('company_phone', '')) ?></div>
<div><?= h(tr('هاتف', 'Tel')) ?>: <?= h(phone_display(get_setting('company_phone', ''))) ?></div>
</div>
<div class="divider"></div>
@ -253,6 +254,14 @@ $registerNo = 'REG-01';
<span><?= h(tr('الإجمالي', 'Total')) ?></span>
<span><?= number_format((float)$sale['total_amount'], 3) ?> <?= h(tr('ر.ع', 'OMR')) ?></span>
</div>
<div class="totals-row">
<span><?= h(tr('المدفوع', 'Paid')) ?></span>
<span><?= number_format((float)$paymentSummary['paid_amount'], 3) ?> <?= h(tr('ر.ع', 'OMR')) ?></span>
</div>
<div class="totals-row">
<span><?= h(tr('المتبقي', 'Due')) ?></span>
<span><?= number_format((float)$paymentSummary['due_amount'], 3) ?> <?= h(tr('ر.ع', 'OMR')) ?></span>
</div>
<?php
$pm = $sale['payment_method'];
$pmLabel = $pm;
@ -266,6 +275,10 @@ $registerNo = 'REG-01';
<span><?= h(tr('طريقة الدفع', 'Payment Method')) ?></span>
<span><?= h($pmLabel) ?></span>
</div>
<div class="totals-row">
<span><?= h(tr('الحالة', 'Status')) ?></span>
<span><?= h(payment_status_label($paymentSummary['payment_status'])) ?></span>
</div>
</div>
<!-- Footer -->

View File

@ -551,7 +551,7 @@ require __DIR__ . '/includes/header.php';
<td><?= h(date('Y-m-d H:i', strtotime((string)$sale['sale_date']))) ?></td>
<td><?= h((string)$sale['receipt_no']) ?></td>
<td><?= h((string)($sale['customer_name'] ?: '-')) ?></td>
<td><?= h((string)($sale['customer_phone'] ?: '-')) ?></td>
<td dir="ltr"><?= h((string)($sale['customer_phone'] ? phone_display($sale['customer_phone']) : '-')) ?></td>
<td><?= h(branch_label((string)$sale['branch_code'])) ?></td>
<td class="text-end fw-bold text-danger"><?= h(currency((float)$sale['total_amount'])) ?></td>
<td class="d-print-none text-end">
@ -601,7 +601,7 @@ require __DIR__ . '/includes/header.php';
<td><?= h(date('Y-m-d H:i', strtotime((string)$order['created_at']))) ?></td>
<td>#<?= h((string)$order['id']) ?></td>
<td><?= h((string)($order['customer_name'] ?: '-')) ?></td>
<td><?= h((string)($order['customer_phone'] ?: '-')) ?></td>
<td dir="ltr"><?= h((string)($order['customer_phone'] ? phone_display($order['customer_phone']) : '-')) ?></td>
<td><?= h((string)($order['customer_address'] ?: '-')) ?></td>
<td>
<?php if ($order['status'] === 'pending'): ?>

View File

@ -13,13 +13,14 @@ if ($id > 0) {
$dbError = $e->getMessage();
}
}
$paymentSummary = $sale ? sale_payment_summary($sale) : ['paid_amount' => 0, 'due_amount' => 0, 'payment_status' => 'paid'];
// Company Info for Invoice
$companyName = current_lang() === 'ar' ? get_setting('company_name_ar', 'حلوى الريامي') : get_setting('company_name_en', 'Al Riyami Sweets');
$companyAddress = get_setting('company_address', '');
$companyVat = get_setting('company_vat_number', '300123456789012');
$companyEmail = 'info@flatlogic.com';
$companyPhone = get_setting('company_phone', '');
$companyPhone = phone_display(get_setting('company_phone', ''));
require __DIR__ . '/includes/header.php';
?>
@ -368,7 +369,7 @@ require __DIR__ . '/includes/header.php';
<div class="meta-val"><?= h($pmLabel) ?></div>
<div class="meta-label"><?= h(tr('الحالة', 'Status')) ?>:</div>
<div class="meta-val"><?= ($sale['status'] ?? 'completed') === 'order' ? h(tr('غير مدفوعة', 'Unpaid')) : h(tr('مدفوعة', 'Paid')) ?></div>
<div class="meta-val"><?= h(payment_status_label($paymentSummary['payment_status'])) ?><?= ($sale['status'] ?? 'completed') === 'order' ? ' · ' . h(tr('طلب حجز', 'Order')) : '' ?></div>
</div>
</div>
</div>
@ -440,6 +441,14 @@ require __DIR__ . '/includes/header.php';
<td class="total-label"><?= h(tr('الإجمالي', 'Total')) ?></td>
<td class="total-amount"><?= h(number_format((float) $sale['total_amount'], 3)) ?> <?= h(tr('ر.ع', 'OMR')) ?></td>
</tr>
<tr>
<td class="total-label"><?= h(tr('المدفوع', 'Paid')) ?></td>
<td class="total-amount"><?= h(number_format((float) $paymentSummary['paid_amount'], 3)) ?> <?= h(tr('ر.ع', 'OMR')) ?></td>
</tr>
<tr>
<td class="total-label"><?= h(tr('المتبقي', 'Due')) ?></td>
<td class="total-amount" style="color: <?= $paymentSummary['due_amount'] > 0 ? '#dc3545' : '#198754' ?>;"><?= h(number_format((float) $paymentSummary['due_amount'], 3)) ?> <?= h(tr('ر.ع', 'OMR')) ?></td>
</tr>
</table>
</div>
</div>

108
sales.php
View File

@ -11,11 +11,17 @@ $statusFilter = $_GET['status'] ?? '';
if (isset($_GET['mark_paid']) && is_numeric($_GET['mark_paid'])) {
try {
$id = (int)$_GET['mark_paid'];
db()->prepare("UPDATE sales_orders SET status = 'completed' WHERE id = ?")->execute([$id]);
} catch(Throwable $e) {}
$redirect = $_GET["redirect"] ?? "sales.php";
header("Location: " . $redirect);
$id = (int) $_GET['mark_paid'];
$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);
}
}
} catch (Throwable $e) {}
$redirect = $_GET['redirect'] ?? 'sales.php';
header('Location: ' . $redirect);
exit;
}
@ -59,7 +65,10 @@ try {
$params[':search'] = "%$search%";
}
if ($statusFilter === 'order') {
if (in_array($statusFilter, ['paid', 'partial', 'unpaid'], true)) {
$where .= ' AND payment_status = :payment_status ';
$params[':payment_status'] = $statusFilter;
} elseif ($statusFilter === 'order') {
$where .= " AND status = 'order' ";
} elseif ($statusFilter === 'completed') {
$where .= " AND status = 'completed' ";
@ -112,8 +121,11 @@ require __DIR__ . '/includes/header.php';
<div class="input-group" style="max-width: 600px;">
<select name="status" class="form-select" style="max-width: 150px;" onchange="this.form.submit()">
<option value=""><?= h(tr('كل الحالات', 'All Statuses')) ?></option>
<option value="completed" <?= $statusFilter === 'completed' ? 'selected' : '' ?>><?= h(tr('مدفوع', 'Paid')) ?></option>
<option value="paid" <?= $statusFilter === 'paid' ? 'selected' : '' ?>><?= h(tr('مدفوعة بالكامل', 'Paid')) ?></option>
<option value="partial" <?= $statusFilter === 'partial' ? 'selected' : '' ?>><?= h(tr('مدفوعة جزئياً', 'Partially Paid')) ?></option>
<option value="unpaid" <?= $statusFilter === 'unpaid' ? 'selected' : '' ?>><?= h(tr('غير مدفوعة', 'Unpaid')) ?></option>
<option value="order" <?= $statusFilter === 'order' ? 'selected' : '' ?>><?= h(tr('طلب حجز', 'Order')) ?></option>
<option value="completed" <?= $statusFilter === 'completed' ? 'selected' : '' ?>><?= h(tr('كل الفواتير المكتملة', 'Completed Invoices')) ?></option>
</select>
<input type="text" name="q" class="form-control" placeholder="<?= h(tr('بحث بالإيصال، الكاشير، العميل أو الهاتف...', 'Search receipt, cashier, customer or phone...')) ?>" value="<?= h($search) ?>">
<button class="btn btn-outline-secondary" type="submit"><i class="bi bi-search"></i></button>
@ -144,6 +156,8 @@ require __DIR__ . '/includes/header.php';
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('المجموع', 'Subtotal')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('الضريبة', 'VAT')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('الإجمالي', 'Total')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('المدفوع', 'Paid')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('المتبقي', 'Due')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('الحالة', 'Status')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('التاريخ', 'Date')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('إجراءات', 'Actions')) ?></th>
@ -151,6 +165,7 @@ require __DIR__ . '/includes/header.php';
</thead>
<tbody class="border-top-0">
<?php foreach ($sales as $sale): ?>
<?php $paymentSummary = sale_payment_summary($sale); ?>
<tr>
<td>
<div class="fw-semibold"><?= h($sale['receipt_no']) ?></div>
@ -163,23 +178,30 @@ require __DIR__ . '/includes/header.php';
<td class="text-muted"><?= h(currency((float) $sale['total_amount'] - (float) ($sale['vat_amount'] ?? 0))) ?></td>
<td class="text-muted text-danger"><?= h(currency((float) $sale['vat_amount'])) ?></td>
<td class="fw-bold text-success"><?= h(currency((float) $sale['total_amount'])) ?></td>
<td class="fw-semibold text-primary"><?= h(currency((float) $paymentSummary['paid_amount'])) ?></td>
<td class="fw-semibold <?= $paymentSummary['due_amount'] > 0 ? 'text-danger' : 'text-muted' ?>"><?= h(currency((float) $paymentSummary['due_amount'])) ?></td>
<td>
<?php if (($sale['payment_status'] ?? 'paid') === 'unpaid'): ?>
<span class="badge bg-danger text-white px-3 py-2 rounded-pill"><i class="bi bi-clock-history"></i> <?= h(tr('آجل / غير مدفوع', 'Unpaid')) ?></span>
<?php elseif (($sale['status'] ?? 'completed') === 'order'): ?>
<span class="badge bg-warning text-dark px-3 py-2 rounded-pill"><i class="bi bi-clock"></i> <?= h(tr('طلب حجز', 'Order')) ?></span>
<?php else: ?>
<span class="badge bg-success px-3 py-2 rounded-pill"><i class="bi bi-check-circle"></i> <?= h(tr('مدفوع', 'Paid')) ?></span>
<?php endif; ?>
<?php if (($sale['payment_method'] ?? '') === 'pay_later'): ?>
<span class="badge <?= h(payment_status_badge_class($paymentSummary['payment_status'])) ?> px-3 py-2 rounded-pill">
<?php if ($paymentSummary['payment_status'] === 'partial'): ?>
<i class="bi bi-pie-chart"></i>
<?php elseif ($paymentSummary['payment_status'] === 'unpaid'): ?>
<i class="bi bi-clock-history"></i>
<?php else: ?>
<i class="bi bi-check-circle"></i>
<?php endif; ?>
<?= h(payment_status_label($paymentSummary['payment_status'])) ?>
</span>
<?php if (($sale['status'] ?? 'completed') === 'order'): ?>
<small class="d-block text-muted mt-1"><?= h(tr('طلب حجز', 'Order')) ?></small>
<?php elseif (($sale['payment_method'] ?? '') === 'pay_later'): ?>
<small class="d-block text-muted mt-1"><?= h(tr('دفع آجل', 'Pay Later')) ?></small>
<?php endif; ?>
</td>
<td><?= h(date('Y-m-d H:i', strtotime((string) $sale['sale_date']))) ?></td>
<td>
<?php if (($sale['status'] ?? 'completed') === 'order' || ($sale['payment_status'] ?? 'paid') === 'unpaid'): ?>
<button class="btn btn-sm btn-outline-success rounded-circle shadow-sm me-1" style="width: 34px; height: 34px; padding: 0;" onclick="markAsPaid(<?= $sale['id'] ?>)" title="<?= h(tr('تأكيد الدفع', 'Confirm Payment')) ?>">
<i class="bi bi-check-lg"></i>
<?php if ($paymentSummary['due_amount'] > 0.0005): ?>
<button class="btn btn-sm btn-outline-success rounded-circle shadow-sm me-1" style="width: 34px; height: 34px; padding: 0;" onclick="receivePayment(<?= (int) $sale['id'] ?>, <?= json_encode((float) $paymentSummary['due_amount']) ?>, <?= ($sale['status'] ?? 'completed') === 'order' ? 'true' : 'false' ?>)" title="<?= h(tr('استلام دفعة', 'Receive Payment')) ?>">
<i class="bi bi-cash-coin"></i>
</button>
<?php endif; ?>
<a class="btn btn-sm btn-light text-primary border me-1" href="<?= h(url_for('sale.php', ['id' => $sale['id']])) ?>" title="<?= h(tr('تفاصيل', 'Detail')) ?>">
@ -213,20 +235,48 @@ require __DIR__ . '/includes/header.php';
</section>
<script>
function markAsPaid(id) {
Swal.fire({
title: "<?= h(tr('تأكيد الدفع والاستلام؟', 'Confirm payment and pickup?')) ?>",
text: "<?= h(tr('سيتم تحويل هذا الطلب إلى فاتورة مبيعات مدفوعة.', 'This order will be marked as a paid sale.')) ?>",
icon: "question",
async function receivePayment(id, dueAmount, completeOrder = false) {
const { value: paymentAmount } = await Swal.fire({
title: '<?= h(tr('استلام دفعة', 'Receive Payment')) ?>',
text: '<?= h(tr('أدخل المبلغ المستلم لهذه الفاتورة.', 'Enter the amount received for this invoice.')) ?>',
input: 'number',
inputAttributes: { min: '0.001', step: '0.001', max: String(dueAmount) },
inputValue: Number(dueAmount).toFixed(3),
showCancelButton: true,
confirmButtonColor: "#198754",
confirmButtonText: "<?= h(tr('نعم، تم الدفع', 'Yes, Paid')) ?>",
cancelButtonText: "<?= h(tr('إلغاء', 'Cancel')) ?>"
}).then((result) => {
if (result.isConfirmed) {
window.location.href = "sales.php?mark_paid=" + id;
confirmButtonColor: '#198754',
confirmButtonText: '<?= h(tr('حفظ الدفعة', 'Save Payment')) ?>',
cancelButtonText: '<?= h(tr('إلغاء', 'Cancel')) ?>',
inputValidator: (value) => {
const amount = parseFloat(value || '0');
if (!amount || amount <= 0) {
return '<?= h(tr('أدخل مبلغاً صحيحاً.', 'Enter a valid amount.')) ?>';
}
if (amount - dueAmount > 0.0005) {
return '<?= h(tr('المبلغ لا يمكن أن يتجاوز المتبقي.', 'Amount cannot exceed the due balance.')) ?>';
}
return null;
}
});
if (!paymentAmount) {
return;
}
const formData = new FormData();
formData.append('sale_id', String(id));
formData.append('payment_amount', String(paymentAmount));
if (completeOrder) {
formData.append('complete_order', '1');
}
const response = await fetch('api/sales_payment.php', { method: 'POST', body: formData });
const data = await response.json();
if (data.success) {
await Swal.fire({ icon: 'success', text: data.message, confirmButtonText: 'OK' });
window.location.reload();
} else {
Swal.fire({ icon: 'error', text: data.error || '<?= h(tr('تعذر تسجيل الدفعة.', 'Could not record the payment.')) ?>' });
}
}
function mockEdit() {

View File

@ -234,7 +234,11 @@ body { background-color: #f8f9fa; }
</div>
<div class="mb-3">
<label class="form-label fw-semibold"><?= h(tr('رقم الهاتف', 'Telephone')) ?> *</label>
<input type="tel" class="form-control form-control-lg rounded-3" id="customerPhone" required>
<div class="input-group input-group-lg" dir="ltr">
<span class="input-group-text">968</span>
<input type="tel" class="form-control form-control-lg rounded-end-3" id="customerPhone" inputmode="numeric" maxlength="8" pattern="\d{8}" placeholder="91234567" required>
</div>
<div class="form-text"><?= h(tr('أدخل 8 أرقام فقط وسيتم إرسالها عبر واتساب مع المقدمة 968.', 'Enter 8 digits only; WhatsApp will send it with the 968 prefix.')) ?></div>
</div>
<div class="mb-3">
<label class="form-label fw-semibold"><?= h(tr('العنوان', 'Address')) ?> *</label>