Compare commits

...

6 Commits

Author SHA1 Message Date
3d13eb5b2e [Doc] Ajout tâche amélioration libellé consentement photo (#62)
Ajout d'une tâche dans le suivi des évolutions du CDC :
- Note pour améliorer le libellé de la checkbox de consentement photo
  sur l'écran d'inscription des nounous (étape 2)

Le libellé actuel 'J'accepte l'utilisation de ma photo' devra être
rendu plus explicite et conforme RGPD.

Refs: #62 (Amendement CDC)
2026-01-27 16:44:23 +01:00
5b37d09fa9 [Doc] Guide d'architecture technique et déploiement (#61 #16)
Ajout d'une documentation technique complète pour l'infrastructure
et le déploiement de l'application P'titsPas.

Contenu du guide :
- Vue d'ensemble de l'architecture (Flutter frontend + Node.js backend)
- Prérequis serveur (Node.js, PostgreSQL, ressources recommandées)
- Instructions d'installation pas à pas
- Configuration de la base de données PostgreSQL
- Déploiement du backend (NestJS)
- Build et déploiement du frontend Flutter Web
- Configuration NGINX comme reverse proxy
- Sécurisation SSL/TLS avec Let's Encrypt
- Monitoring et maintenance
- Sauvegarde et restauration
- Troubleshooting des problèmes courants

Ce document est essentiel pour le déploiement on-premise de l'application
par les collectivités locales.

Refs: #61 (Guide installation & configuration), #16 (Doc config on-premise)
2026-01-27 16:44:23 +01:00
53f3af9794 [Frontend] Ajout nouvelles icônes SVG de l'application
Ajout de deux versions d'icônes SVG pour l'application :
- icon.svg : Icône standard de l'application
- icon_improved.svg : Version améliorée de l'icône

Ces icônes seront utilisées pour le branding de l'application
et les différentes tailles d'affichage.
2026-01-27 16:44:23 +01:00
105cf53e7b [Frontend] Parcours complet inscription Assistantes Maternelles (#40 #41 #42)
Implémentation du parcours d'inscription des assistantes maternelles en 4 étapes
+ écran de confirmation, en utilisant Provider pour la gestion d'état.

Fonctionnalités implémentées :
- Étape 1 : Identité (nom, prénom, adresse, email, mot de passe)
- Étape 2 : Infos professionnelles (photo, agrément, NIR, capacité d'accueil)
- Étape 3 : Présentation personnelle et acceptation CGU
- Étape 4 : Récapitulatif et validation finale
- Écran de confirmation post-inscription

Fichiers ajoutés :
- models/nanny_registration_data.dart : Modèle de données avec Provider
- screens/auth/nanny_register_step1_screen.dart : Identité
- screens/auth/nanny_register_step2_screen.dart : Infos pro
- screens/auth/nanny_register_step3_screen.dart : Présentation
- screens/auth/nanny_register_step4_screen.dart : Récapitulatif
- screens/auth/nanny_register_confirmation_screen.dart : Confirmation
- screens/unknown_screen.dart : Écran pour routes inconnues
- config/app_router.dart : Copie du routeur (à intégrer)

Refs: #40 (Panneau 1 Identité), #41 (Panneau 2 Infos pro), #42 (Finalisation)
2026-01-27 16:44:23 +01:00
29bee9fa80 [Frontend] Refactorisation inscription Parents avec Provider (#38 #39)
Refactorisation complète du parcours d'inscription des parents pour utiliser
Provider au lieu du passage de données par paramètres de navigation.

Modifications principales :
- Utilisation de Provider pour partager UserRegistrationData entre les étapes
- Simplification du routeur (suppression des paramètres)
- Amélioration de la persistance des données entre les étapes
- Meilleure expérience utilisateur lors de la navigation

Fichiers modifiés :
- models/user_registration_data.dart : Modèle avec ChangeNotifier
- screens/auth/parent_register_step1-5_screen.dart : Intégration Provider
- navigation/app_router.dart : Simplification du routing
- main.dart : Configuration du Provider
- login_screen.dart : Ajout navigation vers inscription
- register_choice_screen.dart : Navigation vers parcours parent/AM
- utils/data_generator.dart : Génération de données de test

Refs: #38 (Étape 3 Enfants), #39 (Étapes 4-6 Finalisation)
2026-01-27 16:44:23 +01:00
Julien Martin
dbd56637e1 chore: ignore les fichiers générés par Flutter pour Android et Windows 2026-01-27 16:44:23 +01:00
28 changed files with 2494 additions and 426 deletions

4
.gitignore vendored
View File

@ -37,6 +37,10 @@ yarn-error.log*
.pub-cache/
.pub/
/build/
**/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java
**/windows/flutter/generated_plugin_registrant.cc
**/windows/flutter/generated_plugin_registrant.h
**/windows/flutter/generated_plugins.cmake
# Coverage
coverage/

View File

@ -0,0 +1,592 @@
# Architecture Technique - P'titsPas
## Guide d'Infrastructure et de Déploiement
---
## Vue d'ensemble
P'titsPas est une application de gestion de garde d'enfants pour les collectivités locales, basée sur une architecture **client-serveur moderne** :
- **Frontend** : Application web Flutter (Single Page Application)
- **Backend** : API REST Node.js/Express avec TypeScript
- **Base de données** : PostgreSQL avec ORM Prisma
- **Architecture** : Séparation claire frontend/backend avec API REST
---
## Prérequis Serveur
### Environnement d'exécution
#### Backend
- **Node.js** : Version 18+ (LTS recommandée : 18.19.0+)
- **npm** : Version 9+
- **TypeScript** : Inclus dans les dépendances du projet
#### Base de données
- **PostgreSQL** : Version 15+
- **Extensions** : UUID (pour les clés primaires)
#### Frontend
- **Serveur web statique** : nginx, Apache, ou similaire
- **Flutter Web** : Compilation en JavaScript (pas de prérequis runtime)
### Ressources recommandées
#### Environnement de développement
- **RAM** : 4GB minimum
- **CPU** : 2 vCPU
- **Storage** : 10GB
#### Environnement de production
- **RAM** : 8GB recommandé (4GB minimum)
- **CPU** : 4 vCPU recommandé (2 vCPU minimum)
- **Storage** : 50GB minimum (base de données + logs + backups)
- **Réseau** :
- Port 3000 : API Backend (interne)
- Port 80/443 : Web (externe)
- Port 5432 : PostgreSQL (interne uniquement)
---
## Stack Technique Détaillée
### Backend (API)
```json
{
"runtime": "Node.js 18+",
"language": "TypeScript",
"framework": "Express.js 4.18+",
"orm": "Prisma 6.7+",
"database_client": "@prisma/client",
"security": [
"helmet (sécurité headers)",
"cors (CORS policy)",
"bcrypt (hashage mots de passe)",
"jsonwebtoken (JWT auth)"
],
"logging": "morgan",
"validation": "@nestjs/common"
}
```
### Frontend (Web)
```json
{
"framework": "Flutter 3.2.6+",
"language": "Dart 3.0+",
"compilation": "JavaScript (Flutter Web)",
"navigation": "go_router 13.2+",
"state_management": "provider 6.1+",
"ui_framework": "Material Design",
"fonts": "Google Fonts",
"http_client": "http 1.2+"
}
```
### Base de données
```sql
-- Structure PostgreSQL
-- Tables principales :
-- - Parent (utilisateurs parents)
-- - Child (enfants)
-- - Contract (contrats de garde)
-- - Admin (administrateurs)
-- - Theme (thèmes interface)
-- - AppSettings (paramètres app)
-- Types de données :
-- - UUID pour toutes les clés primaires
-- - Timestamps automatiques (createdAt, updatedAt)
-- - Enums pour les statuts
```
---
## Installation et Configuration
### 1. Prérequis système
```bash
# Ubuntu/Debian
sudo apt update
sudo apt install -y nodejs npm postgresql postgresql-contrib nginx
# Vérification versions
node --version # >= 18.0.0
npm --version # >= 9.0.0
psql --version # >= 15.0
```
### 2. Configuration base de données
```sql
-- Se connecter en tant que postgres
sudo -u postgres psql
-- Créer la base de données et l'utilisateur
CREATE DATABASE ptitspas;
CREATE USER ptitspas_user WITH PASSWORD 'secure_password_here';
GRANT ALL PRIVILEGES ON DATABASE ptitspas TO ptitspas_user;
ALTER USER ptitspas_user CREATEDB; -- Pour les migrations
-- Quitter
\q
```
### 3. Installation Backend
```bash
cd backend
# Installation des dépendances
npm install
# Configuration environnement
cp .env.example .env
# Éditer .env avec vos paramètres
# Génération du client Prisma et migrations
npx prisma generate
npx prisma migrate deploy
# Initialisation admin (optionnel)
npm run init-admin
```
### 4. Installation Frontend
```bash
cd frontend
# Installation des dépendances Flutter
flutter pub get
# Build pour production
flutter build web --release
```
---
## Variables d'Environnement
### Backend (.env)
```bash
# Base de données
DATABASE_URL="postgresql://ptitspas_user:secure_password_here@localhost:5432/ptitspas"
# Sécurité
JWT_SECRET="your-super-secret-jwt-key-minimum-32-characters"
JWT_EXPIRES_IN="24h"
# Serveur
PORT=3000
NODE_ENV=production
# Optionnel
CORS_ORIGIN="https://ptitspas.yourdomain.com"
```
---
## Déploiement
### Option 1 : Déploiement classique (recommandé)
#### Backend
```bash
cd backend
# Installation production
npm ci --only=production
# Build TypeScript
npm run build
# Démarrage (avec PM2 recommandé)
npm install -g pm2
pm2 start dist/index.js --name "ptitspas-api"
pm2 startup
pm2 save
```
#### Frontend
```bash
cd frontend
# Build production
flutter build web --release
# Copier vers serveur web
sudo cp -r build/web/* /var/www/ptitspas/
sudo chown -R www-data:www-data /var/www/ptitspas/
```
### Option 2 : Conteneurisation Docker
#### Dockerfile Backend
```dockerfile
FROM node:18-alpine
# Créer répertoire app
WORKDIR /app
# Copier package files
COPY package*.json ./
COPY prisma ./prisma/
# Installer dépendances
RUN npm ci --only=production
# Copier code source
COPY . .
# Build
RUN npm run build
# Générer client Prisma
RUN npx prisma generate
# Exposer port
EXPOSE 3000
# Variables d'environnement
ENV NODE_ENV=production
# Commande démarrage
CMD ["npm", "start"]
```
#### Dockerfile Frontend
```dockerfile
FROM nginx:alpine
# Copier build Flutter
COPY build/web /usr/share/nginx/html
# Configuration nginx
COPY nginx.conf /etc/nginx/nginx.conf
# Exposer port
EXPOSE 80
# Démarrage nginx
CMD ["nginx", "-g", "daemon off;"]
```
#### Docker Compose
```yaml
version: '3.8'
services:
postgres:
image: postgres:15-alpine
environment:
POSTGRES_DB: ptitspas
POSTGRES_USER: ptitspas_user
POSTGRES_PASSWORD: secure_password_here
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
backend:
build: ./backend
environment:
DATABASE_URL: postgresql://ptitspas_user:secure_password_here@postgres:5432/ptitspas
JWT_SECRET: your-super-secret-jwt-key
NODE_ENV: production
ports:
- "3000:3000"
depends_on:
- postgres
frontend:
build: ./frontend
ports:
- "80:80"
depends_on:
- backend
volumes:
postgres_data:
```
---
## Configuration Nginx
### Configuration complète
```nginx
server {
listen 80;
server_name ptitspas.yourdomain.com;
# Redirection HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name ptitspas.yourdomain.com;
# Certificats SSL
ssl_certificate /etc/letsencrypt/live/ptitspas.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ptitspas.yourdomain.com/privkey.pem;
# Configuration SSL sécurisée
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# Frontend statique (Flutter Web)
location / {
root /var/www/ptitspas;
index index.html;
try_files $uri $uri/ /index.html;
# Cache statique
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
# API Backend
location /api/ {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Logs
access_log /var/log/nginx/ptitspas_access.log;
error_log /var/log/nginx/ptitspas_error.log;
}
```
---
## Sécurité
### Obligatoire
1. **HTTPS avec certificat SSL**
```bash
# Installation Certbot
sudo apt install certbot python3-certbot-nginx
# Génération certificat
sudo certbot --nginx -d ptitspas.yourdomain.com
# Renouvellement automatique
sudo crontab -e
# Ajouter : 0 12 * * * /usr/bin/certbot renew --quiet
```
2. **Firewall**
```bash
# UFW (Ubuntu)
sudo ufw allow 22 # SSH
sudo ufw allow 80 # HTTP
sudo ufw allow 443 # HTTPS
sudo ufw enable
```
3. **Base de données sécurisée**
```bash
# PostgreSQL : accès local uniquement
sudo nano /etc/postgresql/15/main/postgresql.conf
# Commenter : #listen_addresses = 'localhost'
sudo nano /etc/postgresql/15/main/pg_hba.conf
# Vérifier que seules les connexions locales sont autorisées
```
4. **Backup automatique**
```bash
# Script backup
#!/bin/bash
BACKUP_DIR="/var/backups/ptitspas"
DATE=$(date +%Y%m%d_%H%M%S)
pg_dump -U ptitspas_user -h localhost ptitspas > $BACKUP_DIR/ptitspas_$DATE.sql
# Nettoyer les backups > 30 jours
find $BACKUP_DIR -name "*.sql" -mtime +30 -delete
# Crontab : tous les jours à 2h
# 0 2 * * * /path/to/backup_script.sh
```
### Recommandé
1. **Fail2Ban** (protection brute force)
```bash
sudo apt install fail2ban
sudo systemctl enable fail2ban
```
2. **Monitoring des logs**
```bash
# Logrotate pour éviter les gros fichiers
sudo nano /etc/logrotate.d/ptitspas
```
3. **Updates automatiques**
```bash
sudo apt install unattended-upgrades
sudo dpkg-reconfigure unattended-upgrades
```
---
## Commandes de Gestion
### Démarrage des services
```bash
# Backend (développement)
cd backend && npm run dev
# Backend (production avec PM2)
pm2 start ptitspas-api
pm2 status
# Base de données
sudo systemctl start postgresql
sudo systemctl status postgresql
# Serveur web
sudo systemctl start nginx
sudo systemctl status nginx
```
### Maintenance
```bash
# Migrations base de données
cd backend
npx prisma migrate deploy
# Logs Backend
pm2 logs ptitspas-api
# Logs Nginx
sudo tail -f /var/log/nginx/ptitspas_access.log
sudo tail -f /var/log/nginx/ptitspas_error.log
# Restart services
pm2 restart ptitspas-api
sudo systemctl restart nginx
```
---
## Monitoring et Healthcheck
### Endpoints de santé
```bash
# API Health (à implémenter)
curl https://ptitspas.yourdomain.com/api/health
# Base de données
psql -h localhost -U ptitspas_user -d ptitspas -c "SELECT 1;"
# Frontend
curl -I https://ptitspas.yourdomain.com/
```
### Logs à surveiller
1. **Backend** : Via PM2 ou logs applicatifs
2. **PostgreSQL** : `/var/log/postgresql/postgresql-15-main.log`
3. **Nginx** : `/var/log/nginx/ptitspas_*.log`
4. **Système** : `/var/log/syslog`
### Métriques importantes
- **CPU/RAM** : Usage serveur
- **Espace disque** : Base de données et logs
- **Connexions DB** : Nombre de connexions actives
- **Temps de réponse** : API et frontend
- **Erreurs 5xx** : Erreurs serveur
---
## Troubleshooting
### Problèmes courants
1. **Backend ne démarre pas**
```bash
# Vérifier variables d'environnement
cd backend && cat .env
# Vérifier connexion DB
npx prisma db pull
# Logs détaillés
npm run dev
```
2. **Frontend ne s'affiche pas**
```bash
# Vérifier build
cd frontend && flutter build web
# Vérifier nginx
sudo nginx -t
sudo systemctl reload nginx
```
3. **Erreurs base de données**
```bash
# Vérifier statut PostgreSQL
sudo systemctl status postgresql
# Vérifier connexions
sudo -u postgres psql -c "SELECT * FROM pg_stat_activity;"
```
---
## Évolutivité
### Optimisations possibles
1. **Cache Redis** : Pour les sessions et cache applicatif
2. **CDN** : Pour les assets statiques
3. **Load Balancer** : Pour haute disponibilité
4. **Clustering** : Multiple instances Node.js
5. **Database replication** : Master/Slave PostgreSQL
### Monitoring avancé
- **Prometheus + Grafana** : Métriques système et applicatif
- **ELK Stack** : Centralisation des logs
- **Uptime monitoring** : Surveillance externe
Cette architecture est conçue pour être **scalable**, **maintenable** et **sécurisée** pour un environnement de production professionnel.

View File

@ -209,6 +209,7 @@ Pour chaque évolution identifiée, ce document suivra la structure suivante :
- [x] Ajouter d'autres évolutions identifiées
- [ ] Mettre à jour le CDC original
- [ ] Valider les modifications avec les parties prenantes
- [ ] Modifier le texte de la checkbox de consentement photo (libellé actuel : 'J\'accepte l\'utilisation de ma photo.') sur l'écran d'inscription Nounou Étape 2 (`nanny_register_step2_screen.dart`).
# Évolutions proposées au cahier des charges

View File

@ -1,44 +0,0 @@
package io.flutter.plugins;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import io.flutter.Log;
import io.flutter.embedding.engine.FlutterEngine;
/**
* Generated file. Do not edit.
* This file is generated by the Flutter tool based on the
* plugins that support the Android platform.
*/
@Keep
public final class GeneratedPluginRegistrant {
private static final String TAG = "GeneratedPluginRegistrant";
public static void registerWith(@NonNull FlutterEngine flutterEngine) {
try {
flutterEngine.getPlugins().add(new io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin());
} catch (Exception e) {
Log.e(TAG, "Error registering plugin flutter_plugin_android_lifecycle, io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin", e);
}
try {
flutterEngine.getPlugins().add(new io.flutter.plugins.imagepicker.ImagePickerPlugin());
} catch (Exception e) {
Log.e(TAG, "Error registering plugin image_picker_android, io.flutter.plugins.imagepicker.ImagePickerPlugin", e);
}
try {
flutterEngine.getPlugins().add(new io.flutter.plugins.pathprovider.PathProviderPlugin());
} catch (Exception e) {
Log.e(TAG, "Error registering plugin path_provider_android, io.flutter.plugins.pathprovider.PathProviderPlugin", e);
}
try {
flutterEngine.getPlugins().add(new io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin());
} catch (Exception e) {
Log.e(TAG, "Error registering plugin shared_preferences_android, io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin", e);
}
try {
flutterEngine.getPlugins().add(new io.flutter.plugins.urllauncher.UrlLauncherPlugin());
} catch (Exception e) {
Log.e(TAG, "Error registering plugin url_launcher_android, io.flutter.plugins.urllauncher.UrlLauncherPlugin", e);
}
}
}

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<rect x="271" y="17" width="193" height="96" rx="19.2" fill="#f9bc8e" />
<rect x="168" y="108" width="206" height="115" rx="23.0" fill="#f7db75" />
<rect x="131" y="229" width="223" height="132" rx="26.4" fill="#aadac2" />
<rect x="47" y="349" width="238" height="144" rx="28.8" fill="#dfc2cf" />
</svg>

After

Width:  |  Height:  |  Size: 374 B

View File

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- Watercolor like noise -->
<filter id="wcTexture" x="-20%" y="-20%" width="140%" height="140%">
<feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="4" seed="12" result="noise"/>
<feBlend in="SourceGraphic" in2="noise" mode="multiply"/>
</filter>
<!-- Gradients -->
<radialGradient id="gradCoral" cx="50%" cy="40%" r="70%">
<stop offset="0%" stop-color="#ffddc9"/>
<stop offset="100%" stop-color="#f49c6e"/>
</radialGradient>
<radialGradient id="gradYellow" cx="45%" cy="35%" r="70%">
<stop offset="0%" stop-color="#fff6c9"/>
<stop offset="100%" stop-color="#e9c833"/>
</radialGradient>
<radialGradient id="gradMint" cx="40%" cy="30%" r="70%">
<stop offset="0%" stop-color="#d4f4ec"/>
<stop offset="100%" stop-color="#6bbda3"/>
</radialGradient>
<radialGradient id="gradLavender" cx="35%" cy="25%" r="70%">
<stop offset="0%" stop-color="#f5e6ff"/>
<stop offset="100%" stop-color="#b289c9"/>
</radialGradient>
</defs>
<!-- CORAL -->
<path filter="url(#wcTexture)" fill="url(#gradCoral)" d="
M 360 40
Q 380 15 420 22
L 450 28
Q 480 35 490 60
L 495 80
Q 500 105 470 120
L 440 135
Q 410 150 370 135
L 345 120
Q 315 105 325 75
L 330 55
Q 335 50 360 40 Z"/>
<!-- YELLOW -->
<path filter="url(#wcTexture)" fill="url(#gradYellow)" d="
M 280 190
Q 300 170 340 175
L 370 180
Q 405 185 410 210
L 415 230
Q 420 255 390 270
L 355 285
Q 320 300 290 285
L 265 270
Q 235 255 245 225
L 250 205
Q 255 200 280 190 Z"/>
<!-- MINT -->
<path filter="url(#wcTexture)" fill="url(#gradMint)" d="
M 180 330
Q 205 310 255 315
L 285 320
Q 325 325 330 350
L 335 370
Q 340 395 305 410
L 275 425
Q 235 440 200 425
L 175 410
Q 145 395 155 365
L 160 345
Q 165 340 180 330 Z"/>
<!-- LAVENDER -->
<path filter="url(#wcTexture)" fill="url(#gradLavender)" d="
M 80 460
Q 100 440 160 445
L 190 450
Q 235 455 240 480
L 245 500
Q 250 525 210 540
L 170 555
Q 130 570 95 555
L 65 540
Q 35 525 45 495
L 50 475
Q 55 470 80 460 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,123 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
// Models
import '../models/user_registration_data.dart';
import '../models/nanny_registration_data.dart';
// Screens
import '../screens/auth/login_screen.dart';
import '../screens/auth/register_choice_screen.dart';
import '../screens/auth/parent_register_step1_screen.dart';
import '../screens/auth/parent_register_step2_screen.dart';
import '../screens/auth/parent_register_step3_screen.dart';
import '../screens/auth/parent_register_step4_screen.dart';
import '../screens/auth/parent_register_step5_screen.dart';
import '../screens/auth/nanny_register_step1_screen.dart';
import '../screens/auth/nanny_register_step2_screen.dart';
import '../screens/auth/nanny_register_step3_screen.dart';
import '../screens/auth/nanny_register_step4_screen.dart';
import '../screens/auth/nanny_register_confirmation_screen.dart';
import '../screens/home/home_screen.dart';
import '../screens/unknown_screen.dart';
// --- Provider Instances ---
// It's generally better to provide these higher up the widget tree if possible,
// or ensure they are created only once.
// For ShellRoute, creating them here and passing via .value is common.
final userRegistrationDataNotifier = UserRegistrationData();
final nannyRegistrationDataNotifier = NannyRegistrationData();
class AppRouter {
static final GoRouter router = GoRouter(
initialLocation: '/login',
errorBuilder: (context, state) => const UnknownScreen(),
debugLogDiagnostics: true,
routes: <RouteBase>[
GoRoute(
path: '/login',
builder: (BuildContext context, GoRouterState state) => const LoginScreen(),
),
GoRoute(
path: '/register-choice',
builder: (BuildContext context, GoRouterState state) => const RegisterChoiceScreen(),
),
GoRoute(
path: '/home',
builder: (BuildContext context, GoRouterState state) => const HomeScreen(),
),
// --- Parent Registration Flow ---
ShellRoute(
builder: (context, state, child) {
return ChangeNotifierProvider<UserRegistrationData>.value(
value: userRegistrationDataNotifier,
child: child,
);
},
routes: <RouteBase>[
GoRoute(
path: '/parent-register-step1',
builder: (BuildContext context, GoRouterState state) => const ParentRegisterStep1Screen(),
),
GoRoute(
path: '/parent-register-step2',
builder: (BuildContext context, GoRouterState state) => const ParentRegisterStep2Screen(),
),
GoRoute(
path: '/parent-register-step3',
builder: (BuildContext context, GoRouterState state) => const ParentRegisterStep3Screen(),
),
GoRoute(
path: '/parent-register-step4',
builder: (BuildContext context, GoRouterState state) => const ParentRegisterStep4Screen(),
),
GoRoute(
path: '/parent-register-step5',
builder: (BuildContext context, GoRouterState state) => const ParentRegisterStep5Screen(),
),
GoRoute(
path: '/parent-register-confirmation',
builder: (BuildContext context, GoRouterState state) => const NannyRegisterConfirmationScreen(),
),
],
),
// --- Nanny Registration Flow ---
ShellRoute(
builder: (context, state, child) {
return ChangeNotifierProvider<NannyRegistrationData>.value(
value: nannyRegistrationDataNotifier,
child: child,
);
},
routes: <RouteBase>[
GoRoute(
path: '/nanny-register-step1',
builder: (BuildContext context, GoRouterState state) => const NannyRegisterStep1Screen(),
),
GoRoute(
path: '/nanny-register-step2',
builder: (BuildContext context, GoRouterState state) => const NannyRegisterStep2Screen(),
),
GoRoute(
path: '/nanny-register-step3',
builder: (BuildContext context, GoRouterState state) => const NannyRegisterStep3Screen(),
),
GoRoute(
path: '/nanny-register-step4',
builder: (BuildContext context, GoRouterState state) => const NannyRegisterStep4Screen(),
),
GoRoute(
path: '/nanny-register-confirmation',
builder: (BuildContext context, GoRouterState state) {
return const NannyRegisterConfirmationScreen();
},
),
],
),
],
);
}

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; // Import pour la localisation
// import 'package:provider/provider.dart'; // Supprimer Provider
import 'navigation/app_router.dart';
import 'config/app_router.dart'; // <-- Importer le bon routeur (GoRouter)
// import 'theme/app_theme.dart'; // Supprimer AppTheme
// import 'theme/theme_provider.dart'; // Supprimer ThemeProvider
@ -17,7 +17,7 @@ class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
// Pas besoin de Provider.of ici
return MaterialApp(
return MaterialApp.router( // <-- Utilisation de MaterialApp.router
title: 'P\'titsPas',
theme: ThemeData.light().copyWith( // Utiliser un thème simple par défaut
textTheme: GoogleFonts.meriendaTextTheme(
@ -35,8 +35,7 @@ class MyApp extends StatelessWidget {
// Locale('en', 'US'), // Anglais, si besoin
],
locale: const Locale('fr', 'FR'), // Forcer la locale française par défaut
initialRoute: AppRouter.login,
onGenerateRoute: AppRouter.generateRoute,
routerConfig: AppRouter.router, // <-- Passer la configuration du GoRouter
debugShowCheckedModeBanner: false,
);
}

View File

@ -0,0 +1,136 @@
import 'package:flutter/foundation.dart';
class NannyRegistrationData extends ChangeNotifier {
// Step 1: Identity Info
String firstName = '';
String lastName = '';
String streetAddress = ''; // Nouveau pour N° et Rue
String postalCode = ''; // Nouveau
String city = ''; // Nouveau
String phone = '';
String email = '';
String password = '';
// String? photoPath; // Déplacé ou géré à l'étape 2
// bool photoConsent = false; // Déplacé ou géré à l'étape 2
// Step 2: Professional Info
String? photoPath; // Ajouté pour l'étape 2
bool photoConsent = false; // Ajouté pour l'étape 2
DateTime? dateOfBirth;
String birthCity = ''; // Nouveau
String birthCountry = ''; // Nouveau
// String placeOfBirth = ''; // Remplacé par birthCity et birthCountry
String nir = ''; // Numéro de Sécurité Sociale
String agrementNumber = ''; // Numéro d'agrément
int? capacity; // Number of children the nanny can look after
// Step 3: Presentation & CGU
String presentationText = '';
bool cguAccepted = false;
// --- Methods to update data and notify listeners ---
void updateIdentityInfo({
String? firstName,
String? lastName,
String? streetAddress, // Modifié
String? postalCode, // Nouveau
String? city, // Nouveau
String? phone,
String? email,
String? password,
}) {
this.firstName = firstName ?? this.firstName;
this.lastName = lastName ?? this.lastName;
this.streetAddress = streetAddress ?? this.streetAddress; // Modifié
this.postalCode = postalCode ?? this.postalCode; // Nouveau
this.city = city ?? this.city; // Nouveau
this.phone = phone ?? this.phone;
this.email = email ?? this.email;
this.password = password ?? this.password;
// if (photoPath != null || this.photoPath != null) { // Supprimé de l'étape 1
// this.photoPath = photoPath;
// }
// this.photoConsent = photoConsent ?? this.photoConsent; // Supprimé de l'étape 1
notifyListeners();
}
void updateProfessionalInfo({
String? photoPath,
bool? photoConsent,
DateTime? dateOfBirth,
String? birthCity, // Nouveau
String? birthCountry, // Nouveau
// String? placeOfBirth, // Remplacé
String? nir,
String? agrementNumber,
int? capacity,
}) {
// Allow setting photoPath to null explicitly
if (photoPath != null || this.photoPath != null) {
this.photoPath = photoPath;
}
this.photoConsent = photoConsent ?? this.photoConsent;
this.dateOfBirth = dateOfBirth ?? this.dateOfBirth;
this.birthCity = birthCity ?? this.birthCity; // Nouveau
this.birthCountry = birthCountry ?? this.birthCountry; // Nouveau
// this.placeOfBirth = placeOfBirth ?? this.placeOfBirth; // Remplacé
this.nir = nir ?? this.nir;
this.agrementNumber = agrementNumber ?? this.agrementNumber;
this.capacity = capacity ?? this.capacity;
notifyListeners();
}
void updatePresentationAndCgu({
String? presentationText,
bool? cguAccepted,
}) {
this.presentationText = presentationText ?? this.presentationText;
this.cguAccepted = cguAccepted ?? this.cguAccepted;
notifyListeners();
}
// --- Getters for validation or display ---
bool get isStep1Complete =>
firstName.isNotEmpty &&
lastName.isNotEmpty &&
streetAddress.isNotEmpty && // Modifié
postalCode.isNotEmpty && // Nouveau
city.isNotEmpty && // Nouveau
phone.isNotEmpty &&
email.isNotEmpty &&
password.isNotEmpty;
bool get isStep2Complete =>
// photoConsent is mandatory if a photo is system-required, otherwise optional.
// For now, let's assume if photoPath is present, consent should ideally be true.
// Or, make consent always mandatory if photo section exists.
// Based on new mockup, photo is present, so consent might be implicitly or explicitly needed.
(photoPath != null ? photoConsent == true : true) && // Ajuster selon la logique de consentement désirée
dateOfBirth != null &&
birthCity.isNotEmpty &&
birthCountry.isNotEmpty &&
nir.isNotEmpty && // Basic check, could add validation
agrementNumber.isNotEmpty &&
capacity != null && capacity! > 0;
bool get isStep3Complete =>
// presentationText is optional as per CDC (message au gestionnaire)
cguAccepted;
bool get isRegistrationComplete =>
isStep1Complete && isStep2Complete && isStep3Complete;
@override
String toString() {
return 'NannyRegistrationData('
'firstName: $firstName, lastName: $lastName, '
'streetAddress: $streetAddress, postalCode: $postalCode, city: $city, '
'phone: $phone, email: $email, '
// 'photoPath: $photoPath, photoConsent: $photoConsent, ' // Commenté car déplacé/modifié
'dateOfBirth: $dateOfBirth, birthCity: $birthCity, birthCountry: $birthCountry, '
'nir: $nir, agrementNumber: $agrementNumber, capacity: $capacity, '
'photoPath (step2): $photoPath, photoConsent (step2): $photoConsent, '
'presentationText: $presentationText, cguAccepted: $cguAccepted)';
}
}

View File

@ -1,5 +1,8 @@
import 'dart:io'; // Pour File
import '../models/card_assets.dart'; // Import de l'enum CardColorVertical
import 'package:flutter/foundation.dart';
import 'package:intl/intl.dart';
// import 'package:p_tits_pas/models/child.dart'; // Commenté car fichier non trouvé
class ParentData {
String firstName;
@ -47,12 +50,28 @@ class ChildData {
});
}
class UserRegistrationData {
// Nouvelle classe pour les détails bancaires
class BankDetails {
String bankName;
String iban;
String bic;
BankDetails({
this.bankName = '',
this.iban = '',
this.bic = '',
});
}
class UserRegistrationData extends ChangeNotifier {
ParentData parent1;
ParentData? parent2; // Optionnel
List<ChildData> children;
String motivationText;
bool cguAccepted;
BankDetails? bankDetails; // Ajouté
String attestationCafNumber; // Ajouté
bool consentQuotientFamilial; // Ajouté
UserRegistrationData({
ParentData? parent1Data,
@ -60,38 +79,77 @@ class UserRegistrationData {
List<ChildData>? childrenData,
this.motivationText = '',
this.cguAccepted = false,
this.bankDetails, // Ajouté
this.attestationCafNumber = '', // Ajouté
this.consentQuotientFamilial = false, // Ajouté
}) : parent1 = parent1Data ?? ParentData(),
children = childrenData ?? [];
// Méthode pour ajouter/mettre à jour le parent 1
void updateParent1(ParentData data) {
parent1 = data;
notifyListeners(); // Notifier les changements
}
// Méthode pour ajouter/mettre à jour le parent 2
void updateParent2(ParentData? data) {
parent2 = data;
notifyListeners();
}
// Méthode pour ajouter un enfant
void addChild(ChildData child) {
children.add(child);
notifyListeners();
}
// Méthode pour mettre à jour un enfant (si nécessaire plus tard)
void updateChild(int index, ChildData child) {
if (index >= 0 && index < children.length) {
children[index] = child;
notifyListeners();
}
}
// Méthode pour supprimer un enfant
void removeChild(int index) {
if (index >= 0 && index < children.length) {
children.removeAt(index);
notifyListeners();
}
}
// Mettre à jour la motivation
void updateMotivation(String text) {
motivationText = text;
notifyListeners();
}
// Mettre à jour les informations bancaires et CAF
void updateFinancialInfo({
BankDetails? bankDetails,
String? attestationCafNumber,
bool? consentQuotientFamilial,
}) {
if (bankDetails != null) this.bankDetails = bankDetails;
if (attestationCafNumber != null) this.attestationCafNumber = attestationCafNumber;
if (consentQuotientFamilial != null) this.consentQuotientFamilial = consentQuotientFamilial;
notifyListeners();
}
// Accepter les CGU
void acceptCGU() {
cguAccepted = true;
void acceptCGU(bool accepted) { // Prend un booléen
cguAccepted = accepted;
notifyListeners();
}
// Méthode pour vérifier si toutes les données requises sont (simplifié)
bool isRegistrationComplete() {
// Ajouter ici les validations nécessaires
// Exemple : parent1 doit avoir des champs remplis, au moins un enfant, CGU acceptées
return parent1.firstName.isNotEmpty &&
parent1.lastName.isNotEmpty &&
children.isNotEmpty &&
cguAccepted;
}
}

View File

@ -67,11 +67,7 @@ class AppRouter {
slideTransition = true;
break;
case parentRegisterStep5:
if (args is UserRegistrationData) {
screen = ParentRegisterStep5Screen(registrationData: args);
} else {
screen = buildErrorScreen('5');
}
screen = const ParentRegisterStep5Screen();
slideTransition = true;
break;
case home:

View File

@ -8,14 +8,14 @@ import 'package:go_router/go_router.dart';
import '../../widgets/image_button.dart';
import '../../widgets/custom_app_text_field.dart';
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
State<LoginScreen> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
class _LoginPageState extends State<LoginScreen> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
@ -168,12 +168,12 @@ class _LoginPageState extends State<LoginPage> {
),
),
),
const SizedBox(height: 10),
// Lien de création de compte
const SizedBox(height: 20),
// Lien de création de compte (version originale)
Center(
child: TextButton(
onPressed: () {
Navigator.pushNamed(context, '/register-choice');
context.go('/register-choice');
},
child: Text(
'Créer un compte',
@ -185,7 +185,6 @@ class _LoginPageState extends State<LoginPage> {
),
),
),
const SizedBox(height: 20), // Réduit l'espacement en bas
],
),
),
@ -234,13 +233,13 @@ class _LoginPageState extends State<LoginPage> {
_FooterLink(
text: 'Mentions légales',
onTap: () {
Navigator.pushNamed(context, '/legal');
context.go('/legal');
},
),
_FooterLink(
text: 'Politique de confidentialité',
onTap: () {
Navigator.pushNamed(context, '/privacy');
context.go('/privacy');
},
),
],

View File

@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class NannyRegisterConfirmationScreen extends StatelessWidget {
const NannyRegisterConfirmationScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Inscription Soumise'),
automaticallyImplyLeading: false, // Remove back button
),
body: Center(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Icons.check_circle_outline, color: Colors.green, size: 80),
const SizedBox(height: 20),
const Text(
'Votre demande d\'inscription a été soumise avec succès !',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
const SizedBox(height: 15),
const Text(
'Votre compte est en attente de validation par un gestionnaire. Vous recevrez une notification par e-mail une fois votre compte activé.',
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
ElevatedButton(
onPressed: () {
// Navigate back to the login screen
context.go('/login');
},
child: const Text('Retour à la connexion'),
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,239 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import 'dart:math' as math; // Pour la rotation du chevron
import '../../../models/nanny_registration_data.dart';
import '../../../widgets/custom_app_text_field.dart';
import '../../../models/card_assets.dart'; // Pour les cartes
import '../../../utils/data_generator.dart'; // Implied import for DataGenerator
class NannyRegisterStep1Screen extends StatefulWidget {
const NannyRegisterStep1Screen({super.key});
@override
State<NannyRegisterStep1Screen> createState() => _NannyRegisterStep1ScreenState();
}
class _NannyRegisterStep1ScreenState extends State<NannyRegisterStep1Screen> {
final _formKey = GlobalKey<FormState>();
final _firstNameController = TextEditingController();
final _lastNameController = TextEditingController();
final _streetAddressController = TextEditingController();
final _postalCodeController = TextEditingController();
final _cityController = TextEditingController();
final _phoneController = TextEditingController();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
@override
void initState() {
super.initState();
final data = Provider.of<NannyRegistrationData>(context, listen: false);
_firstNameController.text = data.firstName;
_lastNameController.text = data.lastName;
_streetAddressController.text = data.streetAddress;
_postalCodeController.text = data.postalCode;
_cityController.text = data.city;
_phoneController.text = data.phone;
_emailController.text = data.email;
_passwordController.text = data.password;
_confirmPasswordController.text = data.password.isNotEmpty ? data.password : '';
if (data.firstName.isEmpty && data.lastName.isEmpty) {
final String genFirstName = DataGenerator.firstName();
final String genLastName = DataGenerator.lastName();
_firstNameController.text = genFirstName;
_lastNameController.text = genLastName;
_streetAddressController.text = DataGenerator.address();
_postalCodeController.text = DataGenerator.postalCode();
_cityController.text = DataGenerator.city();
_phoneController.text = DataGenerator.phone();
_emailController.text = DataGenerator.email(genFirstName, genLastName);
_passwordController.text = DataGenerator.password();
_confirmPasswordController.text = _passwordController.text;
}
}
@override
void dispose() {
_firstNameController.dispose();
_lastNameController.dispose();
_streetAddressController.dispose();
_postalCodeController.dispose();
_cityController.dispose();
_phoneController.dispose();
_emailController.dispose();
_passwordController.dispose();
_confirmPasswordController.dispose();
super.dispose();
}
void _submitForm() {
if (_formKey.currentState?.validate() ?? false) {
Provider.of<NannyRegistrationData>(context, listen: false)
.updateIdentityInfo(
firstName: _firstNameController.text,
lastName: _lastNameController.text,
streetAddress: _streetAddressController.text,
postalCode: _postalCodeController.text,
city: _cityController.text,
phone: _phoneController.text,
email: _emailController.text,
password: _passwordController.text,
);
context.go('/nanny-register-step2');
}
}
@override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
const cardColor = CardColorHorizontal.blue;
return Scaffold(
body: Stack(
children: [
Positioned.fill(
child: Image.asset(
'assets/images/paper2.png',
fit: BoxFit.cover,
repeat: ImageRepeat.repeat,
),
),
Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 40.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Étape 1/4',
style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54),
),
const SizedBox(height: 10),
Container(
width: screenSize.width * 0.7,
padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 50),
constraints: const BoxConstraints(minHeight: 600),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(cardColor.path),
fit: BoxFit.fill,
),
),
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Vos informations personnelles',
style: GoogleFonts.merienda(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(flex: 12, child: CustomAppTextField(controller: _lastNameController, labelText: 'Nom', hintText: 'Votre nom', fieldWidth: double.infinity)),
Expanded(flex: 1, child: const SizedBox()),
Expanded(flex: 12, child: CustomAppTextField(controller: _firstNameController, labelText: 'Prénom', hintText: 'Votre prénom', fieldWidth: double.infinity)),
],
),
const SizedBox(height: 20),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(flex: 12, child: CustomAppTextField(controller: _phoneController, labelText: 'Téléphone', hintText: 'Votre téléphone', keyboardType: TextInputType.phone, fieldWidth: double.infinity)),
Expanded(flex: 1, child: const SizedBox()),
Expanded(flex: 12, child: CustomAppTextField(controller: _emailController, labelText: 'Email', hintText: 'Votre e-mail', keyboardType: TextInputType.emailAddress, fieldWidth: double.infinity)),
],
),
const SizedBox(height: 20),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(flex: 12, child: CustomAppTextField(controller: _passwordController, labelText: 'Mot de passe', hintText: 'Minimum 6 caractères', obscureText: true, fieldWidth: double.infinity,
validator: (value) {
if (value == null || value.isEmpty) return 'Mot de passe requis';
if (value.length < 6) return '6 caractères minimum';
return null;
}
)),
Expanded(flex: 1, child: const SizedBox()),
Expanded(flex: 12, child: CustomAppTextField(controller: _confirmPasswordController, labelText: 'Confirmation', hintText: 'Confirmez le mot de passe', obscureText: true, fieldWidth: double.infinity,
validator: (value) {
if (value == null || value.isEmpty) return 'Confirmation requise';
if (value != _passwordController.text) return 'Les mots de passe ne correspondent pas';
return null;
}
)),
],
),
const SizedBox(height: 20),
CustomAppTextField(
controller: _streetAddressController,
labelText: 'Adresse (N° et Rue)',
hintText: 'Numéro et nom de votre rue',
fieldWidth: double.infinity,
validator: (v) => v!.isEmpty ? 'Adresse requise' : null,
),
const SizedBox(height: 20),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(flex: 5, child: CustomAppTextField(controller: _postalCodeController, labelText: 'Code Postal', hintText: 'C.P.', keyboardType: TextInputType.number, fieldWidth: double.infinity, validator: (v) => v!.isEmpty ? 'C.P. requis' : null)),
Expanded(flex: 1, child: const SizedBox()),
Expanded(flex: 12, child: CustomAppTextField(controller: _cityController, labelText: 'Ville', hintText: 'Votre ville', fieldWidth: double.infinity, validator: (v) => v!.isEmpty ? 'Ville requise' : null)),
],
),
],
),
),
),
],
),
),
),
Positioned(
top: screenSize.height / 2 - 20,
left: 40,
child: IconButton(
icon: Transform(
alignment: Alignment.center,
transform: Matrix4.rotationY(math.pi),
child: Image.asset('assets/images/chevron_right.png', height: 40),
),
onPressed: () {
if (context.canPop()) {
context.pop();
} else {
context.go('/register-choice');
}
},
tooltip: 'Retour',
),
),
Positioned(
top: screenSize.height / 2 - 20,
right: 40,
child: IconButton(
icon: Image.asset('assets/images/chevron_right.png', height: 40),
onPressed: _submitForm,
tooltip: 'Suivant',
),
),
],
),
);
}
}

