change all page (add indicator spinner rowclick, popover autoclose after search)

This commit is contained in:
faiztyanirh 2026-04-14 10:46:47 +07:00
parent 239147f7ec
commit ea1369f69b
42 changed files with 314 additions and 230 deletions

View File

@ -2,6 +2,7 @@ import PlusIcon from "@lucide/svelte/icons/plus";
import Settings2Icon from "@lucide/svelte/icons/settings-2";
import PencilIcon from "@lucide/svelte/icons/pencil";
import { API } from "$lib/config/api";
import RefreshIcon from "@lucide/svelte/icons/refresh-cw";
export const searchFields = [
{
@ -72,8 +73,13 @@ export const detailSections = [
},
];
export function accountActions(masterDetail) {
export function accountActions(masterDetail, handlers) {
return [
{
Icon: RefreshIcon,
label: 'Refresh Data',
onClick: handlers.refresh,
},
{
Icon: PlusIcon,
label: 'Add Account',

View File

@ -26,8 +26,14 @@
async function handleSave() {
const result = await formState.save(masterDetail.mode);
toast('Account Created!');
masterDetail?.exitForm(true);
if (result.status === 'success') {
toast('Account Created!');
masterDetail?.exitForm(true);
} else {
console.error('Failed to save account');
const errorMessages = result.messages ? Object.values(result.messages).join('\n') : 'Failed to save account';
toast.error(errorMessages)
}
}
const primaryAction = $derived({

View File

@ -8,20 +8,23 @@
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";
import MoveLeftIcon from "@lucide/svelte/icons/move-left";
let props = $props();
const search = useSearch(searchFields, getAccounts);
const initialForm = props.masterDetail.formState.form;
const actions = accountActions(props.masterDetail, initialForm)
const handlers = {
refresh: () => {search.handleSearch()},
};
const actions = accountActions(props.masterDetail, handlers)
actions.find(a => a.label === 'Search Parameters').popoverContent = searchParamSnippet;
let activeRowId = $state(null);
</script>
{#snippet searchParamSnippet()}
{#snippet searchParamSnippet(close)}
<ReusableSearchParam {searchFields}
bind:searchQuery={search.searchQuery} onSearch={search.handleSearch} onReset={search.handleReset} isLoading={search.isLoading}
bind:searchQuery={search.searchQuery} onSearch={() => search.handleSearch(close)} onReset={search.handleReset} isLoading={search.isLoading}
selectOptions={search.selectOptions} loadingOptions={search.loadingOptions} fetchOptions={search.fetchOptions}
/>
{/snippet}
@ -38,10 +41,13 @@
>
<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 class="flex flex-col items-center justify-start gap-4 tracking-widest font-semibold select-none h-full">
<MoveLeftIcon />
<div class="flex flex-col items-center justify-center flex-grow gap-4">
{#each "ACCOUNT".split("") as c}
<span class="leading-none">{c}</span>
{/each}
</div>
</span>
{/if}

View File

@ -4,6 +4,7 @@
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";
import { Spinner } from "$lib/components/ui/spinner/index.js";
let props = $props();
@ -46,7 +47,11 @@
</div>
{/snippet}
{#if masterDetail.selectedItem}
{#if masterDetail.isLoadingDetail}
<div class="h-full w-full flex items-center justify-center">
<Spinner class="size-6" />
</div>
{:else if masterDetail.selectedItem}
<div class="flex flex-col px-2 py-1 gap-2 h-full w-full">
<TopbarWrapper
title={masterDetail.selectedItem.data.AccountName}

View File

@ -22,9 +22,9 @@
let activeRowId = $state(null);
</script>
{#snippet searchParamSnippet()}
{#snippet searchParamSnippet(close)}
<ReusableSearchParam {searchFields}
bind:searchQuery={search.searchQuery} onSearch={search.handleSearch} onReset={search.handleReset} isLoading={search.isLoading}
bind:searchQuery={search.searchQuery} onSearch={() => search.handleSearch(close)} onReset={search.handleReset} isLoading={search.isLoading}
selectOptions={search.selectOptions} loadingOptions={search.loadingOptions} fetchOptions={search.fetchOptions}
/>
{/snippet}

View File

@ -9,6 +9,7 @@
import * as Card from "$lib/components/ui/card/index.js";
import { Badge } from "$lib/components/ui/badge/index.js";
import * as Table from "$lib/components/ui/table/index.js";
import { Spinner } from "$lib/components/ui/spinner/index.js";
let props = $props();
@ -97,7 +98,11 @@
</div>
{/snippet}
{#if masterDetail.selectedItem}
{#if masterDetail.isLoadingDetail}
<div class="h-full w-full flex items-center justify-center">
<Spinner class="size-6" />
</div>
{:else if masterDetail.selectedItem}
<div class="flex flex-col px-2 py-1 gap-2 h-full w-full">
<TopbarWrapper
title={masterDetail.selectedItem.data.NameFirst}

View File

@ -1,6 +1,7 @@
import PlusIcon from "@lucide/svelte/icons/plus";
import Settings2Icon from "@lucide/svelte/icons/settings-2";
import PencilIcon from "@lucide/svelte/icons/pencil";
import RefreshIcon from "@lucide/svelte/icons/refresh-cw";
export const searchFields = [
{
@ -35,8 +36,13 @@ export const detailSections = [
},
];
export function containerActions(masterDetail) {
export function containerActions(masterDetail, handlers) {
return [
{
Icon: RefreshIcon,
label: 'Refresh Data',
onClick: handlers.refresh,
},
{
Icon: PlusIcon,
label: 'Add Location',

View File

@ -26,8 +26,14 @@
async function handleSave() {
const result = await formState.save(masterDetail.mode);
toast('Container Created!');
masterDetail?.exitForm(true);
if (result.status === 'success') {
toast('Container Created!');
masterDetail?.exitForm(true);
} else {
console.error('Failed to save container');
const errorMessages = result.messages ? Object.values(result.messages).join('\n') : 'Failed to save container';
toast.error(errorMessages)
}
}
const primaryAction = $derived({

View File

@ -8,20 +8,23 @@
import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte";
import ReusableDataTable from "$lib/components/reusable/reusable-data-table.svelte";
import ArchiveXIcon from "@lucide/svelte/icons/archive-x";
import MoveLeftIcon from "@lucide/svelte/icons/move-left";
let props = $props();
const search = useSearch(searchFields, getContainers);
const initialForm = props.masterDetail.formState.form;
const actions = containerActions(props.masterDetail, initialForm)
const handlers = {
refresh: () => {search.handleSearch()},
};
const actions = containerActions(props.masterDetail, handlers)
actions.find(a => a.label === 'Search Parameters').popoverContent = searchParamSnippet;
let activeRowId = $state(null);
</script>
{#snippet searchParamSnippet()}
{#snippet searchParamSnippet(close)}
<ReusableSearchParam {searchFields}
bind:searchQuery={search.searchQuery} onSearch={search.handleSearch} onReset={search.handleReset} isLoading={search.isLoading}
bind:searchQuery={search.searchQuery} onSearch={() => search.handleSearch(close)} onReset={search.handleReset} isLoading={search.isLoading}
selectOptions={search.selectOptions} loadingOptions={search.loadingOptions} fetchOptions={search.fetchOptions}
/>
{/snippet}
@ -38,10 +41,13 @@
>
<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 "CONTAINER".split("") as c}
<span class="leading-none">{c}</span>
{/each}
<span class="flex flex-col items-center justify-start gap-4 tracking-widest font-semibold select-none h-full">
<MoveLeftIcon />
<div class="flex flex-col items-center justify-center flex-grow gap-4">
{#each "CONTAINER".split("") as c}
<span class="leading-none">{c}</span>
{/each}
</div>
</span>
{/if}

View File

@ -4,6 +4,7 @@
import TopbarWrapper from "$lib/components/topbar/topbar-wrapper.svelte";
import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte";
import ArchiveXIcon from "@lucide/svelte/icons/archive-x";
import { Spinner } from "$lib/components/ui/spinner/index.js";
let props = $props();
@ -46,7 +47,11 @@
</div>
{/snippet}
{#if masterDetail.selectedItem}
{#if masterDetail.isLoadingDetail}
<div class="h-full w-full flex items-center justify-center">
<Spinner class="size-6" />
</div>
{:else if masterDetail.selectedItem}
<div class="flex flex-col px-2 py-1 gap-2 h-full w-full">
<TopbarWrapper
title={masterDetail.selectedItem.data.ConName}

View File

@ -2,6 +2,7 @@ import PlusIcon from "@lucide/svelte/icons/plus";
import Settings2Icon from "@lucide/svelte/icons/settings-2";
import PencilIcon from "@lucide/svelte/icons/pencil";
import { API } from "$lib/config/api";
import RefreshIcon from "@lucide/svelte/icons/refresh-cw";
export const searchFields = [
{
@ -28,8 +29,13 @@ export const detailSections = [
},
];
export function departmentActions(masterDetail) {
export function departmentActions(masterDetail, handlers) {
return [
{
Icon: RefreshIcon,
label: 'Refresh Data',
onClick: handlers.refresh,
},
{
Icon: PlusIcon,
label: 'Add Department',

View File

@ -26,8 +26,14 @@
async function handleSave() {
const result = await formState.save(masterDetail.mode);
toast('Department Created!');
masterDetail?.exitForm(true);
if (result.status === 'success') {
toast('Department Created!');
masterDetail?.exitForm(true);
} else {
console.error('Failed to save department');
const errorMessages = result.messages ? Object.values(result.messages).join('\n') : 'Failed to save department';
toast.error(errorMessages)
}
}
const primaryAction = $derived({

View File

@ -8,20 +8,23 @@
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";
import MoveLeftIcon from "@lucide/svelte/icons/move-left";
let props = $props();
const search = useSearch(searchFields, getDepartments);
const initialForm = props.masterDetail.formState.form;
const actions = departmentActions(props.masterDetail, initialForm)
const handlers = {
refresh: () => {search.handleSearch()},
};
const actions = departmentActions(props.masterDetail, handlers)
actions.find(a => a.label === 'Search Parameters').popoverContent = searchParamSnippet;
let activeRowId = $state(null);
</script>
{#snippet searchParamSnippet()}
{#snippet searchParamSnippet(close)}
<ReusableSearchParam {searchFields}
bind:searchQuery={search.searchQuery} onSearch={search.handleSearch} onReset={search.handleReset} isLoading={search.isLoading}
bind:searchQuery={search.searchQuery} onSearch={() => search.handleSearch(close)} onReset={search.handleReset} isLoading={search.isLoading}
selectOptions={search.selectOptions} loadingOptions={search.loadingOptions} fetchOptions={search.fetchOptions}
/>
{/snippet}
@ -38,10 +41,13 @@
>
<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 class="flex flex-col items-center justify-start gap-4 tracking-widest font-semibold select-none h-full">
<MoveLeftIcon />
<div class="flex flex-col items-center justify-center flex-grow gap-4">
{#each "DEPARTMENT".split("") as c}
<span class="leading-none">{c}</span>
{/each}
</div>
</span>
{/if}

View File

@ -4,6 +4,7 @@
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";
import { Spinner } from "$lib/components/ui/spinner/index.js";
let props = $props();
@ -46,7 +47,11 @@
</div>
{/snippet}
{#if masterDetail.selectedItem}
{#if masterDetail.isLoadingDetail}
<div class="h-full w-full flex items-center justify-center">
<Spinner class="size-6" />
</div>
{:else if masterDetail.selectedItem}
<div class="flex flex-col px-2 py-1 gap-2 h-full w-full">
<TopbarWrapper
title={masterDetail.selectedItem.data.DepartmentName}

View File

@ -2,6 +2,7 @@ import PlusIcon from "@lucide/svelte/icons/plus";
import Settings2Icon from "@lucide/svelte/icons/settings-2";
import PencilIcon from "@lucide/svelte/icons/pencil";
import { API } from "$lib/config/api";
import RefreshIcon from "@lucide/svelte/icons/refresh-cw";
export const searchFields = [
{
@ -30,8 +31,13 @@ export const detailSections = [
},
];
export function disciplineActions(masterDetail) {
export function disciplineActions(masterDetail, handlers) {
return [
{
Icon: RefreshIcon,
label: 'Refresh Data',
onClick: handlers.refresh,
},
{
Icon: PlusIcon,
label: 'Add Discipline',

View File

@ -26,8 +26,14 @@
async function handleSave() {
const result = await formState.save(masterDetail.mode);
toast('Discipline Created!');
masterDetail?.exitForm(true);
if (result.status === 'success') {
toast('Discipline Created!');
masterDetail?.exitForm(true);
} else {
console.error('Failed to save discipline');
const errorMessages = result.messages ? Object.values(result.messages).join('\n') : 'Failed to save discipline';
toast.error(errorMessages)
}
}
const primaryAction = $derived({

View File

@ -8,20 +8,23 @@
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";
import MoveLeftIcon from "@lucide/svelte/icons/move-left";
let props = $props();
const search = useSearch(searchFields, getDisciplines);
const initialForm = props.masterDetail.formState.form;
const actions = disciplineActions(props.masterDetail, initialForm)
const handlers = {
refresh: () => {search.handleSearch()},
};
const actions = disciplineActions(props.masterDetail, handlers)
actions.find(a => a.label === 'Search Parameters').popoverContent = searchParamSnippet;
let activeRowId = $state(null);
</script>
{#snippet searchParamSnippet()}
{#snippet searchParamSnippet(close)}
<ReusableSearchParam {searchFields}
bind:searchQuery={search.searchQuery} onSearch={search.handleSearch} onReset={search.handleReset} isLoading={search.isLoading}
bind:searchQuery={search.searchQuery} onSearch={() => search.handleSearch(close)} onReset={search.handleReset} isLoading={search.isLoading}
selectOptions={search.selectOptions} loadingOptions={search.loadingOptions} fetchOptions={search.fetchOptions}
/>
{/snippet}
@ -38,10 +41,13 @@
>
<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 class="flex flex-col items-center justify-start gap-4 tracking-widest font-semibold select-none h-full">
<MoveLeftIcon />
<div class="flex flex-col items-center justify-center flex-grow gap-4">
{#each "DISCIPLINE".split("") as c}
<span class="leading-none">{c}</span>
{/each}
</div>
</span>
{/if}

View File

@ -4,6 +4,7 @@
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";
import { Spinner } from "$lib/components/ui/spinner/index.js";
let props = $props();
@ -46,7 +47,11 @@
</div>
{/snippet}
{#if masterDetail.selectedItem}
{#if masterDetail.isLoadingDetail}
<div class="h-full w-full flex items-center justify-center">
<Spinner class="size-6" />
</div>
{:else if masterDetail.selectedItem}
<div class="flex flex-col px-2 py-1 gap-2 h-full w-full">
<TopbarWrapper
title={masterDetail.selectedItem.data.DisciplineName}

View File

@ -22,9 +22,9 @@
let activeRowId = $state(null);
</script>
{#snippet searchParamSnippet()}
{#snippet searchParamSnippet(close)}
<ReusableSearchParam {searchFields}
bind:searchQuery={search.searchQuery} onSearch={search.handleSearch} onReset={search.handleReset} isLoading={search.isLoading}
bind:searchQuery={search.searchQuery} onSearch={() => search.handleSearch(close)} onReset={search.handleReset} isLoading={search.isLoading}
selectOptions={search.selectOptions} loadingOptions={search.loadingOptions} fetchOptions={search.fetchOptions}
/>
{/snippet}

View File

@ -4,6 +4,7 @@
import TopbarWrapper from "$lib/components/topbar/topbar-wrapper.svelte";
import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte";
import MapPinXIcon from "@lucide/svelte/icons/map-pin-x";
import { Spinner } from "$lib/components/ui/spinner/index.js";
let props = $props();
@ -46,7 +47,11 @@
</div>
{/snippet}
{#if masterDetail.selectedItem}
{#if masterDetail.isLoadingDetail}
<div class="h-full w-full flex items-center justify-center">
<Spinner class="size-6" />
</div>
{:else if masterDetail.selectedItem}
<div class="flex flex-col px-2 py-1 gap-2 h-full w-full">
<TopbarWrapper
title={masterDetail.selectedItem.data.LocFull}

View File

@ -26,8 +26,14 @@
async function handleSave() {
const result = await formState.save(masterDetail.mode);
toast('Occupation Created!');
masterDetail?.exitForm(true);
if (result.status === 'success') {
toast('Occupation Created!');
masterDetail?.exitForm(true);
} else {
console.error('Failed to save occupation');
const errorMessages = result.messages ? Object.values(result.messages).join('\n') : 'Failed to save occupation';
toast.error(errorMessages)
}
}
const primaryAction = $derived({

View File

@ -2,6 +2,7 @@ import PlusIcon from "@lucide/svelte/icons/plus";
import Settings2Icon from "@lucide/svelte/icons/settings-2";
import PencilIcon from "@lucide/svelte/icons/pencil";
import { API } from "$lib/config/api";
import RefreshIcon from "@lucide/svelte/icons/refresh-cw";
export const searchFields = [
{
@ -35,8 +36,13 @@ export const detailSections = [
},
];
export function siteActions(masterDetail) {
export function siteActions(masterDetail, handlers) {
return [
{
Icon: RefreshIcon,
label: 'Refresh Data',
onClick: handlers.refresh,
},
{
Icon: PlusIcon,
label: 'Add Site',

View File

@ -26,8 +26,14 @@
async function handleSave() {
const result = await formState.save(masterDetail.mode);
toast('Site Created!');
masterDetail?.exitForm(true);
if (result.status === 'success') {
toast('Site Created!');
masterDetail?.exitForm(true);
} else {
console.error('Failed to save site');
const errorMessages = result.messages ? Object.values(result.messages).join('\n') : 'Failed to save site';
toast.error(errorMessages)
}
}
const primaryAction = $derived({

View File

@ -8,20 +8,23 @@
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";
import MoveLeftIcon from "@lucide/svelte/icons/move-left";
let props = $props();
const search = useSearch(searchFields, getSites);
const initialForm = props.masterDetail.formState.form;
const actions = siteActions(props.masterDetail, initialForm)
const handlers = {
refresh: () => {search.handleSearch()},
};
const actions = siteActions(props.masterDetail, handlers)
actions.find(a => a.label === 'Search Parameters').popoverContent = searchParamSnippet;
let activeRowId = $state(null);
</script>
{#snippet searchParamSnippet()}
{#snippet searchParamSnippet(close)}
<ReusableSearchParam {searchFields}
bind:searchQuery={search.searchQuery} onSearch={search.handleSearch} onReset={search.handleReset} isLoading={search.isLoading}
bind:searchQuery={search.searchQuery} onSearch={() => search.handleSearch(close)} onReset={search.handleReset} isLoading={search.isLoading}
selectOptions={search.selectOptions} loadingOptions={search.loadingOptions} fetchOptions={search.fetchOptions}
/>
{/snippet}
@ -38,10 +41,13 @@
>
<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 class="flex flex-col items-center justify-start gap-4 tracking-widest font-semibold select-none h-full">
<MoveLeftIcon />
<div class="flex flex-col items-center justify-center flex-grow gap-4">
{#each "SITE".split("") as c}
<span class="leading-none">{c}</span>
{/each}
</div>
</span>
{/if}

View File

@ -4,6 +4,7 @@
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";
import { Spinner } from "$lib/components/ui/spinner/index.js";
let props = $props();
@ -46,7 +47,11 @@
</div>
{/snippet}
{#if masterDetail.selectedItem}
{#if masterDetail.isLoadingDetail}
<div class="h-full w-full flex items-center justify-center">
<Spinner class="size-6" />
</div>
{:else if masterDetail.selectedItem}
<div class="flex flex-col px-2 py-1 gap-2 h-full w-full">
<TopbarWrapper
title={masterDetail.selectedItem.data.SiteName}

View File

@ -2,6 +2,7 @@ import PlusIcon from "@lucide/svelte/icons/plus";
import Settings2Icon from "@lucide/svelte/icons/settings-2";
import PencilIcon from "@lucide/svelte/icons/pencil";
import { API } from "$lib/config/api";
import RefreshIcon from "@lucide/svelte/icons/refresh-cw";
export const searchFields = [
{
@ -36,8 +37,13 @@ export const detailSections = [
},
];
export function testActions(masterDetail) {
export function testActions(masterDetail, handlers) {
return [
{
Icon: RefreshIcon,
label: 'Refresh Data',
onClick: handlers.refresh,
},
{
Icon: PlusIcon,
label: 'Add Test',

View File

@ -132,8 +132,14 @@
const result = await formState.save(masterDetail.mode, payload);
toast('Test Created!');
masterDetail?.exitForm(true);
if (result.status === 'success') {
toast('Test Created!');
masterDetail?.exitForm(true);
} else {
console.error('Failed to save test');
const errorMessages = result.messages ? Object.values(result.messages).join('\n') : 'Failed to save test';
toast.error(errorMessages)
}
}
const primaryAction = $derived({

View File

@ -125,8 +125,15 @@
const result = await formState.save(masterDetail.mode, payload);
toast('Test Updated!');
masterDetail?.exitForm(true);
if (result.status === 'success') {
console.log('Test updated successfully');
toast('Test Updated!');
masterDetail.exitForm(true);
} else {
console.error('Failed to update test:', result.message);
const errorMessages = result.messages ? Object.values(result.messages).join('\n') : 'Failed to update test';
toast.error(errorMessages)
}
}
const primaryAction = $derived({

View File

@ -8,20 +8,23 @@
import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte";
import ReusableDataTable from "$lib/components/reusable/reusable-data-table.svelte";
import FlaskConicalOffIcon from "@lucide/svelte/icons/flask-conical-off";
import MoveLeftIcon from "@lucide/svelte/icons/move-left";
let props = $props();
const search = useSearch(searchFields, getTests);
const initialForm = props.masterDetail.formState.form;
const actions = testActions(props.masterDetail, initialForm)
const handlers = {
refresh: () => {search.handleSearch()},
};
const actions = testActions(props.masterDetail, handlers)
actions.find(a => a.label === 'Search Parameters').popoverContent = searchParamSnippet;
let activeRowId = $state(null);
</script>
{#snippet searchParamSnippet()}
{#snippet searchParamSnippet(close)}
<ReusableSearchParam {searchFields}
bind:searchQuery={search.searchQuery} onSearch={search.handleSearch} onReset={search.handleReset} isLoading={search.isLoading}
bind:searchQuery={search.searchQuery} onSearch={() => search.handleSearch(close)} onReset={search.handleReset} isLoading={search.isLoading}
selectOptions={search.selectOptions} loadingOptions={search.loadingOptions} fetchOptions={search.fetchOptions}
/>
{/snippet}
@ -38,10 +41,13 @@
>
<div class={`flex w-full ${props.masterDetail.isFormMode ? "flex-col justify-center h-full items-center" : "flex-col justify-start h-full"}`} >
{#if props.masterDetail.isFormMode}
<span class="flex flex-col items-center justify-center gap-4 tracking-widest font-semibold select-none">
{#each "TEST".split("") as c}
<span class="leading-none">{c}</span>
{/each}
<span class="flex flex-col items-center justify-start gap-4 tracking-widest font-semibold select-none h-full">
<MoveLeftIcon />
<div class="flex flex-col items-center justify-center flex-grow gap-4">
{#each "TEST".split("") as c}
<span class="leading-none">{c}</span>
{/each}
</div>
</span>
{/if}

View File

@ -4,6 +4,7 @@
import TopbarWrapper from "$lib/components/topbar/topbar-wrapper.svelte";
import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte";
import FlaskConicalOffIcon from "@lucide/svelte/icons/flask-conical-off";
import { Spinner } from "$lib/components/ui/spinner/index.js";
let props = $props();
@ -46,7 +47,11 @@
</div>
{/snippet}
{#if masterDetail.selectedItem}
{#if masterDetail.isLoadingDetail}
<div class="h-full w-full flex items-center justify-center">
<Spinner class="size-6" />
</div>
{:else if masterDetail.selectedItem}
<div class="flex flex-col px-2 py-1 gap-2 h-full w-full">
<TopbarWrapper
title={masterDetail.selectedItem?.data?.TestSiteName}

View File

@ -2,6 +2,7 @@ import PlusIcon from "@lucide/svelte/icons/plus";
import Settings2Icon from "@lucide/svelte/icons/settings-2";
import PencilIcon from "@lucide/svelte/icons/pencil";
import { API } from "$lib/config/api";
import RefreshIcon from "@lucide/svelte/icons/refresh-cw";
export const searchFields = [
{
@ -35,8 +36,13 @@ export const detailSections = [
},
];
export function testMapActions(masterDetail) {
export function testMapActions(masterDetail, handlers) {
return [
{
Icon: RefreshIcon,
label: 'Refresh Data',
onClick: handlers.refresh,
},
{
Icon: PlusIcon,
label: 'Add TestMap',

View File

@ -115,8 +115,14 @@
console.log(payload)
const result = await formState.save(masterDetail.mode, payload);
toast('Test Map Created!');
masterDetail?.exitForm(true);
if (result.status === 'success') {
toast('Test Map Created!');
masterDetail?.exitForm(true);
} else {
console.error('Failed to save test map');
const errorMessages = result.messages ? Object.values(result.messages).join('\n') : 'Failed to save test map';
toast.error(errorMessages)
}
}
const primaryAction = $derived({

View File

@ -13,16 +13,18 @@
let props = $props();
const search = useSearch(searchFields, getTestMaps);
const initialForm = props.masterDetail.formState.form;
const actions = testMapActions(props.masterDetail, initialForm)
const handlers = {
refresh: () => {search.handleSearch()},
};
const actions = testMapActions(props.masterDetail, handlers)
actions.find(a => a.label === 'Search Parameters').popoverContent = searchParamSnippet;
let activeRowId = $state(null);
</script>
{#snippet searchParamSnippet()}
{#snippet searchParamSnippet(close)}
<ReusableSearchParam {searchFields}
bind:searchQuery={search.searchQuery} onSearch={search.handleSearch} onReset={search.handleReset} isLoading={search.isLoading}
bind:searchQuery={search.searchQuery} onSearch={() => search.handleSearch(close)} onReset={search.handleReset} isLoading={search.isLoading}
selectOptions={search.selectOptions} loadingOptions={search.loadingOptions} fetchOptions={search.fetchOptions}
/>
{/snippet}
@ -42,7 +44,7 @@
<span class="flex flex-col items-center justify-start gap-4 tracking-widest font-semibold select-none h-full">
<MoveLeftIcon />
<div class="flex flex-col items-center justify-center flex-grow gap-4">
{#each "BACK TO TEST MAP".split("") as c}
{#each "TEST MAP".split("") as c}
<span class="leading-none">{c}</span>
{/each}
</div>

View File

@ -1,123 +0,0 @@
<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 testMap = $derived(masterDetail?.selectedItem?.data);
$inspect(testMap)
const handlers = {
editTest: () => masterDetail.enterEdit("data"),
};
const actions = viewActions(handlers);
function getFieldValue(field) {
if (!testMap) return "-";
if (field.keys) {
return field.keys
.map(k => field.parentKey ? testMap[field.parentKey]?.[k] : testMap[k])
.filter(val => val && val.trim() !== "")
.join(" / ");
}
return field.parentKey ? testMap[field.parentKey]?.[field.key] : testMap[field.key];
}
</script>
{#snippet DetailsTable({ value, label })}
<div class="space-y-1.5">
<dt class="text-xs font-medium text-muted-foreground uppercase tracking-wider">
{label}
</dt>
<dd>
{#if value && Array.isArray(value) && value.length > 0}
<table class="w-full text-sm border border-border rounded-md overflow-hidden">
<thead class="bg-muted text-muted-foreground">
<tr>
<th class="px-3 py-2 text-left font-medium text-xs uppercase tracking-wider">Host Test Code</th>
<th class="px-3 py-2 text-left font-medium text-xs uppercase tracking-wider">Host Test Name</th>
<th class="px-3 py-2 text-left font-medium text-xs uppercase tracking-wider">Client Test Code</th>
<th class="px-3 py-2 text-left font-medium text-xs uppercase tracking-wider">Client Test Name</th>
</tr>
</thead>
<tbody>
{#each value as row, i}
<tr class={i % 2 === 0 ? 'bg-background' : 'bg-muted/30'}>
<td class="px-3 py-2">{row.HostTestCode ?? '-'}</td>
<td class="px-3 py-2">{row.HostTestName ?? '-'}</td>
<td class="px-3 py-2">{row.ClientTestCode ?? '-'}</td>
<td class="px-3 py-2">{row.ClientTestName ?? '-'}</td>
</tr>
{/each}
</tbody>
</table>
{:else}
<span class="text-sm font-medium">-</span>
{/if}
</dd>
</div>
{/snippet}
{#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?.HostType}
{actions}
/>
<div class="flex-1 min-h-0 overflow-y-auto space-y-4">
{#each detailSections as section}
<div class="p-4">
<div class={section.class}>
{#each section.fields as field}
{#if field.fullWidth}
<div class="col-span-2">
{#if field.key === "details"}
{@render DetailsTable({ label: field.label, value: getFieldValue(field) })}
{:else}
{@render Fieldset({ label: field.label, value: getFieldValue(field), isUTCDate: field.isUTCDate })}
{/if}
</div>
{:else if field.key === "details"}
{@render DetailsTable({
label: field.label,
value: getFieldValue(field),
})}
{:else}
{@render Fieldset({
label: field.label,
value: getFieldValue(field),
isUTCDate: field.isUTCDate
})}
{/if}
{/each}
</div>
</div>
{/each}
</div>
</div>
{:else}
<ReusableEmpty icon={MapIcon} desc="Select a test map to see details"/>
{/if}

View File

@ -5,6 +5,7 @@
import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte";
import MapIcon from "@lucide/svelte/icons/map";
import * as Table from "$lib/components/ui/table/index.js";
import { Spinner } from "$lib/components/ui/spinner/index.js";
let props = $props();
@ -85,7 +86,11 @@
</div>
{/snippet}
{#if masterDetail.selectedItem}
{#if masterDetail.isLoadingDetail}
<div class="h-full w-full flex items-center justify-center">
<Spinner class="size-6" />
</div>
{:else if masterDetail.selectedItem}
<div class="flex flex-col px-2 py-1 gap-2 h-full w-full">
<TopbarWrapper
title={masterDetail.selectedItem?.data?.HostType}

View File

@ -2,6 +2,7 @@ import PlusIcon from "@lucide/svelte/icons/plus";
import Settings2Icon from "@lucide/svelte/icons/settings-2";
import PencilIcon from "@lucide/svelte/icons/pencil";
import { API } from "$lib/config/api";
import RefreshIcon from "@lucide/svelte/icons/refresh-cw";
export const searchFields = [
{
@ -30,8 +31,13 @@ export const detailSections = [
},
];
export function workstationActions(masterDetail) {
export function workstationActions(masterDetail, handlers) {
return [
{
Icon: RefreshIcon,
label: 'Refresh Data',
onClick: handlers.refresh,
},
{
Icon: PlusIcon,
label: 'Add Workstation',

View File

@ -26,8 +26,14 @@
async function handleSave() {
const result = await formState.save(masterDetail.mode);
toast('Workstation Created!');
masterDetail?.exitForm(true);
if (result.status === 'success') {
toast('Workstation Created!');
masterDetail?.exitForm(true);
} else {
console.error('Failed to save workstation');
const errorMessages = result.messages ? Object.values(result.messages).join('\n') : 'Failed to save workstation';
toast.error(errorMessages)
}
}
const primaryAction = $derived({

View File

@ -8,20 +8,23 @@
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";
import MoveLeftIcon from "@lucide/svelte/icons/move-left";
let props = $props();
const search = useSearch(searchFields, getWorkstations);
const initialForm = props.masterDetail.formState.form;
const actions = workstationActions(props.masterDetail, initialForm)
const handlers = {
refresh: () => {search.handleSearch()},
};
const actions = workstationActions(props.masterDetail, handlers)
actions.find(a => a.label === 'Search Parameters').popoverContent = searchParamSnippet;
let activeRowId = $state(null);
</script>
{#snippet searchParamSnippet()}
{#snippet searchParamSnippet(close)}
<ReusableSearchParam {searchFields}
bind:searchQuery={search.searchQuery} onSearch={search.handleSearch} onReset={search.handleReset} isLoading={search.isLoading}
bind:searchQuery={search.searchQuery} onSearch={() => search.handleSearch(close)} onReset={search.handleReset} isLoading={search.isLoading}
selectOptions={search.selectOptions} loadingOptions={search.loadingOptions} fetchOptions={search.fetchOptions}
/>
{/snippet}
@ -38,10 +41,13 @@
>
<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 class="flex flex-col items-center justify-start gap-4 tracking-widest font-semibold select-none h-full">
<MoveLeftIcon />
<div class="flex flex-col items-center justify-center flex-grow gap-4">
{#each "WORKSTATION".split("") as c}
<span class="leading-none">{c}</span>
{/each}
</div>
</span>
{/if}

View File

@ -4,6 +4,7 @@
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";
import { Spinner } from "$lib/components/ui/spinner/index.js";
let props = $props();
@ -46,7 +47,11 @@
</div>
{/snippet}
{#if masterDetail.selectedItem}
{#if masterDetail.isLoadingDetail}
<div class="h-full w-full flex items-center justify-center">
<Spinner class="size-6" />
</div>
{:else if masterDetail.selectedItem}
<div class="flex flex-col px-2 py-1 gap-2 h-full w-full">
<TopbarWrapper
title={masterDetail.selectedItem.data.WorkstationName}

View File

@ -21,8 +21,8 @@
let activeRowId = $state(null);
</script>
{#snippet searchParamSnippet()}
<ReusableSearchParam {searchFields} bind:searchQuery={search.searchQuery} onSearch={search.handleSearch} onReset={search.handleReset} isLoading={search.isLoading}/>
{#snippet searchParamSnippet(close)}
<ReusableSearchParam {searchFields} bind:searchQuery={search.searchQuery} onSearch={() => search.handleSearch(close)} onReset={search.handleReset} isLoading={search.isLoading}/>
{/snippet}
<div

View File

@ -4,6 +4,7 @@
import { detailSections, viewActions } from "$lib/components/patient/list/config/patient-config";
import TopbarWrapper from "$lib/components/topbar/topbar-wrapper.svelte";
import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte";
import { Spinner } from "$lib/components/ui/spinner/index.js";
let props = $props();
@ -95,7 +96,11 @@
</div>
{/snippet}
{#if masterDetail.selectedItem}
{#if masterDetail.isLoadingDetail}
<div class="h-full w-full flex items-center justify-center">
<Spinner class="size-6" />
</div>
{:else if masterDetail.selectedItem}
<div class="flex flex-col px-2 py-1 gap-2 h-full w-full">
<TopbarWrapper title={fullName} subtitle={prefix} {actions} />
<div class="flex-1 min-h-0 overflow-y-auto space-y-4">

View File

@ -30,7 +30,6 @@
{/snippet}
</Popover.Trigger>
<Popover.Content collisionPadding={props.collisionPadding ?? 0} class={props.popoverWidth ?? "w-72"}>
<!-- {@render props.popoverContent()} -->
{@render props.popoverContent(() => open = false)}
</Popover.Content>
</Popover.Root>