- Consolidate fragmented test modal components into unified TestFormModal.svelte - Reorganize reference range components into organized tabs (RefNumTab, RefTxtTab) - Add new tab components: BasicInfoTab, TechDetailsTab, CalcDetailsTab, MappingsTab, GroupMembersTab - Move test-related type definitions to src/lib/types/test.types.ts for better type organization - Delete old component files: TestModal.svelte and 10+ sub-components - Add backup directory for old component preservation - Update AGENTS.md with coding guidelines and project conventions - Update tests.js API client with improved structure - Add documentation for frontend test management architecture
57 KiB
CLQMS Master Data - Test Management Frontend Development Prompt
📋 Project Overview
Build a modern, responsive Svelte 5 frontend for CLQMS (Clinical Laboratory Quality Management System) Test Management module. The frontend will consume the existing REST API backend (/api/tests) and provide a comprehensive interface for managing laboratory test definitions across all test types.
🎯 Objective
Create a user-friendly, type-safe frontend application that enables laboratory administrators to:
- Browse and search all test definitions with filtering
- Create new tests of any type (TEST, PARAM, CALC, GROUP, TITLE)
- Edit existing tests with type-specific configurations
- Manage reference ranges (numeric and text-based)
- Configure test mappings for external systems
- Organize tests into groups and panels
- Manage calculated test formulas
🛠️ Technology Stack (Svelte 5)
Core Requirements
- Framework: Svelte 5 with runes (
$state,$derived,$effect) - Meta-Framework: SvelteKit (for routing and server-side features)
- Language: TypeScript (strict mode enabled)
- Styling: Tailwind CSS
- UI Components: Skeleton UI (or Melt UI)
- Forms: Headless form components with validation
- HTTP Client: Axios (with request/response interceptors)
- State Management: Svelte 5 runes (no external store library required)
- Build Tool: Vite (comes with SvelteKit)
Recommended Dependencies
{
"dependencies": {
"svelte": "^5.0.0",
"@sveltejs/kit": "^2.0.0",
"axios": "^1.6.0",
"zod": "^3.22.0",
"date-fns": "^3.0.0"
},
"devDependencies": {
"skeleton": "^2.8.0",
"@skeletonlabs/tw-plugin": "^0.3.0",
"tailwindcss": "^3.4.0",
"autoprefixer": "^10.4.0",
"postcss": "^8.4.0",
"typescript": "^5.3.0"
}
}
📁 Project Structure
src/
├── lib/
│ ├── components/
│ │ ├── test/
│ │ │ ├── TestList.svelte # Main test listing page
│ │ │ ├── TestForm.svelte # Main form container with tabs
│ │ │ ├── TestCard.svelte # Single test card/row
│ │ │ ├── TestFilterPanel.svelte # Search/filter panel
│ │ │ ├── SidebarTabs.svelte # Left navigation tabs
│ │ │ ├── tabs/
│ │ │ │ ├── BasicInfoTab.svelte # Basic test info
│ │ │ │ ├── TechDetailsTab.svelte # Technical specifications
│ │ │ │ ├── CalcDetailsTab.svelte # Calculated test formula
│ │ │ │ ├── GroupMembersTab.svelte # Group member management
│ │ │ │ ├── MappingsTab.svelte # System mappings
│ │ │ │ ├── RefNumTab.svelte # Numeric reference ranges
│ │ │ │ └── RefTxtTab.svelte # Text reference ranges
│ │ │ └── modals/
│ │ │ ├── RefNumModal.svelte # Edit reference range modal
│ │ │ ├── RefTxtModal.svelte # Edit text reference modal
│ │ │ ├── MappingModal.svelte # Edit mapping modal
│ │ │ └── MemberModal.svelte # Add group member modal
│ │ └── ui/ # Reusable UI components
│ │ ├── Button.svelte
│ │ ├── Input.svelte
│ │ ├── Select.svelte
│ │ ├── Checkbox.svelte
│ │ ├── Table.svelte
│ │ ├── Modal.svelte
│ │ ├── Badge.svelte
│ │ ├── Tabs.svelte
│ │ ├── Alert.svelte
│ │ └── Spinner.svelte
│ ├── stores/
│ │ ├── testStore.ts # Test form state with runes
│ │ ├── valueSetStore.ts # ValueSet/dropdown data
│ │ ├── authStore.ts # Authentication state
│ │ └── uiStore.ts # UI state (modals, tabs)
│ ├── services/
│ │ ├── api.ts # Axios instance with interceptors
│ │ ├── testService.ts # Test API operations
│ │ ├── valueSetService.ts # ValueSet API calls
│ │ └── validationService.ts # Frontend validation logic
│ ├── types/
│ │ ├── test.types.ts # All test-related types
│ │ ├── api.types.ts # API response/request types
│ │ ├── valueset.types.ts # ValueSet types
│ │ └── index.ts # Type exports
│ └── utils/
│ ├── validation.ts # Validation helpers
│ ├── format.ts # Formatters (dates, numbers)
│ ├── constants.ts # App constants
│ └── helpers.ts # Utility functions
├── routes/
│ ├── +layout.svelte # Root layout with nav
│ ├── +page.svelte # Landing page
│ ├── tests/
│ │ ├── +page.svelte # Test list page
│ │ └── [id]/
│ │ └── +page.svelte # Test detail/edit page
│ └── login/
│ └── +page.svelte # Login page
├── app.html # HTML template
└── app.css # Global styles
🔌 API Integration
Base Configuration
API Base URL: http://localhost:8080/api (configurable via environment variable)
Authentication: JWT token via HTTP header
Authorization: Bearer {token}
Endpoints
1. List Tests
GET /api/tests
Query Parameters:
- SiteID (optional): Filter by site
- TestType (optional): Filter by test type (TEST, PARAM, CALC, GROUP, TITLE)
- VisibleScr (optional): Filter by screen visibility (0/1)
- VisibleRpt (optional): Filter by report visibility (0/1)
- TestSiteName (optional): Search by test name (partial match)
Response:
{
status: "success";
message: string;
data: TestSummary[];
}
interface TestSummary {
TestSiteID: number;
TestSiteCode: string;
TestSiteName: string;
TestType: string;
TestTypeLabel: string;
SeqScr: number;
SeqRpt: number;
VisibleScr: number;
VisibleRpt: number;
CountStat: number;
StartDate: string;
DisciplineID?: number;
DepartmentID?: number;
DisciplineName?: string;
DepartmentName?: string;
}
2. Get Single Test
GET /api/tests/:id
Response:
{
status: "success";
message: string;
data: TestDetail;
}
interface TestDetail {
// Base fields
TestSiteID: number;
TestSiteCode: string;
TestSiteName: string;
TestType: string;
TestTypeLabel: string;
Description?: string;
SiteID: number;
SeqScr: number;
SeqRpt: number;
VisibleScr: number;
VisibleRpt: number;
CountStat: number;
StartDate: string;
EndDate?: string;
// Technical details (TEST/PARAM/CALC)
DisciplineID?: number;
DepartmentID?: number;
DisciplineName?: string;
DepartmentName?: string;
ResultType?: string;
RefType?: string;
VSet?: string;
Unit1?: string;
Factor?: number;
Unit2?: string;
Decimal?: number;
ReqQty?: number;
ReqQtyUnit?: string;
CollReq?: string;
Method?: string;
ExpectedTAT?: number;
// Nested data based on TestType
testdefcal?: Calculation[]; // For CALC type
testdefgrp?: GroupMember[]; // For GROUP type
testmap?: TestMapping[]; // For all types
testdeftech?: TechDetail[]; // For TEST/PARAM
refnum?: RefNumRange[]; // For TEST/PARAM (numeric ref)
reftxt?: RefTxtRange[]; // For TEST/PARAM (text ref)
}
interface RefNumRange {
RefNumID: number;
NumRefType: string; // REF, CRTC, VAL, RERUN
NumRefTypeLabel: string;
RangeType: string; // RANGE, THOLD
RangeTypeLabel: string;
Sex: string; // 0=All, 1=Female, 2=Male
SexLabel: string;
AgeStart: number;
AgeEnd: number;
LowSign?: string; // =, <, <=
LowSignLabel?: string;
Low?: number;
HighSign?: string; // =, >, >=
HighSignLabel?: string;
High?: number;
Flag?: string; // H, L, A, etc.
Interpretation?: string;
}
interface RefTxtRange {
RefTxtID: number;
TxtRefType: string; // Normal, Abnormal, Critical
TxtRefTypeLabel: string;
Sex: string;
SexLabel: string;
AgeStart: number;
AgeEnd: number;
RefTxt: string;
Flag?: string;
}
3. Create Test
POST /api/tests
Content-Type: application/json
Body: CreateTestPayload
Request:
interface CreateTestPayload {
SiteID: number;
TestSiteCode: string;
TestSiteName: string;
TestType: 'TEST' | 'PARAM' | 'CALC' | 'GROUP' | 'TITLE';
Description?: string;
SeqScr?: number;
SeqRpt?: number;
VisibleScr?: number;
VisibleRpt?: number;
CountStat?: number;
StartDate?: string;
// Nested details (based on TestType)
details?: {
// Technical (TEST/PARAM/CALC)
DisciplineID?: number;
DepartmentID?: number;
ResultType?: string;
RefType?: string;
VSet?: string;
Unit1?: string;
Factor?: number;
Unit2?: string;
Decimal?: number;
ReqQty?: number;
ReqQtyUnit?: string;
CollReq?: string;
Method?: string;
ExpectedTAT?: number;
// CALC only
FormulaInput?: string;
FormulaCode?: string;
// GROUP only
members?: number[]; // Array of TestSiteIDs
};
// Reference ranges (TEST/PARAM)
refnum?: Omit<RefNumRange, 'RefNumID' | 'NumRefTypeLabel' | 'RangeTypeLabel' | 'SexLabel' | 'LowSignLabel' | 'HighSignLabel'>[];
reftxt?: Omit<RefTxtRange, 'RefTxtID' | 'TxtRefTypeLabel' | 'SexLabel'>[];
// Mappings (all types)
testmap?: TestMapping[];
}
Response:
{
status: "created";
message: "Test created successfully";
data: { TestSiteId: number };
}
4. Update Test
PATCH /api/tests
Content-Type: application/json
Body: CreateTestPayload & { TestSiteID: number }
Response:
{
status: "success";
message: "Test updated successfully";
data: { TestSiteId: number };
}
5. Delete Test (Soft Delete)
DELETE /api/tests
Content-Type: application/json
Body: { TestSiteID: number }
Response:
{
status: "success";
message: "Test disabled successfully";
data: { TestSiteId: number; EndDate: string };
}
🎨 UI/UX Design Specifications
Layout Architecture
Page Layout: Fixed sidebar + scrollable content area
┌─────────────────────────────────────────────────────────────┐
│ Header: CLQMS Test Management [User] [Logout] │
├────────────┬────────────────────────────────────────────────┤
│ │ │
│ Sidebar │ Main Content Area │
│ (Left) │ │
│ │ │
│ Tab 1 │ ┌──────────────────────────────────────────┐ │
│ Tab 2 │ │ │ │
│ Tab 3 │ │ Dynamic Content │ │
│ ... │ │ │ │
│ │ │ │ │
│ │ │ │ │
│ │ └──────────────────────────────────────────┘ │
│ │ │
│ │ [Save] [Cancel] [Delete] │
│ │ │
└────────────┴────────────────────────────────────────────────┘
Test List Page
Components:
-
Filter Panel (top of page):
- Site dropdown (multi-select)
- Test Type dropdown (checkboxes)
- Visibility toggles (Screen/Report)
- Search input (debounced, 300ms)
- Clear filters button
-
Test Table/Card Grid:
- Columns: Code, Name, Type, Discipline, Department, SeqScr, SeqRpt, Visibility, Actions
- Sortable headers (Code, Name, SeqScr, SeqRpt)
- Row actions: View, Edit, Delete (with confirmation)
- Row hover effect
- Test type badge with color coding
-
Pagination:
- Show 20 items per page
- Page navigation buttons
- Page size selector (10, 20, 50, 100)
- Total count display
Table Design:
┌─────────┬────────────────────┬───────┬────────────┬────────┬────────┬────────┬────────────┬─────────┐
│ Code │ Name │ Type │ Discipline │ Dept │ ScrVis │ RptVis │ Actions │ │
├─────────┼────────────────────┼───────┼────────────┼────────┼────────┼────────┼────────────┼─────────┤
│ CBC │ Complete Blood │ TEST │ Hematology │ Hema │ ☑ │ ☑ │ 👁️ ✏️ 🗑️ │ │
│ │ Count │ │ │ │ │ │ │ │
│ HGB │ Hemoglobin │ PARAM │ Hematology │ Hema │ ☑ │ ☑ │ 👁️ ✏️ 🗑️ │ │
│ CALC_A1C│ A1C Calculated │ CALC │ Chemistry │ Chem │ ☑ │ ☑ │ 👁️ ✏️ 🗑️ │ │
│ CMP_GRP │ Comprehensive │ GROUP │ - │ - │ ☑ │ ☐ │ 👁️ ✏️ 🗑️ │ │
│ │ Panel │ │ │ │ │ │ │ │
│ HEADER1 │ Chemistry Results │ TITLE │ - │ - │ ☐ │ ☑ │ 👁️ ✏️ 🗑️ │ │
└─────────┴────────────────────┴───────┴────────────┴────────┴────────┴────────┴────────────┴─────────┘
Test Form Page
Left Sidebar Tabs (Navigation):
┌──────────────┐
│ Basic Info │
├──────────────┤
│ Tech Details │
├──────────────┤
│ Calculations │ (CALC only)
├──────────────┤
│ Group Memb │ (GROUP only)
├──────────────┤
│ Mappings │
├──────────────┤
│ Ref Num │ (TEST/PARAM/CALC)
├──────────────┤
│ Ref Txt │ (TEST/PARAM)
└──────────────┘
Tab Visibility Rules:
| Tab | TEST | PARAM | CALC | GROUP | TITLE |
|---|---|---|---|---|---|
| Basic Info | ✅ | ✅ | ✅ | ✅ | ✅ |
| Tech Details | ✅ | ✅ | ❌ | ❌ | ❌ |
| Calculations | ❌ | ❌ | ✅ | ❌ | ❌ |
| Group Members | ❌ | ❌ | ❌ | ✅ | ❌ |
| Mappings | ✅ | ✅ | ✅ | ✅ | ✅ |
| Ref Num | ✅ | ✅ | ✅ | ❌ | ❌ |
| Ref Txt | ✅ | ✅ | ❌ | ❌ | ❌ |
Active Tab Styling:
- Left border accent color (primary theme color)
- Light background highlight
- Bold text
- Icon indicator
Tab Content Specifications
1. Basic Info Tab
Form Layout (Two-column grid):
┌──────────────────────────────────────────────────────────────────┐
│ Test Code: [CBC_______] *Required │
│ Test Name: [Complete Blood Count________] *Required │
│ Test Type: [TEST ▼] (dropdown) │
│ Description: [Standard hematology test_______________] │
│ │
│ Site: [Main Lab ▼] │
│ │
│ Screen Seq: [1___] Report Seq: [1___] │
│ │
│ [☑] Visible on Screen [☑] Visible on Report │
│ │
│ [☑] Count in Statistics │
│ │
│ Start Date: [2024-01-01_______] │
└──────────────────────────────────────────────────────────────────┘
Fields:
- TestSiteCode (required, 3-6 chars, uppercase, unique validation)
- TestSiteName (required, 3-255 chars)
- TestType (required, dropdown: TEST, PARAM, CALC, GROUP, TITLE)
- Description (optional, textarea, max 500 chars)
- SiteID (required, dropdown from sites API)
- SeqScr (optional, number, default 0)
- SeqRpt (optional, number, default 0)
- VisibleScr (checkbox, default true)
- VisibleRpt (checkbox, default true)
- CountStat (checkbox, default true)
- StartDate (optional, datetime, default current)
Dynamic Behavior:
- When TestType changes → Show/hide relevant tabs
- When TestType = CALC/PARAM/TEST → Auto-populate defaults
- TestType change triggers confirmation if form has unsaved changes
2. Tech Details Tab
Form Layout (Three sections):
Section 1: Categorization
┌──────────────────────────────────────────────────────────────┐
│ Discipline: [Hematology ▼] │
│ Department: [CBC Dept ▼] │
└──────────────────────────────────────────────────────────────┘
Section 2: Result Configuration
┌──────────────────────────────────────────────────────────────┐
│ Result Type: [Numeric ▼] (dynamic based on TestType) │
│ Ref Type: [Range ▼] (dynamic based on ResultType) │
│ Value Set: [____________] (if ResultType = VSET) │
└──────────────────────────────────────────────────────────────┘
Section 3: Units & Conversion
┌──────────────────────────────────────────────────────────────┐
│ Unit 1: [g/dL ▼] │
│ Factor: [1.0__] (conversion factor) │
│ Unit 2: [g/L ▼] │
│ Decimal: [2__] (decimal places) │
└──────────────────────────────────────────────────────────────┘
Section 4: Sample Requirements
┌──────────────────────────────────────────────────────────────┐
│ Required Qty: [5.0__] │
│ Qty Unit: [mL ▼] │
│ Collection Req: [Fasting required_______________] │
└──────────────────────────────────────────────────────────────┘
Section 5: Method & TAT
┌──────────────────────────────────────────────────────────────┐
│ Method: [Automated Analyzer_______________] │
│ Expected TAT: [60__] (minutes) │
└──────────────────────────────────────────────────────────────┘
Fields:
- DisciplineID (dropdown, optional)
- DepartmentID (dropdown, optional)
- ResultType (dropdown, dynamic options based on TestType)
- RefType (dropdown, dynamic options based on ResultType)
- VSet (text input, shown only if ResultType = VSET)
- Unit1 (dropdown from units valueset)
- Factor (number, optional)
- Unit2 (dropdown from units valueset)
- Decimal (number, default 2, min 0, max 6)
- ReqQty (number, optional)
- ReqQtyUnit (dropdown)
- CollReq (textarea, optional)
- Method (text input, optional)
- ExpectedTAT (number, optional)
Dynamic Dropdown Logic:
// TestType → ResultType mapping
const getResultTypeOptions = (testType: string) => {
switch (testType) {
case 'CALC': return ['NMRIC'];
case 'GROUP':
case 'TITLE': return ['NORES'];
default: return ['NMRIC', 'RANGE', 'TEXT', 'VSET'];
}
};
// ResultType → RefType mapping
const getRefTypeOptions = (resultType: string) => {
switch (resultType) {
case 'NMRIC':
case 'RANGE': return ['RANGE', 'THOLD'];
case 'VSET': return ['VSET'];
case 'TEXT': return ['TEXT'];
case 'NORES': return ['NOREF'];
default: return [];
}
};
3. Calculations Tab (CALC only)
Form Layout:
┌──────────────────────────────────────────────────────────────┐
│ Formula Input: [HGB + MCV + MCHC________] │
│ │
│ Formula Code: [{HGB} + {MCV} + {MCHC}_____________] │
│ │
│ Discipline: [Hematology ▼] │
│ Department: [CBC Dept ▼] │
│ │
│ Method: [Calculated from components_______] │
│ │
│ Unit 1: [g/dL ▼] │
│ Factor: [1.0__] │
│ Unit 2: [g/L ▼] │
│ Decimal: [2__] │
│ │
│ Ref Type: [Range ▼] │
└──────────────────────────────────────────────────────────────┘
Fields:
- FormulaInput (text input, description of calculation)
- FormulaCode (text input, actual formula with placeholders like {A}, {B})
- DisciplineID (dropdown)
- DepartmentID (dropdown)
- Method (text input)
- Unit1, Factor, Unit2, Decimal (same as Tech Details)
- RefType (dropdown: RANGE, THOLD)
Validation:
- FormulaCode must contain valid syntax
- FormulaCode must reference valid test codes
- Test codes in formula must exist in system
4. Group Members Tab (GROUP only)
Form Layout:
┌──────────────────────────────────────────────────────────────┐
│ Group: CBC - Complete Blood Count │
│ │
│ Current Members: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Code │ Name │ Type │ Seq │ Actions │ │
│ ├──────┼─────────────────┼────────┼─────┼───────────────┤ │
│ │ HGB │ Hemoglobin │ PARAM │ 1 │ [↑] [↓] [✕] │ │
│ │ RBC │ Red Blood Cells │ TEST │ 2 │ [↑] [↓] [✕] │ │
│ │ WBC │ White Blood Cell│ TEST │ 3 │ [↑] [↓] [✕] │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ Add Member: [HGB ▼] [Add Member +] │
│ │
│ Available Tests: (searchable dropdown) │
│ - HGB - Hemoglobin │
│ - RBC - Red Blood Cells │
│ - WBC - White Blood Cells │
└──────────────────────────────────────────────────────────────┘
Features:
- List current members with Code, Name, Type
- Reorder members (drag-and-drop or ↑↓ buttons)
- Remove member button (with confirmation)
- Add member dropdown (searchable, excludes current group and members)
- Prevent circular references (group cannot contain itself)
- Prevent duplicate members
Member Selection:
- Dropdown with search
- Group by TestType (TEST, PARAM, CALC)
- Show TestSiteCode - TestSiteName format
- Filter out already added members
- Filter out current group itself
5. Mappings Tab (All test types)
Form Layout:
┌──────────────────────────────────────────────────────────────┐
│ Test: CBC - Complete Blood Count │
│ │
│ Current Mappings: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Host │ Host Code │ Client │ Client Code │ Actions │ │
│ ├──────┼───────────┼────────┼────────────┼───────────┤ │
│ │ HIS │ CBC │ WST-1 │ CBC01 │ [✏️] [✕] │ │
│ │ SITE │ CBC │ INST-1 │ CBC │ [✏️] [✕] │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ [Add Mapping +] │
└──────────────────────────────────────────────────────────────┘
Add/Edit Mapping Modal:
┌──────────────────────────────────────────────────────────────┐
│ Add/Edit Mapping │
│ │
│ Host System: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Type: [HIS ▼] │ │
│ │ ID: [1____] │ │
│ │ Data Src: [DB____] │ │
│ │ Test Code:[CBC____] │ │
│ │ Test Name:[Complete Blood Count___________] │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ Client System: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Type: [WST ▼] │ │
│ │ ID: [1____] │ │
│ │ Data Src: [API____] │ │
│ │ Container: [Tube1 ▼] │ │
│ │ Test Code: [CBC01____] │ │
│ │ Test Name: [CBC_____________] │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ [Save] [Cancel] │
└──────────────────────────────────────────────────────────────┘
Fields:
- HostType: dropdown (HIS, SITE, WST, INST)
- HostID: text/number input
- HostDataSource: text input
- HostTestCode: text input
- HostTestName: text input
- ClientType: dropdown (HIS, SITE, WST, INST)
- ClientID: text/number input
- ClientDataSource: text input
- ConDefID: dropdown (container definitions)
- ClientTestCode: text input
- ClientTestName: text input
Validation:
- At least one of Host or Client must be specified
- Test codes must be unique per Host/Client combination
6. Ref Num Tab (Numeric Reference Ranges)
Form Layout:
┌──────────────────────────────────────────────────────────────┐
│ Test: CBC - Complete Blood Count │
│ Numeric Reference Ranges │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │Type │Range │Sex │ Age │ Low │High │Flag │ │
│ │ │ │ │ │Bound │Bound │ │ │
│ ├───────┼──────┼────┼───────┼──────────┼─────────┼─────┤ │
│ │Ref │RANGE │All │0-150 │[≥ 4.0] │[< 5.5] │ N │ │
│ │ │ │ │ │ │ │ │ │
│ │Crtc │THOLD │All │0-150 │[< 3.5] │[> 6.0] │ H/L │ │
│ │ │ │ │ │ │ │ │ │
│ │Ref │RANGE │F │18-150 │[≥ 4.5] │[< 5.0] │ N │ │
│ │ │ │ │ │ │ │ │ │
│ │Crtc │THOLD │M │18-150 │[< 3.8] │[> 5.8] │ H/L │ │
│ │ │ │ │ │ │ │ │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ [Add Range] [Delete Selected] [Copy from Similar Test] │
│ │
│ Selected Ranges: 0 │
└──────────────────────────────────────────────────────────────┘
Add/Edit Reference Range Modal:
┌──────────────────────────────────────────────────────────────┐
│ Numeric Reference Range │
│ │
│ Reference Type: [Reference ▼] │
│ (Reference, Critical, Validation, Rerun) │
│ │
│ Range Type: [Range ▼] │
│ (Range, Threshold) │
│ │
│ Sex: [All ▼] │
│ (All, Female, Male) │
│ │
│ Age Start: [0__] Age End: [150__] │
│ │
│ Low Bound: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Sign: [≥ ▼] Value: [4.0__] │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ High Bound: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Sign: [< ▼] Value: [5.5__] │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ Flag: [H/L/A/N___] (High/Low/Abnormal/Normal) │
│ │
│ Interpretation:[Normal range for hemoglobin_______________] │
│ │
│ [Save] [Cancel] │
└──────────────────────────────────────────────────────────────┘
Fields:
- NumRefType: dropdown (REF, CRTC, VAL, RERUN)
- RangeType: dropdown (RANGE, THOLD)
- Sex: dropdown (0=All, 1=Female, 2=Male)
- AgeStart: number input (min 0, max 150)
- AgeEnd: number input (min 0, max 150)
- LowSign: dropdown (=, <, <=)
- Low: number input (optional)
- HighSign: dropdown (=, >, >=)
- High: number input (optional)
- Flag: text input (single char: H, L, A, N)
- Interpretation: textarea (optional)
Validation:
- AgeStart must be less than AgeEnd
- If both Low and High are present: Low must be less than High
- LowSign must be appropriate for Low value (e.g., if Low = 4.0, LowSign should be >=)
- HighSign must be appropriate for High value (e.g., if High = 5.5, HighSign should be <=)
Reference Range Logic:
- Reference (REF): Normal ranges for reporting
- Critical (CRTC): Critical values requiring immediate notification
- Validation (VAL): Validation checks for result entry
- Rerun (RERUN): Conditions triggering automatic rerun
Range Type:
- RANGE: Standard range (Low to High)
- THOLD: Threshold (single value with comparison)
7. Ref Txt Tab (Text Reference Ranges)
Form Layout:
┌──────────────────────────────────────────────────────────────┐
│ Test: URINE - Urinalysis │
│ Text Reference Ranges │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │Type │Sex │ Age │ Reference Text │ Flag │ │
│ ├───────┼────┼───────┼─────────────────┼─────────────┤ │
│ │Normal │All │0-150 │Clear │ N │ │
│ │ │ │ │ │ │ │
│ │Abnml │All │0-150 │Cloudy │ A │ │
│ │ │ │ │ │ │ │
│ │Crtc │All │0-150 │Bloody │ C │ │
│ │ │ │ │ │ │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ [Add Range] [Delete Selected] [Copy from Similar Test] │
└──────────────────────────────────────────────────────────────┘
Add/Edit Text Reference Modal:
┌──────────────────────────────────────────────────────────────┐
│ Text Reference Range │
│ │
│ Reference Type: [Normal ▼] │
│ (Normal, Abnormal, Critical) │
│ │
│ Sex: [All ▼] │
│ (All, Female, Male) │
│ │
│ Age Start: [0__] Age End: [150__] │
│ │
│ Reference Text: [Clear_______________] │
│ │
│ Flag: [N/A/C___] (Normal/Abnormal/Critical) │
│ │
│ [Save] [Cancel] │
└──────────────────────────────────────────────────────────────┘
Fields:
- TxtRefType: dropdown (Normal, Abnormal, Critical)
- Sex: dropdown (0=All, 1=Female, 2=Male)
- AgeStart: number input
- AgeEnd: number input
- RefTxt: text input
- Flag: text input (N, A, C)
Validation:
- AgeStart must be less than AgeEnd
- RefTxt is required
- Flag must match TxtRefType (Normal=N, Abnormal=A, Critical=C)
🏗️ Component Implementation
Core Component Requirements
1. State Management with Svelte 5 Runes
testStore.ts:
import { writable } from 'svelte/store';
interface TestFormState {
TestSiteID?: number;
TestSiteCode: string;
TestSiteName: string;
TestType: 'TEST' | 'PARAM' | 'CALC' | 'GROUP' | 'TITLE';
Description?: string;
SiteID: number;
SeqScr: number;
SeqRpt: number;
VisibleScr: number;
VisibleRpt: number;
CountStat: number;
StartDate?: string;
details?: {
DisciplineID?: number;
DepartmentID?: number;
ResultType?: string;
RefType?: string;
VSet?: string;
Unit1?: string;
Factor?: number;
Unit2?: string;
Decimal?: number;
ReqQty?: number;
ReqQtyUnit?: string;
CollReq?: string;
Method?: string;
ExpectedTAT?: number;
FormulaInput?: string;
FormulaCode?: string;
members?: number[];
};
refnum?: RefNumRange[];
reftxt?: RefTxtRange[];
testmap?: TestMapping[];
}
export const testStore = writable<TestFormState>({
TestSiteCode: '',
TestSiteName: '',
TestType: 'TEST',
SiteID: 1,
SeqScr: 0,
SeqRpt: 0,
VisibleScr: 1,
VisibleRpt: 1,
CountStat: 1,
refnum: [],
reftxt: [],
testmap: [],
});
Derived Stores:
// Derive visible tabs based on TestType
export const visibleTabs = derived(testStore, ($store) => {
const type = $store.TestType;
return allTabs.filter(tab => tab.isVisible(type));
});
// Derive valid ResultType options
export const validResultTypes = derived(testStore, ($store) => {
const type = $store.TestType;
// Return options based on type
});
// Derive valid RefType options
export const validRefTypes = derived([testStore, validResultTypes], ([$store, $resultTypes]) => {
const resultType = $store.details?.ResultType;
// Return options based on resultType
});
2. Reusable UI Components
Button.svelte:
<script lang="ts">
export let variant: 'primary' | 'secondary' | 'danger' | 'ghost' = 'primary';
export let size: 'sm' | 'md' | 'lg' = 'md';
export let disabled = false;
export let loading = false;
export let type: 'button' | 'submit' | 'reset' = 'button';
</script>
<button
{type}
{disabled}
class="btn btn-{variant} btn-{size}"
disabled={disabled || loading}
>
{#if loading}
<Spinner />
{:else}
<slot />
{/if}
</button>
Input.svelte:
<script lang="ts">
export let label = '';
export let value = '';
export let type = 'text';
export let required = false;
export let error = '';
export let placeholder = '';
export let disabled = false;
</script>
<div class="input-group">
{#if label}
<label class="input-label">
{label}
{#if required}<span class="required">*</span>{/if}
</label>
{/if}
<input
{type}
{value}
{placeholder}
{disabled}
{required}
class="input-field"
class:has-error={error}
/>
{#if error}
<span class="input-error">{error}</span>
{/if}
</div>
Select.svelte:
<script lang="ts">
export let label = '';
export let value = '';
export let options: Array<{value: string, label: string}> = [];
export let required = false;
export let disabled = false;
export let placeholder = 'Select...';
</script>
<div class="select-group">
{#if label}
<label class="select-label">
{label}
{#if required}<span class="required">*</span>{/if}
</label>
{/if}
<select
bind:value
{disabled}
{required}
class="select-field"
>
<option value="">{placeholder}</option>
{#each options as option}
<option {value}>{option.label}</option>
{/each}
</select>
</div>
3. Test Form Component
TestForm.svelte (Main container):
<script lang="ts">
import { onMount } from 'svelte';
import { testStore, visibleTabs } from '$lib/stores/testStore';
import SidebarTabs from '$lib/components/test/SidebarTabs.svelte';
import BasicInfoTab from '$lib/components/test/tabs/BasicInfoTab.svelte';
// ... other tabs
let currentTab = 'basic';
let isLoading = false;
let isDirty = false;
onMount(async () => {
// Load existing test if editing
const id = params.id;
if (id) {
isLoading = true;
try {
const data = await testService.getById(id);
testStore.set(data);
} catch (error) {
// Handle error
} finally {
isLoading = false;
}
}
});
const handleTabChange = (tabId: string) => {
if (isDirty) {
// Confirm before changing tab if unsaved changes
if (!confirm('You have unsaved changes. Continue?')) {
return;
}
}
currentTab = tabId;
};
const handleSave = async () => {
// Validate form
// Call API
// Handle response
};
</script>
<div class="test-form-layout">
<SidebarTabs
tabs={$visibleTabs}
currentTab={currentTab}
onTabChange={handleTabChange}
/>
<main class="test-form-content">
{#if isLoading}
<Spinner />
{:else}
<h2 class="form-title">
{$testStore.TestSiteID ? 'Edit Test' : 'New Test'}
</h2>
{#if currentTab === 'basic'}
<BasicInfoTab bind:dirty={isDirty} />
{:else if currentTab === 'tech'}
<TechDetailsTab bind:dirty={isDirty} />
{:else if currentTab === 'calc'}
<CalcDetailsTab bind:dirty={isDirty} />
{:else if currentTab === 'group'}
<GroupMembersTab bind:dirty={isDirty} />
{:else if currentTab === 'mappings'}
<MappingsTab bind:dirty={isDirty} />
{:else if currentTab === 'refnum'}
<RefNumTab bind:dirty={isDirty} />
{:else if currentTab === 'reftxt'}
<RefTxtTab bind:dirty={isDirty} />
{/if}
<div class="form-actions">
<Button variant="primary" on:click={handleSave}>
Save
</Button>
<Button variant="secondary" on:click={() => navigate('/tests')}>
Cancel
</Button>
{#if $testStore.TestSiteID}
<Button variant="danger" on:click={handleDelete}>
Delete
</Button>
{/if}
</div>
{/if}
</main>
</div>
✅ Validation Requirements
Frontend Validation
TestSiteCode
- Required
- 3-6 characters
- Uppercase only
- Alphanumeric only
- Unique (check via API)
- Regex:
^[A-Z0-9]{3,6}$
TestSiteName
- Required
- 3-255 characters
- No special characters (except hyphen, space, parenthesis)
- Regex:
^[a-zA-Z0-9\s\-\(\)]{3,255}$
TestType
- Required
- Must be one of: TEST, PARAM, CALC, GROUP, TITLE
Type Combination Validation
const validateTypeCombination = (testType: string, resultType: string, refType: string) => {
const valid = TestValidationService.validate(testType, resultType, refType);
if (!valid.valid) {
throw new Error(valid.error);
}
};
Reference Range Validation
Numeric Ranges:
- AgeStart < AgeEnd (both 0-150)
- If Low and High both present: Low < High
- LowSign appropriate for Low value
- HighSign appropriate for High value
- Flag is single character (H, L, A, N)
Text Ranges:
- AgeStart < AgeEnd
- RefTxt is required
- Flag matches TxtRefType
Group Validation
- Group cannot contain itself
- No circular references (Group A contains Group B, Group B contains Group A)
- No duplicate members
- Minimum 1 member for GROUP type
🎨 Styling & Design System
Color Palette
:root {
/* Primary */
--primary-50: #e0f2fe;
--primary-100: #bae6fd;
--primary-500: #0ea5e9;
--primary-600: #0284c7;
--primary-700: #0369a1;
/* Secondary */
--secondary-500: #64748b;
--secondary-600: #475569;
/* Success */
--success-500: #22c55e;
--success-600: #16a34a;
/* Danger */
--danger-500: #ef4444;
--danger-600: #dc2626;
/* Warning */
--warning-500: #f59e0b;
--warning-600: #d97706;
/* Neutral */
--gray-50: #f9fafb;
--gray-100: #f3f4f6;
--gray-200: #e5e7eb;
--gray-300: #d1d5db;
--gray-500: #6b7280;
--gray-700: #374151;
--gray-900: #111827;
}
Typography
/* Font sizes */
--text-xs: 0.75rem; /* 12px */
--text-sm: 0.875rem; /* 14px */
--text-base: 1rem; /* 16px */
--text-lg: 1.125rem; /* 18px */
--text-xl: 1.25rem; /* 20px */
--text-2xl: 1.5rem; /* 24px */
--text-3xl: 1.875rem; /* 30px */
Spacing
--space-1: 0.25rem; /* 4px */
--space-2: 0.5rem; /* 8px */
--space-3: 0.75rem; /* 12px */
--space-4: 1rem; /* 16px */
--space-6: 1.5rem; /* 24px */
--space-8: 2rem; /* 32px */
Components
Buttons:
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: var(--space-2) var(--space-4);
border-radius: 0.375rem;
font-weight: 500;
transition: all 0.2s;
cursor: pointer;
}
.btn-primary {
background-color: var(--primary-600);
color: white;
}
.btn-primary:hover:not(:disabled) {
background-color: var(--primary-700);
}
.btn-secondary {
background-color: var(--gray-200);
color: var(--gray-700);
}
.btn-danger {
background-color: var(--danger-600);
color: white;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
Inputs:
.input-field {
width: 100%;
padding: var(--space-2) var(--space-3);
border: 1px solid var(--gray-300);
border-radius: 0.375rem;
font-size: var(--text-base);
transition: border-color 0.2s;
}
.input-field:focus {
outline: none;
border-color: var(--primary-500);
box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.1);
}
.input-field.has-error {
border-color: var(--danger-500);
}
Sidebar Tabs:
.sidebar-tab {
padding: var(--space-3) var(--space-4);
border-left: 3px solid transparent;
cursor: pointer;
transition: all 0.2s;
}
.sidebar-tab:hover {
background-color: var(--gray-100);
}
.sidebar-tab.active {
background-color: var(--primary-50);
border-left-color: var(--primary-600);
font-weight: 600;
}
📱 Responsive Design
Breakpoints
--breakpoint-sm: 640px;
--breakpoint-md: 768px;
--breakpoint-lg: 1024px;
--breakpoint-xl: 1280px;
Responsive Behavior
Desktop (> 1024px):
- Sidebar: Fixed width, visible
- Content: Full width
- Form: 2-column grid layout
Tablet (768px - 1024px):
- Sidebar: Collapsible (hamburger menu)
- Content: Full width
- Form: Single column layout
- Table: Horizontal scroll
Mobile (< 768px):
- Sidebar: Off-canvas drawer
- Content: Full width
- Form: Single column, stacked
- Table: Card view instead of table
🚀 Implementation Checklist
Phase 1: Project Setup & Infrastructure
- Initialize SvelteKit project with TypeScript
- Install and configure Tailwind CSS
- Set up Skeleton UI or Melt UI
- Configure Axios with interceptors
- Create type definitions (test.types.ts, api.types.ts)
- Set up API service layer
- Create auth store and testStore
- Set up routing structure
Phase 2: Reusable Components
- Button component
- Input component
- Select component
- Checkbox component
- Table component
- Modal component
- Badge component
- Alert component
- Spinner component
- Tabs component
Phase 3: Test List Page
- Test list page layout
- Filter panel component
- Test table component
- Pagination component
- Search functionality
- Filter functionality
- Sort functionality
- Load test data from API
Phase 4: Test Form - Basic & Tech
- Test form container with sidebar tabs
- Basic Info tab
- Tech Details tab
- Dynamic dropdown logic
- Form validation
- Save functionality
- Update functionality
Phase 5: Type-Specific Tabs
- Calculations tab (CALC)
- Group Members tab (GROUP)
- Mappings tab (all types)
- Member selection dropdown
- Mapping add/edit modal
Phase 6: Reference Ranges
- RefNum tab with table
- RefTxt tab with table
- Reference range modal
- Reference range validation
- Add/Edit/Delete operations
Phase 7: Polish & Testing
- Responsive design
- Loading states
- Error handling
- Form dirty state tracking
- Confirmation dialogs
- Toast notifications
- Accessibility (ARIA labels)
- Keyboard navigation
- Cross-browser testing
Phase 8: Documentation
- Component documentation
- API integration guide
- User guide
- Deployment instructions
📚 Additional Notes
ValueSet Integration
- Use backend API
/api/valuesetto fetch dropdown options - Cache valuesets locally to reduce API calls
- Transform labels: API returns both code and label (e.g.,
TestTypeandTestTypeLabel) - Display labels in UI, use codes for API calls
Authentication
- JWT token stored in localStorage
- Include token in Authorization header for all API calls
- Handle token expiration and refresh
- Redirect to login if unauthorized
Error Handling
- Display user-friendly error messages
- Log technical errors to console
- Retry logic for failed requests (with backoff)
- Show appropriate feedback for network errors
Performance
- Implement debounced search (300ms)
- Lazy load test data (pagination)
- Optimize re-renders with Svelte 5 runes
- Memoize expensive computations
Accessibility
- ARIA labels for form inputs
- Keyboard navigation support
- Screen reader compatibility
- Focus management in modals
- Color contrast compliance (WCAG AA)
🔗 References
Backend Documentation
- API Endpoints:
/api/tests - Models:
TestDefSiteModel,TestDefCalModel,TestDefGrpModel,TestMapModel - Validation:
TestValidationService
Type System
- Test types: TEST, PARAM, CALC, GROUP, TITLE
- Result types: NMRIC, RANGE, TEXT, VSET, NORES
- Reference types: RANGE, THOLD, TEXT, VSET, NOREF
Business Rules
- Soft deletes only (set EndDate)
- Test type + ResultType + RefType must be valid combination
- Reference ranges are type-specific (numeric vs text)
- Calculations use formula placeholders like {A}, {B}
🎯 Success Criteria
The frontend is considered complete when:
-
Functional Requirements
- All CRUD operations work for all test types
- Reference ranges can be managed (add/edit/delete)
- Group members can be added/removed/reordered
- Mappings can be configured for external systems
- Form validation prevents invalid data submission
-
User Experience
- Intuitive navigation with sidebar tabs
- Clear visual feedback for actions
- Responsive design works on all devices
- Loading states indicate progress
- Error messages are helpful and actionable
-
Code Quality
- TypeScript strict mode with no errors
- Component reusability and modularity
- Proper error handling throughout
- Clean, readable code with comments
- Efficient state management with Svelte 5 runes
-
Performance
- Page load time < 2 seconds
- Search results appear within 300ms
- Form submissions complete within 1 second
- No memory leaks or performance degradation
-
Testing
- Unit tests for components
- Integration tests for API calls
- E2E tests for critical user flows
- Cross-browser compatibility verified
📞 Support
For questions or issues during development:
- Review backend API documentation in
README.md - Check model definitions in
app/Models/Test/ - Refer to validation service in
app/Libraries/TestValidationService.php - Test API endpoints directly using tools like Postman
Last Updated: February 2025 Version: 1.0