388 lines
16 KiB
PHP
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>
|