diff --git a/.serena/.gitignore b/.serena/.gitignore index 14d86ad..2e510af 100644 --- a/.serena/.gitignore +++ b/.serena/.gitignore @@ -1 +1,2 @@ /cache +/project.local.yml diff --git a/.serena/memories/code_style_conventions.md b/.serena/memories/code_style_conventions.md deleted file mode 100644 index 8f2f42c..0000000 --- a/.serena/memories/code_style_conventions.md +++ /dev/null @@ -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 - - - - -``` - -### 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 || ''; -} -``` diff --git a/.serena/memories/plans/mvp-roadmap.md b/.serena/memories/plans/mvp-roadmap.md deleted file mode 100644 index 0204111..0000000 --- a/.serena/memories/plans/mvp-roadmap.md +++ /dev/null @@ -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 - diff --git a/.serena/memories/plans/testmap-dropdown-change.md b/.serena/memories/plans/testmap-dropdown-change.md deleted file mode 100644 index 1fae6e6..0000000 --- a/.serena/memories/plans/testmap-dropdown-change.md +++ /dev/null @@ -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 - -``` - -### 3. Replace Client ID Input with Dropdown -Replace lines 260-265 with: -```svelte - -``` - -## 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? \ No newline at end of file diff --git a/.serena/memories/project_overview.md b/.serena/memories/project_overview.md index cd7f45d..82a5a9c 100644 --- a/.serena/memories/project_overview.md +++ b/.serena/memories/project_overview.md @@ -1,62 +1,29 @@ -# CLQMS Frontend - Project Overview +# CLQMS Frontend Project Overview -## Project Purpose -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. +- Purpose: Frontend for Clinical Laboratory Quality Management System (CLQMS), handling authenticated lab quality 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 -- **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 +## Tech stack -## Architecture Principles -- **KISS**: Keep It Simple - plain JavaScript, minimal tooling -- **Manual API Wrapper**: No codegen, simple fetch-based API client -- **File-based Routing**: Standard SvelteKit routing patterns -- **Server-side Auth Check**: SvelteKit hooks for session validation +- SvelteKit `^2.50.2` +- Svelte `^5.49.2` with runes (`$props`, `$state`, `$derived`, `$effect`, `$bindable`) +- Vite `^7.3.1` +- Tailwind CSS 4 + DaisyUI 5 +- Lucide Svelte icons +- Package manager: pnpm +- Module system: ES Modules (`"type": "module"`) -## Project 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 -``` +## Rough structure -## Key Features Implemented -- Phase 0: Foundation (Auth, base API client, layouts) ✅ -- Phase 1: Foundation Data (ValueSets, Locations, Contacts, Occupations, Specialties, Counters, Geography) ✅ -- Phase 2a: Patient CRUD ✅ -- Phase 11: Authentication (login/logout, JWT handling) ✅ - -## Pending Features -- Phase 2b: Advanced Patient Features -- Phase 3: Patient Visits (list page exists, needs create/edit forms) -- 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) +- `src/lib/api/` API client and feature endpoints +- `src/lib/stores/` shared stores (auth, config, valuesets) +- `src/lib/components/` reusable UI (Modal, DataTable, Sidebar) +- `src/lib/utils/` helpers and toast utilities +- `src/lib/types/` TS type definitions +- `src/routes/(app)/` authenticated pages (`dashboard`, `patients`, `master-data`) +- `src/routes/login/` public login route +- `static/` static assets +- `build/` production output \ No newline at end of file diff --git a/.serena/memories/refactoring/patient-page.md b/.serena/memories/refactoring/patient-page.md deleted file mode 100644 index c99ca77..0000000 --- a/.serena/memories/refactoring/patient-page.md +++ /dev/null @@ -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 diff --git a/.serena/memories/style_and_conventions.md b/.serena/memories/style_and_conventions.md index 003e71c..4aaf182 100644 --- a/.serena/memories/style_and_conventions.md +++ b/.serena/memories/style_and_conventions.md @@ -1,156 +1,42 @@ -# CLQMS Frontend - Code Style and Conventions +# Style and Conventions -## JavaScript/TypeScript -- **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 +Primary source: `AGENTS.md`. -## Svelte Components (Svelte 5 Runes) +## JavaScript/TypeScript style -### Component Structure -```svelte - -``` +- Use ES modules (`import`/`export`). +- Semicolons required. +- Single quotes for strings. +- 2-space indentation. +- Trailing commas in multi-line arrays/objects. +- Document exported functions with JSDoc including `@param` and `@returns`. -## Naming Conventions -- **Components**: PascalCase (`LoginForm.svelte`, `DataTable.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`, `valuesets`) -- **Event handlers**: prefix with `handle` (`handleSubmit`, `handleClick`) +## Import ordering -## Imports Order -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`) +1. Svelte / `$app/*` +2. `$lib/*` +3. External libraries (e.g., `lucide-svelte`) +4. Relative imports (minimize, prefer `$lib`) -## API Client Pattern +## Naming -### Base Client (src/lib/api/client.js) -The base client handles JWT token management and 401 redirects automatically. +- Components: PascalCase (`LoginForm.svelte`) +- Route/files: lowercase with hyphens +- Variables/stores: camelCase +- Constants: UPPER_SNAKE_CASE +- Event handlers: `handle...` +- Form state fields: `formLoading`, `formError`, etc. -### 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'; +## Svelte 5 patterns -export async function getItem(id) { - return get(`/api/items/${id}`); -} +- Follow component script order: imports -> props -> state -> derived -> effects -> handlers. +- Prefer DaisyUI component classes (`btn`, `input`, `card`, etc.). +- For icon inputs, use DaisyUI label+input flex pattern (not absolute-positioned icons). +- Access browser-only APIs behind `$app/environment` `browser` checks. -export async function createItem(data) { - return post('/api/items', data); -} -``` +## API/store patterns -## 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 +- Use shared API helpers from `$lib/api/client.js` (`get/post/put/patch/del`). +- Build query strings using `URLSearchParams`. +- Use try/catch with toast error/success utilities. +- LocalStorage keys should be descriptive (e.g., `clqms_username`, `auth_token`). \ No newline at end of file diff --git a/.serena/memories/suggested_commands.md b/.serena/memories/suggested_commands.md index 7fe4013..ebcd98e 100644 --- a/.serena/memories/suggested_commands.md +++ b/.serena/memories/suggested_commands.md @@ -1,219 +1,32 @@ -# CLQMS Frontend - Suggested Commands +# Suggested Commands (Windows project shell) -## Development Commands +## Core project commands (pnpm) -```bash -# Start development server -pnpm run dev +- `pnpm install` - install dependencies. +- `pnpm run dev` - run local dev server. +- `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 -pnpm run dev -- --open +## Testing/linting/formatting status -# Production build -pnpm run build +- No lint command configured yet. +- 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 -pnpm run preview +## Useful Windows shell commands -# Sync SvelteKit (runs automatically on install) -pnpm run prepare -``` +- `dir` - list files (cmd). +- `Get-ChildItem` or `ls` - list files (PowerShell). +- `cd ` - 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 -- **Primary**: `pnpm` (preferred) -- Can also use `npm` or `yarn` if needed +## Environment notes -## Development Server -- Dev server runs by default on `http://localhost:5173` -- 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" -``` +- Node.js 18+ required. +- Backend API should be running at `http://localhost:8000` for dev proxying. \ No newline at end of file diff --git a/.serena/memories/task_completion.md b/.serena/memories/task_completion.md deleted file mode 100644 index 414dc36..0000000 --- a/.serena/memories/task_completion.md +++ /dev/null @@ -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 diff --git a/.serena/memories/task_completion_checklist.md b/.serena/memories/task_completion_checklist.md index 6f07555..41d3acc 100644 --- a/.serena/memories/task_completion_checklist.md +++ b/.serena/memories/task_completion_checklist.md @@ -1,89 +1,15 @@ -# CLQMS Frontend - Post-Task Checklist +# Task Completion Checklist -## When a Task is Completed +Given current project setup: -### Verification Steps - -1. **Build Check** (if changes are significant) - - Run `pnpm run build` to ensure the application builds successfully - - Check for build errors or warnings - -2. **Manual Testing** - - Start dev server: `pnpm run dev` - - Test the feature/fix in the browser - - Verify no console errors - - Check that existing functionality is not broken - -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 +1. Run relevant build verification: + - `pnpm run build` +2. If runtime behavior changed, also sanity check with: + - `pnpm run dev` (manual smoke test) + - optionally `pnpm run preview` for production-like validation +3. Since lint/format/test scripts are not configured, mention this explicitly in handoff. +4. Ensure code follows `AGENTS.md` conventions: + - semicolons, single quotes, import order, Svelte 5 rune patterns, naming. +5. For API/auth/localStorage changes: + - verify browser-only access guards (`browser`) and auth redirect behavior are preserved. +6. In final handoff, include changed file paths and any manual verification steps performed. \ No newline at end of file diff --git a/.serena/project.yml b/.serena/project.yml index e04b5a7..49f31c8 100644 --- a/.serena/project.yml +++ b/.serena/project.yml @@ -32,11 +32,24 @@ languages: # For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings 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 ignore_all_files_in_gitignore: true -# list of additional paths to ignore in all projects -# same syntax as gitignore, so you can use * and ** +# list of additional paths to ignore in this project. +# Same syntax as gitignore, so you can use * and **. +# Note: global ignored_paths from serena_config.yml are also applied additively. ignored_paths: [] # whether the project is in read-only mode @@ -111,22 +124,12 @@ default_modes: # (contrary to the memories, which are loaded on demand). initial_prompt: "" -# override of the corresponding setting in serena_config.yml, see the documentation there. -# If null or missing, the value from the global config is used. +# time budget (seconds) per tool call for the retrieval of additional symbol information +# 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: -# 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. # Extends the list from the global configuration, merging the two lists. 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: diff --git a/docs/api-docs.bundled.yaml b/docs/api-docs.bundled.yaml index 9d35699..314c9c2 100644 --- a/docs/api-docs.bundled.yaml +++ b/docs/api-docs.bundled.yaml @@ -1454,6 +1454,12 @@ paths: type: string DisciplineCode: type: string + SeqScr: + type: integer + description: Display order on screen + SeqRpt: + type: integer + description: Display order in reports responses: '200': description: Discipline updated @@ -5532,6 +5538,12 @@ components: type: string DisciplineCode: type: string + SeqScr: + type: integer + description: Display order on screen + SeqRpt: + type: integer + description: Display order in reports Department: type: object properties: @@ -5890,9 +5902,6 @@ components: EndDate: type: string format: date-time - FormulaInput: - type: string - description: Input variables for calculated tests FormulaCode: type: string description: Formula expression for calculated tests @@ -5903,7 +5912,7 @@ components: type: object testdefgrp: type: array - description: Group members (only for GROUP type) + description: Group members (for GROUP and CALC types) items: type: object properties: @@ -6154,10 +6163,18 @@ components: - TestCalID: 1 DisciplineID: 2 DepartmentID: 2 - FormulaInput: CREA,AGE,GENDER FormulaCode: CKD_EPI(CREA,AGE,GENDER) Unit1: mL/min/1.73m2 Decimal: 0 + testdefgrp: + - TestSiteID: 21 + TestSiteCode: CREA + TestSiteName: Creatinine + TestType: TEST + - TestSiteID: 51 + TestSiteCode: AGE + TestSiteName: Age + TestType: PARAM refnum: - RefNumID: 5 NumRefType: NMRC diff --git a/src/lib/api/tests.js b/src/lib/api/tests.js index ba78b3c..c7d2c33 100644 --- a/src/lib/api/tests.js +++ b/src/lib/api/tests.js @@ -89,17 +89,25 @@ function buildPayload(formData, isUpdate = false) { // Add formula fields for CALC type if (type === 'CALC') { - payload.details.FormulaInput = formData.details?.FormulaInput || ''; 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 - if (type === 'GROUP' && formData.details?.members) { + if (type === 'GROUP') { + const groupMembers = formData.testdefgrp || formData.details?.members || []; payload.details = { - members: formData.details.members.map(m => ({ + members: groupMembers.map(m => ({ TestSiteID: m.TestSiteID, - Member: m.Member + Member: m.Member || m.TestSiteID })) }; } diff --git a/src/lib/types/test.types.ts b/src/lib/types/test.types.ts index 9f9b513..eaf68aa 100644 --- a/src/lib/types/test.types.ts +++ b/src/lib/types/test.types.ts @@ -56,7 +56,6 @@ export interface TechDetail { // Calculation Details (extends TechDetail) export interface CalcDetails extends TechDetail { - FormulaInput?: string; FormulaCode?: string; } @@ -154,7 +153,6 @@ export interface TestDetail { ExpectedTAT?: number; // Calculated test specific - FormulaInput?: string; FormulaCode?: string; // Nested data @@ -199,7 +197,6 @@ export interface CreateTestPayload { ExpectedTAT?: number; // CALC only - FormulaInput?: string; FormulaCode?: string; // GROUP only @@ -248,7 +245,6 @@ export interface TestFormState { CollReq?: string; Method?: string; ExpectedTAT?: number; - FormulaInput?: string; FormulaCode?: string; members?: number[]; }; diff --git a/src/routes/(app)/master-data/tests/test-modal/TestFormModal.svelte b/src/routes/(app)/master-data/tests/test-modal/TestFormModal.svelte index f87406c..13c73bf 100644 --- a/src/routes/(app)/master-data/tests/test-modal/TestFormModal.svelte +++ b/src/routes/(app)/master-data/tests/test-modal/TestFormModal.svelte @@ -129,7 +129,6 @@ CollReq: '', Method: '', ExpectedTAT: null, - FormulaInput: '', FormulaCode: '', members: [] }, @@ -185,8 +184,7 @@ CollReq: test.CollReq || '', Method: test.Method || '', ExpectedTAT: test.ExpectedTAT || null, - FormulaInput: test.FormulaInput || '', - FormulaCode: test.FormulaCode || '', + FormulaCode: test.testdefcal?.[0]?.FormulaCode || test.FormulaCode || '', members: test.testdefgrp?.map(m => ({ TestSiteID: m.TestSiteID, TestSiteCode: m.TestSiteCode, @@ -264,6 +262,39 @@ 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; return Object.keys(errors).length === 0; } @@ -271,7 +302,11 @@ async function handleSave() { if (!validateForm()) { toastError('Please fix validation errors'); - currentTab = 'basic'; + if (validationErrors.FormulaCode || validationErrors.members) { + currentTab = 'calc'; + } else { + currentTab = 'basic'; + } return; } @@ -318,7 +353,7 @@
Please fix the following errors: @@ -370,21 +405,10 @@ /> {:else if currentTab === 'calc'} - {:else if currentTab === 'group'} - - {:else if currentTab === 'calc'} - {:else if currentTab === 'group'} {/snippet} - \ No newline at end of file + diff --git a/src/routes/(app)/master-data/tests/test-modal/tabs/CalcDetailsTab.svelte b/src/routes/(app)/master-data/tests/test-modal/tabs/CalcDetailsTab.svelte index 8027838..642d1c4 100644 --- a/src/routes/(app)/master-data/tests/test-modal/tabs/CalcDetailsTab.svelte +++ b/src/routes/(app)/master-data/tests/test-modal/tabs/CalcDetailsTab.svelte @@ -1,18 +1,163 @@ -
+

