import { createApp, h, defineAsyncComponent, ref, watch } from 'vue'; import { createPinia } from 'pinia'; import axios from 'axios'; import { handleSessionExpired } from './composables/Core/useSessionGuard.js'; import TopHeader from './Pages/Core/Fragments/TopHeader.vue'; import BottomNav from './Pages/Core/Fragments/BottomNav.vue'; import { deepEqual } from './utils/deepEqual'; import { useModal } from './composables/Core/useModal.js'; import { useNavigate, setPageModules } from './composables/Core/useNavigate.js'; import BaseModal from './Components/Core/BaseModal.vue'; import { useNetworkStore } from './stores/network.js'; import { useUserStore } from './stores/user.js'; import { useUIStore } from './stores/ui.js'; import { useAuth } from './composables/Core/useAuth.js'; import { useSessionGuard, startGuard } from './composables/Core/useSessionGuard.js'; import LottiePlayer from './Components/Core/Animations/LottiePlayer.vue'; import AnimatedButton from './Components/Core/Animations/AnimatedButton.vue'; import RouteTransition from './Components/Core/Animations/RouteTransition.vue'; // --- Chunk load error handler (stale hash after rebuild) --- function handleChunkError(error) { const isChunkError = error?.message?.includes('Failed to fetch dynamically imported module') || error?.message?.includes('Importing a module script failed') || error?.message?.includes('error loading dynamically imported module') || error?.message?.includes('Loading chunk') || error?.message?.includes('Loading CSS chunk'); if (isChunkError) { // Guard against infinite reload loops — only retry once per URL const reloadKey = 'chunk-reload:' + window.location.pathname; if (!sessionStorage.getItem(reloadKey)) { sessionStorage.setItem(reloadKey, '1'); console.warn('[ChunkReload] Stale chunk detected, reloading page…'); window.location.reload(); } else { // Already retried once — clear the flag for next time and log sessionStorage.removeItem(reloadKey); console.error('[ChunkReload] Reload already attempted, chunk still failing:', error); } } } // Safety-net: catch any unhandled dynamic import rejections globally window.addEventListener('unhandledrejection', (event) => { handleChunkError(event.reason); }); // Axios request interceptor: ensure POST requests always have at least an empty object as data. // This prevents 400 Bad Request errors on some production environments (Nginx/WAF) that reject empty POST bodies. axios.interceptors.request.use(config => { if (config.method?.toLowerCase() === 'post' && (config.data === undefined || config.data === null)) { config.data = {}; } return config; }); // Axios interceptor: catch 401/419 from any API call and trigger logout. // Acts as a last-resort fallback when SSE and polling have both failed. // Skips guest-probe endpoints that legitimately 401 for unauthenticated visitors, // and only redirects when we previously had an authenticated session — otherwise // a guest hitting "/" would be bounced to /login the moment any auth-required // probe returns 401. const SESSION_GUARD_SKIP = [ '/post/loginnow', '/get/isloggedin', '/sse/stream', '/account_settings/details', '/get/user/acct-type', ]; axios.interceptors.response.use( response => response, error => { const status = error?.response?.status; const url = error?.config?.url ?? ''; const onLoginPage = window.location.pathname === '/login' || window.location.pathname === '/sp/login'; const acctType = sessionStorage.getItem('user_acct_type'); const hadAuthenticatedSession = acctType && acctType !== 'public'; if ( (status === 401 || status === 419) && !onLoginPage && hadAuthenticatedSession && !SESSION_GUARD_SKIP.some(s => url.includes(s)) ) { handleSessionExpired(); } return Promise.reject(error); } ); const modal = useModal() // --- Auto-import all pages (exclude Fragments) --- const modules = import.meta.glob('./Pages/**/*.vue'); const pages = {}; for (const path in modules) { // Exclude global layout/core fragments that aren't standalone pages if (path.includes('Core/Fragments')) continue; const key = path .replace('./Pages/', '') .replace('.vue', '') .replace(/\//g, '.'); pages[key] = modules[path]; // Also add a simplified lowercase version for easier server-side matching const simplifiedKey = key.toLowerCase().replace(/\./g, '/'); pages[simplifiedKey] = modules[path]; } // Ensure fallback NotFound if (!pages['Core.NotFound']) { pages['Core.NotFound'] = modules['./Pages/Core/NotFound.vue']; } // Wire up page modules for prefetching (hover-based eager loading) setPageModules(pages); // --- Get initial page from server --- const el = document.getElementById('main-body'); const initialPage = JSON.parse(el.dataset.page); const app = createApp({ setup() { const currentPage = ref(initialPage.component); const currentProps = ref(initialPage.props || {}); const loading = ref(false); // UI Store for global state const uiStore = useUIStore(); // Start polling /get/isloggedin — auto-logout on session expiry useSessionGuard(); const { navigate, reloadPage } = useNavigate({ currentPage, currentProps, loading }) // Initialize User Store and fetch user data on app load const userStore = useUserStore(); userStore.fetchCurrentUser().then(() => { if (userStore.user?.settings) { uiStore.syncSettings(userStore.user.settings); } // Re-attempt session guard once acctType is known. The initial call in // setup() bails for guests (no acctType yet); after fetchCurrentUser // resolves with a real authenticated user we kick it off here. startGuard(); }); // Ensure the initial page is in the history state so that back/forward works correctly from the start if (typeof window !== 'undefined' && !window.history.state) { window.history.replaceState({ page: initialPage.component, props: initialPage.props }, '', window.location.href); } // Fetch public system settings (branding, disabled pages, etc.) // First sync from initial props to avoid flutters if (initialPage.props?.systemSettings) { uiStore.syncSettings(initialPage.props.systemSettings); } uiStore.refreshSettings(); // Start background interval to refresh system settings every 1 minute setInterval(() => { uiStore.refreshSettings(); }, 60000); // Provide global access to user details app.config.globalProperties.$user = userStore; app.config.globalProperties.$auth = useAuth(); // --- AsyncComponent based on currentPage const AsyncComponent = ref( defineAsyncComponent({ loader: () => (pages[currentPage.value] || pages['Core.NotFound'])(), onError: (error, retry, fail) => { handleChunkError(error); fail(); // let Vue handle the error if reload didn't trigger }, }) ); // Watch page changes and reload async component watch(currentPage, (newPage) => { // Close any open modals on page change modal.close(); // Reset titles on page change (components can override this in onMounted) uiStore.resetPageTitle(); const mainContent = document.querySelector('.main-content'); if (mainContent) mainContent.scrollTop = 0; AsyncComponent.value = defineAsyncComponent({ loader: () => (pages[newPage] || pages['Core.NotFound'])(), onError: (error, retry, fail) => { handleChunkError(error); fail(); }, }); }); // Watch for logo changes and update favicon/apple-touch-icon DOM elements watch(() => uiStore.appLogo, (logoUrl) => { if (!logoUrl) return; document.querySelectorAll('link[rel="shortcut icon"], link[rel="icon"], link[rel="apple-touch-icon"], link[rel="apple-touch-icon-precomposed"]').forEach(link => { link.href = logoUrl; }); }, { immediate: true }); // Watch for dark mode and update body class watch(() => uiStore.darkMode, (isDark) => { if (isDark) { document.body.classList.add('dark-mode'); } else { document.body.classList.remove('dark-mode'); } }, { immediate: true }); // Watch for full-width mode and update body class watch(() => uiStore.isFullWidth, (isFull) => { if (isFull) { document.body.classList.add('is-full-width'); } else { document.body.classList.remove('is-full-width'); } }, { immediate: true }); return { currentPage, currentProps, loading, navigate, AsyncComponent, uiStore }; }, render() { return h('div', { class: ['app-layout', { 'is-full-width': this.uiStore.isFullWidth }] }, [ this.uiStore.showHeader ? h(TopHeader) : null, h('main', { class: ['main-content', { 'no-header': !this.uiStore.showHeader, 'no-nav': !this.uiStore.showBottomNav }] }, [ h(RouteTransition, { routeKey: this.currentPage }, { default: () => h(this.AsyncComponent, this.currentProps) }) ]), this.uiStore.showBottomNav ? h(BottomNav) : null, h( BaseModal, { modelValue: modal.show.value, 'onUpdate:modelValue': v => (modal.show.value = v), modalTitle: modal.title.value, body: modal.body.value, footerClose: !modal.footer.value, }, modal.footer.value ? { footer: () => h(modal.footer.value) } : {} ), ]); }, }); // Add global styles for the layout to ensure no overlap const style = document.createElement('style'); style.textContent = ` :root { --bg-primary: #ffffff; --bg-secondary: #f8f9fa; --bg-tertiary: #f0f2f5; --bg-card: #ffffff; --text-primary: #1e1e1e; --text-secondary: #717171; --text-muted: #a0a0a0; --border-color: rgba(0, 0, 0, 0.08); --accent-color: #533dea; --accent-soft: rgba(83, 61, 234, 0.1); --header-bg: rgba(255, 255, 255, 0.85); --nav-bg: rgba(255, 255, 255, 0.85); --bg-html: #fcebeb; --icon-filter: none; --layout-max-width: 1440px; } body.dark-mode { --bg-primary: #121418; --bg-secondary: #1a1c22; --bg-tertiary: #24272d; --bg-card: #1f2228; --text-primary: #e0e0e0; --text-secondary: #b0b0b0; --text-muted: #888888; --border-color: rgba(255, 255, 255, 0.08); --accent-color: #816ef0; --accent-soft: rgba(129, 110, 240, 0.15); --header-bg: rgba(18, 20, 24, 0.9); --nav-bg: rgba(18, 20, 24, 0.95); --bg-html: #1A0D0D; --icon-filter: brightness(0) invert(1); } html { background-color: var(--bg-html); transition: background-color 0.3s ease; } .app-layout { display: flex; flex-direction: column; min-height: 100vh; background-color: var(--bg-primary); color: var(--text-primary); transition: background-color 0.3s ease, color 0.3s ease; } body { margin: 0 auto !important; padding: 0; overflow: hidden; /* Prevent double scrollbars if main-content scrolls */ max-width: var(--layout-max-width, 100%); width: 100%; transition: max-width 0.3s ease, background-color 0.3s ease; background-color: var(--bg-primary); color: var(--text-primary); } body.is-full-width { max-width: none !important; } body.is-full-width .header, body.is-full-width .bottom-navigation-bar { max-width: none !important; } .app-layout.is-full-width .tf-container { max-width: none !important; width: 100% !important; padding: 0 !important; } .main-content { flex: 1; padding-top: 66px; /* Height of TopHeader (56px) + safe margin */ padding-bottom: 80px; /* Height of BottomNav + some breathing room */ overflow-y: auto; background-color: var(--bg-primary); transition: background-color 0.3s ease, padding 0.3s ease; } .main-content.no-header { padding-top: 0 !important; } .main-content.no-nav { padding-bottom: 0 !important; } /* Global theme consistency overrides */ .dark-mode .tf-container, .dark-mode .manage-stores-page, .dark-mode .home-page, .dark-mode .account-settings-page, .dark-mode .card-section, .dark-mode .store-details-page, .dark-mode .pos-container, .dark-mode .box-user { background-color: transparent !important; background: transparent !important; color: var(--text-primary) !important; } .dark-mode h1, .dark-mode h2, .dark-mode h3, .dark-mode h4, .dark-mode h5, .dark-mode h6, .dark-mode span, .dark-mode p, .dark-mode label, .dark-mode i, .dark-mode div:not(.icon-box):not(.btn) { color: var(--text-primary) !important; } .dark-mode .text-muted, .dark-mode .text-secondary, .dark-mode .smallest { color: var(--text-secondary) !important; } .dark-mode input, .dark-mode select, .dark-mode textarea, .dark-mode .search-field { background-color: var(--bg-secondary) !important; color: var(--text-primary) !important; border-color: var(--border-color) !important; } .dark-mode .card, .dark-mode .list-user-info, .dark-mode .store-table-container, .dark-mode .manage-stores-page .store-table-container, .dark-mode .pos-container .products-side, .dark-mode .pos-container .cart-side, .dark-mode .pos-container .product-card, .dark-mode .profile-card, .dark-mode .suggestions-dropdown, .dark-mode .scanner-container { background-color: var(--bg-card) !important; color: var(--text-primary) !important; border-color: var(--border-color) !important; } .dark-mode .table tbody tr, .dark-mode .table td { background-color: transparent !important; color: var(--text-primary) !important; border-color: var(--border-color) !important; } .dark-mode .table tr.active, .dark-mode .table tr:hover { background-color: var(--bg-tertiary) !important; } .dark-mode .icon-wrapper, .dark-mode .icon-box { background-color: var(--accent-soft) !important; border: 1px solid var(--border-color); } /* Force standard border color for all themed elements */ .dark-mode div, .dark-mode section, .dark-mode ul, .dark-mode li, .dark-mode header, .dark-mode footer, .dark-mode nav { border-color: var(--border-color) !important; } .dark-mode .table thead th { background-color: var(--bg-tertiary) !important; color: var(--accent-color) !important; border-bottom: 1px solid var(--border-color); } .dark-mode .table tbody td { border-bottom: 1px solid var(--border-color); color: var(--text-primary) !important; } .dark-mode .header, .dark-mode .top-header { background-color: var(--header-bg) !important; backdrop-filter: blur(12px) !important; -webkit-backdrop-filter: blur(12px) !important; border-bottom-color: var(--border-color) !important; } .dark-mode .bottom-navigation-bar { background-color: var(--nav-bg) !important; backdrop-filter: blur(12px) !important; -webkit-backdrop-filter: blur(12px) !important; border-top-color: var(--border-color) !important; box-shadow: 0 -4px 30px rgba(0, 0, 0, 0.5) !important; } /* Bootstrap class overrides for dark mode — neutralizes hardcoded bg-white / bg-light / text-dark so components don't need per-file :global(.dark-mode) patches for these specific classes */ .dark-mode .bg-white { background-color: var(--bg-card) !important; } .dark-mode .bg-light { background-color: var(--bg-secondary) !important; } .dark-mode .text-dark { color: var(--text-primary) !important; } .dark-mode .border { border-color: var(--border-color) !important; } .dark-mode .modal-content { background-color: var(--bg-card) !important; color: var(--text-primary) !important; border-color: var(--border-color) !important; } .dark-mode .dropdown-menu { background-color: var(--bg-card) !important; border-color: var(--border-color) !important; } .dark-mode .dropdown-item { color: var(--text-primary) !important; } .dark-mode .dropdown-item:hover { background-color: var(--bg-tertiary) !important; } .dark-mode .list-group-item { background-color: var(--bg-card) !important; color: var(--text-primary) !important; border-color: var(--border-color) !important; } .dark-mode .modal-header, .dark-mode .modal-footer { border-color: var(--border-color) !important; } /* Glassmorphism utility */ .glass-card { background: rgba(255, 255, 255, 0.4) !important; backdrop-filter: blur(10px) !important; -webkit-backdrop-filter: blur(10px) !important; border: 1px solid rgba(0, 0, 0, 0.05) !important; box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.07) !important; } .dark-mode .glass-card { background: rgba(31, 34, 40, 0.7) !important; backdrop-filter: blur(10px) !important; -webkit-backdrop-filter: blur(10px) !important; border: 1px solid rgba(255, 255, 255, 0.08) !important; box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3) !important; } /* Theme-aware icons */ .icon-box img, .icon-wrapper img, .nav-icon, .icon-user { filter: var(--icon-filter); transition: filter 0.3s ease; } `; document.head.appendChild(style); app.config.globalProperties.$modal = modal; // Initialize Pinia const pinia = createPinia(); app.use(pinia); // --- Global navigate helper let rootInstance = null; app.config.globalProperties.$navigate = payload => { if (rootInstance) { rootInstance.navigate(payload); } else { console.warn('App not yet mounted, $navigate called too early.'); } }; const network = useNetworkStore() // app.use(network) // Removed: Pinia stores are not Vue plugins. Pinia (the instance) is already registered above. // Provide global access to user details - moved inside setup function window.addEventListener('online', () => { network.setOnline(true) }) window.addEventListener('offline', () => { network.setOnline(false) }) // Register components app.component('TopHeader', TopHeader); app.component('BottomNav', BottomNav); app.component('LottiePlayer', LottiePlayer); app.component('AnimatedButton', AnimatedButton); rootInstance = app.mount('#app'); window.$navigateHelper = app.config.globalProperties.$navigate; // Expose prefetch helper globally for hover-based chunk preloading window.$prefetchPage = (pageName) => { if (!pageName) return; const loader = pages[pageName]; if (loader) loader().catch(() => {}); }; // Register Service Worker if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js') .then(registration => { console.log('ServiceWorker registration successful with scope: ', registration.scope); }) .catch(error => { console.log('ServiceWorker registration failed: ', error); }); }); }