Skip to content
Merged
16 changes: 11 additions & 5 deletions app/(app)/app/diagnostics/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { notFound } from "next/navigation";
import Link from "next/link";
import { ArrowLeft, FileEdit, Stethoscope } from "lucide-react";
import { getDiagnosticById } from "@/actions/diagnostics";
import { getDoctorSignature } from "@/actions/doctor-signature";
import { getDoctorSignatureByUserId } from "@/actions/doctor-signature";
import { PrescriptionPrintDocument } from "@/components/diagnostics/prescription-print-document";
import { DiagnosticValidationPanel } from "@/components/diagnostics/diagnostic-validation-panel";
import { ComorbiditiesPanel } from "@/components/diagnostics/comorbidities-panel";
Expand All @@ -13,13 +13,19 @@ interface DiagnosticDetailPageProps {

export default async function DiagnosticDetailPage({ params }: DiagnosticDetailPageProps) {
const { id } = await params;
const [diagnostic, signature] = await Promise.all([
getDiagnosticById(id),
getDoctorSignature(),
]);
const diagnostic = await getDiagnosticById(id);

if (!diagnostic) notFound();

// Le document porte la signature du médecin qui a validé le diagnostic, et non
// celle de la personne qui consulte la page : afficher la signature du lecteur
// sous le nom et le n° RPPS d'un autre praticien produirait un document faux.
// Tant que la validation n'a pas eu lieu, aucune signature n'est apposée.
const signature =
diagnostic.validation_status === "validated" && diagnostic.validated_by_user_id
? await getDoctorSignatureByUserId(diagnostic.validated_by_user_id)
: null;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

const STATUS_STYLES: Record<string, string> = {
draft: "bg-gray-100 text-gray-600",
pending_validation: "bg-amber-100 text-amber-700",
Expand Down
104 changes: 85 additions & 19 deletions components/diagnostics/prescription-builder-step.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useTranslations } from "next-intl";
import { useForm, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Plus, Trash2, AlertTriangle, Pill, ClipboardList, ActivitySquare, Loader2 } from "lucide-react";
import { Plus, Trash2, AlertTriangle, Pill, ClipboardList, ActivitySquare, Loader2, Check } from "lucide-react";
import { AtcDrugSearch } from "@/components/diagnostics/atc-drug-search";
import { IcfSearchField } from "@/components/diagnostics/icf-search-field";
import { DrugInteractionWarning } from "@/components/diagnostics/drug-interaction-warning";
Expand Down Expand Up @@ -98,6 +98,9 @@ function VigibaseSignalBadge({ signal }: { signal: PharmacovigilanceSignal }) {
);
}

/** Issue du contrôle d'interactions, rendue visible dans tous les cas. */
type InteractionCheckState = "idle" | "checking" | "clear" | "found" | "failed";

interface PrescriptionBuilderStepProps {
validatedDiagnosisName: string;
patientAllergies: string[];
Expand All @@ -123,7 +126,8 @@ export function PrescriptionBuilderStep({

const [icfCodes, setIcfCodes] = useState<IcfCode[]>([]);
const [drugInteractions, setDrugInteractions] = useState<DrugInteractionPair[]>([]);
const [interactionLoading, setInteractionLoading] = useState(false);
const [interactionCheckState, setInteractionCheckState] = useState<InteractionCheckState>("idle");
const [uncodedDrugCount, setUncodedDrugCount] = useState(0);
const [vigibaseSignals, setVigibaseSignals] = useState<Map<string, PharmacovigilanceSignal>>(new Map());

const { register, handleSubmit, control, watch, formState: { errors } } = useForm<Omit<PrescriptionInput, "treatments" | "recommendations" | "follow_up_tests" | "icf_codes">>({
Expand All @@ -137,32 +141,49 @@ export function PrescriptionBuilderStep({

const selectedDocumentType = watch("document_type");

// Check drug-drug interactions whenever treatments with rxcui change
// Contrôle des interactions à chaque changement de traitement.
//
// Chaque issue est rendue visible : un échec réseau ne doit jamais ressembler
// à « aucune interaction », sans quoi le prescripteur lit l'absence d'alerte
// comme un feu vert. Les médicaments sans code RxNorm sortent du contrôle et
// sont comptés pour être signalés explicitement.
const checkInteractions = useCallback(async (currentTreatments: PrescriptionTreatment[]) => {
const rxcuis = currentTreatments
const namedTreatments = currentTreatments.filter(
(treatment) => treatment.drug_name && treatment.drug_name.trim() !== ""
);
const rxcuis = namedTreatments
.map((treatment) => treatment.rxcui)
.filter((code) => code && code.trim() !== "" && /^\d+$/.test(code));
.filter((code): code is string => Boolean(code) && /^\d+$/.test(code!));

setUncodedDrugCount(namedTreatments.length - rxcuis.length);

if (rxcuis.length < 2) {
setDrugInteractions([]);
setInteractionCheckState("idle");
return;
}

setInteractionLoading(true);
setInteractionCheckState("checking");
try {
const response = await fetch("/api/who/drug-interactions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ rxcuis }),
});
if (response.ok) {
const result: { interactions: DrugInteractionPair[] } = await response.json();
setDrugInteractions(result.interactions ?? []);

if (!response.ok) {
setDrugInteractions([]);
setInteractionCheckState("failed");
return;
}

const result: { interactions: DrugInteractionPair[] } = await response.json();
const foundInteractions = result.interactions ?? [];
setDrugInteractions(foundInteractions);
setInteractionCheckState(foundInteractions.length > 0 ? "found" : "clear");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Localiser les schémas de validation existants avant d'ajouter un schéma de réponse.
fd -a '^validations\.ts$' . | while IFS= read -r file; do
  ast-grep outline "$file" --items all
done

# Rechercher les contrats et schémas existants pour les interactions médicamenteuses.
rg -n -C 3 --glob '*.{ts,tsx}' \
  'DrugInteractionPair|drugInteraction.*Schema|interactions.*z\.(array|object)'

Repository: Zoubeir23/DocFlowAI

Length of output: 1542


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspectez le composant concerné et ses dépendances d'interaction médicamenteuse.
target="components/diagnostics/prescription-builder-step.tsx"
if [ -f "$target" ]; then
  echo "== file exists =="
  wc -l "$target"
  echo "== outline =="
  ast-grep outline "$target" --items all || true
  echo "== relevant lines =="
  sed -n '1,260p' "$target" | nl -ba
fi

echo "== search DrugInteractionPair and related symbols =="
rg -n -C 2 --glob '*.{ts,tsx}' 'DrugInteractionPair|drugInteraction|interactions|Warning|severity|failure|failed' || true

echo "== validations file =="
sed -n '1,180p' lib/validations.ts | nl -ba

Repository: Zoubeir23/DocFlowAI

Length of output: 2019


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Listiez les outils disponibles pour lire les segments de fichier.
command -v sed || true
command -v awk || true

file="components/diagnostics/prescription-builder-step.tsx"
types_file="types.ts"
for target in "$file" "$types_file"; do
  if [ -f "$target" ]; then
    echo "== $target =="
    wc -l "$target"
    sed -n '1,260p' "$target"
  else
    echo "== missing $target =="
  fi
done

echo "== DrugInteractionWarning =="
fd -a 'drug-interaction-warning\.(tsx|ts)$' . | while IFS= read -r f; do
  echo "== $f =="
  wc -l "$f"
  sed -n '1,220p' "$f"
done

echo "== DrugInteractionPair candidates =="
rg -n -C 3 --glob '*.{ts,tsx}' 'type DrugInteractionPair|interface DrugInteractionPair|DrugInteractionPair' || true

Repository: Zoubeir23/DocFlowAI

Length of output: 13094


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate types files =="
fd -a '\.(ts|tsx)$' . | rg '/types\.ts$|types/$|types' || true

echo "== search DrugInteractionPair in tracked files =="
git ls-files | xargs rg -n -C 4 'DrugInteractionPair' || true

echo "== read candidate types files =="
for f in $(git ls-files | rg '/types\.ts$'); do
  echo "== $f =="
  wc -l "$f"
  sed -n '1,260p' "$f"
done

echo "== inspect response body path if any =="
git ls-files | rg 'api/who/drug-interactions|drug.*interaction' | while IFS= read -r f; do
  echo "== $f =="
  wc -l "$f"
  sed -n '1,220p' "$f"
done

echo "== validate empty/missing JSON behavior =="
node - <<'JS'
for const value of [{}, {interactions: undefined}, {interactions: null}, {interactions: true}, {interactions: [{severity: "unknown"}]}] {
  const found = value.interactions ?? []
  const state = found.length > 0 ? "found" : "clear"
  try {
    const severityOrder = { high: 0, moderate: 1, low: 2 }
    const sorted = [...found].sort((first, second) => severityOrder[first.severity] - second.severity)
    console.log(JSON.stringify(value), "=>", state, "sorted:", JSON.stringify(sorted))
  } catch (e) {
    console.log(JSON.stringify(value), "=> crashed:", e.message)
  }
}
JS

Repository: Zoubeir23/DocFlowAI

Length of output: 17442


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== node behavior probe =="
node - <<'JS'
for (const value of [
  {},
  {interactions: undefined},
  {interactions: null},
  {interactions: true},
  {interactions: [{drug1Name: "A", drug2Name: "B", severity: "unknown", description: "x", source: "r"}]},
  {interactions: [{drug1Name: "A", drug2Name: "B", severity: "high", description: "x", source: "r"}]},
]) {
  const result = { interactions: value.interactions ?? [] }
  const foundInteractions = result.interactions
  const state = foundInteractions.length > 0 ? "found" : "clear"
  try {
    const sortedInteractions = [...foundInteractions].sort((first, second) => {
      const severityOrder = { high: 0, moderate: 1, low: 2 }
      return severityOrder[first.severity] - second.severity
    })
    const config = {
      high: { containerClass: "bg-red-50 border-red-200" },
      moderate: { containerClass: "bg-amber-50 border-amber-200" },
      low: { containerClass: "bg-blue-50 border-blue-200" },
    }
    const interaction = sortedInteractions[0]
    const actualConfig = config[interaction && interaction.severity] || {}
    console.log(JSON.stringify(value), "=> result.interactions =", JSON.stringify(result.interactions), "state =", state, "className =", actualConfig.containerClass || "undefined")
  } catch (error) {
    console.log(JSON.stringify(value), "=> crashed =", error.message)
  }
}
JS

echo "== read full route =="
sed -n '1,160p' app/api/who/drug-interactions/route.ts

Repository: Zoubeir23/DocFlowAI

Length of output: 2208


Validez la réponse /api/who/drug-interactions avant de mettre à jour l’état.

La validation TypeScript de result.interactions ne protège pas les réponses JSON corrompues. Une clé interactions manquante ou null est affichée comme interactionsClear; un tableau contenant une sévérité invalide peut faire échouer DrugInteractionWarning. Ajoutez un schéma Zod pour interactions et chaque DrugInteractionPair, puis passez l’état à failed en cas d’échec.

🤖 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 `@components/diagnostics/prescription-builder-step.tsx` around lines 180 - 183,
Validate the parsed response in the drug-interactions request before updating
state: define or reuse a Zod schema for the response and each
DrugInteractionPair, including the allowed interaction severity values, and
parse the JSON result. Only call setDrugInteractions and
setInteractionCheckState with validated data; on schema failure, set the
interaction check state to "failed" instead of treating missing, null, or
malformed interactions as clear.

Source: Coding guidelines

} catch {
// Silently ignore network errors — interactions are advisory only
} finally {
setInteractionLoading(false);
setDrugInteractions([]);
setInteractionCheckState("failed");
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}, []);

Expand Down Expand Up @@ -419,17 +440,62 @@ export function PrescriptionBuilderStep({
))}
</section>

{/* Drug-drug interactions */}
{(drugInteractions.length > 0 || interactionLoading) && (
{/* Contrôle des interactions : chaque issue est affichée, y compris l'échec */}
{(interactionCheckState !== "idle" || uncodedDrugCount > 0) && (
<section className="space-y-3 border-t border-border pt-6">
<div className="flex items-center gap-2">
<AlertTriangle className="w-4 h-4 text-amber-500" />
<h3 className="text-sm font-semibold uppercase tracking-wide text-amber-600">
Interactions médicamenteuses
{interactionLoading && <Loader2 className="inline ml-2 w-3 h-3 animate-spin" />}
<AlertTriangle
className={`w-4 h-4 ${interactionCheckState === "failed" ? "text-destructive" : "text-amber-500"}`}
/>
<h3
className={`text-sm font-semibold uppercase tracking-wide ${
interactionCheckState === "failed" ? "text-destructive" : "text-amber-600"
}`}
>
{t("prescriptionStep.interactionsTitle")}
{interactionCheckState === "checking" && (
<Loader2 className="inline ml-2 w-3 h-3 animate-spin" />
)}
</h3>
</div>
<DrugInteractionWarning interactions={drugInteractions} />

{interactionCheckState === "checking" && (
<p className="text-sm text-muted-foreground">
{t("prescriptionStep.interactionsChecking")}
</p>
)}

{interactionCheckState === "found" && (
<DrugInteractionWarning interactions={drugInteractions} />
)}

{interactionCheckState === "clear" && (
<p className="flex items-center gap-2 text-sm text-emerald-600">
<Check className="w-4 h-4" />
{t("prescriptionStep.interactionsClear")}
Comment on lines +466 to +494

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remplacez les couleurs Tailwind brutes par des tokens de thème.

text-amber-500, text-amber-600 et text-emerald-600 ne suivent pas les tokens définis dans app/globals.css. Ils peuvent aussi produire un contraste incohérent dans le thème sombre.

Utilisez des tokens sémantiques pour les états alerte et succès.

As per coding guidelines, « Utiliser uniquement les tokens de thème définis dans app/globals.css; ne pas coder de couleurs hexadécimales ou de couleurs sombres en dur ».

🤖 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 `@components/diagnostics/prescription-builder-step.tsx` around lines 447 - 475,
Replace the raw Tailwind color classes in the interaction status UI—specifically
the amber classes on the AlertTriangle and heading plus the emerald class in the
clear-state paragraph—with the semantic alert and success theme tokens defined
in the global theme configuration. Preserve the existing failed-state
destructive styling and all interactionCheckState behavior.

Source: Coding guidelines

</p>
)}

{interactionCheckState === "failed" && (
<div className="rounded-xl border border-destructive/30 bg-destructive/5 p-4 space-y-3">
<p className="text-sm text-destructive">
{t("prescriptionStep.interactionsFailed")}
</p>
<button
type="button"
onClick={() => checkInteractions(treatments)}
className="text-xs font-semibold uppercase tracking-wide text-destructive underline underline-offset-4"
>
{t("prescriptionStep.interactionsRetry")}
</button>
</div>
)}

{uncodedDrugCount > 0 && (
<p className="text-xs text-muted-foreground">
{t("prescriptionStep.interactionsUncoded", { count: uncodedDrugCount })}
</p>
)}
</section>
)}

Expand Down
8 changes: 7 additions & 1 deletion messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1161,7 +1161,13 @@
"back": "Back",
"generating": "Generating...",
"generateAndSave": "Generate and save prescription",
"allergyWarning": "Allergy detected — verify this medication"
"allergyWarning": "Allergy detected — verify this medication",
"interactionsTitle": "Drug interactions",
"interactionsChecking": "Checking…",
"interactionsClear": "No known interaction between the coded medications.",
"interactionsFailed": "Interaction check unavailable. Verify manually before prescribing.",
"interactionsRetry": "Run the check again",
"interactionsUncoded": "{count} medication(s) without an RxNorm code are not covered by the check."
},
"patientStep": {
"nextButton": "Next: Symptoms and vital signs"
Expand Down
8 changes: 7 additions & 1 deletion messages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -1161,7 +1161,13 @@
"back": "Retour",
"generating": "Génération...",
"generateAndSave": "Générer et enregistrer l'ordonnance",
"allergyWarning": "Allergie détectée — vérifiez ce médicament"
"allergyWarning": "Allergie détectée — vérifiez ce médicament",
"interactionsTitle": "Interactions médicamenteuses",
"interactionsChecking": "Contrôle en cours…",
"interactionsClear": "Aucune interaction connue entre les médicaments codés.",
"interactionsFailed": "Contrôle des interactions indisponible. Vérifiez manuellement avant de prescrire.",
"interactionsRetry": "Relancer le contrôle",
"interactionsUncoded": "{count} médicament(s) sans code RxNorm ne sont pas couverts par le contrôle."
},
"patientStep": {
"nextButton": "Suivant : Symptômes et signes vitaux"
Expand Down
2 changes: 2 additions & 0 deletions types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ export interface DiagnosticRecord {
validated_diagnosis_code: string | null;
validated_diagnosis_name: string | null;
validated_by: string | null;
/** Compte réellement à l'origine de la validation (migration 010). */
validated_by_user_id: string | null;
validated_at: string | null;
rejection_reason: string | null;

Expand Down
Loading