898 lines
32 KiB
Svelte
898 lines
32 KiB
Svelte
<script>
|
|
import { getResultTypeOptions, getRefTypeOptions, validateTypeCombination } from '$lib/api/tests.js';
|
|
import { AlertCircle, Hash, Type, Plus, ExternalLink, Trash2 } from 'lucide-svelte';
|
|
|
|
let { formData = $bindable(), disciplines = [], departments = [], isDirty = $bindable(false), onSwitchTab = null } = $props();
|
|
|
|
// Simple inline state for single range entry
|
|
let simpleRefNum = $state({
|
|
Low: '',
|
|
High: '',
|
|
Sex: '0',
|
|
AgeStart: '',
|
|
AgeEnd: '',
|
|
AgeUnit: 'years'
|
|
});
|
|
|
|
let simpleRefTxt = $state({
|
|
RefTxt: '',
|
|
Sex: '0',
|
|
AgeStart: '',
|
|
AgeEnd: '',
|
|
AgeUnit: 'years'
|
|
});
|
|
|
|
let validationErrors = $state({
|
|
typeCombination: '',
|
|
simpleRefNum: '',
|
|
simpleRefTxt: ''
|
|
});
|
|
|
|
const sexOptions = [
|
|
{ value: '0', label: 'All' },
|
|
{ value: '1', label: 'Female' },
|
|
{ value: '2', label: 'Male' }
|
|
];
|
|
|
|
const ageUnits = [
|
|
{ value: 'days', label: 'Days' },
|
|
{ value: 'weeks', label: 'Weeks' },
|
|
{ value: 'months', label: 'Months' },
|
|
{ value: 'years', label: 'Years' }
|
|
];
|
|
|
|
const disciplineOptions = $derived(disciplines.map(d => ({ value: d.DisciplineID, label: d.DisciplineName })));
|
|
const departmentOptions = $derived(departments.map(d => ({ value: d.DepartmentID, label: d.DepartmentName })));
|
|
|
|
const resultTypeOptions = $derived(getResultTypeOptions(formData.TestType));
|
|
const refTypeOptions = $derived(getRefTypeOptions(formData.details?.ResultType));
|
|
|
|
const typeValidation = $derived.by(() => {
|
|
const result = validateTypeCombination(
|
|
formData.TestType,
|
|
formData.details?.ResultType,
|
|
formData.details?.RefType
|
|
);
|
|
return result;
|
|
});
|
|
|
|
// Computed: Check if numeric reference ranges should be shown
|
|
const showRefNumSection = $derived.by(() => {
|
|
const resultType = formData.details?.ResultType;
|
|
const refType = formData.details?.RefType;
|
|
return ['NMRIC', 'RANGE'].includes(resultType) && ['RANGE', 'THOLD'].includes(refType);
|
|
});
|
|
|
|
// Computed: Check if text reference ranges should be shown
|
|
const showRefTxtSection = $derived.by(() => {
|
|
const resultType = formData.details?.ResultType;
|
|
const refType = formData.details?.RefType;
|
|
return resultType === 'TEXT' && refType === 'TEXT';
|
|
});
|
|
|
|
// Computed: Check if we should show sex/age (only if multiple ranges exist or user chooses to)
|
|
const showAdvancedRefFields = $derived.by(() => {
|
|
return (formData.refnum?.length > 1) || (formData.reftxt?.length > 1);
|
|
});
|
|
|
|
function handleFieldChange() {
|
|
isDirty = true;
|
|
validationErrors.typeCombination = '';
|
|
}
|
|
|
|
function handleResultTypeChange() {
|
|
const resultType = formData.details.ResultType;
|
|
if (resultType === 'VSET') {
|
|
formData.details.RefType = 'VSET';
|
|
} else if (resultType === 'TEXT') {
|
|
formData.details.RefType = 'TEXT';
|
|
} else if (resultType === 'NORES') {
|
|
formData.details.RefType = 'NOREF';
|
|
} else if (resultType === 'NMRIC' || resultType === 'RANGE') {
|
|
formData.details.RefType = 'RANGE';
|
|
}
|
|
|
|
if (resultType !== 'VSET') {
|
|
formData.details.VSet = '';
|
|
}
|
|
|
|
// Clear references when type changes
|
|
formData.refnum = [];
|
|
formData.reftxt = [];
|
|
resetSimpleRefNum();
|
|
resetSimpleRefTxt();
|
|
|
|
handleFieldChange();
|
|
}
|
|
|
|
function resetSimpleRefNum() {
|
|
simpleRefNum = {
|
|
Low: '',
|
|
High: '',
|
|
Sex: '0',
|
|
AgeStart: '',
|
|
AgeEnd: '',
|
|
AgeUnit: 'years'
|
|
};
|
|
validationErrors.simpleRefNum = '';
|
|
}
|
|
|
|
function resetSimpleRefTxt() {
|
|
simpleRefTxt = {
|
|
RefTxt: '',
|
|
Sex: '0',
|
|
AgeStart: '',
|
|
AgeEnd: '',
|
|
AgeUnit: 'years'
|
|
};
|
|
validationErrors.simpleRefTxt = '';
|
|
}
|
|
|
|
// Convert age to days for storage
|
|
function convertAgeToDays(value, unit) {
|
|
if (!value && value !== 0) return null;
|
|
const num = parseInt(value);
|
|
switch (unit) {
|
|
case 'days': return num;
|
|
case 'weeks': return num * 7;
|
|
case 'months': return num * 30;
|
|
case 'years': return num * 365;
|
|
default: return num;
|
|
}
|
|
}
|
|
|
|
// Convert days to display unit
|
|
function convertDaysToUnit(days, unit) {
|
|
if (days === null || days === undefined) return '';
|
|
switch (unit) {
|
|
case 'days': return days;
|
|
case 'weeks': return Math.floor(days / 7);
|
|
case 'months': return Math.floor(days / 30);
|
|
case 'years': return Math.floor(days / 365);
|
|
default: return days;
|
|
}
|
|
}
|
|
|
|
function validateSimpleRefNum() {
|
|
if (!simpleRefNum.Low && !simpleRefNum.High) {
|
|
return { valid: false, error: 'Please enter at least one value' };
|
|
}
|
|
|
|
const low = simpleRefNum.Low !== '' ? parseFloat(simpleRefNum.Low) : null;
|
|
const high = simpleRefNum.High !== '' ? parseFloat(simpleRefNum.High) : null;
|
|
|
|
if (low !== null && high !== null && low >= high) {
|
|
return { valid: false, error: 'Low value must be less than high value' };
|
|
}
|
|
|
|
// Validate age range if provided
|
|
if (simpleRefNum.AgeStart !== '' || simpleRefNum.AgeEnd !== '') {
|
|
const ageStart = simpleRefNum.AgeStart !== '' ? parseInt(simpleRefNum.AgeStart) : null;
|
|
const ageEnd = simpleRefNum.AgeEnd !== '' ? parseInt(simpleRefNum.AgeEnd) : null;
|
|
|
|
if (ageStart !== null && ageEnd !== null && ageStart >= ageEnd) {
|
|
return { valid: false, error: 'Age start must be less than age end' };
|
|
}
|
|
}
|
|
|
|
return { valid: true };
|
|
}
|
|
|
|
function addSimpleRefNum() {
|
|
const validation = validateSimpleRefNum();
|
|
if (!validation.valid) {
|
|
validationErrors.simpleRefNum = validation.error;
|
|
return;
|
|
}
|
|
|
|
const newRef = {
|
|
NumRefType: 'REF',
|
|
RangeType: 'RANGE',
|
|
Sex: simpleRefNum.Sex,
|
|
AgeStart: convertAgeToDays(simpleRefNum.AgeStart, simpleRefNum.AgeUnit) || 0,
|
|
AgeEnd: convertAgeToDays(simpleRefNum.AgeEnd, simpleRefNum.AgeUnit) || 54750, // 150 years in days
|
|
Low: simpleRefNum.Low !== '' ? parseFloat(simpleRefNum.Low) : null,
|
|
High: simpleRefNum.High !== '' ? parseFloat(simpleRefNum.High) : null,
|
|
Flag: null,
|
|
Interpretation: null
|
|
};
|
|
|
|
formData.refnum = [...(formData.refnum || []), newRef];
|
|
resetSimpleRefNum();
|
|
isDirty = true;
|
|
}
|
|
|
|
function validateSimpleRefTxt() {
|
|
if (!simpleRefTxt.RefTxt?.trim()) {
|
|
return { valid: false, error: 'Reference text is required' };
|
|
}
|
|
|
|
// Validate age range if provided
|
|
if (simpleRefTxt.AgeStart !== '' || simpleRefTxt.AgeEnd !== '') {
|
|
const ageStart = simpleRefTxt.AgeStart !== '' ? parseInt(simpleRefTxt.AgeStart) : null;
|
|
const ageEnd = simpleRefTxt.AgeEnd !== '' ? parseInt(simpleRefTxt.AgeEnd) : null;
|
|
|
|
if (ageStart !== null && ageEnd !== null && ageStart >= ageEnd) {
|
|
return { valid: false, error: 'Age start must be less than age end' };
|
|
}
|
|
}
|
|
|
|
return { valid: true };
|
|
}
|
|
|
|
function addSimpleRefTxt() {
|
|
const validation = validateSimpleRefTxt();
|
|
if (!validation.valid) {
|
|
validationErrors.simpleRefTxt = validation.error;
|
|
return;
|
|
}
|
|
|
|
const newRef = {
|
|
TxtRefType: 'Normal',
|
|
Sex: simpleRefTxt.Sex,
|
|
AgeStart: convertAgeToDays(simpleRefTxt.AgeStart, simpleRefTxt.AgeUnit) || 0,
|
|
AgeEnd: convertDaysToUnit(simpleRefTxt.AgeEnd, simpleRefTxt.AgeUnit) || 54750,
|
|
RefTxt: simpleRefTxt.RefTxt.trim(),
|
|
Flag: 'N'
|
|
};
|
|
|
|
formData.reftxt = [...(formData.reftxt || []), newRef];
|
|
resetSimpleRefTxt();
|
|
isDirty = true;
|
|
}
|
|
|
|
function removeRefNum(index) {
|
|
const newRanges = formData.refnum?.filter((_, i) => i !== index) || [];
|
|
formData.refnum = newRanges;
|
|
isDirty = true;
|
|
}
|
|
|
|
function removeRefTxt(index) {
|
|
const newRanges = formData.reftxt?.filter((_, i) => i !== index) || [];
|
|
formData.reftxt = newRanges;
|
|
isDirty = true;
|
|
}
|
|
|
|
function switchToRefNumTab() {
|
|
if (onSwitchTab) onSwitchTab('refnum');
|
|
}
|
|
|
|
function switchToRefTxtTab() {
|
|
if (onSwitchTab) onSwitchTab('reftxt');
|
|
}
|
|
|
|
function getSexLabel(sex) {
|
|
return sexOptions.find(s => s.value === sex)?.label || sex;
|
|
}
|
|
|
|
function getAgeDisplay(ageDays) {
|
|
if (ageDays === null || ageDays === undefined) return '';
|
|
if (ageDays < 30) return `${ageDays}d`;
|
|
if (ageDays < 365) return `${Math.floor(ageDays / 30)}mo`;
|
|
return `${Math.floor(ageDays / 365)}y`;
|
|
}
|
|
|
|
export function validateAll() {
|
|
validationErrors.typeCombination = '';
|
|
|
|
if (!typeValidation.valid) {
|
|
validationErrors.typeCombination = typeValidation.error;
|
|
return false;
|
|
}
|
|
|
|
if (formData.details.Decimal !== null && formData.details.Decimal !== undefined) {
|
|
if (formData.details.Decimal < 0 || formData.details.Decimal > 6) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
</script>
|
|
|
|
<div class="space-y-5">
|
|
<h2 class="text-lg font-semibold text-gray-800">Technical Details</h2>
|
|
|
|
{#if !typeValidation.valid}
|
|
<div class="alert alert-warning text-sm">
|
|
<AlertCircle class="w-4 h-4" />
|
|
<span>{typeValidation.error}</span>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Categorization -->
|
|
<div>
|
|
<h3 class="text-sm font-semibold text-gray-700 mb-3">Categorization</h3>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div class="space-y-1">
|
|
<label for="discipline" class="block text-sm font-medium text-gray-700">Discipline</label>
|
|
<select
|
|
id="discipline"
|
|
class="select select-sm select-bordered w-full"
|
|
bind:value={formData.details.DisciplineID}
|
|
onchange={handleFieldChange}
|
|
>
|
|
<option value={null}>Select discipline...</option>
|
|
{#each disciplineOptions as opt (opt.value)}
|
|
<option value={opt.value}>{opt.label}</option>
|
|
{/each}
|
|
</select>
|
|
</div>
|
|
|
|
<div class="space-y-1">
|
|
<label for="department" class="block text-sm font-medium text-gray-700">Department</label>
|
|
<select
|
|
id="department"
|
|
class="select select-sm select-bordered w-full"
|
|
bind:value={formData.details.DepartmentID}
|
|
onchange={handleFieldChange}
|
|
>
|
|
<option value={null}>Select department...</option>
|
|
{#each departmentOptions as opt (opt.value)}
|
|
<option value={opt.value}>{opt.label}</option>
|
|
{/each}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Result Configuration -->
|
|
<div>
|
|
<h3 class="text-sm font-semibold text-gray-700 mb-3">Result Configuration</h3>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div class="space-y-1">
|
|
<label for="resultType" class="block text-sm font-medium text-gray-700">Result Type</label>
|
|
{#if formData.TestType === 'CALC'}
|
|
<div class="input input-sm input-bordered w-full bg-base-200 text-gray-600 flex items-center">
|
|
Numeric
|
|
</div>
|
|
<input type="hidden" bind:value={formData.details.ResultType} />
|
|
{:else}
|
|
<select
|
|
id="resultType"
|
|
class="select select-sm select-bordered w-full"
|
|
bind:value={formData.details.ResultType}
|
|
onchange={handleResultTypeChange}
|
|
>
|
|
<option value="">Select result type...</option>
|
|
{#each resultTypeOptions as opt (opt.value)}
|
|
<option value={opt.value}>{opt.label}</option>
|
|
{/each}
|
|
</select>
|
|
{/if}
|
|
</div>
|
|
|
|
<div class="space-y-1">
|
|
<label for="refType" class="block text-sm font-medium text-gray-700">Reference Type</label>
|
|
<select
|
|
id="refType"
|
|
class="select select-sm select-bordered w-full"
|
|
bind:value={formData.details.RefType}
|
|
onchange={handleFieldChange}
|
|
disabled={refTypeOptions.length === 0 || refTypeOptions.length === 1}
|
|
>
|
|
<option value="">Select reference type...</option>
|
|
{#each refTypeOptions as opt (opt.value)}
|
|
<option value={opt.value}>{opt.label}</option>
|
|
{/each}
|
|
</select>
|
|
{#if refTypeOptions.length === 1}
|
|
<span class="text-xs text-gray-500">Automatically set based on Result Type</span>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
{#if formData.details.ResultType === 'VSET'}
|
|
<div class="mt-4 space-y-1">
|
|
<label for="vset" class="block text-sm font-medium text-gray-700">
|
|
Value Set <span class="text-error">*</span>
|
|
</label>
|
|
<input
|
|
id="vset"
|
|
type="text"
|
|
class="input input-sm input-bordered w-full md:w-1/2"
|
|
bind:value={formData.details.VSet}
|
|
placeholder="Enter value set key..."
|
|
oninput={handleFieldChange}
|
|
required={formData.details.ResultType === 'VSET'}
|
|
/>
|
|
<span class="text-xs text-gray-500">Required when Result Type is 'Value Set'</span>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- Units & Conversion -->
|
|
<div>
|
|
<h3 class="text-sm font-semibold text-gray-700 mb-3">Units & Conversion</h3>
|
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
<div class="space-y-1">
|
|
<label for="unit1" class="block text-sm font-medium text-gray-700">Unit 1</label>
|
|
<input
|
|
id="unit1"
|
|
type="text"
|
|
class="input input-sm input-bordered w-full"
|
|
bind:value={formData.details.Unit1}
|
|
placeholder="e.g., g/dL"
|
|
oninput={handleFieldChange}
|
|
/>
|
|
</div>
|
|
|
|
<div class="space-y-1">
|
|
<label for="factor" class="block text-sm font-medium text-gray-700">Factor</label>
|
|
<input
|
|
id="factor"
|
|
type="number"
|
|
step="0.01"
|
|
class="input input-sm input-bordered w-full"
|
|
bind:value={formData.details.Factor}
|
|
placeholder="1.0"
|
|
oninput={handleFieldChange}
|
|
/>
|
|
</div>
|
|
|
|
<div class="space-y-1">
|
|
<label for="unit2" class="block text-sm font-medium text-gray-700">Unit 2</label>
|
|
<input
|
|
id="unit2"
|
|
type="text"
|
|
class="input input-sm input-bordered w-full"
|
|
bind:value={formData.details.Unit2}
|
|
placeholder="e.g., g/L"
|
|
oninput={handleFieldChange}
|
|
/>
|
|
</div>
|
|
|
|
<div class="space-y-1">
|
|
<label for="decimal" class="block text-sm font-medium text-gray-700">Decimal</label>
|
|
<input
|
|
id="decimal"
|
|
type="number"
|
|
class="input input-sm input-bordered w-full"
|
|
bind:value={formData.details.Decimal}
|
|
min="0"
|
|
max="6"
|
|
oninput={handleFieldChange}
|
|
/>
|
|
<span class="text-xs text-gray-500">Max 6</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sample Requirements (hidden for CALC tests) -->
|
|
{#if formData.TestType !== 'CALC'}
|
|
<div>
|
|
<h3 class="text-sm font-semibold text-gray-700 mb-3">Sample Requirements</h3>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div class="space-y-1">
|
|
<label for="reqQty" class="block text-sm font-medium text-gray-700">Required Quantity</label>
|
|
<input
|
|
id="reqQty"
|
|
type="number"
|
|
step="0.1"
|
|
min="0"
|
|
class="input input-sm input-bordered w-full"
|
|
bind:value={formData.details.ReqQty}
|
|
placeholder="e.g., 5.0"
|
|
oninput={handleFieldChange}
|
|
/>
|
|
</div>
|
|
|
|
<div class="space-y-1">
|
|
<label for="reqQtyUnit" class="block text-sm font-medium text-gray-700">Quantity Unit</label>
|
|
<input
|
|
id="reqQtyUnit"
|
|
type="text"
|
|
class="input input-sm input-bordered w-full"
|
|
bind:value={formData.details.ReqQtyUnit}
|
|
placeholder="e.g., mL"
|
|
oninput={handleFieldChange}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-4 space-y-1">
|
|
<label for="collReq" class="block text-sm font-medium text-gray-700">Collection Requirements</label>
|
|
<textarea
|
|
id="collReq"
|
|
class="textarea textarea-sm textarea-bordered w-full"
|
|
bind:value={formData.details.CollReq}
|
|
placeholder="e.g., Fasting required, Collect in lavender tube..."
|
|
rows="2"
|
|
oninput={handleFieldChange}
|
|
></textarea>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Method & TAT -->
|
|
<div>
|
|
<h3 class="text-sm font-semibold text-gray-700 mb-3">Method & TAT</h3>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div class="space-y-1">
|
|
<label for="method" class="block text-sm font-medium text-gray-700">Method</label>
|
|
<input
|
|
id="method"
|
|
type="text"
|
|
class="input input-sm input-bordered w-full"
|
|
bind:value={formData.details.Method}
|
|
placeholder="e.g., Automated Analyzer"
|
|
oninput={handleFieldChange}
|
|
/>
|
|
</div>
|
|
|
|
<div class="space-y-1">
|
|
<label for="expectedTAT" class="block text-sm font-medium text-gray-700">Expected TAT (minutes)</label>
|
|
<input
|
|
id="expectedTAT"
|
|
type="number"
|
|
class="input input-sm input-bordered w-full"
|
|
bind:value={formData.details.ExpectedTAT}
|
|
min="0"
|
|
placeholder="e.g., 60"
|
|
oninput={handleFieldChange}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Inline Numeric Reference Ranges - Simple Form -->
|
|
{#if showRefNumSection}
|
|
<div class="border-t pt-5">
|
|
<div class="flex justify-between items-center mb-3">
|
|
<h3 class="text-sm font-semibold text-gray-700 flex items-center gap-2">
|
|
<Hash class="w-4 h-4" />
|
|
Reference Range
|
|
{#if formData.refnum?.length > 0}
|
|
<span class="badge badge-sm badge-primary">{formData.refnum.length}</span>
|
|
{/if}
|
|
</h3>
|
|
{#if formData.refnum?.length > 0}
|
|
<button class="btn btn-ghost btn-xs text-primary" onclick={switchToRefNumTab}>
|
|
<ExternalLink class="w-3 h-3 mr-1" />
|
|
Full Manager
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
|
|
{#if formData.refnum?.length === 0}
|
|
<!-- Simple inline form for first entry -->
|
|
<div class="bg-base-100 border border-base-300 rounded-lg p-4 space-y-4">
|
|
{#if validationErrors.simpleRefNum}
|
|
<div class="alert alert-error alert-sm">
|
|
<span>{validationErrors.simpleRefNum}</span>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Primary: Just Low and High values -->
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="space-y-1">
|
|
<label class="block text-sm font-medium text-gray-700">Low Value</label>
|
|
<input
|
|
type="number"
|
|
step="0.01"
|
|
class="input input-sm input-bordered w-full"
|
|
bind:value={simpleRefNum.Low}
|
|
placeholder="e.g., 10.5"
|
|
/>
|
|
</div>
|
|
<div class="space-y-1">
|
|
<label class="block text-sm font-medium text-gray-700">High Value</label>
|
|
<input
|
|
type="number"
|
|
step="0.01"
|
|
class="input input-sm input-bordered w-full"
|
|
bind:value={simpleRefNum.High}
|
|
placeholder="e.g., 50.0"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Optional: Sex and Age (collapsed by default) -->
|
|
<div class="border-t pt-3">
|
|
<details>
|
|
<summary class="text-sm text-gray-600 cursor-pointer hover:text-gray-800">
|
|
Add Sex/Age Specific Ranges (Optional)
|
|
</summary>
|
|
<div class="mt-3 space-y-3">
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="space-y-1">
|
|
<label class="block text-sm font-medium text-gray-700">Sex</label>
|
|
<select class="select select-sm select-bordered w-full" bind:value={simpleRefNum.Sex}>
|
|
{#each sexOptions as opt (opt.value)}
|
|
<option value={opt.value}>{opt.label}</option>
|
|
{/each}
|
|
</select>
|
|
</div>
|
|
<div class="space-y-1">
|
|
<label class="block text-sm font-medium text-gray-700">Age Unit</label>
|
|
<select class="select select-sm select-bordered w-full" bind:value={simpleRefNum.AgeUnit}>
|
|
{#each ageUnits as opt (opt.value)}
|
|
<option value={opt.value}>{opt.label}</option>
|
|
{/each}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="space-y-1">
|
|
<label class="block text-sm font-medium text-gray-700">From</label>
|
|
<div class="flex items-center gap-2">
|
|
<input
|
|
type="number"
|
|
class="input input-sm input-bordered flex-1"
|
|
bind:value={simpleRefNum.AgeStart}
|
|
placeholder="0"
|
|
min="0"
|
|
/>
|
|
<span class="text-sm text-gray-600 whitespace-nowrap">{simpleRefNum.AgeUnit}</span>
|
|
</div>
|
|
</div>
|
|
<div class="space-y-1">
|
|
<label class="block text-sm font-medium text-gray-700">To</label>
|
|
<div class="flex items-center gap-2">
|
|
<input
|
|
type="number"
|
|
class="input input-sm input-bordered flex-1"
|
|
bind:value={simpleRefNum.AgeEnd}
|
|
placeholder="150"
|
|
min="0"
|
|
/>
|
|
<span class="text-sm text-gray-600 whitespace-nowrap">{simpleRefNum.AgeUnit}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Quick Presets -->
|
|
<div class="pt-1">
|
|
<span class="text-xs text-gray-500 block mb-1">Quick presets:</span>
|
|
<div class="flex flex-wrap gap-1">
|
|
<button class="btn btn-xs btn-ghost" onclick={() => { simpleRefNum.AgeStart = ''; simpleRefNum.AgeEnd = ''; }}>All ages</button>
|
|
<button class="btn btn-xs btn-ghost" onclick={() => { simpleRefNum.AgeStart = 0; simpleRefNum.AgeEnd = 18; simpleRefNum.AgeUnit = 'years'; }}>0-18 years</button>
|
|
<button class="btn btn-xs btn-ghost" onclick={() => { simpleRefNum.AgeStart = 18; simpleRefNum.AgeEnd = 150; simpleRefNum.AgeUnit = 'years'; }}>18+ years</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
|
|
<div class="flex justify-end pt-2">
|
|
<button class="btn btn-sm btn-primary" onclick={addSimpleRefNum}>
|
|
<Plus class="w-4 h-4 mr-1" />
|
|
Add Range
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{:else}
|
|
<!-- Show existing ranges as list -->
|
|
<div class="space-y-2">
|
|
{#each formData.refnum as ref, idx (idx)}
|
|
<div class="bg-base-100 border border-base-300 rounded-lg p-3 flex justify-between items-center">
|
|
<div class="flex items-center gap-3 text-sm">
|
|
<span class="font-mono bg-base-200 px-2 py-1 rounded">
|
|
{ref.Low !== null ? ref.Low : '—'} - {ref.High !== null ? ref.High : '—'}
|
|
</span>
|
|
<span class="text-gray-600">
|
|
{getSexLabel(ref.Sex)}
|
|
{#if ref.AgeStart > 0 || ref.AgeEnd < 54750}
|
|
· {getAgeDisplay(ref.AgeStart)}-{getAgeDisplay(ref.AgeEnd)}
|
|
{/if}
|
|
</span>
|
|
</div>
|
|
<button class="btn btn-ghost btn-xs text-error" onclick={() => removeRefNum(idx)} title="Remove">
|
|
<Trash2 class="w-3 h-3" />
|
|
</button>
|
|
</div>
|
|
{/each}
|
|
|
|
<!-- Add another simple form -->
|
|
<div class="border-t pt-3">
|
|
{#if validationErrors.simpleRefNum}
|
|
<div class="alert alert-error alert-sm mb-3">
|
|
<span>{validationErrors.simpleRefNum}</span>
|
|
</div>
|
|
{/if}
|
|
<div class="grid grid-cols-4 gap-2 items-end">
|
|
<div>
|
|
<label class="block text-xs text-gray-600 mb-1">Low</label>
|
|
<input
|
|
type="number"
|
|
step="0.01"
|
|
class="input input-sm input-bordered w-full"
|
|
bind:value={simpleRefNum.Low}
|
|
placeholder="Low"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs text-gray-600 mb-1">High</label>
|
|
<input
|
|
type="number"
|
|
step="0.01"
|
|
class="input input-sm input-bordered w-full"
|
|
bind:value={simpleRefNum.High}
|
|
placeholder="High"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs text-gray-600 mb-1">Sex</label>
|
|
<select class="select select-sm select-bordered w-full" bind:value={simpleRefNum.Sex}>
|
|
{#each sexOptions as opt (opt.value)}
|
|
<option value={opt.value}>{opt.label}</option>
|
|
{/each}
|
|
</select>
|
|
</div>
|
|
<button class="btn btn-sm btn-primary" onclick={addSimpleRefNum}>
|
|
<Plus class="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Inline Text Reference Ranges - Simple Form -->
|
|
{#if showRefTxtSection}
|
|
<div class="border-t pt-5">
|
|
<div class="flex justify-between items-center mb-3">
|
|
<h3 class="text-sm font-semibold text-gray-700 flex items-center gap-2">
|
|
<Type class="w-4 h-4" />
|
|
Text Reference
|
|
{#if formData.reftxt?.length > 0}
|
|
<span class="badge badge-sm badge-primary">{formData.reftxt.length}</span>
|
|
{/if}
|
|
</h3>
|
|
{#if formData.reftxt?.length > 0}
|
|
<button class="btn btn-ghost btn-xs text-primary" onclick={switchToRefTxtTab}>
|
|
<ExternalLink class="w-3 h-3 mr-1" />
|
|
Full Manager
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
|
|
{#if formData.reftxt?.length === 0}
|
|
<!-- Simple inline form for first entry -->
|
|
<div class="bg-base-100 border border-base-300 rounded-lg p-4 space-y-4">
|
|
{#if validationErrors.simpleRefTxt}
|
|
<div class="alert alert-error alert-sm">
|
|
<span>{validationErrors.simpleRefTxt}</span>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Primary: Just the text value -->
|
|
<div class="space-y-1">
|
|
<label class="block text-sm font-medium text-gray-700">Reference Text</label>
|
|
<input
|
|
type="text"
|
|
class="input input-sm input-bordered w-full"
|
|
bind:value={simpleRefTxt.RefTxt}
|
|
placeholder="e.g., Clear, Positive, Negative"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Optional: Sex and Age (collapsed by default) -->
|
|
<div class="border-t pt-3">
|
|
<details>
|
|
<summary class="text-sm text-gray-600 cursor-pointer hover:text-gray-800">
|
|
Add Sex/Age Specific References (Optional)
|
|
</summary>
|
|
<div class="mt-3 space-y-3">
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="space-y-1">
|
|
<label class="block text-sm font-medium text-gray-700">Sex</label>
|
|
<select class="select select-sm select-bordered w-full" bind:value={simpleRefTxt.Sex}>
|
|
{#each sexOptions as opt (opt.value)}
|
|
<option value={opt.value}>{opt.label}</option>
|
|
{/each}
|
|
</select>
|
|
</div>
|
|
<div class="space-y-1">
|
|
<label class="block text-sm font-medium text-gray-700">Age Unit</label>
|
|
<select class="select select-sm select-bordered w-full" bind:value={simpleRefTxt.AgeUnit}>
|
|
{#each ageUnits as opt (opt.value)}
|
|
<option value={opt.value}>{opt.label}</option>
|
|
{/each}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="space-y-1">
|
|
<label class="block text-sm font-medium text-gray-700">From</label>
|
|
<div class="flex items-center gap-2">
|
|
<input
|
|
type="number"
|
|
class="input input-sm input-bordered flex-1"
|
|
bind:value={simpleRefTxt.AgeStart}
|
|
placeholder="0"
|
|
min="0"
|
|
/>
|
|
<span class="text-sm text-gray-600 whitespace-nowrap">{simpleRefTxt.AgeUnit}</span>
|
|
</div>
|
|
</div>
|
|
<div class="space-y-1">
|
|
<label class="block text-sm font-medium text-gray-700">To</label>
|
|
<div class="flex items-center gap-2">
|
|
<input
|
|
type="number"
|
|
class="input input-sm input-bordered flex-1"
|
|
bind:value={simpleRefTxt.AgeEnd}
|
|
placeholder="150"
|
|
min="0"
|
|
/>
|
|
<span class="text-sm text-gray-600 whitespace-nowrap">{simpleRefTxt.AgeUnit}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Quick Presets -->
|
|
<div class="pt-1">
|
|
<span class="text-xs text-gray-500 block mb-1">Quick presets:</span>
|
|
<div class="flex flex-wrap gap-1">
|
|
<button class="btn btn-xs btn-ghost" onclick={() => { simpleRefTxt.AgeStart = ''; simpleRefTxt.AgeEnd = ''; }}>All ages</button>
|
|
<button class="btn btn-xs btn-ghost" onclick={() => { simpleRefTxt.AgeStart = 0; simpleRefTxt.AgeEnd = 18; simpleRefTxt.AgeUnit = 'years'; }}>0-18 years</button>
|
|
<button class="btn btn-xs btn-ghost" onclick={() => { simpleRefTxt.AgeStart = 18; simpleRefTxt.AgeEnd = 150; simpleRefTxt.AgeUnit = 'years'; }}>18+ years</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
|
|
<div class="flex justify-end pt-2">
|
|
<button class="btn btn-sm btn-primary" onclick={addSimpleRefTxt}>
|
|
<Plus class="w-4 h-4 mr-1" />
|
|
Add Reference
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{:else}
|
|
<!-- Show existing ranges as list -->
|
|
<div class="space-y-2">
|
|
{#each formData.reftxt as ref, idx (idx)}
|
|
<div class="bg-base-100 border border-base-300 rounded-lg p-3 flex justify-between items-center">
|
|
<div class="flex items-center gap-3 text-sm">
|
|
<span class="font-mono bg-base-200 px-2 py-1 rounded">{ref.RefTxt}</span>
|
|
<span class="text-gray-600">
|
|
{getSexLabel(ref.Sex)}
|
|
{#if ref.AgeStart > 0 || ref.AgeEnd < 54750}
|
|
· {getAgeDisplay(ref.AgeStart)}-{getAgeDisplay(ref.AgeEnd)}
|
|
{/if}
|
|
</span>
|
|
</div>
|
|
<button class="btn btn-ghost btn-xs text-error" onclick={() => removeRefTxt(idx)} title="Remove">
|
|
<Trash2 class="w-3 h-3" />
|
|
</button>
|
|
</div>
|
|
{/each}
|
|
|
|
<!-- Add another simple form -->
|
|
<div class="border-t pt-3">
|
|
{#if validationErrors.simpleRefTxt}
|
|
<div class="alert alert-error alert-sm mb-3">
|
|
<span>{validationErrors.simpleRefTxt}</span>
|
|
</div>
|
|
{/if}
|
|
<div class="grid grid-cols-4 gap-2 items-end">
|
|
<div class="col-span-2">
|
|
<label class="block text-xs text-gray-600 mb-1">Reference Text</label>
|
|
<input
|
|
type="text"
|
|
class="input input-sm input-bordered w-full"
|
|
bind:value={simpleRefTxt.RefTxt}
|
|
placeholder="Text value"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs text-gray-600 mb-1">Sex</label>
|
|
<select class="select select-sm select-bordered w-full" bind:value={simpleRefTxt.Sex}>
|
|
{#each sexOptions as opt (opt.value)}
|
|
<option value={opt.value}>{opt.label}</option>
|
|
{/each}
|
|
</select>
|
|
</div>
|
|
<button class="btn btn-sm btn-primary" onclick={addSimpleRefTxt}>
|
|
<Plus class="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</div>
|