265 lines
7.8 KiB
Svelte
265 lines
7.8 KiB
Svelte
|
|
<script>
|
||
|
|
import { validateTestCode, validateTestName } from '$lib/api/tests.js';
|
||
|
|
import { AlertCircle } from 'lucide-svelte';
|
||
|
|
|
||
|
|
let { formData = $bindable(), isDirty = $bindable(false), onTypeChange = null } = $props();
|
||
|
|
|
||
|
|
let validationErrors = $state({
|
||
|
|
TestSiteCode: '',
|
||
|
|
TestSiteName: '',
|
||
|
|
TestType: ''
|
||
|
|
});
|
||
|
|
|
||
|
|
const testTypes = [
|
||
|
|
{ value: 'TEST', label: 'Test - Single Test' },
|
||
|
|
{ value: 'PARAM', label: 'Parameter - Test Parameter' },
|
||
|
|
{ value: 'CALC', label: 'Calculated - Formula-based' },
|
||
|
|
{ value: 'GROUP', label: 'Panel - Test Group' },
|
||
|
|
{ value: 'TITLE', label: 'Header - Section Header' }
|
||
|
|
];
|
||
|
|
|
||
|
|
function validateField(field) {
|
||
|
|
validationErrors[field] = '';
|
||
|
|
|
||
|
|
switch (field) {
|
||
|
|
case 'TestSiteCode':
|
||
|
|
const codeResult = validateTestCode(formData.TestSiteCode);
|
||
|
|
if (!codeResult.valid) {
|
||
|
|
validationErrors[field] = codeResult.error;
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
|
||
|
|
case 'TestSiteName':
|
||
|
|
const nameResult = validateTestName(formData.TestSiteName);
|
||
|
|
if (!nameResult.valid) {
|
||
|
|
validationErrors[field] = nameResult.error;
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
|
||
|
|
case 'TestType':
|
||
|
|
if (!formData.TestType) {
|
||
|
|
validationErrors[field] = 'Test type is required';
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function validateAll() {
|
||
|
|
const fields = ['TestSiteCode', 'TestSiteName', 'TestType'];
|
||
|
|
let isValid = true;
|
||
|
|
|
||
|
|
for (const field of fields) {
|
||
|
|
if (!validateField(field)) {
|
||
|
|
isValid = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return isValid;
|
||
|
|
}
|
||
|
|
|
||
|
|
function handleFieldChange() {
|
||
|
|
isDirty = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
function handleCodeInput(event) {
|
||
|
|
const value = event.target.value.toUpperCase();
|
||
|
|
formData.TestSiteCode = value;
|
||
|
|
handleFieldChange();
|
||
|
|
validateField('TestSiteCode');
|
||
|
|
}
|
||
|
|
|
||
|
|
function handleNameInput(event) {
|
||
|
|
handleFieldChange();
|
||
|
|
validateField('TestSiteName');
|
||
|
|
}
|
||
|
|
|
||
|
|
function handleTestTypeChange(event) {
|
||
|
|
const newType = event.target.value;
|
||
|
|
if (onTypeChange) {
|
||
|
|
onTypeChange(newType);
|
||
|
|
}
|
||
|
|
handleFieldChange();
|
||
|
|
validateField('TestType');
|
||
|
|
}
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<div class="space-y-5">
|
||
|
|
<!-- Test Identity -->
|
||
|
|
<div>
|
||
|
|
<h3 class="text-sm font-semibold text-gray-700 mb-3">Test Identity</h3>
|
||
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
|
|
<!-- Test Code -->
|
||
|
|
<div class="space-y-1">
|
||
|
|
<label for="testCode" class="block text-sm font-medium text-gray-700">
|
||
|
|
Test Code <span class="text-error">*</span>
|
||
|
|
</label>
|
||
|
|
<input
|
||
|
|
id="testCode"
|
||
|
|
type="text"
|
||
|
|
class="input input-sm input-bordered w-full font-mono uppercase"
|
||
|
|
class:input-error={validationErrors.TestSiteCode}
|
||
|
|
bind:value={formData.TestSiteCode}
|
||
|
|
oninput={handleCodeInput}
|
||
|
|
placeholder="e.g., CBC, HGB, WBC"
|
||
|
|
required
|
||
|
|
/>
|
||
|
|
{#if validationErrors.TestSiteCode}
|
||
|
|
<span class="text-xs text-error flex items-center gap-1">
|
||
|
|
<AlertCircle class="w-3 h-3" />
|
||
|
|
{validationErrors.TestSiteCode}
|
||
|
|
</span>
|
||
|
|
{/if}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Test Name -->
|
||
|
|
<div class="space-y-1">
|
||
|
|
<label for="testName" class="block text-sm font-medium text-gray-700">
|
||
|
|
Test Name <span class="text-error">*</span>
|
||
|
|
</label>
|
||
|
|
<input
|
||
|
|
id="testName"
|
||
|
|
type="text"
|
||
|
|
class="input input-sm input-bordered w-full"
|
||
|
|
class:input-error={validationErrors.TestSiteName}
|
||
|
|
bind:value={formData.TestSiteName}
|
||
|
|
oninput={handleNameInput}
|
||
|
|
placeholder="e.g., Complete Blood Count"
|
||
|
|
maxlength="255"
|
||
|
|
required
|
||
|
|
/>
|
||
|
|
{#if validationErrors.TestSiteName}
|
||
|
|
<span class="text-xs text-error flex items-center gap-1">
|
||
|
|
<AlertCircle class="w-3 h-3" />
|
||
|
|
{validationErrors.TestSiteName}
|
||
|
|
</span>
|
||
|
|
{:else}
|
||
|
|
<span class="text-xs text-gray-500">3-255 characters</span>
|
||
|
|
{/if}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Classification -->
|
||
|
|
<div>
|
||
|
|
<h3 class="text-sm font-semibold text-gray-700 mb-3">Classification</h3>
|
||
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
|
|
<!-- Test Type -->
|
||
|
|
<div class="space-y-1">
|
||
|
|
<label for="testType" class="block text-sm font-medium text-gray-700">
|
||
|
|
Test Type <span class="text-error">*</span>
|
||
|
|
</label>
|
||
|
|
<select
|
||
|
|
id="testType"
|
||
|
|
class="select select-sm select-bordered w-full"
|
||
|
|
class:select-error={validationErrors.TestType}
|
||
|
|
bind:value={formData.TestType}
|
||
|
|
onchange={handleTestTypeChange}
|
||
|
|
>
|
||
|
|
{#each testTypes as type (type.value)}
|
||
|
|
<option value={type.value}>{type.label}</option>
|
||
|
|
{/each}
|
||
|
|
</select>
|
||
|
|
{#if validationErrors.TestType}
|
||
|
|
<span class="text-xs text-error flex items-center gap-1">
|
||
|
|
<AlertCircle class="w-3 h-3" />
|
||
|
|
{validationErrors.TestType}
|
||
|
|
</span>
|
||
|
|
{/if}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Description -->
|
||
|
|
<div class="space-y-1">
|
||
|
|
<label for="description" class="block text-sm font-medium text-gray-700">Description</label>
|
||
|
|
<input
|
||
|
|
id="description"
|
||
|
|
type="text"
|
||
|
|
class="input input-sm input-bordered w-full"
|
||
|
|
bind:value={formData.Description}
|
||
|
|
placeholder="Optional description..."
|
||
|
|
maxlength="500"
|
||
|
|
oninput={handleFieldChange}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Display Settings -->
|
||
|
|
<div>
|
||
|
|
<h3 class="text-sm font-semibold text-gray-700 mb-3">Display Settings</h3>
|
||
|
|
<div class="grid grid-cols-5 gap-4">
|
||
|
|
<!-- Screen Sequence -->
|
||
|
|
<div class="space-y-1">
|
||
|
|
<label for="seqScr" class="block text-sm font-medium text-gray-700">Screen Seq</label>
|
||
|
|
<input
|
||
|
|
id="seqScr"
|
||
|
|
type="number"
|
||
|
|
class="input input-sm input-bordered w-full"
|
||
|
|
bind:value={formData.SeqScr}
|
||
|
|
min="0"
|
||
|
|
oninput={handleFieldChange}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Report Sequence -->
|
||
|
|
<div class="space-y-1">
|
||
|
|
<label for="seqRpt" class="block text-sm font-medium text-gray-700">Report Seq</label>
|
||
|
|
<input
|
||
|
|
id="seqRpt"
|
||
|
|
type="number"
|
||
|
|
class="input input-sm input-bordered w-full"
|
||
|
|
bind:value={formData.SeqRpt}
|
||
|
|
min="0"
|
||
|
|
oninput={handleFieldChange}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Visible Screen -->
|
||
|
|
<div class="space-y-1">
|
||
|
|
<span class="block text-sm font-medium text-gray-700">Screen</span>
|
||
|
|
<label class="flex items-center gap-2 cursor-pointer">
|
||
|
|
<input
|
||
|
|
type="checkbox"
|
||
|
|
class="checkbox checkbox-sm checkbox-primary"
|
||
|
|
bind:checked={formData.VisibleScr}
|
||
|
|
onchange={handleFieldChange}
|
||
|
|
/>
|
||
|
|
<span class="text-sm">Visible</span>
|
||
|
|
</label>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Visible Report -->
|
||
|
|
<div class="space-y-1">
|
||
|
|
<span class="block text-sm font-medium text-gray-700">Report</span>
|
||
|
|
<label class="flex items-center gap-2 cursor-pointer">
|
||
|
|
<input
|
||
|
|
type="checkbox"
|
||
|
|
class="checkbox checkbox-sm checkbox-primary"
|
||
|
|
bind:checked={formData.VisibleRpt}
|
||
|
|
onchange={handleFieldChange}
|
||
|
|
/>
|
||
|
|
<span class="text-sm">Visible</span>
|
||
|
|
</label>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Count Statistics -->
|
||
|
|
<div class="space-y-1">
|
||
|
|
<span class="block text-sm font-medium text-gray-700">Statistics</span>
|
||
|
|
<label class="flex items-center gap-2 cursor-pointer">
|
||
|
|
<input
|
||
|
|
type="checkbox"
|
||
|
|
class="checkbox checkbox-sm checkbox-primary"
|
||
|
|
bind:checked={formData.CountStat}
|
||
|
|
onchange={handleFieldChange}
|
||
|
|
/>
|
||
|
|
<span class="text-sm">Count</span>
|
||
|
|
</label>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|