diff --git a/docs/test-types-reference.md b/docs/test-types-reference.md
new file mode 100644
index 0000000..899acb5
--- /dev/null
+++ b/docs/test-types-reference.md
@@ -0,0 +1,417 @@
+# ๐ Test Types & Reference Types Guide
+
+> **Quick Overview**: This guide helps you understand the different types of tests and how to display them in the frontend.
+
+---
+
+## ๐ฏ What Are Test Types?
+
+Think of test types as "categories" that determine how a test behaves and what information it needs.
+
+### Quick Reference Card
+
+```
+โโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ Type โ Use This For... โ Example โ
+โโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโค
+โ TEST โ Standard lab tests โ Blood Glucose, CBC โ
+โ PARAM โ Components of a test โ WBC count (in CBC) โ
+โ CALC โ Formula-based results โ BMI, eGFR โ
+โ GROUP โ Panels/batteries โ Lipid Panel, CMP โ
+โโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโ
+```
+
+---
+
+## ๐งช Detailed Test Types
+
+### 1. TEST - Standard Laboratory Test
+**Icon**: ๐งซ **Color**: Blue
+
+Use this for regular tests that have:
+- Reference ranges (normal values)
+- Units (mg/dL, mmol/L, etc.)
+- Collection requirements
+
+**Example**: Blood Glucose, Hemoglobin, Cholesterol
+
+**What to Display**:
+- Test code and name
+- Reference range
+- Units
+- Collection instructions
+- Expected turnaround time
+
+---
+
+### 2. PARAM - Parameter
+**Icon**: ๐ **Color**: Light Blue
+
+Use this for individual components within a larger test.
+
+**Example**:
+- Complete Blood Count (GROUP) contains:
+ - WBC (PARAM)
+ - RBC (PARAM)
+ - Hemoglobin (PARAM)
+
+**What to Display**:
+- Same as TEST, but shown indented under parent
+- Often part of a GROUP
+
+---
+
+### 3. CALC - Calculated Test
+**Icon**: ๐งฎ **Color**: Purple
+
+Use this for tests computed from other test results using formulas.
+
+**Example**:
+- BMI (calculated from height & weight)
+- eGFR (calculated from creatinine, age, etc.)
+
+**What to Display**:
+- Formula description
+- Input parameters (which tests feed into this)
+- Result value
+- Reference range (if applicable)
+
+**Special Fields**:
+- `FormulaInput` - What values go in?
+- `FormulaCode` - How is it calculated?
+
+---
+
+### 4. GROUP - Group Test (Panel/Battery)
+**Icon**: ๐ฆ **Color**: Green
+
+Use this to bundle multiple related tests together.
+
+**Example**:
+- Lipid Panel (GROUP) contains:
+ - Total Cholesterol (PARAM)
+ - HDL (PARAM)
+ - LDL (PARAM)
+ - Triglycerides (PARAM)
+
+**What to Display**:
+- Group name
+- List of member tests
+- Individual results for each member
+
+**Structure**:
+```
+๐ฆ Lipid Panel (GROUP)
+ โโโ Total Cholesterol (PARAM): 180 mg/dL
+ โโโ HDL (PARAM): 55 mg/dL
+ โโโ LDL (PARAM): 110 mg/dL
+ โโโ Triglycerides (PARAM): 150 mg/dL
+```
+
+---
+
+## ๐ Reference Types Explained
+
+Reference types tell you **how to interpret the results** - what "normal" looks like.
+
+### Choose Your Reference Type:
+
+```
+ โโโโโโโโโโโโโโโโโโโโโโโโ
+ โ Reference Type? โ
+ โโโโโโโโโโโโฌโโโโโโโโโโโโ
+ โ
+ โโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโ
+ โ โ โ
+ Numbers? Text values? Threshold?
+ โ โ โ
+ โโโโโโโโผโโโโโโโ โโโโโโโโผโโโโโโโ โโโโโโโโผโโโโโโโ
+ โ NMRC โ โ TEXT โ โ THOLD โ
+ โ (Numeric) โ โ (Text) โ โ (Threshold) โ
+ โโโโโโโโฌโโโโโโโ โโโโโโโโฌโโโโโโโ โโโโโโโโฌโโโโโโโ
+ โ โ โ
+ โโโโโโโโผโโโโโโโ โโโโโโโโผโโโโโโโ โโโโโโโโผโโโโโโโ
+ โ RANGE โ โ Free โ โ Positive/ โ
+ โ (Min-Max) โ โ Text โ โ Negative โ
+ โ OR โ โ OR โ โ OR โ
+ โ THOLD โ โ VSET โ โ < > = โ
+ โ (Threshold) โ โ (Dropdown) โ โ cutoff โ
+ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ
+```
+
+### Reference Type Details
+
+| Type | Visual | Example |
+|------|--------|---------|
+| **NMRC** - Numeric Range | `70 - 100 mg/dL` | Blood glucose: 70-100 mg/dL |
+| **TEXT** - Text Value | `"Normal"` or `"Positive"` | Urinalysis: "Clear", "Cloudy" |
+| **THOLD** - Threshold | `> 60` or `< 5.5` | eGFR: > 60 (normal) |
+| **VSET** - Value Set | Dropdown options | Organism: [E.coli, Staph, etc.] |
+
+---
+
+## ๐จ Frontend Display Patterns
+
+### Pattern 1: Test List View
+
+```javascript
+// When showing a list of tests
+function renderTestCard(test) {
+ const typeIcon = getIcon(test.TestType);
+ const typeColor = getColor(test.TestType);
+
+ return `
+
+ ${typeIcon}
+
${test.TestSiteName}
+ ${test.TestTypeLabel}
+ ${test.TestSiteCode}
+
+ `;
+}
+```
+
+### Pattern 2: Reference Range Display
+
+```javascript
+// Show reference range based on type
+function renderReferenceRange(test) {
+ switch(test.RefType) {
+ case 'NMRC':
+ return `${test.MinValue} - ${test.MaxValue} ${test.Unit}`;
+
+ case 'TEXT':
+ return test.ReferenceText || 'See report';
+
+ case 'THOLD':
+ return `${test.ThresholdOperator} ${test.ThresholdValue}`;
+
+ case 'VSET':
+ return 'Select from list';
+ }
+}
+```
+
+### Pattern 3: Group Test Expansion
+
+```javascript
+// Expandable group test
+function renderGroupTest(test) {
+ return `
+
+
+ ๐ฆ ${test.TestSiteName} (${test.testdefgrp.length} tests)
+
+
+ ${test.testdefgrp.map(member => renderTestRow(member)).join('')}
+
+
+ `;
+}
+```
+
+---
+
+## ๐๏ธ Data Structure Visualization
+
+### Test Hierarchy
+
+```
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ TEST DEFINITION โ
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
+โ TestSiteID: 12345 โ
+โ TestSiteCode: GLUC โ
+โ TestSiteName: Glucose โ
+โ TestType: TEST โ
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
+โ ๐ Additional Info (loaded based on TestType): โ
+โ โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ โ TestDefTech (for TEST/PARAM) โ โ
+โ โ - DisciplineID, DepartmentID โ โ
+โ โ - ResultType, RefType โ โ
+โ โ - Unit, Method, ExpectedTAT โ โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ โ TestDefCal (for CALC) โ โ
+โ โ - FormulaInput, FormulaCode โ โ
+โ โ - RefType, Unit, Decimal โ โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ โ TestDefGrp (for GROUP) โ โ
+โ โ - Array of member tests โ โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ โ TestMap (for ALL types) โ โ
+โ โ - Mapping to external systems โ โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+```
+
+---
+
+## ๐ก Common Scenarios
+
+### Scenario 1: Creating a Simple Test
+
+**Need**: Add "Hemoglobin" test
+**Choose**: TEST type with NMRC reference
+
+```javascript
+const hemoglobin = {
+ TestSiteCode: 'HGB',
+ TestSiteName: 'Hemoglobin',
+ TestType: 'TEST',
+ testdeftech: {
+ RefType: 'NMRC',
+ Unit: 'g/dL',
+ // Reference range: 12-16 g/dL
+ }
+};
+```
+
+### Scenario 2: Creating a Panel
+
+**Need**: "Complete Blood Count" with multiple components
+**Choose**: GROUP type with PARAM children
+
+```javascript
+const cbc = {
+ TestSiteCode: 'CBC',
+ TestSiteName: 'Complete Blood Count',
+ TestType: 'GROUP',
+ testdefgrp: [
+ { TestSiteCode: 'WBC', TestType: 'PARAM' },
+ { TestSiteCode: 'RBC', TestType: 'PARAM' },
+ { TestSiteCode: 'HGB', TestType: 'PARAM' },
+ { TestSiteCode: 'PLT', TestType: 'PARAM' }
+ ]
+};
+```
+
+### Scenario 3: Calculated Result
+
+**Need**: BMI from height and weight
+**Choose**: CALC type with formula
+
+```javascript
+const bmi = {
+ TestSiteCode: 'BMI',
+ TestSiteName: 'Body Mass Index',
+ TestType: 'CALC',
+ testdefcal: {
+ FormulaInput: 'HEIGHT,WEIGHT',
+ FormulaCode: 'WEIGHT / ((HEIGHT/100) * (HEIGHT/100))',
+ RefType: 'NMRC',
+ Unit: 'kg/mยฒ'
+ }
+};
+```
+
+---
+
+## ๐จ Visual Style Guide
+
+### Color Coding
+
+| Type | Primary Color | Background | Usage |
+|------|---------------|------------|-------|
+| TEST | `#0066CC` | `#E6F2FF` | Main test cards |
+| PARAM | `#3399FF` | `#F0F8FF` | Component rows |
+| CALC | `#9933CC` | `#F5E6FF` | Calculated fields |
+| GROUP | `#00AA44` | `#E6F9EE` | Expandable panels |
+
+### Icons
+
+| Type | Icon | Unicode |
+|------|------|---------|
+| TEST | ๐งซ | `\u{1F9EB}` |
+| PARAM | ๐ | `\u{1F4CA}` |
+| CALC | ๐งฎ | `\u{1F9EE}` |
+| GROUP | ๐ฆ | `\u{1F4E6}` |
+
+---
+
+## ๐ Quick API Reference
+
+### Fetch Tests
+
+```javascript
+// Get all tests for a site
+GET /api/tests?siteId=123
+
+// Get specific test type
+GET /api/tests?testType=GROUP
+
+// Search tests
+GET /api/tests?search=glucose
+
+// Get single test with details
+GET /api/tests/12345
+```
+
+### Value Set Helpers
+
+```javascript
+// Get human-readable labels
+const labels = {
+ testType: valueSet.getLabel('test_type', test.TestType),
+ refType: valueSet.getLabel('reference_type', test.RefType),
+ resultType: valueSet.getLabel('result_type', test.ResultType)
+};
+
+// Get dropdown options
+const testTypes = valueSet.get('test_type');
+// Returns: [{value: "TEST", label: "Test"}, ...]
+```
+
+---
+
+## ๐ Value Set File Locations
+
+```
+app/Libraries/Data/
+โโโ test_type.json # TEST, PARAM, CALC, GROUP
+โโโ reference_type.json # NMRC, TEXT, THOLD, VSET
+โโโ result_type.json # NMRIC, RANGE, TEXT, VSET
+โโโ numeric_ref_type.json # RANGE, THOLD
+โโโ text_ref_type.json # VSET, TEXT
+```
+
+---
+
+## โ
Checklist for Frontend Developers
+
+- [ ] Show correct icon for each test type
+- [ ] Display reference ranges based on RefType
+- [ ] Handle GROUP tests as expandable panels
+
+- [ ] Display CALC formulas when available
+- [ ] Use ValueSet labels for all coded fields
+- [ ] Respect VisibleScr/VisibleRpt flags
+- [ ] Handle soft-deleted tests (EndDate check)
+
+---
+
+## ๐ Need Help?
+
+**Q: When do I use PARAM vs TEST?**
+A: Use PARAM for sub-components of a GROUP. Use TEST for standalone tests.
+
+**Q: What's the difference between NMRC and THOLD?**
+A: NMRC shows a range (70-100). THOLD shows a threshold (> 60 or < 5.5).
+
+**Q: Can a GROUP contain other GROUPs?**
+A: Yes! Groups can be nested (though typically only 1-2 levels deep).
+
+**Q: How do I know which details to fetch?**
+A: Check `TestType` first, then look for the corresponding property:
+- TEST/PARAM โ `testdeftech`
+- CALC โ `testdefcal`
+- GROUP โ `testdefgrp`
+- All types โ `testmap`
+
diff --git a/src/lib/api/client.js b/src/lib/api/client.js
index 71b4fd7..ac74728 100644
--- a/src/lib/api/client.js
+++ b/src/lib/api/client.js
@@ -1,7 +1,16 @@
import { goto } from '$app/navigation';
import { auth } from '$lib/stores/auth.js';
+import { config } from '$lib/stores/config.js';
-const API_URL = import.meta.env.VITE_API_URL || '';
+/**
+ * Get the API URL from runtime config
+ * Falls back to build-time env var if runtime config not available
+ * @returns {string}
+ */
+function getApiUrl() {
+ const runtimeUrl = config.getApiUrl();
+ return runtimeUrl || import.meta.env.VITE_API_URL || '';
+}
/**
* Base API client with JWT handling
@@ -27,8 +36,8 @@ export async function apiClient(endpoint, options = {}) {
headers['Authorization'] = `Bearer ${token}`;
}
- // Build full URL
- const url = `${API_URL}${endpoint}`;
+ // Build full URL using runtime config
+ const url = `${getApiUrl()}${endpoint}`;
try {
const response = await fetch(url, {
diff --git a/src/lib/components/DataTable.svelte b/src/lib/components/DataTable.svelte
index 547f286..85ab52f 100644
--- a/src/lib/components/DataTable.svelte
+++ b/src/lib/components/DataTable.svelte
@@ -104,7 +104,7 @@
{#each columns as column}
{#if cell}
- {@render cell({ column, row, value: getValue(row, column.key) })}
+ {@render cell({ column, row, value: getValue(row, column.key), index })}
{:else if column.render}
{@render column.render(row)}
{:else}
diff --git a/src/lib/components/TestModal.svelte b/src/lib/components/TestModal.svelte
new file mode 100644
index 0000000..a7f486e
--- /dev/null
+++ b/src/lib/components/TestModal.svelte
@@ -0,0 +1,104 @@
+
+
+
+
+
+ activeTab = 'basic'}
+ >
+ Basic Information
+
+ {#if canHaveRefRange}
+ activeTab = 'refrange'}
+ >
+ Reference Range
+ {#if formData.refnum?.length > 0 || formData.reftxt?.length > 0}
+ {(formData.refnum?.length || 0) + (formData.reftxt?.length || 0)}
+ {/if}
+
+ {/if}
+
+
+ {#if activeTab === 'basic'}
+
+ {:else if activeTab === 'refrange' && canHaveRefRange}
+
+ {/if}
+
+ {#snippet footer()}
+ Cancel
+
+ {#if saving}
+
+ {/if}
+ {saving ? 'Saving...' : 'Save'}
+
+ {/snippet}
+
diff --git a/src/lib/components/test-modal/BasicInfoForm.svelte b/src/lib/components/test-modal/BasicInfoForm.svelte
new file mode 100644
index 0000000..99354ad
--- /dev/null
+++ b/src/lib/components/test-modal/BasicInfoForm.svelte
@@ -0,0 +1,185 @@
+
+
+
diff --git a/src/lib/components/test-modal/NumericRefRange.svelte b/src/lib/components/test-modal/NumericRefRange.svelte
new file mode 100644
index 0000000..7df6b83
--- /dev/null
+++ b/src/lib/components/test-modal/NumericRefRange.svelte
@@ -0,0 +1,134 @@
+
+
+
+
+
+
+
Numeric Reference Ranges
+
+
+
+ Add Range
+
+
+
+ {#if refnum?.length === 0}
+
+
+
No numeric ranges defined
+
+
+ Add First Range
+
+
+ {/if}
+
+ {#each refnum || [] as ref, index (index)}
+
+
+
+ Range {index + 1}
+ removeRefRange(index)}>
+
+ Remove
+
+
+
+
+
+ Sex
+
+ {#each sexOptions as option (option.value)}
+ {option.label}
+ {/each}
+
+
+
+
+ Age From
+
+
+
+
+ Age To
+
+
+
+
+ Flag
+
+ {#each flagOptions as option (option.value)}
+ {option.label} - {option.description}
+ {/each}
+
+
+
+
+
+
+
+ Interpretation
+
+
+
+
+ {/each}
+
diff --git a/src/lib/components/test-modal/ReferenceRangeSection.svelte b/src/lib/components/test-modal/ReferenceRangeSection.svelte
new file mode 100644
index 0000000..7cc8c41
--- /dev/null
+++ b/src/lib/components/test-modal/ReferenceRangeSection.svelte
@@ -0,0 +1,210 @@
+
+
+
+
+
+
+
+
Reference Range Type
+
+
+
+
+
+
+ {#if formData.refRangeType === 'num'}
+
+ {/if}
+
+
+ {#if formData.refRangeType === 'thold'}
+
+ {/if}
+
+
+ {#if formData.refRangeType === 'text'}
+
+ {/if}
+
+
+ {#if formData.refRangeType === 'vset'}
+
+ {/if}
+
diff --git a/src/lib/components/test-modal/TextRefRange.svelte b/src/lib/components/test-modal/TextRefRange.svelte
new file mode 100644
index 0000000..b970237
--- /dev/null
+++ b/src/lib/components/test-modal/TextRefRange.svelte
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
Text Reference Ranges
+
+
+
+ Add Range
+
+
+
+ {#if reftxt?.length === 0}
+
+
+
No text ranges defined
+
+
+ Add First Range
+
+
+ {/if}
+
+ {#each reftxt || [] as ref, index (index)}
+
+
+
+ Range {index + 1}
+ removeRefRange(index)}>
+
+ Remove
+
+
+
+
+
+ Sex
+
+ {#each sexOptions as option (option.value)}
+ {option.label}
+ {/each}
+
+
+
+
+ Age From
+
+
+
+
+ Age To
+
+
+
+
+ Flag
+
+ N - Normal
+ A - Abnormal
+
+
+
+
+
+ Reference Text
+
+
+
+
+ {/each}
+
diff --git a/src/lib/components/test-modal/ThresholdRefRange.svelte b/src/lib/components/test-modal/ThresholdRefRange.svelte
new file mode 100644
index 0000000..892b962
--- /dev/null
+++ b/src/lib/components/test-modal/ThresholdRefRange.svelte
@@ -0,0 +1,134 @@
+
+
+
+
+
+
+
Threshold Reference Ranges
+
+
+
+ Add Range
+
+
+
+ {#if refthold?.length === 0}
+
+
+
No threshold ranges defined
+
+
+ Add First Range
+
+
+ {/if}
+
+ {#each refthold || [] as ref, index (index)}
+
+
+
+ Range {index + 1}
+ removeRefRange(index)}>
+
+ Remove
+
+
+
+
+
+ Sex
+
+ {#each sexOptions as option (option.value)}
+ {option.label}
+ {/each}
+
+
+
+
+ Age From
+
+
+
+
+ Age To
+
+
+
+
+ Flag
+
+ {#each flagOptions as option (option.value)}
+ {option.label} - {option.description}
+ {/each}
+
+
+
+
+
+
+
+ Interpretation
+
+
+
+
+ {/each}
+
diff --git a/src/lib/components/test-modal/ValueSetRefRange.svelte b/src/lib/components/test-modal/ValueSetRefRange.svelte
new file mode 100644
index 0000000..1ed440c
--- /dev/null
+++ b/src/lib/components/test-modal/ValueSetRefRange.svelte
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
Value Set Reference Ranges
+
+
+
+ Add Range
+
+
+
+ {#if refvset?.length === 0}
+
+
+
No value set ranges defined
+
+
+ Add First Range
+
+
+ {/if}
+
+ {#each refvset || [] as ref, index (index)}
+
+
+
+ Range {index + 1}
+ removeRefRange(index)}>
+
+ Remove
+
+
+
+
+
+ Sex
+
+ {#each sexOptions as option (option.value)}
+ {option.label}
+ {/each}
+
+
+
+
+ Age From
+
+
+
+
+ Age To
+
+
+
+
+ Flag
+
+ N - Normal
+ A - Abnormal
+
+
+
+
+
+ Value Set
+
+ Comma-separated list of allowed values
+
+
+
+ {/each}
+
diff --git a/src/lib/components/test-modal/refRangeConstants.js b/src/lib/components/test-modal/refRangeConstants.js
new file mode 100644
index 0000000..4e865e2
--- /dev/null
+++ b/src/lib/components/test-modal/refRangeConstants.js
@@ -0,0 +1,26 @@
+/**
+ * Reference range shared constants
+ */
+
+/** @type {Array<{value: string, label: string, description: string}>} */
+export const signOptions = [
+ { value: 'GE', label: 'โฅ', description: 'Greater than or equal to' },
+ { value: 'GT', label: '>', description: 'Greater than' },
+ { value: 'LE', label: 'โค', description: 'Less than or equal to' },
+ { value: 'LT', label: '<', description: 'Less than' }
+];
+
+/** @type {Array<{value: string, label: string, description: string}>} */
+export const flagOptions = [
+ { value: 'N', label: 'N', description: 'Normal' },
+ { value: 'L', label: 'L', description: 'Low' },
+ { value: 'H', label: 'H', description: 'High' },
+ { value: 'C', label: 'C', description: 'Critical' }
+];
+
+/** @type {Array<{value: string, label: string}>} */
+export const sexOptions = [
+ { value: '2', label: 'Male' },
+ { value: '1', label: 'Female' },
+ { value: '0', label: 'Any' }
+];
diff --git a/src/lib/stores/config.js b/src/lib/stores/config.js
new file mode 100644
index 0000000..9aa0ef7
--- /dev/null
+++ b/src/lib/stores/config.js
@@ -0,0 +1,58 @@
+import { writable } from 'svelte/store';
+import { browser } from '$app/environment';
+
+/**
+ * Create config store with runtime configuration
+ */
+function createConfigStore() {
+ const { subscribe, set, update } = writable({
+ apiUrl: '',
+ loaded: false,
+ error: null,
+ });
+
+ return {
+ subscribe,
+
+ /**
+ * Load configuration from config.json
+ */
+ load: async () => {
+ if (!browser) return;
+
+ try {
+ const response = await fetch('/config.json');
+ if (!response.ok) {
+ throw new Error(`Failed to load config: ${response.status}`);
+ }
+ const config = await response.json();
+ set({
+ apiUrl: config.apiUrl || '',
+ loaded: true,
+ error: null,
+ });
+ } catch (err) {
+ console.error('Failed to load config:', err);
+ set({
+ apiUrl: '',
+ loaded: true,
+ error: err.message,
+ });
+ }
+ },
+
+ /**
+ * Get current API URL
+ * @returns {string}
+ */
+ getApiUrl: () => {
+ let url = '';
+ subscribe((config) => {
+ url = config.apiUrl;
+ })();
+ return url;
+ },
+ };
+}
+
+export const config = createConfigStore();
diff --git a/src/routes/(app)/master-data/contacts/+page.svelte b/src/routes/(app)/master-data/contacts/+page.svelte
index 6dd0909..7468261 100644
--- a/src/routes/(app)/master-data/contacts/+page.svelte
+++ b/src/routes/(app)/master-data/contacts/+page.svelte
@@ -208,7 +208,7 @@
Loading contacts...
- {:else if filteredContacts().length === 0}
+ {:else if filteredContacts.length === 0}
@@ -232,7 +232,7 @@
{:else}
({
+ data={filteredContacts.map((c) => ({
...c,
FullName: getDisplayName(c),
SpecialtyLabel: specialtyMap[c.Specialty] || '-',
diff --git a/src/routes/(app)/master-data/tests/+page.svelte b/src/routes/(app)/master-data/tests/+page.svelte
index fc26287..05548af 100644
--- a/src/routes/(app)/master-data/tests/+page.svelte
+++ b/src/routes/(app)/master-data/tests/+page.svelte
@@ -1,480 +1,80 @@
-
-
-
+
Test Definitions
-
Manage laboratory tests and panels
+
Manage laboratory tests, panels, and calculated values
-
-
- Add Test
-
+
Add Test
-
-
({
- ...t,
- DisciplineName: disciplines.find(d => d.DisciplineID === t.DisciplineID)?.DisciplineName || '-',
- DepartmentName: departments.find(d => d.DepartmentID === t.DepartmentID)?.DepartmentName || '-'
- }))}
- {loading}
- emptyMessage="No tests found"
- hover={true}
- bordered={false}
- >
- {#snippet cell({ column, row, value })}
- {#if column.key === 'TestType'}
-
- {testTypeLabels[value] || value}
-
- {:else if column.key === 'actions'}
-
- openEditModal(row)}>
-
-
- openDeleteModal(row)}>
-
-
-
- {:else}
- {value || '-'}
- {/if}
+ handleRowClick(idx)}>
+ {#snippet cell({ column, row, index })}
+ {@const isSelected = index === selectedRowIndex} {@const typeConfig = getTestTypeConfig(row.TestType)} {@const isGroup = row.TestType === 'GROUP'} {@const isExpanded = expandedGroups.has(row.TestSiteID)}
+ {#if column.key === 'expand'}{#if isGroup} toggleGroup(row.TestSiteID)}>{#if isExpanded} {:else} {/if} {:else} {/if}{/if}
+ {#if column.key === 'TestType'} {typeConfig.label} {/if}
+ {#if column.key === 'TestSiteName'}{row.TestSiteName} {#if isGroup && isExpanded && row.testdefgrp}
{#each row.testdefgrp as member}{@const memberConfig = getTestTypeConfig(member.TestType)}
{member.TestSiteCode} {member.TestSiteName}
{/each}
{/if}
{/if}
+ {#if column.key === 'ReferenceRange'}{formatReferenceRange(row)} {/if}
+ {#if column.key === 'actions'} openEditModal(row)}> openDeleteModal(row)}>
{/if}
+ {#if column.key === 'TestSiteCode'}{row.TestSiteCode} {/if}
{/snippet}
-
- {#if totalPages > 1}
-
-
- Showing {(currentPage - 1) * perPage + 1} - {Math.min(currentPage * perPage, totalItems)} of {totalItems}
-
-
- handlePageChange(currentPage - 1)} disabled={currentPage === 1}>
- Previous
-
- Page {currentPage} of {totalPages}
- handlePageChange(currentPage + 1)} disabled={currentPage === totalPages}>
- Next
-
-
-
- {/if}
+ {#if totalPages > 1}Showing {(currentPage - 1) * perPage + 1} - {Math.min(currentPage * perPage, totalItems)} of {totalItems}
handlePageChange(currentPage - 1)} disabled={currentPage === 1}>Previous Page {currentPage} of {totalPages} handlePageChange(currentPage + 1)} disabled={currentPage === totalPages}>Next
{/if}
-
-
-
- activeTab = 'basic'}
- >
- Basic Information
-
- {#if canHaveRefRange}
- activeTab = 'refrange'}
- >
- Reference Range
- {#if formData.refnum.length > 0 || formData.reftxt.length > 0}
- {formData.refnum.length + formData.reftxt.length}
- {/if}
-
- {/if}
-
-
- {#if activeTab === 'basic'}
-
- {:else if activeTab === 'refrange' && canHaveRefRange}
-
-
-
-
-
-
Reference Range Type
-
-
-
-
-
-
- {#if formData.refRangeType === 'numeric'}
-
-
-
-
-
Numeric Reference Ranges
-
-
-
-
- Add Range
-
-
-
- {#if formData.refnum.length === 0}
-
-
-
No numeric ranges defined
-
-
- Add First Range
-
-
- {/if}
-
- {#each formData.refnum as ref, index (index)}
- {@const validationErrors = validateNumericRange(ref, index)}
-
-
-
-
-
- Range {index + 1}
- {#if validationErrors.length > 0}
- Invalid
- {/if}
-
-
removeNumericRefRange(index)}>
-
- Remove
-
-
-
-
-
-
- 1
- Patient Demographics
-
-
-
- Range Type
-
- Normal Range
- Threshold
-
-
-
-
- Sex
-
- {#each sexOptions as option (option.value)}
- {option.label}
- {/each}
-
-
-
-
- Age From (years)
- ref.AgeEnd}
- />
-
-
-
- Age To (years)
- ref.AgeEnd}
- />
-
-
- {#if ref.AgeStart !== null && ref.AgeEnd !== null && ref.AgeStart > ref.AgeEnd}
-
Age start cannot be greater than age end
- {/if}
-
-
-
-
-
- 2
- Range Values
-
-
-
-
-
-
- {#if ref.Low !== null && ref.High !== null}
- {@const lowLabel = getSignLabel(ref.LowSign)}
- {@const highLabel = getSignLabel(ref.HighSign)}
-
- Preview:
-
- {lowLabel} {ref.Low} and {highLabel} {ref.High}
-
- {#if ref.Low > ref.High}
- โ Low value exceeds High value
- {/if}
-
- {/if}
-
-
-
-
-
-
- 3
- Result Interpretation
-
-
-
-
- Flag
-
-
-
- {#each flagOptions as option (option.value)}
- {option.label} - {option.description}
- {/each}
-
-
-
-
- Interpretation
-
-
-
-
-
-
- {/each}
-
- {/if}
-
-
- {#if formData.refRangeType === 'text'}
-
-
-
-
-
Text Reference Ranges
-
-
-
-
- Add Range
-
-
-
- {#if formData.reftxt.length === 0}
-
-
-
No text ranges defined
-
-
- Add First Range
-
-
- {/if}
-
- {#each formData.reftxt as ref, index (index)}
- {@const validationErrors = validateTextRange(ref, index)}
-
-
-
-
-
- Range {index + 1}
- {#if validationErrors.length > 0}
- Invalid
- {/if}
-
-
removeTextRefRange(index)}>
-
- Remove
-
-
-
-
-
-
- 1
- Patient Demographics
-
-
-
- Reference Type
-
- Free Text
- Value Set
-
-
-
-
- Sex
-
- {#each sexOptions as option (option.value)}
- {option.label}
- {/each}
-
-
-
-
- Age From (years)
- ref.AgeEnd}
- />
-
-
-
- Age To (years)
- ref.AgeEnd}
- />
-
-
- {#if ref.AgeStart !== null && ref.AgeEnd !== null && ref.AgeStart > ref.AgeEnd}
-
Age start cannot be greater than age end
- {/if}
-
-
-
-
-
- 2
- Reference Text
-
-
-
-
-
- {#if ref.TxtRefType === 'VSET'}
- Format: CODE=Description;CODE2=Description2
- {:else}
- Enter descriptive text for the reference range
- {/if}
-
-
-
-
-
-
-
- 3
- Result Flag
-
-
-
-
- N - Normal
- A - Abnormal
-
-
-
-
-
- {/each}
-
- {/if}
-
- {/if}
-
- {#snippet footer()}
- (modalOpen = false)} type="button">Cancel
-
- {#if saving}
-
- {/if}
- {saving ? 'Saving...' : 'Save'}
-
- {/snippet}
-
+ modalOpen = false} onupdateFormData={(data) => formData = data} />
Are you sure you want to delete this test?
-
- Code: {testToDelete?.TestSiteCode}
- Name: {testToDelete?.TestSiteName}
-
-
- This will deactivate the test. It will no longer appear in test lists but historical data will be preserved.
-
+
Code: {testToDelete?.TestSiteCode} Name: {testToDelete?.TestSiteName}
+
This will deactivate the test. Historical data will be preserved.
- {#snippet footer()}
- (deleteModalOpen = false)} type="button">Cancel
-
- {#if deleting}
-
- {/if}
- {deleting ? 'Deleting...' : 'Delete'}
-
- {/snippet}
-
+ {#snippet footer()} deleteModalOpen = false} type="button">Cancel {#if deleting} {/if}{deleting ? 'Deleting...' : 'Delete'} {/snippet}
+
\ No newline at end of file
diff --git a/src/routes/(app)/master-data/tests/TestModal.svelte b/src/routes/(app)/master-data/tests/TestModal.svelte
new file mode 100644
index 0000000..3ec14c4
--- /dev/null
+++ b/src/routes/(app)/master-data/tests/TestModal.svelte
@@ -0,0 +1,104 @@
+
+
+
+
+
+ activeTab = 'basic'}
+ >
+ Basic Information
+
+ {#if canHaveRefRange}
+ activeTab = 'refrange'}
+ >
+ Reference Range
+ {#if formData.refnum?.length > 0 || formData.reftxt?.length > 0}
+ {(formData.refnum?.length || 0) + (formData.reftxt?.length || 0)}
+ {/if}
+
+ {/if}
+
+
+ {#if activeTab === 'basic'}
+
+ {:else if activeTab === 'refrange' && canHaveRefRange}
+
+ {/if}
+
+ {#snippet footer()}
+ Cancel
+
+ {#if saving}
+
+ {/if}
+ {saving ? 'Saving...' : 'Save'}
+
+ {/snippet}
+
diff --git a/src/routes/(app)/master-data/tests/referenceRange.js b/src/routes/(app)/master-data/tests/referenceRange.js
new file mode 100644
index 0000000..a774ff7
--- /dev/null
+++ b/src/routes/(app)/master-data/tests/referenceRange.js
@@ -0,0 +1,107 @@
+// Reference Range Management Functions
+
+export const signOptions = [
+ { value: 'GE', label: 'โฅ', description: 'Greater than or equal to' },
+ { value: 'GT', label: '>', description: 'Greater than' },
+ { value: 'LE', label: 'โค', description: 'Less than or equal to' },
+ { value: 'LT', label: '<', description: 'Less than' }
+];
+
+export const flagOptions = [
+ { value: 'N', label: 'N', description: 'Normal' },
+ { value: 'L', label: 'L', description: 'Low' },
+ { value: 'H', label: 'H', description: 'High' },
+ { value: 'C', label: 'C', description: 'Critical' }
+];
+
+export const sexOptions = [
+ { value: '2', label: 'Male' },
+ { value: '1', label: 'Female' },
+ { value: '0', label: 'Any' }
+];
+
+export function createNumRef() {
+ return {
+ Sex: '2',
+ LowSign: 'GE',
+ HighSign: 'LE',
+ Low: null,
+ High: null,
+ AgeStart: 0,
+ AgeEnd: 120,
+ Flag: 'N',
+ Interpretation: 'Normal'
+ };
+}
+
+export function createTholdRef() {
+ return {
+ Sex: '2',
+ LowSign: 'GE',
+ HighSign: 'LE',
+ Low: null,
+ High: null,
+ AgeStart: 0,
+ AgeEnd: 120,
+ Flag: 'N',
+ Interpretation: 'Normal'
+ };
+}
+
+export function createTextRef() {
+ return {
+ Sex: '2',
+ AgeStart: 0,
+ AgeEnd: 120,
+ RefTxt: '',
+ Flag: 'N'
+ };
+}
+
+export function createVsetRef() {
+ return {
+ Sex: '2',
+ AgeStart: 0,
+ AgeEnd: 120,
+ valueset: '',
+ Flag: 'N'
+ };
+}
+
+export function validateNumericRange(ref, index) {
+ const errors = [];
+ if (ref.Low !== null && ref.High !== null && ref.Low > ref.High) {
+ errors.push(`Range ${index + 1}: Low value cannot be greater than High value`);
+ }
+ if (ref.AgeStart !== null && ref.AgeEnd !== null && ref.AgeStart > ref.AgeEnd) {
+ errors.push(`Range ${index + 1}: Age start cannot be greater than Age end`);
+ }
+ return errors;
+}
+
+export function validateTholdRange(ref, index) {
+ const errors = [];
+ if (ref.Low !== null && ref.High !== null && ref.Low > ref.High) {
+ errors.push(`Range ${index + 1}: Low value cannot be greater than High value`);
+ }
+ if (ref.AgeStart !== null && ref.AgeEnd !== null && ref.AgeStart > ref.AgeEnd) {
+ errors.push(`Range ${index + 1}: Age start cannot be greater than Age end`);
+ }
+ return errors;
+}
+
+export function validateTextRange(ref, index) {
+ const errors = [];
+ if (ref.AgeStart !== null && ref.AgeEnd !== null && ref.AgeStart > ref.AgeEnd) {
+ errors.push(`Range ${index + 1}: Age start cannot be greater than Age end`);
+ }
+ return errors;
+}
+
+export function validateVsetRange(ref, index) {
+ const errors = [];
+ if (ref.AgeStart !== null && ref.AgeEnd !== null && ref.AgeStart > ref.AgeEnd) {
+ errors.push(`Range ${index + 1}: Age start cannot be greater than Age end`);
+ }
+ return errors;
+}
diff --git a/src/routes/(app)/master-data/tests/test-modal/BasicInfoForm.svelte b/src/routes/(app)/master-data/tests/test-modal/BasicInfoForm.svelte
new file mode 100644
index 0000000..1e27145
--- /dev/null
+++ b/src/routes/(app)/master-data/tests/test-modal/BasicInfoForm.svelte
@@ -0,0 +1,185 @@
+
+
+
diff --git a/src/routes/(app)/master-data/tests/test-modal/NumericRefRange.svelte b/src/routes/(app)/master-data/tests/test-modal/NumericRefRange.svelte
new file mode 100644
index 0000000..fd7bb0f
--- /dev/null
+++ b/src/routes/(app)/master-data/tests/test-modal/NumericRefRange.svelte
@@ -0,0 +1,266 @@
+
+
+
+
+
+
+
Numeric Reference Ranges
+
+
+
+ Add Range
+
+
+
+ {#if refnum?.length === 0}
+
+
+
No numeric ranges defined
+
+
+ Add First Range
+
+
+ {/if}
+
+ {#each refnum || [] as ref, index (index)}
+
+
+
+
+
+
+ Range {index + 1}
+ {getFlagLabel(ref.Flag)}
+
+
+
+
+ {getRangePreview(ref)}
+
+
+
removeRefRange(index)}>
+
+
+
+
+
+
+
+
+
+
+ Sex
+
+ {#each sexOptions as option (option.value)}
+ {option.label}
+ {/each}
+
+
+
+
+ Flag
+
+ {#each flagOptions as option (option.value)}
+ {option.label} - {option.description}
+ {/each}
+
+
+
+
+
+
+
toggleAgeExpand(index)}
+ >
+
+ Age Range
+ {#if ref.AgeStart !== 0 || ref.AgeEnd !== 120}
+ ({ref.AgeStart}-{ref.AgeEnd})
+ {:else}
+ (All ages)
+ {/if}
+
+ {#if expandedRanges[index]?.age}
+
+ {:else}
+
+ {/if}
+
+
+ {#if expandedRanges[index]?.age}
+
+ {/if}
+
+
+
+
+
toggleInterpExpand(index)}
+ >
+
+ Interpretation
+ {#if ref.Interpretation}
+ ({ref.Interpretation})
+ {/if}
+
+ {#if expandedRanges[index]?.interpretation}
+
+ {:else}
+
+ {/if}
+
+
+ {#if expandedRanges[index]?.interpretation}
+
+ {/if}
+
+
+
+ {/each}
+
diff --git a/src/routes/(app)/master-data/tests/test-modal/ReferenceRangeSection.svelte b/src/routes/(app)/master-data/tests/test-modal/ReferenceRangeSection.svelte
new file mode 100644
index 0000000..e44fb44
--- /dev/null
+++ b/src/routes/(app)/master-data/tests/test-modal/ReferenceRangeSection.svelte
@@ -0,0 +1,179 @@
+
+
+
+
+
+
+
+
Reference Range Type
+
+
+
+
+
+
+ {#if formData.refRangeType === 'num'}
+
+ {/if}
+
+
+ {#if formData.refRangeType === 'thold'}
+
+ {/if}
+
+
+ {#if formData.refRangeType === 'text'}
+
+ {/if}
+
+
+ {#if formData.refRangeType === 'vset'}
+
+ {/if}
+
diff --git a/src/routes/(app)/master-data/tests/test-modal/TextRefRange.svelte b/src/routes/(app)/master-data/tests/test-modal/TextRefRange.svelte
new file mode 100644
index 0000000..97b7487
--- /dev/null
+++ b/src/routes/(app)/master-data/tests/test-modal/TextRefRange.svelte
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+
Text Reference Ranges
+
+
+
+ Add Range
+
+
+
+ {#if reftxt?.length === 0}
+
+
+
No text ranges defined
+
+
+ Add First Range
+
+
+ {/if}
+
+ {#each reftxt || [] as ref, index (index)}
+
+
+
+ Range {index + 1}
+ removeRefRange(index)}>
+
+ Remove
+
+
+
+
+
+ Sex
+
+ {#each sexOptions as option (option.value)}
+ {option.label}
+ {/each}
+
+
+
+
+ Age From
+
+
+
+
+ Age To
+
+
+
+
+ Flag
+
+ N - Normal
+ A - Abnormal
+
+
+
+
+
+ Reference Text
+
+
+
+
+ {/each}
+
diff --git a/src/routes/(app)/master-data/tests/test-modal/ThresholdRefRange.svelte b/src/routes/(app)/master-data/tests/test-modal/ThresholdRefRange.svelte
new file mode 100644
index 0000000..3abd96c
--- /dev/null
+++ b/src/routes/(app)/master-data/tests/test-modal/ThresholdRefRange.svelte
@@ -0,0 +1,124 @@
+
+
+
+
+
+
+
Threshold Reference Ranges
+
+
+
+ Add Range
+
+
+
+ {#if refthold?.length === 0}
+
+
+
No threshold ranges defined
+
+
+ Add First Range
+
+
+ {/if}
+
+ {#each refthold || [] as ref, index (index)}
+
+
+
+ Range {index + 1}
+ removeRefRange(index)}>
+
+ Remove
+
+
+
+
+
+ Sex
+
+ {#each sexOptions as option (option.value)}
+ {option.label}
+ {/each}
+
+
+
+
+ Age From
+
+
+
+
+ Age To
+
+
+
+
+ Flag
+
+ {#each flagOptions as option (option.value)}
+ {option.label} - {option.description}
+ {/each}
+
+
+
+
+
+
+
+ Interpretation
+
+
+
+
+ {/each}
+
diff --git a/src/routes/(app)/master-data/tests/test-modal/ValueSetRefRange.svelte b/src/routes/(app)/master-data/tests/test-modal/ValueSetRefRange.svelte
new file mode 100644
index 0000000..66dedbd
--- /dev/null
+++ b/src/routes/(app)/master-data/tests/test-modal/ValueSetRefRange.svelte
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
Value Set Reference Ranges
+
+
+
+ Add Range
+
+
+
+ {#if refvset?.length === 0}
+
+
+
No value set ranges defined
+
+
+ Add First Range
+
+
+ {/if}
+
+ {#each refvset || [] as ref, index (index)}
+
+
+
+ Range {index + 1}
+ removeRefRange(index)}>
+
+ Remove
+
+
+
+
+
+ Sex
+
+ {#each sexOptions as option (option.value)}
+ {option.label}
+ {/each}
+
+
+
+
+ Age From
+
+
+
+
+ Age To
+
+
+
+
+ Flag
+
+ N - Normal
+ A - Abnormal
+
+
+
+
+
+ Value Set
+
+ Comma-separated list of allowed values
+
+
+
+ {/each}
+
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index 69a1469..9f7a1b4 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -1,9 +1,17 @@
@@ -11,6 +19,15 @@
- {@render children()}
+ {#if configLoading}
+
+
+
+
Loading configuration...
+
+
+ {:else}
+ {@render children()}
+ {/if}
diff --git a/static/config.json b/static/config.json
new file mode 100644
index 0000000..1483461
--- /dev/null
+++ b/static/config.json
@@ -0,0 +1,3 @@
+{
+ "apiUrl": "http://localhost/clqms01"
+}