feat: implement invoice listing functionality with DataTables integration

This commit is contained in:
Focuslinkstech 2025-04-08 18:30:36 +01:00
parent 182add517c
commit 24b713804a
4 changed files with 368 additions and 269 deletions

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,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;
}
}

View File

@ -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');
}

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`;"
@ -206,5 +204,8 @@
],
"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

@ -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"}