mahdahar 22ee1ebfd1 feat: update organization pages and test modal
- Update organization discipline and site pages with new features
- Enhance TestFormModal component
- Refactor GroupMembersTab implementation
- Update API documentation
- Add organization API methods
2026-03-10 16:40:44 +07:00

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>