feat: implement invoice listing functionality with DataTables integration
This commit is contained in:
parent
182add517c
commit
24b713804a
@ -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,10 +84,9 @@ class Invoice
|
||||
}
|
||||
|
||||
if ($key === 'bill_rows') {
|
||||
return html_entity_decode($invoiceData[$key]);
|
||||
return $invoiceData[$key];
|
||||
}
|
||||
|
||||
|
||||
return htmlspecialchars($invoiceData[$key] ?? '');
|
||||
}, $template);
|
||||
}
|
||||
@ -92,14 +95,14 @@ class Invoice
|
||||
* Send invoice to user
|
||||
*
|
||||
* @param int $userId
|
||||
* @param array $invoice // $invoice['plan_name'] = 'Plan Name', $invoice['price'] = 100
|
||||
* @param array $invoice
|
||||
* @param array $bills
|
||||
* @param string $status
|
||||
* @param string $invoiceNo
|
||||
* @return bool
|
||||
*/
|
||||
|
||||
public static function sendInvoice($userId, $invoice = [], $bills = [], $status = "Unpaid", $invoiceNo = "INV-" . Package::_raid())
|
||||
public static function sendInvoice($userId, $invoice = null, $bills = [], $status = "Unpaid", $invoiceNo = null)
|
||||
{
|
||||
global $config, $root_path, $UPLOAD_PATH;
|
||||
|
||||
@ -108,11 +111,39 @@ class Invoice
|
||||
|
||||
$account = ORM::for_table('tbl_customers')->find_one($userId);
|
||||
self::validateAccount($account);
|
||||
|
||||
if (!$invoiceNo) {
|
||||
$invoiceNo = "INV-" . Package::_raid();
|
||||
}
|
||||
// Fetch invoice if not provided
|
||||
$invoice = $invoice ?: ORM::for_table("tbl_transactions")->where("username", $account->username)->find_one();
|
||||
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) {
|
||||
throw new Exception("Transaction not found for user: {$userId}");
|
||||
throw new Exception(Lang::T("Transaction not found for username: ") . $account->username);
|
||||
}
|
||||
|
||||
// Get additional bills if not provided
|
||||
@ -148,12 +179,12 @@ class Invoice
|
||||
];
|
||||
|
||||
if (empty($invoiceData['bill_rows'])) {
|
||||
throw new Exception("Bill rows data is empty.");
|
||||
throw new Exception(Lang::T("Bill rows data is empty."));
|
||||
}
|
||||
|
||||
$filename = self::generateInvoice($invoiceData);
|
||||
if (!$filename) {
|
||||
throw new Exception("Failed to generate invoice PDF");
|
||||
throw new Exception(Lang::T("Failed to generate invoice PDF"));
|
||||
}
|
||||
|
||||
$pdfPath = "system/uploads/invoices/{$filename}";
|
||||
@ -162,23 +193,23 @@ class Invoice
|
||||
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) {
|
||||
throw new Exception("Failed to send invoice email: " . $e->getMessage());
|
||||
throw new Exception(Lang::T("Failed to send email invoice to ") . $account->email . ". " . Lang::T("Reason: ") . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static function validateAccount($account)
|
||||
{
|
||||
if (!$account) {
|
||||
throw new Exception("User not found");
|
||||
throw new Exception(Lang::T("User not found"));
|
||||
}
|
||||
if (!$account->email || !filter_var($account->email, FILTER_VALIDATE_EMAIL)) {
|
||||
throw new Exception("Invalid user email");
|
||||
throw new Exception(Lang::T("Invalid user email"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -186,22 +217,22 @@ class Invoice
|
||||
{
|
||||
$items = [
|
||||
[
|
||||
'description' => $invoice->plan_name,
|
||||
'details' => 'Monthly Subscription',
|
||||
'amount' => (float) $invoice->price
|
||||
'description' => $invoice['plan_name'],
|
||||
'details' => Lang::T('Subscription'),
|
||||
'amount' => (float) $invoice['price']
|
||||
]
|
||||
];
|
||||
|
||||
if ($add_cost > 0 && $invoice->routers != 'balance') {
|
||||
if ($invoice->routers != 'balance') {
|
||||
foreach ($bills as $description => $amount) {
|
||||
if (is_numeric($amount)) {
|
||||
$items[] = [
|
||||
'description' => $description,
|
||||
'details' => 'Additional Bill',
|
||||
'details' => Lang::T('Additional Bill'),
|
||||
'amount' => (float) $amount
|
||||
];
|
||||
} else {
|
||||
_log("Invalid bill amount for {$description}: {$amount}");
|
||||
_log(Lang::T("Invalid bill amount for {$description}: {$amount}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -246,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>";
|
||||
}
|
||||
@ -289,4 +322,26 @@ class Invoice
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,41 +1,27 @@
|
||||
<?php
|
||||
// Advanced usage with custom parameters
|
||||
try {
|
||||
$userId = 8; // Customer ID
|
||||
$status = "Unpaid";
|
||||
$invoiceNo = "INV-2023-00987";
|
||||
|
||||
// Manually provide invoice data
|
||||
$invoiceData = [
|
||||
'plan_name' => 'Premium Plan',
|
||||
'price' => 49.99,
|
||||
'routers' => 'router123',
|
||||
// Add other required fields from tbl_transactions
|
||||
];
|
||||
/**
|
||||
* PHP Mikrotik Billing (https://github.com/hotspotbilling/phpnuxbill/)
|
||||
* by https://t.me/ibnux
|
||||
*
|
||||
**/
|
||||
|
||||
// Custom bills
|
||||
$bills = [
|
||||
'Additional Bandwidth' => 15.00,
|
||||
'Support Fee' => 10.00,
|
||||
'IP Address' => 5.00,
|
||||
'Custom Service' => 25.00,
|
||||
'Late Fee' => 5.00,
|
||||
'Discount' => -10.00,
|
||||
];
|
||||
|
||||
$add_cost = 20;
|
||||
_auth();
|
||||
$ui->assign('_title', Lang::T('Invoices'));
|
||||
$ui->assign('_system_menu', 'Reports');
|
||||
|
||||
$result = Invoice::sendInvoice(
|
||||
userId: $userId,
|
||||
invoice: $invoiceData,
|
||||
bills: $bills,
|
||||
status: $status,
|
||||
invoiceNo: $invoiceNo
|
||||
);
|
||||
$action = $routes['1'];
|
||||
$user = User::_info();
|
||||
$ui->assign('_user', $user);
|
||||
|
||||
if($result) {
|
||||
echo "Custom invoice sent! PDF generated at: system/uploads/invoices/invoice_{$invoiceNo}.pdf";
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
echo "Invoice Error: " . $e->getMessage();
|
||||
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/invoice/list.tpl');
|
||||
break;
|
||||
default:
|
||||
$ui->display('admin/404.tpl');
|
||||
}
|
||||
|
@ -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);"
|
||||
]
|
||||
}
|
57
ui/ui/admin/invoice/list.tpl
Normal file
57
ui/ui/admin/invoice/list.tpl
Normal 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_date}</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"}
|
Loading…
x
Reference in New Issue
Block a user