Autosave: 20260422-182218
This commit is contained in:
parent
5bfaa401f2
commit
b2cb1b5a0b
@ -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;
|
|
||||||
}
|
}
|
||||||
@ -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;
|
|
||||||
|
|||||||
112
includes/app.php
112
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 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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 + '"]');
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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
20
pos.php
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user