mirror of
https://github.com/faiztyanirh/clqms-shadcn-v1.git
synced 2026-04-28 01:55:54 +07:00
initial create admission, fix view detail admission
This commit is contained in:
parent
a4c2e0ec5f
commit
aee9ea4b8f
@ -8,8 +8,17 @@ const optionsMode = {
|
|||||||
const res = await fetch(field.optionsEndpoint);
|
const res = await fetch(field.optionsEndpoint);
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
|
|
||||||
selectOptions[field.key] = json?.data ?? [];
|
// selectOptions[field.key] = json?.data ?? [];
|
||||||
|
|
||||||
|
const data = json?.data ?? [];
|
||||||
|
const valueKey = field.valueKey ?? 'value';
|
||||||
|
const labelKey = field.labelKey ?? 'label';
|
||||||
|
|
||||||
|
selectOptions[field.key] = data.map((item) => ({
|
||||||
|
value: item[valueKey],
|
||||||
|
label: typeof labelKey === 'function' ? labelKey(item) : item[labelKey],
|
||||||
|
}));
|
||||||
|
console.log(selectOptions);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to fetch options for", field.key, err);
|
console.error("Failed to fetch options for", field.key, err);
|
||||||
selectOptions[field.key] = [];
|
selectOptions[field.key] = [];
|
||||||
@ -52,8 +61,17 @@ const optionsMode = {
|
|||||||
const res = await fetch(endpoint);
|
const res = await fetch(endpoint);
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
|
|
||||||
selectOptions[field.key] = json?.data ?? [];
|
// selectOptions[field.key] = json?.data ?? [];
|
||||||
|
|
||||||
|
const data = json?.data ?? [];
|
||||||
|
const valueKey = field.valueKey ?? 'value';
|
||||||
|
const labelKey = field.labelKey ?? 'label';
|
||||||
|
|
||||||
|
selectOptions[field.key] = data.map((item) => ({
|
||||||
|
value: item[valueKey],
|
||||||
|
label: typeof labelKey === 'function' ? labelKey(item) : item[labelKey],
|
||||||
|
}));
|
||||||
|
console.log(selectOptions);
|
||||||
// Track last fetched parent value for dependent fields
|
// Track last fetched parent value for dependent fields
|
||||||
if (field.dependsOn) {
|
if (field.dependsOn) {
|
||||||
lastFetched[field.key] = parentValue;
|
lastFetched[field.key] = parentValue;
|
||||||
|
|||||||
@ -13,17 +13,10 @@ export async function getVisit(searchQuery) {
|
|||||||
return await getById(API.PATVISIT, searchQuery)
|
return await getById(API.PATVISIT, searchQuery)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPatient(searchQuery) {
|
export async function createAdmission(newAdmissionForm) {
|
||||||
const { data: patient, error } = await getById(API.PATIENTS, searchQuery)
|
return await create(API.PATVISIT, newAdmissionForm)
|
||||||
return { patient };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createPatient(newContactForm) {
|
export async function editAdmission(editAdmissionForm) {
|
||||||
// console.log(JSON.stringify(newContactForm));
|
return await update(API.PATVISIT, editAdmissionForm)
|
||||||
return await create(API.PATIENTS, newContactForm)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function editPatient(editContactForm) {
|
|
||||||
// console.log(JSON.stringify(editContactForm));
|
|
||||||
return await update(API.PATIENTS, editContactForm)
|
|
||||||
}
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import PlusIcon from "@lucide/svelte/icons/plus";
|
import PlusIcon from "@lucide/svelte/icons/plus";
|
||||||
import Settings2Icon from "@lucide/svelte/icons/settings-2";
|
import Settings2Icon from "@lucide/svelte/icons/settings-2";
|
||||||
|
import PencilIcon from "@lucide/svelte/icons/pencil";
|
||||||
|
|
||||||
export const searchFields = [
|
export const searchFields = [
|
||||||
{
|
{
|
||||||
@ -38,6 +39,66 @@ export const searchFields = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// export const detailSections = [
|
||||||
|
// {
|
||||||
|
// class: "grid grid-cols-1 sm:grid-cols-2 gap-3",
|
||||||
|
// fields: [
|
||||||
|
// { key: "PVID", label: "Visit ID" },
|
||||||
|
// { key: "EpisodeID", label: "Episode ID" },
|
||||||
|
// { key: "", label: "Visit Class" },
|
||||||
|
// { key: "", label: "Service Class" },
|
||||||
|
// { key: "LocationID", label: "Location" },
|
||||||
|
// { key: "AttDoc", label: "Attending Doctor" },
|
||||||
|
// { key: "RefDoc", label: "Reffering Doctor" },
|
||||||
|
// { key: "AdmDoc", label: "Admitting Doctor" },
|
||||||
|
// { key: "CnsDoc", label: "Consulting Doctor" },
|
||||||
|
// { key: "", label: "Admission Date", isUTCDate: true },
|
||||||
|
// { key: "", label: "Discharge Date", isUTCDate: true },
|
||||||
|
// { key: "Diagnosis", label: "Clinical Diagnosis" },
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
// ]
|
||||||
|
|
||||||
|
export const detailSections = [
|
||||||
|
{
|
||||||
|
title: "", // No title for top row
|
||||||
|
class: "grid grid-cols-1 md:grid-cols-2 gap-4",
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
class: "space-y-3",
|
||||||
|
fields: [
|
||||||
|
{ key: "PVID", label: "Visit ID" },
|
||||||
|
{ key: "EpisodeID", label: "Episode ID" },
|
||||||
|
{ key: "VisitClass", label: "Visit Class" },
|
||||||
|
{ key: "ServiceClass", label: "Service Class" },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
class: "space-y-3",
|
||||||
|
fields: [
|
||||||
|
{ key: "LocationID", label: "Location" },
|
||||||
|
{ key: "AttDoc", label: "Attending Doctor" },
|
||||||
|
{ key: "RefDoc", label: "Referring Doctor" },
|
||||||
|
{ key: "AdmDoc", label: "Admitting Doctor" },
|
||||||
|
{ key: "CnsDoc", label: "Consulting Doctor" },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
class: "grid grid-cols-2 gap-4 items-center",
|
||||||
|
fields: [
|
||||||
|
{ key: "AdmissionDate", label: "Admission Date", isUTCDate: true },
|
||||||
|
{ key: "DischargeDate", label: "Discharge Date", isUTCDate: true },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fields: [
|
||||||
|
{ key: "Diagnosis", label: "Clinical Diagnosis" },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
export function admissionActions(masterDetail) {
|
export function admissionActions(masterDetail) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@ -51,4 +112,15 @@ export function admissionActions(masterDetail) {
|
|||||||
popoverWidth: "w-256",
|
popoverWidth: "w-256",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function viewActions(handlers){
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
Icon: PencilIcon,
|
||||||
|
label: 'Edit Patient',
|
||||||
|
onClick: handlers.editPatient,
|
||||||
|
|
||||||
|
},
|
||||||
|
]
|
||||||
}
|
}
|
||||||
@ -0,0 +1,209 @@
|
|||||||
|
import { API } from "$lib/config/api";
|
||||||
|
import EraserIcon from "@lucide/svelte/icons/eraser";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const admissionSchema = z.object({});
|
||||||
|
|
||||||
|
export const admissionInitialForm = {
|
||||||
|
InternalPVID: "",
|
||||||
|
InternalPID: "",
|
||||||
|
PVID: "",
|
||||||
|
EpisodeID: "",
|
||||||
|
DiagCode: "",
|
||||||
|
Diagnosis: "",
|
||||||
|
ADTCode: "",
|
||||||
|
LocationID: "",
|
||||||
|
AttDoc: "",
|
||||||
|
RefDoc: "",
|
||||||
|
AdmDoc: "",
|
||||||
|
CnsDoc: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const admissionDefaultErrors = {};
|
||||||
|
|
||||||
|
export const admissionFormFields = [
|
||||||
|
{
|
||||||
|
title: "Visit Information",
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
type: "row",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
key: "PVID",
|
||||||
|
label: "Visit ID",
|
||||||
|
required: false,
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "EpisodeID",
|
||||||
|
label: "Episode ID",
|
||||||
|
required: false,
|
||||||
|
type: "text",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Medical Team",
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
type: "row",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
key: "AttDoc",
|
||||||
|
label: "Attended Doctor",
|
||||||
|
required: false,
|
||||||
|
type: "select",
|
||||||
|
optionsEndpoint: `${API.BASE_URL}${API.CONTACT}`,
|
||||||
|
valueKey: "ContactID",
|
||||||
|
labelKey: (item) => `${item.Initial} - ${item.NameFirst} ${item.NameLast}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "RefDoc",
|
||||||
|
label: "Reference Doctor",
|
||||||
|
required: false,
|
||||||
|
type: "select",
|
||||||
|
optionsEndpoint: `${API.BASE_URL}${API.CONTACT}`,
|
||||||
|
valueKey: "ContactID",
|
||||||
|
labelKey: (item) => `${item.Initial} - ${item.NameFirst} ${item.NameLast}`,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "row",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
key: "AdmDoc",
|
||||||
|
label: "Admitted Doctor",
|
||||||
|
required: false,
|
||||||
|
type: "select",
|
||||||
|
optionsEndpoint: `${API.BASE_URL}${API.CONTACT}`,
|
||||||
|
valueKey: "ContactID",
|
||||||
|
labelKey: (item) => `${item.Initial} - ${item.NameFirst} ${item.NameLast}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "CnsDoc",
|
||||||
|
label: "Consulte Doctor",
|
||||||
|
required: false,
|
||||||
|
type: "select",
|
||||||
|
optionsEndpoint: `${API.BASE_URL}${API.CONTACT}`,
|
||||||
|
valueKey: "ContactID",
|
||||||
|
labelKey: (item) => `${item.Initial} - ${item.NameFirst} ${item.NameLast}`,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Visit Classification",
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
type: "row",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
key: "LocationID",
|
||||||
|
label: "Location",
|
||||||
|
required: false,
|
||||||
|
type: "select",
|
||||||
|
optionsEndpoint: `${API.BASE_URL}${API.LOCATION}`,
|
||||||
|
valueKey: "LocationID",
|
||||||
|
labelKey: (item) => `${item.LocCode} - ${item.LocFull}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "VisitClass",
|
||||||
|
label: "Visit Class",
|
||||||
|
required: false,
|
||||||
|
type: "select",
|
||||||
|
optionsEndpoint: `${API.BASE_URL}${API.VALUESET}/visit_classes`,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "row",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
key: "ServiceClass",
|
||||||
|
label: "Service Class",
|
||||||
|
required: false,
|
||||||
|
type: "select",
|
||||||
|
optionsEndpoint: `${API.BASE_URL}${API.VALUESET}/service_classes`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Discharge",
|
||||||
|
label: "Discharge Status",
|
||||||
|
required: false,
|
||||||
|
type: "toggle",
|
||||||
|
defaultValue: false,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Clinical Information",
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
type: "row",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
key: "Diagnosis",
|
||||||
|
label: "Diagnosis",
|
||||||
|
required: false,
|
||||||
|
type: "textarea",
|
||||||
|
rows: 4,
|
||||||
|
maxLength: 1000,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export function getAdmissionFormActions(handlers) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
Icon: EraserIcon,
|
||||||
|
label: 'Clear Form',
|
||||||
|
onClick: handlers.clearForm,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const admissionTemplate = {
|
||||||
|
PVID: 'PVID',
|
||||||
|
InternalPID: 'InternalPID',
|
||||||
|
EpisodeID: 'EpisodeID',
|
||||||
|
PatDiag: {
|
||||||
|
DiagCode: 'DiagCode',
|
||||||
|
Diagnosis: 'Diagnosis',
|
||||||
|
},
|
||||||
|
PatVisitADT: {
|
||||||
|
ADTCode: () => 'A04',
|
||||||
|
LocationID: 'LocationID',
|
||||||
|
AttDoc: 'AttDoc',
|
||||||
|
RefDoc: 'RefDoc',
|
||||||
|
AdmDoc: 'AdmDoc',
|
||||||
|
CnsDoc: 'CnsDoc',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function buildPayload(form, schema = admissionTemplate) {
|
||||||
|
const payload = {};
|
||||||
|
|
||||||
|
for (const [key, config] of Object.entries(schema)) {
|
||||||
|
if (typeof config === 'string') {
|
||||||
|
// Kirim nilai dari form, atau null jika tidak ada (agar key tetap ada)
|
||||||
|
payload[key] = form[config] ?? null;
|
||||||
|
}
|
||||||
|
else if (typeof config === 'function') {
|
||||||
|
payload[key] = config(form);
|
||||||
|
}
|
||||||
|
else if (typeof config === 'object' && config !== null) {
|
||||||
|
// Rekursif tanpa pengecekan panjang keys, agar objek nested selalu dibuat
|
||||||
|
payload[key] = buildPayload(form, config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
@ -1 +1,60 @@
|
|||||||
cr
|
<script>
|
||||||
|
import { useForm } from "$lib/components/composable/use-form.svelte";
|
||||||
|
import { admissionSchema, admissionInitialForm, admissionDefaultErrors, admissionFormFields, getAdmissionFormActions, buildPayload } from "$lib/components/patient/admission/config/admission-form-config";
|
||||||
|
import { createAdmission } from "$lib/components/patient/admission/api/patient-admission-api";
|
||||||
|
import { usePatientForm } from "$lib/components/composable/use-patient-form.svelte";
|
||||||
|
import FormPageContainer from "$lib/components/patient/reusable/form-page-container.svelte";
|
||||||
|
import PatientFormRenderer from "$lib/components/patient/reusable/patient-form-renderer.svelte";
|
||||||
|
|
||||||
|
let props = $props();
|
||||||
|
|
||||||
|
let formState = useForm({
|
||||||
|
schema: admissionSchema,
|
||||||
|
initialForm: admissionInitialForm,
|
||||||
|
defaultErrors: admissionDefaultErrors,
|
||||||
|
mode: 'create',
|
||||||
|
modeOpt: 'default',
|
||||||
|
saveEndpoint: createAdmission,
|
||||||
|
editEndpoint: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const helpers = usePatientForm(formState, admissionSchema);
|
||||||
|
|
||||||
|
const handlers = {
|
||||||
|
clearForm: () => {
|
||||||
|
formState.reset();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const actions = getAdmissionFormActions(handlers);
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
const payload = buildPayload(formState.form);
|
||||||
|
|
||||||
|
console.log("Payload siap kirim:", payload);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const primaryAction = $derived({
|
||||||
|
label: 'Save',
|
||||||
|
onClick: handleSave,
|
||||||
|
disabled: helpers.hasErrors || formState.isSaving.current,
|
||||||
|
loading: formState.isSaving.current
|
||||||
|
});
|
||||||
|
|
||||||
|
const secondaryActions = [];
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FormPageContainer title="Create Admission" {primaryAction} {secondaryActions} {actions}>
|
||||||
|
<PatientFormRenderer
|
||||||
|
{formState}
|
||||||
|
formFields={admissionFormFields}
|
||||||
|
uploadErrors={helpers.uploadErrors}
|
||||||
|
isChecking={helpers.isChecking}
|
||||||
|
linkToDisplay={helpers.linkToDisplay}
|
||||||
|
validateIdentifier={helpers.validateIdentifier}
|
||||||
|
validateFieldAsync={helpers.validateFieldAsync}
|
||||||
|
mode="create"
|
||||||
|
/>
|
||||||
|
</FormPageContainer>
|
||||||
@ -1 +1,112 @@
|
|||||||
vw
|
<script>
|
||||||
|
import { formatUTCDate } from "$lib/utils/formatUTCDate";
|
||||||
|
import { detailSections, viewActions } from "$lib/components/patient/admission/config/admission-config";
|
||||||
|
import TopbarWrapper from "$lib/components/topbar/topbar-wrapper.svelte";
|
||||||
|
import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte";
|
||||||
|
|
||||||
|
let props = $props();
|
||||||
|
|
||||||
|
let visit = $derived(props.masterDetail?.selectedItem?.data);
|
||||||
|
|
||||||
|
const handlers = {
|
||||||
|
editPatient: () => props.masterDetail.enterEdit(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const actions = viewActions(handlers);
|
||||||
|
|
||||||
|
function getFieldValue(field) {
|
||||||
|
if (!visit) return "-";
|
||||||
|
|
||||||
|
if (field.keys) {
|
||||||
|
return field.keys
|
||||||
|
.map(k => field.parentKey ? visit[field.parentKey]?.[k] : visit[k])
|
||||||
|
.filter(val => val && val.trim() !== "")
|
||||||
|
.join(" / ");
|
||||||
|
}
|
||||||
|
|
||||||
|
return field.parentKey ? visit[field.parentKey]?.[field.key] : visit[field.key];
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#snippet Fieldset({ value, label, isUTCDate = false })}
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<dt class="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||||
|
{label}
|
||||||
|
</dt>
|
||||||
|
<dd class="text-sm font-medium">
|
||||||
|
{#if isUTCDate}
|
||||||
|
{formatUTCDate(value)}
|
||||||
|
{:else}
|
||||||
|
{value ?? "-"}
|
||||||
|
{/if}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
{#if props.masterDetail.selectedItem}
|
||||||
|
<div class="flex flex-col px-2 py-1 gap-2 h-full w-full">
|
||||||
|
<TopbarWrapper
|
||||||
|
title={props.masterDetail.selectedItem.data.PVID}
|
||||||
|
{actions}
|
||||||
|
/>
|
||||||
|
<div class="flex-1 min-h-0 overflow-y-auto space-y-4">
|
||||||
|
{#each detailSections as section}
|
||||||
|
<div class="p-4">
|
||||||
|
{#if section.groups}
|
||||||
|
<div class={section.class}>
|
||||||
|
{#each section.groups as group}
|
||||||
|
<div>
|
||||||
|
<div class={group.class}>
|
||||||
|
{#each group.fields as field}
|
||||||
|
{@render Fieldset({
|
||||||
|
label: field.label,
|
||||||
|
value: getFieldValue(field),
|
||||||
|
isUTCDate: field.isUTCDate
|
||||||
|
})}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class={section.class}>
|
||||||
|
{#each section.fields as field}
|
||||||
|
{@render Fieldset({
|
||||||
|
label: field.label,
|
||||||
|
value: getFieldValue(field),
|
||||||
|
isUTCDate: field.isUTCDate
|
||||||
|
})}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<ReusableEmpty desc="Select a visit to see details"/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- {#if props.masterDetail.selectedItem}
|
||||||
|
<div class="flex flex-col px-2 py-1 gap-2 h-full w-full">
|
||||||
|
<TopbarWrapper title={props.masterDetail.selectedItem.data.PVID} {actions} />
|
||||||
|
<div class="flex-1 min-h-0 overflow-y-auto space-y-4">
|
||||||
|
{#each detailSections as section}
|
||||||
|
<div class="p-4">
|
||||||
|
<div class={section.class}>
|
||||||
|
{#each section.fields as field}
|
||||||
|
{@render Fieldset({
|
||||||
|
label: field.label,
|
||||||
|
value: getFieldValue(field),
|
||||||
|
isUTCDate: field.isUTCDate,
|
||||||
|
})}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<ReusableEmpty desc="Select a visit to see details"/>
|
||||||
|
{/if} -->
|
||||||
@ -6,7 +6,6 @@
|
|||||||
import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte";
|
import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte";
|
||||||
|
|
||||||
let props = $props();
|
let props = $props();
|
||||||
|
|
||||||
let patient = $derived(props.masterDetail?.selectedItem?.patient);
|
let patient = $derived(props.masterDetail?.selectedItem?.patient);
|
||||||
|
|
||||||
const handlers = {
|
const handlers = {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
|
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
|
||||||
import * as Select from "$lib/components/ui/select/index.js";
|
import * as Select from "$lib/components/ui/select/index.js";
|
||||||
|
import * as ToggleGroup from "$lib/components/ui/toggle-group/index.js";
|
||||||
import { Button } from "$lib/components/ui/button/index.js";
|
import { Button } from "$lib/components/ui/button/index.js";
|
||||||
import { Input } from "$lib/components/ui/input/index.js";
|
import { Input } from "$lib/components/ui/input/index.js";
|
||||||
import { Label } from "$lib/components/ui/label/index.js";
|
import { Label } from "$lib/components/ui/label/index.js";
|
||||||
@ -11,6 +12,8 @@
|
|||||||
import LinktoModal from "$lib/components/patient/list/modal/linkto-modal.svelte";
|
import LinktoModal from "$lib/components/patient/list/modal/linkto-modal.svelte";
|
||||||
import ReusableUpload from "$lib/components/reusable/reusable-upload.svelte";
|
import ReusableUpload from "$lib/components/reusable/reusable-upload.svelte";
|
||||||
import ChevronUpIcon from "@lucide/svelte/icons/chevron-up";
|
import ChevronUpIcon from "@lucide/svelte/icons/chevron-up";
|
||||||
|
import CheckIcon from "@lucide/svelte/icons/check";
|
||||||
|
import XIcon from "@lucide/svelte/icons/x";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
formState,
|
formState,
|
||||||
@ -75,7 +78,7 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#snippet Fieldset({ key, label, required, type, optionsEndpoint, options, validateOn, dependsOn, endpointParamKey })}
|
{#snippet Fieldset({ key, label, required, type, optionsEndpoint, options, validateOn, dependsOn, endpointParamKey, valueKey, labelKey })}
|
||||||
<div class="flex w-full flex-col gap-1.5">
|
<div class="flex w-full flex-col gap-1.5">
|
||||||
<div class="flex justify-between items-center w-full">
|
<div class="flex justify-between items-center w-full">
|
||||||
<Label>{label}</Label>
|
<Label>{label}</Label>
|
||||||
@ -184,7 +187,7 @@
|
|||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
if (open && optionsEndpoint) {
|
if (open && optionsEndpoint) {
|
||||||
formState.fetchOptions(
|
formState.fetchOptions(
|
||||||
{ key, optionsEndpoint, dependsOn, endpointParamKey },
|
{ key, optionsEndpoint, dependsOn, endpointParamKey, valueKey, labelKey },
|
||||||
formState.form
|
formState.form
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -279,6 +282,26 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
{:else if type === "toggle"}
|
||||||
|
<div class="flex items-center w-full">
|
||||||
|
<ToggleGroup.Root variant="outline" type="single" class="w-full" >
|
||||||
|
<ToggleGroup.Item
|
||||||
|
value="yes"
|
||||||
|
aria-label="Toggle Yes"
|
||||||
|
class="flex gap-2 px-4 w-1/2 transition-all data-[state=on]:bg-green-50 data-[state=on]:text-green-700 data-[state=on]:border-green-200 data-[state=on]:*:[svg]:stroke-[5px]"
|
||||||
|
>
|
||||||
|
<CheckIcon class="h-4 w-4" />
|
||||||
|
</ToggleGroup.Item>
|
||||||
|
|
||||||
|
<ToggleGroup.Item
|
||||||
|
value="no"
|
||||||
|
aria-label="Toggle No"
|
||||||
|
class="flex gap-2 px-4 w-1/2 transition-all data-[state=on]:bg-red-50 data-[state=on]:text-red-700 data-[state=on]:border-red-200 data-[state=on]:*:[svg]:stroke-[5px]"
|
||||||
|
>
|
||||||
|
<XIcon class="h-6 w-6" />
|
||||||
|
</ToggleGroup.Item>
|
||||||
|
</ToggleGroup.Root>
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
|
|||||||
10
src/lib/components/ui/toggle-group/index.js
Normal file
10
src/lib/components/ui/toggle-group/index.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import Root from "./toggle-group.svelte";
|
||||||
|
import Item from "./toggle-group-item.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Item,
|
||||||
|
//
|
||||||
|
Root as ToggleGroup,
|
||||||
|
Item as ToggleGroupItem,
|
||||||
|
};
|
||||||
35
src/lib/components/ui/toggle-group/toggle-group-item.svelte
Normal file
35
src/lib/components/ui/toggle-group/toggle-group-item.svelte
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<script>
|
||||||
|
import { ToggleGroup as ToggleGroupPrimitive } from "bits-ui";
|
||||||
|
import { getToggleGroupCtx } from "./toggle-group.svelte";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
import { toggleVariants } from "$lib/components/ui/toggle/index.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
value = $bindable(),
|
||||||
|
class: className,
|
||||||
|
size,
|
||||||
|
variant,
|
||||||
|
...restProps
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
const ctx = getToggleGroupCtx();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ToggleGroupPrimitive.Item
|
||||||
|
bind:ref
|
||||||
|
data-slot="toggle-group-item"
|
||||||
|
data-variant={ctx.variant || variant}
|
||||||
|
data-size={ctx.size || size}
|
||||||
|
data-spacing={ctx.spacing}
|
||||||
|
class={cn(
|
||||||
|
toggleVariants({
|
||||||
|
variant: ctx.variant || variant,
|
||||||
|
size: ctx.size || size,
|
||||||
|
}),
|
||||||
|
"w-auto min-w-0 shrink-0 px-3 focus:z-10 focus-visible:z-10 data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{value}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
52
src/lib/components/ui/toggle-group/toggle-group.svelte
Normal file
52
src/lib/components/ui/toggle-group/toggle-group.svelte
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<script module>
|
||||||
|
import { getContext, setContext } from "svelte";
|
||||||
|
import { toggleVariants } from "$lib/components/ui/toggle/index.js";
|
||||||
|
|
||||||
|
export function setToggleGroupCtx(props) {
|
||||||
|
setContext("toggleGroup", props);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getToggleGroupCtx() {
|
||||||
|
return getContext("toggleGroup");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { ToggleGroup as ToggleGroupPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
value = $bindable(),
|
||||||
|
class: className,
|
||||||
|
size = "default",
|
||||||
|
spacing = 0,
|
||||||
|
variant = "default",
|
||||||
|
...restProps
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
setToggleGroupCtx({
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
spacing,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Discriminated Unions + Destructing (required for bindable) do not
|
||||||
|
get along, so we shut typescript up by casting `value` to `never`.
|
||||||
|
-->
|
||||||
|
<ToggleGroupPrimitive.Root
|
||||||
|
bind:value={value}
|
||||||
|
bind:ref
|
||||||
|
data-slot="toggle-group"
|
||||||
|
data-variant={variant}
|
||||||
|
data-size={size}
|
||||||
|
data-spacing={spacing}
|
||||||
|
style={`--gap: ${spacing}`}
|
||||||
|
class={cn(
|
||||||
|
"group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
11
src/lib/components/ui/toggle/index.js
Normal file
11
src/lib/components/ui/toggle/index.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import Root from "./toggle.svelte";
|
||||||
|
export {
|
||||||
|
toggleVariants,
|
||||||
|
|
||||||
|
} from "./toggle.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
//
|
||||||
|
Root as Toggle,
|
||||||
|
};
|
||||||
46
src/lib/components/ui/toggle/toggle.svelte
Normal file
46
src/lib/components/ui/toggle/toggle.svelte
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<script module>
|
||||||
|
import { tv } from "tailwind-variants";
|
||||||
|
|
||||||
|
export const toggleVariants = tv({
|
||||||
|
base: "hover:bg-muted hover:text-muted-foreground data-[state=on]:bg-accent data-[state=on]:text-accent-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-transparent",
|
||||||
|
outline:
|
||||||
|
"border-input hover:bg-accent hover:text-accent-foreground border bg-transparent shadow-xs",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-9 min-w-9 px-2",
|
||||||
|
sm: "h-8 min-w-8 px-1.5",
|
||||||
|
lg: "h-10 min-w-10 px-2.5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { Toggle as TogglePrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
pressed = $bindable(false),
|
||||||
|
class: className,
|
||||||
|
size = "default",
|
||||||
|
variant = "default",
|
||||||
|
...restProps
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<TogglePrimitive.Root
|
||||||
|
bind:ref
|
||||||
|
bind:pressed
|
||||||
|
data-slot="toggle"
|
||||||
|
class={cn(toggleVariants({ variant, size }), className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@ -12,7 +12,6 @@
|
|||||||
return await getVisit(row.PVID);
|
return await getVisit(row.PVID);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
$inspect(masterDetail.selectedItem)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex w-full h-full overflow-hidden">
|
<div class="flex w-full h-full overflow-hidden">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user