version WebApp Aidants & Seniors (Agent-3 / Next.js / TypeScript / Zod) pour l’auto-déclaration infirmier(ère)

➡️ la version prête à intégrer dans votre WebApp Aidants & Seniors (Agent-3 / Next.js / TypeScript / Zod) pour l’auto-déclaration infirmier(ère) et la génération du profil typé.

Voici LE PROMPT COMPLET, OPTIMISÉ, CLÉ-EN-MAIN POUR AGENT-3 DE REPLIT,

pour que Agent-3 génère toute la WebApp “Typage Infirmier(ère)”, mobile-first, avec :

  • 🟦 Next.js 14 (App Router)
  • 🟩 TypeScript
  • 🟧 Tailwind minimaliste
  • 🟪 Zod pour la validation
  • 🟥 Wizard multi-écrans (auto-déclaration infirmier)
  • 🟫 Chargement dynamique de la nomenclature JSON
  • 🟨 API route pour générer le profil typé
  • 🟩 Intégration du moteur buildNurseProfile()
  • 🟣 Préparation à l’intégration n8n → Odoo
  • 🇫🇷 Tous les textes affichés dans la WebApp doivent rester en français
  • 🔒 Aucun secret exposé côté client


 Voir le prompt

Un deuxième prompt complet pour Agent-3, dédié au sous-système de matching Seniors → Infirmiers

à partir des profils JSON typés.

Vous pourrez l’utiliser en complément du premier projet, ou comme projet séparé.


Découvrir plus

ANNEXE 


Explication des codes sous jacents 

Je vais vous donner quelque chose que vous pouvez copier-coller dans un projet Next.js 14 :

  1. Les types & schémas Zod (nomenclature + réponses infirmier)
  2. Une fonction buildNurseProfile() qui transforme les réponses → profil typé
  3. Un squelette de composant React “wizard” (multi-sections) pour l’auto-déclaration

1️⃣ Types & Zod : Nomenclature + réponses infirmier

Un schéma Zod est un modèle de validation de données utilisé en JavaScript et TypeScript pour vérifier, typer et sécuriser des données — notamment dans les API, les formulaires web, et les applications React/Next.js.


Vous avez déjà la nomenclature JSON (families, competences, questionnaire).

Dans le code, vous pouvez la mettre dans un fichier nomenclatureInfirmiers.json et charger ça côté front/server.

a) Types TypeScript

// types/nurseNomenclature.ts export type CompetenceCode = | "GEN_SOINS_TECHNIQUES" | "GEN_SURVEILLANCE_CLINIQUE" | "GEN_EDUCATION_THERAPEUTIQUE" | "GER_AUTONOMIE_DEPENDANCE" | "GER_TROUBLES_COGNITIFS" | "GER_SOUTIEN_PSYCHOSOCIAL" | "WOUND_ESCARRES" | "WOUND_PLAIES_CHRONIQUES" | "WOUND_STOMATHERAPIE" | "CHRONIC_DIABETE" | "CHRONIC_INSUFFISANCE_CARDIO_RESP" | "CHRONIC_ONCOLOGIE" | "IPA_PATHOLOGIES_CHRONIQUES" | "IPA_PSYCHIATRIE" | "IPA_ONCOLOGIE" | "IPA_INSUFFISANCE_RENALE" | "IPA_URGENCES" | "COORD_SORTIE_HOPITAL" | "COORD_HAD" | "COORD_CASE_MANAGER" | "COORD_AIDANTS" | "TELE_TELESOINS" | "TELE_TELESURVEILLANCE" | "TELE_OUTILS_NUMERIQUES"; export interface Competence { id: string; code: CompetenceCode; label_fr: string; description_fr: string; keywords_fr: string[]; } export interface Family { id: string; code: string; label_fr: string; description_fr: string; competences: Competence[]; } export type QuestionType = "multi_select" | "single_select"; export interface QuestionOption { id: string; // ex: "Q1_O1" label_fr: string; map_to_competences: CompetenceCode[]; } export interface Question { id: string; // ex: "Q1" type: QuestionType; text_fr: string; options: QuestionOption[]; } export interface QuestionnaireSection { id: string; label_fr: string; questions: Question[]; } export interface NurseNomenclature { version: string; context: string; families: Family[]; questionnaire: { language: string; sections: QuestionnaireSection[]; }; }

b) Schéma Zod pour les réponses infirmier

On va stocker les réponses sous la forme :

type NurseAnswers = Record<string, string[]>;

  • clé = id de question (Q1, Q2, Q5B…)
  • valeur = liste des id d’options cochées (Q1_O1, Q1_O4…)

