add dict occupation

This commit is contained in:
faiztyanirh 2026-02-15 15:29:43 +07:00
parent b82441d9af
commit c834876d49
14 changed files with 513 additions and 27 deletions

View File

@ -70,21 +70,6 @@
},
],
},
{
title: "Value",
url: "#",
icon: BookOpenIcon,
submenus: [
{
title: "Value Set Def",
url: "#",
},
{
title: "Value Set",
url: "#",
},
],
},
{
title: "Sample",
url: "#",

View File

@ -63,7 +63,6 @@ export function useMasterDetail(options = {}) {
}
function enterEdit(param) {
console.log('f');
if (!selectedItem) return;
mode = "edit";

View File

@ -1,6 +1,7 @@
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 = [
{
@ -17,7 +18,7 @@ export const searchFields = [
key: "LocType",
label: "Location Type",
type: "select",
optionsEndpoint: `https://clqms01-api.services-summit.my.id/api/valueset/location_type`,
optionsEndpoint: `${API.BASE_URL}${API.VALUESET}/location_type`,
},
];

View File

@ -39,15 +39,14 @@
const secondaryActions = [];
$effect(() => {
if (masterDetail.form?.PatientID) {
formState.setForm({
...formState.form,
...masterDetail.form
});
}
});
$inspect(formState.form)
// $effect(() => {
// if (masterDetail.form?.PatientID) {
// formState.setForm({
// ...formState.form,
// ...masterDetail.form
// });
// }
// });
</script>
<FormPageContainer title="Create Location" {primaryAction} {secondaryActions} {actions}>

View File

@ -28,7 +28,7 @@
formState.fetchOptions(child, formState.form);
}
});
} else if ((col.type === "select" || col.type === "identity") && col.optionsEndpoint) {
} else if ((col.type === "select") && col.optionsEndpoint) {
formState.fetchOptions(col, formState.form);
}
});

View 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 getOccupations(searchQuery) {
return await searchWithParams(API.OCCUPATION, searchQuery)
}
export async function getOccupation(searchQuery) {
return await getById(API.OCCUPATION, searchQuery)
}
export async function createOccupation(newOccupationForm) {
return await create(API.OCCUPATION, newOccupationForm)
}
export async function editOccupation(editOccupationForm) {
return await update(API.OCCUPATION, editOccupationForm)
}

View File

@ -0,0 +1,62 @@
import PlusIcon from "@lucide/svelte/icons/plus";
import Settings2Icon from "@lucide/svelte/icons/settings-2";
import PencilIcon from "@lucide/svelte/icons/pencil";
export const searchFields = [
{
key: "OccCode",
label: "Occupation Code",
type: "text",
},
{
key: "OccText",
label: "Occupation Name",
type: "text",
},
{
key: "Description",
label: "Description",
type: "text",
},
];
export const detailSections = [
{
title: "",
class: "grid grid-cols-1 gap-4",
groups: [
{
class: "space-y-3",
fields: [
{ key: "OccCode", label: "Occupation Code" },
{ key: "OccText", label: "Occupation Text" },
{ key: "Description", label: "Description" },
]
},
]
},
];
export function occupationActions(masterDetail) {
return [
{
Icon: PlusIcon,
label: 'Add Location',
onClick: () => masterDetail.enterCreate(),
},
{
Icon: Settings2Icon,
label: 'Search Parameters',
},
];
}
export function viewActions(handlers){
return [
{
Icon: PencilIcon,
label: 'Edit Occupation',
onClick: handlers.editOccupation,
},
]
}

View File

@ -0,0 +1,65 @@
import { API } from "$lib/config/api";
import EraserIcon from "@lucide/svelte/icons/eraser";
import { z } from "zod";
export const occupationSchema = z.object({
OccCode: z.string().min(1, "Required"),
});
export const occupationInitialForm = {
OccupationID: '',
OccCode: '',
OccText: '',
Description: '',
};
export const occupationDefaultErrors = {
OccCode: "Required",
};
export const occupationFormFields = [
{
title: "Basic Information",
rows: [
{
type: "row",
columns: [
{
key: "OccCode",
label: "Occupation Code",
required: true,
type: "text",
validateOn: ["input"]
},
{
key: "OccText",
label: "Occupation Name",
required: false,
type: "text",
},
]
},
{
type: "row",
columns: [
{
key: "Description",
label: "Description",
required: false,
type: "textarea",
},
]
}
]
},
];
export function getOccupationFormActions(handlers) {
return [
{
Icon: EraserIcon,
label: 'Clear Form',
onClick: handlers.clearForm,
},
];
}

View File

