MED-203: fix doctor feedback form
This commit is contained in:
156
app/doctor/_components/analysis-feedback.tsx
Normal file
156
app/doctor/_components/analysis-feedback.tsx
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { giveFeedbackAction } from '@/packages/features/doctor/src/lib/server/actions/doctor-server-actions';
|
||||||
|
import {
|
||||||
|
DoctorFeedback,
|
||||||
|
Order,
|
||||||
|
Patient,
|
||||||
|
} from '@/packages/features/doctor/src/lib/server/schema/doctor-analysis-detail-view.schema';
|
||||||
|
import {
|
||||||
|
DoctorAnalysisFeedbackForm,
|
||||||
|
doctorAnalysisFeedbackFormSchema,
|
||||||
|
} from '@/packages/features/doctor/src/lib/server/schema/doctor-analysis.schema';
|
||||||
|
import ConfirmationModal from '@/packages/shared/src/components/confirmation-modal';
|
||||||
|
import { useUser } from '@/packages/supabase/src/hooks/use-user';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { Trans } from '@kit/ui/makerkit/trans';
|
||||||
|
import { Button } from '@kit/ui/shadcn/button';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormMessage,
|
||||||
|
} from '@kit/ui/shadcn/form';
|
||||||
|
import { toast } from '@kit/ui/shadcn/sonner';
|
||||||
|
import { Textarea } from '@kit/ui/shadcn/textarea';
|
||||||
|
|
||||||
|
const AnalysisFeedback = ({
|
||||||
|
feedback,
|
||||||
|
patient,
|
||||||
|
order,
|
||||||
|
}: {
|
||||||
|
feedback?: DoctorFeedback;
|
||||||
|
patient: Patient;
|
||||||
|
order: Order;
|
||||||
|
}) => {
|
||||||
|
const [isDraftSubmitting, setIsDraftSubmitting] = useState(false);
|
||||||
|
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
|
||||||
|
const { data: user } = useUser();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
resolver: zodResolver(doctorAnalysisFeedbackFormSchema),
|
||||||
|
reValidateMode: 'onChange',
|
||||||
|
defaultValues: {
|
||||||
|
feedbackValue: feedback?.value ?? '',
|
||||||
|
userId: patient.userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const isReadOnly =
|
||||||
|
!!feedback?.doctor_user_id && feedback?.doctor_user_id !== user?.id;
|
||||||
|
|
||||||
|
const handleDraftSubmit = async (e: React.FormEvent) => {
|
||||||
|
setIsDraftSubmitting(true);
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
form.formState.errors.feedbackValue = undefined;
|
||||||
|
const formData = form.getValues();
|
||||||
|
await onSubmit(formData, 'DRAFT');
|
||||||
|
setIsDraftSubmitting(false);
|
||||||
|
};
|
||||||
|
const handleCompleteSubmit = form.handleSubmit(async () => {
|
||||||
|
setIsConfirmOpen(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (
|
||||||
|
data: DoctorAnalysisFeedbackForm,
|
||||||
|
status: 'DRAFT' | 'COMPLETED',
|
||||||
|
) => {
|
||||||
|
const result = await giveFeedbackAction({
|
||||||
|
...data,
|
||||||
|
analysisOrderId: order.analysisOrderId,
|
||||||
|
status,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return toast.error(<Trans i18nKey="common:genericServerError" />);
|
||||||
|
}
|
||||||
|
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
predicate: (query) => query.queryKey.includes('doctor-jobs'),
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success(<Trans i18nKey={'doctor:updateFeedbackSuccess'} />);
|
||||||
|
|
||||||
|
return setIsConfirmOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmComplete = form.handleSubmit(async (data) => {
|
||||||
|
await onSubmit(data, 'COMPLETED');
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3>
|
||||||
|
<Trans i18nKey="doctor:feedback" />
|
||||||
|
</h3>
|
||||||
|
<p>{feedback?.value ?? '-'}</p>
|
||||||
|
{!isReadOnly && (
|
||||||
|
<Form {...form}>
|
||||||
|
<form className="space-y-4 lg:w-1/2">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="feedbackValue"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea {...field} disabled={isReadOnly} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="xs:flex block justify-end gap-2 space-y-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleDraftSubmit}
|
||||||
|
disabled={
|
||||||
|
isReadOnly || isDraftSubmitting || form.formState.isSubmitting
|
||||||
|
}
|
||||||
|
className="xs:w-auto w-full text-xs"
|
||||||
|
>
|
||||||
|
<Trans i18nKey="common:saveAsDraft" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={handleCompleteSubmit}
|
||||||
|
disabled={
|
||||||
|
isReadOnly || isDraftSubmitting || form.formState.isSubmitting
|
||||||
|
}
|
||||||
|
className="xs:w-1/4 w-full"
|
||||||
|
>
|
||||||
|
<Trans i18nKey="common:save" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
<ConfirmationModal
|
||||||
|
isOpen={isConfirmOpen}
|
||||||
|
onClose={() => setIsConfirmOpen(false)}
|
||||||
|
onConfirm={confirmComplete}
|
||||||
|
titleKey="doctor:confirmFeedbackModal.title"
|
||||||
|
descriptionKey="doctor:confirmFeedbackModal.description"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AnalysisFeedback;
|
||||||
@@ -1,13 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { capitalize } from 'lodash';
|
import { capitalize } from 'lodash';
|
||||||
import { useForm } from 'react-hook-form';
|
|
||||||
|
|
||||||
import { giveFeedbackAction } from '@kit/doctor/actions/doctor-server-actions';
|
|
||||||
import {
|
import {
|
||||||
getDOBWithAgeStringFromPersonalCode,
|
getDOBWithAgeStringFromPersonalCode,
|
||||||
getResultSetName,
|
getResultSetName,
|
||||||
@@ -18,28 +13,14 @@ import {
|
|||||||
Order,
|
Order,
|
||||||
Patient,
|
Patient,
|
||||||
} from '@kit/doctor/schema/doctor-analysis-detail-view.schema';
|
} from '@kit/doctor/schema/doctor-analysis-detail-view.schema';
|
||||||
import {
|
|
||||||
DoctorAnalysisFeedbackForm,
|
|
||||||
doctorAnalysisFeedbackFormSchema,
|
|
||||||
} from '@kit/doctor/schema/doctor-analysis.schema';
|
|
||||||
import ConfirmationModal from '@kit/shared/components/confirmation-modal';
|
|
||||||
import { useCurrentLocaleLanguageNames } from '@kit/shared/hooks';
|
import { useCurrentLocaleLanguageNames } from '@kit/shared/hooks';
|
||||||
import { getFullName } from '@kit/shared/utils';
|
import { getFullName } from '@kit/shared/utils';
|
||||||
import { useUser } from '@kit/supabase/hooks/use-user';
|
import { useUser } from '@kit/supabase/hooks/use-user';
|
||||||
import { Button } from '@kit/ui/button';
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormMessage,
|
|
||||||
} from '@kit/ui/form';
|
|
||||||
import { toast } from '@kit/ui/sonner';
|
|
||||||
import { Textarea } from '@kit/ui/textarea';
|
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import { bmiFromMetric } from '~/lib/utils';
|
import { bmiFromMetric } from '~/lib/utils';
|
||||||
|
|
||||||
|
import AnalysisFeedback from './analysis-feedback';
|
||||||
import DoctorAnalysisWrapper from './doctor-analysis-wrapper';
|
import DoctorAnalysisWrapper from './doctor-analysis-wrapper';
|
||||||
import DoctorJobSelect from './doctor-job-select';
|
import DoctorJobSelect from './doctor-job-select';
|
||||||
|
|
||||||
@@ -54,10 +35,8 @@ export default function AnalysisView({
|
|||||||
analyses: AnalysisResponse[];
|
analyses: AnalysisResponse[];
|
||||||
feedback?: DoctorFeedback;
|
feedback?: DoctorFeedback;
|
||||||
}) {
|
}) {
|
||||||
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
|
|
||||||
const [isDraftSubmitting, setIsDraftSubmitting] = useState(false);
|
|
||||||
|
|
||||||
const { data: user } = useUser();
|
const { data: user } = useUser();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const languageNames = useCurrentLocaleLanguageNames();
|
const languageNames = useCurrentLocaleLanguageNames();
|
||||||
|
|
||||||
@@ -68,65 +47,11 @@ export default function AnalysisView({
|
|||||||
);
|
);
|
||||||
const isCurrentDoctorJob =
|
const isCurrentDoctorJob =
|
||||||
!!feedback?.doctor_user_id && feedback?.doctor_user_id === user?.id;
|
!!feedback?.doctor_user_id && feedback?.doctor_user_id === user?.id;
|
||||||
const isReadOnly =
|
|
||||||
!!feedback?.doctor_user_id && feedback?.doctor_user_id !== user?.id;
|
|
||||||
|
|
||||||
const form = useForm({
|
|
||||||
resolver: zodResolver(doctorAnalysisFeedbackFormSchema),
|
|
||||||
reValidateMode: 'onChange',
|
|
||||||
defaultValues: {
|
|
||||||
feedbackValue: feedback?.value ?? '',
|
|
||||||
userId: patient.userId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
if (!patient || !order || !analyses) {
|
if (!patient || !order || !analyses) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSubmit = async (
|
|
||||||
data: DoctorAnalysisFeedbackForm,
|
|
||||||
status: 'DRAFT' | 'COMPLETED',
|
|
||||||
) => {
|
|
||||||
const result = await giveFeedbackAction({
|
|
||||||
...data,
|
|
||||||
analysisOrderId: order.analysisOrderId,
|
|
||||||
status,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
return toast.error(<Trans i18nKey="common:genericServerError" />);
|
|
||||||
}
|
|
||||||
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
predicate: (query) => query.queryKey.includes('doctor-jobs'),
|
|
||||||
});
|
|
||||||
|
|
||||||
toast.success(<Trans i18nKey={'doctor:updateFeedbackSuccess'} />);
|
|
||||||
|
|
||||||
return setIsConfirmOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDraftSubmit = async (e: React.FormEvent) => {
|
|
||||||
setIsDraftSubmitting(true);
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
form.formState.errors.feedbackValue = undefined;
|
|
||||||
const formData = form.getValues();
|
|
||||||
await onSubmit(formData, 'DRAFT');
|
|
||||||
setIsDraftSubmitting(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCompleteSubmit = form.handleSubmit(async () => {
|
|
||||||
setIsConfirmOpen(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
const confirmComplete = form.handleSubmit(async (data) => {
|
|
||||||
await onSubmit(data, 'COMPLETED');
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="xs:flex xs:justify-between">
|
<div className="xs:flex xs:justify-between">
|
||||||
@@ -228,59 +153,9 @@ export default function AnalysisView({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<h3>
|
{order.isPackage && (
|
||||||
<Trans i18nKey="doctor:feedback" />
|
<AnalysisFeedback order={order} patient={patient} feedback={feedback} />
|
||||||
</h3>
|
|
||||||
<p>{feedback?.value ?? '-'}</p>
|
|
||||||
{!isReadOnly && (
|
|
||||||
<Form {...form}>
|
|
||||||
<form className="space-y-4 lg:w-1/2">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="feedbackValue"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormControl>
|
|
||||||
<Textarea {...field} disabled={isReadOnly} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
)}
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="xs:flex block justify-end gap-2 space-y-2">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={handleDraftSubmit}
|
|
||||||
disabled={
|
|
||||||
isReadOnly || isDraftSubmitting || form.formState.isSubmitting
|
|
||||||
}
|
|
||||||
className="xs:w-1/4 w-full"
|
|
||||||
>
|
|
||||||
<Trans i18nKey="common:saveAsDraft" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
onClick={handleCompleteSubmit}
|
|
||||||
disabled={
|
|
||||||
isReadOnly || isDraftSubmitting || form.formState.isSubmitting
|
|
||||||
}
|
|
||||||
className="xs:w-1/4 w-full"
|
|
||||||
>
|
|
||||||
<Trans i18nKey="common:save" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
)}
|
|
||||||
<ConfirmationModal
|
|
||||||
isOpen={isConfirmOpen}
|
|
||||||
onClose={() => setIsConfirmOpen(false)}
|
|
||||||
onConfirm={confirmComplete}
|
|
||||||
titleKey="doctor:confirmFeedbackModal.title"
|
|
||||||
descriptionKey="doctor:confirmFeedbackModal.description"
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export const AnalysisResponseSchema = z.object({
|
|||||||
export type AnalysisResponse = z.infer<typeof AnalysisResponseSchema>;
|
export type AnalysisResponse = z.infer<typeof AnalysisResponseSchema>;
|
||||||
|
|
||||||
export const AnalysisResultDetailsSchema = z.object({
|
export const AnalysisResultDetailsSchema = z.object({
|
||||||
analysisResponse: z.array(AnalysisResponseSchema).nullable(),
|
analysisResponse: z.array(AnalysisResponseSchema),
|
||||||
order: OrderSchema,
|
order: OrderSchema,
|
||||||
doctorFeedback: DoctorFeedbackSchema,
|
doctorFeedback: DoctorFeedbackSchema,
|
||||||
patient: PatientSchema,
|
patient: PatientSchema,
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
grant execute on function medreport.update_analysis_order_status (
|
||||||
|
bigint,
|
||||||
|
text,
|
||||||
|
medreport.analysis_order_status
|
||||||
|
) to authenticated, service_role;
|
||||||
|
|
||||||
|
create policy "doctor_update_analysis_order"
|
||||||
|
on "medreport"."analysis_orders"
|
||||||
|
as PERMISSIVE
|
||||||
|
for UPDATE
|
||||||
|
to authenticated
|
||||||
|
using (medreport.is_doctor());
|
||||||
Reference in New Issue
Block a user