diff --git a/install/phpnuxbill.sql b/install/phpnuxbill.sql index d5e685ee..9ef4b190 100644 --- a/install/phpnuxbill.sql +++ b/install/phpnuxbill.sql @@ -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`), diff --git a/system/autoload/Message.php b/system/autoload/Message.php index e4610b4d..ece9ad4a 100644 --- a/system/autoload/Message.php +++ b/system/autoload/Message.php @@ -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'); } //
@@ -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(); } } diff --git a/system/autoload/User.php b/system/autoload/User.php index b7d9c564..2c867512 100644 --- a/system/autoload/User.php +++ b/system/autoload/User.php @@ -320,4 +320,10 @@ class User } return $html; } + public static function find($id) + { + return ORM::for_table('tbl_customers')->find_one($id); + } } + + diff --git a/system/controllers/logs.php b/system/controllers/logs.php index 9f259721..02aab1ce 100644 --- a/system/controllers/logs.php +++ b/system/controllers/logs.php @@ -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', ''); diff --git a/system/updates.json b/system/updates.json index 09897a5a..c0d490d9 100644 --- a/system/updates.json +++ b/system/updates.json @@ -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;" ] } \ No newline at end of file diff --git a/ui/ui/admin/header.tpl b/ui/ui/admin/header.tpl index ac7e9396..9ab008c6 100644 --- a/ui/ui/admin/header.tpl +++ b/ui/ui/admin/header.tpl @@ -364,6 +364,8 @@ href="{Text::url('logs/radius')}">Radius {/if} +
{Lang::T('ID')} | +{Lang::T('Date Sent')} | +{Lang::T('Type')} | +{Lang::T('Status')} | +
---|---|---|---|
{$ds['id']} | +{Lang::dateTimeFormat($ds['sent_at'])} | +{$ds['message_type']} | ++ {if $ds['status'] == 'Success'} + {$ds['status']} + {else} + {$ds['status']} + {/if} + | +
+ {nl2br($ds['message_content'])} | +|||
+ {nl2br($ds['error_message'])} | +|||
+ {Lang::T('No logs found.')} + | +