fix for location and patient list

location :
tambah rule loccode max 6
fix handlesave if result.status success

patient list :
fix emailaddress1&2 kosong masih trigger rule
add rule for phone
change deceased to type toggle isDead
fix rule timeOfDeath
fix derived isDeathDateDisabled
This commit is contained in:
faiztyanirh 2026-04-13 13:42:46 +07:00
parent 3ebea51ee7
commit ffb57539e8
9 changed files with 68 additions and 36 deletions

View File

@ -28,7 +28,7 @@ export function usePatientForm(formState, patientSchema) {
), ),
EmailAddress1: patientSchema.shape.EmailAddress1.refine( EmailAddress1: patientSchema.shape.EmailAddress1.refine(
async (value) => { async (value) => {
if (!value) return false; if (!value) return true;
const res = await fetch(`${API.BASE_URL}${API.CHECK}?EmailAddress1=${value}`); const res = await fetch(`${API.BASE_URL}${API.CHECK}?EmailAddress1=${value}`);
const { status, data } = await res.json(); const { status, data } = await res.json();
return status === "success" && data === false ? false : true; return status === "success" && data === false ? false : true;
@ -37,13 +37,22 @@ export function usePatientForm(formState, patientSchema) {
), ),
EmailAddress2: patientSchema.shape.EmailAddress2.refine( EmailAddress2: patientSchema.shape.EmailAddress2.refine(
async (value) => { async (value) => {
if (!value) return false; if (!value) return true;
const res = await fetch(`${API.BASE_URL}${API.CHECK}?EmailAddress1=${value}`); const res = await fetch(`${API.BASE_URL}${API.CHECK}?EmailAddress1=${value}`);
const { status, data } = await res.json(); const { status, data } = await res.json();
return status === "success" && data === false ? false : true; return status === "success" && data === false ? false : true;
}, },
{ message: "Email address already used" } { message: "Email address already used" }
), ),
Phone: patientSchema.shape.Phone.refine(
async (value) => {
if (!value) return true;
const res = await fetch(`${API.BASE_URL}${API.CHECK}?Phone=${value}`);
const { status, data } = await res.json();
return status === "success" && data === false ? false : true;
},
{ message: "Phone number already used" }
),
}); });
const partial = asyncSchema.pick({ [field]: true }); const partial = asyncSchema.pick({ [field]: true });

View File