@ -0,0 +1,64 @@
<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('Occupation Created!');
masterDetail?.exitForm(true);
}
const primaryAction = $derived({
label: 'Save',
onClick: handleSave,
disabled: helpers.hasErrors || formState.isSaving.current,
loading: formState.isSaving.current
});
const secondaryActions = [];
// $effect(() => {
// if (masterDetail.form?.PatientID) {
// formState.setForm({
// ...formState.form,
// ...masterDetail.form
// });
// }
// });
// $inspect(formState.form)
</script>
<FormPageContainer title="Create Occupation" {primaryAction} {secondaryActions} {actions}>
<DictionaryFormRenderer
{formState}
formFields={formFields}
mode="create"
/>
</FormPageContainer>
<ReusableAlertDialog
bind:open={masterDetail.showExitConfirm}
onConfirm={masterDetail.confirmExit}
/>

View 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('Occupation updated successfully');
toast('Occupation Updated!');
masterDetail.exitForm(true);
} else {
console.error('Failed to update occupation:', 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 Occupation" {primaryAction} {secondaryActions}>
<DictionaryFormRenderer
{formState}
formFields={formFields}
mode="edit"
/>
</FormPageContainer>
<ReusableAlertDialog
bind:open={masterDetail.showExitConfirm}
onConfirm={masterDetail.confirmExit}
/>

View File

@ -0,0 +1,68 @@
<script>
import { occupationColumns } from "$lib/components/dictionary/occupation/table/occupation-columns";
import { getOccupations, getOccupation } from "$lib/components/dictionary/occupation/api/occupation-api";
import { useSearch } from "$lib/components/composable/use-search.svelte";
import { searchFields, occupationActions } from "$lib/components/dictionary/occupation/config/occupation-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 FolderXIcon from "@lucide/svelte/icons/folder-x";
let props = $props();
const search = useSearch(searchFields, getOccupations);
const initialForm = props.masterDetail.formState.form;
const actions = occupationActions(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 "OCCUPATION".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={occupationColumns} handleRowClick={props.masterDetail.select} {activeRowId} rowIdKey="OccupationID"/>
{:else}
<div class="flex h-full">
<ReusableEmpty icon={FolderXIcon} desc="Try searching from search parameters"/>
</div>
{/if}
</div>
</div>
{/if}
</div>
</div>

View File

@ -0,0 +1,91 @@
<script>
import { formatUTCDate } from "$lib/utils/formatUTCDate";
import { detailSections, viewActions } from "$lib/components/dictionary/occupation/config/occupation-config";
import TopbarWrapper from "$lib/components/topbar/topbar-wrapper.svelte";
import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte";
import FolderXIcon from "@lucide/svelte/icons/folder-x";
let props = $props();
const { masterDetail, formFields, formActions, schema } = props.context;
let occupation = $derived(masterDetail?.selectedItem?.data);
const handlers = {
editOccupation: () => masterDetail.enterEdit("data"),
};
const actions = viewActions(handlers);
function getFieldValue(field) {
if (!occupation) return "-";
if (field.keys) {
return field.keys
.map(k => field.parentKey ? occupation[field.parentKey]?.[k] : occupation[k])
.filter(val => val && val.trim() !== "")
.join(" / ");
}
return field.parentKey ? occupation[field.parentKey]?.[field.key] : occupation[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.OccText}
{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={FolderXIcon} desc="Select an occupation to see details"/>
{/if}

View File

@ -0,0 +1,10 @@
export const occupationColumns = [
{
accessorKey: "OccCode",
header: "Occupation Code",
},
{
accessorKey: "OccText",
header: "Occupation Name",
},
];

View 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 { getOccupation, createOccupation, editOccupation } from "$lib/components/dictionary/occupation/api/occupation-api";
import MasterPage from "$lib/components/dictionary/occupation/page/master-page.svelte";
import ViewPage from "$lib/components/dictionary/occupation/page/view-page.svelte";
import CreatePage from "$lib/components/dictionary/occupation/page/create-page.svelte";
import EditPage from "$lib/components/dictionary/occupation/page/edit-page.svelte";
import { occupationSchema, occupationInitialForm, occupationDefaultErrors, occupationFormFields, getOccupationFormActions } from "$lib/components/dictionary/occupation/config/occupation-form-config";
const masterDetail = useMasterDetail({
onSelect: async (row) => {
return await getOccupation(row.OccupationID);
},
formConfig: {
schema: occupationSchema,
initialForm: occupationInitialForm,
defaultErrors: occupationDefaultErrors,
mode: 'create',
modeOpt: 'default',
saveEndpoint: createOccupation,
editEndpoint: editOccupation,
}
});
const pageContext = {
masterDetail,
formFields: occupationFormFields,
formActions: getOccupationFormActions,
schema: occupationSchema,
initialForm: occupationInitialForm,
}
</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>