307 lines
10 KiB
Svelte
307 lines
10 KiB
Svelte
|
|
<script>
|
||
|
|
import { getResultTypeOptions, getRefTypeOptions, validateTypeCombination } from '$lib/api/tests.js';
|
||
|
|
import { AlertCircle } from 'lucide-svelte';
|
||
|
|
|
||
|
|
let { formData = $bindable(), disciplines = [], departments = [], isDirty = $bindable(false) } = $props();
|
||
|
|
|
||
|
|
let validationErrors = $state({
|
||
|
|
typeCombination: ''
|
||
|
|
});
|
||
|
|
|
||
|
|
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;
|
||
|
|
});
|
||
|
|
|
||
|
|
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 = '';
|
||
|
|
}
|
||
|
|
|
||
|
|
handleFieldChange();
|
||
|
|
}
|
||
|
|
|
||
|
|
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>
|
||
|
|
<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 formData.TestType === 'CALC'}
|
||
|
|
<span class="text-xs text-gray-500">CALC type always uses Numeric Reference</span>
|
||
|
|
{/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 -->
|
||
|
|
<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>
|
||
|
|
|
||
|
|
<!-- 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>
|
||
|
|
</div>
|