Autosave: 20260422-182218

This commit is contained in:
Flatlogic Bot 2026-04-22 18:22:05 +00:00
parent 5bfaa401f2
commit b2cb1b5a0b
7 changed files with 276 additions and 53 deletions

View File

@ -10,11 +10,36 @@ if (!in_array($user['role'], ['owner', 'manager'])) {
} }
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$isAjax = strtolower((string) ($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '')) === 'xmlhttprequest';
$respond = static function (bool $success, string $type, string $message, ?string $redirect = null) use ($isAjax): void {
if ($isAjax) {
header('Content-Type: application/json; charset=UTF-8');
echo json_encode([
'success' => $success,
'type' => $type,
'message' => $message,
'redirect' => $redirect,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
set_flash($type, $message);
header('Location: ' . ($redirect ?: '../index.php'));
exit;
};
$redirectBack = static function (): string {
$referer = $_SERVER['HTTP_REFERER'] ?? '../index.php';
$returnModal = trim((string) ($_POST['return_modal'] ?? ''));
if ($returnModal === 'wablas') {
return append_query_params($referer, ['open_modal' => 'wablas']);
}
return $referer;
};
$pdo = db(); $pdo = db();
$keys = [ $keys = [
'timezone', 'company_name_ar', 'company_name_en', 'vat_percentage', 'timezone', 'company_name_ar', 'company_name_en', 'vat_percentage',
'company_vat_number', 'company_phone', 'company_email', 'company_address', 'company_vat_number', 'company_phone', 'company_email', 'company_address',
'wablas_enabled', 'wablas_token', 'wablas_secret_key', 'wablas_enabled', 'wablas_token', 'wablas_secret_key', 'wablas_api_url',
'wablas_invoice_recipients', 'wablas_report_recipients', 'wablas_invoice_recipients', 'wablas_report_recipients',
'wablas_daily_auto_send', 'wablas_daily_auto_time', 'wablas_daily_auto_last_date', 'wablas_daily_auto_send', 'wablas_daily_auto_time', 'wablas_daily_auto_last_date',
'wablas_template_invoice', 'wablas_template_daily_report', 'wablas_template_invoice', 'wablas_template_daily_report',
@ -30,10 +55,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($companyPhone !== '') { if ($companyPhone !== '') {
$companyPhone = normalize_oman_phone($companyPhone); $companyPhone = normalize_oman_phone($companyPhone);
if ($companyPhone === '') { if ($companyPhone === '') {
set_flash('danger', tr('رقم هاتف الشركة يجب أن يكون عمانياً من 8 خانات.', 'Company phone must be an 8-digit Oman number.')); $respond(false, 'danger', tr('رقم هاتف الشركة يجب أن يكون عمانياً من 8 خانات.', 'Company phone must be an 8-digit Oman number.'), $redirectBack());
$referer = $_SERVER['HTTP_REFERER'] ?? '../index.php';
header('Location: ' . $referer);
exit;
} }
$_POST['company_phone'] = $companyPhone; $_POST['company_phone'] = $companyPhone;
} }
@ -41,10 +63,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
foreach (['wablas_invoice_recipients', 'wablas_report_recipients'] as $phoneListKey) { foreach (['wablas_invoice_recipients', 'wablas_report_recipients'] as $phoneListKey) {
$parsed = wablas_parse_phone_list((string) ($_POST[$phoneListKey] ?? '')); $parsed = wablas_parse_phone_list((string) ($_POST[$phoneListKey] ?? ''));
if (!empty($parsed['invalid'])) { if (!empty($parsed['invalid'])) {
set_flash('danger', tr('يوجد رقم واتساب غير صالح في الحقل.', 'There is an invalid WhatsApp number in the field.') . ' ' . implode(', ', $parsed['invalid'])); $respond(false, 'danger', tr('يوجد رقم واتساب غير صالح في الحقل.', 'There is an invalid WhatsApp number in the field.') . ' ' . implode(', ', $parsed['invalid']), $redirectBack());
$referer = $_SERVER['HTTP_REFERER'] ?? '../index.php';
header('Location: ' . $referer);
exit;
} }
$_POST[$phoneListKey] = implode(',', $parsed['phones']); $_POST[$phoneListKey] = implode(',', $parsed['phones']);
} }
@ -90,10 +109,5 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} }
} }
set_flash('success', tr('تم حفظ الإعدادات بنجاح.', 'Settings saved successfully.')); $respond(true, 'success', tr('تم حفظ الإعدادات بنجاح.', 'Settings saved successfully.'), $redirectBack());
// Redirect back to referring page
$referer = $_SERVER['HTTP_REFERER'] ?? '../index.php';
header('Location: ' . $referer);
exit;
} }

