wablas gateway
This commit is contained in:
parent
1554df04a2
commit
013232adaa
45
cron_wablas.php
Normal file
45
cron_wablas.php
Normal 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 . "
|
||||
";
|
||||
}
|
||||
21
db/migrations/20260503_zzz_wablas_dispatch_logs.sql
Normal file
21
db/migrations/20260503_zzz_wablas_dispatch_logs.sql
Normal 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
1090
includes/wablas_helper.php
Normal file
File diff suppressed because it is too large
Load Diff
35
index.php
35
index.php
@ -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
156
pages/logs_view.php
Normal 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>
|
||||
@ -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(); }
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 +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 today’s daily summary is already due, then processes pending/failed Wablas jobs immediately." data-ar="يتحقق هذا مما إذا كان الملخص اليومي لليوم مستحقًا بالفعل، ثم يعالج مهام Wablas المعلقة/الفاشلة فورًا.">This checks whether today’s 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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user