Update test modal components with improved UX and add TestTypeSelector

This commit is contained in:
mahdahar 2026-02-19 07:12:11 +07:00
parent 8d77370357
commit 995cdd3fec
5 changed files with 135 additions and 131 deletions

View File

@ -6,11 +6,12 @@
import DataTable from '$lib/components/DataTable.svelte'; import DataTable from '$lib/components/DataTable.svelte';
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 { validateNumericRange, validateTholdRange, validateTextRange, validateVsetRange } from './referenceRange.js'; import { validateNumericRange, validateTholdRange, validateTextRange, validateVsetRange } 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([]);
let modalOpen = $state(false), selectedRowIndex = $state(-1), expandedGroups = $state(new Set()); let modalOpen = $state(false), selectedRowIndex = $state(-1), expandedGroups = $state(new Set()), typeSelectorOpen = $state(false);
let currentPage = $state(1), perPage = $state(20), totalItems = $state(0), totalPages = $state(1); let currentPage = $state(1), perPage = $state(20), totalItems = $state(0), totalPages = $state(1);
let modalMode = $state('create'), saving = $state(false), selectedType = $state(''), searchQuery = $state(''), searchInputRef = $state(null); let modalMode = $state('create'), saving = $state(false), selectedType = $state(''), searchQuery = $state(''), searchInputRef = $state(null);
let deleteModalOpen = $state(false), testToDelete = $state(null), deleting = $state(false); let deleteModalOpen = $state(false), testToDelete = $state(null), deleting = $state(false);
@ -70,14 +71,23 @@ const canHaveRefRange = $derived(formData.TestType === 'TEST' || formData.TestTy
function getVisibleTests() { return tests.filter(t => t.IsActive !== '0' && t.IsActive !== 0); } function getVisibleTests() { return tests.filter(t => t.IsActive !== '0' && t.IsActive !== 0); }
function getTestTypeConfig(type) { return testTypeConfig[type] || testTypeConfig.TEST; } function getTestTypeConfig(type) { return testTypeConfig[type] || testTypeConfig.TEST; }
function formatReferenceRange(test) { return '-'; } function formatReferenceRange(test) { return '-'; }
function openCreateModal() { function openTypeSelector() {
typeSelectorOpen = true;
}
function handleTypeSelect(type) {
typeSelectorOpen = false;
openCreateModal(type);
}
function openCreateModal(type = 'TEST') {
modalMode = 'create'; modalMode = 'create';
formData = { formData = {
// Basic Info // Basic Info
TestSiteID: null, TestSiteID: null,
TestSiteCode: '', TestSiteCode: '',
TestSiteName: '', TestSiteName: '',
TestType: 'TEST', TestType: type,
DisciplineID: null, DisciplineID: null,
DepartmentID: null, DepartmentID: null,
SeqScr: '0', SeqScr: '0',
@ -284,7 +294,7 @@ const canHaveRefRange = $derived(formData.TestType === 'TEST' || formData.TestTy
<h1 class="text-3xl font-bold text-gray-800">Test Definitions</h1> <h1 class="text-3xl font-bold text-gray-800">Test Definitions</h1>
<p class="text-gray-600">Manage laboratory tests, panels, and calculated values</p> <p class="text-gray-600">Manage laboratory tests, panels, and calculated values</p>
</div> </div>
<button class="btn btn-primary" onclick={openCreateModal}><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>
<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">
@ -296,7 +306,7 @@ const canHaveRefRange = $derived(formData.TestType === 'TEST' || formData.TestTy
<div class="w-full sm:w-48"> <div class="w-full sm:w-48">
<select class="select select-sm select-bordered w-full" bind:value={selectedType} onchange={handleFilter}> <select class="select select-sm select-bordered w-full" bind:value={selectedType} onchange={handleFilter}>
<option value="">All Types</option> <option value="">All Types</option>
<option value="TEST">Technical Test</option> <option value="TEST">Single Test</option>
<option value="PARAM">Parameter</option> <option value="PARAM">Parameter</option>
<option value="CALC">Calculated</option> <option value="CALC">Calculated</option>
<option value="GROUP">Panel/Profile</option> <option value="GROUP">Panel/Profile</option>
@ -323,6 +333,13 @@ const canHaveRefRange = $derived(formData.TestType === 'TEST' || formData.TestTy
</div> </div>
</div> </div>
<Modal bind:open={typeSelectorOpen} title="Add Test" size="md">
<TestTypeSelector
onselect={handleTypeSelect}
oncancel={() => typeSelectorOpen = false}
/>
</Modal>
<TestModal <TestModal
bind:open={modalOpen} bind:open={modalOpen}
mode={modalMode} mode={modalMode}

View File

@ -20,6 +20,13 @@
onsave = () => {} onsave = () => {}
} = $props(); } = $props();
const typeLabels = {
TEST: 'Single Test',
PARAM: 'Parameter',
CALC: 'Calculated',
GROUP: 'Panel'
};
function handleSubmit(e) { function handleSubmit(e) {
e.preventDefault(); e.preventDefault();
onsave(); onsave();
@ -27,6 +34,12 @@
</script> </script>
<form class="space-y-3" onsubmit={handleSubmit}> <form class="space-y-3" onsubmit={handleSubmit}>
<!-- Test Type Header -->
<div class="bg-base-200 rounded-lg px-4 py-3 mb-4">
<span class="text-sm text-gray-500">Test Type</span>
<div class="font-semibold text-lg">{typeLabels[formData.TestType]}</div>
</div>
<!-- Basic Info --> <!-- Basic Info -->
<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">
@ -59,27 +72,8 @@
</div> </div>
</div> </div>
<!-- Type and Sequence --> <!-- Sequence -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="form-control max-w-xs">
<div class="form-control">
<label class="label" for="testType">
<span class="label-text font-medium">Test Type</span>
<span class="label-text-alt text-error">*</span>
</label>
<select
id="testType"
class="select select-sm select-bordered w-full"
bind:value={formData.TestType}
required
>
<option value="TEST">Technical Test</option>
<option value="PARAM">Parameter</option>
<option value="CALC">Calculated</option>
<option value="GROUP">Panel/Profile</option>
<option value="TITLE">Section Header</option>
</select>
</div>
<div class="form-control">
<label class="label" for="seqScr"> <label class="label" for="seqScr">
<span class="label-text font-medium">Screen Sequence</span> <span class="label-text font-medium">Screen Sequence</span>
</label> </label>
@ -91,7 +85,6 @@
placeholder="0" placeholder="0"
/> />
</div> </div>
</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">

View File

@ -1,5 +1,5 @@
<script> <script>
import { Search, Plus, X, GripVertical, Microscope } from 'lucide-svelte'; import { Search, Plus, X, Microscope } from 'lucide-svelte';
/** /**
* @typedef {Object} Props * @typedef {Object} Props
@ -16,7 +16,6 @@
} = $props(); } = $props();
let searchQuery = $state(''); let searchQuery = $state('');
let draggedIndex = $state(-1);
// Filter out the current test and already selected tests // Filter out the current test and already selected tests
let filteredTests = $derived( let filteredTests = $derived(
@ -35,8 +34,7 @@
TestSiteID: test.TestSiteID, TestSiteID: test.TestSiteID,
TestSiteCode: test.TestSiteCode, TestSiteCode: test.TestSiteCode,
TestSiteName: test.TestSiteName, TestSiteName: test.TestSiteName,
TestType: test.TestType, TestType: test.TestType
Sequence: (formData.groupMembers?.length || 0) + 1
} }
]; ];
onupdateFormData({ ...formData, groupMembers: newMembers }); onupdateFormData({ ...formData, groupMembers: newMembers });
@ -45,43 +43,9 @@
function removeMember(index) { function removeMember(index) {
const newMembers = formData.groupMembers.filter((_, i) => i !== index); const newMembers = formData.groupMembers.filter((_, i) => i !== index);
// Reorder sequences
newMembers.forEach((member, i) => {
member.Sequence = i + 1;
});
onupdateFormData({ ...formData, groupMembers: newMembers }); onupdateFormData({ ...formData, groupMembers: newMembers });
} }
function moveMember(fromIndex, toIndex) {
if (toIndex < 0 || toIndex >= formData.groupMembers.length) return;
const members = [...formData.groupMembers];
const [moved] = members.splice(fromIndex, 1);
members.splice(toIndex, 0, moved);
// Reorder sequences
members.forEach((member, i) => {
member.Sequence = i + 1;
});
onupdateFormData({ ...formData, groupMembers: members });
}
function handleDragStart(index) {
draggedIndex = index;
}
function handleDragOver(e, index) {
e.preventDefault();
if (draggedIndex === -1 || draggedIndex === index) return;
moveMember(draggedIndex, index);
draggedIndex = index;
}
function handleDragEnd() {
draggedIndex = -1;
}
function getTestTypeBadge(testType) { function getTestTypeBadge(testType) {
const badges = { const badges = {
'TEST': 'badge-primary', 'TEST': 'badge-primary',
@ -94,15 +58,15 @@
} }
</script> </script>
<div class="space-y-3"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4 h-[500px]">
<!-- Add Member Section --> <!-- Left: Search for Tests -->
<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 flex flex-col">
<div class="flex items-center gap-2 mb-4"> <div class="flex items-center gap-2 mb-4">
<Microscope class="w-5 h-5 text-primary" /> <Microscope class="w-5 h-5 text-primary" />
<h3 class="font-semibold">Add Group Members</h3> <h3 class="font-semibold">Add Group Members</h3>
</div> </div>
<div class="form-control"> <div class="form-control mb-3">
<div class="relative"> <div class="relative">
<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
@ -114,8 +78,8 @@
</div> </div>
</div> </div>
<div class="flex-1 overflow-y-auto border border-base-200 rounded-lg">
{#if searchQuery} {#if searchQuery}
<div class="mt-2 max-h-60 overflow-y-auto border border-base-200 rounded-lg">
{#if filteredTests.length === 0} {#if filteredTests.length === 0}
<div class="p-4 text-center text-gray-500"> <div class="p-4 text-center text-gray-500">
No tests found matching "{searchQuery}" No tests found matching "{searchQuery}"
@ -138,70 +102,57 @@
</button> </button>
{/each} {/each}
{/if} {/if}
{:else}
<div class="p-8 text-center text-gray-400">
<Search class="w-12 h-12 mx-auto mb-2 opacity-50" />
<p>Type to search for tests</p>
</div> </div>
{/if} {/if}
</div> </div>
</div>
<!-- Selected Members --> <!-- Right: Selected Members -->
<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-2 flex flex-col">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-2 px-1">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="font-semibold">Group Members</span> <span class="font-semibold text-sm">Group Members</span>
<span class="badge badge-sm badge-primary">{formData.groupMembers?.length || 0}</span> <span class="badge badge-xs badge-primary">{formData.groupMembers?.length || 0}</span>
</div> </div>
{#if formData.groupMembers?.length > 0}
<span class="text-sm text-gray-500">Drag to reorder</span>
{/if}
</div> </div>
<div class="flex-1 overflow-y-auto">
{#if !formData.groupMembers || formData.groupMembers.length === 0} {#if !formData.groupMembers || formData.groupMembers.length === 0}
<div class="text-center py-8 bg-base-200 rounded-lg border-2 border-dashed border-base-300"> <div class="h-full flex flex-col items-center justify-center text-center py-6 bg-base-200 rounded-lg border-2 border-dashed border-base-300">
<Microscope class="w-12 h-12 mx-auto text-gray-400 mb-2" /> <Microscope class="w-8 h-8 text-gray-400 mb-1" />
<p class="text-gray-500">No members added yet</p> <p class="text-gray-500 text-sm">No members added yet</p>
<p class="text-sm text-gray-400 mt-1">Search and add tests above</p> <p class="text-xs text-gray-400 mt-0.5">Search and add tests from the left</p>
</div> </div>
{:else} {:else}
<div class="space-y-2"> <div class="space-y-1">
{#each formData.groupMembers as member, index (member.TestSiteID)} {#each formData.groupMembers as member, index (`${member.TestSiteID}-${index}`)}
<div <div class="flex items-center gap-2 px-2 py-1.5 bg-base-100 border border-base-200 rounded hover:border-primary/50 transition-colors">
class="card bg-base-100 border border-base-200 hover:border-primary/50 transition-colors" <div class="flex-1 min-w-0">
draggable="true" <div class="flex items-center gap-1.5">
ondragstart={() => handleDragStart(index)} <span class="font-mono text-xs text-gray-600">{member.TestSiteCode}</span>
ondragover={(e) => handleDragOver(e, index)} <span class="text-sm truncate">{member.TestSiteName}</span>
ondragend={handleDragEnd}
class:opacity-50={draggedIndex === index}
>
<div class="card-body p-3 flex flex-row items-center gap-3">
<div class="cursor-move text-gray-400 hover:text-gray-600">
<GripVertical class="w-5 h-5" />
</div>
<div class="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center text-primary font-bold text-sm">
{member.Sequence}
</div>
<div class="flex-1">
<div class="flex items-center gap-2">
<span class="font-mono text-sm text-gray-600">{member.TestSiteCode}</span>
<span class="font-medium">{member.TestSiteName}</span>
</div> </div>
</div> </div>
<span class="badge badge-sm {getTestTypeBadge(member.TestType)}"> <span class="badge badge-xs {getTestTypeBadge(member.TestType)}">
{member.TestType} {member.TestType}
</span> </span>
<button <button
type="button" type="button"
class="btn btn-sm btn-ghost text-error" class="btn btn-xs btn-ghost text-error p-0 min-h-0 h-6 w-6"
onclick={() => removeMember(index)} onclick={() => removeMember(index)}
> >
<X class="w-4 h-4" /> <X class="w-3 h-3" />
</button> </button>
</div> </div>
</div>
{/each} {/each}
</div> </div>
{/if} {/if}
</div> </div>
</div> </div>
</div>

View File

@ -89,9 +89,9 @@
if (ref.Low !== null && ref.High !== null) { if (ref.Low !== null && ref.High !== null) {
rangeText = `${ref.Low} - ${ref.High}`; rangeText = `${ref.Low} - ${ref.High}`;
} else if (ref.Low !== null) { } else if (ref.Low !== null) {
rangeText = `${ref.Low}`; rangeText = `> ${ref.Low}`;
} else if (ref.High !== null) { } else if (ref.High !== null) {
rangeText = `${ref.High}`; rangeText = `< ${ref.High}`;
} else { } else {
rangeText = 'Not set'; rangeText = 'Not set';
} }

View File

@ -0,0 +1,43 @@
<script>
import { Microscope, Variable, Calculator, Box } from 'lucide-svelte';
/**
* @typedef {Object} Props
* @property {(type: string) => void} onselect - Selection handler
* @property {() => void} oncancel - Cancel handler
*/
/** @type {Props} */
let {
onselect = () => {},
oncancel = () => {}
} = $props();
const types = [
{ value: 'TEST', label: 'Single Test', icon: Microscope, color: 'text-primary', bg: 'bg-primary/10' },
{ value: 'PARAM', label: 'Parameter', icon: Variable, color: 'text-secondary', bg: 'bg-secondary/10' },
{ value: 'CALC', label: 'Calculated', icon: Calculator, color: 'text-accent', bg: 'bg-accent/10' },
{ value: 'GROUP', label: 'Panel', icon: Box, color: 'text-info', bg: 'bg-info/10' }
];
</script>
<div class="space-y-4">
<p class="text-center text-gray-600">Select test type to create</p>
<div class="grid grid-cols-2 gap-4">
{#each types as type}
<button
type="button"
class="card bg-base-100 border border-base-200 hover:border-primary hover:shadow-md transition-all p-6 flex flex-col items-center gap-3"
onclick={() => onselect(type.value)}
>
<div class="w-12 h-12 rounded-full {type.bg} flex items-center justify-center">
<svelte:component this={type.icon} class="w-6 h-6 {type.color}" />
</div>
<span class="font-medium">{type.label}</span>
</button>
{/each}
</div>
<div class="flex justify-center pt-2">
<button class="btn btn-ghost btn-sm" onclick={oncancel} type="button">Cancel</button>
</div>
</div>