mirror of
https://github.com/faiztyanirh/clqms-shadcn-v1.git
synced 2026-04-28 17:52:31 +07:00
continue ordertest create
This commit is contained in:
parent
ee04b2e8d5
commit
47873b172a
@ -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',
|
||||
|
||||
@ -41,7 +41,7 @@
|
||||
// toast.error(errorMessages)
|
||||
// }
|
||||
}
|
||||
$inspect(formState.form)
|
||||
$inspect(formState.errors)
|
||||
const primaryAction = $derived({
|
||||
label: 'Save',
|
||||
onClick: handleSave,
|
||||
|
||||
@ -74,8 +74,6 @@
|
||||
patientStore.pending = null;
|
||||
}
|
||||
})
|
||||
|
||||
$inspect(orderStore)
|
||||
</script>
|
||||
|
||||
{#snippet searchParamSnippet()}
|
||||
|
||||
@ -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}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user