Author SHA1 Message Date
Hanim 9fd6cb7b76 feat: Implement authentication service and login handling with role-based navigation 2025-09-03 11:03:53 +02:00
hmoussa 864d72eb40 Merge branch 'master' into dev 2025-09-01 10:05:08 +00:00
hmoussa 04ab6e0a7e Merge pull request 'feature/FRONT-07' (#62) from feature/FRONT-07 into dev
Reviewed-on: #62
2025-09-01 10:02:05 +00:00
Hanim 2b377db1c6 feat: Add route for ParentRegisterStep1Screen in AppRouter 2025-09-01 11:58:14 +02:00
Hanim 9f874f30e7 feat: Add dashboard layout with sidebar and main content area
- Implemented AppFooter widget for mobile and desktop views.
- Created ChildrenSidebar widget to display children's information.
- Developed AppLayout to manage app structure with optional footer.
- Added ChildrenSidebar for selecting children and displaying their status.
- Introduced DashboardAppBar for navigation and user actions.
- Built WMainContentArea for displaying assistant details and calendar.
- Created MainContentArea to manage contracts and events display.
- Implemented MessagingSidebar for messaging functionality.
- Updated widget tests to reflect new structure and imports.
2025-08-28 12:58:44 +02:00
jmartin 979114b93d Nettoyage et tet hook 2025-08-26 17:49:11 +02:00
ynov-deploy ee940e25b7 Merge branch 'master' of https://git.ptits-pas.fr/Ynov/ptitspas-ynov 2025-08-26 12:09:31 +02:00
ynov-deploy 10a5cb1fed Ajout: Configuration développement local pour le frontend 2025-08-26 11:59:45 +02:00
hmoussa bd2139b3aa Merge pull request 'feat: Add legal and privacy routes to AppRouter' (#53) from dev into master
Reviewed-on: #53
2025-08-25 09:29:25 +00:00
Hanim 7d97de3086 feat: Add legal and privacy routes to AppRouter 2025-08-25 11:27:07 +02:00
hmoussa 1e9803a4a7 Merge pull request 'dev' (#51) from dev into master
Reviewed-on: #51
2025-08-22 08:26:30 +00:00
ynov-deploy 74c56b900e Fix: Version Flutter pour compatibilité Dart 3.2.6+ 2025-08-22 00:06:10 +02:00
ynov-deploy 4caec0a104 Config: Adaptation frontend pour déploiement serveur
- Dockerfile optimisé avec build multi-stage
- Configuration nginx pour ynov.ptits-pas.fr
- Correction URL API: supernounou.local → ynov.ptits-pas.fr/api
- Support SPA avec try_files pour Flutter routing
2025-08-21 23:55:43 +02:00
hmoussa bfafbb955b Merge pull request 'feature/FRONT-05b' (#49) from feature/FRONT-05b into dev
Reviewed-on: #49
2025-08-19 10:24:17 +00:00
Hanim 332f4be7d7 feat: Update identity information labels in registration screens for clarity 2025-08-18 16:48:04 +02:00
Hanim 01a7937004 feat: Implement multi-step registration for Childminder with data validation and summary display
- Added routes for registration steps 2, 3, and 4 in app_router.dart.
- Created AmRegisterStep2Screen for entering professional details including birth date, city, country, social security number, agreement number, and max children.
- Implemented validation for social security number and max children fields.
- Developed AmRegisterStep3Screen for entering a motivation message and accepting terms and conditions.
- Created AmRegisterStep4Screen to display a summary of the registration data for review before submission.
- Introduced SummaryCard widget for displaying user information in a structured format.
- Enhanced DataGenerator utility to provide realistic data for testing.
2025-08-18 16:37:27 +02:00
hmoussa 9418932791 Merge pull request 'feature/FRONT-05a' (#48) from feature/FRONT-05a into dev
Reviewed-on: #48
2025-08-14 13:38:10 +00:00
Hanim acda4244a9 feat: Update registration data model for Childminder and adjust related screen AM logic 2025-08-14 11:33:40 +02:00
Hanim 14efccc711 feat: Refactor user AM Part 1 registration data model and update registration screens 2025-08-12 16:16:54 +02:00
hmoussa 916d83755f Merge pull request 'feat: Add Flutter Code Check workflow' (#47) from feature/FRONT-02 into dev
Reviewed-on: #47
2025-08-12 14:00:51 +00:00
Hanim a442766096 feat: Add Flutter Code Check workflow 2025-08-12 15:58:52 +02:00
hmoussa 8bd1694f70 Merge pull request 'feat: Implement Parent Registration Step 4 and Step 5 Screens' (#46) from feature/FRONT-01 into dev
Reviewed-on: #46
2025-08-12 13:54:32 +00:00
77 changed files with 4122 additions and 4201 deletions
+4
View File
@@ -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
+1
View File
@@ -52,3 +52,4 @@ Xcf/**
# Release notes
CHANGELOG.md
Ressources/
.env
+62
View File
@@ -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
```
-3320
View File
File diff suppressed because it is too large Load Diff
-36
View File
@@ -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"
}
}
-108
View File
@@ -1,108 +0,0 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// Modèle pour les parents
model Parent {
id String @id @default(uuid())
email String @unique
password String
firstName String
lastName String
phoneNumber String?
address String?
status AccountStatus @default(PENDING)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
children Child[]
contracts Contract[]
}
// Modèle pour les enfants
model Child {
id String @id @default(uuid())
firstName String
dateOfBirth DateTime
photoUrl String?
photoConsent Boolean @default(false)
isMultiple Boolean @default(false)
isUnborn Boolean @default(false)
parentId String
parent Parent @relation(fields: [parentId], references: [id])
contracts Contract[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// Modèle pour les contrats
model Contract {
id String @id @default(uuid())
parentId String
childId String
startDate DateTime
endDate DateTime?
status ContractStatus @default(ACTIVE)
parent Parent @relation(fields: [parentId], references: [id])
child Child @relation(fields: [childId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// Modèle pour les thèmes
model Theme {
id String @id @default(uuid())
name String @unique
primaryColor String
secondaryColor String
backgroundColor String
textColor String
isActive Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
appSettings AppSettings[]
}
// Modèle pour les paramètres de l'application
model AppSettings {
id String @id @default(uuid())
currentThemeId String
currentTheme Theme @relation(fields: [currentThemeId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([currentThemeId])
}
// Modèle pour les administrateurs
model Admin {
id String @id @default(uuid())
email String @unique
password String
firstName String
lastName String
passwordChanged Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// Enums
enum AccountStatus {
PENDING
VALIDATED
REJECTED
SUSPENDED
}
enum ContractStatus {
ACTIVE
ENDED
CANCELLED
}
-18
View File
@@ -1,18 +0,0 @@
import { Controller, Post, Body, UseGuards, Req } from '@nestjs/common';
import { AdminService } from './admin.service';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
@Controller('admin')
export class AdminController {
constructor(private readonly adminService: AdminService) {}
@Post('change-password')
@UseGuards(JwtAuthGuard)
async changePassword(
@Req() req,
@Body('oldPassword') oldPassword: string,
@Body('newPassword') newPassword: string,
) {
return this.adminService.changePassword(req.user.id, oldPassword, newPassword);
}
}
-18
View File
@@ -1,18 +0,0 @@
import { Module } from '@nestjs/common';
import { AdminController } from './admin.controller';
import { AdminService } from './admin.service';
import { PrismaModule } from '../prisma/prisma.module';
import { JwtModule } from '@nestjs/jwt';
@Module({
imports: [
PrismaModule,
JwtModule.register({
secret: process.env.JWT_SECRET,
signOptions: { expiresIn: '1d' },
}),
],
controllers: [AdminController],
providers: [AdminService],
})
export class AdminModule {}
-40
View File
@@ -1,40 +0,0 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { PrismaService } from '../prisma/prisma.service';
import * as bcrypt from 'bcrypt';
@Injectable()
export class AdminService {
constructor(
private prisma: PrismaService,
private jwtService: JwtService,
) {}
async changePassword(adminId: string, oldPassword: string, newPassword: string) {
// Récupérer l'administrateur
const admin = await this.prisma.admin.findUnique({
where: { id: adminId },
});
if (!admin) {
throw new UnauthorizedException('Administrateur non trouvé');
}
// Vérifier l'ancien mot de passe
const isPasswordValid = await bcrypt.compare(oldPassword, admin.password);
if (!isPasswordValid) {
throw new UnauthorizedException('Ancien mot de passe incorrect');
}
// Hasher le nouveau mot de passe
const hashedPassword = await bcrypt.hash(newPassword, 10);
// Mettre à jour le mot de passe
await this.prisma.admin.update({
where: { id: adminId },
data: { password: hashedPassword },
});
return { message: 'Mot de passe modifié avec succès' };
}
}
-17
View File
@@ -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' });
}
}
}
-28
View File
@@ -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}`);
});
-95
View File
@@ -1,95 +0,0 @@
import { Router } from 'express';
import { PrismaClient } from '@prisma/client';
import * as bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
const router = Router();
const prisma = new PrismaClient();
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
// Route de connexion
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
// Vérifier les identifiants
const admin = await prisma.admin.findUnique({
where: { email }
});
if (!admin) {
return res.status(401).json({ error: 'Identifiants invalides' });
}
// Vérifier le mot de passe
const validPassword = await bcrypt.compare(password, admin.password);
if (!validPassword) {
return res.status(401).json({ error: 'Identifiants invalides' });
}
// Vérifier si le mot de passe doit être changé
if (!admin.passwordChanged) {
return res.status(403).json({
error: 'Changement de mot de passe requis',
requiresPasswordChange: true
});
}
// Générer le token JWT
const token = jwt.sign(
{
id: admin.id,
email: admin.email,
role: 'admin'
},
JWT_SECRET,
{ expiresIn: '24h' }
);
res.json({ token });
} catch (error) {
console.error('Erreur lors de la connexion:', error);
res.status(500).json({ error: 'Erreur serveur' });
}
});
// Route de changement de mot de passe
router.post('/change-password', async (req, res) => {
try {
const { email, currentPassword, newPassword } = req.body;
// Vérifier l'administrateur
const admin = await prisma.admin.findUnique({
where: { email }
});
if (!admin) {
return res.status(404).json({ error: 'Administrateur non trouvé' });
}
// Vérifier l'ancien mot de passe
const validPassword = await bcrypt.compare(currentPassword, admin.password);
if (!validPassword) {
return res.status(401).json({ error: 'Mot de passe actuel incorrect' });
}
// Hasher le nouveau mot de passe
const hashedPassword = await bcrypt.hash(newPassword, 10);
// Mettre à jour le mot de passe
await prisma.admin.update({
where: { id: admin.id },
data: {
password: hashedPassword,
passwordChanged: true
}
});
res.json({ message: 'Mot de passe changé avec succès' });
} catch (error) {
console.error('Erreur lors du changement de mot de passe:', error);
res.status(500).json({ error: 'Erreur serveur' });
}
});
export default router;
-14
View File
@@ -1,14 +0,0 @@
import { Router } from 'express';
import { ThemeController } from '../controllers/theme.controller';
const router = Router();
// Routes pour les thèmes
router.post('/', ThemeController.createTheme);
router.get('/', ThemeController.getAllThemes);
router.get('/active', ThemeController.getActiveTheme);
router.put('/:themeId/activate', ThemeController.activateTheme);
router.put('/:themeId', ThemeController.updateTheme);
router.delete('/:themeId', ThemeController.deleteTheme);
export default router;
-39
View File
@@ -1,39 +0,0 @@
import { PrismaClient } from '@prisma/client';
import * as bcrypt from 'bcrypt';
const prisma = new PrismaClient();
async function main() {
try {
// Vérifier si l'administrateur existe déjà
const existingAdmin = await prisma.admin.findUnique({
where: { email: 'administrateur@ptitspas.fr' }
});
if (!existingAdmin) {
// Hasher le mot de passe
const hashedPassword = await bcrypt.hash('password', 10);
// Créer l'administrateur
await prisma.admin.create({
data: {
email: 'administrateur@ptitspas.fr',
password: hashedPassword,
firstName: 'Administrateur',
lastName: 'P\'titsPas',
passwordChanged: false
}
});
console.log('✅ Administrateur créé avec succès');
} else {
console.log('️ L\'administrateur existe déjà');
}
} catch (error) {
console.error('❌ Erreur lors de la création de l\'administrateur:', error);
} finally {
await prisma.$disconnect();
}
}
main();
-77
View File
@@ -1,77 +0,0 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export interface ThemeData {
name: string;
primaryColor: string;
secondaryColor: string;
backgroundColor: string;
textColor: string;
}
export class ThemeService {
// Créer un nouveau thème
static async createTheme(data: ThemeData) {
return prisma.theme.create({
data: {
...data,
isActive: false,
},
});
}
// Récupérer tous les thèmes
static async getAllThemes() {
return prisma.theme.findMany();
}
// Récupérer le thème actif
static async getActiveTheme() {
const settings = await prisma.appSettings.findFirst({
include: {
currentTheme: true,
},
});
return settings?.currentTheme;
}
// Activer un thème
static async activateTheme(themeId: string) {
// Désactiver tous les thèmes
await prisma.theme.updateMany({
where: { isActive: true },
data: { isActive: false },
});
// Activer le thème sélectionné
const updatedTheme = await prisma.theme.update({
where: { id: themeId },
data: { isActive: true },
});
// Mettre à jour les paramètres de l'application
await prisma.appSettings.upsert({
where: { id: '1' },
update: { currentThemeId: themeId },
create: { id: '1', currentThemeId: themeId },
});
return updatedTheme;
}
// Mettre à jour un thème
static async updateTheme(themeId: string, data: Partial<ThemeData>) {
return prisma.theme.update({
where: { id: themeId },
data,
});
}
// Supprimer un thème
static async deleteTheme(themeId: string) {
return prisma.theme.delete({
where: { id: themeId },
});
}
}
-28
View File
@@ -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"]
}
+21
View File
@@ -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
+34
View File
@@ -0,0 +1,34 @@
name: Flutter Code Check
on:
push:
branches: [main, dev, feature/*, hotfix]
pull_request:
branches: [main, dev]
jobs:
flutter-check:
name: Analyse & Test Flutter
runs-on: ubuntu-latest
steps:
- name: ⬇️ Checkout code
uses: actions/checkout@v3
- name: 💡 Set up Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.19.0' # ou celle que tu utilises
channel: stable
- name: 📦 Install dependencies
run: flutter pub get
- name: 🔍 Dart Analyzer
run: flutter analyze
# - name: 🧪 Run tests (if present)
# run: flutter test || echo "No tests found"
- name: 🧱 Build (Flutter Web)
run: flutter build web --release
+16
View File
@@ -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;"]
@@ -20,6 +20,11 @@ public final class GeneratedPluginRegistrant {
} catch (Exception e) {
Log.e(TAG, "Error registering plugin flutter_plugin_android_lifecycle, io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin", e);
}
try {
flutterEngine.getPlugins().add(new com.it_nomads.fluttersecurestorage.FlutterSecureStoragePlugin());
} catch (Exception e) {
Log.e(TAG, "Error registering plugin flutter_secure_storage, com.it_nomads.fluttersecurestorage.FlutterSecureStoragePlugin", e);
}
try {
flutterEngine.getPlugins().add(new io.flutter.plugins.imagepicker.ImagePickerPlugin());
} catch (Exception e) {
+14
View File
@@ -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,138 @@
import 'package:flutter/material.dart';
import 'package:p_tits_pas/models/m_dashbord/assistant_model.dart';
import 'package:p_tits_pas/models/m_dashbord/child_model.dart';
import 'package:p_tits_pas/models/m_dashbord/contract_model.dart';
import 'package:p_tits_pas/models/m_dashbord/conversation_model.dart';
import 'package:p_tits_pas/models/m_dashbord/event_model.dart';
import 'package:p_tits_pas/models/m_dashbord/notification_model.dart';
import 'package:p_tits_pas/services/dashboardService.dart';
class ParentDashboardController extends ChangeNotifier {
final DashboardService _dashboardService;
ParentDashboardController(this._dashboardService);
// État des données
List<ChildModel> _children = [];
String? _selectedChildId;
AssistantModel? _selectedAssistant;
List<EventModel> _upcomingEvents = [];
List<ContractModel> _contracts = [];
List<ConversationModel> _conversations = [];
List<NotificationModel> _notifications = [];
// État de chargement
bool _isLoading = false;
String? _error;
// Getters
List<ChildModel> get children => _children;
String? get selectedChildId => _selectedChildId;
ChildModel? get selectedChild => _children.where((c) => c.id == _selectedChildId).firstOrNull;
AssistantModel? get selectedAssistant => _selectedAssistant;
List<EventModel> get upcomingEvents => _upcomingEvents;
List<ContractModel> get contracts => _contracts;
List<ConversationModel> get conversations => _conversations;
List<NotificationModel> get notifications => _notifications;
bool get isLoading => _isLoading;
String? get error => _error;
// Initialisation du dashboard
Future<void> initDashboard() async {
_isLoading = true;
_error = null;
notifyListeners();
try {
await Future.wait([
_loadChildren(),
_loadUpcomingEvents(),
_loadContracts(),
_loadConversations(),
_loadNotifications(),
]);
// Sélectionner le premier enfant par défaut
if (_children.isNotEmpty && _selectedChildId == null) {
await selectChild(_children.first.id);
}
} catch (e) {
_error = 'Erreur lors du chargement du tableau de bord: $e';
} finally {
_isLoading = false;
notifyListeners();
}
}
// Sélection d'un enfant
Future<void> selectChild(String childId) async {
_selectedChildId = childId;
notifyListeners();
// Charger les données spécifiques à cet enfant
await _loadChildSpecificData(childId);
}
// Afficher le modal d'ajout d'enfant
void showAddChildModal() {
// Logique pour ouvrir le modal d'ajout d'enfant
// Sera implémentée dans le ticket FRONT-09
}
// Méthodes privées de chargement des données
Future<void> _loadChildren() async {
_children = await _dashboardService.getChildren();
notifyListeners();
}
Future<void> _loadChildSpecificData(String childId) async {
try {
// Charger l'assistante maternelle associée à cet enfant
_selectedAssistant = await _dashboardService.getAssistantForChild(childId);
// Filtrer les événements et contrats pour cet enfant
_upcomingEvents = await _dashboardService.getEventsForChild(childId);
_contracts = await _dashboardService.getContractsForChild(childId);
notifyListeners();
} catch (e) {
_error = 'Erreur lors du chargement des données pour l\'enfant: $e';
notifyListeners();
}
}
Future<void> _loadUpcomingEvents() async {
_upcomingEvents = await _dashboardService.getUpcomingEvents();
notifyListeners();
}
Future<void> _loadContracts() async {
_contracts = await _dashboardService.getContracts();
notifyListeners();
}
Future<void> _loadConversations() async {
_conversations = await _dashboardService.getConversations();
notifyListeners();
}
Future<void> _loadNotifications() async {
_notifications = await _dashboardService.getNotifications();
notifyListeners();
}
// Méthodes d'action
Future<void> markNotificationAsRead(String notificationId) async {
try {
await _dashboardService.markNotificationAsRead(notificationId);
await _loadNotifications(); // Recharger les notifications
} catch (e) {
_error = 'Erreur lors du marquage de la notification: $e';
notifyListeners();
}
}
Future<void> refreshDashboard() async {
await initDashboard();
}
}
+1 -4
View File
@@ -1,10 +1,7 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; // Import pour la localisation
// import 'package:provider/provider.dart'; // Supprimer Provider
import 'package:flutter_localizations/flutter_localizations.dart';
import 'navigation/app_router.dart';
// import 'theme/app_theme.dart'; // Supprimer AppTheme
// import 'theme/theme_provider.dart'; // Supprimer ThemeProvider
void main() {
runApp(const MyApp()); // Exécution simple
@@ -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));
}
}
@@ -0,0 +1,51 @@
class AssistantModel {
final String id;
final String firstName;
final String lastName;
final String? photoUrl;
final double hourlyRate;
final double dailyFees;
final AssistantStatus status;
final String? address;
final String? phone;
final String? email;
AssistantModel({
required this.id,
required this.firstName,
required this.lastName,
this.photoUrl,
required this.hourlyRate,
required this.dailyFees,
required this.status,
this.address,
this.phone,
this.email,
});
factory AssistantModel.fromJson(Map<String, dynamic> json) {
return AssistantModel(
id: json['id'],
firstName: json['firstName'],
lastName: json['lastName'],
photoUrl: json['photoUrl'],
hourlyRate: json['hourlyRate'].toDouble(),
dailyFees: json['dailyFees'].toDouble(),
status: AssistantStatus.values.byName(json['status']),
address: json['address'],
phone: json['phone'],
email: json['email'],
);
}
String get fullName => '$firstName $lastName';
String get hourlyRateFormatted => '${hourlyRate.toStringAsFixed(2)} €/h';
String get dailyFeesFormatted => '${dailyFees.toStringAsFixed(2)} €/jour';
}
enum AssistantStatus {
available,
busy,
onHoliday,
unavailable,
}
@@ -0,0 +1,58 @@
class ChildModel {
final String id;
final String firstName;
final String? lastName;
final String? photoUrl;
final DateTime birthDate;
final ChildStatus status;
final String? assistantId;
ChildModel({
required this.id,
required this.firstName,
this.lastName,
this.photoUrl,
required this.birthDate,
required this.status,
this.assistantId,
});
factory ChildModel.fromJson(Map<String, dynamic> json) {
return ChildModel(
id: json['id'],
firstName: json['firstName'],
lastName: json['lastName'],
photoUrl: json['photoUrl'],
birthDate: DateTime.parse(json['birthDate']),
status: ChildStatus.values.byName(json['status']),
assistantId: json['assistantId'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'firstName': firstName,
'lastName': lastName,
'photoUrl': photoUrl,
'birthDate': birthDate.toIso8601String(),
'status': status.name,
'assistantId': assistantId,
};
}
String get fullName => lastName != null ? '$firstName $lastName' : firstName;
int get ageInMonths {
final now = DateTime.now();
return (now.year - birthDate.year) * 12 + (now.month - birthDate.month);
}
}
enum ChildStatus {
withAssistant, // En garde chez l'assistante
available, // Disponible
onHoliday, // En vacances
sick, // Malade
searching, // Recherche d'assistante
}
@@ -0,0 +1,65 @@
class ContractModel {
final String id;
final String childId;
final String assistantId;
final ContractStatus status;
final DateTime startDate;
final DateTime? endDate;
final double hourlyRate;
final Map<String, dynamic>? terms;
final DateTime createdAt;
final DateTime? signedAt;
ContractModel({
required this.id,
required this.childId,
required this.assistantId,
required this.status,
required this.startDate,
this.endDate,
required this.hourlyRate,
this.terms,
required this.createdAt,
this.signedAt,
});
factory ContractModel.fromJson(Map<String, dynamic> json) {
return ContractModel(
id: json['id'],
childId: json['childId'],
assistantId: json['assistantId'],
status: ContractStatus.values.byName(json['status']),
startDate: DateTime.parse(json['startDate']),
endDate: json['endDate'] != null ? DateTime.parse(json['endDate']) : null,
hourlyRate: json['hourlyRate'].toDouble(),
terms: json['terms'],
createdAt: DateTime.parse(json['createdAt']),
signedAt: json['signedAt'] != null ? DateTime.parse(json['signedAt']) : null,
);
}
bool get isActive => status == ContractStatus.active;
bool get needsSignature => status == ContractStatus.draft;
String get statusLabel {
switch (status) {
case ContractStatus.draft:
return 'Brouillon';
case ContractStatus.pending:
return 'En attente de validation';
case ContractStatus.active:
return 'En cours';
case ContractStatus.ended:
return 'Terminé';
case ContractStatus.cancelled:
return 'Annulé';
}
}
}
enum ContractStatus {
draft,
pending,
active,
ended,
cancelled,
}
@@ -0,0 +1,46 @@
class ConversationModel {
final String id;
final String title;
final List<String> participantIds;
final List<MessageModel> messages;
final DateTime lastMessageAt;
final int unreadCount;
final String? childId;
ConversationModel({
required this.id,
required this.title,
required this.participantIds,
required this.messages,
required this.lastMessageAt,
this.unreadCount = 0,
this.childId,
});
MessageModel? get lastMessage => messages.isNotEmpty ? messages.last : null;
bool get hasUnreadMessages => unreadCount > 0;
}
class MessageModel {
final String id;
final String content;
final String senderId;
final DateTime sentAt;
final bool isFromAI;
final MessageStatus status;
MessageModel({
required this.id,
required this.content,
required this.senderId,
required this.sentAt,
this.isFromAI = false,
required this.status,
});
}
enum MessageStatus {
sent,
delivered,
read,
}
@@ -0,0 +1,66 @@
class EventModel {
final String id;
final String title;
final String? description;
final DateTime startDate;
final DateTime? endDate;
final EventType type;
final EventStatus status;
final String? childId;
final String? assistantId;
final String? createdBy;
EventModel({
required this.id,
required this.title,
this.description,
required this.startDate,
this.endDate,
required this.type,
required this.status,
this.childId,
this.assistantId,
this.createdBy,
});
factory EventModel.fromJson(Map<String, dynamic> json) {
return EventModel(
id: json['id'],
title: json['title'],
description: json['description'],
startDate: DateTime.parse(json['startDate']),
endDate: json['endDate'] != null ? DateTime.parse(json['endDate']) : null,
type: EventType.values.byName(json['type']),
status: EventStatus.values.byName(json['status']),
childId: json['childId'],
assistantId: json['assistantId'],
createdBy: json['createdBy'],
);
}
bool get isMultiDay => endDate != null && !isSameDay(startDate, endDate!);
bool get isPending => status == EventStatus.pending;
bool get needsConfirmation => isPending && createdBy != 'current_user';
static bool isSameDay(DateTime date1, DateTime date2) {
return date1.year == date2.year &&
date1.month == date2.month &&
date1.day == date2.day;
}
}
enum EventType {
parentVacation, // Vacances parents
childAbsence, // Absence enfant
rpeActivity, // Activité RPE
assistantVacation, // Congés assistante maternelle
sickLeave, // Arrêt maladie
personalNote, // Note personnelle
}
enum EventStatus {
confirmed,
pending,
refused,
cancelled,
}
@@ -0,0 +1,42 @@
class NotificationModel {
final String id;
final String title;
final String content;
final NotificationType type;
final DateTime createdAt;
final bool isRead;
final String? actionUrl;
final Map<String, dynamic>? metadata;
NotificationModel({
required this.id,
required this.title,
required this.content,
required this.type,
required this.createdAt,
this.isRead = false,
this.actionUrl,
this.metadata,
});
factory NotificationModel.fromJson(Map<String, dynamic> json) {
return NotificationModel(
id: json['id'],
title: json['title'],
content: json['content'],
type: NotificationType.values.byName(json['type']),
createdAt: DateTime.parse(json['createdAt']),
isRead: json['isRead'] ?? false,
actionUrl: json['actionUrl'],
metadata: json['metadata'],
);
}
}
enum NotificationType {
newEvent, // Nouvel événement
fileModified, // Dossier modifié
contractPending, // Contrat en attente
paymentPending, // Paiement en attente
unreadMessage, // Message non lu
}
+61 -7
View File
@@ -1,5 +1,13 @@
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/home/parent_screen/ParentDashboardScreen.dart';
import 'package:p_tits_pas/screens/home/parent_screen/find_nanny.dart';
import 'package:p_tits_pas/screens/legal/legal_page.dart';
import 'package:p_tits_pas/screens/legal/privacy_page.dart';
import '../screens/auth/login_screen.dart';
import '../screens/auth/register_choice_screen.dart';
import '../screens/auth/parent/parent_register_step1_screen.dart';
@@ -7,18 +15,25 @@ import '../screens/auth/parent/parent_register_step2_screen.dart';
import '../screens/auth/parent/parent_register_step3_screen.dart';
import '../screens/auth/parent/parent_register_step4_screen.dart';
import '../screens/auth/parent/parent_register_step5_screen.dart';
import '../screens/home/home_screen.dart';
import '../models/user_registration_data.dart';
import '../models/parent_user_registration_data.dart';
class AppRouter {
static const String login = '/login';
static const String registerChoice = '/register-choice';
static const String legal = '/legal';
static const String privacy = '/privacy';
static const String parentRegisterStep1 = '/parent-register/step1';
static const String parentRegisterStep2 = '/parent-register/step2';
static const String parentRegisterStep3 = '/parent-register/step3';
static const String parentRegisterStep4 = '/parent-register/step4';
static const String parentRegisterStep5 = '/parent-register/step5';
static const String home = '/home';
static const String amRegisterStep1 = '/am-register/step1';
static const String amRegisterStep2 = '/am-register/step2';
static const String amRegisterStep3 = '/am-register/step3';
static const String amRegisterStep4 = '/am-register/step4';
static const String parentDashboard = '/parent-dashboard';
static const String findNanny = '/find-nanny';
static Route<dynamic> generateRoute(RouteSettings settings) {
Widget screen;
@@ -38,8 +53,16 @@ class AppRouter {
screen = const RegisterChoiceScreen();
slideTransition = true;
break;
case legal:
screen = const LegalPage();
slideTransition = true;
break;
case privacy:
screen = const PrivacyPage();
slideTransition = true;
break;
case parentRegisterStep1:
screen = const ParentRegisterStep1Screen();
screen = ParentRegisterStep1Screen();
slideTransition = true;
break;
case parentRegisterStep2:
@@ -74,8 +97,39 @@ class AppRouter {
}
slideTransition = true;
break;
case home:
screen = const HomeScreen();
case amRegisterStep1:
screen = const AmRegisterStep1Screen();
slideTransition = true;
break;
case amRegisterStep2:
if (args is ChildminderRegistrationData) {
screen = AmRegisterStep2Screen(registrationData: args);
} else {
screen = AmRegisterStep2Screen(registrationData: ChildminderRegistrationData());
}
slideTransition = true;
break;
case amRegisterStep3:
if (args is ChildminderRegistrationData) {
screen = AmRegisterStep3Screen(registrationData: args);
} else {
screen = AmRegisterStep3Screen(registrationData: ChildminderRegistrationData());
}
slideTransition = true;
break;
case amRegisterStep4:
if (args is ChildminderRegistrationData) {
screen = AmRegisterStep4Screen(registrationData: args);
} else {
screen = AmRegisterStep4Screen(registrationData: ChildminderRegistrationData());
}
slideTransition = true;
break;
case parentDashboard:
screen = const ParentDashboardScreen();
break;
case findNanny:
screen = const FindNannyScreen();
break;
default:
screen = Scaffold(
@@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
class GestionnairesCreate extends StatelessWidget {
const GestionnairesCreate({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Créer un gestionnaire'),
),
body: const Center(
child: Text('Formulaire de création de gestionnaire'),
),
);
}
}
@@ -1,17 +1,281 @@
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
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
return Scaffold(
appBar: AppBar(
title: const Text('Étape 1 - Inscription AM'),
),
body: const Center(
child: Text('Contenu de l\'étape 1'),
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 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 didentité',
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);
},
),
],
);
},
);
}
}
+43 -5
View File
@@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:p_tits_pas/services/auth_service.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:p_tits_pas/services/bug_report_service.dart';
import 'package:go_router/go_router.dart';
@@ -19,6 +20,7 @@ class _LoginPageState extends State<LoginPage> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final AuthService _authService = AuthService();
@override
void dispose() {
@@ -47,6 +49,46 @@ class _LoginPageState extends State<LoginPage> {
return null;
}
Future<void> _handleLogin() async {
if (_formKey.currentState?.validate() ?? false) {
try {
final response = await _authService.login(
_emailController.text,
_passwordController.text,
);
if (!mounted) return;
// Navigation selon le rôle
switch (response.role.toLowerCase()) {
case 'parent':
Navigator.pushReplacementNamed(context, '/parent-dashboard');
break;
case 'assistante_maternelle':
Navigator.pushReplacementNamed(context, '/assistante_maternelle_dashboard');
break;
case 'admin':
Navigator.pushReplacementNamed(context, '/admin_dashboard');
break;
case 'gestionnaire':
Navigator.pushReplacementNamed(context, '/gestionnaire_dashboard');
break;
default:
Navigator.pushReplacementNamed(context, '/home');
}
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Échec de la connexion. Vérifiez vos identifiants.'),
backgroundColor: Colors.red,
),
);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -144,11 +186,7 @@ class _LoginPageState extends State<LoginPage> {
height: 40,
text: 'Se connecter',
textColor: const Color(0xFF2D6A4F),
onPressed: () {
if (_formKey.currentState?.validate() ?? false) {
// TODO: Implémenter la logique de connexion
}
},
onPressed: _handleLogin,
),
),
const SizedBox(height: 10),
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'dart:math' as math; // Pour la rotation du chevron
import '../../../models/user_registration_data.dart'; // Import du modèle de données
import '../../../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 '../../../widgets/custom_app_text_field.dart'; // Import du widget CustomAppTextField
import '../../../models/card_assets.dart'; // Import des enums de cartes
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'dart:math' as math; // Pour la rotation du chevron
import '../../../models/user_registration_data.dart'; // Import du modèle
import '../../../models/parent_user_registration_data.dart'; // Import du modèle
import '../../../utils/data_generator.dart'; // Import du générateur
import '../../../widgets/custom_app_text_field.dart'; // Import du widget
import '../../../models/card_assets.dart'; // Import des enums de cartes
@@ -9,7 +9,7 @@ import 'dart:io' show File, Platform; // Ajout de Platform
import 'package:flutter/foundation.dart' show kIsWeb; // Import pour kIsWeb
import '../../../widgets/custom_app_text_field.dart'; // Import du nouveau widget TextField
import '../../../widgets/app_custom_checkbox.dart'; // Import du nouveau widget Checkbox
import '../../../models/user_registration_data.dart'; // Import du modèle de données
import '../../../models/parent_user_registration_data.dart'; // Import du modèle de données
import '../../../utils/data_generator.dart'; // Import du générateur
import '../../../models/card_assets.dart'; // Import des enums de cartes
@@ -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 'package:p_tits_pas/widgets/app_custom_checkbox.dart'; // Import de la checkbox personnalisée
// import 'package:p_tits_pas/models/placeholder_registration_data.dart'; // Remplacé
import '../../../models/user_registration_data.dart'; // Import du vrai modèle
import '../../../models/parent_user_registration_data.dart'; // Import du vrai modèle
import '../../../utils/data_generator.dart'; // Import du générateur
import '../../../models/card_assets.dart'; // Import des enums de cartes
@@ -1,6 +1,7 @@
import 'package:flutter/material.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 '../../../models/card_assets.dart'; // Import des enums de cartes
import 'package:flutter/foundation.dart' show kIsWeb;
@@ -127,7 +128,7 @@ class ParentRegisterStep5Screen extends StatelessWidget {
const SizedBox(height: verticalSpacing),
_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,
title: 'Deuxième Parent',
content: details,
@@ -100,6 +100,8 @@ class RegisterChoiceScreen extends StatelessWidget {
onPressed: () {
// TODO: Naviguer vers l'écran d'inscription assmat
print('Choix: Assistante Maternelle');
Navigator.pushNamed(context, '/am-register/step1');
},
),
],
+1 -1
View File
@@ -14,4 +14,4 @@ class HomeScreen extends StatelessWidget {
),
);
}
}
}
@@ -0,0 +1,242 @@
import 'package:flutter/material.dart';
import 'package:p_tits_pas/controllers/parent_dashboard_controller.dart';
import 'package:p_tits_pas/services/dashboardService.dart';
import 'package:p_tits_pas/widgets/app_footer.dart';
import 'package:p_tits_pas/widgets/dashbord_parent/app_layout.dart';
import 'package:p_tits_pas/widgets/dashbord_parent/children_sidebar.dart';
import 'package:p_tits_pas/widgets/dashbord_parent/dashboard_app_bar.dart';
import 'package:p_tits_pas/widgets/dashbord_parent/wid_dashbord.dart';
import 'package:p_tits_pas/widgets/main_content_area.dart';
import 'package:p_tits_pas/widgets/messaging_sidebar.dart';
import 'package:provider/provider.dart';
class ParentDashboardScreen extends StatefulWidget {
const ParentDashboardScreen({Key? key}) : super(key: key);
@override
State<ParentDashboardScreen> createState() => _ParentDashboardScreenState();
}
class _ParentDashboardScreenState extends State<ParentDashboardScreen> {
int selectedIndex = 0;
void onTabChange(int index) {
setState(() {
selectedIndex = index;
});
}
@override
void initState() {
super.initState();
// Initialiser les données du dashboard
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<ParentDashboardController>().initDashboard();
});
}
Widget _getBody() {
switch (selectedIndex) {
case 0:
return Dashbord_body();
case 1:
return const Center(child: Text("🔍 Trouver une nounou"));
case 2:
return const Center(child: Text("⚙️ Paramètres"));
default:
return const Center(child: Text("Page non trouvée"));
}
}
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => ParentDashboardController(DashboardService())..initDashboard(),
child: Scaffold(
appBar: PreferredSize(preferredSize: const Size.fromHeight(60.0),
child: Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: Colors.grey.shade300),
),
),
child: DashboardAppBar(
selectedIndex: selectedIndex,
onTabChange: onTabChange,
),
),
),
body: Column(
children: [
Expanded (child: _getBody(),
),
const AppFooter(),
],
),
)
// body: _buildResponsiveBody(context, controller),
// footer: const AppFooter(),
);
}
Widget _buildResponsiveBody(BuildContext context, ParentDashboardController controller) {
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth < 768) {
// Layout mobile : colonnes empilées
return _buildMobileLayout(controller);
} else if (constraints.maxWidth < 1024) {
// Layout tablette : 2 colonnes
return _buildTabletLayout(controller);
} else {
// Layout desktop : 3 colonnes
return _buildDesktopLayout(controller);
}
},
);
}
Widget _buildDesktopLayout(ParentDashboardController controller) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Sidebar gauche - Enfants
SizedBox(
width: 280,
child: ChildrenSidebar(
children: controller.children,
selectedChildId: controller.selectedChildId,
onChildSelected: controller.selectChild,
onAddChild: controller.showAddChildModal,
),
),
// Contenu central
Expanded(
flex: 2,
child: MainContentArea(
selectedChild: controller.selectedChild,
selectedAssistant: controller.selectedAssistant,
events: controller.upcomingEvents,
contracts: controller.contracts,
),
),
// Sidebar droite - Messagerie
SizedBox(
width: 320,
child: MessagingSidebar(
conversations: controller.conversations,
notifications: controller.notifications,
),
),
],
);
}
Widget _buildTabletLayout(ParentDashboardController controller) {
return Row(
children: [
// Sidebar enfants plus étroite
SizedBox(
width: 240,
child: ChildrenSidebar(
children: controller.children,
selectedChildId: controller.selectedChildId,
onChildSelected: controller.selectChild,
onAddChild: controller.showAddChildModal,
isCompact: true,
),
),
// Contenu principal avec messagerie intégrée
Expanded(
child: Column(
children: [
Expanded(
flex: 2,
child: MainContentArea(
selectedChild: controller.selectedChild,
selectedAssistant: controller.selectedAssistant,
events: controller.upcomingEvents,
contracts: controller.contracts,
),
),
SizedBox(
height: 200,
child: MessagingSidebar(
conversations: controller.conversations,
notifications: controller.notifications,
isCompact: true,
),
),
],
),
),
],
);
}
Widget _buildMobileLayout(ParentDashboardController controller) {
return DefaultTabController(
length: 4,
child: Column(
children: [
// Navigation par onglets sur mobile
Container(
color: Theme.of(context).primaryColor.withOpacity(0.1),
child: const TabBar(
isScrollable: true,
tabs: [
Tab(text: 'Enfants', icon: Icon(Icons.child_care)),
Tab(text: 'Planning', icon: Icon(Icons.calendar_month)),
Tab(text: 'Contrats', icon: Icon(Icons.description)),
Tab(text: 'Messages', icon: Icon(Icons.message)),
],
),
),
Expanded(
child: TabBarView(
children: [
// Onglet Enfants
ChildrenSidebar(
children: controller.children,
selectedChildId: controller.selectedChildId,
onChildSelected: controller.selectChild,
onAddChild: controller.showAddChildModal,
isMobile: true,
),
// Onglet Planning
MainContentArea(
selectedChild: controller.selectedChild,
selectedAssistant: controller.selectedAssistant,
events: controller.upcomingEvents,
contracts: controller.contracts,
showOnlyCalendar: true,
),
// Onglet Contrats
MainContentArea(
selectedChild: controller.selectedChild,
selectedAssistant: controller.selectedAssistant,
events: controller.upcomingEvents,
contracts: controller.contracts,
showOnlyContracts: true,
),
// Onglet Messages
MessagingSidebar(
conversations: controller.conversations,
notifications: controller.notifications,
isMobile: true,
),
],
),
),
],
),
);
}
}
@@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
class FindNannyScreen extends StatelessWidget {
const FindNannyScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Trouver une nounou"),
),
body: Center(
child: const Text("Contenu de la page Trouver une nounou"),
),
);
}
}
+21
View File
@@ -0,0 +1,21 @@
class ApiConfig {
// static const String baseUrl = 'https://ynov.ptits-pas.fr/api/v1';
static const String baseUrl = 'http://localhost:3000/api/v1';
// Auth endpoints
static const String login = '/auth/login';
static const String register = '/auth/register';
static const String refreshToken = '/auth/refresh';
// Users endpoints
static const String users = '/users';
static const String userProfile = '/users/profile';
static const String userChildren = '/users/children';
// Dashboard endpoints
static const String dashboard = '/dashboard';
static const String events = '/events';
static const String contracts = '/contracts';
static const String conversations = '/conversations';
static const String notifications = '/notifications';
}
+72 -3
View File
@@ -1,9 +1,78 @@
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:p_tits_pas/services/api/api_config.dart';
import '../models/user.dart';
import 'package:http/http.dart' as http;
class AuthResponse {
final String acessToken;
final String role;
AuthResponse({required this.acessToken, required this.role});
factory AuthResponse.fromJson(Map<String, dynamic> json) {
return AuthResponse(
acessToken: json['acessToken'],
role: json['role'],
);
}
}
class AuthService {
static const String _usersKey = 'users';
ApiConfig apiConfig = ApiConfig();
String baseUrl = ApiConfig.baseUrl;
final storage = const FlutterSecureStorage();
//login
Future<AuthResponse> login(String email, String password) async {
final response = await http.post(
Uri.parse('$baseUrl${ApiConfig.login}'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'email': email, 'password': password}),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
final authResponse = AuthResponse.fromJson(data);
await storage.write(key: 'access_token', value: authResponse.acessToken);
await storage.write(key: 'role', value: authResponse.role);
return authResponse;
} else {
throw Exception('Failed to login');
}
}
//register
Future<AppUser> register({
required String email,
required String password,
required String firstName,
required String lastName,
required String role,
}) async {
final response = await http.post(
Uri.parse('$baseUrl${ApiConfig.register}'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'email': email,
'password': password,
'firstName': firstName,
'lastName': lastName,
'role': role,
}),
);
if (response.statusCode == 201) {
final data = jsonDecode(response.body);
return AppUser.fromJson(data['user']);
} else {
throw Exception('Failed to register');
}
}
/*static const String _usersKey = 'users';
static const String _parentsKey = 'parents';
static const String _childrenKey = 'children';
@@ -38,5 +107,5 @@ class AuthService {
// Méthode pour récupérer l'utilisateur connecté (mode démonstration)
static Future<AppUser?> getCurrentUser() async {
return null; // Aucun utilisateur en mode démonstration
}
}*/
}
@@ -1,8 +1,9 @@
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:p_tits_pas/config/env.dart';
class BugReportService {
static const String _apiUrl = 'https://api.supernounou.local/bug-reports';
static final String _apiUrl = Env.apiV1('/bug-reports');
static Future<void> sendReport(String description) async {
try {
+202
View File
@@ -0,0 +1,202 @@
import 'package:p_tits_pas/models/m_dashbord/assistant_model.dart';
import 'package:p_tits_pas/models/m_dashbord/child_model.dart';
import 'package:p_tits_pas/models/m_dashbord/contract_model.dart';
import 'package:p_tits_pas/models/m_dashbord/conversation_model.dart';
import 'package:p_tits_pas/models/m_dashbord/event_model.dart';
import 'package:p_tits_pas/models/m_dashbord/notification_model.dart';
class DashboardService {
// URL de base de l'API
static const String _baseUrl = 'YOUR_API_BASE_URL';
// Récupérer la liste des enfants
Future<List<ChildModel>> getChildren() async {
try {
// TODO: Implémenter l'appel API
// Exemple de mock data pour le développement
return [
ChildModel(
id: '1',
firstName: 'Emma',
birthDate: DateTime(2020, 5, 15),
photoUrl: 'assets/images/child1.jpg',
status: ChildStatus.onHoliday,
),
ChildModel(
id: '2',
firstName: 'Lucas',
birthDate: DateTime(2021, 3, 10),
photoUrl: 'assets/images/child2.jpg',
status: ChildStatus.searching,
),
];
} catch (e) {
throw Exception('Erreur lors de la récupération des enfants: $e');
}
}
// Récupérer l'assistante maternelle pour un enfant
Future<AssistantModel> getAssistantForChild(String childId) async {
try {
// TODO: Implémenter l'appel API
return AssistantModel(
id: 'am1',
firstName: 'Marie',
lastName: 'Dupont',
hourlyRate: 10.0,
dailyFees: 80.0,
status: AssistantStatus.available,
photoUrl: 'assets/images/assistant1.jpg',
address: '123 rue des Lilas',
phone: '0123456789',
);
} catch (e) {
throw Exception('Erreur lors de la récupération de l\'assistante: $e');
}
}
// Récupérer les événements pour un enfant
Future<List<EventModel>> getEventsForChild(String childId) async {
try {
// TODO: Implémenter l'appel API
return [
EventModel(
id: 'evt1',
title: 'Rendez-vous médical',
startDate: DateTime.now().add(const Duration(days: 2)),
type: EventType.parentVacation,
status: EventStatus.pending,
description: 'Visite de routine',
childId: childId,
),
];
} catch (e) {
throw Exception('Erreur lors de la récupération des événements: $e');
}
}
// Récupérer tous les événements à venir
Future<List<EventModel>> getUpcomingEvents() async {
try {
// TODO: Implémenter l'appel API
return [
EventModel(
id: 'evt1',
title: 'Activité peinture',
startDate: DateTime.now().add(const Duration(days: 1)),
endDate: DateTime.now().add(const Duration(days: 1, hours: 2)),
type: EventType.parentVacation,
status: EventStatus.pending,
description: 'Atelier créatif',
childId: '1',
),
];
} catch (e) {
throw Exception('Erreur lors de la récupération des événements: $e');
}
}
// Récupérer les contrats
Future<List<ContractModel>> getContracts() async {
try {
// TODO: Implémenter l'appel API
return [
ContractModel(
id: 'contract1',
childId: '1',
assistantId: 'am1',
startDate: DateTime(2023, 9, 1),
endDate: DateTime(2024, 8, 31),
status: ContractStatus.pending,
hourlyRate: 10.0,
createdAt: DateTime.now(),
),
];
} catch (e) {
throw Exception('Erreur lors de la récupération des contrats: $e');
}
}
// Récupérer les contrats pour un enfant spécifique
Future<List<ContractModel>> getContractsForChild(String childId) async {
try {
// TODO: Implémenter l'appel API
return [
ContractModel(
id: 'contract1',
childId: childId,
assistantId: 'am1',
startDate: DateTime(2023, 9, 1),
endDate: DateTime(2024, 8, 31),
status: ContractStatus.active,
hourlyRate: 10.0,
createdAt: DateTime.now(),
),
];
} catch (e) {
throw Exception('Erreur lors de la récupération des contrats: $e');
}
}
// Récupérer les conversations
Future<List<ConversationModel>> getConversations() async {
try {
// TODO: Implémenter l'appel API
return [
ConversationModel(
id: 'conv1',
title: 'Conversation avec Marie Dupont',
participantIds: ['am1'],
messages: [
MessageModel(
id: 'msg1',
content: 'Bonjour, comment ça va ?',
senderId: 'am1',
sentAt: DateTime.now().subtract(const Duration(hours: 2)),
status: MessageStatus.read,
),
MessageModel(
id: 'msg2',
content: 'Tout va bien, merci !',
senderId: 'parent1',
sentAt: DateTime.now().subtract(const Duration(hours: 1, minutes: 30)),
status: MessageStatus.read,
),
],
lastMessageAt: DateTime.now().subtract(const Duration(hours: 2)),
unreadCount: 2,
),
];
} catch (e) {
throw Exception('Erreur lors de la récupération des conversations: $e');
}
}
// Récupérer les notifications
Future<List<NotificationModel>> getNotifications() async {
try {
// TODO: Implémenter l'appel API
return [
NotificationModel(
id: 'notif1',
title: 'Nouveau message',
createdAt: DateTime.now(),
isRead: false,
type: NotificationType.contractPending,
content: 'Votre contrat est en attente',
),
];
} catch (e) {
throw Exception('Erreur lors de la récupération des notifications: $e');
}
}
// Marquer une notification comme lue
Future<void> markNotificationAsRead(String notificationId) async {
try {
// TODO: Implémenter l'appel API
} catch (e) {
throw Exception('Erreur lors du marquage de la notification: $e');
}
}
}
@@ -0,0 +1,20 @@
import 'package:flutter/cupertino.dart';
class NavigationService {
static void handleLoginSuccess(BuildContext context, String role) {
switch (role) {
case 'admin':
Navigator.pushReplacementNamed(context, '/admin_dashboard');
break;
case 'gestionnaire':
Navigator.pushReplacementNamed(context, '/gestionnaire_dashboard');
break;
case 'parent':
Navigator.pushReplacementNamed(context, '/parent-dashboard');
break;
case 'assistante_maternelle':
Navigator.pushReplacementNamed(context, '/assistante_maternelle_dashboard');
break;
}
}
}
+123 -1
View File
@@ -26,6 +26,25 @@ class DataGenerator {
'Nous avons hâte de vous rencontrer.',
'La pédagogie Montessori nous intéresse.'
];
static final List<String> _frenchCities = [
'Paris', 'Lyon', 'Marseille', 'Toulouse', 'Nice', 'Nantes', 'Montpellier', 'Strasbourg',
'Bordeaux', 'Lille', 'Rennes', 'Reims', 'Saint-Étienne', 'Toulon', 'Le Havre', 'Grenoble',
'Dijon', 'Angers', 'Nîmes', 'Villeurbanne', 'Clermont-Ferrand', 'Le Mans', 'Aix-en-Provence',
'Brest', 'Tours', 'Amiens', 'Limoges', 'Annecy', 'Boulogne-Billancourt', 'Perpignan'
];
static final List<String> _countries = [
'France', 'Belgique', 'Suisse', 'Canada', 'Maroc', 'Algérie', 'Tunisie', 'Sénégal',
'Côte d\'Ivoire', 'Madagascar', 'Espagne', 'Italie', 'Portugal', 'Allemagne', 'Royaume-Uni'
];
static final List<String> _childminderPresentations = [
'Bonjour,\n\nJe suis assistante maternelle agréée depuis plusieurs années et je souhaite rejoindre votre plateforme. J\'ai de l\'expérience avec les enfants de tous âges et je privilégie un accompagnement bienveillant.\n\nCordialement',
'Madame, Monsieur,\n\nTitulaire d\'un agrément d\'assistante maternelle, je propose un accueil personnalisé dans un environnement sécurisé et stimulant. Je suis disponible pour discuter de vos besoins.\n\nBien à vous',
'Bonjour,\n\nAssistante maternelle passionnée, je propose un accueil de qualité dans ma maison adaptée aux enfants. J\'ai suivi plusieurs formations et je suis disponible à temps plein.\n\nÀ bientôt',
'Cher gestionnaire,\n\nJe suis une professionnelle expérimentée dans la garde d\'enfants. Mon domicile est aménagé pour accueillir les petits dans les meilleures conditions. Je serais ravie de faire partie de votre réseau.\n\nCordialement',
'Bonjour,\n\nDepuis 5 ans, j\'exerce comme assistante maternelle avec passion. Je propose des activités d\'éveil adaptées et un suivi personnalisé de chaque enfant. Mon agrément me permet d\'accueillir jusqu\'à 4 enfants.\n\nBien cordialement'
];
static String firstName() => _firstNames[_random.nextInt(_firstNames.length)];
static String lastName() => _lastNames[_random.nextInt(_lastNames.length)];
@@ -62,4 +81,107 @@ class DataGenerator {
}
return chosenSnippets.join(' ');
}
}
static String birthDate() {
final now = DateTime.now();
final age = _random.nextInt(31) + 25; // Entre 25 et 55 ans
final birthYear = now.year - age;
final birthMonth = _random.nextInt(12) + 1;
final birthDay = _random.nextInt(28) + 1;
return "${birthDay.toString().padLeft(2, '0')}/${birthMonth.toString().padLeft(2, '0')}/${birthYear}";
}
/// Génère une ville de naissance française
static String birthCity() => _frenchCities[_random.nextInt(_frenchCities.length)];
/// Génère un pays de naissance
static String birthCountry() => _countries[_random.nextInt(_countries.length)];
/// Génère un numéro de sécurité sociale français (NIR)
static String socialSecurityNumber() {
// Format NIR français : 1 YYMM DD CCC KK
// 1 = sexe (1 homme, 2 femme)
final sex = _random.nextBool() ? '1' : '2';
// YY = année de naissance (2 derniers chiffres)
final currentYear = DateTime.now().year;
final birthYear = currentYear - (_random.nextInt(31) + 25); // 25-55 ans
final yy = (birthYear % 100).toString().padLeft(2, '0');
// MM = mois de naissance
final mm = (_random.nextInt(12) + 1).toString().padLeft(2, '0');
// DD = département de naissance (01-95)
final dd = (_random.nextInt(95) + 1).toString().padLeft(2, '0');
// CCC = numéro d'ordre (001-999)
final ccc = (_random.nextInt(999) + 1).toString().padLeft(3, '0');
// KK = clé de contrôle (simulation)
final kk = _random.nextInt(100).toString().padLeft(2, '0');
return '$sex$yy$mm$dd$ccc$kk';
}
/// Génère un numéro d'agrément pour assistante maternelle
static String agreementNumber() {
final year = DateTime.now().year;
final dept = _random.nextInt(95) + 1; // Département 01-95
final sequence = _random.nextInt(9999) + 1; // Numéro de séquence
return 'AM${dept.toString().padLeft(2, '0')}$year${sequence.toString().padLeft(4, '0')}';
}
/// Génère un nombre d'enfants maximum pour l'agrément (1-4)
static int maxChildren() => _random.nextInt(4) + 1;
/// Génère un message de présentation pour assistante maternelle
static String childminderPresentation() =>
_childminderPresentations[_random.nextInt(_childminderPresentations.length)];
/// Génère un âge d'enfant en mois (0-36 mois)
static int childAgeInMonths() => _random.nextInt(37);
/// Génère une durée d'expérience en années (1-15 ans)
static int experienceYears() => _random.nextInt(15) + 1;
/// Génère un tarif horaire (entre 3.50€ et 6.00€)
static double hourlyRate() {
final rate = 3.50 + (_random.nextDouble() * 2.50);
return double.parse(rate.toStringAsFixed(2));
}
/// Génère des disponibilités (jours de la semaine)
static List<String> availability() {
final days = ['Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi', 'Dimanche'];
final availableDays = <String>[];
// Au moins 3 jours de disponibilité
final minDays = 3;
final maxDays = days.length;
final numDays = _random.nextInt(maxDays - minDays + 1) + minDays;
final selectedIndices = <int>[];
while (selectedIndices.length < numDays) {
final index = _random.nextInt(days.length);
if (!selectedIndices.contains(index)) {
selectedIndices.add(index);
availableDays.add(days[index]);
}
}
return availableDays;
}
/// Génère des horaires (format "08h30-17h30")
static String workingHours() {
final startHours = [7, 8, 9];
final endHours = [16, 17, 18, 19];
final startHour = startHours[_random.nextInt(startHours.length)];
final endHour = endHours[_random.nextInt(endHours.length)];
final startMinutes = _random.nextBool() ? '00' : '30';
final endMinutes = _random.nextBool() ? '00' : '30';
return '${startHour}h$startMinutes-${endHour}h$endMinutes';
}
}
+114
View File
@@ -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),
),
),
],
),
),
);
}
}
+60
View File
@@ -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,
),
),
],
),
),
);
}
}
+209
View File
@@ -0,0 +1,209 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:p_tits_pas/models/m_dashbord/child_model.dart';
import 'package:p_tits_pas/services/bug_report_service.dart';
class AppFooter extends StatelessWidget {
const AppFooter({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
decoration: BoxDecoration(
// color: Colors.white,
border: Border(
top: BorderSide(color: Colors.grey.shade300),
),
),
child: LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth < 768) {
return _buildMobileFooter(context);
} else {
return _buildDesktopFooter(context);
}
},
),
);
}
Widget _buildDesktopFooter(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildFooterLink(context, 'Contact support', () => _handleContactSupport(context)),
SizedBox(width: MediaQuery.of(context).size.width * 0.1),
// _buildFooterDivider(),
_buildFooterLink(context, 'Signaler un bug', () => _handleReportBug(context)),
SizedBox(width: MediaQuery.of(context).size.width * 0.1),
// _buildFooterDivider(),
_buildFooterLink(context, 'Mentions légales', () => _handleLegalNotices(context)),
// _buildFooterDivider(),
SizedBox(width: MediaQuery.of(context).size.width * 0.1),
_buildFooterLink(context, 'Politique de confidentialité', () => _handlePrivacyPolicy(context)),
],
);
}
Widget _buildMobileFooter(BuildContext context) {
return PopupMenuButton<String>(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: const [
Icon(Icons.info_outline, size: 20),
SizedBox(width: 8),
Text('Informations'),
Icon(Icons.keyboard_arrow_down),
],
),
),
itemBuilder: (context) => [
const PopupMenuItem(value: 'support', child: Text('Contact support')),
const PopupMenuItem(value: 'bug', child: Text('Signaler un bug')),
const PopupMenuItem(value: 'legal', child: Text('Mentions légales')),
const PopupMenuItem(value: 'privacy', child: Text('Politique de confidentialité')),
],
onSelected: (value) {
switch (value) {
case 'support':
_handleContactSupport(context);
break;
case 'bug':
_handleReportBug(context);
break;
case 'legal':
_handleLegalNotices(context);
break;
case 'privacy':
_handlePrivacyPolicy(context);
break;
}
},
);
}
Widget _buildFooterLink(BuildContext context, String text, VoidCallback onTap) {
return InkWell(
onTap: onTap,
child: Text(
text,
style: TextStyle(
color: Theme.of(context).primaryColor,
),
),
);
}
void _handleReportBug(BuildContext context) {
final TextEditingController controller = TextEditingController();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(
'Signaler un bug',
style: GoogleFonts.merienda(),
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: controller,
maxLines: 5,
decoration: InputDecoration(
hintText: 'Décrivez le problème rencontré...',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
'Annuler',
style: GoogleFonts.merienda(),
),
),
TextButton(
onPressed: () async {
if (controller.text.trim().isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Veuillez décrire le problème',
style: GoogleFonts.merienda(),
),
),
);
return;
}
try {
await BugReportService.sendReport(controller.text);
if (context.mounted) {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Rapport envoyé avec succès',
style: GoogleFonts.merienda(),
),
),
);
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Erreur lors de l\'envoi du rapport',
style: GoogleFonts.merienda(),
),
),
);
}
}
},
child: Text(
'Envoyer',
style: GoogleFonts.merienda(),
),
),
],
),
);
}
void _handleLegalNotices(BuildContext context) {
// Handle legal notices action
Navigator.pushNamed(context, '/legal');
}
void _handlePrivacyPolicy(BuildContext context) {
// Handle privacy policy action
Navigator.pushNamed(context, '/privacy');
}
void _handleContactSupport(BuildContext context) {
// Handle contact support action
// Navigator.pushNamed(context, '/support');
}
Widget _buildFooterDivider() {
return Divider(
color: Colors.grey[300],
thickness: 1,
height: 40,
);
}
}
@@ -0,0 +1,58 @@
import 'package:flutter/material.dart';
class Childrensidebarwidget extends StatelessWidget{
final void Function(String childId) onChildSelected;
const Childrensidebarwidget({
Key? key,
required this.onChildSelected,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final children = [
{'id': '1', 'name': 'Léna', 'photo': null, 'status': 'Actif'},
{'id': '2', 'name': 'Noé', 'photo': null, 'status': 'Inactif'},
];
return Container(
color: const Color(0xFFF7F7F7),
padding: const EdgeInsets.all(16),
child: Column(
children: [
// Avatar parent + bouton
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const CircleAvatar(radius: 24, child: Icon(Icons.person)),
IconButton(
icon: const Icon(Icons.add),
onPressed: () {
// Naviguer vers ajout d'enfant
},
)
],
),
const SizedBox(height: 16),
const Text("Mes enfants", style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 16),
// Liste des enfants
...children.map((child) {
return GestureDetector(
onTap: () => onChildSelected(child['id']!),
child: Card(
color: child['status'] == 'Actif' ? Colors.teal.shade50 : Colors.white,
child: ListTile(
leading: const CircleAvatar(child: Icon(Icons.child_care)),
title: Text(child['name']!),
subtitle: Text(child['status']!),
),
),
);
}).toList()
],
),
);
}
}
@@ -0,0 +1,28 @@
import 'package:flutter/material.dart';
class AppLayout extends StatelessWidget {
final PreferredSizeWidget appBar;
final Widget body;
final Widget? footer;
const AppLayout({
Key? key,
required this.appBar,
required this.body,
this.footer,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF5F7FA),
appBar: appBar,
body: Column(
children: [
Expanded(child: body),
if (footer != null) footer!,
],
),
);
}
}
@@ -0,0 +1,203 @@
import 'package:flutter/material.dart';
import 'package:p_tits_pas/models/m_dashbord/child_model.dart';
class ChildrenSidebar extends StatelessWidget {
final List<ChildModel> children;
final String? selectedChildId;
final Function(String) onChildSelected;
final VoidCallback onAddChild;
final bool isCompact;
final bool isMobile;
const ChildrenSidebar({
Key? key,
required this.children,
this.selectedChildId,
required this.onChildSelected,
required this.onAddChild,
this.isCompact = false,
this.isMobile = false,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(isMobile ? 16 : 24),
color: Colors.white,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(context),
const SizedBox(height: 20),
_buildAddChildButton(context),
const SizedBox(height: 16),
Expanded(child: _buildChildrenList()),
],
),
);
}
Widget _buildHeader(BuildContext context) {
return Row(
children: [
// UserAvatar(
// size: isCompact ? 40 : 60,
// name: 'Emma Dupont', // TODO: Récupérer depuis le contexte utilisateur
// ),
if (!isCompact) ...[
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text(
'Emma Dupont',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
Icon(Icons.keyboard_arrow_down),
],
),
),
],
],
);
}
Widget _buildAddChildButton(BuildContext context) {
return SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: onAddChild,
icon: const Icon(Icons.add),
label: Text(isCompact ? 'Ajouter' : 'Ajouter un enfant'),
style: OutlinedButton.styleFrom(
padding: EdgeInsets.symmetric(
horizontal: 16,
vertical: isCompact ? 8 : 12,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
);
}
Widget _buildChildrenList() {
if (children.isEmpty) {
return const Center(
child: Text(
'Aucun enfant ajouté',
style: TextStyle(
color: Colors.grey,
fontSize: 14,
),
),
);
}
return ListView.separated(
itemCount: children.length,
separatorBuilder: (context, index) => const SizedBox(height: 12),
itemBuilder: (context, index) {
final child = children[index];
final isSelected = child.id == selectedChildId;
return _buildChildCard(context, child, isSelected);
},
);
}
Widget _buildChildCard(BuildContext context, ChildModel child, bool isSelected) {
return InkWell(
onTap: () => onChildSelected(child.id),
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isSelected ? const Color(0xFF9CC5C0).withOpacity(0.1) : Colors.transparent,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSelected ? const Color(0xFF9CC5C0) : Colors.grey.shade300,
width: isSelected ? 2 : 1,
),
),
child: Row(
children: [
// UserAvatar(
// // size: isCompact ? 32 : 40,
// // name: child.fullName,
// // imageUrl: child.photoUrl,
// ),
if (!isCompact) ...[
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
child.firstName,
style: TextStyle(
fontSize: 14,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
),
),
const SizedBox(height: 4),
_buildChildStatus(child.status),
],
),
),
],
],
),
),
);
}
Widget _buildChildStatus(ChildStatus status) {
String label;
Color color;
switch (status) {
case ChildStatus.withAssistant:
label = 'En garde';
color = Colors.green;
break;
case ChildStatus.available:
label = 'Disponible';
color = Colors.blue;
break;
case ChildStatus.onHoliday:
label = 'En vacances';
color = Colors.orange;
break;
case ChildStatus.sick:
label = 'Malade';
color = Colors.red;
break;
case ChildStatus.searching:
label = 'Recherche AM';
color = Colors.purple;
break;
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
label,
style: TextStyle(
fontSize: 11,
color: color,
fontWeight: FontWeight.w500,
),
),
);
}
}
@@ -0,0 +1,156 @@
import 'package:flutter/material.dart';
class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget {
final int selectedIndex;
final ValueChanged<int> onTabChange;
const DashboardAppBar({Key? key, required this.selectedIndex, required this.onTabChange}) : super(key: key);
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight + 10);
@override
Widget build(BuildContext context) {
final isMobile = MediaQuery.of(context).size.width < 768;
return AppBar(
// backgroundColor: Colors.white,
elevation: 0,
title: Row(
children: [
// Logo de la ville
// Container(
// height: 32,
// width: 32,
// decoration: BoxDecoration(
// color: Colors.white,
// borderRadius: BorderRadius.circular(8),
// ),
// child: const Icon(
// Icons.location_city,
// color: Color(0xFF9CC5C0),
// size: 20,
// ),
// ),
SizedBox(width: MediaQuery.of(context).size.width * 0.19),
const Text(
"P'tit Pas",
style: TextStyle(
color: Color(0xFF9CC5C0),
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
SizedBox(width: MediaQuery.of(context).size.width * 0.1),
// Navigation principale
_buildNavItem(context, 'Mon tableau de bord', 0),
const SizedBox(width: 24),
_buildNavItem(context, 'Trouver une nounou', 1),
const SizedBox(width: 24),
_buildNavItem(context, 'Paramètres', 2),
],
),
actions: isMobile
? [_buildMobileMenu(context)]
: [
// Nom de l'utilisateur
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Center(
child: Text(
'Jean Dupont',
style: TextStyle(
color: Colors.black,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
),
// Bouton déconnexion
Padding(
padding: const EdgeInsets.only(right: 16),
child: TextButton(
onPressed: () => _handleLogout(context),
style: TextButton.styleFrom(
backgroundColor: const Color(0xFF9CC5C0),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5),
),
),
child: const Text('Se déconnecter'),
),
),
SizedBox(width: MediaQuery.of(context).size.width * 0.1),
],
);
}
Widget _buildNavItem(BuildContext context, String title, int index) {
final bool isActive = index == selectedIndex;
return InkWell(
onTap: () => onTabChange(index),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: isActive ? const Color(0xFF9CC5C0) : Colors.transparent,
borderRadius: BorderRadius.circular(20),
border: isActive ? null : Border.all(color: Colors.black26),
),
child: Text(
title,
style: TextStyle(
color: isActive ? Colors.white : Colors.black,
fontWeight: isActive ? FontWeight.w600 : FontWeight.normal,
fontSize: 14,
),
),
),
);
}
Widget _buildMobileMenu(BuildContext context) {
return PopupMenuButton<int>(
icon: const Icon(Icons.menu, color: Colors.white),
onSelected: (value) {
if (value == 3) {
_handleLogout(context);
}
},
itemBuilder: (context) => [
const PopupMenuItem(value: 0, child: Text("Mon tableau de bord")),
const PopupMenuItem(value: 1, child: Text("Trouver une nounou")),
const PopupMenuItem(value: 2, child: Text("Paramètres")),
const PopupMenuDivider(),
const PopupMenuItem(value: 3, child: Text("Se déconnecter")),
],
);
}
void _handleLogout(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Déconnexion'),
content: const Text('Êtes-vous sûr de vouloir vous déconnecter ?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
// TODO: Implémenter la logique de déconnexion
},
child: const Text('Déconnecter'),
),
],
),
);
}
}
@@ -0,0 +1,31 @@
import 'package:flutter/material.dart';
import 'package:p_tits_pas/widgets/dashbord_parent/ChildrenSidebarwidget.dart';
import 'package:p_tits_pas/widgets/dashbord_parent/children_sidebar.dart';
import 'package:p_tits_pas/widgets/dashbord_parent/wid_mainContentArea.dart';
import 'package:p_tits_pas/widgets/messaging_sidebar.dart';
Widget Dashbord_body() {
return Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 1️⃣ Colonne de gauche : enfants
SizedBox(
width: 250,
child: Childrensidebarwidget(
onChildSelected: (childId) {
// Met à jour l'enfant sélectionné
// Tu peux stocker cet ID dans un state `selectedChildId`
},
),
),
Expanded(
flex: 2,
child: WMainContentArea(
// Passe lenfant sélectionné si besoin
),
),
],
);
}
@@ -0,0 +1,94 @@
import 'package:flutter/material.dart';
import 'package:p_tits_pas/widgets/messaging_sidebar.dart';
class WMainContentArea extends StatelessWidget {
const WMainContentArea({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
color: Colors.white,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 🔷 Informations assistante maternelle (ligne complète)
Card(
margin: const EdgeInsets.only(bottom: 16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
const CircleAvatar(
radius: 30,
backgroundImage: AssetImage("assets/images/am_photo.jpg"), // à adapter
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text("Julie Dupont", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
SizedBox(height: 4),
Text("Taux horaire : 10€/h"),
Text("Frais journaliers : 5€"),
],
),
),
ElevatedButton(
onPressed: () {
// Ouvrir le contrat
},
child: const Text("Voir le contrat"),
)
],
),
),
),
// 🔷 Deux colonnes : planning + messagerie
Expanded(
child: Row(
children: [
// 📆 Planning de garde
Expanded(
flex: 2,
child: Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text("Planning de garde", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
SizedBox(height: 12),
Expanded(
child: Center(
child: Text("Composant calendrier à intégrer ici"),
),
)
],
),
),
),
),
const SizedBox(width: 16),
// 💬 Messagerie
Expanded(
flex: 1,
child: MessagingSidebar(
conversations: [],
notifications: [],
isCompact: false,
isMobile: false,
),
),
],
),
)
],
),
);
}
}
+326
View File
@@ -0,0 +1,326 @@
import 'package:flutter/material.dart';
import 'package:p_tits_pas/models/m_dashbord/assistant_model.dart';
import 'package:p_tits_pas/models/m_dashbord/child_model.dart';
import 'package:p_tits_pas/models/m_dashbord/contract_model.dart';
import 'package:p_tits_pas/models/m_dashbord/event_model.dart';
class MainContentArea extends StatelessWidget {
final ChildModel? selectedChild;
final AssistantModel? selectedAssistant;
final List<EventModel> events;
final List<ContractModel> contracts;
final bool showOnlyCalendar;
final bool showOnlyContracts;
const MainContentArea({
Key? key,
this.selectedChild,
this.selectedAssistant,
required this.events,
required this.contracts,
this.showOnlyCalendar = false,
this.showOnlyContracts = false,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!showOnlyCalendar && !showOnlyContracts) ...[
if (selectedAssistant != null) _buildAssistantProfile(),
const SizedBox(height: 24),
],
if (showOnlyContracts || (!showOnlyCalendar && !showOnlyContracts)) ...[
_buildContractsSection(),
if (!showOnlyContracts) const SizedBox(height: 24),
],
if (showOnlyCalendar || (!showOnlyCalendar && !showOnlyContracts)) ...[
Expanded(child: _buildCalendarSection()),
],
],
),
);
}
Widget _buildAssistantProfile() {
if (selectedAssistant == null) {
return _buildSearchAssistantCard();
}
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
spreadRadius: 1,
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: const Color(0xFF9CC5C0),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.person,
color: Colors.white,
size: 40,
),
),
const SizedBox(width: 20),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
selectedAssistant!.fullName,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
Row(
children: [
Text(
'Taux horaire : ${selectedAssistant!.hourlyRateFormatted}',
style: const TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
const SizedBox(width: 20),
Text(
'Frais journaliers : ${selectedAssistant!.dailyFeesFormatted}',
style: const TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
],
),
],
),
),
ElevatedButton(
onPressed: () {
// TODO: Navigation vers le contrat
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF9CC5C0),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text('Voir le contrat'),
),
],
),
);
}
Widget _buildSearchAssistantCard() {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade300),
),
child: Column(
children: [
Icon(
Icons.search,
size: 48,
color: Colors.grey.shade400,
),
const SizedBox(height: 16),
const Text(
'Aucune assistante maternelle assignée',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
Text(
'Trouvez une assistante maternelle pour votre enfant',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
// TODO: Navigation vers la recherche
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF9CC5C0),
foregroundColor: Colors.white,
),
child: const Text('Rechercher une assistante maternelle'),
),
],
),
);
}
Widget _buildCalendarSection() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
spreadRadius: 1,
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Planning de garde pour ${selectedChild?.firstName ?? "votre enfant"}',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
TextButton(
onPressed: () {
// TODO: Mode sélection de plage
},
child: const Text('Mode sélection de plage'),
),
],
),
const SizedBox(height: 20),
Expanded(
child: _buildCalendar(),
),
],
),
);
}
Widget _buildCalendar() {
// Placeholder pour le calendrier - sera développé dans FRONT-11
return const Center(
child: Text(
'Calendrier à implémenter\n(FRONT-11)',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
color: Colors.grey,
),
),
);
}
Widget _buildContractsSection() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
spreadRadius: 1,
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Contrats',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 16),
if (contracts.isEmpty)
const Text(
'Aucun contrat en cours',
style: TextStyle(color: Colors.grey),
)
else
...contracts.map((contract) => _buildContractItem(contract)),
],
),
);
}
Widget _buildContractItem(ContractModel contract) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: _getContractStatusColor(contract.status),
borderRadius: BorderRadius.circular(6),
),
),
const SizedBox(width: 12),
Expanded(
child: Text(contract.statusLabel),
),
if (contract.needsSignature)
TextButton(
onPressed: () {
// TODO: Action signature
},
child: const Text('Signer'),
),
],
),
);
}
Color _getContractStatusColor(ContractStatus status) {
switch (status) {
case ContractStatus.draft:
return Colors.grey;
case ContractStatus.pending:
return Colors.orange;
case ContractStatus.active:
return Colors.green;
case ContractStatus.ended:
return Colors.blue;
case ContractStatus.cancelled:
return Colors.red;
}
}
}
+207
View File
@@ -0,0 +1,207 @@
import 'package:flutter/material.dart';
import 'package:p_tits_pas/models/m_dashbord/conversation_model.dart';
import 'package:p_tits_pas/models/m_dashbord/notification_model.dart';
class MessagingSidebar extends StatelessWidget {
final List<ConversationModel> conversations;
final List<NotificationModel> notifications;
final bool isCompact;
final bool isMobile;
const MessagingSidebar({
Key? key,
required this.conversations,
required this.notifications,
this.isCompact = false,
this.isMobile = false,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(isMobile ? 16 : 20),
color: Colors.white,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildMessagingHeader(),
const SizedBox(height: 20),
Expanded(
child: _buildMessagingContent(),
),
const SizedBox(height: 16),
_buildContactRPEButton(),
],
),
);
}
Widget _buildMessagingHeader() {
return const Text(
'Messagerie avec Emma Dupont',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
);
}
Widget _buildMessagingContent() {
return Column(
children: [
// Messages existants
Expanded(
child: _buildMessagesList(),
),
const SizedBox(height: 12),
// Zone de saisie
_buildMessageInput(),
],
);
}
Widget _buildMessagesList() {
if (conversations.isEmpty) {
return const Center(
child: Text(
'Aucun message',
style: TextStyle(
color: Colors.grey,
fontSize: 14,
),
),
);
}
// Pour la démo, on affiche quelques messages fictifs
return ListView(
children: [
_buildMessageBubble(
'Bonjour, Emma a bien dormi aujourd\'hui.',
isFromCurrentUser: false,
timestamp: DateTime.now().subtract(const Duration(hours: 2)),
),
const SizedBox(height: 12),
_buildMessageBubble(
'Merci pour l\'information. Elle a bien mangé ?',
isFromCurrentUser: true,
timestamp: DateTime.now().subtract(const Duration(hours: 1)),
),
],
);
}
Widget _buildMessageBubble(String content, {required bool isFromCurrentUser, required DateTime timestamp}) {
return Align(
alignment: isFromCurrentUser ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
constraints: const BoxConstraints(maxWidth: 250),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: isFromCurrentUser
? const Color(0xFF9CC5C0)
: Colors.grey.shade200,
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
content,
style: TextStyle(
color: isFromCurrentUser ? Colors.white : Colors.black87,
fontSize: 14,
),
),
const SizedBox(height: 4),
Text(
_formatTimestamp(timestamp),
style: TextStyle(
color: isFromCurrentUser
? Colors.white.withOpacity(0.8)
: Colors.grey.shade600,
fontSize: 11,
),
),
],
),
),
);
}
Widget _buildMessageInput() {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(20),
),
child: Row(
children: [
const Expanded(
child: TextField(
decoration: InputDecoration(
hintText: 'Écrivez votre message...',
border: InputBorder.none,
isDense: true,
),
maxLines: null,
),
),
const SizedBox(width: 8),
CircleAvatar(
radius: 16,
backgroundColor: const Color(0xFF9CC5C0),
child: IconButton(
iconSize: 16,
padding: EdgeInsets.zero,
onPressed: () {
// TODO: Envoyer le message
},
icon: const Icon(
Icons.send,
color: Colors.white,
),
),
),
],
),
);
}
Widget _buildContactRPEButton() {
return SizedBox(
width: double.infinity,
child: OutlinedButton(
onPressed: () {
// TODO: Contacter le RPE
},
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text(
'Contacter le Relais Petite Enfance',
textAlign: TextAlign.center,
),
),
);
}
String _formatTimestamp(DateTime timestamp) {
final now = DateTime.now();
final difference = now.difference(timestamp);
if (difference.inMinutes < 1) {
return 'À l\'instant';
} else if (difference.inHours < 1) {
return '${difference.inMinutes}m';
} else if (difference.inDays < 1) {
return '${difference.inHours}h';
} else {
return '${timestamp.day}/${timestamp.month}';
}
}
}
+13
View File
@@ -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;
}
-40
View File
@@ -1,40 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>P'titsPas - Connexion</title>
<link rel="stylesheet" href="styles.css">
<link href="https://fonts.googleapis.com/css2?family=Merienda:wght@400;600&display=swap" rel="stylesheet">
</head>
<body>
<img src="assets/river.png" alt="" class="river" aria-hidden="true">
<main class="login-container">
<img src="assets/logo.png" alt="P'titsPas" class="logo">
<h1 class="slogan">Grandir pas à pas, sereinement</h1>
<form class="login-form">
<div class="form-group">
<label for="email">Adresse e-mail</label>
<input type="email" id="email" name="email"
placeholder="Votre adresse e-mail"
aria-label="Saisissez votre adresse e-mail"
required>
</div>
<div class="form-group">
<label for="password">Mot de passe</label>
<input type="password" id="password" name="password"
placeholder="Votre mot de passe"
aria-label="Saisissez votre mot de passe"
required>
</div>
<button type="submit" class="login-button">
Se connecter
</button>
</form>
</main>
</body>
</html>
-133
View File
@@ -1,133 +0,0 @@
/* Reset et styles de base */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Merienda', cursive;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background-color: #ffffff;
}
/* Décor de fond */
.river {
position: fixed;
left: 0;
top: 0;
width: 60vw;
opacity: 0.15;
pointer-events: none;
z-index: -1;
}
/* Container principal */
.login-container {
width: 100%;
max-width: 480px;
padding: 2rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 2rem;
}
/* Logo */
.logo {
max-width: 220px;
height: auto;
}
/* Slogan */
.slogan {
font-family: 'Merienda', cursive;
text-align: center;
color: #3a3a3a;
font-size: 1.5rem;
margin-bottom: 2rem;
}
/* Formulaire */
.login-form {
width: 100%;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
/* Labels */
label {
color: #3a3a3a;
font-weight: 600;
font-size: 1rem;
}
/* Champs de saisie */
input {
height: 80px;
border: none;
border-radius: 30px;
padding: 0 1.2rem;
font-size: 1rem;
font-family: inherit;
background-size: cover;
}
input[type="email"] {
background-image: url('assets/field_email.png');
color: #fff;
}
input[type="password"] {
background-image: url('assets/field_password.png');
color: #000;
}
/* Bouton de connexion */
.login-button {
height: 80px;
background-image: url('assets/btn_green.png');
background-size: cover;
border: none;
border-radius: 40px;
color: #ffffff;
font-family: "Merienda", cursive;
font-size: 1.1rem;
cursor: pointer;
transition: filter 0.2s ease;
}
.login-button:hover {
filter: brightness(1.08);
}
/* Media queries pour mobile */
@media (max-width: 480px) {
.river {
width: 40vw;
opacity: 0.10;
clip-path: inset(0 0 20% 0);
}
.logo {
max-width: 120px;
}
.login-container {
padding: 1rem;
}
.slogan {
font-size: 1.2rem;
}
}
+58 -2
View File
@@ -139,6 +139,54 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.28"
flutter_secure_storage:
dependency: "direct main"
description:
name: flutter_secure_storage
sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea"
url: "https://pub.dev"
source: hosted
version: "9.2.4"
flutter_secure_storage_linux:
dependency: transitive
description:
name: flutter_secure_storage_linux
sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688
url: "https://pub.dev"
source: hosted
version: "1.2.3"
flutter_secure_storage_macos:
dependency: transitive
description:
name: flutter_secure_storage_macos
sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247"
url: "https://pub.dev"
source: hosted
version: "3.1.3"
flutter_secure_storage_platform_interface:
dependency: transitive
description:
name: flutter_secure_storage_platform_interface
sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8
url: "https://pub.dev"
source: hosted
version: "1.1.2"
flutter_secure_storage_web:
dependency: transitive
description:
name: flutter_secure_storage_web
sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9
url: "https://pub.dev"
source: hosted
version: "1.2.1"
flutter_secure_storage_windows:
dependency: transitive
description:
name: flutter_secure_storage_windows
sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709
url: "https://pub.dev"
source: hosted
version: "3.1.2"
flutter_test:
dependency: "direct dev"
description: flutter
@@ -169,10 +217,10 @@ packages:
dependency: "direct main"
description:
name: http
sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007
url: "https://pub.dev"
source: hosted
version: "1.3.0"
version: "1.5.0"
http_parser:
dependency: transitive
description:
@@ -626,6 +674,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.1"
win32:
dependency: transitive
description:
name: win32
sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba"
url: "https://pub.dev"
source: hosted
version: "5.13.0"
xdg_directories:
dependency: transitive
description:
+2 -1
View File
@@ -18,7 +18,8 @@ dependencies:
image_picker: ^1.0.7
js: ^0.6.7
url_launcher: ^6.2.4
http: ^1.2.0
http: ^1.5.0
flutter_secure_storage: ^9.2.4
dev_dependencies:
flutter_test:
+1 -1
View File
@@ -7,8 +7,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:p_tits_pas/main.dart';
import 'package:petitspas/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
@@ -7,11 +7,14 @@
#include "generated_plugin_registrant.h"
#include <file_selector_windows/file_selector_windows.h>
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}
@@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_windows
flutter_secure_storage_windows
url_launcher_windows
)
-55
View File
@@ -1,55 +0,0 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:p_tits_pas/screens/auth/login_screen.dart';
import 'package:p_tits_pas/screens/auth/register_choice_screen.dart';
import 'package:p_tits_pas/screens/auth/parent_register_step1_screen.dart';
import 'package:p_tits_pas/screens/auth/parent_register_step2_screen.dart';
import 'package:p_tits_pas/screens/auth/parent_register_step3_screen.dart';
void main() {
// TODO: Initialiser SharedPreferences, Provider, etc.
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'P\'titsPas',
theme: ThemeData(
primarySwatch: Colors.blue, // TODO: Utiliser la palette de la charte graphique
textTheme: GoogleFonts.merriweatherTextTheme(
Theme.of(context).textTheme,
),
visualDensity: VisualDensity.adaptivePlatformDensity,
),
// Gestionnaire de routes initial (simple pour l'instant)
initialRoute: '/', // Ou '/login' selon le point d'entrée désiré
routes: {
'/': (context) => const LoginScreen(), // Exemple, pourrait être RegisterChoiceScreen aussi
'/login': (context) => const LoginScreen(),
'/register-choice': (context) => const RegisterChoiceScreen(),
'/parent-register/step1': (context) => const ParentRegisterStep1Screen(),
'/parent-register/step2': (context) => const ParentRegisterStep2Screen(),
'/parent-register/step3': (context) => const ParentRegisterStep3Screen(),
// TODO: Ajouter les autres routes (step 4, etc., dashboard...)
},
// Gestion des routes inconnues
onUnknownRoute: (settings) {
return MaterialPageRoute(
builder: (context) => Scaffold(
body: Center(
child: Text(
'Route inconnue :\n${settings.name}',
style: GoogleFonts.merriweather(fontSize: 20, color: Colors.red),
textAlign: TextAlign.center,
),
),
),
);
},
);
}
}
-1
View File
@@ -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,
),
View File