diff --git a/AGENTS.md b/AGENTS.md index 213236f..195984a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -43,48 +43,46 @@ pnpm run prepare ```svelte ``` ### Naming Conventions -- **Components**: PascalCase (`LoginForm.svelte`) +- **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` ### Imports Order 1. Svelte imports (`svelte`, `$app/*`) -2. $lib aliases (`$lib/stores/*`, `$lib/api/*`, `$lib/components/*`) +2. $lib aliases (`$lib/stores/*`, `$lib/api/*`, `$lib/components/*`, `$lib/utils/*`) 3. External libraries (`lucide-svelte`) 4. Relative imports (minimize these, prefer `$lib`) @@ -93,12 +91,13 @@ pnpm run prepare ``` src/ lib/ - api/ # API client and endpoints - stores/ # Svelte stores - components/ # Reusable components + api/ # API client and endpoints (per feature) + stores/ # Svelte stores (auth, config, valuesets) + components/ # Reusable components (DataTable, Modal, Sidebar) + utils/ # Utilities (toast, helpers) assets/ # Static assets routes/ # SvelteKit routes - (app)/ # Route groups + (app)/ # Route groups (protected) login/ dashboard/ ``` @@ -106,59 +105,163 @@ src/ ### API Client Patterns ```javascript -// src/lib/api/client.js - Base client handles auth -import { apiClient, get, post, put, del } from '$lib/api/client.js'; +// src/lib/api/client.js - Base client handles auth, 401 redirects +import { apiClient, get, post, put, patch, del } from '$lib/api/client.js'; -// src/lib/api/feature.js - Feature-specific endpoints -export async function getItem(id) { - return get(`/api/items/${id}`); +// 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 }); + } + }; +} +``` + +### Component Patterns + +```svelte + +let { open = $bindable(false), selected = $bindable(null) } = $props(); + + +{@render children?.()} +{@render footer()} + + + + + + ``` ### Error Handling ```javascript -// API errors are thrown with message try { - const result = await api.login(username, password); + const result = await api.fetchData(); + toastSuccess('Operation successful'); } catch (err) { - error = err.message || 'An unexpected error occurred'; - console.error('Login failed:', err); + const message = err.message || 'An unexpected error occurred'; + toastError(message); + console.error('Operation failed:', err); } ``` ### Styling with Tailwind & DaisyUI -- Use Tailwind utility classes -- DaisyUI components: `btn`, `card`, `alert`, `input`, `navbar` -- Color scheme: `primary` (emerald), `base-100`, `base-200` -- Custom colors in `app.css` with `@theme` +- 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 ### Authentication Patterns -- Auth state in `$lib/stores/auth.js` -- Check auth in layout `onMount` -- Redirect to `/login` if unauthenticated -- API client auto-redirects on 401 +- Auth state in `$lib/stores/auth.js` using writable store +- Check `$auth.isAuthenticated` in layout `onMount` +- Redirect to `/login` if unauthenticated using `goto('/login')` +- API client auto-redirects on 401 responses -### Environment Variables +### Runtime Config Pattern ```javascript -const API_URL = import.meta.env.VITE_API_URL || ''; +// Use runtime config for API URL that can be changed at runtime +import { config } from '$lib/stores/config.js'; +function getApiUrl() { + return config.getApiUrl() || import.meta.env.VITE_API_URL || ''; +} ``` ### LocalStorage - Only access in browser: check `browser` from `$app/environment` -- Use descriptive keys: `clqms_username`, `clqms_remember` +- Use descriptive keys: `clqms_username`, `clqms_remember`, `auth_token` + +### Form Handling Patterns + +```javascript +// Form state with validation +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; + } +} +``` + +### Toast/Notification System + +```javascript +import { success, error, info, warning } from '$lib/utils/toast.js'; + +// Use toast notifications for user feedback +success('Item created successfully'); +error('Failed to create item'); +info('New message received'); +warning('Action requires confirmation'); +``` ## Proxy Configuration -API requests to `/api` are proxied to `http://localhost:8000` in dev. +API requests to `/api` are proxied to `http://localhost:8000` in dev. See `vite.config.js`. ## API Documentation @@ -168,5 +271,7 @@ API Reference (Swagger UI): https://clqms01-api.services-summit.my.id/swagger/ - No ESLint or Prettier configured yet - add if needed - No test framework configured yet -- Uses Svelte 5 runes: `$props`, `$state`, `$derived`, `$effect` +- Uses Svelte 5 runes: `$props`, `$state`, `$derived`, `$effect`, `$bindable` - SvelteKit file-based routing with `+page.svelte`, `+layout.svelte` +- Static adapter configured for static export +- Runtime config allows API URL changes without rebuild diff --git a/backup/tests_backup/+page.svelte b/backup/tests_backup/+page.svelte new file mode 100644 index 0000000..66cc4e2 --- /dev/null +++ b/backup/tests_backup/+page.svelte @@ -0,0 +1,432 @@ + + +
+
+ +
+

Test Definitions

+

Manage laboratory tests, panels, and calculated values