View File

@ -0,0 +1,339 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
import 'package:google_fonts/google_fonts.dart';
import 'dart:math' as math;
import 'dart:io'; // Pour FileImage si _pickPhoto utilise un File
import '../../../models/nanny_registration_data.dart';
import '../../../widgets/custom_app_text_field.dart';
import '../../../widgets/app_custom_checkbox.dart'; // Import de la checkbox
import '../../../widgets/hover_relief_widget.dart'; // Import du HoverReliefWidget
import '../../../models/card_assets.dart';
// import '../../../utils/data_generator.dart'; // Plus besoin pour l'initialisation directe ici
class NannyRegisterStep2Screen extends StatefulWidget {
const NannyRegisterStep2Screen({super.key});
@override
State<NannyRegisterStep2Screen> createState() => _NannyRegisterStep2ScreenState();
}
class _NannyRegisterStep2ScreenState extends State<NannyRegisterStep2Screen> {
final _formKey = GlobalKey<FormState>();
final _dateOfBirthController = TextEditingController();
// final _placeOfBirthController = TextEditingController(); // Remplacé
final _birthCityController = TextEditingController(); // Nouveau
final _birthCountryController = TextEditingController(); // Nouveau
final _nirController = TextEditingController();
final _agrementController = TextEditingController();
final _capacityController = TextEditingController();
DateTime? _selectedDate;
String? _photoPathFramework; // Pour stocker le chemin de la photo (Asset ou File path)
File? _photoFile; // Pour stocker le fichier image si sélectionné localement
bool _photoConsent = false;
@override
void initState() {
super.initState();
final data = Provider.of<NannyRegistrationData>(context, listen: false);
_selectedDate = data.dateOfBirth;
_dateOfBirthController.text = data.dateOfBirth != null ? DateFormat('dd/MM/yyyy').format(data.dateOfBirth!) : '';
_birthCityController.text = data.birthCity;
_birthCountryController.text = data.birthCountry;
_nirController.text = data.nir;
_agrementController.text = data.agrementNumber;
_capacityController.text = data.capacity?.toString() ?? '';
// Gérer la photo existante (pourrait être un path d'asset ou un path de fichier)
if (data.photoPath != null) {
if (data.photoPath!.startsWith('assets/')) {
_photoPathFramework = data.photoPath;
_photoFile = null;
} else {
_photoFile = File(data.photoPath!);
_photoPathFramework = data.photoPath; // ou _photoFile.path
}
}
_photoConsent = data.photoConsent;
}
@override
void dispose() {
_dateOfBirthController.dispose();
_birthCityController.dispose();
_birthCountryController.dispose();
_nirController.dispose();
_agrementController.dispose();
_capacityController.dispose();
super.dispose();
}
Future<void> _selectDate(BuildContext context) async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: _selectedDate ?? DateTime.now().subtract(const Duration(days: 365 * 25)), // Default à 25 ans si null
firstDate: DateTime(1920, 1),
lastDate: DateTime.now().subtract(const Duration(days: 365 * 18)), // Assurer un âge minimum de 18 ans
locale: const Locale('fr', 'FR'),
);
if (picked != null && picked != _selectedDate) {
setState(() {
_selectedDate = picked;
_dateOfBirthController.text = DateFormat('dd/MM/yyyy').format(picked);
});
}
}
Future<void> _pickPhoto() async {
// TODO: Remplacer par la vraie logique ImagePicker
// final imagePicker = ImagePicker();
// final pickedFile = await imagePicker.pickImage(source: ImageSource.gallery);
// if (pickedFile != null) {
// setState(() {
// _photoFile = File(pickedFile.path);
// _photoPathFramework = pickedFile.path; // pour la sauvegarde
// });
// } else {
// // Simuler la sélection d'un asset pour test si aucun fichier n'est choisi
setState(() {
_photoPathFramework = 'assets/images/icon_assmat.png'; // Simule une photo asset
_photoFile = null; // Assurez-vous que _photoFile est null si c'est un asset
});
// }
print("Photo sélectionnée: $_photoPathFramework");
}
void _submitForm() {
if (_formKey.currentState!.validate()) {
if (_photoPathFramework != null && !_photoConsent) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Veuillez accepter le consentement photo pour continuer.')),
);
return;
}
Provider.of<NannyRegistrationData>(context, listen: false)
.updateProfessionalInfo(
photoPath: _photoPathFramework, // Sauvegarder le chemin (asset ou fichier)
photoConsent: _photoConsent,
dateOfBirth: _selectedDate,
birthCity: _birthCityController.text,
birthCountry: _birthCountryController.text,
nir: _nirController.text,
agrementNumber: _agrementController.text,
capacity: int.tryParse(_capacityController.text)
);
context.go('/nanny-register-step3');
}
}
@override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
const cardColor = CardColorHorizontal.green; // Couleur de la carte
final Color baseCardColorForShadow = Colors.green.shade300;
final Color initialPhotoShadow = baseCardColorForShadow.withAlpha(90);
final Color hoverPhotoShadow = baseCardColorForShadow.withAlpha(130);
ImageProvider? currentImageProvider;
if (_photoFile != null) {
currentImageProvider = FileImage(_photoFile!);
} else if (_photoPathFramework != null && _photoPathFramework!.startsWith('assets/')) {
currentImageProvider = AssetImage(_photoPathFramework!);
}
return Scaffold(
body: Stack(
children: [
Positioned.fill(
child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeat),
),
Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 40.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Étape 2/4', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)),
const SizedBox(height: 10),
Container(
width: screenSize.width * 0.7, // Largeur de la carte
padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 50),
constraints: const BoxConstraints(minHeight: 650), // Hauteur minimale ajustée
decoration: BoxDecoration(
image: DecorationImage(image: AssetImage(cardColor.path), fit: BoxFit.fill),
),
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Vos informations professionnelles',
style: GoogleFonts.merienda(
fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black87, // Couleur du titre ajustée
),
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Colonne Gauche: Photo et Checkbox
SizedBox(
width: 300, // Largeur fixe pour la colonne photo (200 * 1.5)
child: Column(
crossAxisAlignment: CrossAxisAlignment.center, // Centrer les éléments horizontalement
children: [
HoverReliefWidget(
onPressed: _pickPhoto,
borderRadius: BorderRadius.circular(10.0),
initialShadowColor: initialPhotoShadow,
hoverShadowColor: hoverPhotoShadow,
child: SizedBox(
height: 270, // (180 * 1.5)
width: 270, // (180 * 1.5)
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
image: currentImageProvider != null
? DecorationImage(image: currentImageProvider, fit: BoxFit.cover)
: null,
),
child: currentImageProvider == null
? Image.asset('assets/images/photo.png', fit: BoxFit.contain)
: null,
),
),
),
const SizedBox(height: 10), // Espace réduit
AppCustomCheckbox(
label: 'J\'accepte l\'utilisation de ma photo.',
value: _photoConsent,
onChanged: (val) => setState(() => _photoConsent = val ?? false),
),
],
),
),
const SizedBox(width: 30), // Augmenter l'espace entre les colonnes
// Colonne Droite: Champs de naissance
Expanded(
child: Column(
children: [
CustomAppTextField(
controller: _birthCityController,
labelText: 'Ville de naissance',
hintText: 'Votre ville de naissance',
fieldWidth: double.infinity,
validator: (v) => v!.isEmpty ? 'Ville requise' : null,
),
const SizedBox(height: 20),
CustomAppTextField(
controller: _birthCountryController,
labelText: 'Pays de naissance',
hintText: 'Votre pays de naissance',
fieldWidth: double.infinity,
validator: (v) => v!.isEmpty ? 'Pays requis' : null,
),
const SizedBox(height: 20),
CustomAppTextField(
controller: _dateOfBirthController,
labelText: 'Date de naissance',
hintText: 'JJ/MM/AAAA',
readOnly: true,
onTap: () => _selectDate(context),
suffixIcon: Icons.calendar_today, // Assurez-vous que CustomAppTextField gère suffixIcon
fieldWidth: double.infinity,
validator: (v) => _selectedDate == null ? 'Date requise' : null,
),
],
),
),
],
),
const SizedBox(height: 20),
CustomAppTextField(
controller: _nirController,
labelText: 'N° Sécurité Sociale (NIR)',
hintText: 'Votre NIR à 13 chiffres',
keyboardType: TextInputType.number,
fieldWidth: double.infinity,
validator: (v) { // Validation plus précise du NIR
if (v == null || v.isEmpty) return 'NIR requis';
if (v.length != 13) return 'Le NIR doit contenir 13 chiffres';
if (!RegExp(r'^[1-3]').hasMatch(v[0])) return 'Le NIR doit commencer par 1, 2 ou 3';
// D'autres validations plus complexes (clé de contrôle) peuvent être ajoutées
return null;
},
),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: CustomAppTextField(
controller: _agrementController,
labelText: 'N° d\'agrément',
hintText: 'Votre numéro d\'agrément',
fieldWidth: double.infinity,
validator: (v) => v!.isEmpty ? 'Agrément requis' : null,
),
),
const SizedBox(width: 20),
Expanded(
child: CustomAppTextField(
controller: _capacityController,
labelText: 'Capacité d\'accueil',
hintText: 'Ex: 3',
keyboardType: TextInputType.number,
fieldWidth: double.infinity,
validator: (v) {
if (v == null || v.isEmpty) return 'Capacité requise';
final n = int.tryParse(v);
if (n == null || n <= 0) return 'Nombre invalide';
return null;
},
),
),
],
),
],
),
),
),
],
),
),
),
// Chevron Gauche (Retour)
Positioned(
top: screenSize.height / 2 - 20,
left: 40,
child: IconButton(
icon: Transform(alignment: Alignment.center, transform: Matrix4.rotationY(math.pi), child: Image.asset('assets/images/chevron_right.png', height: 40)),
onPressed: () {
if (context.canPop()) {
context.pop();
} else {
context.go('/nanny-register-step1');
}
},
tooltip: 'Précédent',
),
),
// Chevron Droit (Suivant)
Positioned(
top: screenSize.height / 2 - 20,
right: 40,
child: IconButton(
icon: Image.asset('assets/images/chevron_right.png', height: 40),
onPressed: _submitForm,
tooltip: 'Suivant',
),
),
],
),
);
}
}

