Enhancement and Improvements

Refactor CSRF class: improve token handling and update session variable names

Replace bulk message with ajax based message sending.
Added support for multiple recipients in bulk message, and also router based filtering.

Add support for multiple recipients in bulk message from customer list as requested by one of our Member. you can now send messages to multiple recipients at once from customer list.

Added Exception for CRON but not tested yet. i dont have multiple routers.
Added notify to know if cron has been executed or not.
This commit is contained in:
Focuslinks Digital Solutions 2025-02-09 16:06:59 +01:00
parent 60d945d87f
commit 0a3205915f
5 changed files with 863 additions and 493 deletions

View File

@ -6,50 +6,83 @@
**/ **/
class Csrf class Csrf
{ {
private static $tokenExpiration = 1800; // 30 minutes private const int TOKEN_LENGTH = 16;
private const int TOKEN_EXPIRATION = 1800;
public static function generateToken($length = 16)
{ /**
return bin2hex(random_bytes($length)); * Generate a CSRF token.
} *
* @param int $length
public static function validateToken($token, $storedToken) * @return string
{ */
return hash_equals($token, $storedToken); public static function generateToken(int $length = self::TOKEN_LENGTH): string
} {
return bin2hex(random_bytes($length));
public static function check($token) }
{
global $config; /**
if($config['csrf_enabled'] == 'yes') { * Validate the provided CSRF token against the stored token.
if (isset($_SESSION['csrf_token'], $_SESSION['csrf_token_time'], $token)) { *
$storedToken = $_SESSION['csrf_token']; * @param string $token
$tokenTime = $_SESSION['csrf_token_time']; * @param string $storedToken
* @return bool
if (time() - $tokenTime > self::$tokenExpiration) { */
self::clearToken(); public static function validateToken(string $token, string $storedToken): bool
return false; {
} return hash_equals($token, $storedToken);
}
return self::validateToken($token, $storedToken);
} /**
return false; * Check if the CSRF token is valid.
} *
return true; * @param string|null $token
} * @return bool
*/
public static function generateAndStoreToken() public static function check(?string $token): bool
{ {
$token = self::generateToken(); global $config;
$_SESSION['csrf_token'] = $token;
$_SESSION['csrf_token_time'] = time(); if ($config['csrf_enabled'] === 'yes') {
return $token; if (isset($_SESSION['nux_csrf_token'], $_SESSION['nux_csrf_token_time'], $token)) {
} $storedToken = $_SESSION['nux_csrf_token'];
$tokenTime = $_SESSION['nux_csrf_token_time'];
public static function clearToken()
{ if (time() - $tokenTime > self::TOKEN_EXPIRATION) {
unset($_SESSION['csrf_token'], $_SESSION['csrf_token_time']); self::clearToken();
} return false;
} }
return self::validateToken($token, $storedToken);
}
return false;
}
return true; // CSRF is disabled
}
/**
* Generate and store a new CSRF token in the session.
*
* @return string
*/
public static function generateAndStoreToken(): string
{
$token = self::generateToken();
$_SESSION['nux_csrf_token'] = $token;
$_SESSION['nux_csrf_token_time'] = time();
return $token;
}
/**
* Clear the stored CSRF token from the session.
*
* @return void
*/
public static function clearToken(): void
{
unset($_SESSION['nux_csrf_token'], $_SESSION['nux_csrf_token_time']);
}
}

View File