View File

@ -12,48 +12,68 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
redirect_to('../index.php'); redirect_to('../index.php');
} }
$isAjax = strtolower((string) ($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '')) === 'xmlhttprequest';
$respond = static function (bool $success, string $type, string $message, ?string $redirect = null) use ($isAjax): void {
if ($isAjax) {
header('Content-Type: application/json; charset=UTF-8');
echo json_encode([
'success' => $success,
'type' => $type,
'message' => $message,
'redirect' => $redirect,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
set_flash($type, $message);
header('Location: ' . ($redirect ?: '../index.php'));
exit;
};
$redirectBack = static function (): string {
$referer = $_SERVER['HTTP_REFERER'] ?? '../index.php';
$returnModal = trim((string) ($_POST['return_modal'] ?? ''));
if ($returnModal === 'wablas') {
return append_query_params($referer, ['open_modal' => 'wablas']);
}
return $referer;
};
$phoneInput = trim((string) ($_POST['wablas_test_phone'] ?? '')); $phoneInput = trim((string) ($_POST['wablas_test_phone'] ?? ''));
$message = trim((string) ($_POST['wablas_test_message'] ?? '')); $message = trim((string) ($_POST['wablas_test_message'] ?? ''));
$token = trim((string) ($_POST['wablas_token'] ?? get_setting('wablas_token', ''))); $token = trim((string) ($_POST['wablas_token'] ?? get_setting('wablas_token', '')));
$secretKey = trim((string) ($_POST['wablas_secret_key'] ?? get_setting('wablas_secret_key', ''))); $secretKey = trim((string) ($_POST['wablas_secret_key'] ?? get_setting('wablas_secret_key', '')));
$apiUrl = trim((string) ($_POST['wablas_api_url'] ?? get_setting('wablas_api_url', '')));
if ($phoneInput === '') { if ($phoneInput === '') {
set_flash('danger', tr('أدخل رقم واتساب تجريبي صالحاً من 8 خانات.', 'Enter a valid 8-digit test WhatsApp number.')); $respond(false, 'danger', tr('أدخل رقم واتساب تجريبي صالحاً من 8 خانات.', 'Enter a valid 8-digit test WhatsApp number.'), $redirectBack());
header('Location: ' . ($_SERVER['HTTP_REFERER'] ?? '../index.php'));
exit;
} }
$phone = normalize_oman_phone($phoneInput); $phone = normalize_oman_phone($phoneInput);
if ($phone === '') { if ($phone === '') {
set_flash('danger', tr('رقم الاختبار يجب أن يكون عمانياً من 8 خانات.', 'The test phone must be a valid 8-digit Oman number.')); $respond(false, 'danger', tr('رقم الاختبار يجب أن يكون عمانياً من 8 خانات.', 'The test phone must be a valid 8-digit Oman number.'), $redirectBack());
header('Location: ' . ($_SERVER['HTTP_REFERER'] ?? '../index.php'));
exit;
} }
if ($message === '') { if ($message === '') {
set_flash('danger', tr('اكتب رسالة الاختبار أولاً.', 'Write the test message first.')); $respond(false, 'danger', tr('اكتب رسالة الاختبار أولاً.', 'Write the test message first.'), $redirectBack());
header('Location: ' . ($_SERVER['HTTP_REFERER'] ?? '../index.php'));
exit;
} }
if (!wablas_has_credentials($token, $secretKey)) { 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.')); $respond(false, 'danger', tr('أدخل Wablas Token و Secret Key قبل إرسال الاختبار.', 'Enter the Wablas token and secret key before sending a test message.'), $redirectBack());
header('Location: ' . ($_SERVER['HTTP_REFERER'] ?? '../index.php'));
exit;
} }
$result = wablas_send_message($phone, $message, [ $result = wablas_send_message($phone, $message, [
'token' => $token, 'token' => $token,
'secret_key' => $secretKey, 'secret_key' => $secretKey,
'api_url' => $apiUrl,
'ignore_enabled' => true, 'ignore_enabled' => true,
]); ]);
if (!empty($result['success'])) { if (!empty($result['success'])) {
set_flash('success', tr('تم إرسال رسالة الاختبار إلى 968 ', 'Test message sent to 968 ') . $phone . '.'); $respond(true, 'success', tr('تم إرسال رسالة الاختبار إلى 968 ', 'Test message sent to 968 ') . $phone . '.', $redirectBack());
} else { } else {
$status = isset($result['status']) ? (' (' . (int) $result['status'] . ')') : ''; $status = isset($result['status']) ? (' (' . (int) $result['status'] . ')') : '';
set_flash('danger', tr('فشل إرسال رسالة الاختبار.', 'Failed to send the test message.') . ' ' . (string) ($result['error'] ?? ('Wablas error' . $status))); $respond(false, 'danger', tr('فشل إرسال رسالة الاختبار.', 'Failed to send the test message.') . ' ' . (string) ($result['error'] ?? ('Wablas error' . $status)), $redirectBack());
} }
header('Location: ' . ($_SERVER['HTTP_REFERER'] ?? '../index.php')); $respond(false, 'danger', tr('فشل إرسال رسالة الاختبار.', 'Failed to send the test message.'), $redirectBack());
exit;

View File

@ -44,6 +44,7 @@ try {
$pdo->exec("UPDATE sales_orders SET due_amount = GREATEST(total_amount - 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"); $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");
$pdo->exec("INSERT IGNORE INTO settings (setting_key, setting_value) VALUES $pdo->exec("INSERT IGNORE INTO settings (setting_key, setting_value) VALUES
('wablas_api_url', 'https://wablas.com/api/send-message'),
('wablas_invoice_recipients', ''), ('wablas_invoice_recipients', ''),
('wablas_report_recipients', ''), ('wablas_report_recipients', ''),
('wablas_template_invoice', ''), ('wablas_template_invoice', ''),
@ -1006,6 +1007,25 @@ function wablas_is_configured(bool $requireEnabled = true): bool
&& (!$requireEnabled || wablas_is_enabled()); && (!$requireEnabled || wablas_is_enabled());
} }
function wablas_normalize_api_url(?string $value = null): string
{
$url = trim((string) ($value ?? get_setting('wablas_api_url', '')));
if ($url === '') {
return 'https://wablas.com/api/send-message';
}
if (!preg_match('#^https?://#i', $url)) {
$url = 'https://' . ltrim($url, '/');
}
$url = rtrim($url, '/');
if (!preg_match('#/api/(v2/)?send-message$#i', $url)) {
$url .= '/api/send-message';
}
return $url;
}
function wablas_order_status_label(string $status): string function wablas_order_status_label(string $status): string
{ {
return match ($status) { return match ($status) {
@ -1502,6 +1522,7 @@ function wablas_send_message(string $phone, string $message, array $options = []
$token = trim((string) ($options['token'] ?? get_setting('wablas_token', ''))); $token = trim((string) ($options['token'] ?? get_setting('wablas_token', '')));
$secretKey = trim((string) ($options['secret_key'] ?? get_setting('wablas_secret_key', ''))); $secretKey = trim((string) ($options['secret_key'] ?? get_setting('wablas_secret_key', '')));
$ignoreEnabled = !empty($options['ignore_enabled']); $ignoreEnabled = !empty($options['ignore_enabled']);
$apiUrl = wablas_normalize_api_url($options['api_url'] ?? null);
$message = trim($message); $message = trim($message);
if ($localPhone === '') { if ($localPhone === '') {
@ -1517,7 +1538,7 @@ function wablas_send_message(string $phone, string $message, array $options = []
return ['success' => false, 'error' => 'Wablas is not configured']; return ['success' => false, 'error' => 'Wablas is not configured'];
} }
$endpoint = 'https://wablas.com/api/send-message'; $endpoint = $apiUrl;
$payload = http_build_query([ $payload = http_build_query([
'phone' => '968' . $localPhone, 'phone' => '968' . $localPhone,
'message' => $message, 'message' => $message,
@ -1567,12 +1588,23 @@ function wablas_send_message(string $phone, string $message, array $options = []
$decoded = json_decode((string) $responseBody, true); $decoded = json_decode((string) $responseBody, true);
$success = $httpCode >= 200 && $httpCode < 300; $success = $httpCode >= 200 && $httpCode < 300;
$error = '';
if (!$success) {
$error = (string) ($decoded['message'] ?? $decoded['data']['message'] ?? '');
if ($error === '') {
$error = 'Wablas request failed';
}
$error .= ' | URL: ' . $endpoint;
}
return [ return [
'success' => $success, 'success' => $success,
'status' => $httpCode, 'status' => $httpCode,
'data' => $decoded, 'data' => $decoded,
'raw' => $responseBody, 'raw' => $responseBody,
'phone' => '968' . $localPhone, 'phone' => '968' . $localPhone,
'endpoint' => $endpoint,
'error' => $error,
]; ];
} }
@ -1656,20 +1688,26 @@ function create_sale(array $data): int
{ {
ensure_sales_table(); ensure_sales_table();
db()->beginTransaction(); $pdo = db();
$pdo->beginTransaction();
try { try {
$stmt = db()->prepare('INSERT INTO sales_orders $receiptNo = isset($data['receipt_no']) && trim((string) $data['receipt_no']) !== ''
? trim((string) $data['receipt_no'])
: next_receipt_code($pdo);
$stmt = $pdo->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, paid_amount, due_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 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, :paid_amount, :due_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(':receipt_no', $receiptNo);
$stmt->bindValue(':sale_mode', $data['sale_mode']); $stmt->bindValue(':sale_mode', $data['sale_mode']);
$stmt->bindValue(':branch_code', $data['branch_code']); $stmt->bindValue(':branch_code', $data['branch_code']);
$stmt->bindValue(':cashier_username', $data['cashier_username']); $stmt->bindValue(':cashier_username', $data['cashier_username']);
$stmt->bindValue(':cashier_name', $data['cashier_name']); $stmt->bindValue(':cashier_name', $data['cashier_name']);
$stmt->bindValue(':role_name', $data['role_name']); $stmt->bindValue(':role_name', $data['role_name']);
$stmt->bindValue(':customer_id', $data['customer_id'] ?? null, PDO::PARAM_INT); $customerId = isset($data['customer_id']) && $data['customer_id'] !== '' ? (int) $data['customer_id'] : null;
$stmt->bindValue(':customer_id', $customerId, $customerId === null ? PDO::PARAM_NULL : PDO::PARAM_INT);
$stmt->bindValue(':customer_name', $data['customer_name']); $stmt->bindValue(':customer_name', $data['customer_name']);
$stmt->bindValue(':payment_method', $data['payment_method']); $stmt->bindValue(':payment_method', $data['payment_method']);
$stmt->bindValue(':payment_status', $data['payment_status'] ?? 'paid'); $stmt->bindValue(':payment_status', $data['payment_status'] ?? 'paid');
@ -1684,14 +1722,14 @@ function create_sale(array $data): int
$stmt->bindValue(':notes', $data['notes']); $stmt->bindValue(':notes', $data['notes']);
$stmt->execute(); $stmt->execute();
$saleId = (int) db()->lastInsertId(); $saleId = (int) $pdo->lastInsertId();
sync_order_stock_reservation([], 'completed', $data['items'] ?? [], (string) ($data['status'] ?? 'completed')); sync_order_stock_reservation([], 'completed', $data['items'] ?? [], (string) ($data['status'] ?? 'completed'));
db()->commit(); $pdo->commit();
return $saleId; return $saleId;
} catch (Throwable $e) { } catch (Throwable $e) {
if (db()->inTransaction()) { if ($pdo->inTransaction()) {
db()->rollBack(); $pdo->rollBack();
} }
throw $e; throw $e;
} }
@ -1988,7 +2026,63 @@ function purchase_pipeline(): array
]; ];
} }
function next_receipt_code(PDO $pdo): string
{
$settingKey = 'invoice_sequence_next';
$seedStmt = $pdo->prepare("INSERT INTO settings (setting_key, setting_value) VALUES (:key, '1') ON DUPLICATE KEY UPDATE setting_key = VALUES(setting_key)");
$seedStmt->bindValue(':key', $settingKey);
$seedStmt->execute();
$selectStmt = $pdo->prepare("SELECT setting_value FROM settings WHERE setting_key = :key FOR UPDATE");
$selectStmt->bindValue(':key', $settingKey);
$selectStmt->execute();
$nextNumber = max(1, (int) $selectStmt->fetchColumn());
$existsStmt = $pdo->prepare('SELECT 1 FROM sales_orders WHERE receipt_no = :receipt_no LIMIT 1');
while (true) {
$candidate = (string) $nextNumber;
$existsStmt->bindValue(':receipt_no', $candidate);
$existsStmt->execute();
if (!$existsStmt->fetchColumn()) {
break;
}
$nextNumber++;
}
$updateStmt = $pdo->prepare("UPDATE settings SET setting_value = :next_value WHERE setting_key = :key");
$updateStmt->bindValue(':next_value', (string) ($nextNumber + 1));
$updateStmt->bindValue(':key', $settingKey);
$updateStmt->execute();
return (string) $nextNumber;
}
function receipt_code(): string function receipt_code(): string
{
$pdo = db();
$ownsTransaction = !$pdo->inTransaction();
if ($ownsTransaction) {
$pdo->beginTransaction();
}
try {
$receiptNo = next_receipt_code($pdo);
if ($ownsTransaction && $pdo->inTransaction()) {
$pdo->commit();
}
return $receiptNo;
} catch (Throwable $e) {
if ($ownsTransaction && $pdo->inTransaction()) {
$pdo->rollBack();
}
throw $e;
}
}
function purchase_reference_code(): string
{ {
return (string) random_int(100000, 999999); return (string) random_int(100000, 999999);
} }

View File

@ -360,7 +360,8 @@
<div class="modal fade" id="wablasSettingsModal" tabindex="-1" aria-hidden="true"> <div class="modal fade" id="wablasSettingsModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-fullscreen"> <div class="modal-dialog modal-fullscreen">
<div class="modal-content border-0 rounded-0"> <div class="modal-content border-0 rounded-0">
<form action="api/settings.php" method="POST"> <form action="api/settings.php" method="POST" id="wablasSettingsForm">
<input type="hidden" name="return_modal" value="wablas">
<div class="modal-header"> <div class="modal-header">
<div> <div>
<h5 class="modal-title mb-1"><?= h(tr('إعدادات واتساب', 'WhatsApp Settings')) ?></h5> <h5 class="modal-title mb-1"><?= h(tr('إعدادات واتساب', 'WhatsApp Settings')) ?></h5>
@ -369,6 +370,7 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="alert d-none" id="wablasSettingsAlert" role="alert"></div>
<div class="row g-3"> <div class="row g-3">
<div class="col-12"> <div class="col-12">
<div class="border rounded-4 px-3 py-3 bg-body-tertiary"> <div class="border rounded-4 px-3 py-3 bg-body-tertiary">
@ -394,6 +396,11 @@
<label class="form-label mb-1"><?= h(tr('Wablas Secret Key', 'Wablas Secret Key')) ?></label> <label class="form-label mb-1"><?= 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')) ?>"> <input type="password" class="form-control" name="wablas_secret_key" value="<?= h(get_setting('wablas_secret_key')) ?>">
</div> </div>
<div class="col-12">
<label class="form-label mb-1"><?= h(tr('رابط بوابة Wablas', 'Wablas Gateway URL')) ?></label>
<input type="text" class="form-control" name="wablas_api_url" value="<?= h(get_setting('wablas_api_url', 'https://wablas.com/api/send-message')) ?>" placeholder="https://wablas.com/api/send-message">
<div class="small text-muted mt-1"><?= h(tr('إذا كان حسابك مربوطاً بسيرفر محدد مثل tegal.wablas.com أو texas.wablas.com فاكتب رابط ذلك السيرفر هنا. يمكنك إدخال الدومين فقط أو رابط /api/send-message الكامل.', 'If your account is tied to a specific server like tegal.wablas.com or texas.wablas.com, enter that server here. You can paste either the domain or the full /api/send-message URL.')) ?></div>
</div>
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label mb-1"><?= h(tr('رقم الاختبار', 'Test Phone')) ?></label> <label class="form-label mb-1"><?= h(tr('رقم الاختبار', 'Test Phone')) ?></label>
@ -521,8 +528,28 @@
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
var settingsModalEl = document.getElementById('settingsModal'); var settingsModalEl = document.getElementById('settingsModal');
var wablasModalEl = document.getElementById('wablasSettingsModal'); var wablasModalEl = document.getElementById('wablasSettingsModal');
var wablasForm = document.getElementById('wablasSettingsForm');
var wablasAlertEl = document.getElementById('wablasSettingsAlert');
var reopenSettingsAfterWablas = false; var reopenSettingsAfterWablas = false;
var showWablasAlert = function (type, message) {
if (!wablasAlertEl) {
return;
}
var alertClass = type === 'success' ? 'alert-success' : (type === 'warning' ? 'alert-warning' : 'alert-danger');
wablasAlertEl.className = 'alert ' + alertClass;
wablasAlertEl.textContent = message || '';
wablasAlertEl.classList.remove('d-none');
};
var clearWablasAlert = function () {
if (!wablasAlertEl) {
return;
}
wablasAlertEl.className = 'alert d-none';
wablasAlertEl.textContent = '';
};
document.querySelectorAll('[data-open-wablas-settings]').forEach(function (trigger) { document.querySelectorAll('[data-open-wablas-settings]').forEach(function (trigger) {
trigger.addEventListener('click', function () { trigger.addEventListener('click', function () {
if (!wablasModalEl || typeof bootstrap === 'undefined') { if (!wablasModalEl || typeof bootstrap === 'undefined') {
@ -546,6 +573,7 @@ document.addEventListener('DOMContentLoaded', function () {
if (settingsModalEl && wablasModalEl && typeof bootstrap !== 'undefined') { if (settingsModalEl && wablasModalEl && typeof bootstrap !== 'undefined') {
wablasModalEl.addEventListener('hidden.bs.modal', function () { wablasModalEl.addEventListener('hidden.bs.modal', function () {
clearWablasAlert();
if (!reopenSettingsAfterWablas) { if (!reopenSettingsAfterWablas) {
return; return;
} }
@ -554,6 +582,74 @@ document.addEventListener('DOMContentLoaded', function () {
}); });
} }
if (wablasModalEl && typeof bootstrap !== 'undefined') {
var wablasUrl = new URL(window.location.href);
if (wablasUrl.searchParams.get('open_modal') === 'wablas') {
bootstrap.Modal.getOrCreateInstance(wablasModalEl).show();
wablasUrl.searchParams.delete('open_modal');
if (window.history && typeof window.history.replaceState === 'function') {
window.history.replaceState({}, document.title, wablasUrl.pathname + (wablasUrl.search ? '?' + wablasUrl.searchParams.toString() : '') + wablasUrl.hash);
}
}
}
if (wablasForm && window.fetch) {
wablasForm.addEventListener('submit', function (event) {
var submitter = event.submitter;
if (!submitter) {
return;
}
event.preventDefault();
clearWablasAlert();
var action = submitter.getAttribute('formaction') || wablasForm.getAttribute('action') || window.location.href;
var method = submitter.getAttribute('formmethod') || wablasForm.getAttribute('method') || 'POST';
var formData = new FormData(wablasForm, submitter);
var originalHtml = submitter.innerHTML;
submitter.disabled = true;
submitter.setAttribute('aria-busy', 'true');
submitter.innerHTML = '<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span><?= h(tr('جارٍ الحفظ...', 'Saving...')) ?>';
fetch(action, {
method: method.toUpperCase(),
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json'
},
credentials: 'same-origin'
}).then(function (response) {
return response.json().catch(function () {
return { success: false, type: 'danger', message: 'Unexpected response.' };
});
}).then(function (data) {
var type = data && data.type ? data.type : (data && data.success ? 'success' : 'danger');
var message = data && data.message ? data.message : 'Request finished.';
showWablasAlert(type, message);
if (typeof Swal !== 'undefined') {
Swal.fire({
icon: type === 'success' ? 'success' : (type === 'warning' ? 'warning' : 'error'),
title: message,
toast: true,
position: 'top-end',
showConfirmButton: false,
timer: 3000,
timerProgressBar: true
});
}
}).catch(function () {
showWablasAlert('danger', '<?= h(tr('تعذر حفظ الإعدادات حالياً. حاول مرة أخرى.', 'Could not save the settings right now. Please try again.')) ?>');
}).finally(function () {
submitter.disabled = false;
submitter.removeAttribute('aria-busy');
submitter.innerHTML = originalHtml;
});
});
}
document.querySelectorAll('[data-wablas-preview-source]').forEach(function (field) { document.querySelectorAll('[data-wablas-preview-source]').forEach(function (field) {
var key = field.getAttribute('data-wablas-preview-source'); var key = field.getAttribute('data-wablas-preview-source');
var preview = document.querySelector('[data-wablas-preview="' + key + '"]'); var preview = document.querySelector('[data-wablas-preview="' + key + '"]');

View File

@ -63,7 +63,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} else { } else {
$cashierName = current_lang() === 'ar' ? $user['name_ar'] : $user['name_en']; $cashierName = current_lang() === 'ar' ? $user['name_ar'] : $user['name_en'];
$purchaseId = create_purchase([ $purchaseId = create_purchase([
'reference_no' => receipt_code(), 'reference_no' => purchase_reference_code(),
'branch_code' => $branchCode, 'branch_code' => $branchCode,
'user_username' => $user['username'], 'user_username' => $user['username'],
'user_name' => $cashierName, 'user_name' => $cashierName,

View File

@ -100,7 +100,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($error === '') { if ($error === '') {
$cashierName = current_lang() === 'ar' ? $user['name_ar'] : $user['name_en']; $cashierName = current_lang() === 'ar' ? $user['name_ar'] : $user['name_en'];
$saleId = create_sale([ $saleId = create_sale([
'receipt_no' => receipt_code(),
'sale_mode' => $saleMode, 'sale_mode' => $saleMode,
'branch_code' => $branchCode, 'branch_code' => $branchCode,
'cashier_username' => $user['username'], 'cashier_username' => $user['username'],

20
pos.php
View File

@ -80,7 +80,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($error === '') { if ($error === '') {
$cashierName = current_lang() === 'ar' ? $user['name_ar'] : $user['name_en']; $cashierName = current_lang() === 'ar' ? $user['name_ar'] : $user['name_en'];
$saleId = create_sale([ $saleId = create_sale([
'receipt_no' => receipt_code(),
'sale_mode' => $saleMode, 'sale_mode' => $saleMode,
'branch_code' => $branchCode, 'branch_code' => $branchCode,
'cashier_username' => $user['username'], 'cashier_username' => $user['username'],
@ -954,7 +953,7 @@ function getCartTotals() {
} }
function updateModalDueHint() { function updateModalDueHint() {
const total = getCartTotals().total; const total = Number(getCartTotals().total.toFixed(3));
const paidField = document.getElementById('modalPaidAmount'); const paidField = document.getElementById('modalPaidAmount');
const dueHint = document.getElementById('modalDueHint'); const dueHint = document.getElementById('modalDueHint');
if (!paidField || !dueHint) { if (!paidField || !dueHint) {
@ -967,14 +966,12 @@ function updateModalDueHint() {
function openPaymentModal() { function openPaymentModal() {
if (Object.keys(cart).length === 0) return; if (Object.keys(cart).length === 0) return;
const total = getCartTotals().total; const total = Number(getCartTotals().total.toFixed(3));
const paidField = document.getElementById('modalPaidAmount'); const paidField = document.getElementById('modalPaidAmount');
document.getElementById('modalTotalAmount').innerText = total.toFixed(3) + ' ' + currencyLabel; document.getElementById('modalTotalAmount').innerText = total.toFixed(3) + ' ' + currencyLabel;
if (paidField) { if (paidField) {
if (paidField.value === '') { paidField.value = total.toFixed(3);
paidField.value = total.toFixed(3); paidField.dataset.manual = '0';
}
paidField.dataset.manual = paidField.value !== '' ? '1' : '0';
} }
updateModalDueHint(); updateModalDueHint();
paymentModalObj.show(); paymentModalObj.show();
@ -985,6 +982,7 @@ function submitSale(method) {
const customer = document.getElementById('posCustomer').value; const customer = document.getElementById('posCustomer').value;
const customerId = document.getElementById('posCustomer').dataset.id || ''; const customerId = document.getElementById('posCustomer').dataset.id || '';
const totals = getCartTotals(); const totals = getCartTotals();
const totalAmount = Number(totals.total.toFixed(3));
const paidField = document.getElementById('modalPaidAmount'); const paidField = document.getElementById('modalPaidAmount');
let paidAmount = Math.max(0, parseFloat(paidField?.value || '0') || 0); let paidAmount = Math.max(0, parseFloat(paidField?.value || '0') || 0);
const manual = paidField?.dataset.manual === '1'; const manual = paidField?.dataset.manual === '1';
@ -992,14 +990,16 @@ function submitSale(method) {
if (method === 'pay_later' && !manual) { if (method === 'pay_later' && !manual) {
paidAmount = 0; paidAmount = 0;
} else if (method !== 'pay_later' && !manual) { } else if (method !== 'pay_later' && !manual) {
paidAmount = totals.total; paidAmount = totalAmount;
} }
if (paidAmount > totals.total + 0.0005) { paidAmount = Number(Math.min(totalAmount, Math.max(0, paidAmount)).toFixed(3));
if (paidAmount > totalAmount + 0.0005) {
Swal.fire({icon: 'warning', text: '<?= h(tr('المبلغ المدفوع لا يمكن أن يتجاوز إجمالي الفاتورة.', 'Paid amount cannot exceed the invoice total.')) ?>'}); Swal.fire({icon: 'warning', text: '<?= h(tr('المبلغ المدفوع لا يمكن أن يتجاوز إجمالي الفاتورة.', 'Paid amount cannot exceed the invoice total.')) ?>'});
return; return;
} }
if (paidAmount < totals.total && !customerId) { if (paidAmount + 0.0005 < totalAmount && !customerId) {
Swal.fire({icon: 'warning', text: '<?= h(tr('يجب اختيار عميل مسجل عند وجود مبلغ متبقٍ.', 'Select a registered customer when there is a remaining balance.')) ?>'}); Swal.fire({icon: 'warning', text: '<?= h(tr('يجب اختيار عميل مسجل عند وجود مبلغ متبقٍ.', 'Select a registered customer when there is a remaining balance.')) ?>'});
return; return;
} }