View File

@ -0,0 +1,145 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import 'dart:math' as math;
import '../../../models/nanny_registration_data.dart';
import '../../../widgets/custom_decorated_text_field.dart';
import '../../../models/card_assets.dart';
class NannyRegisterStep3Screen extends StatefulWidget {
const NannyRegisterStep3Screen({super.key});
@override
State<NannyRegisterStep3Screen> createState() => _NannyRegisterStep3ScreenState();
}
class _NannyRegisterStep3ScreenState extends State<NannyRegisterStep3Screen> {
final _formKey = GlobalKey<FormState>();
final _presentationController = TextEditingController();
bool _cguAccepted = false;
@override
void initState() {
super.initState();
final data = Provider.of<NannyRegistrationData>(context, listen: false);
_presentationController.text = 'Disponible immédiatement, expérience avec les tout-petits.';
_cguAccepted = true;
}
@override
void dispose() {
_presentationController.dispose();
super.dispose();
}
void _submitForm() {
final nannyData = Provider.of<NannyRegistrationData>(context, listen: false);
nannyData.updatePresentationAndCgu(presentationText: _presentationController.text);
// Validation CGU désactivée temporairement
nannyData.updatePresentationAndCgu(cguAccepted: _cguAccepted);
context.go('/nanny-register-step4');
}
@override
Widget build(BuildContext context) {
final nannyData = Provider.of<NannyRegistrationData>(context, listen: false);
final screenSize = MediaQuery.of(context).size;
const cardColor = CardColorHorizontal.peach; // Couleur différente
return Scaffold(
body: Stack(
children: [
Positioned.fill(
child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeat),
),
Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 40.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Étape 3/4', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)),
const SizedBox(height: 10),
Container(
width: screenSize.width * 0.7,
padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 50),
constraints: const BoxConstraints(minHeight: 500), // Ajuster hauteur
decoration: BoxDecoration(
image: DecorationImage(image: AssetImage(cardColor.path), fit: BoxFit.fill),
),
child: Form( // Garder Form même si validation simple
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Présentation et Conditions',
style: GoogleFonts.merienda(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black87),
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
Text(
'Rédigez un court message à destination du gestionnaire (facultatif) :',
style: TextStyle(fontSize: 16, color: Colors.black87),
),
const SizedBox(height: 10),
CustomDecoratedTextField(
controller: _presentationController,
hintText: 'Ex: Disponible immédiatement, formation premiers secours...',
maxLines: 6,
// style: cardColor.textFieldStyle, // Utiliser style par défaut ou adapter
),
const SizedBox(height: 30),
CheckboxListTile(
title: const Text('J\'ai lu et j\'accepte les Conditions Générales d\'Utilisation et la Politique de confidentialité de P\'titsPas.', style: TextStyle(fontSize: 14)),
subtitle: Text('Vous devez accepter pour continuer.', style: TextStyle(color: Colors.black54.withOpacity(0.7))),
value: _cguAccepted,
onChanged: (bool? value) {
setState(() { _cguAccepted = value ?? false; });
},
controlAffinity: ListTileControlAffinity.leading,
dense: true,
activeColor: Theme.of(context).primaryColor,
),
// TODO: Ajouter lien vers CGU
],
),
),
),
],
),
),
),
// Chevron Gauche (Retour)
Positioned(
top: screenSize.height / 2 - 20,
left: 40,
child: IconButton(
icon: Transform(alignment: Alignment.center, transform: Matrix4.rotationY(math.pi), child: Image.asset('assets/images/chevron_right.png', height: 40)),
onPressed: () {
if (context.canPop()) {
context.pop();
} else {
context.go('/nanny-register-step2');
}
},
tooltip: 'Précédent',
),
),
// Chevron Droit (Suivant)
Positioned(
top: screenSize.height / 2 - 20,
right: 40,
child: IconButton(
icon: Image.asset('assets/images/chevron_right.png', height: 40),
onPressed: _submitForm,
tooltip: 'Suivant',
),
),
],
),
);
}
}

