From fdd72c11351f32c40fb9a1553bf8a87783ad42d7 Mon Sep 17 00:00:00 2001 From: philippe Date: Tue, 14 Oct 2025 10:43:33 +0200 Subject: [PATCH] Initial commit: Family Planner application MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .gitignore | 56 + LISEZ-MOI.txt | 172 + LISEZ_MOI_EN_PREMIER.txt | 120 + Lancer-Family-Planner.bat | 13 + NETTOYAGE_COMPLET.md | 197 + PORTS.md | 93 + QUICK_START.md | 109 + README.md | 180 + RESTART_CLEAN.bat | 71 + RESUME_FINAL.txt | 270 + START.bat | 88 + backend/.env.example | 21 + backend/.eslintrc.cjs | 22 + backend/.prettierrc | 7 + backend/package.json | 47 + backend/src/config/env.ts | 32 + backend/src/controllers/alerts.ts | 7 + backend/src/controllers/children.ts | 117 + backend/src/controllers/grandparents.ts | 82 + backend/src/controllers/holidays.ts | 70 + backend/src/controllers/parents.ts | 81 + backend/src/controllers/personal-leaves.ts | 156 + backend/src/controllers/schedules.ts | 101 + backend/src/data/client.json.backup.1 | 88 + backend/src/data/client.json.backup.2 | 88 + backend/src/data/client.json.backup.3 | 88 + backend/src/data/client.json.backup.4 | 88 + backend/src/data/client.json.backup.5 | 88 + backend/src/middleware/error-handler.ts | 93 + backend/src/middleware/file-upload.ts | 223 + backend/src/middleware/security.ts | 94 + backend/src/models/activity.ts | 11 + backend/src/models/alert.ts | 10 + backend/src/models/child.ts | 27 + backend/src/models/grandparent.ts | 14 + backend/src/models/holiday.ts | 13 + backend/src/models/parent.ts | 13 + backend/src/models/personal-leave.ts | 11 + backend/src/models/schedule.ts | 15 + backend/src/routes/alerts.ts | 6 + backend/src/routes/calendar.ts | 179 + backend/src/routes/children.ts | 20 + backend/src/routes/grandparents.ts | 15 + backend/src/routes/health.ts | 10 + backend/src/routes/holidays.ts | 31 + backend/src/routes/index.ts | 24 + backend/src/routes/ingestion.ts | 216 + backend/src/routes/parents.ts | 10 + backend/src/routes/personal-leaves.ts | 44 + backend/src/routes/schedules.ts | 18 + backend/src/routes/uploads.ts | 505 + backend/src/server.ts | 81 + backend/src/services/alert-service.ts | 20 + backend/src/services/child-service.ts | 102 + backend/src/services/file-db.ts | 123 + backend/src/services/grandparent-service.ts | 65 + backend/src/services/holiday-service.ts | 372 + backend/src/services/parent-service.ts | 64 + .../src/services/personal-leave-service.ts | 132 + backend/src/services/schedule-service.ts | 146 + backend/src/services/secret-store.ts | 58 + backend/src/utils/logger.ts | 46 + backend/tsconfig.json | 17 + backend/vitest.config.ts | 20 + config/README.md | 7 + config/backend.env.example | 6 + config/frontend.env.example | 3 + config/ingestion.env.example | 5 + docs/ULTRA_OCR_SYSTEM.md | 394 + docs/architecture.md | 126 + docs/archive/ANALYSE_CODE_CALENDAR.md | 846 ++ docs/archive/BOUTONS_FONCTIONNELS.md | 298 + docs/archive/CHANGEMENTS_ERGONOMIE.md | 117 + docs/archive/CORRECTIONS_BOUTONS.md | 287 + docs/archive/CORRECTIONS_OAUTH.md | 282 + docs/archive/IMPROVEMENTS.md | 339 + docs/archive/INSTRUCTIONS_PRONOTE.md | 261 + docs/archive/INTEGRATION_MONACO.md | 414 + docs/archive/MONACO_READY.md | 268 + docs/archive/OAUTH_CONFIGURATION_COMPLETE.md | 407 + docs/archive/OAUTH_SETUP.md | 174 + docs/archive/OPTIMISATION_AFFICHAGE_CONGES.md | 446 + docs/archive/PAGES_PROFILS_DETAILLES.md | 228 + docs/archive/QUICK_START_OAUTH.md | 221 + docs/archive/README_OAUTH_GOOGLE.md | 302 + docs/archive/SECURITY_IMPROVEMENTS.md | 251 + docs/archive/SOLUTION_MONACO.md | 215 + docs/archive/TROUBLESHOOTING.md | 291 + docs/data-contracts.md | 142 + docs/product-vision.md | 64 + docs/ux-flow.md | 50 + frontend/.eslintrc.cjs | 29 + frontend/.prettierrc | 7 + frontend/index.html | 12 + frontend/package.json | 45 + frontend/src/App.js | 17 + frontend/src/App.tsx | 42 + frontend/src/components/ChildCard.js | 77 + frontend/src/components/ChildCard.tsx | 133 + frontend/src/components/ChildProfilePanel.js | 394 + frontend/src/components/ChildProfilePanel.tsx | 604 ++ frontend/src/components/DailyScheduleGrid.js | 115 + frontend/src/components/DailyScheduleGrid.tsx | 200 + frontend/src/components/ErrorBoundary.js | 119 + frontend/src/components/ErrorBoundary.test.js | 39 + .../src/components/ErrorBoundary.test.tsx | 66 + frontend/src/components/ErrorBoundary.tsx | 171 + frontend/src/components/Layout.js | 88 + frontend/src/components/Layout.tsx | 135 + frontend/src/components/ParentProfilePanel.js | 324 + .../src/components/ParentProfilePanel.tsx | 504 + .../components/PlanningIntegrationDialog.js | 401 + .../components/PlanningIntegrationDialog.tsx | 689 ++ frontend/src/components/ProfileActionBar.js | 67 + frontend/src/components/ProfileActionBar.tsx | 126 + frontend/src/components/SampleScheduleGrid.js | 51 + .../src/components/SampleScheduleGrid.tsx | 95 + frontend/src/components/TimeGridMulti.js | 238 + frontend/src/components/TimeGridMulti.tsx | 366 + frontend/src/components/ToastProvider.js | 69 + frontend/src/components/ToastProvider.tsx | 96 + frontend/src/components/UpcomingAlerts.js | 86 + frontend/src/components/UpcomingAlerts.tsx | 131 + frontend/src/main.js | 10 + frontend/src/main.tsx | 23 + frontend/src/screens/AddChildScreen.js | 20 + frontend/src/screens/AddChildScreen.tsx | 30 + frontend/src/screens/AddPersonScreen.js | 35 + frontend/src/screens/AddPersonScreen.tsx | 70 + frontend/src/screens/AdultDetailScreen.js | 359 + frontend/src/screens/AdultDetailScreen.tsx | 544 + .../screens/CalendarOAuthCallbackScreen.js | 151 + .../screens/CalendarOAuthCallbackScreen.tsx | 189 + frontend/src/screens/ChildDetailScreen.tsx | 1030 ++ frontend/src/screens/ChildPlanningScreen.js | 142 + frontend/src/screens/ChildPlanningScreen.tsx | 188 + frontend/src/screens/ChildrenScreen.js | 100 + frontend/src/screens/ChildrenScreen.tsx | 151 + frontend/src/screens/DashboardScreen.js | 439 + frontend/src/screens/DashboardScreen.tsx | 577 + .../src/screens/GrandParentDetailScreen.tsx | 262 + .../src/screens/GrandparentDetailScreen.js | 647 ++ .../src/screens/MonthlyCalendarScreen.tsx | 758 ++ frontend/src/screens/ParentDetailScreen.js | 647 ++ frontend/src/screens/ParentDetailScreen.tsx | 262 + frontend/src/screens/ParentsScreen.js | 416 + frontend/src/screens/ParentsScreen.tsx | 626 ++ frontend/src/screens/PersonPlanningScreen.js | 86 + frontend/src/screens/PersonPlanningScreen.tsx | 147 + frontend/src/screens/SettingsScreen.js | 234 + frontend/src/screens/SettingsScreen.tsx | 479 + frontend/src/services/alert-service.js | 158 + frontend/src/services/alert-service.ts | 211 + frontend/src/services/api-client.js | 146 + frontend/src/services/api-client.ts | 244 + frontend/src/state/ChildrenContext.js | 112 + frontend/src/state/ChildrenContext.tsx | 171 + frontend/src/state/ParentsLocal.js | 13 + frontend/src/state/ParentsLocal.ts | 24 + frontend/src/state/useCalendarIntegrations.js | 116 + frontend/src/state/useCalendarIntegrations.ts | 140 + .../src/state/useCalendarOAuthListener.js | 25 + .../src/state/useCalendarOAuthListener.ts | 39 + frontend/src/styles/global-style.js | 32 + frontend/src/styles/global-style.ts | 33 + frontend/src/test/setup.js | 31 + frontend/src/test/setup.ts | 34 + frontend/src/types/api.js | 5 + frontend/src/types/api.ts | 78 + frontend/src/types/calendar.js | 1 + frontend/src/types/calendar.ts | 15 + frontend/src/types/global.d.ts | 16 + frontend/src/utils/calendar-oauth.js | 80 + frontend/src/utils/calendar-oauth.ts | 89 + frontend/tsconfig.json | 23 + frontend/tsconfig.node.json | 9 + frontend/vite-env.d.ts | 1 + frontend/vite.config.ts | 23 + frontend/vitest.config.ts | 31 + ingestion-service/pyproject.toml | 34 + ingestion-service/src/__init__.py | 1 + .../PKG-INFO | 16 + .../SOURCES.txt | 15 + .../dependency_links.txt | 1 + .../requires.txt | 12 + .../top_level.txt | 2 + ingestion-service/src/ingestion/__init__.py | 1 + ingestion-service/src/ingestion/config.json | 4 + ingestion-service/src/ingestion/main.py | 107 + .../src/ingestion/pipelines/__init__.py | 24 + .../src/ingestion/pipelines/csvfile.py | 66 + .../src/ingestion/pipelines/image.py | 350 + .../src/ingestion/pipelines/jsonfile.py | 64 + .../ingestion/pipelines/local_ocr_enhanced.py | 285 + .../src/ingestion/pipelines/pdf.py | 267 + .../src/ingestion/pipelines/spreadsheet.py | 183 + .../src/ingestion/pipelines/ultra_ocr.py | 498 + ingestion-service/src/ingestion/schemas.py | 32 + ingestion-service/tests/test_health.py | 9 + package-lock.json | 9347 +++++++++++++++++ package.json | 22 + planning-ingestion/.env.example | 20 + planning-ingestion/.gitignore | 9 + planning-ingestion/README.md | 492 + planning-ingestion/package-lock.json | 2742 +++++ planning-ingestion/package.json | 38 + planning-ingestion/src/cli.ts | 141 + planning-ingestion/src/crypto/encryption.ts | 45 + planning-ingestion/src/database/db.ts | 126 + planning-ingestion/src/extractors/excel.ts | 38 + planning-ingestion/src/extractors/ocr.ts | 26 + planning-ingestion/src/extractors/pdf.ts | 16 + planning-ingestion/src/llm/client.ts | 118 + planning-ingestion/src/normalizer/parser.ts | 177 + planning-ingestion/src/server.ts | 156 + planning-ingestion/src/services/ingestion.ts | 144 + planning-ingestion/src/types/schema.ts | 69 + planning-ingestion/tsconfig.json | 20 + shared/types/.eslintrc.cjs | 13 + shared/types/.prettierrc | 6 + shared/types/package.json | 22 + shared/types/src/index.js | 1 + shared/types/src/index.ts | 92 + shared/types/tsconfig.json | 15 + shared/ui/.eslintrc.cjs | 25 + shared/ui/.prettierrc | 7 + shared/ui/package.json | 28 + shared/ui/src/components/Button.js | 24 + shared/ui/src/components/Button.tsx | 30 + shared/ui/src/components/Card.js | 10 + shared/ui/src/components/Card.tsx | 11 + shared/ui/src/components/SectionTitle.js | 6 + shared/ui/src/components/SectionTitle.tsx | 7 + shared/ui/src/index.js | 3 + shared/ui/src/index.ts | 3 + shared/ui/tsconfig.json | 16 + start-app.ps1 | 185 + start-family-planner.ps1 | 16 + stop.bat | 21 + 239 files changed, 44160 insertions(+) create mode 100644 .gitignore create mode 100644 LISEZ-MOI.txt create mode 100644 LISEZ_MOI_EN_PREMIER.txt create mode 100644 Lancer-Family-Planner.bat create mode 100644 NETTOYAGE_COMPLET.md create mode 100644 PORTS.md create mode 100644 QUICK_START.md create mode 100644 README.md create mode 100644 RESTART_CLEAN.bat create mode 100644 RESUME_FINAL.txt create mode 100644 START.bat create mode 100644 backend/.env.example create mode 100644 backend/.eslintrc.cjs create mode 100644 backend/.prettierrc create mode 100644 backend/package.json create mode 100644 backend/src/config/env.ts create mode 100644 backend/src/controllers/alerts.ts create mode 100644 backend/src/controllers/children.ts create mode 100644 backend/src/controllers/grandparents.ts create mode 100644 backend/src/controllers/holidays.ts create mode 100644 backend/src/controllers/parents.ts create mode 100644 backend/src/controllers/personal-leaves.ts create mode 100644 backend/src/controllers/schedules.ts create mode 100644 backend/src/data/client.json.backup.1 create mode 100644 backend/src/data/client.json.backup.2 create mode 100644 backend/src/data/client.json.backup.3 create mode 100644 backend/src/data/client.json.backup.4 create mode 100644 backend/src/data/client.json.backup.5 create mode 100644 backend/src/middleware/error-handler.ts create mode 100644 backend/src/middleware/file-upload.ts create mode 100644 backend/src/middleware/security.ts create mode 100644 backend/src/models/activity.ts create mode 100644 backend/src/models/alert.ts create mode 100644 backend/src/models/child.ts create mode 100644 backend/src/models/grandparent.ts create mode 100644 backend/src/models/holiday.ts create mode 100644 backend/src/models/parent.ts create mode 100644 backend/src/models/personal-leave.ts create mode 100644 backend/src/models/schedule.ts create mode 100644 backend/src/routes/alerts.ts create mode 100644 backend/src/routes/calendar.ts create mode 100644 backend/src/routes/children.ts create mode 100644 backend/src/routes/grandparents.ts create mode 100644 backend/src/routes/health.ts create mode 100644 backend/src/routes/holidays.ts create mode 100644 backend/src/routes/index.ts create mode 100644 backend/src/routes/ingestion.ts create mode 100644 backend/src/routes/parents.ts create mode 100644 backend/src/routes/personal-leaves.ts create mode 100644 backend/src/routes/schedules.ts create mode 100644 backend/src/routes/uploads.ts create mode 100644 backend/src/server.ts create mode 100644 backend/src/services/alert-service.ts create mode 100644 backend/src/services/child-service.ts create mode 100644 backend/src/services/file-db.ts create mode 100644 backend/src/services/grandparent-service.ts create mode 100644 backend/src/services/holiday-service.ts create mode 100644 backend/src/services/parent-service.ts create mode 100644 backend/src/services/personal-leave-service.ts create mode 100644 backend/src/services/schedule-service.ts create mode 100644 backend/src/services/secret-store.ts create mode 100644 backend/src/utils/logger.ts create mode 100644 backend/tsconfig.json create mode 100644 backend/vitest.config.ts create mode 100644 config/README.md create mode 100644 config/backend.env.example create mode 100644 config/frontend.env.example create mode 100644 config/ingestion.env.example create mode 100644 docs/ULTRA_OCR_SYSTEM.md create mode 100644 docs/architecture.md create mode 100644 docs/archive/ANALYSE_CODE_CALENDAR.md create mode 100644 docs/archive/BOUTONS_FONCTIONNELS.md create mode 100644 docs/archive/CHANGEMENTS_ERGONOMIE.md create mode 100644 docs/archive/CORRECTIONS_BOUTONS.md create mode 100644 docs/archive/CORRECTIONS_OAUTH.md create mode 100644 docs/archive/IMPROVEMENTS.md create mode 100644 docs/archive/INSTRUCTIONS_PRONOTE.md create mode 100644 docs/archive/INTEGRATION_MONACO.md create mode 100644 docs/archive/MONACO_READY.md create mode 100644 docs/archive/OAUTH_CONFIGURATION_COMPLETE.md create mode 100644 docs/archive/OAUTH_SETUP.md create mode 100644 docs/archive/OPTIMISATION_AFFICHAGE_CONGES.md create mode 100644 docs/archive/PAGES_PROFILS_DETAILLES.md create mode 100644 docs/archive/QUICK_START_OAUTH.md create mode 100644 docs/archive/README_OAUTH_GOOGLE.md create mode 100644 docs/archive/SECURITY_IMPROVEMENTS.md create mode 100644 docs/archive/SOLUTION_MONACO.md create mode 100644 docs/archive/TROUBLESHOOTING.md create mode 100644 docs/data-contracts.md create mode 100644 docs/product-vision.md create mode 100644 docs/ux-flow.md create mode 100644 frontend/.eslintrc.cjs create mode 100644 frontend/.prettierrc create mode 100644 frontend/index.html create mode 100644 frontend/package.json create mode 100644 frontend/src/App.js create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/components/ChildCard.js create mode 100644 frontend/src/components/ChildCard.tsx create mode 100644 frontend/src/components/ChildProfilePanel.js create mode 100644 frontend/src/components/ChildProfilePanel.tsx create mode 100644 frontend/src/components/DailyScheduleGrid.js create mode 100644 frontend/src/components/DailyScheduleGrid.tsx create mode 100644 frontend/src/components/ErrorBoundary.js create mode 100644 frontend/src/components/ErrorBoundary.test.js create mode 100644 frontend/src/components/ErrorBoundary.test.tsx create mode 100644 frontend/src/components/ErrorBoundary.tsx create mode 100644 frontend/src/components/Layout.js create mode 100644 frontend/src/components/Layout.tsx create mode 100644 frontend/src/components/ParentProfilePanel.js create mode 100644 frontend/src/components/ParentProfilePanel.tsx create mode 100644 frontend/src/components/PlanningIntegrationDialog.js create mode 100644 frontend/src/components/PlanningIntegrationDialog.tsx create mode 100644 frontend/src/components/ProfileActionBar.js create mode 100644 frontend/src/components/ProfileActionBar.tsx create mode 100644 frontend/src/components/SampleScheduleGrid.js create mode 100644 frontend/src/components/SampleScheduleGrid.tsx create mode 100644 frontend/src/components/TimeGridMulti.js create mode 100644 frontend/src/components/TimeGridMulti.tsx create mode 100644 frontend/src/components/ToastProvider.js create mode 100644 frontend/src/components/ToastProvider.tsx create mode 100644 frontend/src/components/UpcomingAlerts.js create mode 100644 frontend/src/components/UpcomingAlerts.tsx create mode 100644 frontend/src/main.js create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/screens/AddChildScreen.js create mode 100644 frontend/src/screens/AddChildScreen.tsx create mode 100644 frontend/src/screens/AddPersonScreen.js create mode 100644 frontend/src/screens/AddPersonScreen.tsx create mode 100644 frontend/src/screens/AdultDetailScreen.js create mode 100644 frontend/src/screens/AdultDetailScreen.tsx create mode 100644 frontend/src/screens/CalendarOAuthCallbackScreen.js create mode 100644 frontend/src/screens/CalendarOAuthCallbackScreen.tsx create mode 100644 frontend/src/screens/ChildDetailScreen.tsx create mode 100644 frontend/src/screens/ChildPlanningScreen.js create mode 100644 frontend/src/screens/ChildPlanningScreen.tsx create mode 100644 frontend/src/screens/ChildrenScreen.js create mode 100644 frontend/src/screens/ChildrenScreen.tsx create mode 100644 frontend/src/screens/DashboardScreen.js create mode 100644 frontend/src/screens/DashboardScreen.tsx create mode 100644 frontend/src/screens/GrandParentDetailScreen.tsx create mode 100644 frontend/src/screens/GrandparentDetailScreen.js create mode 100644 frontend/src/screens/MonthlyCalendarScreen.tsx create mode 100644 frontend/src/screens/ParentDetailScreen.js create mode 100644 frontend/src/screens/ParentDetailScreen.tsx create mode 100644 frontend/src/screens/ParentsScreen.js create mode 100644 frontend/src/screens/ParentsScreen.tsx create mode 100644 frontend/src/screens/PersonPlanningScreen.js create mode 100644 frontend/src/screens/PersonPlanningScreen.tsx create mode 100644 frontend/src/screens/SettingsScreen.js create mode 100644 frontend/src/screens/SettingsScreen.tsx create mode 100644 frontend/src/services/alert-service.js create mode 100644 frontend/src/services/alert-service.ts create mode 100644 frontend/src/services/api-client.js create mode 100644 frontend/src/services/api-client.ts create mode 100644 frontend/src/state/ChildrenContext.js create mode 100644 frontend/src/state/ChildrenContext.tsx create mode 100644 frontend/src/state/ParentsLocal.js create mode 100644 frontend/src/state/ParentsLocal.ts create mode 100644 frontend/src/state/useCalendarIntegrations.js create mode 100644 frontend/src/state/useCalendarIntegrations.ts create mode 100644 frontend/src/state/useCalendarOAuthListener.js create mode 100644 frontend/src/state/useCalendarOAuthListener.ts create mode 100644 frontend/src/styles/global-style.js create mode 100644 frontend/src/styles/global-style.ts create mode 100644 frontend/src/test/setup.js create mode 100644 frontend/src/test/setup.ts create mode 100644 frontend/src/types/api.js create mode 100644 frontend/src/types/api.ts create mode 100644 frontend/src/types/calendar.js create mode 100644 frontend/src/types/calendar.ts create mode 100644 frontend/src/types/global.d.ts create mode 100644 frontend/src/utils/calendar-oauth.js create mode 100644 frontend/src/utils/calendar-oauth.ts create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.node.json create mode 100644 frontend/vite-env.d.ts create mode 100644 frontend/vite.config.ts create mode 100644 frontend/vitest.config.ts create mode 100644 ingestion-service/pyproject.toml create mode 100644 ingestion-service/src/__init__.py create mode 100644 ingestion-service/src/family_planner_ingestion.egg-info/PKG-INFO create mode 100644 ingestion-service/src/family_planner_ingestion.egg-info/SOURCES.txt create mode 100644 ingestion-service/src/family_planner_ingestion.egg-info/dependency_links.txt create mode 100644 ingestion-service/src/family_planner_ingestion.egg-info/requires.txt create mode 100644 ingestion-service/src/family_planner_ingestion.egg-info/top_level.txt create mode 100644 ingestion-service/src/ingestion/__init__.py create mode 100644 ingestion-service/src/ingestion/config.json create mode 100644 ingestion-service/src/ingestion/main.py create mode 100644 ingestion-service/src/ingestion/pipelines/__init__.py create mode 100644 ingestion-service/src/ingestion/pipelines/csvfile.py create mode 100644 ingestion-service/src/ingestion/pipelines/image.py create mode 100644 ingestion-service/src/ingestion/pipelines/jsonfile.py create mode 100644 ingestion-service/src/ingestion/pipelines/local_ocr_enhanced.py create mode 100644 ingestion-service/src/ingestion/pipelines/pdf.py create mode 100644 ingestion-service/src/ingestion/pipelines/spreadsheet.py create mode 100644 ingestion-service/src/ingestion/pipelines/ultra_ocr.py create mode 100644 ingestion-service/src/ingestion/schemas.py create mode 100644 ingestion-service/tests/test_health.py create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 planning-ingestion/.env.example create mode 100644 planning-ingestion/.gitignore create mode 100644 planning-ingestion/README.md create mode 100644 planning-ingestion/package-lock.json create mode 100644 planning-ingestion/package.json create mode 100644 planning-ingestion/src/cli.ts create mode 100644 planning-ingestion/src/crypto/encryption.ts create mode 100644 planning-ingestion/src/database/db.ts create mode 100644 planning-ingestion/src/extractors/excel.ts create mode 100644 planning-ingestion/src/extractors/ocr.ts create mode 100644 planning-ingestion/src/extractors/pdf.ts create mode 100644 planning-ingestion/src/llm/client.ts create mode 100644 planning-ingestion/src/normalizer/parser.ts create mode 100644 planning-ingestion/src/server.ts create mode 100644 planning-ingestion/src/services/ingestion.ts create mode 100644 planning-ingestion/src/types/schema.ts create mode 100644 planning-ingestion/tsconfig.json create mode 100644 shared/types/.eslintrc.cjs create mode 100644 shared/types/.prettierrc create mode 100644 shared/types/package.json create mode 100644 shared/types/src/index.js create mode 100644 shared/types/src/index.ts create mode 100644 shared/types/tsconfig.json create mode 100644 shared/ui/.eslintrc.cjs create mode 100644 shared/ui/.prettierrc create mode 100644 shared/ui/package.json create mode 100644 shared/ui/src/components/Button.js create mode 100644 shared/ui/src/components/Button.tsx create mode 100644 shared/ui/src/components/Card.js create mode 100644 shared/ui/src/components/Card.tsx create mode 100644 shared/ui/src/components/SectionTitle.js create mode 100644 shared/ui/src/components/SectionTitle.tsx create mode 100644 shared/ui/src/index.js create mode 100644 shared/ui/src/index.ts create mode 100644 shared/ui/tsconfig.json create mode 100644 start-app.ps1 create mode 100644 start-family-planner.ps1 create mode 100644 stop.bat 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."} + +
+ + + + + +