Compare commits

..

No commits in common. "cef197d1332f7c58e38f2f931c1839aeda00b7f3" and "c934466e47a8259ae0bec4e363e79bac04dd0954" have entirely different histories.

37 changed files with 449 additions and 4256 deletions

Binary file not shown.

View File

@ -1,108 +0,0 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// Modèle pour les parents
model Parent {
id String @id @default(uuid())
email String @unique
password String
firstName String
lastName String
phoneNumber String?
address String?
status AccountStatus @default(PENDING)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
children Child[]
contracts Contract[]
}
// Modèle pour les enfants
model Child {
id String @id @default(uuid())
firstName String
dateOfBirth DateTime
photoUrl String?
photoConsent Boolean @default(false)
isMultiple Boolean @default(false)
isUnborn Boolean @default(false)
parentId String
parent Parent @relation(fields: [parentId], references: [id])
contracts Contract[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// Modèle pour les contrats
model Contract {
id String @id @default(uuid())
parentId String
childId String
startDate DateTime
endDate DateTime?
status ContractStatus @default(ACTIVE)
parent Parent @relation(fields: [parentId], references: [id])
child Child @relation(fields: [childId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// Modèle pour les thèmes
model Theme {
id String @id @default(uuid())
name String @unique
primaryColor String
secondaryColor String
backgroundColor String
textColor String
isActive Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
appSettings AppSettings[]
}
// Modèle pour les paramètres de l'application
model AppSettings {
id String @id @default(uuid())
currentThemeId String
currentTheme Theme @relation(fields: [currentThemeId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([currentThemeId])
}
// Modèle pour les administrateurs
model Admin {
id String @id @default(uuid())
email String @unique
password String
firstName String
lastName String
passwordChanged Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// Enums
enum AccountStatus {
PENDING
VALIDATED
REJECTED
SUSPENDED
}
enum ContractStatus {
ACTIVE
ENDED
CANCELLED
}

View File

@ -1,18 +0,0 @@
import { Controller, Post, Body, UseGuards, Req } from '@nestjs/common';
import { AdminService } from './admin.service';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
@Controller('admin')
export class AdminController {
constructor(private readonly adminService: AdminService) {}
@Post('change-password')
@UseGuards(JwtAuthGuard)
async changePassword(
@Req() req,
@Body('oldPassword') oldPassword: string,
@Body('newPassword') newPassword: string,
) {
return this.adminService.changePassword(req.user.id, oldPassword, newPassword);
}
}

View File

@ -1,18 +0,0 @@
import { Module } from '@nestjs/common';
import { AdminController } from './admin.controller';
import { AdminService } from './admin.service';
import { PrismaModule } from '../prisma/prisma.module';
import { JwtModule } from '@nestjs/jwt';
@Module({
imports: [
PrismaModule,
JwtModule.register({
secret: process.env.JWT_SECRET,
signOptions: { expiresIn: '1d' },
}),
],
controllers: [AdminController],
providers: [AdminService],
})
export class AdminModule {}

View File

@ -1,40 +0,0 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { PrismaService } from '../prisma/prisma.service';
import * as bcrypt from 'bcrypt';
@Injectable()
export class AdminService {
constructor(
private prisma: PrismaService,
private jwtService: JwtService,
) {}
async changePassword(adminId: string, oldPassword: string, newPassword: string) {
// Récupérer l'administrateur
const admin = await this.prisma.admin.findUnique({
where: { id: adminId },
});
if (!admin) {
throw new UnauthorizedException('Administrateur non trouvé');
}
// Vérifier l'ancien mot de passe
const isPasswordValid = await bcrypt.compare(oldPassword, admin.password);
if (!isPasswordValid) {
throw new UnauthorizedException('Ancien mot de passe incorrect');
}
// Hasher le nouveau mot de passe
const hashedPassword = await bcrypt.hash(newPassword, 10);
// Mettre à jour le mot de passe
await this.prisma.admin.update({
where: { id: adminId },
data: { password: hashedPassword },
});
return { message: 'Mot de passe modifié avec succès' };
}
}

View File

@ -1,72 +0,0 @@
import { Request, Response } from 'express';
import { ThemeService, ThemeData } from '../services/theme.service';
export class ThemeController {
// Créer un nouveau thème
static async createTheme(req: Request, res: Response) {
try {
const themeData: ThemeData = req.body;
const theme = await ThemeService.createTheme(themeData);
res.status(201).json(theme);
} catch (error) {
res.status(500).json({ error: 'Erreur lors de la création du thème' });
}
}
// Récupérer tous les thèmes
static async getAllThemes(req: Request, res: Response) {
try {
const themes = await ThemeService.getAllThemes();
res.json(themes);
} catch (error) {
res.status(500).json({ error: 'Erreur lors de la récupération des thèmes' });
}
}
// Récupérer le thème actif
static async getActiveTheme(req: Request, res: Response) {
try {
const theme = await ThemeService.getActiveTheme();
if (!theme) {
return res.status(404).json({ error: 'Aucun thème actif trouvé' });
}
res.json(theme);
} catch (error) {
res.status(500).json({ error: 'Erreur lors de la récupération du thème actif' });
}
}
// Activer un thème
static async activateTheme(req: Request, res: Response) {
try {
const { themeId } = req.params;
const theme = await ThemeService.activateTheme(themeId);
res.json(theme);
} catch (error) {
res.status(500).json({ error: 'Erreur lors de l\'activation du thème' });
}
}
// Mettre à jour un thème
static async updateTheme(req: Request, res: Response) {
try {
const { themeId } = req.params;
const themeData: Partial<ThemeData> = req.body;
const theme = await ThemeService.updateTheme(themeId, themeData);
res.json(theme);
} catch (error) {
res.status(500).json({ error: 'Erreur lors de la mise à jour du thème' });
}
}
// Supprimer un thème
static async deleteTheme(req: Request, res: Response) {
try {
const { themeId } = req.params;
await ThemeService.deleteTheme(themeId);
res.status(204).send();
} catch (error) {
res.status(500).json({ error: 'Erreur lors de la suppression du thème' });
}
}
}

View File

@ -1,95 +0,0 @@
import { Router } from 'express';
import { PrismaClient } from '@prisma/client';
import * as bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
const router = Router();
const prisma = new PrismaClient();
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
// Route de connexion
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
// Vérifier les identifiants
const admin = await prisma.admin.findUnique({
where: { email }
});
if (!admin) {
return res.status(401).json({ error: 'Identifiants invalides' });
}
// Vérifier le mot de passe
const validPassword = await bcrypt.compare(password, admin.password);
if (!validPassword) {
return res.status(401).json({ error: 'Identifiants invalides' });
}
// Vérifier si le mot de passe doit être changé
if (!admin.passwordChanged) {
return res.status(403).json({
error: 'Changement de mot de passe requis',
requiresPasswordChange: true
});
}
// Générer le token JWT
const token = jwt.sign(
{
id: admin.id,
email: admin.email,
role: 'admin'
},
JWT_SECRET,
{ expiresIn: '24h' }
);
res.json({ token });
} catch (error) {
console.error('Erreur lors de la connexion:', error);
res.status(500).json({ error: 'Erreur serveur' });
}
});
// Route de changement de mot de passe
router.post('/change-password', async (req, res) => {
try {
const { email, currentPassword, newPassword } = req.body;
// Vérifier l'administrateur
const admin = await prisma.admin.findUnique({
where: { email }
});
if (!admin) {
return res.status(404).json({ error: 'Administrateur non trouvé' });
}
// Vérifier l'ancien mot de passe
const validPassword = await bcrypt.compare(currentPassword, admin.password);
if (!validPassword) {
return res.status(401).json({ error: 'Mot de passe actuel incorrect' });
}
// Hasher le nouveau mot de passe
const hashedPassword = await bcrypt.hash(newPassword, 10);
// Mettre à jour le mot de passe
await prisma.admin.update({
where: { id: admin.id },
data: {
password: hashedPassword,
passwordChanged: true
}
});
res.json({ message: 'Mot de passe changé avec succès' });
} catch (error) {
console.error('Erreur lors du changement de mot de passe:', error);
res.status(500).json({ error: 'Erreur serveur' });
}
});
export default router;

View File

@ -1,14 +0,0 @@
import { Router } from 'express';
import { ThemeController } from '../controllers/theme.controller';
const router = Router();
// Routes pour les thèmes
router.post('/', ThemeController.createTheme);
router.get('/', ThemeController.getAllThemes);
router.get('/active', ThemeController.getActiveTheme);
router.put('/:themeId/activate', ThemeController.activateTheme);
router.put('/:themeId', ThemeController.updateTheme);
router.delete('/:themeId', ThemeController.deleteTheme);
export default router;

View File

@ -1,39 +0,0 @@
import { PrismaClient } from '@prisma/client';
import * as bcrypt from 'bcrypt';
const prisma = new PrismaClient();
async function main() {
try {
// Vérifier si l'administrateur existe déjà
const existingAdmin = await prisma.admin.findUnique({
where: { email: 'administrateur@ptitspas.fr' }
});
if (!existingAdmin) {
// Hasher le mot de passe
const hashedPassword = await bcrypt.hash('password', 10);
// Créer l'administrateur
await prisma.admin.create({
data: {
email: 'administrateur@ptitspas.fr',
password: hashedPassword,
firstName: 'Administrateur',
lastName: 'P\'titsPas',
passwordChanged: false
}
});
console.log('✅ Administrateur créé avec succès');
} else {
console.log(' L\'administrateur existe déjà');
}
} catch (error) {
console.error('❌ Erreur lors de la création de l\'administrateur:', error);
} finally {
await prisma.$disconnect();
}
}
main();

View File

@ -1,77 +0,0 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export interface ThemeData {
name: string;
primaryColor: string;
secondaryColor: string;
backgroundColor: string;
textColor: string;
}
export class ThemeService {
// Créer un nouveau thème
static async createTheme(data: ThemeData) {
return prisma.theme.create({
data: {
...data,
isActive: false,
},
});
}
// Récupérer tous les thèmes
static async getAllThemes() {
return prisma.theme.findMany();
}
// Récupérer le thème actif
static async getActiveTheme() {
const settings = await prisma.appSettings.findFirst({
include: {
currentTheme: true,
},
});
return settings?.currentTheme;
}
// Activer un thème
static async activateTheme(themeId: string) {
// Désactiver tous les thèmes
await prisma.theme.updateMany({
where: { isActive: true },
data: { isActive: false },
});
// Activer le thème sélectionné
const updatedTheme = await prisma.theme.update({
where: { id: themeId },
data: { isActive: true },
});
// Mettre à jour les paramètres de l'application
await prisma.appSettings.upsert({
where: { id: '1' },
update: { currentThemeId: themeId },
create: { id: '1', currentThemeId: themeId },
});
return updatedTheme;
}
// Mettre à jour un thème
static async updateTheme(themeId: string, data: Partial<ThemeData>) {
return prisma.theme.update({
where: { id: themeId },
data,
});
}
// Supprimer un thème
static async deleteTheme(themeId: string) {
return prisma.theme.delete({
where: { id: themeId },
});
}
}

View File

@ -1,51 +0,0 @@
# Charte graphique · **P'titsPas**
*Version 1.0 avril 2025*
---
## 1. Essentiel de la marque
| Élément | Raison d'être |
|---------|---------------|
| **Nom** | *P'titsPas* · évoque le cheminement serein des 0-3 ans |
| **Signature** | « Grandir pas à pas, sereinement » |
| **Valeurs** | Bienveillance · Transparence · Simplicité · Modernité |
---
## 2. Logos officiels
| Variante | Aperçu | Usage |
|----------|--------|-------|
| **Principal** | ![logo principal](P'tisPas_logo_trans.png) | Headers, back-office, print A4+ |
| **Icône** | ![icône](P'titsPas_icone.png) | Favicon, PWA, app mobile |
| **Monochrome** | ![mono](mono_placeholder.svg) | Sérigraphie, tampon, textile sombre |
> **Zone de protection** : laisser au minimum l'équivalent d'une pierre pastel autour du logotype.
---
## 3. Palette de couleurs
| Nom | Hex | Rôle |
|-----|-----|------|
| Violet Pastel | `#c6a3d8` | Accent / information |
| Turquoise | `#8ad0c8` | Actions primaires |
| Jaune Doux | `#f2d269` | Avertissements légers |
| Corail | `#f4a28c` | États d'erreur ou badges « conflit » |
| Encre | `#2f2f2f` | Texte principal |
| Ivoire BG | `#fffef9` | Fond d'application & documents |
---
## 4. Typographies
| Contexte | Fonte | Chargement |
|----------|-------|------------|
| Titres & accroches | **Merienda 600** | Google Fonts |
| Texte courant | **Merriweather 300/400** | Google Fonts |
| UI compact | **Inter** (fallback système) | CDN |
```css
h1, h2 { font-family: "Merienda", cursive; }
body { font-family: "Merriweather", serif; }
```

View File

@ -1,257 +0,0 @@
# Évolutions du Cahier des Charges
Ce document liste les modifications à apporter au cahier des charges original pour le rendre conforme à l'application développée.
## 1. Gestion des Enfants
### Modifications à apporter dans la section "Création de compte parent"
#### Situation actuelle dans le CDC :
- Mentionne uniquement la collecte d'informations sur l'enfant
- Ne précise pas la possibilité d'ajouter plusieurs enfants
- Ne mentionne pas la gestion des naissances multiples
- Ne mentionne pas la gestion des enfants à naître
#### Modifications proposées :
Ajouter le paragraphe suivant après la description de la collecte d'informations sur l'enfant :
```
Les parents peuvent ajouter autant d'enfants que nécessaire. Pour chaque enfant, les informations suivantes sont collectées :
- Prénom
- Date de naissance (ou date prévue pour les enfants à naître)
- Photo (optionnelle)
- Consentement pour l'utilisation de la photo
- Indication si l'enfant fait partie d'une naissance multiple (jumeaux, triplés, etc.)
Les parents peuvent :
- Ajouter un nouvel enfant à tout moment
- Supprimer un enfant ajouté
- Modifier les informations d'un enfant existant
- Indiquer si l'enfant est à naître
- Indiquer si l'enfant fait partie d'une naissance multiple
- Donner ou retirer leur consentement pour l'utilisation de la photo de l'enfant
```
### Modifications à apporter dans la section "Workflow de création de compte"
#### Situation actuelle dans le CDC :
- Étape 3 : "Collecte des informations sur l'enfant"
#### Modifications proposées :
Remplacer l'étape 3 par :
```
3. Collecte des informations sur les enfants
- Ajout d'un premier enfant
- Possibilité d'ajouter d'autres enfants
- Pour chaque enfant :
* Saisie du prénom
* Saisie de la date de naissance (ou date prévue)
* Option d'ajout d'une photo
* Option de consentement photo
* Indication si naissance multiple
* Indication si enfant à naître
- Possibilité de modifier ou supprimer un enfant
```
## 2. Workflow de Création de Compte
### Modifications à apporter dans la section "Workflow de création de compte"
#### Situation actuelle dans le CDC :
- Ne précise pas l'ordre exact des étapes
- Ne mentionne pas le statut du compte après création
- Ne détaille pas le processus de validation
#### Modifications proposées :
Ajouter les précisions suivantes au workflow :
```
Le processus de création de compte suit l'ordre suivant :
1. Collecte des informations du premier parent
2. Option d'ajout d'un second parent
3. Collecte des informations sur les enfants
4. Description de la situation familiale
5. Acceptation des conditions générales
6. Résumé et validation finale
Après la validation :
- Le compte est créé avec le statut "en attente"
- Un gestionnaire doit valider le compte avant son activation
- Les parents reçoivent une notification de la création de leur compte
- Une notification est envoyée aux gestionnaires pour validation
```
## 3. Informations Supplémentaires
### Modifications à apporter dans la section "Création de compte parent"
#### Situation actuelle dans le CDC :
- Ne mentionne pas la possibilité de présentation personnelle
- Ne mentionne pas la gestion des photos
- Ne précise pas les statuts possibles du compte
#### Modifications proposées :
Ajouter les sections suivantes :
```
### Informations complémentaires
Le premier parent peut optionnellement ajouter une présentation personnelle pour décrire sa situation et ses attentes.
### Gestion des photos
Pour chaque enfant, les parents peuvent :
- Ajouter une photo
- Donner ou retirer leur consentement pour l'utilisation de la photo
- La photo est stockée de manière sécurisée
- Le consentement est enregistré avec date et heure
### Statut du compte
Les statuts possibles du compte sont :
- En attente : compte créé, en attente de validation
- Validé : compte activé par un gestionnaire
- Rejeté : compte refusé par un gestionnaire
- Suspendu : compte temporairement désactivé
```
## 4. Validation et Sécurité
### Modifications à apporter dans la section "Validation"
#### Situation actuelle dans le CDC :
- Mentionne la validation par un gestionnaire
- Ne précise pas le processus de validation
- Ne mentionne pas les notifications
#### Modifications proposées :
Ajouter la section suivante :
```
### Processus de validation
1. Création du compte avec statut "en attente"
2. Notification automatique aux gestionnaires
3. Revue des informations par un gestionnaire
4. Décision de validation ou rejet
5. Notification aux parents de la décision
6. Activation ou rejet du compte selon la décision
### Notifications
- Les parents reçoivent une notification à chaque changement de statut
- Les gestionnaires reçoivent une notification pour chaque nouveau compte
- Un historique des validations est conservé
```
## 5. Initialisation de l'Application
### Ajout de l'administrateur par défaut
#### Situation actuelle dans le CDC :
- Ne mentionne pas l'existence d'un administrateur par défaut
- Ne précise pas les identifiants de connexion par défaut
#### Modifications proposées :
Ajouter la section suivante :
```
### Administrateur par défaut
Lors du premier démarrage de l'application, un compte administrateur est automatiquement créé avec les identifiants suivants :
- Email : administrateur@ptitspas.fr
- Mot de passe : password
Ce compte permet d'accéder à toutes les fonctionnalités administratives de l'application.
Le changement de mot de passe est obligatoire lors de la première connexion.
L'application doit forcer ce changement avant d'autoriser l'accès aux fonctionnalités administratives.
```
## 6. Changement de Nom de l'Application
### Situation actuelle dans le CDC :
- L'application est nommée "SuperNounou" dans tout le document
- Les références à l'application utilisent ce nom
### Modifications proposées :
Ajouter la section suivante :
```
### Changement de nom
L'application est renommée "P'titsPas" dans toute la documentation et l'interface utilisateur.
Ce changement implique :
- Mise à jour de toutes les références à "SuperNounou" dans le CDC
- Mise à jour des mentions légales
- Mise à jour de la documentation technique
- Mise à jour des interfaces utilisateur
- Mise à jour des messages système et notifications
- Mise à jour des adresses email (ex: support@ptitspas.fr)
```
### Impact sur l'application :
- Mise à jour de tous les textes statiques dans le code
- Mise à jour des templates d'email
- Mise à jour des messages de notification
- Mise à jour de la documentation utilisateur
- Mise à jour des mentions légales et CGU
## Format de présentation
Pour chaque évolution identifiée, ce document suivra la structure suivante :
1. Section concernée dans le CDC
2. Situation actuelle
3. Modifications proposées
4. Impact sur l'application
## Prochaines évolutions à documenter
- [x] Ajouter d'autres évolutions identifiées
- [ ] Mettre à jour le CDC original
- [ ] Valider les modifications avec les parties prenantes
# Évolutions proposées au cahier des charges
## 1. Workflow de création de compte
### 1.1 Récupération de compte
#### 1.1.1 Fonctionnalités
- Ajout d'un lien "Mot de passe oublié" sur la page de connexion
- Processus de récupération en 3 étapes :
1. Saisie de l'adresse email
2. Envoi d'un lien unique de réinitialisation (valide 24h)
3. Création d'un nouveau mot de passe
#### 1.1.2 Sécurité
- Le lien de réinitialisation doit être unique et à usage unique
- Le lien expire après 24 heures
- Le nouveau mot de passe doit respecter les mêmes critères que lors de la création de compte
- Notification par email lors de la réinitialisation du mot de passe
#### 1.1.3 Interface
- Page dédiée pour la saisie de l'email
- Page de confirmation d'envoi du lien
- Formulaire de réinitialisation du mot de passe
- Messages d'erreur clairs en cas de :
- Email non trouvé
- Lien expiré
- Mot de passe non conforme
## X. Amélioration de la Gestion des Photos Utilisateurs (Proposition)
### X.1 Recadrage et Redimensionnement des Photos
#### X.1.1 Fonctionnalités
- **Contexte :** Lors du téléchargement de photos par les utilisateurs (photos de profil, photos d'enfants).
- **Besoin :** Permettre à l'utilisateur de recadrer l'image (notamment en format carré pour les avatars) et potentiellement de la faire pivoter ou de zoomer avant son enregistrement final.
- **Objectif :** Améliorer l'expérience utilisateur, assurer une meilleure qualité et cohérence visuelle des images stockées et affichées dans l'application.
#### X.1.2 Solution Technique Envisagée (pour discussion)
- L'intégration d'une librairie Flutter tierce dédiée au recadrage d'image (par exemple, `image_cropper` ou `crop_image`) sera nécessaire après la sélection initiale de l'image via `image_picker`.
- La tentative initiale avec `image_cropper` (version 5.0.1) a rencontré des difficultés techniques d'intégration (erreur "Too many positional arguments" persistante avec `AndroidUiSettings`) et a été mise en attente. Une investigation plus approfondie ou l'évaluation d'alternatives sera requise.
#### X.1.3 Impact sur l'application
- Modification du flux de sélection d'image dans les écrans concernés (ex: `parent_register_step3_screen.dart`).
- Ajout potentiel de nouvelles dépendances et configurations spécifiques aux plateformes.
- Mise à jour de la documentation utilisateur si cette fonctionnalité est implémentée.

File diff suppressed because it is too large Load Diff

View File

@ -1,108 +0,0 @@
# SuperNounou SSS-001
## Spécification technique & opérationnelle unifiée
_Version 0.2 24 avril 2025_
---
## 1. Objet
Centraliser tous les aspects **techniques et opérationnels** de la plateforme SuperNounou :
- Sauvegarde & Plan de Reprise dActivité (PRA)
- Spécifications des API & intégrations
- Directives de déploiement, dobservabilité, de CI/CD
## 2. Portée
Instances de production, pré-production et recette (Frontend, Backend, PostgreSQL, stockage objets), scripts dinstallation, pipelines CI/CD, journaux et métriques.
## 3. Références
- CDC SuperNounou V1.1
- ISO 27001 / ISO 22301 bonnes pratiques
- Politique sécurité DSI Enedis #SEC-POL-2024
- RGPD (2016/679)
---
# A Sauvegarde & Plan de Reprise dActivité
### A.1 Architecture de sauvegarde
Schéma bloc, chiffrement AES-256 (KMS), réplication hors-site « Object Storage B ».
### A.2 Stratégie de sauvegarde
| Type | Fréquence | Rétention | Support |
|-------------------|-------------|-----------|--------------------|
| Incrémentale | Quotidienne | 30 j | Object Storage A |
| Complète | Hebdomadaire| 6 mois | Object Storage B |
| Export logique DB | Mensuelle | 5 ans | Stockage Glacier |
### A.3 PRA
RPO 24 h / RTO 4 h scénarios : panne VM, corruption DB, sinistre DC procédure détaillée + escalade.
### A.4 Tests de restauration
Intégrale semestrielle, partielle trimestrielle rapport daudit et actions correctives.
### A.5 Monitoring & alertes
Endpoint Prometheus `/metrics`, tableau Grafana « Backup status », alerte > 26 h sans backup.
### A.6 Rôles
DevOps Lead (implémentation), DBA (tests restore), RSSI (audit).
---
# B API & Intégrations
### B.1 Conventions
OpenAPI 3 livré (`openapi.yaml`), version URL `/api/v1`, ISO 8601 dates.
### B.2 Sécurité API
JWT Bearer (ou OAuth 2), TLS 1.3, rate-limit 100 req/min/IP, signature HMAC pour webhooks.
### B.3 Exemples
Collection Postman, scripts cURL, guide « Appeler lAPI ».
### B.4 Intégrations futures
SSO LDAP/SAML, webhook `contract.validated`, export statistiques CSV.
---
# C Déploiement, CI/CD et Observabilité *(nouveau)*
### C.1 Déploiement communal (on-premise)
- **Objectif** : installation complète sur un serveur Linux ou VM en moins d1 h.
- **Livrable** : solution de packaging **au choix** (Docker Compose, image VM, paquet .deb/.rpm).
- **Script update / rollback** : `update.sh` ou équivalent (backup ➜ pull ➜ migrate ➜ vérif ; rollback ≤ 5 min).
- **Config** : fichier `.env.sample` décrivant toutes les variables.
### C.2 Environnements
- Developpement local (Docker Compose).
- Recette & Production (serveur communal).
- Les étudiants doivent décrire la procédure de bascule Recette → Prod.
### C.3 Pipeline CI/CD
- Pipeline automatisé (tests unitaires + build image + scan CVE) déclenché à chaque merge.
- Lécole fournit son propre dépôt Git/runner.
- Artifacts : images taguées, notes de version (`CHANGELOG.md`).
### C.4 Observabilité & logs
- Journaux applicatifs JSON (timestamp UTC, level, traceId).
- Rotation/retention : 7 jours sur disque, 30 jours sur archive compressée.
- Export métriques Prometheus (`/metrics`) : latence API, nombre de sessions, files dattente hors-ligne.
- Tableaux Grafana dexemple inclus (`grafana_dashboard.json`).
### C.5 SLA et performances (indicatifs)
- Disponibilité mensuelle cible : **≥ 98 %**.
- Temps de réponse P95 des opérations courantes : **< 500 ms**.
- Capacité test : **≈ 50 sessions simultanées** sans dégradation (> 1 s).
---
# D Glossaire
AES-256, JWT, KMS, OpenAPI, RPO, RTO, rate-limit, HMAC, Compose, CI/CD…
---
# E Historique des versions
| Version | Date | Auteur | Commentaire |
|---------|------------|------------------|---------------------------------|
| 0.1-draft | 2025-04-24 | Équipe projet | Création du SSS unifié |
| 0.2 | 2025-04-24 | ChatGPT & Julien | Ajout déploiement / CI/CD / logs |

View File

@ -1,2 +1,2 @@
flutter.sdk=/home/deploy/snap/flutter/common/flutter
flutter.sdk=C:\\Users\\myhan\\flutter
sdk.dir=C:\\Users\\myhan\\AppData\\Local\\Android\\Sdk

View File

@ -1,10 +1,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 'package:flutter_localizations/flutter_localizations.dart';
import 'navigation/app_router.dart';
// import 'theme/app_theme.dart'; // Supprimer AppTheme
// import 'theme/theme_provider.dart'; // Supprimer ThemeProvider
void main() {
runApp(const MyApp()); // Exécution simple

View File

@ -1,97 +0,0 @@
import 'dart:io'; // Pour File
import '../models/card_assets.dart'; // Import de l'enum CardColorVertical
class ParentData {
String firstName;
String lastName;
String address; // Rue et numéro
String postalCode; // Ajout
String city; // Ajout
String phone;
String email;
String password; // Peut-être pas nécessaire pour le récap, mais pour la création initiale si
File? profilePicture; // Chemin ou objet File
ParentData({
this.firstName = '',
this.lastName = '',
this.address = '', // Rue
this.postalCode = '', // Ajout
this.city = '', // Ajout
this.phone = '',
this.email = '',
this.password = '',
this.profilePicture,
});
}
class ChildData {
String firstName;
String lastName;
String dob; // Date de naissance ou prévisionnelle
bool photoConsent;
bool multipleBirth;
bool isUnbornChild;
File? imageFile;
CardColorVertical cardColor; // Nouveau champ pour la couleur de la carte
ChildData({
this.firstName = '',
this.lastName = '',
this.dob = '',
this.photoConsent = false,
this.multipleBirth = false,
this.isUnbornChild = false,
this.imageFile,
required this.cardColor, // Rendre requis dans le constructeur
});
}
class UserRegistrationData {
ParentData parent1;
ParentData? parent2; // Optionnel
List<ChildData> children;
String motivationText;
bool cguAccepted;
UserRegistrationData({
ParentData? parent1Data,
this.parent2,
List<ChildData>? childrenData,
this.motivationText = '',
this.cguAccepted = false,
}) : parent1 = parent1Data ?? ParentData(),
children = childrenData ?? [];
// Méthode pour ajouter/mettre à jour le parent 1
void updateParent1(ParentData data) {
parent1 = data;
}
// Méthode pour ajouter/mettre à jour le parent 2
void updateParent2(ParentData? data) {
parent2 = data;
}
// Méthode pour ajouter un enfant
void addChild(ChildData child) {
children.add(child);
}
// 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;
}
}
// Mettre à jour la motivation
void updateMotivation(String text) {
motivationText = text;
}
// Accepter les CGU
void acceptCGU() {
cguAccepted = true;
}
}

View File

@ -1,24 +1,41 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:p_tits_pas/models/am_user_registration_data.dart';
import 'package:p_tits_pas/screens/administrateurs/admin_dashboardScreen.dart';
import 'package:p_tits_pas/screens/auth/am/am_register_step1_sceen.dart';
import 'package:p_tits_pas/screens/auth/am/am_register_step2_sceen.dart';
import 'package:p_tits_pas/screens/auth/am/am_register_step3_sceen.dart';
import 'package:p_tits_pas/screens/auth/am/am_register_step4_sceen.dart';
import 'package:p_tits_pas/screens/home/parent_screen/ParentDashboardScreen.dart';
import 'package:p_tits_pas/screens/home/parent_screen/find_nanny.dart';
import 'package:p_tits_pas/screens/legal/legal_page.dart';
import 'package:p_tits_pas/screens/legal/privacy_page.dart';
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/home/home_screen.dart';
import '../models/user_registration_data.dart';
import '../screens/auth/parent/parent_register_step1_screen.dart';
import '../screens/auth/parent/parent_register_step2_screen.dart';
import '../screens/auth/parent/parent_register_step3_screen.dart';
import '../screens/auth/parent/parent_register_step4_screen.dart';
import '../screens/auth/parent/parent_register_step5_screen.dart';
import '../models/parent_user_registration_data.dart';
class AppRouter {
static const String login = '/login';
static const String registerChoice = '/register-choice';
static const String legal = '/legal';
static const String privacy = '/privacy';
static const String parentRegisterStep1 = '/parent-register/step1';
static const String parentRegisterStep2 = '/parent-register/step2';
static const String parentRegisterStep3 = '/parent-register/step3';
static const String parentRegisterStep4 = '/parent-register/step4';
static const String parentRegisterStep5 = '/parent-register/step5';
static const String home = '/home';
static const String amRegisterStep1 = '/am-register/step1';
static const String amRegisterStep2 = '/am-register/step2';
static const String amRegisterStep3 = '/am-register/step3';
static const String amRegisterStep4 = '/am-register/step4';
static const String parentDashboard = '/parent-dashboard';
static const String admin_dashboard = '/admin_dashboard';
static const String findNanny = '/find-nanny';
static Route<dynamic> generateRoute(RouteSettings settings) {
Widget screen;
@ -38,8 +55,16 @@ class AppRouter {
screen = const RegisterChoiceScreen();
slideTransition = true;
break;
case legal:
screen = const LegalPage();
slideTransition = true;
break;
case privacy:
screen = const PrivacyPage();
slideTransition = true;
break;
case parentRegisterStep1:
screen = const ParentRegisterStep1Screen();
screen = ParentRegisterStep1Screen();
slideTransition = true;
break;
case parentRegisterStep2:
@ -74,8 +99,42 @@ class AppRouter {
}
slideTransition = true;
break;
case home:
screen = const HomeScreen();
case amRegisterStep1:
screen = const AmRegisterStep1Screen();
slideTransition = true;
break;
case amRegisterStep2:
if (args is ChildminderRegistrationData) {
screen = AmRegisterStep2Screen(registrationData: args);
} else {
screen = AmRegisterStep2Screen(registrationData: ChildminderRegistrationData());
}
slideTransition = true;
break;
case amRegisterStep3:
if (args is ChildminderRegistrationData) {
screen = AmRegisterStep3Screen(registrationData: args);
} else {
screen = AmRegisterStep3Screen(registrationData: ChildminderRegistrationData());
}
slideTransition = true;
break;
case amRegisterStep4:
if (args is ChildminderRegistrationData) {
screen = AmRegisterStep4Screen(registrationData: args);
} else {
screen = AmRegisterStep4Screen(registrationData: ChildminderRegistrationData());
}
slideTransition = true;
break;
case parentDashboard:
screen = const ParentDashboardScreen();
break;
case admin_dashboard:
screen = const AdminDashboardScreen();
break;
case findNanny:
screen = const FindNannyScreen();
break;
default:
screen = Scaffold(

View File

@ -2,9 +2,10 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:p_tits_pas/services/api/tokenService.dart';
import 'package:p_tits_pas/services/auth_service.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:p_tits_pas/services/bug_report_service.dart';
import 'package:go_router/go_router.dart';
import '../../widgets/image_button.dart';
import '../../widgets/custom_app_text_field.dart';
@ -19,6 +20,8 @@ class _LoginPageState extends State<LoginPage> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final AuthService _authService = AuthService();
bool _isLoading = false;
@override
void dispose() {
@ -47,6 +50,89 @@ class _LoginPageState extends State<LoginPage> {
return null;
}
Future<void> _handleLogin() async {
if (_formKey.currentState?.validate() ?? false) {
setState(() {
_isLoading = true;
});
try {
final response = await _authService.login(
_emailController.text.trim(),
_passwordController.text,
);
print('Login response: ${response}');
if (!mounted) return;
// Navigation selon le rôle
final role = await TokenService.getRole();
print('User role: $role');
if (role != null) {
switch (role.toLowerCase()) {
case 'parent':
Navigator.pushReplacementNamed(context, '/parent-dashboard');
break;
case 'assistante_maternelle':
Navigator.pushReplacementNamed(
context, '/assistante_maternelle_dashboard');
break;
case 'super_admin' || 'administrateur':
Navigator.pushReplacementNamed(context, '/admin_dashboard');
break;
case 'gestionnaire':
Navigator.pushReplacementNamed(
context, '/gestionnaire_dashboard');
break;
default:
_showErrorSnackBar('Rôle utilisateur non reconnu: $role');
return;
}
} else {
_showErrorSnackBar('Rôle utilisateur non trouvé');
}
} catch (e) {
print('Login error: $e');
if (!mounted) return;
String errorMessage = e.toString();
String errorString = e.toString();
if (errorString.contains('Failed to login:')) {
// Extraire le message d'erreur réel
errorMessage =
errorString.replaceFirst('Exception: Failed to login: ', '');
}
_showErrorSnackBar(errorMessage);
} finally {
if (mounted) {
setState(() {
_isLoading = false; // AJOUT : Fin du chargement
});
}
}
}
}
void _showErrorSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red,
duration: const Duration(seconds: 4), // Plus long pour lire l'erreur
),
);
}
void _showSuccessSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.green,
duration: const Duration(seconds: 2),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
@ -54,7 +140,8 @@ class _LoginPageState extends State<LoginPage> {
body: LayoutBuilder(
builder: (context, constraints) {
// Version desktop (web)
if (kIsWeb) {
// if (kIsWeb) {
final w = constraints.maxWidth;
final h = constraints.maxHeight;
@ -67,7 +154,8 @@ class _LoginPageState extends State<LoginPage> {
final imageDimensions = snapshot.data!;
final imageHeight = h;
final imageWidth = imageHeight * (imageDimensions.width / imageDimensions.height);
final imageWidth = imageHeight *
(imageDimensions.width / imageDimensions.height);
final remainingWidth = w - imageWidth;
final leftMargin = remainingWidth / 4;
@ -96,10 +184,10 @@ class _LoginPageState extends State<LoginPage> {
Positioned(
right: 0,
bottom: 0,
width: w * 0.6, // 60% de la largeur de l'écran
height: h * 0.5, // 50% de la hauteur de l'écran
width: w * 0.6, // 60% de la largeur de l'écran
height: h * 0.5, // 50% de la hauteur de l'écran
child: Padding(
padding: EdgeInsets.all(w * 0.02), // 2% de padding
padding: EdgeInsets.all(w * 0.02), // 2% de padding
child: Form(
key: _formKey,
child: Column(
@ -118,6 +206,7 @@ class _LoginPageState extends State<LoginPage> {
style: CustomAppTextFieldStyle.lavande,
fieldHeight: 53,
fieldWidth: double.infinity,
enabled: !_isLoading,
),
),
const SizedBox(width: 20),
@ -131,6 +220,7 @@ class _LoginPageState extends State<LoginPage> {
style: CustomAppTextFieldStyle.jaune,
fieldHeight: 53,
fieldWidth: double.infinity,
enabled: !_isLoading,
),
),
],
@ -138,17 +228,21 @@ class _LoginPageState extends State<LoginPage> {
const SizedBox(height: 20),
// Bouton centré
Center(
child: ImageButton(
child: _isLoading
? const SizedBox(
width: 300,
height: 40,
child: Center(
child: CircularProgressIndicator(),
),
)
: ImageButton(
bg: 'assets/images/btn_green.png',
width: 300,
height: 40,
text: 'Se connecter',
textColor: const Color(0xFF2D6A4F),
onPressed: () {
if (_formKey.currentState?.validate() ?? false) {
// TODO: Implémenter la logique de connexion
}
},
onPressed: _handleLogin,
),
),
const SizedBox(height: 10),
@ -173,7 +267,8 @@ class _LoginPageState extends State<LoginPage> {
Center(
child: TextButton(
onPressed: () {
Navigator.pushNamed(context, '/register-choice');
Navigator.pushNamed(
context, '/register-choice');
},
child: Text(
'Créer un compte',
@ -185,7 +280,8 @@ class _LoginPageState extends State<LoginPage> {
),
),
),
const SizedBox(height: 20), // Réduit l'espacement en bas
const SizedBox(
height: 20), // Réduit l'espacement en bas
],
),
),
@ -251,12 +347,12 @@ class _LoginPageState extends State<LoginPage> {
);
},
);
}
// }
// Version mobile (à implémenter)
return const Center(
child: Text('Version mobile à implémenter'),
);
// return const Center(
// child: Text('Version mobile à implémenter'),
// );
},
),
);
@ -298,14 +394,7 @@ class _LoginPageState extends State<LoginPage> {
TextButton(
onPressed: () async {
if (controller.text.trim().isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Veuillez décrire le problème',
style: GoogleFonts.merienda(),
),
),
);
_showErrorSnackBar('Veuillez décrire le problème');
return;
}
@ -313,25 +402,11 @@ class _LoginPageState extends State<LoginPage> {
await BugReportService.sendReport(controller.text);
if (context.mounted) {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Rapport envoyé avec succès',
style: GoogleFonts.merienda(),
),
),
);
_showSuccessSnackBar('Rapport envoyé avec succès');
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Erreur lors de l\'envoi du rapport',
style: GoogleFonts.merienda(),
),
),
);
_showErrorSnackBar('Erreur lors de l\'envoi du rapport');
}
}
},

View File

@ -1,226 +0,0 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'dart:math' as math; // Pour la rotation du chevron
import '../../models/user_registration_data.dart'; // Import du modèle de données
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
class ParentRegisterStep1Screen extends StatefulWidget {
const ParentRegisterStep1Screen({super.key});
@override
State<ParentRegisterStep1Screen> createState() => _ParentRegisterStep1ScreenState();
}
class _ParentRegisterStep1ScreenState extends State<ParentRegisterStep1Screen> {
final _formKey = GlobalKey<FormState>();
late UserRegistrationData _registrationData;
// Contrôleurs pour les champs (restauration CP et Ville)
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é
@override
void initState() {
super.initState();
_registrationData = UserRegistrationData();
_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();
_firstNameController.text = genFirstName;
_lastNameController.text = genLastName;
_phoneController.text = DataGenerator.phone();
_emailController.text = DataGenerator.email(genFirstName, genLastName);
_passwordController.text = DataGenerator.password();
_confirmPasswordController.text = _passwordController.text;
}
@override
void dispose() {
_lastNameController.dispose();
_firstNameController.dispose();
_phoneController.dispose();
_emailController.dispose();
_passwordController.dispose();
_confirmPasswordController.dispose();
_addressController.dispose();
_postalCodeController.dispose();
_cityController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
return Scaffold(
body: Stack(
children: [
// Fond papier
Positioned.fill(
child: Image.asset(
'assets/images/paper2.png',
fit: BoxFit.cover,
repeat: ImageRepeat.repeat,
),
),
// Contenu centré
Center(
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Indicateur d'étape (à rendre dynamique)
Text(
'Étape 1/5',
style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54),
),
const SizedBox(height: 10),
// Texte d'instruction
Text(
'Informations du Parent Principal',
style: GoogleFonts.merienda(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
// Carte jaune contenant le formulaire
Container(
width: screenSize.width * 0.6,
padding: const EdgeInsets.symmetric(vertical: 50, horizontal: 50),
constraints: const BoxConstraints(minHeight: 570),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(CardColorHorizontal.peach.path),
fit: BoxFit.fill,
),
),
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Expanded(flex: 12, child: CustomAppTextField(controller: _lastNameController, labelText: 'Nom', hintText: 'Votre nom de famille', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
Expanded(flex: 1, child: const SizedBox()), // Espace de 4%
Expanded(flex: 12, child: CustomAppTextField(controller: _firstNameController, labelText: 'Prénom', hintText: 'Votre prénom', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
],
),
const SizedBox(height: 20),
Row(
children: [
Expanded(flex: 12, child: CustomAppTextField(controller: _phoneController, labelText: 'Téléphone', keyboardType: TextInputType.phone, hintText: 'Votre numéro de téléphone', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
Expanded(flex: 1, child: const SizedBox()), // Espace de 4%
Expanded(flex: 12, child: CustomAppTextField(controller: _emailController, labelText: 'Email', keyboardType: TextInputType.emailAddress, hintText: 'Votre adresse e-mail', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
],
),
const SizedBox(height: 20),
Row(
children: [
Expanded(flex: 12, child: CustomAppTextField(controller: _passwordController, labelText: 'Mot de passe', obscureText: true, hintText: 'Créez votre mot de passe', style: CustomAppTextFieldStyle.beige, 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()), // Espace de 4%
Expanded(flex: 12, child: CustomAppTextField(controller: _confirmPasswordController, labelText: 'Confirmation', obscureText: true, hintText: 'Confirmez le mot de passe', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, validator: (value) {
if (value == null || value.isEmpty) return 'Confirmation requise';
if (value != _passwordController.text) return 'Ne correspond pas';
return null;
})),
],
),
const SizedBox(height: 20),
CustomAppTextField(
controller: _addressController,
labelText: 'Adresse (N° et Rue)',
hintText: 'Numéro et nom de votre rue',
style: CustomAppTextFieldStyle.beige,
fieldWidth: double.infinity,
),
const SizedBox(height: 20),
Row(
children: [
Expanded(flex: 1, child: CustomAppTextField(controller: _postalCodeController, labelText: 'Code Postal', keyboardType: TextInputType.number, hintText: 'Code postal', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
const SizedBox(width: 20),
Expanded(flex: 4, child: CustomAppTextField(controller: _cityController, labelText: 'Ville', hintText: 'Votre ville', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
],
),
],
),
),
),
],
),
),
),
// Chevron de navigation gauche (Retour)
Positioned(
top: screenSize.height / 2 - 20, // Centré verticalement
left: 40,
child: IconButton(
icon: Transform(
alignment: Alignment.center,
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
tooltip: 'Retour',
),
),
// Chevron de navigation droit (Suivant)
Positioned(
top: screenSize.height / 2 - 20, // Centré verticalement
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);
}
},
tooltip: 'Suivant',
),
),
],
),
);
}
}

View File

@ -1,255 +0,0 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'dart:math' as math; // Pour la rotation du chevron
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
class ParentRegisterStep2Screen extends StatefulWidget {
final UserRegistrationData registrationData; // Accepte les données de l'étape 1
const ParentRegisterStep2Screen({super.key, required this.registrationData});
@override
State<ParentRegisterStep2Screen> createState() => _ParentRegisterStep2ScreenState();
}
class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
final _formKey = GlobalKey<FormState>();
late UserRegistrationData _registrationData; // Copie locale pour modification
bool _addParent2 = true; // Pour le test, on ajoute toujours le parent 2
bool _sameAddressAsParent1 = false; // Peut être généré aléatoirement aussi
// Contrôleurs pour les champs du parent 2 (restauration CP et Ville)
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é
@override
void initState() {
super.initState();
_registrationData = widget.registrationData; // Récupère les données de l'étape 1
if (_addParent2) {
_generateAndFillParent2Data();
}
}
void _generateAndFillParent2Data() {
final String genFirstName = DataGenerator.firstName();
final String genLastName = DataGenerator.lastName();
_firstNameController.text = genFirstName;
_lastNameController.text = genLastName;
_phoneController.text = DataGenerator.phone();
_emailController.text = DataGenerator.email(genFirstName, genLastName);
_passwordController.text = DataGenerator.password();
_confirmPasswordController.text = _passwordController.text;
_sameAddressAsParent1 = DataGenerator.boolean();
if (!_sameAddressAsParent1) {
// Générer adresse, CP, Ville séparément
_addressController.text = DataGenerator.address();
_postalCodeController.text = DataGenerator.postalCode();
_cityController.text = DataGenerator.city();
} else {
// Vider les champs si même adresse (seront désactivés)
_addressController.clear();
_postalCodeController.clear();
_cityController.clear();
}
}
@override
void dispose() {
_lastNameController.dispose();
_firstNameController.dispose();
_phoneController.dispose();
_emailController.dispose();
_passwordController.dispose();
_confirmPasswordController.dispose();
_addressController.dispose();
_postalCodeController.dispose();
_cityController.dispose();
super.dispose();
}
bool get _parent2FieldsEnabled => _addParent2;
bool get _addressFieldsEnabled => _addParent2 && !_sameAddressAsParent1;
@override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
return Scaffold(
body: Stack(
children: [
Positioned.fill(
child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeat),
),
Center(
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Étape 2/5', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)),
const SizedBox(height: 10),
Text(
'Informations du Deuxième Parent (Optionnel)',
style: GoogleFonts.merienda(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black87),
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
Container(
width: screenSize.width * 0.6,
padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 50),
decoration: BoxDecoration(
image: DecorationImage(image: AssetImage(CardColorHorizontal.blue.path), fit: BoxFit.fill),
),
child: Form(
key: _formKey,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
flex: 12,
child: Row(children: [
const Icon(Icons.person_add_alt_1, size: 20), const SizedBox(width: 8),
Flexible(child: Text('Ajouter Parent 2 ?', style: GoogleFonts.merienda(fontWeight: FontWeight.bold), overflow: TextOverflow.ellipsis)),
const Spacer(),
Switch(value: _addParent2, onChanged: (val) => setState(() {
_addParent2 = val ?? false;
if (_addParent2) _generateAndFillParent2Data(); else _clearParent2Fields();
}), activeColor: Theme.of(context).primaryColor),
]),
),
Expanded(flex: 1, child: const SizedBox()),
Expanded(
flex: 12,
child: Row(children: [
Icon(Icons.home_work_outlined, size: 20, color: _addParent2 ? null : Colors.grey),
const SizedBox(width: 8),
Flexible(child: Text('Même Adresse ?', style: GoogleFonts.merienda(color: _addParent2 ? null : Colors.grey), overflow: TextOverflow.ellipsis)),
const Spacer(),
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;
} else {
_addressController.text = DataGenerator.address();
_postalCodeController.text = DataGenerator.postalCode();
_cityController.text = DataGenerator.city();
}
}) : null, activeColor: Theme.of(context).primaryColor),
]),
),
]),
const SizedBox(height: 25),
Row(
children: [
Expanded(flex: 12, child: CustomAppTextField(controller: _lastNameController, labelText: 'Nom', hintText: 'Nom du parent 2', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
Expanded(flex: 1, child: const SizedBox()), // Espace de 4%
Expanded(flex: 12, child: CustomAppTextField(controller: _firstNameController, labelText: 'Prénom', hintText: 'Prénom du parent 2', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
],
),
const SizedBox(height: 20),
Row(
children: [
Expanded(flex: 12, child: CustomAppTextField(controller: _phoneController, labelText: 'Téléphone', keyboardType: TextInputType.phone, hintText: 'Son téléphone', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
Expanded(flex: 1, child: const SizedBox()), // Espace de 4%
Expanded(flex: 12, child: CustomAppTextField(controller: _emailController, labelText: 'Email', keyboardType: TextInputType.emailAddress, hintText: 'Son email', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
],
),
const SizedBox(height: 20),
Row(
children: [
Expanded(flex: 12, child: CustomAppTextField(controller: _passwordController, labelText: 'Mot de passe', obscureText: true, hintText: 'Son mot de passe', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, validator: _addParent2 ? (v) => (v == null || v.isEmpty ? 'Requis' : (v.length < 6 ? '6 car. min' : null)) : null)),
Expanded(flex: 1, child: const SizedBox()), // Espace de 4%
Expanded(flex: 12, child: CustomAppTextField(controller: _confirmPasswordController, labelText: 'Confirmation', obscureText: true, hintText: 'Confirmer mot de passe', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, validator: _addParent2 ? (v) => (v == null || v.isEmpty ? 'Requis' : (v != _passwordController.text ? 'Différent' : null)) : null)),
],
),
const SizedBox(height: 20),
CustomAppTextField(controller: _addressController, labelText: 'Adresse (N° et Rue)', hintText: 'Son numéro et nom de rue', enabled: _addressFieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity),
const SizedBox(height: 20),
Row(
children: [
Expanded(flex: 1, child: CustomAppTextField(controller: _postalCodeController, labelText: 'Code Postal', keyboardType: TextInputType.number, hintText: 'Son code postal', enabled: _addressFieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
const SizedBox(width: 20),
Expanded(flex: 4, child: CustomAppTextField(controller: _cityController, labelText: 'Ville', hintText: 'Sa ville', enabled: _addressFieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
],
),
],
),
),
),
),
],
),
),
),
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: () => Navigator.pop(context),
tooltip: 'Retour',
),
),
Positioned(
top: screenSize.height / 2 - 20,
right: 40,
child: IconButton(
icon: Image.asset('assets/images/chevron_right.png', height: 40),
onPressed: () {
if (!_addParent2 || (_formKey.currentState?.validate() ?? false)) {
if (_addParent2) {
_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,
phone: _phoneController.text,
email: _emailController.text,
password: _passwordController.text,
)
);
} else {
_registrationData.updateParent2(null);
}
Navigator.pushNamed(context, '/parent-register/step3', arguments: _registrationData);
}
},
tooltip: 'Suivant',
),
),
],
),
);
}
void _clearParent2Fields() {
_formKey.currentState?.reset();
_lastNameController.clear(); _firstNameController.clear(); _phoneController.clear();
_emailController.clear(); _passwordController.clear(); _confirmPasswordController.clear();
_addressController.clear();
_postalCodeController.clear();
_cityController.clear();
_sameAddressAsParent1 = false;
setState(() {});
}
}

