Skip to content

feat: système de diagnostic médical complet — ICD-11 + ordonnance PDF - #2

Merged
Zoubeir23 merged 8 commits into
mainfrom
Feature/diagnostic-medical-patient
May 25, 2026
Merged

Zoubeir23 merged 8 commits into
mainfrom
Feature/diagnostic-medical-patient

Conversation

@Zoubeir23

@Zoubeir23 Zoubeir23 commented May 25, 2026

Copy link
Copy Markdown
Owner

Résumé

Système de diagnostic médical complet intégré à l'app, couvrant le flux entier :

Patient → Symptômes → Analyse ICD-11 (API OMS) → Validation médecin → Ordonnance PDF

Fichiers ajoutés (22 fichiers, ~3600 lignes)

Fichier Rôle
lib/who-icd.ts Client OAuth2 WHO ICD-11, cache token auto-refresh
lib/diagnostic-scoring.ts Scoring probabiliste des diagnostics + détection examens
actions/diagnostics.ts CRUD Supabase par étape, vérification rôle médecin
app/api/icd/search Proxy sécurisé recherche ICD-11 (auth requise)
app/api/icd/entity/[id] Proxy sécurisé détail entité ICD-11
components/diagnostics/patient-profile-step.tsx Étape 1 : démographie, antécédents, allergies, groupe sanguin
components/diagnostics/symptoms-vitals-step.tsx Étape 2 : symptômes, signes vitaux avec alertes hors norme
components/diagnostics/icd-analysis-step.tsx Étape 3 : analyse ICD-11 + classement réorganisable
components/diagnostics/doctor-validation-step.tsx Étape 4 : validation médecin obligatoire
components/diagnostics/prescription-builder-step.tsx Étape 5 : ordonnance, médicaments, suivi
components/diagnostics/prescription-pdf-document.tsx Document PDF React-PDF (A4, structuré)
components/diagnostics/pdf-download-button.tsx Bouton téléchargement PDF lazy-loaded
app/(app)/app/diagnostics/page.tsx Liste des dossiers avec filtres et search serveur
app/(app)/app/diagnostics/new/page.tsx Wizard 5 étapes
app/(app)/app/diagnostics/[id]/page.tsx Vue détail + téléchargement PDF
supabase/migrations/017_diagnostics.sql Table diagnostics (vitaux, JSONB, RLS)

