mirror of
https://github.com/faiztyanirh/clqms-shadcn-v1.git
synced 2026-04-27 11:25:53 +07:00
add page/feature dictionary contact
This commit is contained in:
parent
13617ba532
commit
a3811bba70
@ -10,6 +10,9 @@
|
|||||||
import SendIcon from "@lucide/svelte/icons/send";
|
import SendIcon from "@lucide/svelte/icons/send";
|
||||||
import Settings2Icon from "@lucide/svelte/icons/settings-2";
|
import Settings2Icon from "@lucide/svelte/icons/settings-2";
|
||||||
import SquareTerminalIcon from "@lucide/svelte/icons/square-terminal";
|
import SquareTerminalIcon from "@lucide/svelte/icons/square-terminal";
|
||||||
|
import ArchiveIcon from "@lucide/svelte/icons/archive";
|
||||||
|
import FlaskConicalIcon from "@lucide/svelte/icons/flask-conical";
|
||||||
|
import BrickWallIcon from "@lucide/svelte/icons/brick-wall";
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
user: {
|
user: {
|
||||||
@ -73,7 +76,7 @@
|
|||||||
{
|
{
|
||||||
title: "Sample",
|
title: "Sample",
|
||||||
url: "/dictionary",
|
url: "/dictionary",
|
||||||
icon: BookOpenIcon,
|
icon: ArchiveIcon,
|
||||||
submenus: [
|
submenus: [
|
||||||
{
|
{
|
||||||
title: "Container",
|
title: "Container",
|
||||||
@ -84,7 +87,7 @@
|
|||||||
{
|
{
|
||||||
title: "Organization",
|
title: "Organization",
|
||||||
url: "/dictionary",
|
url: "/dictionary",
|
||||||
icon: BookOpenIcon,
|
icon: BrickWallIcon,
|
||||||
submenus: [
|
submenus: [
|
||||||
{
|
{
|
||||||
title: "Account",
|
title: "Account",
|
||||||
@ -111,7 +114,7 @@
|
|||||||
{
|
{
|
||||||
title: "Test",
|
title: "Test",
|
||||||
url: "/dictionary/test",
|
url: "/dictionary/test",
|
||||||
icon: BookOpenIcon,
|
icon: FlaskConicalIcon,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
18
src/lib/components/dictionary/contact/api/contact-api.js
Normal file
18
src/lib/components/dictionary/contact/api/contact-api.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { API } from '$lib/config/api.js';
|
||||||
|
import { getById, searchWithParams, create, update } from '$lib/api/api-client';
|
||||||
|
|
||||||
|
export async function getContacts(searchQuery) {
|
||||||
|
return await searchWithParams(API.CONTACT, searchQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getContact(searchQuery) {
|
||||||
|
return await getById(API.CONTACT, searchQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createContact(newContactForm) {
|
||||||
|
return await create(API.CONTACT, newContactForm)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function editContact(editContactForm) {
|
||||||
|
return await update(API.CONTACT, editContactForm)
|
||||||
|
}
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
import PlusIcon from "@lucide/svelte/icons/plus";
|
||||||
|
import Settings2Icon from "@lucide/svelte/icons/settings-2";
|
||||||
|
import PencilIcon from "@lucide/svelte/icons/pencil";
|
||||||
|
import { API } from "$lib/config/api";
|
||||||
|
|
||||||
|
export const searchFields = [
|
||||||
|
{
|
||||||
|
key: "Initial",
|
||||||
|
label: "Initial",
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const detailSections = [
|
||||||
|
{
|
||||||
|
title: "",
|
||||||
|
class: "grid grid-cols-1 md:grid-cols-2 gap-4",
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
class: "space-y-3",
|
||||||
|
fields: [
|
||||||
|
{ key: "Title", label: "Title" },
|
||||||
|
{ key: "Initial", label: "Initial" },
|
||||||
|
{ key: "NameFirst", label: "First Name" },
|
||||||
|
{ key: "NameLast", label: "Last Name" },
|
||||||
|
{ key: "Birthdate", label: "Birthdate" },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
class: "space-y-3",
|
||||||
|
fields: [
|
||||||
|
{ key: "EmailAddress1", label: "Email Address 1" },
|
||||||
|
{ key: "EmailAddress2", label: "Email Address 2" },
|
||||||
|
{ key: "Phone", label: "Phone" },
|
||||||
|
{ key: "MobilePhone1", label: "Mobile Phone 1" },
|
||||||
|
{ key: "MobilePhone2", label: "Mobile Phone 2" },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
class: "grid grid-cols-2 gap-4 items-center",
|
||||||
|
fields: [
|
||||||
|
{ key: "Specialty", label: "Specialty" },
|
||||||
|
{ key: "SubSpecialty", label: "Sub Specialty" },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function contactActions(masterDetail) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
Icon: PlusIcon,
|
||||||
|
label: 'Add Contact',
|
||||||
|
onClick: () => masterDetail.enterCreate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Icon: Settings2Icon,
|
||||||
|
label: 'Search Parameters',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function viewActions(handlers){
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
Icon: PencilIcon,
|
||||||
|
label: 'Edit Contact',
|
||||||
|
onClick: handlers.editContact,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -0,0 +1,224 @@
|
|||||||
|
import { API } from "$lib/config/api";
|
||||||
|
import EraserIcon from "@lucide/svelte/icons/eraser";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const contactSchema = z.object({
|
||||||
|
NameFirst: z.string().min(1, "Required"),
|
||||||
|
Initial: z.string().min(1, "Required"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const contactInitialForm = {
|
||||||
|
ContactID: '',
|
||||||
|
NameFirst: '',
|
||||||
|
NameLast: '',
|
||||||
|
Title: '',
|
||||||
|
Initial: '',
|
||||||
|
Birthdate: '',
|
||||||
|
EmailAddress1: '',
|
||||||
|
EmailAddress2: '',
|
||||||
|
Phone: '',
|
||||||
|
MobilePhone1: '',
|
||||||
|
MobilePhone2: '',
|
||||||
|
Specialty: '',
|
||||||
|
SubSpecialty: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const contactDetailInitialForm = {
|
||||||
|
SiteID: '',
|
||||||
|
ContactDetID: '',
|
||||||
|
ContactCode: '',
|
||||||
|
ContactEmail: '',
|
||||||
|
OccupationID: '',
|
||||||
|
JobTitle: '',
|
||||||
|
Department: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const contactDefaultErrors = {
|
||||||
|
NameFirst: "Required",
|
||||||
|
Initial: "Required",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const contactFormFields = [
|
||||||
|
{
|
||||||
|
title: "Personal Information",
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
type: "row",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
key: "NameFirst",
|
||||||
|
label: "First Name",
|
||||||
|
required: true,
|
||||||
|
type: "text",
|
||||||
|
validateOn: ["input"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "NameLast",
|
||||||
|
label: "Last Name",
|
||||||
|
required: false,
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "row",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
key: "Title",
|
||||||
|
label: "Title",
|
||||||
|
required: false,
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Initial",
|
||||||
|
label: "Initial",
|
||||||
|
required: true,
|
||||||
|
type: "text",
|
||||||
|
validateOn: ["input"]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "row",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
key: "Birthdate",
|
||||||
|
label: "Birthdate",
|
||||||
|
required: false,
|
||||||
|
type: "date",
|
||||||
|
fullWidth: false
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Contact Information",
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
type: "row",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
key: "EmailAddress1",
|
||||||
|
label: "Email Address 1",
|
||||||
|
required: false,
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "EmailAddress2",
|
||||||
|
label: "Email Address 2",
|
||||||
|
required: false,
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "row",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
key: "MobilePhone1",
|
||||||
|
label: "Mobile Phone 1",
|
||||||
|
required: false,
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "MobilePhone2",
|
||||||
|
label: "Mobile Phone 2",
|
||||||
|
required: false,
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Phone",
|
||||||
|
label: "Phone",
|
||||||
|
required: false,
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Professional Detail",
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
type: "row",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
key: "Specialty",
|
||||||
|
label: "Specialty",
|
||||||
|
required: false,
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "SubSpecialty",
|
||||||
|
label: "Sub Specialty",
|
||||||
|
required: false,
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export const contactDetailFormFields = [
|
||||||
|
{
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
type: "row",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
key: "SiteID",
|
||||||
|
label: "Site",
|
||||||
|
required: false,
|
||||||
|
type: "select",
|
||||||
|
optionsEndpoint: `${API.BASE_URL}${API.SITE}`,
|
||||||
|
valueKey: "SiteID",
|
||||||
|
labelKey: "SiteName",
|
||||||
|
},
|
||||||
|
{ key: "ContactCode", label: "Code", required: false, type: "text" },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "row",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
key: "Department",
|
||||||
|
label: "Department",
|
||||||
|
required: false,
|
||||||
|
type: "select",
|
||||||
|
optionsEndpoint: `${API.BASE_URL}${API.DEPARTMENT}`,
|
||||||
|
valueKey: "DepartmentID",
|
||||||
|
labelKey: "DepartmentName",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "OccupationID",
|
||||||
|
label: "Occupation",
|
||||||
|
required: false,
|
||||||
|
type: "select",
|
||||||
|
optionsEndpoint: `${API.BASE_URL}${API.OCCUPATION}`,
|
||||||
|
valueKey: "OccupationID",
|
||||||
|
labelKey: "OccText",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "row",
|
||||||
|
columns: [
|
||||||
|
{ key: "JobTitle", label: "Job Title", required: false, type: "text" },
|
||||||
|
{ key: "ContactEmail", label: "Email", required: false, type: "text" },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export function getContactFormActions(handlers) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
Icon: EraserIcon,
|
||||||
|
label: 'Clear Form',
|
||||||
|
onClick: handlers.clearForm,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
<script>
|
||||||
|
import { useDictionaryForm } from "$lib/components/composable/use-dictionary-form.svelte";
|
||||||
|
import FormPageContainer from "$lib/components/reusable/form/form-page-container.svelte";
|
||||||
|
import DictionaryFormRenderer from "$lib/components/reusable/form/dictionary-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 = useDictionaryForm(formState);
|
||||||
|
|
||||||
|
const handlers = {
|
||||||
|
clearForm: () => {
|
||||||
|
formState.reset();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const actions = formActions(handlers);
|
||||||
|
|
||||||
|
let showConfirm = $state(false);
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
const result = await formState.save(masterDetail.mode);
|
||||||
|
|
||||||
|
toast('Contact Created!');
|
||||||
|
masterDetail?.exitForm(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const primaryAction = $derived({
|
||||||
|
label: 'Save',
|
||||||
|
onClick: handleSave,
|
||||||
|
disabled: helpers.hasErrors || formState.isSaving.current,
|
||||||
|
loading: formState.isSaving.current
|
||||||
|
});
|
||||||
|
|
||||||
|
const secondaryActions = [];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FormPageContainer title="Create Contact" {primaryAction} {secondaryActions} {actions}>
|
||||||
|
<DictionaryFormRenderer
|
||||||
|
{formState}
|
||||||
|
formFields={formFields}
|
||||||
|
mode="create"
|
||||||
|
/>
|
||||||
|
</FormPageContainer>
|
||||||
|
|
||||||
|
<ReusableAlertDialog
|
||||||
|
bind:open={masterDetail.showExitConfirm}
|
||||||
|
onConfirm={masterDetail.confirmExit}
|
||||||
|
/>
|
||||||
281
src/lib/components/dictionary/contact/page/edit-page.svelte
Normal file
281
src/lib/components/dictionary/contact/page/edit-page.svelte
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
<script>
|
||||||
|
import { useDictionaryForm } from "$lib/components/composable/use-dictionary-form.svelte";
|
||||||
|
import FormPageContainer from "$lib/components/reusable/form/form-page-container.svelte";
|
||||||
|
import DictionaryFormRenderer from "$lib/components/reusable/form/dictionary-form-renderer.svelte";
|
||||||
|
import { toast } from "svelte-sonner";
|
||||||
|
import { untrack } from "svelte";
|
||||||
|
import { API } from "$lib/config/api";
|
||||||
|
import ReusableAlertDialog from "$lib/components/reusable/reusable-alert-dialog.svelte";
|
||||||
|
import { Separator } from "$lib/components/ui/separator/index.js";
|
||||||
|
import PlusIcon from "@lucide/svelte/icons/plus";
|
||||||
|
import * as Card from "$lib/components/ui/card/index.js";
|
||||||
|
import { Badge } from "$lib/components/ui/badge/index.js";
|
||||||
|
import TopbarWrapper from "$lib/components/topbar/topbar-wrapper.svelte";
|
||||||
|
import { contactDetailInitialForm, contactDetailFormFields } from "$lib/components/dictionary/contact/config/contact-form-config";
|
||||||
|
import { Button } from "$lib/components/ui/button/index.js";
|
||||||
|
import { useForm } from "$lib/components/composable/use-form.svelte";
|
||||||
|
import XIcon from "@lucide/svelte/icons/x";
|
||||||
|
import Edit2Icon from "@lucide/svelte/icons/edit-2";
|
||||||
|
|
||||||
|
let props = $props();
|
||||||
|
|
||||||
|
const { masterDetail, formFields, formActions, schema, initialForm } = props.context;
|
||||||
|
|
||||||
|
const { formState } = masterDetail;
|
||||||
|
|
||||||
|
const detailFormState = useForm({
|
||||||
|
schema: null,
|
||||||
|
initialForm: contactDetailInitialForm,
|
||||||
|
defaultErrors: {},
|
||||||
|
mode: 'create',
|
||||||
|
modeOpt: 'default',
|
||||||
|
saveEndpoint: null,
|
||||||
|
editEndpoint: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
let showDetailForm = $state(false);
|
||||||
|
|
||||||
|
let editingDetailIndex = $state(null);
|
||||||
|
|
||||||
|
let isEditingDetail = $derived(editingDetailIndex !== null);
|
||||||
|
|
||||||
|
const helpers = useDictionaryForm(formState);
|
||||||
|
|
||||||
|
let showConfirm = $state(false);
|
||||||
|
|
||||||
|
function getLabel(fieldKey, value) {
|
||||||
|
if (!detailFormState.selectOptions?.[fieldKey]) return value;
|
||||||
|
const option = detailFormState.selectOptions[fieldKey].find(opt => opt.value === value);
|
||||||
|
return option?.label || value || "-";
|
||||||
|
}
|
||||||
|
$inspect(detailFormState.selectOptions)
|
||||||
|
$effect(() => {
|
||||||
|
untrack(() => {
|
||||||
|
formFields.forEach(group => {
|
||||||
|
group.rows.forEach(row => {
|
||||||
|
row.columns.forEach(col => {
|
||||||
|
if (col.type === "group") {
|
||||||
|
col.columns.forEach(child => {
|
||||||
|
if (child.type === "select" && child.optionsEndpoint) {
|
||||||
|
formState.fetchOptions(child, formState.form);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if ((col.type === "select") && col.optionsEndpoint) {
|
||||||
|
formState.fetchOptions(col, formState.form);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
untrack(() => {
|
||||||
|
contactDetailFormFields.forEach(group => {
|
||||||
|
group.rows.forEach(row => {
|
||||||
|
row.columns.forEach(col => {
|
||||||
|
if (col.type === "select" && col.optionsEndpoint) {
|
||||||
|
detailFormState.fetchOptions(col, detailFormState.form);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleEdit() {
|
||||||
|
const customPayload = {
|
||||||
|
...formState.form,
|
||||||
|
Details: masterDetail.selectedItem?.data?.Details || []
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await formState.save(masterDetail.mode, customPayload);
|
||||||
|
|
||||||
|
if (result.status === 'success') {
|
||||||
|
console.log('Contact updated successfully');
|
||||||
|
toast('Contact Updated!');
|
||||||
|
masterDetail.exitForm(true);
|
||||||
|
} else {
|
||||||
|
console.error('Failed to update contact:', result.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const primaryAction = $derived({
|
||||||
|
label: 'Edit',
|
||||||
|
onClick: handleEdit,
|
||||||
|
disabled: helpers.hasErrors || formState.isSaving.current,
|
||||||
|
loading: formState.isSaving.current
|
||||||
|
});
|
||||||
|
|
||||||
|
const secondaryActions = [];
|
||||||
|
|
||||||
|
const actionsDetail = [
|
||||||
|
{
|
||||||
|
Icon: PlusIcon,
|
||||||
|
label: 'Add Contact Detail',
|
||||||
|
onClick: () => addDetail(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function addDetail() {
|
||||||
|
editingDetailIndex = null; // Mode create baru
|
||||||
|
detailFormState.reset(); // Reset form ke initialForm
|
||||||
|
detailFormState.setForm({ ...contactDetailInitialForm }); // Set form kosong
|
||||||
|
showDetailForm = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveDetail() {
|
||||||
|
// Ambil current form dari detailFormState.form
|
||||||
|
const newDetail = { ...detailFormState.form };
|
||||||
|
|
||||||
|
if (isEditingDetail) {
|
||||||
|
// Mode edit: update detail yang ada
|
||||||
|
masterDetail.selectedItem.data.Details[editingDetailIndex] = newDetail;
|
||||||
|
toast('Contact Detail Updated!');
|
||||||
|
} else {
|
||||||
|
// Mode create: tambah detail baru
|
||||||
|
if (!masterDetail.selectedItem.data.Details) {
|
||||||
|
masterDetail.selectedItem.data.Details = [];
|
||||||
|
}
|
||||||
|
masterDetail.selectedItem.data.Details.push(newDetail);
|
||||||
|
toast('Contact Detail Added!');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset form dan tutup form
|
||||||
|
detailFormState.reset();
|
||||||
|
detailFormState.setForm({ ...contactDetailInitialForm });
|
||||||
|
editingDetailIndex = null;
|
||||||
|
showDetailForm = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function editDetail(index) {
|
||||||
|
const detailToEdit = masterDetail.selectedItem.data.Details[index];
|
||||||
|
editingDetailIndex = index; // Set mode edit dengan index
|
||||||
|
detailFormState.setForm({ ...detailToEdit }); // Load data ke form
|
||||||
|
showDetailForm = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelDetail() {
|
||||||
|
detailFormState.reset();
|
||||||
|
detailFormState.setForm({ ...contactDetailInitialForm });
|
||||||
|
editingDetailIndex = null;
|
||||||
|
showDetailForm = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeDetail(index) {
|
||||||
|
masterDetail.selectedItem.data.Details = masterDetail.selectedItem.data.Details.filter((_, i) => i !== index);
|
||||||
|
toast('Contact Detail Removed!');
|
||||||
|
|
||||||
|
// Jika sedang mengedit detail yang dihapus, reset form
|
||||||
|
if (editingDetailIndex === index) {
|
||||||
|
cancelDetail();
|
||||||
|
} else if (editingDetailIndex !== null && editingDetailIndex > index) {
|
||||||
|
// Adjust index jika mengedit detail setelah yang dihapus
|
||||||
|
editingDetailIndex--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FormPageContainer title="Edit Contact" {primaryAction} {secondaryActions}>
|
||||||
|
<DictionaryFormRenderer
|
||||||
|
{formState}
|
||||||
|
formFields={formFields}
|
||||||
|
mode="edit"
|
||||||
|
/>
|
||||||
|
<Separator class="my-4"/>
|
||||||
|
<div class="flex flex-col px-2 py-1 gap-2 h-fit w-full">
|
||||||
|
<TopbarWrapper
|
||||||
|
title="Contact Detail"
|
||||||
|
actions={actionsDetail}
|
||||||
|
/>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
{#if showDetailForm}
|
||||||
|
<Card.Root class="w-full gap-2 2xl:gap-4 py-2 2xl:py-4">
|
||||||
|
<Card.Content class="space-y-3">
|
||||||
|
<DictionaryFormRenderer
|
||||||
|
formState={detailFormState}
|
||||||
|
formFields={contactDetailFormFields}
|
||||||
|
/>
|
||||||
|
</Card.Content>
|
||||||
|
<Card.Footer class="flex justify-end flex-end gap-2">
|
||||||
|
<Button size="sm" onclick={saveDetail}>
|
||||||
|
{isEditingDetail ? 'Update Detail' : 'Save Detail'}
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="outline" onclick={cancelDetail}>Cancel</Button>
|
||||||
|
</Card.Footer>
|
||||||
|
</Card.Root>
|
||||||
|
{/if}
|
||||||
|
{#each masterDetail.selectedItem?.data?.Details as contactdetail, index}
|
||||||
|
<Card.Root class="w-full gap-2 2xl:gap-4 py-2 2xl:py-4">
|
||||||
|
<Card.Header>
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<Card.Title class="text-sm font-medium">
|
||||||
|
{contactdetail.ContactCode || "null"}
|
||||||
|
</Card.Title>
|
||||||
|
<Card.Description class="text-sm font-medium">
|
||||||
|
{contactdetail.ContactEmail || "null"}
|
||||||
|
</Card.Description>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<Badge variant="outline" class="text-xs">
|
||||||
|
{getLabel('SiteID', contactdetail.SiteID)}
|
||||||
|
</Badge>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost" class="size-7"
|
||||||
|
onclick={() => editDetail(index)}
|
||||||
|
>
|
||||||
|
<Edit2Icon class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost" class="size-7"
|
||||||
|
onclick={() => removeDetail(index)}
|
||||||
|
>
|
||||||
|
<XIcon class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content class="space-y-3">
|
||||||
|
<div class="grid grid-cols-3 gap-3">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||||
|
Department
|
||||||
|
</p>
|
||||||
|
<p class="text-sm font-medium">
|
||||||
|
{getLabel('Department', contactdetail.Department)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||||
|
Job Title
|
||||||
|
</p>
|
||||||
|
<p class="text-sm font-medium">
|
||||||
|
{contactdetail.JobTitle || "-"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||||
|
Occupation
|
||||||
|
</p>
|
||||||
|
<p class="text-sm font-medium">
|
||||||
|
<!-- {contactdetail.OccupationID || "-"} -->
|
||||||
|
{getLabel('OccupationID', contactdetail.OccupationID)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FormPageContainer>
|
||||||
|
|
||||||
|
<ReusableAlertDialog
|
||||||
|
bind:open={masterDetail.showExitConfirm}
|
||||||
|
onConfirm={masterDetail.confirmExit}
|
||||||
|
/>
|
||||||
@ -0,0 +1,68 @@
|
|||||||
|
<script>
|
||||||
|
import { contactColumns } from "$lib/components/dictionary/contact/table/contact-columns";
|
||||||
|
import { getContacts, getContact } from "$lib/components/dictionary/contact/api/contact-api";
|
||||||
|
import { useSearch } from "$lib/components/composable/use-search.svelte";
|
||||||
|
import { searchFields, contactActions } from "$lib/components/dictionary/contact/config/contact-config";
|
||||||
|
import TopbarWrapper from "$lib/components/topbar/topbar-wrapper.svelte";
|
||||||
|
import ReusableSearchParam from "$lib/components/reusable/reusable-search-param.svelte";
|
||||||
|
import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte";
|
||||||
|
import ReusableDataTable from "$lib/components/reusable/reusable-data-table.svelte";
|
||||||
|
import UserXIcon from "@lucide/svelte/icons/user-x";
|
||||||
|
|
||||||
|
let props = $props();
|
||||||
|
|
||||||
|
const search = useSearch(searchFields, getContacts);
|
||||||
|
const initialForm = props.masterDetail.formState.form;
|
||||||
|
const actions = contactActions(props.masterDetail, initialForm)
|
||||||
|
actions.find(a => a.label === 'Search Parameters').popoverContent = searchParamSnippet;
|
||||||
|
|
||||||
|
let activeRowId = $state(null);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#snippet searchParamSnippet()}
|
||||||
|
<ReusableSearchParam {searchFields}
|
||||||
|
bind:searchQuery={search.searchQuery} onSearch={search.handleSearch} onReset={search.handleReset} isLoading={search.isLoading}
|
||||||
|
selectOptions={search.selectOptions} loadingOptions={search.loadingOptions} fetchOptions={search.fetchOptions}
|
||||||
|
/>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
onclick={() => props.masterDetail.isFormMode && props.masterDetail.exitForm()}
|
||||||
|
onkeydown={(e) => e.key === 'Enter' && props.masterDetail.isFormMode && props.masterDetail.exitForm()}
|
||||||
|
class={`
|
||||||
|
${props.masterDetail.isMobile ? "w-full" : props.masterDetail.isFormMode ? "w-[3%] cursor-pointer" : "w-[35%]"}
|
||||||
|
transition-all duration-300 flex flex-col items-center p-2 h-full overflow-y-auto
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<div class={`flex w-full ${props.masterDetail.isFormMode ? "flex-col justify-center h-full items-center" : "flex-col justify-start h-full"}`} >
|
||||||
|
{#if props.masterDetail.isFormMode}
|
||||||
|
<span class="flex flex-col items-center justify-center gap-4 tracking-widest font-semibold select-none">
|
||||||
|
{#each "CONTACT".split("") as c}
|
||||||
|
<span class="leading-none">{c}</span>
|
||||||
|
{/each}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if !props.masterDetail.isFormMode}
|
||||||
|
<div role="button" tabindex="0" class="flex flex-1 flex-col" onclick={(e) => e.stopPropagation()} onkeydown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<TopbarWrapper {actions}/>
|
||||||
|
<div class="flex-1 w-full h-full">
|
||||||
|
{#if search.searchData.length > 0}
|
||||||
|
<ReusableDataTable data={search.searchData} columns={contactColumns} handleRowClick={props.masterDetail.select} {activeRowId} rowIdKey="ContactID"/>
|
||||||
|
{:else}
|
||||||
|
<div class="flex h-full">
|
||||||
|
<ReusableEmpty icon={UserXIcon} desc="Try searching from search parameters"/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
157
src/lib/components/dictionary/contact/page/view-page.svelte
Normal file
157
src/lib/components/dictionary/contact/page/view-page.svelte
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
<script>
|
||||||
|
import { formatUTCDate } from "$lib/utils/formatUTCDate";
|
||||||
|
import { detailSections, viewActions } from "$lib/components/dictionary/contact/config/contact-config";
|
||||||
|
import TopbarWrapper from "$lib/components/topbar/topbar-wrapper.svelte";
|
||||||
|
import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte";
|
||||||
|
import UserXIcon from "@lucide/svelte/icons/user-x";
|
||||||
|
import { Separator } from "$lib/components/ui/separator/index.js";
|
||||||
|
import PlusIcon from "@lucide/svelte/icons/plus";
|
||||||
|
import * as Card from "$lib/components/ui/card/index.js";
|
||||||
|
import { Badge } from "$lib/components/ui/badge/index.js";
|
||||||
|
|
||||||
|
let props = $props();
|
||||||
|
|
||||||
|
const { masterDetail, formFields, formActions, schema } = props.context;
|
||||||
|
|
||||||
|
let contact = $derived(masterDetail?.selectedItem?.data);
|
||||||
|
|
||||||
|
const handlers = {
|
||||||
|
editContact: () => masterDetail.enterEdit("data"),
|
||||||
|
};
|
||||||
|
|
||||||
|
const actions = viewActions(handlers);
|
||||||
|
const actionsDetail = [
|
||||||
|
// {
|
||||||
|
// Icon: PlusIcon,
|
||||||
|
// label: 'Add Contact Detail',
|
||||||
|
// },
|
||||||
|
];
|
||||||
|
|
||||||
|
function getFieldValue(field) {
|
||||||
|
if (!contact) return "-";
|
||||||
|
|
||||||
|
if (field.keys) {
|
||||||
|
return field.keys
|
||||||
|
.map(k => field.parentKey ? contact[field.parentKey]?.[k] : contact[k])
|
||||||
|
.filter(val => val && val.trim() !== "")
|
||||||
|
.join(" / ");
|
||||||
|
}
|
||||||
|
|
||||||
|
return field.parentKey ? contact[field.parentKey]?.[field.key] : contact[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 masterDetail.selectedItem}
|
||||||
|
<div class="flex flex-col px-2 py-1 gap-2 h-full w-full">
|
||||||
|
<TopbarWrapper
|
||||||
|
title={masterDetail.selectedItem.data.NameFirst}
|
||||||
|
{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>
|
||||||
|
<Separator />
|
||||||
|
<div class="flex flex-col px-2 py-1 gap-2 h-full w-full">
|
||||||
|
<TopbarWrapper
|
||||||
|
title="Contact Detail"
|
||||||
|
actions={actionsDetail}
|
||||||
|
/>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
{#each masterDetail.selectedItem?.data?.Details as contactdetail}
|
||||||
|
<Card.Root class="w-full gap-2 2xl:gap-4 py-2 2xl:py-4">
|
||||||
|
<Card.Header>
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<Card.Title class="text-sm font-medium">
|
||||||
|
{contactdetail.ContactCode}
|
||||||
|
</Card.Title>
|
||||||
|
<Card.Description class="text-sm font-medium">
|
||||||
|
{contactdetail.ContactEmail}
|
||||||
|
</Card.Description>
|
||||||
|
</div>
|
||||||
|
<Badge variant="secondary" class="text-xs">
|
||||||
|
Site {contactdetail.SiteID}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content class="space-y-3">
|
||||||
|
<div class="grid grid-cols-3 gap-3">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||||
|
Department
|
||||||
|
</p>
|
||||||
|
<p class="text-sm font-medium">
|
||||||
|
{contactdetail.Department || "-"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||||
|
Job Title
|
||||||
|
</p>
|
||||||
|
<p class="text-sm font-medium">
|
||||||
|
{contactdetail.JobTitle || "-"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||||
|
Occupation
|
||||||
|
</p>
|
||||||
|
<p class="text-sm font-medium">
|
||||||
|
{contactdetail.OccupationID || "-"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<ReusableEmpty icon={UserXIcon} desc="Select a contact to see details"/>
|
||||||
|
{/if}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
export const contactColumns = [
|
||||||
|
{
|
||||||
|
accessorKey: "Initial",
|
||||||
|
header: "Initial",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "NameFirst",
|
||||||
|
header: "Contact Name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "Specialty",
|
||||||
|
header: "Specialty",
|
||||||
|
},
|
||||||
|
];
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { API } from '$lib/config/api.js';
|
import { API } from '$lib/config/api.js';
|
||||||
import { getById, searchWithParams, create, update } from '$lib/api/api-client';
|
import { getById, searchWithParams, create, update } from '$lib/api/api-client';
|
||||||
|
|
||||||
export async function searchParam(searchQuery) {
|
export async function getTests(searchQuery) {
|
||||||
return await searchWithParams(API.TEST, searchQuery)
|
return await searchWithParams(API.TEST, searchQuery)
|
||||||
}
|
}
|
||||||
|
|
||||||
50
src/lib/components/dictionary/test/config/test-config.js
Normal file
50
src/lib/components/dictionary/test/config/test-config.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import PlusIcon from "@lucide/svelte/icons/plus";
|
||||||
|
import Settings2Icon from "@lucide/svelte/icons/settings-2";
|
||||||
|
import PencilIcon from "@lucide/svelte/icons/pencil";
|
||||||
|
import { API } from "$lib/config/api";
|
||||||
|
|
||||||
|
export const searchFields = [
|
||||||
|
{
|
||||||
|
key: "TestSiteCode",
|
||||||
|
label: "Test Code",
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "TestSiteName",
|
||||||
|
label: "Test Name",
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "TestType",
|
||||||
|
label: "Test Type",
|
||||||
|
type: "select",
|
||||||
|
optionsEndpoint: `${API.BASE_URL}${API.VALUESET}/test_type`,
|
||||||
|
},
|
||||||
|
|
||||||
|
];
|
||||||
|
|
||||||
|
export const detailSections = [];
|
||||||
|
|
||||||
|
export function testActions(masterDetail) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
Icon: PlusIcon,
|
||||||
|
label: 'Add Test',
|
||||||
|
onClick: () => masterDetail.enterCreate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Icon: Settings2Icon,
|
||||||
|
label: 'Search Parameters',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function viewActions(handlers){
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
Icon: PencilIcon,
|
||||||
|
label: 'Edit Test',
|
||||||
|
onClick: handlers.editTest,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
310
src/lib/components/dictionary/test/config/test-form-config.js
Normal file
310
src/lib/components/dictionary/test/config/test-form-config.js
Normal file
@ -0,0 +1,310 @@
|
|||||||
|
import { API } from "$lib/config/api";
|
||||||
|
import EraserIcon from "@lucide/svelte/icons/eraser";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { cleanEmptyStrings } from "$lib/utils/cleanEmptyStrings";
|
||||||
|
|
||||||
|
export const testSchema = z.object({});
|
||||||
|
|
||||||
|
export const testInitialForm = {
|
||||||
|
TestSiteID: "",
|
||||||
|
SiteID: "",
|
||||||
|
TestSiteCode: "",
|
||||||
|
TestSiteName: "",
|
||||||
|
TestType: "",
|
||||||
|
Description: "",
|
||||||
|
DisciplineID: "",
|
||||||
|
DepartmentID: "",
|
||||||
|
ResultType: "",
|
||||||
|
RefType: "",
|
||||||
|
Vset: "",
|
||||||
|
Unit1: "",
|
||||||
|
Factor: "",
|
||||||
|
Unit2: "",
|
||||||
|
Decimal: "",
|
||||||
|
ReqQty: "",
|
||||||
|
ReqQtyUnit: "",
|
||||||
|
CollReq: "",
|
||||||
|
Method: "",
|
||||||
|
ExpectedTXT: "",
|
||||||
|
SeqScr: "",
|
||||||
|
SeqRpt: "",
|
||||||
|
VisibleScr: "",
|
||||||
|
VisibleRpt: "",
|
||||||
|
CountStat: "",
|
||||||
|
Level: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const testDefaultErrors = {};
|
||||||
|
|
||||||
|
export const testFormFields = [
|
||||||
|
{
|
||||||
|
title: "Basic Information",
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
type: "row",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
key: "SiteID",
|
||||||
|
label: "Site ID",
|
||||||
|
required: false,
|
||||||
|
type: "select",
|
||||||
|
optionsEndpoint: `${API.BASE_URL}${API.SITE}`,
|
||||||
|
valueKey: "SiteID",
|
||||||
|
labelKey: (item) => `${item.SiteCode} - ${item.SiteName}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "TestSiteCode",
|
||||||
|
label: "Test Code",
|
||||||
|
required: true,
|
||||||
|
type: "text",
|
||||||
|
validateOn: ["input"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "row",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
key: "TestSiteName",
|
||||||
|
label: "Test Name",
|
||||||
|
required: true,
|
||||||
|
type: "text",
|
||||||
|
validateOn: ["input"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "TestType",
|
||||||
|
label: "Test Type",
|
||||||
|
required: true,
|
||||||
|
type: "text",
|
||||||
|
validateOn: ["input"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "row",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
key: "Description",
|
||||||
|
label: "Description",
|
||||||
|
required: false,
|
||||||
|
type: "textarea",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Organizational Assignment",
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
type: "row",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
key: "DisciplineID",
|
||||||
|
label: "Discipline",
|
||||||
|
required: false,
|
||||||
|
type: "select",
|
||||||
|
optionsEndpoint: `${API.BASE_URL}${API.DISCIPLINE}`,
|
||||||
|
valueKey: "DisciplineID",
|
||||||
|
labelKey: "DisciplineName",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "DepartmentID",
|
||||||
|
label: "Department",
|
||||||
|
required: false,
|
||||||
|
type: "select",
|
||||||
|
optionsEndpoint: `${API.BASE_URL}${API.DEPARTMENT}`,
|
||||||
|
valueKey: "DepartmentID",
|
||||||
|
labelKey: "DepartmentName",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Result Configuration",
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
type: "row",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
key: "ResultType",
|
||||||
|
label: "Result Type",
|
||||||
|
required: true,
|
||||||
|
type: "select",
|
||||||
|
optionsEndpoint: `${API.BASE_URL}${API.VALUESET}/result_type`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "RefType",
|
||||||
|
label: "Reference Type",
|
||||||
|
required: true,
|
||||||
|
type: "select",
|
||||||
|
optionsEndpoint: `${API.BASE_URL}${API.VALUESET}/reference_type`,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "row",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
key: "VSet",
|
||||||
|
label: "Value Set",
|
||||||
|
required: false,
|
||||||
|
type: "select",
|
||||||
|
optionsEndpoint: `${API.BASE_URL}${API.VALUESET}/service_classes`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "row",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
key: "Unit1",
|
||||||
|
label: "Unit 1",
|
||||||
|
required: false,
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Factor",
|
||||||
|
label: "Factor",
|
||||||
|
required: false,
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Unit2",
|
||||||
|
label: "Unit 2",
|
||||||
|
required: false,
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Decimal",
|
||||||
|
label: "Decimal",
|
||||||
|
required: false,
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "row",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
key: "ExpectedTAT",
|
||||||
|
label: "Expected TAT",
|
||||||
|
required: false,
|
||||||
|
type: "select",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Specimen Requirements",
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
type: "row",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
key: "ReqQty",
|
||||||
|
label: "Required Quantity",
|
||||||
|
required: false,
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "CollReq",
|
||||||
|
label: "Collection Requirement",
|
||||||
|
required: false,
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "row",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
key: "ReqQtyUnit",
|
||||||
|
label: "Quantity Unit",
|
||||||
|
required: false,
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Method",
|
||||||
|
label: "Method",
|
||||||
|
required: false,
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Display Settings",
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
type: "row",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
key: "SeqScr",
|
||||||
|
label: "Sequence on Screen",
|
||||||
|
required: false,
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "VisibleScr",
|
||||||
|
label: "Visible on Screen",
|
||||||
|
required: false,
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "row",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
key: "SeqRpt",
|
||||||
|
label: "Sequence on Report",
|
||||||
|
required: false,
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "VisibleRpt",
|
||||||
|
label: "Visible on Report",
|
||||||
|
required: false,
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Other Settings",
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
type: "row",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
key: "CountStat",
|
||||||
|
label: "Statistic",
|
||||||
|
required: false,
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Level",
|
||||||
|
label: "Level",
|
||||||
|
required: false,
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function getTestFormActions(handlers) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
Icon: EraserIcon,
|
||||||
|
label: 'Clear Form',
|
||||||
|
onClick: handlers.clearForm,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
54
src/lib/components/dictionary/test/page/create-page.svelte
Normal file
54
src/lib/components/dictionary/test/page/create-page.svelte
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<script>
|
||||||
|
import { useDictionaryForm } from "$lib/components/composable/use-dictionary-form.svelte";
|
||||||
|
import FormPageContainer from "$lib/components/reusable/form/form-page-container.svelte";
|
||||||
|
import DictionaryFormRenderer from "$lib/components/reusable/form/dictionary-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 = useDictionaryForm(formState);
|
||||||
|
|
||||||
|
const handlers = {
|
||||||
|
clearForm: () => {
|
||||||
|
formState.reset();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const actions = formActions(handlers);
|
||||||
|
|
||||||
|
let showConfirm = $state(false);
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
const result = await formState.save(masterDetail.mode);
|
||||||
|
|
||||||
|
toast('Test Created!');
|
||||||
|
masterDetail?.exitForm(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const primaryAction = $derived({
|
||||||
|
label: 'Save',
|
||||||
|
onClick: handleSave,
|
||||||
|
disabled: helpers.hasErrors || formState.isSaving.current,
|
||||||
|
loading: formState.isSaving.current
|
||||||
|
});
|
||||||
|
|
||||||
|
const secondaryActions = [];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FormPageContainer title="Test Location" {primaryAction} {secondaryActions} {actions}>
|
||||||
|
<DictionaryFormRenderer
|
||||||
|
{formState}
|
||||||
|
formFields={formFields}
|
||||||
|
mode="create"
|
||||||
|
/>
|
||||||
|
</FormPageContainer>
|
||||||
|
|
||||||
|
<ReusableAlertDialog
|
||||||
|
bind:open={masterDetail.showExitConfirm}
|
||||||
|
onConfirm={masterDetail.confirmExit}
|
||||||
|
/>
|
||||||
73
src/lib/components/dictionary/test/page/edit-page.svelte
Normal file
73
src/lib/components/dictionary/test/page/edit-page.svelte
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
<script>
|
||||||
|
import { useDictionaryForm } from "$lib/components/composable/use-dictionary-form.svelte";
|
||||||
|
import FormPageContainer from "$lib/components/reusable/form/form-page-container.svelte";
|
||||||
|
import DictionaryFormRenderer from "$lib/components/reusable/form/dictionary-form-renderer.svelte";
|
||||||
|
import { toast } from "svelte-sonner";
|
||||||
|
import { untrack } from "svelte";
|
||||||
|
import { API } from "$lib/config/api";
|
||||||
|
import ReusableAlertDialog from "$lib/components/reusable/reusable-alert-dialog.svelte";
|
||||||
|
|
||||||
|
let props = $props();
|
||||||
|
|
||||||
|
const { masterDetail, formFields, formActions, schema, initialForm } = props.context;
|
||||||
|
|
||||||
|
const { formState } = masterDetail;
|
||||||
|
|
||||||
|
const helpers = useDictionaryForm(formState);
|
||||||
|
|
||||||
|
let showConfirm = $state(false);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
untrack(() => {
|
||||||
|
formFields.forEach(group => {
|
||||||
|
group.rows.forEach(row => {
|
||||||
|
row.columns.forEach(col => {
|
||||||
|
if (col.type === "group") {
|
||||||
|
col.columns.forEach(child => {
|
||||||
|
if (child.type === "select" && child.optionsEndpoint) {
|
||||||
|
formState.fetchOptions(child, formState.form);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if ((col.type === "select") && col.optionsEndpoint) {
|
||||||
|
formState.fetchOptions(col, formState.form);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleEdit() {
|
||||||
|
const result = await formState.save(masterDetail.mode);
|
||||||
|
|
||||||
|
if (result.status === 'success') {
|
||||||
|
console.log('Test updated successfully');
|
||||||
|
toast('Test Updated!');
|
||||||
|
masterDetail.exitForm(true);
|
||||||
|
} else {
|
||||||
|
console.error('Failed to update test:', result.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const primaryAction = $derived({
|
||||||
|
label: 'Edit',
|
||||||
|
onClick: handleEdit,
|
||||||
|
disabled: helpers.hasErrors || formState.isSaving.current,
|
||||||
|
loading: formState.isSaving.current
|
||||||
|
});
|
||||||
|
|
||||||
|
const secondaryActions = [];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FormPageContainer title="Edit Test" {primaryAction} {secondaryActions}>
|
||||||
|
<DictionaryFormRenderer
|
||||||
|
{formState}
|
||||||
|
formFields={formFields}
|
||||||
|
mode="edit"
|
||||||
|
/>
|
||||||
|
</FormPageContainer>
|
||||||
|
|
||||||
|
<ReusableAlertDialog
|
||||||
|
bind:open={masterDetail.showExitConfirm}
|
||||||
|
onConfirm={masterDetail.confirmExit}
|
||||||
|
/>
|
||||||
68
src/lib/components/dictionary/test/page/master-page.svelte
Normal file
68
src/lib/components/dictionary/test/page/master-page.svelte
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<script>
|
||||||
|
import { testColumns } from "$lib/components/dictionary/test/table/test-columns";
|
||||||
|
import { getTests, getTest } from "$lib/components/dictionary/test/api/test-api";
|
||||||
|
import { useSearch } from "$lib/components/composable/use-search.svelte";
|
||||||
|
import { searchFields, testActions } from "$lib/components/dictionary/test/config/test-config";
|
||||||
|
import TopbarWrapper from "$lib/components/topbar/topbar-wrapper.svelte";
|
||||||
|
import ReusableSearchParam from "$lib/components/reusable/reusable-search-param.svelte";
|
||||||
|
import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte";
|
||||||
|
import ReusableDataTable from "$lib/components/reusable/reusable-data-table.svelte";
|
||||||
|
import FlaskConicalOffIcon from "@lucide/svelte/icons/flask-conical-off";
|
||||||
|
|
||||||
|
let props = $props();
|
||||||
|
|
||||||
|
const search = useSearch(searchFields, getTests);
|
||||||
|
const initialForm = props.masterDetail.formState.form;
|
||||||
|
const actions = testActions(props.masterDetail, initialForm)
|
||||||
|
actions.find(a => a.label === 'Search Parameters').popoverContent = searchParamSnippet;
|
||||||
|
|
||||||
|
let activeRowId = $state(null);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#snippet searchParamSnippet()}
|
||||||
|
<ReusableSearchParam {searchFields}
|
||||||
|
bind:searchQuery={search.searchQuery} onSearch={search.handleSearch} onReset={search.handleReset} isLoading={search.isLoading}
|
||||||
|
selectOptions={search.selectOptions} loadingOptions={search.loadingOptions} fetchOptions={search.fetchOptions}
|
||||||
|
/>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
onclick={() => props.masterDetail.isFormMode && props.masterDetail.exitForm()}
|
||||||
|
onkeydown={(e) => e.key === 'Enter' && props.masterDetail.isFormMode && props.masterDetail.exitForm()}
|
||||||
|
class={`
|
||||||
|
${props.masterDetail.isMobile ? "w-full" : props.masterDetail.isFormMode ? "w-[3%] cursor-pointer" : "w-[35%]"}
|
||||||
|
transition-all duration-300 flex flex-col items-center p-2 h-full overflow-y-auto
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<div class={`flex w-full ${props.masterDetail.isFormMode ? "flex-col justify-center h-full items-center" : "flex-col justify-start h-full"}`} >
|
||||||
|
{#if props.masterDetail.isFormMode}
|
||||||
|
<span class="flex flex-col items-center justify-center gap-4 tracking-widest font-semibold select-none">
|
||||||
|
{#each "TEST".split("") as c}
|
||||||
|
<span class="leading-none">{c}</span>
|
||||||
|
{/each}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if !props.masterDetail.isFormMode}
|
||||||
|
<div role="button" tabindex="0" class="flex flex-1 flex-col" onclick={(e) => e.stopPropagation()} onkeydown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<TopbarWrapper {actions}/>
|
||||||
|
<div class="flex-1 w-full h-full">
|
||||||
|
{#if search.searchData.length > 0}
|
||||||
|
<ReusableDataTable data={search.searchData} columns={testColumns} handleRowClick={props.masterDetail.select} {activeRowId} rowIdKey="TestSiteID"/>
|
||||||
|
{:else}
|
||||||
|
<div class="flex h-full">
|
||||||
|
<ReusableEmpty icon={FlaskConicalOffIcon} desc="Try searching from search parameters"/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
91
src/lib/components/dictionary/test/page/view-page.svelte
Normal file
91
src/lib/components/dictionary/test/page/view-page.svelte
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
<script>
|
||||||
|
import { formatUTCDate } from "$lib/utils/formatUTCDate";
|
||||||
|
import { detailSections, viewActions } from "$lib/components/dictionary/test/config/test-config";
|
||||||
|
import TopbarWrapper from "$lib/components/topbar/topbar-wrapper.svelte";
|
||||||
|
import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte";
|
||||||
|
import FlaskConicalOffIcon from "@lucide/svelte/icons/flask-conical-off";
|
||||||
|
|
||||||
|
let props = $props();
|
||||||
|
|
||||||
|
const { masterDetail, formFields, formActions, schema } = props.context;
|
||||||
|
|
||||||
|
let test = $derived(masterDetail?.selectedItem?.data);
|
||||||
|
$inspect(test)
|
||||||
|
const handlers = {
|
||||||
|
editTest: () => masterDetail.enterEdit("data"),
|
||||||
|
};
|
||||||
|
|
||||||
|
const actions = viewActions(handlers);
|
||||||
|
|
||||||
|
function getFieldValue(field) {
|
||||||
|
if (!test) return "-";
|
||||||
|
|
||||||
|
if (field.keys) {
|
||||||
|
return field.keys
|
||||||
|
.map(k => field.parentKey ? test[field.parentKey]?.[k] : test[k])
|
||||||
|
.filter(val => val && val.trim() !== "")
|
||||||
|
.join(" / ");
|
||||||
|
}
|
||||||
|
|
||||||
|
return field.parentKey ? test[field.parentKey]?.[field.key] : test[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 masterDetail.selectedItem}
|
||||||
|
<div class="flex flex-col px-2 py-1 gap-2 h-full w-full">
|
||||||
|
<TopbarWrapper
|
||||||
|
title={masterDetail.selectedItem.data.TestSiteName}
|
||||||
|
{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 icon={FlaskConicalOffIcon} desc="Select a test to see details"/>
|
||||||
|
{/if}
|
||||||
14
src/lib/components/dictionary/test/table/test-columns.js
Normal file
14
src/lib/components/dictionary/test/table/test-columns.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
export const testColumns = [
|
||||||
|
{
|
||||||
|
accessorKey: "TestSiteCode",
|
||||||
|
header: "Test Code",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "TestSiteName",
|
||||||
|
header: "Test Name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "TestType",
|
||||||
|
header: "Test Type",
|
||||||
|
},
|
||||||
|
];
|
||||||
@ -1,211 +0,0 @@
|
|||||||
import { API } from "$lib/config/api";
|
|
||||||
import EraserIcon from "@lucide/svelte/icons/eraser";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { cleanEmptyStrings } from "$lib/utils/cleanEmptyStrings";
|
|
||||||
|
|
||||||
export const testSchema = z.object({});
|
|
||||||
|
|
||||||
export const admissionInitialForm = {
|
|
||||||
InternalPVID: "",
|
|
||||||
InternalPID: "",
|
|
||||||
PVID: "",
|
|
||||||
EpisodeID: "",
|
|
||||||
DiagCode: "",
|
|
||||||
Diagnosis: "",
|
|
||||||
ADTCode: "",
|
|
||||||
LocationID: "",
|
|
||||||
AttDoc: "",
|
|
||||||
RefDoc: "",
|
|
||||||
AdmDoc: "",
|
|
||||||
CnsDoc: "",
|
|
||||||
isDischarge: false
|
|
||||||
};
|
|
||||||
|
|
||||||
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: "isDischarge",
|
|
||||||
label: "Admission 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 cleanEmptyStrings(payload);
|
|
||||||
}
|
|
||||||
@ -17,8 +17,11 @@
|
|||||||
|
|
||||||
<div class="flex flex-col p-2 gap-4 h-full w-full">
|
<div class="flex flex-col p-2 gap-4 h-full w-full">
|
||||||
<TopbarWrapper actions={actions} title={title}/>
|
<TopbarWrapper actions={actions} title={title}/>
|
||||||
{@render children()}
|
<div class="flex-1 min-h-0 overflow-y-auto p-2">
|
||||||
<div class="mt-auto flex justify-end items-center pt-2">
|
{@render children()}
|
||||||
|
</div>
|
||||||
|
<!-- <div class="mt-auto flex justify-end items-center pt-2"> -->
|
||||||
|
<div class="shrink-0 border-t pt-2 flex justify-end items-center">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
class="cursor-pointer {secondaryActions.length ? 'rounded-r-none' : ''}"
|
class="cursor-pointer {secondaryActions.length ? 'rounded-r-none' : ''}"
|
||||||
|
|||||||
@ -323,7 +323,8 @@
|
|||||||
</div>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
<div class="flex-1 min-h-0 overflow-y-auto p-2 space-y-6">
|
<!-- <div class="flex-1 min-h-0 overflow-y-auto p-2 space-y-6"> -->
|
||||||
|
<div class="p-2 space-y-6">
|
||||||
{#each formFields as group}
|
{#each formFields as group}
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
{#if group.title}
|
{#if group.title}
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
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";
|
||||||
import { Spinner } from "$lib/components/ui/spinner/index.js";
|
import { Spinner } from "$lib/components/ui/spinner/index.js";
|
||||||
|
import ReusableCalendar from "$lib/components/reusable/reusable-calendar.svelte";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
formState,
|
formState,
|
||||||
@ -17,9 +18,9 @@
|
|||||||
|
|
||||||
function getFilteredOptions(key) {
|
function getFilteredOptions(key) {
|
||||||
const query = searchQuery[key] || "";
|
const query = searchQuery[key] || "";
|
||||||
if (!query) return formState.selectOptions[key] ?? [];
|
if (!query) return formState.selectOptions?.[key] ?? [];
|
||||||
|
|
||||||
return (formState.selectOptions[key] ?? []).filter(opt =>
|
return (formState.selectOptions?.[key] ?? []).filter(opt =>
|
||||||
opt.label.toLowerCase().includes(query.toLowerCase())
|
opt.label.toLowerCase().includes(query.toLowerCase())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -126,23 +127,29 @@
|
|||||||
bind:value={formState.form[key]}
|
bind:value={formState.form[key]}
|
||||||
></textarea>
|
></textarea>
|
||||||
{:else if type === "select"}
|
{:else if type === "select"}
|
||||||
{@const selectedLabel = formState.selectOptions[key]?.find(opt => opt.value === formState.form[key])?.label || "Choose"}
|
{@const selectedLabel = formState.selectOptions?.[key]?.find(opt => opt.value === formState.form[key])?.label || "Choose"}
|
||||||
{@const filteredOptions = getFilteredOptions(key)}
|
{@const filteredOptions = getFilteredOptions(key)}
|
||||||
<Select.Root type="single" bind:value={formState.form[key]}
|
<Select.Root type="single" bind:value={formState.form[key]}
|
||||||
onValueChange={(val) => {
|
onValueChange={(val) => {
|
||||||
formState.form[key] = val;
|
formState.form[key] = val;
|
||||||
if (validateOn?.includes("input")) {
|
if (validateOn?.includes("input")) {
|
||||||
formState.validateField(key, formState.form[key], false);
|
formState.validateField?.(key, formState.form[key], false);
|
||||||
}
|
}
|
||||||
if (key === "Province") {
|
if (key === "Province") {
|
||||||
formState.form.City = "";
|
formState.form.City = "";
|
||||||
formState.selectOptions.City = [];
|
// formState.selectOptions.City = [];
|
||||||
formState.lastFetched.City = null;
|
// formState.lastFetched.City = null;
|
||||||
|
if (formState.selectOptions) {
|
||||||
|
formState.selectOptions.City = [];
|
||||||
|
}
|
||||||
|
if (formState.lastFetched) {
|
||||||
|
formState.lastFetched.City = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
if (open && optionsEndpoint) {
|
if (open && optionsEndpoint) {
|
||||||
formState.fetchOptions(
|
formState.fetchOptions?.(
|
||||||
{ key, optionsEndpoint, dependsOn, endpointParamKey, valueKey, labelKey },
|
{ key, optionsEndpoint, dependsOn, endpointParamKey, valueKey, labelKey },
|
||||||
formState.form
|
formState.form
|
||||||
);
|
);
|
||||||
@ -161,7 +168,7 @@
|
|||||||
bind:value={searchQuery[key]}
|
bind:value={searchQuery[key]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{#if formState.loadingOptions[key]}
|
{#if formState.loadingOptions?.[key]}
|
||||||
<Select.Item disabled value="loading">Loading...</Select.Item>
|
<Select.Item disabled value="loading">Loading...</Select.Item>
|
||||||
{:else}
|
{:else}
|
||||||
{#if !required}
|
{#if !required}
|
||||||
@ -175,6 +182,16 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</Select.Content>
|
</Select.Content>
|
||||||
</Select.Root>
|
</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);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
@ -194,7 +211,8 @@
|
|||||||
</div>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
<div class="flex-1 min-h-0 overflow-y-auto p-2 space-y-6">
|
<!-- <div class="flex-1 min-h-0 overflow-y-auto p-2 space-y-6"> -->
|
||||||
|
<div class="p-2 space-y-6">
|
||||||
{#each formFields as group}
|
{#each formFields as group}
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
{#if group.title}
|
{#if group.title}
|
||||||
@ -204,12 +222,6 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#each group.rows as row}
|
{#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}
|
|
||||||
> -->
|
|
||||||
<div
|
<div
|
||||||
class="grid grid-cols-1 space-y-2 gap-6 md:gap-4"
|
class="grid grid-cols-1 space-y-2 gap-6 md:gap-4"
|
||||||
class:md:grid-cols-1={row.columns.length === 1 && row.columns[0].fullWidth !== false}
|
class:md:grid-cols-1={row.columns.length === 1 && row.columns[0].fullWidth !== false}
|
||||||
@ -218,12 +230,6 @@
|
|||||||
>
|
>
|
||||||
{#each row.columns as col}
|
{#each row.columns as col}
|
||||||
{#if col.type === "group"}
|
{#if col.type === "group"}
|
||||||
<!-- <div
|
|
||||||
class="grid grid-cols-1 gap-6 md:gap-2"
|
|
||||||
class:md:grid-cols-1={col.columns.length === 1}
|
|
||||||
class:md:grid-cols-2={col.columns.length === 2}
|
|
||||||
class:md:grid-cols-3={col.columns.length === 3}
|
|
||||||
> -->
|
|
||||||
<div
|
<div
|
||||||
class="grid grid-cols-1 gap-6 md:gap-2"
|
class="grid grid-cols-1 gap-6 md:gap-2"
|
||||||
class:md:grid-cols-1={col.columns.length === 1 && col.columns[0].fullWidth !== false}
|
class:md:grid-cols-1={col.columns.length === 1 && col.columns[0].fullWidth !== false}
|
||||||
|
|||||||
@ -17,8 +17,13 @@
|
|||||||
|
|
||||||
<div class="flex flex-col p-2 gap-4 h-full w-full">
|
<div class="flex flex-col p-2 gap-4 h-full w-full">
|
||||||
<TopbarWrapper actions={actions} title={title}/>
|
<TopbarWrapper actions={actions} title={title}/>
|
||||||
{@render children()}
|
<!-- <div class="flex-1 min-h-0 flex flex-col gap-4"> -->
|
||||||
<div class="mt-auto flex justify-end items-center pt-2">
|
<div class="flex-1 min-h-0 overflow-y-auto p-2">
|
||||||
|
{@render children()}
|
||||||
|
</div>
|
||||||
|
<!-- <div class="mt-auto flex justify-end items-center pt-2"> -->
|
||||||
|
<!-- <div class="flex justify-end items-center pt-2"> -->
|
||||||
|
<div class="shrink-0 border-t pt-2 flex justify-end items-center">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
class="cursor-pointer {secondaryActions.length ? 'rounded-r-none' : ''}"
|
class="cursor-pointer {secondaryActions.length ? 'rounded-r-none' : ''}"
|
||||||
|
|||||||
45
src/lib/components/ui/badge/badge.svelte
Normal file
45
src/lib/components/ui/badge/badge.svelte
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<script module>
|
||||||
|
import { tv } from "tailwind-variants";
|
||||||
|
|
||||||
|
export const badgeVariants = tv({
|
||||||
|
base: "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 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] [&>svg]:pointer-events-none [&>svg]:size-3",
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"bg-primary text-primary-foreground [a&]:hover:bg-primary/90 border-transparent",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70 border-transparent text-white",
|
||||||
|
outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
href,
|
||||||
|
class: className,
|
||||||
|
variant = "default",
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:element
|
||||||
|
this={href ? "a" : "span"}
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="badge"
|
||||||
|
{href}
|
||||||
|
class={cn(badgeVariants({ variant }), className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</svelte:element>
|
||||||
2
src/lib/components/ui/badge/index.js
Normal file
2
src/lib/components/ui/badge/index.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { default as Badge } from "./badge.svelte";
|
||||||
|
export { badgeVariants, } from "./badge.svelte";
|
||||||
@ -9,6 +9,9 @@ export async function load({ url }) {
|
|||||||
'/patient/admission': {
|
'/patient/admission': {
|
||||||
title: 'Patient Admission',
|
title: 'Patient Admission',
|
||||||
},
|
},
|
||||||
|
'/dictionary/contact': {
|
||||||
|
title: 'Contact'
|
||||||
|
},
|
||||||
'/dictionary/location': {
|
'/dictionary/location': {
|
||||||
title: 'Location'
|
title: 'Location'
|
||||||
},
|
},
|
||||||
|
|||||||
51
src/routes/dictionary/contact/+page.svelte
Normal file
51
src/routes/dictionary/contact/+page.svelte
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<script>
|
||||||
|
import { Separator } from "$lib/components/ui/separator/index.js";
|
||||||
|
import { useMasterDetail } from "$lib/components/composable/use-master-detail.svelte";
|
||||||
|
import { getContact, createContact, editContact } from "$lib/components/dictionary/contact/api/contact-api";
|
||||||
|
import MasterPage from "$lib/components/dictionary/contact/page/master-page.svelte";
|
||||||
|
import ViewPage from "$lib/components/dictionary/contact/page/view-page.svelte";
|
||||||
|
import CreatePage from "$lib/components/dictionary/contact/page/create-page.svelte";
|
||||||
|
import EditPage from "$lib/components/dictionary/contact/page/edit-page.svelte";
|
||||||
|
import { contactSchema, contactInitialForm, contactDefaultErrors, contactFormFields, getContactFormActions } from "$lib/components/dictionary/contact/config/contact-form-config";
|
||||||
|
|
||||||
|
const masterDetail = useMasterDetail({
|
||||||
|
onSelect: async (row) => {
|
||||||
|
return await getContact(row.ContactID);
|
||||||
|
},
|
||||||
|
formConfig: {
|
||||||
|
schema: contactSchema,
|
||||||
|
initialForm: contactInitialForm,
|
||||||
|
defaultErrors: contactDefaultErrors,
|
||||||
|
mode: 'create',
|
||||||
|
modeOpt: 'default',
|
||||||
|
saveEndpoint: createContact,
|
||||||
|
editEndpoint: editContact,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const pageContext = {
|
||||||
|
masterDetail,
|
||||||
|
formFields: contactFormFields,
|
||||||
|
formActions: getContactFormActions,
|
||||||
|
schema: contactSchema,
|
||||||
|
initialForm: contactInitialForm,
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex w-full h-full overflow-hidden">
|
||||||
|
{#if masterDetail.showMaster}
|
||||||
|
<MasterPage {masterDetail} />
|
||||||
|
{/if}
|
||||||
|
<Separator orientation="vertical"/>
|
||||||
|
{#if masterDetail.showDetail}
|
||||||
|
<main class={`${masterDetail.isMobile ? 'w-full' : masterDetail.isFormMode ? 'w-[97%] flex flex-col items-start' : 'w-[65%]'} h-full overflow-y-auto flex flex-col items-center transition-all duration-300`}>
|
||||||
|
{#if masterDetail.mode === "view"}
|
||||||
|
<ViewPage context={pageContext}/>
|
||||||
|
{:else if masterDetail.mode === "create"}
|
||||||
|
<CreatePage context={pageContext}/>
|
||||||
|
{:else if masterDetail.mode === "edit"}
|
||||||
|
<EditPage context={pageContext}/>
|
||||||
|
{/if}
|
||||||
|
</main>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
51
src/routes/dictionary/test/+page.svelte
Normal file
51
src/routes/dictionary/test/+page.svelte
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<script>
|
||||||
|
import { Separator } from "$lib/components/ui/separator/index.js";
|
||||||
|
import { useMasterDetail } from "$lib/components/composable/use-master-detail.svelte";
|
||||||
|
import { getTest, createTest, editTest } from "$lib/components/dictionary/test/api/test-api";
|
||||||
|
import MasterPage from "$lib/components/dictionary/test/page/master-page.svelte";
|
||||||
|
import ViewPage from "$lib/components/dictionary/test/page/view-page.svelte";
|
||||||
|
import CreatePage from "$lib/components/dictionary/test/page/create-page.svelte";
|
||||||
|
import EditPage from "$lib/components/dictionary/test/page/edit-page.svelte";
|
||||||
|
import { testSchema, testInitialForm, testDefaultErrors, testFormFields, getTestFormActions } from "$lib/components/dictionary/test/config/test-form-config";
|
||||||
|
|
||||||
|
const masterDetail = useMasterDetail({
|
||||||
|
onSelect: async (row) => {
|
||||||
|
return await getTest(row.TestSiteID);
|
||||||
|
},
|
||||||
|
formConfig: {
|
||||||
|
schema: testSchema,
|
||||||
|
initialForm: testInitialForm,
|
||||||
|
defaultErrors: testDefaultErrors,
|
||||||
|
mode: 'create',
|
||||||
|
modeOpt: 'default',
|
||||||
|
saveEndpoint: createTest,
|
||||||
|
editEndpoint: editTest,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const pageContext = {
|
||||||
|
masterDetail,
|
||||||
|
formFields: testFormFields,
|
||||||
|
formActions: getTestFormActions,
|
||||||
|
schema: testSchema,
|
||||||
|
initialForm: testInitialForm,
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex w-full h-full overflow-hidden">
|
||||||
|
{#if masterDetail.showMaster}
|
||||||
|
<MasterPage {masterDetail} />
|
||||||
|
{/if}
|
||||||
|
<Separator orientation="vertical"/>
|
||||||
|
{#if masterDetail.showDetail}
|
||||||
|
<main class={`${masterDetail.isMobile ? 'w-full' : masterDetail.isFormMode ? 'w-[97%] flex flex-col items-start' : 'w-[65%]'} h-full overflow-y-auto flex flex-col items-center transition-all duration-300`}>
|
||||||
|
{#if masterDetail.mode === "view"}
|
||||||
|
<ViewPage context={pageContext}/>
|
||||||
|
{:else if masterDetail.mode === "create"}
|
||||||
|
<CreatePage context={pageContext}/>
|
||||||
|
{:else if masterDetail.mode === "edit"}
|
||||||
|
<EditPage context={pageContext}/>
|
||||||
|
{/if}
|
||||||
|
</main>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@ -1,60 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { Separator } from "$lib/components/ui/separator/index.js";
|
|
||||||
import { useMasterDetail } from "$lib/components/composable/use-master-detail.svelte";
|
|
||||||
import { getTest, createTest, editTest } from "$lib/components/dictionary/testdef/api/testdef-api";
|
|
||||||
import MasterPage from "$lib/components/dictionary/testdef/page/master-page.svelte";
|
|
||||||
import ViewPage from "$lib/components/dictionary/testdef/page/view-page.svelte";
|
|
||||||
import CreatePage from "$lib/components/dictionary/testdef/page/create-page.svelte";
|
|
||||||
import EditPage from "$lib/components/dictionary/testdef/page/edit-page.svelte";
|
|
||||||
import { admissionSchema, admissionInitialForm, admissionDefaultErrors, admissionFormFields, getAdmissionFormActions, buildPayload } from "$lib/components/dictionary/testdef/config/testdef-form-config";
|
|
||||||
|
|
||||||
const masterDetail = useMasterDetail({
|
|
||||||
onSelect: async (row) => {
|
|
||||||
const response = await getVisit(row.PVID);
|
|
||||||
if (response?.data) {
|
|
||||||
response.data.isDischarge = response.data.ADTCode === "A03";
|
|
||||||
}
|
|
||||||
|
|
||||||
return response
|
|
||||||
},
|
|
||||||
formConfig: {
|
|
||||||
schema: admissionSchema,
|
|
||||||
initialForm: admissionInitialForm,
|
|
||||||
defaultErrors: admissionDefaultErrors,
|
|
||||||
mode: 'create',
|
|
||||||
modeOpt: 'default',
|
|
||||||
saveEndpoint: createAdmission,
|
|
||||||
editEndpoint: editAdmission,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const pageContext = {
|
|
||||||
masterDetail,
|
|
||||||
formFields: admissionFormFields,
|
|
||||||
formActions: getAdmissionFormActions,
|
|
||||||
schema: admissionSchema,
|
|
||||||
initialForm: admissionInitialForm,
|
|
||||||
defaultErrors: {
|
|
||||||
create: admissionDefaultErrors,
|
|
||||||
edit: {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="flex w-full h-full overflow-hidden">
|
|
||||||
{#if masterDetail.showMaster}
|
|
||||||
<MasterPage {masterDetail} />
|
|
||||||
{/if}
|
|
||||||
<Separator orientation="vertical"/>
|
|
||||||
{#if masterDetail.showDetail}
|
|
||||||
<main class={`${masterDetail.isMobile ? 'w-full' : masterDetail.isFormMode ? 'w-[97%] flex flex-col items-start' : 'w-[65%]'} h-full overflow-y-auto flex flex-col items-center transition-all duration-300`}>
|
|
||||||
{#if masterDetail.mode === "view"}
|
|
||||||
<ViewPage context={pageContext}/>
|
|
||||||
{:else if masterDetail.mode === "create"}
|
|
||||||
<CreatePage context={pageContext}/>
|
|
||||||
{:else if masterDetail.mode === "edit"}
|
|
||||||
<EditPage context={pageContext}/>
|
|
||||||
{/if}
|
|
||||||
</main>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
Loading…
x
Reference in New Issue
Block a user