Compare commits

...

69 Commits

Author SHA1 Message Date
8f099bdefb Merge pull request 'Upload files to "/"' (#4) from nestict/mitrobill:master into master
Reviewed-on: #4
2025-05-14 14:50:13 +02:00
9fae0f84fd Upload files to "/" 2025-05-14 14:37:41 +02:00
d50a66845f Update README.md
Signed-off-by: kevinowino869 <kevinowino869@www.codelab.nestict.africa>
2025-04-14 11:01:36 +02:00
915af6fc03 Merge pull request 'Development' (#2) from Development into master
Reviewed-on: #2
2025-04-14 11:00:04 +02:00
Focuslinkstech
0a29ec9a86 refactor: replace the javascript popup with sweetalert which prevent some devices to purchase data plans and packages via captive portal, clean up footer template and improve script formatting 2025-04-11 09:19:17 +01:00
Focuslinkstech
27fd677a0a feat: add coupon settings panel with enable/disable option and save functionality 2025-04-10 08:13:01 +01:00
Focuslinkstech
e2f24c0cc6 fix: correct subject field handling in message sending logic and update related IDs in bulk template 2025-04-09 20:16:11 +01:00
Focuslinkstech
cf60c470b1 feat: add subject validation for message types and display error if missing 2025-04-09 12:50:30 +01:00
Focuslinkstech
1cc7057dca feat: add subject field for messages and implement validation based on selected channel 2025-04-09 12:36:13 +01:00
Focuslinkstech
1740c568f9 feat: add subject field for bulk messaging and update validation logic 2025-04-09 11:39:00 +01:00
Focuslinkstech
3347b39f3b refactor: improve SMS and email handling in Message class; add success/failure return values and update language file with new translations 2025-04-09 10:29:22 +01:00
Focuslinkstech
f0b9b56bb0 feat: add invoice listing page with DataTables integration and update routing 2025-04-08 18:59:15 +01:00
Focuslinkstech
24b713804a feat: implement invoice listing functionality with DataTables integration 2025-04-08 18:30:36 +01:00
Focuslinkstech
182add517c feat: implement custom invoice generation functionality 2025-04-08 17:45:18 +01:00
Focuslinkstech
45cc2afab5 fix: update invoice parameter documentation for clarity 2025-04-08 14:30:48 +01:00
Focuslinkstech
ba19b1c569 Added new functionality to Invoice class: generate invoice PDF, save to database, and send email. Also, updated language file with new translations. 2025-04-08 14:30:24 +01:00
Focuslinkstech
5caa9f905b feat: add Font Awesome 6.4.0 stylesheet to admin header template 2025-04-04 08:08:34 +01:00
Focuslinkstech
28541f366c feat: add fullname column to transaction reports and update related templates 2025-04-02 11:29:25 +01:00
Focuslinkstech
ad7998ebbf feat: add fullname field to transaction reports and update report template 2025-04-02 11:23:17 +01:00
Focuslinkstech
4c64cfabd2 fix: update demo stage check to use consistent casing 2025-04-02 11:11:05 +01:00
Focuslinkstech
9bae41dbe7 fix: correct notification reminder configuration keys for 3-day and 7-day reminders 2025-03-30 11:26:40 +01:00
Focuslinkstech
bad0545be5 feat: enhance messaging system to support multiple channels including email and inbox 2025-03-24 10:24:08 +01:00
Focuslinkstech
43a92c5d3b feat: add fullname field to transaction reports, pdf export and update language file 2025-03-22 18:34:51 +01:00
Focuslinkstech
11e5ebe103 feat: update .gitignore to include invoices directory and add index.html 2025-03-22 17:52:12 +01:00
Focuslinkstech
c573c49fb9 "Added exception for system/uploads/invoices/ directory to .gitignore" 2025-03-22 17:41:04 +01:00
Focuslinkstech
7bfbdb1efb feat: add fullname field to activation report and update language file 2025-03-22 16:15:16 +01:00
Focuslinkstech
dc28298d53 feat: add username field to customer query 2025-03-22 15:51:36 +01:00
iBNu Maksum
655e0494d3
Merge pull request #411 from dicobaja/patch/which-php
use `where` command for windows host
2025-03-20 15:37:30 +07:00
iBNu Maksum
4a441c5763
fix variable inside ', it must be inside " 2025-03-20 10:43:52 +07:00
dicobaja
5072ff8ba2
fix: use where instead of which for windows host 2025-03-19 21:44:30 +07:00
iBNu Maksum
d506dd66ff
2025.3.19 2025-03-19 14:20:00 +07:00
iBNu Maksum
e9b0cfd8f0
Merge branch 'master' into Development 2025-03-19 14:19:27 +07:00
iBNu Maksum
aa4dbc0cea
Merge pull request #410 from dicobaja/patch/MR-cache-file
update path of monthly registered cache file
2025-03-19 14:18:44 +07:00
dicobaja
4ef054466d
fix: update $cacheMRfile path to cache folder 2025-03-19 11:38:12 +07:00
iBNu Maksum
41a3cbe700
Merge pull request #408 from dicobaja/patch/sql-tbl-port-pool
missing `tbl_port_pool` in phpnuxbill.sql
2025-03-19 09:18:42 +07:00
dicobaja
4938840c5d
fix: missing tbl_port_pool in phpnuxbill.sql 2025-03-19 09:00:03 +07:00
Focuslinkstech
127d43e45d Update .gitignore to include invoices directory and ensure paid.png is tracked 2025-03-18 17:35:43 +01:00
Focuslinkstech
cdfbab7119 Refactor invoice handling to use 'invoice' key and improve payment link generation
Still in development
2025-03-18 16:51:37 +01:00
Focuslinkstech
1a2b85ae4f Merge branch 'Development' of https://github.com/hotspotbilling/phpnuxbill into Development 2025-03-18 15:28:51 +01:00
Focuslinkstech
2e2d967a5b Refactor Email invoice template handling and enhance payment link generation 2025-03-18 15:24:24 +01:00
Focuslinkstech
c45e19189a Fix case sensitivity in invoice template file paths 2025-03-18 13:12:30 +01:00
Focuslinkstech
d372bf4711 remove debug 2025-03-18 13:09:33 +01:00
Focuslinkstech
8b8a0357f0 Fix custom login page image uploading 2025-03-18 13:08:19 +01:00
Focuslinkstech
1cb0e30e6b Implement custom login page settings with validation and image upload support 2025-03-18 13:07:19 +01:00
iBNu Maksum
20916b44f0
throw error if failed disconnect customer 2025-03-17 14:50:56 +07:00
iBNu Maksum
c63545d33a
invoice link 2025-03-17 14:48:01 +07:00
Focuslinkstech
84500cdfc9 Add DejaVuSansCondensed-Oblique font to the project 2025-03-16 13:08:41 +01:00
Focuslinkstech
5b21ffcde5 Add option to allow custom balance amounts and update toggle icon 2025-03-16 13:07:58 +01:00
iBNu Maksum
009040cd3c
Fix expired list widget 2025-03-14 10:27:33 +07:00
iBNu Maksum
781481e118
Fix expired list widget 2025-03-14 10:26:47 +07:00
Focuslinkstech
66d67cb61d Add email attachment support and improve message formatting 2025-03-13 11:05:32 +01:00
iBNu Maksum
3372da2ac4
2025.3.13 new Invoice print 2025-03-13 17:04:07 +07:00
iBNu Maksum
803d04a91d
add fullname to log extend 2025-03-13 15:59:55 +07:00
iBNu Maksum
17de653752
paid icon 2025-03-13 15:22:25 +07:00
iBNu Maksum
9301f1058c
save pdf invoice customer from admin page 2025-03-13 15:07:15 +07:00
iBNu Maksum
0868d61271
Download PDF invoice Customers 2025-03-13 15:07:15 +07:00
iBNu Maksum
5f353392e3
Merge pull request #407 from ahmadhusein17/Development
Fix Text
2025-03-13 15:06:52 +07:00
Ahmad Husein
fdd8dad509
Update indonesia.json 2025-03-12 00:29:50 +07:00
Ahmad Husein
3563fa531b
Update community.tpl
Minor fixes to make it look better
2025-03-12 00:24:11 +07:00
Ahmad Husein
525f2311fc
Update community.tpl
Update a bit to make it look better
2025-03-12 00:18:46 +07:00
Ahmad Husein
3b6a6d2f55
Update recharge-confirm.tpl 2025-03-11 23:10:52 +07:00
Focuslinkstech
3cebfa2171 Refactor login and registration templates to support dynamic logo, wallpaper, and favicon loading; change footer position to fixed 2025-03-11 15:50:06 +01:00
Focuslinkstech
c65b569f94 Enhance mobile responsiveness and improve touch device support in login template 2025-03-11 09:20:00 +01:00
iBNu Maksum
ca5a7d60cf
API Rest Documentation? 2025-03-11 14:51:18 +07:00
iBNu Maksum
5987ffafce
Insomnia API Rest Collection 2025-03-11 14:39:56 +07:00
iBNu Maksum
d7bbb4d18f
Fix API request 2025-03-11 14:39:05 +07:00
iBNu Maksum
e3c173bea4
Different expired message 2025-03-11 12:29:46 +07:00
iBNu Maksum
78e1e2f989
add new Docs wiki 2025-03-11 12:28:22 +07:00
iBNu Maksum
3c7e6c7a64
Different Reminder for PPPOE and Hotspot using <divider> 2025-03-11 11:55:48 +07:00
58 changed files with 3895 additions and 2077 deletions

5
.gitignore vendored
View File

@ -55,3 +55,8 @@ docs/**
!docs/*.md
.htaccess
.idea
!docs/insomnia.rest.json
!system/uploads/paid.png
system/uploads/invoices/**
!system/uploads/invoices/
!system/uploads/invoices/index.html

View File

@ -84,27 +84,3 @@ Contact me at [Telegram](https://t.me/ibnux)
GNU General Public License version 2 or later
see [LICENSE](LICENSE) file
## Donate to ibnux
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://paypal.me/ibnux)
BCA: 5410454825
Mandiri: 163-000-1855-793
a.n Ibnu Maksum
## SPONSORS
- [mixradius.com](https://mixradius.com/) Paid Services Billing Radius
- [mlink.id](https://mlink.id)
- [https://github.com/sonyinside](https://github.com/sonyinside)
## Thanks
We appreciate all people who are participating in this project.
<a href="https://github.com/hotspotbilling/phpnuxbill/graphs/contributors">
<img src="https://contrib.rocks/image?repo=hotspotbilling/phpnuxbill" />
</a>

File diff suppressed because one or more lines are too long

1
docs/insomnia.rest.json Normal file

File diff suppressed because one or more lines are too long

View File

@ -251,6 +251,16 @@ CREATE TABLE `tbl_customers_inbox` (
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
DROP TABLE IF EXISTS `tbl_port_pool`;
CREATE TABLE IF NOT EXISTS `tbl_port_pool` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`public_ip` varchar(40) NOT NULL,
`port_name` varchar(40) NOT NULL,
`range_port` varchar(40) NOT NULL,
`routers` varchar(40) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE IF NOT EXISTS `tbl_meta` (
`id` int UNSIGNED NOT NULL AUTO_INCREMENT,
`tbl` varchar(32) COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Table name',

643
login.html Normal file
View File

@ -0,0 +1,643 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>yatmack</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/core-js/3.8.3/core.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link href='https://fonts.googleapis.com/css2?family=Lexend:wght@300;400;500;600;700&display=swap' rel='stylesheet'>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
navy: {
DEFAULT: '#023047',
50: '#e6f0f5',
100: '#cce1eb',
200: '#99c3d7',
300: '#66a5c3',
400: '#3387af',
500: '#023047',
600: '#022640',
700: '#011d39',
800: '#011332',
900: '#010a2b'
},
orange: {
DEFAULT: '#fb8500',
50: '#fff4e6',
100: '#ffe9cc',
200: '#ffd399',
300: '#ffbd66',
400: '#ffa733',
500: '#fb8500',
600: '#cc6d00',
700: '#995200',
800: '#663600',
900: '#331b00'
}
},
fontFamily: {
'lexend': ['Lexend', 'sans-serif'],
},
animation: {
'spin-fast': 'spin 0.5s linear infinite',
'fade-in': 'fadeIn 0.3s ease-in-out',
'slide-up': 'slideUp 0.3s ease-out'
},
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' }
},
slideUp: {
'0%': { transform: 'translateY(10px)', opacity: '0' },
'100%': { transform: 'translateY(0)', opacity: '1' }
}
}
}
}
}
</script>
</head>
<body class="font-lexend min-h-screen bg-gradient-to-b from-navy to-navy/95 text-gray-100">
<div class="container mx-auto px-2 py-4 max-w-3xl">
<div class="bg-white/10 backdrop-blur-lg rounded-2xl shadow-xl mb-6 overflow-hidden border border-white/10">
<div class="p-3 relative">
<h1 class="text-2xl font-bold text-center text-orange mb-3">YATMACK HOTSPOT</h1>
<p class="text-gray-100/90 text-center text-sm mb-4">
Select package ? Enter M-Pesa number ? Complete payment
</p>
<div class="flex items-center justify-center gap-2 py-2 px-4 bg-white/5 rounded-xl">
<i class="fas fa-headset text-orange"></i>
<p class="text-sm font-medium">Support: 254705042522</p>
</div>
</div>
</div>
<div class="py-2 sm:py-1 lg:py-1">
<div class="mx-auto max-w-screen-2xl px-1 md:px-1">
<div class="mx-auto max-w-lg grid grid-cols-2 sm:grid-cols-2 md:grid-cols-3 gap-4 p-1" id="cards-container">
</div>
</div>
</div>
<button onclick="redeemVoucher()" class="w-full bg-orange hover:bg-orange/90 text-white font-medium py-4 px-6 rounded-xl shadow-lg transition duration-200 flex items-center justify-center gap-3 mb-6">
<i class="fas fa-ticket-alt"></i>
<span>Redeem Voucher</span>
</button>
<div class="bg-white/10 backdrop-blur-lg rounded-2xl shadow-xl overflow-hidden border border-white/10 mb-6">
<div class="p-6 space-y-8">
<!-- Reconnect with M-Pesa -->
<div class="space-y-4">
<h3 class="text-lg font-semibold text-orange">Reconnect with M-Pesa</h3>
<div class="flex flex-col sm:flex-row gap-3">
<input id="mpesaCodeInput" type="text" placeholder="Enter M-Pesa code (e.g., SCK15SKB4Z)" class="flex-grow px-4 py-3 bg-white/5 border border-white/10 rounded-xl focus:outline-none focus:ring-2 focus:ring-orange/50 text-white placeholder:text-gray-400">
<button id="reconnectBtn" class="bg-orange hover:bg-orange/90 text-white font-medium py-3 px-6 rounded-xl transition duration-200">Reconnect</button>
</div>
</div>
<div class="space-y-4">
<h3 class="text-lg font-semibold text-orange">Active Package Login</h3>
<form id="loginForm" action="$(link-login-only)" method="post" $(if chap-id)onSubmit="return doLogin()" $(endif)>
<input type="hidden" name="dst" value="$(link-orig)">
<input type="hidden" name="popup" value="true">
<input type="hidden" name="mac" id="mac" value="$(mac)">
<div class="flex flex-col sm:flex-row gap-3">
<input id="usernameInput" name="username" type="text" placeholder="Enter Username (e.g., ACC123456)" class="flex-grow px-4 py-3 bg-white/5 border border-white/10 rounded-xl focus:outline-none focus:ring-2 focus:ring-orange/50 text-white placeholder:text-gray-400">
<button id="submitBtn" type="button" onclick="submitLogin()" class="bg-orange hover:bg-orange/90 text-white font-medium py-3 px-6 rounded-xl transition duration-200">Connect</button>
</div>
<input type="hidden" name="password" value="1234">
</form>
</div>
</div>
</div>
<div class="text-center">
<p class="text-sm text-gray-400">&copy; 2025 yatmack. Created by Smartisp</p>
</div>
</div>
<script>
function fetchData() {
var domain = 'https://yatmack2.smartisp.co.ke/';
var siteUrl = domain + "/index.php?_route=plugin/hotspot_plan";
var routerName = encodeURIComponent("yatmack");
var dataparams = `routername=${routerName}`;
fetch(siteUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: dataparams
})
.then(response => {
if (!response.ok) {
throw new Error(`Error ${response.status}: ${response.statusText}`);
}
return response.json();
})
.then(data => {
populateCards(data);
})
.catch(error => {
console.error('Fetch error:', error);
});
}
function populateCards(data) {
var cardsContainer = document.getElementById('cards-container');
cardsContainer.innerHTML = ''; // Clear existing content
// Sort the plans by price in ascending order
data.data.forEach(router => {
// Sort hotspot plans by price
router.plans_hotspot.sort((a, b) => parseFloat(a.price) - parseFloat(b.price));
router.plans_hotspot.forEach(item => {
var cardDiv = document.createElement('div');
cardDiv.className = 'bg-white border border-black rounded-lg shadow-md overflow-hidden transition duration-300 hover:shadow-lg flex flex-col items-center justify-between mx-auto mb-4 w-40';
cardDiv.innerHTML = `
<div class="bg-blue-500 text-white w-full py-1">
<h2 class="text-sm font-medium uppercase text-center" style="font-size: clamp(0.75rem, 1.5vw, 1rem); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
${item.planname}
</h2>
</div>
<div class="px-4 py-2 flex-grow">
<p class="text-2xl font-bold text-blue-600 mb-1">
<span class="text-lg font-medium text-black">${item.currency}</span>
${item.price}
</p>
<p class="text-sm text-black mb-2">
Valid for ${item.validity} ${item.timelimit}
</p>
<hr class="border-black mb-2">
</div>
<div class="px-4 py-2 flex-shrink-0">
<a href="#" class="inline-block bg-gray-900 text-white hover:bg-blue-600 font-semibold py-1 px-4 rounded-lg transition duration-300 text-md"
onclick="handlePhoneNumberSubmission('${item.planId}', '${item.routerId}'); return false;"
data-plan-id="${item.planId}"
data-router-id="${item.routerId}">
Buy
</a>
</div>
`;
cardsContainer.appendChild(cardDiv);
});
});
}
fetchData();
function getMacAddress() {
return "$(mac)"; // MikroTik replaces this with the user's MAC address
}
function getOrCreateAccountId() {
var radiaxid = localStorage.getItem('radiaxid');
if (!radiaxid) {
radiaxid = getMacAddress();
localStorage.setItem('radiaxid', radiaxid);
setCookie('radiaxid', radiaxid, 365);
}
return radiaxid;
}
function getAccountId() {
return localStorage.getItem('radiaxid') || getCookie('radiaxid') || getMacAddress();
}
function formatPhoneNumber(phoneNumber) {
if (phoneNumber.startsWith('+')) {
phoneNumber = phoneNumber.substring(1);
}
if (phoneNumber.startsWith('0')) {
phoneNumber = '254' + phoneNumber.substring(1);
}
if (phoneNumber.match(/^(7|1)/)) {
phoneNumber = '254' + phoneNumber;
}
return phoneNumber;
}
function setCookie(name, value, days) {
var expires = "";
if (days) {
var date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
expires = "; expires=" + date.toUTCString();
}
document.cookie = name + "=" + (value || "") + expires + "; path=/";
}
function getCookie(name) {
var nameEQ = name + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
var loginTimeout;
function handlePhoneNumberSubmission(planId, routerId, price) {
var accountId = getOrCreateAccountId();
var modalHtml = `
<div id="paymentModal" class='fixed inset-0 bg-black/30 backdrop-blur-sm z-50 animate-fade-in'>
<div class="fixed left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 w-full max-w-md">
<div class="bg-white text-black rounded-lg shadow-xl">
<div class="flex items-center justify-between p-4 border-b border-gray-300">
<h5 class="text-xl font-semibold">
Enter Your Mpesa Number
</h5>
<button class="text-gray-500 hover:text-black" onclick="closeModal('paymentModal')">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="p-4">
<div class="bg-navy-50 border-l-4 border-navy-400 p-4 rounded-md mb-4"> <div class="flex"> <div class="flex-shrink-0"> <svg class="h-5 w-5 text-navy-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1 4v.01m6.938-2.162A9 9 0 1111 3v0a9 9 0 018.938 10.838z" /> </svg> </div> <div class="ml-3"> <p class="text-sm text-navy-700"> You are about to initiate M-pesa payment. Enter phone number below and click Pay Now to initialize payment. </p> </div> </div></div> <input type="text" class="w-full px-4 py-3 border border-orange-300 rounded-lg focus:ring-2 focus:ring-navy-400 focus:border-blue-500 text-black" id="phoneNumberInput" required placeholder="e.g. 0712345678">
<div class="text-red-500 mt-1 hidden" id="invalidPhone">Please enter a valid phone number!</div>
</div>
<div class="flex justify-end space-x-2 p-4 border-t border-gray-300">
<button onclick="closeModal('paymentModal')" class="px-4 py-2 bg-orange text-black rounded-lg hover:bg-gray-300">Close</button>
<button id="payNowBtn" class="px-4 py-2 bg-navy text-white font-semibold hover:from-blue-600 hover:to-blue-500 flex items-center rounded-lg">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 9V7a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2m2 4h10a2 2 0 002-2v-6a2 2 0 00-2-2H9a2 2 0 00-2 2v6a2 2 0 002 2zm7-5a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
Pay Now
</button>
</div>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHtml);
var modal = document.getElementById('paymentModal');
modal.classList.remove('hidden');
var payNowBtn = document.getElementById('payNowBtn');
var phoneInput = document.getElementById('phoneNumberInput');
phoneInput.focus();
payNowBtn.addEventListener('click', function() { handlePayment(); });
function handlePayment() {
if (!phoneInput.value) {
document.getElementById('invalidPhone').classList.remove('hidden');
return;
}
payNowBtn.disabled = true;
payNowBtn.innerHTML = `
<svg class='animate-spin -ml-1 mr-3 h-5 w-5 text-white' xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24'>
<circle class='opacity-25' cx='12' cy='12' r='10' stroke='currentColor' stroke-width='4'></circle>
<path class='opacity-75' fill='currentColor' d='M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z'></path>
</svg>
Processing...
`;
var formattedPhoneNumber = formatPhoneNumber(phoneInput.value);
document.getElementById('usernameInput').value = accountId;
fetch('https://yatmack2.smartisp.co.ke/index.php?_route=plugin/CreateHotspotuser&type=grant', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
phone_number: formattedPhoneNumber,
plan_id: planId,
router_id: routerId,
account_id: accountId
})
})
.then(response => response.json())
.then(data => {
if (data.status === 'error') throw new Error(data.message);
closeModal('paymentModal');
showProcessingModal();
checkPaymentStatus(formattedPhoneNumber);
})
.catch(error => {
closeModal('paymentModal');
showErrorModal(error.message);
});
}
}
function showProcessingModal() {
var processingModalHtml = `
<div id='processingModal' class='fixed inset-0 bg-black/30 backdrop-blur-sm z-50 animate-fade-in'>
<div class='fixed left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 w-full max-w-md animate-slide-up'>
<div class='bg-gradient-to-br from-white to-gray-50 shadow-2xl rounded-2xl p-8 text-center border border-gray-100'>
<div class='flex justify-center'>
<svg class='animate-spin h-20 w-20 text-navy-500' xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24'>
<circle class='opacity-25' cx='12' cy='12' r='10' stroke='currentColor' stroke-width='4'></circle>
<path class='opacity-75' fill='currentColor' d='M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z'></path>
</svg>
</div>
<div class="mt-6 bg-navy-50/50 backdrop-blur-xs border border-navy-200 rounded-xl p-6 shadow-lg"> <div class="flex items-center space-x-4"> <svg class="h-8 w-8 text-navy-400 flex-shrink-0" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1 4v.01m6.938-2.162A9 9 0 1111 3v0a9 9 0 018.938 10.838z" /> </svg> <div class="flex-1"> <h2 class="text-xl font-semibold text-navy-600 font-lexend">Initializing Payment</h2> <p class="text-navy-500 mt-2">A payment request has been sent to your phone. Please wait while we process your payment.</p> </div> </div> <div class="mt-4 h-2 w-full bg-navy-100 rounded-full overflow-hidden"> <div class="h-2 bg-navy-400 rounded-full animate-pulse"></div> </div> </div> </div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', processingModalHtml);
}
function closeModal(modalId) {
var modal = document.getElementById(modalId);
if (modal) {
modal.classList.add('hidden');
setTimeout(function() { modal.remove(); }, 300);
}
}
function showSuccessModal() {
var successModalHtml = `
<div id='successModal' class='fixed inset-0 bg-black/60 backdrop-blur-sm z-50 transition-all duration-300'>
<div class='fixed left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 w-full max-w-lg transition-all duration-300 ease-out'>
<div class='bg-white/95 backdrop-blur-md shadow-2xl rounded-3xl p-8 text-center border border-gray-200/50 ring-1 ring-gray-900/5'>
<div class='flex justify-center mb-6'>
<div class='relative'>
<div class='absolute inset-0 rounded-full bg-green-500/20 animate-pulse'></div>
<svg class='h-16 w-16 text-green-500' xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='currentColor'>
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z'/>
</svg>
</div>
</div>
<div class='space-y-4'>
<h2 class='text-2xl font-semibold text-gray-900'>Payment Successful</h2>
<div class='bg-green-50 rounded-2xl p-6 border border-green-100'>
<div class='flex items-center gap-4'>
<svg class='h-6 w-6 text-green-500 flex-shrink-0' xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='currentColor'>
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M9 12l2 2 4-4'/>
</svg>
<p class='text-sm text-green-700'>Your transaction has been completed successfully. You will be redirected shortly.</p>
</div>
</div>
</div>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', successModalHtml);
setTimeout(function() { closeModal('successModal'); }, 2000);
}
function showErrorModal(errorMsg) {
var errorModalHtml = `
<div id='errorModal' class='fixed inset-0 bg-black/60 backdrop-blur-sm z-50 transition-all duration-300'>
<div class='fixed left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 w-full max-w-lg transition-all duration-300 ease-out'>
<div class='bg-white/95 backdrop-blur-md shadow-2xl rounded-3xl p-8 text-center border border-gray-200/50 ring-1 ring-gray-900/5'>
<div class='flex justify-center mb-6'>
<div class='relative'>
<div class='absolute inset-0 rounded-full bg-red-500/20 animate-pulse'></div>
<svg class='h-16 w-16 text-red-500' xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='currentColor'>
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M6 18L18 6M6 6l12 12'/>
</svg>
</div>
</div>
<div class='space-y-4'>
<h2 class='text-2xl font-semibold text-gray-900'>Payment Failed</h2>
<div class='bg-red-50 rounded-2xl p-6 border border-red-100'>
<div class='flex items-center gap-4'>
<svg class='h-6 w-6 text-red-500 flex-shrink-0' xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='currentColor'>
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z'/>
</svg>
<p class='text-sm text-red-700'>An error occurred while processing your payment. Please try again.</p>
</div>
</div>
</div>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', errorModalHtml);
setTimeout(function() { closeModal('errorModal'); }, 2000);
}
function checkPaymentStatus(phoneNumber) {
var accountId = getOrCreateAccountId();
var checkInterval = setInterval(function() {
fetch('https://yatmack2.smartisp.co.ke/index.php?_route=plugin/CreateHotspotuser&type=verify', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({account_id: accountId})
})
.then(function(response) { return response.json(); })
.then(function(data) {
console.log('Raw Response:', data);
if (data.Resultcode === '3') {
clearInterval(checkInterval);
closeModal('processingModal');
showSuccessModal();
if (loginTimeout) clearTimeout(loginTimeout);
loginTimeout = setTimeout(function() { document.getElementById('loginForm').submit(); }, 2000);
} else if (data.Resultcode === '2') {
clearInterval(checkInterval);
closeModal('processingModal');
showErrorModal(data.Message);
} else {
console.error('Unexpected result code:', data.Resultcode);
}
})
.catch(function(error) {
console.error('Error during fetch request:', error);
clearInterval(checkInterval);
closeModal('processingModal');
showErrorModal('An error occurred while checking payment status.');
});
}, 1000);
setTimeout(function() {
clearInterval(checkInterval);
closeModal('processingModal');
showErrorModal('Timeout while waiting for payment confirmation.');
}, 300000); // 5 minutes
}
document.addEventListener('DOMContentLoaded', function() {
var accountId = getOrCreateAccountId();
var usernameInput = document.getElementById('usernameInput');
if (usernameInput && !usernameInput.value) {
usernameInput.value = accountId;
}
var submitBtn = document.getElementById('submitBtn');
if (submitBtn) {
submitBtn.addEventListener('click', function() {
document.getElementById('loginForm').submit();
});
}
});
var loginTimeout; // Variable to store the timeout ID
function redeemVoucher() {
Swal.fire({
title: 'Redeem Voucher',
input: 'text',
inputPlaceholder: 'Enter voucher code',
inputValidator: function(value) {
if (!value) {
return 'You need to enter a voucher code!';
}
},
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Redeem',
showLoaderOnConfirm: true,
preConfirm: (voucherCode) => {
var accountId = voucherCode;
return fetch('https://yatmack2.smartisp.co.ke/index.php?_route=plugin/CreateHotspotuser&type=voucher', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({voucher_code: voucherCode, account_id: accountId}),
})
.then(response => {
if (!response.ok) throw new Error('Network response was not ok');
return response.json();
})
.then(data => {
if (data.status === 'error') throw new Error(data.message);
return data;
});
},
allowOutsideClick: () => !Swal.isLoading()
}).then((result) => {
if (result.isConfirmed) {
Swal.fire({
icon: 'success',
title: 'Voucher Redeemed',
text: result.value.message,
showConfirmButton: false,
allowOutsideClick: false,
didOpen: () => {
Swal.showLoading();
var username = result.value.username;
console.log('Received username from server:', username);
var usernameInput = document.querySelector('input[name="username"]');
if (usernameInput) {
console.log('Found username input element.');
usernameInput.value = username;
loginTimeout = setTimeout(function() {
var loginForm = document.getElementById('loginForm');
if (loginForm) {
loginForm.submit();
} else {
console.error('Login form not found.');
Swal.fire({
icon: 'error',
title: 'Error',
text: 'Login form not found. Please try again.',
});
}
}, 2000);
} else {
console.error('Username input element not found.');
Swal.fire({
icon: 'error',
title: 'Error',
text: 'Username input not found. Please try again.',
});
}
}
});
}
}).catch(error => {
Swal.fire({
icon: 'error',
title: 'Oops...',
text: error.message,
});
});
}
document.addEventListener('DOMContentLoaded', function() {
var reconnectBtn = document.getElementById('reconnectBtn');
var mpesaCodeInput = document.getElementById('mpesaCodeInput');
var macInput = document.getElementById('mac');
var loginForm = document.getElementById('loginForm');
if (reconnectBtn) {
reconnectBtn.addEventListener('click', function() {
// Validate inputs before processing
if (!mpesaCodeInput || !macInput || !loginForm) {
Swal.fire({
icon: 'error',
title: 'Error',
text: 'Required form elements are missing'
});
return;
}
var mpesaCode = mpesaCodeInput.value.trim().split(' ')[0];
var mac = macInput.value.trim();
fetch('https://yatmack2.smartisp.co.ke/index.php?_route=plugin/ReconnectUser', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ mpesa_code: mpesaCode, mac: mac }),
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
console.log('Response data:', data);
var resultCode = data.Resultcode;
var message = data.Message;
var status = data.Status;
var username = data.username;
if (resultCode === '1') {
Swal.fire({
icon: 'error',
title: 'Invalid Code',
text: message
});
}
else if (resultCode === '3') {
Swal.fire({
icon: 'error',
title: 'Expired Package',
text: message
});
}
else if (resultCode === '2') {
Swal.fire({
icon: 'success',
title: status,
text: message,
showConfirmButton: false,
allowOutsideClick: false,
didOpen: () => {
Swal.showLoading();
console.log('Received username from server:', username);
var usernameInput = document.querySelector('input[name="username"]');
if (usernameInput) {
console.log('Found username input element.');
usernameInput.value = username;
setTimeout(function() {
var loginForm = document.getElementById('loginForm');
if (loginForm) {
loginForm.submit();
} else {
console.error('Login form not found.');
Swal.fire({
icon: 'error',
title: 'Error',
text: 'Login form not found. Please try again.',
});
}
}, 2000);
} else {
console.error('Username input element not found.');
Swal.fire({
icon: 'error',
title: 'Error',
text: 'Username input not found. Please try again.',
});
}
}
});
}
})
.catch(error => {
Swal.fire({
icon: 'error',
title: 'Oops...',
text: error.message
});
});
});
} else {
console.error('Reconnect button not found');
}
});
</script>
</html>

View File

@ -48,7 +48,28 @@ $ui = new class($key)
}
function getAll()
{
return $this->assign;
$result = [];
foreach ($this->assign as $key => $value) {
if($value instanceof ORM){
$result[$key] = $value->as_array();
}else if($value instanceof IdiormResultSet){
$count = count($value);
for($n=0;$n<$count;$n++){
foreach ($value[$n] as $k=>$v) {
$result[$key][$n][$k] = $v;
}
}
}else{
$result[$key] = $value;
}
}
return $result;
}
function fetch()
{
return "";
}
};

View File

@ -22,8 +22,8 @@ class Csrf
public static function check($token)
{
global $config;
if($config['csrf_enabled'] == 'yes') {
global $config, $isApi;
if($config['csrf_enabled'] == 'yes' && !$isApi) {
if (isset($_SESSION['csrf_token'], $_SESSION['csrf_token_time'], $token)) {
$storedToken = $_SESSION['csrf_token'];
$tokenTime = $_SESSION['csrf_token_time'];

View File

@ -87,7 +87,7 @@ class File
$src_img = $image_create($source_file);
imagecopyresampled($dst_img, $src_img, 0, 0, 0, 0, $nwidth, $nheight, $width, $height);
$image($dst_img, $dst_dir, $quality);
imagepng($dst_img, $dst_dir);
if ($dst_img) imagedestroy($dst_img);
if ($src_img) imagedestroy($src_img);

347
system/autoload/Invoice.php Normal file
View File

@ -0,0 +1,347 @@
<?php
use Mpdf\Mpdf;
class Invoice
{
public static function generateInvoice($invoiceData)
{
try {
if (empty($invoiceData['invoice'])) {
throw new Exception(Lang::T("Invoice No is required"));
}
$template = Lang::getNotifText('email_invoice');
if (!$template) {
throw new Exception(Lang::T("Invoice template not found"));
}
if (strpos($template, '<body') === false) {
$template = "<html><body>$template</body></html>";
}
$processedHtml = self::renderTemplate($template, $invoiceData);
// Debugging: Save processed HTML to file for review
// file_put_contents('debug_invoice.html', $processedHtml);
// Generate PDF
$mpdf = new Mpdf([
'mode' => 'utf-8',
'format' => 'A4',
'margin_left' => 10,
'margin_right' => 10,
'margin_top' => 10,
'margin_bottom' => 10,
'default_font' => 'helvetica',
'orientation' => 'P',
]);
$mpdf->SetDisplayMode('fullpage');
$mpdf->SetProtection(['print']);
$mpdf->shrink_tables_to_fit = 1;
$mpdf->SetWatermarkText(strtoupper($invoiceData['status'] ?? 'UNPAID'), 0.15);
$mpdf->showWatermarkText = true;
$mpdf->WriteHTML($processedHtml);
// 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(Lang::T("Failed to save PDF file"));
}
return $filename;
} catch (\Exception $e) {
_log("Invoice generation failed: " . $e->getMessage());
sendTelegram("Invoice generation failed: " . $e->getMessage());
return false;
}
}
private static function renderTemplate($template, $invoiceData)
{
return preg_replace_callback('/\[\[(\w+)\]\]/', function ($matches) use ($invoiceData) {
$key = $matches[1];
if (!isset($invoiceData[$key])) {
_log(Lang::T("Missing invoice key: ") . $key);
return '';
}
if (in_array($key, ['created_at', 'due_date'])) {
return date('F j, Y', strtotime($invoiceData[$key]));
}
if (in_array($key, ['amount', 'total', 'subtotal', 'tax'])) {
return $invoiceData['currency_code'] . number_format((float) $invoiceData[$key], 2);
}
if ($key === 'bill_rows') {
return $invoiceData[$key];
}
return htmlspecialchars($invoiceData[$key] ?? '');
}, $template);
}
/**
* 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;
// Set default currency code
$config['currency_code'] ??= '$';
$account = ORM::for_table('tbl_customers')->find_one($userId);
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) {
throw new Exception(Lang::T("Transaction not found for username: ") . $account->username);
}
// Get additional bills if not provided
if (empty($bills)) {
[$bills, $add_cost] = User::getBills($account->id);
}
$invoiceItems = self::generateInvoiceItems($invoice, $bills, $add_cost);
$subtotal = array_sum(array_column($invoiceItems, 'amount'));
$tax = $config['enable_tax'] ? Package::tax($subtotal) : 0;
$tax_rate = $config['tax_rate'] ?? 0;
$total = $subtotal + $tax;
$payLink = self::generatePaymentLink($account, $invoice, $status);
$logo = self::getCompanyLogo($UPLOAD_PATH, $root_path);
$invoiceData = [
'invoice' => $invoiceNo,
'fullname' => $account->fullname,
'email' => $account->email,
'address' => $account->address,
'phone' => $account->phonenumber,
'bill_rows' => self::generateBillRows($invoiceItems, $config['currency_code'], $subtotal, $tax_rate, $tax, $total),
'status' => $status,
'created_at' => date('Y-m-d H:i:s'),
'due_date' => date('Y-m-d H:i:s', strtotime('+7 days')),
'currency' => $config['currency_code'],
'company_address' => $config['address'],
'company_name' => $config['CompanyName'],
'company_phone' => $config['phone'],
'logo' => $logo,
'payment_link' => $payLink
];
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"));
}
$pdfPath = "system/uploads/invoices/{$filename}";
self::saveToDatabase($filename, $account->id, $invoiceData, $total);
try {
Message::sendEmail(
$account->email,
Lang::T("Invoice for Account {$account->fullname}"),
Lang::T("Please find your invoice attached"),
$pdfPath
);
return true;
} catch (\Exception $e) {
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(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)
{
$html = "<table style='width: 100%; border-collapse: collapse; margin: 20px 0;'>
<thead>
<tr>
<th style='background: #3498db; color: white; padding: 12px; text-align: left;'>Description</th>
<th style='background: #3498db; color: white; padding: 12px; text-align: left;'>Details</th>
<th style='background: #3498db; color: white; padding: 12px; text-align: left;'>Amount</th>
</tr>
</thead>
<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;'>{$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>";
}
$html .= "<tr>
<td colspan='2' style='text-align: right; padding: 10px; border-top: 2px solid #3498db;'>Subtotal:</td>
<td style='padding: 10px; border-top: 2px solid #3498db;'>{$currency}" . number_format($subtotal, 2) . "</td>
</tr>
<tr>
<td colspan='2' style='text-align: right; padding: 10px;'>TAX ({$tax_rate}%):</td>
<td style='padding: 10px;'>{$currency}" . number_format($tax, 2) . "</td>
</tr>
<tr>
<td colspan='2' style='text-align: right; padding: 10px; font-weight: bold;'>Total:</td>
<td style='padding: 10px; font-weight: bold;'>{$currency}" . number_format($total, 2) . "</td>
</tr>";
$html .= "</tbody></table>";
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) {
@ -120,7 +120,7 @@ class Message
}
}
public static function sendEmail($to, $subject, $body)
public static function sendEmail($to, $subject, $body, $attachmentPath = null)
{
global $config, $PAGES_PATH, $debug_mail;
if (empty($body)) {
@ -130,7 +130,6 @@ 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'])) {
@ -140,6 +139,8 @@ class Message
$attr .= "Reply-To: " . $config['mail_reply_to'] . "\r\n";
}
mail($to, $subject, $body, $attr);
self::logMessage('Email', $to, $body, 'Success');
return true;
} else {
$mail = new PHPMailer();
$mail->isSMTP();
@ -161,6 +162,10 @@ class Message
$mail->addAddress($to);
$mail->Subject = $subject;
// Attachments
if (!empty($attachmentPath)) {
$mail->addAttachment($attachmentPath);
}
if (!file_exists($PAGES_PATH . DIRECTORY_SEPARATOR . 'Email.html')) {
if (!copy($PAGES_PATH . '_template' . DIRECTORY_SEPARATOR . 'Email.html', $PAGES_PATH . DIRECTORY_SEPARATOR . 'Email.html')) {
@ -176,6 +181,7 @@ class Message
$html = str_replace('[[Body]]', nl2br($body), $html);
$mail->isHTML(true);
$mail->Body = $html;
$mail->Body = $html;
} else {
$mail->isHTML(false);
$mail->Body = $body;
@ -183,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;">
@ -303,7 +311,7 @@ class Message
public static function sendInvoice($cust, $trx)
{
global $config;
global $config, $db_pass;
$textInvoice = Lang::getNotifText('invoice_paid');
$textInvoice = str_replace('[[company_name]]', $config['CompanyName'], $textInvoice);
$textInvoice = str_replace('[[address]]', $config['address'], $textInvoice);
@ -328,6 +336,10 @@ class Message
$textInvoice = str_replace('[[password]]', $cust['password'], $textInvoice);
$textInvoice = str_replace('[[expired_date]]', Lang::dateAndTimeFormat($trx['expiration'], $trx['time']), $textInvoice);
$textInvoice = str_replace('[[footer]]', $config['note'], $textInvoice);
$inv_url = "?_route=voucher/invoice/$trx[id]/" . md5($trx['id'] . $db_pass);
$textInvoice = str_replace('[[invoice_link]]', $inv_url, $textInvoice);
// Calculate bills and additional costs
list($bills, $add_cost) = User::getBills($cust['id']);
@ -387,9 +399,24 @@ 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;
}
}
public static function getMessageType($type, $message)
{
if (strpos($message, "<divider>") === false) {
return $message;
}
$msgs = explode("<divider>", $message);
if ($type == "PPPOE") {
return $msgs[1];
} else {
return $msgs[0];
}
}

View File

@ -10,7 +10,7 @@ class Paginator
{
public static function findMany($query, $search = [], $per_page = '10', $append_url = "", $toArray = false)
{
global $routes, $ui;
global $routes, $ui, $isApi;
$adjacents = "2";
$page = _get('p', 1);
$page = (empty($page) ? 1 : $page);
@ -72,7 +72,7 @@ class Paginator
if ($ui) {
$ui->assign('paginator', $result);
}
if($toArray){
if($toArray || $isApi){
return $query->offset($startpoint)->limit($per_page)->find_array();
}else{
return $query->offset($startpoint)->limit($per_page)->find_many();

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);
}
@ -158,6 +161,7 @@ switch ($action) {
<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>
@ -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

@ -196,8 +196,8 @@ if (isset($_GET['recharge']) && !empty($_GET['recharge'])) {
$tur->save();
App::setToken(_get('stoken'), $id);
file_put_contents($path, $m);
_log("Customer $tur[customer_id] $tur[username] extend for $days days", "Customer", $user['id']);
Message::sendTelegram("#u$user[username] #extend #" . $p['type'] . " \n" . $p['name_plan'] .
_log("Customer $tur[customer_id] $user[fullname] ($tur[username]) extend for $days days", "Customer", $user['id']);
Message::sendTelegram("#u$user[username] ($user[fullname]) #id$tur[customer_id] #extend #" . $p['type'] . " \n" . $p['name_plan'] .
"\nLocation: " . $p['routers'] .
"\nCustomer: " . $user['fullname'] .
"\nNew Expired: " . Lang::dateAndTimeFormat($expiration, $tur['time']));

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

@ -371,7 +371,7 @@ switch ($action) {
$d->trx_invoice = $result;
$d->status = 2;
$d->save();
r2(getUrl('order/view/$trx_id'), 's', Lang::T("Success to send package"));
r2(getUrl("order/view/$trx_id"), 's', Lang::T("Success to send package"));
} else {
$errorMessage = "Send Package with Balance Failed\n\n#u$user[username] #send \n" . $plan['name_plan'] .
"\nRouter: " . $router_name .

View File

@ -269,6 +269,19 @@ switch ($action) {
r2(getUrl('plan/view/') . $id, 'd', "Customer not found");
}
Package::createInvoice($in);
$UPLOAD_URL_PATH = str_replace($root_path, '', $UPLOAD_PATH);
$logo = '';
if (file_exists($UPLOAD_PATH . DIRECTORY_SEPARATOR . 'logo.png')) {
$logo = $UPLOAD_URL_PATH . DIRECTORY_SEPARATOR . 'logo.png';
$imgsize = getimagesize($logo);
$width = $imgsize[0];
$height = $imgsize[1];
$ui->assign('wlogo', $width);
$ui->assign('hlogo', $height);
}
$ui->assign('public_url', getUrl("voucher/invoice/$id/".md5($id. $db_pass)));
$ui->assign('logo', $logo);
$ui->assign('_title', 'View Invoice');
$ui->display('admin/plan/invoice.tpl');
break;
@ -1062,11 +1075,11 @@ switch ($action) {
} else {
r2(getUrl('plan'), 's', "Customer not found");
}
Message::sendTelegram("#u$tur[username] #extend #" . $p['type'] . " \n" . $p['name_plan'] .
Message::sendTelegram("#u$tur[username] #id$tur[customer_id] #extend by $admin[fullname] #" . $p['type'] . " \n" . $p['name_plan'] .
"\nLocation: " . $p['routers'] .
"\nCustomer: " . $c['fullname'] .
"\nNew Expired: " . Lang::dateAndTimeFormat($expiration, $tur['time']));
_log("$admin[fullname] extend Customer $tur[customer_id] $tur[username] for $days days", $admin['user_type'], $admin['id']);
_log("$admin[fullname] extend Customer $tur[customer_id] $tur[username] #$tur[customer_id] for $days days", $admin['user_type'], $admin['id']);
r2(getUrl('plan'), 's', "Extend until $expiration");
} else {
r2(getUrl('plan'), 's', "Customer is not expired yet");

View File

@ -113,7 +113,8 @@ switch ($do) {
$d->save();
}
}
if (file_exists($_FILES['photo']['tmp_name'])) unlink($_FILES['photo']['tmp_name']);
if (file_exists($_FILES['photo']['tmp_name']))
unlink($_FILES['photo']['tmp_name']);
User::setFormCustomField($user);
run_hook('register_user'); #HOOK
$msg .= Lang::T('Registration successful') . '<br>';
@ -147,8 +148,45 @@ switch ($do) {
// Display register-otp.tpl if OTP is enabled
$ui->display('customer/register-otp.tpl');
} else {
// Display register.tpl if OTP is not enabled
$UPLOAD_URL_PATH = str_replace($root_path, '', $UPLOAD_PATH);
if (!empty($config['login_page_logo']) && file_exists($UPLOAD_URL_PATH . DIRECTORY_SEPARATOR . $config['login_page_logo'])) {
$login_logo = $UPLOAD_URL_PATH . DIRECTORY_SEPARATOR . $config['login_page_logo'];
} elseif (file_exists($UPLOAD_URL_PATH . DIRECTORY_SEPARATOR . 'login-logo.png')) {
$login_logo = $UPLOAD_URL_PATH . DIRECTORY_SEPARATOR . 'login-logo.png';
} else {
$login_logo = $UPLOAD_URL_PATH . DIRECTORY_SEPARATOR . 'login-logo.default.png';
}
if (!empty($config['login_page_wallpaper']) && file_exists($UPLOAD_URL_PATH . DIRECTORY_SEPARATOR . $config['login_page_wallpaper'])) {
$wallpaper = $UPLOAD_URL_PATH . DIRECTORY_SEPARATOR . $config['login_page_wallpaper'];
} elseif (file_exists($UPLOAD_URL_PATH . DIRECTORY_SEPARATOR . 'wallpaper.png')) {
$wallpaper = $UPLOAD_URL_PATH . DIRECTORY_SEPARATOR . 'wallpaper.png';
} else {
$wallpaper = $UPLOAD_URL_PATH . DIRECTORY_SEPARATOR . 'wallpaper.default.png';
}
if (!empty($config['login_page_favicon']) && file_exists($UPLOAD_URL_PATH . DIRECTORY_SEPARATOR . $config['login_page_favicon'])) {
$favicon = $UPLOAD_URL_PATH . DIRECTORY_SEPARATOR . $config['login_page_favicon'];
} elseif (file_exists($UPLOAD_URL_PATH . DIRECTORY_SEPARATOR . 'favicon.png')) {
$favicon = $UPLOAD_URL_PATH . DIRECTORY_SEPARATOR . 'favicon.png';
} else {
$favicon = $UPLOAD_URL_PATH . DIRECTORY_SEPARATOR . 'favicon.default.png';
}
$ui->assign('login_logo', $login_logo);
$ui->assign('wallpaper', $wallpaper);
$ui->assign('favicon', $favicon);
$ui->assign('csrf_token', $csrf_token);
$ui->assign('_title', Lang::T('Login'));
$ui->assign('customFields', User::getFormCustomField($ui, true));
switch ($config['login_page_type']) {
case 'custom':
$ui->display('customer/reg-login-custom-' . $config['login_Page_template'] . '.tpl');
break;
default:
$ui->display('customer/register.tpl');
break;
}
}
}
break;
@ -196,6 +234,36 @@ switch ($do) {
$ui->display('customer/register-rotp.tpl');
}
} else {
$UPLOAD_URL_PATH = str_replace($root_path, '', $UPLOAD_PATH);
if (!empty($config['login_page_logo']) && file_exists($UPLOAD_URL_PATH . DIRECTORY_SEPARATOR . $config['login_page_logo'])) {
$login_logo = $UPLOAD_URL_PATH . DIRECTORY_SEPARATOR . $config['login_page_logo'];
} elseif (file_exists($UPLOAD_URL_PATH . DIRECTORY_SEPARATOR . 'login-logo.png')) {
$login_logo = $UPLOAD_URL_PATH . DIRECTORY_SEPARATOR . 'login-logo.png';
} else {
$login_logo = $UPLOAD_URL_PATH . DIRECTORY_SEPARATOR . 'login-logo.default.png';
}
if (!empty($config['login_page_wallpaper']) && file_exists($UPLOAD_URL_PATH . DIRECTORY_SEPARATOR . $config['login_page_wallpaper'])) {
$wallpaper = $UPLOAD_URL_PATH . DIRECTORY_SEPARATOR . $config['login_page_wallpaper'];
} elseif (file_exists($UPLOAD_URL_PATH . DIRECTORY_SEPARATOR . 'wallpaper.png')) {
$wallpaper = $UPLOAD_URL_PATH . DIRECTORY_SEPARATOR . 'wallpaper.png';
} else {
$wallpaper = $UPLOAD_URL_PATH . DIRECTORY_SEPARATOR . 'wallpaper.default.png';
}
if (!empty($config['login_page_favicon']) && file_exists($UPLOAD_URL_PATH . DIRECTORY_SEPARATOR . $config['login_page_favicon'])) {
$favicon = $UPLOAD_URL_PATH . DIRECTORY_SEPARATOR . $config['login_page_favicon'];
} elseif (file_exists($UPLOAD_URL_PATH . DIRECTORY_SEPARATOR . 'favicon.png')) {
$favicon = $UPLOAD_URL_PATH . DIRECTORY_SEPARATOR . 'favicon.png';
} else {
$favicon = $UPLOAD_URL_PATH . DIRECTORY_SEPARATOR . 'favicon.default.png';
}
$ui->assign('login_logo', $login_logo);
$ui->assign('wallpaper', $wallpaper);
$ui->assign('favicon', $favicon);
$ui->assign('csrf_token', $csrf_token);
$ui->assign('_title', Lang::T('Login'));
$ui->assign('customFields', User::getFormCustomField($ui, true));
$ui->assign('username', "");
$ui->assign('fullname', "");
@ -204,7 +272,15 @@ switch ($do) {
$ui->assign('otp', false);
$ui->assign('_title', Lang::T('Register'));
run_hook('view_register'); #HOOK
switch ($config['login_page_type']) {
case 'custom':
$ui->display('customer/reg-login-custom-' . $config['login_Page_template'] . '.tpl');
break;
default:
$ui->display('customer/register.tpl');
break;
}
}
break;
}

View File

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

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';
}
@ -248,29 +249,6 @@ switch ($action) {
$_POST['hide_pg'] = _post('hide_pg', 'no');
$_POST['hide_aui'] = _post('hide_aui', 'no');
foreach ($_POST as $key => $value) {
$d = ORM::for_table('tbl_appconfig')->where('setting', $key)->find_one();
if ($d) {
$d->value = $value;
$d->save();
} else {
$d = ORM::for_table('tbl_appconfig')->create();
$d->setting = $key;
$d->value = $value;
$d->save();
}
}
_log('[' . $admin['username'] . ']: ' . Lang::T('Settings Saved Successfully'), $admin['user_type'], $admin['id']);
r2(getUrl('settings/app'), 's', Lang::T('Settings Saved Successfully'));
}
break;
case 'login-page-post':
if ($_app_stage == 'Demo') {
r2(getUrl('settings/app'), 'e', 'You cannot perform this action in Demo mode');
}
// Login page post
$login_page_title = _post('login_page_head');
$login_page_description = _post('login_page_description');
@ -296,13 +274,6 @@ switch ($action) {
return;
}
$settings = [
'login_page_head' => $login_page_title,
'login_page_description' => $login_page_description,
'login_Page_template' => $login_Page_template,
'login_page_type' => $login_page_type,
];
$image_paths = [];
$allowed_types = ['image/jpeg', 'image/png'];
@ -312,7 +283,7 @@ switch ($action) {
$extension = pathinfo($_FILES['login_page_favicon']['name'], PATHINFO_EXTENSION);
$favicon_path = $UPLOAD_PATH . DIRECTORY_SEPARATOR . uniqid('favicon_') . '.' . $extension;
File::resizeCropImage($_FILES['login_page_favicon']['tmp_name'], $favicon_path, 16, 16, 100);
$settings['login_page_favicon'] = basename($favicon_path); // Save dynamic file name
$_POST['login_page_favicon'] = basename($favicon_path); // Save dynamic file name
if (file_exists($_FILES['login_page_favicon']['tmp_name']))
unlink($_FILES['login_page_favicon']['tmp_name']);
} else {
@ -326,7 +297,7 @@ switch ($action) {
$extension = pathinfo($_FILES['login_page_wallpaper']['name'], PATHINFO_EXTENSION);
$wallpaper_path = $UPLOAD_PATH . DIRECTORY_SEPARATOR . uniqid('wallpaper_') . '.' . $extension;
File::resizeCropImage($_FILES['login_page_wallpaper']['tmp_name'], $wallpaper_path, 1920, 1080, 100);
$settings['login_page_wallpaper'] = basename($wallpaper_path); // Save dynamic file name
$_POST['login_page_wallpaper'] = basename($wallpaper_path); // Save dynamic file name
if (file_exists($_FILES['login_page_wallpaper']['tmp_name']))
unlink($_FILES['login_page_wallpaper']['tmp_name']);
} else {
@ -340,15 +311,14 @@ switch ($action) {
$extension = pathinfo($_FILES['login_page_logo']['name'], PATHINFO_EXTENSION);
$logo_path = $UPLOAD_PATH . DIRECTORY_SEPARATOR . uniqid('logo_') . '.' . $extension;
File::resizeCropImage($_FILES['login_page_logo']['tmp_name'], $logo_path, 300, 60, 100);
$settings['login_page_logo'] = basename($logo_path); // Save dynamic file name
$_POST['login_page_logo'] = basename($logo_path); // Save dynamic file name
if (file_exists($_FILES['login_page_logo']['tmp_name']))
unlink($_FILES['login_page_logo']['tmp_name']);
} else {
r2(getUrl('settings/app'), 'e', 'Logo must be a JPG, JPEG, or PNG image.');
}
}
foreach ($settings as $key => $value) {
foreach ($_POST as $key => $value) {
$d = ORM::for_table('tbl_appconfig')->where('setting', $key)->find_one();
if ($d) {
$d->value = $value;
@ -360,9 +330,10 @@ switch ($action) {
$d->save();
}
}
_log('[' . $admin['username'] . ']: ' . Lang::T('Settings Saved Successfully'), $admin['user_type'], $admin['id']);
_log('[' . $admin['username'] . ']: ' . Lang::T('Login Page Settings Saved Successfully'), $admin['user_type'], $admin['id']);
r2(getUrl('settings/app'), 's', Lang::T('Login Page Settings Saved Successfully'));
r2(getUrl('settings/app'), 's', Lang::T('Settings Saved Successfully'));
}
break;
case 'localisation':
@ -590,7 +561,7 @@ switch ($action) {
}
//allow see himself
if ($admin['id'] == $id) {
$d = ORM::for_table('tbl_users')->where('id', $id)->find_array($id)[0];
$d = ORM::for_table('tbl_users')->where('id', $id)->find_array()[0];
} else {
if (in_array($admin['user_type'], ['SuperAdmin', 'Admin'])) {
// Super Admin can see anyone

View File

@ -4,11 +4,47 @@
* PHP Mikrotik Billing (https://github.com/hotspotbilling/phpnuxbill/)
* by https://t.me/ibnux
**/
_auth();
$ui->assign('_title', Lang::T('Voucher'));
$ui->assign('_system_menu', 'voucher');
$action = $routes['1'];
if(!_auth(false)){
if($action== 'invoice'){
$id = $routes[2];
$sign = $routes[3];
if($sign != md5($id. $db_pass)) {
die("beda");
}
if (empty($id)) {
$in = ORM::for_table('tbl_transactions')->order_by_desc('id')->find_one();
} else {
$in = ORM::for_table('tbl_transactions')->where('id', $id)->find_one();
}
if ($in) {
Package::createInvoice($in);
$UPLOAD_URL_PATH = str_replace($root_path, '', $UPLOAD_PATH);
$logo = '';
if (file_exists($UPLOAD_PATH . DIRECTORY_SEPARATOR . 'logo.png')) {
$logo = $UPLOAD_URL_PATH . DIRECTORY_SEPARATOR . 'logo.png';
$imgsize = getimagesize($logo);
$width = $imgsize[0];
$height = $imgsize[1];
$ui->assign('wlogo', $width);
$ui->assign('hlogo', $height);
}
$ui->assign('public_url', getUrl("voucher/invoice/$id/".md5($id. $db_pass)));
$ui->assign('logo', $logo);
$ui->display('customer/invoice-customer.tpl');
die();
} else {
r2(getUrl('voucher/list-activated'), 'e', Lang::T('Not Found'));
}
}else{
r2(getUrl('login'));
}
}
$user = User::_info();
$ui->assign('_user', $user);
@ -64,6 +100,18 @@ switch ($action) {
}
if ($in) {
Package::createInvoice($in);
$UPLOAD_URL_PATH = str_replace($root_path, '', $UPLOAD_PATH);
$logo = '';
if (file_exists($UPLOAD_PATH . DIRECTORY_SEPARATOR . 'logo.png')) {
$logo = $UPLOAD_URL_PATH . DIRECTORY_SEPARATOR . 'logo.png';
$imgsize = getimagesize($logo);
$width = $imgsize[0];
$height = $imgsize[1];
$ui->assign('wlogo', $width);
$ui->assign('hlogo', $height);
}
$ui->assign('public_url', getUrl("voucher/invoice/$id/".md5($id. $db_pass)));
$ui->assign('logo', $logo);
$ui->display('customer/invoice-customer.tpl');
} else {
r2(getUrl('voucher/list-activated'), 'e', Lang::T('Not Found'));

View File

@ -75,13 +75,7 @@ foreach ($d as $ds) {
if ($_app_stage != 'demo') {
if (file_exists($dvc)) {
require_once $dvc;
try {
(new $p['device'])->remove_customer($c, $p);
} catch (Throwable $e) {
_log($e->getMessage());
sendTelegram($e->getMessage());
echo "Error: " . $e->getMessage() . "\n";
}
} else {
throw new Exception("Cron error: Devices " . $p['device'] . "not found, cannot disconnect ".$c['username']."\n");
}
@ -89,7 +83,13 @@ foreach ($d as $ds) {
// Send notification and update user status
try {
echo Message::sendPackageNotification($c, $u['namebp'], $p['price'], $textExpired, $config['user_notification_expired']) . "\n";
echo Message::sendPackageNotification(
$c,
$u['namebp'],
$p['price'],
Message::getMessageType($p['type'], $textExpired),
$config['user_notification_expired']
) . "\n";
$u->status = 'off';
$u->save();
} catch (Throwable $e) {

View File

@ -49,25 +49,42 @@ 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, $p['name_plan'], $price, Lang::getNotifText('reminder_7_day'), $config['user_notification_reminder']) . "\n";
echo Message::sendPackageNotification(
$c,
$p['name_plan'],
$price,
Message::getMessageType($p['type'], Lang::getNotifText('reminder_7_day')),
$config['user_notification_reminder']
) . "\n";
} 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, $p['name_plan'], $price, Lang::getNotifText('reminder_3_day'), $config['user_notification_reminder']) . "\n";
echo Message::sendPackageNotification(
$c,
$p['name_plan'],
$price,
Message::getMessageType($p['type'], Lang::getNotifText('reminder_3_day')),
$config['user_notification_reminder']
) . "\n";
} catch (Exception $e) {
sendTelegram("Cron Reminder failed to send 3-day reminder to " . $ds['username'] . " Error: " . $e->getMessage());
}
} else if ($ds['expiration'] == $day1 && $config['notification_reminder_1day'] !== 'no') {
try {
echo Message::sendPackageNotification($c, $p['name_plan'], $price, Lang::getNotifText('reminder_1_day'), $config['user_notification_reminder']) . "\n";
echo Message::sendPackageNotification(
$c,
$p['name_plan'],
$price,
Message::getMessageType($p['type'], Lang::getNotifText('reminder_1_day')),
$config['user_notification_reminder']
) . "\n";
} catch (Exception $e) {
sendTelegram("Cron Reminder failed to send 1-day reminder to " . $ds['username'] . " Error: " . $e->getMessage());
}
}
}
}

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(

File diff suppressed because it is too large Load Diff

View File

@ -384,6 +384,7 @@
"Vouchers": "Voucher",
"Refill_Customer": "Isi Ulang Voucher",
"Recharge_Customer": "Isi Ulang Paket",
"Plan": "Paket",
"Plans": "Paket",
"PPPOE": "PPPOE",
"Bandwidth": "Bandwidth",
@ -911,5 +912,7 @@
"Mandatory_Fields": "Bidang yang wajib diisi",
"Single_Admin_Session": "Sesi Admin Tunggal",
"Mikrotik_SMS_Command": "Perintah SMS Mikrotik",
"Expired_Cronjob_Every_5_Minutes__Recommended_": "Cronjob Kedaluwarsa Setiap 5 Menit [Direkomendasikan]"
"Expired_Cronjob_Every_5_Minutes__Recommended_": "Cronjob Kedaluwarsa Setiap 5 Menit [Direkomendasikan]",
"Visit": "Kunjungi",
"sync": "Sinkron"
}

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