Sécurité (post code review)

  • Routes /api/icd/* protégées par session Supabase (401 si non connecté)
  • validateDiagnostic bloqué aux non-médecins (rôle owner/doctor requis)
  • Path traversal bloqué sur /api/icd/entity/[id] (regex allowlist)
  • Paramètre limit validé et borné à 50
  • Validation manuelle des traitements (hors scope React Hook Form)
  • Confirmation dialog avant suppression d'un dossier médical
  • UUID tronqué (8 chars) sur documents imprimables
  • Search patient côté serveur via ilike Supabase

PDF

  • Bibliothèque : @react-pdf/renderer (remplace window.print())
  • Lazy-load côté client pour ne pas alourdir le bundle initial
  • Téléchargement direct : ordonnance-{patient}-{ref}.pdf
  • Rendu identique sur tous les navigateurs (A4)

Plan de test

  • Appliquer la migration 017_diagnostics.sql dans Supabase
  • Ajouter WHO_ICD_CLIENT_ID et WHO_ICD_CLIENT_SECRET dans les variables d'env
  • Parcourir le wizard complet : /app/diagnostics/new
  • Vérifier que la recherche ICD-11 retourne des résultats en français
  • Tester la validation médecin avec un compte non-médecin (doit retourner une erreur)
  • Tester la détection d'allergie sur la prescription
  • Télécharger le PDF depuis /app/diagnostics/[id]
  • Vérifier le dialog de confirmation avant suppression
  • Tester le filtrage et la search dans la liste

Summary by CodeRabbit

Notes de Version

  • Nouvelles Fonctionnalités

    • Système complet de gestion des diagnostics avec workflow multi-étapes : saisie du profil patient, enregistrement des symptômes, analyse ICD-11, validation médicale et génération d'ordonnances imprimables.
    • Page de diagnostics avec recherche, filtrage par statut et pagination.
    • Recherche intelligente des codes diagnostiques avec scoring automatique.
    • Gestion des rôles utilisateur basée sur les permissions d'accès.
  • Améliorations

    • Mise à jour du contrôle d'accès et de la navigation selon les rôles.
    • Optimisations SEO avec sitemap dynamique et métadonnées enrichies.

Review Change Stack

Zoubeir23 added 8 commits May 15, 2026 14:34
isOwner was derived from members list looking for role='owner' only,
causing super_admin users to see no invite button despite having a
paid plan. Now fetches current user role via getMyRole() and checks
for owner OR super_admin.
- Root layout: Open Graph, Twitter Card, robots avancés, alternates, canonical
- Metadata passés en français (marché cible)
- sitemap.ts: pages statiques + cliniques dynamiques depuis Supabase
- robots.ts: crawl autorisé sur pages publiques, bloqué sur /app/ /api/ /admin/
- page.tsx: JSON-LD SoftwareApplication + export metadata
- pricing/page.tsx: metadata complète + JSON-LD ItemList des plans
- clinique/[slug]/page.tsx: generateMetadata enrichi avec canonical, Twitter, OG locale fr_FR
updateClinicPlan ne touchait pas les dates de période, laissant
la subscription avec l'expiration du trial original (14j).
Désormais le changement de plan repart toujours sur 30j depuis maintenant.
- lib/rbac.ts: source unique de vérité des permissions et routes protégées
- middleware: bloque les routes owner-only pour receptionist/assistant
- sidebar: items filtrés dynamiquement selon le rôle de l'utilisateur
- use-role: hook client pour récupérer le rôle depuis Supabase
- super_admin hérite de tous les droits owner + accès /admin
- Le plus populaire → bandeau pleine largeur en haut de la carte (gradient primary)
- Plan actuel → badge pill emerald distinct (couleur différente du popular)
- Ring emerald sur la carte active au lieu de ring primary
- Hiérarchie visuelle claire entre les deux états
[CRITICAL-1] getInvitationByToken — suppression de l'email du retour
  Exposait l'email de l'invité (PII) à tout appelant anonyme connaissant
  le token. Retourne désormais uniquement role, clinic_name, expires_at.
  La vérification email reste côté serveur dans acceptInvitation.
  Page /join/[token] mise à jour : champ email saisi par l'utilisateur.

[CRITICAL-2] Webhook crypto — clinicId dérivé de la session serveur
  Le clinicId venait du body client, permettant d'activer un abonnement
  sur n'importe quelle clinique. Retiré du body, dérivé de la session
  Supabase authentifiée côté serveur uniquement.

[CRITICAL-3] HTML injection dans emails admin via message.subject
  subject/replyText interpolés dans le HTML sans escaping complet.
  Ajout de escapeHtml() — & < > " ' — appliqué
  sur tous les champs externes avant insertion dans le template email.
Flow 5 étapes :
1. Profil patient (démographie, antécédents, allergies, groupe sanguin)
2. Symptômes + signes vitaux (température, TA, FC, SpO2) avec alertes
3. Analyse ICD-11 (API OMS + scoring probabiliste + examens recommandés)
4. Validation médecin obligatoire avec vérification de rôle (owner/doctor)
5. Générateur ordonnance (médicaments, posologie, voie, suivi, signature)

Sécurité :
- Routes /api/icd/* protégées par auth Supabase
- validateDiagnostic interdit aux non-médecins
- Validation des traitements hors scope RHF
- Confirmation dialog avant suppression dossier
- UUID tronqué sur documents imprimables
- Path traversal bloqué sur /api/icd/entity/[id]
- Search patient déplacé côté serveur (ilike Supabase)

Nouveau :
- lib/diagnostic-scoring.ts : algorithme scoring + détection examens
- lib/who-icd.ts : client OAuth2 WHO ICD-11 avec cache token
- Migration 017 : table diagnostics (vitaux, JSONB, RLS)
- Document imprimable avec window.print()
- Sidebar : lien /app/diagnostics
Copilot AI review requested due to automatic review settings May 25, 2026 01:09
@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown
ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 1a9f41d6-c52d-4025-a209-1f0dbd27937c

📥 Commits

Reviewing files that changed from the base of the PR and between e73a9a4 and 1a2e7f8.

📒 Files selected for processing (36)
  • actions/diagnostics.ts
  • actions/super-admin.ts
  • actions/team.ts
  • app/(app)/app/billing/page.tsx
  • app/(app)/app/diagnostics/[id]/page.tsx
  • app/(app)/app/diagnostics/new/page.tsx
  • app/(app)/app/diagnostics/page.tsx
  • app/(app)/app/team/page.tsx
  • app/(app)/layout.tsx
  • app/api/icd/entity/[id]/route.ts
  • app/api/icd/search/route.ts
  • app/api/webhooks/crypto/route.ts
  • app/clinique/[slug]/page.tsx
  • app/join/[token]/page.tsx
  • app/layout.tsx
  • app/page.tsx
  • app/pricing/page.tsx
  • app/robots.ts
  • app/sitemap.ts
  • components/diagnostics/doctor-validation-step.tsx
  • components/diagnostics/icd-analysis-step.tsx
  • components/diagnostics/icd-search-field.tsx
  • components/diagnostics/patient-profile-step.tsx
  • components/diagnostics/prescription-builder-step.tsx
  • components/diagnostics/prescription-print-document.tsx
  • components/diagnostics/symptoms-vitals-step.tsx
  • components/layout/sidebar.tsx
  • lib/diagnostic-scoring.ts
  • lib/hooks/use-role.ts
  • lib/rbac.ts
  • lib/supabase/middleware.ts
  • lib/who-icd.ts
  • messages/en.json
  • messages/fr.json
  • supabase/migrations/017_diagnostics.sql
  • types/index.ts

📝 Walkthrough

Walkthrough

Ce PR intègre un système complet de gestion de diagnostics médicaux dans DocFlow IA. Les modifications incluent un cycle de vie multi-étapes de diagnostic (brouillon → symptômes → analyse ICD-11 → validation médecin → ordonnance), une intégration avec l'API WHO ICD-11, un système de contrôle d'accès basé sur les rôles, et des mises à jour SEO globales. Le code étend la base de données Supabase, introduit 8+ nouveaux composants React, et déploie des routes API et des pages côté serveur pour orchestrer le workflow.

Changes

Diagnostic Medical System

Layer / File(s) Summary
Type définitions et schéma de base de données
types/index.ts, supabase/migrations/017_diagnostics.sql
Énumérations et interfaces de diagnostic (PatientAgeGroup, PatientSex, BloodGroup, DiagnosticValidationStatus), structures de candidats ICD et traitement, ainsi que table SQL diagnostics avec RLS, index et triggers pour mise à jour automatique.
Intégration API ICD-11 et scoring diagnostique
lib/who-icd.ts, lib/diagnostic-scoring.ts, app/api/icd/search/route.ts, app/api/icd/entity/[id]/route.ts
Client WHO ICD-11 avec authentification OAuth2 et mise en cache de token, fonctions de recherche/récupération d'entités, système de scoring combinant âge, vitaux, sévérité et allergies, et deux endpoints API pour la recherche et récupération d'identifiants ICD.
Actions serveur pour gestion du diagnostic
actions/diagnostics.ts
Cycle de vie complet : createDiagnosticDraft, updateDiagnosticSymptoms, updateDiagnosticAnalysis, validateDiagnostic (validation par rôle), updateDiagnosticPrescription, avec lectures paginées et suppressions filtrées par clinic.
Pages diagnostiques et orchestration workflow
app/(app)/app/diagnostics/new/page.tsx, app/(app)/app/diagnostics/page.tsx, app/(app)/app/diagnostics/[id]/page.tsx
Page de création multi-étapes (NewDiagnosticPage) orchestrant 5 étapes avec gestion d'état et handlers async, page de liste paginée avec filtres/recherche/suppression, et page de détail avec impression via PrescriptionPrintDocument.
Composants d'étapes et formulaires
components/diagnostics/*
Cinq composants : PatientProfileStep (identité, antécédents), SymptomsVitalsStep (motif, symptômes, vitaux), IcdAnalysisStep (recherche auto/manuelle, réordonnancement), DoctorValidationStep (sélection, motif rejet), PrescriptionBuilderStep (traitements, recommandations, suivi) ; plus IcdSearchField, PrescriptionPrintDocument pour interface utilisateur complète.
Système de contrôle d'accès basé sur les rôles
lib/rbac.ts, lib/hooks/use-role.ts, components/layout/sidebar.tsx, lib/supabase/middleware.ts
Définition UserRole, liste OWNER_ONLY_ROUTES, utilitaires (isOwnerOrAbove, canAccessRoute), hook useRole avec booléens calculés, filtrage sidebar par rôle, et middleware de vérification d'accès pour la zone /app.
Actions d'équipe et mises à jour d'authentification
actions/team.ts, app/join/[token]/page.tsx, app/(app)/layout.tsx
Ajout getMyRole(), suppression email dans getInvitationByToken, ajout champ email partagé dans join page, redirection super_admin → /admin, et fallback clinicName dans sidebar.
Mises à jour de facturation et webhooks
actions/super-admin.ts, app/(app)/app/billing/page.tsx, app/api/webhooks/crypto/route.ts
Ajout escapeHtml pour sécurisation HTML, updateClinicPlan avec fenêtres de période 30j, dérivation côté serveur de clinicId dans webhook crypto au lieu du client, et changement UI badge/bannière "most popular" dans billing.
Configuration SEO, robots, sitemap et traductions
app/layout.tsx, app/page.tsx, app/pricing/page.tsx, app/clinique/[slug]/page.tsx, app/robots.ts, app/sitemap.ts, messages/en.json, messages/fr.json
Métadonnées structurées avec URLs canoniques via NEXT_PUBLIC_APP_URL, JSON-LD pour organisation/pricing, fichiers robots.ts et sitemap.ts dynamiques avec cliniques, traductions étendues avec sections diagnostics, stats, earlyAdopters et rights.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 Voici un système de diagnostic magnifique,
Cinq étapes pour soigner, c'est fantastique !
Des API ICD, du scoring précis,
Des ordonnances générées à merveille, c'est du génie !
Les rôles contrôlent l'accès, tout est en harmonie,
DocFlow IA brille avec ses nouveautés infinies ! 🌟


Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Pro by visiting https://app.coderabbit.ai/login.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Cette PR introduit un nouveau module “diagnostics médicaux” (wizard multi-étapes) avec analyse ICD‑11 via proxy API, persistance Supabase (migration + actions serveur) et intégration UI (sidebar + pages). Elle ajoute aussi des évolutions d’accès (RBAC), et des ajustements SEO (metadata, robots, sitemap).

Changes:

  • Ajout du flux complet de diagnostic (profil patient → symptômes/vitaux → analyse ICD‑11 → validation médecin → prescription) avec stockage Supabase.
  • Ajout de proxies API sécurisés vers l’API WHO ICD‑11 + logique de scoring/détection d’examens complémentaires.
  • Renforcement RBAC dans le middleware + mises à jour SEO (metadata, robots, sitemap) et divers correctifs sécurité (invitations, crypto verify).

Reviewed changes

Copilot reviewed 36 out of 36 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
types/index.ts Ajoute les types du système de diagnostic (dossier, candidats ICD, prescription, etc.).
supabase/migrations/017_diagnostics.sql Crée la table diagnostics, RLS, indexes et trigger updated_at.
messages/fr.json Ajoute/ajuste des libellés navigation + section i18n diagnostics + contenu landing.
messages/en.json Ajoute/ajuste des libellés navigation + section i18n diagnostics + contenu landing.
lib/who-icd.ts Client OAuth2 WHO ICD‑11 + endpoints de recherche/lecture entité.
lib/supabase/middleware.ts Ajoute un contrôle RBAC (role) en plus de is_active.
lib/rbac.ts Introduit la logique RBAC (owner-only routes + helpers).
lib/hooks/use-role.ts Hook client pour récupérer le rôle utilisateur depuis Supabase.
lib/diagnostic-scoring.ts Scoring/ranking des diagnostics + détection d’examens + check allergies.
components/layout/sidebar.tsx Ajoute l’entrée Diagnostics/Waitlist + filtrage des items par rôle.
components/diagnostics/patient-profile-step.tsx Étape 1 du wizard (démographie + antécédents/allergies).
components/diagnostics/symptoms-vitals-step.tsx Étape 2 du wizard (symptômes + vitaux + alertes).
components/diagnostics/icd-search-field.tsx Champ de recherche ICD‑11 avec debounce et sélection multi-codes.
components/diagnostics/icd-analysis-step.tsx Étape 3 (appel proxy ICD search + scoring + réordonnancement).
components/diagnostics/doctor-validation-step.tsx Étape 4 (sélection diagnostic retenu + validation/rejet).
components/diagnostics/prescription-builder-step.tsx Étape 5 (construction ordonnance, contrôles allergies, suivi).
components/diagnostics/prescription-print-document.tsx Rendu “imprimable” du document médical associé au diagnostic.
app/sitemap.ts Génère un sitemap (pages statiques + cliniques si accessibles).
app/robots.ts Déclare les règles robots + référence sitemap.
app/pricing/page.tsx Ajoute metadata SEO + JSON‑LD pour la page pricing.
app/page.tsx Ajoute metadata SEO + JSON‑LD “SoftwareApplication” sur la home.
app/layout.tsx Refonte metadata globale (OG/Twitter/robots/keywords/canonical).
app/join/[token]/page.tsx Change le join flow: email saisi côté client (plus exposé par invitation).
app/clinique/[slug]/page.tsx Améliore metadata/canonical/OG/Twitter pour pages cliniques publiques.
app/api/webhooks/crypto/route.ts Durcit la vérif crypto: derive clinicId depuis session, prix côté serveur.
app/api/icd/search/route.ts Proxy authentifié pour recherche ICD‑11 (validation query/limit).
app/api/icd/entity/[id]/route.ts Proxy authentifié pour détail ICD‑11 (validation d’ID).
app/(app)/layout.tsx Ajuste redirect super_admin sans clinique vers /admin + fallback nom sidebar.
app/(app)/app/team/page.tsx Récupère le rôle courant via action dédiée au lieu d’inférer depuis la liste.
app/(app)/app/diagnostics/page.tsx Liste des diagnostics (filtres, search, pagination, suppression).
app/(app)/app/diagnostics/new/page.tsx Wizard de création diagnostic (5 étapes + appels actions).
app/(app)/app/diagnostics/[id]/page.tsx Détail diagnostic + rendu document (print).
app/(app)/app/billing/page.tsx Ajuste payload crypto verify (clinicId/amount dérivés côté serveur) + UI pricing.
actions/team.ts Ajoute getMyRole() + retire l’email du retour getInvitationByToken().
actions/super-admin.ts Ajoute escapeHtml() + complète la mise à jour/insert de périodes abonnement.
actions/diagnostics.ts Implémente les actions serveur CRUD/étapes diagnostics + queries liste/détail.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +13 to +16
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { PaginationBar } from "@/components/ui/pagination-bar";
import { Skeleton } from "@/components/ui/skeleton";
Comment thread actions/diagnostics.ts
Comment on lines +177 to +199
export async function updateDiagnosticPrescription(
diagnosticId: string,
prescription: PrescriptionInput
): Promise<ActionResult> {
const supabase = await createClient();
const clinicId = await resolveClinicId();
if (!clinicId) return { success: false, error: "Non autorisé" };

const { error } = await (supabase as any)
.from("diagnostics")
.update({
document_type: prescription.document_type,
treatments: prescription.treatments,
recommendations: prescription.recommendations,
follow_up_delay_days: prescription.follow_up_delay_days,
follow_up_tests: prescription.follow_up_tests,
practitioner_name: prescription.practitioner_name,
practitioner_title: prescription.practitioner_title,
practitioner_rpps: prescription.practitioner_rpps,
})
.eq("id", diagnosticId)
.eq("clinic_id", clinicId);

Comment thread actions/diagnostics.ts
Comment on lines +153 to +155
const ALLOWED_VALIDATION_ROLES = ["owner", "doctor", "admin"];
if (!ALLOWED_VALIDATION_ROLES.includes(role)) {
return { success: false, error: "Seul un médecin peut valider un diagnostic" };
Comment on lines +5 to +22
const VALID_ICD_ID_PATTERN = /^[a-zA-Z0-9\-_.]+$/;

export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
}

const { id } = await params;
const language = request.nextUrl.searchParams.get("lang") ?? "fr";

if (!id || !VALID_ICD_ID_PATTERN.test(id)) {
return NextResponse.json({ error: "ID invalide" }, { status: 400 });
}
Comment on lines +14 to +26
const symptomsSchema = z.object({
chief_complaint: z.string().min(5, "Motif de consultation requis"),
symptoms: z.array(z.string()),
symptom_duration: z.string(),
symptom_intensity: z.coerce.number().min(1).max(10),
aggravating_factors: z.array(z.string()),
relieving_factors: z.array(z.string()),
vital_temperature: z.coerce.number().nullable(),
vital_blood_pressure_systolic: z.coerce.number().nullable(),
vital_blood_pressure_diastolic: z.coerce.number().nullable(),
vital_heart_rate: z.coerce.number().nullable(),
vital_respiratory_rate: z.coerce.number().nullable(),
vital_oxygen_saturation: z.coerce.number().nullable(),
const prescriptionSchema = z.object({
document_type: z.enum(["consultation", "prescription", "receipt", "medical_report", "sick_leave"]),
recommendations: z.array(z.string()),
follow_up_delay_days: z.coerce.number().nullable(),
Comment on lines +72 to +77
ALTER TABLE diagnostics ENABLE ROW LEVEL SECURITY;

CREATE POLICY "clinic_members_can_manage_diagnostics"
ON diagnostics FOR ALL
USING (clinic_id IN (SELECT clinic_id FROM users WHERE id = auth.uid()));

Comment on lines +51 to +55
<div className="flex gap-2 justify-end print:hidden">
<Button onClick={() => window.print()} variant="outline" className="rounded-xl gap-2">
<Printer className="w-4 h-4" /> Imprimer
</Button>
</div>
Comment on lines +67 to +84
// Derive clinicId from the authenticated session — never trust the client
const userClient = await createClient();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const userDb = userClient as any;
const { data: { user } } = await userDb.auth.getUser();
if (!user) {
return NextResponse.json({ error: "Non authentifié" }, { status: 401 });
}
const { data: userData } = await userDb
.from("users")
.select("clinic_id")
.eq("id", user.id)
.single();
if (!userData?.clinic_id) {
return NextResponse.json({ error: "Clinique introuvable" }, { status: 403 });
}
const clinicId: string = userData.clinic_id;

Comment thread app/sitemap.ts
Comment on lines +34 to +49
try {
const supabase = await createClient();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const db = supabase as any;
const { data: clinics } = await db
.from("clinics")
.select("slug, updated_at")
.not("slug", "is", null);

const clinicPages: MetadataRoute.Sitemap = (clinics ?? []).map(
(clinic: { slug: string; updated_at: string }) => ({
url: `${siteUrl}/clinique/${clinic.slug}`,
lastModified: new Date(clinic.updated_at),
changeFrequency: "weekly" as const,
priority: 0.7,
})
@Zoubeir23
Zoubeir23 merged commit f70461a into main May 25, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants