- Consolidate fragmented test modal components into unified TestFormModal.svelte - Reorganize reference range components into organized tabs (RefNumTab, RefTxtTab) - Add new tab components: BasicInfoTab, TechDetailsTab, CalcDetailsTab, MappingsTab, GroupMembersTab - Move test-related type definitions to src/lib/types/test.types.ts for better type organization - Delete old component files: TestModal.svelte and 10+ sub-components - Add backup directory for old component preservation - Update AGENTS.md with coding guidelines and project conventions - Update tests.js API client with improved structure - Add documentation for frontend test management architecture
286 lines
9.0 KiB
Svelte
286 lines
9.0 KiB
Svelte
<script>
|
|
import { Ruler, Info } from 'lucide-svelte';
|
|
import HelpTooltip from '$lib/components/HelpTooltip.svelte';
|
|
import NumericRefRange from './NumericRefRange.svelte';
|
|
import TextRefRange from './TextRefRange.svelte';
|
|
import { createNumRef, createTholdRef, createTextRef, createVsetRef } from '../referenceRange.js';
|
|
|
|
/**
|
|
* @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();
|
|
|
|
// Map refRangeType to RefType labels for display
|
|
const refTypeLabels = {
|
|
'none': 'None',
|
|
'num': 'RANGE - Range',
|
|
'thold': 'Threshold (THOLD)',
|
|
'text': 'Text (TEXT)',
|
|
'vset': 'Value Set (VSET)'
|
|
};
|
|
|
|
// 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) {
|
|
return {
|
|
RefType: ref.RefType ?? 'REF',
|
|
Sex: ref.Sex ?? '0',
|
|
LowSign: ref.LowSign ?? 'GE',
|
|
HighSign: ref.HighSign ?? 'LE',
|
|
Low: ref.Low ?? null,
|
|
High: ref.High ?? null,
|
|
AgeStart: ref.AgeStart ?? 0,
|
|
AgeEnd: ref.AgeEnd ?? 120,
|
|
Flag: ref.Flag ?? 'N',
|
|
Interpretation: ref.Interpretation ?? '',
|
|
SpcType: ref.SpcType ?? '',
|
|
Criteria: ref.Criteria ?? ''
|
|
};
|
|
}
|
|
|
|
function normalizeRefTxt(ref) {
|
|
return {
|
|
RefType: ref.RefType ?? 'TEXT',
|
|
Sex: ref.Sex ?? '0',
|
|
AgeStart: ref.AgeStart ?? 0,
|
|
AgeEnd: ref.AgeEnd ?? 120,
|
|
RefTxt: ref.RefTxt ?? '',
|
|
Flag: ref.Flag ?? 'N',
|
|
SpcType: ref.SpcType ?? '',
|
|
Criteria: ref.Criteria ?? ''
|
|
};
|
|
}
|
|
|
|
// Reactive normalized data - filter by RefType
|
|
let allRefnum = $derived((formData.refnum || []).map(normalizeRefNum));
|
|
let allReftxt = $derived((formData.reftxt || []).map(normalizeRefTxt));
|
|
|
|
// Filtered arrays for display
|
|
let normalizedRefnum = $derived(allRefnum.filter(ref => ref.RefType !== 'THOLD'));
|
|
let normalizedRefthold = $derived(allRefnum.filter(ref => ref.RefType === 'THOLD'));
|
|
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 || [];
|
|
const currentReftxt = formData.reftxt || [];
|
|
|
|
if (type === 'num') {
|
|
// Add numeric range to refnum
|
|
onupdateFormData({
|
|
...formData,
|
|
refRangeType: type,
|
|
RefType: 'NMRC',
|
|
refnum: [...currentRefnum, createNumRef()]
|
|
});
|
|
} else if (type === 'thold') {
|
|
// Add threshold range to refnum
|
|
onupdateFormData({
|
|
...formData,
|
|
refRangeType: type,
|
|
RefType: 'THOLD',
|
|
refnum: [...currentRefnum, createTholdRef()]
|
|
});
|
|
} else if (type === 'text') {
|
|
// Add text range to reftxt
|
|
onupdateFormData({
|
|
...formData,
|
|
refRangeType: type,
|
|
RefType: 'TEXT',
|
|
reftxt: [...currentReftxt, createTextRef()]
|
|
});
|
|
} else if (type === 'vset') {
|
|
// Add value set range to reftxt
|
|
onupdateFormData({
|
|
...formData,
|
|
refRangeType: type,
|
|
RefType: 'VSET',
|
|
reftxt: [...currentReftxt, createVsetRef()]
|
|
});
|
|
} else {
|
|
// None selected
|
|
onupdateFormData({
|
|
...formData,
|
|
refRangeType: type,
|
|
RefType: ''
|
|
});
|
|
}
|
|
}
|
|
|
|
function updateRefnum(refnum) {
|
|
onupdateFormData({ ...formData, refnum });
|
|
}
|
|
|
|
function updateRefthold(refthold) {
|
|
// Merge thold items back into refnum
|
|
const nonThold = (formData.refnum || []).filter(ref => ref.RefType !== 'THOLD');
|
|
onupdateFormData({ ...formData, refnum: [...nonThold, ...refthold] });
|
|
}
|
|
|
|
function updateReftxt(reftxt) {
|
|
onupdateFormData({ ...formData, reftxt });
|
|
}
|
|
|
|
function updateRefvset(refvset) {
|
|
// Merge vset items back into reftxt
|
|
const nonVset = (formData.reftxt || []).filter(ref => ref.RefType !== 'VSET');
|
|
onupdateFormData({ ...formData, reftxt: [...nonVset, ...refvset] });
|
|
}
|
|
</script>
|
|
|
|
<div class="space-y-3">
|
|
<!-- Reference Range Type Selection -->
|
|
<div class="bg-base-100 rounded-lg border border-base-200 p-4">
|
|
<div class="flex items-center gap-2 mb-3">
|
|
<Ruler class="w-5 h-5 text-primary" />
|
|
<h3 class="font-semibold">Reference Range Type</h3>
|
|
<HelpTooltip
|
|
text="Choose how to define normal/abnormal ranges for this test."
|
|
title="Reference Range Help"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Dropdown Select -->
|
|
<div class="form-control">
|
|
<select
|
|
class="select select-bordered w-full"
|
|
value={formData.refRangeType || 'none'}
|
|
onchange={(e) => updateRefRangeType(e.target.value)}
|
|
disabled={testType === 'GROUP' || testType === 'TITLE'}
|
|
>
|
|
{#each refTypeOptions() as option (option.value)}
|
|
<option value={option.value}>{option.label}</option>
|
|
{/each}
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Show selected RefType info -->
|
|
{#if formData.refRangeType && formData.refRangeType !== 'none'}
|
|
<div class="mt-3 flex items-center gap-2 p-2 bg-info/10 rounded-lg border border-info/20">
|
|
<Info class="w-4 h-4 text-info" />
|
|
<span class="text-sm">
|
|
<span class="font-medium">Selected:</span>
|
|
{refTypeLabels[formData.refRangeType]}
|
|
</span>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- Numeric Reference Ranges -->
|
|
{#if formData.refRangeType === 'num'}
|
|
<NumericRefRange refnum={normalizedRefnum} onupdateRefnum={updateRefnum} />
|
|
{/if}
|
|
|
|
<!-- Threshold Reference Ranges (uses same component as numeric) -->
|
|
{#if formData.refRangeType === 'thold'}
|
|
<NumericRefRange refnum={normalizedRefthold} onupdateRefnum={updateRefthold} />
|
|
{/if}
|
|
|
|
<!-- Text Reference Ranges -->
|
|
{#if formData.refRangeType === 'text'}
|
|
<TextRefRange reftxt={normalizedReftxt} onupdateReftxt={updateReftxt} />
|
|
{/if}
|
|
|
|
<!-- Value Set Reference Ranges (uses same component as text) -->
|
|
{#if formData.refRangeType === 'vset'}
|
|
<TextRefRange reftxt={normalizedRefvset} onupdateReftxt={updateRefvset} />
|
|
{/if}
|
|
</div>
|