- Reorganized Architecture: Moved Master data management (Controls, Departments, Tests) to a dedicated Master namespace/directory for better modularity.
- Unified Data Entry: Consolidated daily and monthly QC entry views and logic into a single streamlined system.
- UI/UX Modernization:
- Integrated DaisyUI and Alpine.js for a responsive, themeable, and interactive experience.
- Replaced legacy layouts with a new DaisyUI-based template.
- Updated branding with new logo and favicon assets.
- Cleaned up deprecated JavaScript assets (app.js, charts.js, tables.js).
- Backend Enhancements:
- Added `ControlEntryModel` and `ResultCommentsController` for improved data handling and auditing.
- Updated routing to support the new controller structure and consolidated API endpoints.
- Documentation & Assets:
- Added [docs/PRD.md](cci:7://file:///c:/www/tinyqc/docs/PRD.md:0:0-0:0) and [docs/llms.txt](cci:7://file:///c:/www/tinyqc/docs/llms.txt:0:0-0:0) for project context and requirements.
- Included database schema scripts and backups in the `backup/` directory.
- Cleanup: Removed legacy TUI progress files and unused commands.
451 lines
29 KiB
HTML
451 lines
29 KiB
HTML
<!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>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@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">
|
|
|
|
</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>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">Dr. Sarah Mitchell</span>
|
|
<span class="text-xs text-primary font-medium">Lab Director</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 class="text-error hover:bg-error/10"><i class="fa-solid fa-sign-out-alt"></i> Logout</a></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<main class="flex-1 p-6 overflow-auto">
|
|
<div class="mx-auto">
|
|
<div class="flex flex-col md:flex-row md:items-center md:justify-between mb-6 gap-4">
|
|
<div>
|
|
<h2 class="text-2xl font-bold tracking-tight text-base-content">Sample Management</h2>
|
|
<p class="text-sm mt-1 opacity-70">Manage laboratory samples, track status, and generate reports</p>
|
|
</div>
|
|
<button class="btn btn-sm btn-primary gap-2 shadow-lg shadow-primary/20" @click="openModal()">
|
|
<i class="fa-solid fa-plus"></i> Add Data
|
|
</button>
|
|
</div>
|
|
|
|
<div class="rounded-xl border border-base-300 shadow-xl overflow-hidden bg-base-100">
|
|
<div class="p-4 border-b border-base-300 flex flex-col md:flex-row md:items-center gap-4">
|
|
<div class="relative flex-1 max-w-md">
|
|
<i class="fa-solid fa-search absolute left-3 top-1/2 -translate-y-1/2 opacity-50 text-sm"></i>
|
|
<input type="text" placeholder="Search by sample ID, patient name, or test type..." class="w-full pl-10 pr-4 py-2.5 text-sm rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all bg-base-200 border border-base-300 text-base-content placeholder:opacity-50">
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<select class="select select-bordered select-sm focus:outline-none focus:ring-2 focus:ring-primary/50 bg-base-200 border-base-300 text-base-content">
|
|
<option selected>All Status</option>
|
|
<option>Pending</option>
|
|
<option>Processing</option>
|
|
<option>Completed</option>
|
|
<option>Cancelled</option>
|
|
</select>
|
|
<button class="btn btn-sm btn-ghost opacity-70 hover:opacity-100">
|
|
<i class="fa-solid fa-filter"></i> Filter
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full text-sm text-left">
|
|
<thead class="uppercase tracking-wider font-semibold bg-base-200 text-base-content/70 text-xs">
|
|
<tr>
|
|
<th class="py-4 px-5">Sample ID</th>
|
|
<th class="py-4 px-5">Patient Name</th>
|
|
<th class="py-4 px-5">Test Type</th>
|
|
<th class="py-4 px-5">Collection Date</th>
|
|
<th class="py-4 px-5">Status</th>
|
|
<th class="py-4 px-5 text-right">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-base-300">
|
|
<tr class="hover:bg-base-200 transition-colors">
|
|
<td class="py-4 px-5">
|
|
<span class="font-mono text-sm text-primary bg-primary/10 px-2 py-1 rounded">SPL-2024-001</span>
|
|
</td>
|
|
<td class="py-4 px-5">
|
|
<div class="flex items-center gap-3">
|
|
<div class="avatar placeholder">
|
|
<div class="rounded-full w-8 h-8 bg-neutral text-neutral-content flex items-center justify-center">
|
|
<span class="text-xs" x-text="'JD'"></span>
|
|
</div>
|
|
</div>
|
|
<span class="font-medium text-base-content">John Doe</span>
|
|
</div>
|
|
</td>
|
|
<td class="py-4 px-5 opacity-80">Complete Blood Count</td>
|
|
<td class="py-4 px-5 opacity-60">Jan 15, 2024</td>
|
|
<td class="py-4 px-5">
|
|
<div class="badge badge-warning gap-1">
|
|
<i class="fa-solid fa-clock text-xs"></i> Pending
|
|
</div>
|
|
</td>
|
|
<td class="py-4 px-5 text-right">
|
|
<div class="flex justify-end gap-2">
|
|
<button class="btn btn-xs btn-ghost text-primary hover:bg-primary/10" @click="openModal()">
|
|
<i class="fa-solid fa-pencil"></i> Edit
|
|
</button>
|
|
<button class="btn btn-xs btn-ghost text-error hover:bg-error/10">
|
|
<i class="fa-solid fa-trash"></i> Delete
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<tr class="hover:bg-base-200 transition-colors">
|
|
<td class="py-4 px-5">
|
|
<span class="font-mono text-sm text-primary bg-primary/10 px-2 py-1 rounded">SPL-2024-002</span>
|
|
</td>
|
|
<td class="py-4 px-5">
|
|
<div class="flex items-center gap-3">
|
|
<div class="avatar placeholder">
|
|
<div class="rounded-full w-8 h-8 bg-neutral text-neutral-content flex items-center justify-center">
|
|
<span class="text-xs">AS</span>
|
|
</div>
|
|
</div>
|
|
<span class="font-medium text-base-content">Alice Smith</span>
|
|
</div>
|
|
</td>
|
|
<td class="py-4 px-5 opacity-80">Lipid Panel</td>
|
|
<td class="py-4 px-5 opacity-60">Jan 15, 2024</td>
|
|
<td class="py-4 px-5">
|
|
<div class="badge badge-info gap-1">
|
|
<i class="fa-solid fa-flask text-xs"></i> Processing
|
|
</div>
|
|
</td>
|
|
<td class="py-4 px-5 text-right">
|
|
<div class="flex justify-end gap-2">
|
|
<button class="btn btn-xs btn-ghost text-primary hover:bg-primary/10" @click="openModal()">
|
|
<i class="fa-solid fa-pencil"></i> Edit
|
|
</button>
|
|
<button class="btn btn-xs btn-ghost text-error hover:bg-error/10">
|
|
<i class="fa-solid fa-trash"></i> Delete
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<tr class="hover:bg-base-200 transition-colors">
|
|
<td class="py-4 px-5">
|
|
<span class="font-mono text-sm text-primary bg-primary/10 px-2 py-1 rounded">SPL-2024-003</span>
|
|
</td>
|
|
<td class="py-4 px-5">
|
|
<div class="flex items-center gap-3">
|
|
<div class="avatar placeholder">
|
|
<div class="rounded-full w-8 h-8 bg-neutral text-neutral-content flex items-center justify-center">
|
|
<span class="text-xs">BJ</span>
|
|
</div>
|
|
</div>
|
|
<span class="font-medium text-base-content">Bob Johnson</span>
|
|
</div>
|
|
</td>
|
|
<td class="py-4 px-5 opacity-80">Liver Function Test</td>
|
|
<td class="py-4 px-5 opacity-60">Jan 14, 2024</td>
|
|
<td class="py-4 px-5">
|
|
<div class="badge badge-success gap-1">
|
|
<i class="fa-solid fa-check text-xs"></i> Completed
|
|
</div>
|
|
</td>
|
|
<td class="py-4 px-5 text-right">
|
|
<div class="flex justify-end gap-2">
|
|
<button class="btn btn-xs btn-ghost text-primary hover:bg-primary/10" @click="openModal()">
|
|
<i class="fa-solid fa-pencil"></i> Edit
|
|
</button>
|
|
<button class="btn btn-xs btn-ghost text-error hover:bg-error/10">
|
|
<i class="fa-solid fa-trash"></i> Delete
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<tr class="hover:bg-base-200 transition-colors">
|
|
<td class="py-4 px-5">
|
|
<span class="font-mono text-sm text-primary bg-primary/10 px-2 py-1 rounded">SPL-2024-004</span>
|
|
</td>
|
|
<td class="py-4 px-5">
|
|
<div class="flex items-center gap-3">
|
|
<div class="avatar placeholder">
|
|
<div class="rounded-full w-8 h-8 bg-neutral text-neutral-content flex items-center justify-center">
|
|
<span class="text-xs">EW</span>
|
|
</div>
|
|
</div>
|
|
<span class="font-medium text-base-content">Emily Watson</span>
|
|
</div>
|
|
</td>
|
|
<td class="py-4 px-5 opacity-80">Thyroid Panel</td>
|
|
<td class="py-4 px-5 opacity-60">Jan 14, 2024</td>
|
|
<td class="py-4 px-5">
|
|
<div class="badge badge-success gap-1">
|
|
<i class="fa-solid fa-check text-xs"></i> Completed
|
|
</div>
|
|
</td>
|
|
<td class="py-4 px-5 text-right">
|
|
<div class="flex justify-end gap-2">
|
|
<button class="btn btn-xs btn-ghost text-primary hover:bg-primary/10" @click="openModal()">
|
|
<i class="fa-solid fa-pencil"></i> Edit
|
|
</button>
|
|
<button class="btn btn-xs btn-ghost text-error hover:bg-error/10">
|
|
<i class="fa-solid fa-trash"></i> Delete
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<tr class="hover:bg-base-200 transition-colors">
|
|
<td class="py-4 px-5">
|
|
<span class="font-mono text-sm text-primary bg-primary/10 px-2 py-1 rounded">SPL-2024-005</span>
|
|
</td>
|
|
<td class="py-4 px-5">
|
|
<div class="flex items-center gap-3">
|
|
<div class="avatar placeholder">
|
|
<div class="rounded-full w-8 h-8 bg-neutral text-neutral-content flex items-center justify-center">
|
|
<span class="text-xs">MC</span>
|
|
</div>
|
|
</div>
|
|
<span class="font-medium text-base-content">Michael Chen</span>
|
|
</div>
|
|
</td>
|
|
<td class="py-4 px-5 opacity-80">Urinalysis</td>
|
|
<td class="py-4 px-5 opacity-60">Jan 13, 2024</td>
|
|
<td class="py-4 px-5">
|
|
<div class="badge badge-error gap-1">
|
|
<i class="fa-solid fa-times text-xs"></i> Cancelled
|
|
</div>
|
|
</td>
|
|
<td class="py-4 px-5 text-right">
|
|
<div class="flex justify-end gap-2">
|
|
<button class="btn btn-xs btn-ghost text-primary hover:bg-primary/10" @click="openModal()">
|
|
<i class="fa-solid fa-pencil"></i> Edit
|
|
</button>
|
|
<button class="btn btn-xs btn-ghost text-error hover:bg-error/10">
|
|
<i class="fa-solid fa-trash"></i> Delete
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="p-4 border-t border-base-300 flex items-center justify-between">
|
|
<span class="text-sm opacity-60">Showing 1 to 5 of 127 entries</span>
|
|
<div class="join">
|
|
<button class="join-item btn btn-sm bg-base-200 border-base-300">Previous</button>
|
|
<button class="join-item btn btn-sm btn-primary">1</button>
|
|
<button class="join-item btn btn-sm bg-base-200 border-base-300">2</button>
|
|
<button class="join-item btn btn-sm bg-base-200 border-base-300">3</button>
|
|
<button class="join-item btn btn-sm bg-base-200 border-base-300">Next</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
<footer class="text-center text-sm opacity-50 py-4 mt-auto">
|
|
© 2026 - 5Panda
|
|
</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 bg-primary/10 text-primary font-medium h-full">
|
|
<i class="fa-solid fa-chart-line w-5"></i>
|
|
Dashboard
|
|
</a>
|
|
</li>
|
|
<li class="mb-1 min-h-0">
|
|
<a class="flex items-center gap-3 px-4 py-3 rounded-lg opacity-70 hover:bg-base-200 hover:opacity-100 transition-colors h-full">
|
|
<i class="fa-solid fa-users w-5 text-primary"></i>
|
|
Patients
|
|
</a>
|
|
</li>
|
|
<li class="mb-1 min-h-0">
|
|
<a class="flex items-center gap-3 px-4 py-3 rounded-lg opacity-70 hover:bg-base-200 hover:opacity-100 transition-colors h-full">
|
|
<i class="fa-solid fa-vial w-5 text-primary"></i>
|
|
Samples
|
|
</a>
|
|
</li>
|
|
<li class="mb-1 min-h-0">
|
|
<a class="flex items-center gap-3 px-4 py-3 rounded-lg opacity-70 hover:bg-base-200 hover:opacity-100 transition-colors h-full">
|
|
<i class="fa-solid fa-microscope w-5 text-primary"></i>
|
|
Results
|
|
</a>
|
|
</li>
|
|
<li class="mb-1 min-h-0">
|
|
<a class="flex items-center gap-3 px-4 py-3 rounded-lg opacity-70 hover:bg-base-200 hover:opacity-100 transition-colors h-full">
|
|
<i class="fa-solid fa-file-medical-alt w-5 text-primary"></i>
|
|
Reports
|
|
</a>
|
|
</li>
|
|
|
|
<li class="mt-6 mb-2 min-h-0">
|
|
<p class="px-4 text-xs font-semibold opacity-40 uppercase tracking-wider">System</p>
|
|
</li>
|
|
<li class="mb-1 min-h-0">
|
|
<a class="flex items-center gap-3 px-4 py-3 rounded-lg opacity-70 hover:bg-base-200 hover:opacity-100 transition-colors h-full">
|
|
<i class="fa-solid fa-cog w-5 text-primary"></i>
|
|
Settings
|
|
</a>
|
|
</li>
|
|
<li class="mb-1 min-h-0">
|
|
<a class="flex items-center gap-3 px-4 py-3 rounded-lg opacity-70 hover:bg-base-200 hover:opacity-100 transition-colors h-full">
|
|
<i class="fa-solid fa-users-cog w-5 text-primary"></i>
|
|
Users
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
|
|
<div class="p-4 border-t border-base-300">
|
|
<div class="bg-base-100/50 rounded-lg p-4">
|
|
<div class="flex items-center justify-between mb-2">
|
|
<span class="text-xs opacity-60">Storage</span>
|
|
<span class="text-xs text-primary font-bold">68%</span>
|
|
</div>
|
|
<progress class="progress progress-primary w-full h-2" value="68" max="100"></progress>
|
|
<p class="text-xs opacity-50 mt-2">6.8 GB of 10 GB used</p>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
</div>
|
|
</div>
|
|
|
|
<dialog class="modal modal-bottom sm:modal-middle" :class="{ 'modal-open': showModal }">
|
|
<div class="modal-box border border-base-300 shadow-2xl bg-base-100">
|
|
<h3 class="font-bold text-lg mb-4 flex items-center gap-2 text-base-content">
|
|
<i class="fa-solid fa-vial text-primary"></i>
|
|
Add New Sample
|
|
</h3>
|
|
|
|
<div class="space-y-4">
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text font-medium text-base-content opacity-80">Patient Name</span>
|
|
</label>
|
|
<input type="text" placeholder="Enter patient name" class="input input-bordered w-full focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary bg-base-200 border-base-300 text-base-content placeholder:opacity-50">
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text font-medium text-base-content opacity-80">Test Type</span>
|
|
</label>
|
|
<select class="select select-bordered w-full focus:outline-none focus:ring-2 focus:ring-primary/50 bg-base-200 border-base-300 text-base-content">
|
|
<option selected disabled>Select test type</option>
|
|
<option>Complete Blood Count (CBC)</option>
|
|
<option>Lipid Panel</option>
|
|
<option>Liver Function Test</option>
|
|
<option>Kidney Function Test</option>
|
|
<option>Thyroid Panel</option>
|
|
<option>Urinalysis</option>
|
|
<option>Blood Glucose</option>
|
|
<option>Electrolytes Panel</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text font-medium text-base-content opacity-80">Collection Date</span>
|
|
</label>
|
|
<input type="date" class="input input-bordered w-full focus:outline-none focus:ring-2 focus:ring-primary/50 bg-base-200 border-base-300 text-base-content">
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text font-medium text-base-content opacity-80">Notes</span>
|
|
</label>
|
|
<textarea class="textarea textarea-bordered w-full h-24 focus:outline-none focus:ring-2 focus:ring-primary/50 placeholder:opacity-50 resize-none bg-base-200 border-base-300 text-base-content" placeholder="Additional notes or special instructions..."></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-action mt-6">
|
|
<button class="btn btn-ghost opacity-70" @click="closeModal()">Cancel</button>
|
|
<button class="btn btn-primary gap-2 shadow-lg shadow-primary/20 font-medium">
|
|
<i class="fa-solid fa-save"></i> Submit
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<form method="dialog" class="modal-backdrop bg-black/60" @click="closeModal()"></form>
|
|
</dialog>
|
|
|
|
<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>
|
|
</body>
|
|
</html>
|