Calculated Test Formula

@@ -22,23 +167,124 @@
+
+
+
+

Available Tests

+ +
+ +
+ {#if availableTests.length === 0} +
+ +

No tests available

+

+ {searchQuery ? 'Try a different search term' : 'All tests are already added'} +

+
+ {:else} + {#each availableTests as test (test.TestSiteID)} +
+
+
+ {test.SeqScr || '-'} + {test.TestSiteCode} + {test.TestType} +
+

{test.TestSiteName}

+
+ +
+ {/each} + {/if} +
+ +
+ {availableTests.length} tests available +
+
+ +
+
+
+

Selected Members

+ {members.length} selected +
+
+ +
+ {#if members.length === 0} +
+ +

No members selected

+

Click the + button on available tests to add them

+
+ {:else} + + + + + + + + + + + + {#each members as member (member.TestSiteID)} + + + + + + + + {/each} + +
SeqCodeNameType
{member.SeqScr}{member.TestSiteCode} + {member.TestSiteName} + + {member.TestType} + + +
+ {/if} +
+ +
+

Use {'{CODE}'} tokens from these members in your formula.

+ {#if validationErrors.members} +

{validationErrors.members}

+ {/if} +
+
+
+

Formula Definition

-
- - - Human-readable description of the calculation -
-