View File

@ -0,0 +1,251 @@
import 'package:flutter/material.dart';
// import 'package:p_tits_pas/utils/resources/card_color_horizontal.dart'; // Supprimé car incorrect
import 'package:provider/provider.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
import 'dart:math' as math;
import 'package:p_tits_pas/models/card_assets.dart';
import '../../../models/nanny_registration_data.dart';
// import '../../../widgets/registration_scaffold.dart'; // Widget inexistant
// import '../../../widgets/recap_card.dart'; // Widget inexistant
import 'dart:io';
import 'package:google_fonts/google_fonts.dart';
class NannyRegisterStep4Screen extends StatelessWidget {
const NannyRegisterStep4Screen({super.key});
void _submitRegistration(BuildContext context) {
final nannyData = Provider.of<NannyRegistrationData>(context, listen: false);
print('Submitting Nanny Registration: ${nannyData.toString()}');
// TODO: Implement actual submission logic (e.g., API call)
context.go('/nanny-register-confirmation');
}
@override
Widget build(BuildContext context) {
final nannyData = Provider.of<NannyRegistrationData>(context);
final dateFormat = DateFormat('dd/MM/yyyy');
final size = MediaQuery.of(context).size;
final bool canSubmit = nannyData.isRegistrationComplete; // Check completeness
return Scaffold( // Main scaffold to contain the stack
body: Stack(
children: [
// Background image
Positioned.fill(
child: Image.asset(
'assets/images/paper2.png', // Assurez-vous que le chemin est correct
fit: BoxFit.cover,
),
),
// Content centered
Center(
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: size.width * 0.8, // Adjust width as needed
maxHeight: size.height * 0.85, // Adjust height as needed
),
child: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(CardColorHorizontal.blue.path),
fit: BoxFit.fill,
),
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(top: 40.0, bottom: 10.0),
child: Text(
'Récapitulatif',
style: GoogleFonts.merienda(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
),
textAlign: TextAlign.center,
),
),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 15.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Veuillez vérifier attentivement les informations avant de soumettre.',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.white70),
),
const SizedBox(height: 20),
// --- Identity Card (Using standard Card for grouping) ---
Card(
elevation: 2.0,
margin: const EdgeInsets.symmetric(vertical: 8.0),
child: Padding(
padding: const EdgeInsets.all(15.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildCardTitle(context, 'Informations personnelles', '/nanny-register-step1'),
const Divider(),
if (nannyData.photoPath != null && nannyData.photoPath!.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(vertical: 10.0),
child: Center(
child: CircleAvatar(
radius: 40,
backgroundImage: FileImage(File(nannyData.photoPath!)),
onBackgroundImageError: (exception, stackTrace) {
print("Erreur chargement image: $exception");
// Optionnel: afficher un placeholder ou icône d'erreur
},
),
),
),
_buildRecapRow('Nom:', '${nannyData.firstName} ${nannyData.lastName}'),
_buildRecapRow('Adresse:', '${nannyData.streetAddress}${nannyData.postalCode.isNotEmpty ? '\n${nannyData.postalCode}' : ''}${nannyData.city.isNotEmpty ? ' ${nannyData.city}' : ''}'.trim()),
_buildRecapRow('Téléphone:', nannyData.phone),
_buildRecapRow('Email:', nannyData.email),
_buildRecapRow('Consentement Photo:', nannyData.photoConsent ? 'Oui' : 'Non'),
],
),
),
),
// --- Professional Info Card (Using standard Card) ---
Card(
elevation: 2.0,
margin: const EdgeInsets.symmetric(vertical: 8.0),
child: Padding(
padding: const EdgeInsets.all(15.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildCardTitle(context, 'Informations professionnelles', '/nanny-register-step2'),
const Divider(),
_buildRecapRow('Date de naissance:', nannyData.dateOfBirth != null ? dateFormat.format(nannyData.dateOfBirth!) : 'Non renseigné'),
_buildRecapRow('Lieu de naissance:', '${nannyData.birthCity}, ${nannyData.birthCountry}'.isNotEmpty ? '${nannyData.birthCity}, ${nannyData.birthCountry}' : 'Non renseigné'),
_buildRecapRow('N° Sécurité Sociale:', nannyData.nir.isNotEmpty ? nannyData.nir : 'Non renseigné'), // TODO: Mask this?
_buildRecapRow('N° Agrément:', nannyData.agrementNumber.isNotEmpty ? nannyData.agrementNumber : 'Non renseigné'),
_buildRecapRow('Capacité d\'accueil:', nannyData.capacity?.toString() ?? 'Non renseigné'),
],
),
),
),
// --- Presentation Card (Using standard Card) ---
Card(
elevation: 2.0,
margin: const EdgeInsets.symmetric(vertical: 8.0),
child: Padding(
padding: const EdgeInsets.all(15.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildCardTitle(context, 'Présentation & CGU', '/nanny-register-step3'),
const Divider(),
const Text('Votre présentation (facultatif) :', style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 5),
Container(
padding: const EdgeInsets.all(10),
width: double.infinity,
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(5),
border: Border.all(color: Colors.grey[300]!)
),
child: Text(
nannyData.presentationText.isNotEmpty
? nannyData.presentationText
: 'Aucune présentation rédigée.',
style: TextStyle(
color: nannyData.presentationText.isNotEmpty ? Colors.black87 : Colors.grey,
fontStyle: nannyData.presentationText.isNotEmpty ? FontStyle.normal : FontStyle.italic
),
),
),
const SizedBox(height: 15),
_buildRecapRow('CGU Acceptées:', nannyData.cguAccepted ? 'Oui' : 'Non'),
],
),
),
),
if (!canSubmit) // Show warning if incomplete
Padding(
padding: const EdgeInsets.only(top: 15.0, bottom: 5.0), // Add some space
child: Text(
'Veuillez compléter toutes les étapes requises et accepter les CGU pour pouvoir soumettre.',
textAlign: TextAlign.center,
style: TextStyle(color: Theme.of(context).colorScheme.error, fontWeight: FontWeight.bold),
),
),
],
),
),
),
],
),
),
),
),
// Navigation buttons
Positioned(
top: size.height / 2 - 20, // Centré verticalement
left: 40,
child: IconButton(
icon: Transform(
alignment: Alignment.center,
transform: Matrix4.rotationY(math.pi),
child: Image.asset('assets/images/chevron_right.png', height: 40),
),
onPressed: () => context.go('/nanny-register-step3'),
tooltip: 'Précédent',
),
),
Positioned(
top: size.height / 2 - 20, // Centré verticalement
right: 40,
child: IconButton(
icon: Image.asset('assets/images/chevron_right.png', height: 40),
onPressed: canSubmit ? () => _submitRegistration(context) : null,
tooltip: 'Soumettre',
),
),
],
),
);
}
// Helper to build title row with edit button
Widget _buildCardTitle(BuildContext context, String title, String editRoute) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
IconButton(
icon: const Icon(Icons.edit, size: 20),
onPressed: () => context.go(editRoute),
tooltip: 'Modifier',
),
],
);
}
// Helper to build data row
Widget _buildRecapRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('$label ', style: const TextStyle(fontWeight: FontWeight.bold)),
Expanded(child: Text(value)),
],
),
);
}
}

