- Implement Monthly Entry interface with full data entry grid
- Add batch save with validation and statistics for monthly results
- Support daily comments per day per test
- Add result status indicators and validation summaries
- Consolidate Entry API controller
- Refactor EntryApiController to handle both daily/monthly operations
- Add batch save endpoints with comprehensive validation
- Implement statistics calculation for result entries
- Add Control Test master data management
- Create MasterControlsController for CRUD operations
- Add dialog forms for control test configuration
- Implement control-test associations with QC parameters
- Refactor Report API and views
- Implement new report index with Levey-Jennings charts placeholder
- Add monthly report functionality with result statistics
- Include QC summary with mean, SD, and CV calculations
- UI improvements
- Overhaul dashboard with improved layout
- Update daily entry interface with inline editing
- Enhance master data management with DaisyUI components
- Add proper modal dialogs and form validation
- Database and seeding
- Update migration for control_tests table schema
- Remove redundant migration and seed files
- Update seeders with comprehensive test data
- Documentation
- Update CLAUDE.md with comprehensive project documentation
- Add architecture overview and conventions
BREAKING CHANGES:
- Refactored Entry API endpoints structure
- Removed ReportApiController::view() - consolidated into new report index
210 lines
12 KiB
PHP
210 lines
12 KiB
PHP
<!DOCTYPE html>
|
|
<html lang="en" data-theme="autumn">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title><?= $pageData['title'] ?? 'TinyQC - QC Management System' ?></title>
|
|
<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>
|
|
<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.14.1/dist/cdn.min.js"></script>
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
|
<script>
|
|
const BASEURL = '<?= base_url(''); ?>';
|
|
</script>
|
|
</head>
|
|
|
|
<body class="bg-base-200 text-base-content" x-data="appData()">
|
|
<div class="drawer lg:drawer-open">
|
|
<input id="sidebar-drawer" type="checkbox" class="drawer-toggle" x-ref="sidebarDrawer">
|
|
|
|
<div class="drawer-content flex flex-col min-h-screen">
|
|
<nav
|
|
class="navbar bg-base-200/80 backdrop-blur-md border-b border-base-300 sticky top-0 z-30 w-full shadow-sm">
|
|
<div class="flex-none lg:hidden">
|
|
<label for="sidebar-drawer" class="btn btn-square btn-ghost text-primary">
|
|
<i class="fa-solid fa-bars text-xl"></i>
|
|
</label>
|
|
</div>
|
|
<div class="flex-1 px-4">
|
|
<div class="flex items-center gap-3">
|
|
<div
|
|
class="w-10 h-10 rounded-lg bg-primary flex items-center justify-center shadow-lg shadow-primary/20">
|
|
<i class="fa-solid fa-flask text-white text-lg"></i>
|
|
</div>
|
|
<div>
|
|
<h1 class="text-xl font-bold text-base-content tracking-tight">tinyqc</h1>
|
|
<p class="text-xs opacity-70">QC Management System</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="flex-none">
|
|
<button class="btn btn-ghost rounded-full mr-2" @click="toggleTheme()">
|
|
<i class="fa-solid fa-sun text-warning" x-show="currentTheme === themeConfig.light"></i>
|
|
<i class="fa-solid fa-moon text-neutral-content" x-show="currentTheme === themeConfig.dark"></i>
|
|
</button>
|
|
<div class="dropdown dropdown-end" x-data="{ dropdownOpen: false }">
|
|
<div tabindex="0" role="button" class="btn btn-ghost btn-circle avatar placeholder"
|
|
@click="dropdownOpen = !dropdownOpen">
|
|
<div
|
|
class="bg-primary text-primary-content rounded-full w-10 h-10 flex items-center justify-center">
|
|
<span><?= $pageData['userInitials'] ?? 'DR' ?></span>
|
|
</div>
|
|
</div>
|
|
<ul tabindex="0"
|
|
class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow-xl bg-base-100 rounded-box w-52 border border-base-300"
|
|
x-show="dropdownOpen" @click.outside="dropdownOpen = false" x-transition>
|
|
<li class="menu-title px-4 py-2">
|
|
<span
|
|
class="text-base-content font-bold"><?= $pageData['userName'] ?? 'Lab User' ?></span>
|
|
<span
|
|
class="text-xs text-primary font-medium"><?= $pageData['userRole'] ?? 'Administrator' ?></span>
|
|
</li>
|
|
<div class="divider my-0 h-px opacity-10"></div>
|
|
<li><a class="hover:bg-base-200"><i class="fa-solid fa-user text-primary"></i> Profile</a>
|
|
</li>
|
|
<li><a class="hover:bg-base-200"><i class="fa-solid fa-gear text-primary"></i> Settings</a>
|
|
</li>
|
|
<li><a class="hover:bg-base-200"><i class="fa-solid fa-question-circle text-primary"></i>
|
|
Help</a></li>
|
|
<div class="divider my-0 h-px opacity-10"></div>
|
|
<li><a href="<?= base_url('/logout') ?>" class="text-error hover:bg-error/10"><i
|
|
class="fa-solid fa-sign-out-alt"></i> Logout</a></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<div class="flex-1">
|
|
<?= $this->renderSection('content') ?>
|
|
</div>
|
|
|
|
<footer class="footer footer-center p-4 bg-base-300 text-base-content border-t border-base-300 mt-auto">
|
|
<p>©<?= date('Y') ?> made by 5panda for PT.Summit</p>
|
|
</footer>
|
|
</div>
|
|
|
|
<div class="drawer-side z-40">
|
|
<label for="sidebar-drawer" class="drawer-overlay"></label>
|
|
<aside class="bg-base-300 border-r border-base-300 w-64 min-h-full flex flex-col">
|
|
<ul class="menu p-4 text-base-content flex-1 w-full">
|
|
<li class="mb-1 min-h-0">
|
|
<a class="flex items-center gap-3 px-4 py-3 rounded-lg <?= uri_string() === '' ? 'bg-primary/10 text-primary font-medium' : 'opacity-70 hover:bg-base-200 hover:opacity-100' ?> transition-colors h-full"
|
|
href="<?= base_url('/') ?>">
|
|
<i class="fa-solid fa-chart-line w-5"></i>
|
|
Dashboard
|
|
</a>
|
|
</li>
|
|
|
|
<li class="mt-6 mb-2 min-h-0">
|
|
<p class="px-4 text-xs font-semibold opacity-40 uppercase tracking-wider">Master Data</p>
|
|
</li>
|
|
<li class="mb-1 min-h-0">
|
|
<a class="flex items-center gap-3 px-4 py-3 rounded-lg <?= str_contains(uri_string(), 'master/dept') ? 'bg-primary/10 text-primary font-medium' : 'opacity-70 hover:bg-base-200 hover:opacity-100' ?> transition-colors h-full"
|
|
href="<?= base_url('/master/dept') ?>">
|
|
<i class="fa-solid fa-building w-5"></i>
|
|
Departments
|
|
</a>
|
|
</li>
|
|
<li class="mb-1 min-h-0">
|
|
<a class="flex items-center gap-3 px-4 py-3 rounded-lg <?= str_contains(uri_string(), 'master/test') ? 'bg-primary/10 text-primary font-medium' : 'opacity-70 hover:bg-base-200 hover:opacity-100' ?> transition-colors h-full"
|
|
href="<?= base_url('/master/test') ?>">
|
|
<i class="fa-solid fa-flask-vial w-5"></i>
|
|
Tests
|
|
</a>
|
|
</li>
|
|
<li class="mb-1 min-h-0">
|
|
<a class="flex items-center gap-3 px-4 py-3 rounded-lg <?= str_contains(uri_string(), 'master/control') && !str_contains(uri_string(), 'master/control-tests') ? 'bg-primary/10 text-primary font-medium' : 'opacity-70 hover:bg-base-200 hover:opacity-100' ?> transition-colors h-full"
|
|
href="<?= base_url('/master/control') ?>">
|
|
<i class="fa-solid fa-vial w-5"></i>
|
|
Controls
|
|
</a>
|
|
</li>
|
|
<li class="mb-1 min-h-0">
|
|
<a class="flex items-center gap-3 px-4 py-3 rounded-lg <?= str_contains(uri_string(), 'master/control-tests') ? 'bg-primary/10 text-primary font-medium' : 'opacity-70 hover:bg-base-200 hover:opacity-100' ?> transition-colors h-full"
|
|
href="<?= base_url('/master/control-tests') ?>">
|
|
<i class="fa-solid fa-link w-5"></i>
|
|
Control Tests
|
|
</a>
|
|
</li>
|
|
|
|
<li class="mt-6 mb-2 min-h-0">
|
|
<p class="px-4 text-xs font-semibold opacity-40 uppercase tracking-wider">QC Operations</p>
|
|
</li>
|
|
<li class="mb-1 min-h-0">
|
|
<a class="flex items-center gap-3 px-4 py-3 rounded-lg <?= str_contains(uri_string(), 'entry') ? 'bg-primary/10 text-primary font-medium' : 'opacity-70 hover:bg-base-200 hover:opacity-100' ?> transition-colors h-full"
|
|
href="<?= base_url('/entry') ?>">
|
|
<i class="fa-solid fa-pen-to-square w-5"></i>
|
|
QC Entry
|
|
</a>
|
|
</li>
|
|
<li class="mb-1 min-h-0">
|
|
<a class="flex items-center gap-3 px-4 py-3 rounded-lg <?= str_contains(uri_string(), 'entry/daily') ? 'bg-primary/10 text-primary font-medium' : 'opacity-70 hover:bg-base-200 hover:opacity-100' ?> transition-colors h-full"
|
|
href="<?= base_url('/entry/daily') ?>">
|
|
<i class="fa-solid fa-calendar-day w-5"></i>
|
|
Daily Entry
|
|
</a>
|
|
</li>
|
|
<li class="mb-1 min-h-0">
|
|
<a class="flex items-center gap-3 px-4 py-3 rounded-lg <?= str_contains(uri_string(), 'entry/monthly') ? 'bg-primary/10 text-primary font-medium' : 'opacity-70 hover:bg-base-200 hover:opacity-100' ?> transition-colors h-full"
|
|
href="<?= base_url('/entry/monthly') ?>">
|
|
<i class="fa-solid fa-calendar w-5"></i>
|
|
Monthly Entry
|
|
</a>
|
|
</li>
|
|
<li class="mb-1 min-h-0">
|
|
<a class="flex items-center gap-3 px-4 py-3 rounded-lg <?= str_contains(uri_string(), 'report') ? 'bg-primary/10 text-primary font-medium' : 'opacity-70 hover:bg-base-200 hover:opacity-100' ?> transition-colors h-full"
|
|
href="<?= base_url('/report') ?>">
|
|
<i class="fa-solid fa-chart-bar w-5"></i>
|
|
Reports
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
|
|
</aside>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener('alpine:init', () => {
|
|
Alpine.data('appData', () => ({
|
|
showModal: false,
|
|
themeConfig: {
|
|
light: 'autumn',
|
|
dark: 'dracula'
|
|
},
|
|
get currentTheme() {
|
|
return localStorage.getItem('theme') || this.themeConfig.light;
|
|
},
|
|
set currentTheme(value) {
|
|
localStorage.setItem('theme', value);
|
|
document.documentElement.setAttribute('data-theme', value);
|
|
},
|
|
get isDark() {
|
|
return this.currentTheme === this.themeConfig.dark;
|
|
},
|
|
init() {
|
|
document.documentElement.setAttribute('data-theme', this.currentTheme);
|
|
},
|
|
toggleSidebar() {
|
|
this.$refs.sidebarDrawer.checked = !this.$refs.sidebarDrawer.checked;
|
|
},
|
|
toggleTheme() {
|
|
this.currentTheme = this.isDark ? this.themeConfig.light : this.themeConfig.dark;
|
|
},
|
|
openModal() {
|
|
this.showModal = true;
|
|
},
|
|
closeModal() {
|
|
this.showModal = false;
|
|
}
|
|
}));
|
|
});
|
|
</script>
|
|
|
|
<?= $this->renderSection('script') ?>
|
|
</body>
|
|
|
|
</html>
|