16022025 add page

add dict account
add dict site
add dict discipline
add dict department
add dict workstation
This commit is contained in:
faiztyanirh 2026-02-16 17:24:30 +07:00
parent 671a360c4c
commit 13617ba532
46 changed files with 2749 additions and 2 deletions

View File

@ -0,0 +1,97 @@
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: "Parent",
label: "Parent Account",
type: "select",
optionsEndpoint: `${API.BASE_URL}${API.ACCOUNT}`,
valueKey: "AccountID",
labelKey: "AccountName",
},
{
key: "AccountName",
label: "Account Name",
type: "text",
},
];
export const detailSections = [
{
class: "grid grid-cols-2 gap-4 items-center",
fields: [
{ key: "Parent", label: "Parent" },
{ key: "AccountName", label: "Account Name" },
{ key: "Initial", label: "Initial" },
]
},
{
title: "",
class: "grid grid-cols-2 gap-4",
groups: [
{
class: "space-y-3",
fields: [
{ key: "Country", label: "Country" },
{ key: "Province", label: "Province" },
{ key: "City", label: "City" },
]
},
{
class: "space-y-3",
fields: [
{ key: "Street_1", label: "Street 1" },
{ key: "Street_2", label: "Street 2" },
{ key: "Street_3", label: "Street 3" },
{ key: "ZIP", label: "ZIP" },
]
}
]
},
{
class: "grid grid-cols-2 gap-4 items-center",
groups: [
{
class: "space-y-3",
fields: [
{ key: "EmailAddress1", label: "Email Address 1" },
{ key: "EmailAddress2", label: "Email Address 2" },
]
},
{
class: "space-y-3",
fields: [
{ key: "Phone", label: "Phone" },
{ key: "Fax", label: "Fax" },
]
}
]
},
];
export function accountActions(masterDetail) {
return [
{
Icon: PlusIcon,
label: 'Add Account',
onClick: () => masterDetail.enterCreate(),
},
{
Icon: Settings2Icon,
label: 'Search Parameters',
},
];
}
export function viewActions(handlers){
return [
{
Icon: PencilIcon,
label: 'Edit Account',
onClick: handlers.editAccount,
},
]
}

View File

@ -0,0 +1,198 @@
import { API } from "$lib/config/api";
import EraserIcon from "@lucide/svelte/icons/eraser";
import { z } from "zod";
export const accountSchema = z.object({
Initial: z.string().min(1, "Required"),
AccountName: z.string().min(1, "Required"),
});
export const accountInitialForm = {
AccountID: '',
ParentAccount: '',
AccountName: '',
Initial: '',
Country: '',
Province: '',
City: '',
ZIP: '',
Street_1: '',
Street_2: '',
Street_3: '',
EmailAddress1: '',
EmailAddress2: '',
Phone: '',
Fax: '',
};
export const accountDefaultErrors = {
Initial: "Required",
AccountName: "Required",
};
export const accountFormFields = [
{
title: "Basic Information",
rows: [
{
type: "row",
columns: [
{
key: "ParentAccount",
label: "Parent Account",
required: false,
type: "select",
optionsEndpoint: `${API.BASE_URL}${API.ACCOUNT}`,
valueKey: "AccountID",
labelKey: "AccountName",
fullWidth: false
},
]
},
{
type: "row",
columns: [
{
key: "Initial",
label: "Initial",
required: true,
type: "text",
validateOn: ["input"]
},
{
key: "AccountName",
label: "Account Name",
required: true,
type: "text",
validateOn: ["input"]
},
]
},
]
},
{
title: "Address Detail",
rows: [
{
type: "row",
columns: [
{
key: "Country",
label: "Country",
required: false,
type: "select",
optionsEndpoint: `${API.BASE_URL}${API.VALUESET}/country`,
},
{
key: "Street1",
label: "Street 1",
required: false,
type: "text",
},
]
},
{
type: "row",
columns: [
{
key: "Province",
label: "Province",
required: false,
type: "select",
optionsEndpoint: `${API.BASE_URL}${API.PROVINCE}`,
},
{
key: "Street2",
label: "Street 2",
required: false,
type: "text",
}
]
},
{
type: "row",
columns: [
{
key: "City",
label: "City",
required: false,
type: "select",
optionsEndpoint: `${API.BASE_URL}${API.CITY}`,
dependsOn: "Province",
endpointParamKey: "ProvinceID"
},
{
key: "Street3",
label: "Street 3",
required: false,
type: "text",
}
]
},
{
type: "row",
columns: [
{
key: "ZIP",
label: "ZIP",
required: false,
type: "text",
validateOn: ["input"],
fullWidth: false
},
]
},
]
},
{
title: "Contact Information",
rows: [
{
type: "row",
columns: [
{
key: "EmailAddress1",
label: "Email Address 1",
required: false,
type: "text",
validateOn: ["input"]
},
{
key: "Phone",
label: "Phone",
required: false,
type: "text",
},
]
},
{
type: "row",
columns: [
{
key: "EmailAddress2",
label: "Email Address 2",
required: false,
type: "text",
validateOn: ["input"]
},
{
key: "Fax",
label: "Fax",
required: false,
type: "text",
}
]
},
]
},
];
export function getAccountFormActions(handlers) {
return [
{
Icon: EraserIcon,
label: 'Clear Form',
onClick: handlers.clearForm,
},
];
}

