diff --git a/api/settings.php b/api/settings.php index 5507274..f214b57 100644 --- a/api/settings.php +++ b/api/settings.php @@ -10,11 +10,36 @@ if (!in_array($user['role'], ['owner', 'manager'])) { } 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(); $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_enabled', 'wablas_token', 'wablas_secret_key', 'wablas_api_url', 'wablas_invoice_recipients', 'wablas_report_recipients', 'wablas_daily_auto_send', 'wablas_daily_auto_time', 'wablas_daily_auto_last_date', 'wablas_template_invoice', 'wablas_template_daily_report', @@ -30,10 +55,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { 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; + $respond(false, 'danger', tr('رقم هاتف الشركة يجب أن يكون عمانياً من 8 خانات.', 'Company phone must be an 8-digit Oman number.'), $redirectBack()); } $_POST['company_phone'] = $companyPhone; } @@ -41,10 +63,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { foreach (['wablas_invoice_recipients', 'wablas_report_recipients'] as $phoneListKey) { $parsed = wablas_parse_phone_list((string) ($_POST[$phoneListKey] ?? '')); if (!empty($parsed['invalid'])) { - set_flash('danger', tr('يوجد رقم واتساب غير صالح في الحقل.', 'There is an invalid WhatsApp number in the field.') . ' ' . implode(', ', $parsed['invalid'])); - $referer = $_SERVER['HTTP_REFERER'] ?? '../index.php'; - header('Location: ' . $referer); - exit; + $respond(false, 'danger', tr('يوجد رقم واتساب غير صالح في الحقل.', 'There is an invalid WhatsApp number in the field.') . ' ' . implode(', ', $parsed['invalid']), $redirectBack()); } $_POST[$phoneListKey] = implode(',', $parsed['phones']); } @@ -90,10 +109,5 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } } - set_flash('success', tr('تم حفظ الإعدادات بنجاح.', 'Settings saved successfully.')); - - // Redirect back to referring page - $referer = $_SERVER['HTTP_REFERER'] ?? '../index.php'; - header('Location: ' . $referer); - exit; + $respond(true, 'success', tr('تم حفظ الإعدادات بنجاح.', 'Settings saved successfully.'), $redirectBack()); } \ No newline at end of file diff --git a/api/wablas_test.php b/api/wablas_test.php index 23eaf4d..20e0bf2 100644 --- a/api/wablas_test.php +++ b/api/wablas_test.php @@ -12,48 +12,68 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') { 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'] ?? '')); $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', ''))); +$apiUrl = trim((string) ($_POST['wablas_api_url'] ?? get_setting('wablas_api_url', ''))); if ($phoneInput === '') { - set_flash('danger', tr('أدخل رقم واتساب تجريبي صالحاً من 8 خانات.', 'Enter a valid 8-digit test WhatsApp number.')); - header('Location: ' . ($_SERVER['HTTP_REFERER'] ?? '../index.php')); - exit; + $respond(false, 'danger', tr('أدخل رقم واتساب تجريبي صالحاً من 8 خانات.', 'Enter a valid 8-digit test WhatsApp number.'), $redirectBack()); } $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; + $respond(false, 'danger', tr('رقم الاختبار يجب أن يكون عمانياً من 8 خانات.', 'The test phone must be a valid 8-digit Oman number.'), $redirectBack()); } if ($message === '') { - set_flash('danger', tr('اكتب رسالة الاختبار أولاً.', 'Write the test message first.')); - header('Location: ' . ($_SERVER['HTTP_REFERER'] ?? '../index.php')); - exit; + $respond(false, 'danger', tr('اكتب رسالة الاختبار أولاً.', 'Write the test message first.'), $redirectBack()); } 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; + $respond(false, 'danger', tr('أدخل Wablas Token و Secret Key قبل إرسال الاختبار.', 'Enter the Wablas token and secret key before sending a test message.'), $redirectBack()); } $result = wablas_send_message($phone, $message, [ 'token' => $token, 'secret_key' => $secretKey, + 'api_url' => $apiUrl, 'ignore_enabled' => true, ]); 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 { $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')); -exit; +$respond(false, 'danger', tr('فشل إرسال رسالة الاختبار.', 'Failed to send the test message.'), $redirectBack()); diff --git a/includes/app.php b/includes/app.php index 4bfdd78..8dad573 100644 --- a/includes/app.php +++ b/includes/app.php @@ -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 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 + ('wablas_api_url', 'https://wablas.com/api/send-message'), ('wablas_invoice_recipients', ''), ('wablas_report_recipients', ''), ('wablas_template_invoice', ''), @@ -1006,6 +1007,25 @@ function wablas_is_configured(bool $requireEnabled = true): bool && (!$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 { 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', ''))); $secretKey = trim((string) ($options['secret_key'] ?? get_setting('wablas_secret_key', ''))); $ignoreEnabled = !empty($options['ignore_enabled']); + $apiUrl = wablas_normalize_api_url($options['api_url'] ?? null); $message = trim($message); if ($localPhone === '') { @@ -1517,7 +1538,7 @@ function wablas_send_message(string $phone, string $message, array $options = [] return ['success' => false, 'error' => 'Wablas is not configured']; } - $endpoint = 'https://wablas.com/api/send-message'; + $endpoint = $apiUrl; $payload = http_build_query([ 'phone' => '968' . $localPhone, 'message' => $message, @@ -1567,12 +1588,23 @@ function wablas_send_message(string $phone, string $message, array $options = [] $decoded = json_decode((string) $responseBody, true); $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 [ 'success' => $success, 'status' => $httpCode, 'data' => $decoded, 'raw' => $responseBody, 'phone' => '968' . $localPhone, + 'endpoint' => $endpoint, + 'error' => $error, ]; } @@ -1656,20 +1688,26 @@ function create_sale(array $data): int { ensure_sales_table(); - db()->beginTransaction(); + $pdo = db(); + $pdo->beginTransaction(); 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) 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())'); - $stmt->bindValue(':receipt_no', $data['receipt_no']); + $stmt->bindValue(':receipt_no', $receiptNo); $stmt->bindValue(':sale_mode', $data['sale_mode']); $stmt->bindValue(':branch_code', $data['branch_code']); $stmt->bindValue(':cashier_username', $data['cashier_username']); $stmt->bindValue(':cashier_name', $data['cashier_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(':payment_method', $data['payment_method']); $stmt->bindValue(':payment_status', $data['payment_status'] ?? 'paid'); @@ -1684,14 +1722,14 @@ function create_sale(array $data): int $stmt->bindValue(':notes', $data['notes']); $stmt->execute(); - $saleId = (int) db()->lastInsertId(); + $saleId = (int) $pdo->lastInsertId(); sync_order_stock_reservation([], 'completed', $data['items'] ?? [], (string) ($data['status'] ?? 'completed')); - db()->commit(); + $pdo->commit(); return $saleId; } catch (Throwable $e) { - if (db()->inTransaction()) { - db()->rollBack(); + if ($pdo->inTransaction()) { + $pdo->rollBack(); } 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 +{ + $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); } diff --git a/includes/footer_settings.php b/includes/footer_settings.php index 5bc78ab..a5d3886 100644 --- a/includes/footer_settings.php +++ b/includes/footer_settings.php @@ -360,7 +360,8 @@