Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
979114b93d | ||
|
|
ee940e25b7 | ||
|
|
10a5cb1fed | ||
|
|
bd2139b3aa | ||
|
|
7d97de3086 | ||
|
|
1e9803a4a7 | ||
|
|
74c56b900e | ||
|
|
4caec0a104 | ||
|
|
bfafbb955b | ||
|
|
332f4be7d7 | ||
|
|
01a7937004 | ||
|
|
9418932791 | ||
|
|
acda4244a9 | ||
|
|
14efccc711 | ||
|
|
916d83755f |
@@ -0,0 +1,4 @@
|
|||||||
|
# Configuration du Frontend en développement local
|
||||||
|
|
||||||
|
# URL de l'API backend (doit correspondre au backend lancé localement)
|
||||||
|
API_URL=http://localhost:3000/api
|
||||||
@@ -52,3 +52,4 @@ Xcf/**
|
|||||||
# Release notes
|
# Release notes
|
||||||
CHANGELOG.md
|
CHANGELOG.md
|
||||||
Ressources/
|
Ressources/
|
||||||
|
.env
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# 🎨 Guide de développement Frontend
|
||||||
|
|
||||||
|
## Prérequis
|
||||||
|
- Docker et Docker Compose installés
|
||||||
|
- Le backend doit être démarré (voir README-DEV du backend)
|
||||||
|
|
||||||
|
## 🏃♂️ Démarrage rapide
|
||||||
|
|
||||||
|
### 1. Cloner le projet
|
||||||
|
```bash
|
||||||
|
git clone <url-du-depot-frontend>
|
||||||
|
cd ptitspas-frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configuration
|
||||||
|
```bash
|
||||||
|
# Copier le fichier d'exemple
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Lancer le frontend
|
||||||
|
```bash
|
||||||
|
# Démarrer le frontend (le backend doit être déjà lancé)
|
||||||
|
docker compose -f docker-compose.dev.yml up -d
|
||||||
|
|
||||||
|
# Voir les logs
|
||||||
|
docker compose -f docker-compose.dev.yml logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🌐 Accès
|
||||||
|
|
||||||
|
- **Frontend** : http://localhost:8000
|
||||||
|
|
||||||
|
## 📋 Workflow de développement complet
|
||||||
|
|
||||||
|
1. **Démarrer le backend** (dans le dépôt backend) :
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.dev.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Démarrer le frontend** (dans ce dépôt) :
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.dev.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Accéder aux services** :
|
||||||
|
- Frontend : http://localhost:8000
|
||||||
|
- Backend API : http://localhost:3000/api
|
||||||
|
- PgAdmin : http://localhost:8080
|
||||||
|
|
||||||
|
## 🛠️ Commandes utiles
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Arrêter le frontend
|
||||||
|
docker compose -f docker-compose.dev.yml down
|
||||||
|
|
||||||
|
# Rebuild après modification
|
||||||
|
docker compose -f docker-compose.dev.yml up --build
|
||||||
|
|
||||||
|
# Voir l'état
|
||||||
|
docker compose -f docker-compose.dev.yml ps
|
||||||
|
```
|
||||||
Generated
-3320
File diff suppressed because it is too large
Load Diff
@@ -1,36 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
// This is your Prisma schema file,
|
|
||||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
|
||||||
|
|
||||||
generator client {
|
|
||||||
provider = "prisma-client-js"
|
|
||||||
}
|
|
||||||
|
|
||||||
datasource db {
|
|
||||||
provider = "postgresql"
|
|
||||||
url = env("DATABASE_URL")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Modèle pour les parents
|
|
||||||
model Parent {
|
|
||||||
id String @id @default(uuid())
|
|
||||||
email String @unique
|
|
||||||
password String
|
|
||||||
firstName String
|
|
||||||
lastName String
|
|
||||||
phoneNumber String?
|
|
||||||
address String?
|
|
||||||
status AccountStatus @default(PENDING)
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
children Child[]
|
|
||||||
contracts Contract[]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Modèle pour les enfants
|
|
||||||
model Child {
|
|
||||||
id String @id @default(uuid())
|
|
||||||
firstName String
|
|
||||||
dateOfBirth DateTime
|
|
||||||
photoUrl String?
|
|
||||||
photoConsent Boolean @default(false)
|
|
||||||
isMultiple Boolean @default(false)
|
|
||||||
isUnborn Boolean @default(false)
|
|
||||||
parentId String
|
|
||||||
parent Parent @relation(fields: [parentId], references: [id])
|
|
||||||
contracts Contract[]
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
}
|
|
||||||
|
|
||||||
// Modèle pour les contrats
|
|
||||||
model Contract {
|
|
||||||
id String @id @default(uuid())
|
|
||||||
parentId String
|
|
||||||
childId String
|
|
||||||
startDate DateTime
|
|
||||||
endDate DateTime?
|
|
||||||
status ContractStatus @default(ACTIVE)
|
|
||||||
parent Parent @relation(fields: [parentId], references: [id])
|
|
||||||
child Child @relation(fields: [childId], references: [id])
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
}
|
|
||||||
|
|
||||||
// Modèle pour les thèmes
|
|
||||||
model Theme {
|
|
||||||
id String @id @default(uuid())
|
|
||||||
name String @unique
|
|
||||||
primaryColor String
|
|
||||||
secondaryColor String
|
|
||||||
backgroundColor String
|
|
||||||
textColor String
|
|
||||||
isActive Boolean @default(false)
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
appSettings AppSettings[]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Modèle pour les paramètres de l'application
|
|
||||||
model AppSettings {
|
|
||||||
id String @id @default(uuid())
|
|
||||||
currentThemeId String
|
|
||||||
currentTheme Theme @relation(fields: [currentThemeId], references: [id])
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
|
|
||||||
@@index([currentThemeId])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Modèle pour les administrateurs
|
|
||||||
model Admin {
|
|
||||||
id String @id @default(uuid())
|
|
||||||
email String @unique
|
|
||||||
password String
|
|
||||||
firstName String
|
|
||||||
lastName String
|
|
||||||
passwordChanged Boolean @default(false)
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enums
|
|
||||||
enum AccountStatus {
|
|
||||||
PENDING
|
|
||||||
VALIDATED
|
|
||||||
REJECTED
|
|
||||||
SUSPENDED
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ContractStatus {
|
|
||||||
ACTIVE
|
|
||||||
ENDED
|
|
||||||
CANCELLED
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { Controller, Post, Body, UseGuards, Req } from '@nestjs/common';
|
|
||||||
import { AdminService } from './admin.service';
|
|
||||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
|
||||||
|
|
||||||
@Controller('admin')
|
|
||||||
export class AdminController {
|
|
||||||
constructor(private readonly adminService: AdminService) {}
|
|
||||||
|
|
||||||
@Post('change-password')
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
async changePassword(
|
|
||||||
@Req() req,
|
|
||||||
@Body('oldPassword') oldPassword: string,
|
|
||||||
@Body('newPassword') newPassword: string,
|
|
||||||
) {
|
|
||||||
return this.adminService.changePassword(req.user.id, oldPassword, newPassword);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { AdminController } from './admin.controller';
|
|
||||||
import { AdminService } from './admin.service';
|
|
||||||
import { PrismaModule } from '../prisma/prisma.module';
|
|
||||||
import { JwtModule } from '@nestjs/jwt';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [
|
|
||||||
PrismaModule,
|
|
||||||
JwtModule.register({
|
|
||||||
secret: process.env.JWT_SECRET,
|
|
||||||
signOptions: { expiresIn: '1d' },
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
controllers: [AdminController],
|
|
||||||
providers: [AdminService],
|
|
||||||
})
|
|
||||||
export class AdminModule {}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
|
||||||
import { JwtService } from '@nestjs/jwt';
|
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
|
||||||
import * as bcrypt from 'bcrypt';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AdminService {
|
|
||||||
constructor(
|
|
||||||
private prisma: PrismaService,
|
|
||||||
private jwtService: JwtService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async changePassword(adminId: string, oldPassword: string, newPassword: string) {
|
|
||||||
// Récupérer l'administrateur
|
|
||||||
const admin = await this.prisma.admin.findUnique({
|
|
||||||
where: { id: adminId },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!admin) {
|
|
||||||
throw new UnauthorizedException('Administrateur non trouvé');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier l'ancien mot de passe
|
|
||||||
const isPasswordValid = await bcrypt.compare(oldPassword, admin.password);
|
|
||||||
if (!isPasswordValid) {
|
|
||||||
throw new UnauthorizedException('Ancien mot de passe incorrect');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hasher le nouveau mot de passe
|
|
||||||
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
|
||||||
|
|
||||||
// Mettre à jour le mot de passe
|
|
||||||
await this.prisma.admin.update({
|
|
||||||
where: { id: adminId },
|
|
||||||
data: { password: hashedPassword },
|
|
||||||
});
|
|
||||||
|
|
||||||
return { message: 'Mot de passe modifié avec succès' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
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 {}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import { Request, Response } from 'express';
|
|
||||||
import { ThemeService, ThemeData } from '../services/theme.service';
|
|
||||||
|
|
||||||
export class ThemeController {
|
|
||||||
// Créer un nouveau thème
|
|
||||||
static async createTheme(req: Request, res: Response) {
|
|
||||||
try {
|
|
||||||
const themeData: ThemeData = req.body;
|
|
||||||
const theme = await ThemeService.createTheme(themeData);
|
|
||||||
res.status(201).json(theme);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: 'Erreur lors de la création du thème' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Récupérer tous les thèmes
|
|
||||||
static async getAllThemes(req: Request, res: Response) {
|
|
||||||
try {
|
|
||||||
const themes = await ThemeService.getAllThemes();
|
|
||||||
res.json(themes);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: 'Erreur lors de la récupération des thèmes' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Récupérer le thème actif
|
|
||||||
static async getActiveTheme(req: Request, res: Response) {
|
|
||||||
try {
|
|
||||||
const theme = await ThemeService.getActiveTheme();
|
|
||||||
if (!theme) {
|
|
||||||
return res.status(404).json({ error: 'Aucun thème actif trouvé' });
|
|
||||||
}
|
|
||||||
res.json(theme);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: 'Erreur lors de la récupération du thème actif' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Activer un thème
|
|
||||||
static async activateTheme(req: Request, res: Response) {
|
|
||||||
try {
|
|
||||||
const { themeId } = req.params;
|
|
||||||
const theme = await ThemeService.activateTheme(themeId);
|
|
||||||
res.json(theme);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: 'Erreur lors de l\'activation du thème' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mettre à jour un thème
|
|
||||||
static async updateTheme(req: Request, res: Response) {
|
|
||||||
try {
|
|
||||||
const { themeId } = req.params;
|
|
||||||
const themeData: Partial<ThemeData> = req.body;
|
|
||||||
const theme = await ThemeService.updateTheme(themeId, themeData);
|
|
||||||
res.json(theme);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: 'Erreur lors de la mise à jour du thème' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Supprimer un thème
|
|
||||||
static async deleteTheme(req: Request, res: Response) {
|
|
||||||
try {
|
|
||||||
const { themeId } = req.params;
|
|
||||||
await ThemeService.deleteTheme(themeId);
|
|
||||||
res.status(204).send();
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: 'Erreur lors de la suppression du thème' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
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}`);
|
|
||||||
});
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
import { Router } from 'express';
|
|
||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
import * as bcrypt from 'bcrypt';
|
|
||||||
import jwt from 'jsonwebtoken';
|
|
||||||
|
|
||||||
const router = Router();
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
|
|
||||||
|
|
||||||
// Route de connexion
|
|
||||||
router.post('/login', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { email, password } = req.body;
|
|
||||||
|
|
||||||
// Vérifier les identifiants
|
|
||||||
const admin = await prisma.admin.findUnique({
|
|
||||||
where: { email }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!admin) {
|
|
||||||
return res.status(401).json({ error: 'Identifiants invalides' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier le mot de passe
|
|
||||||
const validPassword = await bcrypt.compare(password, admin.password);
|
|
||||||
if (!validPassword) {
|
|
||||||
return res.status(401).json({ error: 'Identifiants invalides' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier si le mot de passe doit être changé
|
|
||||||
if (!admin.passwordChanged) {
|
|
||||||
return res.status(403).json({
|
|
||||||
error: 'Changement de mot de passe requis',
|
|
||||||
requiresPasswordChange: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Générer le token JWT
|
|
||||||
const token = jwt.sign(
|
|
||||||
{
|
|
||||||
id: admin.id,
|
|
||||||
email: admin.email,
|
|
||||||
role: 'admin'
|
|
||||||
},
|
|
||||||
JWT_SECRET,
|
|
||||||
{ expiresIn: '24h' }
|
|
||||||
);
|
|
||||||
|
|
||||||
res.json({ token });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Erreur lors de la connexion:', error);
|
|
||||||
res.status(500).json({ error: 'Erreur serveur' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Route de changement de mot de passe
|
|
||||||
router.post('/change-password', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { email, currentPassword, newPassword } = req.body;
|
|
||||||
|
|
||||||
// Vérifier l'administrateur
|
|
||||||
const admin = await prisma.admin.findUnique({
|
|
||||||
where: { email }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!admin) {
|
|
||||||
return res.status(404).json({ error: 'Administrateur non trouvé' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier l'ancien mot de passe
|
|
||||||
const validPassword = await bcrypt.compare(currentPassword, admin.password);
|
|
||||||
if (!validPassword) {
|
|
||||||
return res.status(401).json({ error: 'Mot de passe actuel incorrect' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hasher le nouveau mot de passe
|
|
||||||
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
|
||||||
|
|
||||||
// Mettre à jour le mot de passe
|
|
||||||
await prisma.admin.update({
|
|
||||||
where: { id: admin.id },
|
|
||||||
data: {
|
|
||||||
password: hashedPassword,
|
|
||||||
passwordChanged: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
res.json({ message: 'Mot de passe changé avec succès' });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Erreur lors du changement de mot de passe:', error);
|
|
||||||
res.status(500).json({ error: 'Erreur serveur' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { Router } from 'express';
|
|
||||||
import { ThemeController } from '../controllers/theme.controller';
|
|
||||||
|
|
||||||
const router = Router();
|
|
||||||
|
|
||||||
// Routes pour les thèmes
|
|
||||||
router.post('/', ThemeController.createTheme);
|
|
||||||
router.get('/', ThemeController.getAllThemes);
|
|
||||||
router.get('/active', ThemeController.getActiveTheme);
|
|
||||||
router.put('/:themeId/activate', ThemeController.activateTheme);
|
|
||||||
router.put('/:themeId', ThemeController.updateTheme);
|
|
||||||
router.delete('/:themeId', ThemeController.deleteTheme);
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
import * as bcrypt from 'bcrypt';
|
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
try {
|
|
||||||
// Vérifier si l'administrateur existe déjà
|
|
||||||
const existingAdmin = await prisma.admin.findUnique({
|
|
||||||
where: { email: 'administrateur@ptitspas.fr' }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!existingAdmin) {
|
|
||||||
// Hasher le mot de passe
|
|
||||||
const hashedPassword = await bcrypt.hash('password', 10);
|
|
||||||
|
|
||||||
// Créer l'administrateur
|
|
||||||
await prisma.admin.create({
|
|
||||||
data: {
|
|
||||||
email: 'administrateur@ptitspas.fr',
|
|
||||||
password: hashedPassword,
|
|
||||||
firstName: 'Administrateur',
|
|
||||||
lastName: 'P\'titsPas',
|
|
||||||
passwordChanged: false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('✅ Administrateur créé avec succès');
|
|
||||||
} else {
|
|
||||||
console.log('ℹ️ L\'administrateur existe déjà');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Erreur lors de la création de l\'administrateur:', error);
|
|
||||||
} finally {
|
|
||||||
await prisma.$disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
export interface ThemeData {
|
|
||||||
name: string;
|
|
||||||
primaryColor: string;
|
|
||||||
secondaryColor: string;
|
|
||||||
backgroundColor: string;
|
|
||||||
textColor: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ThemeService {
|
|
||||||
// Créer un nouveau thème
|
|
||||||
static async createTheme(data: ThemeData) {
|
|
||||||
return prisma.theme.create({
|
|
||||||
data: {
|
|
||||||
...data,
|
|
||||||
isActive: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Récupérer tous les thèmes
|
|
||||||
static async getAllThemes() {
|
|
||||||
return prisma.theme.findMany();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Récupérer le thème actif
|
|
||||||
static async getActiveTheme() {
|
|
||||||
const settings = await prisma.appSettings.findFirst({
|
|
||||||
include: {
|
|
||||||
currentTheme: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return settings?.currentTheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Activer un thème
|
|
||||||
static async activateTheme(themeId: string) {
|
|
||||||
// Désactiver tous les thèmes
|
|
||||||
await prisma.theme.updateMany({
|
|
||||||
where: { isActive: true },
|
|
||||||
data: { isActive: false },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Activer le thème sélectionné
|
|
||||||
const updatedTheme = await prisma.theme.update({
|
|
||||||
where: { id: themeId },
|
|
||||||
data: { isActive: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mettre à jour les paramètres de l'application
|
|
||||||
await prisma.appSettings.upsert({
|
|
||||||
where: { id: '1' },
|
|
||||||
update: { currentThemeId: themeId },
|
|
||||||
create: { id: '1', currentThemeId: themeId },
|
|
||||||
});
|
|
||||||
|
|
||||||
return updatedTheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mettre à jour un thème
|
|
||||||
static async updateTheme(themeId: string, data: Partial<ThemeData>) {
|
|
||||||
return prisma.theme.update({
|
|
||||||
where: { id: themeId },
|
|
||||||
data,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Supprimer un thème
|
|
||||||
static async deleteTheme(themeId: string) {
|
|
||||||
return prisma.theme.delete({
|
|
||||||
where: { id: themeId },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"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"]
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Docker Compose pour développement local du Frontend
|
||||||
|
# Usage: docker compose -f docker-compose.dev.yml up -d
|
||||||
|
|
||||||
|
services:
|
||||||
|
# Frontend Flutter
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: ./frontend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: ptitspas-frontend-dev
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
API_URL: ${API_URL:-http://localhost:3000/api}
|
||||||
|
ports:
|
||||||
|
- "8000:80"
|
||||||
|
networks:
|
||||||
|
- ptitspas_dev
|
||||||
|
|
||||||
|
networks:
|
||||||
|
ptitspas_dev:
|
||||||
|
driver: bridge
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Stage builder
|
||||||
|
FROM ghcr.io/cirruslabs/flutter:3.19.0 AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY pubspec.* ./
|
||||||
|
RUN flutter pub get
|
||||||
|
COPY . .
|
||||||
|
RUN flutter build web --release
|
||||||
|
|
||||||
|
# Stage production
|
||||||
|
FROM nginx:alpine
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY --from=builder /app/build/web /usr/share/nginx/html
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
class Env {
|
||||||
|
// Base URL de l'API, surchargeable à la compilation via --dart-define=API_BASE_URL
|
||||||
|
static const String apiBaseUrl = String.fromEnvironment(
|
||||||
|
'API_BASE_URL',
|
||||||
|
defaultValue: 'https://ynov.ptits-pas.fr',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Construit une URL vers l'API v1 à partir d'un chemin (commençant par '/')
|
||||||
|
static String apiV1(String path) => "${apiBaseUrl}/api/v1$path";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
class ChildminderId {
|
||||||
|
String firstName;
|
||||||
|
String lastName;
|
||||||
|
String address;
|
||||||
|
String postalCode;
|
||||||
|
String city;
|
||||||
|
String phone;
|
||||||
|
String email;
|
||||||
|
String password;
|
||||||
|
File? profilePicture;
|
||||||
|
bool photoConsent;
|
||||||
|
|
||||||
|
ChildminderId({
|
||||||
|
this.firstName = '',
|
||||||
|
this.lastName = '',
|
||||||
|
this.address = '',
|
||||||
|
this.postalCode = '',
|
||||||
|
this.city = '',
|
||||||
|
this.phone = '',
|
||||||
|
this.email = '',
|
||||||
|
this.password = '',
|
||||||
|
this.profilePicture,
|
||||||
|
this.photoConsent = false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChildminderProfessional {
|
||||||
|
String dateOfBirth;
|
||||||
|
String birthCity;
|
||||||
|
String birthCountry;
|
||||||
|
String socialSecurityNumber; // NIR
|
||||||
|
String agreementNumber;
|
||||||
|
int maxChildren;
|
||||||
|
|
||||||
|
ChildminderProfessional({
|
||||||
|
this.dateOfBirth = '',
|
||||||
|
this.birthCity = '',
|
||||||
|
this.birthCountry = '',
|
||||||
|
this.socialSecurityNumber = '',
|
||||||
|
this.agreementNumber = '',
|
||||||
|
this.maxChildren = 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChildminderRegistrationData {
|
||||||
|
ChildminderId identity;
|
||||||
|
ChildminderProfessional professional;
|
||||||
|
String presentationMessage;
|
||||||
|
bool cguAccepted;
|
||||||
|
bool isPhotoRequired;
|
||||||
|
|
||||||
|
ChildminderRegistrationData({
|
||||||
|
ChildminderId? identityData,
|
||||||
|
ChildminderProfessional? professionalData,
|
||||||
|
this.presentationMessage = '',
|
||||||
|
this.cguAccepted = false,
|
||||||
|
this.isPhotoRequired = false,
|
||||||
|
}) : identity = identityData ?? ChildminderId(),
|
||||||
|
professional = professionalData ?? ChildminderProfessional();
|
||||||
|
|
||||||
|
void updateIdentity(ChildminderId data) {
|
||||||
|
identity = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateProfessional(ChildminderProfessional data) {
|
||||||
|
professional = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
void updatePresentation(String message) {
|
||||||
|
presentationMessage = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
void acceptCGU() {
|
||||||
|
cguAccepted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isComplete {
|
||||||
|
return identity.firstName.isNotEmpty &&
|
||||||
|
identity.lastName.isNotEmpty &&
|
||||||
|
identity.address.isNotEmpty &&
|
||||||
|
identity.postalCode.isNotEmpty &&
|
||||||
|
identity.city.isNotEmpty &&
|
||||||
|
identity.phone.isNotEmpty &&
|
||||||
|
identity.email.isNotEmpty &&
|
||||||
|
identity.password.isNotEmpty &&
|
||||||
|
professional.dateOfBirth.isNotEmpty &&
|
||||||
|
professional.birthCity.isNotEmpty &&
|
||||||
|
professional.birthCountry.isNotEmpty &&
|
||||||
|
professional.socialSecurityNumber.isNotEmpty &&
|
||||||
|
professional.agreementNumber.isNotEmpty &&
|
||||||
|
cguAccepted &&
|
||||||
|
(!isPhotoRequired || (identity.profilePicture != null && identity.photoConsent));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:p_tits_pas/models/am_user_registration_data.dart';
|
||||||
|
import 'package:p_tits_pas/screens/auth/am/am_register_step1_sceen.dart';
|
||||||
|
import 'package:p_tits_pas/screens/auth/am/am_register_step2_sceen.dart';
|
||||||
|
import 'package:p_tits_pas/screens/auth/am/am_register_step3_sceen.dart';
|
||||||
|
import 'package:p_tits_pas/screens/auth/am/am_register_step4_sceen.dart';
|
||||||
|
import 'package:p_tits_pas/screens/legal/legal_page.dart';
|
||||||
|
import 'package:p_tits_pas/screens/legal/privacy_page.dart';
|
||||||
import '../screens/auth/login_screen.dart';
|
import '../screens/auth/login_screen.dart';
|
||||||
import '../screens/auth/register_choice_screen.dart';
|
import '../screens/auth/register_choice_screen.dart';
|
||||||
import '../screens/auth/parent/parent_register_step1_screen.dart';
|
import '../screens/auth/parent/parent_register_step1_screen.dart';
|
||||||
@@ -8,16 +14,23 @@ import '../screens/auth/parent/parent_register_step3_screen.dart';
|
|||||||
import '../screens/auth/parent/parent_register_step4_screen.dart';
|
import '../screens/auth/parent/parent_register_step4_screen.dart';
|
||||||
import '../screens/auth/parent/parent_register_step5_screen.dart';
|
import '../screens/auth/parent/parent_register_step5_screen.dart';
|
||||||
import '../screens/home/home_screen.dart';
|
import '../screens/home/home_screen.dart';
|
||||||
import '../models/user_registration_data.dart';
|
import '../models/parent_user_registration_data.dart';
|
||||||
|
|
||||||
class AppRouter {
|
class AppRouter {
|
||||||
static const String login = '/login';
|
static const String login = '/login';
|
||||||
static const String registerChoice = '/register-choice';
|
static const String registerChoice = '/register-choice';
|
||||||
|
static const String legal = '/legal';
|
||||||
|
static const String privacy = '/privacy';
|
||||||
static const String parentRegisterStep1 = '/parent-register/step1';
|
static const String parentRegisterStep1 = '/parent-register/step1';
|
||||||
static const String parentRegisterStep2 = '/parent-register/step2';
|
static const String parentRegisterStep2 = '/parent-register/step2';
|
||||||
static const String parentRegisterStep3 = '/parent-register/step3';
|
static const String parentRegisterStep3 = '/parent-register/step3';
|
||||||
static const String parentRegisterStep4 = '/parent-register/step4';
|
static const String parentRegisterStep4 = '/parent-register/step4';
|
||||||
static const String parentRegisterStep5 = '/parent-register/step5';
|
static const String parentRegisterStep5 = '/parent-register/step5';
|
||||||
|
|
||||||
|
static const String amRegisterStep1 = '/am-register/step1';
|
||||||
|
static const String amRegisterStep2 = '/am-register/step2';
|
||||||
|
static const String amRegisterStep3 = '/am-register/step3';
|
||||||
|
static const String amRegisterStep4 = '/am-register/step4';
|
||||||
static const String home = '/home';
|
static const String home = '/home';
|
||||||
|
|
||||||
static Route<dynamic> generateRoute(RouteSettings settings) {
|
static Route<dynamic> generateRoute(RouteSettings settings) {
|
||||||
@@ -38,8 +51,12 @@ class AppRouter {
|
|||||||
screen = const RegisterChoiceScreen();
|
screen = const RegisterChoiceScreen();
|
||||||
slideTransition = true;
|
slideTransition = true;
|
||||||
break;
|
break;
|
||||||
case parentRegisterStep1:
|
case legal:
|
||||||
screen = const ParentRegisterStep1Screen();
|
screen = const LegalPage();
|
||||||
|
slideTransition = true;
|
||||||
|
break;
|
||||||
|
case privacy:
|
||||||
|
screen = const PrivacyPage();
|
||||||
slideTransition = true;
|
slideTransition = true;
|
||||||
break;
|
break;
|
||||||
case parentRegisterStep2:
|
case parentRegisterStep2:
|
||||||
@@ -74,6 +91,34 @@ class AppRouter {
|
|||||||
}
|
}
|
||||||
slideTransition = true;
|
slideTransition = true;
|
||||||
break;
|
break;
|
||||||
|
case amRegisterStep1:
|
||||||
|
screen = const AmRegisterStep1Screen();
|
||||||
|
slideTransition = true;
|
||||||
|
break;
|
||||||
|
case amRegisterStep2:
|
||||||
|
if (args is ChildminderRegistrationData) {
|
||||||
|
screen = AmRegisterStep2Screen(registrationData: args);
|
||||||
|
} else {
|
||||||
|
screen = AmRegisterStep2Screen(registrationData: ChildminderRegistrationData());
|
||||||
|
}
|
||||||
|
slideTransition = true;
|
||||||
|
break;
|
||||||
|
case amRegisterStep3:
|
||||||
|
if (args is ChildminderRegistrationData) {
|
||||||
|
screen = AmRegisterStep3Screen(registrationData: args);
|
||||||
|
} else {
|
||||||
|
screen = AmRegisterStep3Screen(registrationData: ChildminderRegistrationData());
|
||||||
|
}
|
||||||
|
slideTransition = true;
|
||||||
|
break;
|
||||||
|
case amRegisterStep4:
|
||||||
|
if (args is ChildminderRegistrationData) {
|
||||||
|
screen = AmRegisterStep4Screen(registrationData: args);
|
||||||
|
} else {
|
||||||
|
screen = AmRegisterStep4Screen(registrationData: ChildminderRegistrationData());
|
||||||
|
}
|
||||||
|
slideTransition = true;
|
||||||
|
break;
|
||||||
case home:
|
case home:
|
||||||
screen = const HomeScreen();
|
screen = const HomeScreen();
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1,17 +1,281 @@
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:p_tits_pas/models/am_user_registration_data.dart';
|
||||||
|
import 'package:p_tits_pas/models/card_assets.dart';
|
||||||
|
import 'package:p_tits_pas/utils/data_generator.dart';
|
||||||
|
import 'package:p_tits_pas/widgets/FormFieldConfig.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
class AmRegisterStep1Screen extends StatelessWidget {
|
|
||||||
const AmRegisterStep1Screen({Key? key}) : super(key: key);
|
class AmRegisterStep1Screen extends StatefulWidget {
|
||||||
|
const AmRegisterStep1Screen({super.key});
|
||||||
|
@override
|
||||||
|
State <AmRegisterStep1Screen> createState() => _AmRegisterStep1ScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AmRegisterStep1ScreenState extends State<AmRegisterStep1Screen> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
late ChildminderRegistrationData _registrationData;
|
||||||
|
|
||||||
|
final _lastNameController = TextEditingController();
|
||||||
|
final _firstNameController = TextEditingController();
|
||||||
|
final _phoneController = TextEditingController();
|
||||||
|
final _emailController = TextEditingController();
|
||||||
|
final _passwordController = TextEditingController();
|
||||||
|
final _confirmPasswordController = TextEditingController();
|
||||||
|
final _addressController = TextEditingController();
|
||||||
|
final _postalCodeController = TextEditingController();
|
||||||
|
final _cityController = TextEditingController();
|
||||||
|
|
||||||
|
// File? _selectedImage;
|
||||||
|
// bool _photoConsent = false;
|
||||||
|
// final ImagePicker _picker = ImagePicker();
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_registrationData = ChildminderRegistrationData();
|
||||||
|
_generateAndFillData();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _generateAndFillData() {
|
||||||
|
final String genFirstName = DataGenerator.firstName();
|
||||||
|
final String genLastName = DataGenerator.lastName();
|
||||||
|
final String genAddress = DataGenerator.address();
|
||||||
|
final String genPostalCode = DataGenerator.postalCode();
|
||||||
|
final String genCity = DataGenerator.city();
|
||||||
|
final String genPhone = DataGenerator.phone();
|
||||||
|
final String genEmail = DataGenerator.email(genFirstName, genLastName);
|
||||||
|
final String genPassword = DataGenerator.password();
|
||||||
|
|
||||||
|
_addressController.text = genAddress;
|
||||||
|
_postalCodeController.text = genPostalCode;
|
||||||
|
_cityController.text = genCity;
|
||||||
|
_firstNameController.text = genFirstName;
|
||||||
|
_lastNameController.text = genLastName;
|
||||||
|
_phoneController.text = genPhone;
|
||||||
|
_emailController.text = genEmail;
|
||||||
|
_passwordController.text = genPassword;
|
||||||
|
_confirmPasswordController.text = genPassword;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_registrationData.updateIdentity(
|
||||||
|
ChildminderId(
|
||||||
|
firstName: genFirstName,
|
||||||
|
lastName: genLastName,
|
||||||
|
address: genAddress,
|
||||||
|
postalCode: genPostalCode,
|
||||||
|
city: genCity,
|
||||||
|
phone: genPhone,
|
||||||
|
email: genEmail,
|
||||||
|
password: genPassword.trim(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_lastNameController.dispose();
|
||||||
|
_firstNameController.dispose();
|
||||||
|
_phoneController.dispose();
|
||||||
|
_emailController.dispose();
|
||||||
|
_passwordController.dispose();
|
||||||
|
_confirmPasswordController.dispose();
|
||||||
|
_addressController.dispose();
|
||||||
|
_postalCodeController.dispose();
|
||||||
|
_cityController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<List<ModularFormField>> get formFields => [
|
||||||
|
[
|
||||||
|
ModularFormField(
|
||||||
|
label: 'Nom',
|
||||||
|
hint: 'Votre nom de famille',
|
||||||
|
controller: _lastNameController,
|
||||||
|
isRequired: true,
|
||||||
|
flex: 12,
|
||||||
|
),
|
||||||
|
ModularFormField(
|
||||||
|
label: 'Prénom',
|
||||||
|
hint: 'Votre prénom',
|
||||||
|
controller: _firstNameController,
|
||||||
|
isRequired: true,
|
||||||
|
flex: 12,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ModularFormField(
|
||||||
|
label: 'Téléphone',
|
||||||
|
hint: 'Votre numéro de téléphone',
|
||||||
|
controller: _phoneController,
|
||||||
|
keyboardType: TextInputType.phone,
|
||||||
|
flex: 12,
|
||||||
|
),
|
||||||
|
ModularFormField(
|
||||||
|
label: 'Email',
|
||||||
|
hint: 'Votre adresse email',
|
||||||
|
controller: _emailController,
|
||||||
|
keyboardType: TextInputType.emailAddress,
|
||||||
|
flex: 12,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ModularFormField(
|
||||||
|
label: 'Mot de passe',
|
||||||
|
hint: 'Votre mot de passe',
|
||||||
|
controller: _passwordController,
|
||||||
|
isPassword: true,
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) return 'Mot de passe requis';
|
||||||
|
if (value.length < 6) return '6 caractères minimum';
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
isRequired: true,
|
||||||
|
flex: 12,
|
||||||
|
),
|
||||||
|
ModularFormField(
|
||||||
|
label: 'Confirmer le mot de passe',
|
||||||
|
hint: 'Confirmez votre mot de passe',
|
||||||
|
controller: _confirmPasswordController,
|
||||||
|
isPassword: true,
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) return 'Mot de passe requis';
|
||||||
|
if (value != _passwordController.text) return 'Les mots de passe ne correspondent pas';
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
isRequired: true,
|
||||||
|
flex: 12,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ModularFormField(
|
||||||
|
label: 'Adresse (N° et Rue)',
|
||||||
|
hint: 'Numéro et nom de votre rue',
|
||||||
|
controller: _addressController,
|
||||||
|
isRequired: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ModularFormField(
|
||||||
|
label: 'Code postal',
|
||||||
|
hint: 'Votre code postal',
|
||||||
|
controller: _postalCodeController,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
isRequired: true,
|
||||||
|
flex: 1,
|
||||||
|
),
|
||||||
|
ModularFormField(
|
||||||
|
label: 'Ville',
|
||||||
|
hint: 'Votre ville',
|
||||||
|
controller: _cityController,
|
||||||
|
flex: 4,
|
||||||
|
isRequired: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
void _handleSubmit() {
|
||||||
|
if (_formKey.currentState?.validate() ?? false) {
|
||||||
|
_registrationData.updateIdentity(
|
||||||
|
ChildminderId(
|
||||||
|
firstName: _firstNameController.text,
|
||||||
|
lastName: _lastNameController.text,
|
||||||
|
address: _addressController.text,
|
||||||
|
postalCode: _postalCodeController.text,
|
||||||
|
city: _cityController.text,
|
||||||
|
phone: _phoneController.text,
|
||||||
|
email: _emailController.text,
|
||||||
|
password: _passwordController.text,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
print('Vérification des données:');
|
||||||
|
print('Adresse: ${_registrationData.identity.address}');
|
||||||
|
print('Nom: ${_registrationData.identity.lastName}');
|
||||||
|
print('Prénom: ${_registrationData.identity.firstName}');
|
||||||
|
Navigator.pushNamed(context, '/am-register/step2',
|
||||||
|
arguments: _registrationData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final screenSize = MediaQuery.of(context).size;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
body: Stack(
|
||||||
title: const Text('Étape 1 - Inscription AM'),
|
children: [
|
||||||
),
|
Positioned.fill(
|
||||||
body: const Center(
|
child: Image.asset(
|
||||||
child: Text('Contenu de l\'étape 1'),
|
'assets/images/paper2.png',
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
repeat: ImageRepeat.repeat,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Center(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Étape 1/4',
|
||||||
|
style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Text(
|
||||||
|
'Informations d\'identité de l\'assistante maternelle',
|
||||||
|
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: 50, horizontal: 50),
|
||||||
|
constraints: const BoxConstraints(minHeight: 570),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
image: DecorationImage(
|
||||||
|
image: AssetImage(CardColorHorizontal.lavender.path),
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: ModularForm(
|
||||||
|
formKey: _formKey,
|
||||||
|
fieldGroups: formFields,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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: _handleSubmit,
|
||||||
|
tooltip: 'Suivant',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,251 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:p_tits_pas/models/am_user_registration_data.dart';
|
||||||
|
import 'package:p_tits_pas/models/card_assets.dart';
|
||||||
|
import 'package:p_tits_pas/utils/data_generator.dart';
|
||||||
|
import 'package:p_tits_pas/widgets/FormFieldConfig.dart';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
class AmRegisterStep2Screen extends StatefulWidget {
|
||||||
|
final ChildminderRegistrationData registrationData;
|
||||||
|
const AmRegisterStep2Screen({super.key, required this.registrationData});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AmRegisterStep2Screen> createState() => _AmRegisterStep2ScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AmRegisterStep2ScreenState extends State<AmRegisterStep2Screen> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
late ChildminderRegistrationData _registrationData;
|
||||||
|
|
||||||
|
final _dateOfBirthController = TextEditingController();
|
||||||
|
final _birthCityController = TextEditingController();
|
||||||
|
final _birthCountryController = TextEditingController();
|
||||||
|
final _socialSecurityController = TextEditingController();
|
||||||
|
final _agreementNumberController = TextEditingController();
|
||||||
|
final _maxChildrenController = TextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_registrationData = widget.registrationData;
|
||||||
|
_generateAndFillData();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _generateAndFillData() {
|
||||||
|
_dateOfBirthController.text = DataGenerator.birthDate();
|
||||||
|
_birthCityController.text = DataGenerator.city();
|
||||||
|
_birthCountryController.text = "France";
|
||||||
|
_socialSecurityController.text = DataGenerator.socialSecurityNumber();
|
||||||
|
_agreementNumberController.text = DataGenerator.agreementNumber();
|
||||||
|
_maxChildrenController.text = "3";
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_dateOfBirthController.dispose();
|
||||||
|
_birthCityController.dispose();
|
||||||
|
_birthCountryController.dispose();
|
||||||
|
_socialSecurityController.dispose();
|
||||||
|
_agreementNumberController.dispose();
|
||||||
|
_maxChildrenController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _validateSocialSecurity(String? value) {
|
||||||
|
if (value == null || value.isEmpty)
|
||||||
|
return 'Numéro de sécurité sociale requis';
|
||||||
|
|
||||||
|
// Supprime les espaces pour la validation
|
||||||
|
String cleanValue = value.replaceAll(' ', '');
|
||||||
|
|
||||||
|
// Vérifie que c'est bien 13 ou 15 chiffres
|
||||||
|
if (cleanValue.length != 13 && cleanValue.length != 15) {
|
||||||
|
return 'Format invalide (13 ou 15 chiffres)';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!RegExp(r'^[0-9]+$').hasMatch(cleanValue)) {
|
||||||
|
return 'Seuls les chiffres sont autorisés';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<List<ModularFormField>> get formFields => [
|
||||||
|
[
|
||||||
|
ModularFormField(
|
||||||
|
label: 'Date de naissance',
|
||||||
|
hint: 'JJ/MM/AAAA',
|
||||||
|
controller: _dateOfBirthController,
|
||||||
|
keyboardType: TextInputType.datetime,
|
||||||
|
isRequired: true,
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty)
|
||||||
|
return 'Date de naissance requise';
|
||||||
|
// Validation basique du format de date
|
||||||
|
if (!RegExp(r'^[0-3][0-9]/[0-1][0-9]/[1-2][0-9]{3}$')
|
||||||
|
.hasMatch(value)) {
|
||||||
|
return 'Format invalide (JJ/MM/AAAA)';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
flex: 12,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ModularFormField(
|
||||||
|
label: 'Ville de naissance',
|
||||||
|
hint: 'Votre ville de naissance',
|
||||||
|
controller: _birthCityController,
|
||||||
|
isRequired: true,
|
||||||
|
flex: 12,
|
||||||
|
),
|
||||||
|
ModularFormField(
|
||||||
|
label: 'Pays de naissance',
|
||||||
|
hint: 'Votre pays de naissance',
|
||||||
|
controller: _birthCountryController,
|
||||||
|
isRequired: true,
|
||||||
|
flex: 12,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ModularFormField(
|
||||||
|
label: 'Numéro de Sécurité Sociale (NIR)',
|
||||||
|
hint: '1234567890123',
|
||||||
|
controller: _socialSecurityController,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
isRequired: true,
|
||||||
|
validator: _validateSocialSecurity,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ModularFormField(
|
||||||
|
label: 'Numéro d\'agrément',
|
||||||
|
hint: 'Votre numéro d\'agrément',
|
||||||
|
controller: _agreementNumberController,
|
||||||
|
isRequired: true,
|
||||||
|
flex: 12,
|
||||||
|
),
|
||||||
|
ModularFormField(
|
||||||
|
label: 'Nombre d\'enfants max',
|
||||||
|
hint: 'Ex: 3',
|
||||||
|
controller: _maxChildrenController,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
isRequired: true,
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) return 'Nombre requis';
|
||||||
|
int? number = int.tryParse(value);
|
||||||
|
if (number == null || number < 1 || number > 6) {
|
||||||
|
return 'Entre 1 et 6 enfants';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
flex: 6,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
void _handleSubmit() {
|
||||||
|
print('Vérification des données2:');
|
||||||
|
print('Adresse: ${_registrationData.identity.address}');
|
||||||
|
print('Nom: ${_registrationData.identity.lastName}');
|
||||||
|
print('Prénom: ${_registrationData.identity.firstName}');
|
||||||
|
if (_formKey.currentState?.validate() ?? false) {
|
||||||
|
_registrationData.updateProfessional(
|
||||||
|
ChildminderProfessional(
|
||||||
|
dateOfBirth: _dateOfBirthController.text,
|
||||||
|
birthCity: _birthCityController.text,
|
||||||
|
birthCountry: _birthCountryController.text,
|
||||||
|
socialSecurityNumber: _socialSecurityController.text,
|
||||||
|
agreementNumber: _agreementNumberController.text,
|
||||||
|
maxChildren: int.tryParse(_maxChildrenController.text) ?? 1,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Navigator.pushNamed(context, '/am-register/step3',
|
||||||
|
arguments: _registrationData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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/4',
|
||||||
|
style: GoogleFonts.merienda(
|
||||||
|
fontSize: 16, color: Colors.black54),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Text(
|
||||||
|
'Informations professionnelles de l\'assistante maternelle',
|
||||||
|
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: 50, horizontal: 50),
|
||||||
|
constraints: const BoxConstraints(minHeight: 570),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
image: DecorationImage(
|
||||||
|
image: AssetImage(CardColorHorizontal.peach.path),
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: ModularForm(
|
||||||
|
formKey: _formKey,
|
||||||
|
fieldGroups: formFields,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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: _handleSubmit,
|
||||||
|
tooltip: 'Suivant',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:p_tits_pas/models/am_user_registration_data.dart';
|
||||||
|
import 'package:p_tits_pas/models/card_assets.dart';
|
||||||
|
import 'package:p_tits_pas/widgets/app_custom_checkbox.dart';
|
||||||
|
import 'package:p_tits_pas/widgets/custom_decorated_text_field.dart';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
|
||||||
|
class AmRegisterStep3Screen extends StatefulWidget {
|
||||||
|
final ChildminderRegistrationData registrationData;
|
||||||
|
const AmRegisterStep3Screen({super.key, required this.registrationData});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AmRegisterStep3Screen> createState() => _AmRegisterStep3ScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AmRegisterStep3ScreenState extends State<AmRegisterStep3Screen> {
|
||||||
|
|
||||||
|
late ChildminderRegistrationData _registrationData;
|
||||||
|
final _presentationMessageController = TextEditingController();
|
||||||
|
bool _cguAccepted = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_registrationData = widget.registrationData;
|
||||||
|
_presentationMessageController.text = _registrationData.presentationMessage;
|
||||||
|
// _cguAccepted = _registrationData.cguAccepted;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_presentationMessageController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showCGUModal() {
|
||||||
|
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;
|
||||||
|
final double imageAspectRatio = 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 3/4',
|
||||||
|
style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Text(
|
||||||
|
'Message à destination du gestionnaire pour justifier votre demande ou ajouter des précisions',
|
||||||
|
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: _presentationMessageController,
|
||||||
|
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.updatePresentation(_presentationMessageController.text);
|
||||||
|
_registrationData.acceptCGU();
|
||||||
|
|
||||||
|
Navigator.pushNamed(
|
||||||
|
context,
|
||||||
|
'/am-register/step4',
|
||||||
|
arguments: _registrationData
|
||||||
|
);
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
tooltip: 'Suivant',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:p_tits_pas/models/am_user_registration_data.dart';
|
||||||
|
import 'package:p_tits_pas/models/card_assets.dart';
|
||||||
|
import 'package:p_tits_pas/widgets/Summary.dart';
|
||||||
|
import 'package:p_tits_pas/widgets/custom_decorated_text_field.dart';
|
||||||
|
import 'package:p_tits_pas/widgets/image_button.dart';
|
||||||
|
|
||||||
|
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 AmRegisterStep4Screen extends StatelessWidget {
|
||||||
|
final ChildminderRegistrationData registrationData;
|
||||||
|
|
||||||
|
const AmRegisterStep4Screen({super.key, required this.registrationData});
|
||||||
|
|
||||||
|
Widget _buildAm1Card(BuildContext context, ChildminderRegistrationData 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.identity.lastName, labelFontSize: labelFontSize)),
|
||||||
|
const SizedBox(width: 20),
|
||||||
|
Expanded(child: _buildDisplayFieldValue(context, "Prénom:", data.identity.firstName, labelFontSize: labelFontSize)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: verticalSpacing),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(child: _buildDisplayFieldValue(context, "Téléphone:", data.identity.phone, labelFontSize: labelFontSize)),
|
||||||
|
const SizedBox(width: 20),
|
||||||
|
Expanded(child: _buildDisplayFieldValue(context, "Email:", data.identity.email, multiLine: true, labelFontSize: labelFontSize)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: verticalSpacing),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(child: _buildDisplayFieldValue(context, "Adresse:", "${data.identity.address}\n${data.identity.postalCode} ${data.identity.city}".trim(), labelFontSize: labelFontSize)),
|
||||||
|
const SizedBox(width: 20),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
return SummaryCard(
|
||||||
|
backgroundImagePath: CardColorHorizontal.peach.path,
|
||||||
|
title: 'Informations d’identité',
|
||||||
|
content: details,
|
||||||
|
onEdit: () => Navigator.of(context).pushNamed('/am-register/step1', arguments: registrationData),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAm2Card(BuildContext context, ChildminderRegistrationData data) {
|
||||||
|
const double verticalSpacing = 28.0;
|
||||||
|
const double labelFontSize = 22.0;
|
||||||
|
|
||||||
|
List<Widget> myDetails = [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(child: _buildDisplayFieldValue(context, "Date de naissance:", data.professional.dateOfBirth, labelFontSize: labelFontSize)),
|
||||||
|
const SizedBox(width: 20),
|
||||||
|
Expanded(child: _buildDisplayFieldValue(context, "Ville de naissance:", data.professional.birthCity, labelFontSize: labelFontSize)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: verticalSpacing),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(child: _buildDisplayFieldValue(context, "Pays de naissance:", data.professional.birthCountry, labelFontSize: labelFontSize)),
|
||||||
|
const SizedBox(width: 20),
|
||||||
|
Expanded(child: _buildDisplayFieldValue(context, "Numéro de sécurité sociale:", data.professional.socialSecurityNumber, labelFontSize: labelFontSize)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: verticalSpacing),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(child: _buildDisplayFieldValue(context, "Numéro d'agrément:", data.professional.agreementNumber, labelFontSize: labelFontSize)),
|
||||||
|
const SizedBox(width: 20),
|
||||||
|
Expanded(child: _buildDisplayFieldValue(context, "Nombre d'enfants maximum:", data.professional.maxChildren.toString(), labelFontSize: labelFontSize)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
return SummaryCard(
|
||||||
|
backgroundImagePath: CardColorHorizontal.lavender.path,
|
||||||
|
title: 'Informations professionnelles',
|
||||||
|
content: myDetails,
|
||||||
|
onEdit: () => Navigator.of(context)
|
||||||
|
.pushNamed('/am-register/step2', arguments: registrationData),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMotivationCard(BuildContext context, ChildminderRegistrationData data) {
|
||||||
|
return SummaryCard(
|
||||||
|
backgroundImagePath: CardColorHorizontal.green.path,
|
||||||
|
title: 'Motivation',
|
||||||
|
content: [
|
||||||
|
Expanded(child: CustomDecoratedTextField(
|
||||||
|
controller: TextEditingController(text: data.presentationMessage),
|
||||||
|
hintText: 'Parlez-nous de votre motivation',
|
||||||
|
fieldHeight: 200,
|
||||||
|
maxLines: 10,
|
||||||
|
expandDynamically: true,
|
||||||
|
readOnly: true,
|
||||||
|
fontSize: 18.0,)),
|
||||||
|
],
|
||||||
|
onEdit: () => Navigator.of(context)
|
||||||
|
.pushNamed('/am-register/step3', arguments: registrationData),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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.repeatY),
|
||||||
|
),
|
||||||
|
Center(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(40.0),
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: screenSize.width / 4.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text('Etape 4/4',
|
||||||
|
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),
|
||||||
|
_buildAm1Card(context, registrationData),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
if (registrationData.professional != null) ...[
|
||||||
|
_buildAm2Card(context, registrationData),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
],
|
||||||
|
_buildMotivationCard(context, registrationData),
|
||||||
|
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: () {
|
||||||
|
// Vérification des données requises
|
||||||
|
_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),
|
||||||
|
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();
|
||||||
|
Navigator.of(context)
|
||||||
|
.pushNamedAndRemoveUntil('/login', (route) => false);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'dart:math' as math; // Pour la rotation du chevron
|
import 'dart:math' as math; // Pour la rotation du chevron
|
||||||
import '../../../models/user_registration_data.dart'; // Import du modèle de données
|
import '../../../models/parent_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 '../../../utils/data_generator.dart'; // Import du générateur de données
|
||||||
import '../../../widgets/custom_app_text_field.dart'; // Import du widget CustomAppTextField
|
import '../../../widgets/custom_app_text_field.dart'; // Import du widget CustomAppTextField
|
||||||
import '../../../models/card_assets.dart'; // Import des enums de cartes
|
import '../../../models/card_assets.dart'; // Import des enums de cartes
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'dart:math' as math; // Pour la rotation du chevron
|
import 'dart:math' as math; // Pour la rotation du chevron
|
||||||
import '../../../models/user_registration_data.dart'; // Import du modèle
|
import '../../../models/parent_user_registration_data.dart'; // Import du modèle
|
||||||
import '../../../utils/data_generator.dart'; // Import du générateur
|
import '../../../utils/data_generator.dart'; // Import du générateur
|
||||||
import '../../../widgets/custom_app_text_field.dart'; // Import du widget
|
import '../../../widgets/custom_app_text_field.dart'; // Import du widget
|
||||||
import '../../../models/card_assets.dart'; // Import des enums de cartes
|
import '../../../models/card_assets.dart'; // Import des enums de cartes
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import 'dart:io' show File, Platform; // Ajout de Platform
|
|||||||
import 'package:flutter/foundation.dart' show kIsWeb; // Import pour kIsWeb
|
import 'package:flutter/foundation.dart' show kIsWeb; // Import pour kIsWeb
|
||||||
import '../../../widgets/custom_app_text_field.dart'; // Import du nouveau widget TextField
|
import '../../../widgets/custom_app_text_field.dart'; // Import du nouveau widget TextField
|
||||||
import '../../../widgets/app_custom_checkbox.dart'; // Import du nouveau widget Checkbox
|
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 '../../../models/parent_user_registration_data.dart'; // Import du modèle de données
|
||||||
import '../../../utils/data_generator.dart'; // Import du générateur
|
import '../../../utils/data_generator.dart'; // Import du générateur
|
||||||
import '../../../models/card_assets.dart'; // Import des enums de cartes
|
import '../../../models/card_assets.dart'; // Import des enums de cartes
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'package:p_tits_pas/widgets/custom_decorated_text_field.dart'; // Import
|
|||||||
import 'dart:math' as math; // Pour la rotation du chevron
|
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/widgets/app_custom_checkbox.dart'; // Import de la checkbox personnalisée
|
||||||
// import 'package:p_tits_pas/models/placeholder_registration_data.dart'; // Remplacé
|
// import 'package:p_tits_pas/models/placeholder_registration_data.dart'; // Remplacé
|
||||||
import '../../../models/user_registration_data.dart'; // Import du vrai modèle
|
import '../../../models/parent_user_registration_data.dart'; // Import du vrai modèle
|
||||||
import '../../../utils/data_generator.dart'; // Import du générateur
|
import '../../../utils/data_generator.dart'; // Import du générateur
|
||||||
import '../../../models/card_assets.dart'; // Import des enums de cartes
|
import '../../../models/card_assets.dart'; // Import des enums de cartes
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import '../../../models/user_registration_data.dart'; // Utilisation du vrai modèle
|
import 'package:p_tits_pas/widgets/Summary.dart';
|
||||||
|
import '../../../models/parent_user_registration_data.dart'; // Utilisation du vrai modèle
|
||||||
import '../../../widgets/image_button.dart'; // Import du ImageButton
|
import '../../../widgets/image_button.dart'; // Import du ImageButton
|
||||||
import '../../../models/card_assets.dart'; // Import des enums de cartes
|
import '../../../models/card_assets.dart'; // Import des enums de cartes
|
||||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
@@ -127,7 +128,7 @@ class ParentRegisterStep5Screen extends StatelessWidget {
|
|||||||
const SizedBox(height: verticalSpacing),
|
const SizedBox(height: verticalSpacing),
|
||||||
_buildDisplayFieldValue(context, "Adresse:", "${data.address}\n${data.postalCode} ${data.city}".trim(), multiLine: true, fieldHeight: 80, labelFontSize: labelFontSize),
|
_buildDisplayFieldValue(context, "Adresse:", "${data.address}\n${data.postalCode} ${data.city}".trim(), multiLine: true, fieldHeight: 80, labelFontSize: labelFontSize),
|
||||||
];
|
];
|
||||||
return _SummaryCard(
|
return SummaryCard(
|
||||||
backgroundImagePath: CardColorHorizontal.blue.path,
|
backgroundImagePath: CardColorHorizontal.blue.path,
|
||||||
title: 'Deuxième Parent',
|
title: 'Deuxième Parent',
|
||||||
content: details,
|
content: details,
|
||||||
|
|||||||
@@ -100,6 +100,8 @@ class RegisterChoiceScreen extends StatelessWidget {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
// TODO: Naviguer vers l'écran d'inscription assmat
|
// TODO: Naviguer vers l'écran d'inscription assmat
|
||||||
print('Choix: Assistante Maternelle');
|
print('Choix: Assistante Maternelle');
|
||||||
|
Navigator.pushNamed(context, '/am-register/step1');
|
||||||
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'package:p_tits_pas/config/env.dart';
|
||||||
|
|
||||||
class BugReportService {
|
class BugReportService {
|
||||||
static const String _apiUrl = 'https://api.supernounou.local/bug-reports';
|
static final String _apiUrl = Env.apiV1('/bug-reports');
|
||||||
|
|
||||||
static Future<void> sendReport(String description) async {
|
static Future<void> sendReport(String description) async {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -26,6 +26,25 @@ class DataGenerator {
|
|||||||
'Nous avons hâte de vous rencontrer.',
|
'Nous avons hâte de vous rencontrer.',
|
||||||
'La pédagogie Montessori nous intéresse.'
|
'La pédagogie Montessori nous intéresse.'
|
||||||
];
|
];
|
||||||
|
static final List<String> _frenchCities = [
|
||||||
|
'Paris', 'Lyon', 'Marseille', 'Toulouse', 'Nice', 'Nantes', 'Montpellier', 'Strasbourg',
|
||||||
|
'Bordeaux', 'Lille', 'Rennes', 'Reims', 'Saint-Étienne', 'Toulon', 'Le Havre', 'Grenoble',
|
||||||
|
'Dijon', 'Angers', 'Nîmes', 'Villeurbanne', 'Clermont-Ferrand', 'Le Mans', 'Aix-en-Provence',
|
||||||
|
'Brest', 'Tours', 'Amiens', 'Limoges', 'Annecy', 'Boulogne-Billancourt', 'Perpignan'
|
||||||
|
];
|
||||||
|
|
||||||
|
static final List<String> _countries = [
|
||||||
|
'France', 'Belgique', 'Suisse', 'Canada', 'Maroc', 'Algérie', 'Tunisie', 'Sénégal',
|
||||||
|
'Côte d\'Ivoire', 'Madagascar', 'Espagne', 'Italie', 'Portugal', 'Allemagne', 'Royaume-Uni'
|
||||||
|
];
|
||||||
|
|
||||||
|
static final List<String> _childminderPresentations = [
|
||||||
|
'Bonjour,\n\nJe suis assistante maternelle agréée depuis plusieurs années et je souhaite rejoindre votre plateforme. J\'ai de l\'expérience avec les enfants de tous âges et je privilégie un accompagnement bienveillant.\n\nCordialement',
|
||||||
|
'Madame, Monsieur,\n\nTitulaire d\'un agrément d\'assistante maternelle, je propose un accueil personnalisé dans un environnement sécurisé et stimulant. Je suis disponible pour discuter de vos besoins.\n\nBien à vous',
|
||||||
|
'Bonjour,\n\nAssistante maternelle passionnée, je propose un accueil de qualité dans ma maison adaptée aux enfants. J\'ai suivi plusieurs formations et je suis disponible à temps plein.\n\nÀ bientôt',
|
||||||
|
'Cher gestionnaire,\n\nJe suis une professionnelle expérimentée dans la garde d\'enfants. Mon domicile est aménagé pour accueillir les petits dans les meilleures conditions. Je serais ravie de faire partie de votre réseau.\n\nCordialement',
|
||||||
|
'Bonjour,\n\nDepuis 5 ans, j\'exerce comme assistante maternelle avec passion. Je propose des activités d\'éveil adaptées et un suivi personnalisé de chaque enfant. Mon agrément me permet d\'accueillir jusqu\'à 4 enfants.\n\nBien cordialement'
|
||||||
|
];
|
||||||
|
|
||||||
static String firstName() => _firstNames[_random.nextInt(_firstNames.length)];
|
static String firstName() => _firstNames[_random.nextInt(_firstNames.length)];
|
||||||
static String lastName() => _lastNames[_random.nextInt(_lastNames.length)];
|
static String lastName() => _lastNames[_random.nextInt(_lastNames.length)];
|
||||||
@@ -62,4 +81,107 @@ class DataGenerator {
|
|||||||
}
|
}
|
||||||
return chosenSnippets.join(' ');
|
return chosenSnippets.join(' ');
|
||||||
}
|
}
|
||||||
}
|
static String birthDate() {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final age = _random.nextInt(31) + 25; // Entre 25 et 55 ans
|
||||||
|
final birthYear = now.year - age;
|
||||||
|
final birthMonth = _random.nextInt(12) + 1;
|
||||||
|
final birthDay = _random.nextInt(28) + 1;
|
||||||
|
return "${birthDay.toString().padLeft(2, '0')}/${birthMonth.toString().padLeft(2, '0')}/${birthYear}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Génère une ville de naissance française
|
||||||
|
static String birthCity() => _frenchCities[_random.nextInt(_frenchCities.length)];
|
||||||
|
|
||||||
|
/// Génère un pays de naissance
|
||||||
|
static String birthCountry() => _countries[_random.nextInt(_countries.length)];
|
||||||
|
|
||||||
|
/// Génère un numéro de sécurité sociale français (NIR)
|
||||||
|
static String socialSecurityNumber() {
|
||||||
|
// Format NIR français : 1 YYMM DD CCC KK
|
||||||
|
// 1 = sexe (1 homme, 2 femme)
|
||||||
|
final sex = _random.nextBool() ? '1' : '2';
|
||||||
|
|
||||||
|
// YY = année de naissance (2 derniers chiffres)
|
||||||
|
final currentYear = DateTime.now().year;
|
||||||
|
final birthYear = currentYear - (_random.nextInt(31) + 25); // 25-55 ans
|
||||||
|
final yy = (birthYear % 100).toString().padLeft(2, '0');
|
||||||
|
|
||||||
|
// MM = mois de naissance
|
||||||
|
final mm = (_random.nextInt(12) + 1).toString().padLeft(2, '0');
|
||||||
|
|
||||||
|
// DD = département de naissance (01-95)
|
||||||
|
final dd = (_random.nextInt(95) + 1).toString().padLeft(2, '0');
|
||||||
|
|
||||||
|
// CCC = numéro d'ordre (001-999)
|
||||||
|
final ccc = (_random.nextInt(999) + 1).toString().padLeft(3, '0');
|
||||||
|
|
||||||
|
// KK = clé de contrôle (simulation)
|
||||||
|
final kk = _random.nextInt(100).toString().padLeft(2, '0');
|
||||||
|
|
||||||
|
return '$sex$yy$mm$dd$ccc$kk';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Génère un numéro d'agrément pour assistante maternelle
|
||||||
|
static String agreementNumber() {
|
||||||
|
final year = DateTime.now().year;
|
||||||
|
final dept = _random.nextInt(95) + 1; // Département 01-95
|
||||||
|
final sequence = _random.nextInt(9999) + 1; // Numéro de séquence
|
||||||
|
return 'AM${dept.toString().padLeft(2, '0')}$year${sequence.toString().padLeft(4, '0')}';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Génère un nombre d'enfants maximum pour l'agrément (1-4)
|
||||||
|
static int maxChildren() => _random.nextInt(4) + 1;
|
||||||
|
|
||||||
|
/// Génère un message de présentation pour assistante maternelle
|
||||||
|
static String childminderPresentation() =>
|
||||||
|
_childminderPresentations[_random.nextInt(_childminderPresentations.length)];
|
||||||
|
|
||||||
|
/// Génère un âge d'enfant en mois (0-36 mois)
|
||||||
|
static int childAgeInMonths() => _random.nextInt(37);
|
||||||
|
|
||||||
|
/// Génère une durée d'expérience en années (1-15 ans)
|
||||||
|
static int experienceYears() => _random.nextInt(15) + 1;
|
||||||
|
|
||||||
|
/// Génère un tarif horaire (entre 3.50€ et 6.00€)
|
||||||
|
static double hourlyRate() {
|
||||||
|
final rate = 3.50 + (_random.nextDouble() * 2.50);
|
||||||
|
return double.parse(rate.toStringAsFixed(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Génère des disponibilités (jours de la semaine)
|
||||||
|
static List<String> availability() {
|
||||||
|
final days = ['Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi', 'Dimanche'];
|
||||||
|
final availableDays = <String>[];
|
||||||
|
|
||||||
|
// Au moins 3 jours de disponibilité
|
||||||
|
final minDays = 3;
|
||||||
|
final maxDays = days.length;
|
||||||
|
final numDays = _random.nextInt(maxDays - minDays + 1) + minDays;
|
||||||
|
|
||||||
|
final selectedIndices = <int>[];
|
||||||
|
while (selectedIndices.length < numDays) {
|
||||||
|
final index = _random.nextInt(days.length);
|
||||||
|
if (!selectedIndices.contains(index)) {
|
||||||
|
selectedIndices.add(index);
|
||||||
|
availableDays.add(days[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return availableDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Génère des horaires (format "08h30-17h30")
|
||||||
|
static String workingHours() {
|
||||||
|
final startHours = [7, 8, 9];
|
||||||
|
final endHours = [16, 17, 18, 19];
|
||||||
|
|
||||||
|
final startHour = startHours[_random.nextInt(startHours.length)];
|
||||||
|
final endHour = endHours[_random.nextInt(endHours.length)];
|
||||||
|
|
||||||
|
final startMinutes = _random.nextBool() ? '00' : '30';
|
||||||
|
final endMinutes = _random.nextBool() ? '00' : '30';
|
||||||
|
|
||||||
|
return '${startHour}h$startMinutes-${endHour}h$endMinutes';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:p_tits_pas/widgets/custom_app_text_field.dart';
|
||||||
|
|
||||||
|
class ModularFormField {
|
||||||
|
final String label;
|
||||||
|
final String hint;
|
||||||
|
final TextEditingController controller;
|
||||||
|
final TextInputType? keyboardType;
|
||||||
|
final bool isPassword;
|
||||||
|
final String? Function(String?)? validator;
|
||||||
|
final bool isRequired;
|
||||||
|
final int flex;
|
||||||
|
|
||||||
|
ModularFormField({
|
||||||
|
required this.label,
|
||||||
|
required this.hint,
|
||||||
|
required this.controller,
|
||||||
|
this.keyboardType,
|
||||||
|
this.isPassword = false,
|
||||||
|
this.validator,
|
||||||
|
this.isRequired = false,
|
||||||
|
this.flex = 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class ModularForm extends StatelessWidget {
|
||||||
|
final List<List<ModularFormField>> fieldGroups;
|
||||||
|
final GlobalKey<FormState> formKey;
|
||||||
|
final double? width;
|
||||||
|
final EdgeInsets padding;
|
||||||
|
final String? title;
|
||||||
|
final VoidCallback? onSubmit;
|
||||||
|
final String submitLabel;
|
||||||
|
|
||||||
|
const ModularForm({
|
||||||
|
super.key,
|
||||||
|
required this.fieldGroups,
|
||||||
|
required this.formKey,
|
||||||
|
this.width,
|
||||||
|
this.padding = const EdgeInsets.all(20),
|
||||||
|
this.title,
|
||||||
|
this.onSubmit,
|
||||||
|
this.submitLabel = "Suivant",
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
width: width ?? MediaQuery.of(context).size.width * 0.6,
|
||||||
|
padding: padding,
|
||||||
|
child: Form(
|
||||||
|
key: formKey,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (title != null) ...[
|
||||||
|
Text(
|
||||||
|
title!,
|
||||||
|
style: GoogleFonts.merienda(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
],
|
||||||
|
...fieldGroups.map((group) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: group.asMap().entries.map((entry) {
|
||||||
|
final index = entry.key;
|
||||||
|
final field = entry.value;
|
||||||
|
|
||||||
|
return [
|
||||||
|
Expanded(
|
||||||
|
flex: field.flex,
|
||||||
|
child: CustomAppTextField(
|
||||||
|
controller: field.controller,
|
||||||
|
labelText: field.label,
|
||||||
|
hintText: field.hint,
|
||||||
|
obscureText: field.isPassword,
|
||||||
|
keyboardType: field.keyboardType ?? TextInputType.text,
|
||||||
|
validator: field.validator,
|
||||||
|
style: CustomAppTextFieldStyle.beige,
|
||||||
|
fieldWidth: double.infinity, // CORRECTION PRINCIPALE
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Ajouter un espaceur entre les champs (sauf pour le dernier)
|
||||||
|
if (index < group.length - 1)
|
||||||
|
const Expanded(
|
||||||
|
flex: 1,
|
||||||
|
child: SizedBox(), // Espacement de 4% comme dans l'original
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}).expand((element) => element).toList(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
if (onSubmit != null)
|
||||||
|
Center(
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: onSubmit,
|
||||||
|
child: Text(submitLabel),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name ynov.ptits-pas.fr;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Gestion des erreurs
|
||||||
|
error_page 404 /index.html;
|
||||||
|
}
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="fr">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>P'titsPas - Connexion</title>
|
|
||||||
<link rel="stylesheet" href="styles.css">
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Merienda:wght@400;600&display=swap" rel="stylesheet">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<img src="assets/river.png" alt="" class="river" aria-hidden="true">
|
|
||||||
|
|
||||||
<main class="login-container">
|
|
||||||
<img src="assets/logo.png" alt="P'titsPas" class="logo">
|
|
||||||
<h1 class="slogan">Grandir pas à pas, sereinement</h1>
|
|
||||||
|
|
||||||
<form class="login-form">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="email">Adresse e-mail</label>
|
|
||||||
<input type="email" id="email" name="email"
|
|
||||||
placeholder="Votre adresse e-mail"
|
|
||||||
aria-label="Saisissez votre adresse e-mail"
|
|
||||||
required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="password">Mot de passe</label>
|
|
||||||
<input type="password" id="password" name="password"
|
|
||||||
placeholder="Votre mot de passe"
|
|
||||||
aria-label="Saisissez votre mot de passe"
|
|
||||||
required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" class="login-button">
|
|
||||||
Se connecter
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</main>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
/* Reset et styles de base */
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Merienda', cursive;
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Décor de fond */
|
|
||||||
.river {
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
width: 60vw;
|
|
||||||
opacity: 0.15;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Container principal */
|
|
||||||
.login-container {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 480px;
|
|
||||||
padding: 2rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Logo */
|
|
||||||
.logo {
|
|
||||||
max-width: 220px;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Slogan */
|
|
||||||
.slogan {
|
|
||||||
font-family: 'Merienda', cursive;
|
|
||||||
text-align: center;
|
|
||||||
color: #3a3a3a;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Formulaire */
|
|
||||||
.login-form {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Labels */
|
|
||||||
label {
|
|
||||||
color: #3a3a3a;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Champs de saisie */
|
|
||||||
input {
|
|
||||||
height: 80px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 30px;
|
|
||||||
padding: 0 1.2rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-family: inherit;
|
|
||||||
background-size: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="email"] {
|
|
||||||
background-image: url('assets/field_email.png');
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="password"] {
|
|
||||||
background-image: url('assets/field_password.png');
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Bouton de connexion */
|
|
||||||
.login-button {
|
|
||||||
height: 80px;
|
|
||||||
background-image: url('assets/btn_green.png');
|
|
||||||
background-size: cover;
|
|
||||||
border: none;
|
|
||||||
border-radius: 40px;
|
|
||||||
color: #ffffff;
|
|
||||||
font-family: "Merienda", cursive;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: filter 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-button:hover {
|
|
||||||
filter: brightness(1.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Media queries pour mobile */
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.river {
|
|
||||||
width: 40vw;
|
|
||||||
opacity: 0.10;
|
|
||||||
clip-path: inset(0 0 20% 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
max-width: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-container {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slogan {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
|
||||||
import 'package:p_tits_pas/screens/auth/login_screen.dart';
|
|
||||||
import 'package:p_tits_pas/screens/auth/register_choice_screen.dart';
|
|
||||||
import 'package:p_tits_pas/screens/auth/parent_register_step1_screen.dart';
|
|
||||||
import 'package:p_tits_pas/screens/auth/parent_register_step2_screen.dart';
|
|
||||||
import 'package:p_tits_pas/screens/auth/parent_register_step3_screen.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
// TODO: Initialiser SharedPreferences, Provider, etc.
|
|
||||||
runApp(const MyApp());
|
|
||||||
}
|
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
|
||||||
const MyApp({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return MaterialApp(
|
|
||||||
title: 'P\'titsPas',
|
|
||||||
theme: ThemeData(
|
|
||||||
primarySwatch: Colors.blue, // TODO: Utiliser la palette de la charte graphique
|
|
||||||
textTheme: GoogleFonts.merriweatherTextTheme(
|
|
||||||
Theme.of(context).textTheme,
|
|
||||||
),
|
|
||||||
visualDensity: VisualDensity.adaptivePlatformDensity,
|
|
||||||
),
|
|
||||||
// Gestionnaire de routes initial (simple pour l'instant)
|
|
||||||
initialRoute: '/', // Ou '/login' selon le point d'entrée désiré
|
|
||||||
routes: {
|
|
||||||
'/': (context) => const LoginScreen(), // Exemple, pourrait être RegisterChoiceScreen aussi
|
|
||||||
'/login': (context) => const LoginScreen(),
|
|
||||||
'/register-choice': (context) => const RegisterChoiceScreen(),
|
|
||||||
'/parent-register/step1': (context) => const ParentRegisterStep1Screen(),
|
|
||||||
'/parent-register/step2': (context) => const ParentRegisterStep2Screen(),
|
|
||||||
'/parent-register/step3': (context) => const ParentRegisterStep3Screen(),
|
|
||||||
// TODO: Ajouter les autres routes (step 4, etc., dashboard...)
|
|
||||||
},
|
|
||||||
// Gestion des routes inconnues
|
|
||||||
onUnknownRoute: (settings) {
|
|
||||||
return MaterialPageRoute(
|
|
||||||
builder: (context) => Scaffold(
|
|
||||||
body: Center(
|
|
||||||
child: Text(
|
|
||||||
'Route inconnue :\n${settings.name}',
|
|
||||||
style: GoogleFonts.merriweather(fontSize: 20, color: Colors.red),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
CustomAppTextField(
|
|
||||||
controller: _firstNameController,
|
|
||||||
labelText: 'Prénom',
|
|
||||||
hintText: 'Facultatif si à naître',
|
|
||||||
isRequired: !widget.childData.isUnbornChild,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 6.0),
|
|
||||||
CustomAppTextField(
|
|
||||||
controller: _lastNameController,
|
|
||||||
labelText: 'Nom',
|
|
||||||
hintText: 'Nom de l\'enfant',
|
|
||||||
enabled: true,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 9.0),
|
|
||||||
CustomAppTextField(
|
|
||||||
controller: _dobController,
|
|
||||||
labelText: widget.childData.isUnbornChild ? 'Date prévisionnelle de naissance' : 'Date de naissance',
|
|
||||||
hintText: 'JJ/MM/AAAA',
|
|
||||||
readOnly: true,
|
|
||||||
onTap: widget.onDateSelect,
|
|
||||||
suffixIcon: Icons.calendar_today,
|
|
||||||
),
|
|
||||||
Reference in New Issue
Block a user