View File

@ -1,487 +0,0 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'dart:math' as math; // Pour la rotation du chevron
import 'package:flutter/gestures.dart'; // Pour PointerDeviceKind
import '../../widgets/hover_relief_widget.dart'; // Import du nouveau widget
import 'package:image_picker/image_picker.dart';
// import 'package:image_cropper/image_cropper.dart'; // Supprimé
import 'dart:io' show File, Platform; // Ajout de Platform
import 'package:flutter/foundation.dart' show kIsWeb; // Import pour kIsWeb
import '../../widgets/custom_app_text_field.dart'; // Import du nouveau widget TextField
import '../../widgets/app_custom_checkbox.dart'; // Import du nouveau widget Checkbox
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
// La classe _ChildFormData est supprimée car on utilise ChildData du modèle
class ParentRegisterStep3Screen extends StatefulWidget {
final UserRegistrationData registrationData; // Accepte les données
const ParentRegisterStep3Screen({super.key, required this.registrationData});
@override
State<ParentRegisterStep3Screen> createState() => _ParentRegisterStep3ScreenState();
}
class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
late UserRegistrationData _registrationData; // Stocke l'état complet
final ScrollController _scrollController = ScrollController(); // Pour le défilement horizontal
bool _isScrollable = false;
bool _showLeftFade = false;
bool _showRightFade = false;
static const double _fadeExtent = 0.05; // Pourcentage de fondu
// Liste ordonnée des couleurs de cartes pour les enfants
static const List<CardColorVertical> _childCardColors = [
CardColorVertical.lavender, // Premier enfant toujours lavande
CardColorVertical.pink,
CardColorVertical.peach,
CardColorVertical.lime,
CardColorVertical.red,
CardColorVertical.green,
CardColorVertical.blue,
];
// Garder une trace des couleurs déjà utilisées
final Set<CardColorVertical> _usedColors = {};
// Utilisation de GlobalKey pour les cartes enfants si validation complexe future
// Map<int, GlobalKey<FormState>> _childFormKeys = {};
@override
void initState() {
super.initState();
_registrationData = widget.registrationData;
// Initialiser les couleurs utilisées avec les enfants existants
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();
}
_scrollController.addListener(_scrollListener);
WidgetsBinding.instance.addPostFrameCallback((_) => _scrollListener());
}
@override
void dispose() {
_scrollController.removeListener(_scrollListener);
_scrollController.dispose();
super.dispose();
}
void _scrollListener() {
if (!_scrollController.hasClients) return;
final position = _scrollController.position;
final newIsScrollable = position.maxScrollExtent > 0.0;
final newShowLeftFade = newIsScrollable && position.pixels > (position.viewportDimension * _fadeExtent / 2);
final newShowRightFade = newIsScrollable && position.pixels < (position.maxScrollExtent - (position.viewportDimension * _fadeExtent / 2));
if (newIsScrollable != _isScrollable || newShowLeftFade != _showLeftFade || newShowRightFade != _showRightFade) {
setState(() {
_isScrollable = newIsScrollable;
_showLeftFade = newShowLeftFade;
_showRightFade = newShowRightFade;
});
}
}
void _addChild() {
setState(() {
bool isUnborn = DataGenerator.boolean();
// Trouver la première couleur non utilisée
CardColorVertical cardColor = _childCardColors.firstWhere(
(color) => !_usedColors.contains(color),
orElse: () => _childCardColors[0], // Fallback sur la première couleur si toutes sont utilisées
);
final newChild = ChildData(
lastName: _registrationData.parent1.lastName,
firstName: DataGenerator.firstName(),
dob: DataGenerator.dob(isUnborn: isUnborn),
isUnbornChild: isUnborn,
photoConsent: DataGenerator.boolean(),
multipleBirth: DataGenerator.boolean(),
cardColor: cardColor,
);
_registrationData.addChild(newChild);
_usedColors.add(cardColor);
});
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollListener();
if (_scrollController.hasClients && _scrollController.position.maxScrollExtent > 0.0) {
_scrollController.animateTo(_scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: 300), curve: Curves.easeOut);
}
});
}
void _removeChild(int index) {
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);
});
WidgetsBinding.instance.addPostFrameCallback((_) => _scrollListener());
}
}
Future<void> _pickImage(int childIndex) 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);
}
});
}
} catch (e) { print("Erreur image: $e"); }
}
Future<void> _selectDate(BuildContext context, int childIndex) async {
final ChildData currentChild = _registrationData.children[childIndex];
final DateTime now = DateTime.now();
DateTime initialDatePickerDate = now;
DateTime firstDatePickerDate = DateTime(1980); DateTime lastDatePickerDate = now;
if (currentChild.isUnbornChild) {
firstDatePickerDate = now; lastDatePickerDate = now.add(const Duration(days: 300));
if (currentChild.dob.isNotEmpty) {
try {
List<String> parts = currentChild.dob.split('/');
DateTime? parsedDate = DateTime.tryParse("${parts[2]}-${parts[1].padLeft(2, '0')}-${parts[0].padLeft(2, '0')}");
if (parsedDate != null && !parsedDate.isBefore(firstDatePickerDate) && !parsedDate.isAfter(lastDatePickerDate)) {
initialDatePickerDate = parsedDate;
}
} catch (e) {}
}
} else {
if (currentChild.dob.isNotEmpty) {
try {
List<String> parts = currentChild.dob.split('/');
DateTime? parsedDate = DateTime.tryParse("${parts[2]}-${parts[1].padLeft(2, '0')}-${parts[0].padLeft(2, '0')}");
if (parsedDate != null && !parsedDate.isBefore(firstDatePickerDate) && !parsedDate.isAfter(lastDatePickerDate)) {
initialDatePickerDate = parsedDate;
}
} catch (e) {}
}
}
final DateTime? picked = await showDatePicker(
context: context, initialDate: initialDatePickerDate, firstDate: firstDatePickerDate,
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}";
});
}
}
@override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
return Scaffold(
body: Stack(
children: [
Positioned.fill(
child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeat),
),
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Étape 3/5', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)),
const SizedBox(height: 10),
Text(
'Informations Enfants',
style: GoogleFonts.merienda(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black87),
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 150.0),
child: SizedBox(
height: 684.0,
child: ShaderMask(
shaderCallback: (Rect bounds) {
final Color leftFade = (_isScrollable && _showLeftFade) ? Colors.transparent : Colors.black;
final Color rightFade = (_isScrollable && _showRightFade) ? Colors.transparent : Colors.black;
if (!_isScrollable) { return LinearGradient(colors: const <Color>[Colors.black, Colors.black, Colors.black, Colors.black], stops: const [0.0, _fadeExtent, 1.0 - _fadeExtent, 1.0],).createShader(bounds); }
return LinearGradient( begin: Alignment.centerLeft, end: Alignment.centerRight, colors: <Color>[ leftFade, Colors.black, Colors.black, rightFade ], stops: const [0.0, _fadeExtent, 1.0 - _fadeExtent, 1.0], ).createShader(bounds);
},
blendMode: BlendMode.dstIn,
child: Scrollbar(
controller: _scrollController,
thumbVisibility: true,
child: ListView.builder(
controller: _scrollController,
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 20.0),
itemCount: _registrationData.children.length + 1,
itemBuilder: (context, index) {
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],
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,
),
);
} else {
// Bouton Ajouter
return Center(
child: HoverReliefWidget(
onPressed: _addChild,
borderRadius: BorderRadius.circular(15),
child: Image.asset('assets/images/plus.png', height: 80, width: 80),
),
);
}
},
),
),
),
),
),
const SizedBox(height: 20),
],
),
),
// Chevrons de navigation
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: () => Navigator.pop(context),
tooltip: 'Retour',
),
),
Positioned(
top: screenSize.height / 2 - 20,
right: 40,
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);
},
tooltip: 'Suivant',
),
),
],
),
);
}
}
// Widget pour la carte enfant (adapté pour prendre ChildData et des callbacks)
class _ChildCardWidget extends StatefulWidget { // Transformé en StatefulWidget pour gérer les contrôleurs internes
final ChildData childData;
final int childIndex;
final VoidCallback onPickImage;
final VoidCallback onDateSelect;
final ValueChanged<String> onFirstNameChanged;
final ValueChanged<String> onLastNameChanged;
final ValueChanged<bool> onTogglePhotoConsent;
final ValueChanged<bool> onToggleMultipleBirth;
final ValueChanged<bool> onToggleIsUnborn;
final VoidCallback onRemove;
final bool canBeRemoved;
const _ChildCardWidget({
required Key key,
required this.childData,
required this.childIndex,
required this.onPickImage,
required this.onDateSelect,
required this.onFirstNameChanged,
required this.onLastNameChanged,
required this.onTogglePhotoConsent,
required this.onToggleMultipleBirth,
required this.onToggleIsUnborn,
required this.onRemove,
required this.canBeRemoved,
}) : super(key: key);
@override
State<_ChildCardWidget> createState() => _ChildCardWidgetState();
}
class _ChildCardWidgetState extends State<_ChildCardWidget> {
late TextEditingController _firstNameController;
late TextEditingController _lastNameController;
late TextEditingController _dobController;
@override
void initState() {
super.initState();
// Initialiser les contrôleurs avec les données du widget
_firstNameController = TextEditingController(text: widget.childData.firstName);
_lastNameController = TextEditingController(text: widget.childData.lastName);
_dobController = TextEditingController(text: widget.childData.dob);
// Ajouter des listeners pour mettre à jour les données sources via les callbacks
_firstNameController.addListener(() => widget.onFirstNameChanged(_firstNameController.text));
_lastNameController.addListener(() => widget.onLastNameChanged(_lastNameController.text));
// Pour dob, la mise à jour se fait via _selectDate, pas besoin de listener ici
}
@override
void didUpdateWidget(covariant _ChildCardWidget oldWidget) {
super.didUpdateWidget(oldWidget);
// Mettre à jour les contrôleurs si les données externes changent
// (peut arriver si on recharge l'état global)
if (widget.childData.firstName != _firstNameController.text) {
_firstNameController.text = widget.childData.firstName;
}
if (widget.childData.lastName != _lastNameController.text) {
_lastNameController.text = widget.childData.lastName;
}
if (widget.childData.dob != _dobController.text) {
_dobController.text = widget.childData.dob;
}
}
@override
void dispose() {
_firstNameController.dispose();
_lastNameController.dispose();
_dobController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final File? currentChildImage = widget.childData.imageFile;
// Utiliser la couleur de la carte de childData pour l'ombre si besoin, ou directement pour le fond
final Color baseCardColorForShadow = widget.childData.cardColor == CardColorVertical.lavender
? Colors.purple.shade200
: (widget.childData.cardColor == CardColorVertical.pink ? Colors.pink.shade200 : Colors.grey.shade200); // Placeholder pour autres couleurs
final Color initialPhotoShadow = baseCardColorForShadow.withAlpha(90);
final Color hoverPhotoShadow = baseCardColorForShadow.withAlpha(130);
return Container(
width: 345.0 * 1.1, // 379.5
height: 570.0 * 1.2, // 684.0
padding: const EdgeInsets.all(22.0 * 1.1), // 24.2
decoration: BoxDecoration(
image: DecorationImage(image: AssetImage(widget.childData.cardColor.path), fit: BoxFit.cover),
borderRadius: BorderRadius.circular(20 * 1.1), // 22
),
child: Stack(
children: [
Column(
mainAxisSize: MainAxisSize.min,
children: [
HoverReliefWidget(
onPressed: widget.onPickImage,
borderRadius: BorderRadius.circular(10),
initialShadowColor: initialPhotoShadow,
hoverShadowColor: hoverPhotoShadow,
child: SizedBox(
height: 200.0,
width: 200.0,
child: Center(
child: Padding(
padding: const EdgeInsets.all(5.0 * 1.1), // 5.5
child: currentChildImage != null
? ClipRRect(borderRadius: BorderRadius.circular(10 * 1.1), child: kIsWeb ? Image.network(currentChildImage.path, fit: BoxFit.cover) : Image.file(currentChildImage, fit: BoxFit.cover))
: Image.asset('assets/images/photo.png', fit: BoxFit.contain),
),
),
),
),
const SizedBox(height: 12.0 * 1.1), // Augmenté pour plus d'espace après la photo
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Enfant à naître ?', style: GoogleFonts.merienda(fontSize: 16 * 1.1, fontWeight: FontWeight.w600)),
Switch(value: widget.childData.isUnbornChild, onChanged: widget.onToggleIsUnborn, activeColor: Theme.of(context).primaryColor),
],
),
const SizedBox(height: 9.0 * 1.1), // 9.9
CustomAppTextField(
controller: _firstNameController,
labelText: 'Prénom',
hintText: 'Facultatif si à naître',
isRequired: !widget.childData.isUnbornChild,
fieldHeight: 55.0 * 1.1, // 60.5
),
const SizedBox(height: 6.0 * 1.1), // 6.6
CustomAppTextField(
controller: _lastNameController,
labelText: 'Nom',
hintText: 'Nom de l\'enfant',
enabled: true,
fieldHeight: 55.0 * 1.1, // 60.5
),
const SizedBox(height: 9.0 * 1.1), // 9.9
CustomAppTextField(
controller: _dobController,
labelText: widget.childData.isUnbornChild ? 'Date prévisionnelle de naissance' : 'Date de naissance',
hintText: 'JJ/MM/AAAA',
readOnly: true,
onTap: widget.onDateSelect,
suffixIcon: Icons.calendar_today,
fieldHeight: 55.0 * 1.1, // 60.5
),
const SizedBox(height: 11.0 * 1.1), // 12.1
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AppCustomCheckbox(
label: 'Consentement photo',
value: widget.childData.photoConsent,
onChanged: widget.onTogglePhotoConsent,
checkboxSize: 22.0 * 1.1, // 24.2
),
const SizedBox(height: 6.0 * 1.1), // 6.6
AppCustomCheckbox(
label: 'Naissance multiple',
value: widget.childData.multipleBirth,
onChanged: widget.onToggleMultipleBirth,
checkboxSize: 22.0 * 1.1, // 24.2
),
],
),
],
),
if (widget.canBeRemoved)
Positioned(
top: -5, right: -5,
child: InkWell(
onTap: widget.onRemove,
customBorder: const CircleBorder(),
child: Image.asset(
'images/red_cross2.png',
width: 36,
height: 36,
fit: BoxFit.contain,
),
),
),
],
),
);
}
}