// types/nurseAnswers.ts import { z } from "zod"; export const nurseAnswersSchema = z.record( z.string(), // questionId z.array(z.string()) // optionIds ); export type NurseAnswers = z.infer<typeof nurseAnswersSchema>;

On pourra ainsi valider côté backend ce qui arrive du front.

2️⃣ Fonction de mapping : réponses → profil typé infirmier

On veut transformer NurseAnswers en un profil comme celui que je vous avais montré :

// logic/buildNurseProfile.ts import { NurseAnswers } from "@/types/nurseAnswers"; import { NurseNomenclature, CompetenceCode, QuestionnaireSection, } from "@/types/nurseNomenclature"; export interface NurseProfile { id_infirmier: string; identite: { nom: string; prenom: string; mode_exercice: string[]; // ex: ["libéral", "domicile"] }; competences: Record<string, CompetenceCode[]>; expertises_principales: string[]; score_global_expertise: number; match_recommande_seniors: { autonomie: number; troubles_cognitifs: number; plaies: number; diabete: number; cardio_respiratoire: number; isolement_psychosocial: number; }; } function collectCompetenceCodesFromAnswers( nomenclature: NurseNomenclature, answers: NurseAnswers ): CompetenceCode[] { const codeSet = new Set<CompetenceCode>(); // on parcourt toutes les sections / questions / options for (const section of nomenclature.questionnaire.sections) { for (const q of section.questions) { const selectedOptionIds = answers[q.id] ?? []; for (const opt of q.options) { if (selectedOptionIds.includes(opt.id)) { opt.map_to_competences.forEach((c) => codeSet.add(c)); } } } } return Array.from(codeSet); } function groupCompetencesByFamily( families: NurseNomenclature["families"], codes: CompetenceCode[] ): Record<string, CompetenceCode[]> { const grouped: Record<string, CompetenceCode[]> = {}; for (const fam of families) { const famCodes = new Set( fam.competences.map((c) => c.code) ); const codesInFam = codes.filter((c) => famCodes.has(c)); if (codesInFam.length > 0) { grouped[fam.code] = codesInFam; } } return grouped; } // petit algo simple pour faire un score global function computeGlobalScore(codes: CompetenceCode[]): number { const max = 25; // ex: on considère 25 compétences possibles const ratio = Math.min(codes.length / max, 1); return Math.round(60 + 40 * ratio); // entre 60 et 100 } // mapping très simple → plus tard, vous pourrez raffiner function computeSeniorMatchScores(codes: CompetenceCode[]) { const has = (c: CompetenceCode) => codes.includes(c); return { autonomie: has("GER_AUTONOMIE_DEPENDANCE") ? 90 : 60, troubles_cognitifs: has("GER_TROUBLES_COGNITIFS") ? 90 : 50, plaies: (has("WOUND_ESCARRES") || has("WOUND_PLAIES_CHRONIQUES")) ? 95 : 40, diabete: has("CHRONIC_DIABETE") ? 90 : 40, cardio_respiratoire: has("CHRONIC_INSUFFISANCE_CARDIO_RESP") ? 85 : 50, isolement_psychosocial: has("GER_SOUTIEN_PSYCHOSOCIAL") ? 80 : 40, }; } // extrait quelques expertises “lisibles” function buildExpertisesPrincipales( families: NurseNomenclature["families"], codes: CompetenceCode[] ): string[] { const labels: string[] = []; const pushIf = (code: CompetenceCode, label: string) => { if (codes.includes(code)) labels.push(label); }; pushIf("WOUND_ESCARRES", "Plaies complexes et escarres"); pushIf("WOUND_PLAIES_CHRONIQUES", "Plaies chroniques et ulcères"); pushIf("GER_AUTONOMIE_DEPENDANCE", "Gérontologie – autonomie / dépendance"); pushIf("GER_TROUBLES_COGNITIFS", "Gérontologie – troubles cognitifs"); pushIf("CHRONIC_DIABETE", "Suivi des patients diabétiques"); pushIf("CHRONIC_INSUFFISANCE_CARDIO_RESP", "Suivi cardio-respiratoire"); pushIf("COORD_SORTIE_HOPITAL", "Coordination de sortie d’hôpital"); pushIf("TELE_TELESOINS", "Télésoins et suivi à distance"); // on limite à 4 pour rester lisible return Array.from(new Set(labels)).slice(0, 4); } export function buildNurseProfile( params: { id_infirmier: string; nom: string; prenom: string; mode_exercice: string[]; nomenclature: NurseNomenclature; answers: NurseAnswers; } ): NurseProfile { const { id_infirmier, nom, prenom, mode_exercice, nomenclature, answers } = params; const codes = collectCompetenceCodesFromAnswers(nomenclature, answers); const competencesGrouped = groupCompetencesByFamily( nomenclature.families, codes ); const score_global = computeGlobalScore(codes); const match = computeSeniorMatchScores(codes); const expertises = buildExpertisesPrincipales( nomenclature.families, codes ); return { id_infirmier, identite: { nom, prenom, mode_exercice, }, competences: competencesGrouped, expertises_principales: expertises, score_global_expertise: score_global, match_recommande_seniors: match, }; }

