- 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
138 lines
6.1 KiB
PHP
138 lines
6.1 KiB
PHP
<?= $this->extend("layout/main_layout"); ?>
|
|
|
|
<?= $this->section("content"); ?>
|
|
<main class="flex-1 p-6 overflow-auto">
|
|
<div class="mb-6">
|
|
<h1 class="text-2xl font-bold tracking-tight text-base-content">Dashboard</h1>
|
|
<p class="text-sm mt-1 opacity-70">Quick actions and recent QC results</p>
|
|
</div>
|
|
|
|
<!-- Quick Action Cards -->
|
|
<div class="grid grid-cols-2 md:grid-cols-6 gap-4 mb-6">
|
|
<a href="<?= base_url('entry/daily') ?>"
|
|
class="card bg-primary text-primary-content hover:bg-primary/90 cursor-pointer no-underline hover:scale-[1.02] transition">
|
|
<div class="card-body items-center text-center py-4">
|
|
<i class="fa-solid fa-calendar-day text-2xl mb-2"></i>
|
|
<span class="font-medium">Daily Entry</span>
|
|
</div>
|
|
</a>
|
|
<a href="<?= base_url('entry/monthly') ?>"
|
|
class="card bg-secondary text-secondary-content hover:bg-secondary/90 cursor-pointer no-underline hover:scale-[1.02] transition">
|
|
<div class="card-body items-center text-center py-4">
|
|
<i class="fa-solid fa-calendar-days text-2xl mb-2"></i>
|
|
<span class="font-medium">Monthly Entry</span>
|
|
</div>
|
|
</a>
|
|
<a href="<?= base_url('master/dept') ?>"
|
|
class="card bg-base-100 border border-base-300 hover:border-primary cursor-pointer no-underline hover:scale-[1.02] transition">
|
|
<div class="card-body items-center text-center py-4">
|
|
<i class="fa-solid fa-building text-2xl mb-2"></i>
|
|
<span class="font-medium">Departments</span>
|
|
</div>
|
|
</a>
|
|
<a href="<?= base_url('master/test') ?>"
|
|
class="card bg-base-100 border border-base-300 hover:border-primary cursor-pointer no-underline hover:scale-[1.02] transition">
|
|
<div class="card-body items-center text-center py-4">
|
|
<i class="fa-solid fa-flask-vial text-2xl mb-2"></i>
|
|
<span class="font-medium">Tests</span>
|
|
</div>
|
|
</a>
|
|
<a href="<?= base_url('master/control') ?>"
|
|
class="card bg-base-100 border border-base-300 hover:border-primary cursor-pointer no-underline hover:scale-[1.02] transition">
|
|
<div class="card-body items-center text-center py-4">
|
|
<i class="fa-solid fa-vial text-2xl mb-2"></i>
|
|
<span class="font-medium">Controls</span>
|
|
</div>
|
|
</a>
|
|
<a href="<?= base_url('report') ?>"
|
|
class="card bg-base-100 border border-base-300 hover:border-primary cursor-pointer no-underline hover:scale-[1.02] transition">
|
|
<div class="card-body items-center text-center py-4">
|
|
<i class="fa-solid fa-chart-bar text-2xl mb-2"></i>
|
|
<span class="font-medium">Reports</span>
|
|
</div>
|
|
</a>
|
|
</div>
|
|
|
|
<div class="bg-base-100 rounded-xl border border-base-300 shadow-sm p-6" x-data="dashboardRecentResults()">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h2 class="text-lg font-semibold text-base-content">Recent Results</h2>
|
|
<button @click="fetchResults()" class="btn btn-ghost btn-sm">
|
|
<i class="fa-solid fa-rotate-right" :class="{ 'fa-spin': loading }"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Loading State -->
|
|
<div x-show="loading" class="flex justify-center py-12">
|
|
<span class="loading loading-spinner loading-lg text-primary"></span>
|
|
</div>
|
|
|
|
<!-- Empty State -->
|
|
<div x-show="!loading && results.length === 0" class="text-center py-12">
|
|
<i class="fa-solid fa-flask text-4xl text-base-content/20 mb-3"></i>
|
|
<p class="text-base-content/60">No results yet</p>
|
|
</div>
|
|
|
|
<!-- Table -->
|
|
<div x-show="!loading && results.length > 0" class="overflow-x-auto">
|
|
<table class="table table-sm">
|
|
<thead>
|
|
<tr>
|
|
<th>Date</th>
|
|
<th>Control</th>
|
|
<th>Lot</th>
|
|
<th>Test</th>
|
|
<th class="text-right">Value</th>
|
|
<th>Range (Mean ± 2SD)</th>
|
|
<th class="text-center">Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<template x-for="row in results" :key="row.id">
|
|
<tr class="hover">
|
|
<td x-text="row.resDate || '-'"></td>
|
|
<td x-text="row.controlName || '-'"></td>
|
|
<td x-text="row.lot || '-'"></td>
|
|
<td x-text="row.testName || '-'"></td>
|
|
<td class="text-right font-mono" x-text="row.resValue ?? '-'"></td>
|
|
<td class="font-mono text-sm" x-text="row.rangeDisplay"></td>
|
|
<td class="text-center">
|
|
<span class="badge" :class="row.inRange ? 'badge-success' : 'badge-error'"
|
|
x-text="row.inRange ? 'Pass' : 'Fail'">
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
<?= $this->endSection(); ?>
|
|
|
|
<?= $this->section("script"); ?>
|
|
<script>
|
|
document.addEventListener('alpine:init', () => {
|
|
Alpine.data("dashboardRecentResults", () => ({
|
|
results: [],
|
|
loading: true,
|
|
|
|
init() {
|
|
this.fetchResults();
|
|
},
|
|
|
|
async fetchResults() {
|
|
this.loading = true;
|
|
try {
|
|
const response = await fetch(`${BASEURL}api/dashboard/recent?limit=10`);
|
|
const json = await response.json();
|
|
this.results = json.data || [];
|
|
} catch (e) {
|
|
console.error(e);
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
}
|
|
}));
|
|
});
|
|
</script>
|
|
<?= $this->endSection(); ?>
|