View File

@ -1,217 +0,0 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:p_tits_pas/widgets/custom_decorated_text_field.dart'; // Import du nouveau widget
import 'dart:math' as math; // Pour la rotation du chevron
import 'package:p_tits_pas/widgets/app_custom_checkbox.dart'; // Import de la checkbox personnalisée
// import 'package:p_tits_pas/models/placeholder_registration_data.dart'; // Remplacé
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
class ParentRegisterStep4Screen extends StatefulWidget {
final UserRegistrationData registrationData; // Accepte les données
const ParentRegisterStep4Screen({super.key, required this.registrationData});
@override
State<ParentRegisterStep4Screen> createState() => _ParentRegisterStep4ScreenState();
}
class _ParentRegisterStep4ScreenState extends State<ParentRegisterStep4Screen> {
late UserRegistrationData _registrationData; // État local
final _motivationController = TextEditingController();
bool _cguAccepted = true; // Pour le test, CGU acceptées par défaut
@override
void initState() {
super.initState();
_registrationData = widget.registrationData;
_motivationController.text = DataGenerator.motivation(); // Générer la motivation
}
@override
void dispose() {
_motivationController.dispose();
super.dispose();
}
void _showCGUModal() {
// Un long texte Lorem Ipsum pour simuler les CGU
const String loremIpsumText = '''
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper congue, euismod non, mi. Proin porttitor, orci nec nonummy molestie, enim est eleifend mi, non fermentum diam nisl sit amet erat. Duis semper. Duis arcu massa, scelerisque vitae, consequat in, pretium a, enim. Pellentesque congue. Ut in risus volutpat libero pharetra tempor. Cras vestibulum bibendum augue. Praesent egestas leo in pede. Praesent blandit odio eu enim. Pellentesque sed dui ut augue blandit sodales. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Aliquam nibh. Mauris ac mauris sed pede pellentesque fermentum. Maecenas adipiscing ante non diam sodales hendrerit.
Ut velit mauris, egestas sed, gravida nec, ornare ut, mi. Aenean ut orci vel massa suscipit pulvinar. Nulla sollicitudin. Fusce varius, ligula non tempus aliquam, nunc turpis ullamcorper nibh, in tempus sapien eros vitae ligula. Pellentesque rhoncus nunc et augue. Integer id felis. Curabitur aliquet pellentesque diam. Integer quis metus vitae elit lobortis egestas. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Morbi vel erat non mauris convallis vehicula. Nulla et sapien. Integer tortor tellus, aliquam faucibus, convallis id, congue eu, quam. Mauris ullamcorper felis vitae erat. Proin feugiat, augue non elementum posuere, metus purus iaculis lectus, et tristique ligula justo vitae magna.
Aliquam convallis sollicitudin purus. Praesent aliquam, enim at fermentum mollis, ligula massa adipiscing nisl, ac euismod nibh nisl eu lectus. Fusce vulputate sem at sapien. Vivamus leo. Aliquam euismod libero eu enim. Nulla nec felis sed leo placerat imperdiet. Aenean suscipit nulla in justo. Suspendisse cursus rutrum augue. Nulla tincidunt tincidunt mi. Curabitur iaculis, lorem vel rhoncus faucibus, felis magna fermentum augue, et ultricies lacus lorem varius purus. Curabitur eu amet.
Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper congue, euismod non, mi. Proin porttitor, orci nec nonummy molestie, enim est eleifend mi, non fermentum diam nisl sit amet erat. Duis semper. Duis arcu massa, scelerisque vitae, consequat in, pretium a, enim. Pellentesque congue. Ut in risus volutpat libero pharetra tempor. Cras vestibulum bibendum augue. Praesent egestas leo in pede. Praesent blandit odio eu enim. Pellentesque sed dui ut augue blandit sodales. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Aliquam nibh. Mauris ac mauris sed pede pellentesque fermentum. Maecenas adipiscing ante non diam sodales hendrerit.
Ut velit mauris, egestas sed, gravida nec, ornare ut, mi. Aenean ut orci vel massa suscipit pulvinar. Nulla sollicitudin. Fusce varius, ligula non tempus aliquam, nunc turpis ullamcorper nibh, in tempus sapien eros vitae ligula. Pellentesque rhoncus nunc et augue. Integer id felis. Curabitur aliquet pellentesque diam. Integer quis metus vitae elit lobortis egestas. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Morbi vel erat non mauris convallis vehicula. Nulla et sapien. Integer tortor tellus, aliquam faucibus, convallis id, congue eu, quam. Mauris ullamcorper felis vitae erat. Proin feugiat, augue non elementum posuere, metus purus iaculis lectus, et tristique ligula justo vitae magna. Etiam et felis dolor.
Praesent aliquam, enim at fermentum mollis, ligula massa adipiscing nisl, ac euismod nibh nisl eu lectus. Fusce vulputate sem at sapien. Vivamus leo. Aliquam euismod libero eu enim. Nulla nec felis sed leo placerat imperdiet. Aenean suscipit nulla in justo. Suspendisse cursus rutrum augue. Nulla tincidunt tincidunt mi. Curabitur iaculis, lorem vel rhoncus faucibus, felis magna fermentum augue, et ultricies lacus lorem varius purus. Curabitur eu amet. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.
Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.
''';
showDialog<void>(
context: context,
barrierDismissible: false, // L'utilisateur doit utiliser le bouton
builder: (BuildContext dialogContext) {
return AlertDialog(
title: Text(
'Conditions Générales d\'Utilisation',
style: GoogleFonts.merienda(fontWeight: FontWeight.bold),
),
content: SizedBox(
width: MediaQuery.of(dialogContext).size.width * 0.7, // 70% de la largeur de l'écran
height: MediaQuery.of(dialogContext).size.height * 0.6, // 60% de la hauteur de l'écran
child: SingleChildScrollView(
child: Text(
loremIpsumText,
style: GoogleFonts.merienda(fontSize: 13),
textAlign: TextAlign.justify,
),
),
),
actionsPadding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 10.0),
actionsAlignment: MainAxisAlignment.center,
actions: <Widget>[
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(dialogContext).primaryColor,
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15),
),
child: Text(
'Valider et Accepter',
style: GoogleFonts.merienda(fontSize: 15, color: Colors.white, fontWeight: FontWeight.bold),
),
onPressed: () {
Navigator.of(dialogContext).pop(); // Ferme la modale
setState(() {
_cguAccepted = true; // Met à jour l'état
});
},
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
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)
final cardHeight = cardWidth / imageAspectRatio;
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, horizontal: 50.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'Étape 4/5',
style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54),
),
const SizedBox(height: 20),
Text(
'Motivation de votre demande',
style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87),
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
Container(
width: cardWidth,
height: cardHeight,
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(CardColorHorizontal.green.path),
fit: BoxFit.fill,
),
),
child: Padding(
padding: const EdgeInsets.all(40.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: CustomDecoratedTextField(
controller: _motivationController,
hintText: 'Écrivez ici pour motiver votre demande...',
fieldHeight: cardHeight * 0.6,
maxLines: 10,
expandDynamically: true,
fontSize: 18.0,
),
),
const SizedBox(height: 20),
GestureDetector(
onTap: () {
if (!_cguAccepted) {
_showCGUModal();
}
},
child: AppCustomCheckbox(
label: 'J\'accepte les conditions générales d\'utilisation',
value: _cguAccepted,
onChanged: (newValue) {
if (!_cguAccepted) {
_showCGUModal();
} else {
setState(() => _cguAccepted = false);
}
},
),
),
],
),
),
),
],
),
),
),
// Chevrons de navigation
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: () => Navigator.pop(context),
tooltip: 'Retour',
),
),
Positioned(
top: screenSize.height / 2 - 20,
right: 40,
child: IconButton(
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
);
}
: null,
tooltip: 'Suivant',
),
),
],
),
);
}
}

