Copie du code de la maquette #2
55
.cursorrules
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"project": {
|
||||||
|
"name": "P'titsPas",
|
||||||
|
"description": "Application de gestion de la garde d'enfants pour les collectivités locales"
|
||||||
|
},
|
||||||
|
"conventions": {
|
||||||
|
"language": "fr",
|
||||||
|
"naming": {
|
||||||
|
"package": "p_tits_pas",
|
||||||
|
"classes": "PascalCase",
|
||||||
|
"variables": "camelCase",
|
||||||
|
"constants": "UPPER_CASE"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"formatting": {
|
||||||
|
"indentation": 2,
|
||||||
|
"max_line_length": 80
|
||||||
|
},
|
||||||
|
"documents": {
|
||||||
|
"cahier_des_charges": "docs/SuperNounou_Cahier_Des_Charges_Complet_V1.1.md",
|
||||||
|
"evolutions": "docs/EVOLUTIONS_CDC.md",
|
||||||
|
"charte_graphique": "docs/CHARTE_GRAPHIQUE.md",
|
||||||
|
"specifications_techniques": "docs/SuperNounou_SSS-001.md"
|
||||||
|
},
|
||||||
|
"launch_commands": {
|
||||||
|
"backend": {
|
||||||
|
"start": "cd backend && npm run dev",
|
||||||
|
"description": "Démarre le serveur backend sur le port 3000"
|
||||||
|
},
|
||||||
|
"frontend": {
|
||||||
|
"start": "cd frontend && flutter run -d chrome",
|
||||||
|
"description": "Démarre l'application Flutter dans Chrome"
|
||||||
|
},
|
||||||
|
"full": {
|
||||||
|
"start": "cd backend && npm run dev & cd frontend && flutter run -d chrome",
|
||||||
|
"description": "Démarre le backend et le frontend en parallèle"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rules": [
|
||||||
|
"Toujours répondre en français",
|
||||||
|
"Utiliser le nom 'P'titsPas' dans l'interface utilisateur et la documentation",
|
||||||
|
"Utiliser 'p_tits_pas' pour les noms techniques (packages, fichiers, etc.)",
|
||||||
|
"Respecter les conventions de nommage Flutter/Dart",
|
||||||
|
"Maintenir une cohérence dans le style de code",
|
||||||
|
"Utiliser le camelCase pour les noms de variables, fonctions et méthodes",
|
||||||
|
"Le camelCase doit commencer par une minuscule (ex: maVariable, maFonction)",
|
||||||
|
"Toujours se référer à la documentation officielle en cas de doute",
|
||||||
|
"En cas d'incertitude, poser des questions pour clarifier les besoins",
|
||||||
|
"Si les instructions diffèrent des conventions établies, proposer une évolution à écrire dans le document d'évolution",
|
||||||
|
"Se référer au cahier des charges pour les spécifications fonctionnelles",
|
||||||
|
"Suivre la charte graphique pour tous les éléments visuels",
|
||||||
|
"Consulter les spécifications techniques pour les aspects techniques",
|
||||||
|
"Documenter les évolutions dans le fichier EVOLUTIONS_CDC.md"
|
||||||
|
]
|
||||||
|
}
|
||||||
54
.gitignore
vendored
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
.pub-cache/
|
||||||
|
.dart_tool/
|
||||||
|
.packages
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Database
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
|
||||||
|
# Flutter
|
||||||
|
.flutter-plugins
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
.pub-cache/
|
||||||
|
.pub/
|
||||||
|
/build/
|
||||||
|
|
||||||
|
# Coverage
|
||||||
|
coverage/
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
.cache/
|
||||||
|
Archives/**
|
||||||
|
Xcf/**
|
||||||
|
|
||||||
|
# Release notes
|
||||||
|
CHANGELOG.md
|
||||||
|
Ressources/
|
||||||
135
README.md
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
# P'titsPas
|
||||||
|
|
||||||
|
Plateforme de gestion de la garde d'enfants pour les collectivités locales.
|
||||||
|
|
||||||
|
## Workflow de développement
|
||||||
|
|
||||||
|
Le projet suit un workflow simple :
|
||||||
|
- `develop` : branche principale de développement
|
||||||
|
- `main` : branche des versions stables
|
||||||
|
|
||||||
|
### Processus de release
|
||||||
|
1. Développement sur la branche `develop`
|
||||||
|
2. Tests et validation
|
||||||
|
3. Merge vers `main` avec tag de version
|
||||||
|
4. Mise à jour du CHANGELOG.md
|
||||||
|
|
||||||
|
## Charte graphique
|
||||||
|
|
||||||
|
- **Nom** : P'titsPas
|
||||||
|
- **Couleurs principales** :
|
||||||
|
- Bleu : #2B6CB0 (inspiré du logo)
|
||||||
|
- Blanc : #FFFFFF
|
||||||
|
- Gris clair : #F7FAFC
|
||||||
|
- **Typographie** :
|
||||||
|
- Titres : Google Fonts "Comfortaa"
|
||||||
|
- Corps : Google Fonts "Roboto"
|
||||||
|
|
||||||
|
## Prérequis
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- Node.js (version 18 ou supérieure)
|
||||||
|
- PostgreSQL (version 15 ou supérieure)
|
||||||
|
- npm (version 9 ou supérieure)
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- Flutter SDK (version 3.0 ou supérieure)
|
||||||
|
- Dart SDK (version 3.0 ou supérieure)
|
||||||
|
- Chrome (pour le développement web)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Cloner le dépôt :
|
||||||
|
```bash
|
||||||
|
git clone [URL_DU_REPO]
|
||||||
|
cd ptitspas
|
||||||
|
git checkout develop
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Installer le backend :
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Installer le frontend :
|
||||||
|
```bash
|
||||||
|
cd ../frontend
|
||||||
|
flutter pub get
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Configurer la base de données :
|
||||||
|
```bash
|
||||||
|
# Créer la base de données
|
||||||
|
createdb ptitspas
|
||||||
|
|
||||||
|
# Configurer les variables d'environnement
|
||||||
|
cp .env.example .env
|
||||||
|
# Éditer le fichier .env avec vos paramètres de base de données
|
||||||
|
```
|
||||||
|
|
||||||
|
## Démarrage
|
||||||
|
|
||||||
|
1. Démarrer le backend :
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Démarrer le frontend :
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
flutter run -d chrome
|
||||||
|
```
|
||||||
|
|
||||||
|
## Accès aux services
|
||||||
|
|
||||||
|
- Frontend : http://localhost:8080
|
||||||
|
- Backend API : http://localhost:3000
|
||||||
|
- Base de données PostgreSQL : localhost:5432
|
||||||
|
|
||||||
|
## Structure du projet
|
||||||
|
|
||||||
|
```
|
||||||
|
ptitspas/
|
||||||
|
├── backend/ # API Node.js/Express
|
||||||
|
│ ├── src/ # Code source
|
||||||
|
│ ├── prisma/ # Configuration de la base de données
|
||||||
|
│ └── tests/ # Tests
|
||||||
|
├── frontend/ # Application Flutter
|
||||||
|
│ ├── lib/ # Code source
|
||||||
|
│ ├── assets/ # Images, polices, etc.
|
||||||
|
│ └── test/ # Tests
|
||||||
|
├── CHANGELOG.md # Historique des versions
|
||||||
|
└── README.md # Documentation
|
||||||
|
```
|
||||||
|
|
||||||
|
## Développement
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- Langage : TypeScript
|
||||||
|
- Framework : Express
|
||||||
|
- Base de données : PostgreSQL avec Prisma
|
||||||
|
- API : REST avec OpenAPI 3
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- Framework : Flutter
|
||||||
|
- État : Provider
|
||||||
|
- Navigation : Go Router
|
||||||
|
- UI : Material Design avec thème personnalisé
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backend
|
||||||
|
cd backend
|
||||||
|
npm test
|
||||||
|
|
||||||
|
# Frontend
|
||||||
|
cd frontend
|
||||||
|
flutter test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Licence
|
||||||
|
|
||||||
|
Propriétaire - Tous droits réservés
|
||||||
3320
backend/package-lock.json
generated
Normal file
36
backend/package.json
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"name": "petitspas-backend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Backend pour l'application P'titsPas",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
|
||||||
|
"build": "tsc",
|
||||||
|
"test": "jest",
|
||||||
|
"init-admin": "ts-node src/scripts/initAdmin.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nestjs/common": "^11.1.0",
|
||||||
|
"@prisma/client": "^6.7.0",
|
||||||
|
"@types/jsonwebtoken": "^9.0.9",
|
||||||
|
"bcrypt": "^5.1.1",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"helmet": "^7.1.0",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"morgan": "^1.10.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bcrypt": "^5.0.2",
|
||||||
|
"@types/cors": "^2.8.17",
|
||||||
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/helmet": "^4.0.0",
|
||||||
|
"@types/morgan": "^1.9.9",
|
||||||
|
"@types/node": "^20.11.19",
|
||||||
|
"prisma": "^6.7.0",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"ts-node-dev": "^2.0.0",
|
||||||
|
"typescript": "^5.3.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
108
backend/prisma/schema.prisma
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
// 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
|
||||||
|
}
|
||||||
18
backend/src/admin/admin.controller.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
backend/src/admin/admin.module.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
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 {}
|
||||||
40
backend/src/admin/admin.service.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
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' };
|
||||||
|
}
|
||||||
|
}
|
||||||
17
backend/src/app.module.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { PrismaModule } from './prisma/prisma.module';
|
||||||
|
import { AuthModule } from './auth/auth.module';
|
||||||
|
import { AdminModule } from './admin/admin.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
isGlobal: true,
|
||||||
|
}),
|
||||||
|
PrismaModule,
|
||||||
|
AuthModule,
|
||||||
|
AdminModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
72
backend/src/controllers/theme.controller.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
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' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
backend/src/index.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import cors from 'cors';
|
||||||
|
import helmet from 'helmet';
|
||||||
|
import morgan from 'morgan';
|
||||||
|
import themeRoutes from './routes/theme.routes';
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const port = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
app.use(cors());
|
||||||
|
app.use(helmet());
|
||||||
|
app.use(morgan('dev'));
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Routes
|
||||||
|
app.use('/api/themes', themeRoutes);
|
||||||
|
|
||||||
|
// Gestion des erreurs
|
||||||
|
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
console.error(err.stack);
|
||||||
|
res.status(500).json({ error: 'Une erreur est survenue' });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Démarrage du serveur
|
||||||
|
app.listen(port, () => {
|
||||||
|
console.log(`Serveur démarré sur le port ${port}`);
|
||||||
|
});
|
||||||
95
backend/src/routes/auth.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
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;
|
||||||
14
backend/src/routes/theme.routes.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
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;
|
||||||
39
backend/src/scripts/initAdmin.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
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();
|
||||||
77
backend/src/services/theme.service.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
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 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
28
backend/tsconfig.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2018",
|
||||||
|
"module": "commonjs",
|
||||||
|
"lib": ["es2018", "esnext.asynciterable"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"removeComments": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"strictFunctionTypes": true,
|
||||||
|
"noImplicitThis": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"baseUrl": "."
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules"],
|
||||||
|
"include": ["./src/**/*.ts"]
|
||||||
|
}
|
||||||
51
docs/CHARTE_GRAPHIQUE.md
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# 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** |  | Headers, back-office, print A4+ |
|
||||||
|
| **Icône** |  | Favicon, PWA, app mobile |
|
||||||
|
| **Monochrome** |  | 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; }
|
||||||
|
```
|
||||||
257
docs/EVOLUTIONS_CDC.md
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
# É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.
|
||||||
1212
docs/SuperNounou_Cahier_Des_Charges_Complet_V1.1.md
Normal file
108
docs/SuperNounou_SSS-001.md
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
# 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 d’Activité (PRA)
|
||||||
|
- Spécifications des API & intégrations
|
||||||
|
- Directives de déploiement, d’observabilité, de CI/CD
|
||||||
|
|
||||||
|
## 2. Portée
|
||||||
|
Instances de production, pré-production et recette (Frontend, Backend, PostgreSQL, stockage objets), scripts d’installation, 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 d’Activité
|
||||||
|
|
||||||
|
### 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 d’audit 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 l’API ».
|
||||||
|
|
||||||
|
### 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 d’1 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 d’attente hors-ligne.
|
||||||
|
- Tableaux Grafana d’exemple 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 |
|
||||||
45
frontend/.gitignore
vendored
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.build/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
.swiftpm/
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
# is commented out by default.
|
||||||
|
#.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
**/doc/api/
|
||||||
|
**/ios/Flutter/.last_build_id
|
||||||
|
.dart_tool/
|
||||||
|
.flutter-plugins
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
.pub-cache/
|
||||||
|
.pub/
|
||||||
|
/build/
|
||||||
|
|
||||||
|
# Symbolication related
|
||||||
|
app.*.symbols
|
||||||
|
|
||||||
|
# Obfuscation related
|
||||||
|
app.*.map.json
|
||||||
|
|
||||||
|
# Android Studio will place build artifacts here
|
||||||
|
/android/app/debug
|
||||||
|
/android/app/profile
|
||||||
|
/android/app/release
|
||||||
33
frontend/.metadata
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# This file tracks properties of this Flutter project.
|
||||||
|
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||||
|
#
|
||||||
|
# This file should be version controlled and should not be manually edited.
|
||||||
|
|
||||||
|
version:
|
||||||
|
revision: "ea121f8859e4b13e47a8f845e4586164519588bc"
|
||||||
|
channel: "stable"
|
||||||
|
|
||||||
|
project_type: app
|
||||||
|
|
||||||
|
# Tracks metadata for the flutter migrate command
|
||||||
|
migration:
|
||||||
|
platforms:
|
||||||
|
- platform: root
|
||||||
|
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||||
|
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||||
|
- platform: web
|
||||||
|
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||||
|
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||||
|
- platform: windows
|
||||||
|
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||||
|
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||||
|
|
||||||
|
# User provided section
|
||||||
|
|
||||||
|
# List of Local paths (relative to this file) that should be
|
||||||
|
# ignored by the migrate tool.
|
||||||
|
#
|
||||||
|
# Files that are not part of the templates will be ignored by default.
|
||||||
|
unmanaged_files:
|
||||||
|
- 'lib/main.dart'
|
||||||
|
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||||
16
frontend/README.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# petitspas
|
||||||
|
|
||||||
|
A new Flutter project.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
This project is a starting point for a Flutter application.
|
||||||
|
|
||||||
|
A few resources to get you started if this is your first Flutter project:
|
||||||
|
|
||||||
|
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||||
|
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
|
||||||
|
|
||||||
|
For help getting started with Flutter development, view the
|
||||||
|
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||||
|
samples, guidance on mobile development, and a full API reference.
|
||||||
28
frontend/analysis_options.yaml
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# This file configures the analyzer, which statically analyzes Dart code to
|
||||||
|
# check for errors, warnings, and lints.
|
||||||
|
#
|
||||||
|
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||||
|
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||||
|
# invoked from the command line by running `flutter analyze`.
|
||||||
|
|
||||||
|
# The following line activates a set of recommended lints for Flutter apps,
|
||||||
|
# packages, and plugins designed to encourage good coding practices.
|
||||||
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
linter:
|
||||||
|
# The lint rules applied to this project can be customized in the
|
||||||
|
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||||
|
# included above or to enable additional rules. A list of all available lints
|
||||||
|
# and their documentation is published at https://dart.dev/lints.
|
||||||
|
#
|
||||||
|
# Instead of disabling a lint rule for the entire project in the
|
||||||
|
# section below, it can also be suppressed for a single line of code
|
||||||
|
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||||
|
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||||
|
# producing the lint.
|
||||||
|
rules:
|
||||||
|
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||||
|
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||||
|
|
||||||
|
# Additional information about this file can be found at
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
||||||
32
frontend/android/app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:launchMode="singleTop"
|
||||||
|
android:theme="@style/LaunchTheme"
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||||
|
the Android process has started. This theme is visible to the user
|
||||||
|
while the Flutter UI initializes. After that, this theme continues
|
||||||
|
to determine the Window background behind the Flutter UI. -->
|
||||||
|
<meta-data
|
||||||
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
|
android:resource="@style/NormalTheme"
|
||||||
|
/>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<!-- Suppression de l'activité pour image_cropper -->
|
||||||
|
<!--
|
||||||
|
<activity
|
||||||
|
android:name="com.yalantis.ucrop.UCropActivity"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
|
android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- Don't delete the meta-data below. -->
|
||||||
|
<meta-data
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
package io.flutter.plugins;
|
||||||
|
|
||||||
|
import androidx.annotation.Keep;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import io.flutter.Log;
|
||||||
|
|
||||||
|
import io.flutter.embedding.engine.FlutterEngine;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generated file. Do not edit.
|
||||||
|
* This file is generated by the Flutter tool based on the
|
||||||
|
* plugins that support the Android platform.
|
||||||
|
*/
|
||||||
|
@Keep
|
||||||
|
public final class GeneratedPluginRegistrant {
|
||||||
|
private static final String TAG = "GeneratedPluginRegistrant";
|
||||||
|
public static void registerWith(@NonNull FlutterEngine flutterEngine) {
|
||||||
|
try {
|
||||||
|
flutterEngine.getPlugins().add(new io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin());
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error registering plugin flutter_plugin_android_lifecycle, io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin", e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
flutterEngine.getPlugins().add(new io.flutter.plugins.imagepicker.ImagePickerPlugin());
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error registering plugin image_picker_android, io.flutter.plugins.imagepicker.ImagePickerPlugin", e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
flutterEngine.getPlugins().add(new io.flutter.plugins.pathprovider.PathProviderPlugin());
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error registering plugin path_provider_android, io.flutter.plugins.pathprovider.PathProviderPlugin", e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
flutterEngine.getPlugins().add(new io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin());
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error registering plugin shared_preferences_android, io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin", e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
flutterEngine.getPlugins().add(new io.flutter.plugins.urllauncher.UrlLauncherPlugin());
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error registering plugin url_launcher_android, io.flutter.plugins.urllauncher.UrlLauncherPlugin", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
frontend/android/local.properties
Normal file
@ -0,0 +1 @@
|
|||||||
|
flutter.sdk=C:\\Users\\marti\\dev\\flutter
|
||||||
BIN
frontend/assets/cards/card_blue.png
Normal file
|
After Width: | Height: | Size: 656 KiB |
BIN
frontend/assets/cards/card_blue_h.png
Normal file
|
After Width: | Height: | Size: 693 KiB |
BIN
frontend/assets/cards/card_green.png
Normal file
|
After Width: | Height: | Size: 754 KiB |
BIN
frontend/assets/cards/card_green_h.png
Normal file
|
After Width: | Height: | Size: 784 KiB |
BIN
frontend/assets/cards/card_lavender.png
Normal file
|
After Width: | Height: | Size: 767 KiB |
BIN
frontend/assets/cards/card_lavender_h.png
Normal file
|
After Width: | Height: | Size: 755 KiB |
BIN
frontend/assets/cards/card_lime.png
Normal file
|
After Width: | Height: | Size: 851 KiB |
BIN
frontend/assets/cards/card_lime_h.png
Normal file
|
After Width: | Height: | Size: 850 KiB |
BIN
frontend/assets/cards/card_peach.png
Normal file
|
After Width: | Height: | Size: 517 KiB |
BIN
frontend/assets/cards/card_peach_h.png
Normal file
|
After Width: | Height: | Size: 560 KiB |
BIN
frontend/assets/cards/card_pink.png
Normal file
|
After Width: | Height: | Size: 908 KiB |
BIN
frontend/assets/cards/card_pink_h.png
Normal file
|
After Width: | Height: | Size: 909 KiB |
BIN
frontend/assets/cards/card_red.png
Normal file
|
After Width: | Height: | Size: 721 KiB |
BIN
frontend/assets/cards/card_red_h.png
Normal file
|
After Width: | Height: | Size: 744 KiB |
BIN
frontend/assets/fonts/Merienda-VariableFont_wght.ttf
Normal file
BIN
frontend/assets/images/btn_green.png
Normal file
|
After Width: | Height: | Size: 181 KiB |
BIN
frontend/assets/images/chevron_right.png
Normal file
|
After Width: | Height: | Size: 135 KiB |
BIN
frontend/assets/images/coche.png
Normal file
|
After Width: | Height: | Size: 183 KiB |
BIN
frontend/assets/images/cross.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
frontend/assets/images/icon.png
Normal file
|
After Width: | Height: | Size: 157 KiB |
BIN
frontend/assets/images/icon_assmat.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
frontend/assets/images/icon_parents.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
frontend/assets/images/input_field_bg.png
Normal file
|
After Width: | Height: | Size: 510 KiB |
BIN
frontend/assets/images/input_field_jaune.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
frontend/assets/images/input_field_lavande.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
frontend/assets/images/logo.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/assets/images/paper.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
frontend/assets/images/paper2.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
frontend/assets/images/photo.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/assets/images/plus.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
frontend/assets/images/red_cross2.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
frontend/assets/images/river.png
Normal file
|
After Width: | Height: | Size: 300 KiB |
BIN
frontend/assets/images/river_logo_desktop.png
Normal file
|
After Width: | Height: | Size: 498 KiB |
BIN
frontend/assets/images/square.png
Normal file
|
After Width: | Height: | Size: 890 KiB |
BIN
frontend/assets/images/switch_off.png
Normal file
|
After Width: | Height: | Size: 167 KiB |
BIN
frontend/assets/images/switch_on.png
Normal file
|
After Width: | Height: | Size: 288 KiB |
43
frontend/lib/main.dart
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:flutter_localizations/flutter_localizations.dart'; // Import pour la localisation
|
||||||
|
// import 'package:provider/provider.dart'; // Supprimer Provider
|
||||||
|
import 'navigation/app_router.dart';
|
||||||
|
// import 'theme/app_theme.dart'; // Supprimer AppTheme
|
||||||
|
// import 'theme/theme_provider.dart'; // Supprimer ThemeProvider
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
runApp(const MyApp()); // Exécution simple
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyApp extends StatelessWidget {
|
||||||
|
const MyApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// Pas besoin de Provider.of ici
|
||||||
|
|
||||||
|
return MaterialApp(
|
||||||
|
title: 'P\'titsPas',
|
||||||
|
theme: ThemeData.light().copyWith( // Utiliser un thème simple par défaut
|
||||||
|
textTheme: GoogleFonts.meriendaTextTheme(
|
||||||
|
ThemeData.light().textTheme,
|
||||||
|
),
|
||||||
|
// TODO: Définir les couleurs principales si besoin
|
||||||
|
),
|
||||||
|
localizationsDelegates: const [ // Configuration pour la localisation
|
||||||
|
GlobalMaterialLocalizations.delegate,
|
||||||
|
GlobalWidgetsLocalizations.delegate,
|
||||||
|
GlobalCupertinoLocalizations.delegate,
|
||||||
|
],
|
||||||
|
supportedLocales: const [ // Langues supportées
|
||||||
|
Locale('fr', 'FR'), // Français
|
||||||
|
// Locale('en', 'US'), // Anglais, si besoin
|
||||||
|
],
|
||||||
|
locale: const Locale('fr', 'FR'), // Forcer la locale française par défaut
|
||||||
|
initialRoute: AppRouter.login,
|
||||||
|
onGenerateRoute: AppRouter.generateRoute,
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
25
frontend/lib/models/card_assets.dart
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
enum CardColorVertical {
|
||||||
|
red('assets/cards/card_red.png'),
|
||||||
|
pink('assets/cards/card_pink.png'),
|
||||||
|
peach('assets/cards/card_peach.png'),
|
||||||
|
lime('assets/cards/card_lime.png'),
|
||||||
|
lavender('assets/cards/card_lavender.png'),
|
||||||
|
green('assets/cards/card_green.png'),
|
||||||
|
blue('assets/cards/card_blue.png');
|
||||||
|
|
||||||
|
final String path;
|
||||||
|
const CardColorVertical(this.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CardColorHorizontal {
|
||||||
|
red('assets/cards/card_red_h.png'),
|
||||||
|
pink('assets/cards/card_pink_h.png'),
|
||||||
|
peach('assets/cards/card_peach_h.png'),
|
||||||
|
lime('assets/cards/card_lime_h.png'),
|
||||||
|
lavender('assets/cards/card_lavender_h.png'),
|
||||||
|
green('assets/cards/card_green_h.png'),
|
||||||
|
blue('assets/cards/card_blue_h.png');
|
||||||
|
|
||||||
|
final String path;
|
||||||
|
const CardColorHorizontal(this.path);
|
||||||
|
}
|
||||||
35
frontend/lib/models/user.dart
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
class AppUser {
|
||||||
|
final String id;
|
||||||
|
final String email;
|
||||||
|
final String role;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final DateTime updatedAt;
|
||||||
|
|
||||||
|
AppUser({
|
||||||
|
required this.id,
|
||||||
|
required this.email,
|
||||||
|
required this.role,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.updatedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory AppUser.fromJson(Map<String, dynamic> json) {
|
||||||
|
return AppUser(
|
||||||
|
id: json['id'] as String,
|
||||||
|
email: json['email'] as String,
|
||||||
|
role: json['role'] as String,
|
||||||
|
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||||
|
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'email': email,
|
||||||
|
'role': role,
|
||||||
|
'createdAt': createdAt.toIso8601String(),
|
||||||
|
'updatedAt': updatedAt.toIso8601String(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
97
frontend/lib/models/user_registration_data.dart
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
105
frontend/lib/navigation/app_router.dart
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.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';
|
||||||
|
|
||||||
|
class AppRouter {
|
||||||
|
static const String login = '/login';
|
||||||
|
static const String registerChoice = '/register-choice';
|
||||||
|
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 Route<dynamic> generateRoute(RouteSettings settings) {
|
||||||
|
Widget screen;
|
||||||
|
bool slideTransition = false;
|
||||||
|
Object? args = settings.arguments;
|
||||||
|
|
||||||
|
Widget buildErrorScreen(String step) {
|
||||||
|
print("Erreur: Données UserRegistrationData manquantes ou de mauvais type pour l'étape $step");
|
||||||
|
return const ParentRegisterStep1Screen();
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (settings.name) {
|
||||||
|
case login:
|
||||||
|
screen = const LoginPage();
|
||||||
|
break;
|
||||||
|
case registerChoice:
|
||||||
|
screen = const RegisterChoiceScreen();
|
||||||
|
slideTransition = true;
|
||||||
|
break;
|
||||||
|
case parentRegisterStep1:
|
||||||
|
screen = const ParentRegisterStep1Screen();
|
||||||
|
slideTransition = true;
|
||||||
|
break;
|
||||||
|
case parentRegisterStep2:
|
||||||
|
if (args is UserRegistrationData) {
|
||||||
|
screen = ParentRegisterStep2Screen(registrationData: args);
|
||||||
|
} else {
|
||||||
|
screen = buildErrorScreen('2');
|
||||||
|
}
|
||||||
|
slideTransition = true;
|
||||||
|
break;
|
||||||
|
case parentRegisterStep3:
|
||||||
|
if (args is UserRegistrationData) {
|
||||||
|
screen = ParentRegisterStep3Screen(registrationData: args);
|
||||||
|
} else {
|
||||||
|
screen = buildErrorScreen('3');
|
||||||
|
}
|
||||||
|
slideTransition = true;
|
||||||
|
break;
|
||||||
|
case parentRegisterStep4:
|
||||||
|
if (args is UserRegistrationData) {
|
||||||
|
screen = ParentRegisterStep4Screen(registrationData: args);
|
||||||
|
} else {
|
||||||
|
screen = buildErrorScreen('4');
|
||||||
|
}
|
||||||
|
slideTransition = true;
|
||||||
|
break;
|
||||||
|
case parentRegisterStep5:
|
||||||
|
if (args is UserRegistrationData) {
|
||||||
|
screen = ParentRegisterStep5Screen(registrationData: args);
|
||||||
|
} else {
|
||||||
|
screen = buildErrorScreen('5');
|
||||||
|
}
|
||||||
|
slideTransition = true;
|
||||||
|
break;
|
||||||
|
case home:
|
||||||
|
screen = const HomeScreen();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
screen = Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: Text('Route non définie : ${settings.name}'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (slideTransition) {
|
||||||
|
return PageRouteBuilder(
|
||||||
|
pageBuilder: (context, animation, secondaryAnimation) => screen,
|
||||||
|
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
||||||
|
const begin = Offset(1.0, 0.0);
|
||||||
|
const end = Offset.zero;
|
||||||
|
const curve = Curves.easeInOut;
|
||||||
|
var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
|
||||||
|
var offsetAnimation = animation.drive(tween);
|
||||||
|
return SlideTransition(position: offsetAnimation, child: child);
|
||||||
|
},
|
||||||
|
transitionDuration: const Duration(milliseconds: 400),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return MaterialPageRoute(builder: (_) => screen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
399
frontend/lib/screens/auth/login_screen.dart
Normal file
@ -0,0 +1,399 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
|
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';
|
||||||
|
|
||||||
|
class LoginPage extends StatefulWidget {
|
||||||
|
const LoginPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<LoginPage> createState() => _LoginPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LoginPageState extends State<LoginPage> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
final _emailController = TextEditingController();
|
||||||
|
final _passwordController = TextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_emailController.dispose();
|
||||||
|
_passwordController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _validateEmail(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Veuillez entrer votre email';
|
||||||
|
}
|
||||||
|
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
|
||||||
|
return 'Veuillez entrer un email valide';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _validatePassword(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Veuillez entrer votre mot de passe';
|
||||||
|
}
|
||||||
|
if (value.length < 6) {
|
||||||
|
return 'Le mot de passe doit contenir au moins 6 caractères';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
body: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
// Version desktop (web)
|
||||||
|
if (kIsWeb) {
|
||||||
|
final w = constraints.maxWidth;
|
||||||
|
final h = constraints.maxHeight;
|
||||||
|
|
||||||
|
return FutureBuilder(
|
||||||
|
future: _getImageDimensions(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (!snapshot.hasData) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
final imageDimensions = snapshot.data!;
|
||||||
|
final imageHeight = h;
|
||||||
|
final imageWidth = imageHeight * (imageDimensions.width / imageDimensions.height);
|
||||||
|
final remainingWidth = w - imageWidth;
|
||||||
|
final leftMargin = remainingWidth / 4;
|
||||||
|
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
// Fond en papier
|
||||||
|
Positioned.fill(
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/paper2.png',
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
repeat: ImageRepeat.repeat,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Image principale
|
||||||
|
Positioned(
|
||||||
|
left: leftMargin,
|
||||||
|
top: 0,
|
||||||
|
height: imageHeight,
|
||||||
|
width: imageWidth,
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/river_logo_desktop.png',
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Formulaire dans le cadran en bas à droite
|
||||||
|
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
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(w * 0.02), // 2% de padding
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
// Champs côte à côte
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: CustomAppTextField(
|
||||||
|
controller: _emailController,
|
||||||
|
labelText: 'Email',
|
||||||
|
hintText: 'Votre adresse email',
|
||||||
|
validator: _validateEmail,
|
||||||
|
style: CustomAppTextFieldStyle.lavande,
|
||||||
|
fieldHeight: 53,
|
||||||
|
fieldWidth: double.infinity,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 20),
|
||||||
|
Expanded(
|
||||||
|
child: CustomAppTextField(
|
||||||
|
controller: _passwordController,
|
||||||
|
labelText: 'Mot de passe',
|
||||||
|
hintText: 'Votre mot de passe',
|
||||||
|
obscureText: true,
|
||||||
|
validator: _validatePassword,
|
||||||
|
style: CustomAppTextFieldStyle.jaune,
|
||||||
|
fieldHeight: 53,
|
||||||
|
fieldWidth: double.infinity,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
// Bouton centré
|
||||||
|
Center(
|
||||||
|
child: 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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
// Lien mot de passe oublié
|
||||||
|
Center(
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
// TODO: Implémenter la logique de récupération de mot de passe
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
'Mot de passe oublié ?',
|
||||||
|
style: GoogleFonts.merienda(
|
||||||
|
fontSize: 14,
|
||||||
|
color: const Color(0xFF2D6A4F),
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
// Lien de création de compte
|
||||||
|
Center(
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pushNamed(context, '/register-choice');
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
'Créer un compte',
|
||||||
|
style: GoogleFonts.merienda(
|
||||||
|
fontSize: 16,
|
||||||
|
color: const Color(0xFF2D6A4F),
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20), // Réduit l'espacement en bas
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Pied de page
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.transparent,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
_FooterLink(
|
||||||
|
text: 'Contact support',
|
||||||
|
onTap: () async {
|
||||||
|
final Uri emailLaunchUri = Uri(
|
||||||
|
scheme: 'mailto',
|
||||||
|
path: 'support@supernounou.local',
|
||||||
|
);
|
||||||
|
if (await canLaunchUrl(emailLaunchUri)) {
|
||||||
|
await launchUrl(emailLaunchUri);
|
||||||
|
} else {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'Impossible d\'ouvrir le client mail',
|
||||||
|
style: GoogleFonts.merienda(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_FooterLink(
|
||||||
|
text: 'Signaler un bug',
|
||||||
|
onTap: () {
|
||||||
|
_showBugReportDialog(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_FooterLink(
|
||||||
|
text: 'Mentions légales',
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pushNamed(context, '/legal');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_FooterLink(
|
||||||
|
text: 'Politique de confidentialité',
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pushNamed(context, '/privacy');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version mobile (à implémenter)
|
||||||
|
return const Center(
|
||||||
|
child: Text('Version mobile à implémenter'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showBugReportDialog(BuildContext context) {
|
||||||
|
final TextEditingController controller = TextEditingController();
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Text(
|
||||||
|
'Signaler un bug',
|
||||||
|
style: GoogleFonts.merienda(),
|
||||||
|
),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: controller,
|
||||||
|
maxLines: 5,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Décrivez le problème rencontré...',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: Text(
|
||||||
|
'Annuler',
|
||||||
|
style: GoogleFonts.merienda(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
if (controller.text.trim().isEmpty) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'Veuillez décrire le problème',
|
||||||
|
style: GoogleFonts.merienda(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
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(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'Erreur lors de l\'envoi du rapport',
|
||||||
|
style: GoogleFonts.merienda(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
'Envoyer',
|
||||||
|
style: GoogleFonts.merienda(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ImageDimensions> _getImageDimensions() async {
|
||||||
|
final image = Image.asset('assets/images/river_logo_desktop.png');
|
||||||
|
final completer = Completer<ImageDimensions>();
|
||||||
|
image.image.resolve(const ImageConfiguration()).addListener(
|
||||||
|
ImageStreamListener((info, _) {
|
||||||
|
completer.complete(ImageDimensions(
|
||||||
|
width: info.image.width.toDouble(),
|
||||||
|
height: info.image.height.toDouble(),
|
||||||
|
));
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ImageDimensions {
|
||||||
|
final double width;
|
||||||
|
final double height;
|
||||||
|
|
||||||
|
ImageDimensions({required this.width, required this.height});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────────────────────────────────────────────────────────────
|
||||||
|
// Lien du pied de page
|
||||||
|
// ───────────────────────────────────────────────────────────────
|
||||||
|
class _FooterLink extends StatelessWidget {
|
||||||
|
final String text;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
const _FooterLink({
|
||||||
|
required this.text,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: GoogleFonts.merienda(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.black87,
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
226
frontend/lib/screens/auth/parent_register_step1_screen.dart
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
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',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
255
frontend/lib/screens/auth/parent_register_step2_screen.dart
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
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(() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
487
frontend/lib/screens/auth/parent_register_step3_screen.dart
Normal file
@ -0,0 +1,487 @@
|
|||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
217
frontend/lib/screens/auth/parent_register_step4_screen.dart
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
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',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
464
frontend/lib/screens/auth/parent_register_step5_screen.dart
Normal file
@ -0,0 +1,464 @@
|
|||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
162
frontend/lib/screens/auth/register_choice_screen.dart
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'dart:math' as math; // Pour la rotation du chevron
|
||||||
|
import '../../widgets/hover_relief_widget.dart'; // Import du widget générique
|
||||||
|
import '../../models/card_assets.dart'; // Import des enums de cartes
|
||||||
|
|
||||||
|
class RegisterChoiceScreen extends StatelessWidget {
|
||||||
|
const RegisterChoiceScreen({super.key});
|
||||||
|
|
||||||
|
@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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Bouton Retour (chevron gauche)
|
||||||
|
Positioned(
|
||||||
|
top: 40,
|
||||||
|
left: 40,
|
||||||
|
child: IconButton(
|
||||||
|
icon: Transform(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
transform: Matrix4.rotationY(math.pi),
|
||||||
|
child: Image.asset('assets/images/chevron_right.png', height: 40),
|
||||||
|
),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
tooltip: 'Retour',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Contenu principal en Row (Gauche / Droite)
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: screenSize.width * 0.05),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// Partie Gauche: Texte d'instruction centré
|
||||||
|
Expanded(
|
||||||
|
flex: 1,
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
'Veuillez choisir votre\ntype de compte :',
|
||||||
|
style: GoogleFonts.merienda(
|
||||||
|
fontSize: 36,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black87,
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Espace entre les deux parties
|
||||||
|
SizedBox(width: screenSize.width * 0.05),
|
||||||
|
|
||||||
|
// Partie Droite: Carte rose avec les boutons
|
||||||
|
Expanded(
|
||||||
|
flex: 1,
|
||||||
|
child: Center(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxHeight: screenSize.height * 0.78, // Augmenté pour éviter l'overflow
|
||||||
|
),
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 2 / 3,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
image: DecorationImage(
|
||||||
|
image: AssetImage(CardColorVertical.pink.path),
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
// Bouton "Parents" avec HoverReliefWidget appliqué uniquement à l'image
|
||||||
|
_buildChoiceButton(
|
||||||
|
context: context,
|
||||||
|
iconPath: 'assets/images/icon_parents.png',
|
||||||
|
label: 'Parents',
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pushNamed(context, '/parent-register/step1');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// Bouton "Assistante Maternelle" avec HoverReliefWidget appliqué uniquement à l'image
|
||||||
|
_buildChoiceButton(
|
||||||
|
context: context,
|
||||||
|
iconPath: 'assets/images/icon_assmat.png',
|
||||||
|
label: 'Assistante Maternelle',
|
||||||
|
onPressed: () {
|
||||||
|
// TODO: Naviguer vers l'écran d'inscription assmat
|
||||||
|
print('Choix: Assistante Maternelle');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nouvelle méthode helper pour construire les boutons de choix
|
||||||
|
Widget _buildChoiceButton({
|
||||||
|
required BuildContext context,
|
||||||
|
required String iconPath,
|
||||||
|
required String label,
|
||||||
|
required VoidCallback onPressed,
|
||||||
|
}) {
|
||||||
|
// TODO: Déterminer la couleur de base de card_rose.png et ajuster ces couleurs d'ombre
|
||||||
|
final Color baseRoseColor = Colors.pink.shade300; // Placeholder
|
||||||
|
final Color initialShadow = baseRoseColor.withAlpha(90); // Rose plus foncé et transparent pour l'ombre initiale
|
||||||
|
final Color hoverShadow = baseRoseColor.withAlpha(130); // Rose encore plus foncé pour l'ombre au survol
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
HoverReliefWidget(
|
||||||
|
onPressed: onPressed,
|
||||||
|
borderRadius: BorderRadius.circular(15.0),
|
||||||
|
initialShadowColor: initialShadow, // Ombre rose initiale
|
||||||
|
hoverShadowColor: hoverShadow, // Ombre rose au survol
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Image.asset(iconPath, height: 140),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: GoogleFonts.merienda(
|
||||||
|
fontSize: 26,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.black.withOpacity(0.85),
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- La classe HoverChoiceButton peut maintenant être supprimée si elle n'est plus utilisée ailleurs ---
|
||||||
|
// class HoverChoiceButton extends StatefulWidget { ... }
|
||||||
|
// class _HoverChoiceButtonState extends State<HoverChoiceButton> { ... }
|
||||||
17
frontend/lib/screens/home/home_screen.dart
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class HomeScreen extends StatelessWidget {
|
||||||
|
const HomeScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Accueil'),
|
||||||
|
),
|
||||||
|
body: const Center(
|
||||||
|
child: Text('Bienvenue sur P\'titsPas !'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
64
frontend/lib/screens/legal/legal_page.dart
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
|
class LegalPage extends StatelessWidget {
|
||||||
|
const LegalPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(
|
||||||
|
'Mentions légales',
|
||||||
|
style: GoogleFonts.merienda(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Éditeur',
|
||||||
|
style: GoogleFonts.merienda(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'P\'titsPas est une application développée pour les collectivités locales.',
|
||||||
|
style: GoogleFonts.merienda(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
Text(
|
||||||
|
'Hébergeur',
|
||||||
|
style: GoogleFonts.merienda(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Les données sont hébergées sur des serveurs sécurisés en France.',
|
||||||
|
style: GoogleFonts.merienda(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
Text(
|
||||||
|
'Responsable du traitement',
|
||||||
|
style: GoogleFonts.merienda(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Le responsable du traitement des données est la collectivité locale utilisatrice de l\'application.',
|
||||||
|
style: GoogleFonts.merienda(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
64
frontend/lib/screens/legal/privacy_page.dart
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
|
class PrivacyPage extends StatelessWidget {
|
||||||
|
const PrivacyPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(
|
||||||
|
'Politique de confidentialité',
|
||||||
|
style: GoogleFonts.merienda(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Protection des données personnelles',
|
||||||
|
style: GoogleFonts.merienda(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'P\'titsPas s\'engage à protéger vos données personnelles conformément au RGPD.',
|
||||||
|
style: GoogleFonts.merienda(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
Text(
|
||||||
|
'Données collectées',
|
||||||
|
style: GoogleFonts.merienda(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Les données collectées sont nécessaires au bon fonctionnement de l\'application et à la gestion des contrats de garde d\'enfants.',
|
||||||
|
style: GoogleFonts.merienda(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
Text(
|
||||||
|
'Vos droits',
|
||||||
|
style: GoogleFonts.merienda(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Vous disposez d\'un droit d\'accès, de rectification, d\'effacement et de portabilité de vos données.',
|
||||||
|
style: GoogleFonts.merienda(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
42
frontend/lib/services/auth_service.dart
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import '../models/user.dart';
|
||||||
|
|
||||||
|
class AuthService {
|
||||||
|
static const String _usersKey = 'users';
|
||||||
|
static const String _parentsKey = 'parents';
|
||||||
|
static const String _childrenKey = 'children';
|
||||||
|
|
||||||
|
// Méthode pour se connecter (mode démonstration)
|
||||||
|
static Future<AppUser> login(String email, String password) async {
|
||||||
|
await Future.delayed(const Duration(seconds: 1)); // Simule un délai de traitement
|
||||||
|
throw Exception('Mode démonstration - Connexion désactivée');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthode pour s'inscrire (mode démonstration)
|
||||||
|
static Future<AppUser> register({
|
||||||
|
required String email,
|
||||||
|
required String password,
|
||||||
|
required String firstName,
|
||||||
|
required String lastName,
|
||||||
|
required String role,
|
||||||
|
}) async {
|
||||||
|
await Future.delayed(const Duration(seconds: 1)); // Simule un délai de traitement
|
||||||
|
throw Exception('Mode démonstration - Inscription désactivée');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthode pour se déconnecter (mode démonstration)
|
||||||
|
static Future<void> logout() async {
|
||||||
|
// Ne fait rien en mode démonstration
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthode pour vérifier si l'utilisateur est connecté (mode démonstration)
|
||||||
|
static Future<bool> isLoggedIn() async {
|
||||||
|
return false; // Toujours non connecté en mode démonstration
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
28
frontend/lib/services/bug_report_service.dart
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
class BugReportService {
|
||||||
|
static const String _apiUrl = 'https://api.supernounou.local/bug-reports';
|
||||||
|
|
||||||
|
static Future<void> sendReport(String description) async {
|
||||||
|
try {
|
||||||
|
final response = await http.post(
|
||||||
|
Uri.parse(_apiUrl),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: jsonEncode({
|
||||||
|
'description': description,
|
||||||
|
'timestamp': DateTime.now().toIso8601String(),
|
||||||
|
'platform': 'web', // TODO: Ajouter la détection de la plateforme
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw Exception('Erreur lors de l\'envoi du rapport');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
65
frontend/lib/utils/data_generator.dart
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
class DataGenerator {
|
||||||
|
static final Random _random = Random();
|
||||||
|
|
||||||
|
static final List<String> _firstNames = [
|
||||||
|
'Alice', 'Bob', 'Charlie', 'David', 'Eva', 'Félix', 'Gabrielle', 'Hugo', 'Inès', 'Jules',
|
||||||
|
'Léa', 'Manon', 'Nathan', 'Oscar', 'Pauline', 'Quentin', 'Raphaël', 'Sophie', 'Théo', 'Victoire'
|
||||||
|
];
|
||||||
|
|
||||||
|
static final List<String> _lastNames = [
|
||||||
|
'Martin', 'Bernard', 'Dubois', 'Thomas', 'Robert', 'Richard', 'Petit', 'Durand', 'Leroy', 'Moreau',
|
||||||
|
'Simon', 'Laurent', 'Lefebvre', 'Michel', 'Garcia', 'David', 'Bertrand', 'Roux', 'Vincent', 'Fournier'
|
||||||
|
];
|
||||||
|
|
||||||
|
static final List<String> _addressSuffixes = [
|
||||||
|
'Rue de la Paix', 'Boulevard des Rêves', 'Avenue du Soleil', 'Place des Étoiles', 'Chemin des Champs'
|
||||||
|
];
|
||||||
|
|
||||||
|
static final List<String> _motivationSnippets = [
|
||||||
|
'Nous cherchons une personne de confiance.',
|
||||||
|
'Nos horaires sont atypiques.',
|
||||||
|
'Notre enfant est plein de vie.',
|
||||||
|
'Nous souhaitons une garde à temps plein.',
|
||||||
|
'Une adaptation en douceur est primordiale pour nous.',
|
||||||
|
'Nous avons hâte de vous rencontrer.',
|
||||||
|
'La pédagogie Montessori nous intéresse.'
|
||||||
|
];
|
||||||
|
|
||||||
|
static String firstName() => _firstNames[_random.nextInt(_firstNames.length)];
|
||||||
|
static String lastName() => _lastNames[_random.nextInt(_lastNames.length)];
|
||||||
|
static String address() => "${_random.nextInt(100) + 1} ${_addressSuffixes[_random.nextInt(_addressSuffixes.length)]}";
|
||||||
|
static String postalCode() => "750${_random.nextInt(10)}${_random.nextInt(10)}";
|
||||||
|
static String city() => "Paris";
|
||||||
|
static String phone() => "06${_random.nextInt(10)}${_random.nextInt(10)}${_random.nextInt(10)}${_random.nextInt(10)}${_random.nextInt(10)}${_random.nextInt(10)}${_random.nextInt(10)}${_random.nextInt(10)}";
|
||||||
|
static String email(String firstName, String lastName) => "${firstName.toLowerCase()}.${lastName.toLowerCase()}@example.com";
|
||||||
|
static String password() => "password123"; // Simple pour le test
|
||||||
|
|
||||||
|
static String dob({bool isUnborn = false}) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
if (isUnborn) {
|
||||||
|
final provisionalDate = now.add(Duration(days: _random.nextInt(180) + 30)); // Entre 1 et 7 mois dans le futur
|
||||||
|
return "${provisionalDate.day.toString().padLeft(2, '0')}/${provisionalDate.month.toString().padLeft(2, '0')}/${provisionalDate.year}";
|
||||||
|
} else {
|
||||||
|
final birthYear = now.year - _random.nextInt(3); // Enfants de 0 à 2 ans
|
||||||
|
final birthMonth = _random.nextInt(12) + 1;
|
||||||
|
final birthDay = _random.nextInt(28) + 1; // Simple, évite les pbs de jours/mois
|
||||||
|
return "${birthDay.toString().padLeft(2, '0')}/${birthMonth.toString().padLeft(2, '0')}/${birthYear}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool boolean() => _random.nextBool();
|
||||||
|
|
||||||
|
static String motivation() {
|
||||||
|
int count = _random.nextInt(3) + 2; // 2 à 4 phrases
|
||||||
|
List<String> chosenSnippets = [];
|
||||||
|
while(chosenSnippets.length < count) {
|
||||||
|
String snippet = _motivationSnippets[_random.nextInt(_motivationSnippets.length)];
|
||||||
|
if (!chosenSnippets.contains(snippet)) {
|
||||||
|
chosenSnippets.add(snippet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return chosenSnippets.join(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
62
frontend/lib/widgets/app_custom_checkbox.dart
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
|
class AppCustomCheckbox extends StatelessWidget {
|
||||||
|
final String label;
|
||||||
|
final bool value;
|
||||||
|
final ValueChanged<bool> onChanged;
|
||||||
|
final double checkboxSize;
|
||||||
|
final double checkmarkSizeFactor;
|
||||||
|
|
||||||
|
const AppCustomCheckbox({
|
||||||
|
super.key,
|
||||||
|
required this.label,
|
||||||
|
required this.value,
|
||||||
|
required this.onChanged,
|
||||||
|
this.checkboxSize = 20.0,
|
||||||
|
this.checkmarkSizeFactor = 1.4,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => onChanged(!value), // Inverse la valeur au clic
|
||||||
|
behavior: HitTestBehavior.opaque, // Pour s'assurer que toute la zone du Row est cliquable
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: checkboxSize,
|
||||||
|
height: checkboxSize,
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
Image.asset(
|
||||||
|
'assets/images/square.png',
|
||||||
|
height: checkboxSize,
|
||||||
|
width: checkboxSize,
|
||||||
|
),
|
||||||
|
if (value)
|
||||||
|
Image.asset(
|
||||||
|
'assets/images/coche.png',
|
||||||
|
height: checkboxSize * checkmarkSizeFactor,
|
||||||
|
width: checkboxSize * checkmarkSizeFactor,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
// Utiliser Flexible pour que le texte ne cause pas d'overflow si trop long
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: GoogleFonts.merienda(fontSize: 16),
|
||||||
|
overflow: TextOverflow.ellipsis, // Gérer le texte long
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
140
frontend/lib/widgets/custom_app_text_field.dart
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
|
// Définition de l'enum pour les styles de couleur/fond
|
||||||
|
enum CustomAppTextFieldStyle {
|
||||||
|
beige,
|
||||||
|
lavande,
|
||||||
|
jaune,
|
||||||
|
}
|
||||||
|
|
||||||
|
class CustomAppTextField extends StatefulWidget {
|
||||||
|
final TextEditingController controller;
|
||||||
|
final String labelText;
|
||||||
|
final String hintText;
|
||||||
|
final double fieldWidth;
|
||||||
|
final double fieldHeight;
|
||||||
|
final bool obscureText;
|
||||||
|
final TextInputType keyboardType;
|
||||||
|
final String? Function(String?)? validator;
|
||||||
|
final CustomAppTextFieldStyle style;
|
||||||
|
final bool isRequired;
|
||||||
|
final bool enabled;
|
||||||
|
final bool readOnly;
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
final IconData? suffixIcon;
|
||||||
|
final double labelFontSize;
|
||||||
|
final double inputFontSize;
|
||||||
|
|
||||||
|
const CustomAppTextField({
|
||||||
|
super.key,
|
||||||
|
required this.controller,
|
||||||
|
required this.labelText,
|
||||||
|
this.hintText = '',
|
||||||
|
this.fieldWidth = 300.0,
|
||||||
|
this.fieldHeight = 53.0,
|
||||||
|
this.obscureText = false,
|
||||||
|
this.keyboardType = TextInputType.text,
|
||||||
|
this.validator,
|
||||||
|
this.style = CustomAppTextFieldStyle.beige,
|
||||||
|
this.isRequired = false,
|
||||||
|
this.enabled = true,
|
||||||
|
this.readOnly = false,
|
||||||
|
this.onTap,
|
||||||
|
this.suffixIcon,
|
||||||
|
this.labelFontSize = 18.0,
|
||||||
|
this.inputFontSize = 18.0,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CustomAppTextField> createState() => _CustomAppTextFieldState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CustomAppTextFieldState extends State<CustomAppTextField> {
|
||||||
|
String getBackgroundImagePath() {
|
||||||
|
switch (widget.style) {
|
||||||
|
case CustomAppTextFieldStyle.lavande:
|
||||||
|
return 'assets/images/input_field_lavande.png';
|
||||||
|
case CustomAppTextFieldStyle.jaune:
|
||||||
|
return 'assets/images/input_field_jaune.png';
|
||||||
|
case CustomAppTextFieldStyle.beige:
|
||||||
|
default:
|
||||||
|
return 'assets/images/input_field_bg.png';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
const double fontHeightMultiplier = 1.2;
|
||||||
|
const double internalVerticalPadding = 16.0;
|
||||||
|
final double dynamicFieldHeight = widget.fieldHeight;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
widget.labelText,
|
||||||
|
style: GoogleFonts.merienda(
|
||||||
|
fontSize: widget.labelFontSize,
|
||||||
|
color: Colors.black87,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
SizedBox(
|
||||||
|
width: widget.fieldWidth,
|
||||||
|
height: dynamicFieldHeight,
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
children: [
|
||||||
|
Positioned.fill(
|
||||||
|
child: Image.asset(
|
||||||
|
getBackgroundImagePath(),
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 8.0),
|
||||||
|
child: TextFormField(
|
||||||
|
controller: widget.controller,
|
||||||
|
obscureText: widget.obscureText,
|
||||||
|
keyboardType: widget.keyboardType,
|
||||||
|
enabled: widget.enabled,
|
||||||
|
readOnly: widget.readOnly,
|
||||||
|
onTap: widget.onTap,
|
||||||
|
style: GoogleFonts.merienda(
|
||||||
|
fontSize: widget.inputFontSize,
|
||||||
|
color: widget.enabled ? Colors.black87 : Colors.grey
|
||||||
|
),
|
||||||
|
validator: widget.validator ??
|
||||||
|
(value) {
|
||||||
|
if (!widget.enabled || widget.readOnly) return null;
|
||||||
|
if (widget.isRequired && (value == null || value.isEmpty)) {
|
||||||
|
return 'Ce champ est obligatoire';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: widget.hintText,
|
||||||
|
hintStyle: GoogleFonts.merienda(fontSize: widget.inputFontSize, color: Colors.black54.withOpacity(0.7)),
|
||||||
|
border: InputBorder.none,
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
suffixIcon: widget.suffixIcon != null
|
||||||
|
? Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 0.0),
|
||||||
|
child: Icon(widget.suffixIcon, color: Colors.black54, size: widget.inputFontSize * 1.1),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
isDense: true,
|
||||||
|
),
|
||||||
|
textAlignVertical: TextAlignVertical.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
61
frontend/lib/widgets/custom_decorated_text_field.dart
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
|
class CustomDecoratedTextField extends StatelessWidget {
|
||||||
|
final TextEditingController controller;
|
||||||
|
final String hintText;
|
||||||
|
final int maxLines;
|
||||||
|
final double? fieldHeight; // Hauteur optionnelle pour le champ
|
||||||
|
final bool expandDynamically; // Nouvelle propriété
|
||||||
|
final bool readOnly;
|
||||||
|
final double fontSize;
|
||||||
|
|
||||||
|
const CustomDecoratedTextField({
|
||||||
|
super.key,
|
||||||
|
required this.controller,
|
||||||
|
this.hintText = 'Écrire votre texte ici...',
|
||||||
|
this.maxLines = 10, // Un nombre raisonnable de lignes par défaut si non dynamique
|
||||||
|
this.fieldHeight, // Si non fourni, la hauteur sera intrinsèque ou définie par l'image
|
||||||
|
this.expandDynamically = false, // Par défaut, non dynamique
|
||||||
|
this.readOnly = false,
|
||||||
|
this.fontSize = 15.0,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
height: fieldHeight, // Permet de forcer une hauteur si besoin
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
children: [
|
||||||
|
Image.asset(
|
||||||
|
'assets/images/square.png', // L'image de fond
|
||||||
|
fit: BoxFit.fill, // Pour remplir l'espace du Stack/SizedBox
|
||||||
|
width: double.infinity, // S'assurer qu'elle prend toute la largeur disponible
|
||||||
|
height: fieldHeight != null ? double.infinity : null, // Et toute la hauteur si fieldHeight est spécifié
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
// Ajouter un padding interne pour que le texte ne colle pas aux bords de l'image
|
||||||
|
padding: const EdgeInsets.only(top: 25.0, bottom: 15.0, left: 20.0, right: 20.0), // Augmentation de la marge supérieure
|
||||||
|
child: TextFormField(
|
||||||
|
controller: controller,
|
||||||
|
keyboardType: TextInputType.multiline,
|
||||||
|
maxLines: expandDynamically ? null : maxLines, // S'étend dynamiquement si expandDynamically est true
|
||||||
|
style: GoogleFonts.merienda(fontSize: fontSize, color: Colors.black87),
|
||||||
|
textAlignVertical: TextAlignVertical.top,
|
||||||
|
readOnly: readOnly,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: hintText,
|
||||||
|
hintStyle: GoogleFonts.merienda(fontSize: fontSize, color: Colors.black54.withOpacity(0.7)),
|
||||||
|
border: InputBorder.none, // Pas de bordure pour le TextFormField lui-même
|
||||||
|
contentPadding: EdgeInsets.zero, // Le padding est géré par le widget Padding externe
|
||||||
|
// Pour aligner le hintText en haut à gauche
|
||||||
|
alignLabelWithHint: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
88
frontend/lib/widgets/hover_relief_widget.dart
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class HoverReliefWidget extends StatefulWidget {
|
||||||
|
final Widget child;
|
||||||
|
final VoidCallback? onPressed;
|
||||||
|
final BorderRadius borderRadius;
|
||||||
|
final double initialElevation;
|
||||||
|
final double hoverElevation;
|
||||||
|
final double scaleFactor;
|
||||||
|
final bool enableHoverEffect; // Pour activer/désactiver l'effet de survol
|
||||||
|
final Color initialShadowColor; // Nouveau paramètre
|
||||||
|
final Color hoverShadowColor; // Nouveau paramètre
|
||||||
|
|
||||||
|
const HoverReliefWidget({
|
||||||
|
required this.child,
|
||||||
|
this.onPressed,
|
||||||
|
this.borderRadius = const BorderRadius.all(Radius.circular(15.0)),
|
||||||
|
this.initialElevation = 4.0,
|
||||||
|
this.hoverElevation = 8.0,
|
||||||
|
this.scaleFactor = 1.03, // Légèrement réduit par rapport à l'exemple précédent
|
||||||
|
this.enableHoverEffect = true, // Par défaut, l'effet est activé
|
||||||
|
this.initialShadowColor = const Color(0x26000000), // Default: Colors.black.withOpacity(0.15)
|
||||||
|
this.hoverShadowColor = const Color(0x4D000000), // Default: Colors.black.withOpacity(0.3)
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<HoverReliefWidget> createState() => _HoverReliefWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HoverReliefWidgetState extends State<HoverReliefWidget> {
|
||||||
|
bool _isHovering = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final bool canHover = widget.enableHoverEffect && widget.onPressed != null;
|
||||||
|
|
||||||
|
final hoverTransform = Matrix4.identity()..scale(widget.scaleFactor);
|
||||||
|
final transform = _isHovering && canHover ? hoverTransform : Matrix4.identity();
|
||||||
|
final shadowColor = _isHovering && canHover ? widget.hoverShadowColor : widget.initialShadowColor;
|
||||||
|
final elevation = _isHovering && canHover ? widget.hoverElevation : widget.initialElevation;
|
||||||
|
|
||||||
|
Widget content = AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
transform: transform,
|
||||||
|
transformAlignment: Alignment.center,
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
elevation: elevation,
|
||||||
|
shadowColor: shadowColor,
|
||||||
|
borderRadius: widget.borderRadius,
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: widget.child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (widget.onPressed == null) {
|
||||||
|
// Si non cliquable, on retourne juste le contenu avec l'élévation initiale (pas de survol)
|
||||||
|
// Ajustement: pour toujours avoir un Material de base même si non cliquable et sans hover.
|
||||||
|
return Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
elevation: widget.initialElevation, // Utilise l'élévation initiale
|
||||||
|
shadowColor: widget.initialShadowColor, // Appliqué ici pour l'état non cliquable
|
||||||
|
borderRadius: widget.borderRadius,
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: widget.child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return MouseRegion(
|
||||||
|
onEnter: (_) {
|
||||||
|
if (widget.enableHoverEffect) setState(() => _isHovering = true);
|
||||||
|
},
|
||||||
|
onExit: (_) {
|
||||||
|
if (widget.enableHoverEffect) setState(() => _isHovering = false);
|
||||||
|
},
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: widget.onPressed,
|
||||||
|
borderRadius: widget.borderRadius,
|
||||||
|
hoverColor: Colors.transparent,
|
||||||
|
splashColor: Colors.grey.withOpacity(0.2),
|
||||||
|
highlightColor: Colors.grey.withOpacity(0.1),
|
||||||
|
child: content,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
53
frontend/lib/widgets/image_button.dart
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
|
class ImageButton extends StatelessWidget {
|
||||||
|
final String bg;
|
||||||
|
final double width;
|
||||||
|
final double height;
|
||||||
|
final String text;
|
||||||
|
final Color textColor;
|
||||||
|
final VoidCallback onPressed;
|
||||||
|
final double fontSize; // Ajout pour la flexibilité
|
||||||
|
|
||||||
|
const ImageButton({
|
||||||
|
super.key,
|
||||||
|
required this.bg,
|
||||||
|
required this.width,
|
||||||
|
required this.height,
|
||||||
|
required this.text,
|
||||||
|
required this.textColor,
|
||||||
|
required this.onPressed,
|
||||||
|
this.fontSize = 16, // Valeur par défaut
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MouseRegion(
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: onPressed,
|
||||||
|
child: Container(
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
image: DecorationImage(
|
||||||
|
image: AssetImage(bg),
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: GoogleFonts.merienda(
|
||||||
|
color: textColor,
|
||||||
|
fontSize: fontSize, // Utilisation du paramètre
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
40
frontend/public/ptitspas-login/login.html
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<!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>
|
||||||
133
frontend/public/ptitspas-login/styles.css
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
639
frontend/pubspec.lock
Normal file
@ -0,0 +1,639 @@
|
|||||||
|
# Generated by pub
|
||||||
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
|
packages:
|
||||||
|
async:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: async
|
||||||
|
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.12.0"
|
||||||
|
boolean_selector:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: boolean_selector
|
||||||
|
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.2"
|
||||||
|
characters:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: characters
|
||||||
|
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.0"
|
||||||
|
clock:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: clock
|
||||||
|
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.2"
|
||||||
|
collection:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: collection
|
||||||
|
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.19.1"
|
||||||
|
cross_file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cross_file
|
||||||
|
sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.4+2"
|
||||||
|
crypto:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: crypto
|
||||||
|
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.6"
|
||||||
|
fake_async:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: fake_async
|
||||||
|
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.2"
|
||||||
|
ffi:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: ffi
|
||||||
|
sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.4"
|
||||||
|
file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file
|
||||||
|
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.1"
|
||||||
|
file_selector_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_linux
|
||||||
|
sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.3+2"
|
||||||
|
file_selector_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_macos
|
||||||
|
sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.4+2"
|
||||||
|
file_selector_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_platform_interface
|
||||||
|
sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.6.2"
|
||||||
|
file_selector_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_windows
|
||||||
|
sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.3+4"
|
||||||
|
flutter:
|
||||||
|
dependency: "direct main"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
flutter_lints:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: flutter_lints
|
||||||
|
sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.3"
|
||||||
|
flutter_localizations:
|
||||||
|
dependency: "direct main"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
flutter_plugin_android_lifecycle:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_plugin_android_lifecycle
|
||||||
|
sha256: f948e346c12f8d5480d2825e03de228d0eb8c3a737e4cdaa122267b89c022b5e
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.28"
|
||||||
|
flutter_test:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
flutter_web_plugins:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
go_router:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: go_router
|
||||||
|
sha256: b465e99ce64ba75e61c8c0ce3d87b66d8ac07f0b35d0a7e0263fcfc10f99e836
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "13.2.5"
|
||||||
|
google_fonts:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: google_fonts
|
||||||
|
sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.2.1"
|
||||||
|
http:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: http
|
||||||
|
sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.0"
|
||||||
|
http_parser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: http_parser
|
||||||
|
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.2"
|
||||||
|
image_picker:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: image_picker
|
||||||
|
sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.2"
|
||||||
|
image_picker_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_android
|
||||||
|
sha256: "317a5d961cec5b34e777b9252393f2afbd23084aa6e60fcf601dcf6341b9ebeb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.8.12+23"
|
||||||
|
image_picker_for_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_for_web
|
||||||
|
sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.6"
|
||||||
|
image_picker_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_ios
|
||||||
|
sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.8.12+2"
|
||||||
|
image_picker_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_linux
|
||||||
|
sha256: "34a65f6740df08bbbeb0a1abd8e6d32107941fd4868f67a507b25601651022c9"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.1+2"
|
||||||
|
image_picker_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_macos
|
||||||
|
sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.1+2"
|
||||||
|
image_picker_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_platform_interface
|
||||||
|
sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.10.1"
|
||||||
|
image_picker_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_windows
|
||||||
|
sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.1+1"
|
||||||
|
intl:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: intl
|
||||||
|
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.19.0"
|
||||||
|
js:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: js
|
||||||
|
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.6.7"
|
||||||
|
leak_tracker:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: leak_tracker
|
||||||
|
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "10.0.8"
|
||||||
|
leak_tracker_flutter_testing:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: leak_tracker_flutter_testing
|
||||||
|
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.9"
|
||||||
|
leak_tracker_testing:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: leak_tracker_testing
|
||||||
|
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.1"
|
||||||
|
lints:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: lints
|
||||||
|
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.1"
|
||||||
|
logging:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: logging
|
||||||
|
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.0"
|
||||||
|
matcher:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: matcher
|
||||||
|
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.12.17"
|
||||||
|
material_color_utilities:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: material_color_utilities
|
||||||
|
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.11.1"
|
||||||
|
meta:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: meta
|
||||||
|
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.16.0"
|
||||||
|
mime:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: mime
|
||||||
|
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.0"
|
||||||
|
nested:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: nested
|
||||||
|
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
|
path:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path
|
||||||
|
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.9.1"
|
||||||
|
path_provider:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider
|
||||||
|
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.5"
|
||||||
|
path_provider_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_android
|
||||||
|
sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.17"
|
||||||
|
path_provider_foundation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_foundation
|
||||||
|
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
|
path_provider_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_linux
|
||||||
|
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.1"
|
||||||
|
path_provider_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_platform_interface
|
||||||
|
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.2"
|
||||||
|
path_provider_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_windows
|
||||||
|
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.0"
|
||||||
|
platform:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: platform
|
||||||
|
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.6"
|
||||||
|
plugin_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: plugin_platform_interface
|
||||||
|
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.8"
|
||||||
|
provider:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: provider
|
||||||
|
sha256: "489024f942069c2920c844ee18bb3d467c69e48955a4f32d1677f71be103e310"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.1.4"
|
||||||
|
shared_preferences:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: shared_preferences
|
||||||
|
sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.3"
|
||||||
|
shared_preferences_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_android
|
||||||
|
sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.10"
|
||||||
|
shared_preferences_foundation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_foundation
|
||||||
|
sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.4"
|
||||||
|
shared_preferences_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_linux
|
||||||
|
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
|
shared_preferences_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_platform_interface
|
||||||
|
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
|
shared_preferences_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_web
|
||||||
|
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.3"
|
||||||
|
shared_preferences_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_windows
|
||||||
|
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
|
sky_engine:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
source_span:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: source_span
|
||||||
|
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.10.1"
|
||||||
|
stack_trace:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stack_trace
|
||||||
|
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.12.1"
|
||||||
|
stream_channel:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stream_channel
|
||||||
|
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.4"
|
||||||
|
string_scanner:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: string_scanner
|
||||||
|
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.1"
|
||||||
|
term_glyph:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: term_glyph
|
||||||
|
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.2"
|
||||||
|
test_api:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: test_api
|
||||||
|
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.4"
|
||||||
|
typed_data:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: typed_data
|
||||||
|
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.0"
|
||||||
|
url_launcher:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: url_launcher
|
||||||
|
sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.1"
|
||||||
|
url_launcher_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_android
|
||||||
|
sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.16"
|
||||||
|
url_launcher_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_ios
|
||||||
|
sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.3"
|
||||||
|
url_launcher_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_linux
|
||||||
|
sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.1"
|
||||||
|
url_launcher_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_macos
|
||||||
|
sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.2"
|
||||||
|
url_launcher_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_platform_interface
|
||||||
|
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.2"
|
||||||
|
url_launcher_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_web
|
||||||
|
sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
|
url_launcher_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_windows
|
||||||
|
sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.4"
|
||||||
|
vector_math:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vector_math
|
||||||
|
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.4"
|
||||||
|
vm_service:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vm_service
|
||||||
|
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "14.3.1"
|
||||||
|
web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: web
|
||||||
|
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
|
xdg_directories:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: xdg_directories
|
||||||
|
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
|
sdks:
|
||||||
|
dart: ">=3.7.0 <4.0.0"
|
||||||
|
flutter: ">=3.27.0"
|
||||||
39
frontend/pubspec.yaml
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
name: p_tits_pas
|
||||||
|
description: Application de gestion de la garde d'enfants pour les collectivités locales.
|
||||||
|
publish_to: 'none'
|
||||||
|
version: 0.1.0
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: '>=3.2.6 <4.0.0'
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
flutter_localizations:
|
||||||
|
sdk: flutter
|
||||||
|
provider: ^6.1.1
|
||||||
|
go_router: ^13.2.5
|
||||||
|
google_fonts: ^6.1.0
|
||||||
|
shared_preferences: ^2.2.2
|
||||||
|
image_picker: ^1.0.7
|
||||||
|
js: ^0.6.7
|
||||||
|
url_launcher: ^6.2.4
|
||||||
|
http: ^1.2.0
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
flutter_lints: ^2.0.0
|
||||||
|
|
||||||
|
flutter:
|
||||||
|
uses-material-design: true
|
||||||
|
|
||||||
|
assets:
|
||||||
|
- assets/images/ # Déclarer le dossier entier
|
||||||
|
- assets/cards/ # Nouveau dossier de cartes
|
||||||
|
|
||||||
|
fonts:
|
||||||
|
- family: Merienda
|
||||||
|
fonts:
|
||||||
|
- asset: assets/fonts/Merienda-VariableFont_wght.ttf
|
||||||
|
style: normal
|
||||||
30
frontend/test/widget_test.dart
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// This is a basic Flutter widget test.
|
||||||
|
//
|
||||||
|
// To perform an interaction with a widget in your test, use the WidgetTester
|
||||||
|
// utility in the flutter_test package. For example, you can send tap and scroll
|
||||||
|
// gestures. You can also use WidgetTester to find child widgets in the widget
|
||||||
|
// tree, read text, and verify that the values of widget properties are correct.
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import 'package:petitspas/main.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
||||||
|
// Build our app and trigger a frame.
|
||||||
|
await tester.pumpWidget(const MyApp());
|
||||||
|
|
||||||
|
// Verify that our counter starts at 0.
|
||||||
|
expect(find.text('0'), findsOneWidget);
|
||||||
|
expect(find.text('1'), findsNothing);
|
||||||
|
|
||||||
|
// Tap the '+' icon and trigger a frame.
|
||||||
|
await tester.tap(find.byIcon(Icons.add));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// Verify that our counter has incremented.
|
||||||
|
expect(find.text('0'), findsNothing);
|
||||||
|
expect(find.text('1'), findsOneWidget);
|
||||||
|
});
|
||||||
|
}
|
||||||
BIN
frontend/web/favicon.png
Normal file
|
After Width: | Height: | Size: 917 B |
BIN
frontend/web/icons/Icon-192.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
frontend/web/icons/Icon-512.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
frontend/web/icons/Icon-maskable-192.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
frontend/web/icons/Icon-maskable-512.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
66
frontend/web/index.html
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<!--
|
||||||
|
If you are serving your web app in a path other than the root, change the
|
||||||
|
href value below to reflect the base path you are serving from.
|
||||||
|
|
||||||
|
The path provided below has to start and end with a slash "/" in order for
|
||||||
|
it to work correctly.
|
||||||
|
|
||||||
|
For more details:
|
||||||
|
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
|
||||||
|
|
||||||
|
This is a placeholder for base href that will be replaced by the value of
|
||||||
|
the `--base-href` argument provided to `flutter build`.
|
||||||
|
-->
|
||||||
|
<base href="$FLUTTER_BASE_HREF">
|
||||||
|
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||||
|
<meta name="description" content="Application de gestion de la garde d'enfants pour les collectivités locales.">
|
||||||
|
|
||||||
|
<!-- iOS meta tags & icons -->
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||||
|
<meta name="apple-mobile-web-app-title" content="P'titsPas">
|
||||||
|
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||||
|
|
||||||
|
<!-- Favicon -->
|
||||||
|
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||||
|
|
||||||
|
<title>P'titsPas</title>
|
||||||
|
<link rel="manifest" href="manifest.json">
|
||||||
|
|
||||||
|
<!-- Suppression des dépendances image_cropper web -->
|
||||||
|
<!--
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.12/cropper.min.js"></script>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.12/cropper.min.css">
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// The value below is injected by flutter build, do not touch.
|
||||||
|
const serviceWorkerVersion = "{{flutter_service_worker_version}}";
|
||||||
|
</script>
|
||||||
|
<!-- This script adds the flutter initialization JS code -->
|
||||||
|
<script src="flutter.js" defer></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script>
|
||||||
|
window.addEventListener('load', function(ev) {
|
||||||
|
// Download main.dart.js
|
||||||
|
_flutter.loader.loadEntrypoint({
|
||||||
|
serviceWorker: {
|
||||||
|
serviceWorkerVersion: serviceWorkerVersion,
|
||||||
|
},
|
||||||
|
onEntrypointLoaded: function(engineInitializer) {
|
||||||
|
engineInitializer.initializeEngine().then(function(appRunner) {
|
||||||
|
appRunner.runApp();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
35
frontend/web/manifest.json
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "P'titsPas",
|
||||||
|
"short_name": "P'titsPas",
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#FFFEF9",
|
||||||
|
"theme_color": "#8AD0C8",
|
||||||
|
"description": "P'titsPas - Grandir pas à pas, sereinement",
|
||||||
|
"orientation": "portrait-primary",
|
||||||
|
"prefer_related_applications": false,
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "assets/images/icon.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "assets/images/icon.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "icons/Icon-maskable-192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "icons/Icon-maskable-512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
17
frontend/windows/.gitignore
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
flutter/ephemeral/
|
||||||
|
|
||||||
|
# Visual Studio user-specific files.
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
|
||||||
|
# Visual Studio build-related files.
|
||||||
|
x64/
|
||||||
|
x86/
|
||||||
|
|
||||||
|
# Visual Studio cache files
|
||||||
|
# files ending in .cache can be ignored
|
||||||
|
*.[Cc]ache
|
||||||
|
# but keep track of directories ending in .cache
|
||||||
|
!*.[Cc]ache/
|
||||||