- Implement JWT authentication with HTTP-only cookies - Create /v2/* namespace to avoid conflicts with existing frontend - Upgrade to DaisyUI 5 + Tailwind CSS 4 - Add light/dark theme toggle with smooth transitions - Build login page, dashboard, and patient list UI - Protect V2 routes with auth middleware - Add comprehensive documentation No breaking changes - all new features under /v2/* namespace
223 lines
8.1 KiB
PHP
223 lines
8.1 KiB
PHP
<!DOCTYPE html>
|
|
<html lang="en" data-theme="corporate">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title><?= esc($pageTitle ?? 'CLQMS') ?> - CLQMS</title>
|
|
|
|
<!-- TailwindCSS 4 + DaisyUI 5 CDN -->
|
|
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" />
|
|
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
|
|
|
<!-- FontAwesome -->
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/js/all.min.js"></script>
|
|
|
|
<!-- Alpine.js -->
|
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
|
|
|
<style>
|
|
[x-cloak] { display: none !important; }
|
|
|
|
/* Smooth theme transition */
|
|
* {
|
|
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
|
|
}
|
|
|
|
/* Custom scrollbar - light theme optimized */
|
|
::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
::-webkit-scrollbar-track { background: #f1f5f9; }
|
|
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
|
|
::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
|
|
|
|
/* Dark theme scrollbar */
|
|
[data-theme="business"] ::-webkit-scrollbar-track { background: rgba(0,0,0,0.1); }
|
|
[data-theme="business"] ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); }
|
|
[data-theme="business"] ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.3); }
|
|
|
|
/* Sidebar transition */
|
|
.sidebar-transition { transition: width 0.3s ease, transform 0.3s ease; }
|
|
|
|
/* Menu active state enhancement */
|
|
.menu li > *:not(.menu-title):not(.btn):active,
|
|
.menu li > *:not(.menu-title):not(.btn).active {
|
|
background-color: oklch(var(--p));
|
|
color: oklch(var(--pc));
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="min-h-screen flex bg-base-200" x-data="layout()">
|
|
|
|
<!-- Sidebar -->
|
|
<aside
|
|
class="sidebar-transition fixed lg:relative z-40 h-screen bg-base-300 flex flex-col shadow-xl"
|
|
:class="sidebarOpen ? 'w-56' : 'w-0 lg:w-16'"
|
|
>
|
|
<!-- Sidebar Header -->
|
|
<div class="h-16 flex items-center justify-between px-4 border-b border-base-content/10" x-show="sidebarOpen" x-cloak>
|
|
<span class="text-xl font-bold text-primary">CLQMS</span>
|
|
</div>
|
|
|
|
<!-- Navigation -->
|
|
<nav class="flex-1 py-4 overflow-y-auto" :class="sidebarOpen ? 'px-3' : 'px-2'">
|
|
<ul class="menu space-y-2">
|
|
<!-- Dashboard -->
|
|
<li>
|
|
<a href="<?= base_url('/v2/') ?>"
|
|
:class="'<?= $activePage ?? '' ?>' === 'dashboard' ? 'active' : ''"
|
|
class="flex items-center gap-3">
|
|
<i class="fa-solid fa-th-large w-5 text-center"></i>
|
|
<span x-show="sidebarOpen" x-cloak>Dashboard</span>
|
|
</a>
|
|
</li>
|
|
|
|
<!-- Patients -->
|
|
<li>
|
|
<a href="<?= base_url('/v2/patients') ?>"
|
|
:class="'<?= $activePage ?? '' ?>' === 'patients' ? 'active' : ''"
|
|
class="flex items-center gap-3">
|
|
<i class="fa-solid fa-users w-5 text-center"></i>
|
|
<span x-show="sidebarOpen" x-cloak>Patients</span>
|
|
</a>
|
|
</li>
|
|
|
|
<!-- Lab Requests -->
|
|
<li>
|
|
<a href="<?= base_url('/v2/requests') ?>"
|
|
:class="'<?= $activePage ?? '' ?>' === 'requests' ? 'active' : ''"
|
|
class="flex items-center gap-3">
|
|
<i class="fa-solid fa-flask w-5 text-center"></i>
|
|
<span x-show="sidebarOpen" x-cloak>Lab Requests</span>
|
|
</a>
|
|
</li>
|
|
|
|
<!-- Settings -->
|
|
<li>
|
|
<a href="<?= base_url('/v2/settings') ?>"
|
|
:class="'<?= $activePage ?? '' ?>' === 'settings' ? 'active' : ''"
|
|
class="flex items-center gap-3">
|
|
<i class="fa-solid fa-cog w-5 text-center"></i>
|
|
<span x-show="sidebarOpen" x-cloak>Settings</span>
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</nav>
|
|
</aside>
|
|
|
|
<!-- Overlay for mobile -->
|
|
<div
|
|
x-show="sidebarOpen"
|
|
@click="sidebarOpen = false"
|
|
class="fixed inset-0 bg-black/50 z-30 lg:hidden"
|
|
x-cloak
|
|
></div>
|
|
|
|
<!-- Main Content Wrapper -->
|
|
<div class="flex-1 flex flex-col min-h-screen">
|
|
|
|
<!-- Top Navbar -->
|
|
<nav class="h-16 bg-base-100 border-b border-base-content/10 flex items-center justify-between px-4 sticky top-0 z-20">
|
|
<!-- Left: Burger Menu -->
|
|
<div class="flex items-center gap-4">
|
|
<button @click="sidebarOpen = !sidebarOpen" class="btn btn-ghost btn-sm btn-square">
|
|
<i class="fa-solid fa-bars text-lg"></i>
|
|
</button>
|
|
<h1 class="text-lg font-semibold"><?= esc($pageTitle ?? 'Dashboard') ?></h1>
|
|
</div>
|
|
|
|
<!-- Right: Theme Toggle & User -->
|
|
<div class="flex items-center gap-2">
|
|
<!-- Theme Toggle -->
|
|
<label class="swap swap-rotate btn btn-ghost btn-sm btn-square">
|
|
<input type="checkbox" class="theme-controller" value="corporate" @change="toggleTheme($event)" :checked="lightMode" />
|
|
<i class="swap-off fa-solid fa-moon text-lg"></i>
|
|
<i class="swap-on fa-solid fa-sun text-lg"></i>
|
|
</label>
|
|
|
|
<!-- User Dropdown -->
|
|
<div class="dropdown dropdown-end">
|
|
<div tabindex="0" role="button" class="btn btn-ghost btn-circle avatar placeholder">
|
|
<div class="bg-primary text-primary-content rounded-full w-10">
|
|
<span class="text-sm">U</span>
|
|
</div>
|
|
</div>
|
|
<ul tabindex="0" class="dropdown-content menu bg-base-100 rounded-box z-50 w-52 p-2 shadow-lg border border-base-content/10">
|
|
<li><a href="#"><i class="fa-solid fa-user mr-2"></i> Profile</a></li>
|
|
<li><a href="#"><i class="fa-solid fa-cog mr-2"></i> Settings</a></li>
|
|
<li class="border-t border-base-content/10 mt-1 pt-1">
|
|
<a @click="logout()" class="text-error">
|
|
<i class="fa-solid fa-sign-out-alt mr-2"></i> Logout
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- Page Content -->
|
|
<main class="flex-1 p-4 lg:p-6 overflow-auto">
|
|
<?= $this->renderSection('content') ?>
|
|
</main>
|
|
|
|
<!-- Footer -->
|
|
<footer class="bg-base-100 border-t border-base-content/10 py-4 px-6">
|
|
<div class="flex flex-col sm:flex-row items-center justify-between gap-2 text-sm text-base-content/60">
|
|
<span>© 2025 5Panda. All rights reserved.</span>
|
|
<span>CLQMS v1.0.0</span>
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
|
|
<!-- Global Scripts -->
|
|
<script>
|
|
window.BASEURL = "<?= base_url() ?>";
|
|
|
|
function layout() {
|
|
return {
|
|
sidebarOpen: window.innerWidth >= 1024,
|
|
lightMode: localStorage.getItem('theme') === 'corporate',
|
|
|
|
init() {
|
|
// Apply saved theme (default to light theme)
|
|
const savedTheme = localStorage.getItem('theme') || 'corporate';
|
|
document.documentElement.setAttribute('data-theme', savedTheme);
|
|
this.lightMode = savedTheme === 'corporate';
|
|
|
|
// Handle resize
|
|
window.addEventListener('resize', () => {
|
|
if (window.innerWidth >= 1024) {
|
|
this.sidebarOpen = true;
|
|
}
|
|
});
|
|
},
|
|
|
|
toggleTheme(event) {
|
|
const theme = event.target.checked ? 'corporate' : 'business';
|
|
document.documentElement.setAttribute('data-theme', theme);
|
|
localStorage.setItem('theme', theme);
|
|
this.lightMode = event.target.checked;
|
|
},
|
|
|
|
async logout() {
|
|
try {
|
|
const res = await fetch(`${BASEURL}api/auth/logout`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' }
|
|
});
|
|
|
|
if (res.ok) {
|
|
window.location.href = `${BASEURL}v2/login`;
|
|
}
|
|
} catch (err) {
|
|
console.error('Logout error:', err);
|
|
// Force redirect even on error
|
|
window.location.href = `${BASEURL}v2/login`;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<?= $this->renderSection('script') ?>
|
|
</body>
|
|
</html>
|