File diff suppressed because one or more lines are too long

BIN
system/uploads/paid.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

View File

@ -7,10 +7,25 @@ class customer_expired
public function getWidget()
{
global $ui, $current_date;
global $ui, $current_date, $config;
//user expire
$query = ORM::for_table('tbl_user_recharges')
->table_alias('tur')
->selects([
'c.id',
'tur.username',
'c.fullname',
'c.phonenumber',
'c.email',
'tur.expiration',
'tur.time',
'tur.recharged_on',
'tur.recharged_time',
'tur.namebp',
'tur.routers'
])
->innerJoin('tbl_customers', ['tur.customer_id', '=', 'c.id'], 'c')
->where_lte('expiration', $current_date)
->order_by_desc('expiration');
$expire = Paginator::findMany($query);
@ -23,8 +38,26 @@ class customer_expired
// Pass the total count and current page to the paginator
$paginator['total_count'] = $totalCount;
if(!empty($_COOKIE['expdef']) && $_COOKIE['expdef'] != $config['customer_expired_expdef']) {
$d = ORM::for_table('tbl_appconfig')->where('setting', 'customer_expired_expdef')->find_one();
if ($d) {
$d->value = $_COOKIE['expdef'];
$d->save();
} else {
$d = ORM::for_table('tbl_appconfig')->create();
$d->setting = 'customer_expired_expdef';
$d->value = $_COOKIE['expdef'];
$d->save();
}
}
if(!empty($config['customer_expired_expdef']) && empty($_COOKIE['expdef'])){
$_COOKIE['expdef'] = $config['customer_expired_expdef'];
setcookie('expdef', $config['customer_expired_expdef'], time() + (86400 * 30), "/");
}
// Assign the pagination HTML to the template variable
$ui->assign('expire', $expire);
$ui->assign('cookie', $_COOKIE);
return $ui->fetch('widget/customer_expired.tpl');
}
}

