wablas gateway

This commit is contained in:
Flatlogic Bot 2026-05-03 07:32:13 +00:00
parent 1554df04a2
commit 013232adaa
8 changed files with 1442 additions and 47 deletions

45
cron_wablas.php Normal file
View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
if (PHP_SAPI !== 'cli') {
header('Content-Type: text/plain; charset=utf-8');
header('X-Robots-Tag: noindex, nofollow');
$remote = (string) ($_SERVER['REMOTE_ADDR'] ?? '');
if (!in_array($remote, ['127.0.0.1', '::1'], true)) {
http_response_code(403);
exit("Forbidden
");
}
}
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/DatabaseInstaller.php';
require_once __DIR__ . '/includes/wablas_helper.php';
DatabaseInstaller::ensureCurrentSchema();
$now = wablasNow();
echo '[' . $now->format('Y-m-d H:i:s') . "] Starting Wablas automation run
";
$dailyQueue = wablasQueueDailySummaryIfDue($now);
if (!empty($dailyQueue['queued'])) {
echo 'Queued daily summary for ' . (($dailyQueue['scheduled_for'] ?? $now->format('Y-m-d H:i:s'))) . "
";
} elseif (!empty($dailyQueue['reason']) && ($dailyQueue['reason'] ?? '') !== 'not_due') {
echo 'Daily summary: ' . $dailyQueue['reason'] . "
";
}
$processed = wablasProcessDueDispatches(25);
echo 'Checked: ' . (int) ($processed['checked'] ?? 0)
. ' | Sent: ' . (int) ($processed['sent'] ?? 0)
. ' | Failed: ' . (int) ($processed['failed'] ?? 0)
. ' | Skipped: ' . (int) ($processed['skipped'] ?? 0)
. "
";
foreach (($processed['messages'] ?? []) as $message) {
echo '- ' . $message . "
";
}

View File

@ -0,0 +1,21 @@
-- Wablas automation queue and delivery log
CREATE TABLE IF NOT EXISTS `wablas_dispatch_logs` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`channel` varchar(50) NOT NULL DEFAULT 'wablas',
`event_type` varchar(50) NOT NULL,
`event_key` varchar(190) NOT NULL,
`scheduled_for` datetime DEFAULT NULL,
`last_attempt_at` datetime DEFAULT NULL,
`attempt_count` int(11) NOT NULL DEFAULT 0,
`recipient_count` int(11) NOT NULL DEFAULT 0,
`status` varchar(50) NOT NULL DEFAULT 'pending',
`request_payload` longtext DEFAULT NULL,
`response_payload` longtext DEFAULT NULL,
`error_message` text DEFAULT NULL,
`created_at` timestamp NULL DEFAULT current_timestamp(),
`updated_at` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_wablas_dispatch_event` (`channel`, `event_type`, `event_key`),
KEY `idx_wablas_dispatch_status` (`status`, `scheduled_for`),
KEY `idx_wablas_dispatch_attempts` (`attempt_count`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

1090
includes/wablas_helper.php Normal file

File diff suppressed because it is too large Load Diff

View File

@ -107,6 +107,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/SimpleXLSX.php';
require_once __DIR__ . '/includes/stock_helper.php';
require_once __DIR__ . '/includes/wablas_helper.php';
require_once __DIR__ . '/db/BackupService.php';
// Helper for current outlet
@ -10446,39 +10447,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
<?php endif; ?>
<?php elseif ($page === 'logs'): ?>
<div class="card border-0 shadow-sm rounded-4 overflow-hidden">
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center border-0">
<div>
<h5 class="m-0 fw-bold text-primary" data-en="System Logs" data-ar="سجلات النظام">System Logs</h5>
<p class="text-muted small mb-0" data-en="Monitor system activity and errors" data-ar="مراقبة نشاط النظام والأخطاء">Monitor system activity and errors</p>
</div>
<div class="d-flex gap-2">
<button onclick="window.location.reload()" class="btn btn-outline-primary btn-sm rounded-pill">
<i class="bi bi-arrow-clockwise"></i> Refresh
</button>
</div>
</div>
<div class="card-body p-0">
<div class="bg-dark text-light p-4 font-monospace small" style="max-height: 600px; overflow-y: auto;">
<?php
$log_files = ['runtime_debug.log', 'error_log', 'login_debug.log', 'post_debug.log', 'search_debug.log', 'debug.log'];
$found_logs = false;
foreach ($log_files as $file) {
$path = __DIR__ . '/' . $file;
if (file_exists($path) && is_readable($path)) {
$found_logs = true;
echo "<div class='mb-4'><h6 class='text-warning border-bottom border-secondary pb-2'>--- " . htmlspecialchars(basename($file)) . " ---</h6>";
$lines = shell_exec("tail -n 50 " . escapeshellarg($path));
echo "<pre class='mb-0 text-info'>" . htmlspecialchars((string)$lines) . "</pre></div>";
}
}
if (!$found_logs) {
echo "<div class='text-center py-5'><i class='bi bi-journal-x fs-1 opacity-25'></i><p class='mt-2 opacity-50'>No accessible log files found.</p></div>";
}
?>
</div>
</div>
</div>
<?php require 'pages/logs_view.php'; ?>
<?php endif; ?>
</div>

156
pages/logs_view.php Normal file
View File

@ -0,0 +1,156 @@
<?php
$wablasTableReady = function_exists('wablasDispatchLogTableReady') && wablasDispatchLogTableReady();
$wablasLogs = $wablasTableReady && function_exists('wablasFetchRecentDispatchLogs')
? wablasFetchRecentDispatchLogs(25)
: [];
$wablasPreview = static function (string $value, int $limit = 120): string {
$value = trim($value);
if ($value === '') {
return '';
}
if (function_exists('mb_strlen') && function_exists('mb_substr')) {
return mb_strlen($value) > $limit ? mb_substr($value, 0, $limit - 3) . '...' : $value;
}
return strlen($value) > $limit ? substr($value, 0, $limit - 3) . '...' : $value;
};
$logFiles = ['runtime_debug.log', 'error_log', 'login_debug.log', 'post_debug.log', 'search_debug.log', 'debug.log'];
?>
<div class="d-flex flex-column gap-4">
<div class="card border-0 shadow-sm rounded-4 overflow-hidden">
<div class="card-header bg-white py-3 d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-3 border-0">
<div>
<h1 class="h5 m-0 fw-bold text-primary" data-en="System Logs & Wablas Activity" data-ar="سجلات النظام ونشاط Wablas">System Logs & Wablas Activity</h1>
<p class="text-muted small mb-0" data-en="Monitor file logs plus recent WhatsApp dispatch results" data-ar="راقب سجلات الملفات بالإضافة إلى نتائج إرسال واتساب الأخيرة">Monitor file logs plus recent WhatsApp dispatch results</p>
</div>
<div class="d-flex gap-2 flex-wrap">
<a href="<?= htmlspecialchars(page_url('settings', ['tab' => 'integrations'])) ?>" class="btn btn-outline-secondary btn-sm rounded-pill px-3">
<i class="bi bi-whatsapp me-1"></i>
<span data-en="Wablas Settings" data-ar="إعدادات Wablas">Wablas Settings</span>
</a>
<button onclick="window.location.reload()" class="btn btn-outline-primary btn-sm rounded-pill px-3">
<i class="bi bi-arrow-clockwise me-1"></i>
<span data-en="Refresh" data-ar="تحديث">Refresh</span>
</button>
</div>
</div>
<div class="card-body p-0">
<?php if (!$wablasTableReady): ?>
<div class="p-4">
<div class="alert alert-warning border-0 shadow-sm mb-0" data-en="The Wablas dispatch log table is not available yet. Run the migration before using WhatsApp automation." data-ar="جدول سجل إرسال Wablas غير متاح بعد. شغّل الترحيل قبل استخدام أتمتة واتساب.">The Wablas dispatch log table is not available yet. Run the migration before using WhatsApp automation.</div>
</div>
<?php elseif ($wablasLogs === []): ?>
<div class="text-center py-5 px-4">
<i class="bi bi-chat-square-text fs-1 text-muted opacity-25"></i>
<p class="text-muted mt-3 mb-1" data-en="No Wablas activity has been logged yet." data-ar="لم يتم تسجيل أي نشاط Wablas حتى الآن.">No Wablas activity has been logged yet.</p>
<p class="text-muted small mb-0" data-en="Create a sales invoice, send a test WhatsApp from Settings, or run the queue to see entries here." data-ar="أنشئ فاتورة بيع، أو أرسل رسالة واتساب تجريبية من الإعدادات، أو شغّل قائمة الانتظار لرؤية السجلات هنا.">Create a sales invoice, send a test WhatsApp from Settings, or run the queue to see entries here.</p>
</div>
<?php else: ?>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th class="px-4 py-3" data-en="Event" data-ar="الحدث">Event</th>
<th class="px-4 py-3" data-en="Status" data-ar="الحالة">Status</th>
<th class="px-4 py-3" data-en="Schedule / Attempt" data-ar="الجدولة / المحاولة">Schedule / Attempt</th>
<th class="px-4 py-3" data-en="Recipients / Message" data-ar="المستلمون / الرسالة">Recipients / Message</th>
<th class="px-4 py-3" data-en="Response" data-ar="الاستجابة">Response</th>
</tr>
</thead>
<tbody>
<?php foreach ($wablasLogs as $row): ?>
<?php
$eventType = (string) ($row['event_type'] ?? '');
$eventLabel = function_exists('wablasDispatchEventLabel') ? wablasDispatchEventLabel($eventType) : ucfirst(str_replace('_', ' ', $eventType));
$statusMeta = function_exists('wablasDispatchStatusMeta')
? wablasDispatchStatusMeta((string) ($row['status'] ?? 'pending'))
: ['label' => ucfirst((string) ($row['status'] ?? 'Pending')), 'class' => 'bg-warning bg-opacity-10 text-warning'];
$requestPayload = function_exists('wablasJsonDecode') ? wablasJsonDecode((string) ($row['request_payload'] ?? '')) : [];
$responsePayload = function_exists('wablasJsonDecode') ? wablasJsonDecode((string) ($row['response_payload'] ?? '')) : [];
$numbers = isset($requestPayload['numbers']) && is_array($requestPayload['numbers']) ? array_values($requestPayload['numbers']) : [];
$numbersPreview = implode(', ', array_slice($numbers, 0, 3));
if (count($numbers) > 3) {
$numbersPreview .= ' +' . (count($numbers) - 3) . ' more';
}
$messagePreview = $wablasPreview((string) ($requestPayload['message'] ?? ''), 140);
$httpCode = (string) ($responsePayload['http_code'] ?? '');
$errorMessage = trim((string) ($row['error_message'] ?? ''));
?>
<tr>
<td class="px-4 py-3">
<div class="fw-semibold text-dark"><?= htmlspecialchars($eventLabel !== '' ? $eventLabel : 'Dispatch') ?></div>
<div class="small text-muted"><code><?= htmlspecialchars((string) ($row['event_key'] ?? '')) ?></code></div>
<div class="small text-muted mt-1">
<span data-en="Created" data-ar="أُنشئ">Created</span>:
<?= htmlspecialchars((string) ($row['created_at'] ?? '—')) ?>
</div>
</td>
<td class="px-4 py-3">
<span class="badge rounded-pill <?= htmlspecialchars($statusMeta['class']) ?> px-3 py-2"><?= htmlspecialchars($statusMeta['label']) ?></span>
<div class="small text-muted mt-2">
<span data-en="Attempts" data-ar="المحاولات">Attempts</span>:
<?= (int) ($row['attempt_count'] ?? 0) ?>
</div>
</td>
<td class="px-4 py-3 small text-muted">
<div><span class="fw-semibold text-dark" data-en="Scheduled" data-ar="مجدول">Scheduled</span>: <?= htmlspecialchars((string) ($row['scheduled_for'] ?? '—')) ?></div>
<div class="mt-1"><span class="fw-semibold text-dark" data-en="Last attempt" data-ar="آخر محاولة">Last attempt</span>: <?= htmlspecialchars((string) ($row['last_attempt_at'] ?? '—')) ?></div>
</td>
<td class="px-4 py-3">
<div class="small text-muted mb-1">
<span class="fw-semibold text-dark" data-en="Recipients" data-ar="المستلمون">Recipients</span>:
<?= htmlspecialchars($numbersPreview !== '' ? $numbersPreview : '—') ?>
</div>
<div class="small text-muted">
<span class="fw-semibold text-dark" data-en="Message" data-ar="الرسالة">Message</span>:
<?= htmlspecialchars($messagePreview !== '' ? $messagePreview : '—') ?>
</div>
</td>
<td class="px-4 py-3">
<?php if ($httpCode !== ''): ?>
<div class="small text-muted mb-1"><span class="fw-semibold text-dark">HTTP</span>: <?= htmlspecialchars($httpCode) ?></div>
<?php endif; ?>
<?php if ($errorMessage !== ''): ?>
<div class="small text-danger"><?= htmlspecialchars($wablasPreview($errorMessage, 160)) ?></div>
<?php else: ?>
<div class="small text-muted" data-en="No error recorded" data-ar="لا يوجد خطأ مسجل">No error recorded</div>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
<div class="card border-0 shadow-sm rounded-4 overflow-hidden">
<div class="card-header bg-white py-3 border-0">
<div>
<h2 class="h6 m-0 fw-bold text-primary" data-en="File-based Debug Logs" data-ar="سجلات التصحيح النصية">File-based Debug Logs</h2>
<p class="text-muted small mb-0" data-en="Latest lines from the app log files available in this workspace" data-ar="أحدث السطور من ملفات سجلات التطبيق المتاحة في مساحة العمل هذه">Latest lines from the app log files available in this workspace</p>
</div>
</div>
<div class="card-body p-0">
<div class="bg-dark text-light p-4 font-monospace small" style="max-height: 600px; overflow-y: auto;">
<?php
$foundLogs = false;
foreach ($logFiles as $file) {
$path = __DIR__ . '/../' . $file;
if (file_exists($path) && is_readable($path)) {
$foundLogs = true;
echo "<div class='mb-4'><h6 class='text-warning border-bottom border-secondary pb-2'>--- " . htmlspecialchars(basename($file)) . " ---</h6>";
$lines = shell_exec("tail -n 50 " . escapeshellarg($path));
echo "<pre class='mb-0 text-info'>" . htmlspecialchars((string) $lines) . "</pre></div>";
}
}
if (!$foundLogs) {
echo "<div class='text-center py-5'><i class='bi bi-journal-x fs-1 opacity-25'></i><p class='mt-2 opacity-50'>No accessible log files found.</p></div>";
}
?>
</div>
</div>
</div>
</div>

View File

@ -85,10 +85,17 @@
}
$db->commit();
$wablasNotice = '';
if ($type === 'sale' && function_exists('wablasQueueInvoiceNotification')) {
$wablasQueue = wablasQueueInvoiceNotification((int)$inv_id);
$wablasNotice = (string)($wablasQueue['notice'] ?? '');
}
$_SESSION['trigger_invoice_modal'] = true;
$_SESSION['show_invoice_id'] = (int)$inv_id;
$_SESSION['show_invoice_page'] = ($type === 'purchase') ? 'purchases' : 'sales';
$msg = ($type === 'purchase' ? "Purchase" : "Invoice") . " #$inv_id created!";
$msg = ($type === 'purchase' ? "Purchase" : "Invoice") . " #$inv_id created!" . $wablasNotice;
redirectWithMessage($msg, page_url($type === 'purchase' ? 'purchases' : 'sales'));
} catch (Exception $e) { $db->rollBack(); $message = "Error: " . $e->getMessage(); }
}

View File

@ -1,4 +1,63 @@
<?php
if (isset($_POST['wablas_send_test']) || isset($_POST['wablas_run_due_now'])) {
if (can('settings_view')) {
$wablasSettingsUrl = page_url('settings', ['tab' => 'integrations']);
if (isset($_POST['wablas_send_test'])) {
$defaultCountryCode = (string) (wablasSettingValue('wablas_default_country_code', '') ?? '');
$numbers = wablasNumbersFromText((string) ($_POST['wablas_test_numbers'] ?? ''), $defaultCountryCode);
$message = substr(trim(str_replace(["\r\n", "\r"], "\n", (string) ($_POST['wablas_test_message'] ?? ''))), 0, 3000);
if ($numbers === []) {
redirectWithMessage('Error: Add at least one valid WhatsApp number for the test send.', $wablasSettingsUrl);
}
if ($message === '') {
redirectWithMessage('Error: Enter a test WhatsApp message before sending.', $wablasSettingsUrl);
}
$result = wablasSendManualTest($numbers, $message);
if (!empty($result['success'])) {
$messageText = 'Wablas test message sent to ' . count($numbers) . ' recipient(s).';
if (!empty($result['log_id'])) {
$messageText .= ' It was also added to System Logs.';
}
redirectWithMessage($messageText, $wablasSettingsUrl);
}
$errorText = trim((string) ($result['error'] ?? 'Wablas test send failed.'));
redirectWithMessage('Error: ' . $errorText, $wablasSettingsUrl);
}
if (isset($_POST['wablas_run_due_now'])) {
$dailyQueue = wablasQueueDailySummaryIfDue(wablasNow());
$processed = wablasProcessDueDispatches(25);
$parts = [];
if (!empty($dailyQueue['queued'])) {
$parts[] = 'Daily summary queued.';
} elseif (($dailyQueue['reason'] ?? '') === 'already_exists') {
$parts[] = "Today's daily summary already exists.";
} elseif (($dailyQueue['reason'] ?? '') === 'not_due') {
$parts[] = 'Daily summary is not due yet.';
} elseif (($dailyQueue['reason'] ?? '') === 'no_recipients') {
$parts[] = 'Daily summary recipients are not configured.';
} elseif (($dailyQueue['reason'] ?? '') === 'disabled') {
$parts[] = 'Daily summary automation is disabled.';
} elseif (!empty($dailyQueue['error'])) {
$parts[] = 'Error: ' . (string) $dailyQueue['error'];
}
$parts[] = 'Queue run checked ' . (int) ($processed['checked'] ?? 0) . ' job(s), sent ' . (int) ($processed['sent'] ?? 0) . ', failed ' . (int) ($processed['failed'] ?? 0) . ', skipped ' . (int) ($processed['skipped'] ?? 0) . '.';
if (!empty($processed['messages'][0])) {
$parts[] = (string) $processed['messages'][0];
}
redirectWithMessage(implode(' ', array_filter($parts)), $wablasSettingsUrl);
}
}
}
if (isset($_POST['update_settings'])) {
if (can('settings_view')) {
$db = db();
@ -105,16 +164,8 @@ if (isset($_POST['update_settings'])) {
}
foreach (['wablas_invoice_numbers', 'wablas_daily_summary_numbers'] as $numbersKey) {
$numbersRaw = str_replace(["\r\n", "\r"], "\n", (string)($settings[$numbersKey] ?? ''));
$parts = preg_split('/[\n,;]+/', $numbersRaw) ?: [];
$normalizedNumbers = [];
foreach ($parts as $part) {
$phone = preg_replace('/[^0-9+]/', '', trim((string)$part));
if ($phone !== '') {
$normalizedNumbers[$phone] = true;
}
}
$settings[$numbersKey] = implode("\n", array_slice(array_keys($normalizedNumbers), 0, 50));
$normalizedNumbers = wablasNumbersFromText((string)($settings[$numbersKey] ?? ''), (string)$settings['wablas_default_country_code']);
$settings[$numbersKey] = implode("\n", array_slice($normalizedNumbers, 0, 50));
}
foreach (['wablas_invoice_template' => 3000, 'wablas_daily_summary_template' => 3000] as $templateKey => $limit) {

View File

@ -537,7 +537,7 @@ $wablasConfigured = !empty($data['settings']['wablas_api_url']) && !empty($data[
<div class="col-12">
<label class="form-label text-muted small fw-semibold" data-en="Invoice Template" data-ar="قالب الفاتورة">Invoice Template</label>
<textarea name="settings[wablas_invoice_template]" class="form-control" rows="6" placeholder="Hello {customer_name}, your invoice {invoice_no} total is {grand_total}. View: {invoice_url}"><?= htmlspecialchars($data['settings']['wablas_invoice_template'] ?? '') ?></textarea>
<div class="form-text" data-en="Placeholders: {invoice_no}, {customer_name}, {grand_total}, {invoice_url}, {company_name}" data-ar="المتغيرات: {invoice_no} و {customer_name} و {grand_total} و {invoice_url} و {company_name}">Placeholders: <code>{invoice_no}</code> <code>{customer_name}</code> <code>{grand_total}</code> <code>{invoice_url}</code> <code>{company_name}</code></div>
<div class="form-text" data-en="Placeholders: {invoice_no}, {customer_name}, {grand_total}, {invoice_url}, {company_name}, {invoice_date}, {due_date}, {status}, {outlet_name}" data-ar="المتغيرات: {invoice_no} و {customer_name} و {grand_total} و {invoice_url} و {company_name} و {invoice_date} و {due_date} و {status} و {outlet_name}">Placeholders: <code>{invoice_no}</code> <code>{customer_name}</code> <code>{grand_total}</code> <code>{invoice_url}</code> <code>{company_name}</code> <code>{invoice_date}</code> <code>{due_date}</code> <code>{status}</code> <code>{outlet_name}</code></div>
</div>
</div>
</div>
@ -575,7 +575,7 @@ $wablasConfigured = !empty($data['settings']['wablas_api_url']) && !empty($data[
<div class="col-12">
<label class="form-label text-muted small fw-semibold" data-en="Daily Summary Template" data-ar="قالب الملخص اليومي">Daily Summary Template</label>
<textarea name="settings[wablas_daily_summary_template]" class="form-control" rows="6" placeholder="Daily summary {report_date}: invoices {invoice_count}, sales {sales_total}, cash {cash_total}, card {card_total}"><?= htmlspecialchars($data['settings']['wablas_daily_summary_template'] ?? '') ?></textarea>
<div class="form-text" data-en="Placeholders: {report_date}, {invoice_count}, {sales_total}, {cash_total}, {card_total}, {company_name}" data-ar="المتغيرات: {report_date} و {invoice_count} و {sales_total} و {cash_total} و {card_total} و {company_name}">Placeholders: <code>{report_date}</code> <code>{invoice_count}</code> <code>{sales_total}</code> <code>{cash_total}</code> <code>{card_total}</code> <code>{company_name}</code></div>
<div class="form-text" data-en="Placeholders: {report_date}, {report_time}, {invoice_count}, {sales_total}, {paid_total}, {cash_total}, {card_total}, {due_total}, {company_name}" data-ar="المتغيرات: {report_date} و {report_time} و {invoice_count} و {sales_total} و {paid_total} و {cash_total} و {card_total} و {due_total} و {company_name}">Placeholders: <code>{report_date}</code> <code>{report_time}</code> <code>{invoice_count}</code> <code>{sales_total}</code> <code>{paid_total}</code> <code>{cash_total}</code> <code>{card_total}</code> <code>{due_total}</code> <code>{company_name}</code></div>
</div>
</div>
</div>
@ -584,7 +584,63 @@ $wablasConfigured = !empty($data['settings']['wablas_api_url']) && !empty($data[
<div class="alert alert-light border rounded-4 mt-4 mb-0 small">
<div class="fw-semibold mb-1" data-en="Scheduling note" data-ar="ملاحظة الجدولة">Scheduling note</div>
<div class="text-muted mb-0" data-en="These fields now store the Wablas connection profile, recipients, templates, and preferred times. If you want, I can wire the actual send action / cron job next so the invoice and daily summary messages are dispatched automatically." data-ar="تحفظ هذه الحقول الآن ملف اتصال Wablas والمستلمين والقوالب والأوقات المفضلة. إذا رغبت، يمكنني توصيل إجراء الإرسال الفعلي / مهمة cron بعد ذلك لإرسال رسائل الفاتورة والملخص اليومي تلقائيًا.">These fields now store the Wablas connection profile, recipients, templates, and preferred times. If you want, I can wire the actual send action / cron job next so the invoice and daily summary messages are dispatched automatically.</div>
<div class="text-muted mb-0" data-en="Invoice notifications are now queued from the sales workflow, and daily summaries are queued/sent by cron_wablas.php using the enable/disable switches above. Run the cron script every minute so due WhatsApp messages are dispatched automatically." data-ar="أصبحت إشعارات الفواتير تُضاف الآن إلى قائمة الانتظار من سير عمل المبيعات، كما تتم جدولة/إرسال الملخص اليومي عبر الملف cron_wablas.php باستخدام مفاتيح التفعيل والتعطيل أعلاه. شغّل مهمة cron كل دقيقة لإرسال رسائل واتساب المستحقة تلقائيًا.">Invoice notifications are now queued from the sales workflow, and daily summaries are queued/sent by cron_wablas.php using the enable/disable switches above. Run the cron script every minute so due WhatsApp messages are dispatched automatically.</div>
</div>
<div class="card border-0 bg-light rounded-4 mt-4 shadow-sm overflow-hidden">
<div class="card-body p-4">
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-3 mb-3">
<div>
<h3 class="h6 fw-bold mb-1" data-en="Wablas Manual Tools" data-ar="أدوات Wablas اليدوية">Wablas Manual Tools</h3>
<p class="text-muted small mb-0" data-en="Use these controls to send a test WhatsApp or run the due queue immediately without waiting for cron." data-ar="استخدم هذه الأدوات لإرسال رسالة واتساب تجريبية أو تشغيل قائمة الانتظار المستحقة فورًا دون انتظار مهمة cron.">Use these controls to send a test WhatsApp or run the due queue immediately without waiting for cron.</p>
</div>
<a href="<?= htmlspecialchars(page_url('logs')) ?>" class="btn btn-outline-secondary btn-sm rounded-pill px-3">
<i class="bi bi-clock-history me-1"></i>
<span data-en="View Dispatch Logs" data-ar="عرض سجل الإرسال">View Dispatch Logs</span>
</a>
</div>
<div class="alert alert-warning border-0 py-2 small mb-4">
<div class="fw-semibold mb-1" data-en="Save before testing" data-ar="احفظ قبل الاختبار">Save before testing</div>
<div class="mb-0" data-en="Manual tools use the saved Wablas credentials and templates. If you just changed the gateway settings above, click Save All Changes first." data-ar="تستخدم الأدوات اليدوية بيانات اعتماد Wablas والقوالب المحفوظة. إذا غيّرت إعدادات البوابة أعلاه الآن، فانقر أولاً على حفظ جميع التغييرات.">Manual tools use the saved Wablas credentials and templates. If you just changed the gateway settings above, click Save All Changes first.</div>
</div>
<div class="row g-4 align-items-stretch">
<div class="col-lg-7">
<label class="form-label text-muted small fw-semibold" data-en="Test Recipient Numbers" data-ar="أرقام المستلمين للاختبار">Test Recipient Numbers</label>
<textarea name="wablas_test_numbers" class="form-control" rows="3" placeholder="+96890000000&#10;+96891111111"></textarea>
<div class="form-text" data-en="Use one number per line or separate numbers with commas. The saved default country code is applied when needed." data-ar="استخدم رقمًا واحدًا في كل سطر أو افصل الأرقام بفواصل. سيتم تطبيق رمز الدولة الافتراضي المحفوظ عند الحاجة.">Use one number per line or separate numbers with commas. The saved default country code is applied when needed.</div>
<label class="form-label text-muted small fw-semibold mt-3" data-en="Test Message" data-ar="رسالة الاختبار">Test Message</label>
<textarea name="wablas_test_message" class="form-control" rows="4" placeholder="This is a test WhatsApp message from the admin panel."></textarea>
<div class="form-text" data-en="This sends immediately and writes the result to the Wablas dispatch log." data-ar="يتم الإرسال فورًا ويتم تسجيل النتيجة في سجل إرسال Wablas.">This sends immediately and writes the result to the Wablas dispatch log.</div>
<button type="submit" name="wablas_send_test" value="1" formnovalidate class="btn btn-success rounded-pill px-4 mt-3">
<i class="bi bi-send me-2"></i>
<span data-en="Send Test WhatsApp" data-ar="إرسال واتساب تجريبي">Send Test WhatsApp</span>
</button>
</div>
<div class="col-lg-5">
<div class="card border-0 shadow-sm h-100 rounded-4">
<div class="card-body p-4 d-flex flex-column">
<div class="rounded-circle bg-primary bg-opacity-10 text-primary p-2 align-self-start mb-3"><i class="bi bi-lightning-charge"></i></div>
<h4 class="h6 fw-bold mb-2" data-en="Run Due Queue Now" data-ar="تشغيل قائمة الانتظار المستحقة الآن">Run Due Queue Now</h4>
<p class="text-muted small mb-3" data-en="This checks whether todays daily summary is already due, then processes pending/failed Wablas jobs immediately." data-ar="يتحقق هذا مما إذا كان الملخص اليومي لليوم مستحقًا بالفعل، ثم يعالج مهام Wablas المعلقة/الفاشلة فورًا.">This checks whether todays daily summary is already due, then processes pending/failed Wablas jobs immediately.</p>
<ul class="small text-muted ps-3 mb-4">
<li data-en="Respects the master Wablas enable/disable switch" data-ar="يحترم مفتاح تفعيل/تعطيل Wablas الرئيسي">Respects the master Wablas enable/disable switch</li>
<li data-en="Keeps future-scheduled jobs pending until they are due" data-ar="يبقي المهام المجدولة للمستقبل معلقة حتى يحين وقتها">Keeps future-scheduled jobs pending until they are due</li>
<li data-en="Useful for testing invoices and same-day summaries" data-ar="مفيد لاختبار الفواتير والملخصات اليومية لنفس اليوم">Useful for testing invoices and same-day summaries</li>
</ul>
<button type="submit" name="wablas_run_due_now" value="1" formnovalidate class="btn btn-outline-primary rounded-pill px-4 mt-auto">
<i class="bi bi-play-circle me-2"></i>
<span data-en="Run Due Queue Now" data-ar="تشغيل قائمة الانتظار المستحقة الآن">Run Due Queue Now</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>