feat(tests): improve calc member handling and update project docs
This commit is contained in:
parent
22ee1ebfd1
commit
39cdbb0464
1
.serena/.gitignore
vendored
1
.serena/.gitignore
vendored
@ -1 +1,2 @@
|
|||||||
/cache
|
/cache
|
||||||
|
/project.local.yml
|
||||||
|
|||||||
@ -1,189 +0,0 @@
|
|||||||
# CLQMS Frontend - Code Style & Conventions
|
|
||||||
|
|
||||||
## File Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
src/
|
|
||||||
lib/
|
|
||||||
api/ # API client and endpoints (per feature)
|
|
||||||
stores/ # Svelte stores (auth, config, valuesets)
|
|
||||||
components/ # Reusable components (DataTable, Modal, Sidebar)
|
|
||||||
utils/ # Utility functions (toast, helpers)
|
|
||||||
assets/ # Static assets (favicon, etc.)
|
|
||||||
routes/ # SvelteKit routes
|
|
||||||
(app)/ # Protected routes (authenticated users)
|
|
||||||
dashboard/
|
|
||||||
patients/
|
|
||||||
master-data/
|
|
||||||
login/ # Public routes
|
|
||||||
```
|
|
||||||
|
|
||||||
## Svelte Component Conventions
|
|
||||||
|
|
||||||
### Component Structure Order
|
|
||||||
1. Imports (Svelte, $app, $lib, external)
|
|
||||||
2. Props with `$bindable` for two-way binding
|
|
||||||
3. State (`$state`)
|
|
||||||
4. Derived state (`$derived`)
|
|
||||||
5. Effects (`$effect`)
|
|
||||||
6. Functions (prefix handlers with `handle`)
|
|
||||||
|
|
||||||
### Props and State
|
|
||||||
```svelte
|
|
||||||
let { open = $bindable(false), title = '', children, footer } = $props();
|
|
||||||
let loading = $state(false);
|
|
||||||
let error = $state('');
|
|
||||||
```
|
|
||||||
|
|
||||||
### Imports Order
|
|
||||||
1. Svelte imports (`svelte`, `$app/*`)
|
|
||||||
2. $lib aliases (`$lib/stores/*`, `$lib/api/*`, `$lib/components/*`, `$lib/utils/*`)
|
|
||||||
3. External libraries (`lucide-svelte`)
|
|
||||||
4. Relative imports (minimize, prefer `$lib`)
|
|
||||||
|
|
||||||
## Naming Conventions
|
|
||||||
|
|
||||||
- **Components**: PascalCase (`LoginForm.svelte`, `PatientFormModal.svelte`)
|
|
||||||
- **Files/Routes**: lowercase with hyphens (`+page.svelte`, `user-profile/`)
|
|
||||||
- **Variables**: camelCase (`isLoading`, `userName`)
|
|
||||||
- **Constants**: UPPER_SNAKE_CASE (`API_URL`, `STORAGE_KEY`)
|
|
||||||
- **Stores**: camelCase, descriptive (`auth`, `userStore`)
|
|
||||||
- **Event handlers**: prefix with `handle` (`handleSubmit`, `handleClick`)
|
|
||||||
- **Form state**: `formLoading`, `formError`, `deleteConfirmOpen`
|
|
||||||
|
|
||||||
## Code Style
|
|
||||||
|
|
||||||
- **ES Modules**: Always use `import`/`export`
|
|
||||||
- **Semicolons**: Use consistently
|
|
||||||
- **Quotes**: Use single quotes for strings
|
|
||||||
- **Indentation**: 2 spaces
|
|
||||||
- **Trailing commas**: Use in multi-line objects/arrays
|
|
||||||
- **JSDoc**: Document all exported functions with JSDoc comments
|
|
||||||
|
|
||||||
## Component Patterns
|
|
||||||
|
|
||||||
### Modal Pattern
|
|
||||||
```svelte
|
|
||||||
<dialog class="modal" class:modal-open={open}>
|
|
||||||
<div class="modal-box">
|
|
||||||
<button onclick={close}>X</button>
|
|
||||||
{@render children?.()}
|
|
||||||
</div>
|
|
||||||
<form method="dialog" class="modal-backdrop" onclick={handleBackdropClick}>
|
|
||||||
<button>close</button>
|
|
||||||
</form>
|
|
||||||
</dialog>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Form State with Validation
|
|
||||||
```javascript
|
|
||||||
let formLoading = $state(false);
|
|
||||||
let formError = $state('');
|
|
||||||
let formData = $state({ username: '', password: '' });
|
|
||||||
|
|
||||||
function validateForm() {
|
|
||||||
formError = '';
|
|
||||||
if (!formData.username.trim()) {
|
|
||||||
formError = 'Username is required';
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSubmit() {
|
|
||||||
if (!validateForm()) return;
|
|
||||||
formLoading = true;
|
|
||||||
try {
|
|
||||||
await api.submit(formData);
|
|
||||||
toastSuccess('Success');
|
|
||||||
} catch (err) {
|
|
||||||
formError = err.message;
|
|
||||||
} finally {
|
|
||||||
formLoading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## API Client Patterns
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// src/lib/api/client.js - Base client
|
|
||||||
import { apiClient, get, post, put, patch, del } from '$lib/api/client.js';
|
|
||||||
|
|
||||||
// src/lib/api/feature.js - Feature-specific endpoints with JSDoc
|
|
||||||
export async function fetchItems(params = {}) {
|
|
||||||
const query = new URLSearchParams(params).toString();
|
|
||||||
return get(query ? `/api/items?${query}` : '/api/items');
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createItem(data) {
|
|
||||||
return post('/api/items', data);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateItem(data) {
|
|
||||||
return patch('/api/items', data);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteItem(id) {
|
|
||||||
return del(`/api/items/${id}`);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Store Patterns
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Use writable for stores with localStorage persistence
|
|
||||||
import { writable } from 'svelte/store';
|
|
||||||
import { browser } from '$app/environment';
|
|
||||||
|
|
||||||
function createStore() {
|
|
||||||
const getInitialState = () => {
|
|
||||||
if (!browser) return { data: null };
|
|
||||||
return { data: JSON.parse(localStorage.getItem('key')) };
|
|
||||||
};
|
|
||||||
|
|
||||||
const { subscribe, set, update } = writable(getInitialState());
|
|
||||||
|
|
||||||
return {
|
|
||||||
subscribe,
|
|
||||||
setData: (data) => {
|
|
||||||
if (browser) localStorage.setItem('key', JSON.stringify(data));
|
|
||||||
set({ data });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
try {
|
|
||||||
const result = await api.fetchData();
|
|
||||||
toastSuccess('Operation successful');
|
|
||||||
} catch (err) {
|
|
||||||
const message = err.message || 'An unexpected error occurred';
|
|
||||||
toastError(message);
|
|
||||||
console.error('Operation failed:', err);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Styling with Tailwind & DaisyUI
|
|
||||||
|
|
||||||
- DaisyUI components: `btn`, `card`, `alert`, `input`, `navbar`, `modal`, `dropdown`, `menu`
|
|
||||||
- Color scheme: `primary` (emerald), `secondary` (dark blue), `accent` (royal blue)
|
|
||||||
- Custom compact classes: `.compact-y`, `.compact-p`, `.compact-input`, `.compact-btn`, `.compact-card`
|
|
||||||
- Size modifiers: `.btn-sm`, `.input-sm`, `.select-sm` for compact forms
|
|
||||||
|
|
||||||
## LocalStorage
|
|
||||||
|
|
||||||
- Only access in browser: check `browser` from `$app/environment`
|
|
||||||
- Use descriptive keys: `clqms_username`, `clqms_remember`, `auth_token`
|
|
||||||
|
|
||||||
## Runtime Config Pattern
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { config } from '$lib/stores/config.js';
|
|
||||||
function getApiUrl() {
|
|
||||||
return config.getApiUrl() || import.meta.env.VITE_API_URL || '';
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@ -1,294 +0,0 @@
|
|||||||
# CLQMS MVP Roadmap
|
|
||||||
|
|
||||||
## Executive Summary
|
|
||||||
|
|
||||||
**CLQMS (Clinical Laboratory Quality Management System)** - A comprehensive laboratory information system for clinical diagnostics.
|
|
||||||
|
|
||||||
### Current State
|
|
||||||
- ✅ Authentication & authorization
|
|
||||||
- ✅ Patient management (CRUD)
|
|
||||||
- ✅ Visit management with ADT tracking
|
|
||||||
- ✅ Order entry (create orders with specimens and tests)
|
|
||||||
- ✅ Master data management (contacts, locations, containers, organization, tests)
|
|
||||||
- ⚠️ Dashboard (static mock data)
|
|
||||||
- ❌ **Results management** (CRITICAL GAP)
|
|
||||||
- ❌ **Report viewing** (CRITICAL GAP)
|
|
||||||
|
|
||||||
### MVP Goal
|
|
||||||
Enable end-to-end laboratory workflow: Patient → Visit → Order → Results → Report
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 1: Core Laboratory Workflow (CRITICAL - Weeks 1-3)
|
|
||||||
|
|
||||||
### 1.1 Results Management System
|
|
||||||
**Priority: CRITICAL** - Without results, this is not a laboratory system
|
|
||||||
|
|
||||||
#### API Endpoints Available:
|
|
||||||
- `GET /api/results` - List results (with filters by order_id, patient_id)
|
|
||||||
- `GET /api/results/{id}` - Get single result detail
|
|
||||||
- `PATCH /api/results/{id}` - Update result with auto-validation
|
|
||||||
- `DELETE /api/results/{id}` - Soft delete result
|
|
||||||
|
|
||||||
#### Features:
|
|
||||||
1. **Results Entry Page**
|
|
||||||
- View results by order or patient
|
|
||||||
- Batch entry mode for efficient data input
|
|
||||||
- Auto-validation against reference ranges (L/H flags)
|
|
||||||
- Support for different result types: NMRIC, RANGE, TEXT, VSET
|
|
||||||
|
|
||||||
2. **Results Review & Verification**
|
|
||||||
- List results pending verification
|
|
||||||
- Bulk verify results
|
|
||||||
- Critical/high-low flag highlighting
|
|
||||||
|
|
||||||
3. **Results History**
|
|
||||||
- Cumulative patient results view
|
|
||||||
- Trend charts for numeric results
|
|
||||||
- Delta checking (compare with previous results)
|
|
||||||
|
|
||||||
#### UI Components Needed:
|
|
||||||
- ResultsEntryPage.svelte
|
|
||||||
- ResultInputModal.svelte (handles different result types)
|
|
||||||
- ResultsByOrderView.svelte
|
|
||||||
- ResultsByPatientView.svelte
|
|
||||||
- CriticalResultsAlert.svelte
|
|
||||||
|
|
||||||
#### API Client:
|
|
||||||
- `src/lib/api/results.js` (NEW)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 1.2 Report Generation & Viewing
|
|
||||||
**Priority: CRITICAL** - Final output of laboratory work
|
|
||||||
|
|
||||||
#### API Endpoints Available:
|
|
||||||
- `GET /api/reports/{orderID}` - Generate HTML lab report
|
|
||||||
|
|
||||||
#### Features:
|
|
||||||
1. **Report Viewer**
|
|
||||||
- View HTML reports in browser
|
|
||||||
- Print-friendly layout
|
|
||||||
- PDF export capability (browser print to PDF)
|
|
||||||
|
|
||||||
2. **Report Status Tracking**
|
|
||||||
- Track order status: ORD → SCH → ANA → VER → REV → REP
|
|
||||||
- Visual status indicators
|
|
||||||
|
|
||||||
#### UI Components Needed:
|
|
||||||
- ReportViewerModal.svelte
|
|
||||||
- ReportPrintView.svelte
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 1.3 Live Dashboard
|
|
||||||
**Priority: HIGH** - Replace static data with real metrics
|
|
||||||
|
|
||||||
#### Features:
|
|
||||||
1. **Real-time Metrics**
|
|
||||||
- Pending orders count (from orders API)
|
|
||||||
- Today's results count (from results API)
|
|
||||||
- Critical results count (filter results by flag)
|
|
||||||
- Active patients count (from patients API)
|
|
||||||
|
|
||||||
2. **Activity Feed**
|
|
||||||
- Recent orders created
|
|
||||||
- Recent results received
|
|
||||||
- Recent patients registered
|
|
||||||
|
|
||||||
#### Implementation:
|
|
||||||
- Update `src/routes/(app)/dashboard/+page.svelte`
|
|
||||||
- Add dashboard API endpoint if needed
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 2: Order Management Enhancement (Weeks 4-5)
|
|
||||||
|
|
||||||
### 2.1 Order Workflow Management
|
|
||||||
**Priority: HIGH**
|
|
||||||
|
|
||||||
#### Features:
|
|
||||||
1. **Order Status Tracking**
|
|
||||||
- Visual pipeline: Ordered → Scheduled → Analysis → Verified → Reviewed → Reported
|
|
||||||
- Bulk status updates
|
|
||||||
|
|
||||||
2. **Order Details Enhancement**
|
|
||||||
- View specimens associated with order
|
|
||||||
- View test results within order
|
|
||||||
- Direct link to results entry from order
|
|
||||||
|
|
||||||
#### API Endpoints:
|
|
||||||
- `POST /api/ordertest/status` - Update order status (already available)
|
|
||||||
- `GET /api/ordertest/{id}?include=details` - Get order with specimens/tests
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2.2 Specimen Tracking
|
|
||||||
**Priority: MEDIUM**
|
|
||||||
|
|
||||||
#### Features:
|
|
||||||
1. **Specimen Management**
|
|
||||||
- View specimens by order
|
|
||||||
- Update specimen status (collected, received, processed)
|
|
||||||
- Print specimen labels
|
|
||||||
|
|
||||||
#### API Endpoints Available:
|
|
||||||
- `GET /api/specimen` - List specimens
|
|
||||||
- `GET /api/specimen/{id}` - Get specimen details
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 3: Master Data & Configuration (Week 6)
|
|
||||||
|
|
||||||
### 3.1 Test Management Enhancement
|
|
||||||
**Priority: MEDIUM**
|
|
||||||
|
|
||||||
#### Features:
|
|
||||||
1. **Test Catalog Browser**
|
|
||||||
- Improved search and filtering
|
|
||||||
- View test details with reference ranges
|
|
||||||
- Test-to-container mappings
|
|
||||||
|
|
||||||
#### Already Implemented:
|
|
||||||
- Tests CRUD in master-data/tests
|
|
||||||
- Test mappings in master-data/testmap
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3.2 Reference Range Management
|
|
||||||
**Priority: LOW**
|
|
||||||
|
|
||||||
#### Features:
|
|
||||||
1. **Reference Range Setup**
|
|
||||||
- Manage numeric reference ranges (refnum)
|
|
||||||
- Manage text reference values (reftxt)
|
|
||||||
- Age and sex-specific ranges
|
|
||||||
|
|
||||||
#### API Note:
|
|
||||||
Reference ranges are managed through test definition endpoints
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 4: Quality & Compliance Features (Weeks 7-8)
|
|
||||||
|
|
||||||
### 4.1 Critical Results Management
|
|
||||||
**Priority: MEDIUM**
|
|
||||||
|
|
||||||
#### Features:
|
|
||||||
1. **Critical Results Alerting**
|
|
||||||
- Real-time critical result notifications
|
|
||||||
- Acknowledgment tracking
|
|
||||||
- Escalation workflow
|
|
||||||
|
|
||||||
#### API Endpoints:
|
|
||||||
- Use results API with flag filtering
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4.2 Audit Trail
|
|
||||||
**Priority: LOW**
|
|
||||||
|
|
||||||
#### Features:
|
|
||||||
1. **Activity Logging**
|
|
||||||
- Track who modified results
|
|
||||||
- Track order status changes
|
|
||||||
- Patient data access logs
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Implementation Priority Matrix
|
|
||||||
|
|
||||||
| Feature | Priority | Effort | Impact | Phase |
|
|
||||||
|---------|----------|--------|--------|-------|
|
|
||||||
| Results Entry | CRITICAL | High | Critical | Phase 1 |
|
|
||||||
| Results Verification | CRITICAL | Medium | Critical | Phase 1 |
|
|
||||||
| Report Viewing | CRITICAL | Low | Critical | Phase 1 |
|
|
||||||
| Live Dashboard | HIGH | Medium | High | Phase 1 |
|
|
||||||
| Order Workflow | HIGH | Medium | High | Phase 2 |
|
|
||||||
| Specimen Tracking | MEDIUM | Medium | Medium | Phase 2 |
|
|
||||||
| Critical Results | MEDIUM | Medium | Medium | Phase 4 |
|
|
||||||
| Reference Ranges | LOW | Medium | Low | Phase 3 |
|
|
||||||
| Audit Trail | LOW | High | Low | Phase 4 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Technical Implementation Notes
|
|
||||||
|
|
||||||
### API Client Pattern
|
|
||||||
Following existing patterns in `src/lib/api/`:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// src/lib/api/results.js
|
|
||||||
import { get, patch, del } from './client.js';
|
|
||||||
|
|
||||||
export async function fetchResults(params = {}) {
|
|
||||||
const query = new URLSearchParams(params).toString();
|
|
||||||
return get(query ? `/api/results?${query}` : '/api/results');
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchResultById(id) {
|
|
||||||
return get(`/api/results/${id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateResult(id, data) {
|
|
||||||
return patch(`/api/results/${id}`, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteResult(id) {
|
|
||||||
return del(`/api/results/${id}`);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Route Structure
|
|
||||||
```
|
|
||||||
src/routes/(app)/
|
|
||||||
├── dashboard/ # Update with live data
|
|
||||||
├── patients/ # ✅ Exists
|
|
||||||
├── visits/ # ✅ Exists
|
|
||||||
├── orders/ # ✅ Exists (enhance)
|
|
||||||
├── results/ # NEW - Results management
|
|
||||||
│ ├── +page.svelte
|
|
||||||
│ ├── entry/
|
|
||||||
│ │ └── +page.svelte
|
|
||||||
│ └── verification/
|
|
||||||
│ └── +page.svelte
|
|
||||||
└── reports/ # NEW - Report viewing
|
|
||||||
└── +page.svelte
|
|
||||||
```
|
|
||||||
|
|
||||||
### Database Schema Relationships
|
|
||||||
```
|
|
||||||
Patient (1) → (N) Visits
|
|
||||||
Patient (1) → (N) Orders
|
|
||||||
Visit (1) → (N) Orders
|
|
||||||
Visit (1) → (N) ADT Records
|
|
||||||
Order (1) → (N) Specimens
|
|
||||||
Order (1) → (N) Results
|
|
||||||
Order (1) → (N) PatRes (test records)
|
|
||||||
Specimen (1) → (N) Results
|
|
||||||
Test Definition (1) → (N) Results
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Success Criteria for MVP
|
|
||||||
|
|
||||||
1. **End-to-end workflow works**: Can register patient → create visit → create order → enter results → view report
|
|
||||||
2. **Results validation**: System correctly flags high/low results based on reference ranges
|
|
||||||
3. **Order tracking**: Can track order status through the pipeline
|
|
||||||
4. **Dashboard**: Shows real metrics, not static data
|
|
||||||
5. **Reports**: Can generate and view HTML reports for orders
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Future Enhancements (Post-MVP)
|
|
||||||
|
|
||||||
1. **Instrument Integration** - Edge API for automated result import
|
|
||||||
2. **Barcoding/Labeling** - Specimen label printing
|
|
||||||
3. **QC Management** - Quality control charts and rules
|
|
||||||
4. **Billing Integration** - Connect to billing system
|
|
||||||
5. **External Lab Interface** - Send orders to reference labs
|
|
||||||
6. **Mobile App** - Phlebotomy collection app
|
|
||||||
7. **Patient Portal** - Patients can view their results
|
|
||||||
8. **Analytics** - Advanced reporting and statistics
|
|
||||||
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
# Plan: Change ID Text Inputs to Dropdowns
|
|
||||||
|
|
||||||
## Current State
|
|
||||||
In `src\routes\(app)\master-data\testmap\+page.svelte`, the filter section has:
|
|
||||||
- Host ID filter (lines 239-244): text input
|
|
||||||
- Client ID filter (lines 260-265): text input
|
|
||||||
|
|
||||||
## Changes Required
|
|
||||||
|
|
||||||
### 1. Add Derived State for Dropdown Options
|
|
||||||
Add after line 41 (after filter states):
|
|
||||||
```javascript
|
|
||||||
// Derived unique values for dropdowns
|
|
||||||
let uniqueHostIDs = $derived([...new Set(testMaps.map(m => m.HostID).filter(Boolean))].sort());
|
|
||||||
let uniqueClientIDs = $derived([...new Set(testMaps.map(m => m.ClientID).filter(Boolean))].sort());
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Replace Host ID Input with Dropdown
|
|
||||||
Replace lines 239-244 with:
|
|
||||||
```svelte
|
|
||||||
<select
|
|
||||||
class="select select-sm select-bordered w-full"
|
|
||||||
bind:value={filterHostID}
|
|
||||||
>
|
|
||||||
<option value="">All IDs</option>
|
|
||||||
{#each uniqueHostIDs as id}
|
|
||||||
<option value={id}>{id}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Replace Client ID Input with Dropdown
|
|
||||||
Replace lines 260-265 with:
|
|
||||||
```svelte
|
|
||||||
<select
|
|
||||||
class="select select-sm select-bordered w-full"
|
|
||||||
bind:value={filterClientID}
|
|
||||||
>
|
|
||||||
<option value="">All IDs</option>
|
|
||||||
{#each uniqueClientIDs as id}
|
|
||||||
<option value={id}>{id}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Benefits
|
|
||||||
- Users can select from existing IDs rather than typing
|
|
||||||
- Prevents typos in filter values
|
|
||||||
- Shows all available options at a glance
|
|
||||||
- Maintains exact matching instead of substring matching for IDs
|
|
||||||
|
|
||||||
Ready to implement?
|
|
||||||
@ -1,62 +1,29 @@
|
|||||||
# CLQMS Frontend - Project Overview
|
# CLQMS Frontend Project Overview
|
||||||
|
|
||||||
## Project Purpose
|
- Purpose: Frontend for Clinical Laboratory Quality Management System (CLQMS), handling authenticated lab quality workflows.
|
||||||
CLQMS (Clinical Laboratory Quality Management System) frontend built with SvelteKit. This is a web application for managing clinical laboratory operations including patients, specimens, test orders, results, and laboratory workflows.
|
- App type: SvelteKit SPA/static frontend that talks to backend API.
|
||||||
|
- Backend dependency: API expected at `http://localhost:8000` in development; `/api` is proxied in Vite.
|
||||||
|
- Auth model: JWT-based auth with automatic redirect to `/login` on 401.
|
||||||
|
- Main docs: `README.md`, `AGENTS.md`, `DEPLOY.md`.
|
||||||
|
|
||||||
## Tech Stack
|
## Tech stack
|
||||||
- **Framework**: SvelteKit (latest with Svelte 5 runes)
|
|
||||||
- **Styling**: TailwindCSS 4 + DaisyUI
|
|
||||||
- **Icons**: Lucide Svelte
|
|
||||||
- **Language**: JavaScript (no TypeScript - KISS principle)
|
|
||||||
- **Build Tool**: Vite
|
|
||||||
- **Package Manager**: pnpm
|
|
||||||
- **Authentication**: JWT tokens with HTTP-only cookies
|
|
||||||
|
|
||||||
## Architecture Principles
|
- SvelteKit `^2.50.2`
|
||||||
- **KISS**: Keep It Simple - plain JavaScript, minimal tooling
|
- Svelte `^5.49.2` with runes (`$props`, `$state`, `$derived`, `$effect`, `$bindable`)
|
||||||
- **Manual API Wrapper**: No codegen, simple fetch-based API client
|
- Vite `^7.3.1`
|
||||||
- **File-based Routing**: Standard SvelteKit routing patterns
|
- Tailwind CSS 4 + DaisyUI 5
|
||||||
- **Server-side Auth Check**: SvelteKit hooks for session validation
|
- Lucide Svelte icons
|
||||||
|
- Package manager: pnpm
|
||||||
|
- Module system: ES Modules (`"type": "module"`)
|
||||||
|
|
||||||
## Project Structure
|
## Rough structure
|
||||||
```
|
|
||||||
src/
|
|
||||||
├── lib/
|
|
||||||
│ ├── api/ # API service functions (client.js, auth.js, valuesets.js, etc.)
|
|
||||||
│ ├── components/ # Reusable Svelte components (DataTable, Modal, SelectDropdown, Sidebar, etc.)
|
|
||||||
│ ├── stores/ # Svelte stores (auth.js, valuesets.js)
|
|
||||||
│ └── utils/ # Utility functions (toast.js)
|
|
||||||
├── routes/ # SvelteKit routes
|
|
||||||
│ ├── +layout.svelte # Root layout
|
|
||||||
│ ├── login/ # Login page
|
|
||||||
│ └── (app)/ # Protected route group
|
|
||||||
│ ├── +layout.svelte # Auth check with sidebar
|
|
||||||
│ ├── dashboard/
|
|
||||||
│ ├── master-data/ # Locations, contacts, specialties, valuesets, geography, occupations, counters
|
|
||||||
│ └── patients/
|
|
||||||
├── app.css # Global styles with Tailwind
|
|
||||||
└── app.html # HTML template
|
|
||||||
```
|
|
||||||
|
|
||||||
## Key Features Implemented
|
- `src/lib/api/` API client and feature endpoints
|
||||||
- Phase 0: Foundation (Auth, base API client, layouts) ✅
|
- `src/lib/stores/` shared stores (auth, config, valuesets)
|
||||||
- Phase 1: Foundation Data (ValueSets, Locations, Contacts, Occupations, Specialties, Counters, Geography) ✅
|
- `src/lib/components/` reusable UI (Modal, DataTable, Sidebar)
|
||||||
- Phase 2a: Patient CRUD ✅
|
- `src/lib/utils/` helpers and toast utilities
|
||||||
- Phase 11: Authentication (login/logout, JWT handling) ✅
|
- `src/lib/types/` TS type definitions
|
||||||
|
- `src/routes/(app)/` authenticated pages (`dashboard`, `patients`, `master-data`)
|
||||||
## Pending Features
|
- `src/routes/login/` public login route
|
||||||
- Phase 2b: Advanced Patient Features
|
- `static/` static assets
|
||||||
- Phase 3: Patient Visits (list page exists, needs create/edit forms)
|
- `build/` production output
|
||||||
- Phase 4: Specimen Management
|
|
||||||
- Phase 5: Test Catalog
|
|
||||||
- Phase 6: Orders
|
|
||||||
- Phase 7: Results & Dashboard
|
|
||||||
- Phase 8: User-defined ValueSets
|
|
||||||
- Phase 9: Organization Structure
|
|
||||||
- Phase 10: Edge API (Instrument Integration)
|
|
||||||
|
|
||||||
## API Proxy Configuration
|
|
||||||
API requests to `/api` are proxied to `http://localhost:8000` in development mode (configured in vite.config.js).
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
- `VITE_API_URL`: Base URL for API (default: empty string, uses proxy in dev)
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
# Patient Page Refactoring
|
|
||||||
|
|
||||||
## Current Architecture Issues
|
|
||||||
- File: `src/routes/(app)/patients/+page.svelte` (543 lines)
|
|
||||||
- Mixes patient CRUD + visit management + ADT history in one file
|
|
||||||
- Inline visit card rendering (~100 lines)
|
|
||||||
- Helper functions at bottom (formatDate, formatDateTime)
|
|
||||||
- Patient name formatting repeated 3+ times inline
|
|
||||||
|
|
||||||
## Refactoring Plan
|
|
||||||
1. Extract VisitCard.svelte component
|
|
||||||
2. Create patientUtils.js for shared helpers
|
|
||||||
3. Group modal states into objects (patientModals, visitModals)
|
|
||||||
4. Use $derived for computed values (patientFullName, formattedVisits)
|
|
||||||
5. Add JSDoc types for Patient and Visit
|
|
||||||
|
|
||||||
## Related Files
|
|
||||||
- PatientFormModal.svelte
|
|
||||||
- VisitFormModal.svelte
|
|
||||||
- VisitADTHistoryModal.svelte
|
|
||||||
- $lib/api/patients.js
|
|
||||||
- $lib/api/visits.js
|
|
||||||
@ -1,156 +1,42 @@
|
|||||||
# CLQMS Frontend - Code Style and Conventions
|
# Style and Conventions
|
||||||
|
|
||||||
## JavaScript/TypeScript
|
Primary source: `AGENTS.md`.
|
||||||
- **Language**: Plain JavaScript (no TypeScript)
|
|
||||||
- **Modules**: Always use `import`/`export` (type: "module" in package.json)
|
|
||||||
- **Semicolons**: Use semicolons consistently
|
|
||||||
- **Quotes**: Use single quotes for strings
|
|
||||||
- **Indentation**: 2 spaces
|
|
||||||
- **Trailing commas**: Use in multi-line objects/arrays
|
|
||||||
- **JSDoc**: Document all exported functions with JSDoc comments
|
|
||||||
|
|
||||||
## Svelte Components (Svelte 5 Runes)
|
## JavaScript/TypeScript style
|
||||||
|
|
||||||
### Component Structure
|
- Use ES modules (`import`/`export`).
|
||||||
```svelte
|
- Semicolons required.
|
||||||
<script>
|
- Single quotes for strings.
|
||||||
// 1. Imports first - group by: Svelte, $lib, external
|
- 2-space indentation.
|
||||||
import { onMount } from 'svelte';
|
- Trailing commas in multi-line arrays/objects.
|
||||||
import { goto } from '$app/navigation';
|
- Document exported functions with JSDoc including `@param` and `@returns`.
|
||||||
import { auth } from '$lib/stores/auth.js';
|
|
||||||
import { login } from '$lib/api/auth.js';
|
|
||||||
import { Icon } from 'lucide-svelte';
|
|
||||||
|
|
||||||
// 2. Props (Svelte 5 runes)
|
## Import ordering
|
||||||
let { children, data } = $props();
|
|
||||||
|
|
||||||
// 3. State
|
1. Svelte / `$app/*`
|
||||||
let loading = $state(false);
|
2. `$lib/*`
|
||||||
let error = $state('');
|
3. External libraries (e.g., `lucide-svelte`)
|
||||||
|
4. Relative imports (minimize, prefer `$lib`)
|
||||||
|
|
||||||
// 4. Derived state (if needed)
|
## Naming
|
||||||
let isValid = $derived(username.length > 0);
|
|
||||||
|
|
||||||
// 5. Effects
|
- Components: PascalCase (`LoginForm.svelte`)
|
||||||
$effect(() => {
|
- Route/files: lowercase with hyphens
|
||||||
// side effects
|
- Variables/stores: camelCase
|
||||||
});
|
- Constants: UPPER_SNAKE_CASE
|
||||||
|
- Event handlers: `handle...`
|
||||||
|
- Form state fields: `formLoading`, `formError`, etc.
|
||||||
|
|
||||||
// 6. Functions
|
## Svelte 5 patterns
|
||||||
function handleSubmit() {
|
|
||||||
// implementation
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Naming Conventions
|
- Follow component script order: imports -> props -> state -> derived -> effects -> handlers.
|
||||||
- **Components**: PascalCase (`LoginForm.svelte`, `DataTable.svelte`)
|
- Prefer DaisyUI component classes (`btn`, `input`, `card`, etc.).
|
||||||
- **Files/Routes**: lowercase with hyphens (`+page.svelte`, `user-profile/`)
|
- For icon inputs, use DaisyUI label+input flex pattern (not absolute-positioned icons).
|
||||||
- **Variables**: camelCase (`isLoading`, `userName`)
|
- Access browser-only APIs behind `$app/environment` `browser` checks.
|
||||||
- **Constants**: UPPER_SNAKE_CASE (`API_URL`, `STORAGE_KEY`)
|
|
||||||
- **Stores**: camelCase, descriptive (`auth`, `valuesets`)
|
|
||||||
- **Event handlers**: prefix with `handle` (`handleSubmit`, `handleClick`)
|
|
||||||
|
|
||||||
## Imports Order
|
## API/store patterns
|
||||||
1. Svelte imports (`svelte`, `$app/*`)
|
|
||||||
2. $lib aliases (`$lib/stores/*`, `$lib/api/*`, `$lib/components/*`)
|
|
||||||
3. External libraries (`lucide-svelte`)
|
|
||||||
4. Relative imports (minimize these, prefer `$lib`)
|
|
||||||
|
|
||||||
## API Client Pattern
|
- Use shared API helpers from `$lib/api/client.js` (`get/post/put/patch/del`).
|
||||||
|
- Build query strings using `URLSearchParams`.
|
||||||
### Base Client (src/lib/api/client.js)
|
- Use try/catch with toast error/success utilities.
|
||||||
The base client handles JWT token management and 401 redirects automatically.
|
- LocalStorage keys should be descriptive (e.g., `clqms_username`, `auth_token`).
|
||||||
|
|
||||||
### Feature-Specific API Modules
|
|
||||||
```javascript
|
|
||||||
// src/lib/api/feature.js - Feature-specific endpoints
|
|
||||||
import { apiClient, get, post, put, patch, del } from '$lib/api/client.js';
|
|
||||||
|
|
||||||
export async function getItem(id) {
|
|
||||||
return get(`/api/items/${id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createItem(data) {
|
|
||||||
return post('/api/items', data);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Form Handling Pattern
|
|
||||||
```javascript
|
|
||||||
let formData = { name: '', email: '' };
|
|
||||||
let errors = {};
|
|
||||||
let loading = false;
|
|
||||||
|
|
||||||
async function handleSubmit() {
|
|
||||||
loading = true;
|
|
||||||
errors = {};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await createItem(formData);
|
|
||||||
|
|
||||||
if (result.status === 'error') {
|
|
||||||
errors = result.errors || { general: result.message };
|
|
||||||
} else {
|
|
||||||
// Success - redirect or show message
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
errors = { general: err.message || 'An unexpected error occurred' };
|
|
||||||
}
|
|
||||||
|
|
||||||
loading = false;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
- API errors are thrown with message
|
|
||||||
- Use try/catch blocks for async operations
|
|
||||||
- Store errors in state for display
|
|
||||||
- Toast notifications for user feedback
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
try {
|
|
||||||
const result = await api.login(username, password);
|
|
||||||
toast.success('Login successful');
|
|
||||||
} catch (err) {
|
|
||||||
error = err.message || 'An unexpected error occurred';
|
|
||||||
console.error('Login failed:', err);
|
|
||||||
toast.error('Login failed');
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Styling with Tailwind & DaisyUI
|
|
||||||
- Use Tailwind utility classes
|
|
||||||
- DaisyUI components: `btn`, `card`, `alert`, `input`, `navbar`, `select`
|
|
||||||
- Color scheme: `primary` (emerald), `base-100`, `base-200`
|
|
||||||
- Custom colors in `app.css` with `@theme`
|
|
||||||
|
|
||||||
## Authentication Patterns
|
|
||||||
- Auth state in `$lib/stores/auth.js`
|
|
||||||
- Check auth in layout `onMount` or `+layout.server.js`
|
|
||||||
- Redirect to `/login` if unauthenticated
|
|
||||||
- API client auto-redirects on 401
|
|
||||||
|
|
||||||
## LocalStorage
|
|
||||||
- Only access in browser: check `browser` from `$app/environment`
|
|
||||||
- Use descriptive keys: `auth_token`
|
|
||||||
|
|
||||||
## Reusable Components
|
|
||||||
- **DataTable.svelte**: Sortable, paginated table with actions
|
|
||||||
- **Modal.svelte**: Reusable modal for confirmations and forms
|
|
||||||
- **SelectDropdown.svelte**: Dropdown populated from ValueSets or API data
|
|
||||||
- **Sidebar.svelte**: Navigation sidebar
|
|
||||||
- **ToastContainer.svelte**: Toast notifications
|
|
||||||
|
|
||||||
## SvelteKit Patterns
|
|
||||||
- File-based routing with `+page.svelte`, `+layout.svelte`
|
|
||||||
- Route groups with `(app)` for protected routes
|
|
||||||
- Load data in `+page.js` or `+page.server.js` if needed
|
|
||||||
- Use `invalidateAll()` after mutations to refresh data
|
|
||||||
|
|
||||||
## Code Quality Notes
|
|
||||||
- No ESLint or Prettier configured yet
|
|
||||||
- No test framework configured yet (plan: Vitest for unit tests, Playwright for E2E)
|
|
||||||
- JSDoc comments are required for all exported functions
|
|
||||||
- Keep components focused and reusable
|
|
||||||
- Extract logic into utility functions when possible
|
|
||||||
@ -1,219 +1,32 @@
|
|||||||
# CLQMS Frontend - Suggested Commands
|
# Suggested Commands (Windows project shell)
|
||||||
|
|
||||||
## Development Commands
|
## Core project commands (pnpm)
|
||||||
|
|
||||||
```bash
|
- `pnpm install` - install dependencies.
|
||||||
# Start development server
|
- `pnpm run dev` - run local dev server.
|
||||||
pnpm run dev
|
- `pnpm run build` - create production build (`build/`).
|
||||||
|
- `pnpm run preview` - preview production build.
|
||||||
|
- `pnpm run prepare` - run SvelteKit sync.
|
||||||
|
|
||||||
# Start development server and open in browser
|
## Testing/linting/formatting status
|
||||||
pnpm run dev -- --open
|
|
||||||
|
|
||||||
# Production build
|
- No lint command configured yet.
|
||||||
pnpm run build
|
- No format command configured yet.
|
||||||
|
- No test command configured yet.
|
||||||
|
- Notes in `AGENTS.md` mention future options like Vitest/Playwright, but not currently wired in scripts.
|
||||||
|
|
||||||
# Preview production build
|
## Useful Windows shell commands
|
||||||
pnpm run preview
|
|
||||||
|
|
||||||
# Sync SvelteKit (runs automatically on install)
|
- `dir` - list files (cmd).
|
||||||
pnpm run prepare
|
- `Get-ChildItem` or `ls` - list files (PowerShell).
|
||||||
```
|
- `cd <path>` - change directory.
|
||||||
|
- `git status` - working tree status.
|
||||||
|
- `git diff` - inspect changes.
|
||||||
|
- `git log --oneline -n 10` - recent commits.
|
||||||
|
- `findstr /S /N /I "text" *` - basic content search in cmd.
|
||||||
|
- `Select-String -Path .\* -Pattern "text" -Recurse` - content search in PowerShell.
|
||||||
|
|
||||||
## Package Manager
|
## Environment notes
|
||||||
- **Primary**: `pnpm` (preferred)
|
|
||||||
- Can also use `npm` or `yarn` if needed
|
|
||||||
|
|
||||||
## Development Server
|
- Node.js 18+ required.
|
||||||
- Dev server runs by default on `http://localhost:5173`
|
- Backend API should be running at `http://localhost:8000` for dev proxying.
|
||||||
- API requests to `/api` are proxied to `http://localhost:8000`
|
|
||||||
- Hot module replacement (HMR) enabled
|
|
||||||
|
|
||||||
## Windows System Commands
|
|
||||||
Since the system is Windows, use these commands:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# List files
|
|
||||||
dir
|
|
||||||
|
|
||||||
# Change directory
|
|
||||||
cd path\to\directory
|
|
||||||
|
|
||||||
# Search for files
|
|
||||||
dir /s filename
|
|
||||||
|
|
||||||
# Search for text in files
|
|
||||||
findstr /s /i "searchterm" *.js
|
|
||||||
|
|
||||||
# Delete files
|
|
||||||
del filename
|
|
||||||
|
|
||||||
# Delete directories
|
|
||||||
rmdir /s /q directoryname
|
|
||||||
|
|
||||||
# Copy files
|
|
||||||
copy source destination
|
|
||||||
|
|
||||||
# Move files
|
|
||||||
move source destination
|
|
||||||
|
|
||||||
# Create directory
|
|
||||||
mkdir directoryname
|
|
||||||
|
|
||||||
# Display file content
|
|
||||||
type filename
|
|
||||||
|
|
||||||
# Edit files (use VS Code or other editor)
|
|
||||||
code filename
|
|
||||||
```
|
|
||||||
|
|
||||||
## Git Commands
|
|
||||||
```bash
|
|
||||||
# Check git status
|
|
||||||
git status
|
|
||||||
|
|
||||||
# View changes
|
|
||||||
git diff
|
|
||||||
|
|
||||||
# Stage changes
|
|
||||||
git add .
|
|
||||||
|
|
||||||
# Commit changes
|
|
||||||
git commit -m "commit message"
|
|
||||||
|
|
||||||
# Push to remote
|
|
||||||
git push
|
|
||||||
|
|
||||||
# Pull from remote
|
|
||||||
git pull
|
|
||||||
|
|
||||||
# Create new branch
|
|
||||||
git branch branch-name
|
|
||||||
|
|
||||||
# Switch branch
|
|
||||||
git checkout branch-name
|
|
||||||
|
|
||||||
# View commit history
|
|
||||||
git log --oneline
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing Commands (when configured)
|
|
||||||
```bash
|
|
||||||
# Run all tests (Vitest - when configured)
|
|
||||||
pnpm test
|
|
||||||
|
|
||||||
# Run tests in watch mode
|
|
||||||
pnpm test -- --watch
|
|
||||||
|
|
||||||
# Run single test file
|
|
||||||
pnpm test src/path/to/test.js
|
|
||||||
|
|
||||||
# Run E2E tests (Playwright - when configured)
|
|
||||||
pnpm run test:e2e
|
|
||||||
|
|
||||||
# Run E2E tests in headless mode
|
|
||||||
pnpm run test:e2e -- --headed=false
|
|
||||||
```
|
|
||||||
|
|
||||||
## Linting and Formatting (when configured)
|
|
||||||
```bash
|
|
||||||
# Run ESLint (when configured)
|
|
||||||
pnpm run lint
|
|
||||||
|
|
||||||
# Auto-fix lint issues
|
|
||||||
pnpm run lint -- --fix
|
|
||||||
|
|
||||||
# Format code with Prettier (when configured)
|
|
||||||
pnpm run format
|
|
||||||
```
|
|
||||||
|
|
||||||
## Environment Setup
|
|
||||||
```bash
|
|
||||||
# Install dependencies
|
|
||||||
pnpm install
|
|
||||||
|
|
||||||
# Create .env file for environment variables
|
|
||||||
echo "VITE_API_URL=http://localhost:8000" > .env
|
|
||||||
|
|
||||||
# Install dependencies (if package-lock.json exists)
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
## Useful pnpm Commands
|
|
||||||
```bash
|
|
||||||
# Add a dependency
|
|
||||||
pnpm add package-name
|
|
||||||
|
|
||||||
# Add a dev dependency
|
|
||||||
pnpm add -D package-name
|
|
||||||
|
|
||||||
# Update dependencies
|
|
||||||
pnpm update
|
|
||||||
|
|
||||||
# Remove a dependency
|
|
||||||
pnpm remove package-name
|
|
||||||
|
|
||||||
# List installed packages
|
|
||||||
pnpm list --depth 0
|
|
||||||
|
|
||||||
# Check for outdated packages
|
|
||||||
pnpm outdated
|
|
||||||
```
|
|
||||||
|
|
||||||
## Build and Deploy
|
|
||||||
```bash
|
|
||||||
# Build for production
|
|
||||||
pnpm run build
|
|
||||||
|
|
||||||
# Preview production build locally
|
|
||||||
pnpm run preview
|
|
||||||
|
|
||||||
# Clean build artifacts (if clean script exists)
|
|
||||||
pnpm run clean
|
|
||||||
```
|
|
||||||
|
|
||||||
## SvelteKit Specific Commands
|
|
||||||
```bash
|
|
||||||
# Sync SvelteKit type definitions
|
|
||||||
pnpm run prepare
|
|
||||||
|
|
||||||
# Check SvelteKit configuration
|
|
||||||
pnpm run check
|
|
||||||
|
|
||||||
# Generate types (if using TypeScript)
|
|
||||||
pnpm run check:types
|
|
||||||
```
|
|
||||||
|
|
||||||
## Common Troubleshooting
|
|
||||||
```bash
|
|
||||||
# Clear pnpm cache
|
|
||||||
pnpm store prune
|
|
||||||
|
|
||||||
# Reinstall all dependencies
|
|
||||||
rm -rf node_modules pnpm-lock.yaml && pnpm install
|
|
||||||
|
|
||||||
# Clear Vite cache
|
|
||||||
rm -rf .vite
|
|
||||||
|
|
||||||
# Check Node.js version
|
|
||||||
node --version
|
|
||||||
|
|
||||||
# Check pnpm version
|
|
||||||
pnpm --version
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development Workflow
|
|
||||||
1. Start dev server: `pnpm run dev`
|
|
||||||
2. Open browser to `http://localhost:5173`
|
|
||||||
3. Make changes to files
|
|
||||||
4. See HMR updates in browser
|
|
||||||
5. Test changes
|
|
||||||
6. Commit changes when ready
|
|
||||||
|
|
||||||
## API Testing
|
|
||||||
```bash
|
|
||||||
# Test API endpoints via curl (in Git Bash or WSL)
|
|
||||||
curl -X GET http://localhost:8000/api/valueset
|
|
||||||
|
|
||||||
# Test with authentication (requires JWT token)
|
|
||||||
curl -X GET http://localhost:8000/api/patient -H "Authorization: Bearer YOUR_TOKEN"
|
|
||||||
```
|
|
||||||
@ -1,135 +0,0 @@
|
|||||||
# CLQMS Frontend - Task Completion Checklist
|
|
||||||
|
|
||||||
## When Completing a Task
|
|
||||||
|
|
||||||
### 1. Code Quality
|
|
||||||
- [ ] Code follows project conventions (camelCase, 2-space indent, semicolons)
|
|
||||||
- [ ] Components use Svelte 5 runes (`$state`, `$derived`, `$effect`, `$props`)
|
|
||||||
- [ ] All exported functions have JSDoc comments
|
|
||||||
- [ ] Imports are ordered correctly (Svelte, $lib, external)
|
|
||||||
- [ ] No hardcoded values (use environment variables or constants)
|
|
||||||
- [ ] Error handling is properly implemented
|
|
||||||
- [ ] Loading states are shown for async operations
|
|
||||||
- [ ] Toast notifications for user feedback
|
|
||||||
|
|
||||||
### 2. Testing (when test framework is configured)
|
|
||||||
- [ ] Unit tests written for new functions
|
|
||||||
- [ ] Component tests written for new components
|
|
||||||
- [ ] All tests pass: `pnpm test`
|
|
||||||
- [ ] Test coverage is adequate
|
|
||||||
|
|
||||||
### 3. Linting and Formatting (when configured)
|
|
||||||
- [ ] Run linter: `pnpm run lint`
|
|
||||||
- [ ] Fix any linting errors
|
|
||||||
- [ ] Run formatter: `pnpm run format`
|
|
||||||
- [ ] No linting or formatting errors
|
|
||||||
|
|
||||||
### 4. Build Verification
|
|
||||||
- [ ] Production build succeeds: `pnpm run build`
|
|
||||||
- [ ] No build errors or warnings
|
|
||||||
- [ ] Preview build works: `pnpm run preview`
|
|
||||||
- [ ] Application runs without console errors
|
|
||||||
|
|
||||||
### 5. Manual Testing
|
|
||||||
- [ ] Feature works as expected in dev environment
|
|
||||||
- [ ] Navigation works correctly
|
|
||||||
- [ ] Forms validate properly
|
|
||||||
- [ ] Error messages are clear
|
|
||||||
- [ ] Loading states display correctly
|
|
||||||
- [ ] Toast notifications appear for success/error
|
|
||||||
- [ ] Responsive design works on mobile/tablet
|
|
||||||
- [ ] Accessibility: keyboard navigation, ARIA labels
|
|
||||||
|
|
||||||
### 6. Documentation
|
|
||||||
- [ ] API endpoints documented in comments
|
|
||||||
- [ ] Complex logic explained in comments
|
|
||||||
- [ ] New components documented
|
|
||||||
- [ ] README updated (if needed)
|
|
||||||
- [ ] Implementation plan updated (if applicable)
|
|
||||||
|
|
||||||
### 7. Security Considerations
|
|
||||||
- [ ] No sensitive data exposed to client
|
|
||||||
- [ ] API calls use proper authentication
|
|
||||||
- [ ] User input is validated
|
|
||||||
- [ ] XSS vulnerabilities checked
|
|
||||||
- [ ] CSRF protection (if applicable)
|
|
||||||
|
|
||||||
### 8. Performance
|
|
||||||
- [ ] No unnecessary re-renders
|
|
||||||
- [ ] Large lists use pagination
|
|
||||||
- [ ] Images are optimized (if applicable)
|
|
||||||
- [ ] Bundle size impact is minimal
|
|
||||||
|
|
||||||
### 9. Browser Compatibility
|
|
||||||
- [ ] Works in modern browsers (Chrome, Firefox, Edge, Safari)
|
|
||||||
- [ ] Feature detection used for newer APIs (if needed)
|
|
||||||
- [ ] Polyfills considered (if needed)
|
|
||||||
|
|
||||||
## Before Marking Task Complete
|
|
||||||
|
|
||||||
1. **Review Code**: Check all items in the checklist above
|
|
||||||
2. **Test Thoroughly**: Manual testing of all new/modified features
|
|
||||||
3. **Check Build**: Ensure production build succeeds
|
|
||||||
4. **Run Tests**: Ensure all tests pass (when test framework is configured)
|
|
||||||
5. **Run Linter**: Ensure no linting errors (when configured)
|
|
||||||
|
|
||||||
## Common Issues to Check
|
|
||||||
|
|
||||||
### API Issues
|
|
||||||
- Check API endpoints match backend documentation
|
|
||||||
- Verify request/response format
|
|
||||||
- Check error handling for failed requests
|
|
||||||
- Ensure 401 redirects to login work
|
|
||||||
|
|
||||||
### Svelte/Component Issues
|
|
||||||
- Check reactivity with `$state` and `$derived`
|
|
||||||
- Verify lifecycle hooks (`onMount`, `onDestroy`)
|
|
||||||
- Check prop passing between components
|
|
||||||
- Ensure event handlers work correctly
|
|
||||||
|
|
||||||
### Styling Issues
|
|
||||||
- Check responsive design (mobile, tablet, desktop)
|
|
||||||
- Verify DaisyUI component usage
|
|
||||||
- Check color scheme consistency
|
|
||||||
- Ensure accessibility (contrast, focus states)
|
|
||||||
|
|
||||||
### State Management Issues
|
|
||||||
- Verify store updates trigger UI updates
|
|
||||||
- Check localStorage handling (browser check)
|
|
||||||
- Ensure auth state is managed correctly
|
|
||||||
- Verify derived state calculations
|
|
||||||
|
|
||||||
## Git Commit Guidelines (if committing)
|
|
||||||
|
|
||||||
Follow conventional commits format:
|
|
||||||
- `feat: add new feature`
|
|
||||||
- `fix: fix bug`
|
|
||||||
- `docs: update documentation`
|
|
||||||
- `style: format code`
|
|
||||||
- `refactor: refactor code`
|
|
||||||
- `test: add tests`
|
|
||||||
- `chore: maintenance tasks`
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
- `feat: add patient create form with validation`
|
|
||||||
- `fix: handle API errors properly in patient list`
|
|
||||||
- `docs: update API endpoint documentation`
|
|
||||||
- `refactor: extract form handling logic to utility function`
|
|
||||||
|
|
||||||
## When Tests Are Not Available
|
|
||||||
|
|
||||||
Currently, no test framework is configured. Until tests are added:
|
|
||||||
- Focus on manual testing
|
|
||||||
- Test all user flows
|
|
||||||
- Check edge cases (empty data, errors, network issues)
|
|
||||||
- Verify error handling
|
|
||||||
- Test responsive design
|
|
||||||
|
|
||||||
## Future: When Tests Are Added
|
|
||||||
|
|
||||||
Once Vitest and Playwright are configured:
|
|
||||||
- Add unit tests for new functions
|
|
||||||
- Add component tests for new components
|
|
||||||
- Add E2E tests for user flows
|
|
||||||
- Ensure test coverage is maintained
|
|
||||||
- Run tests before marking task complete
|
|
||||||
@ -1,89 +1,15 @@
|
|||||||
# CLQMS Frontend - Post-Task Checklist
|
# Task Completion Checklist
|
||||||
|
|
||||||
## When a Task is Completed
|
Given current project setup:
|
||||||
|
|
||||||
### Verification Steps
|
1. Run relevant build verification:
|
||||||
|
- `pnpm run build`
|
||||||
1. **Build Check** (if changes are significant)
|
2. If runtime behavior changed, also sanity check with:
|
||||||
- Run `pnpm run build` to ensure the application builds successfully
|
- `pnpm run dev` (manual smoke test)
|
||||||
- Check for build errors or warnings
|
- optionally `pnpm run preview` for production-like validation
|
||||||
|
3. Since lint/format/test scripts are not configured, mention this explicitly in handoff.
|
||||||
2. **Manual Testing**
|
4. Ensure code follows `AGENTS.md` conventions:
|
||||||
- Start dev server: `pnpm run dev`
|
- semicolons, single quotes, import order, Svelte 5 rune patterns, naming.
|
||||||
- Test the feature/fix in the browser
|
5. For API/auth/localStorage changes:
|
||||||
- Verify no console errors
|
- verify browser-only access guards (`browser`) and auth redirect behavior are preserved.
|
||||||
- Check that existing functionality is not broken
|
6. In final handoff, include changed file paths and any manual verification steps performed.
|
||||||
|
|
||||||
3. **Code Review Checklist**
|
|
||||||
- Follow code style conventions (see `code_style_conventions.md`)
|
|
||||||
- Imports are in correct order
|
|
||||||
- Components use Svelte 5 runes (`$props`, `$state`, `$derived`, `$effect`, `$bindable`)
|
|
||||||
- Event handlers prefixed with `handle`
|
|
||||||
- JSDoc comments on exported functions
|
|
||||||
- Proper error handling with try-catch
|
|
||||||
- Toast notifications for user feedback
|
|
||||||
|
|
||||||
### Code Style Verification
|
|
||||||
|
|
||||||
- ✅ Single quotes for strings
|
|
||||||
- ✅ Semicolons used consistently
|
|
||||||
- ✅ 2-space indentation
|
|
||||||
- ✅ Trailing commas in multi-line objects/arrays
|
|
||||||
- ✅ PascalCase for components
|
|
||||||
- ✅ camelCase for variables and functions
|
|
||||||
- ✅ Descriptive naming
|
|
||||||
|
|
||||||
### Svelte 5 Runes Verification
|
|
||||||
|
|
||||||
- ✅ Using `$props()` for component props
|
|
||||||
- ✅ Using `$state()` for reactive state
|
|
||||||
- ✅ Using `$derived()` for computed values
|
|
||||||
- ✅ Using `$effect()` for side effects
|
|
||||||
- ✅ Using `$bindable()` for two-way binding props
|
|
||||||
- ✅ Using `{@render snippet()}` for slots/content
|
|
||||||
|
|
||||||
### API Integration Verification
|
|
||||||
|
|
||||||
- ✅ Proper error handling with try-catch
|
|
||||||
- ✅ Toast notifications for success/error
|
|
||||||
- ✅ Loading states during async operations
|
|
||||||
- ✅ Form validation before submission
|
|
||||||
- ✅ API calls follow established patterns (get, post, put, patch, del from client.js)
|
|
||||||
|
|
||||||
### Accessibility Verification
|
|
||||||
|
|
||||||
- ✅ Semantic HTML elements
|
|
||||||
- ✅ ARIA labels where needed
|
|
||||||
- ✅ Keyboard navigation support
|
|
||||||
- ✅ Proper form labels and focus management
|
|
||||||
|
|
||||||
### Performance Considerations
|
|
||||||
|
|
||||||
- ✅ No unnecessary re-renders
|
|
||||||
- ✅ Proper use of `$derived` for computed values
|
|
||||||
- ✅ Efficient event handling (avoid inline functions in templates when possible)
|
|
||||||
|
|
||||||
### Browser Compatibility
|
|
||||||
|
|
||||||
- ✅ Check browser console for errors
|
|
||||||
- ✅ Test in Chrome/Firefox/Edge if possible
|
|
||||||
- ✅ Verify responsive design on different screen sizes
|
|
||||||
|
|
||||||
## NO Auto-Commit Policy
|
|
||||||
|
|
||||||
**IMPORTANT**: Do NOT commit changes automatically. Only commit when explicitly requested by the user.
|
|
||||||
|
|
||||||
If user asks to commit:
|
|
||||||
1. Run `git status` to see all untracked files
|
|
||||||
2. Run `git diff` to see changes
|
|
||||||
3. Run `git log` to see commit message style
|
|
||||||
4. Stage relevant files
|
|
||||||
5. Create commit with descriptive message
|
|
||||||
6. Run `git status` to verify
|
|
||||||
|
|
||||||
## Before Marking Task as Complete
|
|
||||||
|
|
||||||
- Ensure all verification steps are completed
|
|
||||||
- Confirm the application runs without errors
|
|
||||||
- If applicable, verify the feature works as expected
|
|
||||||
- Only confirm completion if you have tested the changes
|
|
||||||
@ -32,11 +32,24 @@ languages:
|
|||||||
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
|
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
|
||||||
encoding: "utf-8"
|
encoding: "utf-8"
|
||||||
|
|
||||||
|
# line ending convention to use when writing source files.
|
||||||
|
# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default)
|
||||||
|
# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings.
|
||||||
|
line_ending:
|
||||||
|
|
||||||
|
# The language backend to use for this project.
|
||||||
|
# If not set, the global setting from serena_config.yml is used.
|
||||||
|
# Valid values: LSP, JetBrains
|
||||||
|
# Note: the backend is fixed at startup. If a project with a different backend
|
||||||
|
# is activated post-init, an error will be returned.
|
||||||
|
language_backend:
|
||||||
|
|
||||||
# whether to use project's .gitignore files to ignore files
|
# whether to use project's .gitignore files to ignore files
|
||||||
ignore_all_files_in_gitignore: true
|
ignore_all_files_in_gitignore: true
|
||||||
|
|
||||||
# list of additional paths to ignore in all projects
|
# list of additional paths to ignore in this project.
|
||||||
# same syntax as gitignore, so you can use * and **
|
# Same syntax as gitignore, so you can use * and **.
|
||||||
|
# Note: global ignored_paths from serena_config.yml are also applied additively.
|
||||||
ignored_paths: []
|
ignored_paths: []
|
||||||
|
|
||||||
# whether the project is in read-only mode
|
# whether the project is in read-only mode
|
||||||
@ -111,22 +124,12 @@ default_modes:
|
|||||||
# (contrary to the memories, which are loaded on demand).
|
# (contrary to the memories, which are loaded on demand).
|
||||||
initial_prompt: ""
|
initial_prompt: ""
|
||||||
|
|
||||||
# override of the corresponding setting in serena_config.yml, see the documentation there.
|
# time budget (seconds) per tool call for the retrieval of additional symbol information
|
||||||
# If null or missing, the value from the global config is used.
|
# such as docstrings or parameter information.
|
||||||
|
# This overrides the corresponding setting in the global configuration; see the documentation there.
|
||||||
|
# If null or missing, use the setting from the global configuration.
|
||||||
symbol_info_budget:
|
symbol_info_budget:
|
||||||
|
|
||||||
# The language backend to use for this project.
|
|
||||||
# If not set, the global setting from serena_config.yml is used.
|
|
||||||
# Valid values: LSP, JetBrains
|
|
||||||
# Note: the backend is fixed at startup. If a project with a different backend
|
|
||||||
# is activated post-init, an error will be returned.
|
|
||||||
language_backend:
|
|
||||||
|
|
||||||
# list of regex patterns which, when matched, mark a memory entry as read‑only.
|
# list of regex patterns which, when matched, mark a memory entry as read‑only.
|
||||||
# Extends the list from the global configuration, merging the two lists.
|
# Extends the list from the global configuration, merging the two lists.
|
||||||
read_only_memory_patterns: []
|
read_only_memory_patterns: []
|
||||||
|
|
||||||
# line ending convention to use when writing source files.
|
|
||||||
# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default)
|
|
||||||
# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings.
|
|
||||||
line_ending:
|
|
||||||
|
|||||||
@ -1454,6 +1454,12 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
DisciplineCode:
|
DisciplineCode:
|
||||||
type: string
|
type: string
|
||||||
|
SeqScr:
|
||||||
|
type: integer
|
||||||
|
description: Display order on screen
|
||||||
|
SeqRpt:
|
||||||
|
type: integer
|
||||||
|
description: Display order in reports
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Discipline updated
|
description: Discipline updated
|
||||||
@ -5532,6 +5538,12 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
DisciplineCode:
|
DisciplineCode:
|
||||||
type: string
|
type: string
|
||||||
|
SeqScr:
|
||||||
|
type: integer
|
||||||
|
description: Display order on screen
|
||||||
|
SeqRpt:
|
||||||
|
type: integer
|
||||||
|
description: Display order in reports
|
||||||
Department:
|
Department:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@ -5890,9 +5902,6 @@ components:
|
|||||||
EndDate:
|
EndDate:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
FormulaInput:
|
|
||||||
type: string
|
|
||||||
description: Input variables for calculated tests
|
|
||||||
FormulaCode:
|
FormulaCode:
|
||||||
type: string
|
type: string
|
||||||
description: Formula expression for calculated tests
|
description: Formula expression for calculated tests
|
||||||
@ -5903,7 +5912,7 @@ components:
|
|||||||
type: object
|
type: object
|
||||||
testdefgrp:
|
testdefgrp:
|
||||||
type: array
|
type: array
|
||||||
description: Group members (only for GROUP type)
|
description: Group members (for GROUP and CALC types)
|
||||||
items:
|
items:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@ -6154,10 +6163,18 @@ components:
|
|||||||
- TestCalID: 1
|
- TestCalID: 1
|
||||||
DisciplineID: 2
|
DisciplineID: 2
|
||||||
DepartmentID: 2
|
DepartmentID: 2
|
||||||
FormulaInput: CREA,AGE,GENDER
|
|
||||||
FormulaCode: CKD_EPI(CREA,AGE,GENDER)
|
FormulaCode: CKD_EPI(CREA,AGE,GENDER)
|
||||||
Unit1: mL/min/1.73m2
|
Unit1: mL/min/1.73m2
|
||||||
Decimal: 0
|
Decimal: 0
|
||||||
|
testdefgrp:
|
||||||
|
- TestSiteID: 21
|
||||||
|
TestSiteCode: CREA
|
||||||
|
TestSiteName: Creatinine
|
||||||
|
TestType: TEST
|
||||||
|
- TestSiteID: 51
|
||||||
|
TestSiteCode: AGE
|
||||||
|
TestSiteName: Age
|
||||||
|
TestType: PARAM
|
||||||
refnum:
|
refnum:
|
||||||
- RefNumID: 5
|
- RefNumID: 5
|
||||||
NumRefType: NMRC
|
NumRefType: NMRC
|
||||||
|
|||||||
@ -89,17 +89,25 @@ function buildPayload(formData, isUpdate = false) {
|
|||||||
|
|
||||||
// Add formula fields for CALC type
|
// Add formula fields for CALC type
|
||||||
if (type === 'CALC') {
|
if (type === 'CALC') {
|
||||||
payload.details.FormulaInput = formData.details?.FormulaInput || '';
|
|
||||||
payload.details.FormulaCode = formData.details?.FormulaCode || '';
|
payload.details.FormulaCode = formData.details?.FormulaCode || '';
|
||||||
|
|
||||||
|
const calcMembers = formData.testdefgrp || formData.details?.members || [];
|
||||||
|
if (calcMembers.length > 0) {
|
||||||
|
payload.details.members = calcMembers.map(m => ({
|
||||||
|
TestSiteID: m.TestSiteID,
|
||||||
|
Member: m.Member || m.TestSiteID
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add members for GROUP type
|
// Add members for GROUP type
|
||||||
if (type === 'GROUP' && formData.details?.members) {
|
if (type === 'GROUP') {
|
||||||
|
const groupMembers = formData.testdefgrp || formData.details?.members || [];
|
||||||
payload.details = {
|
payload.details = {
|
||||||
members: formData.details.members.map(m => ({
|
members: groupMembers.map(m => ({
|
||||||
TestSiteID: m.TestSiteID,
|
TestSiteID: m.TestSiteID,
|
||||||
Member: m.Member
|
Member: m.Member || m.TestSiteID
|
||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,7 +56,6 @@ export interface TechDetail {
|
|||||||
|
|
||||||
// Calculation Details (extends TechDetail)
|
// Calculation Details (extends TechDetail)
|
||||||
export interface CalcDetails extends TechDetail {
|
export interface CalcDetails extends TechDetail {
|
||||||
FormulaInput?: string;
|
|
||||||
FormulaCode?: string;
|
FormulaCode?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,7 +153,6 @@ export interface TestDetail {
|
|||||||
ExpectedTAT?: number;
|
ExpectedTAT?: number;
|
||||||
|
|
||||||
// Calculated test specific
|
// Calculated test specific
|
||||||
FormulaInput?: string;
|
|
||||||
FormulaCode?: string;
|
FormulaCode?: string;
|
||||||
|
|
||||||
// Nested data
|
// Nested data
|
||||||
@ -199,7 +197,6 @@ export interface CreateTestPayload {
|
|||||||
ExpectedTAT?: number;
|
ExpectedTAT?: number;
|
||||||
|
|
||||||
// CALC only
|
// CALC only
|
||||||
FormulaInput?: string;
|
|
||||||
FormulaCode?: string;
|
FormulaCode?: string;
|
||||||
|
|
||||||
// GROUP only
|
// GROUP only
|
||||||
@ -248,7 +245,6 @@ export interface TestFormState {
|
|||||||
CollReq?: string;
|
CollReq?: string;
|
||||||
Method?: string;
|
Method?: string;
|
||||||
ExpectedTAT?: number;
|
ExpectedTAT?: number;
|
||||||
FormulaInput?: string;
|
|
||||||
FormulaCode?: string;
|
FormulaCode?: string;
|
||||||
members?: number[];
|
members?: number[];
|
||||||
};
|
};
|
||||||
|
|||||||
@ -129,7 +129,6 @@
|
|||||||
CollReq: '',
|
CollReq: '',
|
||||||
Method: '',
|
Method: '',
|
||||||
ExpectedTAT: null,
|
ExpectedTAT: null,
|
||||||
FormulaInput: '',
|
|
||||||
FormulaCode: '',
|
FormulaCode: '',
|
||||||
members: []
|
members: []
|
||||||
},
|
},
|
||||||
@ -185,8 +184,7 @@
|
|||||||
CollReq: test.CollReq || '',
|
CollReq: test.CollReq || '',
|
||||||
Method: test.Method || '',
|
Method: test.Method || '',
|
||||||
ExpectedTAT: test.ExpectedTAT || null,
|
ExpectedTAT: test.ExpectedTAT || null,
|
||||||
FormulaInput: test.FormulaInput || '',
|
FormulaCode: test.testdefcal?.[0]?.FormulaCode || test.FormulaCode || '',
|
||||||
FormulaCode: test.FormulaCode || '',
|
|
||||||
members: test.testdefgrp?.map(m => ({
|
members: test.testdefgrp?.map(m => ({
|
||||||
TestSiteID: m.TestSiteID,
|
TestSiteID: m.TestSiteID,
|
||||||
TestSiteCode: m.TestSiteCode,
|
TestSiteCode: m.TestSiteCode,
|
||||||
@ -264,6 +262,39 @@
|
|||||||
errors.TestType = 'Test type is required';
|
errors.TestType = 'Test type is required';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (formData.TestType === 'CALC') {
|
||||||
|
const formulaCode = formData?.details?.FormulaCode || '';
|
||||||
|
const membersFromTestDefGrp = Array.isArray(formData?.testdefgrp) ? formData.testdefgrp : [];
|
||||||
|
const membersFromDetails = Array.isArray(formData?.details?.members) ? formData.details.members : [];
|
||||||
|
|
||||||
|
const seenCodes = [];
|
||||||
|
const selectedMembers = [];
|
||||||
|
[...membersFromTestDefGrp, ...membersFromDetails].forEach((member) => {
|
||||||
|
const code = member?.TestSiteCode;
|
||||||
|
if (code && !seenCodes.includes(code)) {
|
||||||
|
seenCodes.push(code);
|
||||||
|
selectedMembers.push(member);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (selectedMembers.length === 0) {
|
||||||
|
errors.members = 'At least one member is required for CALC tests';
|
||||||
|
}
|
||||||
|
|
||||||
|
const braceRefs = formulaCode.match(/\{[^{}]+\}/g) || [];
|
||||||
|
if (braceRefs.length === 0) {
|
||||||
|
errors.FormulaCode = 'Formula must include at least one member reference like {CODE}';
|
||||||
|
} else {
|
||||||
|
const missingMemberCodes = selectedMembers
|
||||||
|
.map((member) => member.TestSiteCode)
|
||||||
|
.filter((code) => !formulaCode.includes(`{${code}}`));
|
||||||
|
|
||||||
|
if (missingMemberCodes.length > 0) {
|
||||||
|
const formattedMissing = missingMemberCodes.map((code) => `{${code}}`).join(', ');
|
||||||
|
errors.FormulaCode = `Formula must reference all members: ${formattedMissing}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
validationErrors = errors;
|
validationErrors = errors;
|
||||||
return Object.keys(errors).length === 0;
|
return Object.keys(errors).length === 0;
|
||||||
}
|
}
|
||||||
@ -271,7 +302,11 @@
|
|||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
if (!validateForm()) {
|
if (!validateForm()) {
|
||||||
toastError('Please fix validation errors');
|
toastError('Please fix validation errors');
|
||||||
|
if (validationErrors.FormulaCode || validationErrors.members) {
|
||||||
|
currentTab = 'calc';
|
||||||
|
} else {
|
||||||
currentTab = 'basic';
|
currentTab = 'basic';
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -318,7 +353,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<span class="font-semibold">Please fix the following errors:</span>
|
<span class="font-semibold">Please fix the following errors:</span>
|
||||||
<ul class="list-disc list-inside text-sm mt-1">
|
<ul class="list-disc list-inside text-sm mt-1">
|
||||||
{#each formErrors as error}
|
{#each formErrors as error, i (`${error}-${i}`)}
|
||||||
<li>{error}</li>
|
<li>{error}</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
@ -370,21 +405,10 @@
|
|||||||
/>
|
/>
|
||||||
{:else if currentTab === 'calc'}
|
{:else if currentTab === 'calc'}
|
||||||
<CalcDetailsTab
|
<CalcDetailsTab
|
||||||
bind:formData
|
|
||||||
bind:isDirty
|
|
||||||
/>
|
|
||||||
{:else if currentTab === 'group'}
|
|
||||||
<GroupMembersTab
|
|
||||||
bind:formData
|
bind:formData
|
||||||
{tests}
|
{tests}
|
||||||
bind:isDirty
|
bind:isDirty
|
||||||
/>
|
{validationErrors}
|
||||||
{:else if currentTab === 'calc'}
|
|
||||||
<CalcDetailsTab
|
|
||||||
bind:formData
|
|
||||||
{disciplines}
|
|
||||||
{departments}
|
|
||||||
bind:isDirty
|
|
||||||
/>
|
/>
|
||||||
{:else if currentTab === 'group'}
|
{:else if currentTab === 'group'}
|
||||||
<GroupMembersTab
|
<GroupMembersTab
|
||||||
|
|||||||
@ -1,18 +1,163 @@
|
|||||||
<script>
|
<script>
|
||||||
let { formData = $bindable(), isDirty = $bindable(false) } = $props();
|
import { Plus, Trash2, Box, Search, WandSparkles } from 'lucide-svelte';
|
||||||
|
|
||||||
|
let { formData = $bindable(), tests = [], isDirty = $bindable(false), validationErrors = {} } = $props();
|
||||||
|
|
||||||
|
let searchQuery = $state('');
|
||||||
|
|
||||||
|
const members = $derived.by(() => {
|
||||||
|
const testdefgrp = Array.isArray(formData?.testdefgrp) ? formData.testdefgrp : [];
|
||||||
|
const detailMembers = Array.isArray(formData?.details?.members) ? formData.details.members : [];
|
||||||
|
const sourceMembers = testdefgrp.length > 0 ? testdefgrp : detailMembers;
|
||||||
|
|
||||||
|
return sourceMembers
|
||||||
|
.map((m) => ({
|
||||||
|
TestSiteID: m.TestSiteID,
|
||||||
|
TestSiteCode: m.TestSiteCode,
|
||||||
|
TestSiteName: m.TestSiteName,
|
||||||
|
TestType: m.TestType,
|
||||||
|
TestTypeLabel: m.TestTypeLabel,
|
||||||
|
SeqScr: m.SeqScr || m.Member || 0,
|
||||||
|
}))
|
||||||
|
.sort((a, b) => parseInt(a.SeqScr || 0) - parseInt(b.SeqScr || 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
const availableTests = $derived.by(() => {
|
||||||
|
const selectedIds = new Set(members.map((member) => Number(member.TestSiteID)));
|
||||||
|
|
||||||
|
let filtered = tests.filter((test) =>
|
||||||
|
Number(test.TestSiteID) !== Number(formData?.TestSiteID) &&
|
||||||
|
!selectedIds.has(Number(test.TestSiteID)) &&
|
||||||
|
test.IsActive !== '0' &&
|
||||||
|
test.IsActive !== 0
|
||||||
|
);
|
||||||
|
|
||||||
|
if (searchQuery.trim()) {
|
||||||
|
const query = searchQuery.toLowerCase();
|
||||||
|
filtered = filtered.filter((test) =>
|
||||||
|
test.TestSiteCode?.toLowerCase().includes(query) ||
|
||||||
|
test.TestSiteName?.toLowerCase().includes(query)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered.sort((a, b) => parseInt(a.SeqScr || 0) - parseInt(b.SeqScr || 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
const formulaTokens = $derived(tokenizeBraceRefs(formData?.details?.FormulaCode || ''));
|
||||||
|
|
||||||
|
const formulaSyntax = $derived.by(() => getFormulaSyntaxStatus(formData?.details?.FormulaCode || ''));
|
||||||
|
|
||||||
|
const missingMemberCodes = $derived.by(() =>
|
||||||
|
getMissingMemberCodes(formData?.details?.FormulaCode || '', members)
|
||||||
|
);
|
||||||
|
|
||||||
function handleFieldChange() {
|
function handleFieldChange() {
|
||||||
isDirty = true;
|
isDirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateFormula(code) {
|
function tokenizeBraceRefs(formulaCode) {
|
||||||
const pattern = /\{[^}]+\}/g;
|
const matches = formulaCode.match(/\{[^{}]+\}/g) || [];
|
||||||
const matches = code.match(pattern) || [];
|
return matches.map((token) => token.slice(1, -1));
|
||||||
return matches.length > 0;
|
}
|
||||||
|
|
||||||
|
function getFormulaSyntaxStatus(formulaCode) {
|
||||||
|
const formula = formulaCode || '';
|
||||||
|
if (!formula.trim()) {
|
||||||
|
return { tone: 'neutral', text: 'Enter formula with test code references' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const opens = (formula.match(/\{/g) || []).length;
|
||||||
|
const closes = (formula.match(/\}/g) || []).length;
|
||||||
|
if (opens !== closes) {
|
||||||
|
return { tone: 'error', text: 'Unbalanced braces in formula' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const stripped = formula.replace(/\{[^{}]+\}/g, '');
|
||||||
|
if (stripped.includes('{') || stripped.includes('}')) {
|
||||||
|
return { tone: 'error', text: 'Invalid brace token format detected' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tokenizeBraceRefs(formula).length === 0) {
|
||||||
|
return { tone: 'warning', text: 'No test references found' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { tone: 'success', text: 'Formula syntax looks good' };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMissingMemberCodes(formulaCode, selectedMembers) {
|
||||||
|
const formula = formulaCode || '';
|
||||||
|
return selectedMembers
|
||||||
|
.map((member) => member.TestSiteCode)
|
||||||
|
.filter((code) => code && !formula.includes(`{${code}}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
function addMember(test) {
|
||||||
|
const currentMembers = Array.isArray(formData?.testdefgrp)
|
||||||
|
? formData.testdefgrp
|
||||||
|
: (Array.isArray(formData?.details?.members) ? formData.details.members : []);
|
||||||
|
|
||||||
|
const newMember = {
|
||||||
|
TestSiteID: test.TestSiteID,
|
||||||
|
TestSiteCode: test.TestSiteCode || '',
|
||||||
|
TestSiteName: test.TestSiteName || '',
|
||||||
|
TestType: test.TestType || 'TEST',
|
||||||
|
TestTypeLabel: test.TestTypeLabel || 'Test',
|
||||||
|
SeqScr: test.SeqScr || '0',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Object.prototype.hasOwnProperty.call(formData, 'testdefgrp')) {
|
||||||
|
formData.testdefgrp = [...currentMembers, newMember];
|
||||||
|
} else {
|
||||||
|
formData.details.members = [...currentMembers, newMember];
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFieldChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeMember(testId) {
|
||||||
|
const currentMembers = Array.isArray(formData?.testdefgrp)
|
||||||
|
? formData.testdefgrp
|
||||||
|
: (Array.isArray(formData?.details?.members) ? formData.details.members : []);
|
||||||
|
|
||||||
|
const remainingMembers = currentMembers.filter((member) => Number(member.TestSiteID) !== Number(testId));
|
||||||
|
|
||||||
|
if (Object.prototype.hasOwnProperty.call(formData, 'testdefgrp')) {
|
||||||
|
formData.testdefgrp = remainingMembers;
|
||||||
|
} else {
|
||||||
|
formData.details.members = remainingMembers;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFieldChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
function prettifyFormula() {
|
||||||
|
const original = formData?.details?.FormulaCode || '';
|
||||||
|
if (!original.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokens = [];
|
||||||
|
let normalized = original.replace(/\{[^{}]+\}/g, (match) => {
|
||||||
|
const index = tokens.push(match) - 1;
|
||||||
|
return `__TOKEN_${index}__`;
|
||||||
|
});
|
||||||
|
|
||||||
|
normalized = normalized
|
||||||
|
.replace(/[\r\n\t]+/g, ' ')
|
||||||
|
.replace(/\s*([+\-*/])\s*/g, ' $1 ')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
normalized = normalized.replace(/__TOKEN_(\d+)__/g, (_, index) => tokens[Number(index)]);
|
||||||
|
|
||||||
|
if (normalized !== original) {
|
||||||
|
formData.details.FormulaCode = normalized;
|
||||||
|
handleFieldChange();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="space-y-5">
|
<div class="space-y-4">
|
||||||
<h2 class="text-lg font-semibold text-gray-800">Calculated Test Formula</h2>
|
<h2 class="text-lg font-semibold text-gray-800">Calculated Test Formula</h2>
|
||||||
|
|
||||||
<div class="alert alert-info text-sm">
|
<div class="alert alert-info text-sm">
|
||||||
@ -22,23 +167,124 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 h-[520px] overflow-hidden">
|
||||||
|
<div class="flex flex-col border border-base-300 rounded-lg bg-base-100 overflow-hidden">
|
||||||
|
<div class="p-3 border-b border-base-300 bg-base-200 rounded-t-lg shrink-0">
|
||||||
|
<h3 class="text-sm font-medium text-gray-700 mb-2">Available Tests</h3>
|
||||||
|
<label class="input input-sm input-bordered flex items-center gap-2 w-full">
|
||||||
|
<Search class="w-4 h-4 text-gray-400" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="grow bg-transparent outline-none text-sm"
|
||||||
|
placeholder="Search by code or name..."
|
||||||
|
bind:value={searchQuery}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 overflow-y-auto p-2 space-y-1 min-h-0">
|
||||||
|
{#if availableTests.length === 0}
|
||||||
|
<div class="text-center py-8 text-gray-500">
|
||||||
|
<Box class="w-10 h-10 mx-auto mb-2 opacity-50" />
|
||||||
|
<p class="text-sm">No tests available</p>
|
||||||
|
<p class="text-xs opacity-70">
|
||||||
|
{searchQuery ? 'Try a different search term' : 'All tests are already added'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
{#each availableTests as test (test.TestSiteID)}
|
||||||
|
<div class="flex items-center justify-between p-2 hover:bg-base-200 rounded-md group">
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="font-mono text-xs text-gray-500 w-8">{test.SeqScr || '-'}</span>
|
||||||
|
<span class="font-mono text-sm font-medium truncate">{test.TestSiteCode}</span>
|
||||||
|
<span class="badge badge-xs badge-ghost">{test.TestType}</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-gray-600 truncate pl-10">{test.TestSiteName}</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="btn btn-ghost btn-xs opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
onclick={() => addMember(test)}
|
||||||
|
title="Add to calculation"
|
||||||
|
>
|
||||||
|
<Plus class="w-4 h-4 text-primary" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-2 border-t border-base-300 text-xs text-gray-500 text-center shrink-0">
|
||||||
|
{availableTests.length} tests available
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col border border-base-300 rounded-lg bg-base-100 overflow-hidden">
|
||||||
|
<div class="p-3 border-b border-base-300 bg-base-200 rounded-t-lg shrink-0">
|
||||||
|
<div class="flex items-center justify-between gap-2">
|
||||||
|
<h3 class="text-sm font-medium text-gray-700">Selected Members</h3>
|
||||||
|
<span class="badge badge-sm badge-ghost">{members.length} selected</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 overflow-y-auto min-h-0">
|
||||||
|
{#if members.length === 0}
|
||||||
|
<div class="flex flex-col items-center justify-center h-full text-gray-500 py-8">
|
||||||
|
<Box class="w-12 h-12 mb-3 opacity-50" />
|
||||||
|
<p class="text-sm font-medium">No members selected</p>
|
||||||
|
<p class="text-xs opacity-70 mt-1">Click the + button on available tests to add them</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<table class="table table-sm w-full">
|
||||||
|
<thead class="sticky top-0 bg-base-200">
|
||||||
|
<tr>
|
||||||
|
<th class="w-12 text-center text-xs">Seq</th>
|
||||||
|
<th class="w-20 text-xs">Code</th>
|
||||||
|
<th class="text-xs">Name</th>
|
||||||
|
<th class="w-16 text-xs">Type</th>
|
||||||
|
<th class="w-10 text-center text-xs"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each members as member (member.TestSiteID)}
|
||||||
|
<tr class="hover:bg-base-200">
|
||||||
|
<td class="text-center font-mono text-xs text-gray-600">{member.SeqScr}</td>
|
||||||
|
<td class="font-mono text-xs">{member.TestSiteCode}</td>
|
||||||
|
<td class="text-xs truncate max-w-[150px]" title={member.TestSiteName}>
|
||||||
|
{member.TestSiteName}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge badge-xs badge-ghost">{member.TestType}</span>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<button
|
||||||
|
class="btn btn-ghost btn-xs text-error p-0 min-h-0 h-auto"
|
||||||
|
onclick={() => removeMember(member.TestSiteID)}
|
||||||
|
title="Remove"
|
||||||
|
>
|
||||||
|
<Trash2 class="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-2 border-t border-base-300 text-xs text-gray-500 shrink-0">
|
||||||
|
<p>Use {'{CODE}'} tokens from these members in your formula.</p>
|
||||||
|
{#if validationErrors.members}
|
||||||
|
<p class="text-error mt-1">{validationErrors.members}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Formula Definition -->
|
<!-- Formula Definition -->
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-sm font-semibold text-gray-700 mb-3">Formula Definition</h3>
|
<h3 class="text-sm font-semibold text-gray-700 mb-3">Formula Definition</h3>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="space-y-1">
|
|
||||||
<label for="formulaInput" class="block text-sm font-medium text-gray-700">Formula Description</label>
|
|
||||||
<input
|
|
||||||
id="formulaInput"
|
|
||||||
type="text"
|
|
||||||
class="input input-sm input-bordered w-full"
|
|
||||||
bind:value={formData.details.FormulaInput}
|
|
||||||
placeholder="e.g., Hemoglobin plus MCV"
|
|
||||||
oninput={handleFieldChange}
|
|
||||||
/>
|
|
||||||
<span class="text-xs text-gray-500">Human-readable description of the calculation</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<label for="formulaCode" class="block text-sm font-medium text-gray-700">
|
<label for="formulaCode" class="block text-sm font-medium text-gray-700">
|
||||||
Formula Code <span class="text-error">*</span>
|
Formula Code <span class="text-error">*</span>
|
||||||
@ -48,19 +294,38 @@
|
|||||||
class="textarea textarea-sm textarea-bordered w-full font-mono text-sm"
|
class="textarea textarea-sm textarea-bordered w-full font-mono text-sm"
|
||||||
bind:value={formData.details.FormulaCode}
|
bind:value={formData.details.FormulaCode}
|
||||||
placeholder="e.g., {'{HGB}'} + {'{MCV}'} + {'{MCHC}'}"
|
placeholder="e.g., {'{HGB}'} + {'{MCV}'} + {'{MCHC}'}"
|
||||||
rows="3"
|
rows="4"
|
||||||
oninput={handleFieldChange}
|
oninput={handleFieldChange}
|
||||||
required
|
required
|
||||||
></textarea>
|
></textarea>
|
||||||
<span class="text-xs">
|
<div class="flex flex-wrap items-center gap-2 text-xs">
|
||||||
{#if formData?.details?.FormulaCode && validateFormula(formData.details.FormulaCode)}
|
{#if formulaSyntax.tone === 'success'}
|
||||||
<span class="text-success">Valid formula syntax</span>
|
<span class="badge badge-success badge-outline">{formulaSyntax.text}</span>
|
||||||
{:else if formData?.details?.FormulaCode}
|
{:else if formulaSyntax.tone === 'warning'}
|
||||||
<span class="text-warning">No test references found</span>
|
<span class="badge badge-warning badge-outline">{formulaSyntax.text}</span>
|
||||||
|
{:else if formulaSyntax.tone === 'error'}
|
||||||
|
<span class="badge badge-error badge-outline">{formulaSyntax.text}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="text-gray-500">Enter formula with test code references</span>
|
<span class="text-gray-500">{formulaSyntax.text}</span>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<span class="badge badge-ghost badge-outline">{formulaTokens.length} token(s)</span>
|
||||||
|
|
||||||
|
<button class="btn btn-ghost btn-xs" type="button" onclick={prettifyFormula}>
|
||||||
|
<WandSparkles class="w-3.5 h-3.5" />
|
||||||
|
Prettify
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if validationErrors.FormulaCode}
|
||||||
|
<p class="text-error text-xs mt-1">{validationErrors.FormulaCode}</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if members.length > 0 && missingMemberCodes.length > 0}
|
||||||
|
<p class="text-warning text-xs mt-1">
|
||||||
|
Missing member references: {missingMemberCodes.map((code) => `{${code}}`).join(', ')}
|
||||||
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user