mirror of
https://github.com/faiztyanirh/clqms-shadcn-v1.git
synced 2026-04-22 17:19:52 +07:00
continue ordertest page :
-search param modal -create page
This commit is contained in:
parent
cba87ecbbe
commit
84a14c118f
@ -49,7 +49,7 @@ export const detailSections = [
|
||||
},
|
||||
];
|
||||
|
||||
export function orderTestActions(masterDetail, selectedPatient) {
|
||||
export function orderTestActions(masterDetail, selectedPatient, selectedVisit) {
|
||||
return [
|
||||
{
|
||||
Icon: PlusIcon,
|
||||
|
||||
@ -5,11 +5,129 @@ import { cleanEmptyStrings } from "$lib/utils/cleanEmptyStrings";
|
||||
|
||||
export const orderTestSchema = z.object({});
|
||||
|
||||
export const orderTestInitialForm = {};
|
||||
export const orderTestInitialForm = {
|
||||
InternalOID: '',
|
||||
OrderID: '',
|
||||
PlacerID: '',
|
||||
InternalPID: '',
|
||||
SiteID: '',
|
||||
PVADTID: '',
|
||||
ReqApp: '',
|
||||
Priority: '',
|
||||
TrnDate: '',
|
||||
EffDate: '',
|
||||
Comment: '',
|
||||
OrderAtt: '',
|
||||
Tests: '',
|
||||
};
|
||||
|
||||
export const orderTestDefaultErrors = {};
|
||||
|
||||
export const orderTestFormFields = [];
|
||||
export const orderTestFormFields = [
|
||||
{
|
||||
title: "Order Details",
|
||||
rows: [
|
||||
{
|
||||
type: "row",
|
||||
columns: [
|
||||
{
|
||||
key: "SiteID",
|
||||
label: "Site",
|
||||
required: false,
|
||||
type: "select",
|
||||
optionsEndpoint: `${API.BASE_URL}${API.SITE}`,
|
||||
valueKey: "SiteID",
|
||||
labelKey: "SiteName",
|
||||
},
|
||||
{
|
||||
key: "Priority",
|
||||
label: "Priority",
|
||||
required: false,
|
||||
type: "select",
|
||||
optionsEndpoint: `${API.BASE_URL}${API.VALUESET}/order_priority`,
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "row",
|
||||
columns: [
|
||||
{
|
||||
key: "PlacerID",
|
||||
label: "Placer ID",
|
||||
required: false,
|
||||
type: "text",
|
||||
},
|
||||
{
|
||||
key: "ReqApp",
|
||||
label: "Requested Application",
|
||||
required: false,
|
||||
type: "text",
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "row",
|
||||
columns: [
|
||||
{
|
||||
key: "TrnDate",
|
||||
label: "Transaction Date",
|
||||
required: false,
|
||||
type: "date",
|
||||
allowFuture: true
|
||||
},
|
||||
{
|
||||
key: "EffDate",
|
||||
label: "Effective Date",
|
||||
required: false,
|
||||
type: "date",
|
||||
allowFuture: true
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "row",
|
||||
columns: [
|
||||
{
|
||||
key: "Comment",
|
||||
label: "Comment",
|
||||
required: false,
|
||||
type: "textarea",
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "row",
|
||||
columns: [
|
||||
{
|
||||
key: "OrderAtt",
|
||||
label: "Attachment",
|
||||
required: false,
|
||||
type: "fileupload",
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Tests",
|
||||
rows: [
|
||||
{
|
||||
type: "row",
|
||||
columns: [
|
||||
{
|
||||
key: "Tests",
|
||||
label: "Search Test",
|
||||
required: false,
|
||||
type: "tests",
|
||||
optionsEndpoint: `${API.BASE_URL}${API.TEST}`,
|
||||
valueKey: 'TestSiteCode',
|
||||
labelKey: (item) => `${item.TestSiteCode} - ${item.TestSiteName}`,
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
];
|
||||
|
||||
export function getOrderTestFormActions(handlers) {
|
||||
return [
|
||||
|
||||
@ -59,15 +59,14 @@
|
||||
|
||||
function handleButtonClick() {
|
||||
if (tempSelectedVisit) {
|
||||
props.onConfirm(tempSelectedPatient);
|
||||
props.onConfirm(tempSelectedPatient, tempSelectedVisit);
|
||||
|
||||
tempSelectedVisit = null;
|
||||
}
|
||||
}
|
||||
$inspect(tempSelectedVisit)
|
||||
</script>
|
||||
|
||||
<div class="w-280 h-110 transition-all duration-300">
|
||||
<div class="w-320 h-110 transition-all duration-300">
|
||||
<div class="flex gap-4 h-full">
|
||||
<div class="flex flex-col w-1/3 h-full">
|
||||
<div class="flex-1 overflow-y-auto min-h-0 space-y-2 px-2">
|
||||
@ -113,7 +112,7 @@
|
||||
</div>
|
||||
<div class="flex flex-col w-2/3 h-full">
|
||||
{#if props.search.searchData && props.search.searchData.length > 0}
|
||||
<div class="flex-1 overflow-y-auto min-h-0 flex flex-col gap-2">
|
||||
<div class="flex-1 overflow-y-auto min-h-0 flex flex-col gap-2 [scrollbar-width:thin] [&::-webkit-scrollbar]:w-1">
|
||||
<div class="flex-1">
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
@ -154,25 +153,26 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="min-w-60 flex flex-col h-full items-center justify-center">
|
||||
<div class="min-w-100 flex flex-col h-full items-center justify-center">
|
||||
{#if isLoadingVisit}
|
||||
<Spinner />
|
||||
{:else if visitData && visitData.data?.length > 0}
|
||||
<div class="flex-1 overflow-y-auto min-h-0 flex flex-col gap-2">
|
||||
<div class="flex-1">
|
||||
<span class="font-medium">Active Visit</span>
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
<Table.Row class="hover:bg-transparent">
|
||||
<Table.Head class="w-8"></Table.Head>
|
||||
<Table.Head class="">Visit ID</Table.Head>
|
||||
<Table.Head class="">Visit Date</Table.Head>
|
||||
<Table.Head class="">Location</Table.Head>
|
||||
<Table.Head class="">Doctor</Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#each visitData.data as visit, i}
|
||||
<Table.Row
|
||||
class="cursor-pointer hover:bg-muted/50"
|
||||
class="cursor-pointer hover:bg-muted/50 h-12"
|
||||
onclick={() => handleCheckboxVisit(visit)}
|
||||
>
|
||||
<Table.Cell onclick={(e) => e.stopPropagation()}>
|
||||
@ -182,8 +182,23 @@
|
||||
onCheckedChange={() => handleCheckboxVisit(visit)}
|
||||
/>
|
||||
</Table.Cell>
|
||||
<Table.Cell class="">{visit.PVID}</Table.Cell>
|
||||
<Table.Cell class="">{formatUTCDate(visit.PVCreateDate)}</Table.Cell>
|
||||
<Table.Cell class="font-medium">{visit.PVID}</Table.Cell>
|
||||
<Table.Cell>
|
||||
<div>{formatUTCDate(visit.PVCreateDate).split(' ')[0]}</div>
|
||||
<div class="text-xs opacity-60">{formatUTCDate(visit.PVCreateDate).split(' ')[1]}</div>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<div class="flex flex-col py-1">
|
||||
<span class="font-medium">{visit.LocCode ?? "-"}</span>
|
||||
<span class="text-xs opacity-60">{visit.LocFull ?? "-"}</span>
|
||||
</div>
|
||||
</Table.Cell>
|
||||
<Table.Cell class="flex flex-col py-2">
|
||||
<div class="flex flex-col py-1">
|
||||
<span class="font-medium">{visit.AttDocContactCode ?? "-"}</span>
|
||||
<span class="text-xs opacity-60">{visit.AttDocFirstName} {visit.AttDocLastName}</span>
|
||||
</div>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
</Table.Body>
|
||||
|
||||
@ -0,0 +1,66 @@
|
||||
<script>
|
||||
import { usePatientForm } from "$lib/components/composable/use-patient-form.svelte";
|
||||
import FormPageContainer from "$lib/components/patient/reusable/form-page-container.svelte";
|
||||
import OrderFormRenderer from "$lib/components/reusable/form/order-form-renderer.svelte";
|
||||
import { toast } from "svelte-sonner";
|
||||
import ReusableAlertDialog from "$lib/components/reusable/reusable-alert-dialog.svelte";
|
||||
|
||||
let props = $props();
|
||||
|
||||
const { masterDetail, formFields, formActions, schema } = props.context;
|
||||
|
||||
const { formState } = masterDetail;
|
||||
|
||||
const helpers = usePatientForm(formState, schema);
|
||||
|
||||
const handlers = {
|
||||
clearForm: () => {
|
||||
formState.reset();
|
||||
}
|
||||
};
|
||||
|
||||
const actions = formActions(handlers);
|
||||
|
||||
let showConfirm = $state(false);
|
||||
|
||||
async function handleSave() {
|
||||
// const payload = buildPayload(formState.form);
|
||||
|
||||
// const result = await formState.save(masterDetail.mode, payload);
|
||||
|
||||
// console.log(payload);
|
||||
// toast('Visit Created!');
|
||||
// masterDetail?.exitForm(true);
|
||||
}
|
||||
|
||||
const primaryAction = $derived({
|
||||
label: 'Save',
|
||||
onClick: handleSave,
|
||||
disabled: helpers.hasErrors || formState.isSaving.current,
|
||||
loading: formState.isSaving.current
|
||||
});
|
||||
|
||||
const secondaryActions = [];
|
||||
$inspect(formState.form)
|
||||
// $effect(() => {
|
||||
// if (masterDetail.form?.PatientID) {
|
||||
// formState.setForm({
|
||||
// ...formState.form,
|
||||
// ...masterDetail.form
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
</script>
|
||||
|
||||
<FormPageContainer title="Create Order for {formState.form?.PatientID}" {primaryAction} {secondaryActions} {actions}>
|
||||
<OrderFormRenderer
|
||||
{formState}
|
||||
formFields={formFields}
|
||||
mode="create"
|
||||
/>
|
||||
</FormPageContainer>
|
||||
|
||||
<ReusableAlertDialog
|
||||
bind:open={masterDetail.showExitConfirm}
|
||||
onConfirm={masterDetail.confirmExit}
|
||||
/>
|
||||
@ -14,17 +14,19 @@
|
||||
import { page } from '$app/stores';
|
||||
import { getPatient } from "$lib/components/patient/list/api/patient-list-api";
|
||||
import { patientStore } from "$lib/components/patient/list/store/patient-store.svelte";
|
||||
import { formatUTCDate } from "$lib/utils/formatUTCDate";
|
||||
|
||||
let props = $props();
|
||||
|
||||
let selectedPatient = $state(null);
|
||||
let selectedVisit = $state(null);
|
||||
let isLoading = $state(false);
|
||||
let searchData = $state([]);
|
||||
|
||||
$inspect(selectedPatient)
|
||||
const search = useSearch(searchFields, searchParam);
|
||||
|
||||
let actions = $derived.by(() => {
|
||||
return orderTestActions(props.masterDetail, selectedPatient).map(action => {
|
||||
return orderTestActions(props.masterDetail, selectedPatient, selectedVisit).map(action => {
|
||||
if (action.label === 'Search Parameters') {
|
||||
return { ...action, popoverContent: searchParamSnippet };
|
||||
}
|
||||
@ -34,12 +36,13 @@
|
||||
|
||||
let activeRowId = $state(null);
|
||||
|
||||
async function handlePatientConfirm(patient) {
|
||||
async function handlePatientConfirm(patient, visit) {
|
||||
selectedPatient = patient;
|
||||
selectedVisit = visit;
|
||||
isLoading = true;
|
||||
try {
|
||||
searchData = await getOrderList({
|
||||
InternalPID: selectedPatient.InternalPID
|
||||
PVADTID: selectedVisit.PVADTID
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Search failed:', error);
|
||||
@ -61,31 +64,15 @@
|
||||
SexLabel: `${patient.patient.SexLabel}`,
|
||||
};
|
||||
console.log(transformedPatient);
|
||||
// return {
|
||||
// InternalPID: p.InternalPID,
|
||||
// PatientID: p.PatientID,
|
||||
// FullName: [p.NameFirst, p.NameMiddle, p.NameLast].filter(Boolean).join(' '),
|
||||
// Sex: p.Sex,
|
||||
// Birthdate: p.Birthdate,
|
||||
// Email: p.Email,
|
||||
// MobilePhone: p.MobilePhone,
|
||||
// SexLabel: p.SexLabel,
|
||||
// }
|
||||
handlePatientConfirm(transformedPatient);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// const pid = $page.url.searchParams.get('InternalPID'); //kalau dari url parameter
|
||||
// if (pid) {
|
||||
// loadPatientFromUrl(pid);
|
||||
// }
|
||||
if (patientStore.pending) {
|
||||
loadPatientFromUrl(patientStore.pending.InternalPID);
|
||||
patientStore.pending = null;
|
||||
}
|
||||
})
|
||||
|
||||
$inspect(patientStore)
|
||||
</script>
|
||||
|
||||
{#snippet searchParamSnippet()}
|
||||
@ -131,13 +118,18 @@
|
||||
<span class="font-semibold">
|
||||
{selectedPatient?.FullName}
|
||||
</span>
|
||||
<span class="text-xs">
|
||||
{selectedPatient?.Age ? selectedPatient.Age.split(' ').slice(0, 2).join(' ') : 'N/A'} -
|
||||
{selectedPatient?.SexLabel} -
|
||||
{selectedPatient?.BirthdateConversion}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col justify-end items-end">
|
||||
<span class="font-bold tracking-wide">
|
||||
{selectedPatient?.Birthdate}
|
||||
<span class="font-bold tracking-wide underline">
|
||||
{selectedVisit?.PVID}
|
||||
</span>
|
||||
<span class="font-semibold">
|
||||
{selectedPatient?.SexLabel}
|
||||
<span class="text-xs">
|
||||
{formatUTCDate(selectedVisit?.PVCreateDate)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -147,7 +139,8 @@
|
||||
<ReusableDataTable data={searchData} columns={orderTestColumns} handleRowClick={props.masterDetail.select} {activeRowId} rowIdKey="InternalOID" offset="7"/>
|
||||
{:else}
|
||||
<div class="flex h-full">
|
||||
<ReusableEmpty icon={ClipboardXIcon} desc="Try searching from search parameters"/>
|
||||
<!-- <ReusableEmpty icon={ClipboardXIcon} desc="Try searching from search parameters"/> -->
|
||||
<ReusableEmpty title="No Order" icon={ClipboardXIcon} desc={searchData ? "No order available" : "Try searching from search parameters"}/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@ -7,8 +7,4 @@ export const orderTestColumns = [
|
||||
accessorKey: "PlacerID",
|
||||
header: "Host ID",
|
||||
},
|
||||
{
|
||||
accessorKey: "Priority",
|
||||
header: "Priority",
|
||||
},
|
||||
];
|
||||
@ -15,7 +15,7 @@
|
||||
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col p-2 gap-4 h-full w-full">
|
||||
<div class="flex flex-col p-2 h-full w-full">
|
||||
<TopbarWrapper actions={actions} title={title}/>
|
||||
<div class="flex-1 min-h-0 overflow-y-auto p-2">
|
||||
{@render children()}
|
||||
|
||||
@ -21,6 +21,7 @@
|
||||
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";
|
||||
|
||||
let {
|
||||
formState,
|
||||
@ -860,6 +861,10 @@
|
||||
{isOn ? toggleOn.label : toggleOff.label}
|
||||
</Toggle>
|
||||
</div>
|
||||
{:else if type === "fileupload"}
|
||||
<div class="flex flex-col w-full">
|
||||
<ReusableUpload attachments={formState.form[key]} />
|
||||
</div>
|
||||
{:else}
|
||||
<Input
|
||||
type="text"
|
||||
|
||||
345
src/lib/components/reusable/form/order-form-renderer.svelte
Normal file
345
src/lib/components/reusable/form/order-form-renderer.svelte
Normal file
@ -0,0 +1,345 @@
|
||||
<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 ReusableCalendar from '$lib/components/reusable/reusable-calendar.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";
|
||||
|
||||
let {
|
||||
formState,
|
||||
formFields,
|
||||
uploadErrors,
|
||||
mode = 'create',
|
||||
} = $props();
|
||||
|
||||
let searchQuery = $state({});
|
||||
|
||||
const leftGroups = $derived([formFields[0]]);
|
||||
const rightGroups = $derived(formFields.slice(1));
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
initializeDefaultValues();
|
||||
});
|
||||
</script>
|
||||
|
||||
{#snippet Fieldset({
|
||||
key,
|
||||
label,
|
||||
required,
|
||||
type,
|
||||
optionsEndpoint,
|
||||
options,
|
||||
validateOn,
|
||||
allowFuture,
|
||||
valueKey,
|
||||
labelKey,
|
||||
})}
|
||||
<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>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="relative flex flex-col items-start w-full">
|
||||
{#if type === 'text'}
|
||||
<Input
|
||||
type="text"
|
||||
bind:value={formState.form[key]}
|
||||
oninput={() => {
|
||||
if (validateOn?.includes('input')) {
|
||||
formState.validateField(key, formState.form[key], false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{:else if type === 'textarea'}
|
||||
<Textarea
|
||||
class="flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
bind:value={formState.form[key]}
|
||||
oninput={() => {
|
||||
if (validateOn?.includes('input')) {
|
||||
formState.validateField(key, formState.form[key], false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{:else if type === 'select'}
|
||||
{@const selectedLabel =
|
||||
formState.selectOptions?.[key]?.find((opt) => opt.value === formState.form[key])?.label ||
|
||||
'Choose'}
|
||||
{@const filteredOptions = getFilteredOptions(key)}
|
||||
<Select.Root
|
||||
type="single"
|
||||
bind:value={formState.form[key]}
|
||||
onValueChange={(val) => {
|
||||
formState.form[key] = val;
|
||||
}}
|
||||
onOpenChange={(open) => {
|
||||
if (open) {
|
||||
if (options && options.length > 0) {
|
||||
if (formState.selectOptions) {
|
||||
formState.selectOptions[key] = options;
|
||||
}
|
||||
} else if (optionsEndpoint) {
|
||||
formState.fetchOptions?.(
|
||||
{ key, optionsEndpoint, valueKey, labelKey },
|
||||
formState.form
|
||||
);
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Select.Trigger class="w-full truncate">
|
||||
{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}>
|
||||
{option.label}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
{/if}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
{:else if type === 'date'}
|
||||
<ReusableCalendar
|
||||
bind:value={formState.form[key]}
|
||||
parentFunction={(dateStr) => {
|
||||
formState.form[key] = dateStr;
|
||||
if (validateOn?.includes('input')) {
|
||||
formState.validateField(key, dateStr, false);
|
||||
}
|
||||
}}
|
||||
allowFuture={allowFuture}
|
||||
/>
|
||||
{:else if type === "fileupload"}
|
||||
<div class="flex flex-col w-full">
|
||||
<ReusableUpload attachments={formState.form[key]} />
|
||||
</div>
|
||||
{:else if type === "tests"}
|
||||
{@const selectedLabel =
|
||||
formState.selectOptions?.[key]?.find((opt) => opt.value === formState.form[key])?.label ||
|
||||
'Choose'}
|
||||
{@const filteredOptions = getFilteredOptions(key)}
|
||||
<div class="flex gap-2 w-full">
|
||||
<div class="flex-1">
|
||||
<Select.Root
|
||||
type="single"
|
||||
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}>
|
||||
{option.label}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
{/if}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
<Button>
|
||||
<PlusIcon class="size-4" />
|
||||
Add Test
|
||||
</Button>
|
||||
</div>
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
<Table.Row class="hover:bg-transparent">
|
||||
<Table.Head>No</Table.Head>
|
||||
<Table.Head>Test Name</Table.Head>
|
||||
<Table.Head>Discipline</Table.Head>
|
||||
<Table.Head>Container</Table.Head>
|
||||
<Table.Head class="w-[40px]"></Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
<Table.Row
|
||||
class="cursor-pointer hover:bg-muted/50"
|
||||
>
|
||||
<Table.Cell>1</Table.Cell>
|
||||
<Table.Cell>Hematologi Rutin</Table.Cell>
|
||||
<Table.Cell>Hematologi</Table.Cell>
|
||||
<Table.Cell>EDTA</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"
|
||||
>
|
||||
<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>
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
{:else}
|
||||
<Input
|
||||
type="text"
|
||||
bind:value={formState.form[key]}
|
||||
placeholder="Custom field type: {type}"
|
||||
/>
|
||||
{/if}
|
||||
<div class="absolute top-8 min-h-[1rem] w-full">
|
||||
{#if formState.errors[key]}
|
||||
<span class="text-destructive text-sm leading-none">
|
||||
{formState.errors[key]}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
{#snippet GroupBlock(groups)}
|
||||
<div class="p-2 space-y-6">
|
||||
{#each groups as group}
|
||||
<div class="space-y-6">
|
||||
{#if group.title}
|
||||
<div class="text-md 2xl:text-lg font-semibold italic">
|
||||
<span class="border-b-2 border-primary">{group.title}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#each group.rows as row}
|
||||
<div
|
||||
class="grid grid-cols-1 space-y-2 gap-6 md:gap-4"
|
||||
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}
|
||||
>
|
||||
{#each row.columns as col}
|
||||
{@render Fieldset(col)}
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</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">
|
||||
{@render GroupBlock(leftGroups)}
|
||||
</div>
|
||||
<div class="flex-1 h-full min-w-0 overflow-y-auto [scrollbar-width:thin] [&::-webkit-scrollbar]:w-1">
|
||||
{@render GroupBlock(rightGroups)}
|
||||
</div>
|
||||
</div>
|
||||
@ -7,10 +7,12 @@
|
||||
import { getLocalTimeZone, today, parseDate } from "@internationalized/date";
|
||||
|
||||
const id = $props.id();
|
||||
let { title, parentFunction, value = $bindable("") } = $props();
|
||||
let { title, parentFunction, value = $bindable(""), allowFuture = false } = $props();
|
||||
let open = $state(false);
|
||||
let calendarValue = $state();
|
||||
|
||||
const maxValue = $derived(allowFuture ? undefined : today(getLocalTimeZone()));
|
||||
|
||||
$effect(() => {
|
||||
if (value && typeof value === "string") {
|
||||
try {
|
||||
@ -64,7 +66,7 @@
|
||||
bind:value={calendarValue}
|
||||
captionLayout="dropdown"
|
||||
onValueChange={handleChange}
|
||||
maxValue={today(getLocalTimeZone())}
|
||||
maxValue={maxValue}
|
||||
/>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user