clqms-be/app/Views/v2/layout/main_layout.php
mahdahar 97451496c3 feat(tests): enhance Test Management module with v2 UI dialogs
- Add new dialog forms for test calc, group, param, and title management
- Refactor test_dialog.php to new location (master/tests/)
- Update TestDefCalModel, TestDefSiteModel, TestDefTechModel, TestMapModel
- Modify Tests controller and Routes for new dialog handlers
- Update migration schema for test definitions
- Add new styles for v2 test management interface
- Include Test Management documentation files
2025-12-30 16:54:33 +07:00

390 lines
16 KiB
PHP

<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= esc($pageTitle ?? 'CLQMS') ?> - CLQMS</title>
<!-- Google Fonts - Inter -->
<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=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<!-- TailwindCSS 4 CDN -->
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<!-- Custom Styles -->
<link rel="stylesheet" href="<?= base_url('css/v2/styles.css') ?>">
<!-- 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/collapse@3.x.x/dist/cdn.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
</head>
<body class="min-h-screen flex" style="background: rgb(var(--color-bg));" x-data="layout()">
<!-- Sidebar -->
<aside
class="sidebar sticky top-0 z-40 h-screen flex flex-col shadow-2xl"
:class="sidebarOpen ? 'w-64' : 'w-0 lg:w-20'"
style="transition: width 300ms cubic-bezier(0.4, 0, 0.2, 1);"
>
<!-- Sidebar Header -->
<div class="h-16 flex items-center justify-between px-4 border-b border-white/10" x-show="sidebarOpen" x-cloak>
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-blue-600 to-blue-900 flex items-center justify-center shadow-lg">
<i class="fa-solid fa-flask text-white text-lg"></i>
</div>
<span class="text-xl font-bold text-white">CLQMS</span>
</div>
</div>
<!-- Collapsed Logo -->
<div class="h-16 flex items-center justify-center border-b border-white/10" x-show="!sidebarOpen" x-cloak>
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-blue-600 to-blue-900 flex items-center justify-center shadow-lg">
<i class="fa-solid fa-flask text-white text-lg"></i>
</div>
</div>
<!-- Navigation -->
<nav class="flex-1 py-6 overflow-y-auto" :class="sidebarOpen ? 'px-4' : 'px-2'">
<ul class="menu">
<!-- Dashboard -->
<li>
<a href="<?= base_url('/v2/') ?>"
:class="isActive('v2') ? 'active' : ''"
class="group">
<i class="fa-solid fa-th-large w-5 text-center transition-transform group-hover:scale-110"></i>
<span x-show="sidebarOpen" x-cloak>Dashboard</span>
</a>
</li>
<!-- Patients -->
<li>
<a href="<?= base_url('/v2/patients') ?>"
:class="isActive('patients') ? 'active' : ''"
class="group">
<i class="fa-solid fa-users w-5 text-center transition-transform group-hover:scale-110"></i>
<span x-show="sidebarOpen" x-cloak>Patients</span>
</a>
</li>
<!-- Lab Requests -->
<li>
<a href="<?= base_url('/v2/requests') ?>"
:class="isActive('requests') ? 'active' : ''"
class="group">
<i class="fa-solid fa-flask w-5 text-center transition-transform group-hover:scale-110"></i>
<span x-show="sidebarOpen" x-cloak>Lab Requests</span>
</a>
</li>
<!-- Master Data Sections -->
<template x-if="sidebarOpen">
<li class="px-3 py-2 mt-4 mb-1">
<span class="text-xs font-semibold uppercase tracking-wider opacity-60">Master Data</span>
</li>
</template>
<!-- Organization (Nested Group) -->
<li>
<div x-data="{
isOpen: orgOpen,
toggle() { this.isOpen = !this.isOpen; $root.layout().orgOpen = this.isOpen }
}" x-init="$watch('orgOpen', v => isOpen = v)">
<button @click="isOpen = !isOpen"
class="group w-full flex items-center justify-between"
:class="isParentActive('organization') ? 'text-primary font-medium' : ''">
<div class="flex items-center gap-3">
<i class="fa-solid fa-building w-5 text-center transition-transform group-hover:scale-110"></i>
<span x-show="sidebarOpen">Organization</span>
</div>
<i x-show="sidebarOpen" class="fa-solid fa-chevron-down text-xs transition-transform" :class="isOpen && 'rotate-180'"></i>
</button>
<ul x-show="isOpen && sidebarOpen" x-collapse class="ml-8 mt-2 space-y-1">
<li>
<a href="<?= base_url('/v2/master/organization/accounts') ?>"
:class="isActive('organization/accounts') ? 'active' : ''"
class="text-sm">Accounts</a>
</li>
<li>
<a href="<?= base_url('/v2/master/organization/sites') ?>"
:class="isActive('organization/sites') ? 'active' : ''"
class="text-sm">Sites</a>
</li>
<li>
<a href="<?= base_url('/v2/master/organization/disciplines') ?>"
:class="isActive('organization/disciplines') ? 'active' : ''"
class="text-sm">Disciplines</a>
</li>
<li>
<a href="<?= base_url('/v2/master/organization/departments') ?>"
:class="isActive('organization/departments') ? 'active' : ''"
class="text-sm">Departments</a>
</li>
<li>
<a href="<?= base_url('/v2/master/organization/workstations') ?>"
:class="isActive('organization/workstations') ? 'active' : ''"
class="text-sm">Workstations</a>
</li>
</ul>
</div>
</li>
<!-- Specimen (Nested Group) -->
<li>
<div x-data="{
isOpen: specimenOpen,
toggle() { this.isOpen = !this.isOpen; $root.layout().specimenOpen = this.isOpen }
}" x-init="$watch('specimenOpen', v => isOpen = v)">
<button @click="isOpen = !isOpen"
class="group w-full flex items-center justify-between"
:class="isParentActive('specimen') ? 'text-primary font-medium' : ''">
<div class="flex items-center gap-3">
<i class="fa-solid fa-vial w-5 text-center transition-transform group-hover:scale-110"></i>
<span x-show="sidebarOpen">Specimen</span>
</div>
<i x-show="sidebarOpen" class="fa-solid fa-chevron-down text-xs transition-transform" :class="isOpen && 'rotate-180'"></i>
</button>
<ul x-show="isOpen && sidebarOpen" x-collapse class="ml-8 mt-2 space-y-1">
<li>
<a href="<?= base_url('/v2/master/specimen/containers') ?>"
:class="isActive('specimen/containers') ? 'active' : ''"
class="text-sm">Container Defs</a>
</li>
<li>
<a href="<?= base_url('/v2/master/specimen/preparations') ?>"
:class="isActive('specimen/preparations') ? 'active' : ''"
class="text-sm">Preparations</a>
</li>
</ul>
</div>
</li>
<!-- Lab Tests -->
<li>
<a href="<?= base_url('/v2/master/tests') ?>"
:class="isActive('master/tests') ? 'active' : ''"
class="group">
<i class="fa-solid fa-microscope w-5 text-center transition-transform group-hover:scale-110"></i>
<span x-show="sidebarOpen">Lab Tests</span>
</a>
</li>
<!-- Value Sets -->
<li>
<a href="<?= base_url('/v2/master/valuesets') ?>"
:class="isActive('master/valuesets') ? 'active' : ''"
class="group">
<i class="fa-solid fa-list-check w-5 text-center transition-transform group-hover:scale-110"></i>
<span x-show="sidebarOpen">Value Sets</span>
</a>
</li>
<!-- Settings -->
<li class="mt-4">
<a href="<?= base_url('/v2/settings') ?>"
:class="isActive('settings') ? 'active' : ''"
class="group">
<i class="fa-solid fa-cog w-5 text-center transition-transform group-hover:scale-110"></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 backdrop-blur-sm"
x-cloak
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
></div>
<!-- Main Content Wrapper -->
<div class="flex-1 flex flex-col min-h-screen">
<!-- Top Navbar -->
<nav class="h-16 flex items-center justify-between px-4 lg:px-6 sticky top-0 z-20 glass shadow-sm">
<!-- Left: Burger Menu & Title -->
<div class="flex items-center gap-4">
<button @click="sidebarOpen = !sidebarOpen" class="btn btn-ghost btn-square">
<i class="fa-solid fa-bars text-lg"></i>
</button>
<h1 class="text-lg font-semibold" style="color: rgb(var(--color-text));"><?= esc($pageTitle ?? 'Dashboard') ?></h1>
</div>
<!-- Right: Theme Toggle & User -->
<div class="flex items-center gap-2">
<!-- Theme Toggle -->
<button @click="toggleTheme()" class="btn btn-ghost btn-square group" title="Toggle theme">
<i x-show="lightMode" class="fa-solid fa-moon text-lg transition-transform group-hover:rotate-12"></i>
<i x-show="!lightMode" class="fa-solid fa-sun text-lg transition-transform group-hover:rotate-45"></i>
</button>
<!-- User Dropdown -->
<div class="dropdown dropdown-end" x-data="{ open: false }">
<button @click="open = !open" class="btn btn-ghost gap-2 px-3">
<div class="w-8 h-8 rounded-full bg-gradient-to-br from-blue-600 to-blue-900 flex items-center justify-center shadow-md">
<span class="text-xs font-semibold text-white">U</span>
</div>
<span class="hidden sm:inline text-sm font-medium">User</span>
<i class="fa-solid fa-chevron-down text-xs opacity-60 transition-transform" :class="open && 'rotate-180'"></i>
</button>
<!-- Dropdown Content -->
<div
x-show="open"
@click.away="open = false"
x-cloak
class="dropdown-content mt-2 w-72 shadow-2xl"
x-transition:enter="transition ease-out duration-150"
x-transition:enter-start="opacity-0 transform scale-95"
x-transition:enter-end="opacity-100 transform scale-100"
x-transition:leave="transition ease-in duration-100"
x-transition:leave-start="opacity-100 transform scale-100"
x-transition:leave-end="opacity-0 transform scale-95"
>
<!-- User Info Header -->
<div class="px-4 py-4" style="border-bottom: 1px solid rgb(var(--color-border));">
<div class="flex items-center gap-3">
<div class="w-12 h-12 rounded-full bg-gradient-to-br from-blue-600 to-blue-900 flex items-center justify-center shadow-lg">
<span class="text-sm font-semibold text-white">U</span>
</div>
<div>
<p class="font-semibold text-sm" style="color: rgb(var(--color-text));">User Name</p>
<p class="text-xs" style="color: rgb(var(--color-text-muted));">user@example.com</p>
</div>
</div>
</div>
<!-- Menu Items -->
<ul class="menu menu-sm p-2">
<li>
<a href="#" class="flex items-center gap-3 py-2">
<i class="fa-solid fa-user w-4 text-center"></i>
<span>Profile</span>
</a>
</li>
<li>
<a href="#" class="flex items-center gap-3 py-2">
<i class="fa-solid fa-cog w-4 text-center"></i>
<span>Settings</span>
</a>
</li>
</ul>
<!-- Logout -->
<div style="border-top: 1px solid rgb(var(--color-border));" class="p-2">
<button @click="logout()" class="btn btn-ghost btn-sm w-full justify-start gap-3 hover:bg-red-50" style="color: rgb(var(--color-error));">
<i class="fa-solid fa-sign-out-alt w-4 text-center"></i>
<span>Logout</span>
</button>
</div>
</div>
</div>
</div>
</nav>
<!-- Page Content -->
<main class="flex-1 p-4 lg:p-6 overflow-auto">
<?= $this->renderSection('content') ?>
</main>
<!-- Footer -->
<footer class="glass border-t py-4 px-6" style="border-color: rgb(var(--color-border));">
<div class="flex flex-col sm:flex-row items-center justify-between gap-2 text-sm" style="color: rgb(var(--color-text-muted));">
<span>© 2025 5Panda. All rights reserved.</span>
<span>CLQMS v1.0.0</span>
</div>
</footer>
</div>
<!-- Global Scripts -->
<script>
window.BASEURL = "<?= base_url() ?>".replace(/\/$/, "") + "/";
function layout() {
return {
sidebarOpen: localStorage.getItem('sidebarOpen') !== 'false',
lightMode: localStorage.getItem('theme') !== 'dark',
orgOpen: false,
specimenOpen: false,
currentPath: window.location.pathname,
init() {
// Apply saved theme (default to light theme)
const savedTheme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-theme', savedTheme);
this.lightMode = savedTheme === 'light';
// Detect sidebar open/closed for mobile
if (window.innerWidth < 1024) this.sidebarOpen = false;
// Auto-expand menus based on active path
this.orgOpen = this.currentPath.includes('organization');
this.specimenOpen = this.currentPath.includes('specimen');
// Watch sidebar state to persist
this.$watch('sidebarOpen', val => localStorage.setItem('sidebarOpen', val));
},
isActive(path) {
// Get the current path without query strings or hash
const current = window.location.pathname;
// Handle dashboard as root - exact match only
if (path === 'v2') {
return current === '/v2' || current === '/v2/' || current === '/clqms-be/v2' || current === '/clqms-be/v2/';
}
// For other paths, check if current path contains the expected path segment
// Use exact match with /v2/ prefix
const checkPath = '/v2/' + path;
return current.includes(checkPath);
},
isParentActive(parent) {
return this.currentPath.includes(parent);
},
toggleTheme() {
this.lightMode = !this.lightMode;
const theme = this.lightMode ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
},
async logout() {
try {
const res = await fetch(`${BASEURL}v2/auth/logout`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include'
});
if (res.ok) {
window.location.href = `${BASEURL}v2/login`;
}
} catch (err) {
console.error('Logout error:', err);
window.location.href = `${BASEURL}v2/login`;
}
}
}
}
</script>
<?= $this->renderSection('script') ?>
</body>
</html>