mirror of
https://github.com/faiztyanirh/clqms-shadcn-v1.git
synced 2026-04-22 09:35:34 +07:00
16022025 add page
add dict account add dict site add dict discipline add dict department add dict workstation
This commit is contained in:
parent
671a360c4c
commit
13617ba532
@ -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,
|
||||
},
|
||||
]
|
||||
}
|
||||
@ -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,
|
||||
},
|
||||
];
|
||||
}
|
||||
@ -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}
|
||||
/>
|
||||
85
src/lib/components/dictionary/account/page/edit-page.svelte
Normal file
85
src/lib/components/dictionary/account/page/edit-page.svelte
Normal 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}
|
||||
/>
|
||||
@ -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>
|
||||
91
src/lib/components/dictionary/account/page/view-page.svelte
Normal file
91
src/lib/components/dictionary/account/page/view-page.svelte
Normal 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}
|
||||
@ -0,0 +1,14 @@
|
||||
export const accountColumns = [
|
||||
{
|
||||
accessorKey: "Initial",
|
||||
header: "Initial",
|
||||
},
|
||||
{
|
||||
accessorKey: "AccountName",
|
||||
header: "Account Name",
|
||||
},
|
||||
{
|
||||
accessorKey: "ParentAccount",
|
||||
header: "Parent Account",
|
||||
},
|
||||
];
|
||||
@ -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)
|
||||
}
|
||||
@ -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,
|
||||
},
|
||||
]
|
||||
}
|
||||
@ -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,
|
||||
},
|
||||
];
|
||||
}
|
||||
@ -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}
|
||||
/>
|
||||
@ -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}
|
||||
/>
|
||||
@ -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>
|
||||
@ -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}
|
||||
@ -0,0 +1,10 @@
|
||||
export const departmentColumns = [
|
||||
{
|
||||
accessorKey: "DepartmentCode",
|
||||
header: "Department Code",
|
||||
},
|
||||
{
|
||||
accessorKey: "DepartmentName",
|
||||
header: "Department Name",
|
||||
},
|
||||
];
|
||||
@ -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)
|
||||
}
|
||||
@ -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,
|
||||
},
|
||||
]
|
||||
}
|
||||
@ -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,
|
||||
},
|
||||
];
|
||||
}
|
||||
@ -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}
|
||||
/>
|
||||
@ -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}
|
||||
/>
|
||||
@ -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>
|
||||
@ -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}
|
||||
@ -0,0 +1,10 @@
|
||||
export const disciplineColumns = [
|
||||
{
|
||||
accessorKey: "DisciplineCode",
|
||||
header: "Discipline Code",
|
||||
},
|
||||
{
|
||||
accessorKey: "DisciplineName",
|
||||
header: "Discipline Name",
|
||||
},
|
||||
];
|
||||
@ -41,7 +41,7 @@
|
||||
key: "City",
|
||||
optionsEndpoint: `${API.BASE_URL}${API.CITY}`,
|
||||
dependsOn: "Province",
|
||||
endpointParamKey: "Parent"
|
||||
endpointParamKey: "ProvinceID"
|
||||
},
|
||||
formState.form
|
||||
);
|
||||
|
||||
18
src/lib/components/dictionary/site/api/site-api.js
Normal file
18
src/lib/components/dictionary/site/api/site-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 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)
|
||||
}
|
||||
71
src/lib/components/dictionary/site/config/site-config.js
Normal file
71
src/lib/components/dictionary/site/config/site-config.js
Normal 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,
|
||||
},
|
||||
]
|
||||
}
|
||||
107
src/lib/components/dictionary/site/config/site-form-config.js
Normal file
107
src/lib/components/dictionary/site/config/site-form-config.js
Normal 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,
|
||||
},
|
||||
];
|
||||
}
|
||||
54
src/lib/components/dictionary/site/page/create-page.svelte
Normal file
54
src/lib/components/dictionary/site/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('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}
|
||||
/>
|
||||
73
src/lib/components/dictionary/site/page/edit-page.svelte
Normal file
73
src/lib/components/dictionary/site/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('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}
|
||||
/>
|
||||
68
src/lib/components/dictionary/site/page/master-page.svelte
Normal file
68
src/lib/components/dictionary/site/page/master-page.svelte
Normal 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>
|
||||
91
src/lib/components/dictionary/site/page/view-page.svelte
Normal file
91
src/lib/components/dictionary/site/page/view-page.svelte
Normal 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}
|
||||
10
src/lib/components/dictionary/site/table/site-columns.js
Normal file
10
src/lib/components/dictionary/site/table/site-columns.js
Normal file
@ -0,0 +1,10 @@
|
||||
export const siteColumns = [
|
||||
{
|
||||
accessorKey: "SiteCode",
|
||||
header: "Site Code",
|
||||
},
|
||||
{
|
||||
accessorKey: "SiteName",
|
||||
header: "Site Name",
|
||||
},
|
||||
];
|
||||
@ -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)
|
||||
}
|
||||
@ -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,
|
||||
},
|
||||
]
|
||||
}
|
||||
@ -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,
|
||||
},
|
||||
];
|
||||
}
|
||||
@ -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}
|
||||
/>
|
||||
@ -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}
|
||||
/>
|
||||
@ -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>
|
||||
@ -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}
|
||||
@ -0,0 +1,10 @@
|
||||
export const workstationColumns = [
|
||||
{
|
||||
accessorKey: "WorkstationCode",
|
||||
header: "Workstation Code",
|
||||
},
|
||||
{
|
||||
accessorKey: "WorkstationName",
|
||||
header: "Workstation Name",
|
||||
},
|
||||
];
|
||||
@ -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] || {
|
||||
|
||||
@ -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>
|
||||
51
src/routes/dictionary/department/+page.svelte
Normal file
51
src/routes/dictionary/department/+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 { 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>
|
||||
51
src/routes/dictionary/discipline/+page.svelte
Normal file
51
src/routes/dictionary/discipline/+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 { 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>
|
||||
51
src/routes/dictionary/site/+page.svelte
Normal file
51
src/routes/dictionary/site/+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 { 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>
|
||||
51
src/routes/dictionary/workstation/+page.svelte
Normal file
51
src/routes/dictionary/workstation/+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 { 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>
|
||||
Loading…
x
Reference in New Issue
Block a user