Compare commits
69 Commits
Author | SHA1 | Date | |
---|---|---|---|
8f099bdefb | |||
9fae0f84fd | |||
d50a66845f | |||
915af6fc03 | |||
|
0a29ec9a86 | ||
|
27fd677a0a | ||
|
e2f24c0cc6 | ||
|
cf60c470b1 | ||
|
1cc7057dca | ||
|
1740c568f9 | ||
|
3347b39f3b | ||
|
f0b9b56bb0 | ||
|
24b713804a | ||
|
182add517c | ||
|
45cc2afab5 | ||
|
ba19b1c569 | ||
|
5caa9f905b | ||
|
28541f366c | ||
|
ad7998ebbf | ||
|
4c64cfabd2 | ||
|
9bae41dbe7 | ||
|
bad0545be5 | ||
|
43a92c5d3b | ||
|
11e5ebe103 | ||
|
c573c49fb9 | ||
|
7bfbdb1efb | ||
|
dc28298d53 | ||
|
655e0494d3 | ||
|
4a441c5763 | ||
|
5072ff8ba2 | ||
|
d506dd66ff | ||
|
e9b0cfd8f0 | ||
|
aa4dbc0cea | ||
|
4ef054466d | ||
|
41a3cbe700 | ||
|
4938840c5d | ||
|
127d43e45d | ||
|
cdfbab7119 | ||
|
1a2b85ae4f | ||
|
2e2d967a5b | ||
|
c45e19189a | ||
|
d372bf4711 | ||
|
8b8a0357f0 | ||
|
1cb0e30e6b | ||
|
20916b44f0 | ||
|
c63545d33a | ||
|
84500cdfc9 | ||
|
5b21ffcde5 | ||
|
009040cd3c | ||
|
781481e118 | ||
|
66d67cb61d | ||
|
3372da2ac4 | ||
|
803d04a91d | ||
|
17de653752 | ||
|
9301f1058c | ||
|
0868d61271 | ||
|
5f353392e3 | ||
|
fdd8dad509 | ||
|
3563fa531b | ||
|
525f2311fc | ||
|
3b6a6d2f55 | ||
|
3cebfa2171 | ||
|
c65b569f94 | ||
|
ca5a7d60cf | ||
|
5987ffafce | ||
|
d7bbb4d18f | ||
|
e3c173bea4 | ||
|
78e1e2f989 | ||
|
3c7e6c7a64 |
5
.gitignore
vendored
5
.gitignore
vendored
@ -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
|
24
README.md
24
README.md
@ -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
|
||||
|
||||
[](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
1
docs/insomnia.rest.json
Normal file
File diff suppressed because one or more lines are too long
@ -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
643
login.html
Normal 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">© 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>
|
@ -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 "";
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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'];
|
||||
|
@ -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
347
system/autoload/Invoice.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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>
|
||||
|
@ -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']));
|
||||
|
27
system/controllers/invoices.php
Normal file
27
system/controllers/invoices.php
Normal 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');
|
||||
}
|
@ -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) {
|
||||
|
@ -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 .
|
||||
|
@ -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");
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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'));
|
||||
|
@ -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) {
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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
@ -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"
|
||||
}
|
||||
|
@ -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);"
|
||||
]
|
||||
}
|
0
system/uploads/invoices/index.html
Normal file
0
system/uploads/invoices/index.html
Normal file
File diff suppressed because one or more lines are too long
BIN
system/uploads/paid.png
Normal file
BIN
system/uploads/paid.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
BIN
system/vendor/mpdf/mpdf/ttfonts/DejaVuSansCondensed-Oblique.ttf
vendored
Normal file
BIN
system/vendor/mpdf/mpdf/ttfonts/DejaVuSansCondensed-Oblique.ttf
vendored
Normal file
Binary file not shown.
@ -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');
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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" }
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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'}
|
||||
|
57
ui/ui/admin/invoices/list.tpl
Normal file
57
ui/ui/admin/invoices/list.tpl
Normal file
@ -0,0 +1,57 @@
|
||||
{include file="sections/header.tpl"}
|
||||
|
||||
<!-- Add a Table for Sent History -->
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">{Lang::T('Invoices')}</div>
|
||||
<div class="panel-body" style="overflow: auto;">
|
||||
<table class="table table-bordered" id="invoiceTable" style="width:100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{Lang::T('Invoice No')}</th>
|
||||
<th>{Lang::T('Customer Name')}</th>
|
||||
<th>{Lang::T('Email')}</th>
|
||||
<th>{Lang::T('Address')}</th>
|
||||
<th>{Lang::T('Amount')}</th>
|
||||
<th>{Lang::T('Status')}</th>
|
||||
<th>{Lang::T('Created Date')}</th>
|
||||
<th>{Lang::T('Due Date')}</th>
|
||||
<th>{Lang::T('Actions')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{foreach $invoices as $invoice}
|
||||
<tr>
|
||||
<td>{$invoice->number}</td>
|
||||
<td>{$invoice->fullname}</td>
|
||||
<td>{$invoice->email}</td>
|
||||
<td>{$invoice->address}</td>
|
||||
<td>{$invoice->amount}</td>
|
||||
<td>
|
||||
{if $invoice->status == 'paid'}
|
||||
<span class="label label-success">{Lang::T('Paid')}</span>
|
||||
{elseif $invoice->status == 'unpaid'}
|
||||
<span class="label label-danger">{Lang::T('Unpaid')}</span>
|
||||
{else}
|
||||
<span class="label label-warning">{Lang::T('Pending')}</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td>{$invoice->created_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"}
|
@ -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>
|
||||
`);
|
||||
}
|
||||
|
@ -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"}
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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 = "";
|
||||
|
@ -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}
|
||||
|
@ -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"}
|
@ -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>
|
448
ui/ui/customer/reg-login-custom-moon.tpl
Normal file
448
ui/ui/customer/reg-login-custom-moon.tpl
Normal 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 & 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>
|
@ -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>
|
||||
{include file="pagination.tpl"}
|
||||
</div>
|
||||
<script>
|
||||
function changeExpiredDefault(fl) {
|
||||
setCookie('expdef', fl.value, 365);
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 1000);
|
||||
}
|
||||
</script>
|
@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "2025.3.10"
|
||||
"version": "2025.3.20"
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user