2025-12-24 16:43:52 +07:00

388 lines
16 KiB
PHP

<!DOCTYPE html>
<html lang="en" data-theme="clqms">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= $title ?? 'V2' ?> - CLQMS</title>
<meta name="description" content="Clinical Laboratory Quality Management System - Your trusted partner for laboratory quality excellence.">
<!-- Tailwind 4 + DaisyUI 5 CDN -->
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" />
<link href="https://cdn.jsdelivr.net/npm/daisyui@5/themes.css" rel="stylesheet" type="text/css" />
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<!-- Google Fonts - Inter for modern typography -->
<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">
<!-- Alpine.js -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<!-- Lucide Icons (local) -->
<script src="<?= base_url('assets/js/lucide.min.js') ?>"></script>
<style type="text/tailwindcss">
/* Custom CLQMS Medical Theme */
@theme {
/* Custom colors for medical/laboratory feel */
--color-medical-teal: oklch(65% 0.15 180);
--color-medical-dark: oklch(25% 0.03 240);
--color-medical-light: oklch(97% 0.01 180);
--color-medical-accent: oklch(70% 0.18 200);
}
/* CLQMS Theme Override */
[data-theme="clqms"] {
--color-base-100: oklch(98% 0.005 240);
--color-base-200: oklch(95% 0.01 240);
--color-base-300: oklch(90% 0.015 240);
--color-base-content: oklch(25% 0.02 240);
--color-primary: oklch(55% 0.2 175);
--color-primary-content: oklch(100% 0 0);
--color-secondary: oklch(60% 0.15 250);
--color-secondary-content: oklch(100% 0 0);
--color-accent: oklch(70% 0.2 140);
--color-accent-content: oklch(20% 0.05 140);
--color-neutral: oklch(30% 0.02 250);
--color-neutral-content: oklch(95% 0.01 250);
--color-info: oklch(65% 0.2 230);
--color-info-content: oklch(100% 0 0);
--color-success: oklch(65% 0.2 145);
--color-success-content: oklch(100% 0 0);
--color-warning: oklch(80% 0.18 85);
--color-warning-content: oklch(25% 0.05 85);
--color-error: oklch(60% 0.25 25);
--color-error-content: oklch(100% 0 0);
}
/* Animations */
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
@keyframes pulse-soft {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
.animate-fade-in-up {
animation: fadeInUp 0.5s ease-out forwards;
}
.animate-shimmer {
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
background-size: 200% 100%;
animation: shimmer 2s infinite;
}
.animate-pulse-soft {
animation: pulse-soft 2s infinite;
}
</style>
<style>
/* Base typography */
body {
font-family: 'Inter', system-ui, sans-serif;
}
/* Sidebar gradient backdrop */
.sidebar-gradient {
background: linear-gradient(180deg,
oklch(30% 0.05 250) 0%,
oklch(25% 0.04 260) 50%,
oklch(22% 0.03 270) 100%
);
}
/* Glass effect for header */
.glass-header {
backdrop-filter: blur(16px) saturate(180%);
-webkit-backdrop-filter: blur(16px) saturate(180%);
}
/* Subtle pattern overlay */
.pattern-dots {
background-image: radial-gradient(circle, rgba(0,0,0,0.03) 1px, transparent 1px);
background-size: 20px 20px;
}
/* Menu item hover effects */
.menu-glow:hover {
box-shadow: 0 0 20px rgba(45, 212, 191, 0.15);
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: oklch(70% 0.05 240);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: oklch(60% 0.08 240);
}
</style>
<?= $this->renderSection('styles') ?>
</head>
<body class="min-h-screen bg-base-200 font-sans antialiased">
<div class="drawer lg:drawer-open">
<input id="v2-drawer" type="checkbox" class="drawer-toggle" />
<!-- Main Content -->
<div class="drawer-content flex flex-col transition-all duration-300 ease-out">
<!-- Mobile Navbar -->
<div class="navbar bg-base-100/95 glass-header shadow-sm border-b border-base-200/50 lg:hidden sticky top-0 z-40">
<div class="flex-none">
<label for="v2-drawer" class="btn btn-square btn-ghost drawer-button">
<i data-lucide="menu" class="w-5 h-5"></i>
</label>
</div>
<div class="flex-1 gap-3">
<img src="<?= base_url('assets/images/logo.png') ?>" alt="CLQMS Logo" class="w-8 h-8 rounded-lg">
<span class="font-bold text-lg bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent"><?= $title ?? 'CLQMS' ?></span>
</div>
<div class="flex-none">
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost btn-circle avatar ring-2 ring-primary/20">
<div class="w-9 rounded-full bg-gradient-to-br from-primary to-secondary flex items-center justify-center">
<span class="text-white font-semibold text-sm"><?= substr(esc($user->username ?? 'U'), 0, 1) ?></span>
</div>
</div>
</div>
</div>
</div>
<!-- Desktop Header -->
<header class="bg-base-100/80 glass-header text-base-content shadow-sm px-8 py-4 hidden lg:flex justify-between items-center sticky top-0 z-30 border-b border-base-200/50">
<div class="flex items-center gap-4">
<div>
<h1 class="text-2xl font-bold text-base-content tracking-tight"><?= $title ?? 'Dashboard' ?></h1>
<p class="text-xs text-base-content/50 flex items-center gap-1.5">
<span class="inline-block w-1.5 h-1.5 rounded-full bg-success animate-pulse"></span>
Clinical Laboratory Quality Management System
</p>
</div>
</div>
<div class="flex items-center gap-3">
<!-- System Time -->
<div class="hidden md:flex flex-col items-end px-4 py-2 bg-base-200/50 rounded-xl">
<span class="text-[10px] uppercase tracking-wider text-base-content/50 font-medium">System Time</span>
<span class="font-mono text-sm font-semibold text-base-content"><?= date('H:i') ?></span>
</div>
<!-- Notifications -->
<div class="indicator">
<span class="indicator-item badge badge-error badge-xs animate-pulse">3</span>
<button class="btn btn-circle btn-ghost btn-sm hover:bg-error/10">
<i data-lucide="bell" class="w-5 h-5"></i>
</button>
</div>
<!-- Quick Search -->
<button class="btn btn-ghost btn-sm gap-2 hidden lg:inline-flex hover:bg-primary/10">
<i data-lucide="search" class="w-4 h-4"></i>
<span class="text-base-content/60">Search...</span>
<kbd class="kbd kbd-xs bg-base-200">⌘K</kbd>
</button>
</div>
</header>
<!-- Page Content -->
<main class="flex-1 p-4 lg:p-8 pattern-dots">
<div class="animate-fade-in-up">
<?= $this->renderSection('content') ?>
</div>
</main>
<!-- Footer -->
<footer class="bg-base-100/50 border-t border-base-200/50 px-8 py-4 hidden lg:block">
<div class="flex justify-between items-center text-xs text-base-content/40">
<span>© <?= date('Y') ?> CLQMS - Clinical Laboratory QMS</span>
<span>Version 2.0.0</span>
</div>
</footer>
</div>
<!-- Sidebar -->
<div class="drawer-side z-50">
<label for="v2-drawer" class="drawer-overlay"></label>
<aside class="sidebar-gradient text-neutral-content w-60 min-h-screen flex flex-col shadow-xl">
<!-- Logo Section -->
<div class="p-4 border-b border-white/5">
<div class="flex items-center gap-3">
<img src="<?= base_url('assets/images/logo.png') ?>" alt="CLQMS" class="w-9 h-9 rounded-lg" onerror="this.style.display='none'">
<div class="flex flex-col">
<span class="font-bold text-lg text-white">CLQMS</span>
<span class="text-[9px] uppercase tracking-wide text-white/50">Laboratory QMS</span>
</div>
</div>
</div>
<!-- Navigation Menu -->
<nav class="flex-1 overflow-y-auto w-full">
<ul class="menu menu-sm w-full p-0 [&_li>*]:rounded-none text-sm gap-0.5">
<li>
<a href="<?= site_url('v2') ?>" class="<?= ($activePage ?? '') === 'dashboard' ? 'active bg-primary text-primary-content' : 'text-white/70 hover:text-white hover:bg-white/5' ?>">
<i data-lucide="layout-dashboard" class="w-4 h-4"></i>
Dashboard
</a>
</li>
<li class="menu-title text-[10px] text-white/40 mt-3">System</li>
<li>
<details <?= strpos($activePage ?? '', 'organization') !== false ? 'open' : '' ?>>
<summary class="text-white/70 hover:text-white hover:bg-white/5">
<i data-lucide="building-2" class="w-4 h-4"></i>
Organization
</summary>
<ul>
<li><a href="<?= site_url('v2/organization/account') ?>" class="<?= ($activePage ?? '') === 'organization-account' ? 'active' : 'text-white/60 hover:text-white' ?>">Accounts</a></li>
<li><a href="<?= site_url('v2/organization/site') ?>" class="<?= ($activePage ?? '') === 'organization-site' ? 'active' : 'text-white/60 hover:text-white' ?>">Sites</a></li>
<li><a href="<?= site_url('v2/organization/discipline') ?>" class="<?= ($activePage ?? '') === 'organization-discipline' ? 'active' : 'text-white/60 hover:text-white' ?>">Disciplines</a></li>
<li><a href="<?= site_url('v2/organization/department') ?>" class="<?= ($activePage ?? '') === 'organization-department' ? 'active' : 'text-white/60 hover:text-white' ?>">Departments</a></li>
<li><a href="<?= site_url('v2/organization/workstation') ?>" class="<?= ($activePage ?? '') === 'organization-workstation' ? 'active' : 'text-white/60 hover:text-white' ?>">Workstations</a></li>
</ul>
</details>
</li>
<li>
<a href="<?= site_url('v2/valuesets') ?>" class="<?= ($activePage ?? '') === 'valuesets' ? 'active bg-primary text-primary-content' : 'text-white/70 hover:text-white hover:bg-white/5' ?>">
<i data-lucide="list-tree" class="w-4 h-4"></i>
Value Sets
</a>
</li>
<li class="menu-title text-[10px] text-white/40 mt-3">Clinical</li>
<li>
<a href="<?= site_url('v2/patients') ?>" class="<?= ($activePage ?? '') === 'patients' ? 'active bg-primary text-primary-content' : 'text-white/70 hover:text-white hover:bg-white/5' ?>">
<i data-lucide="users" class="w-4 h-4"></i>
Patients
</a>
</li>
<li class="menu-title text-[10px] text-white/40 mt-3">Tools</li>
<li>
<details>
<summary class="text-white/70 hover:text-white hover:bg-white/5">
<i data-lucide="terminal" class="w-4 h-4"></i>
Dev Tools
</summary>
<ul>
<li><a href="<?= site_url('v2/api-tester') ?>" class="<?= ($activePage ?? '') === 'api-tester' ? 'active' : 'text-white/60 hover:text-white' ?>">API Tester</a></li>
<li><a href="<?= site_url('v2/db-browser') ?>" class="<?= ($activePage ?? '') === 'db-browser' ? 'active' : 'text-white/60 hover:text-white' ?>">DB Browser</a></li>
<li><a href="<?= site_url('v2/logs') ?>" class="<?= ($activePage ?? '') === 'logs' ? 'active' : 'text-white/60 hover:text-white' ?>">Logs</a></li>
<li><a href="<?= site_url('v2/jwt-decoder') ?>" class="<?= ($activePage ?? '') === 'jwt-decoder' ? 'active' : 'text-white/60 hover:text-white' ?>">JWT Decoder</a></li>
</ul>
</details>
</li>
</ul>
</nav>
<!-- User Profile Section -->
<div class="p-4 border-t border-white/5 bg-black/20">
<div class="flex items-center gap-3">
<div class="avatar placeholder">
<div class="w-10 h-10 rounded-full bg-gradient-to-tr from-primary to-secondary text-white ring-2 ring-white/10 shadow-lg">
<span class="font-bold text-sm"><?= substr(esc($user->username ?? 'U'), 0, 1) ?></span>
</div>
</div>
<div class="flex-1 min-w-0 flex flex-col justify-center">
<div class="text-sm font-bold text-white truncate leading-tight"><?= esc($user->username ?? 'User') ?></div>
<div class="text-[10px] text-white/50 truncate">View Profile</div>
</div>
<a href="<?= site_url('logout') ?>" class="btn btn-ghost btn-circle btn-sm text-white/60 hover:text-error hover:bg-error/10" title="Sign Out">
<i data-lucide="log-out" class="w-4 h-4"></i>
</a>
</div>
</div>
</aside>
</div>
</div>
<!-- Toast Container -->
<div class="toast toast-end z-50" x-data>
<template x-for="toast in $store.toast?.messages || []" :key="toast.id">
<div
class="alert shadow-xl backdrop-blur-sm"
:class="{
'alert-success bg-success/90': toast.type === 'success',
'alert-error bg-error/90': toast.type === 'error',
'alert-info bg-info/90': toast.type === 'info',
'alert-warning bg-warning/90': toast.type === 'warning'
}"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 translate-x-8"
x-transition:enter-end="opacity-100 translate-x-0"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 translate-x-0"
x-transition:leave-end="opacity-0 translate-x-8"
@click="$store.toast.dismiss(toast.id)"
>
<span x-text="toast.message" class="font-medium"></span>
</div>
</template>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
if(window.lucide) lucide.createIcons();
});
// Set Base URL for JS
window.BASEURL = '<?= base_url() ?>';
// Toast Store for Alpine
document.addEventListener('alpine:init', () => {
Alpine.store('toast', {
messages: [],
show(message, type = 'info') {
const id = Date.now();
this.messages.push({ id, message, type });
setTimeout(() => {
this.dismiss(id);
}, 4000);
},
// Convenience methods following the workflow pattern
success(message) { this.show(message, 'success'); },
error(message) { this.show(message, 'error'); },
info(message) { this.show(message, 'info'); },
warning(message) { this.show(message, 'warning'); },
dismiss(id) {
this.messages = this.messages.filter(m => m.id !== id);
}
});
});
</script>
<?= $this->renderSection('script') ?>
</body>
</html>