App Permission and Event Load
August 7, 2025 · AI Conversations
if (cordova.platformId === 'android') {
var permissions = cordova.plugins.permissions;
permissions.checkPermission(permissions.CAMERA, function (status) {
if (!status.hasPermission) {
// Request permission
permissions.requestPermission(
permissions.CAMERA,
function (status) {
if (status.hasPermission) {
navigator.splashscreen.hide(); // Hide splash after permission
initializeApp(); // Proceed with your app
} else {
navigator.app.exitApp(); // Exit if denied
}
},
function () {
navigator.app.exitApp(); // Exit on request error
}
);
} else {
navigator.splashscreen.hide();
initializeApp(); // Permission already granted
}
});
} else {
navigator.splashscreen.hide(); // For browser/iOS fallback
initializeApp();
}
}, false);
function initializeApp() {
window.TOKEN = 'fossettscircustoken';
window.eventId = -1;
window.eventDateId = -1;
window.ApiUrl = 'https://staging.fossettscircus.com/boxoffice/index.php/api/';
var scanConfig = {
preferFrontCamera : false, // iOS and Android
showFlipCameraButton : true, // iOS and Android
showTorchButton : true, // iOS and Android
torchOn: false, // Android, launch with the torch switched on (if available)
saveHistory: false, // Android, save scan history (default false)
prompt : "Place a code inside the scan area", // Android
resultDisplayDuration: 500, // Android, display scanned text for X ms. 0 suppresses it entirely, default 1500
formats : "QR_CODE", // default: all but PDF_417 and RSS_EXPANDED
//orientation : "landscape", // Android only (portrait|landscape), default unset so it rotates with the device
disableAnimations : true, // iOS
disableSuccessBeep: false // iOS and Android
};
$(document).ready(function()
{
$(document).on('page:init','.page[data-name="event-details"]',function (e) {
const page = e.detail;
const title = page.route.query.title || 'No Title';
const location = page.route.query.location || '';
const time = page.route.query.time || '';
const code = page.route.query.code || '';
eventDateId = page.route.query.eventDateId || -1;
eventId = page.route.query.eventId || -1;
console.log(title, location, time, code);
page.$el.find('.event-title').text(title);
page.$el.find('.event-location').text(location);
page.$el.find('.event-time').text(time);
page.$el.find('input[name="code"]').val(code);
var sendCode = function(code,type)
{
if(type=='qr')
f7App.dialog.preloader('Scanning code…');
else
f7App.dialog.preloader('Sending code…');
$('.scan').prop('disabled', true);
$('.manually').prop('disabled', true);
$('#scan-result').removeClass('bg-success').addClass('bg-dark').html('Code '+ code +' sent to boxoffice, plase wait…');
$.ajax({
url: `${ApiUrl}scan`,
method: 'POST',
data: {
token: TOKEN,
event_date_id: eventDateId,
order_code: code
},
success: function(result,status,xhr)
{
var apiResult = JSON.parse(result);
if ( apiResult.result )
{
$('#scan-result').removeClass('bg-dark').addClass('bg-success').html(apiResult.message+''+apiResult.seating+''+apiResult.tickets);
f7App.dialog.alert(apiResult.message, 'Scan Failed');
// reset input to set next code
$('[name="code"]').val(eventId +'.'+ eventDateId +'.');
}
else
{
$('#scan-result').removeClass('bg-success').addClass('bg-dark').html(apiResult.message);
f7App.dialog.alert(apiResult.message, 'Scan Failed');
}
},
error: function(xhr,status,error)
{
$('#scan-result').removeClass('bg-success').addClass('bg-dark').html('Code '+ result.text +' could not be sent to boxoffice, please try again');
f7App.dialog.alert(`${result.text} could not be sent to boxoffice, please try again`, 'Scan Failed');
},
complete: function()
{
$('.scan').prop('disabled', false);
$('.manually').prop('disabled', false);
f7App.dialog.close();
}
});
}
var scanError = function (error)
{
$('#scan-result').removeClass('bg-success').addClass('bg-dark').html("Scanning failed: " + error);
//f7App.popup.open('#modal-scan-result');
};
var scanSuccess = function(result)
{
if (! result.cancelled )
{
if (eventDateId > -1)
{
sendCode(result.text,'qr');
}
else
{
$('#scan-result').removeClass('bg-success').addClass('bg-dark').html('You must select an event date to scan tickets');
}
}
else
{
$('#scan-result').removeClass('bg-success').addClass('bg-dark').html('Scan was cancelled');
}
//f7App.popup.open('#modal-scan-result');
};
$(document).on('click', '.scan', function()
{
cordova.plugins.barcodeScanner.scan(scanSuccess, scanError, scanConfig);
});
$(document).on('click', '.manually', function()
{
sendCode($('[name="code"]').val(),'manual');
});
});
// Page: Customer
$(document).on('page:init', '.page[data-name="home"]', function () {
loadEvents();
$(document).on('click', '.reload-events', function()
{
loadEvents();
});
$(document).on('click', '.customer-screen .events .list-group-item', function()
{
// $('.customer-screen .events .list-group-item').removeClass('active');
// $(this).addClass('active');
// $('#scan-info').html($(this).html());
eventId = $(this).attr('data-event-id');
eventDateId = $(this).attr('data-event-date-id');
$('[name="code"]').val(eventId +'.'+ eventDateId +'.');
// first scan on event date click
//cordova.plugins.barcodeScanner.scan(scanSuccess, scanError, scanConfig);
// f7App.popup.open('#modal-scan-result');
});
});
// $('ons-button').on('click', function(e) {
// ons.notification.alert('Button is tapped!');
// })
});
}
function loadEvents()
{
f7App.dialog.preloader('Loading events…');
$.ajax({
url: `${ApiUrl}today`,
method: 'POST',
data: {
token: TOKEN
},
success: function(result,status,xhr)
{
console.log(result);
var events = JSON.parse(result);
if (events.result) {
$('div.events').html('');
const weekday = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];
$.each(events.data, function(i, val)
{
var eventDatetime = new Date(val.event_datetime);
var dateOptions = { year: '2-digit', month: '2-digit', day: '2-digit' };
var timeOptions = { hour12: true, hour: '2-digit', minute: '2-digit' };
var dayname=weekday[eventDatetime.getDay()];
var code=val.event_id +'.'+ val.event_date_id +'.';
$('div.events').append(`
${val.venue_title}-${val.venue_full}
${dayname} at ${eventDatetime.toLocaleDateString('en-GB', dateOptions)} –
${eventDatetime.toLocaleTimeString('en-US', timeOptions)}
0/500
`);
});
}else{
$('div.events').html(`${events.message}`);
}
},
error: function(xhr,status,error)
{
f7App.dialog.alert('Something went wrong. Please try again.', 'Error');
},
complete: function()
{
f7App.dialog.close();
}
});
}
we are using frameqork 7
// Initialize Framework7 app
document.addEventListener('deviceready', function () {
window.f7App = new Framework7({
el: '#app',
name: 'FossettsCircusApp',
theme: 'auto',
routes: [
{
path: '/',
url: 'pages/home.html',
},
{
path: '/event/:id/:dateId/',
url: 'pages/event-details.html',
},
],
});
// Create main view
window.f7App.views.create('.view-main');
},false);
and created an android app but some time event not loads
—
## ✅ **Potential Causes and Fixes**
### 1. **`ApiUrl` not set correctly at runtime**
Sometimes, `initializeApp()` may run **before** `deviceready`, or the `window.ApiUrl` isn’t ready when `loadEvents()` is called.
**Fix: Make sure `initializeApp()` is only called after `deviceready` and `navigator.splashscreen.hide()` completes.** You're already doing that correctly, but just ensure **no code anywhere else calls `loadEvents()` directly** outside `page:init`.
—
### 2. **Framework7 route/page timing issues**
Your `page:init` for the home page loads the events. But if the Framework7 app hasn't finished initializing, it might not bind the event correctly.
**Fix: Ensure `f7App` is initialized before page events fire. Combine it like this:**
document.addEventListener('deviceready', function () {
window.f7App = new Framework7({
el: '#app',
name: 'FossettsCircusApp',
theme: 'auto',
routes: [
{
path: '/',
url: 'pages/home.html',
},
{
path: '/event/:id/:dateId/',
url: 'pages/event-details.html',
},
],
on: {
init: function () {
// Safe to call initializeApp here
initializeApp();
}
}
});
window.f7App.views.create('.view-main');
});
—
### 3. **Timing: `page:init` might miss first time**
If the `page:init` event for `home` fires **before your jQuery `$(document).ready`**, the event handler might never bind.
**Fix:** Move your `.on('page:init', …)` bindings **outside** `$(document).ready()`.
// This should be outside $(document).ready()
$(document).on('page:init', '.page[data-name="home"]', function () {
loadEvents();
$(document).on('click', '.reload-events', function () {
loadEvents();
});
$(document).on('click', '.customer-screen .events .list-group-item', function () {
eventId = $(this).attr('data-event-id');
eventDateId = $(this).attr('data-event-date-id');
$('[name="code"]').val(eventId + '.' + eventDateId + '.');
});
});
—
### 4. **Network errors (no internet or delay)**
If the Android device has slow/no connectivity, your AJAX call will fail.
**Fix:**
– Add a retry mechanism.
– Log `xhr.status` in your `error` callback.
– Also show user-friendly error messages.
error: function(xhr, status, error) {
console.error('AJAX Error', status, error);
f7App.dialog.alert('Network error. Please check your connection and try again.', 'Error');
}
—
### 5. **Events missing in backend response**
Sometimes your backend may return `result: false` or an empty list even though events exist.
**Fix: Temporarily log full JSON for debugging:**
console.log("Raw response:", result);
Also ensure the backend returns `Content-Type: application/json` correctly.
—
### 6. **Android WebView caching/SSL issues**
On older Android devices, WebView might fail to load `https` APIs properly.
**Fix:**
– Ensure API URL uses HTTPS (you are).
– In `config.xml`, add:
<access origin="*" />
<allow-navigation href="*" />
<allow-intent href="http://*/*" />
<allow-intent href="https://*/*" />
– Add “ in `AndroidManifest.xml`.
—
### ✅ Suggested Debug Enhancements
Add an explicit loading error alert like this:
success: function(result, status, xhr) {
try {
var events = JSON.parse(result);
if (!events.result) throw new Error(events.message);
// continue rendering
} catch (err) {
console.error('Parse or API error:', err);
$('div.events').html(`<div class="text-red-500 p-4">${err.message || 'Could not load events'}</div>`);
}
}
—
## ✅ Summary of Fixes
| Issue | Fix |
|———————————-|———————————————————————-|
| `page:init` not always firing | Move `$(document).on('page:init',…)` **outside** `$(document).ready` |
| `f7App` not ready | Use `on: { init: … }` when initializing Framework7 |
| Network/API issue | Add better error handling/logging and retry |
| Race conditions or misorder | Ensure `initializeApp()` runs only after `deviceready` & `f7App` |
| Android WebView issues | Check `config.xml` and WebView settings |
Let me know if you’d like a modified/cleaned-up version of your code with all these fixes applied.
if (cordova.platformId === 'android') {
var permissions = cordova.plugins.permissions;
permissions.checkPermission(permissions.CAMERA, function (status) {
if (!status.hasPermission) {
// Request permission
permissions.requestPermission(
permissions.CAMERA,
function (status) {
if (status.hasPermission) {
navigator.splashscreen.hide(); // Hide splash after permission
initializeApp(); // Proceed with your app
} else {
navigator.app.exitApp(); // Exit if denied
}
},
function () {
navigator.app.exitApp(); // Exit on request error
}
);
} else {
navigator.splashscreen.hide();
initializeApp(); // Permission already granted
}
});
} else {
navigator.splashscreen.hide(); // For browser/iOS fallback
initializeApp();
}
}, false);
function initializeApp() {
window.TOKEN = 'fossettscircustoken';
window.eventId = -1;
window.eventDateId = -1;
window.ApiUrl = 'https://staging.fossettscircus.com/boxoffice/index.php/api/';
var scanConfig = {
preferFrontCamera : false, // iOS and Android
showFlipCameraButton : true, // iOS and Android
showTorchButton : true, // iOS and Android
torchOn: false, // Android, launch with the torch switched on (if available)
saveHistory: false, // Android, save scan history (default false)
prompt : "Place a code inside the scan area", // Android
resultDisplayDuration: 500, // Android, display scanned text for X ms. 0 suppresses it entirely, default 1500
formats : "QR_CODE", // default: all but PDF_417 and RSS_EXPANDED
//orientation : "landscape", // Android only (portrait|landscape), default unset so it rotates with the device
disableAnimations : true, // iOS
disableSuccessBeep: false // iOS and Android
};
$(document).ready(function()
{
$(document).on('page:init','.page[data-name="event-details"]',function (e) {
const page = e.detail;
const title = page.route.query.title || 'No Title';
const location = page.route.query.location || '';
const time = page.route.query.time || '';
const code = page.route.query.code || '';
console.log(title, location, time, code);
eventDateId = page.route.query.eventDateId || -1;
eventId = page.route.query.eventId || -1;
page.$el.find('.event-title').text(title);
page.$el.find('.event-location').text(location);
page.$el.find('.event-time').text(time);
page.$el.find('input[name="code"]').val(code);
var sendCode = function(code,type)
{
if(type=='qr')
f7App.dialog.preloader('Scanning code…');
else
f7App.dialog.preloader('Sending code…');
$('.scan').prop('disabled', true);
$.ajax({
url: `${ApiUrl}staff_scan`,
method: 'POST',
data: {
token: TOKEN,
event_date_id: eventDateId,
order_code: code
},
success: function(result,status,xhr)
{
var apiResult = JSON.parse(result);
if ( apiResult.result )
{
f7App.dialog.alert(apiResult.message, 'Scan Success');
}
else
{
f7App.dialog.alert(apiResult.message, 'Scan Failed');
}
},
error: function(xhr,status,error)
{
f7App.dialog.alert(`${result.text} could not be sent to boxoffice, please try again`, 'Scan Failed');
},
complete: function()
{
$('.scan').prop('disabled', false);
//$('.manually').prop('disabled', false);
f7App.dialog.close();
}
});
}
var scanSuccess = function(result)
{
if (! result.cancelled )
{
if (eventDateId > -1)
{
sendCode(result.text,'qr');
}
else
{
f7App.dialog.alert('You must select an event date to scan tickets', 'Scan Failed');
}
}
else
{
f7App.dialog.alert('Scan was cancelled', 'Scan Failed');
}
//f7App.popup.open('#modal-scan-result');
};
var scanError = function (error)
{
f7App.dialog.alert("Scanning failed: " + error, 'Scan Failed');
//f7App.popup.open('#modal-scan-result');
};
$(document).on('click', '.scan', function()
{
cordova.plugins.barcodeScanner.scan(scanSuccess, scanError, scanConfig);
});
});
$(document).on('page:init', '.page[data-name="home"]', function () {
});
$(document).on('page:init', '.page[data-name="staff-pay-terminal"]', function () {
$(document).on('input', '#amountInput', function () {
const amount = parseFloat($(this).val());
const btn = $('#chargeBtn');
if (amount > 0) {
btn.removeClass('disabled:opacity-50').addClass('color-green');
btn.text('Charge €' + amount.toFixed(2));
} else {
btn.addClass('disabled:opacity-50').removeClass('color-green');
btn.text('Charge €0.00');
}
});
})
$(document).on('page:init', '.page[data-name="staff-tickets"]', function () {
loadEvents('yes');
$(document).on('click', '.reload-events-staff', function()
{
loadEvents('yes');
});
});
$(document).on('page:init', '.page[data-name="staff-check-orders"]', function () {
});
$(document).on('page:init', '.page[data-name="staff-dashboard"]', function () {
});
});
}
function loadEvents(staff)
{
f7App.dialog.preloader('Loading events…');
$.ajax({
url: `${ApiUrl}today`,
method: 'POST',
data: {
token: TOKEN
},
success: function(result,status,xhr)
{
console.log(result);
var events = JSON.parse(result);
if (events.result) {
$('.customer-screen div.events').html('');
$('.staff-screen div.events').html('');
const weekday = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];
$.each(events.data, function(i, val)
{
var eventDatetime = new Date(val.event_datetime);
var dateOptions = { year: '2-digit', month: '2-digit', day: '2-digit' };
var timeOptions = { hour12: true, hour: '2-digit', minute: '2-digit' };
var dayname=weekday[eventDatetime.getDay()];
var code=val.event_id +'.'+ val.event_date_id +'.';
$('.staff-screen div.events').append(`
${val.venue_full}
${dayname} at ${eventDatetime.toLocaleDateString('en-GB', dateOptions)} –
${eventDatetime.toLocaleTimeString('en-US', timeOptions)}
0/500
`);
});
}else{
$('.events').html(`${events.message}`);
}
},
error: function(xhr,status,error)
{
f7App.dialog.alert('Something went wrong. Please try again.', 'Error');
},
complete: function()
{
f7App.dialog.close();
}
});
}
above code working fine but When we scan two ticket with in 2 min of time then its show scanning failed ,scanning already progress
This is likely due to the fact that `.scan()` is being triggered while another is still pending or hasn't fully cleaned up (like dialogs not closed, or buttons not re-enabled).
—
## ✅ Recommended Fix: Add a scan lock to prevent double triggers
You can solve this cleanly by introducing a **`scanInProgress` lock** that prevents a new scan until the previous one has fully completed.
—
### ✅ **Solution – Add `scanInProgress` flag**
### 🛠 Add this to your global scope inside `initializeApp()`:
window.scanInProgress = false;
—
### 🔒 Update your `.scan` click handler like this:
$(document).on('click', '.scan', function () {
if (scanInProgress) {
f7App.dialog.alert('Please wait, scanning is still in progress.', 'Hold On');
return;
}
scanInProgress = true; // lock scanning
cordova.plugins.barcodeScanner.scan(scanSuccess, scanError, scanConfig);
});
—
### ✅ Modify `scanSuccess()` and `scanError()` to **reset the lock**
var scanSuccess = function(result) {
if (!result.cancelled) {
if (eventDateId > -1) {
sendCode(result.text, 'qr');
} else {
f7App.dialog.alert('You must select an event date to scan tickets', 'Scan Failed');
}
} else {
f7App.dialog.alert('Scan was cancelled', 'Scan Info');
}
scanInProgress = false; // release lock
};
var scanError = function(error) {
f7App.dialog.alert("Scanning failed: " + error, 'Scan Failed');
scanInProgress = false; // release lock
};
—
### ✅ Also ensure `sendCode()` releases the lock if it was QR-triggered:
Modify the `complete` callback in your AJAX call inside `sendCode()`:
complete: function() {
$('.scan').prop('disabled', false);
f7App.dialog.close();
if (type === 'qr') {
scanInProgress = false; // only unlock for QR scans
}
}
—
### ✅ Optional: Add Visual Feedback During Scan
If you'd like, show a toast or disable the button with a spinner when `scanInProgress = true`.
—
## ✅ Final Result
– Prevents accidental rapid scanning.
– Ensures only one scan operation is allowed at a time.
– Auto unlocks on success, failure, or cancel.
– Improves user feedback.
—
Let me know if you’d like the full cleaned-up version of your `initializeApp()` and related handlers with this fix applied.
if (cordova.platformId === 'android') {
var permissions = cordova.plugins.permissions;
permissions.checkPermission(permissions.CAMERA, function (status) {
if (!status.hasPermission) {
// Request permission
permissions.requestPermission(
permissions.CAMERA,
function (status) {
if (status.hasPermission) {
navigator.splashscreen.hide(); // Hide splash after permission
initializeApp(); // Proceed with your app
} else {
navigator.app.exitApp(); // Exit if denied
}
},
function () {
navigator.app.exitApp(); // Exit on request error
}
);
} else {
navigator.splashscreen.hide();
initializeApp(); // Permission already granted
}
});
} else {
navigator.splashscreen.hide(); // For browser/iOS fallback
initializeApp();
}
}, false);
function initializeApp() {
window.TOKEN = 'fossettscircustoken';
window.eventId = -1;
window.eventDateId = -1;
window.ApiUrl = 'https://staging.fossettscircus.com/boxoffice/index.php/api/';
var scanConfig = {
preferFrontCamera : false, // iOS and Android
showFlipCameraButton : true, // iOS and Android
showTorchButton : true, // iOS and Android
torchOn: false, // Android, launch with the torch switched on (if available)
saveHistory: false, // Android, save scan history (default false)
prompt : "Place a code inside the scan area", // Android
resultDisplayDuration: 500, // Android, display scanned text for X ms. 0 suppresses it entirely, default 1500
formats : "QR_CODE", // default: all but PDF_417 and RSS_EXPANDED
//orientation : "landscape", // Android only (portrait|landscape), default unset so it rotates with the device
disableAnimations : true, // iOS
disableSuccessBeep: false // iOS and Android
};
$(document).ready(function()
{
$(document).on('page:init','.page[data-name="event-details"]',function (e) {
const page = e.detail;
const title = page.route.query.title || 'No Title';
const location = page.route.query.location || '';
const time = page.route.query.time || '';
const code = page.route.query.code || '';
console.log(title, location, time, code);
eventDateId = page.route.query.eventDateId || -1;
eventId = page.route.query.eventId || -1;
page.$el.find('.event-title').text(title);
page.$el.find('.event-location').text(location);
page.$el.find('.event-time').text(time);
page.$el.find('input[name="code"]').val(code);
var sendCode = function(code,type)
{
if(type=='qr')
f7App.dialog.preloader('Scanning code…');
else
f7App.dialog.preloader('Sending code…');
$('.scan').prop('disabled', true);
$.ajax({
url: `${ApiUrl}staff_scan`,
method: 'POST',
data: {
token: TOKEN,
event_date_id: eventDateId,
order_code: code
},
success: function(result,status,xhr)
{
var apiResult = JSON.parse(result);
if ( apiResult.result )
{
f7App.dialog.alert(apiResult.message, 'Scan Success');
}
else
{
f7App.dialog.alert(apiResult.message, 'Scan Failed');
}
},
error: function(xhr,status,error)
{
f7App.dialog.alert(`${result.text} could not be sent to boxoffice, please try again`, 'Scan Failed');
},
complete: function()
{
$('.scan').prop('disabled', false);
//$('.manually').prop('disabled', false);
f7App.dialog.close();
}
});
}
var scanSuccess = function(result)
{
if (! result.cancelled )
{
if (eventDateId > -1)
{
sendCode(result.text,'qr');
}
else
{
f7App.dialog.alert('You must select an event date to scan tickets', 'Scan Failed');
}
}
else
{
f7App.dialog.alert('Scan was cancelled', 'Scan Failed');
}
//f7App.popup.open('#modal-scan-result');
};
var scanError = function (error)
{
f7App.dialog.alert("Scanning failed: " + error, 'Scan Failed');
//f7App.popup.open('#modal-scan-result');
};
$(document).on('click', '.scan', function()
{
cordova.plugins.barcodeScanner.scan(scanSuccess, scanError, scanConfig);
});
});
$(document).on('page:init', '.page[data-name="home"]', function () {
});
$(document).on('page:init', '.page[data-name="staff-pay-terminal"]', function () {
$(document).on('input', '#amountInput', function () {
const amount = parseFloat($(this).val());
const btn = $('#chargeBtn');
if (amount > 0) {
btn.removeClass('disabled:opacity-50').addClass('color-green');
btn.text('Charge €' + amount.toFixed(2));
} else {
btn.addClass('disabled:opacity-50').removeClass('color-green');
btn.text('Charge €0.00');
}
});
})
$(document).on('page:init', '.page[data-name="staff-tickets"]', function () {
loadEvents('yes');
$(document).on('click', '.reload-events-staff', function()
{
loadEvents('yes');
});
});
$(document).on('page:init', '.page[data-name="staff-check-orders"]', function () {
});
$(document).on('page:init', '.page[data-name="staff-dashboard"]', function () {
});
});
}
function loadEvents(staff)
{
f7App.dialog.preloader('Loading events…');
$.ajax({
url: `${ApiUrl}today`,
method: 'POST',
data: {
token: TOKEN
},
success: function(result,status,xhr)
{
console.log(result);
var events = JSON.parse(result);
if (events.result) {
$('.customer-screen div.events').html('');
$('.staff-screen div.events').html('');
const weekday = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];
$.each(events.data, function(i, val)
{
var eventDatetime = new Date(val.event_datetime);
var dateOptions = { year: '2-digit', month: '2-digit', day: '2-digit' };
var timeOptions = { hour12: true, hour: '2-digit', minute: '2-digit' };
var dayname=weekday[eventDatetime.getDay()];
var code=val.event_id +'.'+ val.event_date_id +'.';
$('.staff-screen div.events').append(`
${val.venue_full}
${dayname} at ${eventDatetime.toLocaleDateString('en-GB', dateOptions)} –
${eventDatetime.toLocaleTimeString('en-US', timeOptions)}
0/500
`);
});
}else{
$('.events').html(`${events.message}`);
}
},
error: function(xhr,status,error)
{
f7App.dialog.alert('Something went wrong. Please try again.', 'Error');
},
complete: function()
{
f7App.dialog.close();
}
});
}
Adjust in above code also some time framework 7 alert show multiple time
—
## ✅ Issue 1: **Scanning twice within 2 minutes triggers “Scanning already in progress”**
This happens because your scan function doesn't block repeated calls until the AJAX call completes.
—
## ✅ Issue 2: **`f7App.dialog.alert` shows multiple times (alert overlaps)**
This happens because Framework7 allows multiple alerts if called in succession without waiting for the previous one to close.
—
## ✅ **Fixed and Improved Code Adjustments**
### ✅ Step 1: Add a scan lock globally
Place this **above `$(document).ready()` inside `initializeApp()`**:
let scanInProgress = false;
let alertVisible = false;
—
### ✅ Step 2: Safe Framework7 Alert Wrapper
Add this helper function inside `initializeApp()`:
function showAlert(message, title = 'Alert') {
if (alertVisible) return;
alertVisible = true;
f7App.dialog.alert(message, title, () => {
alertVisible = false; // reset after user closes
});
}
Use `showAlert()` **instead of `f7App.dialog.alert()`** everywhere.
—
### ✅ Step 3: Updated `.scan` click handler
Inside your `.page[data-name="event-details"]` block, replace this:
$(document).on('click', '.scan', function() {
cordova.plugins.barcodeScanner.scan(scanSuccess, scanError, scanConfig);
});
With this:
$(document).on('click', '.scan', function () {
if (scanInProgress) {
showAlert('Scan is already in progress. Please wait.', 'Hold On');
return;
}
scanInProgress = true;
cordova.plugins.barcodeScanner.scan(scanSuccess, scanError, scanConfig);
});
—
### ✅ Step 4: Update `scanSuccess`, `scanError`, and `sendCode`
#### ✅ `scanSuccess`:
var scanSuccess = function(result) {
if (!result.cancelled) {
if (eventDateId > -1) {
sendCode(result.text, 'qr');
} else {
showAlert('You must select an event date to scan tickets', 'Scan Failed');
scanInProgress = false;
}
} else {
showAlert('Scan was cancelled', 'Info');
scanInProgress = false;
}
};
—
#### ✅ `scanError`:
var scanError = function(error) {
showAlert('Scanning failed: ' + error, 'Scan Failed');
scanInProgress = false;
};
—
#### ✅ Update `complete` callback in `sendCode()`:
complete: function () {
$('.scan').prop('disabled', false);
f7App.dialog.close();
scanInProgress = false;
}
—
### ✅ Step 5: Replace all `f7App.dialog.alert(…)` with `showAlert(…)`
For example:
// Instead of this
f7App.dialog.alert(apiResult.message, 'Scan Success');
// Use this
showAlert(apiResult.message, 'Scan Success');
—
## ✅ Summary of Benefits
| Issue | Fix |
|——|—–|
| Scan triggered too fast | `scanInProgress` flag prevents re-entry |
| Multiple alerts stack | `alertVisible` lock ensures only one alert at a time |
| Cleaner UX | Alerts don't overlap, and scans behave reliably |
—
Let me know if you'd like the **full rewritten version** of `initializeApp()` with all these changes applied.
– ✅ `scanInProgress` lock to prevent double scans
– ✅ `alertVisible` flag to avoid multiple overlapping alerts
– ✅ All `f7App.dialog.alert` calls replaced with `showAlert()` for safety
– ✅ Cleaned up structure for clarity
—
### ✅ Full `initializeApp()` Code (Updated)
function initializeApp() {
window.TOKEN = 'fossettscircustoken';
window.eventId = -1;
window.eventDateId = -1;
window.ApiUrl = 'https://staging.fossettscircus.com/boxoffice/index.php/api/';
let scanInProgress = false;
let alertVisible = false;
const scanConfig = {
preferFrontCamera: false,
showFlipCameraButton: true,
showTorchButton: true,
torchOn: false,
saveHistory: false,
prompt: "Place a code inside the scan area",
resultDisplayDuration: 500,
formats: "QR_CODE",
disableAnimations: true,
disableSuccessBeep: false
};
function showAlert(message, title = 'Alert') {
if (alertVisible) return;
alertVisible = true;
f7App.dialog.alert(message, title, () => {
alertVisible = false;
});
}
$(document).ready(function () {
// Event Details Page
$(document).on('page:init', '.page[data-name="event-details"]', function (e) {
const page = e.detail;
const title = page.route.query.title || 'No Title';
const location = page.route.query.location || '';
const time = page.route.query.time || '';
const code = page.route.query.code || '';
eventDateId = page.route.query.eventDateId || -1;
eventId = page.route.query.eventId || -1;
page.$el.find('.event-title').text(title);
page.$el.find('.event-location').text(location);
page.$el.find('.event-time').text(time);
page.$el.find('input[name="code"]').val(code);
function sendCode(code, type) {
if (type === 'qr') {
f7App.dialog.preloader('Scanning code...');
} else {
f7App.dialog.preloader('Sending code...');
}
$('.scan').prop('disabled', true);
$.ajax({
url: `${ApiUrl}staff_scan`,
method: 'POST',
data: {
token: TOKEN,
event_date_id: eventDateId,
order_code: code
},
success: function (result, status, xhr) {
const apiResult = JSON.parse(result);
if (apiResult.result) {
showAlert(apiResult.message, 'Scan Success');
} else {
showAlert(apiResult.message, 'Scan Failed');
}
},
error: function (xhr, status, error) {
showAlert('Something went wrong. Please try again.', 'Scan Failed');
},
complete: function () {
$('.scan').prop('disabled', false);
f7App.dialog.close();
scanInProgress = false;
}
});
}
function scanSuccess(result) {
if (!result.cancelled) {
if (eventDateId > -1) {
sendCode(result.text, 'qr');
} else {
showAlert('You must select an event date to scan tickets', 'Scan Failed');
scanInProgress = false;
}
} else {
showAlert('Scan was cancelled', 'Info');
scanInProgress = false;
}
}
function scanError(error) {
showAlert('Scanning failed: ' + error, 'Scan Failed');
scanInProgress = false;
}
$(document).on('click', '.scan', function () {
if (scanInProgress) {
showAlert('Scan is already in progress. Please wait.', 'Hold On');
return;
}
scanInProgress = true;
cordova.plugins.barcodeScanner.scan(scanSuccess, scanError, scanConfig);
});
});
// Staff Pay Terminal Page
$(document).on('page:init', '.page[data-name="staff-pay-terminal"]', function () {
$(document).on('input', '#amountInput', function () {
const amount = parseFloat($(this).val());
const btn = $('#chargeBtn');
if (amount > 0) {
btn.removeClass('disabled:opacity-50').addClass('color-green');
btn.text('Charge €' + amount.toFixed(2));
} else {
btn.addClass('disabled:opacity-50').removeClass('color-green');
btn.text('Charge €0.00');
}
});
});
// Staff Tickets Page
$(document).on('page:init', '.page[data-name="staff-tickets"]', function () {
loadEvents('yes');
$(document).on('click', '.reload-events-staff', function () {
loadEvents('yes');
});
});
// Other page inits (for future use)
$(document).on('page:init', '.page[data-name="home"]', function () {});
$(document).on('page:init', '.page[data-name="staff-check-orders"]', function () {});
$(document).on('page:init', '.page[data-name="staff-dashboard"]', function () {});
});
}
—
### ✅ No changes needed in `loadEvents()` unless you want the same alert lock.
Let me know if you'd like me to also apply the `showAlert` improvement in the `loadEvents()` error/success UI, or want debounce for `.reload-events-staff`.
if (cordova.platformId === 'android') {
var permissions = cordova.plugins.permissions;
permissions.checkPermission(permissions.CAMERA, function (status) {
if (!status.hasPermission) {
// Request permission
permissions.requestPermission(
permissions.CAMERA,
function (status) {
if (status.hasPermission) {
navigator.splashscreen.hide(); // Hide splash after permission
initializeApp(); // Proceed with your app
} else {
navigator.app.exitApp(); // Exit if denied
}
},
function () {
navigator.app.exitApp(); // Exit on request error
}
);
} else {
navigator.splashscreen.hide();
initializeApp(); // Permission already granted
}
});
} else {
navigator.splashscreen.hide(); // For browser/iOS fallback
initializeApp();
}
}, false);
Do above also need change
—
## ✅ Your current code is functionally correct.
But here's what we can improve:
### 🔍 Issues to consider:
1. **Nested callbacks** (callback hell) – we can flatten it slightly.
2. `navigator.splashscreen.hide()` is called in 3 different places – this could be centralized.
3. `initializeApp()` is scattered inside those conditionals – centralizing it improves maintainability.
4. In rare cases, plugins may not be ready instantly — we can add a fallback using `setTimeout`.
—
## ✅ Improved version of `deviceready` handler
document.addEventListener('deviceready', function () {
function startApp() {
navigator.splashscreen.hide();
initializeApp();
}
if (cordova.platformId === 'android' && cordova.plugins && cordova.plugins.permissions) {
const permissions = cordova.plugins.permissions;
permissions.checkPermission(permissions.CAMERA, function (status) {
if (status.hasPermission) {
startApp();
} else {
permissions.requestPermission(
permissions.CAMERA,
function (status) {
if (status.hasPermission) {
startApp();
} else {
navigator.app.exitApp(); // Exit if permission denied
}
},
function () {
navigator.app.exitApp(); // Exit on error
}
);
}
});
} else {
// iOS, browser, or fallback
startApp();
}
}, false);
—
## ✅ What's better in this version?
| Improvement | Benefit |
|————————–|———|
| `startApp()` function | Centralized `splashscreen.hide()` and `initializeApp()` logic |
| Flat structure | Easier to read, debug, and modify |
| Extra check for `cordova.plugins` | Avoids runtime errors if permissions plugin hasn't loaded yet |
| Ready for fallback | Easier to add retries or `setTimeout` if needed later |
—
Let me know if you'd like me to integrate this into the complete `initializeApp()` block you’re using, or refactor the whole app boot sequence in one file.
document.addEventListener('deviceready', function () {
window.f7App = new Framework7({
el: '#app',
name: 'FossettsCircusApp',
theme: 'auto',
routes: [
{
path: '/',
url: 'pages/home.html',
},
{
path: '/staff-tickets/',
url: 'pages/staff-tickets.html',
},
{
path: '/staff-pay-terminal/',
url: 'pages/staff-pay-terminal.html',
},
{
path: '/staff-check-orders/',
url: 'pages/staff-check-orders.html',
},
{
path: '/event/:id/:dateId/',
url: 'pages/event-details.html',
},
],
});
// Create main view
window.f7App.views.create('.view-main');
},false);
—
### ✅ Problem:
You're now using **two separate `document.addEventListener('deviceready', …` blocks**:
1. One for permissions + `initializeApp()`
2. One for Framework7 initialization
### ❗️This can cause **race conditions**:
– If the F7 app is not ready before `initializeApp()` runs (especially if `f7App.dialog.alert()` is used), it might throw an error like `f7App is undefined`.
—
### ✅ Unified, clean version:
Below is a **merged version** that ensures:
– Framework7 is initialized **before** `initializeApp()` runs
– Camera permissions are handled properly
– Only a **single `deviceready` listener**
—
### ✅ Full Combined Code:
document.addEventListener('deviceready', function () {
// Initialize Framework7 first
window.f7App = new Framework7({
el: '#app',
name: 'FossettsCircusApp',
theme: 'auto',
routes: [
{ path: '/', url: 'pages/home.html' },
{ path: '/staff-tickets/', url: 'pages/staff-tickets.html' },
{ path: '/staff-pay-terminal/', url: 'pages/staff-pay-terminal.html' },
{ path: '/staff-check-orders/', url: 'pages/staff-check-orders.html' },
{ path: '/event/:id/:dateId/', url: 'pages/event-details.html' }
]
});
// Create the main view
window.f7App.views.create('.view-main');
// Helper to hide splash and start the app
function startApp() {
navigator.splashscreen.hide();
initializeApp(); // Your custom logic goes here
}
// Handle Android camera permission
if (cordova.platformId === 'android' && cordova.plugins && cordova.plugins.permissions) {
const permissions = cordova.plugins.permissions;
permissions.checkPermission(permissions.CAMERA, function (status) {
if (status.hasPermission) {
startApp();
} else {
permissions.requestPermission(
permissions.CAMERA,
function (status) {
if (status.hasPermission) {
startApp();
} else {
navigator.app.exitApp();
}
},
function () {
navigator.app.exitApp();
}
);
}
});
} else {
// For iOS / browser fallback
startApp();
}
});
—
### ✅ Benefits:
– Only **one `deviceready` block**
– Framework7 is always initialized **before `initializeApp()`**
– No redundant logic or duplicated splash hide calls
– Safe for Android + iOS + browser fallback
—
Let me know if you'd like me to inject this directly into your full `index.js` or main app file structure.
if (cordova.platformId === 'android') {
var permissions = cordova.plugins.permissions;
permissions.checkPermission(permissions.CAMERA, function (status) {
if (!status.hasPermission) {
// Request permission
permissions.requestPermission(
permissions.CAMERA,
function (status) {
if (status.hasPermission) {
navigator.splashscreen.hide(); // Hide splash after permission
initializeApp(); // Proceed with your app
} else {
navigator.app.exitApp(); // Exit if denied
}
},
function () {
navigator.app.exitApp(); // Exit on request error
}
);
} else {
navigator.splashscreen.hide();
initializeApp(); // Permission already granted
}
});
} else {
navigator.splashscreen.hide(); // For browser/iOS fallback
initializeApp();
}
}, false);
function initializeApp() {
window.TOKEN = 'fossettscircustoken';
window.eventId = -1;
window.eventDateId = -1;
window.ApiUrl = 'https://staging.fossettscircus.com/boxoffice/index.php/api/';
var scanConfig = {
preferFrontCamera : false, // iOS and Android
showFlipCameraButton : true, // iOS and Android
showTorchButton : true, // iOS and Android
torchOn: false, // Android, launch with the torch switched on (if available)
saveHistory: false, // Android, save scan history (default false)
prompt : "Place a code inside the scan area", // Android
resultDisplayDuration: 500, // Android, display scanned text for X ms. 0 suppresses it entirely, default 1500
formats : "QR_CODE", // default: all but PDF_417 and RSS_EXPANDED
//orientation : "landscape", // Android only (portrait|landscape), default unset so it rotates with the device
disableAnimations : true, // iOS
disableSuccessBeep: false // iOS and Android
};
$(document).on('page:init','.page[data-name="event-details"]',function (e) {
const page = e.detail;
const title = page.route.query.title || 'No Title';
const location = page.route.query.location || '';
const time = page.route.query.time || '';
const code = page.route.query.code || '';
eventDateId = page.route.query.eventDateId || -1;
eventId = page.route.query.eventId || -1;
console.log(title, location, time, code);
page.$el.find('.event-title').text(title);
page.$el.find('.event-location').text(location);
page.$el.find('.event-time').text(time);
page.$el.find('input[name="code"]').val(code);
var sendCode = function(code,type)
{
if(type=='qr')
f7App.dialog.preloader('Scanning code…');
else
f7App.dialog.preloader('Sending code…');
$('.scan').prop('disabled', true);
$('.manually').prop('disabled', true);
$('#scan-result').removeClass('bg-success').addClass('bg-dark').html('Code '+ code +' sent to boxoffice, plase wait…');
$.ajax({
url: `${ApiUrl}scan`,
method: 'POST',
data: {
token: TOKEN,
event_date_id: eventDateId,
order_code: code
},
success: function(result,status,xhr)
{
var apiResult = JSON.parse(result);
if ( apiResult.result )
{
$('#scan-result').removeClass('bg-dark').addClass('bg-success').html(apiResult.message+''+apiResult.seating+''+apiResult.tickets);
f7App.dialog.alert(apiResult.message, 'Scan Failed');
// reset input to set next code
$('[name="code"]').val(eventId +'.'+ eventDateId +'.');
}
else
{
$('#scan-result').removeClass('bg-success').addClass('bg-dark').html(apiResult.message);
f7App.dialog.alert(apiResult.message, 'Scan Failed');
}
},
error: function(xhr,status,error)
{
$('#scan-result').removeClass('bg-success').addClass('bg-dark').html('Code '+ result.text +' could not be sent to boxoffice, please try again');
f7App.dialog.alert(`${result.text} could not be sent to boxoffice, please try again`, 'Scan Failed');
},
complete: function()
{
$('.scan').prop('disabled', false);
$('.manually').prop('disabled', false);
f7App.dialog.close();
}
});
}
var scanError = function (error)
{
$('#scan-result').removeClass('bg-success').addClass('bg-dark').html("Scanning failed: " + error);
//f7App.popup.open('#modal-scan-result');
};
var scanSuccess = function(result)
{
if (! result.cancelled )
{
if (eventDateId > -1)
{
sendCode(result.text,'qr');
}
else
{
$('#scan-result').removeClass('bg-success').addClass('bg-dark').html('You must select an event date to scan tickets');
}
}
else
{
$('#scan-result').removeClass('bg-success').addClass('bg-dark').html('Scan was cancelled');
}
//f7App.popup.open('#modal-scan-result');
};
$(document).on('click', '.scan', function()
{
cordova.plugins.barcodeScanner.scan(scanSuccess, scanError, scanConfig);
});
$(document).on('click', '.manually', function()
{
sendCode($('[name="code"]').val(),'manual');
});
});
// Page: Customer
$(document).on('page:init', '.page[data-name="home"]', function () {
loadEvents();
$(document).on('click', '.reload-events', function()
{
loadEvents();
});
});
}
function loadEvents()
{
f7App.dialog.preloader('Loading events…');
$.ajax({
url: `${ApiUrl}today`,
method: 'POST',
data: {
token: TOKEN
},
success: function(result,status,xhr)
{
console.log(result);
var events = JSON.parse(result);
if (events.result) {
$('div.events').html('');
const weekday = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];
$.each(events.data, function(i, val)
{
var eventDatetime = new Date(val.event_datetime);
var dateOptions = { year: '2-digit', month: '2-digit', day: '2-digit' };
var timeOptions = { hour12: true, hour: '2-digit', minute: '2-digit' };
var dayname=weekday[eventDatetime.getDay()];
var code=val.event_id +'.'+ val.event_date_id +'.';
$('div.events').append(`
${val.venue_title}-${val.venue_full}
${dayname} at ${eventDatetime.toLocaleDateString('en-GB', dateOptions)} –
${eventDatetime.toLocaleTimeString('en-US', timeOptions)}
0/500
`);
});
}else{
$('div.events').html(`${events.message}`);
}
},
error: function(xhr,status,error)
{
f7App.dialog.alert('Something went wrong. Please try again.', 'Error');
},
complete: function()
{
f7App.dialog.close();
}
});
}
Also make for above
f7inti
// Initialize Framework7 app
document.addEventListener('deviceready', function () {
window.f7App = new Framework7({
el: '#app',
name: 'FossettsCircusApp',
theme: 'auto',
routes: [
{
path: '/',
url: 'pages/home.html',
},
{
path: '/event/:id/:dateId/',
url: 'pages/event-details.html',
},
],
});
// Create main view
window.f7App.views.create('.view-main');
},false);
—
### ✅ Key Improvements
– Combined Framework7 and `initializeApp()` inside a **single `deviceready` block**
– Added **`scanInProgress`** and **`alertVisible`** to prevent repeated scans and overlapping alerts
– Centralized **alert logic** with `showAlert()`
– Cleaned up **event bindings** and comments for maintainability
—
### ✅ Final, Clean Version
document.addEventListener('deviceready', function () {
// Initialize Framework7
window.f7App = new Framework7({
el: '#app',
name: 'FossettsCircusApp',
theme: 'auto',
routes: [
{ path: '/', url: 'pages/home.html' },
{ path: '/event/:id/:dateId/', url: 'pages/event-details.html' }
]
});
// Create main view
window.f7App.views.create('.view-main');
// Start the app after permissions
function startApp() {
navigator.splashscreen.hide();
initializeApp();
}
// Handle camera permissions on Android
if (cordova.platformId === 'android' && cordova.plugins && cordova.plugins.permissions) {
const permissions = cordova.plugins.permissions;
permissions.checkPermission(permissions.CAMERA, function (status) {
if (status.hasPermission) {
startApp();
} else {
permissions.requestPermission(
permissions.CAMERA,
function (status) {
status.hasPermission ? startApp() : navigator.app.exitApp();
},
function () {
navigator.app.exitApp();
}
);
}
});
} else {
startApp(); // For iOS or browser
}
}, false);
—
### ✅ `initializeApp()` Function
function initializeApp() {
window.TOKEN = 'fossettscircustoken';
window.eventId = -1;
window.eventDateId = -1;
window.ApiUrl = 'https://staging.fossettscircus.com/boxoffice/index.php/api/';
let scanInProgress = false;
let alertVisible = false;
const scanConfig = {
preferFrontCamera: false,
showFlipCameraButton: true,
showTorchButton: true,
torchOn: false,
saveHistory: false,
prompt: "Place a code inside the scan area",
resultDisplayDuration: 500,
formats: "QR_CODE",
disableAnimations: true,
disableSuccessBeep: false
};
function showAlert(message, title = 'Alert') {
if (alertVisible) return;
alertVisible = true;
f7App.dialog.alert(message, title, () => {
alertVisible = false;
});
}
// Event Details Page
$(document).on('page:init', '.page[data-name="event-details"]', function (e) {
const page = e.detail;
const title = page.route.query.title || 'No Title';
const location = page.route.query.location || '';
const time = page.route.query.time || '';
const code = page.route.query.code || '';
eventDateId = page.route.query.eventDateId || -1;
eventId = page.route.query.eventId || -1;
page.$el.find('.event-title').text(title);
page.$el.find('.event-location').text(location);
page.$el.find('.event-time').text(time);
page.$el.find('input[name="code"]').val(code);
function sendCode(code, type) {
f7App.dialog.preloader(type === 'qr' ? 'Scanning code...' : 'Sending code...');
$('.scan').prop('disabled', true);
$('.manually').prop('disabled', true);
$('#scan-result').removeClass('bg-success').addClass('bg-dark')
.html(`Code <u class="bg-primary text-white p-1">${code}</u> sent to boxoffice, please wait...`);
$.ajax({
url: `${ApiUrl}scan`,
method: 'POST',
data: {
token: TOKEN,
event_date_id: eventDateId,
order_code: code
},
success: function (result) {
const apiResult = JSON.parse(result);
if (apiResult.result) {
$('#scan-result').removeClass('bg-dark').addClass('bg-success')
.html(`${apiResult.message}<br>${apiResult.seating}<br>${apiResult.tickets}`);
showAlert(apiResult.message, 'Scan Success');
$('[name="code"]').val(`${eventId}.${eventDateId}.`);
} else {
$('#scan-result').removeClass('bg-success').addClass('bg-dark')
.html(apiResult.message);
showAlert(apiResult.message, 'Scan Failed');
}
},
error: function () {
$('#scan-result').removeClass('bg-success').addClass('bg-dark')
.html(`Code <u class="bg-primary text-white p-1">${code}</u> could not be sent to boxoffice, please try again`);
showAlert('Scan failed. Please try again.', 'Scan Failed');
},
complete: function () {
$('.scan').prop('disabled', false);
$('.manually').prop('disabled', false);
f7App.dialog.close();
scanInProgress = false;
}
});
}
function scanSuccess(result) {
if (!result.cancelled) {
if (eventDateId > -1) {
sendCode(result.text, 'qr');
} else {
$('#scan-result').removeClass('bg-success').addClass('bg-dark')
.html('You must select an event date to scan tickets');
}
} else {
$('#scan-result').removeClass('bg-success').addClass('bg-dark')
.html('Scan was cancelled');
}
scanInProgress = false;
}
function scanError(error) {
$('#scan-result').removeClass('bg-success').addClass('bg-dark')
.html(`Scanning failed: ${error}`);
showAlert(`Scanning failed: ${error}`, 'Scan Error');
scanInProgress = false;
}
$(document).on('click', '.scan', function () {
if (scanInProgress) {
showAlert('Scan is already in progress. Please wait.', 'Please Wait');
return;
}
scanInProgress = true;
cordova.plugins.barcodeScanner.scan(scanSuccess, scanError, scanConfig);
});
$(document).on('click', '.manually', function () {
sendCode($('[name="code"]').val(), 'manual');
});
});
// Home Page
$(document).on('page:init', '.page[data-name="home"]', function () {
loadEvents();
$(document).on('click', '.reload-events', function () {
loadEvents();
});
});
}
—
### ✅ `loadEvents()` (unchanged but compatible)
Your existing `loadEvents()` function works fine with this setup. If you'd like to use `showAlert()` in it too for consistency, let me know.
—
Let me know if you'd also like this split into modular files (like `f7-init.js`, `events.js`, etc.) for better maintainability.