feat: Update test modal UX and improve DataTable/Modal components

This commit is contained in:
mahdahar 2026-02-19 16:30:41 +07:00
parent 995cdd3fec
commit 1af4adddf7
21 changed files with 5454 additions and 703 deletions

4866
docs/api-docs.bundled.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -68,7 +68,7 @@
<thead>
<tr class="bg-base-200">
{#each columns as column}
<th class="font-semibold {column.class || ''}">
<th class="text-sm font-semibold {column.class || ''}">
{column.label}
</th>
{/each}
@ -102,7 +102,7 @@
role={onRowClick ? 'button' : undefined}
>
{#each columns as column}
<td class="{column.class || ''}">
<td class="text-sm {column.class || ''}">
{#if cell}
{@render cell({ column, row, value: getValue(row, column.key), index })}
{:else if column.render}

View File

@ -83,7 +83,7 @@
<!-- Header -->
<div class="flex items-center justify-between mb-4">
{#if title}
<h3 id="modal-title" class="font-bold text-lg">{title}</h3>
<h3 id="modal-title" class="font-bold text-base">{title}</h3>
{:else}
<div></div>
{/if}

View File

@ -80,8 +80,8 @@
</script>
<div class="p-4">
<h1 class="text-3xl font-bold text-gray-800 mb-2">Master Data</h1>
<p class="text-gray-600 mb-8">Manage reference data and lookup values used throughout the system</p>
<h1 class="text-xl font-bold text-gray-800 mb-2">Master Data</h1>
<p class="text-sm text-gray-600 mb-8">Manage reference data and lookup values used throughout the system</p>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{#each modules as module}

View File

@ -172,8 +172,8 @@
<ArrowLeft class="w-5 h-5" />
</a>
<div class="flex-1">
<h1 class="text-3xl font-bold text-gray-800">Contacts</h1>
<p class="text-gray-600">Manage physicians and contacts</p>
<h1 class="text-xl font-bold text-gray-800">Contacts</h1>
<p class="text-sm text-gray-600">Manage physicians and contacts</p>
</div>
<button class="btn btn-primary" onclick={openCreateModal}>
<Plus class="w-4 h-4 mr-2" />
@ -187,7 +187,7 @@
<Search class="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
<input
type="text"
class="input input-bordered w-full pl-10"
class="input input-sm input-bordered w-full pl-10"
placeholder="Search by name or initial..."
bind:value={searchQuery}
/>
@ -214,10 +214,10 @@
<div class="bg-base-200 rounded-full p-6 mb-4">
<Users class="w-12 h-12 text-gray-400" />
</div>
<h3 class="text-lg font-semibold text-gray-700 mb-1">
<h3 class="text-base font-semibold text-gray-700 mb-1">
{searchQuery ? 'No contacts found' : 'No contacts yet'}
</h3>
<p class="text-gray-500 text-center max-w-sm mb-4">
<p class="text-xs text-gray-500 text-center max-w-sm mb-4">
{searchQuery
? `No contacts matching "${searchQuery}". Try a different search term.`
: 'Get started by adding your first physician or contact.'}
@ -266,7 +266,7 @@
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control">
<label class="label" for="initial">
<span class="label-text font-medium flex items-center gap-2">
<span class="label-text text-sm font-medium flex items-center gap-2">
Initial
<HelpTooltip
text="A unique short code used to identify this doctor (e.g., 'JS' for John Smith). This will be used in reports and quick reference."
@ -274,7 +274,7 @@
position="right"
/>
</span>
<span class="label-text-alt text-error">*</span>
<span class="label-text-alt text-xs text-error">*</span>
</label>
<input
id="initial"
@ -286,12 +286,12 @@
required
/>
<label class="label" for="initial">
<span class="label-text-alt text-gray-500">Unique identifier for this doctor</span>
<span class="label-text-alt text-xs text-gray-500">Unique identifier for this doctor</span>
</label>
</div>
<div class="form-control">
<label class="label" for="title">
<span class="label-text font-medium">Title</span>
<span class="label-text text-sm font-medium">Title</span>
</label>
<input
id="title"
@ -305,7 +305,7 @@
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control">
<label class="label" for="nameFirst">
<span class="label-text font-medium">First Name</span>
<span class="label-text text-sm font-medium">First Name</span>
</label>
<input
id="nameFirst"
@ -317,7 +317,7 @@
</div>
<div class="form-control">
<label class="label" for="nameLast">
<span class="label-text font-medium">Last Name</span>
<span class="label-text text-sm font-medium">Last Name</span>
</label>
<input
id="nameLast"

View File

@ -152,8 +152,8 @@
<ArrowLeft class="w-5 h-5" />
</a>
<div class="flex-1">
<h1 class="text-3xl font-bold text-gray-800">Containers</h1>
<p class="text-gray-600">Manage specimen containers and tubes</p>
<h1 class="text-xl font-bold text-gray-800">Containers</h1>
<p class="text-sm text-gray-600">Manage specimen containers and tubes</p>
</div>
<button class="btn btn-primary" onclick={openCreateModal}>
<Plus class="w-4 h-4 mr-2" />
@ -188,10 +188,10 @@
<div class="bg-base-200 rounded-full p-6 mb-4">
<FlaskConical class="w-12 h-12 text-gray-400" />
</div>
<h3 class="text-lg font-semibold text-gray-700 mb-2">
<h3 class="text-base font-semibold text-gray-700 mb-2">
{searchQuery.trim() ? 'No containers match your search' : 'No containers found'}
</h3>
<p class="text-gray-500 text-center max-w-md mb-6">
<p class="text-xs text-gray-500 text-center max-w-md mb-6">
{searchQuery.trim()
? `No containers found matching "${searchQuery}". Try a different search term or clear the filter.`
: 'Get started by adding your first specimen container or tube to the system.'}
@ -253,8 +253,8 @@
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control">
<label class="label" for="code">
<span class="label-text font-medium">Container Code</span>
<span class="label-text-alt text-error">*</span>
<span class="label-text text-sm font-medium">Container Code</span>
<span class="label-text-alt text-xs text-error">*</span>
</label>
<input
id="code"
@ -264,12 +264,12 @@
placeholder="e.g., SST, EDTA, HEP"
required
/>
<span class="label-text-alt text-gray-500">Unique identifier for this container type</span>
<span class="label-text-alt text-xs text-gray-500">Unique identifier for this container type</span>
</div>
<div class="form-control">
<label class="label" for="name">
<span class="label-text font-medium">Container Name</span>
<span class="label-text-alt text-error">*</span>
<span class="label-text text-sm font-medium">Container Name</span>
<span class="label-text-alt text-xs text-error">*</span>
</label>
<input
id="name"
@ -279,12 +279,12 @@
placeholder="e.g., Serum Separator Tube"
required
/>
<span class="label-text-alt text-gray-500">Descriptive name displayed in the system</span>
<span class="label-text-alt text-xs text-gray-500">Descriptive name displayed in the system</span>
</div>
</div>
<div class="form-control">
<label class="label" for="desc">
<span class="label-text font-medium">Description</span>
<span class="label-text text-sm font-medium">Description</span>
</label>
<input
id="desc"
@ -293,12 +293,12 @@
bind:value={formData.ConDesc}
placeholder="e.g., Evacuated blood collection tube with gel separator"
/>
<span class="label-text-alt text-gray-500">Optional detailed description of the container</span>
<span class="label-text-alt text-xs text-gray-500">Optional detailed description of the container</span>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="form-control">
<label class="label" for="class">
<span class="label-text font-medium flex items-center gap-2">
<span class="label-text text-sm font-medium flex items-center gap-2">
Container Class
<HelpTooltip
text="The general category of this container. Examples: Tube (for blood collection tubes), Cup (for urine or fluid cups), Swab (for specimen swabs)."
@ -312,11 +312,11 @@
bind:value={formData.ConClass}
placeholder="Select class..."
/>
<span class="label-text-alt text-gray-500">Category of container</span>
<span class="label-text-alt text-xs text-gray-500">Category of container</span>
</div>
<div class="form-control">
<label class="label" for="additive">
<span class="label-text font-medium flex items-center gap-2">
<span class="label-text text-sm font-medium flex items-center gap-2">
Additive
<HelpTooltip
text="Any chemical additive present in the container. Examples: EDTA (anticoagulant for CBC), Heparin (anticoagulant for chemistry), SST (Serum Separator Tube with gel), None (plain tube)."
@ -330,11 +330,11 @@
bind:value={formData.Additive}
placeholder="Select additive..."
/>
<span class="label-text-alt text-gray-500">Chemical additive inside</span>
<span class="label-text-alt text-xs text-gray-500">Chemical additive inside</span>
</div>
<div class="form-control">
<label class="label" for="color">
<span class="label-text font-medium flex items-center gap-2">
<span class="label-text text-sm font-medium flex items-center gap-2">
Cap Color
<HelpTooltip
text="The color of the container cap or closure. This is an industry standard for identifying container types at a glance (e.g., Lavender = EDTA, Red = Plain serum, Green = Heparin)."
@ -348,7 +348,7 @@
bind:value={formData.Color}
placeholder="Select color..."
/>
<span class="label-text-alt text-gray-500">Visual identification color</span>
<span class="label-text-alt text-xs text-gray-500">Visual identification color</span>
</div>
</div>
</form>

View File

@ -204,8 +204,8 @@
<ArrowLeft class="w-5 h-5" />
</a>
<div class="flex-1">
<h1 class="text-3xl font-bold text-gray-800">Counters</h1>
<p class="text-gray-600">Manage ID generation counters</p>
<h1 class="text-xl font-bold text-gray-800">Counters</h1>
<p class="text-sm text-gray-600">Manage ID generation counters</p>
</div>
<button class="btn btn-primary" onclick={openCreateModal}>
<Plus class="w-4 h-4 mr-2" />
@ -232,7 +232,7 @@
<Search class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-base-content/50" />
<input
type="text"
class="input input-bordered w-full pl-10"
class="input input-sm input-bordered w-full pl-10"
placeholder="Search by counter description..."
bind:value={searchQuery}
/>
@ -262,7 +262,7 @@
<div class="bg-base-200 rounded-full p-4 mb-4">
<Hash class="w-8 h-8 text-base-content/40" />
</div>
<h3 class="text-lg font-semibold text-base-content/70 mb-1">
<h3 class="text-base font-semibold text-base-content/70 mb-1">
{#if searchQuery}
No counters found
{:else}
@ -331,8 +331,8 @@
<!-- Description -->
<div class="form-control">
<label class="label" for="counterDesc">
<span class="label-text font-medium">Counter Description</span>
<span class="label-text-alt text-error">*</span>
<span class="label-text text-sm font-medium">Counter Description</span>
<span class="label-text-alt text-xs text-error">*</span>
</label>
<input
id="counterDesc"
@ -344,7 +344,7 @@
required
/>
<label class="label" for="counterDesc">
<span class="label-text-alt text-base-content/60">
<span class="label-text-alt text-xs text-base-content/60">
A descriptive name for this counter
</span>
</label>
@ -355,8 +355,8 @@
<!-- Current Value -->
<div class="form-control">
<label class="label" for="counterValue">
<span class="label-text font-medium">Current Value</span>
<span class="label-text-alt text-error">*</span>
<span class="label-text text-sm font-medium">Current Value</span>
<span class="label-text-alt text-xs text-error">*</span>
<HelpTooltip
text="The next number that will be assigned. This increments automatically each time an ID is generated."
title="Current Value"
@ -376,7 +376,7 @@
/>
{#if formErrors.CounterValue}
<label class="label" for="counterValue">
<span class="label-text-alt text-error">{formErrors.CounterValue}</span>
<span class="label-text-alt text-xs text-error">{formErrors.CounterValue}</span>
</label>
{/if}
</div>
@ -384,8 +384,8 @@
<!-- Start Value -->
<div class="form-control">
<label class="label" for="counterStart">
<span class="label-text font-medium">Start Value</span>
<span class="label-text-alt text-error">*</span>
<span class="label-text text-sm font-medium">Start Value</span>
<span class="label-text-alt text-xs text-error">*</span>
<HelpTooltip
text="The minimum value this counter can have. When reset, the counter returns to this value."
title="Start Value"
@ -405,7 +405,7 @@
/>
{#if formErrors.CounterStart}
<label class="label" for="counterStart">
<span class="label-text-alt text-error">{formErrors.CounterStart}</span>
<span class="label-text-alt text-xs text-error">{formErrors.CounterStart}</span>
</label>
{/if}
</div>
@ -413,7 +413,7 @@
<!-- End Value -->
<div class="form-control">
<label class="label" for="counterEnd">
<span class="label-text font-medium">End Value</span>
<span class="label-text text-sm font-medium">End Value</span>
<HelpTooltip
text="Optional maximum value. When reached, the counter will reset to the start value. Leave empty for no limit."
title="End Value"
@ -432,7 +432,7 @@
/>
{#if formErrors.CounterEnd}
<label class="label" for="counterEnd">
<span class="label-text-alt text-error">{formErrors.CounterEnd}</span>
<span class="label-text-alt text-xs text-error">{formErrors.CounterEnd}</span>
</label>
{/if}
</div>
@ -441,7 +441,7 @@
<!-- Reset Pattern -->
<div class="form-control">
<label class="label" for="counterReset">
<span class="label-text font-medium">Reset Pattern</span>
<span class="label-text text-sm font-medium">Reset Pattern</span>
<HelpTooltip
text="Determines when the counter automatically resets to the start value. Daily resets at midnight, monthly on the 1st, yearly on Jan 1st."
title="Reset Pattern"
@ -460,7 +460,7 @@
{/each}
</select>
<label class="label" for="counterReset">
<span class="label-text-alt text-base-content/60">
<span class="label-text-alt text-xs text-base-content/60">
Choose when the counter automatically resets to the start value
</span>
</label>

View File

@ -190,14 +190,14 @@
</a>
<div class="flex-1">
<div class="flex items-center gap-2">
<h1 class="text-3xl font-bold text-gray-800">Geography</h1>
<h1 class="text-xl font-bold text-gray-800">Geography</h1>
<HelpTooltip
title="Geography Data"
text="Geography data is used for patient address management throughout the system. This includes province, city, and detailed area information."
position="right"
/>
</div>
<p class="text-gray-600">View geographical areas, provinces, and cities</p>
<p class="text-sm text-gray-600">View geographical areas, provinces, and cities</p>
</div>
</div>
@ -264,7 +264,7 @@
<input
type="text"
placeholder="Search provinces by name..."
class="input input-bordered w-full pl-10"
class="input input-sm input-bordered w-full pl-10"
bind:value={provinceSearch}
/>
</div>
@ -350,7 +350,7 @@
? `Search cities in ${selectedProvinceLabel}...`
: "Search cities by name..."
}
class="input input-bordered w-full pl-10"
class="input input-sm input-bordered w-full pl-10"
bind:value={citySearch}
/>
</div>
@ -412,7 +412,7 @@
<input
type="text"
placeholder="Search areas by name, code, or class..."
class="input input-bordered w-full pl-10"
class="input input-sm input-bordered w-full pl-10"
bind:value={areaSearch}
/>
</div>

View File

@ -169,8 +169,8 @@
<ArrowLeft class="w-5 h-5" />
</a>
<div class="flex-1">
<h1 class="text-3xl font-bold text-gray-800">Locations</h1>
<p class="text-gray-600">Manage locations and facilities</p>
<h1 class="text-xl font-bold text-gray-800">Locations</h1>
<p class="text-sm text-gray-600">Manage locations and facilities</p>
</div>
<button class="btn btn-primary" onclick={openCreateModal}>
<Plus class="w-4 h-4 mr-2" />
@ -185,7 +185,7 @@
<Search class="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
<input
type="text"
class="input input-bordered w-full pl-10"
class="input input-sm input-bordered w-full pl-10"
placeholder="Search by code or name..."
bind:value={searchQuery}
/>
@ -203,7 +203,7 @@
<div class="w-16 h-16 rounded-full bg-base-200 flex items-center justify-center mb-4">
<MapPin class="w-8 h-8 text-gray-400" />
</div>
<h3 class="text-lg font-semibold text-base-content mb-1">
<h3 class="text-base font-semibold text-base-content mb-1">
{searchQuery ? 'No locations found' : 'No locations yet'}
</h3>
<p class="text-sm text-base-content/60 text-center max-w-sm mb-4">
@ -258,8 +258,8 @@
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control">
<label class="label" for="code">
<span class="label-text font-medium">Location Code</span>
<span class="label-text-alt text-error">*</span>
<span class="label-text text-sm font-medium">Location Code</span>
<span class="label-text-alt text-xs text-error">*</span>
</label>
<input
id="code"
@ -270,13 +270,13 @@
required
/>
<label class="label" for="code">
<span class="label-text-alt text-gray-500">Unique identifier for this location</span>
<span class="label-text-alt text-xs text-gray-500">Unique identifier for this location</span>
</label>
</div>
<div class="form-control">
<label class="label" for="name">
<span class="label-text font-medium">Location Name</span>
<span class="label-text-alt text-error">*</span>
<span class="label-text text-sm font-medium">Location Name</span>
<span class="label-text-alt text-xs text-error">*</span>
</label>
<input
id="name"
@ -287,15 +287,15 @@
required
/>
<label class="label" for="name">
<span class="label-text-alt text-gray-500">Descriptive name for this location</span>
<span class="label-text-alt text-xs text-gray-500">Descriptive name for this location</span>
</label>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control">
<label class="label" for="type">
<span class="label-text font-medium">Location Type</span>
<span class="label-text-alt text-error">*</span>
<span class="label-text text-sm font-medium">Location Type</span>
<span class="label-text-alt text-xs text-error">*</span>
</label>
<select
id="type"
@ -311,12 +311,12 @@
<option value="AREA">Area</option>
</select>
<label class="label" for="type">
<span class="label-text-alt text-gray-500">Category of this location</span>
<span class="label-text-alt text-xs text-gray-500">Category of this location</span>
</label>
</div>
<div class="form-control">
<label class="label" for="parent">
<span class="label-text font-medium">Parent Location</span>
<span class="label-text text-sm font-medium">Parent Location</span>
<HelpTooltip
text="Select a parent location to create a hierarchy. For example, a Room can be inside a Building, or a Floor can be part of a Building."
title="Location Hierarchy"
@ -330,7 +330,7 @@
placeholder="None (top-level location)"
/>
<label class="label" for="parent">
<span class="label-text-alt text-gray-500">Optional: parent location in hierarchy</span>
<span class="label-text-alt text-xs text-gray-500">Optional: parent location in hierarchy</span>
</label>
</div>
</div>

View File

@ -160,8 +160,8 @@
<ArrowLeft class="w-5 h-5" />
</a>
<div class="flex-1">
<h1 class="text-3xl font-bold text-gray-800">Occupations</h1>
<p class="text-gray-600">Manage occupation codes for patient demographics</p>
<h1 class="text-xl font-bold text-gray-800">Occupations</h1>
<p class="text-sm text-gray-600">Manage occupation codes for patient demographics</p>
</div>
<button class="btn btn-primary" onclick={openCreateModal}>
<Plus class="w-4 h-4 mr-2" />
@ -177,7 +177,7 @@
<input
type="text"
placeholder="Search by code or occupation name..."
class="input input-bordered w-full pl-10"
class="input input-sm input-bordered w-full pl-10"
bind:value={searchQuery}
onkeydown={handleSearchKeydown}
/>
@ -196,10 +196,10 @@
<Briefcase class="w-12 h-12 text-gray-400" />
{/if}
</div>
<h3 class="text-lg font-semibold text-gray-700 mb-2">
<h3 class="text-base font-semibold text-gray-700 mb-2">
{searchQuery.trim() ? 'No occupations match your search' : 'No occupations found'}
</h3>
<p class="text-gray-500 text-center max-w-md mb-6">
<p class="text-xs text-gray-500 text-center max-w-md mb-6">
{searchQuery.trim()
? `No occupations found matching "${searchQuery}". Try a different search term or clear the filter.`
: 'Get started by adding your first occupation code. These codes are used when registering patients to identify their profession.'}
@ -264,7 +264,7 @@
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control">
<label class="label" for="occCode">
<span class="label-text font-medium flex items-center gap-2">
<span class="label-text text-sm font-medium flex items-center gap-2">
Occupation Code
<HelpTooltip
text="A short, unique code used to identify this occupation. This code will be displayed in patient demographics. Examples: DR (Doctor), NUR (Nurse), ENG (Engineer), TCH (Teacher). Keep it short (2-5 characters) for easy reference."
@ -272,7 +272,7 @@
position="top"
/>
</span>
<span class="label-text-alt text-error">*</span>
<span class="label-text-alt text-xs text-error">*</span>
</label>
<input
id="occCode"
@ -285,15 +285,15 @@
disabled={saving}
/>
{#if formErrors.OccCode}
<span class="label-text-alt text-error mt-1">{formErrors.OccCode}</span>
<span class="label-text-alt text-xs text-error mt-1">{formErrors.OccCode}</span>
{:else}
<span class="label-text-alt text-gray-500">Short unique code (max 10 characters)</span>
<span class="label-text-alt text-xs text-gray-500">Short unique code (max 10 characters)</span>
{/if}
</div>
<div class="form-control">
<label class="label" for="occText">
<span class="label-text font-medium">Occupation Name</span>
<span class="label-text-alt text-error">*</span>
<span class="label-text text-sm font-medium">Occupation Name</span>
<span class="label-text-alt text-xs text-error">*</span>
</label>
<input
id="occText"
@ -306,15 +306,15 @@
disabled={saving}
/>
{#if formErrors.OccText}
<span class="label-text-alt text-error mt-1">{formErrors.OccText}</span>
<span class="label-text-alt text-xs text-error mt-1">{formErrors.OccText}</span>
{:else}
<span class="label-text-alt text-gray-500">Full name of the occupation</span>
<span class="label-text-alt text-xs text-gray-500">Full name of the occupation</span>
{/if}
</div>
</div>
<div class="form-control">
<label class="label" for="description">
<span class="label-text font-medium">Description</span>
<span class="label-text text-sm font-medium">Description</span>
</label>
<input
id="description"
@ -326,9 +326,9 @@
disabled={saving}
/>
{#if formErrors.Description}
<span class="label-text-alt text-error mt-1">{formErrors.Description}</span>
<span class="label-text-alt text-xs text-error mt-1">{formErrors.Description}</span>
{:else}
<span class="label-text-alt text-gray-500">Optional additional details about this occupation</span>
<span class="label-text-alt text-xs text-gray-500">Optional additional details about this occupation</span>
{/if}
</div>
</form>

View File

@ -149,8 +149,8 @@
<ArrowLeft class="w-5 h-5" />
</a>
<div class="flex-1">
<h1 class="text-3xl font-bold text-gray-800">Medical Specialties</h1>
<p class="text-gray-600">Manage medical specialty codes and their hierarchical relationships</p>
<h1 class="text-xl font-bold text-gray-800">Medical Specialties</h1>
<p class="text-sm text-gray-600">Manage medical specialty codes and their hierarchical relationships</p>
</div>
<button class="btn btn-primary" onclick={openCreateModal}>
<Plus class="w-4 h-4 mr-2" />
@ -164,7 +164,7 @@
<Search class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
type="text"
class="input input-bordered w-full pl-10"
class="input input-sm input-bordered w-full pl-10"
placeholder="Search by specialty name, title, or parent..."
bind:value={searchQuery}
/>
@ -187,7 +187,7 @@
<div class="bg-base-200 rounded-full p-6 mb-4">
<Stethoscope class="w-12 h-12 text-base-content/40" />
</div>
<h3 class="text-lg font-semibold text-base-content mb-2">
<h3 class="text-base font-semibold text-base-content mb-2">
{searchQuery ? 'No specialties match your search' : 'No specialties yet'}
</h3>
<p class="text-base-content/60 text-center max-w-sm mb-4">
@ -256,8 +256,8 @@
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control">
<label class="label" for="specialtyText">
<span class="label-text font-medium">Specialty Name</span>
<span class="label-text-alt text-error">*</span>
<span class="label-text text-sm font-medium">Specialty Name</span>
<span class="label-text-alt text-xs text-error">*</span>
</label>
<input
id="specialtyText"
@ -268,12 +268,12 @@
required
/>
<label class="label" for="specialtyText">
<span class="label-text-alt text-base-content/50">Unique name for the specialty</span>
<span class="label-text-alt text-xs text-base-content/50">Unique name for the specialty</span>
</label>
</div>
<div class="form-control">
<label class="label" for="title">
<span class="label-text font-medium">Professional Title</span>
<span class="label-text text-sm font-medium">Professional Title</span>
</label>
<input
id="title"
@ -283,14 +283,14 @@
placeholder="e.g., Sp. PD, Sp. A, Sp. And"
/>
<label class="label" for="title">
<span class="label-text-alt text-base-content/50">Official abbreviation or title</span>
<span class="label-text-alt text-xs text-base-content/50">Official abbreviation or title</span>
</label>
</div>
</div>
<div class="form-control">
<div class="flex items-center gap-2 mb-1">
<label class="label py-0" for="parent">
<span class="label-text font-medium">Parent Specialty</span>
<span class="label-text text-sm font-medium">Parent Specialty</span>
</label>
<HelpTooltip
text="Organize specialties hierarchically. For example, select 'Internal Medicine' as the parent for 'Cardiology' to show it as a subspecialty."
@ -306,7 +306,7 @@
placeholder="None (Top-level specialty)"
/>
<label class="label" for="parent">
<span class="label-text-alt text-base-content/50">
<span class="label-text-alt text-xs text-base-content/50">
Optional: Select a parent specialty to create a subspecialty
</span>
</label>

View File

@ -7,7 +7,7 @@
import Modal from '$lib/components/Modal.svelte';
import TestModal from './TestModal.svelte';
import TestTypeSelector from './test-modal/TestTypeSelector.svelte';
import { validateNumericRange, validateTholdRange, validateTextRange, validateVsetRange } from './referenceRange.js';
import { validateNumericRange, validateTextRange } from './referenceRange.js';
import { Plus, Edit2, Trash2, ArrowLeft, Filter, Search, ChevronDown, ChevronRight, Microscope, Variable, Calculator, Box, Layers } from 'lucide-svelte';
let loading = $state(false), tests = $state([]), disciplines = $state([]), departments = $state([]);
@ -30,13 +30,11 @@
Description: '',
CountStat: false,
Unit: '',
Formula: '',
refnum: [],
refthold: [],
reftxt: [],
refvset: [],
refRangeType: 'none',
// Technical Config (testdeftech)
Formula: '',
refnum: [],
reftxt: [],
refRangeType: 'none',
// Technical Config (flat structure)
ResultType: '',
RefType: '',
ReqQty: null,
@ -99,9 +97,7 @@ function openCreateModal(type = 'TEST') {
Unit: '',
Formula: '',
refnum: [],
refthold: [],
reftxt: [],
refvset: [],
refRangeType: 'none',
// Technical Config
ResultType: '',
@ -127,15 +123,33 @@ function openCreateModal(type = 'TEST') {
const testDetail = response.data;
modalMode = 'edit';
// Consolidate refthold into refnum and refvset into reftxt
const consolidatedRefnum = [
...(testDetail.refnum || []),
...(testDetail.refthold || []).map(ref => ({ ...ref, RefType: 'THOLD' }))
];
const consolidatedReftxt = [
...(testDetail.reftxt || []),
...(testDetail.refvset || []).map(ref => ({ ...ref, RefType: 'VSET' }))
];
// Determine refRangeType based on consolidated arrays
let refRangeType = 'none';
if (testDetail.refnum?.length > 0) refRangeType = 'num';
else if (testDetail.refthold?.length > 0) refRangeType = 'thold';
else if (testDetail.reftxt?.length > 0) refRangeType = 'text';
else if (testDetail.refvset?.length > 0) refRangeType = 'vset';
const hasNum = consolidatedRefnum.some(ref => ref.RefType !== 'THOLD');
const hasThold = consolidatedRefnum.some(ref => ref.RefType === 'THOLD');
const hasText = consolidatedReftxt.some(ref => ref.RefType !== 'VSET');
const hasVset = consolidatedReftxt.some(ref => ref.RefType === 'VSET');
if (hasNum) refRangeType = 'num';
else if (hasThold) refRangeType = 'thold';
else if (hasText) refRangeType = 'text';
else if (hasVset) refRangeType = 'vset';
// Normalize reference range data to ensure all fields have values (not undefined)
const normalizeRefNum = (ref) => ({
Sex: ref.Sex ?? '2',
RefType: ref.RefType ?? 'REF',
Sex: ref.Sex ?? '0',
LowSign: ref.LowSign ?? 'GE',
HighSign: ref.HighSign ?? 'LE',
Low: ref.Low ?? null,
@ -143,37 +157,14 @@ function openCreateModal(type = 'TEST') {
AgeStart: ref.AgeStart ?? 0,
AgeEnd: ref.AgeEnd ?? 120,
Flag: ref.Flag ?? 'N',
Interpretation: ref.Interpretation ?? 'Normal',
SpcType: ref.SpcType ?? '',
Criteria: ref.Criteria ?? ''
});
const normalizeRefThold = (ref) => ({
Sex: ref.Sex ?? '2',
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 ?? 'Normal',
Interpretation: ref.Interpretation ?? '',
SpcType: ref.SpcType ?? '',
Criteria: ref.Criteria ?? ''
});
const normalizeRefTxt = (ref) => ({
Sex: ref.Sex ?? '2',
AgeStart: ref.AgeStart ?? 0,
AgeEnd: ref.AgeEnd ?? 120,
RefTxt: ref.RefTxt ?? '',
Flag: ref.Flag ?? 'N',
SpcType: ref.SpcType ?? '',
Criteria: ref.Criteria ?? ''
});
const normalizeRefVset = (ref) => ({
Sex: ref.Sex ?? '2',
RefType: ref.RefType ?? 'TEXT',
Sex: ref.Sex ?? '0',
AgeStart: ref.AgeStart ?? 0,
AgeEnd: ref.AgeEnd ?? 120,
RefTxt: ref.RefTxt ?? '',
@ -188,8 +179,8 @@ function openCreateModal(type = 'TEST') {
TestSiteCode: testDetail.TestSiteCode,
TestSiteName: testDetail.TestSiteName,
TestType: testDetail.TestType,
DisciplineID: testDetail.testdeftech?.[0]?.DisciplineID || null,
DepartmentID: testDetail.testdeftech?.[0]?.DepartmentID || null,
DisciplineID: testDetail.DisciplineID || null,
DepartmentID: testDetail.DepartmentID || null,
SeqScr: testDetail.SeqScr || '0',
SeqRpt: testDetail.SeqRpt || '0',
VisibleScr: testDetail.VisibleScr === '1' || testDetail.VisibleScr === 1 || testDetail.VisibleScr === true,
@ -198,22 +189,20 @@ function openCreateModal(type = 'TEST') {
CountStat: testDetail.CountStat === '1' || testDetail.CountStat === 1 || testDetail.CountStat === true,
Unit: testDetail.Unit || '',
Formula: testDetail.Formula || '',
refnum: (testDetail.refnum || []).map(normalizeRefNum),
refthold: (testDetail.refthold || []).map(normalizeRefThold),
reftxt: (testDetail.reftxt || []).map(normalizeRefTxt),
refvset: (testDetail.refvset || []).map(normalizeRefVset),
refnum: consolidatedRefnum.map(normalizeRefNum),
reftxt: consolidatedReftxt.map(normalizeRefTxt),
refRangeType,
// Technical Config (from testdeftech[0])
ResultType: testDetail.testdeftech?.[0]?.ResultType || '',
RefType: testDetail.testdeftech?.[0]?.RefType || '',
ReqQty: testDetail.testdeftech?.[0]?.ReqQty || null,
ReqQtyUnit: testDetail.testdeftech?.[0]?.ReqQtyUnit || '',
Unit1: testDetail.testdeftech?.[0]?.Unit1 || '',
Factor: testDetail.testdeftech?.[0]?.Factor || null,
Unit2: testDetail.testdeftech?.[0]?.Unit2 || '',
Decimal: testDetail.testdeftech?.[0]?.Decimal || 0,
Method: testDetail.testdeftech?.[0]?.Method || '',
ExpectedTAT: testDetail.testdeftech?.[0]?.ExpectedTAT || null,
// Technical Config (flat structure)
ResultType: testDetail.ResultType || '',
RefType: testDetail.RefType || '',
ReqQty: testDetail.ReqQty || null,
ReqQtyUnit: testDetail.ReqQtyUnit || '',
Unit1: testDetail.Unit1 || '',
Factor: testDetail.Factor || null,
Unit2: testDetail.Unit2 || '',
Decimal: testDetail.Decimal || 0,
Method: testDetail.Method || '',
ExpectedTAT: testDetail.ExpectedTAT || null,
// Group Members - API returns as testdefgrp
groupMembers: testDetail.testdefgrp || []
};
@ -228,10 +217,25 @@ function openCreateModal(type = 'TEST') {
async function handleSave() {
if (isDuplicateCode(formData.TestSiteCode, modalMode === 'edit' ? formData.TestSiteID : null)) { toastError(`Test code '${formData.TestSiteCode}' already exists`); return; }
if (canHaveFormula && !formData.Formula.trim()) { toastError('Formula is required for calculated tests'); return; }
if (formData.refRangeType === 'num') { for (let i = 0; i < formData.refnum.length; i++) { const errors = validateNumericRange(formData.refnum[i], i); if (errors.length > 0) { toastError(errors[0]); return; } } }
else if (formData.refRangeType === 'thold') { for (let i = 0; i < formData.refthold.length; i++) { const errors = validateTholdRange(formData.refthold[i], i); if (errors.length > 0) { toastError(errors[0]); return; } } }
else if (formData.refRangeType === 'text') { for (let i = 0; i < formData.reftxt.length; i++) { const errors = validateTextRange(formData.reftxt[i], i); if (errors.length > 0) { toastError(errors[0]); return; } } }
else if (formData.refRangeType === 'vset') { for (let i = 0; i < formData.refvset.length; i++) { const errors = validateVsetRange(formData.refvset[i], i); if (errors.length > 0) { toastError(errors[0]); return; } } }
// Validate reference ranges based on type
if (formData.refRangeType === 'num' || formData.refRangeType === 'thold') {
const rangesToValidate = (formData.refnum || []).filter(ref =>
formData.refRangeType === 'num' ? ref.RefType !== 'THOLD' : ref.RefType === 'THOLD'
);
for (let i = 0; i < rangesToValidate.length; i++) {
const errors = validateNumericRange(rangesToValidate[i], i);
if (errors.length > 0) { toastError(errors[0]); return; }
}
}
else if (formData.refRangeType === 'text' || formData.refRangeType === 'vset') {
const rangesToValidate = (formData.reftxt || []).filter(ref =>
formData.refRangeType === 'text' ? ref.RefType !== 'VSET' : ref.RefType === 'VSET'
);
for (let i = 0; i < rangesToValidate.length; i++) {
const errors = validateTextRange(rangesToValidate[i], i);
if (errors.length > 0) { toastError(errors[0]); return; }
}
}
saving = true;
try {
const payload = { ...formData };
@ -240,9 +244,30 @@ function openCreateModal(type = 'TEST') {
if (!canHaveFormula) delete payload.Formula;
if (!canHaveRefRange) {
delete payload.refnum;
delete payload.refthold;
delete payload.reftxt;
delete payload.refvset;
} else {
// Filter refnum and reftxt based on selected refRangeType
if (formData.refRangeType === 'num') {
// Keep only non-THOLD items in refnum
payload.refnum = (formData.refnum || []).filter(ref => ref.RefType !== 'THOLD');
delete payload.reftxt;
} else if (formData.refRangeType === 'thold') {
// Keep only THOLD items in refnum
payload.refnum = (formData.refnum || []).filter(ref => ref.RefType === 'THOLD');
delete payload.reftxt;
} else if (formData.refRangeType === 'text') {
// Keep only non-VSET items in reftxt
payload.reftxt = (formData.reftxt || []).filter(ref => ref.RefType !== 'VSET');
delete payload.refnum;
} else if (formData.refRangeType === 'vset') {
// Keep only VSET items in reftxt
payload.reftxt = (formData.reftxt || []).filter(ref => ref.RefType === 'VSET');
delete payload.refnum;
} else {
// No ref range type selected
delete payload.refnum;
delete payload.reftxt;
}
}
if (!canHaveTechnical) {
delete payload.ResultType;
@ -291,8 +316,8 @@ function openCreateModal(type = 'TEST') {
<div class="flex items-center gap-4 mb-6">
<a href="/master-data" class="btn btn-ghost btn-circle"><ArrowLeft class="w-5 h-5" /></a>
<div class="flex-1">
<h1 class="text-3xl font-bold text-gray-800">Test Definitions</h1>
<p class="text-gray-600">Manage laboratory tests, panels, and calculated values</p>
<h1 class="text-xl font-bold text-gray-800">Test Definitions</h1>
<p class="text-sm text-gray-600">Manage laboratory tests, panels, and calculated values</p>
</div>
<button class="btn btn-primary" onclick={openTypeSelector}><Plus class="w-4 h-4 mr-2" />Add Test</button>
</div>
@ -300,7 +325,7 @@ function openCreateModal(type = 'TEST') {
<div class="bg-base-100 rounded-lg shadow border border-base-200 p-4 mb-4">
<div class="flex flex-col sm:flex-row gap-4">
<div class="flex-1 relative">
<input type="text" placeholder="Search by code or name..." class="input input-bordered w-full pl-10" bind:value={searchQuery} bind:this={searchInputRef} onkeydown={(e) => e.key === 'Enter' && handleSearch()} />
<input type="text" placeholder="Search by code or name..." class="input input-sm input-bordered w-full pl-10" bind:value={searchQuery} bind:this={searchInputRef} onkeydown={(e) => e.key === 'Enter' && handleSearch()} />
<Search class="w-5 h-5 absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
</div>
<div class="w-full sm:w-48">

View File

@ -59,8 +59,7 @@
// Get tab count badge for reference range
function getRefRangeCount() {
return (formData.refnum?.length || 0) + (formData.reftxt?.length || 0) +
(formData.refthold?.length || 0) + (formData.refvset?.length || 0);
return (formData.refnum?.length || 0) + (formData.reftxt?.length || 0);
}
// Get tab count badge for group members

View File

@ -14,6 +14,19 @@ export const flagOptions = [
{ value: 'C', label: 'C', description: 'Critical' }
];
export const refTypeOptions = [
{ value: 'REF', label: 'REF', description: 'Reference Range' },
{ value: 'CRTC', label: 'CRTC', description: 'Critical Range' },
{ value: 'VAL', label: 'VAL', description: 'Validation Range' },
{ value: 'RERUN', label: 'RERUN', description: 'Rerun Range' },
{ value: 'THOLD', label: 'THOLD', description: 'Threshold Range' }
];
export const textRefTypeOptions = [
{ value: 'TEXT', label: 'TEXT', description: 'Text Reference' },
{ value: 'VSET', label: 'VSET', description: 'Value Set Reference' }
];
export const sexOptions = [
{ value: '2', label: 'Male' },
{ value: '1', label: 'Female' },
@ -22,7 +35,8 @@ export const sexOptions = [
export function createNumRef() {
return {
Sex: '2',
RefType: 'REF',
Sex: '0',
LowSign: 'GE',
HighSign: 'LE',
Low: null,
@ -30,7 +44,7 @@ export function createNumRef() {
AgeStart: 0,
AgeEnd: 120,
Flag: 'N',
Interpretation: 'Normal',
Interpretation: '',
SpcType: '',
Criteria: ''
};
@ -38,7 +52,8 @@ export function createNumRef() {
export function createTholdRef() {
return {
Sex: '2',
RefType: 'THOLD',
Sex: '0',
LowSign: 'GE',
HighSign: 'LE',
Low: null,
@ -46,7 +61,7 @@ export function createTholdRef() {
AgeStart: 0,
AgeEnd: 120,
Flag: 'N',
Interpretation: 'Normal',
Interpretation: '',
SpcType: '',
Criteria: ''
};
@ -54,7 +69,8 @@ export function createTholdRef() {
export function createTextRef() {
return {
Sex: '2',
RefType: 'TEXT',
Sex: '0',
AgeStart: 0,
AgeEnd: 120,
RefTxt: '',
@ -66,7 +82,8 @@ export function createTextRef() {
export function createVsetRef() {
return {
Sex: '2',
RefType: 'VSET',
Sex: '0',
AgeStart: 0,
AgeEnd: 120,
RefTxt: '',
@ -87,16 +104,8 @@ export function validateNumericRange(ref, index) {
return errors;
}
export function validateTholdRange(ref, index) {
const errors = [];
if (ref.Low !== null && ref.High !== null && ref.Low > ref.High) {
errors.push(`Range ${index + 1}: Low value cannot be greater than High value`);
}
if (ref.AgeStart !== null && ref.AgeEnd !== null && ref.AgeStart > ref.AgeEnd) {
errors.push(`Range ${index + 1}: Age start cannot be greater than Age end`);
}
return errors;
}
// Alias for threshold validation (same logic)
export const validateTholdRange = validateNumericRange;
export function validateTextRange(ref, index) {
const errors = [];
@ -106,10 +115,5 @@ export function validateTextRange(ref, index) {
return errors;
}
export function validateVsetRange(ref, index) {
const errors = [];
if (ref.AgeStart !== null && ref.AgeEnd !== null && ref.AgeStart > ref.AgeEnd) {
errors.push(`Range ${index + 1}: Age start cannot be greater than Age end`);
}
return errors;
}
// Alias for value set validation (same logic)
export const validateVsetRange = validateTextRange;

View File

@ -21,7 +21,7 @@
} = $props();
const typeLabels = {
TEST: 'Single Test',
TEST: 'Test',
PARAM: 'Parameter',
CALC: 'Calculated',
GROUP: 'Panel'
@ -44,8 +44,8 @@ const typeLabels = {
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control">
<label class="label" for="testCode">
<span class="label-text font-medium">Test Code</span>
<span class="label-text-alt text-error">*</span>
<span class="label-text text-sm font-medium">Test Code</span>
<span class="label-text-alt text-xs text-error">*</span>
</label>
<input
id="testCode"
@ -58,8 +58,8 @@ const typeLabels = {
</div>
<div class="form-control">
<label class="label" for="testName">
<span class="label-text font-medium">Test Name</span>
<span class="label-text-alt text-error">*</span>
<span class="label-text text-sm font-medium">Test Name</span>
<span class="label-text-alt text-xs text-error">*</span>
</label>
<input
id="testName"
@ -72,20 +72,6 @@ const typeLabels = {
</div>
</div>
<!-- Sequence -->
<div class="form-control max-w-xs">
<label class="label" for="seqScr">
<span class="label-text font-medium">Screen Sequence</span>
</label>
<input
id="seqScr"
type="number"
class="input input-sm input-bordered w-full"
bind:value={formData.SeqScr}
placeholder="0"
/>
</div>
<!-- Discipline and Department -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<SelectDropdown
@ -109,14 +95,14 @@ const typeLabels = {
<div class="grid grid-cols-1 md:grid-cols-1 gap-4">
<div class="form-control">
<label class="label" for="formula">
<span class="label-text font-medium flex items-center gap-2">
<span class="label-text text-sm font-medium flex items-center gap-2">
Formula
<HelpTooltip
text="Enter a mathematical formula using test codes (e.g., BUN / Creatinine). Supported operators: +, -, *, /, parentheses."
title="Formula Help"
/>
</span>
<span class="label-text-alt text-error">*</span>
<span class="label-text-alt text-xs text-error">*</span>
</label>
<input
id="formula"
@ -126,7 +112,7 @@ const typeLabels = {
placeholder="e.g., BUN / Creatinine"
required={canHaveFormula}
/>
<span class="label-text-alt text-gray-500">Use test codes with operators: +, -, *, /</span>
<span class="label-text-alt text-xs text-gray-500">Use test codes with operators: +, -, *, /</span>
</div>
</div>
{/if}
@ -134,7 +120,7 @@ const typeLabels = {
<!-- Description -->
<div class="form-control">
<label class="label" for="description">
<span class="label-text font-medium">Description</span>
<span class="label-text text-sm font-medium">Description</span>
</label>
<textarea
id="description"
@ -145,11 +131,23 @@ const typeLabels = {
></textarea>
</div>
<!-- Report Sequence, Visibility, and CountStat -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<!-- Screen Sequence, Report Sequence, Visibility, and Statistics -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div class="form-control">
<label class="label" for="seqScr">
<span class="label-text text-sm font-medium">Screen Sequence</span>
</label>
<input
id="seqScr"
type="number"
class="input input-sm input-bordered w-full"
bind:value={formData.SeqScr}
placeholder="0"
/>
</div>
<div class="form-control">
<label class="label" for="seqRpt">
<span class="label-text font-medium">Report Sequence</span>
<span class="label-text text-sm font-medium">Report Sequence</span>
</label>
<input
id="seqRpt"
@ -160,7 +158,7 @@ const typeLabels = {
/>
</div>
<div class="form-control">
<span class="label-text font-medium mb-2 block">Visibility</span>
<span class="label-text text-sm font-medium mb-2 block">Visibility</span>
<div class="flex gap-4">
<label class="label cursor-pointer gap-2">
<input type="checkbox" class="checkbox" bind:checked={formData.VisibleScr} />
@ -173,7 +171,7 @@ const typeLabels = {
</div>
</div>
<div class="form-control">
<span class="label-text font-medium mb-2 block">Statistics</span>
<span class="label-text text-sm font-medium mb-2 block">Statistics</span>
<label class="label cursor-pointer gap-2">
<input type="checkbox" class="checkbox" bind:checked={formData.CountStat} />
<span class="label-text">Count in Statistics</span>

View File

@ -71,7 +71,7 @@
<Search class="w-5 h-5 absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
<input
type="text"
class="input input-bordered w-full pl-10"
class="input input-sm input-bordered w-full pl-10"
bind:value={searchQuery}
placeholder="Search by test name or code..."
/>

View File

@ -1,6 +1,6 @@
<script>
import { PlusCircle, Calculator, X, ChevronDown, ChevronUp, Info, Beaker, Filter } from 'lucide-svelte';
import { flagOptions, sexOptions, createNumRef } from '../referenceRange.js';
import { PlusCircle, Calculator, X, ChevronDown, ChevronUp, Beaker } from 'lucide-svelte';
import { refTypeOptions, createNumRef } from '../referenceRange.js';
import { fetchValueSetByKey } from '$lib/api/valuesets.js';
import { onMount } from 'svelte';
@ -16,8 +16,7 @@
onupdateRefnum = () => {}
} = $props();
// Track expanded state for each range's optional fields
let expandedRanges = $state({});
let showAdvanced = $state({});
let specimenTypeOptions = $state([]);
onMount(async () => {
@ -34,77 +33,36 @@
function addRefRange() {
const newRef = createNumRef();
// Set smarter defaults
newRef.Sex = '0'; // Any
newRef.Flag = 'N'; // Normal
newRef.Sex = '0';
newRef.Flag = 'N';
onupdateRefnum([...refnum, newRef]);
// Auto-expand the new range
expandedRanges[refnum.length] = { age: false, interpretation: false, specimen: false };
}
function removeRefRange(index) {
onupdateRefnum(refnum.filter((_, i) => i !== index));
delete expandedRanges[index];
delete showAdvanced[index];
}
function toggleAgeExpand(index) {
expandedRanges[index] = { ...expandedRanges[index], age: !expandedRanges[index]?.age };
function toggleAdvanced(index) {
showAdvanced[index] = !showAdvanced[index];
}
function toggleInterpExpand(index) {
expandedRanges[index] = { ...expandedRanges[index], interpretation: !expandedRanges[index]?.interpretation };
}
function toggleSpecimenExpand(index) {
expandedRanges[index] = { ...expandedRanges[index], specimen: !expandedRanges[index]?.specimen };
}
function getSexLabel(value) {
return sexOptions.find(o => o.value === value)?.label || 'Any';
}
function getFlagColor(flag) {
const colors = {
'N': 'badge-success',
'L': 'badge-warning',
'H': 'badge-warning',
'C': 'badge-error'
};
return colors[flag] || 'badge-ghost';
}
function getFlagLabel(flag) {
const option = flagOptions.find(o => o.value === flag);
return option ? `${option.label} (${option.description})` : flag;
}
// Generate human-readable preview
function getRangePreview(ref) {
const sex = getSexLabel(ref.Sex);
const ageText = (ref.AgeStart !== 0 || ref.AgeEnd !== 120)
? `Age ${ref.AgeStart}-${ref.AgeEnd}`
: 'All ages';
let rangeText = '';
if (ref.Low !== null && ref.High !== null) {
rangeText = `${ref.Low} - ${ref.High}`;
} else if (ref.Low !== null) {
rangeText = `> ${ref.Low}`;
} else if (ref.High !== null) {
rangeText = `< ${ref.High}`;
} else {
rangeText = 'Not set';
}
return `${sex}, ${ageText}: ${rangeText}`;
function hasAdvancedData(ref) {
return ref.Interpretation || ref.SpcType || ref.Criteria ||
ref.Sex !== '0' || ref.Flag !== 'N' ||
(ref.AgeStart !== 0 || ref.AgeEnd !== 120);
}
</script>
<div class="space-y-3">
<div class="space-y-2">
<!-- Header -->
<div class="flex justify-between items-center">
<div class="flex items-center gap-2">
<Calculator class="w-5 h-5 text-primary" />
<h3 class="font-semibold">Numeric Reference Ranges</h3>
{#if refnum?.length > 0}
<span class="badge badge-sm badge-ghost">{refnum.length}</span>
{/if}
</div>
<button type="button" class="btn btn-sm btn-primary" onclick={addRefRange}>
<PlusCircle class="w-4 h-4 mr-1" />
@ -112,6 +70,7 @@
</button>
</div>
<!-- Empty State -->
{#if refnum?.length === 0}
<div class="text-center py-8 bg-base-200 rounded-lg border-2 border-dashed border-base-300">
<Calculator class="w-12 h-12 mx-auto text-gray-400 mb-2" />
@ -123,198 +82,164 @@
</div>
{/if}
{#each refnum || [] as ref, index (index)}
<div class="card bg-base-100 border-2 border-base-200 hover:border-primary/50 transition-colors">
<div class="card-body p-4">
<!-- Header with Preview -->
<div class="flex justify-between items-start mb-4 pb-3 border-b border-base-200">
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<span class="badge badge-primary">Range {index + 1}</span>
<span class="badge {getFlagColor(ref.Flag)}">{getFlagLabel(ref.Flag)}</span>
</div>
<!-- Live Preview -->
<div class="text-sm text-gray-600 bg-base-200 p-2 rounded flex items-center gap-2">
<Info class="w-4 h-4 text-primary flex-shrink-0" />
<span>{getRangePreview(ref)}</span>
</div>
</div>
<button type="button" class="btn btn-sm btn-ghost text-error ml-2" onclick={() => removeRefRange(index)}>
<X class="w-4 h-4" />
</button>
</div>
<!-- Main Fields - Range Values (Most Important) -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-3">
<div class="form-control">
<span class="label-text text-xs mb-1 font-medium">Lower Bound</span>
<input
type="number"
step="0.01"
class="input input-sm input-bordered w-full"
bind:value={ref.Low}
placeholder="70"
/>
</div>
<div class="form-control">
<span class="label-text text-xs mb-1 font-medium">Upper Bound</span>
<input
type="number"
step="0.01"
class="input input-sm input-bordered w-full"
bind:value={ref.High}
placeholder="100"
/>
</div>
</div>
<!-- Sex & Flag Row -->
<div class="grid grid-cols-2 gap-3 mb-3">
<div class="form-control">
<span class="label-text text-xs mb-1">Sex</span>
<select class="select select-sm select-bordered w-full" bind:value={ref.Sex}>
{#each sexOptions as option (option.value)}
<option value={option.value}>{option.label}</option>
{/each}
</select>
</div>
<div class="form-control">
<span class="label-text text-xs mb-1">Flag</span>
<select class="select select-sm select-bordered w-full" bind:value={ref.Flag}>
{#each flagOptions as option (option.value)}
<option value={option.value}>{option.label} - {option.description}</option>
{/each}
</select>
</div>
</div>
<!-- Expandable: Age Range -->
<div class="border border-base-200 rounded-lg mb-2 overflow-hidden">
<button
type="button"
class="w-full px-3 py-2 bg-base-200 hover:bg-base-300 flex items-center justify-between text-sm transition-colors"
onclick={() => toggleAgeExpand(index)}
>
<span class="flex items-center gap-2">
<span class="text-xs">Age Range</span>
{#if ref.AgeStart !== 0 || ref.AgeEnd !== 120}
<span class="text-xs text-primary">({ref.AgeStart}-{ref.AgeEnd})</span>
{:else}
<span class="text-xs text-gray-500">(All ages)</span>
{/if}
</span>
{#if expandedRanges[index]?.age}
<ChevronUp class="w-4 h-4" />
{:else}
<ChevronDown class="w-4 h-4" />
{/if}
</button>
{#if expandedRanges[index]?.age}
<div class="p-3 bg-base-100">
<div class="grid grid-cols-2 gap-3">
<div class="form-control">
<span class="label-text text-xs mb-1">From (years)</span>
<input type="number" min="0" max="120" class="input input-sm input-bordered w-full" bind:value={ref.AgeStart} />
</div>
<div class="form-control">
<span class="label-text text-xs mb-1">To (years)</span>
<input type="number" min="0" max="120" class="input input-sm input-bordered w-full" bind:value={ref.AgeEnd} />
</div>
</div>
</div>
{/if}
</div>
<!-- Expandable: Interpretation -->
<div class="border border-base-200 rounded-lg overflow-hidden mb-2">
<button
type="button"
class="w-full px-3 py-2 bg-base-200 hover:bg-base-300 flex items-center justify-between text-sm transition-colors"
onclick={() => toggleInterpExpand(index)}
>
<span class="flex items-center gap-2">
<span class="text-xs">Interpretation</span>
{#if ref.Interpretation}
<span class="text-xs text-primary truncate max-w-[200px]">({ref.Interpretation})</span>
{/if}
</span>
{#if expandedRanges[index]?.interpretation}
<ChevronUp class="w-4 h-4" />
{:else}
<ChevronDown class="w-4 h-4" />
{/if}
</button>
{#if expandedRanges[index]?.interpretation}
<div class="p-3 bg-base-100">
<div class="form-control">
<textarea
class="textarea textarea-bordered w-full text-sm"
rows="2"
bind:value={ref.Interpretation}
placeholder="e.g., Normal fasting glucose range"
></textarea>
</div>
</div>
{/if}
</div>
<!-- Expandable: Specimen and Criteria -->
<div class="border border-base-200 rounded-lg overflow-hidden">
<button
type="button"
class="w-full px-3 py-2 bg-base-200 hover:bg-base-300 flex items-center justify-between text-sm transition-colors"
onclick={() => toggleSpecimenExpand(index)}
>
<span class="flex items-center gap-2">
<Beaker class="w-3 h-3" />
<span class="text-xs">Specimen & Criteria</span>
{#if ref.SpcType || ref.Criteria}
<span class="text-xs text-primary">(Custom)</span>
{:else}
<span class="text-xs text-gray-500">(Optional)</span>
{/if}
</span>
{#if expandedRanges[index]?.specimen}
<ChevronUp class="w-4 h-4" />
{:else}
<ChevronDown class="w-4 h-4" />
{/if}
</button>
{#if expandedRanges[index]?.specimen}
<div class="p-3 bg-base-100 space-y-3">
<div class="form-control">
<span class="label-text text-xs mb-1 flex items-center gap-1">
<Beaker class="w-3 h-3" />
Specimen Type
</span>
<select class="select select-sm select-bordered w-full" bind:value={ref.SpcType}>
<option value="">Any specimen</option>
{#each specimenTypeOptions as option}
<option value={option.value}>{option.label}</option>
{/each}
</select>
</div>
<div class="form-control">
<span class="label-text text-xs mb-1 flex items-center gap-1">
<Filter class="w-3 h-3" />
Criteria
</span>
<input
type="text"
class="input input-sm input-bordered w-full"
bind:value={ref.Criteria}
placeholder="e.g., Fasting, Morning sample"
/>
</div>
</div>
{/if}
</div>
<!-- Range List - Table-like rows -->
{#if refnum?.length > 0}
<div class="border border-base-300 rounded-lg overflow-hidden">
<!-- Table Header -->
<div class="bg-base-200 px-4 py-3 grid grid-cols-12 gap-3 text-xs font-medium text-gray-600 border-b border-base-300">
<div class="col-span-1">#</div>
<div class="col-span-3">Type</div>
<div class="col-span-3">Low</div>
<div class="col-span-3">High</div>
<div class="col-span-2"></div>
</div>
<!-- Table Rows -->
{#each refnum || [] as ref, index (index)}
<div class="border-b border-base-200 last:border-b-0">
<!-- Main Row -->
<div class="px-4 py-3 grid grid-cols-12 gap-3 items-center hover:bg-base-100">
<!-- Row Number -->
<div class="col-span-1 flex items-center gap-1">
<span class="text-xs text-gray-500">{index + 1}</span>
</div>
<!-- Type Dropdown -->
<div class="col-span-3">
<select class="select select-sm select-bordered w-full" bind:value={ref.RefType}>
{#each refTypeOptions as option (option.value)}
<option value={option.value}>{option.label}</option>
{/each}
</select>
</div>
<!-- Low Input -->
<div class="col-span-3">
<input
type="number"
step="0.01"
class="input input-sm input-bordered w-full text-right"
bind:value={ref.Low}
placeholder="-"
/>
</div>
<!-- High Input -->
<div class="col-span-3">
<input
type="number"
step="0.01"
class="input input-sm input-bordered w-full text-right"
bind:value={ref.High}
placeholder="-"
/>
</div>
<!-- More & Delete Buttons -->
<div class="col-span-2 flex items-center justify-end gap-1">
<button
type="button"
class="btn btn-xs btn-ghost btn-square relative"
onclick={() => toggleAdvanced(index)}
title={showAdvanced[index] ? 'Hide details' : 'Show details'}
>
{#if hasAdvancedData(ref)}
<span class="badge badge-xs badge-primary absolute -top-1 -right-1 w-2 h-2 p-0 min-w-0"></span>
{/if}
{#if showAdvanced[index]}
<ChevronUp class="w-4 h-4" />
{:else}
<ChevronDown class="w-4 h-4" />
{/if}
</button>
<button
type="button"
class="btn btn-xs btn-ghost text-error btn-square"
onclick={() => removeRefRange(index)}
title="Remove"
>
<X class="w-4 h-4" />
</button>
</div>
</div>
<!-- Advanced Section (Expanded) -->
{#if showAdvanced[index]}
<div class="px-4 pb-4 pt-2 bg-base-100/50 border-t border-base-200">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Sex -->
<div class="form-control">
<span class="label-text text-xs text-gray-500">Sex</span>
<select class="select select-sm select-bordered w-full" bind:value={ref.Sex}>
<option value="0">Any</option>
<option value="1">Female</option>
<option value="2">Male</option>
</select>
</div>
<!-- Age Range -->
<div class="form-control">
<span class="label-text text-xs text-gray-500">Age Range</span>
<div class="flex items-center gap-2">
<input
type="number"
min="0"
max="120"
class="input input-sm input-bordered w-20 text-right"
bind:value={ref.AgeStart}
/>
<span class="text-gray-400">to</span>
<input
type="number"
min="0"
max="120"
class="input input-sm input-bordered w-20 text-right"
bind:value={ref.AgeEnd}
/>
<span class="text-xs text-gray-400">years</span>
</div>
</div>
<!-- Specimen -->
<div class="form-control md:col-span-2">
<span class="label-text text-xs text-gray-500 flex items-center gap-1">
<Beaker class="w-3 h-3" />
Specimen Type
</span>
<select class="select select-sm select-bordered w-full" bind:value={ref.SpcType}>
<option value="">Any specimen</option>
{#each specimenTypeOptions as option (option.value)}
<option value={option.value}>{option.label}</option>
{/each}
</select>
</div>
<!-- Interpretation -->
<div class="form-control md:col-span-2">
<span class="label-text text-xs text-gray-500">Interpretation</span>
<input
type="text"
class="input input-sm input-bordered w-full"
bind:value={ref.Interpretation}
placeholder="e.g., Normal fasting glucose range"
/>
</div>
<!-- Criteria -->
<div class="form-control md:col-span-2">
<span class="label-text text-xs text-gray-500">Criteria</span>
<input
type="text"
class="input input-sm input-bordered w-full"
bind:value={ref.Criteria}
placeholder="e.g., Fasting, Morning sample"
/>
</div>
</div>
</div>
{/if}
</div>
{/each}
</div>
{/each}
{/if}
</div>

View File

@ -1,10 +1,8 @@
<script>
import { Ruler, Calculator, FileText, Box } from 'lucide-svelte';
import { Ruler, Info } from 'lucide-svelte';
import HelpTooltip from '$lib/components/HelpTooltip.svelte';
import NumericRefRange from './NumericRefRange.svelte';
import ThresholdRefRange from './ThresholdRefRange.svelte';
import TextRefRange from './TextRefRange.svelte';
import ValueSetRefRange from './ValueSetRefRange.svelte';
import { createNumRef, createTholdRef, createTextRef, createVsetRef } from '../referenceRange.js';
/**
@ -19,10 +17,28 @@
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)'
};
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' }
];
// Ensure all reference range items have defined values, never undefined
function normalizeRefNum(ref) {
return {
Sex: ref.Sex ?? '2',
RefType: ref.RefType ?? 'REF',
Sex: ref.Sex ?? '0',
LowSign: ref.LowSign ?? 'GE',
HighSign: ref.HighSign ?? 'LE',
Low: ref.Low ?? null,
@ -30,23 +46,7 @@
AgeStart: ref.AgeStart ?? 0,
AgeEnd: ref.AgeEnd ?? 120,
Flag: ref.Flag ?? 'N',
Interpretation: ref.Interpretation ?? 'Normal',
SpcType: ref.SpcType ?? '',
Criteria: ref.Criteria ?? ''
};
}
function normalizeRefThold(ref) {
return {
Sex: ref.Sex ?? '2',
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 ?? 'Normal',
Interpretation: ref.Interpretation ?? '',
SpcType: ref.SpcType ?? '',
Criteria: ref.Criteria ?? ''
};
@ -54,7 +54,8 @@
function normalizeRefTxt(ref) {
return {
Sex: ref.Sex ?? '2',
RefType: ref.RefType ?? 'TEXT',
Sex: ref.Sex ?? '0',
AgeStart: ref.AgeStart ?? 0,
AgeEnd: ref.AgeEnd ?? 120,
RefTxt: ref.RefTxt ?? '',
@ -64,46 +65,61 @@
};
}
function normalizeRefVset(ref) {
return {
Sex: ref.Sex ?? '2',
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));
// Reactive normalized data
let normalizedRefnum = $derived((formData.refnum || []).map(normalizeRefNum));
let normalizedRefthold = $derived((formData.refthold || []).map(normalizeRefThold));
let normalizedReftxt = $derived((formData.reftxt || []).map(normalizeRefTxt));
let normalizedRefvset = $derived((formData.refvset || []).map(normalizeRefVset));
// 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'));
function updateRefRangeType(type) {
let newFormData = {
...formData,
refRangeType: type,
refnum: [],
refthold: [],
reftxt: [],
refvset: []
};
// Initialize arrays if they don't exist
const currentRefnum = formData.refnum || [];
const currentReftxt = formData.reftxt || [];
// Initialize the selected type
if (type === 'num') {
newFormData.refnum = [createNumRef()];
// Add numeric range to refnum
onupdateFormData({
...formData,
refRangeType: type,
RefType: 'NMRC',
refnum: [...currentRefnum, createNumRef()]
});
} else if (type === 'thold') {
newFormData.refthold = [createTholdRef()];
// Add threshold range to refnum
onupdateFormData({
...formData,
refRangeType: type,
RefType: 'THOLD',
refnum: [...currentRefnum, createTholdRef()]
});
} else if (type === 'text') {
newFormData.reftxt = [createTextRef()];
// Add text range to reftxt
onupdateFormData({
...formData,
refRangeType: type,
RefType: 'TEXT',
reftxt: [...currentReftxt, createTextRef()]
});
} else if (type === 'vset') {
newFormData.refvset = [createVsetRef()];
// Add value set range to reftxt
onupdateFormData({
...formData,
refRangeType: type,
RefType: 'VSET',
reftxt: [...currentReftxt, createVsetRef()]
});
} else {
// None selected
onupdateFormData({
...formData,
refRangeType: type,
RefType: ''
});
}
onupdateFormData(newFormData);
}
function updateRefnum(refnum) {
@ -111,7 +127,9 @@
}
function updateRefthold(refthold) {
onupdateFormData({ ...formData, refthold });
// Merge thold items back into refnum
const nonThold = (formData.refnum || []).filter(ref => ref.RefType !== 'THOLD');
onupdateFormData({ ...formData, refnum: [...nonThold, ...refthold] });
}
function updateReftxt(reftxt) {
@ -119,7 +137,9 @@
}
function updateRefvset(refvset) {
onupdateFormData({ ...formData, refvset });
// Merge vset items back into reftxt
const nonVset = (formData.reftxt || []).filter(ref => ref.RefType !== 'VSET');
onupdateFormData({ ...formData, reftxt: [...nonVset, ...refvset] });
}
</script>
@ -134,90 +154,30 @@
title="Reference Range Help"
/>
</div>
<div class="flex flex-wrap gap-3">
<label class="label cursor-pointer gap-2 p-3 bg-base-200 rounded-lg hover:bg-base-300 transition-colors flex-1 min-w-[100px]">
<input
type="radio"
name="refRangeType"
class="radio radio-primary"
value="none"
checked={formData.refRangeType === 'none'}
onchange={() => updateRefRangeType('none')}
/>
<div class="flex flex-col">
<span class="label-text font-medium">None</span>
<span class="text-xs text-gray-500">No reference range</span>
</div>
</label>
<label class="label cursor-pointer gap-2 p-3 bg-base-200 rounded-lg hover:bg-base-300 transition-colors flex-1 min-w-[100px]">
<input
type="radio"
name="refRangeType"
class="radio radio-primary"
value="num"
checked={formData.refRangeType === 'num'}
onchange={() => updateRefRangeType('num')}
/>
<div class="flex flex-col">
<span class="label-text font-medium flex items-center gap-1">
Numeric
<Calculator class="w-3 h-3" />
</span>
<span class="text-xs text-gray-500">Range (e.g., 70-100)</span>
</div>
</label>
<label class="label cursor-pointer gap-2 p-3 bg-base-200 rounded-lg hover:bg-base-300 transition-colors flex-1 min-w-[100px]">
<input
type="radio"
name="refRangeType"
class="radio radio-primary"
value="thold"
checked={formData.refRangeType === 'thold'}
onchange={() => updateRefRangeType('thold')}
/>
<div class="flex flex-col">
<span class="label-text font-medium flex items-center gap-1">
Threshold
<Ruler class="w-3 h-3" />
</span>
<span class="text-xs text-gray-500">Limit values</span>
</div>
</label>
<label class="label cursor-pointer gap-2 p-3 bg-base-200 rounded-lg hover:bg-base-300 transition-colors flex-1 min-w-[100px]">
<input
type="radio"
name="refRangeType"
class="radio radio-primary"
value="text"
checked={formData.refRangeType === 'text'}
onchange={() => updateRefRangeType('text')}
/>
<div class="flex flex-col">
<span class="label-text font-medium flex items-center gap-1">
Text
<FileText class="w-3 h-3" />
</span>
<span class="text-xs text-gray-500">Descriptive</span>
</div>
</label>
<label class="label cursor-pointer gap-2 p-3 bg-base-200 rounded-lg hover:bg-base-300 transition-colors flex-1 min-w-[100px]">
<input
type="radio"
name="refRangeType"
class="radio radio-primary"
value="vset"
checked={formData.refRangeType === 'vset'}
onchange={() => updateRefRangeType('vset')}
/>
<div class="flex flex-col">
<span class="label-text font-medium flex items-center gap-1">
Value Set
<Box class="w-3 h-3" />
</span>
<span class="text-xs text-gray-500">Predefined values</span>
</div>
</label>
<!-- Dropdown Select -->
<div class="form-control">
<select
class="select select-bordered w-full"
value={formData.refRangeType || 'none'}
onchange={(e) => updateRefRangeType(e.target.value)}
>
{#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 -->
@ -225,9 +185,9 @@
<NumericRefRange refnum={normalizedRefnum} onupdateRefnum={updateRefnum} />
{/if}
<!-- Threshold Reference Ranges -->
<!-- Threshold Reference Ranges (uses same component as numeric) -->
{#if formData.refRangeType === 'thold'}
<ThresholdRefRange refthold={normalizedRefthold} onupdateRefthold={updateRefthold} />
<NumericRefRange refnum={normalizedRefthold} onupdateRefnum={updateRefthold} />
{/if}
<!-- Text Reference Ranges -->
@ -235,8 +195,8 @@
<TextRefRange reftxt={normalizedReftxt} onupdateReftxt={updateReftxt} />
{/if}
<!-- Value Set Reference Ranges -->
<!-- Value Set Reference Ranges (uses same component as text) -->
{#if formData.refRangeType === 'vset'}
<ValueSetRefRange refvset={normalizedRefvset} onupdateRefvset={updateRefvset} />
<TextRefRange reftxt={normalizedRefvset} onupdateReftxt={updateRefvset} />
{/if}
</div>

View File

@ -17,36 +17,24 @@
// Value set options
let resultTypeOptions = $state([]);
let refTypeOptions = $state([]);
let loading = $state(true);
onMount(async () => {
try {
loading = true;
const [resultTypeRes, refTypeRes] = await Promise.all([
fetchValueSetByKey('result_type'),
fetchValueSetByKey('reference_type')
]);
const resultTypeRes = await fetchValueSetByKey('result_type');
console.log('result_type response:', resultTypeRes);
console.log('reference_type response:', refTypeRes);
// Handle different response structures
const resultItems = resultTypeRes.data?.items || resultTypeRes.data?.ValueSetItems || (Array.isArray(resultTypeRes.data) ? resultTypeRes.data : []) || [];
const refItems = refTypeRes.data?.items || refTypeRes.data?.ValueSetItems || (Array.isArray(refTypeRes.data) ? refTypeRes.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);
refTypeOptions = refItems.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);
console.log('resultTypeOptions:', resultTypeOptions);
console.log('refTypeOptions:', refTypeOptions);
} catch (err) {
console.error('Failed to load value sets:', err);
} finally {
@ -57,6 +45,9 @@
function updateField(field, value) {
onupdateFormData({ ...formData, [field]: value });
}
// Check if test is calculated type (doesn't have specimen requirements)
const isCalculated = $derived(formData.TestType === 'CALC');
</script>
{#if loading}
@ -72,40 +63,21 @@
<h3 class="font-semibold">Result Configuration</h3>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control">
<label class="label" for="resultType">
<span class="label-text font-medium">Result Type</span>
</label>
<select
id="resultType"
class="select select-sm select-bordered w-full"
bind:value={formData.ResultType}
onchange={(e) => updateField('ResultType', e.target.value)}
>
<option value="">Select result type...</option>
{#each resultTypeOptions as option}
<option value={option.value}>{option.label}</option>
{/each}
</select>
</div>
<div class="form-control">
<label class="label" for="refType">
<span class="label-text font-medium">Reference Type</span>
</label>
<select
id="refType"
class="select select-sm select-bordered w-full"
bind:value={formData.RefType}
onchange={(e) => updateField('RefType', e.target.value)}
>
<option value="">Select reference type...</option>
{#each refTypeOptions as option}
<option value={option.value}>{option.label}</option>
{/each}
</select>
</div>
<div class="form-control max-w-md">
<label class="label" for="resultType">
<span class="label-text text-sm font-medium">Result Type</span>
</label>
<select
id="resultType"
class="select select-sm select-bordered w-full"
bind:value={formData.ResultType}
onchange={(e) => updateField('ResultType', e.target.value)}
>
<option value="">Select result type...</option>
{#each resultTypeOptions as option}
<option value={option.value}>{option.label}</option>
{/each}
</select>
</div>
</div>
@ -119,7 +91,7 @@
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div class="form-control">
<label class="label" for="unit1">
<span class="label-text font-medium">Unit 1</span>
<span class="label-text text-sm font-medium">Unit 1</span>
</label>
<input
id="unit1"
@ -133,7 +105,7 @@
<div class="form-control">
<label class="label" for="factor">
<span class="label-text font-medium">Factor</span>
<span class="label-text text-sm font-medium">Factor</span>
</label>
<input
id="factor"
@ -148,7 +120,7 @@
<div class="form-control">
<label class="label" for="unit2">
<span class="label-text font-medium">Unit 2</span>
<span class="label-text text-sm font-medium">Unit 2</span>
</label>
<input
id="unit2"
@ -162,7 +134,7 @@
<div class="form-control">
<label class="label" for="decimal">
<span class="label-text font-medium">Decimal Places</span>
<span class="label-text text-sm font-medium">Decimal Places</span>
</label>
<input
id="decimal"
@ -185,43 +157,45 @@
</div>
<!-- Specimen Requirements -->
<div class="bg-base-100 rounded-lg border border-base-200 p-4">
<div class="flex items-center gap-2 mb-4">
<Beaker class="w-5 h-5 text-primary" />
<h3 class="font-semibold">Specimen Requirements</h3>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control">
<label class="label" for="reqQty">
<span class="label-text font-medium">Required Quantity</span>
</label>
<input
id="reqQty"
type="number"
step="0.01"
class="input input-sm input-bordered w-full"
value={formData.ReqQty}
oninput={(e) => updateField('ReqQty', e.target.value ? parseFloat(e.target.value) : null)}
placeholder="Amount"
/>
{#if !isCalculated}
<div class="bg-base-100 rounded-lg border border-base-200 p-4">
<div class="flex items-center gap-2 mb-4">
<Beaker class="w-5 h-5 text-primary" />
<h3 class="font-semibold">Specimen Requirements</h3>
</div>
<div class="form-control">
<label class="label" for="reqQtyUnit">
<span class="label-text font-medium">Quantity Unit</span>
</label>
<input
id="reqQtyUnit"
type="text"
class="input input-sm input-bordered w-full"
value={formData.ReqQtyUnit}
oninput={(e) => updateField('ReqQtyUnit', e.target.value)}
placeholder="e.g., mL"
/>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control">
<label class="label" for="reqQty">
<span class="label-text text-sm font-medium">Required Quantity</span>
</label>
<input
id="reqQty"
type="number"
step="0.01"
class="input input-sm input-bordered w-full"
value={formData.ReqQty}
oninput={(e) => updateField('ReqQty', e.target.value ? parseFloat(e.target.value) : null)}
placeholder="Amount"
/>
</div>
<div class="form-control">
<label class="label" for="reqQtyUnit">
<span class="label-text text-sm font-medium">Quantity Unit</span>
</label>
<input
id="reqQtyUnit"
type="text"
class="input input-sm input-bordered w-full"
value={formData.ReqQtyUnit}
oninput={(e) => updateField('ReqQtyUnit', e.target.value)}
placeholder="e.g., mL"
/>
</div>
</div>
</div>
</div>
{/if}
<!-- Method and TAT -->
<div class="bg-base-100 rounded-lg border border-base-200 p-4">
@ -233,7 +207,7 @@
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control">
<label class="label" for="method">
<span class="label-text font-medium">Method</span>
<span class="label-text text-sm font-medium">Method</span>
</label>
<input
id="method"
@ -247,7 +221,7 @@
<div class="form-control">
<label class="label" for="expectedTAT">
<span class="label-text font-medium">Expected TAT (minutes)</span>
<span class="label-text text-sm font-medium">Expected TAT (minutes)</span>
</label>
<input
id="expectedTAT"

View File

@ -113,7 +113,7 @@
<div class="form-control mt-3">
<span class="label-text text-xs mb-1">Value Set</span>
<input type="text" class="input input-sm input-bordered w-full" bind:value={ref.RefTxt} placeholder="e.g., Positive, Negative, Borderline" />
<span class="label-text-alt text-gray-500 mt-1">Comma-separated list of allowed values</span>
<span class="label-text-alt text-xs text-gray-500 mt-1">Comma-separated list of allowed values</span>
</div>
<!-- Expandable: Specimen and Criteria -->

View File

@ -133,14 +133,14 @@
</a>
<div class="flex-1">
<div class="flex items-center gap-3">
<h1 class="text-3xl font-bold text-gray-800">ValueSets</h1>
<h1 class="text-xl font-bold text-gray-800">ValueSets</h1>
<HelpTooltip
text="ValueSets are reusable lookup tables used throughout the system for dropdown menus, form fields, and standardized values. They ensure consistency in data entry and reporting."
title="About ValueSets"
position="right"
/>
</div>
<p class="text-gray-600">System lookup values and dropdown options</p>
<p class="text-sm text-gray-600">System lookup values and dropdown options</p>
</div>
</div>
@ -154,7 +154,7 @@
<input
type="text"
placeholder="Search ValueSets by key (e.g., priority_status, test_category)..."
class="input input-bordered w-full pl-10"
class="input input-sm input-bordered w-full pl-10"
bind:value={searchQuery}
onkeydown={handleSearchKeydown}
/>