continue ordertest

This commit is contained in:
faiztyanirh 2026-04-24 17:29:17 +07:00
parent e236a20527
commit d4becb0a12
9 changed files with 373 additions and 42 deletions

16
package-lock.json generated
View File

@ -17,14 +17,14 @@
"devDependencies": {
"@eslint/compat": "^1.4.0",
"@eslint/js": "^9.39.1",
"@internationalized/date": "^3.10.1",
"@internationalized/date": "^3.12.1",
"@lucide/svelte": "^0.561.0",
"@sveltejs/adapter-auto": "^7.0.0",
"@sveltejs/kit": "^2.49.1",
"@sveltejs/vite-plugin-svelte": "^6.2.1",
"@tanstack/table-core": "^8.21.3",
"@types/node": "^22",
"bits-ui": "^2.15.4",
"bits-ui": "^2.18.0",
"clsx": "^2.1.1",
"eslint": "^9.39.1",
"eslint-config-prettier": "^10.1.8",
@ -715,9 +715,9 @@
}
},
"node_modules/@internationalized/date": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.10.1.tgz",
"integrity": "sha512-oJrXtQiAXLvT9clCf1K4kxp3eKsQhIaZqxEyowkBcsvZDdZkbWrVmnGknxs5flTD0VGsxrxKgBCZty1EzoiMzA==",
"version": "3.12.1",
"resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.1.tgz",
"integrity": "sha512-6IedsVWXyq4P9Tj+TxuU8WGWM70hYLl12nbYU8jkikVpa6WXapFazPUcHUMDMoWftIDE2ILDkFFte6W2nFCkRQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@ -1618,9 +1618,9 @@
"license": "MIT"
},
"node_modules/bits-ui": {
"version": "2.15.4",
"resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.15.4.tgz",
"integrity": "sha512-7H9YUfp03KOk1LVDh8wPYSRPxlZgG/GRWLNSA8QC73/8Z8ytun+DWJhIuibyFyz7A0cP/RANVcB4iDrbY8q+Og==",
"version": "2.18.0",
"resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.18.0.tgz",
"integrity": "sha512-GLOBZRVy3hxNHIQ2MpD/+5aK9KcBFZRhUJtZ1UDABXdlVR4K6zFpgt4T+Rwuhf2sQzlc6yK1q/DprHPjwT4Pjw==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@ -14,14 +14,14 @@
"devDependencies": {
"@eslint/compat": "^1.4.0",
"@eslint/js": "^9.39.1",
"@internationalized/date": "^3.10.1",
"@internationalized/date": "^3.12.1",
"@lucide/svelte": "^0.561.0",
"@sveltejs/adapter-auto": "^7.0.0",
"@sveltejs/kit": "^2.49.1",
"@sveltejs/vite-plugin-svelte": "^6.2.1",
"@tanstack/table-core": "^8.21.3",
"@types/node": "^22",
"bits-ui": "^2.15.4",
"bits-ui": "^2.18.0",
"clsx": "^2.1.1",
"eslint": "^9.39.1",
"eslint-config-prettier": "^10.1.8",

View File

@ -143,10 +143,11 @@ export const orderTestFormFields = [
columns: [
{
key: "Tests",
label: "Search Test",
required: true,
type: "tests",
withLabel: false,
type: "tests2",
optionsEndpoint: `${API.BASE_URL}${API.TEST}`,
otherEndpoint: `${API.BASE_URL}${API.DISCIPLINE}`,
valueKey: 'TestSiteCode',
labelKey: (item) => `${item.TestSiteCode} - ${item.TestSiteName}`,
validateOn: ['input']

View File

@ -53,7 +53,7 @@ $inspect(formState.form)
</script>
<FormPageContainer title="Create Order for {formState.form?.PatientID} - {formState.form?.PatientName}" {primaryAction} {secondaryActions} {actions}>
{#if orderStore.selectedPatient}
<!-- {#if orderStore.selectedPatient}
<div class="flex justify-between w-full rounded-md border p-2">
<div class="flex flex-col">
<span class="font-bold tracking-wide underline">
@ -77,7 +77,7 @@ $inspect(formState.form)
</span>
</div>
</div>
{/if}
{/if} -->
<OrderFormRenderer
{formState}
formFields={formFields}

View File

@ -24,6 +24,11 @@
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';
let {
formState,
@ -39,7 +44,9 @@
let selectedCodes = $derived(
new Set((formState.form.Tests ?? []).map(t => t.TestSiteCode))
);
let discipline = $state([]);
let selectedDiscipline = $state(null);
$inspect(discipline)
function getFilteredOptions(key) {
const query = searchQuery[key] || '';
if (!query) return formState.selectOptions?.[key] ?? [];
@ -101,10 +108,30 @@
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);
}
}
}
}
});
});
</script>
{#snippet Fieldset({
@ -113,34 +140,26 @@
required,
type,
optionsEndpoint,
otherEndpoint,
options,
validateOn,
allowFuture,
valueKey,
labelKey,
withLabel = true,
})}
<div class="flex w-full flex-col gap-1.5">
<div class="flex justify-between items-center w-full">
<Label>{label}</Label>
{#if required}
<span class="text-destructive text-xl leading-none h-3.5">*</span>
{/if}
{#if key === 'FormulaCode' && formState.form.FormulaInput?.length}
{@const inputStatus = onGetErrorStatus?.()}
<div class="flex items-center gap-2 text-sm text-destructive">
<span>Must included :</span>
<div class="flex gap-1 flex-wrap">
{#each inputStatus as item (item.value)}
<Badge class="px-1 text-[10px]" variant={item.done ? 'default' : 'destructive'}>
{item.value}
</Badge>
{/each}
</div>
<div class="flex w-full flex-col min-h-0 flex-1 h-full gap-1.5 overflow-hidden">
{#if withLabel}
<div class="flex justify-between items-center w-full">
<div class="ps-1">
<Label>{label}</Label>
</div>
{/if}
</div>
<div class="relative flex flex-col items-start w-full">
{#if required}
<span class="text-destructive text-xl leading-none h-3.5">*</span>
{/if}
</div>
{/if}
<div class="relative flex flex-col h-full items-start w-full flex-1 p-1 overflow-hidden">
{#if type === 'text'}
<Input
type="text"
@ -335,6 +354,212 @@
{/if}
</Table.Body>
</Table.Root>
{:else if type === "tests2"}
{@const allTests = formState.selectOptions?.[key] ?? []}
{@const _ = console.log('allTests', allTests)}
{@const disciplineList = [
...new Map(
allTests
.filter(t => t.DisciplineID !== null)
.map(t => [t.DisciplineID, { id: t.DisciplineID, name: t.DisciplineName }])
).values()
]}
{@const filteredOptions = getFilteredOptions(key)}
{@const filteredTests = selectedDiscipline
? allTests.filter(t => t.DisciplineID === selectedDiscipline)
: allTests}
<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 border-b-2 flex flex-col">
<p class="text-xs font-medium text-muted-foreground uppercase tracking-wide">Test list</p>
<div class="flex gap-2">
<Select.Root
type="single"
onValueChange={(val) => selectedDiscipline = val || null}
onOpenChange={(open) => {
if (open && otherEndpoint) {
formState.fetchOptions?.(
{ key, otherEndpoint, valueKey, labelKey },
formState.form
);
}
}}
>
<Select.Trigger class="w-full truncate">
{disciplineList.find(d => d.id === selectedDiscipline)?.name || 'All Disciplines'}
</Select.Trigger>
<Select.Content>
<Select.Item value="">- All -</Select.Item>
{#each discipline as disc}
<Select.Item value={disc.DisciplineID}>{disc.DisciplineName}</Select.Item>
{/each}
</Select.Content>
</Select.Root>
<Input
type="text"
/>
</div>
</div>
<div class="flex-1 min-h-0 overflow-y-auto [scrollbar-width:thin] [&::-webkit-scrollbar]:w-1" >
<Collapsible.Root>
<Collapsible.Trigger
class="flex w-full items-center justify-between px-3 py-2 rounded-md bg-muted/50 hover:bg-muted transition-colors text-left"
>
<div class="flex items-center gap-2">
<ChevronDownIcon />
<span class="text-sm font-medium">Hematologi</span>
<span class="text-xs text-muted-foreground">5 tests</span>
</div>
</Collapsible.Trigger>
<Collapsible.Content>
<div class="py-1 px-1">
<div class="flex items-center gap-3 px-3 py-2 rounded-md transition-colors">
<Checkbox />
<span class="text-xs font-semibold text-primary w-14 shrink-0">HB</span>
<Label class="text-sm cursor-pointer">Hemoglobin</Label>
</div>
<div class="flex items-center gap-3 px-3 py-2 rounded-md transition-colors">
<Checkbox />
<span class="text-xs font-semibold text-primary w-14 shrink-0">HCT</span>
<Label class="text-sm cursor-pointer">Hematokrit</Label>
</div>
<div class="flex items-center gap-3 px-3 py-2 rounded-md transition-colors">
<Checkbox />
<span class="text-xs font-semibold text-primary w-14 shrink-0">HCT</span>
<Label class="text-sm cursor-pointer">Hematocrit</Label>
</div>
<div class="flex items-center gap-3 px-3 py-2 rounded-md transition-colors">
<Checkbox />
<span class="text-xs font-semibold text-primary w-14 shrink-0">HCT</span>
<Label class="text-sm cursor-pointer">Hematocret</Label>
</div>
<div class="flex items-center gap-3 px-3 py-2 rounded-md transition-colors">
<Checkbox />
<span class="text-xs font-semibold text-primary w-14 shrink-0">HCT</span>
<Label class="text-sm cursor-pointer">Hematocrut</Label>
</div>
</div>
</Collapsible.Content>
</Collapsible.Root>
<Collapsible.Root>
<Collapsible.Trigger
class="flex w-full items-center justify-between px-3 py-2 rounded-md bg-muted/50 hover:bg-muted transition-colors text-left"
>
<div class="flex items-center gap-2">
<ChevronDownIcon />
<span class="text-sm font-medium">Kimia Klenik</span>
<span class="text-xs text-muted-foreground">17 tests</span>
</div>
</Collapsible.Trigger>
<Collapsible.Content>
<div class="py-1 px-1">
<div class="flex items-center gap-3 px-3 py-2 rounded-md transition-colors">
<Checkbox />
<span class="text-xs font-semibold text-primary w-14 shrink-0">CREA</span>
<Label class="text-sm cursor-pointer">Creatinine</Label>
</div>
<div class="flex items-center gap-3 px-3 py-2 rounded-md transition-colors">
<Checkbox />
<span class="text-xs font-semibold text-primary w-14 shrink-0">GLU</span>
<Label class="text-sm cursor-pointer">Glukosa</Label>
</div>
<div class="flex items-center gap-3 px-3 py-2 rounded-md transition-colors">
<Checkbox />
<span class="text-xs font-semibold text-primary w-14 shrink-0">GLUP</span>
<Label class="text-sm cursor-pointer">Glukosa Puasa</Label>
</div>
<div class="flex items-center gap-3 px-3 py-2 rounded-md transition-colors">
<Checkbox />
<span class="text-xs font-semibold text-primary w-14 shrink-0">GPU</span>
<Label class="text-sm cursor-pointer">Yahud</Label>
</div>
<div class="flex items-center gap-3 px-3 py-2 rounded-md transition-colors">
<Checkbox />
<span class="text-xs font-semibold text-primary w-14 shrink-0">ASOE</span>
<Label class="text-sm cursor-pointer">Israhell</Label>
</div>
</div>
</Collapsible.Content>
</Collapsible.Root>
<Collapsible.Root>
<Collapsible.Trigger
class="flex w-full items-center justify-between px-3 py-2 rounded-md bg-muted/50 hover:bg-muted transition-colors text-left"
>
<div class="flex items-center gap-2">
<ChevronDownIcon />
<span class="text-sm font-medium">Immunologi</span>
<span class="text-xs text-muted-foreground">10 tests</span>
</div>
</Collapsible.Trigger>
<Collapsible.Content>
<div class="py-1 px-1">
<div class="flex items-center gap-3 px-3 py-2 rounded-md transition-colors">
<Checkbox />
<span class="text-xs font-semibold text-primary w-14 shrink-0">HIV</span>
<Label class="text-sm cursor-pointer">HIV</Label>
</div>
<div class="flex items-center gap-3 px-3 py-2 rounded-md transition-colors">
<Checkbox />
<span class="text-xs font-semibold text-primary w-14 shrink-0">TESTO</span>
<Label class="text-sm cursor-pointer">Testosteron</Label>
</div>
<div class="flex items-center gap-3 px-3 py-2 rounded-md transition-colors">
<Checkbox />
<span class="text-xs font-semibold text-primary w-14 shrink-0">ESTR</span>
<Label class="text-sm cursor-pointer">Estrogen</Label>
</div>
<div class="flex items-center gap-3 px-3 py-2 rounded-md transition-colors">
<Checkbox />
<span class="text-xs font-semibold text-primary w-14 shrink-0">GPU</span>
<Label class="text-sm cursor-pointer">Yahud</Label>
</div>
<div class="flex items-center gap-3 px-3 py-2 rounded-md transition-colors">
<Checkbox />
<span class="text-xs font-semibold text-primary w-14 shrink-0">ASOE</span>
<Label class="text-sm cursor-pointer">Israhell</Label>
</div>
</div>
</Collapsible.Content>
</Collapsible.Root>
</div>
<div class="shrink-0 p-2 border-t border-b flex items-center justify-between">
<span class="text-sm">0 selected</span>
<Button size="sm" class="px-4 py-2 rounded">Add selected</Button>
</div>
</div>
<div class="flex flex-1 flex-col overflow-hidden min-h-0 h-full w-1/2 gap-2">
<div class="shrink-0 h-7 border-b-2 flex justify-between items-start">
<p class="text-xs font-medium text-muted-foreground uppercase tracking-wide">Test ordered</p>
<Badge variant="outline">0 test(s)</Badge>
</div>
<div class="flex flex-col gap-2 p-1 flex-1 min-h-0 overflow-y-auto w-full [scrollbar-width:thin] [&::-webkit-scrollbar]:w-1">
{#each [1,2,3,3] as x }
<InputGroup.Root class="p-1">
<InputGroup.Addon align="inline-start">
1
</InputGroup.Addon>
<InputGroup.Addon align="inline-start">
CREAD
</InputGroup.Addon>
<InputGroup.Addon align="inline-start">
MCU PT. XYZ
</InputGroup.Addon>
<InputGroup.Input readonly />
<InputGroup.Addon align="inline-end">
<InputGroup.Button
aria-label="Delete"
title="Delete"
size="icon-xs"
>
<Trash2Icon />
</InputGroup.Button>
</InputGroup.Addon>
</InputGroup.Root>
{/each}
</div>
</div>
</div>
{:else}
<Input
type="text"
@ -354,9 +579,9 @@
{/snippet}
{#snippet GroupBlock(groups)}
<div class="p-2 space-y-6">
<div class="p-2 min-h-0 flex-1 flex flex-col h-full">
{#each groups as group}
<div class="space-y-6">
<div class="flex flex-col flex-1 space-y-4 h-full">
{#if group.title}
<div class="text-md 2xl:text-lg font-semibold italic">
<span class="border-b-2 border-primary">{group.title}</span>
@ -365,7 +590,8 @@
{#each group.rows as row}
<div
class="grid grid-cols-1 space-y-2 gap-6 md:gap-4"
class="grid grid-cols-1 grid-rows-1 space-y-2 h-full gap-3 min-h-0 md:gap-2"
class:h-full={row.columns.some(col => col.key === "Tests")}
class:md:grid-cols-1={row.columns.length === 1}
class:md:grid-cols-2={row.columns.length === 2}
class:md:grid-cols-3={row.columns.length === 3}
@ -380,11 +606,11 @@
</div>
{/snippet}
<div class="flex w-full h-full gap-1">
<div class="w-1/3 h-full min-w-0 overflow-y-auto [scrollbar-width:thin] [&::-webkit-scrollbar]:w-1">
<div class="flex w-full h-full min-h-0 overflow-hidden gap-1">
<div class="w-1/3 h-full min-h-0 overflow-hidden flex flex-col">
{@render GroupBlock(leftGroups)}
</div>
<div class="flex-1 h-full min-w-0 overflow-y-auto [scrollbar-width:thin] [&::-webkit-scrollbar]:w-1">
<div class="flex-1 h-full min-h-0 overflow-hidden flex flex-col">
{@render GroupBlock(rightGroups)}
</div>
</div>

View File

@ -0,0 +1,10 @@
import Scrollbar from "./scroll-area-scrollbar.svelte";
import Root from "./scroll-area.svelte";
export {
Root,
Scrollbar,
//,
Root as ScrollArea,
Scrollbar as ScrollAreaScrollbar,
};

View File

@ -0,0 +1,30 @@
<script>
import { ScrollArea as ScrollAreaPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
orientation = "vertical",
children,
...restProps
} = $props();
</script>
<ScrollAreaPrimitive.Scrollbar
bind:ref
data-slot="scroll-area-scrollbar"
data-orientation={orientation}
{orientation}
class={cn(
"data-horizontal:h-2.5 data-horizontal:flex-col data-horizontal:border-t data-horizontal:border-t-transparent data-vertical:h-full data-vertical:w-2.5 data-vertical:border-l data-vertical:border-l-transparent flex touch-none p-px transition-colors select-none",
className
)}
{...restProps}
>
{@render children?.()}
<ScrollAreaPrimitive.Thumb
data-slot="scroll-area-thumb"
class="rounded-full bg-border relative flex-1"
/>
</ScrollAreaPrimitive.Scrollbar>

View File

@ -0,0 +1,38 @@
<script>
import { ScrollArea as ScrollAreaPrimitive } from "bits-ui";
import { Scrollbar } from "./index.js";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
viewportRef = $bindable(null),
class: className,
orientation = "vertical",
scrollbarXClasses = "",
scrollbarYClasses = "",
children,
...restProps
} = $props();
</script>
<ScrollAreaPrimitive.Root
bind:ref
data-slot="scroll-area"
class={cn("relative", className)}
{...restProps}
>
<ScrollAreaPrimitive.Viewport
bind:ref={viewportRef}
data-slot="scroll-area-viewport"
class="cn-scroll-area-viewport focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
>
{@render children?.()}
</ScrollAreaPrimitive.Viewport>
{#if orientation === "vertical" || orientation === "both"}
<Scrollbar orientation="vertical" class={scrollbarYClasses} />
{/if}
{#if orientation === "horizontal" || orientation === "both"}
<Scrollbar orientation="horizontal" class={scrollbarXClasses} />
{/if}
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>

View File

@ -0,0 +1,26 @@
<div class="flex w-full h-full min-h-0 overflow-hidden gap-1 bg-rose-300">
<div class="flex-1 h-full min-h-0 overflow-hidden flex flex-col">
<div class="p-2 min-h-0 flex-1 flex flex-col h-full">
<div class="flex flex-col flex-1 h-full min-h-0"> <div class="grid grid-cols-1 grid-rows-1 h-full min-h-0">
<div class="flex w-full flex-col min-h-0 flex-1 h-full gap-1.5 overflow-hidden">
<div class="relative flex flex-col h-full items-start w-full flex-1 overflow-hidden">
<div class="flex flex-1 flex-col overflow-hidden min-h-0 bg-rose-100 h-full w-full">
<div class="shrink-0 p-2">...</div>
<div class="flex-1 min-h-0 overflow-y-auto">
<div class="h-[1000px] bg-red-300">long</div>
</div>
<div class="shrink-0 p-2">...</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>