menambahkan alert dialog dan bug fix isDirty form variabel

This commit is contained in:
faiztyanirh 2026-02-12 21:19:48 +07:00
parent 4784ede1a6
commit fccbb5a87f
20 changed files with 321 additions and 10 deletions

View File

@ -1,7 +1,7 @@
export function useFormState(initial) {
const form = $state(structuredClone(initial))
const isSaving = $state({ current: false });
console.log(form);
// function resetForm() {
// Object.assign(form, structuredClone(initial));
// }

View File

@ -13,6 +13,8 @@ export function useForm({schema, initialForm, defaultErrors, mode, modeOpt, save
try {
// const payload = { ...state.form };
const payload = customPayload || { ...state.form };
// const { ProvinceID, CityID, ...rest } = state.form;
// const payload = customPayload || rest;
const result = currentMode === 'edit' ? await editEndpoint(payload) : await saveEndpoint(payload);
return result;
} catch (error) {

View File

@ -1,5 +1,6 @@
import { useResponsive } from "./use-responsive.svelte.js";
import { useForm } from "./use-form.svelte.js";
import { tick } from "svelte";
export function useMasterDetail(options = {}) {
const { onSelect = null, formConfig = null, } = options;
@ -8,6 +9,7 @@ export function useMasterDetail(options = {}) {
let mode = $state("view");
let isLoadingDetail = $state(false);
let formSnapshot = $state(null);
let showExitConfirm = $state(false);
const formState = useForm(formConfig);
@ -28,6 +30,8 @@ export function useMasterDetail(options = {}) {
JSON.stringify(formState.form) !== JSON.stringify(formSnapshot)
);
$inspect(formState.form)
async function select(item) {
mode = "view";
@ -47,7 +51,7 @@ export function useMasterDetail(options = {}) {
}
}
function enterCreate(initialData = null) {
async function enterCreate(initialData = null) {
mode = "create";
selectedItem = null;
@ -56,6 +60,8 @@ export function useMasterDetail(options = {}) {
if (initialData) {
formState.setForm(initialData);
}
await tick();
formSnapshot = $state.snapshot(formState.form);
}
function enterEdit(param) {
@ -84,15 +90,21 @@ export function useMasterDetail(options = {}) {
formSnapshot = $state.snapshot(formState.form);
}
function exitForm() {
if (isDirty) {
const ok = confirm('You have unsaved changes. Discard them?');
if (!ok) return;
function exitForm(force = false) {
if (!force && isDirty) {
showExitConfirm = true;
return;
}
// Direct exit
mode = "view";
selectedItem = null;
formSnapshot = null;
}
function confirmExit() {
mode = "view";
selectedItem = null;
formSnapshot = null;
}
@ -142,11 +154,14 @@ export function useMasterDetail(options = {}) {
get formState() {
return formState;
},
get showExitConfirm() { return showExitConfirm; },
set showExitConfirm(value) { showExitConfirm = value; },
select,
enterCreate,
enterEdit,
exitForm,
confirmExit,
backToList,
saveForm,
};

View File

@ -44,8 +44,10 @@ export const patientInitialForm = {
Citizenship: "",
Street_1: "",
City: "",
CityID: "",
Street_2: "",
Province: "",
ProvinceID: "",
Street_3: "",
ZIP: "",
Country: "",

View File

@ -3,6 +3,7 @@
import FormPageContainer from "$lib/components/patient/reusable/form-page-container.svelte";
import PatientFormRenderer from "$lib/components/patient/reusable/patient-form-renderer.svelte";
import { toast } from "svelte-sonner";
import ReusableAlertDialog from "$lib/components/reusable/reusable-alert-dialog.svelte";
let props = $props();
@ -20,12 +21,25 @@
const actions = formActions(handlers);
let showConfirm = $state(false);
function handleExit() {
const ok = masterDetail.exitForm();
if (!ok) {
showConfirm = true;
}
}
function confirmDiscard() {
masterDetail.exitForm(true);
}
async function handleSave() {
const result = await formState.save(masterDetail.mode);
if (result.status === 'success') {
toast('Patient Created!');
masterDetail?.exitForm();
masterDetail?.exitForm(true);
} else {
console.error('Failed to save patient');
}
@ -62,4 +76,9 @@
validateFieldAsync={helpers.validateFieldAsync}
mode="create"
/>
</FormPageContainer>
</FormPageContainer>
<ReusableAlertDialog
bind:open={masterDetail.showExitConfirm}
onConfirm={masterDetail.confirmExit}
/>

View File

@ -5,6 +5,7 @@
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();
@ -15,6 +16,19 @@
const helpers = usePatientForm(formState, schema);
let showConfirm = $state(false);
function handleExit() {
const ok = masterDetail.exitForm();
if (!ok) {
showConfirm = true;
}
}
function confirmDiscard() {
masterDetail.exitForm(true);
}
$effect(() => {
// const backendData = masterDetail?.selectedItem?.patient;
// if (!backendData) return;
@ -118,3 +132,8 @@
mode="edit"
/>
</FormPageContainer>
<ReusableAlertDialog
bind:open={masterDetail.showExitConfirm}
onConfirm={masterDetail.confirmExit}
/>

View File

@ -0,0 +1,44 @@
<script>
import * as AlertDialog from "$lib/components/ui/alert-dialog/index.js";
let {
open = $bindable(false),
title = "Are you sure?",
description = "You have unsaved changes. Discard them?",
cancelText = "Cancel",
confirmText = "Discard",
onConfirm = () => {},
onCancel = () => {},
} = $props();
function handleConfirm() {
onConfirm();
open = false;
}
function handleCancel() {
onCancel();
open = false;
}
</script>
<AlertDialog.Root bind:open>
<AlertDialog.Content>
<AlertDialog.Header>
<AlertDialog.Title>{title}</AlertDialog.Title>
<AlertDialog.Description>
{description}
</AlertDialog.Description>
</AlertDialog.Header>
<AlertDialog.Footer>
<AlertDialog.Cancel onclick={handleCancel}>
{cancelText}
</AlertDialog.Cancel>
<AlertDialog.Action
onclick={handleConfirm}
>
{confirmText}
</AlertDialog.Action>
</AlertDialog.Footer>
</AlertDialog.Content>
</AlertDialog.Root>

View File

@ -0,0 +1,18 @@
<script>
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import { buttonVariants } from "$lib/components/ui/button/index.js";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
} = $props();
</script>
<AlertDialogPrimitive.Action
bind:ref
data-slot="alert-dialog-action"
class={cn(buttonVariants(), className)}
{...restProps}
/>

View File

@ -0,0 +1,18 @@
<script>
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import { buttonVariants } from "$lib/components/ui/button/index.js";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
} = $props();
</script>
<AlertDialogPrimitive.Cancel
bind:ref
data-slot="alert-dialog-cancel"
class={cn(buttonVariants({ variant: "outline" }), className)}
{...restProps}
/>

View File

@ -0,0 +1,25 @@
<script>
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import AlertDialogPortal from "./alert-dialog-portal.svelte";
import AlertDialogOverlay from "./alert-dialog-overlay.svelte";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
portalProps,
...restProps
} = $props();
</script>
<AlertDialogPortal {...portalProps}>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
bind:ref
data-slot="alert-dialog-content"
class={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...restProps}
/>
</AlertDialogPortal>

View File

@ -0,0 +1,17 @@
<script>
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
} = $props();
</script>
<AlertDialogPrimitive.Description
bind:ref
data-slot="alert-dialog-description"
class={cn("text-muted-foreground text-sm", className)}
{...restProps}
/>

View File

@ -0,0 +1,18 @@
<script>
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
} = $props();
</script>
<div
bind:this={ref}
data-slot="alert-dialog-footer"
class={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@ -0,0 +1,19 @@
<script>
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
} = $props();
</script>
<div
bind:this={ref}
data-slot="alert-dialog-header"
class={cn("flex flex-col gap-2 text-center sm:text-start", className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@ -0,0 +1,20 @@
<script>
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
} = $props();
</script>
<AlertDialogPrimitive.Overlay
bind:ref
data-slot="alert-dialog-overlay"
class={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...restProps}
/>

View File

@ -0,0 +1,7 @@
<script>
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
let { ...restProps } = $props();
</script>
<AlertDialogPrimitive.Portal {...restProps} />

View File

@ -0,0 +1,17 @@
<script>
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
} = $props();
</script>
<AlertDialogPrimitive.Title
bind:ref
data-slot="alert-dialog-title"
class={cn("text-lg font-semibold", className)}
{...restProps}
/>

View File

@ -0,0 +1,7 @@
<script>
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps } = $props();
</script>
<AlertDialogPrimitive.Trigger bind:ref data-slot="alert-dialog-trigger" {...restProps} />

View File

@ -0,0 +1,7 @@
<script>
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
let { open = $bindable(false), ...restProps } = $props();
</script>
<AlertDialogPrimitive.Root bind:open {...restProps} />

View File

@ -0,0 +1,37 @@
import Root from "./alert-dialog.svelte";
import Portal from "./alert-dialog-portal.svelte";
import Trigger from "./alert-dialog-trigger.svelte";
import Title from "./alert-dialog-title.svelte";
import Action from "./alert-dialog-action.svelte";
import Cancel from "./alert-dialog-cancel.svelte";
import Footer from "./alert-dialog-footer.svelte";
import Header from "./alert-dialog-header.svelte";
import Overlay from "./alert-dialog-overlay.svelte";
import Content from "./alert-dialog-content.svelte";
import Description from "./alert-dialog-description.svelte";
export {
Root,
Title,
Action,
Cancel,
Portal,
Footer,
Header,
Trigger,
Overlay,
Content,
Description,
//
Root as AlertDialog,
Title as AlertDialogTitle,
Action as AlertDialogAction,
Cancel as AlertDialogCancel,
Portal as AlertDialogPortal,
Footer as AlertDialogFooter,
Header as AlertDialogHeader,
Trigger as AlertDialogTrigger,
Overlay as AlertDialogOverlay,
Content as AlertDialogContent,
Description as AlertDialogDescription,
};

View File

@ -28,8 +28,8 @@
},
LinkTo: Array.isArray(data.LinkTo) ? data.LinkTo : [],
Custodian: data.Custodian ?? {
InternalPID: "",
PatientID: ""
InternalPID: "",
PatientID: ""
},
})
}