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
5 changes: 5 additions & 0 deletions public/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2049,6 +2049,11 @@
"downloading_abha_card": "Generating ABHA Card, Please hold on",
"downloads": "Downloads",
"draft": "Draft",
"draft_changes_added": "Added on the server",
"draft_changes_removed": "No longer on the server",
"draft_changes_updated": "Updated on the server",
"draft_data_changed_description": "Some records changed on the server since this draft was saved. Your draft has been kept — review the server-side changes below.",
"draft_data_changed_title": "Some information has changed",
"draft_error_loading": "Failed to load draft",
"draft_forms": "Draft Forms",
"draft_invoice_created": "Draft Invoice Created",
Expand Down
145 changes: 145 additions & 0 deletions src/components/Questionnaire/DraftContextChangesScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { useTranslation } from "react-i18next";

import { cn } from "@/lib/utils";

import CareIcon, { IconName } from "@/CAREUI/icons/CareIcon";

import { Button } from "@/components/ui/button";

import { RecordLike } from "@/components/Questionnaire/structured/contextMatch";

export interface DraftContextChange {
questionId: string;
title: string;
added: RecordLike[];
removed: RecordLike[];
changed: RecordLike[];
}

function recordLabel(record: RecordLike): string {
const code = record.code as { display?: string } | undefined;
const medication = record.medication as { display?: string } | undefined;
return (
code?.display ??
medication?.display ??
(record.status as string | undefined) ??
(record.id as string | undefined) ??
"—"
);
}

type ChangeKind = "added" | "removed" | "changed";

const KIND_META: Record<
ChangeKind,
{ labelKey: string; icon: IconName; dot: string; label: string }
> = {
added: {
labelKey: "draft_changes_added",
icon: "l-plus-circle",
dot: "bg-green-500",
label: "text-green-700",
},
removed: {
labelKey: "draft_changes_removed",
icon: "l-minus-circle",
dot: "bg-red-500",
label: "text-red-700",
},
changed: {
labelKey: "draft_changes_updated",
icon: "l-sync",
dot: "bg-amber-500",
label: "text-amber-700",
},
};

