208 lines
6.8 KiB
Svelte
Raw Normal View History

<script>
import { Search, Plus, X, GripVertical, Microscope } from 'lucide-svelte';
/**
* @typedef {Object} Props
* @property {Object} formData - Form data object
* @property {Array} availableTests - Available tests for selection
* @property {(formData: Object) => void} onupdateFormData - Update handler
*/
/** @type {Props} */
let {
formData = $bindable({}),
availableTests = [],
onupdateFormData = () => {}
} = $props();
let searchQuery = $state('');
let draggedIndex = $state(-1);
// Filter out the current test and already selected tests
let filteredTests = $derived(
availableTests.filter(test =>
test.TestSiteID !== formData.TestSiteID &&
!formData.groupMembers?.some(member => member.TestSiteID === test.TestSiteID) &&
(test.TestSiteName.toLowerCase().includes(searchQuery.toLowerCase()) ||
test.TestSiteCode.toLowerCase().includes(searchQuery.toLowerCase()))
)
);
function addMember(test) {
const newMembers = [
...(formData.groupMembers || []),
{
TestSiteID: test.TestSiteID,
TestSiteCode: test.TestSiteCode,
TestSiteName: test.TestSiteName,
TestType: test.TestType,
Sequence: (formData.groupMembers?.length || 0) + 1
}
];
onupdateFormData({ ...formData, groupMembers: newMembers });
searchQuery = '';
}
function removeMember(index) {
const newMembers = formData.groupMembers.filter((_, i) => i !== index);
// Reorder sequences
newMembers.forEach((member, i) => {
member.Sequence = i + 1;
});
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) {
const badges = {
'TEST': 'badge-primary',
'PARAM': 'badge-secondary',
'CALC': 'badge-accent',
'GROUP': 'badge-info',
'TITLE': 'badge-ghost'
};
return badges[testType] || 'badge-ghost';
}
</script>
<div class="space-y-3">
<!-- Add Member Section -->
<div class="bg-base-100 rounded-lg border border-base-200 p-4">
<div class="flex items-center gap-2 mb-4">
<Microscope class="w-5 h-5 text-primary" />
<h3 class="font-semibold">Add Group Members</h3>
</div>
<div class="form-control">
<div class="relative">
<Search class="w-5 h-5 absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
<input
type="text"
class="input input-bordered w-full pl-10"
bind:value={searchQuery}
placeholder="Search by test name or code..."
/>
</div>
</div>
{#if searchQuery}
<div class="mt-2 max-h-60 overflow-y-auto border border-base-200 rounded-lg">
{#if filteredTests.length === 0}
<div class="p-4 text-center text-gray-500">
No tests found matching "{searchQuery}"
</div>
{:else}
{#each filteredTests as test}
<button
type="button"
class="w-full text-left p-3 hover:bg-base-200 flex items-center justify-between group border-b border-base-200 last:border-0"
onclick={() => addMember(test)}
>
<div class="flex items-center gap-3">
<span class="font-mono text-sm text-gray-600">{test.TestSiteCode}</span>
<span class="font-medium">{test.TestSiteName}</span>
<span class="badge badge-sm {getTestTypeBadge(test.TestType)}">
{test.TestType}
</span>
</div>
<Plus class="w-4 h-4 text-gray-400 group-hover:text-primary" />
</button>
{/each}
{/if}
</div>
{/if}
</div>
<!-- Selected Members -->
<div class="bg-base-100 rounded-lg border border-base-200 p-4">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-2">
<span class="font-semibold">Group Members</span>
<span class="badge badge-sm badge-primary">{formData.groupMembers?.length || 0}</span>
</div>
{#if formData.groupMembers?.length > 0}
<span class="text-sm text-gray-500">Drag to reorder</span>
{/if}
</div>
{#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">
<Microscope class="w-12 h-12 mx-auto text-gray-400 mb-2" />
<p class="text-gray-500">No members added yet</p>
<p class="text-sm text-gray-400 mt-1">Search and add tests above</p>
</div>
{:else}
<div class="space-y-2">
{#each formData.groupMembers as member, index (member.TestSiteID)}
<div
class="card bg-base-100 border border-base-200 hover:border-primary/50 transition-colors"
draggable="true"
ondragstart={() => handleDragStart(index)}
ondragover={(e) => handleDragOver(e, index)}
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>
<span class="badge badge-sm {getTestTypeBadge(member.TestType)}">
{member.TestType}
</span>
<button
type="button"
class="btn btn-sm btn-ghost text-error"
onclick={() => removeMember(index)}
>
<X class="w-4 h-4" />
</button>
</div>
</div>
{/each}
</div>
{/if}
</div>
</div>