Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 43 additions & 34 deletions src/components/inputs/RHFCustomSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,26 +25,29 @@ export function RHFCustomSelect({
isRequired = false,
unselectedValue = NAOption,
className = '',
optionClassName = ''
optionClassName = '',
}: RHFCustomSelectProps) {
const {
control,
formState: { errors, isSubmitted },
watch,
setValue
setValue,
} = useFormContext();

const hasError = Boolean(errors[name] && isSubmitted);
const errorMessage = (errors[name]?.message as string) || '';
const watchedValue = watch(name)
const watchedValue = watch(name);

// fix issue where after submitting the registration form, the form values are reset;
// but because the default values are set at field level (instead of during the useForm call),
// calling reset with keepDefaultValues does not actually preserve the default values
// TODO: possibly define all the form fields at the useForm level instead.
useEffect(() => {
if (defaultValue && !watchedValue) setValue(name, defaultValue)
},[defaultValue, watchedValue, setValue, name])
// Only set default when field is truly uninitialized (undefined), not just empty ("")
if (defaultValue !== undefined && watchedValue === undefined) {
setValue(name, defaultValue, { shouldDirty: false });
}
}, [defaultValue, watchedValue, setValue, name]);

return (
<div className={className}>
Expand All @@ -62,27 +65,29 @@ export function RHFCustomSelect({
defaultValue={defaultValue}
render={({ field }) => {
return (
<div className='flex flex-row flex-wrap gap-2'>
<div className="flex flex-row flex-wrap gap-2">
{options.map(v => {
const currValOverride = watchedValue ?? defaultValue
const currValOverride = watchedValue ?? defaultValue;
function handleChange(v: string | number) {
if (currValOverride === v && !isRequired) field.onChange(unselectedValue);
if (currValOverride === v && !isRequired)
field.onChange(unselectedValue);
else field.onChange(v);
}
const value = typeof v === "object" ? v.value : v
const label = typeof v === "object" ? v.label : v
return <Option
key={value}
value={value}
label={label}
className={optionClassName}
selected={currValOverride === value}
onChange={handleChange}
/>
}
)}
const value = typeof v === 'object' ? v.value : v;
const label = typeof v === 'object' ? v.label : v;
return (
<Option
key={value}
value={value}
label={label}
className={optionClassName}
selected={currValOverride === value}
onChange={handleChange}
/>
);
})}
</div>
)
);
}}
/>

