feat(tests): improve calc member handling and update project docs

This commit is contained in:
mahdahar 2026-03-11 16:44:55 +07:00
parent 22ee1ebfd1
commit 39cdbb0464
16 changed files with 480 additions and 1266 deletions

1
.serena/.gitignore vendored
View File

@ -1 +1,2 @@
/cache /cache
/project.local.yml

View File

@ -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 || '';
}
```

View File

@ -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

View File

@ -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?

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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"
```

View File

@ -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

View File

@ -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

View File

@ -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 readonly. # list of regex patterns which, when matched, mark a memory entry as readonly.
# 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:

View File

@ -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

View File

@ -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
})) }))
}; };
} }

View File

@ -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[];
}; };

View File

@ -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');
currentTab = 'basic'; if (validationErrors.FormulaCode || validationErrors.members) {
currentTab = 'calc';
} else {
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

View File

@ -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} {/if}
</span>
<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}
</div> </div>
</div> </div>
</div> </div>