commit fdd72c11351f32c40fb9a1553bf8a87783ad42d7 Author: philippe Date: Tue Oct 14 10:43:33 2025 +0200 Initial commit: Family Planner application Complete family planning application with: - React frontend with TypeScript - Node.js/Express backend with TypeScript - Python ingestion service for document processing - Planning ingestion service with LLM integration - Shared UI components and type definitions - OAuth integration for calendar synchronization - Comprehensive documentation ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a80aea8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,56 @@ +# Node +node_modules/ +*.log +.env +.env.* +!.env.example +dist/ +build/ + +# Frontend tooling +.DS_Store +coverage/ + +# Python +__pycache__/ +.pytest_cache/ +.venv/ +*.pyc + +# IDE +.idea/ +.vscode/ + +# Compiled assets +*.sqlite3 +tmp/ + +# Data files with PII (Personally Identifiable Information) +backend/src/data/client.json +**/public/plans/ +**/public/avatars/ + +# Security - never commit secrets +backend/src/data/secrets.json +**/secrets.json +*.pem +*.key +*.cert + +# Backup files +*.bak +*.backup +*.old +*.tmp + +# Logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# OS +.DS_Store +Thumbs.db +nul diff --git a/LISEZ-MOI.txt b/LISEZ-MOI.txt new file mode 100644 index 0000000..300d82c --- /dev/null +++ b/LISEZ-MOI.txt @@ -0,0 +1,172 @@ +================================================================================ + FAMILY PLANNER - INTEGRATION PRONOTE + Application Redesignee +================================================================================ + +DEMARRAGE RAPIDE +================================================================================ + +1. Double-cliquez sur : LANCER_APPLICATION.bat + +2. Attendez que les 3 serveurs se lancent (environ 10 secondes) + +3. L'application s'ouvre automatiquement dans votre navigateur + URL : http://localhost:5173/profiles + +4. Cliquez sur un profil d'enfant (ex: Robin Heyraud) + +5. Vous verrez la nouvelle page redesignee avec : + - Tous les boutons d'action integres (Planning, Importer, Modifier, Supprimer) + - Bouton "Connexion Pronote" pour se connecter + - Badge de statut Pronote + +================================================================================ +CONNEXION A PRONOTE - PREMIERE UTILISATION +================================================================================ + +1. Sur la page de profil, cliquez sur "Connexion Pronote" + +2. Entrez vos identifiants : + - URL Pronote : https://[votre-etablissement].index-education.net/pronote + - Nom d'utilisateur : votre identifiant Pronote + - Mot de passe : votre mot de passe Pronote + +3. Cliquez sur "Se connecter" + +4. Les donnees Pronote s'affichent automatiquement : + - Moyennes generales et classement + - Dernieres notes avec code couleur + - Prochains devoirs a rendre + - Emploi du temps du jour + - Absences et retards + - Conges scolaires selon votre zone + +================================================================================ +FONCTIONNALITES +================================================================================ + +BOUTONS D'ACTION : +- [Planning] : Voir le planning complet +- [Importer] : Synchroniser les donnees depuis Pronote +- [Modifier] : Modifier le profil +- [Connexion] : Se connecter/reconnecter a Pronote +- [Supprimer] : Supprimer le profil + +DONNEES PRONOTE : +- Moyennes generales (personnelle, classe, classement) +- Dernieres notes (4 plus recentes) +- Absences et retards (compteurs + historique) +- Prochains devoirs (3 plus urgents) +- Emploi du temps du jour +- Conges scolaires (selon zone A/B/C) + +AUTRES : +- Notes personnelles editables +- Selection de la zone scolaire +- Sauvegarde automatique + +================================================================================ +DONNEES DE DEMONSTRATION +================================================================================ + +Si vous n'avez pas de compte Pronote reel, l'application utilise des donnees +de demonstration pour tester l'interface : + +- Moyenne generale : 15.2 +- Moyenne de classe : 13.8 +- 5 notes recentes en differentes matieres +- Emploi du temps du lundi et mardi +- 5 devoirs a venir +- 2 absences et 3 retards + +Ces donnees permettent de voir comment l'interface fonctionne sans connexion +reelle a Pronote. + +================================================================================ +SERVEURS LANCES +================================================================================ + +Le script de demarrage lance automatiquement 3 serveurs : + +1. API Pronote (Port 3000) : + - Backend pour la connexion Pronote + - Gestion des donnees et authentification + - URL : http://localhost:3000 + +2. Backend Family Planner (Port 3001) : + - API backend de l'application + - Gestion des profils et calendriers + - URL : http://localhost:3001 + +3. Frontend (Port 5173) : + - Interface utilisateur React + - URL : http://localhost:5173 + +================================================================================ +ARRETER L'APPLICATION +================================================================================ + +Pour arreter tous les serveurs : +- Fermez la fenetre du terminal/PowerShell +- Ou appuyez sur Ctrl+C dans le terminal + +================================================================================ +DEPANNAGE RAPIDE +================================================================================ + +PROBLEME : Les boutons ne s'affichent pas +SOLUTION : Verifiez que vous etes sur la page d'un profil enfant + URL correcte : http://localhost:5173/child/[ID] + +PROBLEME : "Erreur de connexion a Pronote" +SOLUTION : - Verifiez l'URL Pronote (doit commencer par https://) + - Verifiez vos identifiants + - Redemarrez l'application + +PROBLEME : Les donnees ne se chargent pas +SOLUTION : - Cliquez sur "Importer" + - Rafraichissez la page (F5) + - Reconnectez-vous a Pronote + +PROBLEME : L'application ne demarre pas +SOLUTION : - Verifiez que Node.js est installe : node --version + - Fermez tous les processus Node.js + - Relancez LANCER_APPLICATION.bat + +================================================================================ +DOCUMENTATION COMPLETE +================================================================================ + +Pour plus de details, consultez : +- INSTRUCTIONS_PRONOTE.md : Documentation complete en francais +- README.md : Documentation technique + +================================================================================ +SECURITE +================================================================================ + +- Les mots de passe Pronote ne sont PAS stockes en clair +- Utilisation de JWT pour l'authentification +- Sessions expirees automatiquement apres 1 heure +- Tokens cryptes dans localStorage + +RECOMMANDATIONS : +1. Ne partagez jamais vos identifiants Pronote +2. Fermez l'application apres utilisation +3. Videz le cache si ordinateur partage + +================================================================================ +SUPPORT +================================================================================ + +En cas de probleme : +1. Consultez INSTRUCTIONS_PRONOTE.md +2. Verifiez les logs des serveurs +3. Redemarrez l'application +4. Consultez la documentation technique + +================================================================================ +VERSION : 1.0.0 +DATE : 13 Octobre 2025 +DEVELOPPE AVEC : Claude Code +================================================================================ diff --git a/LISEZ_MOI_EN_PREMIER.txt b/LISEZ_MOI_EN_PREMIER.txt new file mode 100644 index 0000000..607736d --- /dev/null +++ b/LISEZ_MOI_EN_PREMIER.txt @@ -0,0 +1,120 @@ +โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— +โ•‘ โ•‘ +โ•‘ FAMILY PLANNER - PROJET NETTOYร‰ ET OPTIMISร‰ โ•‘ +โ•‘ โ•‘ +โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +โœจ NETTOYAGE COMPLET EFFECTUร‰ LE 13 OCTOBRE 2025 + +โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” + +๐Ÿ“ CE QUI A ร‰Tร‰ NETTOYร‰ : + + โœ… 4 fichiers dupliquรฉs supprimรฉs (ChildDetailScreen) + โœ… 18 fichiers de documentation archivรฉs dans docs/archive/ + โœ… 5 scripts de dรฉmarrage obsolรจtes supprimรฉs + โœ… Configuration des ports clarifiรฉe et documentรฉe + โœ… Monaco intรฉgrรฉ et vรฉrifiรฉ + +โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” + +๐Ÿš€ POUR Dร‰MARRER L'APPLICATION : + + 1. Double-cliquez sur : START.bat + + 2. Attendez 30 secondes (2 fenรชtres s'ouvrent) + + 3. Ouvrez votre navigateur : http://localhost:5173 + + 4. Appuyez sur Ctrl + Shift + R pour vider le cache + +โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” + +๐Ÿ‡ฒ๐Ÿ‡จ MONACO EST MAINTENANT DISPONIBLE ! + + Oรน ? Dans un profil enfant โ†’ Section "Congรฉs scolaires" + โ†’ Menu dรฉroulant "Zone scolaire" โ†’ Sรฉlectionnez "Monaco" + + Inclut : - 5 pรฉriodes de vacances scolaires + - 13 jours fรฉriรฉs (dont Sainte Dรฉvote, Fรชte du Prince) + - Donnรฉes officielles 2024-2025 + +โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” + +๐Ÿ“š DOCUMENTATION : + + ๐Ÿ“„ README.md โ†’ Documentation complรจte du projet + ๐Ÿ“„ QUICK_START.md โ†’ Guide de dรฉmarrage rapide (3 รฉtapes) + ๐Ÿ“„ PORTS.md โ†’ Configuration des ports (5000, 5173, 8000) + ๐Ÿ“„ NETTOYAGE_COMPLET โ†’ Dรฉtails du nettoyage effectuรฉ + + ๐Ÿ“ docs/archive/ โ†’ Anciennes documentations (18 fichiers) + +โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” + +โš™๏ธ PORTS UTILISร‰S : + + Backend โ†’ Port 5000 (http://localhost:5000) + Frontend โ†’ Port 5173 (http://localhost:5173) + Ingestionโ†’ Port 8000 (http://localhost:8000) [optionnel] + +โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” + +๐Ÿ›‘ POUR ARRรŠTER : + + Double-cliquez sur : STOP.bat + + Ou fermez simplement les 2 fenรชtres de terminal + +โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” + +โ“ PROBLรˆME ? MONACO N'APPARAรŽT PAS ? + + Solution rapide : + + 1. Fermez TOUS les terminaux + 2. Double-cliquez sur START.bat + 3. Attendez 30 secondes MINIMUM + 4. Ouvrez http://localhost:5173 + 5. Ctrl + Shift + R (rechargement forcรฉ) + 6. Crรฉez un nouveau profil enfant + 7. Vรฉrifiez dans "Congรฉs scolaires" + + Si toujours pas : Voir QUICK_START.md section "Problรจmes courants" + +โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” + +โœ… CHECKLIST DE Dร‰MARRAGE : + + โ–ก Node.js installรฉ (version 20+) + โ–ก Double-cliquรฉ sur START.bat + โ–ก 2 fenรชtres de terminal ouvertes (Backend + Frontend) + โ–ก Backend affiche "Server ready on port 5000" + โ–ก Frontend affiche "ready in XXX ms" + โ–ก Navigateur ouvert sur localhost:5173 + โ–ก Profils enfants se chargent + โ–ก Monaco visible dans "Zone scolaire" + +โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” + +๐ŸŽฏ STRUCTURE DU PROJET : + +family-planner/ +โ”œโ”€โ”€ backend/ โ†’ API Node.js (TypeScript) +โ”œโ”€โ”€ frontend/ โ†’ Application React (TypeScript) +โ”œโ”€โ”€ ingestion-service/โ†’ Service Python OCR +โ”œโ”€โ”€ docs/ โ†’ Documentation +โ”‚ โ””โ”€โ”€ archive/ โ†’ Docs historiques +โ”œโ”€โ”€ START.bat โ†’ โญ Dร‰MARRAGE ICI ! +โ”œโ”€โ”€ STOP.bat โ†’ Arrรชt des serveurs +โ”œโ”€โ”€ README.md โ†’ Documentation complรจte +โ”œโ”€โ”€ QUICK_START.md โ†’ Guide rapide +โ””โ”€โ”€ PORTS.md โ†’ Config des ports + +โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” + +Projet nettoyรฉ, optimisรฉ et prรชt ร  l'emploi ! ๐ŸŽ‰ + +Pour toute question : Voir QUICK_START.md ou README.md + +โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• diff --git a/Lancer-Family-Planner.bat b/Lancer-Family-Planner.bat new file mode 100644 index 0000000..64b496f --- /dev/null +++ b/Lancer-Family-Planner.bat @@ -0,0 +1,13 @@ +@echo off +setlocal +cd /d "%~dp0" + +echo Ouveture des services Family Planner... +start "Backend API" cmd /k npm run dev --workspace backend +start "Frontend" cmd /k npm run dev --workspace frontend +REM Optionnel: service d'ingestion si Python est installe +REM start "Ingestion" cmd /k uvicorn ingestion.main:app --reload --port 8000 --app-dir ingestion-service/src + +echo Fenetres lancees. Fermez ce terminal. +exit /b 0 + diff --git a/NETTOYAGE_COMPLET.md b/NETTOYAGE_COMPLET.md new file mode 100644 index 0000000..a12a359 --- /dev/null +++ b/NETTOYAGE_COMPLET.md @@ -0,0 +1,197 @@ +# ๐Ÿงน Rapport de Nettoyage Complet + +**Date** : 13 octobre 2025 +**Projet** : Family Planner + +--- + +## โœ… Nettoyage effectuรฉ + +### 1. **Suppression des fichiers dupliquรฉs** + +#### ChildDetailScreen (4 doublons supprimรฉs) +- โŒ `ChildDetailScreen.backup.js` (9 KB) +- โŒ `ChildDetailScreen.js` (9 KB) +- โŒ `ChildDetailScreen.old.tsx` (9 KB) +- โŒ `ChildDetailScreen.New.js` (36 KB) +- โœ… **Conservรฉ** : `ChildDetailScreen.tsx` (36 KB, le plus rรฉcent) + +**Monaco** : โœ… Vรฉrifiรฉ prรฉsent dans le fichier conservรฉ + +--- + +### 2. **Consolidation de la documentation** + +#### Fichiers dรฉplacรฉs vers `docs/archive/` (18 fichiers) +- `ANALYSE_CODE_CALENDAR.md` +- `BOUTONS_FONCTIONNELS.md` +- `CHANGEMENTS_ERGONOMIE.md` +- `CORRECTIONS_BOUTONS.md` +- `CORRECTIONS_OAUTH.md` +- `IMPROVEMENTS.md` +- `INSTRUCTIONS_PRONOTE.md` +- `INTEGRATION_MONACO.md` +- `MONACO_READY.md` +- `OAUTH_CONFIGURATION_COMPLETE.md` +- `OAUTH_SETUP.md` +- `OPTIMISATION_AFFICHAGE_CONGES.md` +- `PAGES_PROFILS_DETAILLES.md` +- `QUICK_START_OAUTH.md` +- `README_OAUTH_GOOGLE.md` +- `SECURITY_IMPROVEMENTS.md` +- `SOLUTION_MONACO.md` +- `TROUBLESHOOTING.md` + +#### Fichiers conservรฉs ร  la racine +- โœ… `README.md` (mis ร  jour) +- โœ… `PORTS.md` (nouveau) +- โœ… `QUICK_START.md` (nouveau) + +--- + +### 3. **Scripts de dรฉmarrage** + +#### Supprimรฉs (5 scripts obsolรจtes) +- โŒ `start.bat` +- โŒ `start-app.bat` +- โŒ `LANCER_APPLICATION.bat` +- โŒ `REBUILD_FRONTEND.bat` +- โŒ `DEMARRER_TOUT_PROPREMENT.bat` + +#### Conservรฉs/Crรฉรฉs +- โœ… `START.bat` (nouveau, propre et robuste) +- โœ… `STOP.bat` (nouveau) +- โœ… `start-family-planner.ps1` (conservรฉ) +- โœ… `Lancer-Family-Planner.bat` (conservรฉ, ancien mais fonctionnel) + +**Recommandation** : Utiliser `START.bat` (le plus rรฉcent et complet) + +--- + +### 4. **Documentation crรฉรฉe** + +| Fichier | Description | +|---------|-------------| +| **PORTS.md** | Configuration complรจte des ports (5000, 5173, 8000) | +| **QUICK_START.md** | Guide de dรฉmarrage rapide en 3 รฉtapes | +| **START.bat** | Script de dรฉmarrage robuste avec vรฉrifications | +| **STOP.bat** | Script d'arrรชt propre | +| **NETTOYAGE_COMPLET.md** | Ce rapport | + +--- + +## ๐Ÿ“Š Structure aprรจs nettoyage + +``` +family-planner/ +โ”œโ”€โ”€ backend/ # API Node.js (port 5000) +โ”œโ”€โ”€ frontend/ # React App (port 5173) +โ”‚ โ””โ”€โ”€ src/ +โ”‚ โ””โ”€โ”€ screens/ +โ”‚ โ””โ”€โ”€ ChildDetailScreen.tsx โœ… Monaco inclus +โ”œโ”€โ”€ ingestion-service/ # Python OCR (port 8000) +โ”œโ”€โ”€ shared/ # Types et composants partagรฉs +โ”œโ”€โ”€ config/ # Configurations +โ”œโ”€โ”€ docs/ # Documentation +โ”‚ โ””โ”€โ”€ archive/ # ๐Ÿ“ฆ Docs historiques (18 fichiers) +โ”œโ”€โ”€ README.md # โœจ Mis ร  jour avec Monaco +โ”œโ”€โ”€ PORTS.md # โš™๏ธ Configuration ports +โ”œโ”€โ”€ QUICK_START.md # ๐Ÿš€ Guide rapide +โ”œโ”€โ”€ START.bat # โ–ถ๏ธ Dรฉmarrage propre +โ”œโ”€โ”€ STOP.bat # โน๏ธ Arrรชt propre +โ””โ”€โ”€ package.json # Orchestration racine +``` + +--- + +## ๐Ÿ”ง Configuration des ports + +| Service | Port | Configuration | +|---------|------|---------------| +| Backend | 5000 | `backend/src/config/env.ts` | +| Frontend | 5173 | Vite (dรฉfaut) | +| Ingestion | 8000 | Python FastAPI | + +**API URL Frontend** : `http://localhost:5000/api` (dรฉfini dans `frontend/src/services/api-client.ts`) + +--- + +## ๐Ÿ‡ฒ๐Ÿ‡จ Monaco - Statut + +### Backend +โœ… Fichier : `backend/src/services/holiday-service.ts` +- Lignes 55-80 : Vacances scolaires Monaco 2024-2025 +- Lignes 169-212 : Jours fรฉriรฉs Monaco (12 jours) +- **Donnรฉes officielles** : Arrรชtรฉ ministรฉriel nยฐ 2023-221 + +### Frontend +โœ… Fichier : `frontend/src/screens/ChildDetailScreen.tsx` +- Ligne 522 : `monaco: "Monaco"` dans `REGION_LABELS` +- Ligne 934 : Rendu dans le `` avec les zones +3. Regardez si `` existe +4. Si NON โ†’ Problรจme de build frontend + +**Solution si Monaco n'est pas dans le HTML :** +```bash +cd frontend +npm run build +npm run dev +``` + +--- + +## ๐Ÿ“ Checklist complรจte + +- [ ] Backend dรฉmarrรฉ sur port 3000 +- [ ] Frontend dรฉmarrรฉ sur port 5173 +- [ ] Navigateur ouvert sur http://localhost:5173 +- [ ] Cache navigateur vidรฉ (Ctrl + Shift + R) +- [ ] Profil enfant ouvert +- [ ] Menu dรฉroulant "Zone scolaire" ouvert +- [ ] Monaco visible dans la liste + +--- + +## ๐ŸŽฌ Dรฉmarrage simple avec le script + +**Option rapide : Utilisez le script de dรฉmarrage** + +Double-cliquez sur : +``` +C:\Users\philh\OneDrive\Documents\Codes\family-planner\Lancer-Family-Planner.bat +``` + +Cela dรฉmarre automatiquement : +1. Backend sur port 3000 +2. Frontend sur port 5173 + +**Attendez 30 secondes** que tout compile, puis : +- Ouvrez http://localhost:5173 +- Appuyez sur Ctrl + Shift + R + +--- + +## ๐Ÿ› Debug avancรฉ + +### Voir le code compilรฉ + +Dans le navigateur : +1. **F12** โ†’ Sources +2. Cherchez `ChildDetailScreen` +3. Trouvez `REGION_LABELS` +4. Vรฉrifiez si `monaco: "Monaco"` existe + +### Tester l'API directement + +```bash +# Test 1 : Santรฉ du backend +curl http://localhost:3000/api/holidays?region=monaco&year=2025 + +# Test 2 : Doit retourner des donnรฉes JSON avec Monaco +``` + +### Rebuild complet + +Si rien ne marche : +```bash +cd "C:\Users\philh\OneDrive\Documents\Codes\family-planner" + +# Nettoyer +cd frontend +rm -rf node_modules .vite dist +npm install +npm run dev + +# Dans un autre terminal +cd backend +rm -rf dist +npm run build +npm run dev +``` + +--- + +## ๐Ÿ’ก Astuce : Vรฉrifier dans le code source + +Le code est lร  ligne 522 du fichier : +``` +frontend/src/screens/ChildDetailScreen.tsx +``` + +```typescript +const REGION_LABELS = { + "zone-a": "Zone A (...)", + "zone-b": "Zone B (...)", + "zone-c": "Zone C (...)", + corse: "Corse", + monaco: "Monaco", // โ† ICI ! + guadeloupe: "Guadeloupe", + // ... +}; +``` + +Et utilisรฉ ligne 420 : +```typescript +Object.entries(REGION_LABELS).map(([value, label]) => ( + +)) +``` + +**Donc Monaco DOIT apparaรฎtre dans le select !** + +--- + +## โœ… Test final + +Une fois tout dรฉmarrรฉ, vous devriez voir dans le menu dรฉroulant : + +``` +-- Aucune zone sรฉlectionnรฉe -- +Zone A (Besanรงon, Bordeaux, Clermont-Ferrand...) +Zone B (Aix-Marseille, Amiens, Caen...) +Zone C (Crรฉteil, Montpellier, Paris...) +Corse +Monaco โ† ICI ! ๐Ÿ‡ฒ๐Ÿ‡จ +Guadeloupe +Guyane +Martinique +Rรฉunion +Mayotte +``` + +Si vous voyez "Monaco", **รงa marche** ! ๐ŸŽ‰ + +Sรฉlectionnez-le, cliquez "Enregistrer la rรฉgion", et les congรฉs de Monaco s'afficheront. + +--- + +**Date** : 13 octobre 2025 +**Statut** : Le code est correct, juste besoin de dรฉmarrer le backend ! diff --git a/docs/archive/TROUBLESHOOTING.md b/docs/archive/TROUBLESHOOTING.md new file mode 100644 index 0000000..f0c6373 --- /dev/null +++ b/docs/archive/TROUBLESHOOTING.md @@ -0,0 +1,291 @@ +# ๐Ÿ”ง GUIDE DE Dร‰PANNAGE - Family Planner Hub + +## โš ๏ธ PROBLรˆME : "Mes profils d'enfants ont disparu !" + +Si vos profils d'enfants disparaissent aprรจs avoir fermรฉ et relancรฉ l'application, voici pourquoi et comment corriger. + +--- + +## ๐Ÿ•ต๏ธ LES 5 CAUSES PRINCIPALES + +### 1. ๐Ÿ”ด **Multiples serveurs backend en conflit** (90% des cas) + +**Symptรดmes :** +- Les profils apparaissent parfois, disparaissent d'autres fois +- Les modifications ne se sauvegardent pas +- L'application est lente + +**Cause :** +Vous avez lancรฉ le backend plusieurs fois, et plusieurs serveurs tournent sur le port 5000 en mรชme temps. Chacun lit/รฉcrit dans le mรชme fichier `client.json`, crรฉant des conflits. + +**Solution :** +```batch +# Fermer TOUS les serveurs +npx kill-port 5000 + +# Lancer UN SEUL serveur +cd family-planner\backend +npm run dev +``` + +--- + +### 2. ๐ŸŸก **Fichier JSON corrompu** (60% des cas) + +**Symptรดmes :** +- Tous les profils disparaissent d'un coup +- Message d'erreur dans la console du backend + +**Cause :** +- ร‰criture interrompue (coupure de courant, crash) +- Plusieurs serveurs รฉcrivent en mรชme temps +- Antivirus qui bloque le fichier + +**Solution :** +1. Vรฉrifiez si une sauvegarde existe : + ``` + backend\src\data\client.json.backup + backend\src\data\client.json.backup.1 + backend\src\data\client.json.backup.2 + ... + ``` + +2. Restaurez la derniรจre sauvegarde : + ```batch + cd family-planner\backend\src\data + copy client.json.backup client.json + ``` + +3. Relancez le backend + +--- + +### 3. ๐ŸŸก **Cache navigateur** (50% des cas) + +**Symptรดmes :** +- Les profils sont lร  dans la base de donnรฉes mais pas dans l'interface +- F5 ne rรฉsout pas le problรจme + +**Solution :** +1. Ouvrez la console du navigateur (F12) +2. Tapez : + ```javascript + localStorage.clear() + ``` +3. Rechargez la page (Ctrl+F5) + +--- + +### 4. ๐ŸŸก **Race condition au dรฉmarrage** (70% des cas) + +**Symptรดmes :** +- Les profils n'apparaissent pas au premier lancement +- Ils apparaissent aprรจs un F5 + +**Cause :** +Le frontend dรฉmarre avant que le backend ne soit prรชt. La premiรจre requรชte รฉchoue. + +**Solution :** +Utilisez les scripts `start.bat` et `stop.bat` fournis. Ils s'assurent que le backend est prรชt avant de lancer le frontend. + +--- + +### 5. ๐ŸŸก **Erreur silencieuse** (40% des cas) + +**Symptรดmes :** +- Rien ne se passe, pas d'erreur visible +- Les profils ne chargent pas + +**Cause :** +Le backend n'est pas lancรฉ, ou il y a une erreur rรฉseau. + +**Solution :** +1. Vรฉrifiez que le backend tourne : + - Ouvrez http://localhost:5000/api/children dans votre navigateur + - Vous devriez voir un JSON avec vos enfants + +2. Si erreur, regardez la console du backend + +--- + +## ๐Ÿš€ UTILISATION CORRECTE + +### โœ… LANCEMENT (Mร‰THODE RECOMMANDร‰E) + +1. **Double-cliquez sur `start.bat`** + - Ce script : + - Tue les anciens processus + - Crรฉe une sauvegarde + - Lance le backend + - Attend 6 secondes + - Lance le frontend + +2. **Attendez** les 2 fenรชtres : + - Une fenรชtre Backend (bleue) + - Une fenรชtre Frontend (violette) + +3. **Ouvrez votre navigateur** sur http://localhost:5173 + +### โœ… ARRรŠT PROPRE + +1. **Double-cliquez sur `stop.bat`** + - Ce script ferme proprement les 2 serveurs + - Les donnรฉes sont sauvegardรฉes automatiquement + +### โŒ ร€ NE PAS FAIRE + +- โŒ Lancer plusieurs fois le backend +- โŒ Fermer la fenรชtre en cliquant sur la croix +- โŒ Ouvrir plusieurs onglets sur l'application +- โŒ Faire Ctrl+C dans les terminaux sans utiliser stop.bat + +--- + +## ๐Ÿ›ก๏ธ SYSTรˆME DE PROTECTION + +### Sauvegardes automatiques + +ร€ chaque modification, **5 sauvegardes** sont crรฉรฉes : +``` +backend\src\data\ + โ”œโ”€โ”€ client.json โ† Fichier actif + โ”œโ”€โ”€ client.json.backup โ† Derniรจre sauvegarde rapide + โ”œโ”€โ”€ client.json.backup.1 โ† Sauvegarde -1 + โ”œโ”€โ”€ client.json.backup.2 โ† Sauvegarde -2 + โ”œโ”€โ”€ client.json.backup.3 โ† Sauvegarde -3 + โ”œโ”€โ”€ client.json.backup.4 โ† Sauvegarde -4 + โ””โ”€โ”€ client.json.backup.5 โ† Sauvegarde -5 +``` + +### Restauration automatique + +Si le fichier est corrompu, le backend essaie automatiquement de restaurer depuis `client.json.backup`. + +### Logs amรฉliorรฉs + +Le backend affiche maintenant : +- โœ… Succรจs : nombre d'enfants/parents chargรฉs +- โš ๏ธ Avertissements : structure invalide, backup crรฉรฉ +- โŒ Erreurs : dรฉtails de l'erreur avec stack trace + +Le frontend affiche dans la console : +- โœ… "X enfants chargรฉs avec succรจs" +- ๐Ÿ”„ "Retry du chargement des enfants..." +- โŒ "Erreur chargement enfants: [dรฉtails]" + +--- + +## ๐Ÿ” DIAGNOSTIC RAPIDE + +### Vรฉrifier l'รฉtat du systรจme + +1. **Backend actif ?** + ``` + http://localhost:5000/api/children + ``` + - Si รงa marche : vous voyez un JSON + - Si erreur : backend pas lancรฉ + +2. **Donnรฉes prรฉsentes ?** + ``` + Ouvrez: backend\src\data\client.json + ``` + - Recherchez "children" : il doit y avoir des enfants dedans + +3. **Plusieurs serveurs ?** + ```batch + netstat -ano | findstr :5000 + ``` + - Si vous voyez plusieurs lignes : PROBLรˆME! + - Solution : `npx kill-port 5000` + +--- + +## ๐Ÿ“ž EN CAS DE PROBLรˆME + +### 1. Tentative de rรฉcupรฉration automatique + +```batch +# 1. Arrรชter proprement +stop.bat + +# 2. Tuer tous les processus +npx kill-port 5000 +npx kill-port 5173 + +# 3. Relancer proprement +start.bat +``` + +### 2. Restauration manuelle + +```batch +# Aller dans le dossier data +cd family-planner\backend\src\data + +# Lister les sauvegardes +dir client.json.* + +# Restaurer la derniรจre sauvegarde +copy client.json.backup client.json + +# Relancer +cd ..\..\.. +start.bat +``` + +### 3. Vรฉrification de la base de donnรฉes + +```batch +# Afficher le contenu de client.json +type family-planner\backend\src\data\client.json +``` + +Vรฉrifiez : +- Le JSON est-il valide (accolades, virgules) ? +- Y a-t-il des enfants dans "children": [] ? +- La structure est-elle correcte ? + +--- + +## ๐Ÿ’ก CONSEILS DE PRร‰VENTION + +1. **Toujours utiliser start.bat et stop.bat** +2. **N'ouvrez qu'UN SEUL onglet** de l'application +3. **Attendez** que le backend soit prรชt avant d'ouvrir le frontend +4. **Vรฉrifiez les logs** en cas de comportement รฉtrange +5. **Faites des sauvegardes manuelles** rรฉguliรจrement : + ```batch + copy backend\src\data\client.json backup_YYYYMMDD.json + ``` + +--- + +## ๐Ÿ“Š MESSAGES D'ERREUR COURANTS + +| Message | Cause | Solution | +|---------|-------|----------| +| `API error 500` | Backend crashรฉ | Regarder les logs backend | +| `Impossible de charger les profils` | Backend pas lancรฉ | Vรฉrifier que port 5000 rรฉpond | +| `Empty response` | Backend pas prรชt | Attendre 5 secondes et F5 | +| `EADDRINUSE` | Port dรฉjร  utilisรฉ | `npx kill-port 5000` | +| `JSON.parse error` | Fichier corrompu | Restaurer depuis backup | + +--- + +## ๐ŸŽฏ CHECKLIST DE LANCEMENT + +Avant chaque session : + +- [ ] Fermer tous les anciens processus (stop.bat) +- [ ] Vรฉrifier que client.json existe et n'est pas vide +- [ ] Lancer avec start.bat +- [ ] Attendre "Server ready on port 5000" +- [ ] Attendre "Local: http://localhost:5173" +- [ ] Ouvrir http://localhost:5173 dans UN SEUL onglet +- [ ] Vรฉrifier dans la console : "X enfants chargรฉs avec succรจs" + +--- + +**Derniรจre mise ร  jour : 2025-10-13** +**Version du guide : 1.0** diff --git a/docs/data-contracts.md b/docs/data-contracts.md new file mode 100644 index 0000000..6efe839 --- /dev/null +++ b/docs/data-contracts.md @@ -0,0 +1,142 @@ +# Data contracts + +## API payloads + +### POST `/api/children` + +```json +{ + "fullName": "Alice Durand", + "colorHex": "#FF7F50", + "birthDate": "2016-09-12", + "notes": "Allergie aux arachides" +} +``` + +**Response** + +```json +{ + "id": "ch_001", + "fullName": "Alice Durand", + "colorHex": "#FF7F50", + "birthDate": "2016-09-12", + "notes": "Allergie aux arachides", + "createdAt": "2025-10-11T08:00:00Z" +} +``` + +### POST `/api/children/{id}/schedules` + +Multipart upload (`file`, `metadata`). + +`metadata` example: + +```json +{ + "periodStart": "2025-10-13", + "periodEnd": "2025-10-19", + "tags": ["ecole", "sport"] +} +``` + +**Response** + +```json +{ + "scheduleId": "sc_101", + "status": "processing", + "childId": "ch_001" +} +``` + +### GET `/api/children/{id}/schedules/{scheduleId}` + +```json +{ + "id": "sc_101", + "childId": "ch_001", + "periodStart": "2025-10-13", + "periodEnd": "2025-10-19", + "activities": [ + { + "id": "ac_500", + "title": "Piscine", + "category": "sport", + "startDateTime": "2025-10-14T17:00:00Z", + "endDateTime": "2025-10-14T18:00:00Z", + "location": "Centre aquatique", + "reminders": [ + { + "id": "re_910", + "offsetMinutes": 120, + "channel": "push" + } + ], + "metadata": { + "bring": "maillot, bonnet" + } + } + ], + "sourceFileUrl": "https://storage/planning.pdf", + "status": "ready" +} +``` + +## Ingestion contract + +### Request + +```json +{ + "scheduleId": "sc_101", + "childId": "ch_001", + "filePath": "s3://bucket/planning.pdf", + "options": { + "language": "fr", + "timezone": "Europe/Paris" + } +} +``` + +### Response + +```json +{ + "scheduleId": "sc_101", + "status": "completed", + "activities": [ + { + "title": "Gym", + "startDate": "2025-10-15", + "startTime": "17:30", + "endTime": "18:30", + "category": "sport", + "confidence": 0.88, + "reminders": [ + {"offsetMinutes": 60, "channel": "push"} + ], + "notes": "Tenue sportive + bouteille d eau" + } + ], + "warnings": [ + "Could not detect teacher name" + ], + "rawText": "Mardi: Gym 17h30-18h30..." +} +``` + +## WebSocket feed (future) + +```json +{ + "type": "schedule.updated", + "payload": { + "scheduleId": "sc_101", + "childId": "ch_001", + "updates": [ + {"activityId": "ac_500", "field": "startDateTime", "value": "2025-10-14T16:50:00Z"} + ] + } +} +``` diff --git a/docs/product-vision.md b/docs/product-vision.md new file mode 100644 index 0000000..606aec7 --- /dev/null +++ b/docs/product-vision.md @@ -0,0 +1,64 @@ +# Vision produit + +## Contexte + +Les familles jonglent avec des plannings differents (ecole, garderie, activites sportives, rendez-vous medicaux). Les documents sont souvent disperses (PDF envoyes par mail, photos affichees sur le frigo, Excel partages). L objectif est de construire un hub unique pour centraliser, analyser et diffuser ces informations. + +## Personas + +- **Parent organise**: souhaite une vision globale de qui fait quoi et quand, sur plusieurs appareils. +- **Parent qui improvise**: prise en main rapide, notifications des evenements importants le jour J. +- **Enfant autonome**: consulte son planning sur tablette ou ecran mural. +- **Assistant familial**: gere les transports, synchronise le planning avec son propre agenda. + +## Objectifs + +1. Rendre l import des plannings quasi automatique. +2. Offrir une interface claire et inspiree des tableaux magetiques/kanban familiaux. +3. Mettre en avant les alertes critiques (sport avec sac, sortie scolaire, devoirs). +4. Supporter le multi ecran et un mode plein ecran d un clic. +5. Faciliter la collaboration (ajout d annotations, checklist des affaires a preparer). + +## Fonctionnalites prioritaires + +1. **Gestion des enfants** + - Creation rapide (nom, couleur, icone). + - Option de notes (allergies, infos importantes). +2. **Import planning** + - Dropzone multi format (PDF, image, XLSX). + - Historique par enfant. +3. **Lecture intelligente** + - OCR + detection heuristique (mots cle: piscine, gym, sortie). + - Correction manuelle en ligne. +4. **Vue planning** + - Semaine et journee avec code couleur. + - Mode timeline ou grille. + - Mode plein ecran. +5. **Alertes** + - Notification push/email. + - Rappel la veille et le jour J. +6. **Partage** + - Lien lecteur ou QR code pour affichage sur un autre ecran. + +## Roadmap initiale + +| Phase | Objectifs | Livrables | +| --- | --- | --- | +| Alpha | Import manuel + edition manuelle | CRUD enfants, planning visuel, mode plein ecran | +| Beta | Import OCR semi automatique | Pipeline ingestion, detection mots cle, alertes basiques | +| Release | Automatisation et multi device | PWA, synchronisation agenda, notifications completes | + +## Principes UX + +- Zero friction: chaque action en < 3 clics. +- Toutes les infos critiques visibles dans la vue hebdomadaire. +- Code couleur constant par enfant. +- Mode plein ecran accessible depuis clavier/touch (F11, double tap). +- Feedback visuel et sonore lors des alertes. + +## KPIs success + +- Temps moyen pour ajouter un nouveau planning (< 2 minutes). +- Nombre de corrections manuelles post OCR (objectif < 5 par document). +- Satisfaction des alertes (>= 90% evenements importants captes). +- Adherance multi device (au moins 2 appareils actifs par foyer). diff --git a/docs/ux-flow.md b/docs/ux-flow.md new file mode 100644 index 0000000..f556375 --- /dev/null +++ b/docs/ux-flow.md @@ -0,0 +1,50 @@ +# Parcours UX + +## Hub principal + +1. Arrivee sur la page Dashboard. +2. Bandeau superieur: selection des enfants (chips colorees). +3. Zone centrale: vue agenda semaine avec colonnes par enfant. +4. Barre laterale: alertes a venir, documents recents. +5. Bouton `Plein ecran` fixe en bas a droite. + +## Ajout d un enfant + +1. Bouton `Ajouter un enfant` -> modal. +2. Formulaire minimal: prenom + nom, couleur, notes. +3. Apercu avatar (initiales + couleur). +4. Validation -> creation enfant -> scroll vers enfant nouvellement cree. + +## Import de planning + +1. Bouton `Importer un planning`. +2. Dropzone avec glisser deposer ou navigation fichiers. +3. Metadonnees optionnelles: periode couverte, commentaires. +4. Une fois envoye -> affichage etat `Analyse en cours` avec spinner. +5. Notification system toastee quand l analyse est terminee. + +## Edition du planning + +1. Cliquer sur un bloc activite -> panneau lateral. +2. Champs editables: titre, horaire, lieu, note, rappels. +3. Bouton `Marquer comme important` -> alerte automatique. +4. Historique des modifications (timeline simple). + +## Mode plein ecran + +- Activation via bouton ou touche `F`. +- Cache la navigation, agrandit les colonnes, police plus grande. +- Timer auto refresh (toutes les 5 minutes) affiche en haut a droite. + +## Multi ecran + +1. Bouton `Partager` -> QR code + lien. +2. Option `Afficher sur ecran externe` -> suggestions (Chromecast, AirPlay, HDMI). +3. Mode lecteur: read-only, auto refresh, theme clair/fonce. + +## Alertes + +1. Reglages au niveau enfant (par defaut: veille 19h + jour J 7h). +2. Possibilite d ajouter un rappel custom par activite. +3. Page `Alertes` liste toutes les notifications a venir. +4. Actions rapides: confirmer, snoozer, marquer comme resolu. diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs new file mode 100644 index 0000000..912dad4 --- /dev/null +++ b/frontend/.eslintrc.cjs @@ -0,0 +1,29 @@ +module.exports = { + root: true, + parser: "@typescript-eslint/parser", + parserOptions: { + ecmaVersion: 2020, + sourceType: "module" + }, + settings: { + react: { + version: "detect" + } + }, + env: { + browser: true, + es2021: true + }, + plugins: ["react", "@typescript-eslint", "react-hooks"], + extends: [ + "eslint:recommended", + "plugin:react/recommended", + "plugin:@typescript-eslint/recommended", + "plugin:react-hooks/recommended", + "prettier" + ], + rules: { + "react/react-in-jsx-scope": "off", + "@typescript-eslint/explicit-module-boundary-types": "off" + } +}; diff --git a/frontend/.prettierrc b/frontend/.prettierrc new file mode 100644 index 0000000..c982845 --- /dev/null +++ b/frontend/.prettierrc @@ -0,0 +1,7 @@ +{ + "singleQuote": false, + "trailingComma": "none", + "tabWidth": 2, + "printWidth": 90, + "semi": true +} diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..444c7ae --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + Family Planner Hub + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..b14ca6f --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,45 @@ +{ + "name": "family-planner-frontend", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "test": "vitest", + "test:ui": "vitest --ui", + "test:run": "vitest run", + "test:coverage": "vitest run --coverage", + "lint": "eslint \"src/**/*.{ts,tsx}\" --max-warnings=0", + "format": "prettier --write \"src/**/*.{ts,tsx,css}\"" + }, + "dependencies": { + "@tanstack/react-query": "^5.0.0", + "react": "^18.3.0", + "react-dom": "^18.3.0", + "react-router-dom": "^6.25.0", + "styled-components": "^6.1.0" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", + "@types/node": "^20.11.0", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^7.0.0", + "@typescript-eslint/parser": "^7.0.0", + "@vitejs/plugin-react": "^4.3.0", + "@vitest/ui": "^3.2.4", + "eslint": "^9.0.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-react": "^7.34.0", + "eslint-plugin-react-hooks": "^5.0.0", + "jsdom": "^27.0.0", + "prettier": "^3.3.0", + "typescript": "^5.6.0", + "vite": "^5.1.0", + "vitest": "^3.2.4" + } +} diff --git a/frontend/src/App.js b/frontend/src/App.js new file mode 100644 index 0000000..14f6699 --- /dev/null +++ b/frontend/src/App.js @@ -0,0 +1,17 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { Route, Routes } from "react-router-dom"; +import { Layout } from "./components/Layout"; +import { DashboardScreen } from "./screens/DashboardScreen"; +import { SettingsScreen } from "./screens/SettingsScreen"; +import { AddPersonScreen } from "./screens/AddPersonScreen"; +import { ChildPlanningScreen } from "./screens/ChildPlanningScreen"; +import { MonthlyCalendarScreen } from "./screens/MonthlyCalendarScreen"; +import { ParentsScreen } from "./screens/ParentsScreen"; +import { PersonPlanningScreen } from "./screens/PersonPlanningScreen"; +import { CalendarOAuthCallbackScreen } from "./screens/CalendarOAuthCallbackScreen"; +import { ChildDetailScreen } from "./screens/ChildDetailScreen"; +import { AdultDetailScreen } from "./screens/AdultDetailScreen"; +const App = () => { + return (_jsx(Layout, { children: _jsxs(Routes, { children: [_jsx(Route, { path: "/", element: _jsx(DashboardScreen, {}) }), _jsx(Route, { path: "/profiles", element: _jsx(ParentsScreen, {}) }), _jsx(Route, { path: "/profiles/new", element: _jsx(AddPersonScreen, {}) }), _jsx(Route, { path: "/profiles/child/:childId", element: _jsx(ChildDetailScreen, {}) }), _jsx(Route, { path: "/profiles/:profileType/:profileId", element: _jsx(AdultDetailScreen, {}) }), _jsx(Route, { path: "/profiles/:profileType/:profileId/planning", element: _jsx(PersonPlanningScreen, {}) }), _jsx(Route, { path: "/children/:childId/planning", element: _jsx(ChildPlanningScreen, {}) }), _jsx(Route, { path: "/calendar/month", element: _jsx(MonthlyCalendarScreen, {}) }), _jsx(Route, { path: "/calendar/oauth/callback", element: _jsx(CalendarOAuthCallbackScreen, {}) }), _jsx(Route, { path: "/settings", element: _jsx(SettingsScreen, {}) })] }) })); +}; +export default App; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..002047d --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,42 @@ +๏ปฟimport { Route, Routes } from "react-router-dom"; +import { Layout } from "./components/Layout"; +import { DashboardScreen } from "./screens/DashboardScreen"; +import { SettingsScreen } from "./screens/SettingsScreen"; +import { AddPersonScreen } from "./screens/AddPersonScreen"; +import { ChildPlanningScreen } from "./screens/ChildPlanningScreen"; +import { MonthlyCalendarScreen } from "./screens/MonthlyCalendarScreen"; +import { ParentsScreen } from "./screens/ParentsScreen"; +import { PersonPlanningScreen } from "./screens/PersonPlanningScreen"; +import { CalendarOAuthCallbackScreen } from "./screens/CalendarOAuthCallbackScreen"; +import { ChildDetailScreen } from "./screens/ChildDetailScreen"; +import { ParentDetailScreen } from "./screens/ParentDetailScreen"; +import { GrandParentDetailScreen } from "./screens/GrandParentDetailScreen"; +import { AdultDetailScreen } from "./screens/AdultDetailScreen"; + +const App = () => { + return ( + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + ); +}; + +export default App; + + + + + diff --git a/frontend/src/components/ChildCard.js b/frontend/src/components/ChildCard.js new file mode 100644 index 0000000..fde7b52 --- /dev/null +++ b/frontend/src/components/ChildCard.js @@ -0,0 +1,77 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import styled from "styled-components"; +import { useMemo } from "react"; +import { ProfileActionBar } from "./ProfileActionBar"; +const Card = styled.article ` + padding: 18px 20px; + border-radius: 16px; + background: rgba(29, 36, 66, 0.92); + border: 1px solid rgba(126, 136, 180, 0.22); + display: flex; + gap: 18px; + align-items: center; + transition: transform 0.2s ease, box-shadow 0.2s ease; + cursor: ${({ $clickable }) => ($clickable ? "pointer" : "default")}; + + &:hover { + transform: translateY(-2px); + box-shadow: 0 12px 32px ${({ $color }) => `${$color}33`}; + } +`; +const Avatar = styled.div ` + width: 56px; + height: 56px; + border-radius: 18px; + background: rgba(9, 13, 28, 0.8); + border: 2px solid ${({ $color }) => $color}; + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; + color: #f4f5ff; + font-size: 1.1rem; + overflow: hidden; + box-shadow: 0 0 12px ${({ $color }) => `${$color}55`}; +`; +const AvatarImage = styled.img ` + width: 100%; + height: 100%; + object-fit: cover; +`; +const Content = styled.div ` + display: flex; + flex-direction: column; + gap: 6px; +`; +const Name = styled.span ` + font-size: 1.1rem; + font-weight: 600; +`; +const Email = styled.span ` + color: var(--text-muted); + font-size: 0.9rem; +`; +const Notes = styled.span ` + color: var(--text-muted); + font-size: 0.95rem; +`; +export const ChildCard = ({ child, onDelete, onEdit, onViewProfile, onViewPlanning, onOpenPlanningCenter, importing, connectionsCount }) => { + const initials = useMemo(() => child.fullName + .split(" ") + .map((part) => part.charAt(0)) + .join("") + .slice(0, 2) + .toUpperCase(), [child.fullName]); + const handleCardClick = (e) => { + // Ne pas dรฉclencher si on clique sur un bouton dans la ProfileActionBar + const target = e.target; + if (target.closest("button")) { + return; + } + // Toute la carte est cliquable pour accรฉder au profil + if (onViewProfile) { + onViewProfile(child.id); + } + }; + return (_jsxs(Card, { "$color": child.colorHex, "$clickable": !!onViewProfile, onClick: handleCardClick, children: [_jsx(Avatar, { "$color": child.colorHex, children: child.avatar ? (_jsx(AvatarImage, { src: child.avatar.url, alt: child.avatar.name ?? `Portrait de ${child.fullName}` })) : (initials) }), _jsxs(Content, { children: [_jsx(Name, { children: child.fullName }), child.email && _jsx(Email, { children: child.email }), child.notes && _jsx(Notes, { children: child.notes })] }), _jsx(ProfileActionBar, { onViewProfile: onViewProfile ? () => onViewProfile(child.id) : undefined })] })); +}; diff --git a/frontend/src/components/ChildCard.tsx b/frontend/src/components/ChildCard.tsx new file mode 100644 index 0000000..22bedfd --- /dev/null +++ b/frontend/src/components/ChildCard.tsx @@ -0,0 +1,133 @@ +import styled from "styled-components"; +import { useMemo } from "react"; +import { ChildProfile } from "@family-planner/types"; +import { ProfileActionBar } from "./ProfileActionBar"; + +type ChildCardProps = { + child: ChildProfile; + onDelete?: (id: string) => void; + onEdit?: (id: string) => void; + onViewProfile?: (id: string) => void; + onViewPlanning?: (id: string) => void; + onOpenPlanningCenter?: (child: ChildProfile) => void; + importing?: boolean; + connectionsCount?: number; +}; + +const Card = styled.article<{ $color: string; $clickable?: boolean }>` + padding: 18px 20px; + border-radius: 16px; + background: rgba(29, 36, 66, 0.92); + border: 1px solid rgba(126, 136, 180, 0.22); + display: flex; + gap: 18px; + align-items: center; + transition: transform 0.2s ease, box-shadow 0.2s ease; + cursor: ${({ $clickable }) => ($clickable ? "pointer" : "default")}; + + &:hover { + transform: translateY(-2px); + box-shadow: 0 12px 32px ${({ $color }) => `${$color}33`}; + } +`; + +const Avatar = styled.div<{ $color: string }>` + width: 56px; + height: 56px; + border-radius: 18px; + background: rgba(9, 13, 28, 0.8); + border: 2px solid ${({ $color }) => $color}; + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; + color: #f4f5ff; + font-size: 1.1rem; + overflow: hidden; + box-shadow: 0 0 12px ${({ $color }) => `${$color}55`}; +`; + +const AvatarImage = styled.img` + width: 100%; + height: 100%; + object-fit: cover; +`; + +const Content = styled.div` + display: flex; + flex-direction: column; + gap: 6px; +`; + +const Name = styled.span` + font-size: 1.1rem; + font-weight: 600; +`; + +const Email = styled.span` + color: var(--text-muted); + font-size: 0.9rem; +`; + +const Notes = styled.span` + color: var(--text-muted); + font-size: 0.95rem; +`; + +export const ChildCard = ({ + child, + onDelete, + onEdit, + onViewProfile, + onViewPlanning, + onOpenPlanningCenter, + importing, + connectionsCount +}: ChildCardProps) => { + const initials = useMemo( + () => + child.fullName + .split(" ") + .map((part: string) => part.charAt(0)) + .join("") + .slice(0, 2) + .toUpperCase(), + [child.fullName] + ); + + const handleCardClick = (e: React.MouseEvent) => { + // Ne pas dรฉclencher si on clique sur un bouton dans la ProfileActionBar + const target = e.target as HTMLElement; + if (target.closest("button")) { + return; + } + // Toute la carte est cliquable pour accรฉder au profil + if (onViewProfile) { + onViewProfile(child.id); + } + }; + + return ( + + + {child.avatar ? ( + + ) : ( + initials + )} + + + {child.fullName} + {child.email && {child.email}} + {child.notes && {child.notes}} + + onViewProfile(child.id) : undefined} + /> + + ); +}; diff --git a/frontend/src/components/ChildProfilePanel.js b/frontend/src/components/ChildProfilePanel.js new file mode 100644 index 0000000..7f9447d --- /dev/null +++ b/frontend/src/components/ChildProfilePanel.js @@ -0,0 +1,394 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useEffect, useMemo, useRef, useState } from "react"; +import styled from "styled-components"; +import { useChildren } from "../state/ChildrenContext"; +import { uploadAvatar, listAvatars } from "../services/api-client"; +const Panel = styled.aside ` + flex: 1; + padding: 24px; + border-radius: 18px; + background: rgba(29, 36, 66, 0.92); + border: 1px solid rgba(126, 136, 180, 0.22); + display: flex; + flex-direction: column; + gap: 20px; +`; +const Title = styled.h2 ` + margin: 0; +`; +const Description = styled.p ` + margin: 0; + color: var(--text-muted); +`; +const Form = styled.form ` + display: flex; + flex-direction: column; + gap: 18px; +`; +const Label = styled.label ` + display: flex; + flex-direction: column; + gap: 8px; + font-weight: 600; +`; +const Row = styled.div ` + display: flex; + gap: 12px; + flex-wrap: wrap; +`; +const BaseInput = styled.input ` + padding: 12px 14px; + border-radius: 12px; + border: 1px solid rgba(126, 136, 180, 0.28); + background: rgba(16, 22, 52, 0.9); + color: #ffffff; +`; +const TextArea = styled.textarea ` + padding: 12px 14px; + border-radius: 12px; + border: 1px solid rgba(126, 136, 180, 0.28); + min-height: 96px; + background: rgba(16, 22, 52, 0.9); + color: #ffffff; +`; +const SubmitButton = styled.button ` + padding: 12px 16px; + border-radius: 12px; + border: none; + background: ${({ $loading }) => $loading ? "rgba(85, 98, 255, 0.25)" : "linear-gradient(135deg, #5562ff, #7d6cff)"}; + color: #ffffff; + font-weight: 600; + cursor: ${({ $loading }) => ($loading ? "progress" : "pointer")}; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + transition: opacity 0.2s ease; + opacity: ${({ $loading }) => ($loading ? 0.7 : 1)}; +`; +const SecondaryButton = styled.button ` + padding: 12px 16px; + border-radius: 12px; + border: 1px solid rgba(126, 136, 180, 0.4); + background: rgba(16, 22, 52, 0.9); + color: #d9dcff; + font-weight: 600; + cursor: pointer; + transition: transform 0.2s ease; + + &:hover { + transform: translateY(-1px); + } +`; +const AvatarSection = styled.section ` + display: flex; + flex-direction: column; + gap: 16px; +`; +const AvatarHeader = styled.div ` + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +`; +const AvatarPreview = styled.div ` + display: flex; + align-items: center; + gap: 14px; + padding: 12px 14px; + border-radius: 14px; + background: rgba(12, 18, 42, 0.8); + border: 1px solid rgba(126, 136, 180, 0.24); +`; +const AvatarImage = styled.img ` + width: 64px; + height: 64px; + border-radius: 18px; + object-fit: cover; + border: 2px solid rgba(126, 136, 180, 0.35); +`; +const AvatarFallback = styled.div ` + width: 64px; + height: 64px; + border-radius: 18px; + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; + font-size: 1.4rem; + background: ${({ $color }) => $color}; + color: #040411; +`; +const AvatarInfo = styled.div ` + display: flex; + flex-direction: column; + gap: 4px; + color: var(--text-muted); +`; +const AvatarPicker = styled.div ` + border-radius: 16px; + background: rgba(10, 14, 34, 0.6); + border: 1px solid rgba(148, 156, 210, 0.24); + padding: 16px; + display: flex; + flex-direction: column; + gap: 16px; +`; +const GalleryGrid = styled.div ` + display: grid; + grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); + gap: 12px; +`; +const GalleryItem = styled.button ` + border: ${({ $selected }) => ($selected ? "2px solid #7d6cff" : "1px solid rgba(126,136,180,0.3)")}; + border-radius: 14px; + background: rgba(16, 22, 52, 0.85); + padding: 6px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: transform 0.2s ease, border 0.2s ease; + + &:hover { + transform: translateY(-2px); + } +`; +const GalleryThumbnail = styled.img ` + width: 100%; + height: 100%; + border-radius: 10px; + object-fit: cover; +`; +const Helper = styled.span ` + font-size: 0.85rem; + color: var(--text-muted); +`; +const ErrorText = styled.span ` + font-size: 0.85rem; + color: #ff7b8a; +`; +const StatusMessage = styled.div ` + padding: 12px; + border-radius: 12px; + background: rgba(12, 18, 42, 0.7); + border: 1px solid rgba(126, 136, 180, 0.24); + color: var(--text-muted); + font-size: 0.9rem; +`; +const DEFAULT_COLOR = "#5562ff"; +export const ChildProfilePanel = ({ mode = "create", child, onCancel }) => { + const isEdit = mode === "edit" && !!child; + const { createChild, updateChild } = useChildren(); + const [fullName, setFullName] = useState(child?.fullName ?? ""); + const [colorHex, setColorHex] = useState(child?.colorHex ?? DEFAULT_COLOR); + const [email, setEmail] = useState(child?.email ?? ""); + const [notes, setNotes] = useState(child?.notes ?? ""); + const [avatarSelection, setAvatarSelection] = useState(null); + const [removeExistingAvatar, setRemoveExistingAvatar] = useState(false); + const [avatarPickerOpen, setAvatarPickerOpen] = useState(false); + const [gallery, setGallery] = useState([]); + const [galleryLoading, setGalleryLoading] = useState(false); + const [galleryError, setGalleryError] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(null); + const fileInputRef = useRef(null); + useEffect(() => { + if (avatarSelection?.source === "upload") { + return () => { + URL.revokeObjectURL(avatarSelection.previewUrl); + }; + } + return undefined; + }, [avatarSelection]); + useEffect(() => { + if (!gallery.length) { + setGalleryLoading(true); + listAvatars() + .then((items) => { + setGallery(items); + setGalleryError(null); + }) + .catch(() => { + setGalleryError("Impossible de charger la galerie locale."); + }) + .finally(() => setGalleryLoading(false)); + } + }, [gallery.length]); + useEffect(() => { + if (isEdit && child) { + setFullName(child.fullName); + setColorHex(child.colorHex); + setEmail(child.email ?? ""); + setNotes(child.notes ?? ""); + setAvatarSelection(null); + setRemoveExistingAvatar(false); + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + setError(null); + } + else if (!isEdit) { + setFullName(""); + setColorHex(DEFAULT_COLOR); + setEmail(""); + setNotes(""); + setAvatarSelection(null); + setRemoveExistingAvatar(false); + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + setError(null); + } + }, [isEdit, child]); + const initials = useMemo(() => { + return fullName + .split(" ") + .filter(Boolean) + .map((part) => part.charAt(0)) + .join("") + .slice(0, 2) + .toUpperCase(); + }, [fullName]); + const currentAvatarUrl = useMemo(() => { + if (avatarSelection) { + return avatarSelection.source === "upload" ? avatarSelection.previewUrl : avatarSelection.url; + } + if (removeExistingAvatar) { + return null; + } + return child?.avatar?.url ?? null; + }, [avatarSelection, child?.avatar, removeExistingAvatar]); + const currentAvatarLabel = useMemo(() => { + if (avatarSelection) { + return avatarSelection.source === "upload" + ? avatarSelection.name + : avatarSelection.name ?? "Avatar local"; + } + if (removeExistingAvatar) { + return "Aucun avatar selectionne"; + } + return child?.avatar?.name ?? "Aucun avatar selectionne"; + }, [avatarSelection, child?.avatar, removeExistingAvatar]); + const handleFileChange = (event) => { + const file = event.target.files?.[0]; + if (!file) { + return; + } + if (!file.type.startsWith("image/")) { + setError("Le fichier doit etre une image (png, jpg, svg...)."); + return; + } + setAvatarSelection({ + source: "upload", + file, + previewUrl: URL.createObjectURL(file), + name: file.name + }); + setRemoveExistingAvatar(false); + setError(null); + }; + const handleSelectGallery = (item) => { + setAvatarSelection({ + source: "gallery", + url: item.url, + name: item.filename + }); + setRemoveExistingAvatar(false); + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + }; + const handleClearAvatar = () => { + if (avatarSelection?.source === "upload") { + URL.revokeObjectURL(avatarSelection.previewUrl); + } + setAvatarSelection(null); + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + setRemoveExistingAvatar(isEdit); + }; + const handleSubmit = async (event) => { + event.preventDefault(); + if (isSubmitting) { + return; + } + if (!fullName.trim()) { + setError("Merci de saisir le nom complet de l enfant."); + return; + } + const payload = { + fullName: fullName.trim(), + colorHex, + email: email.trim() ? email.trim() : undefined, + notes: notes.trim() ? notes.trim() : undefined + }; + setIsSubmitting(true); + setError(null); + try { + let avatarPayload = undefined; + if (avatarSelection?.source === "upload") { + const uploaded = await uploadAvatar(avatarSelection.file); + avatarPayload = { + kind: "custom", + url: uploaded.url, + name: avatarSelection.name + }; + URL.revokeObjectURL(avatarSelection.previewUrl); + setAvatarSelection({ + source: "gallery", + url: uploaded.url, + name: avatarSelection.name + }); + } + else if (avatarSelection?.source === "gallery") { + avatarPayload = { + kind: "custom", + url: avatarSelection.url, + name: avatarSelection.name + }; + } + else if (isEdit && child?.avatar && !removeExistingAvatar) { + avatarPayload = child.avatar; + } + else if (removeExistingAvatar) { + avatarPayload = null; + } + if (avatarPayload !== undefined) { + payload.avatar = avatarPayload ?? undefined; + } + if (isEdit && child) { + await updateChild(child.id, payload); + onCancel?.(); + } + else { + await createChild(payload); + setFullName(""); + setColorHex(DEFAULT_COLOR); + setEmail(""); + setNotes(""); + setAvatarSelection(null); + setRemoveExistingAvatar(false); + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + } + } + catch (err) { + setError("Impossible d enregistrer pour le moment. Merci de reessayer."); + } + finally { + setIsSubmitting(false); + } + }; + return (_jsxs(Panel, { children: [_jsx(Title, { children: isEdit ? "Modifier l enfant" : "Ajouter un enfant" }), _jsx(Description, { children: isEdit + ? "Ajuste le profil, la couleur ou l avatar. Les modifications sont visibles partout." + : "Cree rapidement un nouveau profil en renseignant email et avatar pour automatiser les agendas." }), _jsxs(Form, { onSubmit: handleSubmit, children: [_jsxs(Label, { children: ["Prenom et nom", _jsx(BaseInput, { type: "text", placeholder: "Ex: Alice Durand", value: fullName, onChange: (event) => setFullName(event.target.value) })] }), _jsxs(Row, { children: [_jsxs(Label, { style: { flex: "1 1 180px" }, children: ["Adresse email", _jsx(BaseInput, { type: "email", placeholder: "prenom@exemple.com", value: email, onChange: (event) => setEmail(event.target.value) })] }), _jsxs(Label, { style: { width: "120px" }, children: ["Couleur", _jsx(BaseInput, { type: "color", value: colorHex, onChange: (event) => setColorHex(event.target.value) })] })] }), _jsxs(Label, { children: ["Notes", _jsx(TextArea, { placeholder: "Infos importantes, allergies...", value: notes, onChange: (event) => setNotes(event.target.value) })] }), _jsxs(AvatarSection, { children: [_jsxs(AvatarHeader, { children: [_jsx("strong", { children: "Avatar" }), _jsxs(Row, { children: [_jsx(SecondaryButton, { type: "button", onClick: () => setAvatarPickerOpen((open) => !open), children: avatarPickerOpen ? "Fermer" : "Choisir un avatar" }), (avatarSelection || (isEdit && child?.avatar && !removeExistingAvatar)) && (_jsx(SecondaryButton, { type: "button", onClick: handleClearAvatar, children: "Retirer l avatar" }))] })] }), _jsxs(AvatarPreview, { children: [currentAvatarUrl ? (_jsx(AvatarImage, { src: currentAvatarUrl, alt: currentAvatarLabel ?? "Avatar" })) : (_jsx(AvatarFallback, { "$color": colorHex, children: initials || "?" })), _jsxs(AvatarInfo, { children: [_jsx("span", { children: currentAvatarLabel }), _jsx(Helper, { children: "Les avatars importes sont stockes dans le dossier `backend/public/avatars/`." })] })] }), avatarPickerOpen ? (_jsxs(AvatarPicker, { children: [_jsxs("div", { children: [_jsx("strong", { children: "Importer un nouvel avatar" }), _jsx(Label, { children: _jsx(BaseInput, { ref: fileInputRef, type: "file", accept: "image/*", onChange: handleFileChange }) }), _jsx(Helper, { children: "Formats acceptes: png, jpg, svg. Taille conseillee 512x512. Les images importees sont stockees localement." })] }), _jsxs("div", { children: [_jsx("strong", { children: "Galerie locale" }), galleryLoading ? (_jsx(StatusMessage, { children: "Chargement de la galerie..." })) : galleryError ? (_jsx(StatusMessage, { children: galleryError })) : gallery.length === 0 ? (_jsx(StatusMessage, { children: "Aucune image dans `backend/public/avatars/`. Ajoute des fichiers pour les proposer ici." })) : (_jsx(GalleryGrid, { children: gallery.map((item) => (_jsx(GalleryItem, { "$selected": avatarSelection?.source === "gallery" && avatarSelection.url === item.url, type: "button", onClick: () => handleSelectGallery(item), children: _jsx(GalleryThumbnail, { src: item.url, alt: item.filename }) }, item.filename))) }))] })] })) : null] }), error ? _jsx(ErrorText, { children: error }) : null, _jsxs(Row, { children: [_jsx(SubmitButton, { type: "submit", disabled: isSubmitting, "$loading": isSubmitting, children: isSubmitting + ? "Enregistrement..." + : isEdit + ? "Mettre a jour le profil" + : "Enregistrer le profil" }), isEdit ? (_jsx(SecondaryButton, { type: "button", onClick: () => { + onCancel?.(); + }, children: "Annuler" })) : null] })] })] })); +}; diff --git a/frontend/src/components/ChildProfilePanel.tsx b/frontend/src/components/ChildProfilePanel.tsx new file mode 100644 index 0000000..eaf55fe --- /dev/null +++ b/frontend/src/components/ChildProfilePanel.tsx @@ -0,0 +1,604 @@ +import { + ChangeEvent, + FormEvent, + useEffect, + useMemo, + useRef, + useState +} from "react"; +import styled from "styled-components"; +import { ChildProfile } from "@family-planner/types"; +import { + CreateChildPayload, + UpdateChildPayload, + useChildren +} from "../state/ChildrenContext"; +import { uploadAvatar, listAvatars } from "../services/api-client"; + +type ChildProfilePanelProps = { + mode?: "create" | "edit"; + child?: ChildProfile | null; + onCancel?: () => void; +}; + +type AvatarSelection = + | { source: "upload"; file: File; previewUrl: string; name: string } + | { source: "gallery"; url: string; name?: string }; + +type GalleryAvatar = { + filename: string; + url: string; +}; + +const Panel = styled.aside` + flex: 1; + padding: 24px; + border-radius: 18px; + background: rgba(29, 36, 66, 0.92); + border: 1px solid rgba(126, 136, 180, 0.22); + display: flex; + flex-direction: column; + gap: 20px; +`; + +const Title = styled.h2` + margin: 0; +`; + +const Description = styled.p` + margin: 0; + color: var(--text-muted); +`; + +const Form = styled.form` + display: flex; + flex-direction: column; + gap: 18px; +`; + +const Label = styled.label` + display: flex; + flex-direction: column; + gap: 8px; + font-weight: 600; +`; + +const Row = styled.div` + display: flex; + gap: 12px; + flex-wrap: wrap; +`; + +const BaseInput = styled.input` + padding: 12px 14px; + border-radius: 12px; + border: 1px solid rgba(126, 136, 180, 0.28); + background: rgba(16, 22, 52, 0.9); + color: #ffffff; +`; + +const TextArea = styled.textarea` + padding: 12px 14px; + border-radius: 12px; + border: 1px solid rgba(126, 136, 180, 0.28); + min-height: 96px; + background: rgba(16, 22, 52, 0.9); + color: #ffffff; +`; + +const SubmitButton = styled.button<{ $loading?: boolean }>` + padding: 12px 16px; + border-radius: 12px; + border: none; + background: ${({ $loading }) => + $loading ? "rgba(85, 98, 255, 0.25)" : "linear-gradient(135deg, #5562ff, #7d6cff)"}; + color: #ffffff; + font-weight: 600; + cursor: ${({ $loading }) => ($loading ? "progress" : "pointer")}; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + transition: opacity 0.2s ease; + opacity: ${({ $loading }) => ($loading ? 0.7 : 1)}; +`; + +const SecondaryButton = styled.button` + padding: 12px 16px; + border-radius: 12px; + border: 1px solid rgba(126, 136, 180, 0.4); + background: rgba(16, 22, 52, 0.9); + color: #d9dcff; + font-weight: 600; + cursor: pointer; + transition: transform 0.2s ease; + + &:hover { + transform: translateY(-1px); + } +`; + +const AvatarSection = styled.section` + display: flex; + flex-direction: column; + gap: 16px; +`; + +const AvatarHeader = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +`; + +const AvatarPreview = styled.div` + display: flex; + align-items: center; + gap: 14px; + padding: 12px 14px; + border-radius: 14px; + background: rgba(12, 18, 42, 0.8); + border: 1px solid rgba(126, 136, 180, 0.24); +`; + +const AvatarImage = styled.img` + width: 64px; + height: 64px; + border-radius: 18px; + object-fit: cover; + border: 2px solid rgba(126, 136, 180, 0.35); +`; + +const AvatarFallback = styled.div<{ $color: string }>` + width: 64px; + height: 64px; + border-radius: 18px; + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; + font-size: 1.4rem; + background: ${({ $color }) => $color}; + color: #040411; +`; + +const AvatarInfo = styled.div` + display: flex; + flex-direction: column; + gap: 4px; + color: var(--text-muted); +`; + +const AvatarPicker = styled.div` + border-radius: 16px; + background: rgba(10, 14, 34, 0.6); + border: 1px solid rgba(148, 156, 210, 0.24); + padding: 16px; + display: flex; + flex-direction: column; + gap: 16px; +`; + +const GalleryGrid = styled.div` + display: grid; + grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); + gap: 12px; +`; + +const GalleryItem = styled.button<{ $selected?: boolean }>` + border: ${({ $selected }) => ($selected ? "2px solid #7d6cff" : "1px solid rgba(126,136,180,0.3)")}; + border-radius: 14px; + background: rgba(16, 22, 52, 0.85); + padding: 6px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: transform 0.2s ease, border 0.2s ease; + + &:hover { + transform: translateY(-2px); + } +`; + +const GalleryThumbnail = styled.img` + width: 100%; + height: 100%; + border-radius: 10px; + object-fit: cover; +`; + +const Helper = styled.span` + font-size: 0.85rem; + color: var(--text-muted); +`; + +const ErrorText = styled.span` + font-size: 0.85rem; + color: #ff7b8a; +`; + +const StatusMessage = styled.div` + padding: 12px; + border-radius: 12px; + background: rgba(12, 18, 42, 0.7); + border: 1px solid rgba(126, 136, 180, 0.24); + color: var(--text-muted); + font-size: 0.9rem; +`; + +const DEFAULT_COLOR = "#5562ff"; + +export const ChildProfilePanel = ({ + mode = "create", + child, + onCancel +}: ChildProfilePanelProps) => { + const isEdit = mode === "edit" && !!child; + const { createChild, updateChild } = useChildren(); + + const [fullName, setFullName] = useState(child?.fullName ?? ""); + const [colorHex, setColorHex] = useState(child?.colorHex ?? DEFAULT_COLOR); + const [email, setEmail] = useState(child?.email ?? ""); + const [notes, setNotes] = useState(child?.notes ?? ""); + const [avatarSelection, setAvatarSelection] = useState(null); + const [removeExistingAvatar, setRemoveExistingAvatar] = useState(false); + const [avatarPickerOpen, setAvatarPickerOpen] = useState(false); + const [gallery, setGallery] = useState([]); + const [galleryLoading, setGalleryLoading] = useState(false); + const [galleryError, setGalleryError] = useState(null); + + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(null); + const fileInputRef = useRef(null); + + useEffect(() => { + if (avatarSelection?.source === "upload") { + return () => { + URL.revokeObjectURL(avatarSelection.previewUrl); + }; + } + return undefined; + }, [avatarSelection]); + + useEffect(() => { + if (!gallery.length) { + setGalleryLoading(true); + listAvatars() + .then((items) => { + setGallery(items); + setGalleryError(null); + }) + .catch(() => { + setGalleryError("Impossible de charger la galerie locale."); + }) + .finally(() => setGalleryLoading(false)); + } + }, [gallery.length]); + + useEffect(() => { + if (isEdit && child) { + setFullName(child.fullName); + setColorHex(child.colorHex); + setEmail(child.email ?? ""); + setNotes(child.notes ?? ""); + setAvatarSelection(null); + setRemoveExistingAvatar(false); + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + setError(null); + } else if (!isEdit) { + setFullName(""); + setColorHex(DEFAULT_COLOR); + setEmail(""); + setNotes(""); + setAvatarSelection(null); + setRemoveExistingAvatar(false); + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + setError(null); + } + }, [isEdit, child]); + + const initials = useMemo(() => { + return fullName + .split(" ") + .filter(Boolean) + .map((part) => part.charAt(0)) + .join("") + .slice(0, 2) + .toUpperCase(); + }, [fullName]); + + const currentAvatarUrl = useMemo(() => { + if (avatarSelection) { + return avatarSelection.source === "upload" ? avatarSelection.previewUrl : avatarSelection.url; + } + if (removeExistingAvatar) { + return null; + } + return child?.avatar?.url ?? null; + }, [avatarSelection, child?.avatar, removeExistingAvatar]); + + const currentAvatarLabel = useMemo(() => { + if (avatarSelection) { + return avatarSelection.source === "upload" + ? avatarSelection.name + : avatarSelection.name ?? "Avatar local"; + } + if (removeExistingAvatar) { + return "Aucun avatar selectionne"; + } + return child?.avatar?.name ?? "Aucun avatar selectionne"; + }, [avatarSelection, child?.avatar, removeExistingAvatar]); + + const handleFileChange = (event: ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) { + return; + } + if (!file.type.startsWith("image/")) { + setError("Le fichier doit etre une image (png, jpg, svg...)."); + return; + } + setAvatarSelection({ + source: "upload", + file, + previewUrl: URL.createObjectURL(file), + name: file.name + }); + setRemoveExistingAvatar(false); + setError(null); + }; + + const handleSelectGallery = (item: GalleryAvatar) => { + setAvatarSelection({ + source: "gallery", + url: item.url, + name: item.filename + }); + setRemoveExistingAvatar(false); + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + }; + + const handleClearAvatar = () => { + if (avatarSelection?.source === "upload") { + URL.revokeObjectURL(avatarSelection.previewUrl); + } + setAvatarSelection(null); + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + setRemoveExistingAvatar(isEdit); + }; + + const handleSubmit = async (event: FormEvent) => { + event.preventDefault(); + if (isSubmitting) { + return; + } + if (!fullName.trim()) { + setError("Merci de saisir le nom complet de l enfant."); + return; + } + + const payload: CreateChildPayload | UpdateChildPayload = { + fullName: fullName.trim(), + colorHex, + email: email.trim() ? email.trim() : undefined, + notes: notes.trim() ? notes.trim() : undefined + }; + + setIsSubmitting(true); + setError(null); + + try { + let avatarPayload: ChildProfile["avatar"] | null | undefined = undefined; + + if (avatarSelection?.source === "upload") { + const uploaded = await uploadAvatar(avatarSelection.file); + avatarPayload = { + kind: "custom", + url: uploaded.url, + name: avatarSelection.name + }; + URL.revokeObjectURL(avatarSelection.previewUrl); + setAvatarSelection({ + source: "gallery", + url: uploaded.url, + name: avatarSelection.name + }); + } else if (avatarSelection?.source === "gallery") { + avatarPayload = { + kind: "custom", + url: avatarSelection.url, + name: avatarSelection.name + }; + } else if (isEdit && child?.avatar && !removeExistingAvatar) { + avatarPayload = child.avatar; + } else if (removeExistingAvatar) { + avatarPayload = null; + } + + if (avatarPayload !== undefined) { + payload.avatar = avatarPayload ?? undefined; + } + + if (isEdit && child) { + await updateChild(child.id, payload as UpdateChildPayload); + onCancel?.(); + } else { + await createChild(payload as CreateChildPayload); + setFullName(""); + setColorHex(DEFAULT_COLOR); + setEmail(""); + setNotes(""); + setAvatarSelection(null); + setRemoveExistingAvatar(false); + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + } + } catch (err) { + setError("Impossible d enregistrer pour le moment. Merci de reessayer."); + } finally { + setIsSubmitting(false); + } + }; + + return ( + + {isEdit ? "Modifier l enfant" : "Ajouter un enfant"} + + {isEdit + ? "Ajuste le profil, la couleur ou l avatar. Les modifications sont visibles partout." + : "Cree rapidement un nouveau profil en renseignant email et avatar pour automatiser les agendas."} + +
+ + + + + +