View File

@ -1,464 +0,0 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../models/user_registration_data.dart'; // Utilisation du vrai modèle
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
// 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),
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'),
fit: BoxFit.fill,
),
),
child: Text(
value.isNotEmpty ? value : '-',
style: GoogleFonts.merienda(fontSize: labelFontSize > 0 ? labelFontSize : 18.0),
maxLines: multiLine ? null : 1,
overflow: multiLine ? TextOverflow.visible : TextOverflow.ellipsis,
),
),
],
);
}
class ParentRegisterStep5Screen extends StatelessWidget {
final UserRegistrationData registrationData;
const ParentRegisterStep5Screen({super.key, required this.registrationData});
// Méthode pour construire la carte Parent 1
Widget _buildParent1Card(BuildContext context, ParentData data) {
const double verticalSpacing = 28.0; // Espacement vertical augmenté
const double labelFontSize = 22.0; // Taille de label augmentée
List<Widget> details = [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: _buildDisplayFieldValue(context, "Nom:", data.lastName, labelFontSize: labelFontSize)),
const SizedBox(width: 20),
Expanded(child: _buildDisplayFieldValue(context, "Prénom:", data.firstName, labelFontSize: labelFontSize)),
],
),
const SizedBox(height: verticalSpacing),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: _buildDisplayFieldValue(context, "Téléphone:", data.phone, labelFontSize: labelFontSize)),
const SizedBox(width: 20),
Expanded(child: _buildDisplayFieldValue(context, "Email:", data.email, multiLine: true, labelFontSize: labelFontSize)),
],
),
const SizedBox(height: verticalSpacing),
_buildDisplayFieldValue(context, "Adresse:", "${data.address}\n${data.postalCode} ${data.city}".trim(), multiLine: true, fieldHeight: 80, labelFontSize: labelFontSize),
];
return _SummaryCard(
backgroundImagePath: CardColorHorizontal.peach.path,
title: 'Parent Principal',
content: details,
onEdit: () => Navigator.of(context).pushNamed('/parent-register/step1', arguments: registrationData),
);
}
// Méthode pour construire la carte Parent 2
Widget _buildParent2Card(BuildContext context, ParentData data) {
const double verticalSpacing = 28.0;
const double labelFontSize = 22.0;
List<Widget> details = [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: _buildDisplayFieldValue(context, "Nom:", data.lastName, labelFontSize: labelFontSize)),
const SizedBox(width: 20),
Expanded(child: _buildDisplayFieldValue(context, "Prénom:", data.firstName, labelFontSize: labelFontSize)),
],
),
const SizedBox(height: verticalSpacing),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: _buildDisplayFieldValue(context, "Téléphone:", data.phone, labelFontSize: labelFontSize)),
const SizedBox(width: 20),
Expanded(child: _buildDisplayFieldValue(context, "Email:", data.email, multiLine: true, labelFontSize: labelFontSize)),
],
),
const SizedBox(height: verticalSpacing),
_buildDisplayFieldValue(context, "Adresse:", "${data.address}\n${data.postalCode} ${data.city}".trim(), multiLine: true, fieldHeight: 80, labelFontSize: labelFontSize),
];
return _SummaryCard(
backgroundImagePath: CardColorHorizontal.blue.path,
title: 'Deuxième Parent',
content: details,
onEdit: () => Navigator.of(context).pushNamed('/parent-register/step2', arguments: registrationData),
);
}
// Méthode pour construire les cartes Enfants
List<Widget> _buildChildrenCards(BuildContext context, List<ChildData> children) {
return children.asMap().entries.map((entry) {
int index = entry.key;
ChildData child = entry.value;
CardColorHorizontal cardColorHorizontal = CardColorHorizontal.values.firstWhere(
(e) => e.name == child.cardColor.name,
orElse: () => CardColorHorizontal.lavender,
);
return Padding(
padding: const EdgeInsets.only(bottom: 20.0),
child: Stack(
children: [
AspectRatio(
aspectRatio: 2.0,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 25.0),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(cardColorHorizontal.path),
fit: BoxFit.cover,
),
borderRadius: BorderRadius.circular(15),
),
child: Column(
children: [
// Titre centré dans la carte
Row(
children: [
Expanded(
child: Text(
'Enfant ${index + 1}' + (child.isUnbornChild ? ' (à naître)' : ''),
style: GoogleFonts.merienda(fontSize: 28, fontWeight: FontWeight.w600),
textAlign: TextAlign.center,
),
),
IconButton(
icon: const Icon(Icons.edit, color: Colors.black54, size: 28),
onPressed: () {
Navigator.of(context).pushNamed(
'/parent-register/step3',
arguments: registrationData,
);
},
tooltip: 'Modifier',
),
],
),
const SizedBox(height: 18),
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// IMAGE SANS CADRE BLANC, PREND LA HAUTEUR
Expanded(
flex: 1,
child: Center(
child: ClipRRect(
borderRadius: BorderRadius.circular(18),
child: AspectRatio(
aspectRatio: 1,
child: (child.imageFile != null)
? (kIsWeb
? Image.network(child.imageFile!.path, fit: BoxFit.cover)
: Image.file(child.imageFile!, fit: BoxFit.cover))
: Image.asset('assets/images/photo.png', fit: BoxFit.contain),
),
),
),
),
const SizedBox(width: 32),
// INFOS À DROITE (2/3)
Expanded(
flex: 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildDisplayFieldValue(context, 'Prénom :', child.firstName, labelFontSize: 22.0),
const SizedBox(height: 12),
_buildDisplayFieldValue(context, 'Nom :', child.lastName, labelFontSize: 22.0),
const SizedBox(height: 12),
_buildDisplayFieldValue(context, child.isUnbornChild ? 'Date de naissance :' : 'Date de naissance :', child.dob, labelFontSize: 22.0),
],
),
),
],
),
),
const SizedBox(height: 18),
// Ligne des consentements
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
children: [
Checkbox(
value: child.photoConsent,
onChanged: null,
),
Text('Consentement photo', style: GoogleFonts.merienda(fontSize: 16)),
],
),
const SizedBox(width: 32),
Row(
children: [
Checkbox(
value: child.multipleBirth,
onChanged: null,
),
Text('Naissance multiple', style: GoogleFonts.merienda(fontSize: 16)),
],
),
],
),
],
),
),
),
],
),
);
}).toList();
}
// Méthode pour construire la carte Motivation
Widget _buildMotivationCard(BuildContext context, String motivation) {
return _SummaryCard(
backgroundImagePath: CardColorHorizontal.green.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),
);
}
// Helper pour afficher une ligne de détail (police et agencement amélioré)
Widget _buildDetailRow(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"$label: ",
style: GoogleFonts.merienda(fontSize: 18, fontWeight: FontWeight.w600),
),
Expanded(
child: Text(
value.isNotEmpty ? value : '-',
style: GoogleFonts.merienda(fontSize: 18),
softWrap: true,
),
),
],
),
);
}
@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é)
class _SummaryCard extends StatelessWidget {
final String backgroundImagePath;
final String title;
final List<Widget> content;
final VoidCallback onEdit;
const _SummaryCard({
super.key,
required this.backgroundImagePath,
required this.title,
required this.content,
required this.onEdit,
});
@override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: 2.0,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 25.0),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(backgroundImagePath),
fit: BoxFit.cover,
),
borderRadius: BorderRadius.circular(15),
),
child: Column(
children: [
Row(
children: [
Expanded(
child: Text(
title,
style: GoogleFonts.merienda(fontSize: 28, fontWeight: FontWeight.w600),
textAlign: TextAlign.center,
),
),
IconButton(
icon: const Icon(Icons.edit, color: Colors.black54, size: 28),
onPressed: onEdit,
tooltip: 'Modifier',
),
],
),
const SizedBox(height: 18),
Expanded(
child: Column(
children: content,
),
),
],
),
),
);
}
}