View File

@ -6,7 +6,11 @@ class graph_monthly_registered_customers
{
global $CACHE_PATH,$ui;
$cacheMRfile = File::pathFixer('/monthlyRegistered.temp');
$cacheMRfile = $CACHE_PATH . File::pathFixer('/monthlyRegistered.temp');
//Compatibility for old path
if (file_exists($oldCacheMRfile = str_replace($CACHE_PATH, '', $cacheMRfile))) {
rename($oldCacheMRfile, $cacheMRfile);
}
//Cache for 1 hour
if (file_exists($cacheMRfile) && time() - filemtime($cacheMRfile) < 3600) {
$monthlyRegistered = json_decode(file_get_contents($cacheMRfile), true);

View File

@ -10,7 +10,7 @@
<div class="box box-hovered mb20 box-primary">
<div class="box-header">
<h3 class="box-title">{Lang::T('Contributors')}</h3>
<h3 class="box-title">{Lang::T('Contribution')} PHPNuxBill</h3>
</div>
<div class="box-body">
<a href="https://github.com/hotspotbilling/phpnuxbill/graphs/contributors" target="_blank">
@ -22,9 +22,9 @@
<div class="col-sm-6">
<div class="box box-hovered mb20 box-primary">
<div class="box-header">
<h3 class="box-title">{Lang::T('Discussions')}</h3>
<h3 class="box-title">{Lang::T('Discussion Get Help from the Community')}</h3>
</div>
<div class="box-body">{Lang::T('Get help from community')}</div>
<div class="box-body">{Lang::T('Join the discussion to find solutions and support from a community ready to help.')}</div>
<div class="box-footer">
<div class="btn-group btn-group-justified" role="group" aria-label="...">
<a href="https://github.com/hotspotbilling/phpnuxbill/discussions" target="_blank"
@ -35,18 +35,6 @@
</div>
</div>
</div>
<div class="box box-hovered mb20 box-primary">
<div class="box-header">
<h3 class="box-title">{Lang::T('')}Feedback</h3>
</div>
<div class="box-body">
{Lang::T('Feedback and Bug Report')}
</div>
<div class="box-footer">
<a href="https://github.com/hotspotbilling/phpnuxbill/issues" target="_blank"
class="btn btn-primary btn-sm btn-block"><i class="ion ion-chatboxes"></i> {Lang::T('Give Feedback')}</a>
</div>
</div>
</div>
</div>
@ -54,10 +42,9 @@
<div class="col-sm-6">
<div class="box box-hovered mb20 box-primary">
<div class="box-header">
<h3 class="box-title">Donasi</h3>
<h3 class="box-title">{Lang::T('Donations')} 🇮🇩</h3>
</div>
<div class="box-body">Untuk pengembangan lebih baik, donasi ke iBNuX, donasi akan membantu terus
pengembangan aplikasi</div>
<div class="box-body">{Lang::T('To support further development, please donate to iBNuX. Your donation will help ensure the continued development of this application.')}</div>
<div class="table-responsive">
<table class="table table-bordered table-striped">
<tbody>
@ -89,10 +76,10 @@
<div class="col-sm-6">
<div class="box box-hovered mb20 box-primary">
<div class="box-header">
<h3 class="box-title">Donations</h3>
<h3 class="box-title">{Lang::T('Donations other than')} 🇮🇩</h3>
</div>
<div class="box-body">
Donations will help to continue phpnuxbill development
{Lang::T('Your donation will help support and continue the development of PHPNuxBill.')}
</div>
<div class="table-responsive">
<table class="table table-bordered table-striped">
@ -129,19 +116,21 @@
<div class="col-sm-6">
<div class="box box-hovered mb20 box-primary">
<div class="box-header">
<h3 class="box-title">{Lang::T('Chat with me')}</h3>
<h3 class="box-title">{Lang::T('Chat with Me — Paid Support $50')}</h3>
</div>
<div class="box-body">
{Lang::T('Confirm your donation to continue this paid support. Or, ask about alternative donations available to suit your needs.')}
</div>
<div class="box-body">{Lang::T('$50 Paid Support')}<br>{Lang::T('donation confirmation?')}<br>{Lang::T('Or ask any Donation Alternative')}</div>
<div class="box-footer">
<a href="https://t.me/ibnux" target="_blank" class="btn btn-primary btn-sm btn-block">Telegram</a>
<a href="https://t.me/ibnux" target="_blank" class="btn btn-primary btn-sm btn-block">{Lang::T('Telegram')}</a>
</div>
</div>
<div class="box box-primary box-hovered mb20 activities">
<div class="box-header">
<h3 class="box-title">{Lang::T('Free WhatsApp Gateway and Telegram Bot creater')}</h3>
<h3 class="box-title">{Lang::T('WhatsApp Gateway and Free Telegram Bot')}</h3>
</div>
<div class="box-body">
{Lang::T('There is a Telegram bot wizard in here')}
{Lang::T('Connect your PHPNuxBill to WhatsApp efficiently using WhatsApp Gateway. Also, create Telegram bots easily and practically.')}
</div>
<div class="box-footer">
<a href="https://wa.nux.my.id/login" target="_blank"
@ -155,34 +144,127 @@
<h3 class="box-title">PHPNUXBILL</h3>
</div>
<div class="box-body">
<b>PHPNuxBill</b> {Lang::T('is a billing Hotspot and PPPOE for Mikrotik using PHP and Mikrotik API to comunicate
with router. If you get more profit with this application, please donate us.')}<br>{Lang::T('Watch project')} <a
href="https://github.com/hotspotbilling/phpnuxbill" target="_blank">{Lang::T('in here')}</a>
<b>PHPNuxBill</b>
{Lang::T('is a Hotspot and PPPoE billing platform for Mikrotik developed using PHP. The application uses Mikrotik API to communicate with the router, ensuring efficient and easy integration. If you feel you get more benefits from this application, we would greatly appreciate your contribution through donation.')}<br>{Lang::T('Watch project ')} <a
href="https://github.com/hotspotbilling/phpnuxbill" target="_blank">{Lang::T('IN HERE')}</a>
</div>
<div class="box-footer" id="currentVersion">ver</div>
<div class="box-footer" id="latestVersion">ver</div>
<div class="box-footer">
<div class="btn-group btn-group-justified" role="group" aria-label="...">
<a href="./update.php" class="btn btn-success btn-sm btn-block">{Lang::T('Install Latest Version')}</a>
<a href="./update.php"
class="btn btn-success btn-sm btn-block">{Lang::T('Install Latest Version')}</a>
<a href="https://github.com/hotspotbilling/phpnuxbill/archive/refs/heads/master.zip" target="_blank"
class="btn btn-warning btn-sm btn-block text-black">{Lang::T('Download Latest Version')}</a>
</div>
<center><a href="{Text::url('community/rollback')}" class="btn btn-link btn-sm btn-block">{Lang::T('Select Old Version')}</a>
<center><a href="{Text::url('community/rollback')}"
class="btn btn-link btn-sm btn-block">{Lang::T('Select Old Version')}</a>
</center>
</div>
<div class="box-footer">
<div class="btn-group btn-group-justified" role="group" aria-label="...">
<a href="./CHANGELOG.md" target="_blank" class="btn btn-default btn-sm btn-block">{Lang::T('Current Changelog')}</a>
<a href="./CHANGELOG.md" target="_blank"
class="btn btn-default btn-sm btn-block">{Lang::T('Current Changelog')}</a>
<a href="https://github.com/hotspotbilling/phpnuxbill/blob/master/CHANGELOG.md" target="_blank"
class="btn btn-default btn-sm btn-block">{Lang::T('Repo Changelog')}</a>
</div>
</div>
<div class="box-footer">
{Lang::T('If you Download manual the update file, sometime update change database, after uploading, click this
button to update database structure.')}
{Lang::T('If you download the update file manually, sometimes the update may change the database structure. After the file is successfully uploaded, click this button to update the database structure.')}
<a href="./update.php?step=4" class="btn btn-default btn-sm btn-block">{Lang::T('Update Database')}</a>
</div>
</div>
<div class="box box-hovered mb20 box-primary">
<div class="box-header">
<h3 class="box-title">{Lang::T('Credits')}</h3>
</div>
<div class="box-body">
<div class="table-responsive">
<table class="table table-bordered table-striped table-hover">
<thead>
<tr>
<th>{Lang::T('Souce')}</th>
<th>{Lang::T('Details')}</th>
</tr>
</thead>
<tbody>
<tr>
<td>Bootstrap V3</td>
<td>
<a href="https://getbootstrap.com/docs/3.4/" target="_blank">
<i class="glyphicon glyphicon-globe"></i> {Lang::T('Visit')}
</a>
</td>
</tr>
<tr>
<td>Admin LTE V3</td>
<td>
<a href="https://adminlte.io/themes/v3/" target="_blank">
<i class="glyphicon glyphicon-globe"></i> {Lang::T('Visit')}
</a>
</td>
</tr>
<tr>
<td>Smarty Template V4</td>
<td>
<a href="https://www.smarty.net/" target="_blank">
<i class="glyphicon glyphicon-globe"></i> {Lang::T('Visit')}
</a>
</td>
</tr>
<tr>
<td>PHP IdiORM</td>
<td>
<a href="https://idiorm.readthedocs.io/" target="_blank">
<i class="glyphicon glyphicon-globe"></i> {Lang::T('Visit')}
</a>
</td>
</tr>
<tr>
<td>PHP mPDF</td>
<td>
<a href="https://mpdf.github.io/" target="_blank">
<i class="glyphicon glyphicon-globe"></i> {Lang::T('Visit')}
</a>
</td>
</tr>
<tr>
<td>PHP QRCode</td>
<td>
<a href="http://phpqrcode.sourceforge.net/" target="_blank">
<i class="glyphicon glyphicon-globe"></i> {Lang::T('Visit')}
</a>
</td>
</tr>
<tr>
<td>PHP Net_RouterOS</td>
<td>
<a href="https://github.com/pear2/Net_RouterOS" target="_blank">
<i class="glyphicon glyphicon-globe"></i> {Lang::T('Visit')}
</a>
</td>
</tr>
<tr>
<td>Summernote</td>
<td>
<a href="https://summernote.org/" target="_blank">
<i class="glyphicon glyphicon-globe"></i> {Lang::T('Visit')}
</a>
</td>
</tr>
<tr>
<td>PHP Mailer</td>
<td>
<a href="https://github.com/PHPMailer/PHPMailer/" target="_blank">
<i class="glyphicon glyphicon-globe"></i> {Lang::T('Visit')}
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<script>

View File

@ -90,7 +90,7 @@
<label class="col-md-3 control-label">{Lang::T('Coordinates')}</label>
<div class="col-md-9">
<input name="coordinates" id="coordinates" class="form-control" value=""
placeholder="6.465422, 3.406448">
placeholder="-6.465422, 3.406448">
<div id="map" style="width: '100%'; height: 200px; min-height: 150px;"></div>
</div>
</div>
@ -98,9 +98,11 @@
<div class="panel-heading">PPPoE</div>
<div class="panel-body">
<div class="form-group">
<label class="col-md-3 control-label">{Lang::T('Usernames')} <span class="label label-danger" id="warning_username"></span></label>
<label class="col-md-3 control-label">{Lang::T('Usernames')} <span class="label label-danger"
id="warning_username"></span></label>
<div class="col-md-9">
<input type="username" class="form-control" id="pppoe_username" name="pppoe_username" onkeyup="checkUsername(this, '0')">
<input type="username" class="form-control" id="pppoe_username" name="pppoe_username"
onkeyup="checkUsername(this, '0')">
<span class="help-block">{Lang::T('Not Working for freeradius')}</span>
</div>
</div>
@ -112,9 +114,11 @@
</div>
</div>
<div class="form-group">
<label class="col-md-3 control-label">Remote IP <span class="label label-danger" id="warning_ip"></span></label>
<label class="col-md-3 control-label">Remote IP <span class="label label-danger"
id="warning_ip"></span></label>
<div class="col-md-9">
<input type="text" class="form-control" id="pppoe_ip" name="pppoe_ip" onkeyup="checkIP(this, '0')">
<input type="text" class="form-control" id="pppoe_ip" name="pppoe_ip"
onkeyup="checkIP(this, '0')">
<span class="help-block">{Lang::T('Also Working for freeradius')}</span>
</div>
</div>
@ -204,7 +208,8 @@
</div>
</div>
<center>
<button class="btn btn-primary" onclick="return ask(this, '{Lang::T("Continue the process of adding Customer Data?")}')" type="submit">
<button class="btn btn-primary"
onclick="return ask(this, '{Lang::T("Continue the process of adding Customer Data?")}')" type="submit">
{Lang::T('Save Changes')}
</button>
<br><a href="{Text::url('customers/list')}" class="btn btn-link">{Lang::T('Cancel')}</a>

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

@ -85,22 +85,32 @@
listAtts.forEach(function(el) {
if (el.addEventListener) { // all browsers except IE before version 9
el.addEventListener("click", function() {
var txt = $(this).html();
$(this).html(
`<span class="loading"></span>`
);
setTimeout(() => {
$(this).prop("disabled", true);
}, 100);
setTimeout(() => {
$(this).html(txt);
$(this).prop("disabled", false);
}, 5000);
}, false);
} else {
if (el.attachEvent) { // IE before version 9
el.attachEvent("click", function() {
var txt = $(this).html();
$(this).html(
`<span class="loading"></span>`
);
setTimeout(() => {
$(this).prop("disabled", true);
}, 100);
setTimeout(() => {
$(this).html(txt);
$(this).prop("disabled", false);
}, 5000);
});
}
}

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'}

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

@ -1,16 +1,22 @@
{include file="sections/header.tpl"}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/1.3.4/jspdf.min.js"></script>
<div class="row">
<div class="col-md-6 col-sm-12 col-md-offset-3">
<div class="panel panel-hovered panel-primary panel-stacked mb30">
<div class="panel-heading">{$in['invoice']}</div>
<div class="panel-body">
{if !empty($logo)}
<center><img src="{$app_url}/{$logo}?"></center>
{/if}
<form class="form-horizontal" method="post" action="{Text::url('')}plan/print" target="_blank">
<pre id="content" style="text-align: center;"></pre>
<pre id="content"
style="border: 0px; ;text-align: center; background-color: transparent; background-image: url('{$app_url}/system/uploads/paid.png');background-repeat:no-repeat;background-position: center"></pre>
<textarea class="hidden" id="formcontent" name="content">{$invoice}</textarea>
<input type="hidden" name="id" value="{$in['id']}">
<a href="{Text::url('')}plan/list" class="btn btn-default btn-sm"><i
<a href="{Text::url('plan/list')}" class="btn btn-default btn-sm"><i
class="ion-reply-all"></i>{Lang::T('Finish')}</a>
<a href="javascript:download()" class="btn btn-success btn-sm text-black">
<i class="glyphicon glyphicon-share"></i> Download</a>
<a href="https://api.whatsapp.com/send/?text={$whatsapp}" target="_blank"
class="btn btn-primary btn-sm">
<i class="glyphicon glyphicon-share"></i> WhatsApp</a>
@ -32,13 +38,49 @@
class="btn btn-success text-black btn-sm hidden-xs hidden-sm" target="_blank">
<i class="glyphicon glyphicon-phone"></i>
NuxPrint
</a>
</a><br><br>
<input type="text" class="form-control form-sm" style="border: 0px; padding: 1px; background-color: white;" readonly onclick="this.select()" value="{$public_url}">
</form>
</div>
</div>
</div>
</div>
<script type="text/javascript">
<script>
const canvas = document.createElement("canvas");
const ctx = canvas.getContext('2d');
ctx.font = '16px Courier';
var text = document.getElementById("formcontent").innerHTML;
var lines = text.split(/\r\n|\r|\n/).length;
var meas = ctx.measureText("A");
let width = Math.round({$_c['printer_cols']} * 9.6);
var height = Math.round((14 * lines));
console.log(width, height, lines);
var paid = new Image();
paid.src = '{$app_url}/system/uploads/paid.png';
{if !empty($logo)}
var img = new Image();
img.src = '{$app_url}/{$logo}?{time()}';
var new_width = (width / 4) * 2;
var new_height = Math.ceil({$hlogo} * (new_width/{$wlogo}));
height = height + new_height;
{/if}
function download() {
var doc = new jsPDF('p', 'px', [width, height]);
{if !empty($logo)}
try {
doc.addImage(img, 'PNG', (width - new_width) / 2, 10, new_width, new_height);
} catch (err) {}
{/if}
try {
doc.addImage(paid, 'PNG', (width - 200) / 2, (height - 145) / 2, 200, 145);
} catch (err) {}
doc.setFont("Courier");
doc.setFontSize(16);
doc.text($('#formcontent').html(), width / 2, new_height + 30, 'center');
doc.save('{$in['invoice']}.pdf');
}
var s5_taf_parent = window.location;
document.getElementById('content').innerHTML = document.getElementById('formcontent').innerHTML;
</script>

View File

@ -55,7 +55,7 @@
{$plan['validity_unit']}</span>
</li>
<li class="list-group-item">
<b>{Lang::T('Using')}</b> <span class="pull-right">
<b>{Lang::T('Payment via')}</b> <span class="pull-right">
<select name="using"
style="background-color: white;outline: 1px;border: 1px solid #b7b7b7;">
{foreach $usings as $us}

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">
@ -649,6 +680,24 @@
value="{$_c['minimum_transfer']}">
</div>
</div>
<div class="form-group">
<label class="col-md-3 control-label">{Lang::T('Allow Balance Custom
Amount')}</label>
<div class="col-md-5">
<select name="allow_balance_custom" id="allow_balance_custom" class="form-control">
<option value="no">
{Lang::T('No')}
</option>
<option value="yes" {if $_c['allow_balance_custom']=='yes' }selected="selected" {/if}>
{Lang::T('Yes')}
</option>
</select>
</div>
<p class="help-block col-md-4"><small>
{Lang::T('Allow Customer buy balance with any amount')}
</small>
</p>
</div>
<button class="btn btn-success btn-block" type="submit">
{Lang::T('Save Changes')}
</button>

View File

@ -175,24 +175,6 @@
extend')}</small>
</p>
</div>
<div class="form-group has-error">
<label class="col-md-3 control-label">{Lang::T('Allow Balance Custom
Amount')}</label>
<div class="col-md-5">
<select name="allow_balance_custom" id="allow_balance_custom" class="form-control">
<option value="no">
{Lang::T('No')}
</option>
<option value="yes" {if $_c['allow_balance_custom']=='yes' }selected="selected" {/if}>
{Lang::T('Yes')}
</option>
</select>
</div>
<p class="help-block col-md-4"><small>
{Lang::T('Allow Customer buy balance with any amount')}
<br>*Please report any issue or bugs</small>
</p>
</div>
</div>
</div>

View File

@ -113,6 +113,8 @@
<b>[[expired_date]]</b> - {Lang::T('Expired datetime')}.<br>
<b>[[footer]]</b> - {Lang::T('Invoice Footer')}.<br>
<b>[[note]]</b> - {Lang::T('For Notes by admin')}.<br>
<b>[[invoice_link]]</b> - <a href="{$app_url}/docs/#Reminder%20with%20payment%20link"
target="_blank">{Lang::T("read documentation")}</a>.
</p>
</div>
</div>
@ -191,7 +193,43 @@
</div>
</div>
{/if}
{* <div class="panel-body">
<div class="form-group">
<label class="col-md-2 control-label">{Lang::T('PDF Invoice Template')}</label>
<div class="col-md-6">
<textarea class="form-control" id="email_invoice" name="email_invoice"
placeholder="{Lang::T('Template for sending pdf invoice')}" rows="20">
{if !empty($_json['email_invoice'])}
{Lang::htmlspecialchars($_json['email_invoice'])}
{else}
{Lang::htmlspecialchars($_default['email_invoice'])}
{/if}
</textarea>
</div>
<p class="col-md-4 help-block">
<b>[[company_name]]</b> {Lang::T('Your Company Name at Settings')}.<br>
<b>[[company_address]]</b> {Lang::T('Your Company Address at Settings')}.<br>
<b>[[company_phone]]</b> - {Lang::T('Your Company Phone at Settings')}.<br>
<b>[[invoice]]</b> - {Lang::T('Invoice number')}.<br>
<b>[[created_at]]</b> - {Lang::T('Date invoice created')}.<br>
<b>[[payment_gateway]]</b> - {Lang::T('Payment gateway user paid from')}.<br>
<b>[[payment_channel]]</b> - {Lang::T('Payment channel user paid from')}.<br>
<b>[[bill_rows]]</b> - {Lang::T('Bills table, where bills are listed')}.<br>
<b>[[currency]]</b> - {Lang::T('Your currency code at localisation Settings')}.<br>
<b>[[status]]</b> - {Lang::T('Invoice status')}.<br>
<b>[[fullname]]</b> - {Lang::T('Receiver name')}.<br>
<b>[[user_name]]</b> - {Lang::T('Username internet')}.<br>
<b>[[email]]</b> - {Lang::T('Customer email')} .<br>
<b>[[phone]]</b> - {Lang::T('Customer phone')}. <br>
<b>[[address]]</b> - {Lang::T('Customer phone')}. <br>
<b>[[expired_date]]</b> - {Lang::T('Expired datetime')}.<br>
<b>[[logo]]</b> - {Lang::T('Your company logo at Settings')}.<br>
<b>[[due_date]]</b> - {Lang::T('Invoice Due date, 7 Days after invoice created')}.<br>
<b>[[payment_link]]</b> - <a href="{$app_url}/docs/#Reminder%20with%20payment%20link"
target="_blank">{Lang::T("read documentation")}</a>.
</p>
</div>
</div> *}
<div class="panel-body">
<div class="form-group">

View File

@ -96,16 +96,16 @@
const savedMode = localStorage.getItem('mode');
if (savedMode === 'dark') {
body.classList.add('dark-mode');
toggleIcon.textContent = '🌜';
toggleIcon.textContent = '🌞';
}
function setMode(mode) {
if (mode === 'dark') {
body.classList.add('dark-mode');
toggleIcon.textContent = '🌜';
toggleIcon.textContent = '🌞';
} else {
body.classList.remove('dark-mode');
toggleIcon.textContent = '🌞';
toggleIcon.textContent = '🌜';
}
}
@ -160,21 +160,40 @@
});
function ask(field, text) {
var txt = field.innerHTML;
if (confirm(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 = "";

View File

@ -44,7 +44,7 @@
<ul class="nav navbar-nav">
<li>
<a class="toggle-container" href="#">
<i class="toggle-icon" id="toggleIcon">🌞</i>
<i class="toggle-icon" id="toggleIcon">🌜</i>
</a>
</li>
<li class="dropdown tasks-menu">
@ -164,7 +164,7 @@
<li {if $_system_menu eq 'history'}class="active" {/if}>
<a href="{Text::url('order/history')}">
<i class="fa fa-file-text"></i>
<span>{Lang::T('Order History')}</span>
<span>{Lang::T('Payment History')}</span>
</a>
</li>
{/if}

View File

@ -1,21 +1,70 @@
{if empty($_user)}
{include file="customer/header-public.tpl"}
{else}
{include file="customer/header.tpl"}
{/if}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/1.3.4/jspdf.min.js"></script>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<div class="panel panel-hovered panel-primary panel-stacked mb30">
<div class="panel-heading">{$in['invoice']}</div>
<div class="panel-body">
{if !empty($logo)}
<center><img src="{$app_url}/{$logo}"></center>
{/if}
<form class="form-horizontal" method="post" action="{Text::url('plan/print')}" target="_blank">
<pre id="content" style="text-align: center;">{$invoice}</pre>
<pre id="content"
style="border: 0px; ;text-align: center; background-color: transparent; background-image: url('{$app_url}/system/uploads/paid.png');background-repeat:no-repeat;background-position: center">{$invoice}</pre>
<input type="hidden" name="id" value="{$in['id']}">
{if !empty($_user)}
<a href="{Text::url('voucher/list-activated')}" class="btn btn-default btn-sm"><i
class="ion-reply-all"></i>{Lang::T('Finish')}</a>
<a href="https://api.whatsapp.com/send/?text={$whatsapp}" target="_blank"
class="btn btn-primary btn-sm">
{/if}
<a href="javascript:download()" class="btn btn-success btn-sm text-black">
<i class="glyphicon glyphicon-share"></i> Download</a>
<a href="https://api.whatsapp.com/send/?text={$whatsapp}" class="btn btn-primary btn-sm">
<i class="glyphicon glyphicon-share"></i> WhatsApp</a>
<br><br>
<input type="text" class="form-control form-sm" style="border: 0px; padding: 1px; background-color: white;" readonly onclick="this.select()" value="{$public_url}">
</form>
</div>
</div>
</div>
</div>
<script>
const canvas = document.createElement("canvas");
const ctx = canvas.getContext('2d');
ctx.font = '16px Courier';
var text = document.getElementById("content").innerHTML;
var lines = text.split(/\r\n|\r|\n/).length;
var meas = ctx.measureText("A");
let width = Math.round({$_c['printer_cols']} * 9.6);
var height = Math.round((14 * lines));
console.log(width, height, lines);
var paid = new Image();
paid.src = '{$app_url}/system/uploads/paid.png';
{if !empty($logo)}
var img = new Image();
img.src = '{$app_url}/{$logo}?{time()}';
var new_width = (width / 4) * 2;
var new_height = Math.ceil({$hlogo} * (new_width/{$wlogo}));
height = height + new_height;
{/if}
function download() {
var doc = new jsPDF('p', 'px', [width, height]);
{if !empty($logo)}
try {
doc.addImage(img, 'PNG', (width - new_width) / 2, 10, new_width, new_height);
} catch (err) {}
{/if}
try {
doc.addImage(paid, 'PNG', (width - 200) / 2, (height - 145) / 2, 200, 145);
} catch (err) {}
doc.setFont("Courier");
doc.setFontSize(16);
doc.text($('#content').html(), width / 2, new_height + 30, 'center');
doc.save('{$in['invoice']}.pdf');
}
</script>
{include file="customer/footer.tpl"}

View File

@ -3,13 +3,12 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>{$_title} - {$_c['CompanyName']}</title>
<link rel="shortcut icon" href="./{$favicon}" type="image/x-icon" />
<link rel='stylesheet' href='https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css' />
<link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css' />
<!-- SweetAlert CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.css" />
<!-- SweetAlert JS -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.all.min.js"></script>
<style>
.login-fg .form-container {
@ -364,28 +363,6 @@
color: #db4437;
}
@media (max-width: 1200px) {
.login-fg .info h1 {
font-size: 45px;
}
}
@media (max-width: 992px) {
.login-fg .bg {
display: none;
}
}
@media (max-width: 768px) {
.login-fg .login-section .social li a {
width: 100px;
}
.login-fg .logo a {
font-size: 26px;
}
}
footer {
position: absolute;
bottom: 0;
@ -429,6 +406,47 @@
background-color: #00f2fe;
}
.submit-btn {
transition: all 0.3s ease;
-webkit-tap-highlight-color: transparent;
}
.submit-btn:active {
transform: scale(0.98);
}
input {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
.form-group {
margin-bottom: 15px;
}
.logo img {
max-width: 100%;
height: auto;
}
input[type="text"],
input[type="password"] {
inputmode: "verbatim";
}
@media (max-width: 1200px) {
.login-fg .info h1 {
font-size: 45px;
}
}
@media (max-width: 992px) {
.login-fg .bg {
display: none;
}
}
@media (max-width: 768px) {
input,
@ -436,14 +454,76 @@
padding: 12px;
font-size: 15px;
}
.login-fg .login-section {
padding: 15px;
max-width: 100%;
}
.login-fg .logo img {
max-width: 200px;
height: auto;
}
.login-fg .info h1 {
font-size: 32px;
margin-bottom: 10px;
}
.login-fg .info p {
font-size: 14px;
line-height: 1.5;
}
footer {
position: fixed;
margin-top: 20px;
padding: 8px;
font-size: 12px;
}
.login-fg .login-section h4 {
font-size: 18px;
margin-bottom: 20px;
}
.login-fg .login-section .social li a {
width: 100%;
margin: 5px 0;
}
.checkbox a {
display: block;
text-align: right;
margin-top: 10px;
}
.login-fg .login-section .social li a {
width: 100px;
}
.login-fg .logo a {
font-size: 26px;
}
}
@media (max-width: 480px) {
.login-fg .login {
padding: 15px;
}
input,
.submit-btn {
padding: 10px;
font-size: 14px;
font-size: 15px;
}
.login-fg .login-section h4 {
font-size: 16px;
}
footer {
font-size: 11px;
}
}
@ -600,6 +680,32 @@
</script>
<!--End of Tawk.to Script-->
{/if}
<script>
document.body.classList.add('touch-device');
</script>
<script>
document.addEventListener('DOMContentLoaded', function () {
let metaViewport = document.querySelector('meta[name="viewport"]');
const originalContent = metaViewport.content;
document.querySelectorAll('input').forEach(input => {
input.addEventListener('focus', () => {
metaViewport.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0';
});
input.addEventListener('blur', () => {
metaViewport.content = originalContent;
});
});
// Touch detection
if ('ontouchstart' in window) {
document.body.classList.add('touch-device');
} else {
document.body.classList.add('no-touch');
}
});
</script>
</body>
</html>

View File

@ -0,0 +1,448 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>{$_title} - {$_c['CompanyName']}</title>
<link rel="shortcut icon" href="./{$favicon}" type="image/x-icon" />
<link rel='stylesheet' href='https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css' />
<link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css' />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.css" />
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.all.min.js"></script>
<style>
.login-fg .form-container {
color: #ccc;
position: relative;
}
.login-fg .login {
min-height: 100vh;
position: relative;
display: flex;
justify-content: center;
align-items: center;
padding: 30px 15px;
}
.login-fg .login-section {
max-width: 370px;
margin: 0 auto;
text-align: center;
width: 100%;
}
.login-fg .form-fg {
width: 100%;
text-align: center;
}
.login-fg .form-container .form-group {
margin-bottom: 25px;
}
.login-fg .form-container .input-text {
font-size: 14px;
outline: none;
color: #616161;
border-radius: 3px;
font-weight: 500;
border: 1px solid transparent;
background: #fff;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
}
.login-fg .form-container img {
margin-bottom: 5px;
height: 40px;
}
.login-fg .form-container .form-fg input {
float: left;
width: 100%;
padding: 11px 45px 11px 20px;
border-radius: 50px;
}
.login-fg .form-container .form-fg i {
position: absolute;
top: 13px;
right: 20px;
font-size: 19px;
color: #616161;
}
.login-fg .form-container label {
font-weight: 500;
font-size: 14px;
margin-bottom: 5px;
}
.login-fg .form-container .btn-md {
cursor: pointer;
padding: 10px 30px 9px;
height: 45px;
letter-spacing: 1px;
font-size: 14px;
font-weight: 400;
font-family: "Open Sans", sans-serif;
border-radius: 50px;
color: #d6d6d6;
}
.login-fg .form-container .btn-fg {
background: #0f96f9;
border: none;
color: #fff;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
}
.login-fg .form-container .btn-fg:hover {
background: #108ae4;
}
footer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
text-align: center;
font-size: 14px;
color: inherit;
background-color: #f8f8f8;
padding: 10px;
}
input {
width: 100%;
padding: 10px;
margin: 1px 0;
border: 1px solid #ccc;
border-radius: 5px;
font-size: 16px;
box-sizing: border-box;
}
input:focus {
border-color: #4facfe;
outline: none;
}
.submit-btn {
background-color: #4facfe;
color: white;
border: none;
padding: 15px;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
width: 100%;
box-sizing: border-box;
}
.submit-btn:hover {
background-color: #00f2fe;
}
.submit-btn {
transition: all 0.3s ease;
-webkit-tap-highlight-color: transparent;
}
.submit-btn:active {
transform: scale(0.98);
}
.login-section {
max-height: 100vh;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
-ms-overflow-style: -ms-autohiding-scrollbar;
scrollbar-width: thin;
scrollbar-color: #888 #f1f1f1;
}
.login-section::-webkit-scrollbar {
width: 8px;
}
.login-section::-webkit-scrollbar-track {
background: #f1f1f1;
}
.login-section::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
.login-section::-webkit-scrollbar-thumb:hover {
background: #555;
}
.login-fg .info h1 {
font-size: 60px;
color: #fff;
font-weight: 700;
margin-bottom: 15px;
text-transform: uppercase;
text-shadow: 2px 0px #000;
}
.login-fg .info p {
margin-bottom: 0;
color: #fff;
line-height: 28px;
text-shadow: 1px 1px #000;
}
.login-fg .info {
text-align: center;
position: fixed;
top: 50%;
left: 33%;
transform: translate(-50%, -50%);
z-index: 1;
}
@media (max-width: 768px) {
input,
.submit-btn {
padding: 12px;
font-size: 15px;
}
}
@media (max-width: 1200px) {
.login-fg .info h1 {
font-size: 45px;
}
}
@media (max-width: 992px) {
.login-fg .bg {
display: none;
}
}
@media (max-width: 768px) {
input,
.submit-btn {
padding: 12px;
font-size: 15px;
}
.login-fg .login-section {
padding: 15px;
max-width: 100%;
}
.login-fg .logo img {
max-width: 200px;
height: auto;
}
.login-fg .info h1 {
font-size: 32px;
margin-bottom: 10px;
}
.login-fg .info p {
font-size: 14px;
line-height: 1.5;
}
footer {
position: fixed;
margin-top: 20px;
padding: 8px;
font-size: 12px;
}
.login-fg .login-section h4 {
font-size: 18px;
margin-bottom: 20px;
}
.login-fg .login-section .social li a {
width: 100%;
margin: 5px 0;
}
.checkbox a {
display: block;
text-align: right;
margin-top: 10px;
}
.login-fg .login-section .social li a {
width: 100px;
}
.login-fg .logo a {
font-size: 26px;
}
}
@media (max-width: 480px) {
.login-fg .login {
padding: 15px;
}
input,
.submit-btn {
padding: 10px;
font-size: 15px;
}
.login-fg .login-section h4 {
font-size: 16px;
}
footer {
font-size: 11px;
}
}
@media (max-width: 320px) {
input,
.submit-btn {
padding: 8px;
font-size: 12px;
}
}
</style>
</head>
<body>
<!-- SweetAlert Notification -->
{if isset($notify)}
<script>
document.body.style.overflow = 'hidden';
Swal.fire({
icon: '{if $notify_t == "s"}success{else}warning{/if}',
title: '{if $notify_t == "s"}Success{else}Error{/if}',
html: '{$notify}',
backdrop: 'rgba(0, 0, 0, 0.5)',
}).then(() => {
document.body.style.overflow = '';
});
</script>
{/if}
<div class="login-fg">
<div class="container-fluid">
<div class="row">
<div class="col-xl-8 col-lg-7 col-md-12 bg"
style="background-image:url('./{$wallpaper}'); background-attachment: fixed;">
<div class="info">
<h1>{$_c['login_page_head']}</h1>
<p>{$_c['login_page_description']}</p>
</div>
</div>
<div class="col-xl-4 col-lg-5 col-md-12 login">
<div class="login-section">
<div class="logo clearfix">
<a href="./{$login_logo}" target="_blank"><img src="./{$login_logo}" height="60"
alt="Logo"></a>
</div>
<br>
<h4>{Lang::T('Create your account')}</h4>
<div class="form-container">
<form id="register-form" method="POST" action="{Text::url('register/post')}">
<input type="hidden" name="csrf_token" value="{$csrf_token}">
<!-- Basic Information (Initially Visible) -->
<div id="basicFields">
<div class="form-group">
<input type="text" name="username"
placeholder="{if $_c['country_code_phone']!= '' || $_c['registration_username'] == 'phone'}{$_c['country_code_phone']} {Lang::T('Phone Number')}{elseif $_c['registration_username'] == 'email'}{Lang::T('Email')}{else}{Lang::T('Usernames')}{/if}">
</div>
{if $_c['photo_register'] == 'yes'}
<div class="form-group">
<input type="file" required id="photo" name="photo"
accept="image/*">
</div>
{/if}
<div class="form-group">
<input type="text" name="fullname" placeholder="{Lang::T('Full Name')}"
{if $_c['man_fields_fname'] neq 'no'}required{/if} >
</div>
<div class="form-group">
<input type="email" name="email" placeholder="{Lang::T('Email Address')}"
{if $_c['man_fields_email'] neq 'no'}required{/if}>
</div>
<div class="form-group">
<button type="button" onclick="toggleFields()" class="submit-btn">
{Lang::T('Next Step')}
</button>
</div>
</div>
<!-- Password Fields (Initially Hidden) -->
<div id="passwordFields" style="display: none;">
<div class="form-group">
<input type="text" name="address" placeholder="{Lang::T('Home Address')}"
{if $_c['man_fields_address'] neq 'no'}required{/if}>
</div>
<div class="form-group">
<input type="password" name="password" placeholder="{Lang::T('Password')}"
required>
</div>
<div class="form-group">
<input type="password" name="cpassword"
placeholder="{Lang::T('Confirm Password')}" required>
</div>
<div class="form-group">
<button id="register-btn" type="submit" class="submit-btn">
<span id="register-text">{Lang::T('Register')}</span>
</button>
</div>
</div>
</form>
</div>
<p>{Lang::T('Already have an account?')} <a href="{Text::url('login')}"
class="linkButton">{Lang::T('Login')}</a></p>
<footer>
© {$smarty.now|date_format:"%Y"} {$_c['CompanyName']}. All rights reserved. <br> <a
href="pages/Privacy_Policy.html">Privacy</a> | <a
href="pages/Terms_and_Conditions.html">Terms &amp; Conditions</a>
</footer>
</div>
</div>
</div>
</div>
</div>
<script>
const registerForm = document.getElementById('register-form');
const registerBtn = document.getElementById('register-btn');
const registerText = document.getElementById('register-text');
registerForm.addEventListener('submit', function (event) {
registerBtn.classList.add('loading');
registerText.textContent = 'Please Wait...';
});
function toggleFields() {
document.getElementById('basicFields').style.display = 'none';
document.getElementById('passwordFields').style.display = 'block';
const backButton = document.createElement('button');
backButton.className = 'submit-btn';
backButton.textContent = '← Back';
backButton.style.marginBottom = '15px';
backButton.onclick = () => {
document.getElementById('basicFields').style.display = 'block';
document.getElementById('passwordFields').style.display = 'none';
backButton.remove();
};
document.getElementById('passwordFields').prepend(backButton);
}
</script>
</body>
</html>

View File

@ -4,7 +4,20 @@
<table class="table table-condensed">
<thead>
<tr>
<th>{Lang::T('Username')}</th>
<th>
<select style="border: 0px; width: 100%; background-color: #f9f9f9;"
onchange="changeExpiredDefault(this)">
<option value="username" {if $cookie['expdef'] == 'username'}selected{/if}>
{Lang::T('Username')}
</option>
<option value="fullname" {if $cookie['expdef'] == 'fullname'}selected{/if}>
{Lang::T('Full Name')}</option>
<option value="phone" {if $cookie['expdef'] == 'phone'}selected{/if}>{Lang::T('Phone')}
</option>
<option value="email" {if $cookie['expdef'] == 'email'}selected{/if}>{Lang::T('Email')}
</option>
</select>
</th>
<th>{Lang::T('Created / Expired')}</th>
<th>{Lang::T('Internet Package')}</th>
<th>{Lang::T('Location')}</th>
@ -15,7 +28,17 @@
{assign var="rem_exp" value="{$expired['expiration']} {$expired['time']}"}
{assign var="rem_started" value="{$expired['recharged_on']} {$expired['recharged_time']}"}
<tr>
<td><a href="{Text::url('customers/viewu/',$expired['username'])}">{$expired['username']}</a></td>
<td><a href="{Text::url('customers/view/',$expired['id'])}">
{if $cookie['expdef'] == 'fullname'}
{$expired['fullname']}
{elseif $cookie['expdef'] == 'phone'}
{$expired['phonenumber']}
{elseif $cookie['expdef'] == 'email'}
{$expired['email']}
{else}
{$expired['username']}
{/if}
</a></td>
<td><small data-toggle="tooltip" data-placement="top"
title="{Lang::dateAndTimeFormat($expired['recharged_on'],$expired['recharged_time'])}">{Lang::timeElapsed($rem_started)}</small>
/
@ -31,3 +54,11 @@
</div>
&nbsp; {include file="pagination.tpl"}
</div>
<script>
function changeExpiredDefault(fl) {
setCookie('expdef', fl.value, 365);
setTimeout(() => {
location.reload();
}, 1000);
}
</script>

View File

@ -1,3 +1,3 @@
{
"version": "2025.3.10"
"version": "2025.3.20"
}