mirror of
https://github.com/faiztyanirh/clqms-shadcn-v1.git
synced 2026-04-28 17:52:31 +07:00
initial testmap menu
This commit is contained in:
parent
23c3f88f4d
commit
18bb82ac19
@ -113,8 +113,18 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Test",
|
title: "Test",
|
||||||
url: "/dictionary/test",
|
url: "/dictionary",
|
||||||
icon: FlaskConicalIcon,
|
icon: FlaskConicalIcon,
|
||||||
|
submenus: [
|
||||||
|
{
|
||||||
|
title: "Test List",
|
||||||
|
url: "/test",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Test Map",
|
||||||
|
url: "/testmap",
|
||||||
|
},
|
||||||
|
]
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
18
src/lib/components/dictionary/testmap/api/testmap-api.js
Normal file
18
src/lib/components/dictionary/testmap/api/testmap-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 getTestMaps(searchQuery) {
|
||||||
|
return await searchWithParams(API.TESTMAP, searchQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTestMap(searchQuery) {
|
||||||
|
return await getById(API.TESTMAP, searchQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createTestMap(newTestMapForm) {
|
||||||
|
return await create(API.TESTMAP, newTestMapForm)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function editTestMap(editTestMapForm, id) {
|
||||||
|
return await update(API.TESTMAP, editTestMapForm, id)
|
||||||
|
}
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
import PlusIcon from "@lucide/svelte/icons/plus";
|
||||||
|
import Settings2Icon from "@lucide/svelte/icons/settings-2";
|
||||||
|
import PencilIcon from "@lucide/svelte/icons/pencil";
|
||||||
|
import { API } from "$lib/config/api";
|
||||||
|
|
||||||
|
export const searchFields = [
|
||||||
|
{
|
||||||
|
key: "TestSiteCode",
|
||||||
|
label: "Test Code",
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "TestSiteName",
|
||||||
|
label: "Test Name",
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "TestMapType",
|
||||||
|
label: "TestMap Type",
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
|
||||||
|
];
|
||||||
|
|
||||||
|
export const detailSections = [
|
||||||
|
{
|
||||||
|
class: "grid grid-cols-2 gap-4 items-center",
|
||||||
|
fields: [
|
||||||
|
{ key: "HostType", label: "Host Type" },
|
||||||
|
{ key: "HostID", label: "Host ID" },
|
||||||
|
{ key: "HostName", label: "Host Name" },
|
||||||
|
{ key: "ClientType", label: "Client Type" },
|
||||||
|
{ key: "ClientID", label: "Client ID" },
|
||||||
|
{ key: "ClientName", label: "Client Name" },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function testMapActions(masterDetail) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
Icon: PlusIcon,
|
||||||
|
label: 'Add TestMap',
|
||||||
|
onClick: () => masterDetail.enterCreate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Icon: Settings2Icon,
|
||||||
|
label: 'Search Parameters',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function viewActions(handlers){
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
Icon: PencilIcon,
|
||||||
|
label: 'Edit TestMap',
|
||||||
|
onClick: handlers.editTestMap,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -0,0 +1,154 @@
|
|||||||
|
import { API } from '$lib/config/api';
|
||||||
|
import EraserIcon from '@lucide/svelte/icons/eraser';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { cleanEmptyStrings } from '$lib/utils/cleanEmptyStrings';
|
||||||
|
|
||||||
|
export const testMapSchema = z
|
||||||
|
.object({
|
||||||
|
HostID: z.string().optional(),
|
||||||
|
ClientID: z.string().optional()
|
||||||
|
})
|
||||||
|
.superRefine((data, ctx) => {
|
||||||
|
const hostID = data.HostID;
|
||||||
|
const clientID = data.ClientID;
|
||||||
|
|
||||||
|
if (hostID && clientID && hostID === clientID) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: 'ClientID must be different from HostID',
|
||||||
|
path: ['ClientID']
|
||||||
|
});
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: 'HostID must be different from ClientID',
|
||||||
|
path: ['HostID']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const testMapInitialForm = {
|
||||||
|
TestMapID: '',
|
||||||
|
HostType: '',
|
||||||
|
HostID: '',
|
||||||
|
HostTestCode: '',
|
||||||
|
HostTestName: '',
|
||||||
|
ClientType: '',
|
||||||
|
ClientID: '',
|
||||||
|
ClientTestCode: '',
|
||||||
|
ClientTestName: '',
|
||||||
|
ConDefID: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
export const testMapDefaultErrors = {
|
||||||
|
HostID: null,
|
||||||
|
ClientID: null
|
||||||
|
};
|
||||||
|
|
||||||
|
export const testMapFormFields = [
|
||||||
|
{
|
||||||
|
title: 'Host & Client Information',
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
type: 'row',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
key: 'HostType',
|
||||||
|
label: 'Host Type',
|
||||||
|
required: false,
|
||||||
|
type: 'select',
|
||||||
|
optionsEndpoint: `${API.BASE_URL}${API.VALUESET}/entity_type`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'ClientType',
|
||||||
|
label: 'Client Type',
|
||||||
|
required: false,
|
||||||
|
type: 'select',
|
||||||
|
optionsEndpoint: `${API.BASE_URL}${API.VALUESET}/entity_type`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'row',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
key: 'HostID',
|
||||||
|
label: 'Host ID',
|
||||||
|
required: false,
|
||||||
|
type: 'text',
|
||||||
|
validateOn: ['input']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'ClientID',
|
||||||
|
label: 'Client ID',
|
||||||
|
required: false,
|
||||||
|
type: 'text',
|
||||||
|
validateOn: ['input']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'row',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
key: 'HostTestCode',
|
||||||
|
label: 'Host Test Code',
|
||||||
|
required: false,
|
||||||
|
type: 'text'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'ClientTestCode',
|
||||||
|
label: 'Client Test Code',
|
||||||
|
required: false,
|
||||||
|
type: 'text'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'row',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
key: 'HostTestName',
|
||||||
|
label: 'Host Test Name',
|
||||||
|
required: false,
|
||||||
|
type: 'text'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'ClientTestName',
|
||||||
|
label: 'Client Test Name',
|
||||||
|
required: false,
|
||||||
|
type: 'text'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'row',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
key: 'ConDefID',
|
||||||
|
label: 'Container Definition',
|
||||||
|
required: false,
|
||||||
|
type: 'select',
|
||||||
|
optionsEndpoint: `${API.BASE_URL}${API.CONTAINER}`,
|
||||||
|
valueKey: 'ConDefID',
|
||||||
|
labelKey: (item) => `${item.ConCode} - ${item.ConName}`,
|
||||||
|
fullWidth: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export function getTestMapFormActions(handlers) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
Icon: EraserIcon,
|
||||||
|
label: 'Clear Form',
|
||||||
|
onClick: handlers.clearForm
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildTestPayload() {
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
cr
|
||||||
@ -0,0 +1 @@
|
|||||||
|
ed
|
||||||
@ -0,0 +1,68 @@
|
|||||||
|
<script>
|
||||||
|
import { testMapColumns } from "$lib/components/dictionary/testmap/table/testmap-columns";
|
||||||
|
import { getTestMap, getTestMaps } from "$lib/components/dictionary/testmap/api/testmap-api";
|
||||||
|
import { useSearch } from "$lib/components/composable/use-search.svelte";
|
||||||
|
import { searchFields, testMapActions } from "$lib/components/dictionary/testmap/config/testmap-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 MapIcon from "@lucide/svelte/icons/map";
|
||||||
|
|
||||||
|
let props = $props();
|
||||||
|
|
||||||
|
const search = useSearch(searchFields, getTestMaps);
|
||||||
|
const initialForm = props.masterDetail.formState.form;
|
||||||
|
const actions = testMapActions(props.masterDetail, initialForm)
|
||||||
|
actions.find(a => a.label === 'Search Parameters').popoverContent = searchParamSnippet;
|
||||||
|
|
||||||
|
let activeRowId = $state(null);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#snippet searchParamSnippet()}
|
||||||
|
<ReusableSearchParam {searchFields}
|
||||||
|
bind:searchQuery={search.searchQuery} onSearch={search.handleSearch} onReset={search.handleReset} isLoading={search.isLoading}
|
||||||
|
selectOptions={search.selectOptions} loadingOptions={search.loadingOptions} fetchOptions={search.fetchOptions}
|
||||||
|
/>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
onclick={() => props.masterDetail.isFormMode && props.masterDetail.exitForm()}
|
||||||
|
onkeydown={(e) => e.key === 'Enter' && props.masterDetail.isFormMode && props.masterDetail.exitForm()}
|
||||||
|
class={`
|
||||||
|
${props.masterDetail.isMobile ? "w-full" : props.masterDetail.isFormMode ? "w-[3%] cursor-pointer" : "w-[35%]"}
|
||||||
|
transition-all duration-300 flex flex-col items-center p-2 h-full overflow-y-auto
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<div class={`flex w-full ${props.masterDetail.isFormMode ? "flex-col justify-center h-full items-center" : "flex-col justify-start h-full"}`} >
|
||||||
|
{#if props.masterDetail.isFormMode}
|
||||||
|
<span class="flex flex-col items-center justify-center gap-4 tracking-widest font-semibold select-none">
|
||||||
|
{#each "TEST MAP".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={testMapColumns} handleRowClick={props.masterDetail.select} {activeRowId} rowIdKey="TestMapID"/>
|
||||||
|
{:else}
|
||||||
|
<div class="flex h-full">
|
||||||
|
<ReusableEmpty icon={MapIcon} desc="Try searching from search parameters"/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
91
src/lib/components/dictionary/testmap/page/view-page.svelte
Normal file
91
src/lib/components/dictionary/testmap/page/view-page.svelte
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
<script>
|
||||||
|
import { formatUTCDate } from "$lib/utils/formatUTCDate";
|
||||||
|
import { detailSections, viewActions } from "$lib/components/dictionary/testmap/config/testmap-config";
|
||||||
|
import TopbarWrapper from "$lib/components/topbar/topbar-wrapper.svelte";
|
||||||
|
import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte";
|
||||||
|
import MapIcon from "@lucide/svelte/icons/map";
|
||||||
|
|
||||||
|
let props = $props();
|
||||||
|
|
||||||
|
const { masterDetail, formFields, formActions, schema } = props.context;
|
||||||
|
|
||||||
|
let test = $derived(masterDetail?.selectedItem?.data);
|
||||||
|
|
||||||
|
const handlers = {
|
||||||
|
editTest: () => masterDetail.enterEdit("data"),
|
||||||
|
};
|
||||||
|
|
||||||
|
const actions = viewActions(handlers);
|
||||||
|
|
||||||
|
function getFieldValue(field) {
|
||||||
|
if (!test) return "-";
|
||||||
|
|
||||||
|
if (field.keys) {
|
||||||
|
return field.keys
|
||||||
|
.map(k => field.parentKey ? test[field.parentKey]?.[k] : test[k])
|
||||||
|
.filter(val => val && val.trim() !== "")
|
||||||
|
.join(" / ");
|
||||||
|
}
|
||||||
|
|
||||||
|
return field.parentKey ? test[field.parentKey]?.[field.key] : test[field.key];
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#snippet Fieldset({ value, label, isUTCDate = false })}
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<dt class="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||||
|
{label}
|
||||||
|
</dt>
|
||||||
|
<dd class="text-sm font-medium">
|
||||||
|
{#if isUTCDate}
|
||||||
|
{formatUTCDate(value)}
|
||||||
|
{:else}
|
||||||
|
{value ?? "-"}
|
||||||
|
{/if}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
{#if masterDetail.selectedItem}
|
||||||
|
<div class="flex flex-col px-2 py-1 gap-2 h-full w-full">
|
||||||
|
<TopbarWrapper
|
||||||
|
title={masterDetail.selectedItem?.data?.TestSiteName}
|
||||||
|
{actions}
|
||||||
|
/>
|
||||||
|
<div class="flex-1 min-h-0 overflow-y-auto space-y-4">
|
||||||
|
{#each detailSections as section}
|
||||||
|
<div class="p-4">
|
||||||
|
{#if section.groups}
|
||||||
|
<div class={section.class}>
|
||||||
|
{#each section.groups as group}
|
||||||
|
<div>
|
||||||
|
<div class={group.class}>
|
||||||
|
{#each group.fields as field}
|
||||||
|
{@render Fieldset({
|
||||||
|
label: field.label,
|
||||||
|
value: getFieldValue(field),
|
||||||
|
isUTCDate: field.isUTCDate
|
||||||
|
})}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class={section.class}>
|
||||||
|
{#each section.fields as field}
|
||||||
|
{@render Fieldset({
|
||||||
|
label: field.label,
|
||||||
|
value: getFieldValue(field),
|
||||||
|
isUTCDate: field.isUTCDate
|
||||||
|
})}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<ReusableEmpty icon={MapIcon} desc="Select a test to see details"/>
|
||||||
|
{/if}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
export const testMapColumns = [
|
||||||
|
{
|
||||||
|
accessorKey: "HostType",
|
||||||
|
header: "Host",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "ClientType",
|
||||||
|
header: "Client",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "TestMapType",
|
||||||
|
header: "Type",
|
||||||
|
},
|
||||||
|
];
|
||||||
@ -25,4 +25,5 @@ export const API = {
|
|||||||
DEPARTMENT: '/api/organization/department',
|
DEPARTMENT: '/api/organization/department',
|
||||||
WORKSTATION: '/api/organization/workstation',
|
WORKSTATION: '/api/organization/workstation',
|
||||||
TEST: '/api/test',
|
TEST: '/api/test',
|
||||||
|
TESTMAP: '/api/test/testmap',
|
||||||
};
|
};
|
||||||
|
|||||||
@ -37,7 +37,10 @@ export async function load({ url }) {
|
|||||||
title: 'Workstation'
|
title: 'Workstation'
|
||||||
},
|
},
|
||||||
'/dictionary/test': {
|
'/dictionary/test': {
|
||||||
title: 'Test'
|
title: 'Test List'
|
||||||
|
},
|
||||||
|
'/dictionary/testmap': {
|
||||||
|
title: 'Test Map'
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
52
src/routes/dictionary/testmap/+page.svelte
Normal file
52
src/routes/dictionary/testmap/+page.svelte
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<script>
|
||||||
|
import { Separator } from "$lib/components/ui/separator/index.js";
|
||||||
|
import { useMasterDetail } from "$lib/components/composable/use-master-detail.svelte";
|
||||||
|
import { getTestMaps, createTestMap, editTestMap } from "$lib/components/dictionary/testmap/api/testmap-api";
|
||||||
|
import MasterPage from "$lib/components/dictionary/testmap/page/master-page.svelte";
|
||||||
|
import ViewPage from "$lib/components/dictionary/testmap/page/view-page.svelte";
|
||||||
|
import CreatePage from "$lib/components/dictionary/testmap/page/create-page.svelte";
|
||||||
|
import EditPage from "$lib/components/dictionary/testmap/page/edit-page.svelte";
|
||||||
|
import { testMapSchema, testMapInitialForm, testMapDefaultErrors, testMapFormFields, getTestMapFormActions } from "$lib/components/dictionary/testmap/config/testmap-form-config";
|
||||||
|
|
||||||
|
const masterDetail = useMasterDetail({
|
||||||
|
onSelect: async (row) => {
|
||||||
|
return await getTestMaps(row.TestMapID);
|
||||||
|
},
|
||||||
|
formConfig: {
|
||||||
|
schema: testMapSchema,
|
||||||
|
initialForm: testMapInitialForm,
|
||||||
|
defaultErrors: testMapDefaultErrors,
|
||||||
|
mode: 'create',
|
||||||
|
modeOpt: 'default',
|
||||||
|
saveEndpoint: createTestMap,
|
||||||
|
editEndpoint: editTestMap,
|
||||||
|
idKey: 'TestMapID',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const pageContext = {
|
||||||
|
masterDetail,
|
||||||
|
formFields: testMapFormFields,
|
||||||
|
formActions: getTestMapFormActions,
|
||||||
|
schema: testMapSchema,
|
||||||
|
initialForm: testMapInitialForm,
|
||||||
|
}
|
||||||
|
</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