View File

@ -100,6 +100,8 @@ class RegisterChoiceScreen extends StatelessWidget {
onPressed: () {
// TODO: Naviguer vers l'écran d'inscription assmat
print('Choix: Assistante Maternelle');
Navigator.pushNamed(context, '/am-register/step1');
},
),
],

View File

@ -1,9 +1,118 @@
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:p_tits_pas/services/api/api_config.dart';
import 'package:p_tits_pas/services/api/tokenService.dart';
import '../models/user.dart';
import 'package:http/http.dart' as http;
class AuthService {
static const String _usersKey = 'users';
final String baseUrl = ApiConfig.baseUrl;
//login
Future<Map<String, dynamic>> login(String email, String password) async {
try {
final response = await http.post(
Uri.parse('$baseUrl${ApiConfig.login}'),
headers: ApiConfig.headers,
body: jsonEncode({
'email': email,
'password': password
}),
);
if (response.statusCode == 201) {
final data = jsonDecode(response.body);
await TokenService.saveToken(data['access_token']);
await TokenService.saveRefreshToken(data['refresh_token']);
final role = _extractRoleFromToken(data['access_token']);
await TokenService.saveRole(role);
return data;
} else {
throw Exception('Failed to login: ${response.body}');
}
} catch (e) {
throw Exception('Failed to login: $e');
}
}
String _extractRoleFromToken(String token) {
try {
final parts = token.split('.');
if (parts.length != 3) return '';
final payload = parts[1];
final normalizedPayload = base64Url.normalize(payload);
final decoded = utf8.decode(base64Url.decode(normalizedPayload));
final Map<String, dynamic> payloadMap = jsonDecode(decoded);
return payloadMap['role'] ?? '';
} catch (e) {
print('Error extracting role from token: $e');
return '';
}
}
Future<void> logout() async {
await TokenService.clearAll();
}
Future<bool> isAuthenticated() async {
final token = await TokenService.getToken();
if (token == null) return false;
return !_isTokenExpired(token);
}
bool _isTokenExpired(String token) {
try {
final parts = token.split('.');
if (parts.length != 3) return true;
final payload = parts[1];
final normalizedPayload = base64Url.normalize(payload);
final decoded = utf8.decode(base64Url.decode(normalizedPayload));
final Map<String, dynamic> payloadMap = jsonDecode(decoded);
final exp = payloadMap['exp'];
if (exp == null) return true;
final expirationDate = DateTime.fromMillisecondsSinceEpoch(exp * 1000);
return DateTime.now().isAfter(expirationDate);
} catch (e) {
return true;
}
}
//register
Future<AppUser> register({
required String email,
required String password,
required String firstName,
required String lastName,
required String role,
}) async {
final response = await http.post(
Uri.parse('$baseUrl${ApiConfig.register}'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'email': email,
'password': password,
'firstName': firstName,
'lastName': lastName,
'role': role,
}),
);
if (response.statusCode == 201) {
final data = jsonDecode(response.body);
return AppUser.fromJson(data['user']);
} else {
throw Exception('Failed to register');
}
}
/*static const String _usersKey = 'users';
static const String _parentsKey = 'parents';
static const String _childrenKey = 'children';
@ -38,5 +147,5 @@ class AuthService {
// Méthode pour récupérer l'utilisateur connecté (mode démonstration)
static Future<AppUser?> getCurrentUser() async {
return null; // Aucun utilisateur en mode démonstration
}
}*/
}

