169 lines
5.6 KiB
Svelte
169 lines
5.6 KiB
Svelte
|
|
<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(() => {
|
||
|
|
return tests.filter(t => formData.details.members?.includes(t.TestSiteID))
|
||
|
|
.map(t => ({ ...t, seq: formData.details.members.indexOf(t.TestSiteID) }));
|
||
|
|
});
|
||
|
|
|
||
|
|
const availableOptions = $derived.by(() => {
|
||
|
|
return tests.filter(t =>
|
||
|
|
t.TestSiteID !== formData.TestSiteID &&
|
||
|
|
!formData.details.members?.includes(t.TestSiteID) &&
|
||
|
|
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;
|
||
|
|
|
||
|
|
const newMembers = [...(formData.details.members || []), parseInt(selectedTestId)];
|
||
|
|
formData.details.members = newMembers;
|
||
|
|
selectedTestId = '';
|
||
|
|
handleFieldChange();
|
||
|
|
}
|
||
|
|
|
||
|
|
function removeMember(testId) {
|
||
|
|
const newMembers = formData.details.members?.filter(id => id !== testId) || [];
|
||
|
|
formData.details.members = newMembers;
|
||
|
|
handleFieldChange();
|
||
|
|
}
|
||
|
|
|
||
|
|
function moveMember(index, direction) {
|
||
|
|
const members = [...formData.details.members];
|
||
|
|
const newIndex = index + direction;
|
||
|
|
|
||
|
|
if (newIndex >= 0 && newIndex < members.length) {
|
||
|
|
[members[index], members[newIndex]] = [members[newIndex], members[index]];
|
||
|
|
formData.details.members = members;
|
||
|
|
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>
|