👉 Cette fonction est server-side (ex. dans un route.ts API Next.js) :

  • input : answers validés par Zod + info d’identité
  • output : NurseProfile prêt à être enregistré (PostgreSQL, Supabase, Odoo, n8n, etc.)

3️⃣ Squelette du wizard React (auto-déclaration infirmier)

a) Hook pour gérer les réponses

// hooks/useNurseQuestionnaire.ts import { useState } from "react"; import { NurseAnswers } from "@/types/nurseAnswers"; import { NurseNomenclature } from "@/types/nurseNomenclature"; export function useNurseQuestionnaire(nomenclature: NurseNomenclature) { const [currentSectionIndex, setCurrentSectionIndex] = useState(0); const [answers, setAnswers] = useState<NurseAnswers>({}); const sections = nomenclature.questionnaire.sections; const currentSection = sections[currentSectionIndex]; function toggleOption(questionId: string, optionId: string) { setAnswers((prev) => { const prevValues = prev[questionId] ?? []; const exists = prevValues.includes(optionId); const nextValues = exists ? prevValues.filter((id) => id !== optionId) : [...prevValues, optionId]; return { ...prev, [questionId]: nextValues, }; }); } function setSingle(questionId: string, optionId: string) { setAnswers((prev) => ({ ...prev, [questionId]: [optionId], })); } function nextSection() { setCurrentSectionIndex((i) => Math.min(i + 1, sections.length - 1)); } function prevSection() { setCurrentSectionIndex((i) => Math.max(i - 1, 0)); } const isLast = currentSectionIndex === sections.length - 1; return { currentSection, currentSectionIndex, totalSections: sections.length, answers, toggleOption, setSingle, nextSection, prevSection, isLast, }; }

b) Composant React : NurseOnboardingWizard

(Adaptable pour Tailwind / shadcn)

