feat: système de diagnostic médical complet — ICD-11 + ordonnance PDF - #2
Conversation
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
ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Free Run ID: 📒 Files selected for processing (36)
📝 WalkthroughWalkthroughCe 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. ChangesDiagnostic Medical System
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
Note 🎁 Summarized by CodeRabbit FreeYour 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 |
There was a problem hiding this comment.
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.
| 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"; |
| 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); | ||
|
|
| 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" }; |
| 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 }); | ||
| } |
| 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(), |
| 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())); | ||
|
|
| <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> |
| // 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; | ||
|
|
| 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, | ||
| }) |
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)
lib/who-icd.tslib/diagnostic-scoring.tsactions/diagnostics.tsapp/api/icd/searchapp/api/icd/entity/[id]components/diagnostics/patient-profile-step.tsxcomponents/diagnostics/symptoms-vitals-step.tsxcomponents/diagnostics/icd-analysis-step.tsxcomponents/diagnostics/doctor-validation-step.tsxcomponents/diagnostics/prescription-builder-step.tsxcomponents/diagnostics/prescription-pdf-document.tsxcomponents/diagnostics/pdf-download-button.tsxapp/(app)/app/diagnostics/page.tsxapp/(app)/app/diagnostics/new/page.tsxapp/(app)/app/diagnostics/[id]/page.tsxsupabase/migrations/017_diagnostics.sqlSécurité (post code review)
/api/icd/*protégées par session Supabase (401 si non connecté)validateDiagnosticbloqué aux non-médecins (rôleowner/doctorrequis)/api/icd/entity/[id](regex allowlist)limitvalidé et borné à 50ilikeSupabasePDF
@react-pdf/renderer(remplacewindow.print())ordonnance-{patient}-{ref}.pdfPlan de test
017_diagnostics.sqldans SupabaseWHO_ICD_CLIENT_IDetWHO_ICD_CLIENT_SECRETdans les variables d'env/app/diagnostics/new/app/diagnostics/[id]Summary by CodeRabbit
Notes de Version
Nouvelles Fonctionnalités
Améliorations