function ChangeGroup({
kind,
records,
}: {
kind: ChangeKind;
records: RecordLike[];
}) {
const { t } = useTranslation();
if (!records.length) return null;
const meta = KIND_META[kind];
return (
<div className="space-y-1.5">
<div
className={cn(
"flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide",
meta.label,
)}
>
<CareIcon icon={meta.icon} className="size-3.5" />
{t(meta.labelKey)}
Comment on lines +33 to +76

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use literal t() calls so these keys survive i18n cleanup.

t(meta.labelKey) prevents remove-unused-i18n.js from statically discovering the three translation keys. Resolve the label with explicit branches containing t("draft_changes_added"), t("draft_changes_removed"), and t("draft_changes_updated").

Based on learnings, translation keys must be passed through statically detectable t() calls.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Questionnaire/DraftContextChangesScreen.tsx` around lines 33 -
76, Update ChangeGroup’s dynamic t(meta.labelKey) call to use explicit branches
for each ChangeKind, with statically detectable t("draft_changes_added"),
t("draft_changes_removed"), and t("draft_changes_updated") calls. Preserve the
existing KIND_META styling and label behavior while ensuring all three
translation keys remain discoverable by i18n cleanup.

Source: Learnings

</div>
<ul className="space-y-1">
{records.map((record, i) => (
<li
key={(record.id as string) ?? i}
className="flex items-center gap-2 text-sm text-gray-700"
>
<span
className={cn("size-1.5 shrink-0 rounded-full", meta.dot)}
aria-hidden
/>
{recordLabel(record)}
</li>
))}
</ul>
</div>
);
}

export function DraftContextChangesScreen({
changes,
onContinue,
}: {
changes: DraftContextChange[];
onContinue: () => void;
}) {
const { t } = useTranslation();

return (
<div className="flex min-h-screen items-center justify-center p-4">
<div className="w-full max-w-lg space-y-6 rounded-xl border border-gray-200 bg-white p-6 shadow-lg sm:p-8">
<div className="space-y-3 text-center">
<div className="mx-auto flex size-12 items-center justify-center rounded-full bg-amber-100">
<CareIcon
icon="l-exclamation-triangle"
className="size-6 text-amber-600"
/>
</div>
<h2 className="text-lg font-semibold text-gray-900">
{t("draft_data_changed_title")}
</h2>
<p className="text-sm text-gray-500">
{t("draft_data_changed_description")}
</p>
</div>

<div className="space-y-3">
{changes.map((change) => (
<div
key={change.questionId}
className="space-y-3 rounded-lg border border-gray-200 bg-gray-50 p-4"
>
<h3 className="text-sm font-semibold text-gray-900">
{change.title}
</h3>
<ChangeGroup kind="added" records={change.added} />
<ChangeGroup kind="removed" records={change.removed} />
<ChangeGroup kind="changed" records={change.changed} />
</div>
))}
</div>

<Button onClick={onContinue} className="w-full">
{t("continue")}
</Button>
</div>
</div>
Comment on lines +105 to +143

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Implement this blocking screen as an accessible modal.

The fixed overlay has no dialog semantics, initial focus, focus trap, or background inertness. Keyboard users can tab into and activate the questionnaire underneath. Use the Shadcn/Radix Dialog primitive with an accessible title and description.

As per coding guidelines, components must use Shadcn primitives and provide proper ARIA attributes and keyboard navigation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Questionnaire/DraftContextChangesScreen.tsx` around lines 105
- 143, Update the blocking screen around the component rendering this
questionnaire change content to use the Shadcn/Radix Dialog primitive instead of
a standalone fixed layout. Render the existing title and description through
DialogTitle and DialogDescription, keep the changes and Continue action inside
DialogContent, and configure it as a non-dismissible modal so focus is trapped
and the background questionnaire is inert while preserving onContinue behavior.

Source: Coding guidelines

);
}
4 changes: 4 additions & 0 deletions src/components/Questionnaire/QuestionRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { StructuredQuestionType } from "@/components/Questionnaire/data/Structur
import { QuestionValidationError } from "@/types/questionnaire/batch";
import {
QuestionnaireResponse,
ResponseContext,
ResponseValue,
} from "@/types/questionnaire/form";
import { Question } from "@/types/questionnaire/question";
Expand All @@ -24,6 +25,7 @@ interface QuestionRendererProps {
questions: Question[];
responses: QuestionnaireResponse[];
onResponseChange: (values: ResponseValue[], questionId: string) => void;
setResponseContext: (questionId: string, context: ResponseContext[]) => void;
errors: QuestionValidationError[];
clearError: (questionId: string) => void;
disabled?: boolean;
Expand All @@ -39,6 +41,7 @@ export function QuestionRenderer({
questions,
responses,
onResponseChange,
setResponseContext,
errors,
clearError,
disabled,
Expand Down Expand Up @@ -82,6 +85,7 @@ export function QuestionRenderer({
encounterId={encounterId}
questionnaireResponses={responses}
updateQuestionnaireResponseCB={onResponseChange}
setResponseContext={setResponseContext}
errors={errors}
clearError={clearError}
disabled={disabled || isPreview}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import {
import allergyIntoleranceApi from "@/types/emr/allergyIntolerance/allergyIntoleranceApi";
import type {
QuestionnaireResponse,
ResponseContext,
ResponseValue,
} from "@/types/questionnaire/form";
import type { Question } from "@/types/questionnaire/question";
Expand All @@ -78,6 +79,7 @@ interface AllergyQuestionProps {
questionId: string,
note?: string,
) => void;
setResponseContext: (questionId: string, context: ResponseContext[]) => void;
disabled?: boolean;
}

Expand Down Expand Up @@ -555,6 +557,7 @@ export function AllergyQuestion({
question,
questionnaireResponse,
updateQuestionnaireResponseCB,
setResponseContext,
disabled,
patientId,
}: AllergyQuestionProps) {
Expand Down Expand Up @@ -594,6 +597,10 @@ export function AllergyQuestion({
],
questionnaireResponse.question_id,
);
setResponseContext(
questionnaireResponse.question_id,
patientAllergies.results,
);
}
}, [patientAllergies]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ import {
import diagnosisApi from "@/types/emr/diagnosis/diagnosisApi";
import {
QuestionnaireResponse,
ResponseContext,
ResponseValue,
} from "@/types/questionnaire/form";
import { Question } from "@/types/questionnaire/question";
Expand All @@ -85,6 +86,7 @@ interface DiagnosisQuestionProps {
questionId: string,
note?: string,
) => void;
setResponseContext: (questionId: string, context: ResponseContext[]) => void;
disabled?: boolean;
question: Question;
}
Expand Down Expand Up @@ -348,6 +350,7 @@ export function DiagnosisQuestion({
encounterId,
questionnaireResponse,
updateQuestionnaireResponseCB,
setResponseContext,
disabled,
question,
}: DiagnosisQuestionProps) {
Expand Down Expand Up @@ -402,6 +405,10 @@ export function DiagnosisQuestion({
],
questionnaireResponse.question_id,
);
setResponseContext(
questionnaireResponse.question_id,
patientDiagnoses.results,
);
}
}, [patientDiagnoses]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import encounterApi from "@/types/emr/encounter/encounterApi";
import { QuestionValidationError } from "@/types/questionnaire/batch";
import type {
QuestionnaireResponse,
ResponseContext,
ResponseValue,
} from "@/types/questionnaire/form";
import type { Question } from "@/types/questionnaire/question";
Expand All @@ -58,6 +59,7 @@ interface EncounterQuestionProps {
questionId: string,
note?: string,
) => void;
setResponseContext: (questionId: string, context: ResponseContext[]) => void;
disabled?: boolean;
clearError: () => void;
organizations?: string[];
Expand Down Expand Up @@ -94,6 +96,7 @@ export function EncounterQuestion({
question,
questionnaireResponse,
updateQuestionnaireResponseCB,
setResponseContext,
disabled,
clearError,
encounterId,
Expand Down Expand Up @@ -183,6 +186,7 @@ export function EncounterQuestion({
updates.status = EncounterStatus.DISCHARGED;
}
handleUpdateEncounter(updates);
setResponseContext(questionnaireResponse.question_id, [encounterData]);
}
}, [encounterData]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ import productKnowledgeApi from "@/types/inventory/productKnowledge/productKnowl
import { QuestionValidationError } from "@/types/questionnaire/batch";
import {
QuestionnaireResponse,
ResponseContext,
ResponseValue,
} from "@/types/questionnaire/form";
import { QuestionnaireResponseTemplateReadSpec } from "@/types/questionnaire/questionnaireResponseTemplate";
Expand Down Expand Up @@ -211,6 +212,7 @@ interface MedicationRequestQuestionProps {
questionId: string,
note?: string,
) => void;
setResponseContext: (questionId: string, context: ResponseContext[]) => void;
disabled?: boolean;
encounterId: string;
errors?: QuestionValidationError[];
Expand Down Expand Up @@ -343,6 +345,7 @@ export function validateMedicationRequestQuestion(
export function MedicationRequestQuestion({
questionnaireResponse,
updateQuestionnaireResponseCB,
setResponseContext,
disabled,
patientId,
encounterId,
Expand Down Expand Up @@ -387,7 +390,13 @@ export function MedicationRequestQuestion({
});

useEffect(() => {
if (prescriptionId && patientMedications?.results) {
// Without a prescription there is no existing-medication fetch; still mark
// context as populated ([]) so draft reconciliation doesn't wait on it.
if (!prescriptionId) {
setResponseContext(questionnaireResponse.question_id, []);
return;
}
if (patientMedications?.results) {
updateQuestionnaireResponseCB(
[
{
Expand All @@ -403,6 +412,10 @@ export function MedicationRequestQuestion({
],
questionnaireResponse.question_id,
);
setResponseContext(
questionnaireResponse.question_id,
patientMedications.results,
);
}
}, [patientMedications, prescriptionId]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import medicationStatementApi from "@/types/emr/medicationStatement/medicationSt
import { QuestionValidationError } from "@/types/questionnaire/batch";
import {
QuestionnaireResponse,
ResponseContext,
ResponseValue,
} from "@/types/questionnaire/form";
import { Question } from "@/types/questionnaire/question";
Expand All @@ -83,6 +84,7 @@ interface MedicationStatementQuestionProps {
questionId: string,
note?: string,
) => void;
setResponseContext: (questionId: string, context: ResponseContext[]) => void;
disabled?: boolean;
errors: QuestionValidationError[];
}
Expand Down Expand Up @@ -145,6 +147,7 @@ export function validateMedicationStatementQuestion(
export function MedicationStatementQuestion({
questionnaireResponse,
updateQuestionnaireResponseCB,
setResponseContext,
disabled,
patientId,
encounterId,
Expand Down Expand Up @@ -186,6 +189,10 @@ export function MedicationStatementQuestion({
[{ type: "medication_statement", value: patientMedications.results }],
questionnaireResponse.question_id,
);
setResponseContext(
questionnaireResponse.question_id,
patientMedications.results,
);
}
}, [patientMedications]);

Expand Down
Loading
Loading