continue ordertest create

This commit is contained in:
faiztyanirh 2026-04-27 12:15:15 +07:00
parent ee04b2e8d5
commit 47873b172a
4 changed files with 120 additions and 285 deletions

View File

@ -6,29 +6,11 @@ import { cleanEmptyStrings } from "$lib/utils/cleanEmptyStrings";
export const orderTestSchema = z.object({
Tests: z.array(
z.object({
id: z.number(),
TestSiteCode: z.string(),
TestSiteName: z.string()
})
)
.min(1, " "),
})
.superRefine((data, ctx) => {
if (!data.Tests) return;
const values = data.Tests.map((m) => m.testCode).filter(Boolean);
const duplicates = values.filter((v, i) => values.indexOf(v) !== i);
if (duplicates.length) {
const uniqueDuplicates = [...new Set(duplicates)];
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Duplicate test : ${uniqueDuplicates.join(', ')}`,
path: ['Tests']
});
}
});
export const orderTestInitialForm = {
InternalOID: '',
@ -145,7 +127,7 @@ export const orderTestFormFields = [
key: "Tests",
required: true,
withLabel: false,
type: "tests2",
type: "tests",
optionsEndpoint: `${API.BASE_URL}${API.TEST}`,
otherEndpoint: `${API.BASE_URL}${API.DISCIPLINE}`,
valueKey: 'TestSiteID',

View File

@ -41,7 +41,7 @@
// toast.error(errorMessages)
// }
}
$inspect(formState.form)
$inspect(formState.errors)
const primaryAction = $derived({
label: 'Save',
onClick: handleSave,

View File

@ -74,8 +74,6 @@
patientStore.pending = null;
}
})
$inspect(orderStore)
</script>
{#snippet searchParamSnippet()}

View File

@ -1,31 +1,16 @@
<script>
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
import * as Select from '$lib/components/ui/select/index.js';
import * as Popover from "$lib/components/ui/popover/index.js";
import { Toggle } from '$lib/components/ui/toggle/index.js';
import { Button } from '$lib/components/ui/button/index.js';
import { Input } from '$lib/components/ui/input/index.js';
import { Label } from '$lib/components/ui/label/index.js';
import { Spinner } from '$lib/components/ui/spinner/index.js';
import ReusableCalendarTimepicker from "$lib/components/reusable/reusable-calendar-timepicker.svelte";
import { Badge } from '$lib/components/ui/badge/index.js';
import * as InputGroup from '$lib/components/ui/input-group/index.js';
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
import { Textarea } from '$lib/components/ui/textarea/index.js';
import MoveLeftIcon from '@lucide/svelte/icons/move-left';
import MoveRightIcon from '@lucide/svelte/icons/move-right';
import BrushCleaningIcon from '@lucide/svelte/icons/brush-cleaning';
import DeleteIcon from '@lucide/svelte/icons/delete';
import Trash2Icon from '@lucide/svelte/icons/trash-2';
import PlusIcon from '@lucide/svelte/icons/plus';
import CornerDownLeftIcon from '@lucide/svelte/icons/corner-down-left';
import CheckIcon from "@lucide/svelte/icons/check";
import XIcon from "@lucide/svelte/icons/x";
import ReusableUpload from "$lib/components/reusable/reusable-upload.svelte";
import * as Table from "$lib/components/ui/table/index.js";
import * as Tooltip from "$lib/components/ui/tooltip/index.js";
import * as Card from "$lib/components/ui/card/index.js";
import { ScrollArea } from "$lib/components/ui/scroll-area/index.js";
import * as Collapsible from "$lib/components/ui/collapsible/index.js";
import { Checkbox } from "$lib/components/ui/checkbox/index.js";
import { untrack } from 'svelte';
@ -38,17 +23,15 @@
} = $props();
let searchQuery = $state({});
const leftGroups = $derived([formFields[0]]);
const rightGroups = $derived(formFields.slice(1));
let selectedTest = $state(null);
let selectedTests2 = $state([]);
let selectedTests = $state([]);
let pendingTests = $state([]);
let selectedCodes = $derived(
new Set((formState.form.Tests ?? []).map(t => t.TestSiteCode))
);
let discipline = $state([]);
let selectedDiscipline = $state(null);
let searchText = $state('');
let disciplineSearch = $state('');
const leftGroups = $derived([formFields[0]]);
const rightGroups = $derived(formFields.slice(1));
let searchedTests = $derived.by(() => {
if (!searchText.trim()) return filteredTests;
@ -70,125 +53,10 @@
return result;
});
function toggleTest(test) {
const exists = selectedTests2.find(t => t.rawItem.TestSiteID === test.rawItem.TestSiteID);
if (exists) {
selectedTests2 = selectedTests2.filter(t => t.rawItem.TestSiteID !== test.rawItem.TestSiteID);
} else {
selectedTests2 = [...selectedTests2, test];
}
}
function isSelected(test) {
return selectedTests2.some(t => t.rawItem.TestSiteID === test.rawItem.TestSiteID);
}
function addSelected() {
for (const test of selectedTests2) {
const alreadyAdded = pendingTests.some(t => t.rawItem.TestSiteID === test.rawItem.TestSiteID);
if (!alreadyAdded) {
pendingTests = [...pendingTests, test];
}
}
selectedTests2 = [];
}
function isPending(test) {
return pendingTests.some(t => t.rawItem.TestSiteID === test.rawItem.TestSiteID);
}
function removeTest2(test) {
pendingTests = pendingTests.filter(t => t.rawItem.TestSiteID !== test.rawItem.TestSiteID);
selectedTests2 = selectedTests2.filter(t => t.rawItem.TestSiteID !== test.rawItem.TestSiteID);
}
function getFilteredOptions(key) {
const query = searchQuery[key] || '';
if (!query) return formState.selectOptions?.[key] ?? [];
return (formState.selectOptions?.[key] ?? []).filter((opt) =>
opt.label.toLowerCase().includes(query.toLowerCase())
);
}
async function initializeDefaultValues() {
for (const group of formFields) {
for (const row of group.rows) {
for (const col of row.columns) {
if (col.type === 'group') {
for (const child of col.columns) {
await handleDefaultValue(child);
}
} else {
await handleDefaultValue(col);
}
}
}
}
}
async function handleDefaultValue(field) {
if (!field.defaultValue || !field.optionsEndpoint) return;
await formState.fetchOptions(field, formState.form);
if (!formState.form[field.key]) {
formState.form[field.key] = field.defaultValue;
}
}
function addTest() {
const testCode = selectedTest?.value;
const testName = selectedTest?.rawItem?.TestSiteName;
const testSiteID = selectedTest?.rawItem?.TestSiteID;
if (!testCode) return;
const exists = formState.form.Tests?.some(t => t.testCode === testCode);
if (exists) {
formState.errors.Tests = 'Test already added';
return;
}
formState.form.Tests = [
...(formState.form.Tests ?? []),
{ id: Date.now(), TestSiteCode: testCode, TestSiteName: testName, TestSiteID: testSiteID }
];
selectedTest = null;
formState.validateField?.('Tests', formState.form.Tests, false);
}
function removeTest(id) {
formState.form.Tests = formState.form.Tests.filter((t) => t.id !== id);
formState.validateField?.('Tests', formState.form.Tests, false);
}
async function fetchDiscipline(endpoint) {
if (!endpoint || discipline.length > 0) return;
const res = await fetch(endpoint);
const response = await res.json();
discipline = await response.data;
}
$effect(() => {
initializeDefaultValues();
});
$effect(() => {
untrack(() => {
for (const group of formFields) {
for (const row of group.rows) {
for (const col of row.columns) {
if (col.type === 'tests2' && col.otherEndpoint) {
fetchDiscipline(col.otherEndpoint);
}
if (col.type === "tests2" && col.optionsEndpoint) {
formState.fetchOptions(col, formState.form);
}
}
}
}
});
let filteredDiscipline = $derived.by(() => {
if (!disciplineSearch.trim()) return discipline;
const query = disciplineSearch.toLowerCase().trim();
return discipline.filter(d => d.DisciplineName.toLowerCase().includes(query));
});
let testsByDiscipline = $derived.by(() => {
@ -209,7 +77,75 @@
Object.entries(testsByDiscipline).filter(([disciplineId]) => disciplineId === selectedDiscipline)
);
});
$inspect(pendingTests)
function toggleTest(test) {
const exists = selectedTests.find(t => t.rawItem.TestSiteID === test.rawItem.TestSiteID);
if (exists) {
selectedTests = selectedTests.filter(t => t.rawItem.TestSiteID !== test.rawItem.TestSiteID);
} else {
selectedTests = [...selectedTests, test];
}
}
function isSelected(test) {
return selectedTests.some(t => t.rawItem.TestSiteID === test.rawItem.TestSiteID);
}
function addSelected() {
for (const test of selectedTests) {
const alreadyAdded = pendingTests.some(t => t.rawItem.TestSiteID === test.rawItem.TestSiteID);
if (!alreadyAdded) {
pendingTests = [...pendingTests, test];
formState.form.Tests = [...formState.form.Tests, test.rawItem];
}
}
selectedTests = [];
formState.validateField('Tests', formState.form.Tests, false);
}
function isPending(test) {
return pendingTests.some(t => t.rawItem.TestSiteID === test.rawItem.TestSiteID);
}
function removeTest(test) {
pendingTests = pendingTests.filter(t => t.rawItem.TestSiteID !== test.rawItem.TestSiteID);
selectedTests = selectedTests.filter(t => t.rawItem.TestSiteID !== test.rawItem.TestSiteID);
formState.form.Tests = formState.form.Tests.filter(t => t.TestSiteID !== test.rawItem.TestSiteID);
formState.validateField('Tests', formState.form.Tests, false);
}
function getFilteredOptions(key) {
const query = searchQuery[key] || '';
if (!query) return formState.selectOptions?.[key] ?? [];
return (formState.selectOptions?.[key] ?? []).filter((opt) =>
opt.label.toLowerCase().includes(query.toLowerCase())
);
}
async function fetchDiscipline(endpoint) {
if (!endpoint || discipline.length > 0) return;
const res = await fetch(endpoint);
const response = await res.json();
discipline = await response.data;
}
$effect(() => {
untrack(() => {
for (const group of formFields) {
for (const row of group.rows) {
for (const col of row.columns) {
if (col.type === 'tests' && col.otherEndpoint) {
fetchDiscipline(col.otherEndpoint);
}
if (col.type === "tests" && col.optionsEndpoint) {
formState.fetchOptions(col, formState.form);
}
}
}
}
});
});
</script>
{#snippet Fieldset({
@ -328,116 +264,11 @@
<div class="flex flex-col w-full">
<ReusableUpload attachments={formState.form[key]} />
</div>
{:else if type === "tests"}
{@const selectedLabel = selectedTest?.label || 'Choose'}
{@const filteredOptions = getFilteredOptions(key)}
<div class="flex gap-2 w-full">
<div class="flex-1">
<Select.Root
type="single"
onValueChange={(val) => {
selectedTest = filteredOptions.find((opt) => opt.value === val) ?? null;
}}
onOpenChange={(open) => {
if (open && optionsEndpoint) {
formState.fetchOptions?.(
{ key, optionsEndpoint, valueKey, labelKey },
formState.form
);
}
}}
>
<Select.Trigger class="w-full">
{selectedLabel}
</Select.Trigger>
<Select.Content>
<div class="p-2">
<input
type="text"
placeholder="Search..."
class="w-full border rounded px-2 py-1 text-sm"
bind:value={searchQuery[key]}
/>
</div>
{#if formState.loadingOptions?.[key]}
<Select.Item disabled value="loading">Loading...</Select.Item>
{:else}
{#if !required}
<Select.Item value="">- None -</Select.Item>
{/if}
{#each filteredOptions as option}
<Select.Item
value={option.value}
disabled={selectedCodes.has(option.value)}
>
{option.label}
</Select.Item>
{/each}
{/if}
</Select.Content>
</Select.Root>
</div>
<Button onclick={addTest}>
<PlusIcon class="size-4" />
Add Test
</Button>
</div>
<Table.Root class="mt-3">
<Table.Header>
<Table.Row class="hover:bg-transparent">
<Table.Head class="w-[40px]">No</Table.Head>
<Table.Head>Test Code</Table.Head>
<Table.Head>Test Name</Table.Head>
<Table.Head class="w-[40px]"></Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#if formState.form.Tests.length === 0}
<Table.Row>
<Table.Cell colspan={4} class="text-center text-muted-foreground py-6">
No data. Add a test from Search Test above.
</Table.Cell>
</Table.Row>
{:else}
{#each formState.form.Tests as test, index (test.id)}
<Table.Row
class="cursor-pointer hover:bg-muted/50"
>
<Table.Cell>{index + 1}</Table.Cell>
<Table.Cell>{test.TestSiteCode}</Table.Cell>
<Table.Cell>{test.TestSiteName}</Table.Cell>
<Table.Cell>
<div class="flex gap-1">
<Tooltip.Provider>
<Tooltip.Root>
<Tooltip.Trigger>
<Button
size="icon"
variant="outline"
class="h-7 w-7 cursor-pointer"
onclick={() => removeTest(test.id)}
>
<Trash2Icon class="h-3.5 w-3.5" />
</Button>
</Tooltip.Trigger>
<Tooltip.Content>
<p>Delete</p>
</Tooltip.Content>
</Tooltip.Root>
</Tooltip.Provider>
</div>
</Table.Cell>
</Table.Row>
{/each}
{/if}
</Table.Body>
</Table.Root>
{:else if type === "tests2"}
{@const filteredOptions = getFilteredOptions(key)}
{:else if type === "tests"}
<div class="flex flex-1 h-full min-h-0 w-full gap-2">
<div class="flex flex-1 flex-col gap-2 overflow-hidden min-h-0 h-full w-full">
<div class="shrink-0 h-18 p-1 flex gap-2 flex flex-col">
<div class="shrink-0 h-7 border-b-2 flex justify-between items-center">
<div class="shrink-0 h-18 flex gap-2 flex flex-col">
<div class="h-7 border-b-2 flex justify-between items-center">
<p class="text-xs font-medium text-muted-foreground uppercase tracking-wide">Test list</p>
</div>
<div class="flex gap-2">
@ -446,13 +277,22 @@
onValueChange={(val) => selectedDiscipline = val || null}
>
<Select.Trigger class="w-full truncate">
{discipline.find(d => d.id === selectedDiscipline)?.name || 'All Disciplines'}
{discipline.find(d => d.DisciplineID === selectedDiscipline)?.DisciplineName || 'All Disciplines'}
</Select.Trigger>
<Select.Content>
<div class="p-2">
<input
type="text"
placeholder="Search..."
class="w-full border rounded px-2 py-1 text-sm"
bind:value={disciplineSearch}
/>
</div>
<Select.Item value="">- All -</Select.Item>
{#each discipline as disc}
{#each filteredDiscipline as disc}
<Select.Item value={disc.DisciplineID}>{disc.DisciplineName}</Select.Item>
{/each}
</Select.Content>
</Select.Root>
<Input
@ -477,10 +317,17 @@
<Collapsible.Content>
<div class="py-1 px-1">
{#each tests as test}
<div class="flex items-center gap-3 px-3 py-2 rounded-md transition-colors {isPending(test) ? 'opacity-40 pointer-events-none' : ''}">
<div
role="button"
tabindex="0"
class="flex items-center gap-3 px-3 py-2 rounded-md cursor-pointer transition-colors {isPending(test) ? 'opacity-40 pointer-events-none' : ''} hover:bg-muted"
onclick={() => toggleTest(test)}
onkeydown={(e) => e.key === 'Enter' || e.key === ' ' ? toggleTest(test) : null}
>
<Checkbox
checked={isSelected(test)}
onCheckedChange={() => toggleTest(test)}
onclick={(e) => e.stopPropagation()}
/>
<span class="text-xs font-semibold text-primary w-14 shrink-0">{test.rawItem.TestSiteCode}</span>
<Label class="text-sm cursor-pointer">{test.rawItem.TestSiteName}</Label>
@ -492,12 +339,12 @@
{/each}
</div>
<div class="shrink-0 p-2 border-t border-b flex items-center justify-between">
<span class="text-sm">{selectedTests2.length} selected</span>
<span class="text-sm">{selectedTests.length} selected</span>
<Button
size="sm"
class="px-4 py-2 rounded"
onclick={addSelected}
disabled={selectedTests2.length === 0}
disabled={selectedTests.length === 0}
>
Add selected
</Button>
@ -523,14 +370,22 @@
</InputGroup.Addon>
<InputGroup.Input readonly />
<InputGroup.Addon align="inline-end">
<InputGroup.Button
aria-label="Delete"
title="Delete"
size="icon-xs"
onclick={() => removeTest2(test)}
>
<Trash2Icon />
</InputGroup.Button>
<Tooltip.Provider>
<Tooltip.Root>
<Tooltip.Trigger>
<InputGroup.Button
size="icon-xs"
onclick={() => removeTest(test)}
>
<Trash2Icon />
</InputGroup.Button>
</Tooltip.Trigger>
<Tooltip.Content>
<p>Delete</p>
</Tooltip.Content>
</Tooltip.Root>
</Tooltip.Provider>
</InputGroup.Addon>
</InputGroup.Root>
{/each}