2026-02-20 13:51:54 +07:00
|
|
|
<script>
|
|
|
|
|
import { Plus, Trash2, ArrowUp, ArrowDown, Box } from 'lucide-svelte';
|
|
|
|
|
|
|
|
|
|
let { formData = $bindable(), tests = [], isDirty = $bindable(false) } = $props();
|
|
|
|
|
|
|
|
|
|
let availableTests = $state([]);
|
|
|
|
|
let selectedTestId = $state('');
|
|
|
|
|
let addMemberOpen = $state(false);
|
|
|
|
|
|
|
|
|
|
const members = $derived.by(() => {
|
2026-03-09 16:50:35 +07:00
|
|
|
const memberIds = formData.details.members?.map(m => Number(m.TestSiteID)) || [];
|
|
|
|
|
return tests.filter(t => memberIds.includes(Number(t.TestSiteID)))
|
|
|
|
|
.map(t => {
|
|
|
|
|
const memberObj = formData.details.members?.find(m => Number(m.TestSiteID) === Number(t.TestSiteID));
|
|
|
|
|
return { ...t, seq: memberObj?.Member || 0 };
|
|
|
|
|
})
|
|
|
|
|
.sort((a, b) => a.seq - b.seq);
|
2026-02-20 13:51:54 +07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const availableOptions = $derived.by(() => {
|
2026-03-09 16:50:35 +07:00
|
|
|
const memberIds = formData.details.members?.map(m => Number(m.TestSiteID)) || [];
|
2026-02-20 13:51:54 +07:00
|
|
|
return tests.filter(t =>
|
2026-03-09 16:50:35 +07:00
|
|
|
Number(t.TestSiteID) !== Number(formData.TestSiteID) &&
|
|
|
|
|
!memberIds.includes(Number(t.TestSiteID)) &&
|
2026-02-20 13:51:54 +07:00
|
|
|
t.IsActive !== '0' &&
|
|
|
|
|
t.IsActive !== 0
|
|
|
|
|
).map(t => ({
|
|
|
|
|
value: t.TestSiteID,
|
|
|
|
|
label: `${t.TestSiteCode} - ${t.TestSiteName}`,
|
|
|
|
|
data: t
|
|
|
|
|
}));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function handleFieldChange() {
|
|
|
|
|
isDirty = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function addMember() {
|
|
|
|
|
if (!selectedTestId) return;
|
|
|
|
|
|
2026-03-09 16:50:35 +07:00
|
|
|
const currentMembers = formData.details.members || [];
|
|
|
|
|
const newMember = {
|
|
|
|
|
TestSiteID: parseInt(selectedTestId),
|
|
|
|
|
Member: currentMembers.length + 1
|
|
|
|
|
};
|
|
|
|
|
formData.details.members = [...currentMembers, newMember];
|
2026-02-20 13:51:54 +07:00
|
|
|
selectedTestId = '';
|
|
|
|
|
handleFieldChange();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function removeMember(testId) {
|
2026-03-09 16:50:35 +07:00
|
|
|
const remainingMembers = formData.details.members?.filter(m => Number(m.TestSiteID) !== Number(testId)) || [];
|
|
|
|
|
// Re-sequence the remaining members
|
|
|
|
|
formData.details.members = remainingMembers.map((m, idx) => ({
|
|
|
|
|
...m,
|
|
|
|
|
Member: idx + 1
|
|
|
|
|
}));
|
2026-02-20 13:51:54 +07:00
|
|
|
handleFieldChange();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function moveMember(index, direction) {
|
|
|
|
|
const members = [...formData.details.members];
|
|
|
|
|
const newIndex = index + direction;
|
|
|
|
|
|
|
|
|
|
if (newIndex >= 0 && newIndex < members.length) {
|
2026-03-09 16:50:35 +07:00
|
|
|
// Swap the members
|
2026-02-20 13:51:54 +07:00
|
|
|
[members[index], members[newIndex]] = [members[newIndex], members[index]];
|
2026-03-09 16:50:35 +07:00
|
|
|
// Update sequence numbers
|
|
|
|
|
formData.details.members = members.map((m, idx) => ({
|
|
|
|
|
...m,
|
|
|
|
|
Member: idx + 1
|
|
|
|
|
}));
|
2026-02-20 13:51:54 +07:00
|
|
|
handleFieldChange();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<div class="space-y-6">
|
|
|
|
|
<h2 class="text-lg font-semibold text-gray-800">Group Members</h2>
|
|
|
|
|
|
|
|
|
|
<div class="alert alert-info text-sm">
|
|
|
|
|
<Box class="w-4 h-4" />
|
|
|
|
|
<div>
|
|
|
|
|
<strong>Panel Members:</strong> Add tests, parameters, or calculated values to this panel. Order matters for display on reports.
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="space-y-4">
|
|
|
|
|
<h3 class="text-sm font-medium text-gray-600 uppercase tracking-wide">Current Members ({members.length})</h3>
|
|
|
|
|
|
|
|
|
|
{#if members.length === 0}
|
|
|
|
|
<div class="text-center py-8 bg-base-200 rounded-lg">
|
|
|
|
|
<Box class="w-12 h-12 mx-auto text-gray-400 mb-2" />
|
|
|
|
|
<p class="text-sm text-gray-500">No members added yet</p>
|
|
|
|
|
<p class="text-xs text-gray-400">Add tests to create this panel</p>
|
|
|
|
|
</div>
|
|
|
|
|
{:else}
|
|
|
|
|
<div class="overflow-x-auto border border-base-200 rounded-lg">
|
|
|
|
|
<table class="table table-sm table-compact">
|
|
|
|
|
<thead>
|
|
|
|
|
<tr class="bg-base-200">
|
|
|
|
|
<th class="w-12 text-center">#</th>
|
|
|
|
|
<th class="w-24">Code</th>
|
|
|
|
|
<th>Name</th>
|
|
|
|
|
<th class="w-20">Type</th>
|
|
|
|
|
<th class="w-32 text-center">Actions</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
{#each members as member, idx (member.TestSiteID)}
|
|
|
|
|
<tr class="hover:bg-base-100">
|
|
|
|
|
<td class="text-center text-gray-500">{idx + 1}</td>
|
|
|
|
|
<td class="font-mono text-sm">{member.TestSiteCode}</td>
|
|
|
|
|
<td>{member.TestSiteName}</td>
|
|
|
|
|
<td>
|
|
|
|
|
<span class="badge badge-xs badge-ghost">{member.TestType}</span>
|
|
|
|
|
</td>
|
|
|
|
|
<td>
|
|
|
|
|
<div class="flex justify-center gap-1">
|
|
|
|
|
<button
|
|
|
|
|
class="btn btn-ghost btn-xs"
|
|
|
|
|
onclick={() => moveMember(idx, -1)}
|
|
|
|
|
disabled={idx === 0}
|
|
|
|
|
title="Move Up"
|
|
|
|
|
>
|
|
|
|
|
<ArrowUp class="w-3 h-3" />
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
class="btn btn-ghost btn-xs"
|
|
|
|
|
onclick={() => moveMember(idx, 1)}
|
|
|
|
|
disabled={idx === members.length - 1}
|
|
|
|
|
title="Move Down"
|
|
|
|
|
>
|
|
|
|
|
<ArrowDown class="w-3 h-3" />
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
class="btn btn-ghost btn-xs text-error"
|
|
|
|
|
onclick={() => removeMember(member.TestSiteID)}
|
|
|
|
|
title="Remove Member"
|
|
|
|
|
>
|
|
|
|
|
<Trash2 class="w-3 h-3" />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
{/each}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="space-y-4">
|
|
|
|
|
<h3 class="text-sm font-medium text-gray-600 uppercase tracking-wide">Add Member</h3>
|
|
|
|
|
|
|
|
|
|
{#if availableOptions.length === 0}
|
|
|
|
|
<div class="alert alert-warning text-sm">
|
|
|
|
|
<span>No available tests to add. All tests are either already members or inactive.</span>
|
|
|
|
|
</div>
|
|
|
|
|
{:else}
|
|
|
|
|
<div class="flex gap-2">
|
|
|
|
|
<select
|
|
|
|
|
class="select select-sm select-bordered flex-1"
|
|
|
|
|
bind:value={selectedTestId}
|
|
|
|
|
>
|
|
|
|
|
<option value="">Select a test to add...</option>
|
|
|
|
|
{#each availableOptions as opt (opt.value)}
|
|
|
|
|
<option value={opt.value}>{opt.label}</option>
|
|
|
|
|
{/each}
|
|
|
|
|
</select>
|
|
|
|
|
<button
|
|
|
|
|
class="btn btn-sm btn-primary"
|
|
|
|
|
onclick={addMember}
|
|
|
|
|
disabled={!selectedTestId}
|
|
|
|
|
>
|
|
|
|
|
<Plus class="w-4 h-4 mr-1" />
|
|
|
|
|
Add
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="text-xs text-gray-500 space-y-1">
|
|
|
|
|
<p><strong>Tip:</strong> Members will display on reports in the order shown above.</p>
|
|
|
|
|
<p><strong>Note:</strong> Panels cannot contain themselves or other panels (circular references).</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|