View File

@ -5,6 +5,8 @@ import '../../models/user_registration_data.dart'; // Import du modèle de donn
import '../../utils/data_generator.dart'; // Import du générateur de données
import '../../widgets/custom_app_text_field.dart'; // Import du widget CustomAppTextField
import '../../models/card_assets.dart'; // Import des enums de cartes
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart'; // Importer Provider
class ParentRegisterStep1Screen extends StatefulWidget {
const ParentRegisterStep1Screen({super.key});
@ -15,31 +17,44 @@ class ParentRegisterStep1Screen extends StatefulWidget {
class _ParentRegisterStep1ScreenState extends State<ParentRegisterStep1Screen> {
final _formKey = GlobalKey<FormState>();
late UserRegistrationData _registrationData;
// late UserRegistrationData _registrationData; // Supprimé, on utilisera le Provider
// Contrôleurs pour les champs (restauration CP et Ville)
// Contrôleurs pour les champs
final _lastNameController = TextEditingController();
final _firstNameController = TextEditingController();
final _phoneController = TextEditingController();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
final _addressController = TextEditingController(); // Rue seule
final _postalCodeController = TextEditingController(); // Restauré
final _cityController = TextEditingController(); // Restauré
final _addressController = TextEditingController();
final _postalCodeController = TextEditingController();
final _cityController = TextEditingController();
@override
void initState() {
super.initState();
_registrationData = UserRegistrationData();
// Récupérer les données existantes du Provider pour pré-remplir si l'utilisateur revient
final registrationDataFromProvider = Provider.of<UserRegistrationData>(context, listen: false);
_firstNameController.text = registrationDataFromProvider.parent1.firstName;
_lastNameController.text = registrationDataFromProvider.parent1.lastName;
_phoneController.text = registrationDataFromProvider.parent1.phone;
_emailController.text = registrationDataFromProvider.parent1.email;
_passwordController.text = registrationDataFromProvider.parent1.password;
_confirmPasswordController.text = registrationDataFromProvider.parent1.password; // Ou laisser vide pour reconfirmation
_addressController.text = registrationDataFromProvider.parent1.address;
_postalCodeController.text = registrationDataFromProvider.parent1.postalCode;
_cityController.text = registrationDataFromProvider.parent1.city;
// Si les champs sont vides (première visite), générer des données
if (registrationDataFromProvider.parent1.firstName.isEmpty) {
_generateAndFillData();
}
}
void _generateAndFillData() {
final String genFirstName = DataGenerator.firstName();
final String genLastName = DataGenerator.lastName();
// Utilisation des méthodes publiques de DataGenerator
_addressController.text = DataGenerator.address();
_postalCodeController.text = DataGenerator.postalCode();
_cityController.text = DataGenerator.city();
@ -66,6 +81,25 @@ class _ParentRegisterStep1ScreenState extends State<ParentRegisterStep1Screen> {
super.dispose();
}
void _submitForm() {
if (_formKey.currentState?.validate() ?? false) {
final registrationData = Provider.of<UserRegistrationData>(context, listen: false);
registrationData.updateParent1(
ParentData(
firstName: _firstNameController.text,
lastName: _lastNameController.text,
address: _addressController.text,
postalCode: _postalCodeController.text,
city: _cityController.text,
phone: _phoneController.text,
email: _emailController.text,
password: _passwordController.text, // Sauvegarder le mot de passe
)
);
context.go('/parent-register-step2');
}
}
@override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
@ -188,7 +222,13 @@ class _ParentRegisterStep1ScreenState extends State<ParentRegisterStep1Screen> {
transform: Matrix4.rotationY(math.pi), // Inverse horizontalement
child: Image.asset('assets/images/chevron_right.png', height: 40),
),
onPressed: () => Navigator.pop(context), // Retour à l'écran de choix
onPressed: () {
if (context.canPop()) {
context.pop();
} else {
context.go('/register-choice');
}
},
tooltip: 'Retour',
),
),
@ -199,23 +239,7 @@ class _ParentRegisterStep1ScreenState extends State<ParentRegisterStep1Screen> {
right: 40,
child: IconButton(
icon: Image.asset('assets/images/chevron_right.png', height: 40),
onPressed: () {
if (_formKey.currentState?.validate() ?? false) {
_registrationData.updateParent1(
ParentData(
firstName: _firstNameController.text,
lastName: _lastNameController.text,
address: _addressController.text, // Rue
postalCode: _postalCodeController.text, // Ajout
city: _cityController.text, // Ajout
phone: _phoneController.text,
email: _emailController.text,
password: _passwordController.text,
)
);
Navigator.pushNamed(context, '/parent-register/step2', arguments: _registrationData);
}
},
onPressed: _submitForm, // Appel de la fonction de soumission
tooltip: 'Suivant',
),
),

View File

@ -5,19 +5,22 @@ import '../../models/user_registration_data.dart'; // Import du modèle
import '../../utils/data_generator.dart'; // Import du générateur
import '../../widgets/custom_app_text_field.dart'; // Import du widget
import '../../models/card_assets.dart'; // Import des enums de cartes
import 'package:provider/provider.dart';
import 'package:go_router/go_router.dart'; // Importer GoRouter
class ParentRegisterStep2Screen extends StatefulWidget {
final UserRegistrationData registrationData; // Accepte les données de l'étape 1
// final UserRegistrationData registrationData; // Supprimé
const ParentRegisterStep2Screen({super.key, required this.registrationData});
const ParentRegisterStep2Screen({super.key /*, required this.registrationData */}); // Modifié
@override
State<ParentRegisterStep2Screen> createState() => _ParentRegisterStep2ScreenState();
_ParentRegisterStep2ScreenState createState() =>
_ParentRegisterStep2ScreenState();
}
class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
final _formKey = GlobalKey<FormState>();
late UserRegistrationData _registrationData; // Copie locale pour modification
// late UserRegistrationData _registrationData; // Supprimé
bool _addParent2 = true; // Pour le test, on ajoute toujours le parent 2
bool _sameAddressAsParent1 = false; // Peut être généré aléatoirement aussi
@ -36,13 +39,19 @@ class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
@override
void initState() {
super.initState();
_registrationData = widget.registrationData; // Récupère les données de l'étape 1
// On ne récupère plus _registrationData ici
// Mais on peut récupérer les données initiales pour les contrôleurs si nécessaire
final initialData = Provider.of<UserRegistrationData>(context, listen: false);
_addParent2 = initialData.parent2 != null;
if (_addParent2) {
_generateAndFillParent2Data();
_fillParent2Data(initialData.parent2!, initialData.parent1);
} else {
_generateAndFillParent2Data(initialData.parent1); // Ou générer si pas de données
}
}
void _generateAndFillParent2Data() {
// Modifié pour prendre les données parent1
void _generateAndFillParent2Data(ParentData parent1Data) {
final String genFirstName = DataGenerator.firstName();
final String genLastName = DataGenerator.lastName();
_firstNameController.text = genFirstName;
@ -60,12 +69,30 @@ class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
_cityController.text = DataGenerator.city();
} else {
// Vider les champs si même adresse (seront désactivés)
_addressController.clear();
_postalCodeController.clear();
_cityController.clear();
_addressController.text = parent1Data.address;
_postalCodeController.text = parent1Data.postalCode;
_cityController.text = parent1Data.city;
}
}
// Nouvelle fonction pour remplir depuis les données existantes
void _fillParent2Data(ParentData parent2Data, ParentData parent1Data) {
_firstNameController.text = parent2Data.firstName;
_lastNameController.text = parent2Data.lastName;
_phoneController.text = parent2Data.phone;
_emailController.text = parent2Data.email;
_passwordController.text = parent2Data.password; // Attention à la sécurité
_confirmPasswordController.text = parent2Data.password;
_sameAddressAsParent1 = (parent2Data.address == parent1Data.address &&
parent2Data.postalCode == parent1Data.postalCode &&
parent2Data.city == parent1Data.city);
_addressController.text = parent2Data.address;
_postalCodeController.text = parent2Data.postalCode;
_cityController.text = parent2Data.city;
}
@override
void dispose() {
_lastNameController.dispose();
@ -85,6 +112,8 @@ class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
@override
Widget build(BuildContext context) {
final registrationData = Provider.of<UserRegistrationData>(context, listen: false);
final parent1Data = registrationData.parent1;
final screenSize = MediaQuery.of(context).size;
return Scaffold(
@ -129,7 +158,7 @@ class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
const Spacer(),
Switch(value: _addParent2, onChanged: (val) => setState(() {
_addParent2 = val ?? false;
if (_addParent2) _generateAndFillParent2Data(); else _clearParent2Fields();
if (_addParent2) _generateAndFillParent2Data(parent1Data); else _clearParent2Fields();
}), activeColor: Theme.of(context).primaryColor),
]),
),
@ -144,9 +173,9 @@ class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
Switch(value: _sameAddressAsParent1, onChanged: _addParent2 ? (val) => setState(() {
_sameAddressAsParent1 = val ?? false;
if (_sameAddressAsParent1) {
_addressController.text = _registrationData.parent1.address;
_postalCodeController.text = _registrationData.parent1.postalCode;
_cityController.text = _registrationData.parent1.city;
_addressController.text = parent1Data.address;
_postalCodeController.text = parent1Data.postalCode;
_cityController.text = parent1Data.city;
} else {
_addressController.text = DataGenerator.address();
_postalCodeController.text = DataGenerator.postalCode();
@ -204,7 +233,13 @@ class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
left: 40,
child: IconButton(
icon: Transform(alignment: Alignment.center, transform: Matrix4.rotationY(math.pi), child: Image.asset('assets/images/chevron_right.png', height: 40)),
onPressed: () => Navigator.pop(context),
onPressed: () {
if (context.canPop()) {
context.pop();
} else {
context.go('/parent-register-step1');
}
},
tooltip: 'Retour',
),
),
@ -216,22 +251,22 @@ class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
onPressed: () {
if (!_addParent2 || (_formKey.currentState?.validate() ?? false)) {
if (_addParent2) {
_registrationData.updateParent2(
registrationData.updateParent2(
ParentData(
firstName: _firstNameController.text,
lastName: _lastNameController.text,
address: _sameAddressAsParent1 ? _registrationData.parent1.address : _addressController.text,
postalCode: _sameAddressAsParent1 ? _registrationData.parent1.postalCode : _postalCodeController.text,
city: _sameAddressAsParent1 ? _registrationData.parent1.city : _cityController.text,
address: _sameAddressAsParent1 ? parent1Data.address : _addressController.text,
postalCode: _sameAddressAsParent1 ? parent1Data.postalCode : _postalCodeController.text,
city: _sameAddressAsParent1 ? parent1Data.city : _cityController.text,
phone: _phoneController.text,
email: _emailController.text,
password: _passwordController.text,
)
);
} else {
_registrationData.updateParent2(null);
registrationData.updateParent2(null);
}
Navigator.pushNamed(context, '/parent-register/step3', arguments: _registrationData);
context.go('/parent-register-step3');
}
},
tooltip: 'Suivant',

View File

@ -12,20 +12,23 @@ import '../../widgets/app_custom_checkbox.dart'; // Import du nouveau widget Che
import '../../models/user_registration_data.dart'; // Import du modèle de données
import '../../utils/data_generator.dart'; // Import du générateur
import '../../models/card_assets.dart'; // Import des enums de cartes
import 'package:provider/provider.dart'; // Assurer l'import
import 'package:go_router/go_router.dart';
// La classe _ChildFormData est supprimée car on utilise ChildData du modèle
class ParentRegisterStep3Screen extends StatefulWidget {
final UserRegistrationData registrationData; // Accepte les données
// final UserRegistrationData registrationData; // Supprimé
const ParentRegisterStep3Screen({super.key, required this.registrationData});
const ParentRegisterStep3Screen({super.key /*, required this.registrationData */}); // Modifié
@override
State<ParentRegisterStep3Screen> createState() => _ParentRegisterStep3ScreenState();
_ParentRegisterStep3ScreenState createState() =>
_ParentRegisterStep3ScreenState();
}
class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
late UserRegistrationData _registrationData; // Stocke l'état complet
// late UserRegistrationData _registrationData; // Supprimé
final ScrollController _scrollController = ScrollController(); // Pour le défilement horizontal
bool _isScrollable = false;
bool _showLeftFade = false;
@ -52,14 +55,18 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
@override
void initState() {
super.initState();
_registrationData = widget.registrationData;
final registrationData = Provider.of<UserRegistrationData>(context, listen: false);
// _registrationData = registrationData; // Supprimé
// Initialiser les couleurs utilisées avec les enfants existants
for (var child in _registrationData.children) {
for (var child in registrationData.children) {
_usedColors.add(child.cardColor);
}
// S'il n'y a pas d'enfant, en ajouter un automatiquement avec des données générées
if (_registrationData.children.isEmpty) {
_addChild();
// S'il n'y a pas d'enfant, en ajouter un automatiquement APRÈS le premier build
if (registrationData.children.isEmpty) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_addChild(registrationData);
});
}
_scrollController.addListener(_scrollListener);
WidgetsBinding.instance.addPostFrameCallback((_) => _scrollListener());
@ -87,7 +94,7 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
}
}
void _addChild() {
void _addChild(UserRegistrationData registrationData) { // Prend registrationData
setState(() {
bool isUnborn = DataGenerator.boolean();
@ -98,7 +105,7 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
);
final newChild = ChildData(
lastName: _registrationData.parent1.lastName,
lastName: registrationData.parent1.lastName,
firstName: DataGenerator.firstName(),
dob: DataGenerator.dob(isUnborn: isUnborn),
isUnbornChild: isUnborn,
@ -106,7 +113,7 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
multipleBirth: DataGenerator.boolean(),
cardColor: cardColor,
);
_registrationData.addChild(newChild);
registrationData.addChild(newChild);
_usedColors.add(cardColor);
});
WidgetsBinding.instance.addPostFrameCallback((_) {
@ -117,33 +124,42 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
});
}
void _removeChild(int index) {
if (_registrationData.children.length > 1 && index >= 0 && index < _registrationData.children.length) {
void _removeChild(int index, UserRegistrationData registrationData) {
if (registrationData.children.length > 1 && index >= 0 && index < registrationData.children.length) {
setState(() {
// Ne pas retirer la couleur de _usedColors pour éviter sa réutilisation
_registrationData.children.removeAt(index);
registrationData.children.removeAt(index);
});
WidgetsBinding.instance.addPostFrameCallback((_) => _scrollListener());
}
}
Future<void> _pickImage(int childIndex) async {
Future<void> _pickImage(int childIndex, UserRegistrationData registrationData) async {
final ImagePicker picker = ImagePicker();
try {
final XFile? pickedFile = await picker.pickImage(
source: ImageSource.gallery, imageQuality: 70, maxWidth: 1024, maxHeight: 1024);
if (pickedFile != null) {
setState(() {
if (childIndex < _registrationData.children.length) {
_registrationData.children[childIndex].imageFile = File(pickedFile.path);
if (childIndex < registrationData.children.length) {
final oldChild = registrationData.children[childIndex];
final updatedChild = ChildData(
firstName: oldChild.firstName,
lastName: oldChild.lastName,
dob: oldChild.dob,
photoConsent: oldChild.photoConsent,
multipleBirth: oldChild.multipleBirth,
isUnbornChild: oldChild.isUnbornChild,
imageFile: File(pickedFile.path),
cardColor: oldChild.cardColor,
);
registrationData.updateChild(childIndex, updatedChild);
}
});
}
} catch (e) { print("Erreur image: $e"); }
}
Future<void> _selectDate(BuildContext context, int childIndex) async {
final ChildData currentChild = _registrationData.children[childIndex];
Future<void> _selectDate(BuildContext context, int childIndex, UserRegistrationData registrationData) async {
final ChildData currentChild = registrationData.children[childIndex];
final DateTime now = DateTime.now();
DateTime initialDatePickerDate = now;
DateTime firstDatePickerDate = DateTime(1980); DateTime lastDatePickerDate = now;
@ -175,14 +191,24 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
lastDate: lastDatePickerDate, locale: const Locale('fr', 'FR'),
);
if (picked != null) {
setState(() {
currentChild.dob = "${picked.day.toString().padLeft(2, '0')}/${picked.month.toString().padLeft(2, '0')}/${picked.year}";
});
final oldChild = registrationData.children[childIndex];
final updatedChild = ChildData(
firstName: oldChild.firstName,
lastName: oldChild.lastName,
dob: "${picked.day.toString().padLeft(2, '0')}/${picked.month.toString().padLeft(2, '0')}/${picked.year}",
photoConsent: oldChild.photoConsent,
multipleBirth: oldChild.multipleBirth,
isUnbornChild: oldChild.isUnbornChild,
imageFile: oldChild.imageFile,
cardColor: oldChild.cardColor,
);
registrationData.updateChild(childIndex, updatedChild);
}
}
@override
Widget build(BuildContext context) {
final registrationData = Provider.of<UserRegistrationData>(context /*, listen: true par défaut */);
final screenSize = MediaQuery.of(context).size;
return Scaffold(
body: Stack(
@ -221,36 +247,57 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
controller: _scrollController,
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 20.0),
itemCount: _registrationData.children.length + 1,
itemCount: registrationData.children.length + 1,
itemBuilder: (context, index) {
if (index < _registrationData.children.length) {
if (index < registrationData.children.length) {
// Carte Enfant
return Padding(
padding: const EdgeInsets.only(right: 20.0),
child: _ChildCardWidget(
key: ValueKey(_registrationData.children[index].hashCode), // Utiliser une clé basée sur les données
childData: _registrationData.children[index],
key: ValueKey(registrationData.children[index].hashCode), // Utiliser une clé basée sur les données
childData: registrationData.children[index],
childIndex: index,
onPickImage: () => _pickImage(index),
onDateSelect: () => _selectDate(context, index),
onFirstNameChanged: (value) => setState(() => _registrationData.children[index].firstName = value),
onLastNameChanged: (value) => setState(() => _registrationData.children[index].lastName = value),
onTogglePhotoConsent: (newValue) => setState(() => _registrationData.children[index].photoConsent = newValue),
onToggleMultipleBirth: (newValue) => setState(() => _registrationData.children[index].multipleBirth = newValue),
onToggleIsUnborn: (newValue) => setState(() {
_registrationData.children[index].isUnbornChild = newValue;
// Générer une nouvelle date si on change le statut
_registrationData.children[index].dob = DataGenerator.dob(isUnborn: newValue);
}),
onRemove: () => _removeChild(index),
canBeRemoved: _registrationData.children.length > 1,
onPickImage: () => _pickImage(index, registrationData),
onDateSelect: () => _selectDate(context, index, registrationData),
onFirstNameChanged: (value) => setState(() => registrationData.updateChild(index, ChildData(
firstName: value, lastName: registrationData.children[index].lastName, dob: registrationData.children[index].dob, photoConsent: registrationData.children[index].photoConsent,
multipleBirth: registrationData.children[index].multipleBirth, isUnbornChild: registrationData.children[index].isUnbornChild, imageFile: registrationData.children[index].imageFile, cardColor: registrationData.children[index].cardColor
))),
onLastNameChanged: (value) => setState(() => registrationData.updateChild(index, ChildData(
firstName: registrationData.children[index].firstName, lastName: value, dob: registrationData.children[index].dob, photoConsent: registrationData.children[index].photoConsent,
multipleBirth: registrationData.children[index].multipleBirth, isUnbornChild: registrationData.children[index].isUnbornChild, imageFile: registrationData.children[index].imageFile, cardColor: registrationData.children[index].cardColor
))),
onTogglePhotoConsent: (newValue) {
final oldChild = registrationData.children[index];
registrationData.updateChild(index, ChildData(
firstName: oldChild.firstName, lastName: oldChild.lastName, dob: oldChild.dob, photoConsent: newValue,
multipleBirth: oldChild.multipleBirth, isUnbornChild: oldChild.isUnbornChild, imageFile: oldChild.imageFile, cardColor: oldChild.cardColor
));
},
onToggleMultipleBirth: (newValue) {
final oldChild = registrationData.children[index];
registrationData.updateChild(index, ChildData(
firstName: oldChild.firstName, lastName: oldChild.lastName, dob: oldChild.dob, photoConsent: oldChild.photoConsent,
multipleBirth: newValue, isUnbornChild: oldChild.isUnbornChild, imageFile: oldChild.imageFile, cardColor: oldChild.cardColor
));
},
onToggleIsUnborn: (newValue) {
final oldChild = registrationData.children[index];
registrationData.updateChild(index, ChildData(
firstName: oldChild.firstName, lastName: oldChild.lastName, dob: DataGenerator.dob(isUnborn: newValue),
photoConsent: oldChild.photoConsent, multipleBirth: oldChild.multipleBirth, isUnbornChild: newValue,
imageFile: oldChild.imageFile, cardColor: oldChild.cardColor
));
},
onRemove: () => _removeChild(index, registrationData),
canBeRemoved: registrationData.children.length > 1,
),
);
} else {
// Bouton Ajouter
return Center(
child: HoverReliefWidget(
onPressed: _addChild,
onPressed: () => _addChild(registrationData),
borderRadius: BorderRadius.circular(15),
child: Image.asset('assets/images/plus.png', height: 80, width: 80),
),
@ -272,7 +319,13 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
left: 40,
child: IconButton(
icon: Transform(alignment: Alignment.center, transform: Matrix4.rotationY(math.pi), child: Image.asset('assets/images/chevron_right.png', height: 40)),
onPressed: () => Navigator.pop(context),
onPressed: () {
if (context.canPop()) {
context.pop();
} else {
context.go('/parent-register-step2');
}
},
tooltip: 'Retour',
),
),
@ -282,8 +335,7 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
child: IconButton(
icon: Image.asset('assets/images/chevron_right.png', height: 40),
onPressed: () {
// TODO: Validation (si nécessaire)
Navigator.pushNamed(context, '/parent-register/step4', arguments: _registrationData);
context.go('/parent-register-step4');
},
tooltip: 'Suivant',
),

View File

@ -7,31 +7,50 @@ import 'package:p_tits_pas/widgets/app_custom_checkbox.dart'; // Import de la ch
import '../../models/user_registration_data.dart'; // Import du vrai modèle
import '../../utils/data_generator.dart'; // Import du générateur
import '../../models/card_assets.dart'; // Import des enums de cartes
import 'package:provider/provider.dart'; // Assurer l'import
import 'package:go_router/go_router.dart';
class ParentRegisterStep4Screen extends StatefulWidget {
final UserRegistrationData registrationData; // Accepte les données
// final UserRegistrationData registrationData; // Supprimé
const ParentRegisterStep4Screen({super.key, required this.registrationData});
const ParentRegisterStep4Screen({super.key /*, required this.registrationData */}); // Modifié
@override
State<ParentRegisterStep4Screen> createState() => _ParentRegisterStep4ScreenState();
_ParentRegisterStep4ScreenState createState() =>
_ParentRegisterStep4ScreenState();
}
class _ParentRegisterStep4ScreenState extends State<ParentRegisterStep4Screen> {
late UserRegistrationData _registrationData; // État local
// late UserRegistrationData _registrationData; // Supprimé
final _motivationController = TextEditingController();
bool _cguAccepted = true; // Pour le test, CGU acceptées par défaut
final _bankNameController = TextEditingController();
final _ibanController = TextEditingController();
final _bicController = TextEditingController();
final _attestationController = TextEditingController();
bool _consentQuotientFamilial = false;
@override
void initState() {
super.initState();
_registrationData = widget.registrationData;
_motivationController.text = DataGenerator.motivation(); // Générer la motivation
final registrationData = Provider.of<UserRegistrationData>(context, listen: false);
// _registrationData = registrationData; // Supprimé
_motivationController.text = registrationData.motivationText.isNotEmpty ? registrationData.motivationText : DataGenerator.motivation();
_bankNameController.text = registrationData.bankDetails?.bankName ?? '';
_ibanController.text = registrationData.bankDetails?.iban ?? '';
_bicController.text = registrationData.bankDetails?.bic ?? '';
_attestationController.text = registrationData.attestationCafNumber;
_consentQuotientFamilial = registrationData.consentQuotientFamilial;
_cguAccepted = registrationData.cguAccepted;
}
@override
void dispose() {
_motivationController.dispose();
_bankNameController.dispose();
_ibanController.dispose();
_bicController.dispose();
_attestationController.dispose();
super.dispose();
}
@ -100,6 +119,7 @@ Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lo
@override
Widget build(BuildContext context) {
final registrationData = Provider.of<UserRegistrationData>(context, listen: false); // listen:false car on met à jour
final screenSize = MediaQuery.of(context).size;
final cardWidth = screenSize.width * 0.6; // Largeur de la carte (60% de l'écran)
final double imageAspectRatio = 2.0; // Ratio corrigé (1024/512 = 2.0)
@ -186,7 +206,13 @@ Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lo
left: 40,
child: IconButton(
icon: Transform(alignment: Alignment.center, transform: Matrix4.rotationY(math.pi), child: Image.asset('assets/images/chevron_right.png', height: 40)),
onPressed: () => Navigator.pop(context),
onPressed: () {
if (context.canPop()) {
context.pop();
} else {
context.go('/parent-register-step3');
}
},
tooltip: 'Retour',
),
),
@ -197,14 +223,18 @@ Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lo
icon: Image.asset('assets/images/chevron_right.png', height: 40),
onPressed: _cguAccepted
? () {
_registrationData.updateMotivation(_motivationController.text);
_registrationData.acceptCGU();
Navigator.pushNamed(
context,
'/parent-register/step5',
arguments: _registrationData
registrationData.updateMotivation(_motivationController.text);
registrationData.acceptCGU(_cguAccepted);
registrationData.updateFinancialInfo(
bankDetails: BankDetails(
bankName: _bankNameController.text,
iban: _ibanController.text,
bic: _bicController.text,
),
attestationCafNumber: _attestationController.text,
consentQuotientFamilial: _consentQuotientFamilial,
);
context.go('/parent-register-step5');
}
: null,
tooltip: 'Suivant',

View File

@ -4,58 +4,36 @@ import '../../models/user_registration_data.dart'; // Utilisation du vrai modèl
import '../../widgets/image_button.dart'; // Import du ImageButton
import '../../models/card_assets.dart'; // Import des enums de cartes
import 'package:flutter/foundation.dart' show kIsWeb;
import '../../widgets/custom_decorated_text_field.dart'; // Import du CustomDecoratedTextField
import 'package:provider/provider.dart';
import 'package:go_router/go_router.dart';
// Nouvelle méthode helper pour afficher un champ de type "lecture seule" stylisé
Widget _buildDisplayFieldValue(BuildContext context, String label, String value, {bool multiLine = false, double fieldHeight = 50.0, double labelFontSize = 18.0}) {
const FontWeight labelFontWeight = FontWeight.w600;
// Ne pas afficher le label si labelFontSize est 0 ou si label est vide
bool showLabel = label.isNotEmpty && labelFontSize > 0;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (showLabel)
Text(label, style: GoogleFonts.merienda(fontSize: labelFontSize, fontWeight: labelFontWeight)),
if (showLabel)
const SizedBox(height: 4),
// Utiliser Expanded si multiLine et pas de hauteur fixe, sinon Container
multiLine && fieldHeight == null
? Expanded(
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 12.0),
Container(
width: double.infinity, // Prendra la largeur allouée par son parent (Expanded)
height: multiLine ? null : fieldHeight, // Hauteur flexible pour multiligne, sinon fixe
constraints: multiLine ? const BoxConstraints(minHeight: 50.0) : null, // Hauteur min pour multiligne
padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 12.0), // Ajuster au besoin
decoration: BoxDecoration(
image: const DecorationImage(
image: AssetImage('assets/images/input_field_bg.png'),
fit: BoxFit.fill,
),
),
child: SingleChildScrollView( // Pour le défilement si le texte dépasse
child: Text(
value.isNotEmpty ? value : '-',
style: GoogleFonts.merienda(fontSize: labelFontSize > 0 ? labelFontSize : 18.0), // Garder une taille de texte par défaut si label caché
maxLines: null, // Permettre un nombre illimité de lignes
),
),
),
)
: Container(
width: double.infinity,
height: multiLine ? null : fieldHeight,
constraints: multiLine ? BoxConstraints(minHeight: fieldHeight) : null,
padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 12.0),
decoration: BoxDecoration(
image: const DecorationImage(
image: AssetImage('assets/images/input_field_bg.png'),
image: AssetImage('assets/images/input_field_bg.png'), // Image de fond du champ
fit: BoxFit.fill,
),
// Si votre image input_field_bg.png a des coins arrondis intrinsèques, ce borderRadius n'est pas nécessaire
// ou doit correspondre. Sinon, pour une image rectangulaire, vous pouvez l'ajouter.
// borderRadius: BorderRadius.circular(12),
),
child: Text(
value.isNotEmpty ? value : '-',
style: GoogleFonts.merienda(fontSize: labelFontSize > 0 ? labelFontSize : 18.0),
maxLines: multiLine ? null : 1,
style: GoogleFonts.merienda(fontSize: labelFontSize),
maxLines: multiLine ? null : 1, // Permet plusieurs lignes si multiLine est true
overflow: multiLine ? TextOverflow.visible : TextOverflow.ellipsis,
),
),
@ -63,10 +41,116 @@ Widget _buildDisplayFieldValue(BuildContext context, String label, String value,
);
}
class ParentRegisterStep5Screen extends StatelessWidget {
final UserRegistrationData registrationData;
class ParentRegisterStep5Screen extends StatefulWidget {
const ParentRegisterStep5Screen({super.key});
const ParentRegisterStep5Screen({super.key, required this.registrationData});
@override
_ParentRegisterStep5ScreenState createState() => _ParentRegisterStep5ScreenState();
}
class _ParentRegisterStep5ScreenState extends State<ParentRegisterStep5Screen> {
@override
Widget build(BuildContext context) {
final registrationData = Provider.of<UserRegistrationData>(context);
final screenSize = MediaQuery.of(context).size;
final cardWidth = screenSize.width / 2.0; // Largeur de la carte (50% de l'écran)
final double imageAspectRatio = 2.0; // Ratio corrigé (1024/512 = 2.0)
final cardHeight = cardWidth / imageAspectRatio;
return Scaffold(
body: Stack(
children: [
Positioned.fill(
child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeatY),
),
Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 40.0), // Padding horizontal supprimé ici
child: Padding( // Ajout du Padding horizontal externe
padding: EdgeInsets.symmetric(horizontal: screenSize.width / 4.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text('Étape 5/5', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)),
const SizedBox(height: 20),
Text('Récapitulatif de votre demande', style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87), textAlign: TextAlign.center),
const SizedBox(height: 30),
_buildParent1Card(context, registrationData.parent1),
const SizedBox(height: 20),
if (registrationData.parent2 != null) ...[
_buildParent2Card(context, registrationData.parent2!),
const SizedBox(height: 20),
],
..._buildChildrenCards(context, registrationData.children),
_buildMotivationCard(context, registrationData.motivationText),
const SizedBox(height: 40),
ImageButton(
bg: 'assets/images/btn_green.png',
text: 'Soumettre ma demande',
textColor: const Color(0xFF2D6A4F),
width: 350,
height: 50,
fontSize: 18,
onPressed: () {
print("Données finales: ${registrationData.parent1.firstName}, Enfant(s): ${registrationData.children.length}");
_showConfirmationModal(context);
},
),
],
),
),
),
),
Positioned(
top: screenSize.height / 2 - 20,
left: 40,
child: IconButton(
icon: Transform.flip(flipX: true, child: Image.asset('assets/images/chevron_right.png', height: 40)),
onPressed: () {
if (context.canPop()) {
context.pop();
} else {
context.go('/parent-register-step4');
}
},
tooltip: 'Retour',
),
),
],
),
);
}
void _showConfirmationModal(BuildContext context) {
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (BuildContext dialogContext) {
return AlertDialog(
title: Text(
'Demande enregistrée',
style: GoogleFonts.merienda(fontWeight: FontWeight.bold),
),
content: Text(
'Votre dossier a bien été pris en compte. Un gestionnaire le validera bientôt.',
style: GoogleFonts.merienda(fontSize: 14),
),
actions: <Widget>[
TextButton(
child: Text('OK', style: GoogleFonts.merienda(fontWeight: FontWeight.bold)),
onPressed: () {
Navigator.of(dialogContext).pop(); // Ferme la modale
// Utiliser go_router pour la navigation
context.go('/login');
},
),
],
);
},
);
}
// Méthode pour construire la carte Parent 1
Widget _buildParent1Card(BuildContext context, ParentData data) {
@ -98,7 +182,7 @@ class ParentRegisterStep5Screen extends StatelessWidget {
backgroundImagePath: CardColorHorizontal.peach.path,
title: 'Parent Principal',
content: details,
onEdit: () => Navigator.of(context).pushNamed('/parent-register/step1', arguments: registrationData),
onEdit: () => context.go('/parent-register-step1'),
);
}
@ -131,7 +215,7 @@ class ParentRegisterStep5Screen extends StatelessWidget {
backgroundImagePath: CardColorHorizontal.blue.path,
title: 'Deuxième Parent',
content: details,
onEdit: () => Navigator.of(context).pushNamed('/parent-register/step2', arguments: registrationData),
onEdit: () => context.go('/parent-register-step2'),
);
}
@ -176,10 +260,7 @@ class ParentRegisterStep5Screen extends StatelessWidget {
IconButton(
icon: const Icon(Icons.edit, color: Colors.black54, size: 28),
onPressed: () {
Navigator.of(context).pushNamed(
'/parent-register/step3',
arguments: registrationData,
);
context.go('/parent-register-step3', extra: {'childIndex': index});
},
tooltip: 'Modifier',
),
@ -264,23 +345,17 @@ class ParentRegisterStep5Screen extends StatelessWidget {
// Méthode pour construire la carte Motivation
Widget _buildMotivationCard(BuildContext context, String motivation) {
List<Widget> details = [
Text(motivation.isNotEmpty ? motivation : 'Aucune motivation renseignée.',
style: GoogleFonts.merienda(fontSize: 18),
maxLines: 4,
overflow: TextOverflow.ellipsis)
];
return _SummaryCard(
backgroundImagePath: CardColorHorizontal.green.path,
backgroundImagePath: CardColorHorizontal.pink.path,
title: 'Votre Motivation',
content: [
Expanded(
child: CustomDecoratedTextField(
controller: TextEditingController(text: motivation),
hintText: 'Aucune motivation renseignée.',
fieldHeight: 200,
maxLines: 10,
expandDynamically: true,
readOnly: true,
fontSize: 18.0,
),
),
],
onEdit: () => Navigator.of(context).pushNamed('/parent-register/step4', arguments: registrationData),
content: details,
onEdit: () => context.go('/parent-register-step4'),
);
}
@ -306,102 +381,6 @@ class ParentRegisterStep5Screen extends StatelessWidget {
),
);
}
@override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
final cardWidth = screenSize.width / 2.0; // Largeur de la carte (50% de l'écran)
final double imageAspectRatio = 2.0; // Ratio corrigé (1024/512 = 2.0)
final cardHeight = cardWidth / imageAspectRatio;
return Scaffold(
body: Stack(
children: [
Positioned.fill(
child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeatY),
),
Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 40.0), // Padding horizontal supprimé ici
child: Padding( // Ajout du Padding horizontal externe
padding: EdgeInsets.symmetric(horizontal: screenSize.width / 4.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text('Étape 5/5', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)),
const SizedBox(height: 20),
Text('Récapitulatif de votre demande', style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87), textAlign: TextAlign.center),
const SizedBox(height: 30),
_buildParent1Card(context, registrationData.parent1),
const SizedBox(height: 20),
if (registrationData.parent2 != null) ...[
_buildParent2Card(context, registrationData.parent2!),
const SizedBox(height: 20),
],
..._buildChildrenCards(context, registrationData.children),
_buildMotivationCard(context, registrationData.motivationText),
const SizedBox(height: 40),
ImageButton(
bg: 'assets/images/btn_green.png',
text: 'Soumettre ma demande',
textColor: const Color(0xFF2D6A4F),
width: 350,
height: 50,
fontSize: 18,
onPressed: () {
print("Données finales: ${registrationData.parent1.firstName}, Enfant(s): ${registrationData.children.length}");
_showConfirmationModal(context);
},
),
],
),
),
),
),
Positioned(
top: screenSize.height / 2 - 20,
left: 40,
child: IconButton(
icon: Transform.flip(flipX: true, child: Image.asset('assets/images/chevron_right.png', height: 40)),
onPressed: () => Navigator.pop(context), // Retour à l'étape 4
tooltip: 'Retour',
),
),
],
),
);
}
void _showConfirmationModal(BuildContext context) {
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (BuildContext dialogContext) {
return AlertDialog(
title: Text(
'Demande enregistrée',
style: GoogleFonts.merienda(fontWeight: FontWeight.bold),
),
content: Text(
'Votre dossier a bien été pris en compte. Un gestionnaire le validera bientôt.',
style: GoogleFonts.merienda(fontSize: 14),
),
actions: <Widget>[
TextButton(
child: Text('OK', style: GoogleFonts.merienda(fontWeight: FontWeight.bold)),
onPressed: () {
Navigator.of(dialogContext).pop(); // Ferme la modale
// TODO: Naviguer vers l'écran de connexion ou tableau de bord
Navigator.of(context).pushNamedAndRemoveUntil('/login', (Route<dynamic> route) => false);
},
),
],
);
},
);
}
}
// Widget générique _SummaryCard (ajusté)
@ -422,7 +401,7 @@ class _SummaryCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: 2.0,
aspectRatio: 2.0, // Le ratio largeur/hauteur de nos images de fond
child: Container(
padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 25.0),
decoration: BoxDecoration(
@ -432,32 +411,33 @@ class _SummaryCard extends StatelessWidget {
),
borderRadius: BorderRadius.circular(15),
),
child: Column(
children: [
Row(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, // Pour que la colonne prenne la hauteur du contenu
children: [
Align( // Centrer le titre
alignment: Alignment.center,
child: Text(
title,
style: GoogleFonts.merienda(fontSize: 28, fontWeight: FontWeight.w600),
textAlign: TextAlign.center,
style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87), // Police légèrement augmentée
),
),
const SizedBox(height: 12), // Espacement ajusté après le titre
...content,
],
),
),
IconButton(
icon: const Icon(Icons.edit, color: Colors.black54, size: 28),
icon: const Icon(Icons.edit, color: Colors.black54, size: 28), // Icône un peu plus grande
onPressed: onEdit,
tooltip: 'Modifier',
),
],
),
const SizedBox(height: 18),
Expanded(
child: Column(
children: content,
),
),
],
),
),
);
}

