- Update organization discipline and site pages with new features - Enhance TestFormModal component - Refactor GroupMembersTab implementation - Update API documentation - Add organization API methods
202 lines
7.6 KiB
Svelte
202 lines
7.6 KiB
Svelte
<script>
|
|
import { Plus, Trash2, Box, Search } from 'lucide-svelte';
|
|
|
|
let { formData = $bindable(), tests = [], isDirty = $bindable(false) } = $props();
|
|
|
|
let searchQuery = $state('');
|
|
|
|
const members = $derived.by(() => {
|
|
const testdefgrp = formData.testdefgrp || formData.details?.members || [];
|
|
return testdefgrp
|
|
.map(m => ({
|
|
TestSiteID: m.TestSiteID,
|
|
TestSiteCode: m.TestSiteCode,
|
|
TestSiteName: m.TestSiteName,
|
|
TestType: m.TestType,
|
|
TestTypeLabel: m.TestTypeLabel,
|
|
SeqScr: m.SeqScr || m.Member || 0
|
|
}))
|
|
.sort((a, b) => parseInt(a.SeqScr) - parseInt(b.SeqScr));
|
|
});
|
|
|
|
const availableTests = $derived.by(() => {
|
|
const currentMembers = formData.testdefgrp || formData.details?.members || [];
|
|
const memberIds = new Set(currentMembers.map(m => Number(m.TestSiteID)));
|
|
|
|
let filtered = tests.filter(t =>
|
|
Number(t.TestSiteID) !== Number(formData.TestSiteID) &&
|
|
!memberIds.has(Number(t.TestSiteID)) &&
|
|
t.IsActive !== '0' &&
|
|
t.IsActive !== 0
|
|
);
|
|
|
|
if (searchQuery.trim()) {
|
|
const query = searchQuery.toLowerCase();
|
|
filtered = filtered.filter(t =>
|
|
t.TestSiteCode?.toLowerCase().includes(query) ||
|
|
t.TestSiteName?.toLowerCase().includes(query)
|
|
);
|
|
}
|
|
|
|
return filtered.sort((a, b) => parseInt(a.SeqScr || 0) - parseInt(b.SeqScr || 0));
|
|
});
|
|
|
|
function handleFieldChange() {
|
|
isDirty = true;
|
|
}
|
|
|
|
function addMember(test) {
|
|
const currentMembers = formData.testdefgrp || formData.details?.members || [];
|
|
const newMember = {
|
|
TestSiteID: test.TestSiteID,
|
|
TestSiteCode: test.TestSiteCode || '',
|
|
TestSiteName: test.TestSiteName || '',
|
|
TestType: test.TestType || 'TEST',
|
|
TestTypeLabel: test.TestTypeLabel || 'Test',
|
|
SeqScr: test.SeqScr || '0'
|
|
};
|
|
|
|
if (formData.hasOwnProperty('testdefgrp')) {
|
|
formData.testdefgrp = [...currentMembers, newMember];
|
|
} else {
|
|
formData.details.members = [...currentMembers, newMember];
|
|
}
|
|
|
|
handleFieldChange();
|
|
}
|
|
|
|
function removeMember(testId) {
|
|
const isNewApi = formData.hasOwnProperty('testdefgrp');
|
|
const currentMembers = formData.testdefgrp || formData.details?.members || [];
|
|
const remainingMembers = currentMembers.filter(m => Number(m.TestSiteID) !== Number(testId));
|
|
|
|
if (isNewApi) {
|
|
formData.testdefgrp = remainingMembers;
|
|
} else {
|
|
formData.details.members = remainingMembers;
|
|
}
|
|
handleFieldChange();
|
|
}
|
|
</script>
|
|
|
|
<div class="space-y-4">
|
|
<div class="flex items-center justify-between">
|
|
<h2 class="text-lg font-semibold text-gray-800">Group Members</h2>
|
|
<span class="badge badge-sm badge-ghost">{members.length} selected</span>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 h-[500px] overflow-hidden">
|
|
<!-- Left Column: Available Tests -->
|
|
<div class="flex flex-col border border-base-300 rounded-lg bg-base-100 overflow-hidden">
|
|
<div class="p-3 border-b border-base-300 bg-base-200 rounded-t-lg shrink-0">
|
|
<h3 class="text-sm font-medium text-gray-700 mb-2">Available Tests</h3>
|
|
<label class="input input-sm input-bordered flex items-center gap-2 w-full">
|
|
<Search class="w-4 h-4 text-gray-400" />
|
|
<input
|
|
type="text"
|
|
class="grow bg-transparent outline-none text-sm"
|
|
placeholder="Search by code or name..."
|
|
bind:value={searchQuery}
|
|
/>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="flex-1 overflow-y-auto p-2 space-y-1 min-h-0">
|
|
{#if availableTests.length === 0}
|
|
<div class="text-center py-8 text-gray-500">
|
|
<Box class="w-10 h-10 mx-auto mb-2 opacity-50" />
|
|
<p class="text-sm">No tests available</p>
|
|
<p class="text-xs opacity-70">
|
|
{searchQuery ? 'Try a different search term' : 'All tests are already added'}
|
|
</p>
|
|
</div>
|
|
{:else}
|
|
{#each availableTests as test (test.TestSiteID)}
|
|
<div class="flex items-center justify-between p-2 hover:bg-base-200 rounded-md group">
|
|
<div class="flex-1 min-w-0">
|
|
<div class="flex items-center gap-2">
|
|
<span class="font-mono text-xs text-gray-500 w-8">{test.SeqScr || '-'}</span>
|
|
<span class="font-mono text-sm font-medium truncate">{test.TestSiteCode}</span>
|
|
<span class="badge badge-xs badge-ghost">{test.TestType}</span>
|
|
</div>
|
|
<p class="text-xs text-gray-600 truncate pl-10">{test.TestSiteName}</p>
|
|
</div>
|
|
<button
|
|
class="btn btn-ghost btn-xs opacity-0 group-hover:opacity-100 transition-opacity"
|
|
onclick={() => addMember(test)}
|
|
title="Add to group"
|
|
>
|
|
<Plus class="w-4 h-4 text-primary" />
|
|
</button>
|
|
</div>
|
|
{/each}
|
|
{/if}
|
|
</div>
|
|
|
|
<div class="p-2 border-t border-base-300 text-xs text-gray-500 text-center shrink-0">
|
|
{availableTests.length} tests available
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right Column: Selected Members -->
|
|
<div class="flex flex-col border border-base-300 rounded-lg bg-base-100 overflow-hidden">
|
|
<div class="p-3 border-b border-base-300 bg-base-200 rounded-t-lg shrink-0">
|
|
<h3 class="text-sm font-medium text-gray-700">Selected Members</h3>
|
|
</div>
|
|
|
|
<div class="flex-1 overflow-y-auto min-h-0">
|
|
{#if members.length === 0}
|
|
<div class="flex flex-col items-center justify-center h-full text-gray-500 py-8">
|
|
<Box class="w-12 h-12 mb-3 opacity-50" />
|
|
<p class="text-sm font-medium">No members selected</p>
|
|
<p class="text-xs opacity-70 mt-1">Click the + button on available tests to add them</p>
|
|
</div>
|
|
{:else}
|
|
<table class="table table-sm w-full">
|
|
<thead class="sticky top-0 bg-base-200">
|
|
<tr>
|
|
<th class="w-12 text-center text-xs">Seq</th>
|
|
<th class="w-20 text-xs">Code</th>
|
|
<th class="text-xs">Name</th>
|
|
<th class="w-16 text-xs">Type</th>
|
|
<th class="w-10 text-center text-xs"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{#each members as member (member.TestSiteID)}
|
|
<tr class="hover:bg-base-200">
|
|
<td class="text-center font-mono text-xs text-gray-600">{member.SeqScr}</td>
|
|
<td class="font-mono text-xs">{member.TestSiteCode}</td>
|
|
<td class="text-xs truncate max-w-[150px]" title={member.TestSiteName}>
|
|
{member.TestSiteName}
|
|
</td>
|
|
<td>
|
|
<span class="badge badge-xs badge-ghost">{member.TestType}</span>
|
|
</td>
|
|
<td class="text-center">
|
|
<button
|
|
class="btn btn-ghost btn-xs text-error p-0 min-h-0 h-auto"
|
|
onclick={() => removeMember(member.TestSiteID)}
|
|
title="Remove"
|
|
>
|
|
<Trash2 class="w-3 h-3" />
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
{/each}
|
|
</tbody>
|
|
</table>
|
|
{/if}
|
|
</div>
|
|
|
|
<div class="p-2 border-t border-base-300 text-xs text-gray-500 shrink-0">
|
|
<p>Ordered by SeqScr value</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="text-xs text-gray-500">
|
|
<p><strong>Tip:</strong> Members are automatically ordered by their sequence number. Click + to add, trash icon to remove.</p>
|
|
</div>
|
|
</div>
|