From 5fcab8ea134cd6a65d8da33c212b4a5da95a039b Mon Sep 17 00:00:00 2001 From: NikhilA8606 Date: Tue, 16 Jun 2026 09:18:23 +0530 Subject: [PATCH 1/6] ENG-500 Support for creating multiple diagnostic reports in a service request --- public/locale/en.json | 5 +- .../components/DiagnosticReportForm.tsx | 1139 ++++++++++------- .../components/DiagnosticReportReview.tsx | 298 ++--- 3 files changed, 811 insertions(+), 631 deletions(-) diff --git a/public/locale/en.json b/public/locale/en.json index c925ea88482..c58f2f25666 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -691,6 +691,7 @@ "and_more_medications": "+{{count}} more medication(s)", "and_more_service_requests": "+{{count}} more service request(s)", "and_the_status_of_request_is": "and the status of request is", + "another_diagnostic_report": "Another Diagnostic Report", "answer": "Answer", "answer_options": "Answer options", "answer_options_description": "Define possible answers for this question", @@ -5035,7 +5036,7 @@ "result_date": "Result Date", "result_details": "Result details", "result_on": "Result on", - "result_review": "Result Review", + "result_review": "Result Review of {{name}}", "result_value": "Result value", "resume": "Resume", "retake": "Retake", @@ -5067,6 +5068,7 @@ "review_and_finalise_request_description": "Add more items if needed, or approve to mark this delivery as requested.", "review_before": "Review Before", "review_missed": "Review Missed", + "review_test_results": "Review Test Reult", "revisit_days_non_negative": "Re-visit allowed days cannot be negative", "revoke": "Revoke", "revoke_token": "Revoke Token", @@ -5532,6 +5534,7 @@ "select_register_patient": "Select/Register Patient", "select_report": "Select Report", "select_report_type": "Select Report Type", + "select_report_type_to_create": "Select 'Select Diagnostic Report Type' and create Report", "select_requester": "Select requester", "select_resource": "Select the resource", "select_resource_category": "Select resource category", diff --git a/src/pages/Facility/services/serviceRequests/components/DiagnosticReportForm.tsx b/src/pages/Facility/services/serviceRequests/components/DiagnosticReportForm.tsx index 7355ce7bb43..e096c8788db 100644 --- a/src/pages/Facility/services/serviceRequests/components/DiagnosticReportForm.tsx +++ b/src/pages/Facility/services/serviceRequests/components/DiagnosticReportForm.tsx @@ -3,7 +3,9 @@ import { ChevronsDownUp, ChevronsUpDown, CloudUpload, + FileUp, NotepadText, + Plus, PlusCircle, Save, Trash2, @@ -15,7 +17,6 @@ import { toast } from "sonner"; import { cn } from "@/lib/utils"; -import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { @@ -33,18 +34,9 @@ import { SelectValue, } from "@/components/ui/select"; import { Separator } from "@/components/ui/separator"; -import { Skeleton } from "@/components/ui/skeleton"; - -import { Avatar } from "@/components/Common/Avatar"; -import { FileListTable } from "@/components/Files/FileListTable"; -import FileUploadDialog from "@/components/Files/FileUploadDialog"; import useFileUpload from "@/hooks/useFileUpload"; -import mutate from "@/Utils/request/mutate"; -import query from "@/Utils/request/query"; -import { PaginatedResponse } from "@/Utils/request/types"; -import { formatName } from "@/Utils/utils"; import { Code } from "@/types/base/code/code"; import { DIAGNOSTIC_REPORT_STATUS_COLORS, @@ -68,11 +60,20 @@ import { SpecimenDefinitionRead } from "@/types/emr/specimenDefinition/specimenD import { BACKEND_ALLOWED_EXTENSIONS, FileReadMinimal, + FileType, } from "@/types/files/file"; import fileApi from "@/types/files/fileApi"; +import mutate from "@/Utils/request/mutate"; +import query from "@/Utils/request/query"; +import { PaginatedResponse } from "@/Utils/request/types"; +import { Avatar } from "@/components/Common/Avatar"; +import { FileListTable } from "@/components/Files/FileListTable"; +import FileUploadDialog from "@/components/Files/FileUploadDialog"; +import { Badge } from "@/components/ui/badge"; import { PLUGIN_Component } from "@/PluginEngine"; import { Interpretation } from "@/types/base/qualifiedRange/qualifiedRange"; +import { formatName } from "@/Utils/utils"; interface DiagnosticReportFormProps { patientId: string; @@ -121,54 +122,21 @@ export function DiagnosticReportForm({ disableEdit, }: DiagnosticReportFormProps) { const { t } = useTranslation(); - const [observations, setObservations] = useState( - {}, - ); - const [isExpanded, setIsExpanded] = useState(true); + const queryClient = useQueryClient(); + + const [showReportTypeSelect, setShowReportTypeSelect] = useState(false); const [selectedReportCode, setSelectedReportCode] = useState( null, ); - const [openUploadDialog, setOpenUploadDialog] = useState(false); - const [conclusion, setConclusion] = useState(""); - const queryClient = useQueryClient(); - - // Get the latest report if any exists - const latestReport = - diagnosticReports.length > 0 ? diagnosticReports[0] : null; - const hasReport = !!latestReport; // Check if all required specimens are collected const hasCollectedSpecimens = activityDefinition?.specimen_requirements?.length === 0 || specimens.some((specimen) => specimen.status === SpecimenStatus.available); - // Fetch the full diagnostic report to get observations - const { data: fullReport, isLoading: isLoadingReport } = useQuery({ - queryKey: ["diagnosticReport", latestReport?.id], - queryFn: query(diagnosticReportApi.retrieveDiagnosticReport, { - pathParams: { - patient_external_id: patientId, - external_id: latestReport?.id || "", - }, - }), - enabled: !!latestReport?.id, - }); - - // Query to fetch files for the diagnostic report - const { data: files = { results: [], count: 0 } } = useQuery< - PaginatedResponse - >({ - queryKey: ["files", "diagnostic_report", fullReport?.id], - queryFn: query(fileApi.list, { - queryParams: { - file_type: "diagnostic_report", - associating_id: fullReport?.id, - limit: 100, - offset: 0, - }, - }), - enabled: !!fullReport?.id, - }); + const isMultipleDiagnosticReport = + !!activityDefinition?.diagnostic_report_codes && + activityDefinition.diagnostic_report_codes.length > 0; // Creating a new diagnostic report const { mutate: createDiagnosticReport, isPending: isCreatingReport } = @@ -183,35 +151,180 @@ export function DiagnosticReportForm({ queryClient.invalidateQueries({ queryKey: ["serviceRequest"], }); - // Fetch the newly created report queryClient.invalidateQueries({ queryKey: ["diagnosticReport"], }); }, - onError: (err: any) => { + onError: (err: Error) => { toast.error( `Failed to create diagnostic report: ${err.message || "Unknown error"}`, ); }, }); - // Effect to handle diagnostic reports changes - useEffect(() => { - const latestReport = diagnosticReports[0]; - if (latestReport) { - // If we have a new report, update the UI accordingly - setSelectedReportCode(latestReport.code || null); - setIsExpanded(true); + function handleCreateReport(code?: Code) { + if (!hasCollectedSpecimens) { + toast.error(t("specimen_collection_required")); + return; } - }, [diagnosticReports]); - // Effect to handle fullReport changes - useEffect(() => { - if (fullReport) { - // When we get the full report details, ensure UI is in correct state - setSelectedReportCode(fullReport.code || null); - } - }, [fullReport]); + const category: Code = { + code: "LAB", + display: "Laboratory", + system: "http://terminology.hl7.org/CodeSystem/v2-0074", + }; + + createDiagnosticReport({ + status: DiagnosticReportStatus.preliminary, + category, + service_request: serviceRequestId, + code: code || undefined, + }); + } + + return ( + <> + {diagnosticReports.length > 0 && ( +
+
+ {diagnosticReports.map((report) => ( + + ))} +
+ {isMultipleDiagnosticReport && ( +
+ {showReportTypeSelect ? ( + + ) : ( + + )} +
+ )} +
+ )} + {diagnosticReports.length === 0 && ( + + )} + + ); +} + +function DiagnosticReportItem({ + report, + patientId, + serviceRequestId, + observationDefinitions, + disableEdit, + isMultipleDiagnosticReport, +}: { + report: DiagnosticReportRead; + patientId: string; + serviceRequestId: string; + observationDefinitions: ObservationDefinitionReadSpec[]; + disableEdit: boolean; + isMultipleDiagnosticReport: boolean; +}) { + const { t } = useTranslation(); + const queryClient = useQueryClient(); + const [observations, setObservations] = useState( + {}, + ); + const [isExpanded, setIsExpanded] = useState(true); + const [openUploadDialog, setOpenUploadDialog] = useState(false); + const [conclusion, setConclusion] = useState(""); + + const { data: fullReport } = useQuery({ + queryKey: ["diagnosticReport", report.id], + queryFn: query(diagnosticReportApi.retrieveDiagnosticReport, { + pathParams: { + patient_external_id: patientId, + external_id: report.id, + }, + }), + enabled: !!report.id, + }); + + // Query to fetch files for the diagnostic report + const { data: files = { results: [], count: 0 } } = useQuery< + PaginatedResponse + >({ + queryKey: ["files", "diagnostic_report", report.id], + queryFn: query(fileApi.list, { + queryParams: { + file_type: "diagnostic_report", + associating_id: report.id, + limit: 100, + offset: 0, + }, + }), + enabled: !!report.id, + }); // Upserting observations for a diagnostic report const { mutate: upsertObservations, isPending: isUpsertingObservations } = @@ -219,7 +332,7 @@ export function DiagnosticReportForm({ mutationFn: mutate(observationApi.upsertObservations, { pathParams: { patient_external_id: patientId, - external_id: latestReport?.id || "", + external_id: report.id, }, }), onSuccess: () => { @@ -228,10 +341,10 @@ export function DiagnosticReportForm({ queryKey: ["serviceRequest", serviceRequestId], }); queryClient.invalidateQueries({ - queryKey: ["diagnosticReport", latestReport?.id], + queryKey: ["diagnosticReport", report.id], }); }, - onError: (err: any) => { + onError: (err: Error) => { toast.error( `Failed to save test results: ${err.message || "Unknown error"}`, ); @@ -243,13 +356,13 @@ export function DiagnosticReportForm({ mutationFn: mutate(diagnosticReportApi.updateDiagnosticReport, { pathParams: { patient_external_id: patientId, - external_id: latestReport?.id || "", + external_id: report.id, }, }), onSuccess: () => { toast.success(t("conclusion_updated_successfully")); queryClient.invalidateQueries({ - queryKey: ["diagnosticReport", latestReport?.id], + queryKey: ["diagnosticReport", report.id], }); setIsExpanded(false); }, @@ -260,13 +373,13 @@ export function DiagnosticReportForm({ // Initialize file upload hook const fileUpload = useFileUpload({ - type: "diagnostic_report" as any, + type: "diagnostic_report" as FileType, multiple: true, allowedExtensions: BACKEND_ALLOWED_EXTENSIONS, allowNameFallback: false, onUpload: () => { queryClient.invalidateQueries({ - queryKey: ["diagnosticReport", latestReport?.id], + queryKey: ["diagnosticReport", report.id], }); }, compress: false, @@ -289,9 +402,10 @@ export function DiagnosticReportForm({ if (!openUploadDialog) { fileUpload.clearFiles(); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [openUploadDialog]); - // Initialize form with existing observations from the full report + // Initialize form with existing observations from the report useEffect(() => { if (fullReport?.observations && fullReport.observations.length > 0) { const initialObservations: ObservationsByDefinition = {}; @@ -333,10 +447,10 @@ export function DiagnosticReportForm({ }); setObservations(initialObservations); + } - if (fullReport.conclusion) { - setConclusion(fullReport.conclusion); - } + if (fullReport?.conclusion) { + setConclusion(fullReport.conclusion); } }, [fullReport]); @@ -467,36 +581,7 @@ export function DiagnosticReportForm({ }); } - function handleCreateReport() { - // Only create a new report if no reports exist - if (!hasReport) { - if (!hasCollectedSpecimens) { - toast.error(t("specimen_collection_required")); - return; - } - - const category: Code = { - code: "LAB", - display: "Laboratory", - system: "http://terminology.hl7.org/CodeSystem/v2-0074", - }; - - createDiagnosticReport({ - status: DiagnosticReportStatus.preliminary, - category, - service_request: serviceRequestId, - code: selectedReportCode || undefined, - }); - } - } - function handleSubmit() { - if (!hasReport) { - // First create a report if none exists - handleCreateReport(); - return; - } - try { // Check if all observations have values const hasObservationValue = Object.values(observations).some((obsList) => @@ -645,23 +730,21 @@ export function DiagnosticReportForm({ ) .filter((obs): obs is ObservationUpsertRequest => obs !== null); - if (fullReport) { - // Upsert observations - if (formattedObservations.length > 0) { - upsertObservations({ - observations: formattedObservations, - }); - } - - updateDiagnosticReport({ - id: fullReport.id, - status: fullReport.status, - category: fullReport.category, - code: fullReport.code, - note: fullReport.note, - conclusion, + // Upsert observations + if (formattedObservations.length > 0) { + upsertObservations({ + observations: formattedObservations, }); } + + updateDiagnosticReport({ + id: report.id, + status: report.status, + category: report.category, + code: report.code, + note: report.note, + conclusion, + }); } catch (_error) { toast.error(t("error_validating_form")); } @@ -808,23 +891,7 @@ export function DiagnosticReportForm({ ); } - const isSubmitting = - isCreatingReport || isUpsertingObservations || isUpdatingReport; - - // Show loading state while fetching the report - if (hasReport && isLoadingReport) { - return ( - - -
- - - -
-
-
- ); - } + const isSubmitting = isUpsertingObservations || isUpdatingReport; return ( {" "} - {t("test_results_entry")} + {isMultipleDiagnosticReport + ? report.code?.display + : t("test_results_entry")}

- {hasReport && fullReport?.created_by && ( -
+
+ {report.created_by && ( - - {formatName(fullReport.created_by)} - -
- )} -
- {hasReport && fullReport && ( - - {t(fullReport.status)} - )} + + {formatName(report.created_by)} + +
+
+ + {t(report.status)} + + ) )} - > -
- - {isErrored ? ( - - {t("marked_for_deletion")} - - ) : ( - !disableEdit && ( - - ) - )} -
+
- {/* For blood pressure and similar observations with components, we may or may not need to show the main value field */} - {!hasComponents && ( -
- {definition.permitted_unit && ( -
- - -
- )} - -
+ {/* For blood pressure and similar observations with components, we may or may not need to show the main value field */} + {!hasComponents && ( +
+ {definition.permitted_unit && ( +
- - handleValueChange( +
+ )} + +
+ + + handleValueChange( + definition.id, + index, + e.target.value, + observationData.unit, + ) + } + placeholder={t("result_value")} + type={ + definition.permitted_data_type === + "decimal" || + definition.permitted_data_type === + "integer" + ? "number" + : "text" + } + disabled={isErrored || disableEdit} + />
+
+ )} + + {/* Render component inputs for multi-component observations */} + {hasComponents && + renderComponentInputs( + definition, + observationData, + index, )} +
+ ); + })} + + {/* Add button for multiple observations */} + +
+ + + ); + })} + +
+ {files?.results && files.results.length > 0 && ( +
+
+ {t("uploaded_files")} +
+ +
+ )} - {/* Render component inputs for multi-component observations */} - {hasComponents && - renderComponentInputs( - definition, - observationData, - index, - )} -
- ); + {report?.status === DiagnosticReportStatus.preliminary && ( + + +
+
+ + +
+ {t("allowed_formats_are", { + formats: + BACKEND_ALLOWED_EXTENSIONS.slice(0, 5).join( + ", ", + ) + + ", " + + t("etc"), })} - - {/* Add button for multiple observations */} -
- - - ); - })} + +
- {fullReport.status !== DiagnosticReportStatus.final && ( + {fileUpload.files.length > 0 && ( + + )} +
+
+
+ )} + {report.status !== DiagnosticReportStatus.final && ( )} + {report?.status === DiagnosticReportStatus.preliminary && ( +
+ +
+ )} +
+
+ + + -
- {fullReport?.status === - DiagnosticReportStatus.preliminary && ( -
- -
- )} + {fileUpload.Dialogues} + + + ); +} - {files?.results && files.results.length > 0 && ( -
-
- {t("uploaded_files")} -
- -
- )} +const CreateDiagnosticReportForm = ({ + activityDefinition, + specimens, + isCreatingReport, + disableEdit, + serviceRequestId, + handleCreateReport, +}: { + activityDefinition?: { + diagnostic_report_codes?: Code[]; + classification?: string; + specimen_requirements?: SpecimenDefinitionRead[]; + }; + specimens: SpecimenRead[]; + isCreatingReport: boolean; + disableEdit: boolean; + serviceRequestId: string; + handleCreateReport: (code?: Code) => void; +}) => { + const [isExpanded, setIsExpanded] = useState(false); + const { t } = useTranslation(); + // Check if all required specimens are collected + const hasCollectedSpecimens = + activityDefinition?.specimen_requirements?.length === 0 || + specimens.some((specimen) => specimen.status === SpecimenStatus.available); - {fullReport?.status === - DiagnosticReportStatus.preliminary && ( - - -
-
- - -
- {t("allowed_formats_are", { - formats: - BACKEND_ALLOWED_EXTENSIONS.slice(0, 5).join( - ", ", - ) + - ", " + - t("etc"), - })} -
- -
+ const isMultipleDiagnosticReport = + !!activityDefinition?.diagnostic_report_codes && + activityDefinition.diagnostic_report_codes.length > 0; - {fileUpload.files.length > 0 && ( - - )} -
-
-
- )} + const [selectedReportCode, setSelectedReportCode] = useState( + null, + ); + + return ( + + + + +
+
+ +

+ {" "} + + {t("test_results_entry")} + +

+
+
+
+
+
- ) : ( -
-
-

- {!hasCollectedSpecimens - ? t("collect_specimen_before_report") - : t("no_test_results_recorded")} +

+ + + + + + +
+
+ +

+ {!hasCollectedSpecimens + ? t("collect_specimen_before_report") + : t("no_test_results_recorded")} +

+ {isMultipleDiagnosticReport && ( +

+ {t("select_report_type_to_create")}

-
-
- {activityDefinition?.diagnostic_report_codes && - activityDefinition.diagnostic_report_codes.length > 0 && ( -
- -
- )} + )} +
+
+ {isMultipleDiagnosticReport && ( +
+ + +
+ )} +
+
- )} +
- - {fileUpload.Dialogues} - ); -} +}; diff --git a/src/pages/Facility/services/serviceRequests/components/DiagnosticReportReview.tsx b/src/pages/Facility/services/serviceRequests/components/DiagnosticReportReview.tsx index 475213daf45..d2727c0bc69 100644 --- a/src/pages/Facility/services/serviceRequests/components/DiagnosticReportReview.tsx +++ b/src/pages/Facility/services/serviceRequests/components/DiagnosticReportReview.tsx @@ -22,7 +22,6 @@ import { CollapsibleTrigger, } from "@/components/ui/collapsible"; import { Label } from "@/components/ui/label"; -import { Skeleton } from "@/components/ui/skeleton"; import { Avatar } from "@/components/Common/Avatar"; import ConfirmActionDialog from "@/components/Common/ConfirmActionDialog"; @@ -57,44 +56,68 @@ export function DiagnosticReportReview({ diagnosticReports, disableEdit, }: DiagnosticReportReviewProps) { + return ( + <> + {diagnosticReports.map((report) => ( + + ))} + + ); +} + +function DiagnosticReportReviewItem({ + report, + facilityId, + patientId, + disableEdit, +}: { + report: DiagnosticReportRead; + facilityId: string; + patientId: string; + disableEdit: boolean; +}) { const { t } = useTranslation(); + const queryClient = useQueryClient(); const [isExpanded, setIsExpanded] = useState(true); const [conclusion, setConclusion] = useState(""); const [showApproveDialog, setShowApproveDialog] = useState(false); - const queryClient = useQueryClient(); - const latestReport = diagnosticReports[0]; - // Fetch the full diagnostic report to get observations - const { data: fullReport, isLoading: isLoadingReport } = useQuery({ - queryKey: ["diagnosticReport", latestReport?.id], + const { data: fullReport } = useQuery({ + queryKey: ["diagnosticReport", report.id], queryFn: query(diagnosticReportApi.retrieveDiagnosticReport, { pathParams: { patient_external_id: patientId, - external_id: latestReport?.id || "", + external_id: report.id, }, }), - enabled: !!latestReport?.id, + enabled: !!report.id, }); useEffect(() => { if (fullReport?.conclusion) { setConclusion(fullReport.conclusion); } - }, [fullReport]); + }, [fullReport?.conclusion]); const { data: files = { results: [], count: 0 } } = useQuery< PaginatedResponse >({ - queryKey: ["files", "diagnostic_report", fullReport?.id], + queryKey: ["files", "diagnostic_report", report.id], queryFn: query(fileApi.list, { queryParams: { file_type: "diagnostic_report", - associating_id: fullReport?.id, + associating_id: report.id, limit: 100, offset: 0, }, }), - enabled: !!fullReport?.id, + enabled: !!report.id, }); const { mutate: updateDiagnosticReport, isPending: isUpdatingReport } = @@ -102,7 +125,7 @@ export function DiagnosticReportReview({ mutationFn: mutate(diagnosticReportApi.updateDiagnosticReport, { pathParams: { patient_external_id: patientId, - external_id: latestReport?.id || "", + external_id: report.id, }, }), onSuccess: () => { @@ -118,47 +141,30 @@ export function DiagnosticReportReview({ queryKey: ["files"], }); }, - onError: (err: any) => { + onError: (err: Error) => { toast.error( `Failed to approve diagnostic report: ${err.message || "Unknown error"}`, ); }, }); + // Prefer the full detail (with observations); fall back to the list report + // while the detail request is still loading. + const reportDetail = fullReport ?? report; + const handleApprove = () => { - if (latestReport) { - updateDiagnosticReport({ - ...latestReport, - status: DiagnosticReportStatus.final, - conclusion, - }); - } + updateDiagnosticReport({ + ...reportDetail, + status: DiagnosticReportStatus.final, + conclusion, + }); }; - if (!latestReport) { - return null; - } - - // Show loading state while fetching the report - if (isLoadingReport) { - return ( - - -
- - - -
-
-
- ); - } - - // Don't show the report review if there are no observations and no files and no conclusion + // Don't show the report review if there are no observations, files or conclusion if ( - (!fullReport?.observations || fullReport.observations.length === 0) && + (!reportDetail.observations || reportDetail.observations.length === 0) && (!files?.results || files.results.length === 0) && - !fullReport?.conclusion + !reportDetail.conclusion ) { return null; } @@ -179,31 +185,27 @@ export function DiagnosticReportReview({

{" "} - {t("result_review")} + {t("result_review", { name: report.code?.display })}

- {fullReport?.created_by && ( + {report.created_by && (
- {formatName(fullReport.created_by)} + {formatName(report.created_by)}
)} - {fullReport && ( - - {t(fullReport.status)} - - )} + + {t(report.status)} +