View File

@ -3,6 +3,7 @@ import 'package:google_fonts/google_fonts.dart';
import 'dart:math' as math; // Pour la rotation du chevron
import '../../widgets/hover_relief_widget.dart'; // Import du widget générique
import '../../models/card_assets.dart'; // Import des enums de cartes
import 'package:go_router/go_router.dart';
class RegisterChoiceScreen extends StatelessWidget {
const RegisterChoiceScreen({super.key});
@ -28,12 +29,14 @@ class RegisterChoiceScreen extends StatelessWidget {
top: 40,
left: 40,
child: IconButton(
icon: Transform(
alignment: Alignment.center,
transform: Matrix4.rotationY(math.pi),
child: Image.asset('assets/images/chevron_right.png', height: 40),
),
onPressed: () => Navigator.pop(context),
icon: Transform.flip(flipX: true, child: Image.asset('assets/images/chevron_right.png', height: 40)),
onPressed: () {
if (context.canPop()) {
context.pop();
} else {
context.go('/login');
}
},
tooltip: 'Retour',
),
),
@ -89,7 +92,7 @@ class RegisterChoiceScreen extends StatelessWidget {
iconPath: 'assets/images/icon_parents.png',
label: 'Parents',
onPressed: () {
Navigator.pushNamed(context, '/parent-register/step1');
context.go('/parent-register-step1');
},
),
// Bouton "Assistante Maternelle" avec HoverReliefWidget appliqué uniquement à l'image
@ -98,8 +101,7 @@ class RegisterChoiceScreen extends StatelessWidget {
iconPath: 'assets/images/icon_assmat.png',
label: 'Assistante Maternelle',
onPressed: () {
// TODO: Naviguer vers l'écran d'inscription assmat
print('Choix: Assistante Maternelle');
context.go('/nanny-register-step1');
},
),
],