View File

@ -1,8 +1,9 @@
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:p_tits_pas/config/env.dart';
class BugReportService {
static const String _apiUrl = 'https://api.supernounou.local/bug-reports';
static final String _apiUrl = Env.apiV1('/bug-reports');
static Future<void> sendReport(String description) async {
try {

View File

@ -26,6 +26,25 @@ class DataGenerator {
'Nous avons hâte de vous rencontrer.',
'La pédagogie Montessori nous intéresse.'
];
static final List<String> _frenchCities = [
'Paris', 'Lyon', 'Marseille', 'Toulouse', 'Nice', 'Nantes', 'Montpellier', 'Strasbourg',
'Bordeaux', 'Lille', 'Rennes', 'Reims', 'Saint-Étienne', 'Toulon', 'Le Havre', 'Grenoble',
'Dijon', 'Angers', 'Nîmes', 'Villeurbanne', 'Clermont-Ferrand', 'Le Mans', 'Aix-en-Provence',
'Brest', 'Tours', 'Amiens', 'Limoges', 'Annecy', 'Boulogne-Billancourt', 'Perpignan'
];
static final List<String> _countries = [
'France', 'Belgique', 'Suisse', 'Canada', 'Maroc', 'Algérie', 'Tunisie', 'Sénégal',
'Côte d\'Ivoire', 'Madagascar', 'Espagne', 'Italie', 'Portugal', 'Allemagne', 'Royaume-Uni'
];
static final List<String> _childminderPresentations = [
'Bonjour,\n\nJe suis assistante maternelle agréée depuis plusieurs années et je souhaite rejoindre votre plateforme. J\'ai de l\'expérience avec les enfants de tous âges et je privilégie un accompagnement bienveillant.\n\nCordialement',
'Madame, Monsieur,\n\nTitulaire d\'un agrément d\'assistante maternelle, je propose un accueil personnalisé dans un environnement sécurisé et stimulant. Je suis disponible pour discuter de vos besoins.\n\nBien à vous',
'Bonjour,\n\nAssistante maternelle passionnée, je propose un accueil de qualité dans ma maison adaptée aux enfants. J\'ai suivi plusieurs formations et je suis disponible à temps plein.\n\nÀ bientôt',
'Cher gestionnaire,\n\nJe suis une professionnelle expérimentée dans la garde d\'enfants. Mon domicile est aménagé pour accueillir les petits dans les meilleures conditions. Je serais ravie de faire partie de votre réseau.\n\nCordialement',
'Bonjour,\n\nDepuis 5 ans, j\'exerce comme assistante maternelle avec passion. Je propose des activités d\'éveil adaptées et un suivi personnalisé de chaque enfant. Mon agrément me permet d\'accueillir jusqu\'à 4 enfants.\n\nBien cordialement'
];
static String firstName() => _firstNames[_random.nextInt(_firstNames.length)];
static String lastName() => _lastNames[_random.nextInt(_lastNames.length)];
@ -62,4 +81,107 @@ class DataGenerator {
}
return chosenSnippets.join(' ');
}
static String birthDate() {
final now = DateTime.now();
final age = _random.nextInt(31) + 25; // Entre 25 et 55 ans
final birthYear = now.year - age;
final birthMonth = _random.nextInt(12) + 1;
final birthDay = _random.nextInt(28) + 1;
return "${birthDay.toString().padLeft(2, '0')}/${birthMonth.toString().padLeft(2, '0')}/${birthYear}";
}
/// Génère une ville de naissance française
static String birthCity() => _frenchCities[_random.nextInt(_frenchCities.length)];
/// Génère un pays de naissance
static String birthCountry() => _countries[_random.nextInt(_countries.length)];
/// Génère un numéro de sécurité sociale français (NIR)
static String socialSecurityNumber() {
// Format NIR français : 1 YYMM DD CCC KK
// 1 = sexe (1 homme, 2 femme)
final sex = _random.nextBool() ? '1' : '2';
// YY = année de naissance (2 derniers chiffres)
final currentYear = DateTime.now().year;
final birthYear = currentYear - (_random.nextInt(31) + 25); // 25-55 ans
final yy = (birthYear % 100).toString().padLeft(2, '0');
// MM = mois de naissance
final mm = (_random.nextInt(12) + 1).toString().padLeft(2, '0');
// DD = département de naissance (01-95)
final dd = (_random.nextInt(95) + 1).toString().padLeft(2, '0');
// CCC = numéro d'ordre (001-999)
final ccc = (_random.nextInt(999) + 1).toString().padLeft(3, '0');
// KK = clé de contrôle (simulation)
final kk = _random.nextInt(100).toString().padLeft(2, '0');
return '$sex$yy$mm$dd$ccc$kk';
}
/// Génère un numéro d'agrément pour assistante maternelle
static String agreementNumber() {
final year = DateTime.now().year;
final dept = _random.nextInt(95) + 1; // Département 01-95
final sequence = _random.nextInt(9999) + 1; // Numéro de séquence
return 'AM${dept.toString().padLeft(2, '0')}$year${sequence.toString().padLeft(4, '0')}';
}
/// Génère un nombre d'enfants maximum pour l'agrément (1-4)
static int maxChildren() => _random.nextInt(4) + 1;
/// Génère un message de présentation pour assistante maternelle
static String childminderPresentation() =>
_childminderPresentations[_random.nextInt(_childminderPresentations.length)];
/// Génère un âge d'enfant en mois (0-36 mois)
static int childAgeInMonths() => _random.nextInt(37);
/// Génère une durée d'expérience en années (1-15 ans)
static int experienceYears() => _random.nextInt(15) + 1;
/// Génère un tarif horaire (entre 3.50 et 6.00)
static double hourlyRate() {
final rate = 3.50 + (_random.nextDouble() * 2.50);
return double.parse(rate.toStringAsFixed(2));
}
/// Génère des disponibilités (jours de la semaine)
static List<String> availability() {
final days = ['Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi', 'Dimanche'];
final availableDays = <String>[];
// Au moins 3 jours de disponibilité
final minDays = 3;
final maxDays = days.length;
final numDays = _random.nextInt(maxDays - minDays + 1) + minDays;
final selectedIndices = <int>[];
while (selectedIndices.length < numDays) {
final index = _random.nextInt(days.length);
if (!selectedIndices.contains(index)) {
selectedIndices.add(index);
availableDays.add(days[index]);
}
}
return availableDays;
}
/// Génère des horaires (format "08h30-17h30")
static String workingHours() {
final startHours = [7, 8, 9];
final endHours = [16, 17, 18, 19];
final startHour = startHours[_random.nextInt(startHours.length)];
final endHour = endHours[_random.nextInt(endHours.length)];
final startMinutes = _random.nextBool() ? '00' : '30';
final endMinutes = _random.nextBool() ? '00' : '30';
return '${startHour}h$startMinutes-${endHour}h$endMinutes';
}
}

View File

@ -1,40 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>P'titsPas - Connexion</title>
<link rel="stylesheet" href="styles.css">
<link href="https://fonts.googleapis.com/css2?family=Merienda:wght@400;600&display=swap" rel="stylesheet">
</head>
<body>
<img src="assets/river.png" alt="" class="river" aria-hidden="true">
<main class="login-container">
<img src="assets/logo.png" alt="P'titsPas" class="logo">
<h1 class="slogan">Grandir pas à pas, sereinement</h1>
<form class="login-form">
<div class="form-group">
<label for="email">Adresse e-mail</label>
<input type="email" id="email" name="email"
placeholder="Votre adresse e-mail"
aria-label="Saisissez votre adresse e-mail"
required>
</div>
<div class="form-group">
<label for="password">Mot de passe</label>
<input type="password" id="password" name="password"
placeholder="Votre mot de passe"
aria-label="Saisissez votre mot de passe"
required>
</div>
<button type="submit" class="login-button">
Se connecter
</button>
</form>
</main>
</body>
</html>

View File

@ -1,133 +0,0 @@
/* Reset et styles de base */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Merienda', cursive;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background-color: #ffffff;
}
/* Décor de fond */
.river {
position: fixed;
left: 0;
top: 0;
width: 60vw;
opacity: 0.15;
pointer-events: none;
z-index: -1;
}
/* Container principal */
.login-container {
width: 100%;
max-width: 480px;
padding: 2rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 2rem;
}
/* Logo */
.logo {
max-width: 220px;
height: auto;
}
/* Slogan */
.slogan {
font-family: 'Merienda', cursive;
text-align: center;
color: #3a3a3a;
font-size: 1.5rem;
margin-bottom: 2rem;
}
/* Formulaire */
.login-form {
width: 100%;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
/* Labels */
label {
color: #3a3a3a;
font-weight: 600;
font-size: 1rem;
}
/* Champs de saisie */
input {
height: 80px;
border: none;
border-radius: 30px;
padding: 0 1.2rem;
font-size: 1rem;
font-family: inherit;
background-size: cover;
}
input[type="email"] {
background-image: url('assets/field_email.png');
color: #fff;
}
input[type="password"] {
background-image: url('assets/field_password.png');
color: #000;
}
/* Bouton de connexion */
.login-button {
height: 80px;
background-image: url('assets/btn_green.png');
background-size: cover;
border: none;
border-radius: 40px;
color: #ffffff;
font-family: "Merienda", cursive;
font-size: 1.1rem;
cursor: pointer;
transition: filter 0.2s ease;
}
.login-button:hover {
filter: brightness(1.08);
}
/* Media queries pour mobile */
@media (max-width: 480px) {
.river {
width: 40vw;
opacity: 0.10;
clip-path: inset(0 0 20% 0);
}
.logo {
max-width: 120px;
}
.login-container {
padding: 1rem;
}
.slogan {
font-size: 1.2rem;
}
}

View File

@ -61,10 +61,10 @@ packages:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
version: "1.3.2"
ffi:
dependency: transitive
description:
@ -249,10 +249,10 @@ packages:
dependency: transitive
description:
name: intl
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
url: "https://pub.dev"
source: hosted
version: "0.20.2"
version: "0.19.0"
js:
dependency: "direct main"
description:
@ -265,26 +265,26 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
url: "https://pub.dev"
source: hosted
version: "11.0.2"
version: "10.0.8"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
url: "https://pub.dev"
source: hosted
version: "3.0.10"
version: "3.0.9"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
version: "3.0.1"
lints:
dependency: transitive
description:
@ -321,10 +321,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.dev"
source: hosted
version: "1.17.0"
version: "1.16.0"
mime:
dependency: transitive
description:
@ -526,10 +526,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
url: "https://pub.dev"
source: hosted
version: "0.7.7"
version: "0.7.4"
typed_data:
dependency: transitive
description:
@ -606,10 +606,10 @@ packages:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
version: "2.1.4"
vm_service:
dependency: transitive
description:
@ -635,5 +635,5 @@ packages:
source: hosted
version: "1.1.0"
sdks:
dart: ">=3.8.0-0 <4.0.0"
dart: ">=3.7.0 <4.0.0"
flutter: ">=3.27.0"

View File

@ -1,55 +0,0 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:p_tits_pas/screens/auth/login_screen.dart';
import 'package:p_tits_pas/screens/auth/register_choice_screen.dart';
import 'package:p_tits_pas/screens/auth/parent_register_step1_screen.dart';
import 'package:p_tits_pas/screens/auth/parent_register_step2_screen.dart';
import 'package:p_tits_pas/screens/auth/parent_register_step3_screen.dart';
void main() {
// TODO: Initialiser SharedPreferences, Provider, etc.
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'P\'titsPas',
theme: ThemeData(
primarySwatch: Colors.blue, // TODO: Utiliser la palette de la charte graphique
textTheme: GoogleFonts.merriweatherTextTheme(
Theme.of(context).textTheme,
),
visualDensity: VisualDensity.adaptivePlatformDensity,
),
// Gestionnaire de routes initial (simple pour l'instant)
initialRoute: '/', // Ou '/login' selon le point d'entrée désiré
routes: {
'/': (context) => const LoginScreen(), // Exemple, pourrait être RegisterChoiceScreen aussi
'/login': (context) => const LoginScreen(),
'/register-choice': (context) => const RegisterChoiceScreen(),
'/parent-register/step1': (context) => const ParentRegisterStep1Screen(),
'/parent-register/step2': (context) => const ParentRegisterStep2Screen(),
'/parent-register/step3': (context) => const ParentRegisterStep3Screen(),
// TODO: Ajouter les autres routes (step 4, etc., dashboard...)
},
// Gestion des routes inconnues
onUnknownRoute: (settings) {
return MaterialPageRoute(
builder: (context) => Scaffold(
body: Center(
child: Text(
'Route inconnue :\n${settings.name}',
style: GoogleFonts.merriweather(fontSize: 20, color: Colors.red),
textAlign: TextAlign.center,
),
),
),
);
},
);
}
}

