feat: add message logging functionality with CSV export and management
This commit is contained in:
parent
43b1025d3c
commit
4e3d89a23c
@ -289,6 +289,16 @@ CREATE TABLE IF NOT EXISTS `tbl_widgets` (
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
CREATE TABLE 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;
|
||||
|
||||
ALTER TABLE `rad_acct`
|
||||
ADD PRIMARY KEY (`id`),
|
||||
ADD KEY `username` (`username`),
|
||||
|
@ -44,23 +44,31 @@ class Message
|
||||
try {
|
||||
foreach ($txts as $txt) {
|
||||
self::sendSMS($config['sms_url'], $phone, $txt);
|
||||
self::logMessage('SMS', $phone, $txt, 'Success');
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
} catch (Throwable $e) {
|
||||
// ignore, add to logs
|
||||
_log("Failed to send SMS using Mikrotik.\n" . $e->getMessage(), 'SMS', 0);
|
||||
self::logMessage('SMS', $phone, $txt, 'Error', $e->getMessage());
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
self::MikrotikSendSMS($config['sms_url'], $phone, $txt);
|
||||
} catch (Exception $e) {
|
||||
self::logMessage('MikroTikSMS', $phone, $txt, 'Success');
|
||||
} catch (Throwable $e) {
|
||||
// ignore, add to logs
|
||||
_log("Failed to send SMS using Mikrotik.\n" . $e->getMessage(), 'SMS', 0);
|
||||
self::logMessage('MikroTikSMS', $phone, $txt, 'Error', $e->getMessage());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$smsurl = str_replace('[number]', urlencode($phone), $config['sms_url']);
|
||||
$smsurl = str_replace('[text]', urlencode($txt), $smsurl);
|
||||
return Http::getData($smsurl);
|
||||
try {
|
||||
$response = Http::getData($smsurl);
|
||||
self::logMessage('SMS HTTP Response', $phone, $txt, 'Success', $response);
|
||||
return $response;
|
||||
} catch (Throwable $e) {
|
||||
self::logMessage('SMS HTTP Request', $phone, $txt, 'Error', $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -92,11 +100,20 @@ class Message
|
||||
if (empty($txt)) {
|
||||
return "kosong";
|
||||
}
|
||||
run_hook('send_whatsapp', [$phone, $txt]); #HOOK
|
||||
|
||||
run_hook('send_whatsapp', [$phone, $txt]); // HOOK
|
||||
|
||||
if (!empty($config['wa_url'])) {
|
||||
$waurl = str_replace('[number]', urlencode(Lang::phoneFormat($phone)), $config['wa_url']);
|
||||
$waurl = str_replace('[text]', urlencode($txt), $waurl);
|
||||
return Http::getData($waurl);
|
||||
|
||||
try {
|
||||
$response = Http::getData($waurl);
|
||||
self::logMessage('WhatsApp HTTP Response', $phone, $txt, 'Success', $response);
|
||||
return $response;
|
||||
} catch (Throwable $e) {
|
||||
self::logMessage('WhatsApp HTTP Request', $phone, $txt, 'Error', $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -110,6 +127,7 @@ class Message
|
||||
return "";
|
||||
}
|
||||
run_hook('send_email', [$to, $subject, $body]); #HOOK
|
||||
self::logMessage('Email', $to, $body, 'Success');
|
||||
if (empty($config['smtp_host'])) {
|
||||
$attr = "";
|
||||
if (!empty($config['mail_from'])) {
|
||||
@ -125,12 +143,12 @@ class Message
|
||||
if (isset($debug_mail) && $debug_mail == 'Dev') {
|
||||
$mail->SMTPDebug = SMTP::DEBUG_SERVER;
|
||||
}
|
||||
$mail->Host = $config['smtp_host'];
|
||||
$mail->SMTPAuth = true;
|
||||
$mail->Username = $config['smtp_user'];
|
||||
$mail->Password = $config['smtp_pass'];
|
||||
$mail->Host = $config['smtp_host'];
|
||||
$mail->SMTPAuth = true;
|
||||
$mail->Username = $config['smtp_user'];
|
||||
$mail->Password = $config['smtp_pass'];
|
||||
$mail->SMTPSecure = $config['smtp_ssltls'];
|
||||
$mail->Port = $config['smtp_port'];
|
||||
$mail->Port = $config['smtp_port'];
|
||||
if (!empty($config['mail_from'])) {
|
||||
$mail->setFrom($config['mail_from']);
|
||||
}
|
||||
@ -154,13 +172,16 @@ class Message
|
||||
$html = str_replace('[[Company_Name]]', nl2br($config['CompanyName']), $html);
|
||||
$html = str_replace('[[Body]]', nl2br($body), $html);
|
||||
$mail->isHTML(true);
|
||||
$mail->Body = $html;
|
||||
$mail->Body = $html;
|
||||
} else {
|
||||
$mail->isHTML(false);
|
||||
$mail->Body = $body;
|
||||
$mail->Body = $body;
|
||||
}
|
||||
if (!$mail->send()) {
|
||||
_log(Lang::T("Email not sent, Mailer Error: ") . $mail->ErrorInfo);
|
||||
$errorMessage = Lang::T("Email not sent, Mailer Error: ") . $mail->ErrorInfo;
|
||||
self::logMessage('Email', $to, $body, 'Error', $errorMessage);
|
||||
} else {
|
||||
self::logMessage('Email', $to, $body, 'Success');
|
||||
}
|
||||
|
||||
//<p style="font-family: Helvetica, sans-serif; font-size: 16px; font-weight: normal; margin: 0; margin-bottom: 16px;">
|
||||
@ -198,7 +219,7 @@ class Message
|
||||
$tax_enable = isset($config['enable_tax']) ? $config['enable_tax'] : 'no';
|
||||
if ($tax_enable === 'yes') {
|
||||
$tax_rate_setting = isset($config['tax_rate']) ? $config['tax_rate'] : null;
|
||||
$custom_tax_rate = isset($config['custom_tax_rate']) ? (float)$config['custom_tax_rate'] : null;
|
||||
$custom_tax_rate = isset($config['custom_tax_rate']) ? (float) $config['custom_tax_rate'] : null;
|
||||
|
||||
$tax_rate = ($tax_rate_setting === 'custom') ? $custom_tax_rate : $tax_rate_setting;
|
||||
$tax = Package::tax($price, $tax_rate);
|
||||
@ -295,7 +316,7 @@ class Message
|
||||
$textInvoice = str_replace('[[payment_channel]]', trim($gc[1]), $textInvoice);
|
||||
$textInvoice = str_replace('[[type]]', $trx['type'], $textInvoice);
|
||||
$textInvoice = str_replace('[[plan_name]]', $trx['plan_name'], $textInvoice);
|
||||
$textInvoice = str_replace('[[plan_price]]', Lang::moneyFormat($trx['price']), $textInvoice);
|
||||
$textInvoice = str_replace('[[plan_price]]', Lang::moneyFormat($trx['price']), $textInvoice);
|
||||
$textInvoice = str_replace('[[name]]', $cust['fullname'], $textInvoice);
|
||||
$textInvoice = str_replace('[[note]]', $cust['note'], $textInvoice);
|
||||
$textInvoice = str_replace('[[user_name]]', $trx['username'], $textInvoice);
|
||||
@ -317,12 +338,30 @@ class Message
|
||||
|
||||
public static function addToInbox($to_customer_id, $subject, $body, $from = 'System')
|
||||
{
|
||||
$v = ORM::for_table('tbl_customers_inbox')->create();
|
||||
$v->from = $from;
|
||||
$v->customer_id = $to_customer_id;
|
||||
$v->subject = $subject;
|
||||
$v->date_created = date('Y-m-d H:i:s');
|
||||
$v->body = nl2br($body);
|
||||
$v->save();
|
||||
$user = User::find($to_customer_id);
|
||||
try {
|
||||
$v = ORM::for_table('tbl_customers_inbox')->create();
|
||||
$v->from = $from;
|
||||
$v->customer_id = $to_customer_id;
|
||||
$v->subject = $subject;
|
||||
$v->date_created = date('Y-m-d H:i:s');
|
||||
$v->body = nl2br($body);
|
||||
$v->save();
|
||||
self::logMessage("Inbox", $user->username, $body, "Success");
|
||||
} catch (Throwable $e) {
|
||||
$errorMessage = Lang::T("Error adding message to inbox: " . $e->getMessage());
|
||||
self::logMessage('Inbox', $user->username, $body, 'Error', $errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
public static function logMessage($messageType, $recipient, $messageContent, $status, $errorMessage = null)
|
||||
{
|
||||
$log = ORM::for_table('tbl_message_logs')->create();
|
||||
$log->message_type = $messageType;
|
||||
$log->recipient = $recipient;
|
||||
$log->message_content = $messageContent;
|
||||
$log->status = $status;
|
||||
$log->error_message = $errorMessage;
|
||||
$log->save();
|
||||
}
|
||||
}
|
||||
|
@ -320,4 +320,10 @@ class User
|
||||
}
|
||||
return $html;
|
||||
}
|
||||
public static function find($id)
|
||||
{
|
||||
return ORM::for_table('tbl_customers')->find_one($id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -80,6 +80,39 @@ switch ($action) {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'message-csv':
|
||||
$logs = ORM::for_table('tbl_message_logs')
|
||||
->select('id')
|
||||
->select('message_type')
|
||||
->select('recipient')
|
||||
->select('message_content')
|
||||
->select('status')
|
||||
->select('error_message')
|
||||
->select('sent_at')
|
||||
->order_by_asc('id')->find_array();
|
||||
$h = false;
|
||||
set_time_limit(-1);
|
||||
header('Pragma: public');
|
||||
header('Expires: 0');
|
||||
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
|
||||
header("Content-type: text/csv");
|
||||
header('Content-Disposition: attachment;filename="message-logs_' . date('Y-m-d_H_i') . '.csv"');
|
||||
header('Content-Transfer-Encoding: binary');
|
||||
foreach ($logs as $log) {
|
||||
$ks = [];
|
||||
$vs = [];
|
||||
foreach ($log as $k => $v) {
|
||||
$ks[] = $k;
|
||||
$vs[] = $v;
|
||||
}
|
||||
if (!$h) {
|
||||
echo '"' . implode('";"', $ks) . "\"\n";
|
||||
$h = true;
|
||||
}
|
||||
echo '"' . implode('";"', $vs) . "\"\n";
|
||||
}
|
||||
break;
|
||||
|
||||
case 'list':
|
||||
$q = (_post('q') ? _post('q') : _get('q'));
|
||||
$keep = _post('keep');
|
||||
@ -119,6 +152,33 @@ switch ($action) {
|
||||
$ui->display('admin/logs/radius.tpl');
|
||||
break;
|
||||
|
||||
case 'message':
|
||||
$q = _post('q') ?: _get('q');
|
||||
$keep = (int) _post('keep');
|
||||
if (!empty($keep)) {
|
||||
ORM::raw_execute("DELETE FROM tbl_message_logs WHERE UNIX_TIMESTAMP(sent_at) < UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL $keep DAY))");
|
||||
r2(getUrl('logs/message/'), 's', "Deleted logs older than $keep days");
|
||||
}
|
||||
|
||||
if ($q !== null && $q !== '') {
|
||||
$query = ORM::for_table('tbl_message_logs')
|
||||
->whereRaw("message_type LIKE '%$q%' OR recipient LIKE '%$q%' OR message_content LIKE '%$q%' OR status LIKE '%$q%' OR error_message LIKE '%$q%'")
|
||||
->order_by_desc('sent_at');
|
||||
$d = Paginator::findMany($query, ['q' => $q]);
|
||||
} else {
|
||||
$query = ORM::for_table('tbl_message_logs')->order_by_desc('sent_at');
|
||||
$d = Paginator::findMany($query);
|
||||
}
|
||||
|
||||
if ($d) {
|
||||
$ui->assign('d', $d);
|
||||
} else {
|
||||
$ui->assign('d', []);
|
||||
}
|
||||
|
||||
$ui->assign('q', $q);
|
||||
$ui->display('admin/logs/message.tpl');
|
||||
break;
|
||||
|
||||
default:
|
||||
r2(getUrl('logs/list/'), 's', '');
|
||||
|
@ -203,5 +203,8 @@
|
||||
"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" : [
|
||||
"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;"
|
||||
]
|
||||
}
|
@ -364,6 +364,8 @@
|
||||
href="{Text::url('logs/radius')}">Radius</a>
|
||||
</li>
|
||||
{/if}
|
||||
<li {if $_routes[1] eq 'message' }class="active" {/if}><a
|
||||
href="{Text::url('logs/message')}">Message</a></li>
|
||||
{$_MENU_LOGS}
|
||||
</ul>
|
||||
</li>
|
||||
|
169
ui/ui/admin/logs/message.tpl
Normal file
169
ui/ui/admin/logs/message.tpl
Normal file
@ -0,0 +1,169 @@
|
||||
{include file="sections/header.tpl"}
|
||||
<style>
|
||||
/* Styles for overall layout and responsiveness */
|
||||
body {
|
||||
background-color: #f8f9fa;
|
||||
font-family: 'Arial', sans-serif;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin-top: 20px;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.table th {
|
||||
vertical-align: middle;
|
||||
border-color: #dee2e6;
|
||||
background-color: #343a40;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.table td {
|
||||
vertical-align: middle;
|
||||
border-color: #dee2e6;
|
||||
}
|
||||
|
||||
.table-striped tbody tr:nth-of-type(odd) {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.badge {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.3s, color 0.3s;
|
||||
}
|
||||
|
||||
.badge-danger {
|
||||
color: #721c24;
|
||||
background-color: #f8d7da;
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
color: #155724;
|
||||
background-color: #d4edda;
|
||||
}
|
||||
|
||||
.badge-warning {
|
||||
color: #856404;
|
||||
background-color: #ffeeba;
|
||||
}
|
||||
|
||||
.badge-info {
|
||||
color: #0c5460;
|
||||
background-color: #d1ecf1;
|
||||
}
|
||||
|
||||
.badge:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="panel panel-hovered mb20 panel-primary">
|
||||
<div class="panel-heading">
|
||||
{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('logs/message-csv')}"
|
||||
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('Message Log')}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="text-center" style="padding: 15px">
|
||||
<div class="col-md-4">
|
||||
<form id="site-search" method="post" action="{Text::url('logs/message/')}">
|
||||
<div class="input-group">
|
||||
<div class="input-group-addon">
|
||||
<span class="fa fa-search"></span>
|
||||
</div>
|
||||
<input type="text" name="q" class="form-control" value="{$q}"
|
||||
placeholder="{Lang::T('Search')}...">
|
||||
<div class="input-group-btn">
|
||||
<button class="btn btn-success" type="submit">{Lang::T('Search')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<form class="form-inline" method="post" action="{Text::url('')}logs/message/">
|
||||
<div class="input-group has-error">
|
||||
<span class="input-group-addon">{Lang::T('Keep Logs')} </span>
|
||||
<input type="text" name="keep" class="form-control" placeholder="90" value="90">
|
||||
<span class="input-group-addon">{Lang::T('Days')}</span>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-danger btn-sm" onclick="return ask(this, '{Lang::T("
|
||||
Clear old logs?")}')">{Lang::T('Clean up Logs')}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-striped">
|
||||
<thead class="thead-dark">
|
||||
<tr>
|
||||
<th>{Lang::T('ID')}</th>
|
||||
<th>{Lang::T('Date Sent')}</th>
|
||||
<th>{Lang::T('Type')}</th>
|
||||
<th>{Lang::T('Status')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{if $d} {foreach $d as $ds}
|
||||
<tr>
|
||||
<td>{$ds['id']}</td>
|
||||
<td>{Lang::dateTimeFormat($ds['sent_at'])}</td>
|
||||
<td>{$ds['message_type']}</td>
|
||||
<td>
|
||||
{if $ds['status'] == 'Success'}
|
||||
<span class="badge badge-success"> {$ds['status']} </span>
|
||||
{else}
|
||||
<span class="badge badge-danger"> {$ds['status']} </span>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{if $ds['message_content']}
|
||||
<tr>
|
||||
<td colspan="4" style="text-align: center;" style="overflow-x: scroll;">
|
||||
{nl2br($ds['message_content'])}</td>
|
||||
</tr>
|
||||
{/if}
|
||||
{if $ds['error_message']}
|
||||
<tr>
|
||||
<td colspan="4" style="text-align: center;" style="overflow-x: scroll;">
|
||||
{nl2br($ds['error_message'])}</td>
|
||||
</tr>
|
||||
{/if}
|
||||
{/foreach}{else}
|
||||
<tr>
|
||||
<td colspan="4" style="text-align: center;">
|
||||
{Lang::T('No logs found.')}
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{include file="pagination.tpl"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{include file="sections/footer.tpl"}
|
Loading…
x
Reference in New Issue
Block a user