tinyqc/app/Views/dashboard.php
mahdahar 14baa6b758 docs: add comprehensive documentation and refactor API structure
This commit introduces a complete documentation suite, refactors the API layer for
better consistency, and updates the database schema and seeding logic.

Key Changes:
- Documentation:
  - Added `CLAUDE.md` with development guidelines and architecture overview.
  - Created `docs/` directory with detailed guides for architecture, development,
    and source tree analysis.
- Database & Migrations:
  - Implemented `RenameMasterColumns` migration to standardize column naming
    (e.g., `name` -> `dept_name`, `name` -> `control_name`).
  - Added `CmodQcSeeder` to populate the system with realistic sample data
    for depts, controls, tests, and results.
- Backend API:
  - Created `DashboardApiController` with `getRecent()` for dashboard stats.
  - Created `ReportApiController` for managed reporting access.
  - Updated `app/Config/Routes.php` with new API groupings and documentation routes.
- Frontend & Views:
  - Refactored master data views (`dept`, `test`, `control`) to use Alpine.js
    and the updated API structure.
  - Modernized `dashboard.php` and `main_layout.php` with improved UI/UX.
- Infrastructure:
  - Updated `.gitignore` to exclude development-specific artifacts (`_bmad/`, `.claude/`).
2026-01-20 14:44:46 +07:00

127 lines
5.5 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-5 gap-4 mb-6">
<a href="<?= base_url('entry') ?>" 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-pen-to-square text-2xl mb-2"></i>
<span class="font-medium">QC 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>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.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(); ?>