@ -22,7 +22,7 @@ switch ($action) {
_alert(Lang::T('You do not have permission to access this page'), 'danger', "dashboard"); _alert(Lang::T('You do not have permission to access this page'), 'danger', "dashboard");
} }
$appUrl = APP_URL; $appUrl = APP_URL;
$select2_customer = <<<EOT $select2_customer = <<<EOT
<script> <script>
@ -74,22 +74,22 @@ EOT;
$message = str_replace('[[user_name]]', $c['username'], $message); $message = str_replace('[[user_name]]', $c['username'], $message);
$message = str_replace('[[phone]]', $c['phonenumber'], $message); $message = str_replace('[[phone]]', $c['phonenumber'], $message);
$message = str_replace('[[company_name]]', $config['CompanyName'], $message); $message = str_replace('[[company_name]]', $config['CompanyName'], $message);
if (strpos($message, '[[payment_link]]') !== false) { if (strpos($message, '[[payment_link]]') !== false) {
// token only valid for 1 day, for security reason // token only valid for 1 day, for security reason
$token = User::generateToken($c['id'], 1); $token = User::generateToken($c['id'], 1);
if (!empty($token['token'])) { if (!empty($token['token'])) {
$tur = ORM::for_table('tbl_user_recharges') $tur = ORM::for_table('tbl_user_recharges')
->where('customer_id', $c['id']) ->where('customer_id', $c['id'])
//->where('namebp', $package) //->where('namebp', $package)
->find_one(); ->find_one();
if ($tur) { if ($tur) {
$url = '?_route=home&recharge=' . $tur['id'] . '&uid=' . urlencode($token['token']); $url = '?_route=home&recharge=' . $tur['id'] . '&uid=' . urlencode($token['token']);
$message = str_replace('[[payment_link]]', $url, $message); $message = str_replace('[[payment_link]]', $url, $message);
} }
} else { } else {
$message = str_replace('[[payment_link]]', '', $message); $message = str_replace('[[payment_link]]', '', $message);
} }
} }
//Send the message //Send the message
@ -113,158 +113,274 @@ EOT;
if (!in_array($admin['user_type'], ['SuperAdmin', 'Admin', 'Agent', 'Sales'])) { if (!in_array($admin['user_type'], ['SuperAdmin', 'Admin', 'Agent', 'Sales'])) {
_alert(Lang::T('You do not have permission to access this page'), 'danger', "dashboard"); _alert(Lang::T('You do not have permission to access this page'), 'danger', "dashboard");
} }
$ui->assign('routers', ORM::forTable('tbl_routers')->where('enabled', '1')->find_many());
$ui->display('admin/message/bulk.tpl');
break;
case 'send_bulk_ajax':
// Check user permissions
if (!in_array($admin['user_type'], ['SuperAdmin', 'Admin', 'Agent', 'Sales'])) {
die(json_encode(['status' => 'error', 'message' => 'Permission denied']));
}
set_time_limit(0); set_time_limit(0);
// Initialize counters
// Get request parameters
$group = $_REQUEST['group'] ?? '';
$message = $_REQUEST['message'] ?? '';
$via = $_REQUEST['via'] ?? '';
$batch = $_REQUEST['batch'] ?? 100;
$page = $_REQUEST['page'] ?? 0;
$router = $_REQUEST['router'] ?? null;
$test = isset($_REQUEST['test']) && $_REQUEST['test'] === 'on' ? true : false;
if (empty($group) || empty($message) || empty($via)) {
die(json_encode(['status' => 'error', 'message' => 'All fields are required']));
}
// Get batch of customers based on group
$startpoint = $page * $batch;
$customers = [];
if (isset($router) && !empty($router)) {
$router = ORM::for_table('tbl_routers')->find_one($router);
if (!$router) {
die(json_encode(['status' => 'error', 'message' => 'Invalid router']));
}
$query = ORM::for_table('tbl_user_recharges')
->left_outer_join('tbl_customers', 'tbl_user_recharges.customer_id = tbl_customers.id')
->where('tbl_user_recharges.routers', $router->name)
->offset($startpoint)
->limit($batch);
switch ($group) {
case 'all':
// No additional conditions needed
break;
case 'new':
$query->where_raw("DATE(recharged_on) >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)");
break;
case 'expired':
$query->where('tbl_user_recharges.status', 'off');
break;
case 'active':
$query->where('tbl_user_recharges.status', 'on');
break;
}
$query->selects([
['tbl_customers.phonenumber', 'phonenumber'],
['tbl_user_recharges.customer_id', 'customer_id'],
['tbl_customers.fullname', 'fullname'],
]);
$customers = $query->find_array();
} else {
switch ($group) {
case 'all':
$customers = ORM::for_table('tbl_customers')->offset($startpoint)->limit($batch)->find_array();
break;
case 'new':
$customers = ORM::for_table('tbl_customers')
->where_raw("DATE(created_at) >= DATE_SUB(CURDATE(), INTERVAL 1 MONTH)")
->offset($startpoint)->limit($batch)->find_array();
break;
case 'expired':
$customers = ORM::for_table('tbl_user_recharges')->where('status', 'off')
->select('customer_id')->offset($startpoint)->limit($batch)->find_array();
break;
case 'active':
$customers = ORM::for_table('tbl_user_recharges')->where('status', 'on')
->select('customer_id')->offset($startpoint)->limit($batch)->find_array();
break;
}
}
// Ensure $customers is always an array
if (!$customers) {
$customers = [];
}
// Calculate total customers for the group
$totalCustomers = 0;
if ($router) {
switch ($group) {
case 'all':
$totalCustomers = ORM::for_table('tbl_user_recharges')->where('routers', $router->routers)->count();
break;
case 'new':
$totalCustomers = ORM::for_table('tbl_user_recharges')
->where_raw("DATE(recharged_on) >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)")
->where('routers', $router->routers)
->count();
break;
case 'expired':
$totalCustomers = ORM::for_table('tbl_user_recharges')->where('status', 'off')->where('routers', $router->routers)->count();
break;
case 'active':
$totalCustomers = ORM::for_table('tbl_user_recharges')->where('status', 'on')->where('routers', $router->routers)->count();
break;
}
} else {
switch ($group) {
case 'all':
$totalCustomers = ORM::for_table('tbl_customers')->count();
break;
case 'new':
$totalCustomers = ORM::for_table('tbl_customers')
->where_raw("DATE(created_at) >= DATE_SUB(CURDATE(), INTERVAL 1 MONTH)")
->count();
break;
case 'expired':
$totalCustomers = ORM::for_table('tbl_user_recharges')->where('status', 'off')->count();
break;
case 'active':
$totalCustomers = ORM::for_table('tbl_user_recharges')->where('status', 'on')->count();
break;
}
}
// Send messages
$totalSMSSent = 0; $totalSMSSent = 0;
$totalSMSFailed = 0; $totalSMSFailed = 0;
$totalWhatsappSent = 0; $totalWhatsappSent = 0;
$totalWhatsappFailed = 0; $totalWhatsappFailed = 0;
$totalCustomers = 0; $batchStatus = [];
$batchStatus = $_SESSION['batchStatus'];
$page = _req('page', -1);
if (_req('send') == 'now') { foreach ($customers as $customer) {
// Get form data $currentMessage = str_replace(
$group = $_REQUEST['group']; ['[[name]]', '[[user_name]]', '[[phone]]', '[[company_name]]'],
$message = $_REQUEST['message']; [$customer['fullname'], $customer['username'], $customer['phonenumber'], $config['CompanyName']],
$via = $_REQUEST['via']; $message
$test = isset($_REQUEST['test']) && $_REQUEST['test'] === 'on' ? 'yes' : 'no'; );
$batch = $_REQUEST['batch'];
$delay = $_REQUEST['delay'];
$ui->assign('group', $group); $phoneNumber = preg_replace('/\D/', '', $customer['phonenumber']);
$ui->assign('message', $message);
$ui->assign('via', $via); if (empty($phoneNumber)) {
$ui->assign('test', $test); $batchStatus[] = [
$ui->assign('batch', $batch); 'name' => $customer['fullname'],
$ui->assign('delay', $delay); 'phone' => '',
if($page<0){ 'status' => 'No Phone Number'
$batchStatus = []; ];
$page = 0; continue;
} }
$startpoint = $page * $batch;
$page++; if ($test) {
// Check if fields are empty $batchStatus[] = [
if ($group == '' || $message == '' || $via == '') { 'name' => $customer['fullname'],
r2(getUrl('message/send_bulk'), 'e', Lang::T('All fields are required')); 'phone' => $customer['phonenumber'],
'status' => 'Test Mode',
'message' => $currentMessage
];
} else { } else {
// Get customer details from the database based on the selected group if ($via == 'sms' || $via == 'both') {
if ($group == 'all') { if (Message::sendSMS($customer['phonenumber'], $currentMessage)) {
$customers = ORM::for_table('tbl_customers') $totalSMSSent++;
->offset($startpoint) $batchStatus[] = ['name' => $customer['fullname'], 'phone' => $customer['phonenumber'], 'status' => 'SMS Sent', 'message' => $currentMessage];
->limit($batch)->find_array(); } else {
} elseif ($group == 'new') { $totalSMSFailed++;
// Get customers created just a month ago $batchStatus[] = ['name' => $customer['fullname'], 'phone' => $customer['phonenumber'], 'status' => 'SMS Failed', 'message' => $currentMessage];
$customers = ORM::for_table('tbl_customers')->where_raw("DATE(created_at) >= DATE_SUB(CURDATE(), INTERVAL 1 MONTH)") }
->offset($startpoint)->limit($batch)
->find_array();
} elseif ($group == 'expired') {
// Get expired user recharges where status is 'off'
$expired = ORM::for_table('tbl_user_recharges')->select('customer_id')->where('status', 'off')
->offset($startpoint)->limit($batch)
->find_array();
$customer_ids = array_column($expired, 'customer_id');
$customers = ORM::for_table('tbl_customers')->where_in('id', $customer_ids)->find_array();
} elseif ($group == 'active') {
// Get active user recharges where status is 'on'
$active = ORM::for_table('tbl_user_recharges')->select('customer_id')->where('status', 'on')
->offset($startpoint)->limit($batch)
->find_array();
$customer_ids = array_column($active, 'customer_id');
$customers = ORM::for_table('tbl_customers')->where_in('id', $customer_ids)->find_array();
} }
// Set the batch size if ($via == 'wa' || $via == 'both') {
$batchSize = $batch; if (Message::sendWhatsapp($customer['phonenumber'], $currentMessage)) {
$totalWhatsappSent++;
// Calculate the number of batches $batchStatus[] = ['name' => $customer['fullname'], 'phone' => $customer['phonenumber'], 'status' => 'WhatsApp Sent', 'message' => $currentMessage];
$totalCustomers = count($customers);
$totalBatches = ceil($totalCustomers / $batchSize);
// Loop through customers in the current batch and send messages
foreach ($customers as $customer) {
// Create a copy of the original message for each customer and save it as currentMessage
$currentMessage = $message;
$currentMessage = str_replace('[[name]]', $customer['fullname'], $currentMessage);
$currentMessage = str_replace('[[user_name]]', $customer['username'], $currentMessage);
$currentMessage = str_replace('[[phone]]', $customer['phonenumber'], $currentMessage);
$currentMessage = str_replace('[[company_name]]', $config['CompanyName'], $currentMessage);
if(empty($customer['phonenumber'])){
$batchStatus[] = [
'name' => $customer['fullname'],
'phone' => $customer['phonenumber'],
'message' => $currentMessage,
'status' => 'No Phone Number'
];
}else
// Send the message based on the selected method
if ($test === 'yes') {
// Only for testing, do not send messages to customers
$batchStatus[] = [
'name' => $customer['fullname'],
'phone' => $customer['phonenumber'],
'message' => $currentMessage,
'status' => 'Test Mode - Message not sent'
];
} else { } else {
// Send the actual messages $totalWhatsappFailed++;
if ($via == 'sms' || $via == 'both') { $batchStatus[] = ['name' => $customer['fullname'], 'phone' => $customer['phonenumber'], 'status' => 'WhatsApp Failed', 'message' => $currentMessage];
$smsSent = Message::sendSMS($customer['phonenumber'], $currentMessage);
if ($smsSent) {
$totalSMSSent++;
$batchStatus[] = [
'name' => $customer['fullname'],
'phone' => $customer['phonenumber'],
'message' => $currentMessage,
'status' => 'SMS Message Sent'
];
} else {
$totalSMSFailed++;
$batchStatus[] = [
'name' => $customer['fullname'],
'phone' => $customer['phonenumber'],
'message' => $currentMessage,
'status' => 'SMS Message Failed'
];
}
}
if ($via == 'wa' || $via == 'both') {
$waSent = Message::sendWhatsapp($customer['phonenumber'], $currentMessage);
if ($waSent) {
$totalWhatsappSent++;
$batchStatus[] = [
'name' => $customer['fullname'],
'phone' => $customer['phonenumber'],
'message' => $currentMessage,
'status' => 'WhatsApp Message Sent'
];
} else {
$totalWhatsappFailed++;
$batchStatus[] = [
'name' => $customer['fullname'],
'phone' => $customer['phonenumber'],
'message' => $currentMessage,
'status' => 'WhatsApp Message Failed'
];
}
}
} }
} }
} }
} }
$ui->assign('page', $page);
$ui->assign('totalCustomers', $totalCustomers); // Calculate if there are more customers to process
$_SESSION['batchStatus'] = $batchStatus; $hasMore = ($startpoint + $batch) < $totalCustomers;
$ui->assign('batchStatus', $batchStatus);
$ui->assign('totalSMSSent', $totalSMSSent); // Return JSON response
$ui->assign('totalSMSFailed', $totalSMSFailed); echo json_encode([
$ui->assign('totalWhatsappSent', $totalWhatsappSent); 'status' => 'success',
$ui->assign('totalWhatsappFailed', $totalWhatsappFailed); 'page' => $page + 1,
$ui->display('admin/message/bulk.tpl'); 'batchStatus' => $batchStatus,
'message' => $currentMessage,
'totalSent' => $totalSMSSent + $totalWhatsappSent,
'totalFailed' => $totalSMSFailed + $totalWhatsappFailed,
'hasMore' => $hasMore
]);
break; break;
case 'send_bulk_selected':
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Set headers
header('Content-Type: application/json');
header('Cache-Control: no-cache, no-store, must-revalidate');
// Get the posted data
$customerIds = $_POST['customer_ids'] ?? [];
$via = $_POST['message_type'] ?? '';
$message = isset($_POST['message']) ? trim($_POST['message']) : '';
if (empty($customerIds) || empty($message) || empty($via)) {
echo json_encode(['status' => 'error', 'message' => Lang::T('Invalid customer IDs, Message, or Message Type.')]);
exit;
}
// Prepare to send messages
$sentCount = 0;
$failedCount = 0;
$subject = Lang::T('Notification Message');
$form = 'Admin';
foreach ($customerIds as $customerId) {
$customer = ORM::for_table('tbl_customers')->where('id', $customerId)->find_one();
if ($customer) {
$messageSent = false;
// Check the message type and send accordingly
try {
if ($via === 'sms' || $via === 'all') {
$messageSent = Message::sendSMS($customer['phonenumber'], $message);
}
if (!$messageSent && ($via === 'wa' || $via === 'all')) {
$messageSent = Message::sendWhatsapp($customer['phonenumber'], $message);
}
if (!$messageSent && ($via === 'inbox' || $via === 'all')) {
Message::addToInbox($customer['id'], $subject, $message, $form);
$messageSent = true;
}
if (!$messageSent && ($via === 'email' || $via === 'all')) {
$messageSent = Message::sendEmail($customer['email'], $subject, $message);
}
} catch (Throwable $e) {
$messageSent = false;
$failedCount++;
sendTelegram('Failed to send message to ' . $e->getMessage());
_log('Failed to send message to ' . $customer['fullname'] . ': ' . $e->getMessage());
continue;
}
if ($messageSent) {
$sentCount++;
} else {
$failedCount++;
}
} else {
$failedCount++;
}
}
// Prepare the response
echo json_encode([
'status' => 'success',
'totalSent' => $sentCount,
'totalFailed' => $failedCount
]);
} else {
header('Content-Type: application/json');
echo json_encode(['status' => 'error', 'message' => Lang::T('Invalid request method.')]);
}
break;
default: default:
r2(getUrl('message/send_sms'), 'e', 'action not defined'); r2(getUrl('message/send_sms'), 'e', 'action not defined');
} }