@ -113,10 +113,10 @@
tempDetailContact, tempDetailContact,
}); });
console.log(payload) console.log(payload)
// const result = await formState.save(masterDetail.mode); const result = await formState.save(masterDetail.mode);
// toast('Contact Created!'); toast('Contact Created!');
// masterDetail?.exitForm(true); masterDetail?.exitForm(true);
} }
const primaryAction = $derived({ const primaryAction = $derived({

View File

@ -3,7 +3,7 @@ import EraserIcon from "@lucide/svelte/icons/eraser";
import { z } from "zod"; import { z } from "zod";
export const locationSchema = z.object({ export const locationSchema = z.object({
LocCode: z.string().min(1, "Required"), LocCode: z.string().min(1, "Required").max(6, "Max 6 chars"),
LocFull: z.string().min(1, "Required"), LocFull: z.string().min(1, "Required"),
Email: z.string().trim().optional().refine((val) => !val || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val),"Invalid email format"), Email: z.string().trim().optional().refine((val) => !val || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val),"Invalid email format"),
Phone: z.string().max(14, "Max 14 chars").regex(/^$|^[0-9]+$/, "Can only contain numbers"), Phone: z.string().max(14, "Max 14 chars").regex(/^$|^[0-9]+$/, "Can only contain numbers"),

View File

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

View File

@ -63,7 +63,6 @@
} }
function handleInsertDetail() { function handleInsertDetail() {
console.log('object');
const row = { const row = {
id: ++idCounter, id: ++idCounter,
...snapshotForm() ...snapshotForm()

View File

@ -79,7 +79,7 @@ export const detailSections = [
}, },
{ key: "isDeadLabel", label: "Deceased" }, { key: "isDeadLabel", label: "Deceased" },
{ key: "CreateDate", label: "Create Date", isUTCDate: true }, { key: "CreateDate", label: "Create Date", isUTCDate: true },
{ key: "DelDate", label: "Disabled Date" }, { key: "DelDate", label: "Delete Date" },
{ key: "TimeOfDeath", label: "Time of Death", isUTCDate: true }, { key: "TimeOfDeath", label: "Time of Death", isUTCDate: true },
] ]
}, },

View File

@ -12,12 +12,13 @@ export const patientSchema = z.object({
), ),
EmailAddress1: z.string().trim().optional().refine((val) => !val || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val),"Invalid email format"), EmailAddress1: z.string().trim().optional().refine((val) => !val || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val),"Invalid email format"),
EmailAddress2: z.string().trim().optional().refine((val) => !val || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val),"Invalid email format"), EmailAddress2: z.string().trim().optional().refine((val) => !val || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val),"Invalid email format"),
Phone: z.string().max(14, "Max 14 chars").regex(/^$|^[0-9]+$/, "Can only contain numbers"), Phone: z.string().max(14, "Max 14 chars").regex(/^$|^[0-9]+$/, "Can only contain numbers").optional(),
MobilePhone: z.string().max(14, "Max 14 chars").regex(/^$|^[0-9]+$/, "Can only contain numbers"), MobilePhone: z.string().max(14, "Max 14 chars").regex(/^$|^[0-9]+$/, "Can only contain numbers"),
TimeOfDeath: z.string().optional().refine( TimeOfDeath: z.string().optional().refine(
(date) => new Date(date) <= new Date(), (date) => new Date(date) <= new Date(),
"Cannot exceed today's date" "Cannot exceed today's date"
), ),
ZIP: z.string().max(7, "Max 7 chars").regex(/^$|^[0-9]+$/, "Can only contain numbers"),
}); });
export const patientInitialForm = { export const patientInitialForm = {
@ -55,7 +56,7 @@ export const patientInitialForm = {
Phone: "", Phone: "",
EmailAddress2: "", EmailAddress2: "",
MobilePhone: "", MobilePhone: "",
DeathIndicator: "", isDead: "",
TimeOfDeath: "", TimeOfDeath: "",
LinkTo: [], LinkTo: [],
PatCom: "", PatCom: "",
@ -73,6 +74,7 @@ export const patientDefaultErrors = {
Phone: null, Phone: null,
MobilePhone: null, MobilePhone: null,
TimeOfDeath: null, TimeOfDeath: null,
ZIP: null,
}; };
export const patientFormFields = [ export const patientFormFields = [
@ -236,7 +238,7 @@ export const patientFormFields = [
{ {
type: "group", type: "group",
columns: [ columns: [
{ key: "ZIP", label: "ZIP", required: false, type: "number" }, { key: "ZIP", label: "ZIP", required: false, type: "text", validateOn: ["input"] },
{ {
key: "Country", key: "Country",
label: "Country", label: "Country",
@ -257,7 +259,7 @@ export const patientFormFields = [
type: "row", type: "row",
columns: [ columns: [
{ key: "EmailAddress1", label: "Email Address 1", required: false, type: "email", validateOn: ["input", "blur"] }, { key: "EmailAddress1", label: "Email Address 1", required: false, type: "email", validateOn: ["input", "blur"] },
{ key: "Phone", label: "Phone", required: false, type: "text", validateOn: ["input"] }, { key: "Phone", label: "Phone", required: false, type: "text", validateOn: ["input", "blur"] },
] ]
}, },
{ {
@ -273,13 +275,23 @@ export const patientFormFields = [
{ {
type: "group", type: "group",
columns: [ columns: [
// {
// key: "isDead",
// label: "Deceased",
// required: false,
// type: "select",
// optionsEndpoint: `${API.BASE_URL}${API.VALUESET}/death_indicator`,
// defaultValue: 'N'
// },
{ {
key: "isDead", key: "isDead",
label: "Deceased", label: "Deceased",
required: false, required: false,
type: "select", type: "toggle",
optionsEndpoint: `${API.BASE_URL}${API.VALUESET}/death_indicator`, optionsToggle: [
defaultValue: 'N' { value: 0, label: 'No' },
{ value: 1, label: 'Yes' }
]
}, },
{ key: "TimeOfDeath", label: "Time of Death", required: false, type: "datetime", validateOn: ["input"] } { key: "TimeOfDeath", label: "Time of Death", required: false, type: "datetime", validateOn: ["input"] }
] ]

View File

@ -68,7 +68,7 @@
disabled: formState.isSaving.current disabled: formState.isSaving.current
} }
]; ];
// $inspect(formState.selectOptions) $inspect(formState.errors)
</script> </script>
<FormPageContainer title="Create Patient" {primaryAction} {secondaryActions} {actions}> <FormPageContainer title="Create Patient" {primaryAction} {secondaryActions} {actions}>

View File

@ -68,17 +68,18 @@
} }
let isDeathDateDisabled = $derived( let isDeathDateDisabled = $derived(
formState.form.isDead !== 'Y' formState.form.isDead !== 1 && formState.form.isDead !== "1"
); );
$effect(() => { $effect(() => {
if (isDeathDateDisabled && formState.form.TimeOfDeath) { if (isDeathDateDisabled && formState.form.TimeOfDeath) {
formState.form.TimeOfDeath = ""; formState.form.TimeOfDeath = "";
formState.errors.TimeOfDeath = null;
} }
}); });
</script> </script>
{#snippet Fieldset({ key, label, required, type, optionsEndpoint, options, validateOn, dependsOn, endpointParamKey, valueKey, labelKey })} {#snippet Fieldset({ key, label, required, type, optionsEndpoint, options, optionsToggle, validateOn, dependsOn, endpointParamKey, valueKey, labelKey })}
<div class="flex w-full flex-col gap-1.5"> <div class="flex w-full flex-col gap-1.5">
<div class="flex justify-between items-center w-full"> <div class="flex justify-between items-center w-full">
<Label>{label}</Label> <Label>{label}</Label>
@ -283,22 +284,27 @@
{/if} {/if}
</div> </div>
{:else if type === "toggle"} {:else if type === "toggle"}
<div class="flex items-center w-full"> {@const toggleOff = optionsToggle?.[0] ?? { value: false, label: 'Off' }}
<Toggle {@const toggleOn = optionsToggle?.[1] ?? { value: true, label: 'On' }}
aria-label="Toggle discharge" {@const isOn = String(formState.form[key]) === String(toggleOn.value)}
variant="outline" <div class="flex items-center w-full">
class="w-full transition-all data-[state=on]:text-primary" <Toggle
bind:pressed={formState.form.isDischarge} aria-label="Toggle"
> variant="outline"
class="w-full transition-all data-[state=on]:text-primary"
{#if formState.form.isDischarge} pressed={isOn}
<XIcon class="mr-2 h-4 w-4" /> onPressedChange={(pressed) => {
{:else} formState.form[key] = pressed ? toggleOn.value : toggleOff.value;
<CheckIcon class="mr-2 h-4 w-4" /> }}
{/if} >
{formState.form.isDischarge ? "Discharged" : "Active"} {#if isOn}
</Toggle> <CheckIcon class="mr-2 h-4 w-4" />
</div> {:else}
<XIcon class="mr-2 h-4 w-4" />
{/if}
{isOn ? toggleOn.label : toggleOff.label}
</Toggle>
</div>
{:else} {:else}
<Input <Input
type="text" type="text"