Merge pull request 'Development' (#2) from Development into master

Reviewed-on: #2
This commit is contained in:
kevinowino869 2025-04-14 11:00:04 +02:00
commit 915af6fc03
24 changed files with 1410 additions and 765 deletions

2
.gitignore vendored
View File

@ -58,3 +58,5 @@ docs/**
!docs/insomnia.rest.json
!system/uploads/paid.png
system/uploads/invoices/**
!system/uploads/invoices/
!system/uploads/invoices/index.html

View File

@ -8,12 +8,12 @@ class Invoice
{
try {
if (empty($invoiceData['invoice'])) {
throw new Exception("Invoice ID is required");
throw new Exception(Lang::T("Invoice No is required"));
}
$template = Lang::getNotifText('email_invoice');
if (!$template) {
throw new Exception("Invoice template not found");
throw new Exception(Lang::T("Invoice template not found"));
}
if (strpos($template, '<body') === false) {
@ -47,10 +47,14 @@ class Invoice
// Save PDF
$filename = "invoice_{$invoiceData['invoice']}.pdf";
$outputPath = "system/uploads/invoices/{$filename}";
$dir = dirname($outputPath);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
$mpdf->Output($outputPath, 'F');
if (!file_exists($outputPath)) {
throw new Exception("Failed to save PDF file");
throw new Exception(Lang::T("Failed to save PDF file"));
}
return $filename;
@ -67,7 +71,7 @@ class Invoice
return preg_replace_callback('/\[\[(\w+)\]\]/', function ($matches) use ($invoiceData) {
$key = $matches[1];
if (!isset($invoiceData[$key])) {
_log("Missing invoice key: $key");
_log(Lang::T("Missing invoice key: ") . $key);
return '';
}
@ -80,92 +84,84 @@ class Invoice
}
if ($key === 'bill_rows') {
return html_entity_decode($invoiceData[$key]);
return $invoiceData[$key];
}
return htmlspecialchars($invoiceData[$key] ?? '');
}, $template);
}
public static function sendInvoice($userId, $status = "Unpaid")
/**
* Send invoice to user
*
* @param int $userId
* @param array $invoice
* @param array $bills
* @param string $status
* @param string $invoiceNo
* @return bool
*/
public static function sendInvoice($userId, $invoice = null, $bills = [], $status = "Unpaid", $invoiceNo = null)
{
global $config, $root_path, $UPLOAD_PATH;
if (empty($config['currency_code'])) {
$config['currency_code'] = '$';
}
// Set default currency code
$config['currency_code'] ??= '$';
$account = ORM::for_table('tbl_customers')->find_one($userId);
if (!$account) {
_log("Failed to send invoice: User not found");
sendTelegram("Failed to send invoice: User not found");
return false;
self::validateAccount($account);
if (!$invoiceNo) {
$invoiceNo = "INV-" . Package::_raid();
}
// Fetch invoice if not provided
if ($status === "Unpaid" && !$invoice) {
$data = ORM::for_table('tbl_user_recharges')->where('customer_id', $userId)
->where('status', 'off')
->left_outer_join('tbl_plans', 'tbl_user_recharges.namebp = tbl_plans.name_plan')
->select('tbl_plans.price', 'price')
->select('tbl_plans.name_plan', 'namebp')
->find_one();
if (!$data) {
$data = ORM::for_table('tbl_user_recharges')->where('username', $account->username)
->left_outer_join('tbl_plans', 'tbl_user_recharges.namebp = tbl_plans.name_plan')
->select('tbl_plans.price', 'price')
->select('tbl_plans.name_plan', 'namebp')
->where('status', 'off')
->find_one();
}
if (!$data) {
throw new Exception(Lang::T("No unpaid invoice found for username:") . $account->username);
}
$invoice = [
'price' => $data->price,
'plan_name' => $data->namebp,
'routers' => $data->routers,
];
} else if ($status === "Paid" && !$invoice) {
$invoice = ORM::for_table("tbl_transactions")->where("username", $account->username)->find_one();
}
if (!$invoice) {
_log("Failed to send invoice: Transaction not found");
sendTelegram("Failed to send invoice: Transaction not found");
return false;
throw new Exception(Lang::T("Transaction not found for username: ") . $account->username);
}
[$additionalBills, $add_cost] = User::getBills($account->id);
$invoiceItems = [
[
'description' => $invoice->plan_name,
'details' => 'Monthly Subscription',
'amount' => (float) $invoice->price
]
];
$subtotal = (float) $invoice->price;
if ($add_cost > 0 && $invoice->routers != 'balance') {
foreach ($additionalBills as $description => $amount) {
if (is_numeric($amount)) {
$invoiceItems[] = [
'description' => $description,
'details' => 'Additional Bill',
'amount' => (float) $amount
];
$subtotal += (float) $amount;
} else {
_log("Invalid bill amount for {$description}: {$amount}");
}
}
// Get additional bills if not provided
if (empty($bills)) {
[$bills, $add_cost] = User::getBills($account->id);
}
$tax_rate = (float) ($config['tax_rate'] ?? 0);
$invoiceItems = self::generateInvoiceItems($invoice, $bills, $add_cost);
$subtotal = array_sum(array_column($invoiceItems, 'amount'));
$tax = $config['enable_tax'] ? Package::tax($subtotal) : 0;
$total = ($tax > 0) ? $subtotal + $tax : $subtotal + $tax;
$tax_rate = $config['tax_rate'] ?? 0;
$total = $subtotal + $tax;
$token = User::generateToken($account->id, 1);
if (!empty($token['token'])) {
$tur = ORM::for_table('tbl_user_recharges')
->where('customer_id', $account->id)
->where('namebp', $invoice->plan_name);
$payLink = self::generatePaymentLink($account, $invoice, $status);
$logo = self::getCompanyLogo($UPLOAD_PATH, $root_path);
switch ($status) {
case 'Paid':
$tur->where('status', 'on');
break;
default:
$tur->where('status', 'off');
break;
}
$turResult = $tur->find_one();
$payLink = $turResult ? '?_route=home&recharge=' . $turResult['id'] . '&uid=' . urlencode($token['token']) : '?_route=home';
} else {
$payLink = '?_route=home';
}
$UPLOAD_URL_PATH = str_replace($root_path, '', $UPLOAD_PATH);
$logo = (file_exists($UPLOAD_PATH . DIRECTORY_SEPARATOR . 'logo.png')) ? $UPLOAD_URL_PATH . DIRECTORY_SEPARATOR . 'logo.png?' . time() : $UPLOAD_URL_PATH . DIRECTORY_SEPARATOR . 'logo.default.png';
$invoiceData = [
'invoice' => "INV-" . Package::_raid(),
'invoice' => $invoiceNo,
'fullname' => $account->fullname,
'email' => $account->email,
'address' => $account->address,
@ -182,31 +178,90 @@ class Invoice
'payment_link' => $payLink
];
if (!isset($invoiceData['bill_rows']) || empty($invoiceData['bill_rows'])) {
_log("Invoice Error: Bill rows data is empty.");
if (empty($invoiceData['bill_rows'])) {
throw new Exception(Lang::T("Bill rows data is empty."));
}
$filename = self::generateInvoice($invoiceData);
if (!$filename) {
throw new Exception(Lang::T("Failed to generate invoice PDF"));
}
if ($filename) {
$pdfPath = "system/uploads/invoices/{$filename}";
self::saveToDatabase($filename, $account->id, $invoiceData, $total);
try {
Message::sendEmail(
$account->email,
"Invoice for Account {$account->fullname}",
"Please find your invoice attached",
Lang::T("Invoice for Account {$account->fullname}"),
Lang::T("Please find your invoice attached"),
$pdfPath
);
return true;
} catch (\Exception $e) {
_log("Failed to send invoice email: " . $e->getMessage());
sendTelegram("Failed to send invoice email: " . $e->getMessage());
return false;
throw new Exception(Lang::T("Failed to send email invoice to ") . $account->email . ". " . Lang::T("Reason: ") . $e->getMessage());
}
}
return false;
private static function validateAccount($account)
{
if (!$account) {
throw new Exception(Lang::T("User not found"));
}
if (!$account->email || !filter_var($account->email, FILTER_VALIDATE_EMAIL)) {
throw new Exception(Lang::T("Invalid user email"));
}
}
private static function generateInvoiceItems($invoice, $bills, $add_cost)
{
$items = [
[
'description' => $invoice['plan_name'],
'details' => Lang::T('Subscription'),
'amount' => (float) $invoice['price']
]
];
if ($invoice->routers != 'balance') {
foreach ($bills as $description => $amount) {
if (is_numeric($amount)) {
$items[] = [
'description' => $description,
'details' => Lang::T('Additional Bill'),
'amount' => (float) $amount
];
} else {
_log(Lang::T("Invalid bill amount for {$description}: {$amount}"));
}
}
}
return $items;
}
private static function generatePaymentLink($account, $invoice, $status)
{
$token = User::generateToken($account->id, 1);
if (empty($token['token'])) {
return '?_route=home';
}
$tur = ORM::for_table('tbl_user_recharges')
->where('customer_id', $account->id)
->where('namebp', $invoice->plan_name);
$tur->where('status', $status === 'Paid' ? 'on' : 'off');
$turResult = $tur->find_one();
return $turResult ? '?_route=home&recharge=' . $turResult['id'] . '&uid=' . urlencode($token['token']) : '?_route=home';
}
private static function getCompanyLogo($UPLOAD_PATH, $root_path)
{
$UPLOAD_URL_PATH = str_replace($root_path, '', $UPLOAD_PATH);
return file_exists($UPLOAD_PATH . DIRECTORY_SEPARATOR . 'logo.png') ?
$UPLOAD_URL_PATH . DIRECTORY_SEPARATOR . 'logo.png?' . time() :
$UPLOAD_URL_PATH . DIRECTORY_SEPARATOR . 'logo.default.png';
}
private static function generateBillRows($items, $currency, $subtotal, $tax_rate, $tax, $total)
@ -222,9 +277,11 @@ class Invoice
<tbody>";
foreach ($items as $item) {
$desc = htmlspecialchars($item['description'], ENT_QUOTES);
$details = htmlspecialchars($item['details'], ENT_QUOTES);
$html .= "<tr>
<td style='padding: 10px; border-bottom: 1px solid #ddd;'>{$item['description']}</td>
<td style='padding: 10px; border-bottom: 1px solid #ddd;'>{$item['details']}</td>
<td style='padding: 10px; border-bottom: 1px solid #ddd;'>{$desc}</td>
<td style='padding: 10px; border-bottom: 1px solid #ddd;'>{$details}</td>
<td style='padding: 10px; border-bottom: 1px solid #ddd;'>{$currency}" . number_format((float) $item['amount'], 2) . "</td>
</tr>";
}
@ -247,6 +304,44 @@ class Invoice
return $html;
}
private static function saveToDatabase($filename, $customer_id, $invoiceData, $total)
{
$invoice = ORM::for_table('tbl_invoices')->create();
$invoice->number = $invoiceData['invoice'];
$invoice->customer_id = $customer_id;
$invoice->fullname = $invoiceData['fullname'];
$invoice->email = $invoiceData['email'];
$invoice->address = $invoiceData['address'];
$invoice->status = $invoiceData['status'];
$invoice->due_date = $invoiceData['due_date'];
$invoice->filename = $filename;
$invoice->amount = $total;
$invoice->data = json_encode($invoiceData);
$invoice->created_at = date('Y-m-d H:i:s');
$invoice->save();
return $invoice->id;
}
public static function getAll()
{
return ORM::for_table('tbl_invoices')->order_by_desc('id')->find_many();
}
public static function getById($id)
{
return ORM::for_table('tbl_invoices')->find_one($id);
}
public static function getByNumber($number)
{
return ORM::for_table('tbl_invoices')->where('number', $number)->find_one();
}
public static function delete($id)
{
$invoice = ORM::for_table('tbl_invoices')->find_one($id);
if ($invoice) {
$invoice->delete();
return true;
}
return false;
}
}

View File

@ -46,7 +46,7 @@ class Message
$txts = str_split($txt, 160);
try {
foreach ($txts as $txt) {
self::sendSMS($config['sms_url'], $phone, $txt);
self::sendSMS($phone, $txt);
self::logMessage('SMS', $phone, $txt, 'Success');
}
} catch (Throwable $e) {
@ -140,6 +140,7 @@ class Message
}
mail($to, $subject, $body, $attr);
self::logMessage('Email', $to, $body, 'Success');
return true;
} else {
$mail = new PHPMailer();
$mail->isSMTP();
@ -188,8 +189,10 @@ class Message
if (!$mail->send()) {
$errorMessage = Lang::T("Email not sent, Mailer Error: ") . $mail->ErrorInfo;
self::logMessage('Email', $to, $body, 'Error', $errorMessage);
return false;
} else {
self::logMessage('Email', $to, $body, 'Success');
return true;
}
//<p style="font-family: Helvetica, sans-serif; font-size: 16px; font-weight: normal; margin: 0; margin-bottom: 16px;">
@ -396,9 +399,11 @@ class Message
$v->body = nl2br($body);
$v->save();
self::logMessage("Inbox", $user->username, $body, "Success");
return true;
} catch (Throwable $e) {
$errorMessage = Lang::T("Error adding message to inbox: " . $e->getMessage());
self::logMessage('Inbox', $user->username, $body, 'Error', $errorMessage);
return false;
}
}

View File

@ -114,7 +114,10 @@ switch ($action) {
$query = ORM::for_table('tbl_transactions')
->whereRaw("UNIX_TIMESTAMP(CONCAT(`recharged_on`,' ',`recharged_time`)) >= " . strtotime("$sd $ts"))
->whereRaw("UNIX_TIMESTAMP(CONCAT(`recharged_on`,' ',`recharged_time`)) <= " . strtotime("$ed $te"))
->order_by_desc('id');
->left_outer_join('tbl_customers', 'tbl_transactions.username = tbl_customers.username')
->select('tbl_transactions.*')
->select('tbl_customers.fullname', 'fullname')
->order_by_desc('tbl_transactions.id');
if (count($tps) > 0) {
$query->where_in('type', $tps);
}
@ -154,10 +157,11 @@ switch ($action) {
</div>
<div id="logo"><img id="image" src="' . $logo . '" alt="logo" /></div>
</div>
<div id="header">' . Lang::T('All Transactions at Date') . ': ' . Lang::dateAndTimeFormat($sd, $ts) .' - '. Lang::dateAndTimeFormat($ed, $te) . '</div>
<div id="header">' . Lang::T('All Transactions at Date') . ': ' . Lang::dateAndTimeFormat($sd, $ts) . ' - ' . Lang::dateAndTimeFormat($ed, $te) . '</div>
<table id="customers">
<tr>
<th>' . Lang::T('Username') . '</th>
<th>' . Lang::T('Fullname') . '</th>
<th>' . Lang::T('Plan Name') . '</th>
<th>' . Lang::T('Type') . '</th>
<th>' . Lang::T('Plan Price') . '</th>
@ -170,6 +174,7 @@ switch ($action) {
foreach ($x as $value) {
$username = $value['username'];
$fullname = $value['fullname'];
$plan_name = $value['plan_name'];
$type = $value['type'];
$price = $config['currency_code'] . ' ' . number_format($value['price'], 0, $config['dec_point'], $config['thousands_sep']);
@ -181,6 +186,7 @@ switch ($action) {
$html .= "<tr" . (($c = !$c) ? ' class="alt"' : ' class=""') . ">" . "
<td>$username</td>
<td>$fullname</td>
<td>$plan_name</td>
<td>$type</td>
<td align='right'>$price</td>
@ -245,7 +251,7 @@ $style
$html
EOF;
$mpdf->WriteHTML($nhtml);
$mpdf->Output('phpnuxbill_reports_'.date('Ymd_His') . '.pdf', 'D');
$mpdf->Output('phpnuxbill_reports_' . date('Ymd_His') . '.pdf', 'D');
} else {
echo 'No Data';
}
@ -258,6 +264,10 @@ EOF;
$stype = _post('stype');
$d = ORM::for_table('tbl_transactions');
$d->left_outer_join('tbl_customers', 'tbl_transactions.username = tbl_customers.username')
->select('tbl_transactions.*')
->select('tbl_customers.fullname', 'fullname')
->order_by_desc('tbl_transactions.id');
if ($stype != '') {
$d->where('type', $stype);
}
@ -290,6 +300,10 @@ EOF;
$tdate = _post('tdate');
$stype = _post('stype');
$d = ORM::for_table('tbl_transactions');
$d->left_outer_join('tbl_customers', 'tbl_transactions.username = tbl_customers.username')
->select('tbl_transactions.*')
->select('tbl_customers.fullname', 'fullname')
->order_by_desc('tbl_transactions.id');
if ($stype != '') {
$d->where('type', $stype);
}
@ -332,6 +346,7 @@ EOF;
<table id="customers">
<tr>
<th>' . Lang::T('Username') . '</th>
<th>' . Lang::T('Fullname') . '</th>
<th>' . Lang::T('Plan Name') . '</th>
<th>' . Lang::T('Type') . '</th>
<th>' . Lang::T('Plan Price') . '</th>
@ -344,6 +359,7 @@ EOF;
foreach ($x as $value) {
$username = $value['username'];
$fullname = $value['fullname'];
$plan_name = $value['plan_name'];
$type = $value['type'];
$price = $config['currency_code'] . ' ' . number_format($value['price'], 0, $config['dec_point'], $config['thousands_sep']);
@ -355,6 +371,7 @@ EOF;
$html .= "<tr" . (($c = !$c) ? ' class="alt"' : ' class=""') . ">" . "
<td>$username</td>
<td>$fullname</td>
<td>$plan_name</td>
<td>$type</td>
<td align='right'>$price</td>

View File

@ -0,0 +1,27 @@
<?php
/**
* PHP Mikrotik Billing (https://github.com/hotspotbilling/phpnuxbill/)
* by https://t.me/ibnux
*
**/
_admin();
$ui->assign('_title', Lang::T('Invoice Lists'));
$ui->assign('_system_menu', 'reports');
$action = $routes['1'];
$ui->assign('_admin', $admin);
if (empty($action)) {
$action = 'list';
}
switch ($action) {
case 'list':
$ui->assign('xheader', '<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.11.3/css/jquery.dataTables.min.css">');
$ui->assign('invoices', Invoice::getAll());
$ui->display('admin/invoices/list.tpl');
break;
default:
$ui->display('admin/404.tpl');
}

View File

@ -57,56 +57,79 @@ EOT;
_alert(Lang::T('You do not have permission to access this page'), 'danger', "dashboard");
}
// Get form data
$id_customer = $_POST['id_customer'];
$message = $_POST['message'];
$via = $_POST['via'];
$id_customer = $_POST['id_customer'] ?? '';
$message = $_POST['message'] ?? '';
$via = $_POST['via'] ?? '';
$subject = $_POST['subject'] ?? '';
// Check if fields are empty
if ($id_customer == '' or $message == '' or $via == '') {
r2(getUrl('message/send'), 'e', Lang::T('All field is required'));
} else {
// Get customer details from the database
$c = ORM::for_table('tbl_customers')->find_one($id_customer);
// Validate subject based on the selected channel
if (($via === 'all' || $via === 'email' || $via === 'inbox') && empty($subject)) {
r2(getUrl('message/send'), 'e', LANG::T('Subject is required to send message using') . ' ' . $via . '.');
}
if (empty($id_customer) || empty($message) || empty($via)) {
r2(getUrl('message/send'), 'e', Lang::T('Customer, Message, and Channel are required'));
}
$customer = ORM::for_table('tbl_customers')->find_one($id_customer);
if (!$customer) {
r2(getUrl('message/send'), 'e', Lang::T('Customer not found'));
}
// Replace placeholders in message and subject
$currentMessage = str_replace(
['[[name]]', '[[user_name]]', '[[phone]]', '[[company_name]]'],
[$customer['fullname'], $customer['username'], $customer['phonenumber'], $config['CompanyName']],
$message
);
$currentSubject = str_replace(
['[[name]]', '[[user_name]]', '[[phone]]', '[[company_name]]'],
[$customer['fullname'], $customer['username'], $customer['phonenumber'], $config['CompanyName']],
$subject
);
// Replace placeholders in the message with actual values
$message = str_replace('[[name]]', $c['fullname'], $message);
$message = str_replace('[[user_name]]', $c['username'], $message);
$message = str_replace('[[phone]]', $c['phonenumber'], $message);
$message = str_replace('[[company_name]]', $config['CompanyName'], $message);
if (strpos($message, '[[payment_link]]') !== false) {
// token only valid for 1 day, for security reason
$token = User::generateToken($c['id'], 1);
$token = User::generateToken($customer['id'], 1);
if (!empty($token['token'])) {
$tur = ORM::for_table('tbl_user_recharges')
->where('customer_id', $c['id'])
//->where('namebp', $package)
->where('customer_id', $customer['id'])
->find_one();
if ($tur) {
$url = '?_route=home&recharge=' . $tur['id'] . '&uid=' . urlencode($token['token']);
$message = str_replace('[[payment_link]]', $url, $message);
$currentMessage = str_replace('[[payment_link]]', $url, $currentMessage);
}
} else {
$message = str_replace('[[payment_link]]', '', $message);
$currentMessage = str_replace('[[payment_link]]', '', $currentMessage);
}
}
// Send the message through the selected channels
$smsSent = $waSent = $emailSent = $inboxSent = false;
//Send the message
if ($via == 'sms' || $via == 'both') {
$smsSent = Message::sendSMS($c['phonenumber'], $message);
if ($via === 'sms' || $via === 'both' || $via === 'all') {
$smsSent = Message::sendSMS($customer['phonenumber'], $currentSubject);
}
if ($via == 'wa' || $via == 'both') {
$waSent = Message::sendWhatsapp($c['phonenumber'], $message);
if ($via === 'wa' || $via === 'both' || $via === 'all') {
$waSent = Message::sendWhatsapp($customer['phonenumber'], $currentSubject);
}
if (isset($smsSent) || isset($waSent)) {
if ($via === 'email' || $via === 'all') {
$emailSent = Message::sendEmail($customer['email'], $currentSubject, $currentMessage);
}
if ($via === 'inbox' || $via === 'all') {
$inboxSent = Message::addToInbox($customer['id'], $currentSubject, $currentMessage, 'Admin');
}
// Check if any message was sent successfully
if ($smsSent || $waSent || $emailSent || $inboxSent) {
r2(getUrl('message/send'), 's', Lang::T('Message Sent Successfully'));
} else {
r2(getUrl('message/send'), 'e', Lang::T('Failed to send message'));
}
}
break;
case 'send_bulk':
@ -133,11 +156,16 @@ EOT;
$batch = $_REQUEST['batch'] ?? 100;
$page = $_REQUEST['page'] ?? 0;
$router = $_REQUEST['router'] ?? null;
$test = isset($_REQUEST['test']) && $_REQUEST['test'] === 'on' ? true : false;
$test = isset($_REQUEST['test']) && $_REQUEST['test'] === 'on';
$service = $_REQUEST['service'] ?? '';
$subject = $_REQUEST['subject'] ?? '';
if (empty($group) || empty($message) || empty($via) || empty($service)) {
die(json_encode(['status' => 'error', 'message' => 'All fields are required']));
die(json_encode(['status' => 'error', 'message' => LANG::T('All fields are required')]));
}
if (in_array($via, ['all', 'email', 'inbox']) && empty($subject)) {
die(json_encode(['status' => 'error', 'message' => LANG::T('Subject is required to send message using') . ' ' . $via . '.']));
}
// Get batch of customers based on group
@ -153,7 +181,7 @@ EOT;
default:
$router = ORM::for_table('tbl_routers')->find_one($router);
if (!$router) {
die(json_encode(['status' => 'error', 'message' => 'Invalid router']));
die(json_encode(['status' => 'error', 'message' => LANG::T('Invalid router')]));
}
$routerName = $router->name;
break;
@ -200,6 +228,9 @@ EOT;
['tbl_customers.phonenumber', 'phonenumber'],
['tbl_user_recharges.customer_id', 'customer_id'],
['tbl_customers.fullname', 'fullname'],
['tbl_customers.username', 'username'],
['tbl_customers.email', 'email'],
['tbl_customers.service_type', 'service_type'],
]);
$customers = $query->find_array();
} else {
@ -287,7 +318,13 @@ EOT;
$totalSMSFailed = 0;
$totalWhatsappSent = 0;
$totalWhatsappFailed = 0;
$totalEmailSent = 0;
$totalEmailFailed = 0;
$totalInboxSent = 0;
$totalInboxFailed = 0;
$batchStatus = [];
//$subject = $config['CompanyName'] . ' ' . Lang::T('Notification Message');
$form = 'Admin';
foreach ($customers as $customer) {
$currentMessage = str_replace(
@ -296,6 +333,12 @@ EOT;
$message
);
$currentSubject = str_replace(
['[[name]]', '[[user_name]]', '[[phone]]', '[[company_name]]'],
[$customer['fullname'], $customer['username'], $customer['phonenumber'], $config['CompanyName']],
$subject
);
$phoneNumber = preg_replace('/\D/', '', $customer['phonenumber']);
if (empty($phoneNumber)) {
@ -310,14 +353,14 @@ EOT;
if ($test) {
$batchStatus[] = [
'name' => $customer['fullname'],
'phone' => $customer['phonenumber'],
'channel' => 'Test Channel',
'status' => 'Test Mode',
'message' => $currentMessage,
'service' => $service,
'router' => $routerName,
];
} else {
if ($via == 'sms' || $via == 'both') {
if ($via === 'sms' || $via === 'both' || $via === 'all') {
if (Message::sendSMS($customer['phonenumber'], $currentMessage)) {
$totalSMSSent++;
$batchStatus[] = ['name' => $customer['fullname'], 'phone' => $customer['phonenumber'], 'status' => 'SMS Sent', 'message' => $currentMessage];
@ -327,13 +370,33 @@ EOT;
}
}
if ($via == 'wa' || $via == 'both') {
if ($via === 'wa' || $via == 'both' || $via === 'all') {
if (Message::sendWhatsapp($customer['phonenumber'], $currentMessage)) {
$totalWhatsappSent++;
$batchStatus[] = ['name' => $customer['fullname'], 'phone' => $customer['phonenumber'], 'status' => 'WhatsApp Sent', 'message' => $currentMessage];
$batchStatus[] = ['name' => $customer['fullname'], 'channel' => $customer['phonenumber'], 'status' => 'WhatsApp Sent', 'message' => $currentMessage];
} else {
$totalWhatsappFailed++;
$batchStatus[] = ['name' => $customer['fullname'], 'phone' => $customer['phonenumber'], 'status' => 'WhatsApp Failed', 'message' => $currentMessage];
$batchStatus[] = ['name' => $customer['fullname'], 'channel' => $customer['phonenumber'], 'status' => 'WhatsApp Failed', 'message' => $currentMessage];
}
}
if ($via === 'email' || $via === 'all') {
if (Message::sendEmail($customer['email'], $currentSubject, $currentMessage)) {
$totalEmailSent++;
$batchStatus[] = ['name' => $customer['fullname'], 'channel' => $customer['email'], 'status' => 'Email Sent', 'message' => $currentMessage];
} else {
$totalEmailFailed++;
$batchStatus[] = ['name' => $customer['fullname'], 'channel' => $customer['email'], 'status' => 'Email Failed', 'message' => $currentMessage];
}
}
if ($via === 'inbox' || $via === 'all') {
if (Message::addToInbox($customer['customer_id'], $currentSubject, $currentMessage, $form)) {
$totalInboxSent++;
$batchStatus[] = ['name' => $customer['fullname'], 'channel' => 'Inbox', 'status' => 'Inbox Message Sent', 'message' => $currentMessage];
} else {
$totalInboxFailed++;
$batchStatus[] = ['name' => $customer['fullname'], 'channel' => 'Inbox', 'status' => 'Inbox Message Failed', 'message' => $currentMessage];
}
}
}
@ -348,8 +411,8 @@ EOT;
'page' => $page + 1,
'batchStatus' => $batchStatus,
'message' => $currentMessage,
'totalSent' => $totalSMSSent + $totalWhatsappSent,
'totalFailed' => $totalSMSFailed + $totalWhatsappFailed,
'totalSent' => $totalSMSSent + $totalWhatsappSent + $totalEmailSent + $totalInboxSent,
'totalFailed' => $totalSMSFailed + $totalWhatsappFailed + $totalEmailFailed + $totalInboxFailed,
'hasMore' => $hasMore,
'service' => $service,
'router' => $routerName,
@ -365,16 +428,20 @@ EOT;
// Get the posted data
$customerIds = $_POST['customer_ids'] ?? [];
$via = $_POST['message_type'] ?? '';
$subject = $_POST['subject'] ?? '';
$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;
}
if ($via === 'all' || $via === 'email' || $via === 'inbox' && empty($subject)) {
die(json_encode(['status' => 'error', 'message' => LANG::T('Subject is required to send message using') . ' ' . $via . '.']));
}
// Prepare to send messages
$sentCount = 0;
$failedCount = 0;
$subject = Lang::T('Notification Message');
$form = 'Admin';
foreach ($customerIds as $customerId) {

View File

@ -58,7 +58,7 @@ switch ($action) {
$w = [];
$v = [];
foreach ($mts as $mt) {
$w[] ='method';
$w[] = 'method';
$v[] = "$mt - %";
}
$query->where_likes($w, $v);
@ -91,7 +91,7 @@ switch ($action) {
$w = [];
$v = [];
foreach ($mts as $mt) {
$w[] ='method';
$w[] = 'method';
$v[] = "$mt - %";
}
$query->where_likes($w, $v);
@ -161,7 +161,7 @@ switch ($action) {
$w = [];
$v = [];
foreach ($mts as $mt) {
$w[] ='method';
$w[] = 'method';
$v[] = "$mt - %";
}
$query->where_likes($w, $v);
@ -246,7 +246,7 @@ switch ($action) {
$total += $v;
$array['data'][] = $v;
}
if($total>0){
if ($total > 0) {
$result['datas'][] = $array;
}
}
@ -258,18 +258,29 @@ switch ($action) {
die();
case 'by-date':
case 'activation':
$q = (_post('q') ? _post('q') : _get('q'));
$q = trim(_post('q') ?: _get('q'));
$keep = _post('keep');
if (!empty($keep)) {
ORM::raw_execute("DELETE FROM tbl_transactions WHERE date < UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL $keep DAY))");
r2(getUrl('logs/list/'), 's', "Delete logs older than $keep days");
ORM::raw_execute("DELETE FROM tbl_transactions WHERE date < UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL ? DAY))", [$keep]);
r2(getUrl('reports/activation/'), 's', "Deleted logs older than $keep days");
}
if ($q != '') {
$query = ORM::for_table('tbl_transactions')->where_like('invoice', '%' . $q . '%')->order_by_desc('id');
$query = ORM::for_table('tbl_transactions')
->left_outer_join('tbl_customers', 'tbl_transactions.username = tbl_customers.username')
->select('tbl_transactions.*')
->select('tbl_customers.fullname', 'fullname')
->order_by_desc('tbl_transactions.id');
if ($q !== '') {
$query->where_like('invoice', "%$q%");
}
try {
$d = Paginator::findMany($query, ['q' => $q]);
} else {
$query = ORM::for_table('tbl_transactions')->order_by_desc('id');
$d = Paginator::findMany($query);
} catch (Exception $e) {
r2(getUrl('reports/activation/'), 'e', 'Database query failed: ' . $e->getMessage());
$d = [];
}
$ui->assign('activation', $d);
@ -291,6 +302,10 @@ switch ($action) {
$stype = _post('stype');
$d = ORM::for_table('tbl_transactions');
$d->left_outer_join('tbl_customers', 'tbl_transactions.username = tbl_customers.username')
->select('tbl_transactions.*')
->select('tbl_customers.fullname', 'fullname')
->order_by_desc('tbl_transactions.id');
if ($stype != '') {
$d->where('type', $stype);
}
@ -348,7 +363,10 @@ switch ($action) {
$query = ORM::for_table('tbl_transactions')
->whereRaw("UNIX_TIMESTAMP(CONCAT(`recharged_on`,' ',`recharged_time`)) >= " . strtotime("$sd $ts"))
->whereRaw("UNIX_TIMESTAMP(CONCAT(`recharged_on`,' ',`recharged_time`)) <= " . strtotime("$ed $te"))
->order_by_desc('id');
->left_outer_join('tbl_customers', 'tbl_transactions.username = tbl_customers.username')
->select('tbl_transactions.*')
->select('tbl_customers.fullname', 'fullname')
->order_by_desc('tbl_transactions.id');
if (count($tps) > 0) {
$query->where_in('type', $tps);
}
@ -356,7 +374,7 @@ switch ($action) {
$w = [];
$v = [];
foreach ($mts as $mt) {
$w[] ='method';
$w[] = 'method';
$v[] = "$mt - %";
}
$query->where_likes($w, $v);

View File

@ -146,7 +146,8 @@ switch ($action) {
$r = ORM::for_table('tbl_routers')->find_many();
$ui->assign('r', $r);
if (function_exists("shell_exec")) {
$php = trim(shell_exec('which php'));
$which = stripos(php_uname('s'), "Win") === 0 ? 'where' : 'which';
$php = trim(shell_exec("$which php"));
if (empty($php)) {
$php = 'php';
}

View File

@ -49,7 +49,7 @@ foreach ($d as $ds) {
} else {
$price = $p['price'];
}
if ($ds['expiration'] == $day7 && $config['notification_reminder_7day'] !== 'no') {
if ($ds['expiration'] == $day7 && $config['notification_reminder_7days'] !== 'no') {
try {
echo Message::sendPackageNotification(
$c,
@ -61,7 +61,7 @@ foreach ($d as $ds) {
} catch (Exception $e) {
sendTelegram("Cron Reminder failed to send 7-day reminder to " . $ds['username'] . " Error: " . $e->getMessage());
}
} else if ($ds['expiration'] == $day3 && $config['notification_reminder_3day'] !== 'no') {
} else if ($ds['expiration'] == $day3 && $config['notification_reminder_3days'] !== 'no') {
try {
echo Message::sendPackageNotification(
$c,

View File

@ -244,7 +244,7 @@ class MikrotikPppoe
function add_pool($pool){
global $_app_stage;
if ($_app_stage == 'demo') {
if ($_app_stage == 'Demo') {
return null;
}
$mikrotik = $this->info($pool['routers']);
@ -259,7 +259,7 @@ class MikrotikPppoe
function update_pool($old_pool, $new_pool){
global $_app_stage;
if ($_app_stage == 'demo') {
if ($_app_stage == 'Demo') {
return null;
}
$mikrotik = $this->info($new_pool['routers']);
@ -284,7 +284,7 @@ class MikrotikPppoe
function remove_pool($pool){
global $_app_stage;
if ($_app_stage == 'demo') {
if ($_app_stage == 'Demo') {
return null;
}
$mikrotik = $this->info($pool['routers']);
@ -329,7 +329,7 @@ class MikrotikPppoe
function getClient($ip, $user, $pass)
{
global $_app_stage;
if ($_app_stage == 'demo') {
if ($_app_stage == 'Demo') {
return null;
}
$iport = explode(":", $ip);
@ -339,7 +339,7 @@ class MikrotikPppoe
function removePpoeUser($client, $username)
{
global $_app_stage;
if ($_app_stage == 'demo') {
if ($_app_stage == 'Demo') {
return null;
}
$printRequest = new RouterOS\Request('/ppp/secret/print');
@ -376,7 +376,7 @@ class MikrotikPppoe
function removePpoeActive($client, $username)
{
global $_app_stage;
if ($_app_stage == 'demo') {
if ($_app_stage == 'Demo') {
return null;
}
$onlineRequest = new RouterOS\Request('/ppp/active/print');
@ -392,7 +392,7 @@ class MikrotikPppoe
function getIpHotspotUser($client, $username)
{
global $_app_stage;
if ($_app_stage == 'demo') {
if ($_app_stage == 'Demo') {
return null;
}
$printRequest = new RouterOS\Request(
@ -405,7 +405,7 @@ class MikrotikPppoe
function addIpToAddressList($client, $ip, $listName, $comment = '')
{
global $_app_stage;
if ($_app_stage == 'demo') {
if ($_app_stage == 'Demo') {
return null;
}
$addRequest = new RouterOS\Request('/ip/firewall/address-list/add');
@ -420,7 +420,7 @@ class MikrotikPppoe
function removeIpFromAddressList($client, $ip)
{
global $_app_stage;
if ($_app_stage == 'demo') {
if ($_app_stage == 'Demo') {
return null;
}
$printRequest = new RouterOS\Request(

View File

@ -381,5 +381,166 @@
"_Clear_old_logs_": " Clear old logs?",
"Clean_up_Logs": "Clean up Logs",
"ID": "ID",
"Date_Sent": "Date Sent"
"Date_Sent": "Date Sent",
"Notification_Message": "Notification Message",
"Share": "Share",
"Previous": "Previous",
"Email_not_sent__Mailer_Error__": "Email not sent, Mailer Error: ",
"Invoice_Lists": "Invoice Lists",
"Invoices": "Invoices",
"Invoice_No": "Invoice No",
"Customer_Name": "Customer Name",
"Amount": "Amount",
"Due_Date": "Due Date",
"Actions": "Actions",
"Pending": "Pending",
"Send": "Send",
"": "",
"Commission": "Commission",
"Last_30_Days_Overall_Usage": "Last 30 Days Overall Usage",
"Last_30_Days_Total_Usage": "Last 30 Days Total Usage",
"Top_10_Downloaders": "Top 10 Downloaders",
"Rank": "Rank",
"Downloaded": "Downloaded",
"Upload_Data": "Upload Data",
"Download_Data": "Download Data",
"Total_Data": "Total Data",
"Service": "Service",
"Mac_Address": "Mac Address",
"Location": "Location",
"Last_Updated": "Last Updated",
"View_Details": "View Details",
"Successful_Payments": "Successful Payments",
"Failed_Payments": "Failed Payments",
"Pending_Payments": "Pending Payments",
"Cancelled_Payments": "Cancelled Payments",
"Refresh_Dashboard": "Refresh Dashboard",
"Daily_Sales": "Daily Sales",
"Monthly_Sales": "Monthly Sales",
"Weekly_Sales": "Weekly Sales",
"Hotspot_Vouchers_Overview": "Hotspot Vouchers Overview",
"Transaction_Ref": "Transaction Ref",
"Router_Name": "Router Name",
"Voucher_Code": "Voucher Code",
"Transaction_Status": "Transaction Status",
"Payment_Date": "Payment Date",
"Plan_Expiry_Date": "Plan Expiry Date",
"Action": "Action",
"Block_Mac_Address": "Block Mac Address",
"Unblock_Mac_Address": "Unblock Mac Address",
"Captive_Portal_Dashboard": "Captive Portal Dashboard",
"Manage_Router_NAS": "Manage Router\/NAS",
"Captive_Portal_Routers": "Captive Portal Routers",
"Add_New_Router": "Add New Router",
"Router_Details": "Router Details",
"MAP": "MAP",
"LANDING_PAGE": "LANDING PAGE",
"FILE": "FILE",
"EDIT": "EDIT",
"Edit_Router": "Edit Router",
"Description": "Description",
"Enter_Description": "Enter Description",
"Coordinate": "Coordinate",
"DELETE": "DELETE",
"Back_To_Dashboard": "Back To Dashboard",
"Device_Type": "Device Type",
"Mikrotik_API": "Mikrotik API",
"Choose_Router": "Choose Router",
"Choose": "Choose",
"Select": "Select",
"Add_Router": "Add Router",
"Router_Location": "Router Location",
"Go_Back": "Go Back",
"Preview": "Preview",
"Login_html": "Login.html",
"Branding": "Branding",
"Sliders": "Sliders",
"Advertisements": "Advertisements",
"Announcements": "Announcements",
"Integrations": "Integrations",
"Basic_Settings": "Basic Settings",
"Page_Title": "Page Title",
"Displayed_in_browser_title_bar": "Displayed in browser title bar",
"Page_description_browser_title_bar": "Page description browser title bar",
"Hotspot_Name": "Hotspot Name",
"Hotspot_Name_will_be_display_on_Login_Page_Nav_Bar_if_Logo_is_not_available": "Hotspot Name will be display on Login Page Nav Bar if Logo is not available",
"Footer_Text": "Footer Text",
"Allow_Free_Trial": "Allow Free Trial",
"Choose_No_if_you_dont_want_to_allow_Free_Trial": "Choose No if you dont want to allow Free Trial",
"Make_sure_you_enable_free_trial_in_Mikrotik_Router": "Make sure you enable free trial in Mikrotik Router",
"free_trial_button_wont_display_on_captive_portal_preview__but_will_work_if_you_connect_from_a_hotspot": "free trial button wont display on captive portal preview, but will work if you connect from a hotspot",
"Allow_Member_Login": "Allow Member Login",
"Choose_No_If_you_want_to_disable_Member_Login": "Choose No If you want to disable Member Login",
"Allow_Randomized_MAC": "Allow Randomized MAC",
"Choose_Yes_If_you_want_to_allow_Randomized_or_Private_MAC": "Choose Yes If you want to allow Randomized or Private MAC",
"Assign_Plan": "Assign Plan",
"Service_Plan": "Service Plan",
"Change": "Change",
"Select_Plans": "Select Plans",
"Choose_plan_that_will_that_your_users_will_be_assigned": "Choose plan that will that your users will be assigned",
"Restore_Default_Settings": "Restore Default Settings",
"Restore_Default": "Restore Default",
"Branding_Image": "Branding Image",
"Logo": "Logo",
"Backgrounds": "Backgrounds",
"Background_Type": "Background Type",
"Static_Color": "Static Color",
"Gradient_Color": "Gradient Color",
"Image": "Image",
"Static_Background_Color": "Static Background Color",
"Gradient_Background_Colors": "Gradient Background Colors",
"Gradient_Direction": "Gradient Direction",
"Left_to_Right": "Left to Right",
"Top_to_Bottom": "Top to Bottom",
"Right_to_Left": "Right to Left",
"Bottom_to_Top": "Bottom to Top",
"Choose_Image": "Choose Image",
"Buttons": "Buttons",
"Slider_Contact_Us_Button_BG_Color": "Slider Contact Us Button BG Color",
"Slider_Contact_Us_Button_Text_Color": "Slider Contact Us Button Text Color",
"Slider_Navigation_Button_BG_Color": "Slider Navigation Button BG Color",
"Slider_Navigation_Button_Icon_Color": "Slider Navigation Button Icon Color",
"Trial_Button_BG_Color": "Trial Button BG Color",
"Trial_Button_Text_Color": "Trial Button Text Color",
"Footer_Settings": "Footer Settings",
"Footer_Background_Color": "Footer Background Color",
"Misc": "Misc",
"Title": "Title",
"Link": "Link",
"Button": "Button",
"Add_New_Slider": "Add New Slider",
"Welcome_to_the_Captive_Portal": "Welcome to the Captive Portal",
"Button_Link": "Button Link",
"Button_Text": "Button Text",
"Click_Here": "Click Here",
"Target_Link": "Target Link",
"Create_New_Advert": "Create New Advert",
"Add_New_Advertisement": "Add New Advertisement",
"Content": "Content",
"Enter_Interstitial_Content": "Enter Interstitial Content",
"Upload_File": "Upload File",
"File_size_limit__5MB_for_images__100MB_for_videos": "File size limit: 5MB for images, 100MB for videos",
"Target_URL": "Target URL",
"Create_Advert": "Create Advert",
"You_cannot_perform_this_action_in_Demo_mode": "You cannot perform this action in Demo mode",
"Income_Today": "Income Today",
"Income_This_Month": "Income This Month",
"Customers": "Customers",
"Monthly_Registered_Customers": "Monthly Registered Customers",
"Cron_has_not_run_for_over_1_hour__Please_check_your_setup_": "Cron has not run for over 1 hour. Please check your setup.",
"Total_Monthly_Sales": "Total Monthly Sales",
"Customers_Expired__Today": "Customers Expired, Today",
"Phone": "Phone",
"Created___Expired": "Created \/ Expired",
"Internet_Package": "Internet Package",
"year": "year",
"month": "month",
"week": "week",
"day": "day",
"hour": "hour",
"minute": "minute",
"second": "second",
"ago": "ago",
"All_Users_Insights": "All Users Insights",
"Activity_Log": "Activity Log"
}

View File

@ -63,9 +63,7 @@
"ALTER TABLE `tbl_plans` ADD `list_expired` VARCHAR(32) NOT NULL DEFAULT '' COMMENT 'address list' AFTER `pool_expired`;",
"ALTER TABLE `tbl_bandwidth` ADD `burst` VARCHAR(128) NOT NULL DEFAULT '' AFTER `rate_up_unit`;"
],
"2024.2.20.1": [
"DROP TABLE IF EXISTS `tbl_customers_meta`;"
],
"2024.2.20.1": ["DROP TABLE IF EXISTS `tbl_customers_meta`;"],
"2024.2.23": [
"ALTER TABLE `tbl_transactions` ADD `admin_id` INT NOT NULL DEFAULT '1' AFTER `type`;",
"ALTER TABLE `tbl_user_recharges` ADD `admin_id` INT NOT NULL DEFAULT '1' AFTER `type`;"
@ -191,20 +189,23 @@
"2025.2.14": [
"CREATE TABLE IF NOT EXISTS `tbl_widgets` ( `id` int NOT NULL AUTO_INCREMENT, `orders` int NOT NULL DEFAULT '99', `position` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1. top 2. left 3. right 4. bottom',`enabled` tinyint(1) NOT NULL DEFAULT '1', `title` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, `widget` varchar(64) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '', `content` text COLLATE utf8mb4_general_ci NOT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;"
],
"2025.2.17" : [
"2025.2.17": [
"INSERT INTO `tbl_widgets` (`id`, `orders`, `position`, `enabled`, `title`, `widget`, `content`) VALUES (1, 1, 1, 1, 'Top Widget', 'top_widget', ''),(2, 2, 1, 1, 'Default Info', 'default_info_row', ''),(3, 1, 2, 1, 'Graph Monthly Registered Customers', 'graph_monthly_registered_customers', ''),(4, 2, 2, 1, 'Graph Monthly Sales', 'graph_monthly_sales', ''),(5, 3, 2, 1, 'Voucher Stocks', 'voucher_stocks', ''),(6, 4, 2, 1, 'Customer Expired', 'customer_expired', ''),(7, 1, 3, 1, 'Cron Monitor', 'cron_monitor', ''),(8, 2, 3, 1, 'Mikrotik Cron Monitor', 'mikrotik_cron_monitor', ''),(9, 3, 3, 1, 'Info Payment Gateway', 'info_payment_gateway', ''),(10, 4, 3, 1, 'Graph Customers Insight', 'graph_customers_insight', ''),(11, 5, 3, 1, 'Activity Log', 'activity_log', '');"
],
"2025.2.19" : [
"2025.2.19": [
"ALTER TABLE `tbl_widgets` ADD `user` ENUM('Admin','Agent','Sales','Customer') NOT NULL DEFAULT 'Admin' AFTER `position`;"
],
"2025.2.21" : [
"2025.2.21": [
"INSERT INTO `tbl_widgets` (`id`, `orders`, `position`, `user`, `enabled`, `title`, `widget`, `content`) VALUES (60, 1, 2, 'Customer', 1, 'Account Info', 'account_info', ''),(61, 3, 1, 'Customer', 1, 'Active Internet Plan', 'active_internet_plan', ''),(62, 4, 1, 'Customer', 1, 'Balance Transfer', 'balance_transfer', ''),(63, 1, 1, 'Customer', 1, 'Unpaid Order', 'unpaid_order', ''),(64, 2, 1, 'Customer', 1, 'Announcement', 'announcement', ''),(65, 5, 1, 'Customer', 1, 'Recharge A Friend', 'recharge_a_friend', ''),(66, 2, 2, 'Customer', 1, 'Voucher Activation', 'voucher_activation', '');"
],
"2025.2.25" : [
"2025.2.25": [
"INSERT INTO `tbl_widgets` (`id`, `orders`, `position`, `user`, `enabled`, `title`, `widget`, `content`) VALUES (30, 1, 1, 'Agent', 1, 'Top Widget', 'top_widget', ''), (31, 2, 1, 'Agent', 1, 'Default Info', 'default_info_row', ''), (32, 1, 2, 'Agent', 1, 'Graph Monthly Registered Customers', 'graph_monthly_registered_customers', ''), (33, 2, 2, 'Agent', 1, 'Graph Monthly Sales', 'graph_monthly_sales', ''), (34, 3, 2, 'Agent', 1, 'Voucher Stocks', 'voucher_stocks', ''), (35, 4, 2, 'Agent', 1, 'Customer Expired', 'customer_expired', ''), (36, 1, 3, 'Agent', 1, 'Cron Monitor', 'cron_monitor', ''), (37, 2, 3, 'Agent', 1, 'Mikrotik Cron Monitor', 'mikrotik_cron_monitor', ''), (38, 3, 3, 'Agent', 1, 'Info Payment Gateway', 'info_payment_gateway', ''), (39, 4, 3, 'Agent', 1, 'Graph Customers Insight', 'graph_customers_insight', ''),(40, 5, 3, 'Agent', 1, 'Activity Log', 'activity_log', '');",
"INSERT INTO `tbl_widgets` (`id`, `orders`, `position`, `user`, `enabled`, `title`, `widget`, `content`) VALUES (41, 1, 1, 'Sales', 1, 'Top Widget', 'top_widget', ''), (42, 2, 1, 'Sales', 1, 'Default Info', 'default_info_row', ''), (43, 1, 2, 'Sales', 1, 'Graph Monthly Registered Customers', 'graph_monthly_registered_customers', ''), (44, 2, 2, 'Sales', 1, 'Graph Monthly Sales', 'graph_monthly_sales', ''), (45, 3, 2, 'Sales', 1, 'Voucher Stocks', 'voucher_stocks', ''), (46, 4, 2, 'Sales', 1, 'Customer Expired', 'customer_expired', ''), (47, 1, 3, 'Sales', 1, 'Cron Monitor', 'cron_monitor', ''), (48, 2, 3, 'Sales', 1, 'Mikrotik Cron Monitor', 'mikrotik_cron_monitor', ''), (49, 3, 3, 'Sales', 1, 'Info Payment Gateway', 'info_payment_gateway', ''), (50, 4, 3, 'Sales', 1, 'Graph Customers Insight', 'graph_customers_insight', ''), (51, 5, 3, 'Sales', 1, 'Activity Log', 'activity_log', '');"
],
"2025.3.5" : [
"2025.3.5": [
"CREATE TABLE IF NOT EXISTS `tbl_message_logs` ( `id` SERIAL PRIMARY KEY, `message_type` VARCHAR(50), `recipient` VARCHAR(255), `message_content` TEXT, `status` VARCHAR(50), `error_message` TEXT, `sent_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;"
],
"2025.3.10": [
"CREATE TABLE IF NOT EXISTS `tbl_invoices` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `number` VARCHAR(50) NOT NULL, `customer_id` INT NOT NULL, `fullname` VARCHAR(100) NOT NULL, `email` VARCHAR(100) NOT NULL, `address` TEXT, `status` ENUM('Unpaid', 'Paid', 'Cancelled') NOT NULL DEFAULT 'Unpaid', `due_date` DATETIME NOT NULL, `filename` VARCHAR(255), `amount` DECIMAL(10, 2) NOT NULL, `data` JSON NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP);"
]
}

View File

View File

@ -18,9 +18,9 @@
{if in_array($_admin['user_type'],['SuperAdmin','Admin'])}
<div class="btn-group pull-right">
<a class="btn btn-primary btn-xs" title="save"
href="{Text::url('customers/csv&token=', $csrf_token)}"
onclick="return ask(this, '{Lang::T("This will export to CSV")}?')"><span
class="glyphicon glyphicon-download" aria-hidden="true"></span> CSV</a>
href="{Text::url('customers/csv&token=', $csrf_token)}" onclick="return ask(this, '{Lang::T("
This will export to CSV")}?')"><span class="glyphicon glyphicon-download"
aria-hidden="true"></span> CSV</a>
</div>
{/if}
{Lang::T('Manage Contact')}
@ -205,14 +205,15 @@
</button>
</div>
<div class="modal-body">
<select id="messageType" class="form-control">
<select style="margin-bottom: 10px;" 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>
<input type="text" style="margin-bottom: 10px;" class="form-control" id="subject-content" value=""
placeholder="{Lang::T('Enter message subject here')}">
<textarea id="messageContent" class="form-control" rows="4"
placeholder="{Lang::T('Enter your message here...')}"></textarea>
</div>
@ -260,6 +261,8 @@
$('#sendMessageButton').on('click', function () {
const message = $('#messageContent').val().trim();
const messageType = $('#messageType').val();
const subject = $('#subject-content').val().trim();
if (!message) {
Swal.fire({
@ -271,6 +274,16 @@
return;
}
if (messageType == 'all' || messageType == 'inbox' || messageType == 'email' && !subject) {
Swal.fire({
title: 'Error!',
text: "{Lang::T('Please enter a subject for the message.')}",
icon: 'error',
confirmButtonText: 'OK'
});
return;
}
// Disable the button and show loading text
$(this).prop('disabled', true).text('{Lang::T('Sending...')}');
@ -332,4 +345,31 @@
});
});
</script>
<script>
document.getElementById('messageType').addEventListener('change', function () {
const messageType = this.value;
const subjectField = document.getElementById('subject-content');
subjectField.style.display = (messageType === 'all' || messageType === 'email' || messageType === 'inbox') ? 'block' : 'none';
switch (messageType) {
case 'all':
subjectField.placeholder = 'Enter a subject for all channels';
subjectField.required = true;
break;
case 'email':
subjectField.placeholder = 'Enter a subject for email';
subjectField.required = true;
break;
case 'inbox':
subjectField.placeholder = 'Enter a subject for inbox';
subjectField.required = true;
break;
default:
subjectField.placeholder = 'Enter message subject here';
subjectField.required = false;
break;
}
});
</script>
{include file = "sections/footer.tpl" }

View File

@ -14,6 +14,7 @@
<link rel="stylesheet" href="{$app_url}/ui/ui/styles/bootstrap.min.css">
<link rel="stylesheet" href="{$app_url}/ui/ui/fonts/ionicons/css/ionicons.min.css">
<link rel="stylesheet" href="{$app_url}/ui/ui/fonts/font-awesome/css/font-awesome.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link rel="stylesheet" href="{$app_url}/ui/ui/styles/modern-AdminLTE.min.css">
<link rel="stylesheet" href="{$app_url}/ui/ui/styles/select2.min.css" />
<link rel="stylesheet" href="{$app_url}/ui/ui/styles/select2-bootstrap.min.css" />
@ -77,8 +78,8 @@
<ul class="dropdown-menu">
<li class="user-header">
<img src="{$app_url}/{$UPLOAD_PATH}{$_admin['photo']}.thumb.jpg"
onerror="this.src='{$app_url}/{$UPLOAD_PATH}/admin.default.png'" class="img-circle"
alt="Avatar">
onerror="this.src='{$app_url}/{$UPLOAD_PATH}/admin.default.png'"
class="img-circle" alt="Avatar">
<p>
{$_admin['fullname']}
<small>{Lang::T($_admin['user_type'])}</small>
@ -173,7 +174,8 @@
href="{Text::url('services/hotspot')}">Hotspot</a></li>
<li {if $_routes[1] eq 'pppoe' }class="active" {/if}><a
href="{Text::url('services/pppoe')}">PPPOE</a></li>
<li {if $_routes[1] eq 'vpn' }class="active" {/if}><a href="{Text::url('services/vpn')}">VPN</a>
<li {if $_routes[1] eq 'vpn' }class="active" {/if}><a
href="{Text::url('services/vpn')}">VPN</a>
</li>
<li {if $_routes[1] eq 'list' }class="active" {/if}><a
href="{Text::url('bandwidth/list')}">Bandwidth</a></li>
@ -215,6 +217,8 @@
href="{Text::url('reports')}">{Lang::T('Daily Reports')}</a></li>
<li {if $_routes[1] eq 'activation' }class="active" {/if}><a
href="{Text::url('reports/activation')}">{Lang::T('Activation History')}</a></li>
{* <li {if $_routes[0] eq 'invoices' }class="active" {/if}><a
href="{Text::url('invoices')}">{Lang::T('Invoices')}</a></li> *}
{$_MENU_REPORTS}
</ul>
</li>
@ -285,16 +289,19 @@
<li {if $_routes[1] eq 'Announcement' }class="active" {/if}><a
href="{Text::url('pages/Announcement')}">{Lang::T('Announcement')}</a></li>
<li {if $_routes[1] eq 'Announcement_Customer' }class="active" {/if}><a
href="{Text::url('pages/Announcement_Customer')}">{Lang::T('Customer Announcement')}</a>
href="{Text::url('pages/Announcement_Customer')}">{Lang::T('Customer
Announcement')}</a>
</li>
<li {if $_routes[1] eq 'Registration_Info' }class="active" {/if}><a
href="{Text::url('pages/Registration_Info')}">{Lang::T('Registration Info')}</a></li>
href="{Text::url('pages/Registration_Info')}">{Lang::T('Registration Info')}</a>
</li>
<li {if $_routes[1] eq 'Payment_Info' }class="active" {/if}><a
href="{Text::url('pages/Payment_Info')}">{Lang::T('Payment Info')}</a></li>
<li {if $_routes[1] eq 'Privacy_Policy' }class="active" {/if}><a
href="{Text::url('pages/Privacy_Policy')}">{Lang::T('Privacy Policy')}</a></li>
<li {if $_routes[1] eq 'Terms_and_Conditions' }class="active" {/if}><a
href="{Text::url('pages/Terms_and_Conditions')}">{Lang::T('Terms and Conditions')}</a></li>
href="{Text::url('pages/Terms_and_Conditions')}">{Lang::T('Terms and
Conditions')}</a></li>
{$_MENU_PAGES}
</ul>
</li>
@ -373,7 +380,8 @@
{$_MENU_AFTER_LOGS}
{if in_array($_admin['user_type'],['SuperAdmin','Admin'])}
<li {if $_routes[1] eq 'docs' }class="active" {/if}>
<a href="{if $_c['docs_clicked'] != 'yes'}{Text::url('settings/docs')}{else}{$app_url}/docs{/if}">
<a
href="{if $_c['docs_clicked'] != 'yes'}{Text::url('settings/docs')}{else}{$app_url}/docs{/if}">
<i class="ion ion-ios-bookmarks"></i>
<span class="text">{Lang::T('Documentation')}</span>
{if $_c['docs_clicked'] != 'yes'}
@ -426,4 +434,4 @@
}
});
</script>
{/if}
{/if}

View File

@ -0,0 +1,57 @@
{include file="sections/header.tpl"}
<!-- Add a Table for Sent History -->
<div class="panel panel-default">
<div class="panel-heading">{Lang::T('Invoices')}</div>
<div class="panel-body" style="overflow: auto;">
<table class="table table-bordered" id="invoiceTable" style="width:100%">
<thead>
<tr>
<th>{Lang::T('Invoice No')}</th>
<th>{Lang::T('Customer Name')}</th>
<th>{Lang::T('Email')}</th>
<th>{Lang::T('Address')}</th>
<th>{Lang::T('Amount')}</th>
<th>{Lang::T('Status')}</th>
<th>{Lang::T('Created Date')}</th>
<th>{Lang::T('Due Date')}</th>
<th>{Lang::T('Actions')}</th>
</tr>
</thead>
<tbody>
{foreach $invoices as $invoice}
<tr>
<td>{$invoice->number}</td>
<td>{$invoice->fullname}</td>
<td>{$invoice->email}</td>
<td>{$invoice->address}</td>
<td>{$invoice->amount}</td>
<td>
{if $invoice->status == 'paid'}
<span class="label label-success">{Lang::T('Paid')}</span>
{elseif $invoice->status == 'unpaid'}
<span class="label label-danger">{Lang::T('Unpaid')}</span>
{else}
<span class="label label-warning">{Lang::T('Pending')}</span>
{/if}
</td>
<td>{$invoice->created_at}</td>
<td>{$invoice->due_date}</td>
<td>
<a href="{$app_url}/system/uploads/invoices/{$invoice->filename}" class="btn btn-primary btn-xs">{Lang::T('View')}</a>
<!-- <a href="javascript:void(0);" class="btn btn-danger btn-xs" onclick="deleteInvoice({$invoice->id});">{Lang::T('Delete')}</a>
<a href="javascript:void(0);" class="btn btn-success btn-xs" onclick="sendInvoice({$invoice->id});">{Lang::T('Send')}</a> -->
</td>
</tr>
{/foreach}
</tbody>
</table>
</div>
</div>
<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>
new DataTable('#invoiceTable');
</script>
{include file="sections/footer.tpl"}

View File

@ -30,7 +30,8 @@
<select class="form-control" name="service" id="service">
<option value="all" {if $group=='all' }selected{/if}>{Lang::T('All')}</option>
<option value="PPPoE" {if $service=='PPPoE' }selected{/if}>{Lang::T('PPPoE')}</option>
<option value="Hotspot" {if $service=='Hotspot' }selected{/if}>{Lang::T('Hotspot')}</option>
<option value="Hotspot" {if $service=='Hotspot' }selected{/if}>{Lang::T('Hotspot')}
</option>
<option value="VPN" {if $service=='VPN' }selected{/if}>{Lang::T('VPN')}</option>
</select>
</div>
@ -41,8 +42,10 @@
<select class="form-control" name="group" id="group">
<option value="all" {if $group=='all' }selected{/if}>{Lang::T('All Customers')}</option>
<option value="new" {if $group=='new' }selected{/if}>{Lang::T('New Customers')}</option>
<option value="expired" {if $group=='expired' }selected{/if}>{Lang::T('Expired Customers')}</option>
<option value="active" {if $group=='active' }selected{/if}>{Lang::T('Active Customers')}</option>
<option value="expired" {if $group=='expired' }selected{/if}>{Lang::T('Expired
Customers')}</option>
<option value="active" {if $group=='active' }selected{/if}>{Lang::T('Active Customers')}
</option>
</select>
</div>
</div>
@ -50,9 +53,13 @@
<label class="col-md-2 control-label">{Lang::T('Send Via')}</label>
<div class="col-md-6">
<select class="form-control" name="via" id="via">
<option value="all" {if $via=='all' }selected{/if}>{Lang::T('All Channels')}</option>
<option value="inbox" {if $via=='inbox' }selected{/if}>{Lang::T('Inbox')}</option>
<option value="email" {if $via=='email' }selected{/if}>{Lang::T('Email')}</option>
<option value="sms" {if $via=='sms' }selected{/if}>{Lang::T('SMS')}</option>
<option value="wa" {if $via=='wa' }selected{/if}>{Lang::T('WhatsApp')}</option>
<option value="both" {if $via=='both' }selected{/if}>{Lang::T('SMS and WhatsApp')}</option>
<option value="both" {if $via=='both' }selected{/if}>{Lang::T('SMS and WhatsApp')}
</option>
</select>
</div>
</div>
@ -72,10 +79,21 @@
{Lang::T('Use 20 and above if you are sending to all customers to avoid server time out')}
</div>
</div>
<div class="form-group" id="subject-content">
<label class="col-md-2 control-label">{Lang::T('Subject')}</label>
<div class="col-md-6">
<input type="text" class="form-control" name="subject" id="subject" value=""
placeholder="{Lang::T('Enter message subject here')}">
</div>
<p class="help-block col-md-4">
{Lang::T('You can also use the below placeholders here too')}.
</p>
</div>
<div class="form-group">
<label class="col-md-2 control-label">{Lang::T('Message')}</label>
<div class="col-md-6">
<textarea class="form-control" id="message" name="message" required placeholder="{Lang::T('Compose your message...')}" rows="5">{$message}</textarea>
<textarea class="form-control" id="message" name="message" required
placeholder="{Lang::T('Compose your message...')}" rows="5">{$message}</textarea>
<input name="test" id="test" type="checkbox">
{Lang::T('Testing [if checked no real message is sent]')}
</div>
@ -93,7 +111,8 @@
</div>
<div class="form-group">
<div class="col-lg-offset-2 col-lg-10">
<button type="button" id="startBulk" class="btn btn-primary">{Lang::T('Start Bulk Messaging')}</button>
<button type="button" id="startBulk" class="btn btn-primary">{Lang::T('Start Bulk
Messaging')}</button>
<a href="{Text::url('dashboard')}" class="btn btn-default">{Lang::T('Cancel')}</a>
</div>
</div>
@ -112,7 +131,7 @@
<thead>
<tr>
<th>{Lang::T('Customer')}</th>
<th>{Lang::T('Phone')}</th>
<th>{Lang::T('Channel')}</th>
<th>{Lang::T('Status')}</th>
<th>{Lang::T('Message')}</th>
<th>{Lang::T('Router')}</th>
@ -126,6 +145,34 @@
<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>
document.getElementById('via').addEventListener('change', function () {
const via = this.value;
const subject = document.getElementById('subject-content');
const subjectField = document.getElementById('subject');
subject.style.display = (via === 'all' || via === 'email' || via === 'inbox') ? 'block' : 'none';
switch (via) {
case 'all':
subjectField.placeholder = 'Enter a subject for all channels';
subjectField.required = true;
break;
case 'email':
subjectField.placeholder = 'Enter a subject for email';
subjectField.required = true;
break;
case 'inbox':
subjectField.placeholder = 'Enter a subject for inbox';
subjectField.required = true;
break;
default:
subjectField.placeholder = 'Enter message subject here';
subjectField.required = false;
break;
}
});
</script>
{literal}
<script>
let page = 0;
@ -158,6 +205,7 @@
page: page,
test: $('#test').is(':checked') ? 'on' : 'off',
service: $('#service').val(),
subject: $('#subject').val(),
},
dataType: 'json',
beforeSend: function () {
@ -186,7 +234,7 @@
let statusClass = msg.status.includes('Failed') ? 'danger' : 'success';
historyTable.row.add([
msg.name,
msg.phone,
msg.channel,
`<span class="text-${statusClass}">${msg.status}</span>`,
msg.message || 'No message',
msg.router ? msg.router : 'All Router',
@ -207,7 +255,7 @@
console.error("Unexpected response format:", response);
$('#status').html(`
<div class="alert alert-danger">
<i class="fas fa-exclamation-circle"></i> Error: Unexpected response format.
<i class="fas fa-exclamation-circle"></i> Error: ${response.message}
</div>
`);
}

View File

@ -23,11 +23,25 @@
<label class="col-md-2 control-label">{Lang::T('Send Via')}</label>
<div class="col-md-6">
<select class="form-control" name="via" id="via">
<option value="sms" selected> {Lang::T('via SMS')}</option>
<option value="wa"> {Lang::T('Via WhatsApp')}</option>
<option value="both"> {Lang::T('Via WhatsApp and SMS')}</option>
<option value="all" {if $via=='all' }selected{/if}>{Lang::T('All Channels')}</option>
<option value="inbox" {if $via=='inbox' }selected{/if}>{Lang::T('Inbox')}</option>
<option value="email" {if $via=='email' }selected{/if}>{Lang::T('Email')}</option>
<option value="sms" {if $via=='sms' }selected{/if}>{Lang::T('SMS')}</option>
<option value="wa" {if $via=='wa' }selected{/if}>{Lang::T('WhatsApp')}</option>
<option value="both" {if $via=='both' }selected{/if}>{Lang::T('SMS and WhatsApp')}
</option>
</select>
</div>
</div>
<div class="form-group" id="subject">
<label class="col-md-2 control-label">{Lang::T('Subject')}</label>
<div class="col-md-6">
<input type="text" class="form-control" name="subject" id="subject-content" value=""
placeholder="{Lang::T('Enter message subject here')}">
</div>
<p class="help-block col-md-4">
{Lang::T('You can also use the below placeholders here too')}.
</p>
</div>
<div class="form-group">
<label class="col-md-2 control-label">{Lang::T('Message')}</label>
@ -64,6 +78,33 @@
</div>
</div>
</div>
<script>
document.getElementById('via').addEventListener('change', function () {
const via = this.value;
const subject = document.getElementById('subject');
const subjectField = document.getElementById('subject-content');
subject.style.display = (via === 'all' || via === 'email' || via === 'inbox') ? 'block' : 'none';
switch (via) {
case 'all':
subjectField.placeholder = 'Enter a subject for all channels';
subjectField.required = true;
break;
case 'email':
subjectField.placeholder = 'Enter a subject for email';
subjectField.required = true;
break;
case 'inbox':
subjectField.placeholder = 'Enter a subject for inbox';
subjectField.required = true;
break;
default:
subjectField.placeholder = 'Enter message subject here';
subjectField.required = false;
break;
}
});
</script>
{include file="sections/footer.tpl"}

View File

@ -26,6 +26,7 @@
<div class="table-responsive">
<table class="table table-bordered table-condensed table-striped " style="background: #ffffff">
<th class="text-center">{Lang::T('Username')}</th>
<th class="text-center">{Lang::T('Fullname')}</th>
<th class="text-center">{Lang::T('Plan Name')}</th>
<th class="text-center">{Lang::T('Type')}</th>
<th class="text-center">{Lang::T('Plan Price')}</th>
@ -36,6 +37,7 @@
{foreach $d as $ds}
<tr>
<td>{$ds['username']}</td>
<td>{$ds['fullname']}</td>
<td class="text-center">{$ds['plan_name']}</td>
<td class="text-center">{$ds['type']}</td>
<td class="text-right">{Lang::moneyFormat($ds['price'])}</td>

View File

@ -33,6 +33,7 @@
<tr>
<th>{Lang::T('Invoice')}</th>
<th>{Lang::T('Username')}</th>
<th>{Lang::T('Fullname')}</th>
<th>{Lang::T('Plan Name')}</th>
<th>{Lang::T('Plan Price')}</th>
<th>{Lang::T('Type')}</th>
@ -48,6 +49,7 @@
style="cursor:pointer;">{$ds['invoice']}</td>
<td onclick="window.location.href = '{Text::url('')}customers/viewu/{$ds['username']}'"
style="cursor:pointer;">{$ds['username']}</td>
<td>{$ds['fullname']}</td>
<td>{$ds['plan_name']}</td>
<td>{Lang::moneyFormat($ds['price'])}</td>
<td>{$ds['type']}</td>

View File

@ -94,10 +94,11 @@
<a href="{Text::url('export/pdf-by-date&')}{$filter}" class="btn btn-default"><i
class="fa fa-file-pdf-o"></i></a>
</th>
<th colspan="7"></th>
<th colspan="8"></th>
</tr>
<tr>
<th>{Lang::T('Username')}</th>
<th>{Lang::T('Fullname')}</th>
<th>{Lang::T('Type')}</th>
<th>{Lang::T('Plan Name')}</th>
<th>{Lang::T('Plan Price')}</th>
@ -111,6 +112,7 @@
{foreach $d as $ds}
<tr>
<td>{$ds['username']}</td>
<td>{$ds['fullname']}</td>
<td>{$ds['type']}</td>
<td>{$ds['plan_name']}</td>
<td class="text-right">{Lang::moneyFormat($ds['price'])}</td>
@ -122,9 +124,8 @@
{/foreach}
<tr>
<th>{Lang::T('Total')}</th>
<td colspan="2"></td>
<th class="text-right">{Lang::moneyFormat($dr)}</th>
<td colspan="4"></td>
<td colspan="7"></td>
</tr>
</tbody>
</table>

View File

@ -37,6 +37,7 @@
<thead>
<tr>
<th>{Lang::T('Username')}</th>
<th>{Lang::T('Fullname')}</th>
<th>{Lang::T('Type')}</th>
<th>{Lang::T('Plan Name')}</th>
<th>{Lang::T('Plan Price')}</th>
@ -50,6 +51,7 @@
{foreach $d as $ds}
<tr>
<td>{$ds['username']}</td>
<td>{$ds['fullname']}</td>
<td>{$ds['type']}</td>
<td>{$ds['plan_name']}</td>
<td class="text-right">{Lang::moneyFormat($ds['price'])}</td>

View File

@ -155,7 +155,7 @@
<h3 class="panel-title">
<a role="button" data-toggle="collapse" data-parent="#accordion" href="#collapseLoginPage"
aria-expanded="true" aria-controls="collapseLoginPage">
{Lang::T('Customer Login Page Settings')}
{Lang::T('Customer Login Page')}
</a>
</h3>
</div>
@ -254,6 +254,37 @@
</div>
</div>
<div class="panel">
<div class="panel-heading" role="tab" id="Coupon">
<h4 class="panel-title">
<a class="collapsed" role="button" data-toggle="collapse" data-parent="#accordion"
href="#collapseCoupon" aria-expanded="false" aria-controls="collapseCoupon">
{Lang::T('Coupons')}
</a>
</h4>
</div>
<div id="collapseCoupon" class="panel-collapse collapse" role="tabpanel">
<div class="panel-body">
<div class="form-group">
<label class="col-md-3 control-label">{Lang::T('Enable Coupon')}</label>
<div class="col-md-5">
<select name="enable_coupons" id="enable_coupons" class="form-control text-muted">
<option value="no">{Lang::T('No')}</option>
<option value="yes" {if $_c['enable_coupons'] == 'yes'}selected="selected" {/if}>{Lang::T('Yes')}
</option>
</select>
</div>
<p class="help-block col-md-4">
<small>{Lang::T('Enable or disable coupons')}</small>
</p>
</div>
<button class="btn btn-success btn-block" type="submit">
{Lang::T('Save Changes')}
</button>
</div>
</div>
</div>
<div class="panel">
<div class="panel-heading" role="tab" id="Registration">
<h4 class="panel-title">

View File

@ -1,16 +1,16 @@
</section>
</div>
{if isset($_c['CompanyFooter'])}
<footer class="main-footer">
<footer class="main-footer">
{$_c['CompanyFooter']}
<div class="pull-right">
<a href="javascript:showPrivacy()">Privacy</a>
&bull;
<a href="javascript:showTaC()">T &amp; C</a>
</div>
</footer>
</footer>
{else}
<footer class="main-footer">
<footer class="main-footer">
PHPNuxBill by <a href="https://github.com/hotspotbilling/phpnuxbill" rel="nofollow noreferrer noopener"
target="_blank">iBNuX</a>, Theme by <a href="https://adminlte.io/" rel="nofollow noreferrer noopener"
target="_blank">AdminLTE</a>
@ -19,7 +19,7 @@
&bull;
<a href="javascript:showTaC()">T &amp; C</a>
</div>
</footer>
</footer>
{/if}
</div>
@ -50,22 +50,22 @@
<script src="{$app_url}/ui/ui/scripts/custom.js?2025.2.5"></script>
{if isset($xfooter)}
{$xfooter}
{$xfooter}
{/if}
{if $_c['tawkto'] != ''}
<!--Start of Tawk.to Script-->
<script type="text/javascript">
<!--Start of Tawk.to Script-->
<script type="text/javascript">
var isLoggedIn = false;
var Tawk_API = {
onLoad: function() {
onLoad: function () {
Tawk_API.setAttributes({
'username' : '{$_user['username']}',
'service' : '{$_user['service_type']}',
'balance' : '{$_user['balance']}',
'account' : '{$_user['account_type']}',
'phone' : '{$_user['phonenumber']}'
}, function(error) {
'username': '{$_user['username']}',
'service': '{$_user['service_type']}',
'balance': '{$_user['balance']}',
'account': '{$_user['account_type']}',
'phone': '{$_user['phonenumber']}'
}, function (error) {
console.log(error)
});
@ -77,7 +77,7 @@
email: '{$_user['email']}',
phone: '{$_user['phonenumber']}'
};
(function() {
(function () {
var s1 = document.createElement("script"),
s0 = document.getElementsByTagName("script")[0];
s1.async = true;
@ -86,11 +86,11 @@
s1.setAttribute('crossorigin', '*');
s0.parentNode.insertBefore(s1, s0);
})();
</script>
<!--End of Tawk.to Script-->
{/if}
</script>
<!--End of Tawk.to Script-->
{/if}
<script>
<script>
const toggleIcon = document.getElementById('toggleIcon');
const body = document.body;
const savedMode = localStorage.getItem('mode');
@ -118,22 +118,22 @@
localStorage.setItem('mode', 'dark');
}
});
</script>
</script>
{literal}
<script>
<script>
var listAtts = document.querySelectorAll(`[api-get-text]`);
listAtts.forEach(function(el) {
$.get(el.getAttribute('api-get-text'), function(data) {
listAtts.forEach(function (el) {
$.get(el.getAttribute('api-get-text'), function (data) {
el.innerHTML = data;
});
});
$(document).ready(function() {
$(document).ready(function () {
var listAtts = document.querySelectorAll(`button[type="submit"]`);
listAtts.forEach(function(el) {
listAtts.forEach(function (el) {
if (el.addEventListener) { // all browsers except IE before version 9
el.addEventListener("click", function() {
el.addEventListener("click", function () {
$(this).html(
`<span class="loading"></span>`
);
@ -143,7 +143,7 @@
}, false);
} else {
if (el.attachEvent) { // IE before version 9
el.attachEvent("click", function() {
el.attachEvent("click", function () {
$(this).html(
`<span class="loading"></span>`
);
@ -153,28 +153,47 @@
});
}
}
$(function() {
$(function () {
$('[data-toggle="tooltip"]').tooltip()
})
});
});
function ask(field, text){
var txt = field.innerHTML;
if (confirm(text)) {
function ask(field, text) {
const txt = field.innerHTML;
field.innerHTML = `<span class="loading"></span>`;
field.setAttribute("disabled", true);
Swal.fire({
title: 'Are you sure?',
text: text,
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Yes, proceed',
cancelButtonText: 'Cancel',
}).then((result) => {
let delay = result.isConfirmed ? 400 : 500;
setTimeout(() => {
field.innerHTML = field.innerHTML.replace(`<span class="loading"></span>`, txt);
field.innerHTML = txt;
field.removeAttribute("disabled");
}, 5000);
return true;
if (result.isConfirmed) {
const form = field.closest('form');
if (form) {
form.submit(); // manually submit the form
} else {
setTimeout(() => {
field.innerHTML = field.innerHTML.replace(`<span class="loading"></span>`, txt);
field.removeAttribute("disabled");
}, 500);
return false;
//fallback if not in a form
const href = field.getAttribute("href") || field.dataset.href;
if (href) window.location.href = href;
}
}
}, delay);
});
return false;
}
function setCookie(name, value, days) {
var expires = "";
@ -196,10 +215,10 @@
}
return null;
}
</script>
</script>
{/literal}
<script>
setCookie('user_language', '{$user_language}', 365);
setCookie('user_language', '{$user_language}', 365);
</script>
</body>