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/`).
245 lines
10 KiB
PHP
245 lines
10 KiB
PHP
<?= $this->extend("layout/main_layout"); ?>
|
|
|
|
<?= $this->section("content"); ?>
|
|
<main class="flex-1 p-6 overflow-auto" x-data="tests()">
|
|
<div class="flex justify-between items-center mb-6">
|
|
<div>
|
|
<h1 class="text-2xl font-bold text-base-content tracking-tight">Tests</h1>
|
|
<p class="text-sm mt-1 opacity-70">Manage laboratory tests and parameters</p>
|
|
</div>
|
|
<button
|
|
class="btn btn-sm gap-2 shadow-sm border-0 bg-gradient-to-r from-blue-600 to-blue-500 hover:from-blue-700 hover:to-blue-600 text-white transition-all duration-200"
|
|
@click="showForm()"
|
|
>
|
|
<i class="fa-solid fa-plus"></i> New Test
|
|
</button>
|
|
</div>
|
|
|
|
<div class="bg-base-100 rounded-xl border border-base-300 shadow-sm p-4 mb-6">
|
|
<div class="flex items-center gap-3">
|
|
<div class="relative flex-1 max-w-md">
|
|
<i class="fa-solid fa-magnifying-glass absolute left-3 top-1/2 -translate-y-1/2 opacity-50 text-sm"></i>
|
|
<input
|
|
type="text"
|
|
placeholder="Search by name..."
|
|
class="w-full pl-10 pr-4 py-2.5 text-sm bg-base-200 border border-base-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all"
|
|
x-model="keyword"
|
|
@keyup.enter="fetchList()"
|
|
/>
|
|
</div>
|
|
<button
|
|
class="px-4 py-2.5 text-sm font-medium bg-base-content text-base-100 rounded-lg hover:bg-base-content/90 transition-all duration-200 flex items-center gap-2"
|
|
@click="fetchList()"
|
|
>
|
|
<i class="fa-solid fa-magnifying-glass text-xs"></i> Search
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-base-100 rounded-xl border border-base-300 shadow-sm overflow-hidden">
|
|
<template x-if="loading">
|
|
<div class="p-8 text-center">
|
|
<span class="loading loading-spinner loading-lg text-primary"></span>
|
|
<p class="mt-2 text-base-content/60">Loading...</p>
|
|
</div>
|
|
</template>
|
|
|
|
<template x-if="!loading && error">
|
|
<div class="p-8 text-center">
|
|
<i class="fa-solid fa-triangle-exclamation text-4xl text-error mb-2"></i>
|
|
<p class="text-error" x-text="error"></p>
|
|
</div>
|
|
</template>
|
|
|
|
<template x-if="!loading && !error && list">
|
|
<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-3 px-5 font-semibold">Test Name</th>
|
|
<th class="py-3 px-5 font-semibold">Unit</th>
|
|
<th class="py-3 px-5 font-semibold">Method</th>
|
|
<th class="py-3 px-5 font-semibold text-right">Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="text-base-content/80 divide-y divide-base-300">
|
|
<template x-for="item in list" :key="item.testId">
|
|
<tr class="hover:bg-base-200 transition-colors">
|
|
<td class="py-3 px-5 font-medium text-base-content" x-text="item.testName"></td>
|
|
<td class="py-3 px-5" x-text="item.testUnit"></td>
|
|
<td class="py-3 px-5" x-text="item.testMethod"></td>
|
|
<td class="py-3 px-5 text-right">
|
|
<button
|
|
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-amber-700 bg-amber-50 hover:bg-amber-100 rounded-lg transition-colors"
|
|
@click="showForm(item.testId)"
|
|
>
|
|
<i class="fa-solid fa-pencil"></i> Edit
|
|
</button>
|
|
<button
|
|
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-error bg-error/10 hover:bg-error/20 rounded-lg transition-colors ml-1"
|
|
@click="deleteData(item.testId)"
|
|
>
|
|
<i class="fa-solid fa-trash"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
<template x-if="list.length === 0">
|
|
<tr>
|
|
<td colspan="4" class="py-8 text-center text-base-content/60">No data available</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<?= $this->include('master/test/dialog_test_form'); ?>
|
|
</main>
|
|
<?= $this->endSection(); ?>
|
|
|
|
<?= $this->section("script"); ?>
|
|
<script>
|
|
document.addEventListener('alpine:init', () => {
|
|
Alpine.data("tests", () => ({
|
|
loading: false,
|
|
showModal: false,
|
|
errors: {},
|
|
error: null,
|
|
keyword: "",
|
|
list: null,
|
|
form: {
|
|
testId: null,
|
|
testName: "",
|
|
testUnit: "",
|
|
testMethod: "",
|
|
cva: "",
|
|
ba: "",
|
|
tea: "",
|
|
},
|
|
|
|
init() {
|
|
this.fetchList();
|
|
},
|
|
|
|
async fetchList() {
|
|
this.loading = true;
|
|
this.error = null;
|
|
this.list = null;
|
|
try {
|
|
const params = new URLSearchParams({ keyword: this.keyword });
|
|
const response = await fetch(`${BASEURL}api/master/tests?${params}`, {
|
|
method: "GET",
|
|
headers: { "Content-Type": "application/json" }
|
|
});
|
|
if (!response.ok) throw new Error("Failed to load data");
|
|
const data = await response.json();
|
|
this.list = data.data;
|
|
} catch (err) {
|
|
this.error = err.message;
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
async loadData(id) {
|
|
this.loading = true;
|
|
try {
|
|
const response = await fetch(`${BASEURL}api/master/tests/${id}`, {
|
|
method: "GET",
|
|
headers: { "Content-Type": "application/json" }
|
|
});
|
|
if (!response.ok) throw new Error("Failed to load item");
|
|
const data = await response.json();
|
|
this.form = data.data[0];
|
|
} catch (err) {
|
|
this.error = err.message;
|
|
this.form = {};
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
async showForm(id = null) {
|
|
this.showModal = true;
|
|
this.errors = {};
|
|
if (id) {
|
|
await this.loadData(id);
|
|
} else {
|
|
this.form = { testId: null, testName: "", testUnit: "", testMethod: "", cva: "", ba: "", tea: "" };
|
|
}
|
|
},
|
|
|
|
closeModal() {
|
|
this.showModal = false;
|
|
this.form = { testId: null, testName: "", testUnit: "", testMethod: "", cva: "", ba: "", tea: "" };
|
|
},
|
|
|
|
validate() {
|
|
this.errors = {};
|
|
if (!this.form.testName) this.errors.testName = "Name is required.";
|
|
return Object.keys(this.errors).length === 0;
|
|
},
|
|
|
|
async save() {
|
|
if (!this.validate()) return;
|
|
this.loading = true;
|
|
let method = '';
|
|
let url = '';
|
|
if (this.form.testId) {
|
|
method = 'PATCH';
|
|
url = `${BASEURL}api/master/tests/${this.form.testId}`;
|
|
} else {
|
|
method = 'POST';
|
|
url = `${BASEURL}api/master/tests`;
|
|
}
|
|
try {
|
|
const res = await fetch(url, {
|
|
method: method,
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(this.form),
|
|
});
|
|
const data = await res.json();
|
|
if (data.status === 'success') {
|
|
alert("Data saved successfully!");
|
|
this.closeModal();
|
|
this.fetchList();
|
|
} else {
|
|
alert(data.message || "Something went wrong.");
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
alert("Failed to save data.");
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
async deleteData(id) {
|
|
if (!confirm("Are you sure you want to delete this item?")) return;
|
|
this.loading = true;
|
|
try {
|
|
const res = await fetch(`${BASEURL}api/master/tests/${id}`, {
|
|
method: "DELETE",
|
|
headers: { "Content-Type": "application/json" }
|
|
});
|
|
const data = await res.json();
|
|
if (data.status === 'success') {
|
|
alert("Data deleted successfully!");
|
|
this.fetchList();
|
|
} else {
|
|
alert(data.message || "Failed to delete.");
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
alert("Failed to delete data.");
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
}
|
|
}));
|
|
});
|
|
</script>
|
|
<?= $this->endSection(); ?>
|