View File

@ -0,0 +1,15 @@
import 'package:flutter/material.dart';
class UnknownScreen extends StatelessWidget {
const UnknownScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Page Introuvable')),
body: const Center(
child: Text('Désolé, cette page n\'existe pas.'),
),
);
}
}

View File

@ -3,6 +3,11 @@ import 'dart:math';
class DataGenerator {
static final Random _random = Random();
// Méthodes publiques pour la génération de nombres aléatoires
static int randomInt(int max) => _random.nextInt(max);
static int randomIntInRange(int min, int max) => min + _random.nextInt(max - min);
static bool randomBool() => _random.nextBool();
static final List<String> _firstNames = [
'Alice', 'Bob', 'Charlie', 'David', 'Eva', 'Félix', 'Gabrielle', 'Hugo', 'Inès', 'Jules',
'Léa', 'Manon', 'Nathan', 'Oscar', 'Pauline', 'Quentin', 'Raphaël', 'Sophie', 'Théo', 'Victoire'

View File

@ -1,17 +0,0 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
#include <file_selector_windows/file_selector_windows.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}

View File

@ -1,15 +0,0 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter/plugin_registry.h>
// Registers Flutter plugins.
void RegisterPlugins(flutter::PluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_

View File

@ -1,25 +0,0 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_windows
url_launcher_windows
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)