<!DOCTYPE html>
<html lang="en">
<head>
<base href=""/>
<title>Paynetics - Admin</title>
<meta charset="utf-8"/>
<!-- Theme initialization (must be early to prevent flash) -->
<script>
(function() {
// Cookie helper functions
function getCookie(name) {
var match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
return match ? match[2] : null;
}
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=/; SameSite=Lax';
}
// Get theme from cookie, default to 'light'
var savedTheme = getCookie('theme') || 'light';
// Apply theme immediately to prevent flash
document.documentElement.setAttribute('data-bs-theme', savedTheme);
// Expose functions globally for theme toggle
window.getThemeCookie = getCookie;
window.setThemeCookie = setCookie;
window.currentTheme = savedTheme;
})();
</script>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<meta property="og:locale" content="en_US"/>
<meta property="og:type" content="article"/>
<link rel="shortcut icon" href="/assets/media/logos/favicon.ico"/>
<!-- Google Fonts - IBM Plex Sans -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&display=swap" rel="stylesheet">
<!-- Velok Theme CSS -->
<link href="{{ asset('assets/velok/css/vendor.min.css') }}" rel="stylesheet" type="text/css"/>
<link href="{{ asset('assets/velok/css/app.min.css') }}" rel="stylesheet" type="text/css"/>
<!-- Dark/Light mode toggle styles -->
<style>
/* Theme toggle button icons */
/* In light mode: show moon icon (to switch to dark) */
html[data-bs-theme="light"] #light-dark-mode .light-mode,
html:not([data-bs-theme]) #light-dark-mode .light-mode {
display: inline-block;
}
html[data-bs-theme="light"] #light-dark-mode .dark-mode,
html:not([data-bs-theme]) #light-dark-mode .dark-mode {
display: none;
}
/* In dark mode: show sun icon (to switch to light) */
html[data-bs-theme="dark"] #light-dark-mode .light-mode {
display: none;
}
html[data-bs-theme="dark"] #light-dark-mode .dark-mode {
display: inline-block;
}
/* Logo visibility based on theme */
html[data-bs-theme="light"] .logo-light,
html:not([data-bs-theme]) .logo-light {
display: none !important;
}
html[data-bs-theme="light"] .logo-dark,
html:not([data-bs-theme]) .logo-dark {
display: block !important;
}
html[data-bs-theme="dark"] .logo-dark {
display: none !important;
}
html[data-bs-theme="dark"] .logo-light {
display: block !important;
}
/* Pagination styling */
.pagination .page-link {
display: flex;
align-items: center;
justify-content: center;
min-width: 32px;
width: auto;
height: 32px;
padding: 0.25rem 0.5rem;
white-space: nowrap;
}
.pagination-sm .page-link {
min-width: 28px;
width: auto;
height: 28px;
font-size: 0.8125rem;
}
.pagination-lg .page-link {
min-width: calc(1.5rem + 1.5em + 2px);
width: auto;
}
.pagination .page-link svg {
width: 14px;
height: 14px;
}
.pagination .page-item.active .page-link {
background-color: var(--bs-primary);
border-color: var(--bs-primary);
}
</style>
<!-- Vendor CSS (plugins) -->
<link href="{{ asset('assets/velok/vendor/flatpickr/flatpickr.min.css') }}" rel="stylesheet" type="text/css"/>
<link href="{{ asset('assets/velok/vendor/flatpickr/plugins/monthSelect/style.css') }}" rel="stylesheet" type="text/css"/>
<link href="{{ asset('assets/velok/vendor/choices.js/choices.min.css') }}" rel="stylesheet" type="text/css"/>
<link href="{{ asset('assets/velok/vendor/gridjs/theme/mermaid.min.css') }}" rel="stylesheet" type="text/css"/>
<link href="{{ asset('assets/velok/vendor/sweetalert2/sweetalert2.min.css') }}" rel="stylesheet" type="text/css"/>
<link href="{{ asset('assets/velok/vendor/simplebar/simplebar.min.css') }}" rel="stylesheet" type="text/css"/>
<link href="{{ asset('assets/velok/vendor/daterangepicker/daterangepicker.css') }}" rel="stylesheet" type="text/css"/>
<!-- Select2 CSS -->
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
<link href="https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css" rel="stylesheet" />
<link href="{{ asset('assets/css/select2-dark-theme.min.css') }}" rel="stylesheet" type="text/css"/>
<link href="{{ asset('assets/css/vanilla-jsoneditor-dark-theme.min.css') }}" rel="stylesheet" type="text/css"/>
<style>
/* Select2 compact styling */
.select2-container--bootstrap-5 .select2-selection--single {
min-height: 31px;
padding: 0.25rem 0.5rem;
font-size: 0.8125rem;
}
.select2-container--bootstrap-5 .select2-selection--single .select2-selection__rendered {
padding: 0;
line-height: 1.5;
}
.select2-container--bootstrap-5 .select2-selection--single .select2-selection__arrow {
height: 29px;
}
.select2-container--bootstrap-5 .select2-dropdown .select2-results__option {
padding: 0.375rem 0.75rem !important;
font-size: 0.8125rem !important;
}
/* Fix sidebar submenu visibility - ensure .show overrides sidebar-hover hiding */
.main-nav .navbar-nav .collapse.show,
.main-nav .navbar-nav .collapsing {
display: block !important;
}
html.sidebar-hover .main-nav:hover .menu-item .collapse.show,
html.sidebar-hover .main-nav:hover .menu-item .collapsing {
display: block !important;
}
/* Ensure submenus are visible and properly styled */
.main-nav .sub-menu-nav {
display: flex;
flex-direction: column;
padding-left: 25px;
list-style: none;
margin: 0;
}
/* Fix dropdown menus in nav-pills (application tabs) */
.nav-pills .nav-item.dropdown {
position: relative;
}
.nav-pills .dropdown-toggle {
cursor: pointer;
}
.nav-pills .dropdown-toggle.btn-link {
padding: 0.25rem 0.75rem;
border: none;
background: transparent;
}
.nav-pills .dropdown-toggle.btn-link:hover,
.nav-pills .dropdown-toggle.btn-link:focus {
background-color: rgba(var(--bs-primary-rgb), 0.1);
border-radius: 0.375rem;
}
.nav-pills .dropdown-menu {
position: absolute;
z-index: 1055;
min-width: 12rem;
padding: 0.5rem 0;
margin-top: 0.125rem;
background-color: var(--bs-body-bg, #fff);
border: 1px solid var(--bs-border-color, rgba(0,0,0,.15));
border-radius: 0.375rem;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
.nav-pills .dropdown-item {
display: flex;
align-items: center;
padding: 0.5rem 1rem;
font-size: 0.8125rem;
color: var(--bs-body-color, #212529);
transition: background-color 0.15s ease-in-out;
}
.nav-pills .dropdown-item:hover,
.nav-pills .dropdown-item:focus {
background-color: var(--bs-tertiary-bg, #f8f9fa);
color: var(--bs-body-color, #1e2125);
}
.nav-pills .dropdown-divider {
margin: 0.5rem 0;
}
/* Nav tabs dropdown fix */
.nav-tabs .nav-item.dropdown {
position: relative;
}
.nav-tabs button.nav-link.dropdown-toggle {
background: none;
border: none;
border-bottom: 2px solid transparent;
cursor: pointer;
}
.nav-tabs button.nav-link.dropdown-toggle:hover {
border-bottom-color: var(--bs-primary);
}
.nav-tabs button.nav-link.dropdown-toggle.active {
border-bottom-color: var(--bs-primary);
}
.nav-tabs .dropdown-toggle::after {
margin-left: 0.25em;
}
.nav-tabs .dropdown-menu {
position: absolute;
z-index: 1055;
min-width: 12rem;
padding: 0.5rem 0;
margin-top: 0;
background-color: var(--bs-body-bg, #fff);
border: 1px solid var(--bs-border-color, rgba(0,0,0,.15));
border-radius: 0.375rem;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
.nav-tabs .dropdown-item {
padding: 0.5rem 1rem;
font-size: 0.875rem;
}
.nav-tabs .dropdown-item:hover,
.nav-tabs .dropdown-item:focus {
background-color: var(--bs-tertiary-bg, #f8f9fa);
}
.nav-tabs .dropdown-item.active {
background-color: var(--bs-primary);
color: #fff;
}
</style>
{% block css %}{% endblock %}
<!-- Lucide Icons (loaded in head to prevent icon flash) -->
<script src="{{ asset('assets/velok/vendor/lucide/umd/lucide.min.js') }}"></script>
</head>
<body>
<div class="wrapper">
<!-- Sidebar Navigation -->
{% if is_granted('ROLE_READ_ONLY') %}
{{ include('main-nav-read-only.html.twig') }}
{% else %}
{{ include('main-nav.html.twig') }}
{% endif %}
<!-- Top Header -->
<header class="topbar d-flex">
<div class="container-fluid">
<div class="navbar-header">
<div class="d-flex align-items-center gap-2 ms-auto">
<!-- Theme Color (Light/Dark) -->
<div class="topbar-item">
<button type="button" class="topbar-button fs-24" id="light-dark-mode" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Toggle Dark Mode">
<i data-lucide="moon" class="light-mode"></i>
<i data-lucide="sun" class="dark-mode"></i>
</button>
</div>
<!-- User Info & Logout -->
<div class="d-flex align-items-center gap-2">
<span class="d-flex align-items-center gap-2">
<span class="bg-primary text-white rounded-circle d-flex align-items-center justify-content-center" style="width: 28px; height: 28px; font-size: 12px; font-weight: 600;">
{{ app.user.username|first|upper }}
</span>
<span class="d-none d-md-block text-body fw-medium" style="font-size: 13px;">{{ app.user.username }}</span>
</span>
<a href="{{ path('app_logout') }}" class="btn btn-sm btn-outline-danger d-flex align-items-center gap-1" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Logout">
<i data-lucide="log-out" style="width: 14px; height: 14px;"></i>
<span class="d-none d-lg-inline">Logout</span>
</a>
</div>
</div>
</div>
</div>
</header>
<!-- Page Container -->
<div class="page-container">
<!-- Page Content -->
<div class="page-content">
{% block toolbar %}
{% if toolbarPage|default() %}
<div class="page-title-box mb-4">
<div class="d-flex align-items-center justify-content-between">
<h4 class="page-title mb-0">{{ toolbarPage }}</h4>
</div>
</div>
{% endif %}
{% endblock %}
{% for label, messages in app.flashes %}
{% for message in messages %}
<div class="alert alert-{{ label == 'error' ? 'danger' : (label == 'notice' ? 'info' : label) }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endfor %}
{% block content %}{% endblock %}
</div>
<!-- Footer -->
<footer class="footer">
<div class="container-fluid">
<div class="row">
<div class="col-12 text-center">
{{ "now"|date("Y") }} © Paynetics Admin. All rights reserved.
</div>
</div>
</div>
</footer>
</div>
</div>
<!-- jQuery -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
<!-- FOS JS Routing -->
<script src="{{ asset('bundles/fosjsrouting/js/router.min.js') }}"></script>
<script src="{{ path('fos_js_routing_js', { callback: 'fos.Router.setData' }) }}"></script>
<!-- Vendor JS (includes Bootstrap 5 bundle with Popper) -->
<script src="{{ asset('assets/velok/js/vendor.min.js') }}"></script>
<!-- Simplebar -->
<script src="{{ asset('assets/velok/vendor/simplebar/simplebar.min.js') }}"></script>
<!-- Initialize Lucide icons immediately (script loaded in head) -->
<script>if(typeof lucide!=='undefined'){lucide.createIcons();}</script>
<!-- Choices.js -->
<script src="{{ asset('assets/velok/vendor/choices.js/choices.min.js') }}"></script>
<!-- Select2 -->
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<!-- Flatpickr -->
<script src="{{ asset('assets/velok/vendor/flatpickr/flatpickr.min.js') }}"></script>
<script src="{{ asset('assets/velok/vendor/flatpickr/plugins/monthSelect/index.js') }}"></script>
<!-- Daterangepicker (requires moment.js) -->
<script src="{{ asset('assets/velok/vendor/daterangepicker/moment.min.js') }}"></script>
<script src="{{ asset('assets/velok/vendor/daterangepicker/daterangepicker.js') }}"></script>
<!-- GridJS -->
<script src="{{ asset('assets/velok/vendor/gridjs/gridjs.umd.js') }}"></script>
<!-- SweetAlert2 -->
<script src="{{ asset('assets/velok/vendor/sweetalert2/sweetalert2.min.js') }}"></script>
<!-- ApexCharts -->
<script src="{{ asset('assets/velok/vendor/apexcharts/apexcharts.min.js') }}"></script>
<!-- App JS -->
<script src="{{ asset('assets/velok/js/config.js') }}"></script>
<script src="{{ asset('assets/velok/js/layout.js') }}"></script>
<script src="{{ asset('assets/velok/js/app.js') }}"></script>
<!-- Initialize theme config immediately to prevent flash -->
<script>
if (typeof loadConfig === 'function') {
loadConfig();
}
</script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Theme toggle handler
var themeToggle = document.getElementById('light-dark-mode');
if (themeToggle) {
themeToggle.addEventListener('click', function() {
var currentTheme = document.documentElement.getAttribute('data-bs-theme') || 'light';
var newTheme = currentTheme === 'dark' ? 'light' : 'dark';
// Apply new theme
document.documentElement.setAttribute('data-bs-theme', newTheme);
// Save to cookie (365 days)
if (typeof window.setThemeCookie === 'function') {
window.setThemeCookie('theme', newTheme, 365);
}
window.currentTheme = newTheme;
// Re-initialize Lucide icons after theme change
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
}
// Initialize theme layout (includes dark mode toggle)
if (typeof loadLayout === 'function') {
loadLayout();
}
// Initialize app components
if (typeof loadApps === 'function') {
loadApps();
}
// Initialize Lucide icons
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
// Initialize Bootstrap components
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
});
// Initialize nav dropdowns (tabs and pills)
var navDropdowns = document.querySelectorAll('.nav-tabs .dropdown-toggle, .nav-pills .dropdown-toggle');
navDropdowns.forEach(function(dropdownToggle) {
new bootstrap.Dropdown(dropdownToggle);
});
// Fix dropdowns inside .table-responsive containers being clipped by overflow
document.querySelectorAll('.table-responsive').forEach(function(container) {
container.addEventListener('show.bs.dropdown', function() {
container.style.overflow = 'visible';
});
container.addEventListener('hide.bs.dropdown', function() {
container.style.overflow = '';
});
});
// Initialize Select2 AJAX dropdowns using jQuery ready
jQuery(function($) {
if ($.fn.select2) {
// Custom AJAX transport to set proper headers for JSON response
var jsonAjaxTransport = function(params, success, failure) {
var $request = $.ajax({
url: params.url,
data: params.data,
dataType: 'json',
beforeSend: function(xhr) {
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
}
});
$request.then(success);
$request.fail(failure);
return $request;
};
// Instance dropdowns
if ($('.select2-ajax-instances').length) {
$('.select2-ajax-instances').select2({
theme: 'bootstrap-5',
allowClear: true,
width: '100%',
placeholder: 'Type to search...',
minimumInputLength: 1,
ajax: {
url: function(params) {
return '/instances/' + (params.page || 1);
},
data: function(params) {
return { name: params.term };
},
transport: jsonAjaxTransport,
delay: 300,
cache: true,
processResults: function(data) {
var items = data && data.items ? data.items : [];
return {
results: items.map(function(item) {
return { id: item.token, text: item.name };
}),
pagination: { more: items.length >= 20 }
};
},
error: function(xhr, status, error) {
console.error('Instance AJAX error:', status, error);
}
}
});
}
// Merchant dropdowns
if ($('.select2-ajax-merchants').length) {
$('.select2-ajax-merchants').select2({
theme: 'bootstrap-5',
allowClear: true,
width: '100%',
placeholder: 'Type to search...',
minimumInputLength: 1,
ajax: {
url: function(params) {
return '/merchants/' + (params.page || 1);
},
data: function(params) {
return { name: params.term };
},
transport: jsonAjaxTransport,
delay: 300,
cache: true,
processResults: function(data) {
var items = data && data.items ? data.items : [];
return {
results: items.map(function(item) {
return { id: item.token, text: item.name };
}),
pagination: { more: items.length >= 20 }
};
},
error: function(xhr, status, error) {
console.error('Merchant AJAX error:', status, error);
}
}
});
}
// Program dropdowns
if ($('.select2-ajax-programs').length) {
$('.select2-ajax-programs').select2({
theme: 'bootstrap-5',
allowClear: true,
width: '100%',
placeholder: 'Type to search...',
minimumInputLength: 1,
ajax: {
url: function(params) {
return '/programs/' + (params.page || 1);
},
data: function(params) {
return { name: params.term };
},
transport: jsonAjaxTransport,
delay: 300,
cache: true,
processResults: function(data) {
var items = data && data.items ? data.items : [];
return {
results: items.map(function(item) {
return { id: item.token, text: item.name };
}),
pagination: { more: items.length >= 20 }
};
},
error: function(xhr, status, error) {
console.error('Program AJAX error:', status, error);
}
}
});
}
// Static select2 dropdowns (non-AJAX)
if ($('.select2-static').length) {
$('.select2-static').each(function() {
var $select = $(this);
var placeholder = $select.attr('data-placeholder') || $select.attr('placeholder') || $select.find('option[value=""]').text() || 'Select...';
$select.select2({
theme: 'bootstrap-5',
allowClear: true,
width: '100%',
placeholder: placeholder
});
});
}
console.log('Select2 initialized for',
$('.select2-ajax-instances').length, 'instances,',
$('.select2-ajax-merchants').length, 'merchants,',
$('.select2-ajax-programs').length, 'programs,',
$('.select2-static').length, 'static dropdowns'
);
} else {
console.warn('Select2 not loaded');
}
});
});
</script>
{% block js %}{% endblock %}
<!-- n8n Chat Widget -->
<div id="n8n-chat-container"></div>
<link href="https://cdn.jsdelivr.net/npm/@n8n/chat/dist/style.css" rel="stylesheet" />
<script type="module">
import { createChat } from 'https://cdn.jsdelivr.net/npm/@n8n/chat/dist/chat.bundle.es.js';
createChat({
target: '#n8n-chat-container',
webhookUrl: 'https://dnstart.app.n8n.cloud/webhook/04149076-d358-4fab-8961-7c38e09477d7/chat',
mode: 'window',
showWelcomeScreen: true,
defaultLanguage: 'en',
initialMessages: [
'Hello! 👋',
'How can I help you today?'
],
i18n: {
en: {
title: 'Admin Assistant',
subtitle: 'Powered by n8n',
footer: '',
getStarted: 'Start Chat',
inputPlaceholder: 'Type your question...',
}
}
});
</script>
<style>
/* n8n Chat Widget Positioning */
#n8n-chat-container {
position: fixed;
bottom: 0;
right: 0;
z-index: 9999;
}
</style>
</body>
</html>