diff --git a/src/components/Questionnaire/QuestionnaireForm.tsx b/src/components/Questionnaire/QuestionnaireForm.tsx index a9e7538e781..5b13561a270 100644 --- a/src/components/Questionnaire/QuestionnaireForm.tsx +++ b/src/components/Questionnaire/QuestionnaireForm.tsx @@ -42,13 +42,19 @@ export interface QuestionnaireFormState { errors: QuestionValidationError[]; } -interface BatchRequest { +interface FormBatchRequest { url: string; method: string; body: Record; reference_id: string; } +interface ServerValidationError { + reference_id: string; + message: string; + status_code: number; +} + export interface QuestionnaireFormProps { questionnaireSlug?: string; patientId: string; @@ -59,6 +65,214 @@ export interface QuestionnaireFormProps { facilityId: string; } +interface ValidationErrorDisplayProps { + questionnaireForms: QuestionnaireFormState[]; + serverErrors?: ServerValidationError[]; +} + +function ValidationErrorDisplay({ + questionnaireForms, + serverErrors, +}: ValidationErrorDisplayProps) { + const hasErrors = + questionnaireForms.some((form) => form.errors.length > 0) || + (serverErrors?.length ?? 0) > 0; + + if (!hasErrors) return null; + + const findQuestionText = ( + form: QuestionnaireFormState, + questionId: string, + ): string | undefined => { + const findInQuestions = (questions: Question[]): string | undefined => { + for (const q of questions) { + if (q.id === questionId) return q.text; + if (q.type === "group" && q.questions) { + const found = findInQuestions(q.questions); + if (found) return found; + } + } + }; + return ( + findInQuestions(form.questionnaire.questions) || t("unknown_question") + ); + }; + + const getErrorTitle = (error: ServerValidationError) => { + // Find matching questionnaire title first + const form = questionnaireForms.find( + (f) => f.questionnaire.id === error.reference_id, + ); + if (form) { + return form.questionnaire.title; + } + + // For other cases, transform the reference_id into a readable title + return error.reference_id + .split("_") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "); + }; + + const findStructuredQuestionId = ( + forms: QuestionnaireFormState[], + structuredType: string, + ): { questionId: string; form: QuestionnaireFormState } | undefined => { + for (const form of forms) { + const response = form.responses.find( + (r) => r.structured_type === structuredType, + ); + if (response) { + return { questionId: response.question_id, form }; + } + } + return undefined; + }; + + return ( +
+
+
+ +

Validation Errors

+
+ + {/* Server-level errors */} + {serverErrors?.map((error, index) => { + // Find the structured question if this is a structured data error + const structuredQuestion = findStructuredQuestionId( + questionnaireForms, + error.reference_id, + ); + + return ( +
+
+ {getErrorTitle(error)} +
+
+ + {error.message} +
+ {structuredQuestion && ( + + )} +
+ ); + })} + + {/* Form-level errors */} + {questionnaireForms.map( + (form, index) => + form.errors.length > 0 && ( +
+

+ {form.questionnaire.title} +

+
+ {form.errors.map((error, errorIndex) => ( +
+
+ {findQuestionText(form, error.question_id)} +
+
+ + {error.error} +
+ +
+ ))} +
+
+ ), + )} +
+
+ ); +} + export function QuestionnaireForm({ questionnaireSlug, patientId, @@ -72,6 +286,7 @@ export function QuestionnaireForm({ const [questionnaireForms, setQuestionnaireForms] = useState< QuestionnaireFormState[] >([]); + const [serverErrors, setServerErrors] = useState(); const [activeQuestionnaireId, setActiveQuestionnaireId] = useState(); const [activeGroupId, setActiveGroupId] = useState(); @@ -92,13 +307,91 @@ export function QuestionnaireForm({ const { mutate: submitBatch, isPending } = useMutation({ mutationFn: mutate(routes.batchRequest, { silent: true }), onSuccess: () => { + setServerErrors(undefined); toast.success(t("questionnaire_submitted_successfully")); onSubmit?.(); }, onError: (error) => { - const errorData = error.cause; + const errorData = error.cause as { + results: Array<{ + reference_id: string; + status_code: number; + data: + | { + errors?: Array<{ + question_id?: string; + msg?: string; + error?: string; + type?: string; + loc?: string[]; + }>; + } + | Array<{ + errors: Array<{ + type: string; + loc: string[]; + msg: string; + }>; + }>; + }>; + }; + if (errorData?.results) { - handleSubmissionError(errorData.results as ValidationErrorResponse[]); + const results = errorData.results; + + // Only process failed requests (status_code !== 200) + const failedResults = results.filter( + (result) => result.status_code !== 200, + ); + + setServerErrors( + failedResults.map((result) => { + const reference_id = result.reference_id || ""; + let message = t("validation_failed"); + + // Handle array-style structured data errors + if (Array.isArray(result.data)) { + const errors = result.data.flatMap((d) => d.errors || []); + if (errors.length > 0) { + message = errors + .map((e) => { + if (e.loc) { + return `${e.loc.join(" > ")}: ${e.msg}`; + } + return e.msg; + }) + .join(", "); + } + } + // Handle regular errors + else if (result.data?.errors) { + const firstError = result.data.errors[0]; + if (firstError.loc) { + message = `${firstError.loc.join(" > ")}: ${firstError.msg}`; + } else { + message = + firstError.msg || firstError.error || t("validation_failed"); + } + } + + return { + reference_id, + message, + status_code: result.status_code, + }; + }), + ); + + // Handle form-level validation errors + const validationResults = failedResults.filter( + (r) => + !Array.isArray(r.data) && + r.data?.errors?.some((e) => e.question_id), + ); + + if (validationResults.length > 0) { + handleSubmissionError(validationResults as ValidationErrorResponse[]); + } } toast.error(t("questionnaire_submission_failed")); }, @@ -261,7 +554,7 @@ export function QuestionnaireForm({ } // Continue with existing submission logic... - const requests: BatchRequest[] = []; + const requests: FormBatchRequest[] = []; if (encounterId && patientId) { const context = { facilityId, patientId, encounterId }; // First, collect all structured data requests if encounterId is provided @@ -523,6 +816,11 @@ export function QuestionnaireForm({ )} + + )} diff --git a/src/types/questionnaire/batch.ts b/src/types/questionnaire/batch.ts index 677a2e56016..ba243a95818 100644 --- a/src/types/questionnaire/batch.ts +++ b/src/types/questionnaire/batch.ts @@ -1,5 +1,6 @@ export interface BatchRequestResult { - data: T; + reference_id: string; + data?: T; status_code: number; } @@ -12,41 +13,81 @@ export interface BatchRequestBody { }>; } -interface BaseValidationError { +// Error types +export interface QuestionValidationError { + question_id: string; + error?: string; + msg?: string; + type?: string; +} + +export interface DetailedValidationError { type: string; + loc: string[]; msg: string; + ctx?: { + error?: string; + }; } -export interface QuestionValidationError extends BaseValidationError { - question_id: string; +export interface BatchRequestError { + question_id?: string; + msg?: string; error?: string; + type?: string; + loc?: string[]; + ctx?: { + error?: string; + }; } -export interface DetailedValidationError extends BaseValidationError { - loc: string[]; - input: any; +export interface StructuredDataError { + errors: Array<{ + type: string; + loc: string[]; + msg: string; + ctx?: { + error?: string; + }; + }>; +} + +// Request/Response types +export interface BatchRequest { url: string; - ctx?: { - error: string; - }; + method: string; + reference_id: string; + body: any; // Using any since the body type varies based on the request type +} + +export interface BatchErrorData { + errors: BatchRequestError[]; +} + +export interface BatchResponseBase { + reference_id: string; + status_code: number; +} + +export interface BatchErrorResponse extends BatchResponseBase { + data: BatchErrorData | StructuredDataError[]; } -export interface SuccessResponse { - data: any; - status_code: 200; +export interface BatchSuccessResponse extends BatchResponseBase { + data: unknown; } export interface ValidationErrorResponse { + reference_id: string; + status_code: number; data: { - errors: Array; + errors: QuestionValidationError[]; }; - status_code: 400 | 404; } -export type BatchResponseResult = SuccessResponse | ValidationErrorResponse; +// Type unions +export type BatchResponse = BatchErrorResponse | BatchSuccessResponse; -export interface BatchResponse { - results: BatchResponseResult[]; -} +export type BatchSubmissionResult = BatchRequestResult; -export type BatchSubmissionResult = BatchResponseResult; +export type BatchResponseResult = ValidationErrorResponse | BatchResponse;