View 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('Account 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 Account" {primaryAction} {secondaryActions} {actions}>
<DictionaryFormRenderer
{formState}
formFields={formFields}
mode="create"
/>
</FormPageContainer>
<ReusableAlertDialog
bind:open={masterDetail.showExitConfirm}
onConfirm={masterDetail.confirmExit}
/>

View File

@ -0,0 +1,85 @@
<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);
}
});
});
});
if (formState.form.Province) {
formState.fetchOptions(
{
key: "City",
optionsEndpoint: `${API.BASE_URL}${API.CITY}`,
dependsOn: "Province",
endpointParamKey: "ProvinceID"
},
formState.form
);
}
});
});
async function handleEdit() {
const result = await formState.save(masterDetail.mode);
if (result.status === 'success') {
console.log('Account updated successfully');
toast('Account Updated!');
masterDetail.exitForm(true);
} else {
console.error('Failed to update account:', 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 Account" {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 { accountColumns } from "$lib/components/dictionary/account/table/account-columns";
import { getAccounts, getAccount } from "$lib/components/dictionary/account/api/account-api";
import { useSearch } from "$lib/components/composable/use-search.svelte";
import { searchFields, accountActions } from "$lib/components/dictionary/account/config/account-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 UserRoundXIcon from "@lucide/svelte/icons/user-round-x";
let props = $props();
const search = useSearch(searchFields, getAccounts);
const initialForm = props.masterDetail.formState.form;
const actions = accountActions(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 "ACCOUNT".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={accountColumns} handleRowClick={props.masterDetail.select} {activeRowId} rowIdKey="AccountID"/>
{:else}
<div class="flex h-full">
<ReusableEmpty icon={UserRoundXIcon} 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/account/config/account-config";
import TopbarWrapper from "$lib/components/topbar/topbar-wrapper.svelte";
import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte";
import UserRoundXIcon from "@lucide/svelte/icons/user-round-x";
let props = $props();
const { masterDetail, formFields, formActions, schema } = props.context;
let account = $derived(masterDetail?.selectedItem?.data);
const handlers = {
editAccount: () => masterDetail.enterEdit("data"),
};
const actions = viewActions(handlers);
function getFieldValue(field) {
if (!account) return "-";
if (field.keys) {
return field.keys
.map(k => field.parentKey ? account[field.parentKey]?.[k] : account[k])
.filter(val => val && val.trim() !== "")
.join(" / ");
}
return field.parentKey ? account[field.parentKey]?.[field.key] : account[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.AccountName}
{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={UserRoundXIcon} desc="Select an account to see details"/>
{/if}

View File

@ -0,0 +1,14 @@
export const accountColumns = [
{
accessorKey: "Initial",
header: "Initial",
},
{
accessorKey: "AccountName",
header: "Account Name",
},
{
accessorKey: "ParentAccount",
header: "Parent Account",
},
];

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 getDepartments(searchQuery) {
return await searchWithParams(API.DEPARTMENT, searchQuery)
}
export async function getDepartment(searchQuery) {
return await getById(API.DEPARTMENT, searchQuery)
}
export async function createDepartment(newDepartmentForm) {
return await create(API.DEPARTMENT, newDepartmentForm)
}
export async function editDepartment(editDepartmentForm) {
return await update(API.DEPARTMENT, editDepartmentForm)
}

View File

@ -0,0 +1,53 @@
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: "DepartmentCode",
label: "Department Code",
type: "text",
},
{
key: "DepartmentName",
label: "Department Name",
type: "text",
},
];
export const detailSections = [
{
class: "grid grid-cols-2 gap-4 items-center",
fields: [
{ key: "SiteID", label: "Site" },
{ key: "DisciplineID", label: "Discipline" },
{ key: "DepartmentCode", label: "Department Code" },
{ key: "DepartmentName", label: "Department Name" },
]
},
];
export function departmentActions(masterDetail) {
return [
{
Icon: PlusIcon,
label: 'Add Department',
onClick: () => masterDetail.enterCreate(),
},
{
Icon: Settings2Icon,
label: 'Search Parameters',
},
];
}
export function viewActions(handlers){
return [
{
Icon: PencilIcon,
label: 'Edit Department',
onClick: handlers.editDepartment,
},
]
}

View File

@ -0,0 +1,81 @@
import { API } from "$lib/config/api";
import EraserIcon from "@lucide/svelte/icons/eraser";
import { z } from "zod";
export const departmentSchema = z.object({
DepartmentCode: z.string().min(1, "Required"),
DepartmentName: z.string().min(1, "Required"),
});
export const departmentInitialForm = {
DepartmentID: '',
SiteID: '',
DisciplineID: '',
DepartmentCode: '',
DepartmentName: '',
};
export const departmentDefaultErrors = {
DepartmentCode: "Required",
DepartmentName: "Required",
};
export const departmentFormFields = [
{
title: "Basic Information",
rows: [
{
type: "row",
columns: [
{
key: "SiteID",
label: "Site",
required: false,
type: "select",
optionsEndpoint: `${API.BASE_URL}${API.SITE}`,
valueKey: "SiteID",
labelKey: "SiteName",
},
{
key: "DisciplineID",
label: "Discipline",
required: false,
type: "select",
optionsEndpoint: `${API.BASE_URL}${API.DISCIPLINE}`,
valueKey: "DisciplineID",
labelKey: "DisciplineName",
},
]
},
{
type: "row",
columns: [
{
key: "DepartmentCode",
label: "Department Code",
required: true,
type: "text",
validateOn: ["input"]
},
{
key: "DepartmentName",
label: "Department Name",
required: true,
type: "text",
validateOn: ["input"]
},
]
},
]
},
];
export function getDepartmentFormActions(handlers) {
return [
{
Icon: EraserIcon,
label: 'Clear Form',
onClick: handlers.clearForm,
},
];
}

View 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('Department 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 Department" {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('Department updated successfully');
toast('Department Updated!');
masterDetail.exitForm(true);
} else {
console.error('Failed to update department:', 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 Department" {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 { departmentColumns } from "$lib/components/dictionary/department/table/department-columns";
import { getDepartments, getDepartment } from "$lib/components/dictionary/department/api/department-api";
import { useSearch } from "$lib/components/composable/use-search.svelte";
import { searchFields, departmentActions } from "$lib/components/dictionary/department/config/department-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 FileXIcon from "@lucide/svelte/icons/file-x";
let props = $props();
const search = useSearch(searchFields, getDepartments);
const initialForm = props.masterDetail.formState.form;
const actions = departmentActions(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 "DEPARTMENT".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={departmentColumns} handleRowClick={props.masterDetail.select} {activeRowId} rowIdKey="DepartmentID"/>
{:else}
<div class="flex h-full">
<ReusableEmpty icon={FileXIcon} 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/department/config/department-config";
import TopbarWrapper from "$lib/components/topbar/topbar-wrapper.svelte";
import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte";
import FileXIcon from "@lucide/svelte/icons/file-x";
let props = $props();
const { masterDetail, formFields, formActions, schema } = props.context;
let department = $derived(masterDetail?.selectedItem?.data);
const handlers = {
editDepartment: () => masterDetail.enterEdit("data"),
};
const actions = viewActions(handlers);
function getFieldValue(field) {
if (!department) return "-";
if (field.keys) {
return field.keys
.map(k => field.parentKey ? department[field.parentKey]?.[k] : department[k])
.filter(val => val && val.trim() !== "")
.join(" / ");
}
return field.parentKey ? department[field.parentKey]?.[field.key] : department[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.DepartmentName}
{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={FileXIcon} desc="Select a department to see details"/>
{/if}

View File

@ -0,0 +1,10 @@
export const departmentColumns = [
{
accessorKey: "DepartmentCode",
header: "Department Code",
},
{
accessorKey: "DepartmentName",
header: "Department Name",
},
];

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 getDisciplines(searchQuery) {
return await searchWithParams(API.DISCIPLINE, searchQuery)
}
export async function getDiscipline(searchQuery) {
return await getById(API.DISCIPLINE, searchQuery)
}
export async function createDiscipline(newDisciplineForm) {
return await create(API.DISCIPLINE, newDisciplineForm)
}
export async function editDiscipline(editDisciplineForm) {
return await update(API.DISCIPLINE, editDisciplineForm)
}

View File

@ -0,0 +1,53 @@
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: "DisciplineCode",
label: "Discipline Code",
type: "text",
},
{
key: "DisciplineName",
label: "Discipline Name",
type: "text",
},
];
export const detailSections = [
{
class: "grid grid-cols-2 gap-4 items-center",
fields: [
{ key: "SiteID", label: "Site" },
{ key: "Parent", label: "Parent" },
{ key: "DisciplineCode", label: "Discipline Code" },
{ key: "DisciplineName", label: "Discipline Name" },
]
},
];
export function disciplineActions(masterDetail) {
return [
{
Icon: PlusIcon,
label: 'Add Discipline',
onClick: () => masterDetail.enterCreate(),
},
{
Icon: Settings2Icon,
label: 'Search Parameters',
},
];
}
export function viewActions(handlers){
return [
{
Icon: PencilIcon,
label: 'Edit Discipline',
onClick: handlers.editDiscipline,
},
]
}

View File

@ -0,0 +1,81 @@
import { API } from "$lib/config/api";
import EraserIcon from "@lucide/svelte/icons/eraser";
import { z } from "zod";
export const disciplineSchema = z.object({
DisciplineCode: z.string().min(1, "Required"),
DisciplineName: z.string().min(1, "Required"),
});
export const disciplineInitialForm = {
DisciplineID: '',
SiteID: '',
Parent: '',
DisciplineCode: '',
DisciplineName: '',
};
export const disciplineDefaultErrors = {
DisciplineCode: "Required",
DisciplineName: "Required",
};
export const disciplineFormFields = [
{
title: "Basic Information",
rows: [
{
type: "row",
columns: [
{
key: "SiteID",
label: "Site",
required: false,
type: "select",
optionsEndpoint: `${API.BASE_URL}${API.SITE}`,
valueKey: "SiteID",
labelKey: "SiteName",
},
{
key: "Parent",
label: "Parent",
required: false,
type: "select",
optionsEndpoint: `${API.BASE_URL}${API.DISCIPLINE}`,
valueKey: "DisciplineID",
labelKey: "DisciplineName",
},
]
},
{
type: "row",
columns: [
{
key: "DisciplineCode",
label: "Discipline Code",
required: true,
type: "text",
validateOn: ["input"]
},
{
key: "DisciplineName",
label: "Discipline Name",
required: true,
type: "text",
validateOn: ["input"]
},
]
},
]
},
];
export function getDisciplineFormActions(handlers) {
return [
{
Icon: EraserIcon,
label: 'Clear Form',
onClick: handlers.clearForm,
},
];
}

View 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('Discipline 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 Discipline" {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('Discipline updated successfully');
toast('Discipline Updated!');
masterDetail.exitForm(true);
} else {
console.error('Failed to update discipline:', 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 Discipline" {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 { disciplineColumns } from "$lib/components/dictionary/discipline/table/discipline-columns";
import { getDisciplines, getDiscipline } from "$lib/components/dictionary/discipline/api/discipline-api";
import { useSearch } from "$lib/components/composable/use-search.svelte";
import { searchFields, disciplineActions } from "$lib/components/dictionary/discipline/config/discipline-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 BookXIcon from "@lucide/svelte/icons/book-x";
let props = $props();
const search = useSearch(searchFields, getDisciplines);
const initialForm = props.masterDetail.formState.form;
const actions = disciplineActions(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 "DISCIPLINE".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={disciplineColumns} handleRowClick={props.masterDetail.select} {activeRowId} rowIdKey="DisciplineID"/>
{:else}
<div class="flex h-full">
<ReusableEmpty icon={BookXIcon} 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/discipline/config/discipline-config";
import TopbarWrapper from "$lib/components/topbar/topbar-wrapper.svelte";
import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte";
import BookXIcon from "@lucide/svelte/icons/book-x";
let props = $props();
const { masterDetail, formFields, formActions, schema } = props.context;
let discipline = $derived(masterDetail?.selectedItem?.data);
const handlers = {
editDiscipline: () => masterDetail.enterEdit("data"),
};
const actions = viewActions(handlers);
function getFieldValue(field) {
if (!discipline) return "-";
if (field.keys) {
return field.keys
.map(k => field.parentKey ? discipline[field.parentKey]?.[k] : discipline[k])
.filter(val => val && val.trim() !== "")
.join(" / ");
}
return field.parentKey ? discipline[field.parentKey]?.[field.key] : discipline[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.DisciplineName}
{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={BookXIcon} desc="Select a discipline to see details"/>
{/if}

View File

@ -0,0 +1,10 @@
export const disciplineColumns = [
{
accessorKey: "DisciplineCode",
header: "Discipline Code",
},
{
accessorKey: "DisciplineName",
header: "Discipline Name",
},
];

View File

@ -41,7 +41,7 @@
key: "City",
optionsEndpoint: `${API.BASE_URL}${API.CITY}`,
dependsOn: "Province",
endpointParamKey: "Parent"
endpointParamKey: "ProvinceID"
},
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 getSites(searchQuery) {
return await searchWithParams(API.SITE, searchQuery)
}
export async function getSite(searchQuery) {
return await getById(API.SITE, searchQuery)
}
export async function createSite(newSiteForm) {
return await create(API.SITE, newSiteForm)
}
export async function editSite(editSiteForm) {
return await update(API.SITE, editSiteForm)
}

View File

@ -0,0 +1,71 @@
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: "SiteCode",
label: "Site Code",
type: "text",
},
{
key: "SiteName",
label: "Site Name",
type: "text",
},
];
export const detailSections = [
{
class: "grid grid-cols-2 gap-4 items-center",
fields: [
{ key: "AccountID", label: "Account" },
{ key: "Parent", label: "Parent Account" },
{ key: "SiteCode", label: "Site Code" },
{ key: "SiteName", label: "Site Name" },
]
},
{
title: "",
class: "grid grid-cols-2 gap-4",
groups: [
{
class: "space-y-3",
fields: [
{ key: "SiteType", label: "Site Type" },
]
},
{
class: "space-y-3",
fields: [
{ key: "SiteClass", label: "Site Class" },
]
}
]
},
];
export function siteActions(masterDetail) {
return [
{
Icon: PlusIcon,
label: 'Add Site',
onClick: () => masterDetail.enterCreate(),
},
{
Icon: Settings2Icon,
label: 'Search Parameters',
},
];
}
export function viewActions(handlers){
return [
{
Icon: PencilIcon,
label: 'Edit Site',
onClick: handlers.editSite,
},
]
}

View File

@ -0,0 +1,107 @@
import { API } from "$lib/config/api";
import EraserIcon from "@lucide/svelte/icons/eraser";
import { z } from "zod";
export const siteSchema = z.object({
SiteCode: z.string().min(1, "Required"),
SiteName: z.string().min(1, "Required"),
});
export const siteInitialForm = {
SiteID: '',
AccountID: '',
Parent: '',
SiteCode: '',
SiteName: '',
SiteType: '',
SiteClass: '',
};
export const siteDefaultErrors = {
SiteCode: "Required",
SiteName: "Required",
};
export const siteFormFields = [
{
title: "Basic Information",
rows: [
{
type: "row",
columns: [
{
key: "AccountID",
label: "Account",
required: false,
type: "select",
optionsEndpoint: `${API.BASE_URL}${API.ACCOUNT}`,
valueKey: "AccountID",
labelKey: "AccountName",
},
{
key: "Parent",
label: "Parent",
required: false,
type: "select",
optionsEndpoint: `${API.BASE_URL}${API.ACCOUNT}`,
valueKey: "AccountID",
labelKey: "AccountName",
},
]
},
{
type: "row",
columns: [
{
key: "SiteCode",
label: "Site Code",
required: true,
type: "text",
validateOn: ["input"]
},
{
key: "SiteName",
label: "Site Name",
required: true,
type: "text",
validateOn: ["input"]
},
]
},
]
},
{
title: "Site Classification",
rows: [
{
type: "row",
columns: [
{
key: "SiteType",
label: "Site Type",
required: false,
type: "select",
optionsEndpoint: `${API.BASE_URL}${API.VALUESET}/site_type`,
},
{
key: "SiteClass",
label: "Site Class",
required: false,
type: "select",
optionsEndpoint: `${API.BASE_URL}${API.VALUESET}/site_class`,
},
]
},
]
},
];
export function getSiteFormActions(handlers) {
return [
{
Icon: EraserIcon,
label: 'Clear Form',
onClick: handlers.clearForm,
},
];
}

View 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('Site 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 Site" {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('Site updated successfully');
toast('Site Updated!');
masterDetail.exitForm(true);
} else {
console.error('Failed to update site:', 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 Site" {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 { siteColumns } from "$lib/components/dictionary/site/table/site-columns";
import { getSites, getSite } from "$lib/components/dictionary/site/api/site-api";
import { useSearch } from "$lib/components/composable/use-search.svelte";
import { searchFields, siteActions } from "$lib/components/dictionary/site/config/site-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 ClipboardXIcon from "@lucide/svelte/icons/clipboard-x";
let props = $props();
const search = useSearch(searchFields, getSites);
const initialForm = props.masterDetail.formState.form;
const actions = siteActions(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 "SITE".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={siteColumns} handleRowClick={props.masterDetail.select} {activeRowId} rowIdKey="SiteID"/>
{:else}
<div class="flex h-full">
<ReusableEmpty icon={ClipboardXIcon} 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/site/config/site-config";
import TopbarWrapper from "$lib/components/topbar/topbar-wrapper.svelte";
import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte";
import ClipboardXIcon from "@lucide/svelte/icons/clipboard-x";
let props = $props();
const { masterDetail, formFields, formActions, schema } = props.context;
let site = $derived(masterDetail?.selectedItem?.data);
const handlers = {
editSite: () => masterDetail.enterEdit("data"),
};
const actions = viewActions(handlers);
function getFieldValue(field) {
if (!site) return "-";
if (field.keys) {
return field.keys
.map(k => field.parentKey ? site[field.parentKey]?.[k] : site[k])
.filter(val => val && val.trim() !== "")
.join(" / ");
}
return field.parentKey ? site[field.parentKey]?.[field.key] : site[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.SiteName}
{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={ClipboardXIcon} desc="Select a site to see details"/>
{/if}

View File

@ -0,0 +1,10 @@
export const siteColumns = [
{
accessorKey: "SiteCode",
header: "Site Code",
},
{
accessorKey: "SiteName",
header: "Site Name",
},
];

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 getWorkstations(searchQuery) {
return await searchWithParams(API.WORKSTATION, searchQuery)
}
export async function getWorkstation(searchQuery) {
return await getById(API.WORKSTATION, searchQuery)
}
export async function createWorkstation(newWorkstationForm) {
return await create(API.WORKSTATION, newWorkstationForm)
}
export async function editWorkstation(editWorkstationForm) {
return await update(API.WORKSTATION, editWorkstationForm)
}

View File

@ -0,0 +1,53 @@
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: "WorkstationCode",
label: "Workstation Code",
type: "text",
},
{
key: "WorkstationName",
label: "Workstation Name",
type: "text",
},
];
export const detailSections = [
{
class: "grid grid-cols-2 gap-4 items-center",
fields: [
{ key: "DepartmentID", label: "Department" },
{ key: "Type", label: "Type" },
{ key: "WorkstationCode", label: "Workstation Code" },
{ key: "WorkstationName", label: "Workstation Name" },
]
},
];
export function workstationActions(masterDetail) {
return [
{
Icon: PlusIcon,
label: 'Add Workstation',
onClick: () => masterDetail.enterCreate(),
},
{
Icon: Settings2Icon,
label: 'Search Parameters',
},
];
}
export function viewActions(handlers){
return [
{
Icon: PencilIcon,
label: 'Edit Workstation',
onClick: handlers.editWorkstation,
},
]
}

View File

@ -0,0 +1,107 @@
import { API } from "$lib/config/api";
import EraserIcon from "@lucide/svelte/icons/eraser";
import { z } from "zod";
export const workstationSchema = z.object({
WorkstationCode: z.string().min(1, "Required"),
WorkstationName: z.string().min(1, "Required"),
});
export const workstationInitialForm = {
WorkstationID: '',
DepartmentID: '',
WorkstationCode: '',
WorkstationName: '',
Type: '',
LinkTo: '',
Enable: '',
};
export const workstationDefaultErrors = {
WorkstationCode: "Required",
WorkstationName: "Required",
};
export const workstationFormFields = [
{
title: "Basic Information",
rows: [
{
type: "row",
columns: [
{
key: "DepartmentID",
label: "Department",
required: false,
type: "select",
optionsEndpoint: `${API.BASE_URL}${API.DEPARTMENT}`,
valueKey: "DepartmentID",
labelKey: "DepartmentName",
},
{
key: "Type",
label: "Type",
required: false,
type: "select",
optionsEndpoint: `${API.BASE_URL}${API.VALUESET}/ws_type`,
},
]
},
{
type: "row",
columns: [
{
key: "WorkstationCode",
label: "Workstation Code",
required: true,
type: "text",
validateOn: ["input"]
},
{
key: "WorkstationName",
label: "Workstation Name",
required: true,
type: "text",
validateOn: ["input"]
},
]
},
]
},
{
title: "Workstation Configuration",
rows: [
{
type: "row",
columns: [
{
key: "LinkTo",
label: "LinkTo",
required: false,
type: "select",
optionsEndpoint: `${API.BASE_URL}${API.WORKSTATION}`,
valueKey: "WorkstationID",
labelKey: "WorkstationName",
},
{
key: "Enable",
label: "Enable",
required: false,
type: "select",
optionsEndpoint: `${API.BASE_URL}${API.VALUESET}/enable_disable`,
},
]
},
]
}
];
export function getWorkstationFormActions(handlers) {
return [
{
Icon: EraserIcon,
label: 'Clear Form',
onClick: handlers.clearForm,
},
];
}

View 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('Workstation 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 Workstation" {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('Workstation updated successfully');
toast('Workstation Updated!');
masterDetail.exitForm(true);
} else {
console.error('Failed to update workstation:', 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 Workstation" {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 { workstationColumns } from "$lib/components/dictionary/workstation/table/workstation-columns";
import { getWorkstations, getWorkstation } from "$lib/components/dictionary/workstation/api/workstation-api";
import { useSearch } from "$lib/components/composable/use-search.svelte";
import { searchFields, workstationActions } from "$lib/components/dictionary/workstation/config/workstation-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 MonitorXIcon from "@lucide/svelte/icons/monitor-x";
let props = $props();
const search = useSearch(searchFields, getWorkstations);
const initialForm = props.masterDetail.formState.form;
const actions = workstationActions(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 "WORKSTATION".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={workstationColumns} handleRowClick={props.masterDetail.select} {activeRowId} rowIdKey="WorkstationID"/>
{:else}
<div class="flex h-full">
<ReusableEmpty icon={MonitorXIcon} 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/workstation/config/workstation-config";
import TopbarWrapper from "$lib/components/topbar/topbar-wrapper.svelte";
import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte";
import MonitorXIcon from "@lucide/svelte/icons/monitor-x";
let props = $props();
const { masterDetail, formFields, formActions, schema } = props.context;
let workstation = $derived(masterDetail?.selectedItem?.data);
const handlers = {
editWorkstation: () => masterDetail.enterEdit("data"),
};
const actions = viewActions(handlers);
function getFieldValue(field) {
if (!workstation) return "-";
if (field.keys) {
return field.keys
.map(k => field.parentKey ? workstation[field.parentKey]?.[k] : workstation[k])
.filter(val => val && val.trim() !== "")
.join(" / ");
}
return field.parentKey ? workstation[field.parentKey]?.[field.key] : workstation[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.WorkstationName}
{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={MonitorXIcon} desc="Select a workstation to see details"/>
{/if}

View File

@ -0,0 +1,10 @@
export const workstationColumns = [
{
accessorKey: "WorkstationCode",
header: "Workstation Code",
},
{
accessorKey: "WorkstationName",
header: "Workstation Name",
},
];

View File

@ -9,6 +9,30 @@ export async function load({ url }) {
'/patient/admission': {
title: 'Patient Admission',
},
'/dictionary/location': {
title: 'Location'
},
'/dictionary/occupation': {
title: 'Occupation'
},
'/dictionary/container': {
title: 'Container'
},
'/dictionary/account': {
title: 'Account'
},
'/dictionary/site': {
title: 'Site'
},
'/dictionary/discipline': {
title: 'Discipline'
},
'/dictionary/department': {
title: 'Department'
},
'/dictionary/workstation': {
title: 'Workstation'
},
};
const config = routeConfig[url.pathname] || {

View File

@ -1 +1,51 @@
acc
<script>
import { Separator } from "$lib/components/ui/separator/index.js";
import { useMasterDetail } from "$lib/components/composable/use-master-detail.svelte";
import { getAccount, createAccount, editAccount } from "$lib/components/dictionary/account/api/account-api";
import MasterPage from "$lib/components/dictionary/account/page/master-page.svelte";
import ViewPage from "$lib/components/dictionary/account/page/view-page.svelte";
import CreatePage from "$lib/components/dictionary/account/page/create-page.svelte";
import EditPage from "$lib/components/dictionary/account/page/edit-page.svelte";
import { accountSchema, accountInitialForm, accountDefaultErrors, accountFormFields, getAccountFormActions } from "$lib/components/dictionary/account/config/account-form-config";
const masterDetail = useMasterDetail({
onSelect: async (row) => {
return await getAccount(row.AccountID);
},
formConfig: {
schema: accountSchema,
initialForm: accountInitialForm,
defaultErrors: accountDefaultErrors,
mode: 'create',
modeOpt: 'cascade',
saveEndpoint: createAccount,
editEndpoint: editAccount,
}
});
const pageContext = {
masterDetail,
formFields: accountFormFields,
formActions: getAccountFormActions,
schema: accountSchema,
initialForm: accountInitialForm,
}
</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>

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 { getDepartment, createDepartment, editDepartment } from "$lib/components/dictionary/department/api/department-api";
import MasterPage from "$lib/components/dictionary/department/page/master-page.svelte";
import ViewPage from "$lib/components/dictionary/department/page/view-page.svelte";
import CreatePage from "$lib/components/dictionary/department/page/create-page.svelte";
import EditPage from "$lib/components/dictionary/department/page/edit-page.svelte";
import { departmentSchema, departmentInitialForm, departmentDefaultErrors, departmentFormFields, getDepartmentFormActions } from "$lib/components/dictionary/department/config/department-form-config";
const masterDetail = useMasterDetail({
onSelect: async (row) => {
return await getDepartment(row.DepartmentID);
},
formConfig: {
schema: departmentSchema,
initialForm: departmentInitialForm,
defaultErrors: departmentDefaultErrors,
mode: 'create',
modeOpt: 'default',
saveEndpoint: createDepartment,
editEndpoint: editDepartment,
}
});
const pageContext = {
masterDetail,
formFields: departmentFormFields,
formActions: getDepartmentFormActions,
schema: departmentSchema,
initialForm: departmentInitialForm,
}
</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>

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 { getDiscipline, createDiscipline, editDiscipline } from "$lib/components/dictionary/discipline/api/discipline-api";
import MasterPage from "$lib/components/dictionary/discipline/page/master-page.svelte";
import ViewPage from "$lib/components/dictionary/discipline/page/view-page.svelte";
import CreatePage from "$lib/components/dictionary/discipline/page/create-page.svelte";
import EditPage from "$lib/components/dictionary/discipline/page/edit-page.svelte";
import { disciplineSchema, disciplineInitialForm, disciplineDefaultErrors, disciplineFormFields, getDisciplineFormActions } from "$lib/components/dictionary/discipline/config/discipline-form-config";
const masterDetail = useMasterDetail({
onSelect: async (row) => {
return await getDiscipline(row.DisciplineID);
},
formConfig: {
schema: disciplineSchema,
initialForm: disciplineInitialForm,
defaultErrors: disciplineDefaultErrors,
mode: 'create',
modeOpt: 'default',
saveEndpoint: createDiscipline,
editEndpoint: editDiscipline,
}
});
const pageContext = {
masterDetail,
formFields: disciplineFormFields,
formActions: getDisciplineFormActions,
schema: disciplineSchema,
initialForm: disciplineInitialForm,
}
</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>

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 { getSite, createSite, editSite } from "$lib/components/dictionary/site/api/site-api";
import MasterPage from "$lib/components/dictionary/site/page/master-page.svelte";
import ViewPage from "$lib/components/dictionary/site/page/view-page.svelte";
import CreatePage from "$lib/components/dictionary/site/page/create-page.svelte";
import EditPage from "$lib/components/dictionary/site/page/edit-page.svelte";
import { siteSchema, siteInitialForm, siteDefaultErrors, siteFormFields, getSiteFormActions } from "$lib/components/dictionary/site/config/site-form-config";
const masterDetail = useMasterDetail({
onSelect: async (row) => {
return await getSite(row.SiteID);
},
formConfig: {
schema: siteSchema,
initialForm: siteInitialForm,
defaultErrors: siteDefaultErrors,
mode: 'create',
modeOpt: 'default',
saveEndpoint: createSite,
editEndpoint: editSite,
}
});
const pageContext = {
masterDetail,
formFields: siteFormFields,
formActions: getSiteFormActions,
schema: siteSchema,
initialForm: siteInitialForm,
}
</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>

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 { getWorkstation, createWorkstation, editWorkstation } from "$lib/components/dictionary/workstation/api/workstation-api";
import MasterPage from "$lib/components/dictionary/workstation/page/master-page.svelte";
import ViewPage from "$lib/components/dictionary/workstation/page/view-page.svelte";
import CreatePage from "$lib/components/dictionary/workstation/page/create-page.svelte";
import EditPage from "$lib/components/dictionary/workstation/page/edit-page.svelte";
import { workstationSchema, workstationInitialForm, workstationDefaultErrors, workstationFormFields, getWorkstationFormActions } from "$lib/components/dictionary/workstation/config/workstation-form-config";
const masterDetail = useMasterDetail({
onSelect: async (row) => {
return await getWorkstation(row.WorkstationID);
},
formConfig: {
schema: workstationSchema,
initialForm: workstationInitialForm,
defaultErrors: workstationDefaultErrors,
mode: 'create',
modeOpt: 'default',
saveEndpoint: createWorkstation,
editEndpoint: editWorkstation,
}
});
const pageContext = {
masterDetail,
formFields: workstationFormFields,
formActions: getWorkstationFormActions,
schema: workstationSchema,
initialForm: workstationInitialForm,
}
</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>