View File

@ -30,7 +30,7 @@ if (php_sapi_name() !== 'cli') {
echo "PHP Time\t" . date('Y-m-d H:i:s') . "\n"; echo "PHP Time\t" . date('Y-m-d H:i:s') . "\n";
$res = ORM::raw_execute('SELECT NOW() AS WAKTU;'); $res = ORM::raw_execute('SELECT NOW() AS WAKTU;');
$statement = ORM::get_last_statement(); $statement = ORM::get_last_statement();
$rows = array(); $rows = [];
while ($row = $statement->fetch(PDO::FETCH_ASSOC)) { while ($row = $statement->fetch(PDO::FETCH_ASSOC)) {
echo "MYSQL Time\t" . $row['WAKTU'] . "\n"; echo "MYSQL Time\t" . $row['WAKTU'] . "\n";
} }
@ -45,80 +45,111 @@ echo "Found " . count($d) . " user(s)\n";
run_hook('cronjob'); #HOOK run_hook('cronjob'); #HOOK
foreach ($d as $ds) { foreach ($d as $ds) {
$date_now = strtotime(date("Y-m-d H:i:s")); try {
$expiration = strtotime($ds['expiration'] . ' ' . $ds['time']); $date_now = strtotime(date("Y-m-d H:i:s"));
echo $ds['expiration'] . " : " . (($isCli) ? $ds['username'] : Lang::maskText($ds['username'])); $expiration = strtotime($ds['expiration'] . ' ' . $ds['time']);
if ($date_now >= $expiration) { echo $ds['expiration'] . " : " . ($isCli ? $ds['username'] : Lang::maskText($ds['username']));
echo " : EXPIRED \r\n";
$u = ORM::for_table('tbl_user_recharges')->where('id', $ds['id'])->find_one();
$c = ORM::for_table('tbl_customers')->where('id', $ds['customer_id'])->find_one();
$p = ORM::for_table('tbl_plans')->where('id', $u['plan_id'])->find_one();
if (empty($c)) {
$c = $u;
}
$dvc = Package::getDevice($p);
if ($_app_stage != 'demo') {
if (file_exists($dvc)) {
require_once $dvc;
(new $p['device'])->remove_customer($c, $p);
} else {
echo "Cron error Devices $p[device] not found, cannot disconnect $c[username]";
Message::sendTelegram("Cron error Devices $p[device] not found, cannot disconnect $c[username]");
}
}
echo Message::sendPackageNotification($c, $u['namebp'], $p['price'], $textExpired, $config['user_notification_expired']) . "\n";
//update database user dengan status off
$u->status = 'off';
$u->save();
// autorenewal from deposit if ($date_now >= $expiration) {
if ($config['enable_balance'] == 'yes' && $c['auto_renewal']) { echo " : EXPIRED \r\n";
list($bills, $add_cost) = User::getBills($ds['customer_id']);
if ($add_cost != 0) { // Fetch user recharge details
if (!empty($add_cost)) { $u = ORM::for_table('tbl_user_recharges')->where('id', $ds['id'])->find_one();
if (!$u) {
throw new Exception("User recharge record not found for ID: " . $ds['id']);
}
// Fetch customer details
$c = ORM::for_table('tbl_customers')->where('id', $ds['customer_id'])->find_one();
if (!$c) {
$c = $u;
}
// Fetch plan details
$p = ORM::for_table('tbl_plans')->where('id', $u['plan_id'])->find_one();
if (!$p) {
throw new Exception("Plan not found for ID: " . $u['plan_id']);
}
$dvc = Package::getDevice($p);
if ($_app_stage != 'demo') {
if (file_exists($dvc)) {
require_once $dvc;
try {
(new $p['device'])->remove_customer($c, $p);
} catch (Throwable $e) {
_log($e->getMessage());
sendTelegram($e->getMessage());
echo "Error: " . $e->getMessage() . "\n";
}
} else {
throw new Exception("Cron error: Devices " . $p['device'] . "not found, cannot disconnect ".$c['username']."\n");
}
}
// Send notification and update user status
try {
echo Message::sendPackageNotification($c, $u['namebp'], $p['price'], $textExpired, $config['user_notification_expired']) . "\n";
$u->status = 'off';
$u->save();
} catch (Throwable $e) {
_log($e->getMessage());
sendTelegram($e->getMessage());
echo "Error: " . $e->getMessage() . "\n";
}
// Auto-renewal from deposit
if ($config['enable_balance'] == 'yes' && $c['auto_renewal']) {
[$bills, $add_cost] = User::getBills($ds['customer_id']);
if ($add_cost != 0) {
$p['price'] += $add_cost; $p['price'] += $add_cost;
} }
}
if ($p && $c['balance'] >= $p['price']) { if ($p && $c['balance'] >= $p['price']) {
if (Package::rechargeUser($ds['customer_id'], $ds['routers'], $p['id'], 'Customer', 'Balance')) { if (Package::rechargeUser($ds['customer_id'], $ds['routers'], $p['id'], 'Customer', 'Balance')) {
// if success, then get the balance Balance::min($ds['customer_id'], $p['price']);
Balance::min($ds['customer_id'], $p['price']); echo "plan enabled: " . (string) $p['enabled'] . " | User balance: " . (string) $c['balance'] . " | price " . (string) $p['price'] . "\n";
echo "plan enabled: $p[enabled] | User balance: $c[balance] | price $p[price]\n"; echo "auto renewal Success\n";
echo "auto renewall Success\n"; } else {
echo "plan enabled: " . $p['enabled'] . " | User balance: " . $c['balance'] . " | price " . $p['price'] . "\n";
echo "auto renewal Failed\n";
Message::sendTelegram("FAILED RENEWAL #cron\n\n#u." . $c['username'] . " #buy #Hotspot \n" . $p['name_plan'] .
"\nRouter: " . $p['routers'] .
"\nPrice: " . $p['price']);
}
} else { } else {
echo "plan enabled: $p[enabled] | User balance: $c[balance] | price $p[price]\n"; echo "no renewal | plan enabled: " . (string) $p['enabled'] . " | User balance: " . (string) $c['balance'] . " | price " . (string) $p['price'] . "\n";
echo "auto renewall Failed\n";
Message::sendTelegram("FAILED RENEWAL #cron\n\n#u$c[username] #buy #Hotspot \n" . $p['name_plan'] .
"\nRouter: " . $p['routers'] .
"\nPrice: " . $p['price']);
} }
} else { } else {
echo "no renewall | plan enabled: $p[enabled] | User balance: $c[balance] | price $p[price]\n"; echo "no renewal | balance" . $config['enable_balance'] . " auto_renewal " . $c['auto_renewal'] . "\n";
} }
} else { } else {
echo "no renewall | balance $config[enable_balance] auto_renewal $c[auto_renewal]\n"; echo " : ACTIVE \r\n";
} }
} else { } catch (Throwable $e) {
echo " : ACTIVE \r\n"; // Catch any unexpected errors
_log($e->getMessage());
sendTelegram($e->getMessage());
echo "Unexpected Error: " . $e->getMessage() . "\n";
} }
} }
//Cek interim-update radiusrest //Cek interim-update radiusrest
if ($config['frrest_interim_update'] != 0) { if ($config['frrest_interim_update'] != 0) {
$r_a = ORM::for_table('rad_acct') $r_a = ORM::for_table('rad_acct')
->whereRaw("BINARY acctstatustype = 'Start' OR acctstatustype = 'Interim-Update'") ->whereRaw("BINARY acctstatustype = 'Start' OR acctstatustype = 'Interim-Update'")
->where_lte('dateAdded', date("Y-m-d H:i:s"))->find_many(); ->where_lte('dateAdded', date("Y-m-d H:i:s"))->find_many();
foreach ($r_a as $ra) { foreach ($r_a as $ra) {
$interval = $_c['frrest_interim_update']*60; $interval = $_c['frrest_interim_update'] * 60;
$timeUpdate = strtotime($ra['dateAdded'])+$interval; $timeUpdate = strtotime($ra['dateAdded']) + $interval;
$timeNow = strtotime(date("Y-m-d H:i:s")); $timeNow = strtotime(date("Y-m-d H:i:s"));
if ($timeNow >= $timeUpdate) { if ($timeNow >= $timeUpdate) {
$ra->acctstatustype = 'Stop'; $ra->acctstatustype = 'Stop';
$ra->save(); $ra->save();
} }
} }
} }
if ($config['router_check']) { if ($config['router_check']) {
@ -137,7 +168,7 @@ if ($config['router_check']) {
foreach ($routers as $router) { foreach ($routers as $router) {
// check if custom port // check if custom port
if (strpos($router->ip_address, ':') === false){ if (strpos($router->ip_address, ':') === false) {
$ip = $router->ip_address; $ip = $router->ip_address;
$port = 8728; $port = 8728;
} else { } else {
@ -207,14 +238,7 @@ if ($config['router_check']) {
Message::SendEmail($adminEmail, $subject, $message); Message::SendEmail($adminEmail, $subject, $message);
sendTelegram($message); sendTelegram($message);
} }
echo "Router monitoring finished\n"; echo "Router monitoring finished checking.\n";
}
if (defined('PHP_SAPI') && PHP_SAPI === 'cli') {
echo "Cronjob finished\n";
} else {
echo "</pre>";
} }
flock($lock, LOCK_UN); flock($lock, LOCK_UN);
@ -224,5 +248,5 @@ unlink($lockFile);
$timestampFile = "$UPLOAD_PATH/cron_last_run.txt"; $timestampFile = "$UPLOAD_PATH/cron_last_run.txt";
file_put_contents($timestampFile, time()); file_put_contents($timestampFile, time());
run_hook('cronjob_end'); #HOOK run_hook('cronjob_end'); #HOOK
echo "Cron job finished and completed successfully.\n";

View File

@ -16,12 +16,12 @@
<div class="panel panel-hovered mb20 panel-primary"> <div class="panel panel-hovered mb20 panel-primary">
<div class="panel-heading"> <div class="panel-heading">
{if in_array($_admin['user_type'],['SuperAdmin','Admin'])} {if in_array($_admin['user_type'],['SuperAdmin','Admin'])}
<div class="btn-group pull-right"> <div class="btn-group pull-right">
<a class="btn btn-primary btn-xs" title="save" <a class="btn btn-primary btn-xs" title="save"
href="{Text::url('customers/csv&token=', $csrf_token)}" href="{Text::url('customers/csv&token=', $csrf_token)}"
onclick="return ask(this, 'This will export to CSV?')"><span onclick="return ask(this, 'This will export to CSV?')"><span
class="glyphicon glyphicon-download" aria-hidden="true"></span> CSV</a> class="glyphicon glyphicon-download" aria-hidden="true"></span> CSV</a>
</div> </div>
{/if} {/if}
{Lang::T('Manage Contact')} {Lang::T('Manage Contact')}
</div> </div>
@ -65,8 +65,8 @@
<span class="input-group-addon">Status</span> <span class="input-group-addon">Status</span>
<select class="form-control" id="filter" name="filter"> <select class="form-control" id="filter" name="filter">
{foreach $statuses as $status} {foreach $statuses as $status}
<option value="{$status}" {if $filter eq $status }selected{/if}>{Lang::T($status)} <option value="{$status}" {if $filter eq $status }selected{/if}>{Lang::T($status)}
</option> </option>
{/foreach} {/foreach}
</select> </select>
</div> </div>
@ -97,6 +97,7 @@
<table id="customerTable" class="table table-bordered table-striped table-condensed"> <table id="customerTable" class="table table-bordered table-striped table-condensed">
<thead> <thead>
<tr> <tr>
<th><input type="checkbox" id="select-all"></th>
<th>{Lang::T('Username')}</th> <th>{Lang::T('Username')}</th>
<th>Photo</th> <th>Photo</th>
<th>{Lang::T('Account Type')}</th> <th>{Lang::T('Account Type')}</th>
@ -113,65 +114,222 @@
</thead> </thead>
<tbody> <tbody>
{foreach $d as $ds} {foreach $d as $ds}
<tr {if $ds['status'] != 'Active'}class="danger" {/if}> <tr {if $ds['status'] !='Active' }class="danger" {/if}>
<td onclick="window.location.href = '{Text::url('customers/view/', $ds['id'])}'" <td><input type="checkbox" name="customer_ids[]" value="{$ds['id']}"></td>
style="cursor:pointer;">{$ds['username']}</td> <td onclick="window.location.href = '{Text::url('customers/view/', $ds['id'])}'"
<td> style="cursor:pointer;">{$ds['username']}</td>
<a href="{$app_url}/{$UPLOAD_PATH}{$ds['photo']}" target="photo"> <td>
<img src="{$app_url}/{$UPLOAD_PATH}{$ds['photo']}.thumb.jpg" width="32" alt=""> <a href="{$app_url}/{$UPLOAD_PATH}{$ds['photo']}" target="photo">
</a> <img src="{$app_url}/{$UPLOAD_PATH}{$ds['photo']}.thumb.jpg" width="32" alt="">
</td> </a>
<td>{$ds['account_type']}</td> </td>
<td onclick="window.location.href = '{Text::url('customers/view/', $ds['id'])}'" <td>{$ds['account_type']}</td>
style="cursor: pointer;">{$ds['fullname']}</td> <td onclick="window.location.href = '{Text::url('customers/view/', $ds['id'])}'"
<td>{Lang::moneyFormat($ds['balance'])}</td> style="cursor: pointer;">{$ds['fullname']}</td>
<td align="center"> <td>{Lang::moneyFormat($ds['balance'])}</td>
{if $ds['phonenumber']} <td align="center">
<a href="tel:{$ds['phonenumber']}" class="btn btn-default btn-xs" {if $ds['phonenumber']}
title="{$ds['phonenumber']}"><i class="glyphicon glyphicon-earphone"></i></a> <a href="tel:{$ds['phonenumber']}" class="btn btn-default btn-xs"
{/if} title="{$ds['phonenumber']}"><i class="glyphicon glyphicon-earphone"></i></a>
{if $ds['email']} {/if}
<a href="mailto:{$ds['email']}" class="btn btn-default btn-xs" {if $ds['email']}
title="{$ds['email']}"><i class="glyphicon glyphicon-envelope"></i></a> <a href="mailto:{$ds['email']}" class="btn btn-default btn-xs"
{/if} title="{$ds['email']}"><i class="glyphicon glyphicon-envelope"></i></a>
{if $ds['coordinates']} {/if}
<a href="https://www.google.com/maps/dir//{$ds['coordinates']}/" target="_blank" {if $ds['coordinates']}
class="btn btn-default btn-xs" title="{$ds['coordinates']}"><i <a href="https://www.google.com/maps/dir//{$ds['coordinates']}/" target="_blank"
class="glyphicon glyphicon-map-marker"></i></a> class="btn btn-default btn-xs" title="{$ds['coordinates']}"><i
{/if} class="glyphicon glyphicon-map-marker"></i></a>
</td> {/if}
<td align="center" api-get-text="{Text::url('autoload/plan_is_active/')}{$ds['id']}"> </td>
<span class="label label-default">&bull;</span> <td align="center" api-get-text="{Text::url('autoload/plan_is_active/')}{$ds['id']}">
</td> <span class="label label-default">&bull;</span>
<td>{$ds['service_type']}</td> </td>
<td> <td>{$ds['service_type']}</td>
{$ds['pppoe_username']} <td>
{if !empty($ds['pppoe_username']) && !empty($ds['pppoe_ip'])}:{/if} {$ds['pppoe_username']}
{$ds['pppoe_ip']} {if !empty($ds['pppoe_username']) && !empty($ds['pppoe_ip'])}:{/if}
</td> {$ds['pppoe_ip']}
<td>{Lang::T($ds['status'])}</td> </td>
<td>{Lang::dateTimeFormat($ds['created_at'])}</td> <td>{Lang::T($ds['status'])}</td>
<td align="center"> <td>{Lang::dateTimeFormat($ds['created_at'])}</td>
<a href="{Text::url('customers/view/')}{$ds['id']}" id="{$ds['id']}" <td align="center">
style="margin: 0px; color:black" <a href="{Text::url('customers/view/')}{$ds['id']}" id="{$ds['id']}"
class="btn btn-success btn-xs">&nbsp;&nbsp;{Lang::T('View')}&nbsp;&nbsp;</a> style="margin: 0px; color:black"
<a href="{Text::url('customers/edit/', $ds['id'], '&token=', $csrf_token)}" class="btn btn-success btn-xs">&nbsp;&nbsp;{Lang::T('View')}&nbsp;&nbsp;</a>
id="{$ds['id']}" style="margin: 0px; color:black" <a href="{Text::url('customers/edit/', $ds['id'], '&token=', $csrf_token)}"
class="btn btn-info btn-xs">&nbsp;&nbsp;{Lang::T('Edit')}&nbsp;&nbsp;</a> id="{$ds['id']}" style="margin: 0px; color:black"
<a href="{Text::url('customers/sync/', $ds['id'], '&token=', $csrf_token)}" class="btn btn-info btn-xs">&nbsp;&nbsp;{Lang::T('Edit')}&nbsp;&nbsp;</a>
id="{$ds['id']}" style="margin: 5px; color:black" <a href="{Text::url('customers/sync/', $ds['id'], '&token=', $csrf_token)}"
class="btn btn-success btn-xs">&nbsp;&nbsp;{Lang::T('Sync')}&nbsp;&nbsp;</a> id="{$ds['id']}" style="margin: 5px; color:black"
<a href="{Text::url('plan/recharge/', $ds['id'], '&token=', $csrf_token)}" id="{$ds['id']}" class="btn btn-success btn-xs">&nbsp;&nbsp;{Lang::T('Sync')}&nbsp;&nbsp;</a>
style="margin: 0px;" class="btn btn-primary btn-xs">{Lang::T('Recharge')}</a> <a href="{Text::url('plan/recharge/', $ds['id'], '&token=', $csrf_token)}"
</td> id="{$ds['id']}" style="margin: 0px;"
</tr> class="btn btn-primary btn-xs">{Lang::T('Recharge')}</a>
</td>
</tr>
{/foreach} {/foreach}
</tbody> </tbody>
</table> </table>
<div class="row" style="padding: 5px">
<div class="col-lg-3 col-lg-offset-9">
<div class="btn-group btn-group-justified" role="group">
<!-- <div class="btn-group" role="group">
{if in_array($_admin['user_type'],['SuperAdmin','Admin'])}
<button id="deleteSelectedTokens" class="btn btn-danger">{Lang::T('Delete
Selected')}</button>
{/if}
</div> -->
<div class="btn-group" role="group">
<button id="sendMessageToSelected" class="btn btn-success">{Lang::T('Send
Message')}</button>
</div>
</div>
</div>
</div>
</div> </div>
{include file="pagination.tpl"} {include file="pagination.tpl"}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{include file="sections/footer.tpl"} <!-- Modal for Sending Messages -->
<div id="sendMessageModal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="sendMessageModalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="sendMessageModalLabel">{Lang::T('Send Message')}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<select id="messageType" class="form-control">
<option value="all">{Lang::T('All')}</option>
<option value="email">{Lang::T('Email')}</option>
<option value="inbox">{Lang::T('Inbox')}</option>
<option value="sms">{Lang::T('SMS')}</option>
<option value="wa">{Lang::T('WhatsApp')}</option>
</select>
<br>
<textarea id="messageContent" class="form-control" rows="4"
placeholder="{Lang::T('Enter your message here...')}"></textarea>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{Lang::T('Close')}</button>
<button type="button" id="sendMessageButton" class="btn btn-primary">{Lang::T('Send Message')}</button>
</div>
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
// Select or deselect all checkboxes
document.getElementById('select-all').addEventListener('change', function () {
var checkboxes = document.querySelectorAll('input[name="customer_ids[]"]');
for (var checkbox of checkboxes) {
checkbox.checked = this.checked;
}
});
$(document).ready(function () {
let selectedCustomerIds = [];
// Collect selected customer IDs when the button is clicked
$('#sendMessageToSelected').on('click', function () {
selectedCustomerIds = $('input[name="customer_ids[]"]:checked').map(function () {
return $(this).val();
}).get();
if (selectedCustomerIds.length === 0) {
Swal.fire({
title: 'Error!',
text: 'Please select at least one customer to send a message.',
icon: 'error',
confirmButtonText: 'OK'
});
return;
}
// Open the modal
$('#sendMessageModal').modal('show');
});
// Handle sending the message
$('#sendMessageButton').on('click', function () {
const message = $('#messageContent').val().trim();
const messageType = $('#messageType').val();
if (!message) {
Swal.fire({
title: 'Error!',
text: 'Please enter a message to send.',
icon: 'error',
confirmButtonText: 'OK'
});
return;
}
// Disable the button and show loading text
$(this).prop('disabled', true).text('Sending...');
$.ajax({
url: '?_route=message/send_bulk_selected',
method: 'POST',
data: {
customer_ids: selectedCustomerIds,
message_type: messageType,
message: message
},
dataType: 'json',
success: function (response) {
// Handle success response
if (response.status === 'success') {
Swal.fire({
title: 'Success!',
text: 'Message sent successfully.',
icon: 'success',
confirmButtonText: 'OK'
});
} else {
Swal.fire({
title: 'Error!',
text: 'Error sending message: ' + response.message,
icon: 'error',
confirmButtonText: 'OK'
});
}
$('#sendMessageModal').modal('hide');
$('#messageContent').val(''); // Clear the message content
},
error: function () {
Swal.fire({
title: 'Error!',
text: 'Failed to send the message. Please try again.',
icon: 'error',
confirmButtonText: 'OK'
});
},
complete: function () {
// Re-enable the button and reset text
$('#sendMessageButton').prop('disabled', false).text('{Lang::T('Send Message')}');
}
});
});
});
$(document).ready(function () {
$('#sendMessageModal').on('show.bs.modal', function () {
$(this).attr('inert', 'true');
});
$('#sendMessageModal').on('shown.bs.modal', function () {
$('#messageContent').focus();
$(this).removeAttr('inert');
});
$('#sendMessageModal').on('hidden.bs.modal', function () {
// $('#button').focus();
});
});
</script>
{include file = "sections/footer.tpl" }

View File

@ -1,180 +1,219 @@
{include file="sections/header.tpl"} {include file="sections/header.tpl"}
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.11.3/css/jquery.dataTables.min.css"> <link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.11.3/css/jquery.dataTables.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<div class="row"> <div class="row">
<div class="col-sm-12 col-md-12"> <div class="col-sm-12 col-md-12">
{if $page>0 && $totalCustomers>0} <div id="status" class="mb-3"></div>
<div class="alert alert-info" role="alert"><span class="loading"></span> {Lang::T("Sending message in progress. Don't close this page.")}</div> <div class="panel panel-primary panel-hovered panel-stacked mb30 {if $page>0 && $totalCustomers >0}hidden{/if}">
{/if} <div class="panel-heading">{Lang::T('Send Bulk Message')}</div>
<div class="panel panel-primary panel-hovered panel-stacked mb30 {if $page>0 && $totalCustomers >0}hidden{/if}"> <div class="panel-body">
<div class="panel-heading">{Lang::T('Send Bulk Message')}</div> <form class="form-horizontal" method="get" role="form" id="bulkMessageForm" action="">
<div class="panel-body"> <input type="hidden" name="page" value="{if $page>0 && $totalCustomers==0}-1{else}{$page}{/if}">
<form class="form-horizontal" method="get" role="form" id="bulkMessageForm" action=""> <div class="form-group">
<input type="hidden" name="page" value="{if $page>0 && $totalCustomers==0}-1{else}{$page}{/if}"> <label class="col-md-2 control-label">{Lang::T('Router')}</label>
<div class="form-group"> <div class="col-md-6">
<label class="col-md-2 control-label">{Lang::T('Group')}</label> <select class="form-control select2" name="router" id="router">
<div class="col-md-6"> <option value="">{Lang::T('All Routers')}</option>
<select class="form-control" name="group" id="group"> {foreach $routers as $router}
<option value="all" {if $group == 'all'}selected{/if}>{Lang::T('All Customers')} <option value="{$router['id']}">{$router['name']}</option>
</option> {/foreach}
<option value="new" {if $group == 'new'}selected{/if}>{Lang::T('New Customers')} </select>
</option> </div>
<option value="expired" {if $group == 'expired'}selected{/if}> </div>
{Lang::T('Expired Customers')}</option> <div class="form-group">
<option value="active" {if $group == 'active'}selected{/if}> <label class="col-md-2 control-label">{Lang::T('Group')}</label>
{Lang::T('Active Customers')}</option> <div class="col-md-6">
</select> <select class="form-control" name="group" id="group">
</div> <option value="all" {if $group=='all' }selected{/if}>{Lang::T('All Customers')}</option>
</div> <option value="new" {if $group=='new' }selected{/if}>{Lang::T('New Customers')}</option>
<div class="form-group"> <option value="expired" {if $group=='expired' }selected{/if}>{Lang::T('Expired Customers')}</option>
<label class="col-md-2 control-label">{Lang::T('Send Via')}</label> <option value="active" {if $group=='active' }selected{/if}>{Lang::T('Active Customers')}</option>
<div class="col-md-6"> </select>
<select class="form-control" name="via" id="via"> </div>
<option value="sms" {if $via == 'sms'}selected{/if}>{Lang::T('SMS')}</option> </div>
<option value="wa" {if $via == 'wa'}selected{/if}>{Lang::T('WhatsApp')}</option> <div class="form-group">
<option value="both" {if $via == 'both'}selected{/if}>{Lang::T('SMS and WhatsApp')} <label class="col-md-2 control-label">{Lang::T('Send Via')}</label>
</option> <div class="col-md-6">
</select> <select class="form-control" name="via" id="via">
</div> <option value="sms" {if $via=='sms' }selected{/if}>{Lang::T('SMS')}</option>
</div> <option value="wa" {if $via=='wa' }selected{/if}>{Lang::T('WhatsApp')}</option>
<div class="form-group"> <option value="both" {if $via=='both' }selected{/if}>{Lang::T('SMS and WhatsApp')}</option>
<label class="col-md-2 control-label">{Lang::T('Message per time')}</label> </select>
<div class="col-md-6"> </div>
<select class="form-control" name="batch" id="batch"> </div>
<option value="5" {if $batch == '5'}selected{/if}>{Lang::T('5 Messages')}</option> <div class="form-group">
<option value="10" {if $batch == '10'}selected{/if}>{Lang::T('10 Messages')}</option> <label class="col-md-2 control-label">{Lang::T('Message per time')}</label>
<option value="15" {if $batch == '15'}selected{/if}>{Lang::T('15 Messages')}</option> <div class="col-md-6">
<option value="20" {if $batch == '20'}selected{/if}>{Lang::T('20 Messages')}</option> <select class="form-control" name="batch" id="batch">
<option value="30" {if $batch == '30'}selected{/if}>{Lang::T('30 Messages')}</option> <option value="5" {if $batch=='5' }selected{/if}>{Lang::T('5 Messages')}</option>
<option value="40" {if $batch == '40'}selected{/if}>{Lang::T('40 Messages')}</option> <option value="10" {if $batch=='10' }selected{/if}>{Lang::T('10 Messages')}</option>
<option value="50" {if $batch == '50'}selected{/if}>{Lang::T('50 Messages')}</option> <option value="15" {if $batch=='15' }selected{/if}>{Lang::T('15 Messages')}</option>
<option value="60" {if $batch == '60'}selected{/if}>{Lang::T('60 Messages')}</option> <option value="20" {if $batch=='20' }selected{/if}>{Lang::T('20 Messages')}</option>
</select>{Lang::T('Use 20 and above if you are sending to all customers to avoid server time out')} <option value="30" {if $batch=='30' }selected{/if}>{Lang::T('30 Messages')}</option>
</div> <option value="40" {if $batch=='40' }selected{/if}>{Lang::T('40 Messages')}</option>
</div> <option value="50" {if $batch=='50' }selected{/if}>{Lang::T('50 Messages')}</option>
<div class="form-group"> <option value="60" {if $batch=='60' }selected{/if}>{Lang::T('60 Messages')}</option>
<label class="col-md-2 control-label">{Lang::T('Delay')}</label> </select>
<div class="col-md-6"> {Lang::T('Use 20 and above if you are sending to all customers to avoid server time out')}
<select class="form-control" name="delay" id="delay"> </div>
<option value="1" {if $delay == '1'}selected{/if}>{Lang::T('No Delay')}</option> </div>
<option value="5" {if $delay == '5'}selected{/if}>{Lang::T('5 Seconds')}</option> <div class="form-group">
<option value="10" {if $delay == '10'}selected{/if}>{Lang::T('10 Seconds')}</option> <label class="col-md-2 control-label">{Lang::T('Message')}</label>
<option value="15" {if $delay == '15'}selected{/if}>{Lang::T('15 Seconds')}</option> <div class="col-md-6">
<option value="20" {if $delay == '20'}selected{/if}>{Lang::T('20 Seconds')}</option> <textarea class="form-control" id="message" name="message" required placeholder="{Lang::T('Compose your message...')}" rows="5">{$message}</textarea>
</select>{Lang::T('Use at least 5 secs if you are sending to all customers to avoid being banned by your message provider')} <input name="test" id="test" type="checkbox">
</div> {Lang::T('Testing [if checked no real message is sent]')}
</div> </div>
<div class="form-group"> <p class="help-block col-md-4">
<label class="col-md-2 control-label">{Lang::T('Message')}</label> {Lang::T('Use placeholders:')}
<div class="col-md-6"> <br>
<textarea class="form-control" id="message" name="message" required <b>[[name]]</b> - {Lang::T('Customer Name')}
placeholder="{Lang::T('Compose your message...')}" rows="5">{$message}</textarea> <br>
<input name="test" type="checkbox"> <b>[[user_name]]</b> - {Lang::T('Customer Username')}
{Lang::T('Testing [if checked no real message is sent]')} <br>
</div> <b>[[phone]]</b> - {Lang::T('Customer Phone')}
<p class="help-block col-md-4"> <br>
{Lang::T('Use placeholders:')} <b>[[company_name]]</b> - {Lang::T('Your Company Name')}
<br> </p>
<b>[[name]]</b> - {Lang::T('Customer Name')} </div>
<br> <div class="form-group">
<b>[[user_name]]</b> - {Lang::T('Customer Username')} <div class="col-lg-offset-2 col-lg-10">
<br> <button type="button" id="startBulk" class="btn btn-primary">Start Bulk Messaging</button>
<b>[[phone]]</b> - {Lang::T('Customer Phone')} <a href="{Text::url('dashboard')}" class="btn btn-default">{Lang::T('Cancel')}</a>
<br> </div>
<b>[[company_name]]</b> - {Lang::T('Your Company Name')} </div>
</p> </form>
</div> </div>
<div class="form-group"> </div>
<div class="col-lg-offset-2 col-lg-10"> </div>
{if $page >= 0}
<button class="btn btn-success" id="submit" type="submit" name=send value=now>
{Lang::T('Send Message')}</button>
{else}
<button class="btn btn-success"
onclick="return ask(this, 'Continue the process of sending mass messages?')"
type="submit" name=send value=now>
{Lang::T('Send Message')}</button>
{/if}
<a href="{Text::url('dashboard')}" class="btn btn-default">{Lang::T('Cancel')}</a>
</div>
</div>
</form>
</div>
</div>
</div>
</div> </div>
{if $batchStatus} <!-- Add a Table for Sent History -->
<p><span class="label label-success">{Lang::T('Total SMS Sent')}: {$totalSMSSent}</span> <span <div class="panel panel-default">
class="label label-danger">{Lang::T('Total SMS <div class="panel-heading">{Lang::T('Message Sending History')}</div>
Failed')}: {$totalSMSFailed}</span> <span class="label label-success">{Lang::T('Total WhatsApp Sent')}: <div class="panel-body">
{$totalWhatsappSent}</span> <span class="label label-danger">{Lang::T('Total WhatsApp Failed')}: <div id="status"></div>
{$totalWhatsappFailed}</span></p> <table class="table table-bordered" id="historyTable">
{/if} <thead>
<div class="box"> <tr>
<div class="box-header"> <th>{Lang::T('Customer')}</th>
<h3 class="box-title">{Lang::T('Message Results')}</h3> <th>{Lang::T('Phone')}</th>
</div> <th>{Lang::T('Status')}</th>
<!-- /.box-header --> <th>{Lang::T('Message')}</th>
<div class="box-body"> </tr>
<table id="messageResultsTable" class="table table-bordered table-striped table-condensed"> </thead>
<thead> <tbody></tbody>
<tr> </table>
<th>{Lang::T('Name')}</th> </div>
<th>{Lang::T('Phone')}</th>
<th>{Lang::T('Message')}</th>
<th>{Lang::T('Status')}</th>
</tr>
</thead>
<tbody>
{foreach $batchStatus as $customer}
<tr>
<td>{$customer.name}</td>
<td>{$customer.phone}</td>
<td>{$customer.message}</td>
<td>{$customer.status}</td>
</tr>
{/foreach}
</tbody>
</table>
</div>
<!-- /.box-body -->
</div> </div>
<!-- /.box -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.datatables.net/1.11.3/js/jquery.dataTables.min.js"></script> <script src="https://cdn.datatables.net/1.11.3/js/jquery.dataTables.min.js"></script>
{literal}
<script> <script>
var $j = jQuery.noConflict(); let page = 0;
let totalSent = 0;
let totalFailed = 0;
let hasMore = true;
$j(document).ready(function() { // Initialize DataTable
$j('#messageResultsTable').DataTable(); let historyTable = $('#historyTable').DataTable({
}); paging: true,
searching: true,
ordering: true,
info: true,
autoWidth: false,
responsive: true
});
{if $page>0 && $totalCustomers >0} function sendBatch() {
setTimeout(() => { if (!hasMore) return;
document.getElementById('submit').click();
}, {$delay}000); $.ajax({
{/if} url: '?_route=message/send_bulk_ajax',
{if $page>0 && $totalCustomers==0} method: 'POST',
Swal.fire({ data: {
icon: 'success', group: $('#group').val(),
title: 'Bulk Send Done', message: $('#message').val(),
position: 'top-end', via: $('#via').val(),
showConfirmButton: false, batch: $('#batch').val(),
timer: 5000, router: $('#router').val() || '',
timerProgressBar: true, page: page,
didOpen: (toast) => { test: $('#test').is(':checked') ? 'on' : 'off'
toast.addEventListener('mouseenter', Swal.stopTimer) },
toast.addEventListener('mouseleave', Swal.resumeTimer) dataType: 'json',
} beforeSend: function () {
}); $('#status').html(`
{/if} <div class="alert alert-info">
<i class="fas fa-spinner fa-spin"></i> Sending batch ${page + 1}...
</div>
`);
},
success: function (response) {
console.log("Response received:", response);
if (response && response.status === 'success') {
totalSent += response.totalSent || 0;
totalFailed += response.totalFailed || 0;
page = response.page || 0;
hasMore = response.hasMore || false;
$('#status').html(`
<div class="alert alert-success">
<i class="fas fa-check-circle"></i> Batch ${page} sent! (Total Sent: ${totalSent}, Failed: ${totalFailed})
</div>
`);
(response.batchStatus || []).forEach(msg => {
let statusClass = msg.status.includes('Failed') ? 'danger' : 'success';
historyTable.row.add([
msg.name,
msg.phone,
`<span class="text-${statusClass}">${msg.status}</span>`,
msg.message || 'No message'
]).draw(false); // Add row without redrawing the table
});
if (hasMore) {
sendBatch();
} else {
$('#status').html(`
<div class="alert alert-success">
<i class="fas fa-check-circle"></i> All batches sent! Total Sent: ${totalSent}, Failed: ${totalFailed}
</div>
`);
}
} else {
console.error("Unexpected response format:", response);
$('#status').html(`
<div class="alert alert-danger">
<i class="fas fa-exclamation-circle"></i> Error: Unexpected response format.
</div>
`);
}
},
error: function () {
$('#status').html(`
<div class="alert alert-danger">
<i class="fas fa-exclamation-circle"></i> Error: Failed to send batch ${page + 1}.
</div>
`);
}
});
}
// Start sending on button click
$('#startBulk').on('click', function () {
page = 0;
totalSent = 0;
totalFailed = 0;
hasMore = true;
$('#status').html('<div class="alert alert-info"><i class="fas fa-spinner fa-spin"></i> Starting bulk message sending...</div>');
historyTable.clear().draw(); // Clear history table before starting
sendBatch();
});
</script> </script>
{/literal}
{include file="sections/footer.tpl"} {include file="sections/footer.tpl"}