Expand All @@ -96,22 +101,26 @@ function Option({
label,
className,
selected,
onChange
onChange,
}: {
value: string | number;
label: string;
className: string;
selected: boolean;
onChange: (v: string | number) => void
onChange: (v: string | number) => void;
}) {
const borderColour = selected ? "text- border-2 border-black" : "border-2 border-gray-300";
return <button
className={`flex flex-col items-center justify-center px-2 py-1 bg-gray-100 rounded-sm ${borderColour} ${className}`}
onClick={(e) => {
e.preventDefault();
onChange(value)
}}
>
<div>{label}</div>
</button>
}
const borderColour = selected
? 'text- border-2 border-black'
: 'border-2 border-gray-300';
return (
<button
className={`flex flex-col items-center justify-center rounded-sm bg-gray-100 px-2 py-1 ${borderColour} ${className}`}
onClick={e => {
e.preventDefault();
onChange(value);
}}
>
<div>{label}</div>
</button>
);
}
34 changes: 28 additions & 6 deletions src/components/records/consultation/ConsultationForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { deleteDiagnosis } from '@/data/diagnosis/deleteDiagnosis';
import { getDiagnosisByConsult } from '@/data/diagnosis/getDiagnosis';
import { patchDiagnosis } from '@/data/diagnosis/patchDiagnosis';
import { createReferral } from '@/data/referrals/createReferral';
import useSafeguardUnsavedChanges from '@/hooks/safeguardUnsavedChanges';
import { useSaveOnWrite } from '@/hooks/useSaveOnWrite';
import { ConsultMedicationOrder } from '@/types/ConsultMedicationOrder';
import { Patient } from '@/types/Patient';
Expand Down Expand Up @@ -41,10 +42,17 @@ export function ConsultationForm({
{} as FieldValues,
[visitId, editConsultId]
);
const useFormReturn = useForm({ values: formDetails });
const useFormReturn = useForm();
const loadedConsultIdRef = useRef<number | null>(null);
const isLoadingRef = useRef(false);

useEffect(() => {
if (!editConsultId && formDetails && Object.keys(formDetails).length > 0) {
useFormReturn.reset(formDetails);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

useEffect(() => {
if (editConsultId == null) {
loadedConsultIdRef.current = null;
Expand Down Expand Up @@ -127,6 +135,16 @@ export function ConsultationForm({
const referredFor = useFormReturn.watch('referred_for');
const isEditing = editConsultId != null;

useSafeguardUnsavedChanges(
useFormReturn.formState.isDirty,
'You have unsaved changes to the consultation form. Are you sure you want to leave?',
() => {
// When user confirms they want to leave, clear the form state
reset({});
clearLocalStorageData();
}
);

// Helper function to process and validate diagnoses
const processDiagnoses = (
diagnoses: Array<{
Expand Down Expand Up @@ -291,14 +309,18 @@ export function ConsultationForm({
? 'Medical Consult Updated!'
: 'Medical Consult Completed!'
);
reset({});
clearLocalStorageData();
if (isEditing && onEditComplete) {
onEditComplete();
}
} catch (error) {
console.error('Error submitting consultation form:', error);
toast.error('Unknown Error');
return; // Don't clear form if there was an error
}

// Clear form after successful submission

reset({});
clearLocalStorageData();
if (isEditing && onEditComplete) {
onEditComplete();
}

// only submit the form if 'referred_for' is filled in and is not 'Not Referred'
Expand Down
44 changes: 34 additions & 10 deletions src/components/records/vital/VitalsForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { RHFUnitInputField } from '@/components/inputs/RHFUnitInputField';
import { ChildVitalsFields } from '@/components/records/vital/ChildVitalsFields';
import { NAOption } from '@/constants';
import { patchVital } from '@/data/vital/patchVital';
import useSafeguardUnsavedChanges from '@/hooks/safeguardUnsavedChanges';
import { useSaveOnWrite } from '@/hooks/useSaveOnWrite';
import { Patient } from '@/types/Patient';
import { displayBMI, validateVisualAcuity, Vital } from '@/types/Vital';
import { FormEvent, useEffect } from 'react';
Expand All @@ -24,20 +26,34 @@ export function VitalsForm({
visitId: string;
curVital: Vital; // can be EMPTY_VITAL
}) {
const [, , clearLocalStorageData] = useSaveOnWrite(
'VitalsForm',
{} as FieldValues,
[visitId, curVital]
);
const useFormReturn = useForm({
mode: 'onBlur',
reValidateMode: 'onBlur',
});
const { handleSubmit, reset, watch } = useFormReturn;
const {
handleSubmit,
reset,
watch,
formState: { isDirty },
} = useFormReturn;
const [formHeight, formWeight] = watch(['height', 'weight']);

// when curVital updates (i.e. after submitting the Vitals Form), reset the form values
// so that the text field values are cleared, showing the placeholders which are set from
// the new values in curVitals. The RHFCustomSelect and Dropdown components will have
// their values set to these new values via the defaultValue prop.
useSafeguardUnsavedChanges(
isDirty,
'You have unsaved changes to the vitals form. Are you sure you want to leave?',
() => {
reset({});
clearLocalStorageData();
}
);
useEffect(() => {
reset({})
}, [curVital, reset])
reset({});
}, [curVital, reset]);

// if the user hasn’t provided a new height/weight, fall back
// to the current vital values (curVital.height, curVital.weight).
Expand All @@ -55,10 +71,18 @@ export function VitalsForm({

handleSubmit(
async (data: FieldValues) => {
data.visit_id = visitId;
patchVital(data as Vital).then(() => {
try {
data.visit_id = visitId;
await patchVital(data as Vital);
toast.success('Updated Vital');
});
} catch (error) {
console.error('Error updating vital:', error);
toast.error('Unknown Error');
return;
}

// Clear form after successful submission
reset({});
},
() => {
toast.error('Invalid/Missing Input');
Expand Down
13 changes: 12 additions & 1 deletion src/components/referrals/ReferralStateDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useLoadingState } from '@/hooks/useLoadingState';
import { Referral } from '@/types/Referral';
import { useState } from 'react';
import toast from 'react-hot-toast';

import {
Select,
SelectContent,
Expand All @@ -12,7 +13,13 @@ import {
SelectValue,
} from '../ui/select';

export function ReferralStateDropdown({ referral }: { referral: Referral }) {
export function ReferralStateDropdown({
referral,
onUpdate,
}: {
referral: Referral;
onUpdate?: (referralId: number, updatedReferral: Referral) => void;
}) {
const referralState = [
'None',
'New',
Expand All @@ -37,7 +44,11 @@ export function ReferralStateDropdown({ referral }: { referral: Referral }) {
});
patch()
.then(() => {
if (onUpdate) {
onUpdate(referral.id, { ...referral, referral_state: value });
}
setReferralStatus(value);

toast.success('Updated successfully!');
})
.catch(() => toast.error('Failed to update'));
Expand Down
49 changes: 49 additions & 0 deletions src/hooks/safeguardUnsavedChanges.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { usePathname } from 'next/navigation';
import { useEffect, useRef } from 'react';

export default function useSafeguardUnsavedChanges(
isDirty: boolean,
message: string,
onConfirm?: () => void
) {
const pathname = usePathname();

const isDirtyRef = useRef(isDirty);
const onConfirmRef = useRef(onConfirm);

isDirtyRef.current = isDirty;
onConfirmRef.current = onConfirm;

useEffect(() => {
const handleClick = (e: MouseEvent) => {
const target = e.target as HTMLElement;
const link = target.closest('a[href]') as HTMLAnchorElement | null;

if (!link || !link.href) return;
const linkUrl = new URL(link.href);

if (
linkUrl.pathname === pathname &&
linkUrl.search === window.location.search
) {
return;
}

if (isDirtyRef.current) {
if (window.confirm(message)) {
isDirtyRef.current = false;
} else {
e.preventDefault();
e.stopPropagation(); //kills event before it reaches the Next.js Link component
}
}
};

document.addEventListener('click', handleClick, true);

return () => {
document.removeEventListener('click', handleClick, true);
};
//ref handles the life values
}, [message, pathname]);
}