+
+ +
+ +
+
+
+ e.key === 'Enter' && handleSearch()} /> + +
+
+ +
+ +
+
+ +
+ 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}{: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'}
{/if} + {#if column.key === 'TestSiteCode'}{row.TestSiteCode}{/if} + {/snippet} +
+ {#if totalPages > 1}
Showing {(currentPage - 1) * perPage + 1} - {Math.min(currentPage * perPage, totalItems)} of {totalItems}
Page {currentPage} of {totalPages}
{/if} +
+
+ + + typeSelectorOpen = false} + /> + + + 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. Historical data will be preserved.

+
+ {#snippet footer()}{/snippet} +
\ No newline at end of file diff --git a/src/routes/(app)/master-data/tests/TestModal.svelte b/backup/tests_backup/TestModal.svelte similarity index 98% rename from src/routes/(app)/master-data/tests/TestModal.svelte rename to backup/tests_backup/TestModal.svelte index e5ce513..8a8266b 100644 --- a/src/routes/(app)/master-data/tests/TestModal.svelte +++ b/backup/tests_backup/TestModal.svelte @@ -124,11 +124,13 @@ {:else if activeTab === 'technical' && canHaveTechnical} {:else if activeTab === 'refrange' && canHaveRefRange} {:else if activeTab === 'members' && isGroupTest} diff --git a/src/routes/(app)/master-data/tests/referenceRange.js b/backup/tests_backup/referenceRange.js similarity index 100% rename from src/routes/(app)/master-data/tests/referenceRange.js rename to backup/tests_backup/referenceRange.js diff --git a/src/routes/(app)/master-data/tests/test-modal/BasicInfoForm.svelte b/backup/tests_backup/test-modal/BasicInfoForm.svelte similarity index 100% rename from src/routes/(app)/master-data/tests/test-modal/BasicInfoForm.svelte rename to backup/tests_backup/test-modal/BasicInfoForm.svelte diff --git a/src/routes/(app)/master-data/tests/test-modal/GroupMembersTab.svelte b/backup/tests_backup/test-modal/GroupMembersTab.svelte similarity index 100% rename from src/routes/(app)/master-data/tests/test-modal/GroupMembersTab.svelte rename to backup/tests_backup/test-modal/GroupMembersTab.svelte diff --git a/src/routes/(app)/master-data/tests/test-modal/NumericRefRange.svelte b/backup/tests_backup/test-modal/NumericRefRange.svelte similarity index 100% rename from src/routes/(app)/master-data/tests/test-modal/NumericRefRange.svelte rename to backup/tests_backup/test-modal/NumericRefRange.svelte diff --git a/src/routes/(app)/master-data/tests/test-modal/ReferenceRangeSection.svelte b/backup/tests_backup/test-modal/ReferenceRangeSection.svelte similarity index 65% rename from src/routes/(app)/master-data/tests/test-modal/ReferenceRangeSection.svelte rename to backup/tests_backup/test-modal/ReferenceRangeSection.svelte index 28b77bc..6ad4fd4 100644 --- a/src/routes/(app)/master-data/tests/test-modal/ReferenceRangeSection.svelte +++ b/backup/tests_backup/test-modal/ReferenceRangeSection.svelte @@ -8,12 +8,14 @@ /** * @typedef {Object} Props * @property {Object} formData - Form data object + * @property {string} testType - Test type * @property {(formData: Object) => void} onupdateFormData - Update handler */ /** @type {Props} */ let { formData = $bindable({}), + testType = '', onupdateFormData = () => {} } = $props(); @@ -26,13 +28,37 @@ 'vset': 'Value Set (VSET)' }; - const refTypeOptions = [ - { value: 'none', label: 'None - No reference range' }, - { value: 'num', label: 'RANGE - Range' }, - { value: 'thold', label: 'Threshold (THOLD) - Limit values' }, - { value: 'text', label: 'Text (TEXT) - Descriptive' }, - { value: 'vset', label: 'Value Set (VSET) - Predefined values' } - ]; + // Filter options based on TestType + let refTypeOptions = $derived(() => { + const baseOptions = [ + { value: 'none', label: 'None - No reference range' } + ]; + + if (testType === 'GROUP' || testType === 'TITLE') { + return baseOptions; + } + + // TEST and PARAM can have all types + if (testType === 'TEST' || testType === 'PARAM') { + return [ + ...baseOptions, + { value: 'num', label: 'RANGE - Range' }, + { value: 'thold', label: 'Threshold (THOLD) - Limit values' }, + { value: 'text', label: 'Text (TEXT) - Descriptive' }, + { value: 'vset', label: 'Value Set (VSET) - Predefined values' } + ]; + } + + // CALC only allows RANGE (num) + if (testType === 'CALC') { + return [ + ...baseOptions, + { value: 'num', label: 'RANGE - Range' } + ]; + } + + return baseOptions; + }); // Ensure all reference range items have defined values, never undefined function normalizeRefNum(ref) { @@ -75,6 +101,62 @@ let normalizedReftxt = $derived(allReftxt.filter(ref => ref.RefType !== 'VSET')); let normalizedRefvset = $derived(allReftxt.filter(ref => ref.RefType === 'VSET')); + // Sync refRangeType with ResultType + $effect(() => { + const resultType = formData.ResultType; + const currentRefRangeType = formData.refRangeType; + + if (testType === 'GROUP' || testType === 'TITLE') { + // GROUP and TITLE should have no reference range + if (currentRefRangeType !== 'none') { + onupdateFormData({ ...formData, refRangeType: 'none', RefType: 'NOREF' }); + } + return; + } + + // Map ResultType to refRangeType + let expectedRefRangeType = 'none'; + if (resultType === 'NMRIC' || resultType === 'RANGE') { + expectedRefRangeType = 'num'; + } else if (resultType === 'VSET') { + expectedRefRangeType = 'vset'; + } else if (resultType === 'TEXT') { + expectedRefRangeType = 'text'; + } + + // Auto-sync if they don't match and we're not in the middle of editing + if (expectedRefRangeType !== 'none' && currentRefRangeType !== expectedRefRangeType) { + // Initialize the appropriate reference array if empty + const currentRefnum = formData.refnum || []; + const currentReftxt = formData.reftxt || []; + + if (expectedRefRangeType === 'num' && normalizedRefnum.length === 0) { + onupdateFormData({ + ...formData, + refRangeType: expectedRefRangeType, + RefType: 'RANGE', + refnum: [...currentRefnum, createNumRef()] + }); + } else if (expectedRefRangeType === 'vset' && normalizedRefvset.length === 0) { + onupdateFormData({ + ...formData, + refRangeType: expectedRefRangeType, + RefType: 'VSET', + reftxt: [...currentReftxt, createVsetRef()] + }); + } else if (expectedRefRangeType === 'text' && normalizedReftxt.length === 0) { + onupdateFormData({ + ...formData, + refRangeType: expectedRefRangeType, + RefType: 'TEXT', + reftxt: [...currentReftxt, createTextRef()] + }); + } else { + onupdateFormData({ ...formData, refRangeType: expectedRefRangeType }); + } + } + }); + function updateRefRangeType(type) { // Initialize arrays if they don't exist const currentRefnum = formData.refnum || []; @@ -157,12 +239,13 @@
- diff --git a/src/routes/(app)/master-data/tests/test-modal/TechnicalConfigForm.svelte b/backup/tests_backup/test-modal/TechnicalConfigForm.svelte similarity index 81% rename from src/routes/(app)/master-data/tests/test-modal/TechnicalConfigForm.svelte rename to backup/tests_backup/test-modal/TechnicalConfigForm.svelte index 5a34239..e2daf02 100644 --- a/src/routes/(app)/master-data/tests/test-modal/TechnicalConfigForm.svelte +++ b/backup/tests_backup/test-modal/TechnicalConfigForm.svelte @@ -6,12 +6,14 @@ /** * @typedef {Object} Props * @property {Object} formData - Form data object + * @property {string} testType - Test type (TEST, PARAM, CALC, GROUP, TITLE) * @property {(formData: Object) => void} onupdateFormData - Update handler */ /** @type {Props} */ let { formData = $bindable({}), + testType = '', onupdateFormData = () => {} } = $props(); @@ -29,10 +31,26 @@ // Handle different response structures const resultItems = resultTypeRes.data?.items || resultTypeRes.data?.ValueSetItems || (Array.isArray(resultTypeRes.data) ? resultTypeRes.data : []) || []; - resultTypeOptions = resultItems.map(item => ({ - value: item.value || item.itemCode || item.ItemCode || item.code || item.Code, - label: item.label || item.itemValue || item.ItemValue || item.value || item.Value || item.description || item.Description - })).filter(opt => opt.value); + resultTypeOptions = resultItems + .map(item => ({ + value: item.value || item.itemCode || item.ItemCode || item.code || item.Code, + label: item.label || item.itemValue || item.ItemValue || item.value || item.Value || item.description || item.Description + })) + .filter(opt => opt.value) + .filter(opt => { + // Filter ResultType based on TestType + if (testType === 'CALC') { + // CALC only allows NMRIC + return opt.value === 'NMRIC'; + } else if (testType === 'GROUP' || testType === 'TITLE') { + // GROUP and TITLE only allow NORES + return opt.value === 'NORES'; + } else if (testType === 'TEST' || testType === 'PARAM') { + // TEST and PARAM allow NMRIC, RANGE, TEXT, VSET + return ['NMRIC', 'RANGE', 'TEXT', 'VSET'].includes(opt.value); + } + return true; + }); console.log('resultTypeOptions:', resultTypeOptions); } catch (err) { @@ -46,6 +64,23 @@ onupdateFormData({ ...formData, [field]: value }); } + function handleResultTypeChange(value) { + // Update ResultType and set appropriate RefType + let newRefType = formData.RefType; + + if (value === 'NMRIC' || value === 'RANGE') { + newRefType = 'RANGE'; + } else if (value === 'VSET') { + newRefType = 'VSET'; + } else if (value === 'TEXT') { + newRefType = 'TEXT'; + } else if (value === 'NORES') { + newRefType = 'NOREF'; + } + + onupdateFormData({ ...formData, ResultType: value, RefType: newRefType }); + } + // Check if test is calculated type (doesn't have specimen requirements) const isCalculated = $derived(formData.TestType === 'CALC'); @@ -71,9 +106,12 @@ id="resultType" class="select select-sm select-bordered w-full" bind:value={formData.ResultType} - onchange={(e) => updateField('ResultType', e.target.value)} + onchange={(e) => handleResultTypeChange(e.target.value)} + disabled={testType === 'GROUP' || testType === 'TITLE'} > - + {#if testType !== 'GROUP' && testType !== 'TITLE'} + + {/if} {#each resultTypeOptions as option} {/each} diff --git a/src/routes/(app)/master-data/tests/test-modal/TestTypeSelector.svelte b/backup/tests_backup/test-modal/TestTypeSelector.svelte similarity index 100% rename from src/routes/(app)/master-data/tests/test-modal/TestTypeSelector.svelte rename to backup/tests_backup/test-modal/TestTypeSelector.svelte diff --git a/src/routes/(app)/master-data/tests/test-modal/TextRefRange.svelte b/backup/tests_backup/test-modal/TextRefRange.svelte similarity index 100% rename from src/routes/(app)/master-data/tests/test-modal/TextRefRange.svelte rename to backup/tests_backup/test-modal/TextRefRange.svelte diff --git a/src/routes/(app)/master-data/tests/test-modal/ThresholdRefRange.svelte b/backup/tests_backup/test-modal/ThresholdRefRange.svelte similarity index 100% rename from src/routes/(app)/master-data/tests/test-modal/ThresholdRefRange.svelte rename to backup/tests_backup/test-modal/ThresholdRefRange.svelte diff --git a/src/routes/(app)/master-data/tests/test-modal/ValueSetRefRange.svelte b/backup/tests_backup/test-modal/ValueSetRefRange.svelte similarity index 100% rename from src/routes/(app)/master-data/tests/test-modal/ValueSetRefRange.svelte rename to backup/tests_backup/test-modal/ValueSetRefRange.svelte diff --git a/docs/FRONTEND_TEST_MANAGEMENT_PROMPT.md b/docs/FRONTEND_TEST_MANAGEMENT_PROMPT.md new file mode 100644 index 0000000..4f51eb0 --- /dev/null +++ b/docs/FRONTEND_TEST_MANAGEMENT_PROMPT.md @@ -0,0 +1,1581 @@ +# 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 +```json +{ + "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**: +```typescript +{ + 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**: +```typescript +{ + 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**: +```typescript +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[]; + reftxt?: Omit[]; + + // Mappings (all types) + testmap?: TestMapping[]; +} +``` + +**Response**: +```typescript +{ + status: "created"; + message: "Test created successfully"; + data: { TestSiteId: number }; +} +``` + +#### 4. Update Test +``` +PATCH /api/tests +Content-Type: application/json +Body: CreateTestPayload & { TestSiteID: number } +``` + +**Response**: +```typescript +{ + 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**: +```typescript +{ + 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**: +1. **Filter Panel** (top of page): + - Site dropdown (multi-select) + - Test Type dropdown (checkboxes) + - Visibility toggles (Screen/Report) + - Search input (debounced, 300ms) + - Clear filters button + +2. **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 + +3. **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**: +```typescript +// 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**: +```typescript +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({ + TestSiteCode: '', + TestSiteName: '', + TestType: 'TEST', + SiteID: 1, + SeqScr: 0, + SeqRpt: 0, + VisibleScr: 1, + VisibleRpt: 1, + CountStat: 1, + refnum: [], + reftxt: [], + testmap: [], +}); +``` + +**Derived Stores**: +```typescript +// 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**: +```svelte + + + +``` + +**Input.svelte**: +```svelte + + +
+ {#if label} + + {/if} + + {#if error} + {error} + {/if} +
+``` + +**Select.svelte**: +```svelte + + +
+ {#if label} + + {/if} + +
+``` + +#### 3. Test Form Component + +**TestForm.svelte** (Main container): +```svelte + + +
+ + +
+ {#if isLoading} + + {:else} +

+ {$testStore.TestSiteID ? 'Edit Test' : 'New Test'} +

+ + {#if currentTab === 'basic'} + + {:else if currentTab === 'tech'} + + {:else if currentTab === 'calc'} + + {:else if currentTab === 'group'} + + {:else if currentTab === 'mappings'} + + {:else if currentTab === 'refnum'} + + {:else if currentTab === 'reftxt'} + + {/if} + +
+ + + {#if $testStore.TestSiteID} + + {/if} +
+ {/if} +
+
+``` + +--- + +## āœ… 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 +```typescript +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 +```css +: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 +```css +/* 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 +```css +--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**: +```css +.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**: +```css +.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**: +```css +.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 +```css +--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/valueset` to fetch dropdown options +- Cache valuesets locally to reduce API calls +- Transform labels: API returns both code and label (e.g., `TestType` and `TestTypeLabel`) +- 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: + +1. **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 + +2. **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 + +3. **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 + +4. **Performance** + - Page load time < 2 seconds + - Search results appear within 300ms + - Form submissions complete within 1 second + - No memory leaks or performance degradation + +5. **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: +1. Review backend API documentation in `README.md` +2. Check model definitions in `app/Models/Test/` +3. Refer to validation service in `app/Libraries/TestValidationService.php` +4. Test API endpoints directly using tools like Postman + +--- + +**Last Updated**: February 2025 +**Version**: 1.0 diff --git a/src/lib/api/tests.js b/src/lib/api/tests.js index 461d401..e9c8ac1 100644 --- a/src/lib/api/tests.js +++ b/src/lib/api/tests.js @@ -1,100 +1,338 @@ import { get, post, patch } from './client.js'; +/** + * @typedef {import('$lib/types/test.types.js').TestSummary} TestSummary + * @typedef {import('$lib/types/test.types.js').TestDetail} TestDetail + * @typedef {import('$lib/types/test.types.js').CreateTestPayload} CreateTestPayload + * @typedef {import('$lib/types/test.types.js').UpdateTestPayload} UpdateTestPayload + * @typedef {import('$lib/types/test.types.js').TestListResponse} TestListResponse + * @typedef {import('$lib/types/test.types.js').TestDetailResponse} TestDetailResponse + * @typedef {import('$lib/types/test.types.js').CreateTestResponse} CreateTestResponse + * @typedef {import('$lib/types/test.types.js').UpdateTestResponse} UpdateTestResponse + * @typedef {import('$lib/types/test.types.js').DeleteTestResponse} DeleteTestResponse + * @typedef {import('$lib/types/test.types.js').TestFilterOptions} TestFilterOptions + */ + /** * Fetch tests list with optional filters, pagination, and search - * @param {Object} params - Query parameters - * @param {number} [params.page=1] - Page number - * @param {number} [params.perPage=20] - Items per page - * @param {string} [params.search] - Search by test code or name - * @param {string} [params.TestType] - Filter by test type (TEST, PARAM, CALC, GROUP, TITLE) - * @returns {Promise} API response with test data and pagination + * @param {TestFilterOptions & { page?: number, perPage?: number }} [params] - Query parameters + * @returns {Promise} API response with test data and pagination */ export async function fetchTests(params = {}) { const query = new URLSearchParams(params).toString(); return get(query ? `/api/tests?${query}` : '/api/tests'); } +/** + * Fetch a single test by ID + * @param {number} id - Test Site ID + * @returns {Promise} API response with test detail + */ export async function fetchTest(id) { return get(`/api/tests/${id}`); } -export async function createTest(data) { +/** + * Build payload from form state for API submission + * Follows the specification: nested details object for technical fields + * @param {any} formData - The form state + * @param {boolean} isUpdate - Whether this is an update operation + * @returns {CreateTestPayload | UpdateTestPayload} + */ +function buildPayload(formData, isUpdate = false) { + /** @type {CreateTestPayload} */ const payload = { - TestSiteCode: data.TestSiteCode, - TestSiteName: data.TestSiteName, - TestType: data.TestType, - DisciplineID: data.DisciplineID, - DepartmentID: data.DepartmentID, - SeqScr: data.SeqScr, - SeqRpt: data.SeqRpt, - VisibleScr: data.VisibleScr ? '1' : '0', - VisibleRpt: data.VisibleRpt ? '1' : '0', - // Type-specific fields - Unit: data.Unit, - Formula: data.Formula, - // Technical Config - ResultType: data.ResultType, - RefType: data.RefType, - SpcType: data.SpcType, - ReqQty: data.ReqQty, - ReqQtyUnit: data.ReqQtyUnit, - Unit1: data.Unit1, - Factor: data.Factor, - Unit2: data.Unit2, - Decimal: data.Decimal, - CollReq: data.CollReq, - Method: data.Method, - ExpectedTAT: data.ExpectedTAT, - // Reference ranges (only for TEST and CALC) - refnum: data.refnum, - reftxt: data.reftxt, - refvset: data.refvset, - refthold: data.refthold, + SiteID: formData.SiteID || 1, + TestSiteCode: formData.TestSiteCode, + TestSiteName: formData.TestSiteName, + TestType: formData.TestType, + Description: formData.Description, + SeqScr: parseInt(formData.SeqScr) || 0, + SeqRpt: parseInt(formData.SeqRpt) || 0, + VisibleScr: formData.VisibleScr ? 1 : 0, + VisibleRpt: formData.VisibleRpt ? 1 : 0, + CountStat: formData.CountStat ? 1 : 0, + // StartDate is auto-set by backend (created_at) + details: {}, + refnum: [], + reftxt: [], + testmap: [] }; + + // Add TestSiteID for updates + if (isUpdate && formData.TestSiteID) { + payload.TestSiteID = formData.TestSiteID; + } + + // Build details object based on TestType + const type = formData.TestType; + + // Common fields for TEST, PARAM, CALC + if (['TEST', 'PARAM', 'CALC'].includes(type)) { + payload.details = { + DisciplineID: formData.details?.DisciplineID || null, + DepartmentID: formData.details?.DepartmentID || null, + ResultType: formData.details?.ResultType || '', + RefType: formData.details?.RefType || '', + VSet: formData.details?.VSet || '', + Unit1: formData.details?.Unit1 || '', + Factor: formData.details?.Factor || null, + Unit2: formData.details?.Unit2 || '', + Decimal: formData.details?.Decimal || 2, + ReqQty: formData.details?.ReqQty || null, + ReqQtyUnit: formData.details?.ReqQtyUnit || '', + CollReq: formData.details?.CollReq || '', + Method: formData.details?.Method || '', + ExpectedTAT: formData.details?.ExpectedTAT || null + }; + + // Add formula fields for CALC type + if (type === 'CALC') { + payload.details.FormulaInput = formData.details?.FormulaInput || ''; + payload.details.FormulaCode = formData.details?.FormulaCode || ''; + } + } + + // Add members for GROUP type + if (type === 'GROUP' && formData.details?.members) { + payload.details = { + members: formData.details.members + }; + } + + // Add reference ranges (TEST and PARAM only) + if (['TEST', 'PARAM'].includes(type)) { + if (formData.refnum && Array.isArray(formData.refnum)) { + payload.refnum = formData.refnum.map(r => ({ + NumRefType: r.NumRefType, + RangeType: r.RangeType, + Sex: r.Sex, + AgeStart: parseInt(r.AgeStart) || 0, + AgeEnd: parseInt(r.AgeEnd) || 150, + LowSign: r.LowSign || null, + Low: r.Low !== null && r.Low !== undefined ? parseFloat(r.Low) : null, + HighSign: r.HighSign || null, + High: r.High !== null && r.High !== undefined ? parseFloat(r.High) : null, + Flag: r.Flag || null, + Interpretation: r.Interpretation || null + })); + } + + if (formData.reftxt && Array.isArray(formData.reftxt)) { + payload.reftxt = formData.reftxt.map(r => ({ + TxtRefType: r.TxtRefType, + Sex: r.Sex, + AgeStart: parseInt(r.AgeStart) || 0, + AgeEnd: parseInt(r.AgeEnd) || 150, + RefTxt: r.RefTxt || '', + Flag: r.Flag || null + })); + } + } + + // Add mappings (all types) + if (formData.testmap && Array.isArray(formData.testmap)) { + payload.testmap = formData.testmap.map(m => ({ + HostType: m.HostType || null, + HostID: m.HostID || null, + HostDataSource: m.HostDataSource || null, + HostTestCode: m.HostTestCode || null, + HostTestName: m.HostTestName || null, + ClientType: m.ClientType || null, + ClientID: m.ClientID || null, + ClientDataSource: m.ClientDataSource || null, + ConDefID: m.ConDefID || null, + ClientTestCode: m.ClientTestCode || null, + ClientTestName: m.ClientTestName || null + })); + } + + return payload; +} + +/** + * Create a new test + * @param {any} formData - The form state + * @returns {Promise} API response + */ +export async function createTest(formData) { + const payload = buildPayload(formData, false); return post('/api/tests', payload); } -export async function updateTest(data) { - const payload = { - TestSiteID: data.TestSiteID, - TestSiteCode: data.TestSiteCode, - TestSiteName: data.TestSiteName, - TestType: data.TestType, - DisciplineID: data.DisciplineID, - DepartmentID: data.DepartmentID, - SeqScr: data.SeqScr, - SeqRpt: data.SeqRpt, - VisibleScr: data.VisibleScr ? '1' : '0', - VisibleRpt: data.VisibleRpt ? '1' : '0', - // Type-specific fields - Unit: data.Unit, - Formula: data.Formula, - // Technical Config - ResultType: data.ResultType, - RefType: data.RefType, - SpcType: data.SpcType, - ReqQty: data.ReqQty, - ReqQtyUnit: data.ReqQtyUnit, - Unit1: data.Unit1, - Factor: data.Factor, - Unit2: data.Unit2, - Decimal: data.Decimal, - CollReq: data.CollReq, - Method: data.Method, - ExpectedTAT: data.ExpectedTAT, - // Reference ranges (only for TEST and CALC) - refnum: data.refnum, - reftxt: data.reftxt, - refvset: data.refvset, - refthold: data.refthold, - }; +/** + * Update an existing test + * @param {any} formData - The form state + * @returns {Promise} API response + */ +export async function updateTest(formData) { + const payload = buildPayload(formData, true); return patch('/api/tests', payload); } +/** + * Soft delete a test (set IsActive to '0') + * @param {number} id - Test Site ID + * @returns {Promise} API response + */ export async function deleteTest(id) { - // Soft delete - set IsActive to '0' return patch('/api/tests', { TestSiteID: id, IsActive: '0', }); } + +/** + * Validate test type combination + * According to business rules: TestType + ResultType + RefType must be valid + * @param {string} testType - The test type + * @param {string} resultType - The result type + * @param {string} refType - The reference type + * @returns {{ valid: boolean, error?: string }} + */ +export function validateTypeCombination(testType, resultType, refType) { + // Valid combinations based on specification + const validCombinations = { + TEST: { + NMRIC: ['RANGE', 'THOLD'], + RANGE: ['RANGE', 'THOLD'], + TEXT: ['TEXT'], + VSET: ['VSET'] + }, + PARAM: { + NMRIC: ['RANGE', 'THOLD'], + RANGE: ['RANGE', 'THOLD'], + TEXT: ['TEXT'], + VSET: ['VSET'] + }, + CALC: { + NMRIC: ['RANGE', 'THOLD'] + }, + GROUP: { + NORES: ['NOREF'] + }, + TITLE: { + NORES: ['NOREF'] + } + }; + + const validResultTypes = validCombinations[testType]; + if (!validResultTypes) { + return { valid: false, error: 'Invalid test type' }; + } + + if (!resultType) { + return { valid: true }; // ResultType might be optional + } + + const validRefTypes = validResultTypes[resultType]; + if (!validRefTypes) { + return { valid: false, error: `Result type '${resultType}' is not valid for '${testType}'` }; + } + + if (refType && !validRefTypes.includes(refType)) { + return { valid: false, error: `Reference type '${refType}' is not valid for '${testType}' + '${resultType}'` }; + } + + return { valid: true }; +} + +/** + * Get valid result types for a test type + * @param {string} testType - The test type + * @returns {Array<{value: string, label: string}>} + */ +export function getResultTypeOptions(testType) { + switch (testType) { + case 'CALC': + return [{ value: 'NMRIC', label: 'Numeric' }]; + case 'GROUP': + case 'TITLE': + return [{ value: 'NORES', label: 'No Result' }]; + default: + return [ + { value: 'NMRIC', label: 'Numeric' }, + { value: 'RANGE', label: 'Range' }, + { value: 'TEXT', label: 'Text' }, + { value: 'VSET', label: 'Value Set' } + ]; + } +} + +/** + * Get valid reference types for a result type + * @param {string} resultType - The result type + * @returns {Array<{value: string, label: string}>} + */ +export function getRefTypeOptions(resultType) { + switch (resultType) { + case 'NMRIC': + case 'RANGE': + return [ + { value: 'RANGE', label: 'Range' }, + { value: 'THOLD', label: 'Threshold' } + ]; + case 'VSET': + return [{ value: 'VSET', label: 'Value Set' }]; + case 'TEXT': + return [{ value: 'TEXT', label: 'Text' }]; + case 'NORES': + return [{ value: 'NOREF', label: 'No Reference' }]; + default: + return []; + } +} + +/** + * Validate test code according to specification + * - Required + * - 3-6 characters + * - Uppercase alphanumeric only + * - Regex: ^[A-Z0-9]{3,6}$ + * @param {string} code - The test code + * @returns {{ valid: boolean, error?: string }} + */ +export function validateTestCode(code) { + if (!code || code.trim() === '') { + return { valid: false, error: 'Test code is required' }; + } + + const trimmed = code.trim(); + + const regex = /^[A-Z0-9]+$/; + if (!regex.test(trimmed)) { + return { valid: false, error: 'Test code must be uppercase alphanumeric' }; + } + + return { valid: true }; +} + +/** + * Validate test name according to specification + * - Required + * - 3-255 characters + * - Alphanumeric with hyphen, space, and parentheses allowed + * - Regex: ^[a-zA-Z0-9\s\-\(\)]{3,255}$ + * @param {string} name - The test name + * @returns {{ valid: boolean, error?: string }} + */ +export function validateTestName(name) { + if (!name || name.trim() === '') { + return { valid: false, error: 'Test name is required' }; + } + + const trimmed = name.trim(); + if (trimmed.length < 3) { + return { valid: false, error: 'Test name must be at least 3 characters' }; + } + if (trimmed.length > 255) { + return { valid: false, error: 'Test name must be at most 255 characters' }; + } + + const regex = /^[a-zA-Z0-9\s\-\(\)]{3,255}$/; + if (!regex.test(trimmed)) { + return { valid: false, error: 'Test name can only contain letters, numbers, spaces, hyphens, and parentheses' }; + } + + return { valid: true }; +} diff --git a/src/lib/types/index.ts b/src/lib/types/index.ts new file mode 100644 index 0000000..eba87de --- /dev/null +++ b/src/lib/types/index.ts @@ -0,0 +1,26 @@ +/** + * Type Definitions Index + * Central export point for all type definitions + */ + +// Test Types +export * from './test.types.js'; + +// Re-export specific types for convenience +export type { + TestType, + ResultType, + RefType, + TestSummary, + TestDetail, + CreateTestPayload, + UpdateTestPayload, + RefNumRange, + RefTxtRange, + TestMapping, + GroupMember, + TechDetail, + CalcDetails, + TestFormState, + TabConfig +} from './test.types.js'; diff --git a/src/lib/types/test.types.ts b/src/lib/types/test.types.ts new file mode 100644 index 0000000..9f9b513 --- /dev/null +++ b/src/lib/types/test.types.ts @@ -0,0 +1,306 @@ +/** + * Test Management Type Definitions + * Based on CLQMS API Specification + */ + +// Test Types +export type TestType = 'TEST' | 'PARAM' | 'CALC' | 'GROUP' | 'TITLE'; +export type ResultType = 'NMRIC' | 'RANGE' | 'TEXT' | 'VSET' | 'NORES'; +export type RefType = 'RANGE' | 'THOLD' | 'VSET' | 'TEXT' | 'NOREF'; +export type NumRefType = 'REF' | 'CRTC' | 'VAL' | 'RERUN'; +export type RangeType = 'RANGE' | 'THOLD'; +export type TxtRefType = 'Normal' | 'Abnormal' | 'Critical'; +export type SexType = '0' | '1' | '2'; +export type SexLabel = 'All' | 'Female' | 'Male'; + +// Base Test Summary (from list endpoint) +export interface TestSummary { + TestSiteID: number; + TestSiteCode: string; + TestSiteName: string; + TestType: TestType; + TestTypeLabel: string; + SeqScr: number; + SeqRpt: number; + VisibleScr: number | string; + VisibleRpt: number | string; + CountStat: number; + StartDate: string; + EndDate?: string; + DisciplineID?: number; + DepartmentID?: number; + DisciplineName?: string; + DepartmentName?: string; + IsActive?: string | number; +} + +// Technical Details +export interface TechDetail { + DisciplineID?: number; + DepartmentID?: number; + DisciplineName?: string; + DepartmentName?: string; + ResultType?: ResultType; + RefType?: RefType; + VSet?: string; + Unit1?: string; + Factor?: number; + Unit2?: string; + Decimal?: number; + ReqQty?: number; + ReqQtyUnit?: string; + CollReq?: string; + Method?: string; + ExpectedTAT?: number; +} + +// Calculation Details (extends TechDetail) +export interface CalcDetails extends TechDetail { + FormulaInput?: string; + FormulaCode?: string; +} + +// Group Member +export interface GroupMember { + TestSiteID: number; + TestSiteCode: string; + TestSiteName: string; + TestType: TestType; + Seq?: number; +} + +// Test Mapping +export interface TestMapping { + TestMapID?: number; + HostType?: string; + HostID?: string | number; + HostDataSource?: string; + HostTestCode?: string; + HostTestName?: string; + ClientType?: string; + ClientID?: string | number; + ClientDataSource?: string; + ConDefID?: string | number; + ClientTestCode?: string; + ClientTestName?: string; +} + +// Numeric Reference Range +export interface RefNumRange { + RefNumID?: number; + NumRefType: NumRefType; + NumRefTypeLabel?: string; + RangeType: RangeType; + RangeTypeLabel?: string; + Sex: SexType; + SexLabel?: string; + AgeStart: number; + AgeEnd: number; + LowSign?: string; + LowSignLabel?: string; + Low?: number; + HighSign?: string; + HighSignLabel?: string; + High?: number; + Flag?: string; + Interpretation?: string; +} + +// Text Reference Range +export interface RefTxtRange { + RefTxtID?: number; + TxtRefType: TxtRefType; + TxtRefTypeLabel?: string; + Sex: SexType; + SexLabel?: string; + AgeStart: number; + AgeEnd: number; + RefTxt: string; + Flag?: string; +} + +// Complete Test Detail (from GET /api/tests/:id) +export interface TestDetail { + // Base fields + TestSiteID: number; + TestSiteCode: string; + TestSiteName: string; + TestType: TestType; + TestTypeLabel: string; + Description?: string; + SiteID: number; + SeqScr: number; + SeqRpt: number; + VisibleScr: number | string | boolean; + VisibleRpt: number | string | boolean; + CountStat: number | boolean; + StartDate?: string; + EndDate?: string; + + // Technical details + DisciplineID?: number; + DepartmentID?: number; + ResultType?: ResultType; + RefType?: RefType; + VSet?: string; + Unit1?: string; + Factor?: number; + Unit2?: string; + Decimal?: number; + ReqQty?: number; + ReqQtyUnit?: string; + CollReq?: string; + Method?: string; + ExpectedTAT?: number; + + // Calculated test specific + FormulaInput?: string; + FormulaCode?: string; + + // Nested data + testdefcal?: CalcDetails[]; + testdefgrp?: GroupMember[]; + testmap?: TestMapping[]; + testdeftech?: TechDetail[]; + refnum?: RefNumRange[]; + reftxt?: RefTxtRange[]; +} + +// Create/Update Test Payload +export interface CreateTestPayload { + SiteID: number; + TestSiteCode: string; + TestSiteName: string; + TestType: TestType; + Description?: string; + SeqScr?: number; + SeqRpt?: number; + VisibleScr?: number | boolean; + VisibleRpt?: number | boolean; + CountStat?: number | boolean; + StartDate?: string; + + // Nested details based on TestType + details?: { + // Technical (TEST/PARAM/CALC) + DisciplineID?: number; + DepartmentID?: number; + ResultType?: ResultType; + RefType?: RefType; + 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[]; + }; + + // Reference ranges (TEST/PARAM) + refnum?: Omit[]; + reftxt?: Omit[]; + + // Mappings (all types) + testmap?: TestMapping[]; +} + +// Update Test Payload (includes TestSiteID) +export interface UpdateTestPayload extends CreateTestPayload { + TestSiteID: number; +} + +// Form State (for Svelte 5 runes) +export interface TestFormState { + TestSiteID?: number; + TestSiteCode: string; + TestSiteName: string; + TestType: TestType; + Description?: string; + SiteID: number; + SeqScr: number; + SeqRpt: number; + VisibleScr: boolean; + VisibleRpt: boolean; + CountStat: boolean; + StartDate?: string; + details: { + DisciplineID?: number; + DepartmentID?: number; + ResultType?: ResultType; + RefType?: RefType; + 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[]; +} + +// Tab Configuration +export interface TabConfig { + id: string; + label: string; + icon: string; + isVisible: (testType: TestType) => boolean; +} + +// Test Type Configuration (for UI display) +export interface TestTypeConfig { + label: string; + badgeClass: string; + description: string; +} + +// API Response Types +export interface ApiResponse { + status: 'success' | 'created' | 'error'; + message: string; + data: T; +} + +export interface TestListResponse extends ApiResponse { + pagination?: { + page: number; + perPage: number; + total: number; + }; +} + +export interface TestDetailResponse extends ApiResponse {} + +export interface CreateTestResponse extends ApiResponse<{ TestSiteId: number }> { + status: 'created'; +} + +export interface UpdateTestResponse extends ApiResponse<{ TestSiteId: number }> {} + +export interface DeleteTestResponse extends ApiResponse<{ TestSiteId: number; EndDate: string }> {} + +// Filter Options +export interface TestFilterOptions { + TestType?: TestType; + VisibleScr?: number; + VisibleRpt?: number; + search?: string; +} diff --git a/src/routes/(app)/master-data/tests/+page.svelte b/src/routes/(app)/master-data/tests/+page.svelte index d5fb5b3..a420623 100644 --- a/src/routes/(app)/master-data/tests/+page.svelte +++ b/src/routes/(app)/master-data/tests/+page.svelte @@ -1,392 +1,257 @@
- + + +

Test Definitions

Manage laboratory tests, panels, and calculated values

- +
-
-
-
- e.key === 'Enter' && handleSearch()} /> - -
-
- -
- +
+
+ + + {#if searchQuery} + + {/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}{: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'}
{/if} - {#if column.key === 'TestSiteCode'}{row.TestSiteCode}{/if} - {/snippet} -
- {#if totalPages > 1}
Showing {(currentPage - 1) * perPage + 1} - {Math.min(currentPage * perPage, totalItems)} of {totalItems}
Page {currentPage} of {totalPages}
{/if} + {#if loading} +
+ + Loading tests... +
+ {:else if filteredTests.length === 0} +
+
+ +
+

+ {searchQuery ? 'No tests found' : 'No tests yet'} +

+

+ {searchQuery + ? `No tests matching "${searchQuery}". Try a different search term.` + : 'Get started by adding your first laboratory test.'} +

+ {#if !searchQuery} + + {/if} +
+ {:else} + + {#snippet cell({ column, row, value })} + {@const typeConfig = getTestTypeConfig(row.TestType)} + {@const IconComponent = typeConfig.icon} + {#if column.key === 'TestType'} + + + {typeConfig.label} + + {:else if column.key === 'Visible'} +
+ S + R +
+ {:else if column.key === 'actions'} +
+ + +
+ {:else} + {value || '-'} + {/if} + {/snippet} +
+ {/if}
- - typeSelectorOpen = false} - /> - - - modalOpen = false} - onupdateFormData={(data) => formData = data} + testId={selectedTestId} + {disciplines} + {departments} + {tests} + onsave={async () => { + modalOpen = false; + await loadTests(); + }} /> - +
-

Are you sure you want to delete this test?

-

Code: {testToDelete?.TestSiteCode}
Name: {testToDelete?.TestSiteName}

-

This will deactivate the test. Historical data will be preserved.

+

+ Are you sure you want to delete {deleteItem?.TestSiteName}? +

+ {#if deleteItem?.TestSiteCode} +

Code: {deleteItem.TestSiteCode}

+ {/if} +

This action cannot be undone.

- {#snippet footer()}{/snippet} -
\ No newline at end of file + {#snippet footer()} + + + {/snippet} +
diff --git a/src/routes/(app)/master-data/tests/test-modal/TestFormModal.svelte b/src/routes/(app)/master-data/tests/test-modal/TestFormModal.svelte new file mode 100644 index 0000000..0e06e4c --- /dev/null +++ b/src/routes/(app)/master-data/tests/test-modal/TestFormModal.svelte @@ -0,0 +1,364 @@ + + + + {#if loading} +
+ +
+ {:else} + {#if formErrors.length > 0} +
+ + + +
+ Please fix the following errors: +
    + {#each formErrors as error} +
  • {error}
  • + {/each} +
+
+
+ {/if} + +
+ +
+
+ {#each visibleTabs as tab (tab.id)} + {@const IconComponent = tab.component} + + {/each} +
+
+ + +
+ {#if currentTab === 'basic'} + + {:else if currentTab === 'tech'} + + {:else if currentTab === 'calc'} + + {:else if currentTab === 'group'} + + {:else if currentTab === 'mappings'} + + {/if} +
+
+ + {/if} + + {#snippet footer()} + + + {/snippet} +
\ No newline at end of file diff --git a/src/routes/(app)/master-data/tests/test-modal/tabs/BasicInfoTab.svelte b/src/routes/(app)/master-data/tests/test-modal/tabs/BasicInfoTab.svelte new file mode 100644 index 0000000..8d7a719 --- /dev/null +++ b/src/routes/(app)/master-data/tests/test-modal/tabs/BasicInfoTab.svelte @@ -0,0 +1,264 @@ + + +
+ +
+

Test Identity

+
+ +
+ + + {#if validationErrors.TestSiteCode} + + + {validationErrors.TestSiteCode} + + {/if} +
+ + +
+ + + {#if validationErrors.TestSiteName} + + + {validationErrors.TestSiteName} + + {:else} + 3-255 characters + {/if} +
+
+
+ + +
+

Classification

+
+ +
+ + + {#if validationErrors.TestType} + + + {validationErrors.TestType} + + {/if} +
+ + +
+ + +
+
+
+ + +
+

Display Settings

+
+ +
+ + +
+ + +
+ + +
+ + +
+ Screen + +
+ + +
+ Report + +
+ + +
+ Statistics + +
+
+
+
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 new file mode 100644 index 0000000..9e0931f --- /dev/null +++ b/src/routes/(app)/master-data/tests/test-modal/tabs/CalcDetailsTab.svelte @@ -0,0 +1,197 @@ + + +
+

Calculated Test Formula

+ +
+ +
+ Formula Syntax: Use curly braces to reference test codes, e.g., {'{HGB}'} + {'{MCV}'} +
+
+ + +
+

Formula Definition

+
+
+ + + Human-readable description of the calculation +
+ +
+ + + + {#if formData.details.FormulaCode && validateFormula(formData.details.FormulaCode)} + Valid formula syntax + {:else if formData.details.FormulaCode} + No test references found + {:else} + Enter formula with test code references + {/if} + +
+
+
+ + +
+

Categorization

+
+
+ + +
+ +
+ + +
+
+
+ + +
+

Method

+
+ + +
+
+ + +
+

Result Configuration

+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ + +
+
+
diff --git a/src/routes/(app)/master-data/tests/test-modal/tabs/GroupMembersTab.svelte b/src/routes/(app)/master-data/tests/test-modal/tabs/GroupMembersTab.svelte new file mode 100644 index 0000000..d32f949 --- /dev/null +++ b/src/routes/(app)/master-data/tests/test-modal/tabs/GroupMembersTab.svelte @@ -0,0 +1,168 @@ + + +
+

Group Members

+ +
+ +
+ Panel Members: Add tests, parameters, or calculated values to this panel. Order matters for display on reports. +
+
+ +
+

Current Members ({members.length})

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

No members added yet

+

Add tests to create this panel

+
+ {:else} +
+ + + + + + + + + + + + {#each members as member, idx (member.TestSiteID)} + + + + + + + + {/each} + +
#CodeNameTypeActions
{idx + 1}{member.TestSiteCode}{member.TestSiteName} + {member.TestType} + +
+ + + +
+
+
+ {/if} +
+ +
+

Add Member

+ + {#if availableOptions.length === 0} +
+ No available tests to add. All tests are either already members or inactive. +
+ {:else} +
+ + +
+ {/if} +
+ +
+

Tip: Members will display on reports in the order shown above.

+

Note: Panels cannot contain themselves or other panels (circular references).

+
+
diff --git a/src/routes/(app)/master-data/tests/test-modal/tabs/MappingsTab.svelte b/src/routes/(app)/master-data/tests/test-modal/tabs/MappingsTab.svelte new file mode 100644 index 0000000..f8ade7f --- /dev/null +++ b/src/routes/(app)/master-data/tests/test-modal/tabs/MappingsTab.svelte @@ -0,0 +1,250 @@ + + +
+

System Mappings

+ +
+ +
+ Test Mappings: Configure how this test maps to external systems (HIS, Lab Information Systems, etc.) +
+
+ +
+

Current Mappings ({formData.testmap?.length || 0})

+ + {#if !formData.testmap || formData.testmap.length === 0} +
+ +

No mappings configured

+

Add mappings to external systems

+
+ {:else} +
+ + + + + + + + + + + + {#each formData.testmap as mapping, idx (idx)} + + + + + + + + {/each} + +
Host SystemHost CodeClient SystemClient CodeActions
+
+
{mapping.HostType}
+
ID: {mapping.HostID || '-'}
+
+
+
+
{mapping.HostTestCode || '-'}
+
{mapping.HostTestName || '-'}
+
+
+
+
{mapping.ClientType}
+
ID: {mapping.ClientID || '-'}
+
+
+
+
{mapping.ClientTestCode || '-'}
+
{mapping.ClientTestName || '-'}
+
+
+
+ + +
+
+
+ {/if} +
+ +
+ +
+
+ + +
+
+
+

Host System

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+

Client System

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+
+ + {#snippet footer()} + + + {/snippet} +
diff --git a/src/routes/(app)/master-data/tests/test-modal/tabs/RefNumTab.svelte b/src/routes/(app)/master-data/tests/test-modal/tabs/RefNumTab.svelte new file mode 100644 index 0000000..342ba53 --- /dev/null +++ b/src/routes/(app)/master-data/tests/test-modal/tabs/RefNumTab.svelte @@ -0,0 +1,419 @@ + + +
+

Numeric Reference Ranges

+ +
+ +
+ Numeric Ranges: Define normal, critical, and validation ranges for numeric test results. +
+
+ +
+
+

Current Ranges ({formData.refnum?.length || 0})

+
+ + {#if !formData.refnum || formData.refnum.length === 0} +
+ +

No numeric ranges defined

+

Add reference ranges for this test

+
+ {:else} +
+ + + + + + + + + + + + + + + {#each formData.refnum as range, idx (idx)} + + + + + + + + + + + {/each} + +
TypeRangeSexAgeLow BoundHigh BoundFlagActions
+ {getRefTypeLabel(range.NumRefType)} + + {range.RangeType} + {getSexLabel(range.Sex)}{range.AgeStart}-{range.AgeEnd} + {#if range.Low !== null && range.Low !== ''} + {getSignLabel(range.LowSign)} {range.Low} + {:else} + - + {/if} + + {#if range.High !== null && range.High !== ''} + {getSignLabel(range.HighSign)} {range.High} + {:else} + - + {/if} + + {range.Flag || '-'} + +
+ + +
+
+
+ {/if} +
+ +
+ +
+
+ + +
+ {#if validationError} +
+ + + + {validationError} +
+ {/if} + +
+
+ + +
+ +
+ + +
+
+ +
+
+ + +
+ +
+
+ + +
+
+ + +
+
+
+ +
+

Low Bound

+
+ + +
+
+ +
+

High Bound

+
+ + +
+
+ +
+
+ + +
+ +
+ + +
+
+
+ + {#snippet footer()} + + + {/snippet} +
\ No newline at end of file diff --git a/src/routes/(app)/master-data/tests/test-modal/tabs/RefTxtTab.svelte b/src/routes/(app)/master-data/tests/test-modal/tabs/RefTxtTab.svelte new file mode 100644 index 0000000..960c251 --- /dev/null +++ b/src/routes/(app)/master-data/tests/test-modal/tabs/RefTxtTab.svelte @@ -0,0 +1,241 @@ + + +
+

Text Reference Ranges

+ +
+ +
+ Text Ranges: Define expected text values for tests with text-based results. +
+
+ +
+

Current Ranges ({formData.reftxt?.length || 0})

+ + {#if !formData.reftxt || formData.reftxt.length === 0} +
+ +

No text ranges defined

+

Add reference ranges for this test

+
+ {:else} +
+ + + + + + + + + + + + + {#each formData.reftxt as range, idx (idx)} + + + + + + + + + {/each} + +
TypeSexAgeReference TextFlagActions
+ + {getRefTypeLabel(range.TxtRefType)} + + {getSexLabel(range.Sex)}{range.AgeStart}-{range.AgeEnd}{range.RefTxt || '-'} + {range.Flag} + +
+ + +
+
+
+ {/if} +
+ +
+ +
+
+ + +
+
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + + +
+
+ + {#snippet footer()} + + + {/snippet} +
diff --git a/src/routes/(app)/master-data/tests/test-modal/tabs/TechDetailsTab.svelte b/src/routes/(app)/master-data/tests/test-modal/tabs/TechDetailsTab.svelte new file mode 100644 index 0000000..369d8b1 --- /dev/null +++ b/src/routes/(app)/master-data/tests/test-modal/tabs/TechDetailsTab.svelte @@ -0,0 +1,306 @@ + + +
+

Technical Details

+ + {#if !typeValidation.valid} +
+ + {typeValidation.error} +
+ {/if} + + +
+

Categorization

+
+
+ + +
+ +
+ + +
+
+
+ + +
+

Result Configuration

+
+
+ + + {#if formData.TestType === 'CALC'} + CALC type always uses Numeric Reference + {/if} +
+ +
+ + + {#if refTypeOptions.length === 1} + Automatically set based on Result Type + {/if} +
+
+ + {#if formData.details.ResultType === 'VSET'} +
+ + + Required when Result Type is 'Value Set' +
+ {/if} +
+ + +
+

Units & Conversion

+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + Max 6 +
+
+
+ + +
+

Sample Requirements

+
+
+ + +
+ +
+ + +
+
+ +
+ + +
+
+ + +
+

Method & TAT

+
+
+ + +
+ +
+ + +
+
+
+