View File

@ -1 +0,0 @@

View File

@ -1,22 +0,0 @@
CustomAppTextField(
controller: _firstNameController,
labelText: 'Prénom',
hintText: 'Facultatif si à naître',
isRequired: !widget.childData.isUnbornChild,
),
const SizedBox(height: 6.0),
CustomAppTextField(
controller: _lastNameController,
labelText: 'Nom',
hintText: 'Nom de l\'enfant',
enabled: true,
),
const SizedBox(height: 9.0),
CustomAppTextField(
controller: _dobController,
labelText: widget.childData.isUnbornChild ? 'Date prévisionnelle de naissance' : 'Date de naissance',
hintText: 'JJ/MM/AAAA',
readOnly: true,
onTap: widget.onDateSelect,
suffixIcon: Icons.calendar_today,
),

View File

@ -1,3 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><Error><Code>AuthenticationFailed</Code><Message>Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature.
RequestId:32300bba-601e-0021-763f-bc1466000000
Time:2025-05-03T15:21:39.1449044Z</Message><AuthenticationErrorDetail>Signed expiry time [Sat, 03 May 2025 15:21:15 GMT] must be after signed start time [Sat, 03 May 2025 15:21:39 GMT]</AuthenticationErrorDetail></Error>

View File

@ -1,63 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>PtitsPas Propositions UI Wizard</title>
<link href="https://fonts.googleapis.com/css2?family=Merienda:wght@600&family=Inter:wght@400;500&display=swap" rel="stylesheet">
<style>
body{background:#fffef9;font-family:Inter,sans-serif;color:#2f2f2f;line-height:1.6;padding:2rem;}
h1,h2{font-family:Merienda,cursive;margin:.5rem 0;}
.grid{display:grid;gap:2rem;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));}
.card{background:#fff;border-radius:18px;box-shadow:0 4px 12px rgba(0,0,0,.05);padding:1.5rem;text-align:center;}
img{max-width:100%;height:auto;border-radius:12px;}
.palette{display:flex;justify-content:center;gap:.5rem;margin-top:.75rem;}
.swatch{width:28px;height:28px;border-radius:50%;}
.code{font-size:.75rem;margin-top:.25rem;}
</style>
</head>
<body>
<h1>Création de compte : idées denchaînement de cartes</h1>
<p>Chaque étape saffiche dans une «carte» pastel. En validant, la carte suivante glisse vers lavant (animation CSS: <code>transform: translateX(-100%)</code> + <code>opacity</code>). Trois styles proposés :</p>
<div class="grid">
<div class="card">
<h2>Style 1 : Watercolor</h2>
<img src="A_digital_graphic_design_image_displays_style_opti.png" alt="watercolor stack"/>
<div class="palette">
<div class="swatch" style="background:#FBC9C4"></div>
<div class="swatch" style="background:#FBD38B"></div>
<div class="swatch" style="background:#A9D8C6"></div>
</div>
<div class="code">#FBC9C4 · #FBD38B · #A9D8C6</div>
<p>Bords arrondis 22px, texture papier sur chaque carte.<br><b>Animation</b> : légère rotation (<em>tilt</em>) pour rappeler un paquet de cartes réaliste.</p>
</div>
<div class="card">
<h2>Style 2 : Minimal pastel</h2>
<img src="A_digital_graphic_design_image_displays_style_opti.png" alt="minimal stack"/>
<div class="palette">
<div class="swatch" style="background:#E3DFFD"></div>
<div class="swatch" style="background:#CFEAE3"></div>
<div class="swatch" style="background:#FFE88A"></div>
</div>
<div class="code">#E3DFFD · #CFEAE3 · #FFE88A</div>
<p>Cartes plates, ombre portée subtile (0 2 8 rgba0,05).<br><b>Animation</b> : slide horizontal + fondu rapide.</p>
</div>
<div class="card">
<h2>Style 3 : Modern vibrant</h2>
<img src="A_digital_graphic_design_image_displays_style_opti.png" alt="modern stack"/>
<div class="palette">
<div class="swatch" style="background:#FB86A2"></div>
<div class="swatch" style="background:#F3D468"></div>
<div class="swatch" style="background:#8AC1E3"></div>
</div>
<div class="code">#FB86A2 · #F3D468 · #8AC1E3</div>
<p>Coins arrondis 12px pour une touche «app mobile».<br><b>Animation</b> : carte sort par la gauche, nouvelle carte zoome légèrement.</p>
</div>
</div>
<footer style="font-size:.8rem;margin-top:2rem">© 2025 PtitsPas maquettes UI</footer>
</body>
</html>