diff --git a/apps/mobile/App.tsx b/apps/mobile/App.tsx index 87b1017f..f6e14c1e 100644 --- a/apps/mobile/App.tsx +++ b/apps/mobile/App.tsx @@ -64,19 +64,12 @@ import { CompanionCollectionScreen } from "./src/screens/CompanionCollectionScre import { MedicationsScreen } from "./src/screens/MedicationsScreen"; import { CalmMinutesScreen } from "./src/screens/CalmMinutesScreen"; import { InsightsScreen } from "./src/screens/InsightsScreen"; -import { ActivityScreen } from "./src/screens/ActivityScreen"; import { ReportScreen } from "./src/screens/ReportScreen"; import { BarkleyScreen } from "./src/screens/BarkleyScreen"; import { BarkleyStepScreen } from "./src/screens/BarkleyStepScreen"; import { RewardsScreen } from "./src/screens/RewardsScreen"; -import { DecodeurScreen } from "./src/screens/DecodeurScreen"; -import { ScriptsScreen } from "./src/screens/ScriptsScreen"; -import { StrengthsScreen } from "./src/screens/StrengthsScreen"; import { CrisisListScreen } from "./src/screens/CrisisListScreen"; -import { CarePathwayScreen } from "./src/screens/CarePathwayScreen"; -import { AchievementsScreen } from "./src/screens/AchievementsScreen"; import { SettingsScreen } from "./src/screens/SettingsScreen"; -import { BurnoutScreen } from "./src/screens/BurnoutScreen"; import { ConnaissancesScreen } from "./src/screens/ConnaissancesScreen"; import { ConnaissancesArticleScreen } from "./src/screens/ConnaissancesArticleScreen"; // Auth @@ -149,19 +142,12 @@ function PlusNavigator() { - - - - - - - { - const unlocked = new Set(); - - // first_child is always unlocked — we're on a child-scoped screen so a - // child necessarily exists. - unlocked.add("first_child"); - - if (signals.journalCount >= 1) unlocked.add("first_journal"); - if (signals.strengthCount >= 1) unlocked.add("first_strength"); - if (signals.strengthCount >= 5) unlocked.add("five_strengths"); - if (signals.crisisItemCount >= 1) unlocked.add("first_crisis_list"); - if (signals.routineCount >= 1) unlocked.add("first_routine"); - if (signals.streakDays >= 7) unlocked.add("streak_7"); - if (signals.streakDays >= 30) unlocked.add("streak_30"); - - // Explorer: 5+ distinct features touched. - let score = 1; // child already exists - if (signals.journalCount >= 1) score++; - if (signals.strengthCount >= 1) score++; - if (signals.crisisItemCount >= 1) score++; - if (signals.routineCount >= 1) score++; - if (score >= 5) unlocked.add("explorer"); - - return unlocked; -} - -// ── Hook ───────────────────────────────────────────────────────────────────── - -/** Returns the set of unlocked achievement ids for the given child. */ -export function useAchievements(childId: string): { - unlocked: Set; - total: number; - isLoading: boolean; -} { - const journal = useJournal(childId); - const strengths = useStrengths(childId); - const crisis = useCrisisItems(childId); - const routines = useRoutines(childId); - const stats = useInsights(childId, "week"); - - const isLoading = - journal.isLoading || - strengths.isLoading || - crisis.isLoading || - routines.isLoading || - stats.isLoading; - - const unlocked = useMemo(() => { - const signals: Signals = { - journalCount: journal.data?.length ?? 0, - strengthCount: strengths.data?.length ?? 0, - crisisItemCount: crisis.data?.length ?? 0, - routineCount: routines.data?.length ?? 0, - streakDays: stats.data?.streak ?? 0, - }; - return compute(signals); - }, [ - journal.data, - strengths.data, - crisis.data, - routines.data, - stats.data, - ]); - - return { unlocked, total: ACHIEVEMENTS.length, isLoading }; -} diff --git a/apps/mobile/src/hooks/use-activity.ts b/apps/mobile/src/hooks/use-activity.ts deleted file mode 100644 index c47cd83b..00000000 --- a/apps/mobile/src/hooks/use-activity.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; - -import { api } from "../lib/api"; - -// ─── Local types (mirrors apps/web/src/hooks/use-audit-log.ts) ─────────────── - -export type AuditEntityType = - | "child" - | "symptom" - | "journal" - | "medication" - | "medication_log" - | "crisis_item" - | "child_access" - | "child_invitation" - | "strength" - | "routine" - | "routine_completion" - | "admin_document"; - -export type AuditAction = "create" | "update" | "delete" | "accept" | "revoke"; - -export interface AuditEntry { - id: string; - actorId: string | null; - actorName: string | null; - childId: string | null; - entityType: AuditEntityType; - entityId: string | null; - action: AuditAction; - summary: string | null; - createdAt: string; -} - -// ─── Query key ──────────────────────────────────────────────────────────────── - -const key = (childId: string, limit: number) => - ["activity", "child", childId, limit] as const; - -// ─── Hook ───────────────────────────────────────────────────────────────────── - -/** - * Fetches the activity feed for a child. - * Endpoint: GET /api/audit-log/child/:childId?limit= - * Returns entries ordered most-recent first (the API returns them that way). - */ -export function useActivity(childId: string, limit = 50) { - return useQuery({ - queryKey: key(childId, limit), - queryFn: () => - api.get(`/audit-log/child/${childId}?limit=${limit}`), - enabled: !!childId, - }); -} diff --git a/apps/mobile/src/hooks/use-care-pathway.ts b/apps/mobile/src/hooks/use-care-pathway.ts deleted file mode 100644 index 816958b2..00000000 --- a/apps/mobile/src/hooks/use-care-pathway.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { - CarePathwayProgress, - UpsertCarePathwayProgress, -} from "@focusflow/validators"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; - -import { api } from "../lib/api"; - -// Mirrors apps/web/src/hooks/use-care-pathway.ts: fetch progress rows for a -// child and upsert a step's status (todo → doing → done, or any transition). -const key = (childId: string) => ["care-pathway", childId] as const; - -export function useCarePathwayProgress(childId: string) { - return useQuery({ - queryKey: key(childId), - queryFn: () => api.get(`/care-pathway/${childId}`), - enabled: !!childId, - }); -} - -export function useUpsertCarePathwayStep(childId: string) { - const qc = useQueryClient(); - return useMutation({ - mutationFn: (data: UpsertCarePathwayProgress) => - api.post("/care-pathway", data), - onSettled: () => qc.invalidateQueries({ queryKey: key(childId) }), - }); -} diff --git a/apps/mobile/src/hooks/use-decodeur.ts b/apps/mobile/src/hooks/use-decodeur.ts deleted file mode 100644 index c00e7934..00000000 --- a/apps/mobile/src/hooks/use-decodeur.ts +++ /dev/null @@ -1,154 +0,0 @@ -// Décodeur de comportements — données 100 % statiques. -// Le contenu est adapté du catalogue web (apps/web/src/lib/behavior-decoder-data.ts -// et les traductions fr.json). Aucun appel API n'est nécessaire. - -export type BehaviorEntry = { - id: string; - behavior: string; - explanation: string; - tip: string; - tags: readonly string[]; -}; - -export const BEHAVIOR_ENTRIES: readonly BehaviorEntry[] = [ - { - id: "throwsBackpack", - behavior: "Il jette ses affaires en rentrant de l'école.", - explanation: - "Toute la journée, son cortex préfrontal a freiné les impulsions, l'agitation, les colères. Le retour à la maison, c'est l'endroit safe : le frein lâche. Ce n'est pas dirigé contre vous, c'est une décharge.", - tip: "Prévoyez un rituel de décompression d'abord (5 minutes seul·e dans sa chambre, un encas, un câlin silencieux) avant toute exigence. Les consignes attendront 15 minutes.", - tags: ["école", "retour", "agressif", "affaires", "transition"], - }, - { - id: "interrupts", - behavior: "Il interrompt sans cesse quand je parle.", - explanation: - "Sa mémoire de travail est courte. S'il garde son idée pour son tour, elle s'évanouit. Il interrompt pour ne pas la perdre, pas pour vous manquer de respect.", - tip: "Offrez-lui un papier ou un petit carnet : il note son idée, vous finissez. Vous lui rendez la parole ensuite. Ça apaise l'impulsion sans la réprimer.", - tags: ["interrompt", "coupe la parole", "impulsivité", "discussion"], - }, - { - id: "losesStuff", - behavior: "Il perd ses affaires (cartable, gourde, doudou).", - explanation: - "Ranger un objet demande d'inhiber d'autres pensées au même moment. Son cerveau passe à autre chose avant que le geste de « poser » soit enregistré. L'objet existait, puis a disparu — pas par négligence, par défaut d'ancrage.", - tip: "Une seule place pour chaque chose, visible, sans tiroir. Une étiquette ou une photo. Préférez les rituels visuels aux rappels verbaux.", - tags: ["perd", "cartable", "doudou", "gourde", "oubli", "mémoire"], - }, - { - id: "forgetsInstructions", - behavior: "La même consigne ne se grave pas, même après trois fois.", - explanation: - "Entendre une consigne et l'exécuter passent par deux circuits différents. Chez l'enfant TDAH, le pont entre les deux est fragile. Il n'a pas désobéi : il n'a pas pu traduire ce qu'il a entendu en action.", - tip: "Une consigne à la fois, formulée en regardant l'enfant dans les yeux. Faites-la répéter. Pour les enchaînements, écrivez/dessinez la séquence.", - tags: ["oublie", "consigne", "répéter", "mémoire", "écoute"], - }, - { - id: "wontFinish", - behavior: "Il refuse de finir une activité commencée.", - explanation: - "Le cerveau TDAH carbure à la dopamine de nouveauté. Une fois l'activité connue, la motivation chute brutalement — comme une voiture en panne sèche. Ce n'est pas un manque de volonté, c'est un manque de carburant chimique.", - tip: "Découpez en mini-étapes très courtes (3 minutes, 5 minutes), avec une transition claire à chaque palier. Une mini-récompense ou une variation suffit à relancer la dopamine.", - tags: ["finir", "abandonne", "motivation", "lassitude"], - }, - { - id: "fidgets", - behavior: "Il agite ses mains, ses pieds, son corps en permanence.", - explanation: - "Pour rester attentif, son cerveau a besoin d'un stimulus moteur de fond. Bouger, c'est sa façon d'éveiller son cortex préfrontal. Le forcer à l'immobilité, c'est l'épuiser pour rien.", - tip: "Autorisez le mouvement utile : ballon, coussin pneumatique, élastique de chaise, fidget. La concentration monte, pas l'inverse.", - tags: ["bouge", "agite", "mains", "pieds", "attention"], - }, - { - id: "explosiveAnger", - behavior: "Il a des explosions disproportionnées pour rien.", - explanation: - "Sa régulation émotionnelle est plus lente à mûrir. Quand la fatigue cognitive monte, son seuil de tolérance s'effondre. Une « broutille » est en réalité la dixième micro-frustration de la journée.", - tip: "Ne raisonnez pas pendant la crise — son cortex est hors-ligne. Restez calme, baissez la voix, attendez la redescente. Le débrief, c'est plus tard, à froid.", - tags: ["crise", "colère", "explosion", "émotion", "régulation"], - }, - { - id: "liesToAvoid", - behavior: "Il ment pour éviter ses devoirs ou une corvée.", - explanation: - "Anticiper l'effort déclenche chez lui une vraie souffrance physique. Le mensonge est un évitement, pas une malveillance. Il préfère la honte du mensonge à l'angoisse de la tâche.", - tip: "Réduisez le seuil d'entrée de la tâche (« on fait juste la première ligne ensemble »). La honte recule quand la tâche redevient atteignable.", - tags: ["mensonge", "ment", "évite", "corvée", "devoirs"], - }, - { - id: "cantStartHomework", - behavior: "Il ne peut pas se mettre à ses devoirs.", - explanation: - "L'amorçage d'une tâche non-stimulante demande un effort exécutif énorme. C'est ce qu'on appelle l'inertie de démarrage. Il n'est pas paresseux : il est bloqué sur le seuil, comme une voiture qui patine.", - tip: "Démarrez avec lui pendant 90 secondes (lire la consigne à voix haute, faire le premier mot). Une fois le moteur lancé, il peut continuer seul.", - tags: ["devoirs", "commencer", "démarrer", "procrastination"], - }, - { - id: "screenTransition", - behavior: "Il s'énerve quand je lui demande d'arrêter le jeu vidéo.", - explanation: - "L'écran lui fournit en continu la dopamine qui lui manque. Quand vous l'arrêtez, c'est une chute neurochimique violente. Ce n'est pas une addiction de caprice, c'est un sevrage.", - tip: "Annoncez la fin 5 minutes avant, puis 2 minutes avant (minuteur visuel idéalement). Prévoyez une transition agréable (musique, jeu d'extérieur) — pas un saut direct vers une corvée.", - tags: ["écran", "jeu vidéo", "tablette", "arrêter", "transition"], - }, - { - id: "eveningMeltdown", - behavior: "Il pleure ou s'effondre pour des broutilles le soir.", - explanation: - "C'est l'effet « cocotte-minute ». Il a tenu toute la journée à l'école, retenu ses émotions. Le soir, à la maison où il se sent en sécurité, tout sort en même temps.", - tip: "Diminuez les exigences du soir au minimum vital. Plus de questions sur la journée, plus de devoirs après 18 h si possible. Un câlin, une histoire, un coucher tôt.", - tags: ["soir", "pleure", "effondrement", "fatigue"], - }, - { - id: "tableMovement", - behavior: "Il ne tient pas en place à table.", - explanation: - "Rester immobile pendant un repas demande une régulation motrice qui le coûte énormément. Pour ne pas exploser, il libère le trop-plein par des micro-mouvements continus.", - tip: "Acceptez certains mouvements (les pieds qui bougent, un fidget sur les genoux). Les repas plus courts, plus fréquents marchent mieux que les longs repas familiaux pour les petits.", - tags: ["table", "repas", "bouge", "agité"], - }, - { - id: "doesntListen", - behavior: "L'instituteur dit qu'il « n'écoute pas » en classe.", - explanation: - "Son attention sélective est défaillante. Le moindre stimulus environnant (un bruit, un mouvement) capte sa concentration. Il entend autant que les autres — il ne peut pas filtrer comme les autres.", - tip: "Demandez à l'enseignant·e une place près du tableau, loin de la fenêtre. Un signal discret (toucher l'épaule, code main) pour le rappeler à la tâche, plutôt que la réprimande publique.", - tags: ["école", "n'écoute pas", "attention", "instit", "maîtresse"], - }, - { - id: "slowToGetDressed", - behavior: "Il met une heure à s'habiller le matin.", - explanation: - "Il s'habille, puis voit un jouet, puis pense à autre chose, puis revient. Son attention saute en permanence. Ce n'est pas un sabotage : c'est qu'aucune ancre extérieure ne le maintient sur la tâche.", - tip: "Préparez les vêtements la veille, dans l'ordre. Un minuteur visuel sur l'étape (« 5 minutes pour le pantalon »). Le matin, peu de mots, beaucoup de gestes.", - tags: ["habille", "matin", "lent", "distraction"], - }, - { - id: "givesUpEarly", - behavior: "Il abandonne avant même d'avoir essayé.", - explanation: - "Les déceptions passées laissent une trace. Pour se protéger, il préfère « ne pas essayer » plutôt que « essayer et ne pas y arriver ». Ce n'est pas de la paresse, c'est une protection contre une estime de soi déjà fragilisée.", - tip: "Célébrez l'amorçage, pas le résultat. Une étape minuscule franchie vaut mieux qu'un grand projet abandonné. Évitez les comparaisons avec les autres enfants.", - tags: ["abandonne", "échec", "renoncement", "estime"], - }, -]; - -function normalize(s: string): string { - return s - .toLocaleLowerCase("fr-FR") - .normalize("NFD") - .replace(/\p{Diacritic}/gu, ""); -} - -/** Filter entries by a free-text query (French, diacritic-insensitive). */ -export function filterEntries( - entries: readonly BehaviorEntry[], - query: string, -): readonly BehaviorEntry[] { - const q = normalize(query.trim()); - if (q.length === 0) return entries; - return entries.filter((e) => { - const haystack = [e.behavior, ...e.tags].map(normalize).join(" "); - return haystack.includes(q); - }); -} diff --git a/apps/mobile/src/hooks/use-scripts.ts b/apps/mobile/src/hooks/use-scripts.ts deleted file mode 100644 index 984d6407..00000000 --- a/apps/mobile/src/hooks/use-scripts.ts +++ /dev/null @@ -1,176 +0,0 @@ -// Scripts de communication — données 100 % statiques. -// Adapté du catalogue web (apps/web/src/lib/communication-scripts-data.ts -// et les traductions fr.json). Aucun appel API. - -export type ScriptEntry = { - id: string; - title: string; - whyHard: string; - principles: string[]; - phrases: string[]; - pitfalls: string[]; -}; - -export const SCRIPT_ENTRIES: readonly ScriptEntry[] = [ - { - id: "schoolCall", - title: "Quand l'école appelle pour un comportement", - whyHard: - "Un mélange de honte et de culpabilité, pas le temps de préparer une réponse posée.", - principles: [ - "Demander un fait précis (heure, contexte) plutôt qu'accepter un jugement global", - "Ne pas s'excuser à la place de l'enfant", - "Repositionner sur l'aide à apporter, pas sur la sanction", - ], - phrases: [ - "Merci de m'appeler. Pour bien comprendre, à quel moment précis cela s'est passé ?", - "Mon enfant a un TDAH suivi médicalement. Ce que vous décrivez correspond aux moments où il a le plus de difficulté à se réguler. On peut en parler ensemble pour trouver une réponse qui marche pour lui et pour la classe.", - "Je vais reprendre cela à la maison avec lui, calmement. De votre côté, qu'est-ce qui pourrait l'aider à l'école dans ce type de moment ?", - ], - pitfalls: [ - "« Excusez-le, on traverse une période difficile » — minimise la situation et porte la culpabilité seul", - "« Je vais le punir ce soir » — engage une sanction sans avoir compris ce qui s'est passé", - ], - }, - { - id: "grandparent", - title: "Expliquer le TDAH à un grand-parent qui minimise", - whyHard: - "On a besoin de soutien familial, et on entend « de mon temps on ne faisait pas tant d'histoires ».", - principles: [ - "Court-circuiter le débat éducatif, ramener au médical", - "Donner une seule info concrète plutôt qu'un cours sur les neurosciences", - "Préserver le lien sans céder sur le fond", - ], - phrases: [ - "Le TDAH, c'est un fonctionnement neurologique. Ce n'est pas une question d'éducation.", - "On suit un médecin pour ça. On fait ce qu'on peut, et ce que tu vois n'est pas de la mauvaise volonté.", - "Tu peux nous aider en l'acceptant tel qu'il est quand on est ensemble. C'est tout ce dont on a besoin pour l'instant.", - ], - pitfalls: [ - "Entrer dans le débat sur les neurosciences — épuise et n'aboutit jamais", - "« Tu ne comprends rien » — coupe un lien dont on aura besoin un jour", - ], - }, - { - id: "caprice", - title: "Répondre à « c'est juste un caprice »", - whyHard: - "Cette phrase nie ce qu'on vit au quotidien. Elle vient souvent de gens qui ne voient l'enfant que deux heures le dimanche.", - principles: [ - "Pas de débat ouvert avec quelqu'un qui n'écoute pas vraiment", - "Une phrase de cadrage, puis on change de sujet", - "Pas d'argument scientifique en réponse à un jugement émotionnel", - ], - phrases: [ - "Non, c'est un trouble neurologique diagnostiqué. On en parlera plus longuement une autre fois.", - "Si tu le voyais 24h/24, tu verrais que ce n'est pas du tout ça.", - "Je préfère qu'on ne juge pas mon enfant, surtout en sa présence.", - ], - pitfalls: [ - "Justifier en détail — c'est ce que la personne attend pour démonter chaque argument", - "Laisser passer en silence devant l'enfant — il enregistre le manque de soutien", - ], - }, - { - id: "pedopsyPrep", - title: "Préparer une consultation pédopsy en 10 minutes", - whyHard: - "Le rendez-vous est court, on perd l'essentiel sous le stress. On en sort avec l'impression de ne pas avoir tout dit.", - principles: [ - "Trois sujets maximum, écrits sur papier avant d'entrer", - "Faits concrets datés, pas d'interprétation", - "Une question prioritaire pour soi (pas pour l'enfant)", - ], - phrases: [ - "Depuis la dernière fois, voici les trois choses qu'on a observées : [liste avec dates].", - "Ma question prioritaire aujourd'hui, c'est : [une seule].", - "Avant qu'on se quitte, est-ce qu'il y a un point que vous voulez qu'on surveille d'ici la prochaine fois ?", - ], - pitfalls: [ - "Tout dire dans l'ordre où ça vient — dilue le message principal", - "Parler uniquement des difficultés — on perd la trace des améliorations", - ], - }, - { - id: "announceCondition", - title: "Annoncer le TDAH à votre enfant", - whyHard: - "On a peur de l'étiqueter, ou de lui faire croire qu'il est cassé.", - principles: [ - "Pas le mot « maladie »", - "Cadrer comme une explication, pas une fatalité", - "Lui demander ce qu'il en pense, ne pas faire un monologue", - ], - phrases: [ - "Tu sais quand tu n'arrives pas à te concentrer ou à rester assis ? Ce n'est pas que tu ne veux pas. Ton cerveau fonctionne différemment, c'est ce qu'on appelle un TDAH.", - "Beaucoup d'enfants ont ça. C'est pour ça qu'on va t'aider avec des routines et un suivi médical — pas pour te changer, pour te donner des outils.", - "Qu'est-ce que ça te fait, ce que je te dis là ?", - ], - pitfalls: [ - "« Tu es malade » — fausse représentation, qui s'imprime durablement", - "En faire un sujet tabou — l'enfant entend le silence", - ], - }, - { - id: "papRequest", - title: "Demander un PAP/PPRE à l'école", - whyHard: - "L'école n'a pas le temps. On doit pousser sans devenir le « parent pénible ».", - principles: [ - "Demander un rendez-vous écrit, par mail", - "Citer le médecin — c'est lui qui prescrit l'aménagement", - "Proposer une réunion de 30 minutes, pas plus", - ], - phrases: [ - "Madame, Monsieur, mon enfant est suivi pour un TDAH. Le médecin recommande un PAP. Je souhaiterais qu'on en parle ensemble. Quand seriez-vous disponible pour une réunion de 30 minutes ?", - "Voici les recommandations du médecin (en pièce jointe). Nous sommes ouverts à vos remarques et à vos contraintes.", - "Pouvez-vous me confirmer par mail ce qu'on décide ensemble, pour qu'on en garde une trace ?", - ], - pitfalls: [ - "Demander oralement à la sortie de l'école — pas de trace, pas de suite", - "Arriver avec une liste d'exigences fermées — ferme le dialogue dès la première minute", - ], - }, - { - id: "misplacedRemark", - title: "Gérer une remarque déplacée en famille / au parc", - whyHard: - "Sur le moment on est sidéré, on ne sait pas quoi dire, et on s'en veut après.", - principles: [ - "Une phrase courte, ferme, qu'on apprend par cœur", - "Protéger l'enfant qui entend", - "Pas de débat, on coupe court", - ], - phrases: [ - "Merci, on gère.", - "Ce que tu dis là n'est pas vrai pour mon enfant.", - "Je préfère qu'on ne le juge pas, surtout devant lui.", - "On en reparlera entre adultes, pas maintenant.", - ], - pitfalls: [ - "Se justifier longuement — installe le débat à la place du recadrage", - "Rire jaune — l'enfant entend l'acquiescement implicite", - ], - }, - { - id: "presentTreatment", - title: "Présenter le traitement à votre enfant", - whyHard: - "On veut qu'il accepte le médicament sans en faire un sujet de honte.", - principles: [ - "Nommer ce que le médicament aide, pas ce qu'il « corrige »", - "Comparer à une aide banale (lunettes, asthmateur)", - "Lui laisser un mot pour exprimer ce qu'il ressent, à tout moment", - ], - phrases: [ - "Le médicament aide ton cerveau à faire ce qu'il a du mal à faire seul. C'est un coup de pouce, pas un changement de toi.", - "Comme des lunettes pour mieux voir : ça ne change pas tes yeux, ça t'aide à voir net.", - "Si à un moment tu sens que ça te gêne, tu me le dis et on en parle ensemble au médecin.", - ], - pitfalls: [ - "« Tu seras plus sage » ou « tu seras plus calme » — confond effet du médicament et identité de l'enfant", - "En cacher l'existence à l'enfant — il découvrira un jour avec un sentiment de trahison", - ], - }, -]; diff --git a/apps/mobile/src/hooks/use-strengths.ts b/apps/mobile/src/hooks/use-strengths.ts deleted file mode 100644 index 01f38633..00000000 --- a/apps/mobile/src/hooks/use-strengths.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { - CreateStrength, - Strength, - UpdateStrength, -} from "@focusflow/validators"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; - -import { api } from "../lib/api"; - -// Mirrors apps/web/src/hooks/use-strengths.ts: list a child's strengths and -// add / edit / remove them. Simple CRUD, fully native. -const key = (childId: string) => ["strengths", childId] as const; - -export function useStrengths(childId: string) { - return useQuery({ - queryKey: key(childId), - queryFn: () => api.get(`/strengths/${childId}`), - enabled: !!childId, - }); -} - -export function useCreateStrength(childId: string) { - const qc = useQueryClient(); - return useMutation({ - mutationFn: (data: CreateStrength) => - api.post("/strengths", data), - onSettled: () => qc.invalidateQueries({ queryKey: key(childId) }), - }); -} - -export function useUpdateStrength(childId: string) { - const qc = useQueryClient(); - return useMutation({ - mutationFn: ({ id, ...data }: UpdateStrength & { id: string }) => - api.patch(`/strengths/${id}`, data), - onSettled: () => qc.invalidateQueries({ queryKey: key(childId) }), - }); -} - -export function useDeleteStrength(childId: string) { - const qc = useQueryClient(); - return useMutation({ - mutationFn: (id: string) => api.delete<{ ok: true }>(`/strengths/${id}`), - onSettled: () => qc.invalidateQueries({ queryKey: key(childId) }), - }); -} diff --git a/apps/mobile/src/navigation/types.ts b/apps/mobile/src/navigation/types.ts index 10d94faa..388ede3c 100644 --- a/apps/mobile/src/navigation/types.ts +++ b/apps/mobile/src/navigation/types.ts @@ -25,7 +25,6 @@ export type RootStackParamList = { Medications: ChildParams; CalmMinutes: ChildParams; Insights: ChildParams; - Activity: ChildParams; Report: ChildParams; // Plus (grouped menu) PlusMenu: undefined; @@ -48,15 +47,9 @@ export type RootStackParamList = { Barkley: ChildParams; BarkleyStep: ChildParams & { stepNumber: number }; Rewards: ChildParams; - Decodeur: ChildParams; - Scripts: ChildParams; - Strengths: ChildParams; CrisisList: ChildParams; - CarePathway: ChildParams; - Achievements: ChildParams; // Compte / account Settings: undefined; - Burnout: undefined; Connaissances: undefined; ConnaissancesArticle: { slug: string; title: string }; }; @@ -86,7 +79,6 @@ export type MedicationsProps = S<"Medications">; export type JournalProps = S<"Journal">; export type CalmMinutesProps = S<"CalmMinutes">; export type InsightsProps = S<"Insights">; -export type ActivityProps = S<"Activity">; export type ReportProps = S<"Report">; export type RoutinesProps = S<"Routines">; export type AddRoutineProps = S<"AddRoutine">; @@ -94,14 +86,8 @@ export type EditRoutineProps = S<"EditRoutine">; export type BarkleyProps = S<"Barkley">; export type BarkleyStepProps = S<"BarkleyStep">; export type RewardsProps = S<"Rewards">; -export type DecodeurProps = S<"Decodeur">; -export type ScriptsProps = S<"Scripts">; -export type StrengthsProps = S<"Strengths">; export type CrisisListProps = S<"CrisisList">; -export type CarePathwayProps = S<"CarePathway">; -export type AchievementsProps = S<"Achievements">; export type SettingsProps = S<"Settings">; -export type BurnoutProps = S<"Burnout">; export type ConnaissancesProps = S<"Connaissances">; export type ConnaissancesArticleProps = S<"ConnaissancesArticle">; diff --git a/apps/mobile/src/screens/AchievementsScreen.tsx b/apps/mobile/src/screens/AchievementsScreen.tsx deleted file mode 100644 index ee143458..00000000 --- a/apps/mobile/src/screens/AchievementsScreen.tsx +++ /dev/null @@ -1,257 +0,0 @@ -import { useEffect, useMemo, useState } from "react"; -import AsyncStorage from "@react-native-async-storage/async-storage"; -import { StyleSheet, Text, View } from "react-native"; - -import { - CalloutCard, - Card, - Loader, - Screen, - ScreenHeader, - fonts, -} from "../components/ui"; -import { useTheme, type Palette } from "../lib/theme"; -import { - ACHIEVEMENTS, - useAchievements, - type AchievementId, -} from "../hooks/use-achievements"; -import type { AchievementsProps } from "../navigation/types"; - -export function AchievementsScreen({ navigation, route }: AchievementsProps) { - const c = useTheme(); - const styles = useMemo(() => makeStyles(c), [c]); - - const { childId, childName } = route.params; - const { unlocked, total, isLoading } = useAchievements(childId); - const unlockedCount = unlocked.size; - const pct = total > 0 ? Math.round((unlockedCount / total) * 100) : 0; - - // One-shot celebration of badges unlocked since the last visit. The seen set - // is persisted per child so a revisit stays calm (no repeated fanfare). - const [celebrated, setCelebrated] = useState([]); - useEffect(() => { - if (isLoading || unlocked.size === 0) return; - const key = `toko:achievements:seen:${childId}`; - let cancelled = false; - void AsyncStorage.getItem(key).then((raw) => { - if (cancelled) return; - const seen = new Set(raw ? JSON.parse(raw) : []); - const fresh = [...unlocked].filter((id) => !seen.has(id)); - if (fresh.length > 0) setCelebrated(fresh); - void AsyncStorage.setItem(key, JSON.stringify([...unlocked])); - }); - return () => { - cancelled = true; - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isLoading, childId]); - - const celebratedBadges = celebrated - .map((id) => ACHIEVEMENTS.find((b) => b.id === id)) - .filter((b): b is (typeof ACHIEVEMENTS)[number] => !!b); - - return ( - - navigation.goBack()} - /> - - {/* One-shot celebration of newly unlocked badges */} - {celebratedBadges.length > 0 ? ( - 1 - ? `${celebratedBadges.length} nouveaux badges 🎉` - : "Nouveau badge 🎉" - } - > - {celebratedBadges.map((b) => ( - - {b.emoji} {b.title} - - ))} - - ) : null} - - {/* Progress summary */} - - - - {unlockedCount} badge{unlockedCount > 1 ? "s" : ""} sur {total}{" "} - débloqué{unlockedCount > 1 ? "s" : ""} - - {pct} % - - - {/* Progress bar */} - - - - - - Pas de classement, pas de pression — juste un miroir de ce que vous - avez construit. - - - - {isLoading ? ( - - ) : ( - ACHIEVEMENTS.map((badge) => { - const isUnlocked = unlocked.has(badge.id); - return ( - - - - {badge.emoji} - - - - {badge.title} - - {badge.description} - - - - {isUnlocked ? "Débloqué" : "À venir"} - - - - - ); - }) - )} - - ); -} - -const makeStyles = (c: Palette) => - StyleSheet.create({ - celebrateLine: { fontSize: 15, color: c.text, fontFamily: fonts.semibold }, - progressCard: { - gap: 10, - backgroundColor: c.card, - borderColor: c.border, - }, - progressRow: { - flexDirection: "row", - justifyContent: "space-between", - alignItems: "center", - }, - progressLabel: { - fontSize: 15, - fontWeight: "600", - color: c.text, - }, - progressPct: { - fontSize: 22, - fontWeight: "700", - color: "#7c3aed", - }, - barTrack: { - height: 8, - backgroundColor: c.border, - borderRadius: 999, - overflow: "hidden", - }, - barFill: { - height: "100%", - backgroundColor: "#7c3aed", - borderRadius: 999, - }, - progressNote: { - fontSize: 12, - color: c.muted, - lineHeight: 16, - }, - cardUnlocked: { - backgroundColor: c.card, - borderColor: c.brand, - }, - cardLocked: { - backgroundColor: c.bg, - borderColor: c.border, - borderStyle: "dashed", - }, - badgeRow: { - flexDirection: "row", - alignItems: "flex-start", - gap: 12, - }, - emoji: { - fontSize: 32, - lineHeight: 40, - }, - emojiLocked: { - opacity: 0.35, - }, - badgeContent: { - flex: 1, - gap: 2, - }, - badgeTitle: { - fontSize: 15, - fontWeight: "600", - color: c.text, - lineHeight: 20, - }, - badgeTitleLocked: { - color: c.muted, - }, - badgeDesc: { - fontSize: 13, - color: c.subtext, - lineHeight: 18, - }, - pill: { - paddingHorizontal: 8, - paddingVertical: 3, - borderRadius: 999, - borderWidth: 1, - }, - pillUnlocked: { - backgroundColor: c.successSurface, - borderColor: c.successBorder, - }, - pillLocked: { - backgroundColor: "transparent", - borderColor: c.border, - }, - pillText: { - fontSize: 11, - fontWeight: "600", - }, - pillTextUnlocked: { - color: c.successFg, - }, - pillTextLocked: { - color: c.muted, - }, - }); diff --git a/apps/mobile/src/screens/ActivityScreen.tsx b/apps/mobile/src/screens/ActivityScreen.tsx deleted file mode 100644 index 5b291d50..00000000 --- a/apps/mobile/src/screens/ActivityScreen.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { useMemo } from "react"; -import { StyleSheet, Text, View } from "react-native"; - -import { - Card, - EmptyState, - ErrorNote, - Loader, - Screen, - ScreenHeader, -} from "../components/ui"; -import { useTheme, type Palette } from "../lib/theme"; -import { useActivity, type AuditEntry, type AuditEntityType } from "../hooks/use-activity"; -import type { ActivityProps } from "../navigation/types"; - -// ─── Emoji per entity type — avoids importing Lucide icons ─────────────────── - -const ENTITY_EMOJI: Record = { - child: "👶", - symptom: "📊", - journal: "📖", - medication: "💊", - medication_log: "💊", - crisis_item: "🤝", - child_access: "🔒", - child_invitation: "✉️", - strength: "✨", - routine: "📋", - routine_completion: "✅", - admin_document: "📄", -}; - -// ─── Relative time formatter (FR, no external dep) ─────────────────────────── - -function formatRelativeFr(date: Date): string { - const diff = Date.now() - date.getTime(); - const min = Math.floor(diff / 60_000); - if (min < 1) return "à l'instant"; - if (min < 60) return `il y a ${min} min`; - const hr = Math.floor(min / 60); - if (hr < 24) return `il y a ${hr} h`; - const d = Math.floor(hr / 24); - if (d === 1) return "hier"; - if (d < 7) return `il y a ${d} j`; - return date.toLocaleDateString("fr-FR", { day: "numeric", month: "short" }); -} - -// ─── Single activity row ────────────────────────────────────────────────────── - -function ActivityRow({ - entry, - styles, -}: { - entry: AuditEntry; - styles: ReturnType; -}) { - const emoji = ENTITY_EMOJI[entry.entityType] ?? "🔔"; - const actor = entry.actorName ?? "Quelqu'un"; - const text = entry.summary ?? `${entry.entityType} — ${entry.action}`; - const relative = formatRelativeFr(new Date(entry.createdAt)); - - return ( - - - {emoji} - - - {actor} - {text} - - {relative} - - - - ); -} - -// ─── Screen ─────────────────────────────────────────────────────────────────── - -export function ActivityScreen({ navigation, route }: ActivityProps) { - const { childId, childName } = route.params; - const { data, isLoading, isError } = useActivity(childId, 100); - const c = useTheme(); - const styles = useMemo(() => makeStyles(c), [c]); - - return ( - - navigation.goBack()} - /> - - {isError ? ( - - ) : isLoading ? ( - - ) : data && data.length > 0 ? ( - data.map((entry) => ) - ) : ( - - )} - - ); -} - -// ─── Styles ─────────────────────────────────────────────────────────────────── - -const makeStyles = (c: Palette) => - StyleSheet.create({ - row: { - // Card already has padding + gap; we just tweak internals - }, - rowInner: { - flexDirection: "row", - alignItems: "flex-start", - gap: 12, - }, - emoji: { - fontSize: 22, - lineHeight: 28, - }, - rowBody: { - flex: 1, - gap: 2, - }, - rowText: { - fontSize: 14, - color: c.text, - lineHeight: 20, - }, - actor: { - fontWeight: "600", - color: c.text, - }, - summary: { - color: c.subtext, - }, - time: { - fontSize: 12, - color: c.muted, - }, - }); diff --git a/apps/mobile/src/screens/BurnoutScreen.tsx b/apps/mobile/src/screens/BurnoutScreen.tsx deleted file mode 100644 index 4a05be11..00000000 --- a/apps/mobile/src/screens/BurnoutScreen.tsx +++ /dev/null @@ -1,213 +0,0 @@ -import { useMemo, useState } from "react"; -import { Linking, Pressable, StyleSheet, Text, View } from "react-native"; - -import { - Button, - CalloutCard, - Card, - Screen, - ScreenHeader, - fonts, -} from "../components/ui"; -import { useTheme, type Palette } from "../lib/theme"; -import type { BurnoutProps } from "../navigation/types"; - -// Parental burn-out self-assessment, ported from the PWA /burnout (Roskam & -// Mikolajczak inspired). 7 questions on a 0–3 scale, total /21, three zones. -// Client-side only — nothing is stored (a guilt-free mirror, not a diagnosis). -// The parent mood logger (1–5) lives on the Home dashboard, not here. -const QUESTIONS = [ - "Je me sens épuisé·e dès le matin quand je pense à la journée avec mon enfant.", - "À la fin de la journée, mes ressources émotionnelles sont vides.", - "Je me sens distant·e affectivement avec mon enfant, comme en retrait.", - "Je ne reconnais plus le parent que j'étais avant.", - "Je culpabilise souvent d'être un·e mauvais·e parent.", - "Je n'ai plus d'énergie pour les moments simples : jeu, câlin, rire.", - "Je pense parfois que je ne peux plus assumer ce rôle.", -]; -const SCALE = ["Jamais", "Parfois", "Souvent", "Tout le temps"]; - -type Zone = "green" | "orange" | "red"; -function zoneFromScore(score: number): Zone { - if (score <= 6) return "green"; - if (score <= 13) return "orange"; - return "red"; -} -const ZONE = { - green: { - variant: "success" as const, - label: "Vous tenez bon", - title: "Pas de signaux d'épuisement marqués.", - body: "Vous traversez la parentalité TDAH avec des ressources. Ne sous-estimez pas pour autant la fatigue : prenez les pauses dont vous avez besoin, même quand tout semble aller.", - }, - orange: { - variant: "alert" as const, - label: "Fatigue notable", - title: "Des signes de fatigue qui méritent attention.", - body: "Ce que vous ressentez est réel, et c'est le bon moment pour ralentir. Diminuez ce qui peut l'être, parlez-en à un proche, et envisagez d'en parler à votre médecin si la fatigue s'installe.", - }, - red: { - variant: "alert" as const, - label: "Signaux forts", - title: "Vous portez beaucoup. Vous n'êtes pas seul·e.", - body: "Ce que vous traversez ressemble à un épuisement parental significatif. Ce n'est pas une faiblesse, ce n'est pas votre responsabilité. C'est un signal d'alerte qui mérite un échange avec un professionnel — médecin traitant, psychologue, ou un des numéros d'écoute ci-dessous.", - }, -}; - -const SUPPORT = [ - { label: "3114 — Prévention du suicide", hint: "Gratuit, 24h/24, anonyme", url: "tel:3114" }, - { label: "Allô Parents Bébé", hint: "0 800 235 236 · écoute parentale", url: "tel:0800235236" }, - { label: "HyperSupers TDAH France", hint: "Soutien et orientation TDAH", url: "https://www.tdah-france.fr/" }, -]; - -export function BurnoutScreen({ navigation }: BurnoutProps) { - const c = useTheme(); - const styles = useMemo(() => makeStyles(c), [c]); - - const [answers, setAnswers] = useState<(number | null)[]>( - () => QUESTIONS.map(() => null), - ); - const [submitted, setSubmitted] = useState(false); - - const total = answers.reduce((sum, v) => sum + (v ?? 0), 0); - const allAnswered = answers.every((a) => a !== null); - - function setAnswer(i: number, v: number) { - setAnswers((prev) => { - const next = [...prev]; - next[i] = v; - return next; - }); - } - function reset() { - setAnswers(QUESTIONS.map(() => null)); - setSubmitted(false); - } - - if (submitted) { - const zone = ZONE[zoneFromScore(total)]; - return ( - - navigation.goBack()} - /> - - Score : {total} sur 21 - {zone.title} - {zone.body} - - - - Vous pouvez en parler maintenant - - Ces lignes sont gratuites, anonymes et ouvertes aux parents en - difficulté. Vous n'avez pas besoin d'avoir trouvé les bons mots pour - appeler. - - {SUPPORT.map((s) => ( - Linking.openURL(s.url)} - style={styles.supportRow} - accessibilityRole="button" - > - {s.label} - {s.hint} - - ))} - - -