feat: Update test modal UX and improve DataTable/Modal components
This commit is contained in:
parent
995cdd3fec
commit
1af4adddf7
4866
docs/api-docs.bundled.yaml
Normal file
4866
docs/api-docs.bundled.yaml
Normal file
File diff suppressed because it is too large
Load Diff
@ -68,7 +68,7 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr class="bg-base-200">
|
<tr class="bg-base-200">
|
||||||
{#each columns as column}
|
{#each columns as column}
|
||||||
<th class="font-semibold {column.class || ''}">
|
<th class="text-sm font-semibold {column.class || ''}">
|
||||||
{column.label}
|
{column.label}
|
||||||
</th>
|
</th>
|
||||||
{/each}
|
{/each}
|
||||||
@ -102,7 +102,7 @@
|
|||||||
role={onRowClick ? 'button' : undefined}
|
role={onRowClick ? 'button' : undefined}
|
||||||
>
|
>
|
||||||
{#each columns as column}
|
{#each columns as column}
|
||||||
<td class="{column.class || ''}">
|
<td class="text-sm {column.class || ''}">
|
||||||
{#if cell}
|
{#if cell}
|
||||||
{@render cell({ column, row, value: getValue(row, column.key), index })}
|
{@render cell({ column, row, value: getValue(row, column.key), index })}
|
||||||
{:else if column.render}
|
{:else if column.render}
|
||||||
|
|||||||
@ -83,7 +83,7 @@
|
|||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
{#if title}
|
{#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}
|
{:else}
|
||||||
<div></div>
|
<div></div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@ -80,8 +80,8 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<h1 class="text-3xl font-bold text-gray-800 mb-2">Master Data</h1>
|
<h1 class="text-xl 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>
|
<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">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{#each modules as module}
|
{#each modules as module}
|
||||||
|
|||||||
@ -172,8 +172,8 @@
|
|||||||
<ArrowLeft class="w-5 h-5" />
|
<ArrowLeft class="w-5 h-5" />
|
||||||
</a>
|
</a>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<h1 class="text-3xl font-bold text-gray-800">Contacts</h1>
|
<h1 class="text-xl font-bold text-gray-800">Contacts</h1>
|
||||||
<p class="text-gray-600">Manage physicians and contacts</p>
|
<p class="text-sm text-gray-600">Manage physicians and contacts</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary" onclick={openCreateModal}>
|
<button class="btn btn-primary" onclick={openCreateModal}>
|
||||||
<Plus class="w-4 h-4 mr-2" />
|
<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" />
|
<Search class="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
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..."
|
placeholder="Search by name or initial..."
|
||||||
bind:value={searchQuery}
|
bind:value={searchQuery}
|
||||||
/>
|
/>
|
||||||
@ -214,10 +214,10 @@
|
|||||||
<div class="bg-base-200 rounded-full p-6 mb-4">
|
<div class="bg-base-200 rounded-full p-6 mb-4">
|
||||||
<Users class="w-12 h-12 text-gray-400" />
|
<Users class="w-12 h-12 text-gray-400" />
|
||||||
</div>
|
</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'}
|
{searchQuery ? 'No contacts found' : 'No contacts yet'}
|
||||||
</h3>
|
</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
|
{searchQuery
|
||||||
? `No contacts matching "${searchQuery}". Try a different search term.`
|
? `No contacts matching "${searchQuery}". Try a different search term.`
|
||||||
: 'Get started by adding your first physician or contact.'}
|
: '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="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="initial">
|
<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
|
Initial
|
||||||
<HelpTooltip
|
<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."
|
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"
|
position="right"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span class="label-text-alt text-error">*</span>
|
<span class="label-text-alt text-xs text-error">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="initial"
|
id="initial"
|
||||||
@ -286,12 +286,12 @@
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<label class="label" for="initial">
|
<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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="title">
|
<label class="label" for="title">
|
||||||
<span class="label-text font-medium">Title</span>
|
<span class="label-text text-sm font-medium">Title</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="title"
|
id="title"
|
||||||
@ -305,7 +305,7 @@
|
|||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="nameFirst">
|
<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>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="nameFirst"
|
id="nameFirst"
|
||||||
@ -317,7 +317,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="nameLast">
|
<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>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="nameLast"
|
id="nameLast"
|
||||||
|
|||||||
@ -152,8 +152,8 @@
|
|||||||
<ArrowLeft class="w-5 h-5" />
|
<ArrowLeft class="w-5 h-5" />
|
||||||
</a>
|
</a>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<h1 class="text-3xl font-bold text-gray-800">Containers</h1>
|
<h1 class="text-xl font-bold text-gray-800">Containers</h1>
|
||||||
<p class="text-gray-600">Manage specimen containers and tubes</p>
|
<p class="text-sm text-gray-600">Manage specimen containers and tubes</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary" onclick={openCreateModal}>
|
<button class="btn btn-primary" onclick={openCreateModal}>
|
||||||
<Plus class="w-4 h-4 mr-2" />
|
<Plus class="w-4 h-4 mr-2" />
|
||||||
@ -188,10 +188,10 @@
|
|||||||
<div class="bg-base-200 rounded-full p-6 mb-4">
|
<div class="bg-base-200 rounded-full p-6 mb-4">
|
||||||
<FlaskConical class="w-12 h-12 text-gray-400" />
|
<FlaskConical class="w-12 h-12 text-gray-400" />
|
||||||
</div>
|
</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'}
|
{searchQuery.trim() ? 'No containers match your search' : 'No containers found'}
|
||||||
</h3>
|
</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()
|
{searchQuery.trim()
|
||||||
? `No containers found matching "${searchQuery}". Try a different search term or clear the filter.`
|
? `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.'}
|
: '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="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="code">
|
<label class="label" for="code">
|
||||||
<span class="label-text font-medium">Container Code</span>
|
<span class="label-text text-sm font-medium">Container Code</span>
|
||||||
<span class="label-text-alt text-error">*</span>
|
<span class="label-text-alt text-xs text-error">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="code"
|
id="code"
|
||||||
@ -264,12 +264,12 @@
|
|||||||
placeholder="e.g., SST, EDTA, HEP"
|
placeholder="e.g., SST, EDTA, HEP"
|
||||||
required
|
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>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="name">
|
<label class="label" for="name">
|
||||||
<span class="label-text font-medium">Container Name</span>
|
<span class="label-text text-sm font-medium">Container Name</span>
|
||||||
<span class="label-text-alt text-error">*</span>
|
<span class="label-text-alt text-xs text-error">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="name"
|
id="name"
|
||||||
@ -279,12 +279,12 @@
|
|||||||
placeholder="e.g., Serum Separator Tube"
|
placeholder="e.g., Serum Separator Tube"
|
||||||
required
|
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>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="desc">
|
<label class="label" for="desc">
|
||||||
<span class="label-text font-medium">Description</span>
|
<span class="label-text text-sm font-medium">Description</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="desc"
|
id="desc"
|
||||||
@ -293,12 +293,12 @@
|
|||||||
bind:value={formData.ConDesc}
|
bind:value={formData.ConDesc}
|
||||||
placeholder="e.g., Evacuated blood collection tube with gel separator"
|
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>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="class">
|
<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
|
Container Class
|
||||||
<HelpTooltip
|
<HelpTooltip
|
||||||
text="The general category of this container. Examples: Tube (for blood collection tubes), Cup (for urine or fluid cups), Swab (for specimen swabs)."
|
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}
|
bind:value={formData.ConClass}
|
||||||
placeholder="Select class..."
|
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>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="additive">
|
<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
|
Additive
|
||||||
<HelpTooltip
|
<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)."
|
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}
|
bind:value={formData.Additive}
|
||||||
placeholder="Select 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>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="color">
|
<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
|
Cap Color
|
||||||
<HelpTooltip
|
<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)."
|
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}
|
bind:value={formData.Color}
|
||||||
placeholder="Select 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>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -204,8 +204,8 @@
|
|||||||
<ArrowLeft class="w-5 h-5" />
|
<ArrowLeft class="w-5 h-5" />
|
||||||
</a>
|
</a>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<h1 class="text-3xl font-bold text-gray-800">Counters</h1>
|
<h1 class="text-xl font-bold text-gray-800">Counters</h1>
|
||||||
<p class="text-gray-600">Manage ID generation counters</p>
|
<p class="text-sm text-gray-600">Manage ID generation counters</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary" onclick={openCreateModal}>
|
<button class="btn btn-primary" onclick={openCreateModal}>
|
||||||
<Plus class="w-4 h-4 mr-2" />
|
<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" />
|
<Search class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-base-content/50" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
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..."
|
placeholder="Search by counter description..."
|
||||||
bind:value={searchQuery}
|
bind:value={searchQuery}
|
||||||
/>
|
/>
|
||||||
@ -262,7 +262,7 @@
|
|||||||
<div class="bg-base-200 rounded-full p-4 mb-4">
|
<div class="bg-base-200 rounded-full p-4 mb-4">
|
||||||
<Hash class="w-8 h-8 text-base-content/40" />
|
<Hash class="w-8 h-8 text-base-content/40" />
|
||||||
</div>
|
</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}
|
{#if searchQuery}
|
||||||
No counters found
|
No counters found
|
||||||
{:else}
|
{:else}
|
||||||
@ -331,8 +331,8 @@
|
|||||||
<!-- Description -->
|
<!-- Description -->
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="counterDesc">
|
<label class="label" for="counterDesc">
|
||||||
<span class="label-text font-medium">Counter Description</span>
|
<span class="label-text text-sm font-medium">Counter Description</span>
|
||||||
<span class="label-text-alt text-error">*</span>
|
<span class="label-text-alt text-xs text-error">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="counterDesc"
|
id="counterDesc"
|
||||||
@ -344,7 +344,7 @@
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<label class="label" for="counterDesc">
|
<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
|
A descriptive name for this counter
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
@ -355,8 +355,8 @@
|
|||||||
<!-- Current Value -->
|
<!-- Current Value -->
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="counterValue">
|
<label class="label" for="counterValue">
|
||||||
<span class="label-text font-medium">Current Value</span>
|
<span class="label-text text-sm font-medium">Current Value</span>
|
||||||
<span class="label-text-alt text-error">*</span>
|
<span class="label-text-alt text-xs text-error">*</span>
|
||||||
<HelpTooltip
|
<HelpTooltip
|
||||||
text="The next number that will be assigned. This increments automatically each time an ID is generated."
|
text="The next number that will be assigned. This increments automatically each time an ID is generated."
|
||||||
title="Current Value"
|
title="Current Value"
|
||||||
@ -376,7 +376,7 @@
|
|||||||
/>
|
/>
|
||||||
{#if formErrors.CounterValue}
|
{#if formErrors.CounterValue}
|
||||||
<label class="label" for="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>
|
</label>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@ -384,8 +384,8 @@
|
|||||||
<!-- Start Value -->
|
<!-- Start Value -->
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="counterStart">
|
<label class="label" for="counterStart">
|
||||||
<span class="label-text font-medium">Start Value</span>
|
<span class="label-text text-sm font-medium">Start Value</span>
|
||||||
<span class="label-text-alt text-error">*</span>
|
<span class="label-text-alt text-xs text-error">*</span>
|
||||||
<HelpTooltip
|
<HelpTooltip
|
||||||
text="The minimum value this counter can have. When reset, the counter returns to this value."
|
text="The minimum value this counter can have. When reset, the counter returns to this value."
|
||||||
title="Start Value"
|
title="Start Value"
|
||||||
@ -405,7 +405,7 @@
|
|||||||
/>
|
/>
|
||||||
{#if formErrors.CounterStart}
|
{#if formErrors.CounterStart}
|
||||||
<label class="label" for="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>
|
</label>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@ -413,7 +413,7 @@
|
|||||||
<!-- End Value -->
|
<!-- End Value -->
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="counterEnd">
|
<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
|
<HelpTooltip
|
||||||
text="Optional maximum value. When reached, the counter will reset to the start value. Leave empty for no limit."
|
text="Optional maximum value. When reached, the counter will reset to the start value. Leave empty for no limit."
|
||||||
title="End Value"
|
title="End Value"
|
||||||
@ -432,7 +432,7 @@
|
|||||||
/>
|
/>
|
||||||
{#if formErrors.CounterEnd}
|
{#if formErrors.CounterEnd}
|
||||||
<label class="label" for="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>
|
</label>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@ -441,7 +441,7 @@
|
|||||||
<!-- Reset Pattern -->
|
<!-- Reset Pattern -->
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="counterReset">
|
<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
|
<HelpTooltip
|
||||||
text="Determines when the counter automatically resets to the start value. Daily resets at midnight, monthly on the 1st, yearly on Jan 1st."
|
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"
|
title="Reset Pattern"
|
||||||
@ -460,7 +460,7 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
<label class="label" for="counterReset">
|
<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
|
Choose when the counter automatically resets to the start value
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@ -190,14 +190,14 @@
|
|||||||
</a>
|
</a>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="flex items-center gap-2">
|
<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
|
<HelpTooltip
|
||||||
title="Geography Data"
|
title="Geography Data"
|
||||||
text="Geography data is used for patient address management throughout the system. This includes province, city, and detailed area information."
|
text="Geography data is used for patient address management throughout the system. This includes province, city, and detailed area information."
|
||||||
position="right"
|
position="right"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -264,7 +264,7 @@
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search provinces by name..."
|
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}
|
bind:value={provinceSearch}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -350,7 +350,7 @@
|
|||||||
? `Search cities in ${selectedProvinceLabel}...`
|
? `Search cities in ${selectedProvinceLabel}...`
|
||||||
: "Search cities by name..."
|
: "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}
|
bind:value={citySearch}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -412,7 +412,7 @@
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search areas by name, code, or class..."
|
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}
|
bind:value={areaSearch}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -169,8 +169,8 @@
|
|||||||
<ArrowLeft class="w-5 h-5" />
|
<ArrowLeft class="w-5 h-5" />
|
||||||
</a>
|
</a>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<h1 class="text-3xl font-bold text-gray-800">Locations</h1>
|
<h1 class="text-xl font-bold text-gray-800">Locations</h1>
|
||||||
<p class="text-gray-600">Manage locations and facilities</p>
|
<p class="text-sm text-gray-600">Manage locations and facilities</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary" onclick={openCreateModal}>
|
<button class="btn btn-primary" onclick={openCreateModal}>
|
||||||
<Plus class="w-4 h-4 mr-2" />
|
<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" />
|
<Search class="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
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..."
|
placeholder="Search by code or name..."
|
||||||
bind:value={searchQuery}
|
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">
|
<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" />
|
<MapPin class="w-8 h-8 text-gray-400" />
|
||||||
</div>
|
</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'}
|
{searchQuery ? 'No locations found' : 'No locations yet'}
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-sm text-base-content/60 text-center max-w-sm mb-4">
|
<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="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="code">
|
<label class="label" for="code">
|
||||||
<span class="label-text font-medium">Location Code</span>
|
<span class="label-text text-sm font-medium">Location Code</span>
|
||||||
<span class="label-text-alt text-error">*</span>
|
<span class="label-text-alt text-xs text-error">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="code"
|
id="code"
|
||||||
@ -270,13 +270,13 @@
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<label class="label" for="code">
|
<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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="name">
|
<label class="label" for="name">
|
||||||
<span class="label-text font-medium">Location Name</span>
|
<span class="label-text text-sm font-medium">Location Name</span>
|
||||||
<span class="label-text-alt text-error">*</span>
|
<span class="label-text-alt text-xs text-error">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="name"
|
id="name"
|
||||||
@ -287,15 +287,15 @@
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<label class="label" for="name">
|
<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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="type">
|
<label class="label" for="type">
|
||||||
<span class="label-text font-medium">Location Type</span>
|
<span class="label-text text-sm font-medium">Location Type</span>
|
||||||
<span class="label-text-alt text-error">*</span>
|
<span class="label-text-alt text-xs text-error">*</span>
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
id="type"
|
id="type"
|
||||||
@ -311,12 +311,12 @@
|
|||||||
<option value="AREA">Area</option>
|
<option value="AREA">Area</option>
|
||||||
</select>
|
</select>
|
||||||
<label class="label" for="type">
|
<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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="parent">
|
<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
|
<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."
|
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"
|
title="Location Hierarchy"
|
||||||
@ -330,7 +330,7 @@
|
|||||||
placeholder="None (top-level location)"
|
placeholder="None (top-level location)"
|
||||||
/>
|
/>
|
||||||
<label class="label" for="parent">
|
<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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -160,8 +160,8 @@
|
|||||||
<ArrowLeft class="w-5 h-5" />
|
<ArrowLeft class="w-5 h-5" />
|
||||||
</a>
|
</a>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<h1 class="text-3xl font-bold text-gray-800">Occupations</h1>
|
<h1 class="text-xl font-bold text-gray-800">Occupations</h1>
|
||||||
<p class="text-gray-600">Manage occupation codes for patient demographics</p>
|
<p class="text-sm text-gray-600">Manage occupation codes for patient demographics</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary" onclick={openCreateModal}>
|
<button class="btn btn-primary" onclick={openCreateModal}>
|
||||||
<Plus class="w-4 h-4 mr-2" />
|
<Plus class="w-4 h-4 mr-2" />
|
||||||
@ -177,7 +177,7 @@
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search by code or occupation name..."
|
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}
|
bind:value={searchQuery}
|
||||||
onkeydown={handleSearchKeydown}
|
onkeydown={handleSearchKeydown}
|
||||||
/>
|
/>
|
||||||
@ -196,10 +196,10 @@
|
|||||||
<Briefcase class="w-12 h-12 text-gray-400" />
|
<Briefcase class="w-12 h-12 text-gray-400" />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</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'}
|
{searchQuery.trim() ? 'No occupations match your search' : 'No occupations found'}
|
||||||
</h3>
|
</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()
|
{searchQuery.trim()
|
||||||
? `No occupations found matching "${searchQuery}". Try a different search term or clear the filter.`
|
? `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.'}
|
: '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="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="occCode">
|
<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
|
Occupation Code
|
||||||
<HelpTooltip
|
<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."
|
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"
|
position="top"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span class="label-text-alt text-error">*</span>
|
<span class="label-text-alt text-xs text-error">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="occCode"
|
id="occCode"
|
||||||
@ -285,15 +285,15 @@
|
|||||||
disabled={saving}
|
disabled={saving}
|
||||||
/>
|
/>
|
||||||
{#if formErrors.OccCode}
|
{#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}
|
{: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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="occText">
|
<label class="label" for="occText">
|
||||||
<span class="label-text font-medium">Occupation Name</span>
|
<span class="label-text text-sm font-medium">Occupation Name</span>
|
||||||
<span class="label-text-alt text-error">*</span>
|
<span class="label-text-alt text-xs text-error">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="occText"
|
id="occText"
|
||||||
@ -306,15 +306,15 @@
|
|||||||
disabled={saving}
|
disabled={saving}
|
||||||
/>
|
/>
|
||||||
{#if formErrors.OccText}
|
{#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}
|
{: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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="description">
|
<label class="label" for="description">
|
||||||
<span class="label-text font-medium">Description</span>
|
<span class="label-text text-sm font-medium">Description</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="description"
|
id="description"
|
||||||
@ -326,9 +326,9 @@
|
|||||||
disabled={saving}
|
disabled={saving}
|
||||||
/>
|
/>
|
||||||
{#if formErrors.Description}
|
{#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}
|
{: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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -149,8 +149,8 @@
|
|||||||
<ArrowLeft class="w-5 h-5" />
|
<ArrowLeft class="w-5 h-5" />
|
||||||
</a>
|
</a>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<h1 class="text-3xl font-bold text-gray-800">Medical Specialties</h1>
|
<h1 class="text-xl font-bold text-gray-800">Medical Specialties</h1>
|
||||||
<p class="text-gray-600">Manage medical specialty codes and their hierarchical relationships</p>
|
<p class="text-sm text-gray-600">Manage medical specialty codes and their hierarchical relationships</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary" onclick={openCreateModal}>
|
<button class="btn btn-primary" onclick={openCreateModal}>
|
||||||
<Plus class="w-4 h-4 mr-2" />
|
<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" />
|
<Search class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
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..."
|
placeholder="Search by specialty name, title, or parent..."
|
||||||
bind:value={searchQuery}
|
bind:value={searchQuery}
|
||||||
/>
|
/>
|
||||||
@ -187,7 +187,7 @@
|
|||||||
<div class="bg-base-200 rounded-full p-6 mb-4">
|
<div class="bg-base-200 rounded-full p-6 mb-4">
|
||||||
<Stethoscope class="w-12 h-12 text-base-content/40" />
|
<Stethoscope class="w-12 h-12 text-base-content/40" />
|
||||||
</div>
|
</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'}
|
{searchQuery ? 'No specialties match your search' : 'No specialties yet'}
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-base-content/60 text-center max-w-sm mb-4">
|
<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="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="specialtyText">
|
<label class="label" for="specialtyText">
|
||||||
<span class="label-text font-medium">Specialty Name</span>
|
<span class="label-text text-sm font-medium">Specialty Name</span>
|
||||||
<span class="label-text-alt text-error">*</span>
|
<span class="label-text-alt text-xs text-error">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="specialtyText"
|
id="specialtyText"
|
||||||
@ -268,12 +268,12 @@
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<label class="label" for="specialtyText">
|
<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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="title">
|
<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>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="title"
|
id="title"
|
||||||
@ -283,14 +283,14 @@
|
|||||||
placeholder="e.g., Sp. PD, Sp. A, Sp. And"
|
placeholder="e.g., Sp. PD, Sp. A, Sp. And"
|
||||||
/>
|
/>
|
||||||
<label class="label" for="title">
|
<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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<div class="flex items-center gap-2 mb-1">
|
<div class="flex items-center gap-2 mb-1">
|
||||||
<label class="label py-0" for="parent">
|
<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>
|
</label>
|
||||||
<HelpTooltip
|
<HelpTooltip
|
||||||
text="Organize specialties hierarchically. For example, select 'Internal Medicine' as the parent for 'Cardiology' to show it as a subspecialty."
|
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)"
|
placeholder="None (Top-level specialty)"
|
||||||
/>
|
/>
|
||||||
<label class="label" for="parent">
|
<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
|
Optional: Select a parent specialty to create a subspecialty
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
import Modal from '$lib/components/Modal.svelte';
|
import Modal from '$lib/components/Modal.svelte';
|
||||||
import TestModal from './TestModal.svelte';
|
import TestModal from './TestModal.svelte';
|
||||||
import TestTypeSelector from './test-modal/TestTypeSelector.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';
|
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([]);
|
let loading = $state(false), tests = $state([]), disciplines = $state([]), departments = $state([]);
|
||||||
@ -32,11 +32,9 @@
|
|||||||
Unit: '',
|
Unit: '',
|
||||||
Formula: '',
|
Formula: '',
|
||||||
refnum: [],
|
refnum: [],
|
||||||
refthold: [],
|
|
||||||
reftxt: [],
|
reftxt: [],
|
||||||
refvset: [],
|
|
||||||
refRangeType: 'none',
|
refRangeType: 'none',
|
||||||
// Technical Config (testdeftech)
|
// Technical Config (flat structure)
|
||||||
ResultType: '',
|
ResultType: '',
|
||||||
RefType: '',
|
RefType: '',
|
||||||
ReqQty: null,
|
ReqQty: null,
|
||||||
@ -99,9 +97,7 @@ function openCreateModal(type = 'TEST') {
|
|||||||
Unit: '',
|
Unit: '',
|
||||||
Formula: '',
|
Formula: '',
|
||||||
refnum: [],
|
refnum: [],
|
||||||
refthold: [],
|
|
||||||
reftxt: [],
|
reftxt: [],
|
||||||
refvset: [],
|
|
||||||
refRangeType: 'none',
|
refRangeType: 'none',
|
||||||
// Technical Config
|
// Technical Config
|
||||||
ResultType: '',
|
ResultType: '',
|
||||||
@ -127,15 +123,33 @@ function openCreateModal(type = 'TEST') {
|
|||||||
const testDetail = response.data;
|
const testDetail = response.data;
|
||||||
|
|
||||||
modalMode = 'edit';
|
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';
|
let refRangeType = 'none';
|
||||||
if (testDetail.refnum?.length > 0) refRangeType = 'num';
|
const hasNum = consolidatedRefnum.some(ref => ref.RefType !== 'THOLD');
|
||||||
else if (testDetail.refthold?.length > 0) refRangeType = 'thold';
|
const hasThold = consolidatedRefnum.some(ref => ref.RefType === 'THOLD');
|
||||||
else if (testDetail.reftxt?.length > 0) refRangeType = 'text';
|
const hasText = consolidatedReftxt.some(ref => ref.RefType !== 'VSET');
|
||||||
else if (testDetail.refvset?.length > 0) refRangeType = '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)
|
// Normalize reference range data to ensure all fields have values (not undefined)
|
||||||
const normalizeRefNum = (ref) => ({
|
const normalizeRefNum = (ref) => ({
|
||||||
Sex: ref.Sex ?? '2',
|
RefType: ref.RefType ?? 'REF',
|
||||||
|
Sex: ref.Sex ?? '0',
|
||||||
LowSign: ref.LowSign ?? 'GE',
|
LowSign: ref.LowSign ?? 'GE',
|
||||||
HighSign: ref.HighSign ?? 'LE',
|
HighSign: ref.HighSign ?? 'LE',
|
||||||
Low: ref.Low ?? null,
|
Low: ref.Low ?? null,
|
||||||
@ -143,37 +157,14 @@ function openCreateModal(type = 'TEST') {
|
|||||||
AgeStart: ref.AgeStart ?? 0,
|
AgeStart: ref.AgeStart ?? 0,
|
||||||
AgeEnd: ref.AgeEnd ?? 120,
|
AgeEnd: ref.AgeEnd ?? 120,
|
||||||
Flag: ref.Flag ?? 'N',
|
Flag: ref.Flag ?? 'N',
|
||||||
Interpretation: ref.Interpretation ?? 'Normal',
|
Interpretation: ref.Interpretation ?? '',
|
||||||
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',
|
|
||||||
SpcType: ref.SpcType ?? '',
|
SpcType: ref.SpcType ?? '',
|
||||||
Criteria: ref.Criteria ?? ''
|
Criteria: ref.Criteria ?? ''
|
||||||
});
|
});
|
||||||
|
|
||||||
const normalizeRefTxt = (ref) => ({
|
const normalizeRefTxt = (ref) => ({
|
||||||
Sex: ref.Sex ?? '2',
|
RefType: ref.RefType ?? 'TEXT',
|
||||||
AgeStart: ref.AgeStart ?? 0,
|
Sex: ref.Sex ?? '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',
|
|
||||||
AgeStart: ref.AgeStart ?? 0,
|
AgeStart: ref.AgeStart ?? 0,
|
||||||
AgeEnd: ref.AgeEnd ?? 120,
|
AgeEnd: ref.AgeEnd ?? 120,
|
||||||
RefTxt: ref.RefTxt ?? '',
|
RefTxt: ref.RefTxt ?? '',
|
||||||
@ -188,8 +179,8 @@ function openCreateModal(type = 'TEST') {
|
|||||||
TestSiteCode: testDetail.TestSiteCode,
|
TestSiteCode: testDetail.TestSiteCode,
|
||||||
TestSiteName: testDetail.TestSiteName,
|
TestSiteName: testDetail.TestSiteName,
|
||||||
TestType: testDetail.TestType,
|
TestType: testDetail.TestType,
|
||||||
DisciplineID: testDetail.testdeftech?.[0]?.DisciplineID || null,
|
DisciplineID: testDetail.DisciplineID || null,
|
||||||
DepartmentID: testDetail.testdeftech?.[0]?.DepartmentID || null,
|
DepartmentID: testDetail.DepartmentID || null,
|
||||||
SeqScr: testDetail.SeqScr || '0',
|
SeqScr: testDetail.SeqScr || '0',
|
||||||
SeqRpt: testDetail.SeqRpt || '0',
|
SeqRpt: testDetail.SeqRpt || '0',
|
||||||
VisibleScr: testDetail.VisibleScr === '1' || testDetail.VisibleScr === 1 || testDetail.VisibleScr === true,
|
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,
|
CountStat: testDetail.CountStat === '1' || testDetail.CountStat === 1 || testDetail.CountStat === true,
|
||||||
Unit: testDetail.Unit || '',
|
Unit: testDetail.Unit || '',
|
||||||
Formula: testDetail.Formula || '',
|
Formula: testDetail.Formula || '',
|
||||||
refnum: (testDetail.refnum || []).map(normalizeRefNum),
|
refnum: consolidatedRefnum.map(normalizeRefNum),
|
||||||
refthold: (testDetail.refthold || []).map(normalizeRefThold),
|
reftxt: consolidatedReftxt.map(normalizeRefTxt),
|
||||||
reftxt: (testDetail.reftxt || []).map(normalizeRefTxt),
|
|
||||||
refvset: (testDetail.refvset || []).map(normalizeRefVset),
|
|
||||||
refRangeType,
|
refRangeType,
|
||||||
// Technical Config (from testdeftech[0])
|
// Technical Config (flat structure)
|
||||||
ResultType: testDetail.testdeftech?.[0]?.ResultType || '',
|
ResultType: testDetail.ResultType || '',
|
||||||
RefType: testDetail.testdeftech?.[0]?.RefType || '',
|
RefType: testDetail.RefType || '',
|
||||||
ReqQty: testDetail.testdeftech?.[0]?.ReqQty || null,
|
ReqQty: testDetail.ReqQty || null,
|
||||||
ReqQtyUnit: testDetail.testdeftech?.[0]?.ReqQtyUnit || '',
|
ReqQtyUnit: testDetail.ReqQtyUnit || '',
|
||||||
Unit1: testDetail.testdeftech?.[0]?.Unit1 || '',
|
Unit1: testDetail.Unit1 || '',
|
||||||
Factor: testDetail.testdeftech?.[0]?.Factor || null,
|
Factor: testDetail.Factor || null,
|
||||||
Unit2: testDetail.testdeftech?.[0]?.Unit2 || '',
|
Unit2: testDetail.Unit2 || '',
|
||||||
Decimal: testDetail.testdeftech?.[0]?.Decimal || 0,
|
Decimal: testDetail.Decimal || 0,
|
||||||
Method: testDetail.testdeftech?.[0]?.Method || '',
|
Method: testDetail.Method || '',
|
||||||
ExpectedTAT: testDetail.testdeftech?.[0]?.ExpectedTAT || null,
|
ExpectedTAT: testDetail.ExpectedTAT || null,
|
||||||
// Group Members - API returns as testdefgrp
|
// Group Members - API returns as testdefgrp
|
||||||
groupMembers: testDetail.testdefgrp || []
|
groupMembers: testDetail.testdefgrp || []
|
||||||
};
|
};
|
||||||
@ -228,10 +217,25 @@ function openCreateModal(type = 'TEST') {
|
|||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
if (isDuplicateCode(formData.TestSiteCode, modalMode === 'edit' ? formData.TestSiteID : null)) { toastError(`Test code '${formData.TestSiteCode}' already exists`); return; }
|
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 (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; } } }
|
// Validate reference ranges based on type
|
||||||
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; } } }
|
if (formData.refRangeType === 'num' || formData.refRangeType === 'thold') {
|
||||||
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; } } }
|
const rangesToValidate = (formData.refnum || []).filter(ref =>
|
||||||
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; } } }
|
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;
|
saving = true;
|
||||||
try {
|
try {
|
||||||
const payload = { ...formData };
|
const payload = { ...formData };
|
||||||
@ -240,9 +244,30 @@ function openCreateModal(type = 'TEST') {
|
|||||||
if (!canHaveFormula) delete payload.Formula;
|
if (!canHaveFormula) delete payload.Formula;
|
||||||
if (!canHaveRefRange) {
|
if (!canHaveRefRange) {
|
||||||
delete payload.refnum;
|
delete payload.refnum;
|
||||||
delete payload.refthold;
|
|
||||||
delete payload.reftxt;
|
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) {
|
if (!canHaveTechnical) {
|
||||||
delete payload.ResultType;
|
delete payload.ResultType;
|
||||||
@ -291,8 +316,8 @@ function openCreateModal(type = 'TEST') {
|
|||||||
<div class="flex items-center gap-4 mb-6">
|
<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>
|
<a href="/master-data" class="btn btn-ghost btn-circle"><ArrowLeft class="w-5 h-5" /></a>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<h1 class="text-3xl font-bold text-gray-800">Test Definitions</h1>
|
<h1 class="text-xl font-bold text-gray-800">Test Definitions</h1>
|
||||||
<p class="text-gray-600">Manage laboratory tests, panels, and calculated values</p>
|
<p class="text-sm text-gray-600">Manage laboratory tests, panels, and calculated values</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary" onclick={openTypeSelector}><Plus class="w-4 h-4 mr-2" />Add Test</button>
|
<button class="btn btn-primary" onclick={openTypeSelector}><Plus class="w-4 h-4 mr-2" />Add Test</button>
|
||||||
</div>
|
</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="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 flex-col sm:flex-row gap-4">
|
||||||
<div class="flex-1 relative">
|
<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" />
|
<Search class="w-5 h-5 absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full sm:w-48">
|
<div class="w-full sm:w-48">
|
||||||
|
|||||||
@ -59,8 +59,7 @@
|
|||||||
|
|
||||||
// Get tab count badge for reference range
|
// Get tab count badge for reference range
|
||||||
function getRefRangeCount() {
|
function getRefRangeCount() {
|
||||||
return (formData.refnum?.length || 0) + (formData.reftxt?.length || 0) +
|
return (formData.refnum?.length || 0) + (formData.reftxt?.length || 0);
|
||||||
(formData.refthold?.length || 0) + (formData.refvset?.length || 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get tab count badge for group members
|
// Get tab count badge for group members
|
||||||
|
|||||||
@ -14,6 +14,19 @@ export const flagOptions = [
|
|||||||
{ value: 'C', label: 'C', description: 'Critical' }
|
{ 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 = [
|
export const sexOptions = [
|
||||||
{ value: '2', label: 'Male' },
|
{ value: '2', label: 'Male' },
|
||||||
{ value: '1', label: 'Female' },
|
{ value: '1', label: 'Female' },
|
||||||
@ -22,7 +35,8 @@ export const sexOptions = [
|
|||||||
|
|
||||||
export function createNumRef() {
|
export function createNumRef() {
|
||||||
return {
|
return {
|
||||||
Sex: '2',
|
RefType: 'REF',
|
||||||
|
Sex: '0',
|
||||||
LowSign: 'GE',
|
LowSign: 'GE',
|
||||||
HighSign: 'LE',
|
HighSign: 'LE',
|
||||||
Low: null,
|
Low: null,
|
||||||
@ -30,7 +44,7 @@ export function createNumRef() {
|
|||||||
AgeStart: 0,
|
AgeStart: 0,
|
||||||
AgeEnd: 120,
|
AgeEnd: 120,
|
||||||
Flag: 'N',
|
Flag: 'N',
|
||||||
Interpretation: 'Normal',
|
Interpretation: '',
|
||||||
SpcType: '',
|
SpcType: '',
|
||||||
Criteria: ''
|
Criteria: ''
|
||||||
};
|
};
|
||||||
@ -38,7 +52,8 @@ export function createNumRef() {
|
|||||||
|
|
||||||
export function createTholdRef() {
|
export function createTholdRef() {
|
||||||
return {
|
return {
|
||||||
Sex: '2',
|
RefType: 'THOLD',
|
||||||
|
Sex: '0',
|
||||||
LowSign: 'GE',
|
LowSign: 'GE',
|
||||||
HighSign: 'LE',
|
HighSign: 'LE',
|
||||||
Low: null,
|
Low: null,
|
||||||
@ -46,7 +61,7 @@ export function createTholdRef() {
|
|||||||
AgeStart: 0,
|
AgeStart: 0,
|
||||||
AgeEnd: 120,
|
AgeEnd: 120,
|
||||||
Flag: 'N',
|
Flag: 'N',
|
||||||
Interpretation: 'Normal',
|
Interpretation: '',
|
||||||
SpcType: '',
|
SpcType: '',
|
||||||
Criteria: ''
|
Criteria: ''
|
||||||
};
|
};
|
||||||
@ -54,7 +69,8 @@ export function createTholdRef() {
|
|||||||
|
|
||||||
export function createTextRef() {
|
export function createTextRef() {
|
||||||
return {
|
return {
|
||||||
Sex: '2',
|
RefType: 'TEXT',
|
||||||
|
Sex: '0',
|
||||||
AgeStart: 0,
|
AgeStart: 0,
|
||||||
AgeEnd: 120,
|
AgeEnd: 120,
|
||||||
RefTxt: '',
|
RefTxt: '',
|
||||||
@ -66,7 +82,8 @@ export function createTextRef() {
|
|||||||
|
|
||||||
export function createVsetRef() {
|
export function createVsetRef() {
|
||||||
return {
|
return {
|
||||||
Sex: '2',
|
RefType: 'VSET',
|
||||||
|
Sex: '0',
|
||||||
AgeStart: 0,
|
AgeStart: 0,
|
||||||
AgeEnd: 120,
|
AgeEnd: 120,
|
||||||
RefTxt: '',
|
RefTxt: '',
|
||||||
@ -87,16 +104,8 @@ export function validateNumericRange(ref, index) {
|
|||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateTholdRange(ref, index) {
|
// Alias for threshold validation (same logic)
|
||||||
const errors = [];
|
export const validateTholdRange = validateNumericRange;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validateTextRange(ref, index) {
|
export function validateTextRange(ref, index) {
|
||||||
const errors = [];
|
const errors = [];
|
||||||
@ -106,10 +115,5 @@ export function validateTextRange(ref, index) {
|
|||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateVsetRange(ref, index) {
|
// Alias for value set validation (same logic)
|
||||||
const errors = [];
|
export const validateVsetRange = validateTextRange;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
const typeLabels = {
|
const typeLabels = {
|
||||||
TEST: 'Single Test',
|
TEST: 'Test',
|
||||||
PARAM: 'Parameter',
|
PARAM: 'Parameter',
|
||||||
CALC: 'Calculated',
|
CALC: 'Calculated',
|
||||||
GROUP: 'Panel'
|
GROUP: 'Panel'
|
||||||
@ -44,8 +44,8 @@ const typeLabels = {
|
|||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="testCode">
|
<label class="label" for="testCode">
|
||||||
<span class="label-text font-medium">Test Code</span>
|
<span class="label-text text-sm font-medium">Test Code</span>
|
||||||
<span class="label-text-alt text-error">*</span>
|
<span class="label-text-alt text-xs text-error">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="testCode"
|
id="testCode"
|
||||||
@ -58,8 +58,8 @@ const typeLabels = {
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="testName">
|
<label class="label" for="testName">
|
||||||
<span class="label-text font-medium">Test Name</span>
|
<span class="label-text text-sm font-medium">Test Name</span>
|
||||||
<span class="label-text-alt text-error">*</span>
|
<span class="label-text-alt text-xs text-error">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="testName"
|
id="testName"
|
||||||
@ -72,20 +72,6 @@ const typeLabels = {
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Discipline and Department -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<SelectDropdown
|
<SelectDropdown
|
||||||
@ -109,14 +95,14 @@ const typeLabels = {
|
|||||||
<div class="grid grid-cols-1 md:grid-cols-1 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-1 gap-4">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="formula">
|
<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
|
Formula
|
||||||
<HelpTooltip
|
<HelpTooltip
|
||||||
text="Enter a mathematical formula using test codes (e.g., BUN / Creatinine). Supported operators: +, -, *, /, parentheses."
|
text="Enter a mathematical formula using test codes (e.g., BUN / Creatinine). Supported operators: +, -, *, /, parentheses."
|
||||||
title="Formula Help"
|
title="Formula Help"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span class="label-text-alt text-error">*</span>
|
<span class="label-text-alt text-xs text-error">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="formula"
|
id="formula"
|
||||||
@ -126,7 +112,7 @@ const typeLabels = {
|
|||||||
placeholder="e.g., BUN / Creatinine"
|
placeholder="e.g., BUN / Creatinine"
|
||||||
required={canHaveFormula}
|
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>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@ -134,7 +120,7 @@ const typeLabels = {
|
|||||||
<!-- Description -->
|
<!-- Description -->
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="description">
|
<label class="label" for="description">
|
||||||
<span class="label-text font-medium">Description</span>
|
<span class="label-text text-sm font-medium">Description</span>
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
id="description"
|
id="description"
|
||||||
@ -145,11 +131,23 @@ const typeLabels = {
|
|||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Report Sequence, Visibility, and CountStat -->
|
<!-- Screen Sequence, Report Sequence, Visibility, and Statistics -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<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">
|
<div class="form-control">
|
||||||
<label class="label" for="seqRpt">
|
<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>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="seqRpt"
|
id="seqRpt"
|
||||||
@ -160,7 +158,7 @@ const typeLabels = {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<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">
|
<div class="flex gap-4">
|
||||||
<label class="label cursor-pointer gap-2">
|
<label class="label cursor-pointer gap-2">
|
||||||
<input type="checkbox" class="checkbox" bind:checked={formData.VisibleScr} />
|
<input type="checkbox" class="checkbox" bind:checked={formData.VisibleScr} />
|
||||||
@ -173,7 +171,7 @@ const typeLabels = {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<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">
|
<label class="label cursor-pointer gap-2">
|
||||||
<input type="checkbox" class="checkbox" bind:checked={formData.CountStat} />
|
<input type="checkbox" class="checkbox" bind:checked={formData.CountStat} />
|
||||||
<span class="label-text">Count in Statistics</span>
|
<span class="label-text">Count in Statistics</span>
|
||||||
|
|||||||
@ -71,7 +71,7 @@
|
|||||||
<Search class="w-5 h-5 absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
<Search class="w-5 h-5 absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full pl-10"
|
class="input input-sm input-bordered w-full pl-10"
|
||||||
bind:value={searchQuery}
|
bind:value={searchQuery}
|
||||||
placeholder="Search by test name or code..."
|
placeholder="Search by test name or code..."
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import { PlusCircle, Calculator, X, ChevronDown, ChevronUp, Info, Beaker, Filter } from 'lucide-svelte';
|
import { PlusCircle, Calculator, X, ChevronDown, ChevronUp, Beaker } from 'lucide-svelte';
|
||||||
import { flagOptions, sexOptions, createNumRef } from '../referenceRange.js';
|
import { refTypeOptions, createNumRef } from '../referenceRange.js';
|
||||||
import { fetchValueSetByKey } from '$lib/api/valuesets.js';
|
import { fetchValueSetByKey } from '$lib/api/valuesets.js';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
@ -16,8 +16,7 @@
|
|||||||
onupdateRefnum = () => {}
|
onupdateRefnum = () => {}
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
// Track expanded state for each range's optional fields
|
let showAdvanced = $state({});
|
||||||
let expandedRanges = $state({});
|
|
||||||
let specimenTypeOptions = $state([]);
|
let specimenTypeOptions = $state([]);
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
@ -34,77 +33,36 @@
|
|||||||
|
|
||||||
function addRefRange() {
|
function addRefRange() {
|
||||||
const newRef = createNumRef();
|
const newRef = createNumRef();
|
||||||
// Set smarter defaults
|
newRef.Sex = '0';
|
||||||
newRef.Sex = '0'; // Any
|
newRef.Flag = 'N';
|
||||||
newRef.Flag = 'N'; // Normal
|
|
||||||
onupdateRefnum([...refnum, newRef]);
|
onupdateRefnum([...refnum, newRef]);
|
||||||
// Auto-expand the new range
|
|
||||||
expandedRanges[refnum.length] = { age: false, interpretation: false, specimen: false };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeRefRange(index) {
|
function removeRefRange(index) {
|
||||||
onupdateRefnum(refnum.filter((_, i) => i !== index));
|
onupdateRefnum(refnum.filter((_, i) => i !== index));
|
||||||
delete expandedRanges[index];
|
delete showAdvanced[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleAgeExpand(index) {
|
function toggleAdvanced(index) {
|
||||||
expandedRanges[index] = { ...expandedRanges[index], age: !expandedRanges[index]?.age };
|
showAdvanced[index] = !showAdvanced[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleInterpExpand(index) {
|
function hasAdvancedData(ref) {
|
||||||
expandedRanges[index] = { ...expandedRanges[index], interpretation: !expandedRanges[index]?.interpretation };
|
return ref.Interpretation || ref.SpcType || ref.Criteria ||
|
||||||
}
|
ref.Sex !== '0' || ref.Flag !== 'N' ||
|
||||||
|
(ref.AgeStart !== 0 || ref.AgeEnd !== 120);
|
||||||
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}`;
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="space-y-3">
|
<div class="space-y-2">
|
||||||
|
<!-- Header -->
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Calculator class="w-5 h-5 text-primary" />
|
<Calculator class="w-5 h-5 text-primary" />
|
||||||
<h3 class="font-semibold">Numeric Reference Ranges</h3>
|
<h3 class="font-semibold">Numeric Reference Ranges</h3>
|
||||||
|
{#if refnum?.length > 0}
|
||||||
|
<span class="badge badge-sm badge-ghost">{refnum.length}</span>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-sm btn-primary" onclick={addRefRange}>
|
<button type="button" class="btn btn-sm btn-primary" onclick={addRefRange}>
|
||||||
<PlusCircle class="w-4 h-4 mr-1" />
|
<PlusCircle class="w-4 h-4 mr-1" />
|
||||||
@ -112,6 +70,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
{#if refnum?.length === 0}
|
{#if refnum?.length === 0}
|
||||||
<div class="text-center py-8 bg-base-200 rounded-lg border-2 border-dashed border-base-300">
|
<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" />
|
<Calculator class="w-12 h-12 mx-auto text-gray-400 mb-2" />
|
||||||
@ -123,187 +82,152 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<!-- 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)}
|
{#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="border-b border-base-200 last:border-b-0">
|
||||||
<div class="card-body p-4">
|
<!-- Main Row -->
|
||||||
<!-- Header with Preview -->
|
<div class="px-4 py-3 grid grid-cols-12 gap-3 items-center hover:bg-base-100">
|
||||||
<div class="flex justify-between items-start mb-4 pb-3 border-b border-base-200">
|
<!-- Row Number -->
|
||||||
<div class="flex-1">
|
<div class="col-span-1 flex items-center gap-1">
|
||||||
<div class="flex items-center gap-2 mb-2">
|
<span class="text-xs text-gray-500">{index + 1}</span>
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Main Fields - Range Values (Most Important) -->
|
<!-- Type Dropdown -->
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-3">
|
<div class="col-span-3">
|
||||||
<div class="form-control">
|
<select class="select select-sm select-bordered w-full" bind:value={ref.RefType}>
|
||||||
<span class="label-text text-xs mb-1 font-medium">Lower Bound</span>
|
{#each refTypeOptions as option (option.value)}
|
||||||
<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>
|
<option value={option.value}>{option.label}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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">
|
<div class="form-control">
|
||||||
<span class="label-text text-xs mb-1">Flag</span>
|
<span class="label-text text-xs text-gray-500">Sex</span>
|
||||||
<select class="select select-sm select-bordered w-full" bind:value={ref.Flag}>
|
<select class="select select-sm select-bordered w-full" bind:value={ref.Sex}>
|
||||||
{#each flagOptions as option (option.value)}
|
<option value="0">Any</option>
|
||||||
<option value={option.value}>{option.label} - {option.description}</option>
|
<option value="1">Female</option>
|
||||||
{/each}
|
<option value="2">Male</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Expandable: Age Range -->
|
<!-- 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">
|
<div class="form-control">
|
||||||
<span class="label-text text-xs mb-1">From (years)</span>
|
<span class="label-text text-xs text-gray-500">Age Range</span>
|
||||||
<input type="number" min="0" max="120" class="input input-sm input-bordered w-full" bind:value={ref.AgeStart} />
|
<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>
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Expandable: Interpretation -->
|
<!-- Specimen -->
|
||||||
<div class="border border-base-200 rounded-lg overflow-hidden mb-2">
|
<div class="form-control md:col-span-2">
|
||||||
<button
|
<span class="label-text text-xs text-gray-500 flex items-center gap-1">
|
||||||
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" />
|
<Beaker class="w-3 h-3" />
|
||||||
Specimen Type
|
Specimen Type
|
||||||
</span>
|
</span>
|
||||||
<select class="select select-sm select-bordered w-full" bind:value={ref.SpcType}>
|
<select class="select select-sm select-bordered w-full" bind:value={ref.SpcType}>
|
||||||
<option value="">Any specimen</option>
|
<option value="">Any specimen</option>
|
||||||
{#each specimenTypeOptions as option}
|
{#each specimenTypeOptions as option (option.value)}
|
||||||
<option value={option.value}>{option.label}</option>
|
<option value={option.value}>{option.label}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
|
||||||
<span class="label-text text-xs mb-1 flex items-center gap-1">
|
<!-- Interpretation -->
|
||||||
<Filter class="w-3 h-3" />
|
<div class="form-control md:col-span-2">
|
||||||
Criteria
|
<span class="label-text text-xs text-gray-500">Interpretation</span>
|
||||||
</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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-sm input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
@ -312,9 +236,10 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
<script>
|
<script>
|
||||||
import { Ruler, Calculator, FileText, Box } from 'lucide-svelte';
|
import { Ruler, Info } from 'lucide-svelte';
|
||||||
import HelpTooltip from '$lib/components/HelpTooltip.svelte';
|
import HelpTooltip from '$lib/components/HelpTooltip.svelte';
|
||||||
import NumericRefRange from './NumericRefRange.svelte';
|
import NumericRefRange from './NumericRefRange.svelte';
|
||||||
import ThresholdRefRange from './ThresholdRefRange.svelte';
|
|
||||||
import TextRefRange from './TextRefRange.svelte';
|
import TextRefRange from './TextRefRange.svelte';
|
||||||
import ValueSetRefRange from './ValueSetRefRange.svelte';
|
|
||||||
import { createNumRef, createTholdRef, createTextRef, createVsetRef } from '../referenceRange.js';
|
import { createNumRef, createTholdRef, createTextRef, createVsetRef } from '../referenceRange.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -19,10 +17,28 @@
|
|||||||
onupdateFormData = () => {}
|
onupdateFormData = () => {}
|
||||||
} = $props();
|
} = $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
|
// Ensure all reference range items have defined values, never undefined
|
||||||
function normalizeRefNum(ref) {
|
function normalizeRefNum(ref) {
|
||||||
return {
|
return {
|
||||||
Sex: ref.Sex ?? '2',
|
RefType: ref.RefType ?? 'REF',
|
||||||
|
Sex: ref.Sex ?? '0',
|
||||||
LowSign: ref.LowSign ?? 'GE',
|
LowSign: ref.LowSign ?? 'GE',
|
||||||
HighSign: ref.HighSign ?? 'LE',
|
HighSign: ref.HighSign ?? 'LE',
|
||||||
Low: ref.Low ?? null,
|
Low: ref.Low ?? null,
|
||||||
@ -30,23 +46,7 @@
|
|||||||
AgeStart: ref.AgeStart ?? 0,
|
AgeStart: ref.AgeStart ?? 0,
|
||||||
AgeEnd: ref.AgeEnd ?? 120,
|
AgeEnd: ref.AgeEnd ?? 120,
|
||||||
Flag: ref.Flag ?? 'N',
|
Flag: ref.Flag ?? 'N',
|
||||||
Interpretation: ref.Interpretation ?? 'Normal',
|
Interpretation: ref.Interpretation ?? '',
|
||||||
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',
|
|
||||||
SpcType: ref.SpcType ?? '',
|
SpcType: ref.SpcType ?? '',
|
||||||
Criteria: ref.Criteria ?? ''
|
Criteria: ref.Criteria ?? ''
|
||||||
};
|
};
|
||||||
@ -54,7 +54,8 @@
|
|||||||
|
|
||||||
function normalizeRefTxt(ref) {
|
function normalizeRefTxt(ref) {
|
||||||
return {
|
return {
|
||||||
Sex: ref.Sex ?? '2',
|
RefType: ref.RefType ?? 'TEXT',
|
||||||
|
Sex: ref.Sex ?? '0',
|
||||||
AgeStart: ref.AgeStart ?? 0,
|
AgeStart: ref.AgeStart ?? 0,
|
||||||
AgeEnd: ref.AgeEnd ?? 120,
|
AgeEnd: ref.AgeEnd ?? 120,
|
||||||
RefTxt: ref.RefTxt ?? '',
|
RefTxt: ref.RefTxt ?? '',
|
||||||
@ -64,46 +65,61 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeRefVset(ref) {
|
// Reactive normalized data - filter by RefType
|
||||||
return {
|
let allRefnum = $derived((formData.refnum || []).map(normalizeRefNum));
|
||||||
Sex: ref.Sex ?? '2',
|
let allReftxt = $derived((formData.reftxt || []).map(normalizeRefTxt));
|
||||||
AgeStart: ref.AgeStart ?? 0,
|
|
||||||
AgeEnd: ref.AgeEnd ?? 120,
|
|
||||||
RefTxt: ref.RefTxt ?? '',
|
|
||||||
Flag: ref.Flag ?? 'N',
|
|
||||||
SpcType: ref.SpcType ?? '',
|
|
||||||
Criteria: ref.Criteria ?? ''
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reactive normalized data
|
// Filtered arrays for display
|
||||||
let normalizedRefnum = $derived((formData.refnum || []).map(normalizeRefNum));
|
let normalizedRefnum = $derived(allRefnum.filter(ref => ref.RefType !== 'THOLD'));
|
||||||
let normalizedRefthold = $derived((formData.refthold || []).map(normalizeRefThold));
|
let normalizedRefthold = $derived(allRefnum.filter(ref => ref.RefType === 'THOLD'));
|
||||||
let normalizedReftxt = $derived((formData.reftxt || []).map(normalizeRefTxt));
|
let normalizedReftxt = $derived(allReftxt.filter(ref => ref.RefType !== 'VSET'));
|
||||||
let normalizedRefvset = $derived((formData.refvset || []).map(normalizeRefVset));
|
let normalizedRefvset = $derived(allReftxt.filter(ref => ref.RefType === 'VSET'));
|
||||||
|
|
||||||
function updateRefRangeType(type) {
|
function updateRefRangeType(type) {
|
||||||
let newFormData = {
|
// Initialize arrays if they don't exist
|
||||||
|
const currentRefnum = formData.refnum || [];
|
||||||
|
const currentReftxt = formData.reftxt || [];
|
||||||
|
|
||||||
|
if (type === 'num') {
|
||||||
|
// Add numeric range to refnum
|
||||||
|
onupdateFormData({
|
||||||
...formData,
|
...formData,
|
||||||
refRangeType: type,
|
refRangeType: type,
|
||||||
refnum: [],
|
RefType: 'NMRC',
|
||||||
refthold: [],
|
refnum: [...currentRefnum, createNumRef()]
|
||||||
reftxt: [],
|
});
|
||||||
refvset: []
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize the selected type
|
|
||||||
if (type === 'num') {
|
|
||||||
newFormData.refnum = [createNumRef()];
|
|
||||||
} else if (type === 'thold') {
|
} 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') {
|
} 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') {
|
} 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) {
|
function updateRefnum(refnum) {
|
||||||
@ -111,7 +127,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateRefthold(refthold) {
|
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) {
|
function updateReftxt(reftxt) {
|
||||||
@ -119,7 +137,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateRefvset(refvset) {
|
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>
|
</script>
|
||||||
|
|
||||||
@ -134,90 +154,30 @@
|
|||||||
title="Reference Range Help"
|
title="Reference Range Help"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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]">
|
<!-- Dropdown Select -->
|
||||||
<input
|
<div class="form-control">
|
||||||
type="radio"
|
<select
|
||||||
name="refRangeType"
|
class="select select-bordered w-full"
|
||||||
class="radio radio-primary"
|
value={formData.refRangeType || 'none'}
|
||||||
value="none"
|
onchange={(e) => updateRefRangeType(e.target.value)}
|
||||||
checked={formData.refRangeType === 'none'}
|
>
|
||||||
onchange={() => updateRefRangeType('none')}
|
{#each refTypeOptions as option (option.value)}
|
||||||
/>
|
<option value={option.value}>{option.label}</option>
|
||||||
<div class="flex flex-col">
|
{/each}
|
||||||
<span class="label-text font-medium">None</span>
|
</select>
|
||||||
<span class="text-xs text-gray-500">No reference range</span>
|
|
||||||
</div>
|
</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]">
|
<!-- Show selected RefType info -->
|
||||||
<input
|
{#if formData.refRangeType && formData.refRangeType !== 'none'}
|
||||||
type="radio"
|
<div class="mt-3 flex items-center gap-2 p-2 bg-info/10 rounded-lg border border-info/20">
|
||||||
name="refRangeType"
|
<Info class="w-4 h-4 text-info" />
|
||||||
class="radio radio-primary"
|
<span class="text-sm">
|
||||||
value="num"
|
<span class="font-medium">Selected:</span>
|
||||||
checked={formData.refRangeType === 'num'}
|
{refTypeLabels[formData.refRangeType]}
|
||||||
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>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Numeric Reference Ranges -->
|
<!-- Numeric Reference Ranges -->
|
||||||
@ -225,9 +185,9 @@
|
|||||||
<NumericRefRange refnum={normalizedRefnum} onupdateRefnum={updateRefnum} />
|
<NumericRefRange refnum={normalizedRefnum} onupdateRefnum={updateRefnum} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Threshold Reference Ranges -->
|
<!-- Threshold Reference Ranges (uses same component as numeric) -->
|
||||||
{#if formData.refRangeType === 'thold'}
|
{#if formData.refRangeType === 'thold'}
|
||||||
<ThresholdRefRange refthold={normalizedRefthold} onupdateRefthold={updateRefthold} />
|
<NumericRefRange refnum={normalizedRefthold} onupdateRefnum={updateRefthold} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Text Reference Ranges -->
|
<!-- Text Reference Ranges -->
|
||||||
@ -235,8 +195,8 @@
|
|||||||
<TextRefRange reftxt={normalizedReftxt} onupdateReftxt={updateReftxt} />
|
<TextRefRange reftxt={normalizedReftxt} onupdateReftxt={updateReftxt} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Value Set Reference Ranges -->
|
<!-- Value Set Reference Ranges (uses same component as text) -->
|
||||||
{#if formData.refRangeType === 'vset'}
|
{#if formData.refRangeType === 'vset'}
|
||||||
<ValueSetRefRange refvset={normalizedRefvset} onupdateRefvset={updateRefvset} />
|
<TextRefRange reftxt={normalizedRefvset} onupdateReftxt={updateRefvset} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -17,36 +17,24 @@
|
|||||||
|
|
||||||
// Value set options
|
// Value set options
|
||||||
let resultTypeOptions = $state([]);
|
let resultTypeOptions = $state([]);
|
||||||
let refTypeOptions = $state([]);
|
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
loading = true;
|
loading = true;
|
||||||
const [resultTypeRes, refTypeRes] = await Promise.all([
|
const resultTypeRes = await fetchValueSetByKey('result_type');
|
||||||
fetchValueSetByKey('result_type'),
|
|
||||||
fetchValueSetByKey('reference_type')
|
|
||||||
]);
|
|
||||||
|
|
||||||
console.log('result_type response:', resultTypeRes);
|
console.log('result_type response:', resultTypeRes);
|
||||||
console.log('reference_type response:', refTypeRes);
|
|
||||||
|
|
||||||
// Handle different response structures
|
// Handle different response structures
|
||||||
const resultItems = resultTypeRes.data?.items || resultTypeRes.data?.ValueSetItems || (Array.isArray(resultTypeRes.data) ? resultTypeRes.data : []) || [];
|
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 => ({
|
resultTypeOptions = resultItems.map(item => ({
|
||||||
value: item.value || item.itemCode || item.ItemCode || item.code || item.Code,
|
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
|
label: item.label || item.itemValue || item.ItemValue || item.value || item.Value || item.description || item.Description
|
||||||
})).filter(opt => opt.value);
|
})).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('resultTypeOptions:', resultTypeOptions);
|
||||||
console.log('refTypeOptions:', refTypeOptions);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to load value sets:', err);
|
console.error('Failed to load value sets:', err);
|
||||||
} finally {
|
} finally {
|
||||||
@ -57,6 +45,9 @@
|
|||||||
function updateField(field, value) {
|
function updateField(field, value) {
|
||||||
onupdateFormData({ ...formData, [field]: value });
|
onupdateFormData({ ...formData, [field]: value });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if test is calculated type (doesn't have specimen requirements)
|
||||||
|
const isCalculated = $derived(formData.TestType === 'CALC');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
@ -72,10 +63,9 @@
|
|||||||
<h3 class="font-semibold">Result Configuration</h3>
|
<h3 class="font-semibold">Result Configuration</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div class="form-control max-w-md">
|
||||||
<div class="form-control">
|
|
||||||
<label class="label" for="resultType">
|
<label class="label" for="resultType">
|
||||||
<span class="label-text font-medium">Result Type</span>
|
<span class="label-text text-sm font-medium">Result Type</span>
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
id="resultType"
|
id="resultType"
|
||||||
@ -89,24 +79,6 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Units and Precision -->
|
<!-- Units and Precision -->
|
||||||
@ -119,7 +91,7 @@
|
|||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="unit1">
|
<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>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="unit1"
|
id="unit1"
|
||||||
@ -133,7 +105,7 @@
|
|||||||
|
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="factor">
|
<label class="label" for="factor">
|
||||||
<span class="label-text font-medium">Factor</span>
|
<span class="label-text text-sm font-medium">Factor</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="factor"
|
id="factor"
|
||||||
@ -148,7 +120,7 @@
|
|||||||
|
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="unit2">
|
<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>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="unit2"
|
id="unit2"
|
||||||
@ -162,7 +134,7 @@
|
|||||||
|
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="decimal">
|
<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>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="decimal"
|
id="decimal"
|
||||||
@ -185,6 +157,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Specimen Requirements -->
|
<!-- Specimen Requirements -->
|
||||||
|
{#if !isCalculated}
|
||||||
<div class="bg-base-100 rounded-lg border border-base-200 p-4">
|
<div class="bg-base-100 rounded-lg border border-base-200 p-4">
|
||||||
<div class="flex items-center gap-2 mb-4">
|
<div class="flex items-center gap-2 mb-4">
|
||||||
<Beaker class="w-5 h-5 text-primary" />
|
<Beaker class="w-5 h-5 text-primary" />
|
||||||
@ -194,7 +167,7 @@
|
|||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="reqQty">
|
<label class="label" for="reqQty">
|
||||||
<span class="label-text font-medium">Required Quantity</span>
|
<span class="label-text text-sm font-medium">Required Quantity</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="reqQty"
|
id="reqQty"
|
||||||
@ -209,7 +182,7 @@
|
|||||||
|
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="reqQtyUnit">
|
<label class="label" for="reqQtyUnit">
|
||||||
<span class="label-text font-medium">Quantity Unit</span>
|
<span class="label-text text-sm font-medium">Quantity Unit</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="reqQtyUnit"
|
id="reqQtyUnit"
|
||||||
@ -222,6 +195,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Method and TAT -->
|
<!-- Method and TAT -->
|
||||||
<div class="bg-base-100 rounded-lg border border-base-200 p-4">
|
<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="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="method">
|
<label class="label" for="method">
|
||||||
<span class="label-text font-medium">Method</span>
|
<span class="label-text text-sm font-medium">Method</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="method"
|
id="method"
|
||||||
@ -247,7 +221,7 @@
|
|||||||
|
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="expectedTAT">
|
<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>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="expectedTAT"
|
id="expectedTAT"
|
||||||
|
|||||||
@ -113,7 +113,7 @@
|
|||||||
<div class="form-control mt-3">
|
<div class="form-control mt-3">
|
||||||
<span class="label-text text-xs mb-1">Value Set</span>
|
<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" />
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Expandable: Specimen and Criteria -->
|
<!-- Expandable: Specimen and Criteria -->
|
||||||
|
|||||||
@ -133,14 +133,14 @@
|
|||||||
</a>
|
</a>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="flex items-center gap-3">
|
<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
|
<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."
|
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"
|
title="About ValueSets"
|
||||||
position="right"
|
position="right"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -154,7 +154,7 @@
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search ValueSets by key (e.g., priority_status, test_category)..."
|
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}
|
bind:value={searchQuery}
|
||||||
onkeydown={handleSearchKeydown}
|
onkeydown={handleSearchKeydown}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user