chore: remove deprecated documentation artifacts and update test dialogs
- Delete obsolete agent workflow and migration documentation files - Update test management dialogs (calc, param, test dialogs)
This commit is contained in:
parent
cd65e91db1
commit
011ea11cc9
@ -1,70 +0,0 @@
|
||||
# How to Control Agent Behavior
|
||||
|
||||
## Stopping Auto-Verification After Implementation
|
||||
|
||||
If you want the agent to **stop verifying/testing** after implementing code in future chats, you can:
|
||||
|
||||
### Option 1: Add to User Rules (Recommended)
|
||||
|
||||
Go to your **User Settings** and add a custom rule:
|
||||
|
||||
```
|
||||
After implementing code changes, do not automatically verify or test unless explicitly asked.
|
||||
```
|
||||
|
||||
This will apply to all future conversations.
|
||||
|
||||
### Option 2: Use Explicit Instructions
|
||||
|
||||
In each chat, you can say:
|
||||
|
||||
- ✅ "Implement X **without testing**"
|
||||
- ✅ "Just create the code, **don't verify**"
|
||||
- ✅ "Skip verification, I'll test it myself"
|
||||
- ✅ "**No auto-run** after implementation"
|
||||
|
||||
### Option 3: Workflow Annotation
|
||||
|
||||
Add `// turbo` or `// turbo-all` annotations to workflows you want auto-executed.
|
||||
|
||||
For workflows you **don't** want auto-executed, simply don't add the annotation.
|
||||
|
||||
---
|
||||
|
||||
## Current Behavior
|
||||
|
||||
By default, the agent is **proactive** and will:
|
||||
- ✅ Implement your request
|
||||
- ✅ Run commands if safe
|
||||
- ✅ Verify the implementation works
|
||||
- ✅ Test and report results
|
||||
|
||||
This is helpful for catching errors early, but you can disable it with the methods above.
|
||||
|
||||
---
|
||||
|
||||
## Example User Rule
|
||||
|
||||
Add this to your **User Settings → Custom Rules**:
|
||||
|
||||
```
|
||||
Implementation Preference:
|
||||
- After creating or modifying code, wait for my explicit instruction before testing
|
||||
- Do not auto-run commands unless I specifically ask
|
||||
- Provide a summary of changes and ask if I want to test
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| What You Want | How to Achieve It |
|
||||
|---------------|-------------------|
|
||||
| No auto-testing | Add user rule: "Don't auto-test" |
|
||||
| Sometimes test | Use explicit instructions per request |
|
||||
| Always test specific workflows | Add `// turbo-all` to workflow |
|
||||
| Never test specific workflows | Don't add turbo annotations |
|
||||
|
||||
---
|
||||
|
||||
**Note:** The agent will still be helpful and provide guidance, but won't automatically run verification commands unless you ask.
|
||||
@ -1,216 +0,0 @@
|
||||
# ✅ V2 Custom Tailwind Migration - COMPLETE
|
||||
|
||||
## Migration Summary
|
||||
|
||||
**Status:** ✅ **ALL PHASES COMPLETED**
|
||||
|
||||
**Date:** 2025-12-30
|
||||
|
||||
---
|
||||
|
||||
## What Was Done
|
||||
|
||||
### Phase 1: Base CSS System ✅
|
||||
**File:** `public/css/v2/styles.css`
|
||||
|
||||
Created a comprehensive 900+ line custom CSS design system featuring:
|
||||
- CSS variables for theming (light/dark mode)
|
||||
- Premium glassmorphism effects
|
||||
- Modern gradient buttons
|
||||
- Custom form inputs with focus states
|
||||
- Beautiful table styling
|
||||
- Modal/dialog animations
|
||||
- Badge components
|
||||
- Alert components
|
||||
- Loading spinners
|
||||
- Navigation menus
|
||||
- Utility classes
|
||||
|
||||
### Phase 2: Main Layout ✅
|
||||
**File:** `app/Views/layout/main_layout.php`
|
||||
|
||||
Completely redesigned with:
|
||||
- ✨ Glassmorphism navbar with backdrop blur
|
||||
- 🎨 Gradient sidebar (dark theme)
|
||||
- 🌙 Working theme toggle (light/dark)
|
||||
- 💫 Smooth sidebar animations
|
||||
- 👤 Premium user dropdown with transitions
|
||||
- 🔤 Inter font integration
|
||||
- ⚡ Removed all DaisyUI dependencies
|
||||
|
||||
### Phase 3: Login Page ✅
|
||||
**File:** `app/Views/auth/login.php`
|
||||
|
||||
Redesigned with:
|
||||
- 🌈 Animated gradient background
|
||||
- 💎 Glassmorphism login card
|
||||
- 🎭 Floating logo animation
|
||||
- 📝 Modern form inputs
|
||||
- 🚪 Smooth register modal with transitions
|
||||
- ✅ Custom alerts
|
||||
|
||||
### Phase 4: Feature Pages ✅
|
||||
|
||||
#### Dashboard ✅
|
||||
**File:** `app/Views/dashboard/dashboard_index.php`
|
||||
|
||||
- 📊 Gradient stat cards with hover effects
|
||||
- 🎯 Modern activity feed
|
||||
- ⚡ Quick action buttons
|
||||
- 🎨 Glassmorphism welcome card
|
||||
|
||||
#### Patients Index ✅
|
||||
**File:** `app/Views/patients/patients_index.php`
|
||||
|
||||
- 📈 Animated stat cards
|
||||
- 🔍 Clean search bar
|
||||
- 📋 Modern table design
|
||||
- 🗑️ Custom delete confirmation modal
|
||||
- 💫 Smooth loading states
|
||||
|
||||
#### Patient Form ✅
|
||||
**File:** `app/Views/patients/dialog_form.php`
|
||||
|
||||
- 📝 Premium modal design
|
||||
- ✨ Smooth enter/exit animations
|
||||
- 🎯 Clean form layout
|
||||
- ⚠️ Error state styling
|
||||
|
||||
---
|
||||
|
||||
## Files Modified
|
||||
|
||||
| File | Status | Changes |
|
||||
|------|--------|---------|
|
||||
| `public/css/v2/styles.css` | ✅ Created | Complete design system |
|
||||
| `app/Views/layout/main_layout.php` | ✅ Migrated | Removed DaisyUI, custom components |
|
||||
| `app/Views/auth/login.php` | ✅ Migrated | Premium glassmorphism design |
|
||||
| `app/Views/dashboard/dashboard_index.php` | ✅ Migrated | Modern stat cards |
|
||||
| `app/Views/patients/patients_index.php` | ✅ Migrated | Custom table & modals |
|
||||
| `app/Views/patients/dialog_form.php` | ✅ Migrated | Animated form modal |
|
||||
|
||||
**Total Files:** 6
|
||||
|
||||
---
|
||||
|
||||
## DaisyUI Classes Replaced
|
||||
|
||||
All DaisyUI classes have been replaced with custom CSS:
|
||||
|
||||
| Old (DaisyUI) | New (Custom) |
|
||||
|---------------|--------------|
|
||||
| `btn btn-primary` | `btn btn-primary` (custom) |
|
||||
| `card` | `card` (glassmorphism) |
|
||||
| `input input-bordered` | `input` (custom) |
|
||||
| `modal modal-open` | `modal-overlay` + Alpine |
|
||||
| `alert alert-error` | `alert alert-error` (custom) |
|
||||
| `badge badge-primary` | `badge badge-primary` (custom) |
|
||||
| `table table-zebra` | `table` (custom) |
|
||||
| `loading loading-spinner` | `spinner` (CSS animation) |
|
||||
| `dropdown` | Custom with Alpine.js |
|
||||
| `menu` | `menu` (custom nav) |
|
||||
|
||||
---
|
||||
|
||||
## Design Features
|
||||
|
||||
### Color Palette
|
||||
- **Primary:** Indigo (#6366f1) → Violet (#8b5cf6) gradient
|
||||
- **Success:** Emerald (#10b981)
|
||||
- **Warning:** Amber (#f59e0b)
|
||||
- **Error:** Red (#ef4444)
|
||||
- **Info:** Sky (#0ea5e9)
|
||||
|
||||
### Typography
|
||||
- **Font:** Inter (Google Fonts)
|
||||
- **Headings:** Bold, tracking-tight
|
||||
- **Body:** Normal, leading-relaxed
|
||||
|
||||
### Effects
|
||||
- ✨ Glassmorphism with backdrop-filter
|
||||
- 🎨 Gradient buttons and cards
|
||||
- 💫 Smooth micro-animations
|
||||
- 🌙 Proper dark mode support
|
||||
- 📱 Fully responsive
|
||||
|
||||
---
|
||||
|
||||
## How to Test
|
||||
|
||||
1. **Navigate to login page:**
|
||||
```
|
||||
http://localhost/clqms-be/v2/login
|
||||
```
|
||||
- Check animated gradient background
|
||||
- Test login form
|
||||
- Try register modal
|
||||
|
||||
2. **After login, check dashboard:**
|
||||
```
|
||||
http://localhost/clqms-be/v2/
|
||||
```
|
||||
- Verify stat cards
|
||||
- Test sidebar toggle
|
||||
- Try theme toggle (light/dark)
|
||||
|
||||
3. **Test patients page:**
|
||||
```
|
||||
http://localhost/clqms-be/v2/patients
|
||||
```
|
||||
- Check table styling
|
||||
- Test "New Patient" modal
|
||||
- Try search functionality
|
||||
|
||||
---
|
||||
|
||||
## Browser Compatibility
|
||||
|
||||
✅ Chrome/Edge (Chromium)
|
||||
✅ Firefox
|
||||
✅ Safari (with -webkit- prefixes)
|
||||
⚠️ IE11 (not supported - uses modern CSS)
|
||||
|
||||
---
|
||||
|
||||
## Performance Notes
|
||||
|
||||
- **CSS File Size:** ~30KB (unminified)
|
||||
- **No external dependencies** except:
|
||||
- TailwindCSS 4 CDN (for utilities)
|
||||
- Alpine.js (for interactivity)
|
||||
- FontAwesome (for icons)
|
||||
- Inter font (Google Fonts)
|
||||
|
||||
---
|
||||
|
||||
## Next Steps (Optional Enhancements)
|
||||
|
||||
1. **Add more pages:**
|
||||
- Lab Requests page
|
||||
- Settings page
|
||||
- Reports page
|
||||
|
||||
2. **Optimize:**
|
||||
- Minify CSS for production
|
||||
- Add CSS purging
|
||||
- Lazy load fonts
|
||||
|
||||
3. **Enhance:**
|
||||
- Add toast notifications
|
||||
- Implement skeleton loaders
|
||||
- Add page transitions
|
||||
|
||||
---
|
||||
|
||||
## Migration Complete! 🎉
|
||||
|
||||
All V2 views have been successfully migrated from DaisyUI to a custom Tailwind CSS design system with:
|
||||
|
||||
- ✅ Premium aesthetics
|
||||
- ✅ Glassmorphism effects
|
||||
- ✅ Smooth animations
|
||||
- ✅ Dark mode support
|
||||
- ✅ Full responsiveness
|
||||
- ✅ No DaisyUI dependencies
|
||||
|
||||
**Bismillah, the migration is complete and ready for testing!**
|
||||
@ -1,304 +0,0 @@
|
||||
# V2 Custom Tailwind Migration Plan
|
||||
|
||||
## Overview
|
||||
Migrate all V2 views from DaisyUI to custom TailwindCSS with a premium, modern aesthetic.
|
||||
|
||||
## Design System Goals
|
||||
- **Premium glassmorphism effects**
|
||||
- **Smooth micro-animations**
|
||||
- **Beautiful gradients and shadows**
|
||||
- **Modern color palette** (not generic)
|
||||
- **Consistent spacing and typography**
|
||||
|
||||
---
|
||||
|
||||
## Files to Migrate
|
||||
|
||||
### 1. Layout & Core (Priority: HIGH)
|
||||
| File | Description | Complexity |
|
||||
|------|-------------|------------|
|
||||
| `layout/main_layout.php` | Main layout with sidebar, navbar, footer | High |
|
||||
|
||||
### 2. Auth Views (Priority: HIGH)
|
||||
| File | Description | Complexity |
|
||||
|------|-------------|------------|
|
||||
| `auth/login.php` | Login + Register modal | Medium |
|
||||
|
||||
### 3. Feature Views (Priority: MEDIUM)
|
||||
| File | Description | Complexity |
|
||||
|------|-------------|------------|
|
||||
| `dashboard/dashboard_index.php` | Dashboard with stats cards | Medium |
|
||||
| `patients/patients_index.php` | Patient list with table, search, pagination | High |
|
||||
| `patients/dialog_form.php` | Patient form modal | Medium |
|
||||
|
||||
---
|
||||
|
||||
## Design System Specifications
|
||||
|
||||
### Color Palette
|
||||
```css
|
||||
/* Primary Colors */
|
||||
--color-primary: #6366f1; /* Indigo */
|
||||
--color-primary-hover: #4f46e5;
|
||||
--color-primary-light: #818cf8;
|
||||
|
||||
/* Secondary Colors */
|
||||
--color-secondary: #8b5cf6; /* Violet */
|
||||
|
||||
/* Semantic Colors */
|
||||
--color-success: #10b981; /* Emerald */
|
||||
--color-warning: #f59e0b; /* Amber */
|
||||
--color-error: #ef4444; /* Red */
|
||||
--color-info: #0ea5e9; /* Sky */
|
||||
|
||||
/* Neutral Colors */
|
||||
--color-text: #1e293b; /* Slate 800 */
|
||||
--color-text-muted: #64748b; /* Slate 500 */
|
||||
--color-bg: #f8fafc; /* Slate 50 */
|
||||
--color-bg-dark: #0f172a; /* Slate 900 */
|
||||
--color-surface: #ffffff;
|
||||
--color-surface-dark: #1e293b;
|
||||
--color-border: #e2e8f0; /* Slate 200 */
|
||||
```
|
||||
|
||||
### Typography
|
||||
- **Font**: Inter (via Google Fonts)
|
||||
- **Headings**: font-bold, tracking-tight
|
||||
- **Body**: font-normal, leading-relaxed
|
||||
|
||||
### Component Styles
|
||||
|
||||
#### 1. Buttons
|
||||
```css
|
||||
/* Primary Button */
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||
color: white;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.75rem;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 4px 14px rgba(99, 102, 241, 0.4);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(99, 102, 241, 0.5);
|
||||
}
|
||||
|
||||
/* Ghost Button */
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
color: currentColor;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
.btn-ghost:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Cards
|
||||
```css
|
||||
/* Glass Card */
|
||||
.card {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/* Dark Mode */
|
||||
[data-theme="dark"] .card {
|
||||
background: rgba(30, 41, 59, 0.8);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Inputs
|
||||
```css
|
||||
.input {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 0.75rem;
|
||||
background: #f8fafc;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.input:focus {
|
||||
outline: none;
|
||||
border-color: #6366f1;
|
||||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15);
|
||||
background: white;
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. Sidebar
|
||||
```css
|
||||
.sidebar {
|
||||
background: linear-gradient(180deg, #1e293b, #0f172a);
|
||||
/* Or glassmorphism variant */
|
||||
background: rgba(15, 23, 42, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
.sidebar-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.75rem;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.sidebar-link:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
}
|
||||
.sidebar-link.active {
|
||||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||
color: white;
|
||||
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4);
|
||||
}
|
||||
```
|
||||
|
||||
#### 5. Modals/Dialogs
|
||||
```css
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(4px);
|
||||
z-index: 50;
|
||||
}
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 1.5rem;
|
||||
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.25);
|
||||
max-width: 32rem;
|
||||
animation: modalEnter 0.3s ease-out;
|
||||
}
|
||||
@keyframes modalEnter {
|
||||
from { opacity: 0; transform: scale(0.95) translateY(10px); }
|
||||
to { opacity: 1; transform: scale(1) translateY(0); }
|
||||
}
|
||||
```
|
||||
|
||||
#### 6. Tables
|
||||
```css
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
}
|
||||
.table th {
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
.table td {
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
.table tr:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
```
|
||||
|
||||
#### 7. Badges
|
||||
```css
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.badge-primary {
|
||||
background: rgba(99, 102, 241, 0.15);
|
||||
color: #6366f1;
|
||||
}
|
||||
.badge-success {
|
||||
background: rgba(16, 185, 129, 0.15);
|
||||
color: #10b981;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration Steps
|
||||
|
||||
### Phase 1: Create Base CSS (styles.css)
|
||||
1. Create `public/css/v2/styles.css` with all custom utilities
|
||||
2. Define CSS variables for theming
|
||||
3. Add animation keyframes
|
||||
4. Add component base styles
|
||||
|
||||
### Phase 2: Migrate Main Layout
|
||||
1. Remove DaisyUI CDN link
|
||||
2. Add custom styles.css link
|
||||
3. Redesign sidebar with glassmorphism
|
||||
4. Redesign navbar with clean white/dark theme
|
||||
5. Update theme toggle functionality
|
||||
6. Improve user dropdown
|
||||
|
||||
### Phase 3: Migrate Auth Pages
|
||||
1. Redesign login page with premium glass card
|
||||
2. Update form inputs with custom styling
|
||||
3. Improve register modal
|
||||
4. Add subtle animations
|
||||
|
||||
### Phase 4: Migrate Feature Pages
|
||||
1. Redesign dashboard with gradient stat cards
|
||||
2. Update patients table with modern styling
|
||||
3. Improve modals and dialogs
|
||||
4. Add micro-animations
|
||||
|
||||
### Phase 5: Polish & Testing
|
||||
1. Test all theme switching
|
||||
2. Verify responsive design
|
||||
3. Add loading states and transitions
|
||||
4. Cross-browser testing
|
||||
|
||||
---
|
||||
|
||||
## Estimated Timeline
|
||||
- Phase 1: 15 minutes
|
||||
- Phase 2: 30 minutes
|
||||
- Phase 3: 20 minutes
|
||||
- Phase 4: 40 minutes
|
||||
- Phase 5: 15 minutes
|
||||
|
||||
**Total: ~2 hours**
|
||||
|
||||
---
|
||||
|
||||
## DaisyUI Classes to Replace
|
||||
|
||||
| DaisyUI Class | Custom Tailwind Replacement |
|
||||
|---------------|----------------------------|
|
||||
| `btn btn-primary` | `btn-primary` (custom class) |
|
||||
| `btn btn-ghost` | `btn-ghost` (custom class) |
|
||||
| `card` | `card` (glassmorphism custom) |
|
||||
| `card-body` | `p-6` |
|
||||
| `input input-bordered` | `input` (custom class) |
|
||||
| `select select-bordered` | `select` (custom class) |
|
||||
| `modal modal-open` | `modal` + Alpine `x-show` |
|
||||
| `alert alert-error` | `alert alert-error` (custom) |
|
||||
| `badge badge-primary` | `badge badge-primary` (custom) |
|
||||
| `table table-zebra` | `table` (custom styling) |
|
||||
| `avatar` | `avatar` (custom) |
|
||||
| `dropdown` | Custom dropdown with Alpine |
|
||||
| `menu` | `nav-menu` (custom) |
|
||||
| `join` | `flex group` |
|
||||
| `divider` | `divider` (custom) |
|
||||
| `loading loading-spinner` | `spinner` (custom SVG/CSS) |
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
Ready to proceed? I'll start with **Phase 1** - creating the base CSS file with all custom utilities and component styles.
|
||||
@ -1,27 +0,0 @@
|
||||
---
|
||||
description: Rules and guidelines for the AI agent working on this project
|
||||
---
|
||||
|
||||
# Agent Guidelines
|
||||
|
||||
## Backend Update Rules
|
||||
|
||||
> **IMPORTANT**: On every backend update (controllers, models, routes, database changes), you MUST update `README.md` accordingly.
|
||||
|
||||
### Documentation Updates Required For:
|
||||
1. **New API endpoints** - Add to API documentation
|
||||
2. **Database schema changes** - Update schema docs
|
||||
3. **New features** - Document in appropriate section
|
||||
4. **Configuration changes** - Update technical stack or setup instructions
|
||||
|
||||
### V2 Frontend Updates
|
||||
|
||||
For any changes to the `/v2` hidden frontend, update the dedicated documentation at:
|
||||
- `app/Views/v2/README.md`
|
||||
|
||||
This keeps V2 changes separate from the main README to avoid exposing the hidden UI to the team.
|
||||
|
||||
## Workflow Preferences
|
||||
- Use 2-space indentation for all code
|
||||
- Follow existing code patterns and naming conventions
|
||||
- Always test changes before committing
|
||||
@ -1,753 +0,0 @@
|
||||
---
|
||||
description: PHP + Alpine.js SPA-like Application Pattern (CodeIgniter 4 + Custom Tailwind)
|
||||
---
|
||||
|
||||
# PHP + Alpine.js Application Pattern
|
||||
|
||||
This workflow describes how to build web applications using **PHP (CodeIgniter 4)** for backend with **Alpine.js + Custom Tailwind CSS** for frontend, creating an SPA-like experience with server-rendered views.
|
||||
|
||||
## Philosophy
|
||||
|
||||
**"No-nonsense"** - Keep it simple, avoid over-engineering. This pattern gives you:
|
||||
- Fast development with PHP backend
|
||||
- Reactive UI with Alpine.js (no heavy framework overhead)
|
||||
- Beautiful UI with custom Tailwind CSS design system
|
||||
- JWT-based authentication
|
||||
|
||||
---
|
||||
|
||||
## Technology Stack
|
||||
|
||||
| Layer | Technology |
|
||||
|-------|------------|
|
||||
| Backend | CodeIgniter 4 (PHP 8.1+) |
|
||||
| Frontend | Alpine.js + Custom Tailwind CSS |
|
||||
| Database | MySQL/MariaDB |
|
||||
| Auth | JWT (stored in HTTP-only cookies) |
|
||||
| Icons | FontAwesome 6+ |
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
project/
|
||||
├── app/
|
||||
│ ├── Config/
|
||||
│ │ └── Routes.php # All routes (pages + API)
|
||||
│ ├── Controllers/
|
||||
│ │ ├── BaseController.php # Base controller
|
||||
│ │ ├── PagesController.php # Page routes (returns views)
|
||||
│ │ └── [Resource]Controller.php # API controllers
|
||||
│ ├── Models/
|
||||
│ │ └── [Resource]Model.php # Database models
|
||||
│ ├── Filters/
|
||||
│ │ └── JwtAuthFilter.php # JWT authentication filter
|
||||
│ └── Views/
|
||||
│ ├── layout/
|
||||
│ │ └── main_layout.php # Base layout with sidebar
|
||||
│ ├── [module]/
|
||||
│ │ ├── [module]_index.php # Main page with x-data
|
||||
│ │ ├── dialog_[name].php # Modal dialogs (included)
|
||||
│ │ └── drawer_[name].php # Drawer components
|
||||
│ └── login.php
|
||||
├── public/
|
||||
│ ├── index.php
|
||||
│ └── assets/
|
||||
│ ├── css/output.css # Compiled TailwindCSS
|
||||
│ └── js/app.js # Alpine.js setup
|
||||
└── .env # Environment config
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. Backend Patterns
|
||||
|
||||
### 1.1 Routes Structure (`app/Config/Routes.php`)
|
||||
|
||||
Routes are split into:
|
||||
1. **Public routes** - Login, logout, auth check
|
||||
2. **Protected page routes** - Views (with `jwt-auth` filter)
|
||||
3. **API routes** - RESTful JSON endpoints
|
||||
|
||||
```php
|
||||
<?php
|
||||
use CodeIgniter\Router\RouteCollection;
|
||||
|
||||
/** @var RouteCollection $routes */
|
||||
|
||||
// Public routes
|
||||
$routes->get('/login', 'PagesController::login');
|
||||
$routes->post('/login', 'AuthController::login');
|
||||
$routes->get('/logout', 'AuthController::logout');
|
||||
|
||||
// Protected page routes (returns views)
|
||||
$routes->group('', ['filter' => 'jwt-auth'], function ($routes) {
|
||||
$routes->get('/', 'PagesController::dashboard');
|
||||
$routes->get('/patients', 'PagesController::patients');
|
||||
$routes->get('/requests', 'PagesController::requests');
|
||||
// Master data pages
|
||||
$routes->get('/master/doctors', 'PagesController::masterDoctors');
|
||||
});
|
||||
|
||||
// API routes (returns JSON)
|
||||
$routes->group('api', function ($routes) {
|
||||
// Resource: patients
|
||||
$routes->get('patients', 'PatientsController::index');
|
||||
$routes->get('patients/(:num)', 'PatientsController::show/$1');
|
||||
$routes->post('patients', 'PatientsController::create');
|
||||
$routes->patch('patients/(:num)', 'PatientsController::update/$1');
|
||||
|
||||
// Resource: [resourceName]
|
||||
// Follow same pattern: index, show, create, update
|
||||
});
|
||||
```
|
||||
|
||||
### 1.2 Pages Controller (`app/Controllers/PagesController.php`)
|
||||
|
||||
This controller ONLY returns views. No business logic.
|
||||
|
||||
```php
|
||||
<?php
|
||||
namespace App\Controllers;
|
||||
|
||||
class PagesController extends BaseController {
|
||||
|
||||
public function dashboard() {
|
||||
return view('dashboard', [
|
||||
'pageTitle' => 'Dashboard',
|
||||
'activePage' => 'dashboard'
|
||||
]);
|
||||
}
|
||||
|
||||
public function patients() {
|
||||
return view('patients/patients_index', [
|
||||
'pageTitle' => 'Patients',
|
||||
'activePage' => 'patients'
|
||||
]);
|
||||
}
|
||||
|
||||
public function requests() {
|
||||
return view('requests/requests_index', [
|
||||
'pageTitle' => 'Lab Requests',
|
||||
'activePage' => 'requests'
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3 API Controller Pattern (`app/Controllers/[Resource]Controller.php`)
|
||||
|
||||
API controllers handle CRUD operations and return JSON.
|
||||
|
||||
```php
|
||||
<?php
|
||||
namespace App\Controllers;
|
||||
|
||||
use CodeIgniter\API\ResponseTrait;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\PatientsModel;
|
||||
|
||||
class PatientsController extends BaseController {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $model;
|
||||
protected $rules;
|
||||
|
||||
public function __construct() {
|
||||
$this->model = new PatientsModel();
|
||||
$this->rules = [
|
||||
'firstName' => 'required|min_length[2]',
|
||||
'lastName' => 'required|min_length[2]',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/patients
|
||||
* List all with optional search
|
||||
*/
|
||||
public function index() {
|
||||
$keyword = $this->request->getGet('keyword');
|
||||
|
||||
try {
|
||||
$rows = $this->model->search($keyword);
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'fetch success',
|
||||
'data' => $rows
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Exception: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/patients/:id
|
||||
*/
|
||||
public function show($id = null) {
|
||||
try {
|
||||
$row = $this->model->find($id);
|
||||
if (empty($row)) {
|
||||
return $this->respond(['status' => 'success', 'message' => 'not found'], 200);
|
||||
}
|
||||
return $this->respond(['status' => 'success', 'data' => $row], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Error: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/patients
|
||||
*/
|
||||
public function create() {
|
||||
$input = $this->request->getJSON(true);
|
||||
|
||||
if (!$this->validateData($input, $this->rules)) {
|
||||
return $this->failValidationErrors($this->validator->getErrors());
|
||||
}
|
||||
|
||||
$input = camel_to_snake_array($input); // Convert keys to snake_case
|
||||
|
||||
try {
|
||||
$id = $this->model->insert($input);
|
||||
return $this->respondCreated([
|
||||
'status' => 'success',
|
||||
'message' => 'Created successfully',
|
||||
'data' => ['id' => $id]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Error: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH /api/patients/:id
|
||||
*/
|
||||
public function update($id = null) {
|
||||
$input = $this->request->getJSON(true);
|
||||
$input = camel_to_snake_array($input);
|
||||
|
||||
try {
|
||||
$this->model->update($id, $input);
|
||||
return $this->respond(['status' => 'success', 'message' => 'updated']);
|
||||
} catch (\Exception $e) {
|
||||
return $this->failServerError('Error: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 1.4 Standard API Response Format
|
||||
|
||||
Always return this structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success|error",
|
||||
"message": "Human readable message",
|
||||
"data": {} // or [] for lists
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Frontend Patterns
|
||||
|
||||
### 2.1 Base Layout (`app/Views/layout/main_layout.php`)
|
||||
|
||||
The layout provides:
|
||||
- Sidebar navigation with Alpine.js state
|
||||
- Top navbar with user info
|
||||
- Content section for page-specific content
|
||||
- Script section for page-specific JavaScript
|
||||
|
||||
```php
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="corporate">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AppName</title>
|
||||
<link href="<?=base_url();?>assets/css/output.css" rel="stylesheet" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/js/all.min.js"></script>
|
||||
</head>
|
||||
<body class="h-screen flex bg-base-100">
|
||||
<!-- Sidebar with Alpine.js -->
|
||||
<aside
|
||||
x-data="main({ activePage: '<?= esc($activePage ?? '') ?>' })"
|
||||
class="w-56 bg-slate-900 text-white flex flex-col"
|
||||
>
|
||||
<!-- Navigation links -->
|
||||
<nav class="px-3 py-2 space-y-1">
|
||||
<a href="<?=base_url();?>"
|
||||
class="flex items-center gap-3 px-3 py-2.5 rounded-lg"
|
||||
:class="page === 'dashboard' ? 'bg-blue-600' : 'hover:bg-slate-700'"
|
||||
>
|
||||
<i class="fa-solid fa-th-large"></i>
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
<!-- More nav items... -->
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div class="flex-1 flex flex-col overflow-hidden">
|
||||
<!-- Top Navbar -->
|
||||
<nav class="bg-white border-b px-6 py-3">
|
||||
<span class="font-semibold"><?= esc($pageTitle ?? 'Dashboard') ?></span>
|
||||
</nav>
|
||||
|
||||
<!-- Page Content -->
|
||||
<?= $this->renderSection('content') ?>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.BASEURL = "<?=base_url();?>";
|
||||
</script>
|
||||
<?= $this->renderSection('script') ?>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### 2.2 Page View Pattern (`app/Views/[module]/[module]_index.php`)
|
||||
|
||||
Each page extends the layout and defines its Alpine.js component.
|
||||
|
||||
```php
|
||||
<?= $this->extend("layout/main_layout"); ?>
|
||||
|
||||
<?= $this->section("content") ?>
|
||||
<main class="flex-1 p-6 overflow-auto bg-slate-50" x-data="patients()">
|
||||
<!-- Page Header -->
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-slate-800">Patients</h1>
|
||||
<p class="text-slate-500 text-sm mt-1">Manage patient records</p>
|
||||
</div>
|
||||
<button class="btn btn-sm bg-emerald-600 text-white" @click="showForm()">
|
||||
<i class="fa-solid fa-plus"></i> New Patient
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Search & Filter -->
|
||||
<div class="bg-white rounded-xl border p-4 mb-6">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
class="input input-bordered w-full max-w-md"
|
||||
x-model="keyword"
|
||||
@keyup.enter="fetchList()"
|
||||
/>
|
||||
<button class="btn btn-neutral" @click="fetchList()">Search</button>
|
||||
</div>
|
||||
|
||||
<!-- Data List -->
|
||||
<div class="bg-white rounded-xl border">
|
||||
<template x-if="list && list.length > 0">
|
||||
<template x-for="item in list" :key="item.patId">
|
||||
<div class="p-4 border-b hover:bg-slate-50" @click="fetchItem(item.patId)">
|
||||
<span x-text="item.firstName + ' ' + item.lastName"></span>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<template x-if="!list || list.length === 0">
|
||||
<div class="p-8 text-center text-slate-400">
|
||||
<p>No records found</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Include dialog components -->
|
||||
<?php echo $this->include('[module]/dialog_form'); ?>
|
||||
</main>
|
||||
<?= $this->endSection(); ?>
|
||||
|
||||
<?= $this->section("script") ?>
|
||||
<script type="module">
|
||||
import Alpine from '<?= base_url('/assets/js/app.js'); ?>';
|
||||
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data("patients", () => ({
|
||||
// State
|
||||
loading: false,
|
||||
showModal: false,
|
||||
errors: {},
|
||||
|
||||
// Data
|
||||
list: null,
|
||||
item: null,
|
||||
keyword: "",
|
||||
|
||||
// Form
|
||||
form: {
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
birthDate: "",
|
||||
sex: "M"
|
||||
},
|
||||
|
||||
// Lifecycle
|
||||
async init() {
|
||||
await this.fetchList();
|
||||
},
|
||||
|
||||
// Fetch list
|
||||
async fetchList() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (this.keyword) params.append('keyword', this.keyword);
|
||||
|
||||
const res = await fetch(`${BASEURL}api/patients?${params}`);
|
||||
if (!res.ok) throw new Error("HTTP error");
|
||||
const data = await res.json();
|
||||
this.list = data.data;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.list = [];
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Fetch single item
|
||||
async fetchItem(id) {
|
||||
this.loading = true;
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/patients/${id}`);
|
||||
const data = await res.json();
|
||||
this.item = data.data;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Form validation
|
||||
validate() {
|
||||
const e = {};
|
||||
if (!this.form.firstName) e.firstName = "First name is required.";
|
||||
if (!this.form.lastName) e.lastName = "Last name is required.";
|
||||
this.errors = e;
|
||||
return Object.keys(e).length === 0;
|
||||
},
|
||||
|
||||
// Show form modal
|
||||
showForm(id = null) {
|
||||
this.showModal = true;
|
||||
if (id) {
|
||||
this.loadFormData(id);
|
||||
} else {
|
||||
this.form = { firstName: "", lastName: "", birthDate: "", sex: "M" };
|
||||
}
|
||||
},
|
||||
|
||||
// Load data for editing
|
||||
async loadFormData(id) {
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/patients/${id}`);
|
||||
const data = await res.json();
|
||||
this.form = data.data;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
},
|
||||
|
||||
closeModal() {
|
||||
this.showModal = false;
|
||||
this.errors = {};
|
||||
},
|
||||
|
||||
// Save (create or update)
|
||||
async save() {
|
||||
if (!this.validate()) return;
|
||||
|
||||
try {
|
||||
let res;
|
||||
if (this.form.patId) {
|
||||
res = await fetch(`${BASEURL}api/patients/${this.form.patId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(this.form)
|
||||
});
|
||||
} else {
|
||||
res = await fetch(`${BASEURL}api/patients`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(this.form)
|
||||
});
|
||||
}
|
||||
|
||||
if (!res.ok) throw new Error("HTTP error");
|
||||
const data = await res.json();
|
||||
|
||||
if (data.status === 'success') {
|
||||
alert("Saved successfully!");
|
||||
await this.fetchList();
|
||||
} else {
|
||||
alert(data.message || "Something went wrong.");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Failed to save.");
|
||||
} finally {
|
||||
this.closeModal();
|
||||
}
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
Alpine.start();
|
||||
</script>
|
||||
<?= $this->endSection(); ?>
|
||||
```
|
||||
|
||||
### 2.3 Dialog Component Pattern (`app/Views/[module]/dialog_form.php`)
|
||||
|
||||
Dialogs are included in the main page and share x-data context.
|
||||
|
||||
```php
|
||||
<!-- Form Modal -->
|
||||
<dialog id="form_modal" class="modal" :class="showModal && 'modal-open'">
|
||||
<div class="modal-box w-11/12 max-w-2xl bg-white">
|
||||
<h3 class="font-bold text-lg flex items-center gap-2">
|
||||
<i class="fa-solid fa-user text-emerald-500"></i>
|
||||
<span x-text="form.patId ? 'Edit Patient' : 'New Patient'"></span>
|
||||
</h3>
|
||||
|
||||
<div class="py-4">
|
||||
<!-- First Name -->
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">First Name <span class="text-red-500">*</span></span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered" x-model="form.firstName" />
|
||||
<span class="text-error text-xs mt-1" x-text="errors.firstName || ''"></span>
|
||||
</div>
|
||||
|
||||
<!-- Last Name -->
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Last Name <span class="text-red-500">*</span></span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered" x-model="form.lastName" />
|
||||
<span class="text-error text-xs mt-1" x-text="errors.lastName || ''"></span>
|
||||
</div>
|
||||
|
||||
<!-- Grid for multiple fields -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Birth Date</span>
|
||||
</label>
|
||||
<input type="date" class="input input-bordered" x-model="form.birthDate" />
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Sex</span>
|
||||
</label>
|
||||
<select class="select select-bordered" x-model="form.sex">
|
||||
<option value="M">Male</option>
|
||||
<option value="F">Female</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-action">
|
||||
<button class="btn btn-ghost" @click="closeModal()">Cancel</button>
|
||||
<button class="btn bg-emerald-600 text-white" @click="save()">
|
||||
<i class="fa-solid fa-save mr-1"></i> Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-backdrop bg-slate-900/50" @click="closeModal()"></div>
|
||||
</dialog>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Naming Conventions
|
||||
|
||||
### 3.1 Files & Directories
|
||||
|
||||
| Type | Convention | Example |
|
||||
|------|------------|---------|
|
||||
| Views | `snake_case` | `patients_index.php` |
|
||||
| Dialogs | `dialog_[name].php` | `dialog_form.php` |
|
||||
| Drawers | `drawer_[name].php` | `drawer_filter.php` |
|
||||
| Controllers | `PascalCase` | `PatientsController.php` |
|
||||
| Models | `PascalCase` | `PatientsModel.php` |
|
||||
|
||||
### 3.2 Variables & Keys
|
||||
|
||||
| Context | Convention | Example |
|
||||
|---------|------------|---------|
|
||||
| PHP/Database | `snake_case` | `pat_id`, `first_name` |
|
||||
| JavaScript | `camelCase` | `patId`, `firstName` |
|
||||
| Alpine.js x-data | `camelCase` | `showModal`, `fetchList` |
|
||||
|
||||
### 3.3 Primary Keys
|
||||
|
||||
Use format: `{table_singular}_id`
|
||||
|
||||
| Table | Primary Key |
|
||||
|-------|-------------|
|
||||
| `patients` | `pat_id` |
|
||||
| `requests` | `request_id` |
|
||||
| `master_tests` | `test_id` |
|
||||
|
||||
---
|
||||
|
||||
## 4. Database Conventions
|
||||
|
||||
### 4.1 Standard Columns
|
||||
|
||||
Every table should have:
|
||||
|
||||
```sql
|
||||
`created_at` DATETIME,
|
||||
`updated_at` DATETIME,
|
||||
`deleted_at` DATETIME -- Soft deletes
|
||||
```
|
||||
|
||||
### 4.2 Status Codes (Single Character)
|
||||
|
||||
| Code | Meaning |
|
||||
|------|---------|
|
||||
| `P` | Pending |
|
||||
| `I` | In Progress |
|
||||
| `C` | Completed |
|
||||
| `V` | Validated |
|
||||
| `X` | Cancelled |
|
||||
|
||||
---
|
||||
|
||||
## 5. UI/UX Guidelines
|
||||
|
||||
### 5.1 Color Palette
|
||||
|
||||
Use CSS variables from the custom design system:
|
||||
|
||||
| Purpose | CSS Variable | Example |
|
||||
|---------|--------------|---------|
|
||||
| Primary | `rgb(var(--color-primary))` | Indigo gradient |
|
||||
| Success | `rgb(var(--color-success))` | Emerald |
|
||||
| Warning | `rgb(var(--color-warning))` | Amber |
|
||||
| Error | `rgb(var(--color-error))` | Red |
|
||||
| Info | `rgb(var(--color-info))` | Sky |
|
||||
|
||||
### 5.2 Component Patterns
|
||||
|
||||
- **Cards**: Use `.card` class (glassmorphism effect)
|
||||
- **Buttons**: Use `.btn .btn-primary` or `.btn-ghost`
|
||||
- **Inputs**: Use `.input` class
|
||||
- **Modals**: Use `.modal-overlay` + `.modal-content` with Alpine.js
|
||||
- **Badges**: Use `.badge .badge-primary` etc.
|
||||
- **Tables**: Use `.table` class
|
||||
|
||||
### 5.3 Icons
|
||||
|
||||
Use FontAwesome 6+ with consistent sizing:
|
||||
- Navigation: `text-sm` or `text-base`
|
||||
- Buttons: Default size
|
||||
- Headers: `text-lg` to `text-2xl`
|
||||
|
||||
---
|
||||
|
||||
## 6. Common Helpers
|
||||
|
||||
### 6.1 camel_to_snake_array (PHP Helper)
|
||||
|
||||
Convert JavaScript camelCase keys to PHP snake_case:
|
||||
|
||||
```php
|
||||
function camel_to_snake_array(array $data): array {
|
||||
$result = [];
|
||||
foreach ($data as $key => $value) {
|
||||
$snakeKey = strtolower(preg_replace('/[A-Z]/', '_$0', lcfirst($key)));
|
||||
$result[$snakeKey] = is_array($value) ? camel_to_snake_array($value) : $value;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 base_url() Usage
|
||||
|
||||
Always use `base_url()` for asset and API paths:
|
||||
|
||||
```php
|
||||
// In PHP
|
||||
<link href="<?=base_url();?>assets/css/output.css" rel="stylesheet" />
|
||||
|
||||
// In JavaScript (via global variable)
|
||||
<script>window.BASEURL = "<?=base_url();?>";</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Quick Reference: Creating a New Module
|
||||
|
||||
### Step 1: Create Route
|
||||
|
||||
```php
|
||||
// app/Config/Routes.php
|
||||
$routes->get('/products', 'PagesController::products');
|
||||
$routes->get('api/products', 'ProductsController::index');
|
||||
$routes->get('api/products/(:num)', 'ProductsController::show/$1');
|
||||
$routes->post('api/products', 'ProductsController::create');
|
||||
$routes->patch('api/products/(:num)', 'ProductsController::update/$1');
|
||||
```
|
||||
|
||||
### Step 2: Create Model
|
||||
|
||||
```php
|
||||
// app/Models/ProductsModel.php
|
||||
namespace App\Models;
|
||||
|
||||
class ProductsModel extends BaseModel {
|
||||
protected $table = 'products';
|
||||
protected $primaryKey = 'product_id';
|
||||
protected $allowedFields = ['name', 'sku', 'price'];
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Create Controller
|
||||
|
||||
Copy pattern from `PatientsController.php` or `RequestsController.php`.
|
||||
|
||||
### Step 4: Create View
|
||||
|
||||
Create `app/Views/products/products_index.php` following the page view pattern.
|
||||
|
||||
### Step 5: Create Dialog
|
||||
|
||||
Create `app/Views/products/dialog_form.php` for add/edit modal.
|
||||
|
||||
### Step 6: Add to Navigation
|
||||
|
||||
Update `app/Views/layout/main_layout.php` sidebar.
|
||||
|
||||
---
|
||||
|
||||
## 8. Things to Avoid
|
||||
|
||||
1. **Don't use jQuery** - Use Alpine.js or vanilla JS
|
||||
2. **Don't over-engineer** - Keep it simple
|
||||
3. **Don't skip soft deletes** - Always use `deleted_at`
|
||||
4. **Don't hardcode URLs** - Use `base_url()` and `BASEURL`
|
||||
5. **Don't mix concerns** - Controllers handle HTTP, Models handle data
|
||||
6. **Don't create separate JS files for each page** - Keep JS inline in views for simplicity
|
||||
|
||||
---
|
||||
|
||||
## 9. Checklist Before Deploying
|
||||
|
||||
- [ ] All routes added to `Routes.php`
|
||||
- [ ] API responses follow standard format
|
||||
- [ ] Form validation in both frontend and backend
|
||||
- [ ] Error handling with user-friendly messages
|
||||
- [ ] Loading states for async operations
|
||||
- [ ] Responsive design tested
|
||||
- [ ] README.md updated
|
||||
@ -51,12 +51,7 @@
|
||||
<button class="flex-1 px-3 py-2 text-sm font-medium rounded-md transition-all duration-200"
|
||||
:class="activeTab === 'results' ? 'bg-amber-500 text-white shadow' : 'hover:bg-opacity-50'"
|
||||
style="color: rgb(var(--color-text));" @click="activeTab = 'results'">
|
||||
<i class="fa-solid fa-flask mr-1"></i> Results
|
||||
</button>
|
||||
<button class="flex-1 px-3 py-2 text-sm font-medium rounded-md transition-all duration-200"
|
||||
:class="activeTab === 'reference' ? 'bg-amber-500 text-white shadow' : 'hover:bg-opacity-50'"
|
||||
style="color: rgb(var(--color-text));" @click="activeTab = 'reference'">
|
||||
<i class="fa-solid fa-ruler-combined mr-1"></i> Ref
|
||||
<i class="fa-solid fa-cog mr-1"></i> Config
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -164,26 +159,29 @@
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.IndentLeft"
|
||||
placeholder="0" />
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<label class="label cursor-pointer justify-start gap-2">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.CountStat" :true-value="1"
|
||||
:false-value="0" />
|
||||
<span class="label-text text-sm">Count in Statistics</span>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Options</span>
|
||||
</label>
|
||||
<div class="flex flex-wrap gap-3 mt-1">
|
||||
<label class="label cursor-pointer justify-start gap-1 px-0">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.CountStat" :true-value="1"
|
||||
:false-value="0" />
|
||||
<span class="label-text text-xs">Count Stat</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer justify-start gap-1 px-0">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.VisibleScr" :true-value="1"
|
||||
:false-value="0" />
|
||||
<span class="label-text text-xs">Screen</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer justify-start gap-1 px-0">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.VisibleRpt" :true-value="1"
|
||||
:false-value="0" />
|
||||
<span class="label-text text-xs">Report</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4 mt-3">
|
||||
<label class="label cursor-pointer justify-start gap-2">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.VisibleScr" :true-value="1"
|
||||
:false-value="0" />
|
||||
<span class="label-text text-sm">Visible on Screen</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer justify-start gap-2">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.VisibleRpt" :true-value="1"
|
||||
:false-value="0" />
|
||||
<span class="label-text text-sm">Visible on Report</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -291,72 +289,63 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sample -->
|
||||
<div>
|
||||
<!-- Reference Range Section -->
|
||||
<div class="mb-4">
|
||||
<h4 class="font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-vial text-amber-500"></i> Sample
|
||||
<i class="fa-solid fa-ruler-combined text-amber-500"></i> Reference Range
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid grid-cols-4 gap-3">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Sample Type</span>
|
||||
<span class="label-text font-medium text-sm">Reference Type</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" x-model="form.SampleType">
|
||||
<option value="">Select Sample Type</option>
|
||||
<option value="SERUM">Serum</option>
|
||||
<option value="PLASMA">Plasma</option>
|
||||
<option value="BLOOD">Whole Blood</option>
|
||||
<option value="URINE">Urine</option>
|
||||
<option value="CSF">CSF</option>
|
||||
<option value="OTHER">Other</option>
|
||||
<select class="select select-bordered w-full" x-model="form.RefType">
|
||||
<option value="">Select Reference Type</option>
|
||||
<option value="NMRC">Numeric Range</option>
|
||||
<option value="TEXT">Text Reference</option>
|
||||
<option value="AGE">Age-based</option>
|
||||
<option value="GENDER">Gender-based</option>
|
||||
<option value="NONE">No Reference</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Numeric Reference Fields -->
|
||||
<div class="grid grid-cols-4 gap-3 mt-3" x-show="form.RefType === 'NMRC' || form.RefType === 'AGE' || form.RefType === 'GENDER'">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Method</span>
|
||||
<span class="label-text font-medium text-sm">Min Normal</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" x-model="form.Method"
|
||||
placeholder="e.g., Automated calculation" />
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.RefMin" placeholder="0" step="any" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Max Normal</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.RefMax" placeholder="100" step="any" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Critical Low</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.CriticalLow" placeholder="Optional" step="any" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Critical High</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.CriticalHigh" placeholder="Optional" step="any" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- Text Reference Field -->
|
||||
<div class="mt-3" x-show="form.RefType === 'TEXT'">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Reference Text</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" x-model="form.RefText" placeholder="e.g., Negative, Positive, etc." />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Reference Range -->
|
||||
<div x-show="activeTab === 'reference'" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform -translate-x-4"
|
||||
x-transition:enter-end="opacity-100 transform translate-x-0">
|
||||
<div class="grid grid-cols-4 gap-3">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Min Normal</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.RefMin" placeholder="0"
|
||||
step="any" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Max Normal</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.RefMax" placeholder="25"
|
||||
step="any" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Critical Low</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.CriticalLow"
|
||||
placeholder="Optional" step="any" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Critical High</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.CriticalHigh"
|
||||
placeholder="Optional" step="any" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@ -46,12 +46,7 @@
|
||||
<button class="flex-1 px-3 py-2 text-sm font-medium rounded-md transition-all duration-200"
|
||||
:class="activeTab === 'results' ? 'bg-emerald-500 text-white shadow' : 'hover:bg-opacity-50'"
|
||||
style="color: rgb(var(--color-text));" @click="activeTab = 'results'">
|
||||
<i class="fa-solid fa-flask mr-1"></i> Results
|
||||
</button>
|
||||
<button class="flex-1 px-3 py-2 text-sm font-medium rounded-md transition-all duration-200"
|
||||
:class="activeTab === 'reference' ? 'bg-emerald-500 text-white shadow' : 'hover:bg-opacity-50'"
|
||||
style="color: rgb(var(--color-text));" @click="activeTab = 'reference'">
|
||||
<i class="fa-solid fa-ruler-combined mr-1"></i> Ref
|
||||
<i class="fa-solid fa-cog mr-1"></i> Config
|
||||
</button>
|
||||
<button class="flex-1 px-3 py-2 text-sm font-medium rounded-md transition-all duration-200"
|
||||
:class="activeTab === 'valueset' ? 'bg-emerald-500 text-white shadow' : 'hover:bg-opacity-50'"
|
||||
@ -197,26 +192,29 @@
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.IndentLeft"
|
||||
placeholder="0" />
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<label class="label cursor-pointer justify-start gap-2">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.CountStat" :true-value="1"
|
||||
:false-value="0" />
|
||||
<span class="label-text text-sm">Count in Statistics</span>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Options</span>
|
||||
</label>
|
||||
<div class="flex flex-wrap gap-3 mt-1">
|
||||
<label class="label cursor-pointer justify-start gap-1 px-0">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.CountStat" :true-value="1"
|
||||
:false-value="0" />
|
||||
<span class="label-text text-xs">Count Stat</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer justify-start gap-1 px-0">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.VisibleScr" :true-value="1"
|
||||
:false-value="0" />
|
||||
<span class="label-text text-xs">Screen</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer justify-start gap-1 px-0">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.VisibleRpt" :true-value="1"
|
||||
:false-value="0" />
|
||||
<span class="label-text text-xs">Report</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4 mt-3">
|
||||
<label class="label cursor-pointer justify-start gap-2">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.VisibleScr" :true-value="1"
|
||||
:false-value="0" />
|
||||
<span class="label-text text-sm">Visible on Screen</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer justify-start gap-2">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.VisibleRpt" :true-value="1"
|
||||
:false-value="0" />
|
||||
<span class="label-text text-sm">Visible on Report</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -244,19 +242,6 @@
|
||||
<option value="DTTM">Date/Time</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Reference Type</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" x-model="form.RefType">
|
||||
<option value="">Select Reference Type</option>
|
||||
<option value="NMRC">Numeric Range</option>
|
||||
<option value="TEXT">Text Reference</option>
|
||||
<option value="AGE">Age-based</option>
|
||||
<option value="GENDER">Gender-based</option>
|
||||
<option value="NONE">No Reference</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Unit 1</span>
|
||||
@ -288,79 +273,63 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sample & Method -->
|
||||
<div>
|
||||
<!-- Reference Range Section -->
|
||||
<div class="mb-4">
|
||||
<h4 class="font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-vial text-emerald-500"></i> Sample & Method
|
||||
<i class="fa-solid fa-ruler-combined text-emerald-500"></i> Reference Range
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid grid-cols-4 gap-3">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Sample Type</span>
|
||||
<span class="label-text font-medium text-sm">Reference Type</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" x-model="form.SampleType">
|
||||
<option value="">Select Sample Type</option>
|
||||
<option value="SERUM">Serum</option>
|
||||
<option value="PLASMA">Plasma</option>
|
||||
<option value="BLOOD">Whole Blood</option>
|
||||
<option value="URINE">Urine</option>
|
||||
<option value="CSF">CSF</option>
|
||||
<option value="OTHER">Other</option>
|
||||
<select class="select select-bordered w-full" x-model="form.RefType">
|
||||
<option value="">Select Reference Type</option>
|
||||
<option value="NMRC">Numeric Range</option>
|
||||
<option value="TEXT">Text Reference</option>
|
||||
<option value="AGE">Age-based</option>
|
||||
<option value="GENDER">Gender-based</option>
|
||||
<option value="NONE">No Reference</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Numeric Reference Fields -->
|
||||
<div class="grid grid-cols-4 gap-3 mt-3" x-show="form.RefType === 'NMRC' || form.RefType === 'AGE' || form.RefType === 'GENDER'">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Method</span>
|
||||
<span class="label-text font-medium text-sm">Min Normal</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" x-model="form.Method"
|
||||
placeholder="e.g., Automated Cell Counter" />
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.RefMin" placeholder="0" step="any" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Max Normal</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.RefMax" placeholder="100" step="any" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Critical Low</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.CriticalLow" placeholder="Optional" step="any" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Critical High</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.CriticalHigh" placeholder="Optional" step="any" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- Text Reference Field -->
|
||||
<div class="mt-3" x-show="form.RefType === 'TEXT'">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Reference Text</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" x-model="form.RefText" placeholder="e.g., Negative, Positive, etc." />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Reference Range -->
|
||||
<div x-show="activeTab === 'reference'" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform -translate-x-4"
|
||||
x-transition:enter-end="opacity-100 transform translate-x-0">
|
||||
<div class="grid grid-cols-4 gap-3">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Min Normal</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.RefMin" placeholder="0"
|
||||
step="any" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Max Normal</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.RefMax" placeholder="100"
|
||||
step="any" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Critical Low</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.CriticalLow"
|
||||
placeholder="Alert low value" step="any" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Critical High</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.CriticalHigh"
|
||||
placeholder="Alert high value" step="any" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Reference Text (for text-based reference)</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" x-model="form.RefText"
|
||||
placeholder="e.g., Negative/Positive, Normal/Abnormal" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Value Set Selection -->
|
||||
<div x-show="activeTab === 'valueset'" x-transition:enter="transition ease-out duration-200"
|
||||
|
||||
@ -46,12 +46,7 @@
|
||||
<button class="flex-1 px-3 py-2 text-sm font-medium rounded-md transition-all duration-200"
|
||||
:class="activeTab === 'results' ? 'bg-indigo-500 text-white shadow' : 'hover:bg-opacity-50'"
|
||||
style="color: rgb(var(--color-text));" @click="activeTab = 'results'">
|
||||
<i class="fa-solid fa-flask mr-1"></i> Results
|
||||
</button>
|
||||
<button class="flex-1 px-3 py-2 text-sm font-medium rounded-md transition-all duration-200"
|
||||
:class="activeTab === 'reference' ? 'bg-indigo-500 text-white shadow' : 'hover:bg-opacity-50'"
|
||||
style="color: rgb(var(--color-text));" @click="activeTab = 'reference'">
|
||||
<i class="fa-solid fa-ruler-combined mr-1"></i> Ref
|
||||
<i class="fa-solid fa-cog mr-1"></i> Config
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -192,26 +187,29 @@
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.IndentLeft"
|
||||
placeholder="0" />
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<label class="label cursor-pointer justify-start gap-2">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.CountStat" :true-value="1"
|
||||
:false-value="0" />
|
||||
<span class="label-text text-sm">Count in Statistics</span>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Options</span>
|
||||
</label>
|
||||
<div class="flex flex-wrap gap-3 mt-1">
|
||||
<label class="label cursor-pointer justify-start gap-1 px-0">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.CountStat" :true-value="1"
|
||||
:false-value="0" />
|
||||
<span class="label-text text-xs">Count Stat</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer justify-start gap-1 px-0">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.VisibleScr" :true-value="1"
|
||||
:false-value="0" />
|
||||
<span class="label-text text-xs">Screen</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer justify-start gap-1 px-0">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.VisibleRpt" :true-value="1"
|
||||
:false-value="0" />
|
||||
<span class="label-text text-xs">Report</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4 mt-3">
|
||||
<label class="label cursor-pointer justify-start gap-2">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.VisibleScr" :true-value="1"
|
||||
:false-value="0" />
|
||||
<span class="label-text text-sm">Visible on Screen</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer justify-start gap-2">
|
||||
<input type="checkbox" class="checkbox checkbox-sm" x-model="form.VisibleRpt" :true-value="1"
|
||||
:false-value="0" />
|
||||
<span class="label-text text-sm">Visible on Report</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -240,19 +238,6 @@
|
||||
<option value="DTTM">Date/Time</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Reference Type</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" x-model="form.RefType">
|
||||
<option value="">Select Reference Type</option>
|
||||
<option value="NMRC">Numeric Range</option>
|
||||
<option value="TEXT">Text Reference</option>
|
||||
<option value="AGE">Age-based</option>
|
||||
<option value="GENDER">Gender-based</option>
|
||||
<option value="NONE">No Reference</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Unit 1</span>
|
||||
@ -284,79 +269,63 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sample & Method -->
|
||||
<div>
|
||||
<!-- Reference Range Section -->
|
||||
<div class="mb-4">
|
||||
<h4 class="font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<i class="fa-solid fa-vial text-indigo-500"></i> Sample & Method
|
||||
<i class="fa-solid fa-ruler-combined text-indigo-500"></i> Reference Range
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid grid-cols-4 gap-3">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Sample Type</span>
|
||||
<span class="label-text font-medium text-sm">Reference Type</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" x-model="form.SampleType">
|
||||
<option value="">Select Sample Type</option>
|
||||
<option value="SERUM">Serum</option>
|
||||
<option value="PLASMA">Plasma</option>
|
||||
<option value="BLOOD">Whole Blood</option>
|
||||
<option value="URINE">Urine</option>
|
||||
<option value="CSF">CSF</option>
|
||||
<option value="OTHER">Other</option>
|
||||
<select class="select select-bordered w-full" x-model="form.RefType">
|
||||
<option value="">Select Reference Type</option>
|
||||
<option value="NMRC">Numeric Range</option>
|
||||
<option value="TEXT">Text Reference</option>
|
||||
<option value="AGE">Age-based</option>
|
||||
<option value="GENDER">Gender-based</option>
|
||||
<option value="NONE">No Reference</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Numeric Reference Fields -->
|
||||
<div class="grid grid-cols-4 gap-3 mt-3" x-show="form.RefType === 'NMRC' || form.RefType === 'AGE' || form.RefType === 'GENDER'">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Method</span>
|
||||
<span class="label-text font-medium text-sm">Min Normal</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" x-model="form.Method"
|
||||
placeholder="e.g., CBC Analyzer, Hexokinase" />
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.RefMin" placeholder="0" step="any" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Max Normal</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.RefMax" placeholder="100" step="any" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Critical Low</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.CriticalLow" placeholder="Optional" step="any" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Critical High</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.CriticalHigh" placeholder="Optional" step="any" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- Text Reference Field -->
|
||||
<div class="mt-3" x-show="form.RefType === 'TEXT'">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Reference Text</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" x-model="form.RefText" placeholder="e.g., Negative, Positive, etc." />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Reference Range -->
|
||||
<div x-show="activeTab === 'reference'" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform -translate-x-4"
|
||||
x-transition:enter-end="opacity-100 transform translate-x-0">
|
||||
<div class="grid grid-cols-4 gap-3">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Min Normal</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.RefMin" placeholder="0"
|
||||
step="any" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Max Normal</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.RefMax" placeholder="100"
|
||||
step="any" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Critical Low</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.CriticalLow"
|
||||
placeholder="Optional" step="any" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Critical High</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered w-full" x-model.number="form.CriticalHigh"
|
||||
placeholder="Optional" step="any" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium text-sm">Reference Text</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" x-model="form.RefText"
|
||||
placeholder="e.g., 70-100 mg/dL (for text reference type)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user