// app/infirmiers/onboarding/NurseOnboardingWizard.tsx "use client"; import React, { useState } from "react"; import { NurseNomenclature, Question } from "@/types/nurseNomenclature"; import { useNurseQuestionnaire } from "@/hooks/useNurseQuestionnaire"; import { nurseAnswersSchema } from "@/types/nurseAnswers"; interface Props { nomenclature: NurseNomenclature; onSubmit: (payload: { nom: string; prenom: string; mode_exercice: string[]; answers: any; }) => Promise<void>; } export const NurseOnboardingWizard: React.FC<Props> = ({ nomenclature, onSubmit, }) => { const { currentSection, currentSectionIndex, totalSections, answers, toggleOption, setSingle, nextSection, prevSection, isLast, } = useNurseQuestionnaire(nomenclature); const [nom, setNom] = useState(""); const [prenom, setPrenom] = useState(""); const [modeExercice, setModeExercice] = useState<string[]>([]); const [submitting, setSubmitting] = useState(false); const [errorMsg, setErrorMsg] = useState<string | null>(null); const handleToggleModeExercice = (value: string) => { setModeExercice((prev) => prev.includes(value) ? prev.filter((v) => v !== value) : [...prev, value] ); }; async function handleNext() { if (!isLast) { nextSection(); return; } // dernière étape → on valide et on envoie try { setErrorMsg(null); const parsedAnswers = nurseAnswersSchema.parse(answers); setSubmitting(true); await onSubmit({ nom, prenom, mode_exercice: modeExercice, answers: parsedAnswers, }); } catch (e: any) { console.error(e); setErrorMsg("Erreur de validation des réponses."); } finally { setSubmitting(false); } } const stepLabel = `Étape ${currentSectionIndex + 1} / ${totalSections}`; return ( <div className="max-w-xl mx-auto p-4 space-y-4 border rounded-2xl shadow"> {/* En-tête identité */} <div className="space-y-2"> <h1 className="text-xl font-semibold"> Profil de compétences – Infirmier(ère) </h1> <p className="text-sm text-gray-600"> Merci de renseigner vos compétences. Cela nous permet de mieux vous connecter aux patients et aux aidants. </p> <div className="grid grid-cols-2 gap-2 mt-2"> <input className="border rounded px-2 py-1 text-sm" placeholder="Nom" value={nom} onChange={(e) => setNom(e.target.value)} /> <input className="border rounded px-2 py-1 text-sm" placeholder="Prénom" value={prenom} onChange={(e) => setPrenom(e.target.value)} /> </div> <div className="mt-2"> <p className="text-sm font-medium mb-1"> Mode d'exercice principal </p> <div className="flex flex-wrap gap-2"> {["libéral", "domicile", "HAD", "hôpital", "EHPAD"].map((m) => ( <button key={m} type="button" className={`px-2 py-1 text-xs rounded-full border ${ modeExercice.includes(m) ? "bg-blue-600 text-white" : "bg-white text-gray-700" }`} onClick={() => handleToggleModeExercice(m)} > {m} </button> ))} </div> </div> </div> {/* Section / questions */} <div className="space-y-3"> <div className="flex justify-between items-center"> <h2 className="text-lg font-semibold"> {currentSection.label_fr} </h2> <span className="text-xs text-gray-500">{stepLabel}</span> </div> {currentSection.questions.map((q) => ( <QuestionBlock key={q.id} question={q} selected={answers[q.id] ?? []} onToggle={(optId) => q.type === "multi_select" ? toggleOption(q.id, optId) : setSingle(q.id, optId) } /> ))} </div> {/* Erreurs */} {errorMsg && ( <p className="text-sm text-red-600">{errorMsg}</p> )} {/* Navigation */} <div className="flex justify-between items-center pt-2 border-t mt-2"> <button type="button" className="text-sm px-3 py-1 rounded border" onClick={prevSection} disabled={currentSectionIndex === 0} > ← Précédent </button> <button type="button" className="text-sm px-3 py-1 rounded bg-blue-600 text-white disabled:opacity-60" onClick={handleNext} disabled={submitting} > {isLast ? "Valider mon profil" : "Continuer →"} </button> </div> </div> ); }; interface QuestionBlockProps { question: Question; selected: string[]; onToggle: (optionId: string) => void; } const QuestionBlock: React.FC<QuestionBlockProps> = ({ question, selected, onToggle, }) => { const isMulti = question.type === "multi_select"; return ( <div className="border rounded-lg p-3 space-y-2"> <p className="text-sm font-medium">{question.text_fr}</p> <div className="flex flex-col gap-1"> {question.options.map((opt) => { const isActive = selected.includes(opt.id); return ( <button key={opt.id} type="button" className={`flex items-center justify-between px-2 py-1 text-xs border rounded ${ isActive ? "bg-blue-50 border-blue-500" : "bg-white border-gray-300" }`} onClick={() => onToggle(opt.id)} > <span>{opt.label_fr}</span> <span className="text-[10px] text-gray-500"> {isMulti ? (isActive ? "✓" : "+") : isActive ? "●" : "○"} </span> </button> ); })} </div> </div> ); };

c) Exemple d’appel côté API

Dans app/api/infirmiers/profil/route.ts :

// app/api/infirmiers/profil/route.ts import { NextRequest, NextResponse } from "next/server"; import { nurseAnswersSchema } from "@/types/nurseAnswers"; import nomenclature from "@/data/nomenclatureInfirmiers.json"; import { buildNurseProfile } from "@/logic/buildNurseProfile"; export async function POST(req: NextRequest) { const body = await req.json(); const { nom, prenom, mode_exercice, answers } = body; const parsedAnswers = nurseAnswersSchema.parse(answers); const profile = buildNurseProfile({ id_infirmier: `INF-${Date.now()}`, nom, prenom, mode_exercice, nomenclature, answers: parsedAnswers, }); // ici : enregistrement BDD / envoi n8n / Odoo, etc. // await saveProfile(profile); return NextResponse.json({ ok: true, profile }); }

🎯 Avec ça, vous avez :

  • ✅ La nomenclature codée (compétences & questionnaire)
  • ✅ Le moteur de typage buildNurseProfile()
  • ✅ Un wizard d’auto-déclaration infirmier prêt à brancher dans votre WebApp Aidants & Seniors

LE PROMPT COMPLET, OPTIMISÉ, CLÉ-EN-MAIN POUR AGENT-3 DE REPLIT,

pour que Agent-3 génère toute la WebApp “Typage Infirmier(ère)”, mobile-first, avec :

  • 🟦 Next.js 14 (App Router)
  • 🟩 TypeScript
  • 🟧 Tailwind minimaliste
  • 🟪 Zod pour la validation
  • 🟥 Wizard multi-écrans (auto-déclaration infirmier)
  • 🟫 Chargement dynamique de la nomenclature JSON
  • 🟨 API route pour générer le profil typé
  • 🟩 Intégration du moteur buildNurseProfile()
  • 🟣 Préparation à l’intégration n8n → Odoo
  • 🇫🇷 Tous les textes affichés dans la WebApp doivent rester en français
  • 🔒 Aucun secret exposé côté client


Découvrir plus