Compare commits

...

31 Commits

Author SHA1 Message Date
d4d8d5f5c6 Merge pull request 'Add SidebarAdmin' (#69) from dev into master
Reviewed-on: #69
2025-09-19 13:46:53 +00:00
Hanim
e0083d665b Add SidebarAdmin 2025-09-19 15:44:12 +02:00
Hanim
70033dd9c3 delet e flutter_secure_storage 2025-09-16 12:53:44 +02:00
Hanim
1e85819fea feat: Ajout des requet sur le widget AssistanteMaternelleManagementWidget 2025-09-15 15:21:12 +02:00
Hanim
4392567509 add modif in gitignore 2025-09-15 14:13:18 +02:00
Hanim
c98c4d51d0 remove auto-generated Flutter files from tracking 2025-09-15 14:12:57 +02:00
Hanim
bbbff60a7a resolve conflict 2025-09-15 13:40:30 +02:00
Hanim
050087359c feat: add Parent managment with admin dashbord 2025-09-15 13:28:20 +02:00
Hanim
68adc027cb Add modif of compile 2025-09-12 16:28:55 +02:00
Hanim
05b2380181 add corection 2025-09-12 16:25:20 +02:00
Hanim
c332eb3d86 add modification version http 2025-09-12 16:11:53 +02:00
a8f174a663 Merge pull request 'dev' (#67) from dev into master
Reviewed-on: #67
2025-09-12 13:28:04 +00:00
61554c5edc Merge pull request 'Add routes navigation login and admin dashboard' (#66) from feature/FRONT-07 into dev
Reviewed-on: #66
2025-09-12 13:27:29 +00:00
debc10d974 Merge branch 'dev' into feature/FRONT-07 2025-09-12 13:27:20 +00:00
Hanim
e2e38076aa Add routes navigation login and admin dashboard 2025-09-12 15:24:52 +02:00
c6f9fd3be7 Merge pull request 'feat: Implement authentication service and login handling with role-based navigation' (#64) from feature/FRONT-07 into dev
Reviewed-on: #64
2025-09-03 09:05:58 +00:00
Hanim
9fd6cb7b76 feat: Implement authentication service and login handling with role-based navigation 2025-09-03 11:03:53 +02:00
ad9ca5c5b5 Merge pull request 'dev' (#63) from dev into master
Reviewed-on: #63
2025-09-01 10:05:42 +00:00
864d72eb40 Merge branch 'master' into dev 2025-09-01 10:05:08 +00:00
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
979114b93d Nettoyage et tet hook 2025-08-26 17:49:11 +02:00
ee940e25b7 Merge branch 'master' of https://git.ptits-pas.fr/Ynov/ptitspas-ynov 2025-08-26 12:09:31 +02:00
10a5cb1fed Ajout: Configuration développement local pour le frontend 2025-08-26 11:59:45 +02:00
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
1e9803a4a7 Merge pull request 'dev' (#51) from dev into master
Reviewed-on: #51
2025-08-22 08:26:30 +00:00
74c56b900e Fix: Version Flutter pour compatibilité Dart 3.2.6+ 2025-08-22 00:06:10 +02:00
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
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
75 changed files with 4432 additions and 4345 deletions

4
.env.example Normal file
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
.gitignore vendored
View File

@ -52,3 +52,4 @@ Xcf/**
# Release notes
CHANGELOG.md
Ressources/
.env

62
README-DEV.md Normal file
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
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

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"
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,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 {}

View File

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

View File

@ -1,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}`);
});

View File

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

View File

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

View File

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

View File

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

View File

@ -1,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
docker-compose.dev.yml Normal file
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

6
frontend/.gitignore vendored
View File

@ -43,3 +43,9 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release
# Fichiers générés automatiquement par Flutter pour l'enregistrement des plugins
**/GeneratedPluginRegistrant.java
**/generated_plugin_registrant.*
**/generated_plugins.cmake

16
frontend/Dockerfile Normal file
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;"]

View File

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

View File

@ -0,0 +1,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";
}

View File

@ -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();
}
}

View File

@ -1,10 +1,7 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; // Import pour la localisation
// import 'package:provider/provider.dart'; // Supprimer Provider
import 'package:flutter_localizations/flutter_localizations.dart';
import 'navigation/app_router.dart';
// import 'theme/app_theme.dart'; // Supprimer AppTheme
// import 'theme/theme_provider.dart'; // Supprimer ThemeProvider
void main() {
runApp(const MyApp()); // Exécution simple

View File

@ -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,
}

View File

@ -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
}

View File

@ -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,
}

View File

@ -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,
}

View File

@ -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,
}

View File

@ -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
}

View File

@ -1,10 +1,14 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:p_tits_pas/models/am_user_registration_data.dart';
import 'package:p_tits_pas/screens/administrateurs/admin_dashboardScreen.dart';
import 'package:p_tits_pas/screens/auth/am/am_register_step1_sceen.dart';
import 'package:p_tits_pas/screens/auth/am/am_register_step2_sceen.dart';
import 'package:p_tits_pas/screens/auth/am/am_register_step3_sceen.dart';
import 'package:p_tits_pas/screens/auth/am/am_register_step4_sceen.dart';
import 'package:p_tits_pas/screens/home/parent_screen/ParentDashboardScreen.dart';
import 'package:p_tits_pas/screens/home/parent_screen/find_nanny.dart';
import 'package:p_tits_pas/screens/legal/legal_page.dart';
import 'package:p_tits_pas/screens/legal/privacy_page.dart';
import '../screens/auth/login_screen.dart';
import '../screens/auth/register_choice_screen.dart';
import '../screens/auth/parent/parent_register_step1_screen.dart';
@ -12,12 +16,13 @@ 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/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';
@ -28,7 +33,9 @@ class AppRouter {
static const String amRegisterStep2 = '/am-register/step2';
static const String amRegisterStep3 = '/am-register/step3';
static const String amRegisterStep4 = '/am-register/step4';
static const String home = '/home';
static const String parentDashboard = '/parent-dashboard';
static const String admin_dashboard = '/admin_dashboard';
static const String findNanny = '/find-nanny';
static Route<dynamic> generateRoute(RouteSettings settings) {
Widget screen;
@ -48,8 +55,16 @@ class AppRouter {
screen = const RegisterChoiceScreen();
slideTransition = true;
break;
case legal:
screen = const LegalPage();
slideTransition = true;
break;
case privacy:
screen = const PrivacyPage();
slideTransition = true;
break;
case parentRegisterStep1:
screen = const ParentRegisterStep1Screen();
screen = ParentRegisterStep1Screen();
slideTransition = true;
break;
case parentRegisterStep2:
@ -112,8 +127,14 @@ class AppRouter {
}
slideTransition = true;
break;
case home:
screen = const HomeScreen();
case parentDashboard:
screen = const ParentDashboardScreen();
break;
case admin_dashboard:
screen = const AdminDashboardScreen();
break;
case findNanny:
screen = const FindNannyScreen();
break;
default:
screen = Scaffold(

View File

@ -0,0 +1,86 @@
import 'package:flutter/material.dart';
import 'package:p_tits_pas/widgets/admin/DashboardSidebarAdmin.dart';
import 'package:p_tits_pas/widgets/admin/Statistique_manage_widget.dart';
import 'package:p_tits_pas/widgets/admin/admin_manage_widget.dart';
import 'package:p_tits_pas/widgets/admin/assistante_maternelle_management_widget.dart';
import 'package:p_tits_pas/widgets/admin/gestionnaire_management_widget.dart';
import 'package:p_tits_pas/widgets/admin/parent_managmant_widget.dart';
import 'package:p_tits_pas/widgets/app_footer.dart';
import 'package:p_tits_pas/widgets/admin/dashboard_admin.dart';
class AdminDashboardScreen extends StatefulWidget {
const AdminDashboardScreen({super.key});
@override
_AdminDashboardScreenState createState() => _AdminDashboardScreenState();
}
class _AdminDashboardScreenState extends State<AdminDashboardScreen> {
int selectedIndex = 0;
void onTabChange(int index) {
setState(() {
selectedIndex = index;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(60.0),
child: Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: Colors.grey.shade300),
),
),
child: DashboardAppBarAdmin(
selectedIndex: selectedIndex,
onTabChange: onTabChange,
),
),
),
body: Column(
children: [
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(
width: 250,
child: DashboardSidebarAdmin(
selectedIndex: selectedIndex,
onTabChange: onTabChange,
),
),
Expanded(
flex: 2,
child: _getBody(),
),
],
),
),
const AppFooter(),
],
),
);
}
Widget _getBody() {
switch (selectedIndex) {
case 0:
return const GestionnaireManagementWidget();
case 1:
return const ParentManagementWidget();
case 2:
return const AssistanteMaternelleManagementWidget();
case 3:
return const AdministrateurManagementWidget();
case 4:
return const StatistiqueManageWidget();
default:
return const Center(child: Text("Page non trouvée"));
}
}
}

View File

@ -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'),
),
);
}
}

View File

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

View File

@ -14,4 +14,4 @@ class HomeScreen extends StatelessWidget {
),
);
}
}
}

View File

@ -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,
),
],
),
),
],
),
);
}
}

View File

@ -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"),
),
);
}
}

View File

@ -0,0 +1,32 @@
class ApiConfig {
// static const String baseUrl = 'http://localhost:3000/api/v1/';
static const String baseUrl = 'https://ynov.ptits-pas.fr/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';
// Headers
static Map<String, String> get headers => {
'Content-Type': 'application/json',
'Accept': 'application/json',
};
static Map<String, String> authHeaders(String token) => {
...headers,
'Authorization': 'Bearer $token',
};
}

View File

@ -0,0 +1,71 @@
import 'package:shared_preferences/shared_preferences.dart';
class TokenService {
// static const _storage = FlutterSecureStorage();
static const _tokenKey = 'access_token';
static const String _refreshTokenKey = 'refresh_token';
static const _roleKey = 'user_role';
// Stockage du token
static Future<void> saveToken(String token) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_tokenKey, token);
}
// Stockage du refresh token
static Future<void> saveRefreshToken(String refreshToken) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_refreshTokenKey, refreshToken);
}
// Stockage du rôle
static Future<void> saveRole(String role) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_roleKey, role);
}
// Récupération du token
static Future<String?> getToken() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString(_tokenKey);
}
// Récupération du refresh token
static Future<String?> getRefreshToken() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString(_refreshTokenKey);
}
// Récupération du rôle
static Future<String?> getRole() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString(_roleKey);
}
// Suppression du token
static Future<void> deleteToken() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_tokenKey);
}
// Suppression du refresh token
static Future<void> deleteRefreshToken() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_refreshTokenKey);
}
// Suppression du rôle
static Future<void> deleteRole() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_roleKey);
}
// Nettoyage complet
static Future<void> clearAll() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_tokenKey);
await prefs.remove(_refreshTokenKey);
await prefs.remove(_roleKey);
}
}

View File

@ -1,42 +1,162 @@
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:p_tits_pas/services/api/api_config.dart';
import 'package:p_tits_pas/services/api/tokenService.dart';
import '../models/user.dart';
import 'package:http/http.dart' as http;
class AuthService {
static const String _usersKey = 'users';
static const String _parentsKey = 'parents';
static const String _childrenKey = 'children';
final String baseUrl = ApiConfig.baseUrl;
// Méthode pour se connecter (mode démonstration)
static Future<AppUser> login(String email, String password) async {
await Future.delayed(const Duration(seconds: 1)); // Simule un délai de traitement
throw Exception('Mode démonstration - Connexion désactivée');
//login
Future<Map<String, dynamic>> login(String email, String password) async {
try {
final response = await http.post(
Uri.parse('$baseUrl${ApiConfig.login}'),
headers: ApiConfig.headers,
body: jsonEncode({
'email': email,
'password': password
}),
);
if (response.statusCode == 201) {
final data = jsonDecode(response.body);
await TokenService.saveToken(data['access_token']);
await TokenService.saveRefreshToken(data['refresh_token']);
final role = _extractRoleFromToken(data['access_token']);
await TokenService.saveRole(role);
return data;
} else {
throw Exception('Failed to login: ${response.body}');
}
} catch (e) {
throw Exception('Failed to login: $e');
}
}
// Méthode pour s'inscrire (mode démonstration)
static Future<AppUser> register({
String _extractRoleFromToken(String token) {
try {
final parts = token.split('.');
if (parts.length != 3) return '';
final payload = parts[1];
final normalizedPayload = base64Url.normalize(payload);
final decoded = utf8.decode(base64Url.decode(normalizedPayload));
final Map<String, dynamic> payloadMap = jsonDecode(decoded);
return payloadMap['role'] ?? '';
} catch (e) {
print('Error extracting role from token: $e');
return '';
}
}
Future<void> logout() async {
await TokenService.clearAll();
}
Future<bool> isAuthenticated() async {
final token = await TokenService.getToken();
if (token == null) return false;
return !_isTokenExpired(token);
}
bool _isTokenExpired(String token) {
try {
final parts = token.split('.');
if (parts.length != 3) return true;
final payload = parts[1];
final normalizedPayload = base64Url.normalize(payload);
final decoded = utf8.decode(base64Url.decode(normalizedPayload));
final Map<String, dynamic> payloadMap = jsonDecode(decoded);
final exp = payloadMap['exp'];
if (exp == null) return true;
final expirationDate = DateTime.fromMillisecondsSinceEpoch(exp * 1000);
return DateTime.now().isAfter(expirationDate);
} catch (e) {
return true;
}
}
//register
Future<AppUser> register({
required String email,
required String password,
required String firstName,
required String lastName,
required String role,
}) async {
await Future.delayed(const Duration(seconds: 1)); // Simule un délai de traitement
throw Exception('Mode démonstration - Inscription désactivée');
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');
}
}
// Méthode pour se déconnecter (mode démonstration)
static Future<void> logout() async {
// Ne fait rien en mode démonstration
Future<String> getUserId() async {
final token = await TokenService.getToken();
if (token == null) return '';
try {
final parts = token.split('.');
if (parts.length != 3) return '';
final payload = parts[1];
final normalizedPayload = base64Url.normalize(payload);
final decoded = utf8.decode(base64Url.decode(normalizedPayload));
final Map<String, dynamic> payloadMap = jsonDecode(decoded);
return payloadMap['sub'] ?? '';
} catch (e) {
print('Error extracting user id from token: $e');
return '';
}
}
// Méthode pour vérifier si l'utilisateur est connecté (mode démonstration)
static Future<bool> isLoggedIn() async {
return false; // Toujours non connecté en mode démonstration
}
Future<String?> getUserNameById() async {
final userid = await getUserId();
final token = await TokenService.getToken();
// 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
if (token == null || userid.isEmpty) return null;
try {
final response = await http.get(
Uri.parse('$baseUrl${ApiConfig.users}/$userid'),
headers: {
'Authorization': 'Bearer $token',
'accept': '*/*',
},
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
final firstName = data['prenom'];
// final lastName = data['nom'];
return '$firstName';
} else {
print('Erreur Api: ${response.statusCode} - ${response.body}');
return null;
}
} catch (e) {
print('Error fetching user name: $e');
return null;
}
}
}
}

View File

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

View File

@ -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');
}
}
}

View File

@ -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;
}
}
}

View File

@ -0,0 +1,105 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:p_tits_pas/services/api/api_config.dart';
import 'package:p_tits_pas/services/api/tokenService.dart';
import 'package:http/http.dart' as http;
class UserService {
final String baseUrl = ApiConfig.baseUrl;
//Recuperer tous les utilisateurs
Future<List<Map<String, dynamic>>> getAllUsers() async {
try {
final token = await TokenService.getToken();
if (token == null) {
throw Exception('Token non disponible');
}
final response = await http.get(
Uri.parse('$baseUrl${ApiConfig.users}'),
headers: ApiConfig.authHeaders(token),
);
if (response.statusCode == 200) {
final List<dynamic> data = jsonDecode(response.body);
return data.cast<Map<String, dynamic>>();
} else {
throw Exception('Erreur lors de la récupération des utilisateurs: ${response.statusCode}');
}
} catch (e) {
throw Exception('Erreur de connexion: $e');
}
}
//Récuperer les utilisateurs en fonction du role
Future<List<Map<String, dynamic>>> getUsersByRole(String role) async {
try {
final allUsers = await getAllUsers();
return allUsers.where((user) =>
user['role']?.toString().toLowerCase() == role.toLowerCase()
).toList();
} catch (e) {
throw Exception('Erreur lors de la récupération des utilisateurs par rôle: $e');
}
}
// Filtrer les utilisateurs par statut
Future<List<Map<String, dynamic>>> filterUsersByStatus(String? status) async {
try {
final allUsers = await getAllUsers();
if (status == null || status.isEmpty) return allUsers;
return allUsers
.where((user) =>
user['status']?.toString().toLowerCase() == status.toLowerCase())
.toList();
} catch (e) {
throw Exception('Erreur lors du filtrage: $e');
}
}
/// Supprimer un utilisateur
Future<bool> deleteUser(String userId) async {
try {
final token = await TokenService.getToken();
if (token == null) {
throw Exception('Token non disponible');
}
final response = await http.delete(
Uri.parse('$baseUrl${ApiConfig.users}/$userId'),
headers: ApiConfig.authHeaders(token),
);
return response.statusCode == 200 || response.statusCode == 204;
} catch (e) {
throw Exception('Erreur lors de la suppression: $e');
}
}
/// Récupérer les détails d'un utilisateur
Future<Map<String, dynamic>?> getUserById(String userId) async {
try {
final token = await TokenService.getToken();
if (token == null) {
throw Exception('Token non disponible');
}
final response = await http.get(
Uri.parse('$baseUrl${ApiConfig.users}/$userId'),
headers: ApiConfig.authHeaders(token),
);
if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
return null;
}
} catch (e) {
throw Exception('Erreur lors de la récupération de l\'utilisateur: $e');
}
}
}

View File

@ -0,0 +1,70 @@
import 'package:flutter/material.dart';
class DashboardSidebarAdmin extends StatelessWidget {
final int selectedIndex;
final ValueChanged<int> onTabChange;
const DashboardSidebarAdmin({
Key? key,
required this.selectedIndex,
required this.onTabChange,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final items = [
{'title': 'Gestionnaires', 'icon': Icons.admin_panel_settings},
{'title': 'Parents', 'icon': Icons.family_restroom},
{'title': 'Assistantes maternelles', 'icon': Icons.woman},
{'title': 'Administrateurs', 'icon': Icons.supervisor_account},
{'title': 'Statistiques', 'icon': Icons.bar_chart},
];
return Container(
width: 250,
color: const Color(0xFFF7F7F7),
padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Avatar en haut
Center(
child: Column(
children: const [
CircleAvatar(radius: 32, child: Icon(Icons.person, size: 32)),
SizedBox(height: 8),
Text("Admin", style: TextStyle(fontWeight: FontWeight.bold)),
],
),
),
const SizedBox(height: 32),
const Text("Navigation", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 16),
// Navigation
...List.generate(items.length, (index) {
final item = items[index];
final isActive = index == selectedIndex;
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: ListTile(
tileColor: isActive ? const Color(0xFF9CC5C0) : null,
leading: Icon(item['icon'] as IconData, color: isActive ? Color(0xFF9CC5C0) : Colors.black54),
title: Text(
item['title'] as String,
style: TextStyle(
color: isActive ? Color(0xFF9CC5C0) : Colors.black,
fontWeight: isActive ? FontWeight.bold : FontWeight.normal,
),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
onTap: () => onTabChange(index),
),
);
}),
],
),
);
}
}

View File

@ -0,0 +1,15 @@
import 'package:flutter/material.dart';
class StatistiqueManageWidget extends StatelessWidget {
const StatistiqueManageWidget({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: Text(
'Statistiques',
style: Theme.of(context).textTheme.headlineMedium,
),
);
}
}

View File

@ -0,0 +1,132 @@
import 'package:flutter/material.dart';
import 'package:p_tits_pas/widgets/admin/base_user_management.dart';
class AdministrateurManagementWidget extends StatelessWidget {
const AdministrateurManagementWidget({super.key});
@override
Widget build(BuildContext context) {
return BaseUserManagementWidget(
config: UserDisplayConfig(
title: 'Administrateur',
role: 'administrateur',
defaultIcon: Icons.admin_panel_settings,
filterFields: [
FilterField(
label: 'Rechercher',
hint: 'Nom ou email',
type: FilterType.text,
filter: (user, query) {
final fullName =
'${user['firstName'] ?? ''} ${user['lastName'] ?? ''}'
.toLowerCase();
final email = (user['email'] ?? '').toLowerCase();
return fullName.contains(query.toLowerCase()) ||
email.contains(query.toLowerCase());
},
),
FilterField(
label: 'Statut',
hint: 'Tous',
type: FilterType.dropdown,
options: ['actif', 'en_attente', 'inactif', 'supprimé'],
filter: (user, status) {
if (status.isEmpty) return true;
return user['statut']?.toString().toLowerCase() ==
status.toLowerCase();
},
),
],
actions: [
const UserAction(
icon: Icons.edit,
color: Colors.orange,
tooltip: 'Modifier',
onPressed: _editAdministrateur,
),
const UserAction(
icon: Icons.security,
color: Colors.blue,
tooltip: 'Gérer droits',
onPressed: _manageRights,
),
const UserAction(
icon: Icons.lock_reset,
color: Colors.purple,
tooltip: 'Réinitialiser MDP',
onPressed: _resetPassword,
),
const UserAction(
icon: Icons.toggle_on,
color: Colors.green,
tooltip: 'Activer/Désactiver',
onPressed: _toggleStatus,
),
const UserAction(
icon: Icons.delete,
color: Colors.red,
tooltip: 'Supprimer',
onPressed: _deleteAdministrateur,
),
],
getDisplayName: (user) => '${user['prenom'] ?? ''} ${user['nom'] ?? ''}',
getSubtitle: (user) {
final email = user['email'] ?? '';
final profession = user['profession'] ?? 'Non spécifiée';
final ville = user['ville'] ?? '';
// final statut = AdministrateurService.getStatutDisplay(user['statut']);
final statut = user['statut'] ?? 'inactif';
final changementMdp =
user['changement_mdp_obligatoire'] == true ? 'MDP à changer' : '';
return '$email\n$profession${ville.isNotEmpty ? '$ville' : ''}\nStatut: $statut ${changementMdp.isNotEmpty ? '$changementMdp' : ''}';
},
));
}
static Future<void> _editAdministrateur(
BuildContext context, Map<String, dynamic> user) async {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Modifier administrateur: ${user['prenom']} ${user['nom']}')),
);
}
static Future<void> _manageRights(
BuildContext context, Map<String, dynamic> user) async {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Gérer droits pour: ${user['prenom']} ${user['nom']}')),
);
}
static Future<void> _resetPassword(
BuildContext context, Map<String, dynamic> user) async {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Réinitialiser mot de passe pour: ${user['prenom']} ${user['nom']}')),
);
}
static Future<void> _toggleStatus(
BuildContext context, Map<String, dynamic> user) async {
final currentStatus = user['statut'] ?? 'inactif';
final newStatus = currentStatus == 'actif' ? 'inactif' : 'actif';
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'${newStatus == 'actif' ? 'Activer' : 'Désactiver'} administrateur: ${user['prenom']} ${user['nom']}')),
);
}
static Future<void> _deleteAdministrateur(
BuildContext context, Map<String, dynamic> user) async {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Supprimer administrateur: ${user['prenom']} ${user['nom']}')),
);
}
}

View File

@ -0,0 +1,278 @@
import 'package:flutter/material.dart';
import 'package:p_tits_pas/services/user_service.dart';
import 'package:p_tits_pas/widgets/admin/base_user_management.dart';
class AssistanteMaternelleManagementWidget extends StatelessWidget {
const AssistanteMaternelleManagementWidget({super.key});
@override
Widget build(BuildContext context) {
return BaseUserManagementWidget(
config: UserDisplayConfig(
title: 'Assistantes Maternelles',
role: 'assistante_maternelle',
defaultIcon: Icons.face,
filterFields: [
FilterField(
label: 'Rechercher',
hint: 'Nom ou email',
type: FilterType.text,
filter: (user, query) {
final fullName = '${user['prenom'] ?? ''} ${user['nom'] ?? ''}'.toLowerCase();
final email = (user['email'] ?? '').toLowerCase();
return fullName.contains(query.toLowerCase()) ||
email.contains(query.toLowerCase());
},
),
FilterField(
label: 'Zone géographique',
hint: 'Ville ou département',
type: FilterType.text,
filter: (user, query) {
final zone = (user['zone'] ?? user['ville'] ?? user['code_postal'] ?? '').toLowerCase();
return zone.contains(query.toLowerCase());
},
),
FilterField(
label: 'Capacité minimum',
hint: 'Nombre d\'enfants',
type: FilterType.number,
filter: (user, query) {
final capacite = int.tryParse(user['capacite']?.toString() ?? '0') ?? 0;
final minCapacite = int.tryParse(query) ?? 0;
return capacite >= minCapacite;
},
),
FilterField(
label: 'Statut',
hint: 'Tous',
type: FilterType.dropdown,
options: ['actif', 'en_attente', 'inactif'],
filter: (user, status) {
if (status.isEmpty) return true;
return user['statut']?.toString().toLowerCase() == status.toLowerCase();
},
),
],
actions: [
const UserAction(
icon: Icons.edit,
color: Colors.orange,
tooltip: 'Modifier',
onPressed: _editAssistante,
),
const UserAction(
icon: Icons.delete,
color: Colors.red,
tooltip: 'Supprimer',
onPressed: _deleteAssistante,
),
const UserAction(
icon: Icons.location_on,
color: Colors.green,
tooltip: 'Voir zone',
onPressed: _showZone,
),
],
getSubtitle: (user) {
final email = user['email'] ?? '';
final numeroAgrement = user['numeroAgrement'] ?? user['agrement'] ?? 'N/A';
final zone = user['code_postal'] ?? user['ville'] ?? 'Non spécifiée';
final capacite = user['capacite'] ?? user['capaciteAccueil'] ?? 'N/A';
return '$email\nN° Agrément: $numeroAgrement\nZone: $zone | Capacité: $capacite';
},
getDisplayName: (user) => '${user['prenom'] ?? ''} ${user['nom'] ?? ''}',
),
);
}
static Future<void> _editAssistante(BuildContext context, Map<String, dynamic> assistante) async {
// TODO: Implémenter l'édition
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Fonctionnalité de modification à implémenter'),
backgroundColor: Colors.orange,
),
);
}
static Future<void> _deleteAssistante(BuildContext context, Map<String, dynamic> assistante) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Confirmer la suppression'),
content: Text(
'Êtes-vous sûr de vouloir supprimer le compte de ${assistante['firstName']} ${assistante['lastName']} ?\n\n'
'Cette action supprimera également tous les contrats et données associés.'
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Annuler'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('Supprimer'),
),
],
),
);
if (confirmed == true) {
try {
final userService = UserService();
final success = await userService.deleteUser(assistante['id']);
if (success && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${assistante['firstName']} ${assistante['lastName']} supprimé avec succès'),
backgroundColor: Colors.green,
),
);
// Le widget se rechargera automatiquement via le système de state
} else {
throw Exception('Erreur lors de la suppression');
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur: ${e.toString()}'),
backgroundColor: Colors.red,
),
);
}
}
}
}
static Future<void> _showZone(BuildContext context, Map<String, dynamic> assistante) async {
final zone = assistante['zone'] ?? assistante['ville'] ?? 'Non spécifiée';
final adresse = assistante['adresse'] ?? assistante['address'] ?? '';
final codePostal = assistante['codePostal'] ?? assistante['zipCode'] ?? '';
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Zone d\'intervention - ${assistante['firstName']} ${assistante['lastName']}'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (zone.isNotEmpty) Text('Zone: $zone', style: const TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
if (adresse.isNotEmpty) Text('Adresse: $adresse'),
if (codePostal.isNotEmpty) Text('Code postal: $codePostal'),
const SizedBox(height: 16),
Text('Capacité d\'accueil: ${assistante['capacite'] ?? assistante['capaciteAccueil'] ?? 'N/A'} enfants'),
Text('N° Agrément: ${assistante['numeroAgrement'] ?? assistante['agrement'] ?? 'N/A'}'),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Fermer'),
),
TextButton(
onPressed: () {
// TODO: Ouvrir dans Maps
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Intégration Maps à implémenter'),
),
);
},
child: const Text('Voir sur la carte'),
),
],
),
);
}
}
// return Padding(
// padding: const EdgeInsets.all(16),
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// // 🔎 Zone de filtre
// _buildFilterSection(),
// const SizedBox(height: 16),
// // 📋 Liste des assistantes
// ListView.builder(
// shrinkWrap: true,
// physics: const NeverScrollableScrollPhysics(),
// itemCount: assistantes.length,
// itemBuilder: (context, index) {
// final assistante = assistantes[index];
// return Card(
// margin: const EdgeInsets.symmetric(vertical: 8),
// child: ListTile(
// leading: const Icon(Icons.face),
// title: Text(assistante['nom'].toString()),
// subtitle: Text(
// "N° Agrément : ${assistante['numeroAgrement']}\nZone : ${assistante['zone']} | Capacité : ${assistante['capacite']}"),
// trailing: Row(
// mainAxisSize: MainAxisSize.min,
// children: [
// IconButton(
// icon: const Icon(Icons.edit),
// onPressed: () {
// // TODO: Ajouter modification
// },
// ),
// IconButton(
// icon: const Icon(Icons.delete),
// onPressed: () {
// // TODO: Ajouter suppression
// },
// ),
// ],
// ),
// ),
// );
// },
// ),
// ],
// ),
// );
// }
// Widget _buildFilterSection() {
// return Wrap(
// spacing: 16,
// runSpacing: 8,
// children: [
// SizedBox(
// width: 200,
// child: TextField(
// decoration: const InputDecoration(
// labelText: "Zone géographique",
// border: OutlineInputBorder(),
// ),
// onChanged: (value) {
// // TODO: Ajouter logique de filtrage par zone
// },
// ),
// ),
// SizedBox(
// width: 200,
// child: TextField(
// decoration: const InputDecoration(
// labelText: "Capacité minimum",
// border: OutlineInputBorder(),
// ),
// keyboardType: TextInputType.number,
// onChanged: (value) {
// // TODO: Ajouter logique de filtrage par capacité
// },
// ),
// ),
// ],
// );
// }
// }

View File

@ -0,0 +1,424 @@
import 'package:flutter/material.dart';
import 'package:p_tits_pas/services/user_service.dart';
/// Configuration pour personnaliser l'affichage des utilisateurs
class UserDisplayConfig {
final String title;
final String role;
final IconData defaultIcon;
final List<FilterField> filterFields;
final List<UserAction> actions;
final String Function(Map<String, dynamic>) getSubtitle;
final String Function(Map<String, dynamic>) getDisplayName;
const UserDisplayConfig({
required this.title,
required this.role,
required this.defaultIcon,
required this.filterFields,
required this.actions,
required this.getSubtitle,
required this.getDisplayName,
});
}
/// Configuration d'un champ de filtre
class FilterField {
final String label;
final String hint;
final FilterType type;
final List<String>? options;
final bool Function(Map<String, dynamic>, String) filter;
const FilterField({
required this.label,
required this.hint,
required this.type,
required this.filter,
this.options,
});
}
enum FilterType { text, dropdown, number }
/// Configuration d'une action sur un utilisateur
class UserAction {
final IconData icon;
final Color color;
final String tooltip;
final Future<void> Function(BuildContext, Map<String, dynamic>) onPressed;
const UserAction({
required this.icon,
required this.color,
required this.tooltip,
required this.onPressed,
});
}
/// Widget de gestion d'utilisateurs réutilisable
class BaseUserManagementWidget extends StatefulWidget {
final UserDisplayConfig config;
const BaseUserManagementWidget({
super.key,
required this.config,
});
@override
State<BaseUserManagementWidget> createState() =>
_BaseUserManagementWidgetState();
}
class _BaseUserManagementWidgetState extends State<BaseUserManagementWidget> {
final UserService _userService = UserService();
final Map<String, TextEditingController> _filterControllers = {};
List<Map<String, dynamic>> _allUsers = [];
List<Map<String, dynamic>> _filteredUsers = [];
bool _isLoading = true;
String? _error;
@override
void initState() {
super.initState();
_initializeFilters();
_loadUsers();
}
void _initializeFilters() {
for (final field in widget.config.filterFields) {
_filterControllers[field.label] = TextEditingController();
}
}
@override
void dispose() {
for (final controller in _filterControllers.values) {
controller.dispose();
}
super.dispose();
}
Future<void> _loadUsers() async {
setState(() {
_isLoading = true;
_error = null;
});
try {
final users = await _userService.getUsersByRole(widget.config.role);
setState(() {
_allUsers = users;
_filteredUsers = users;
_isLoading = false;
});
} catch (e) {
setState(() {
_error = e.toString();
_isLoading = false;
});
}
}
void _applyFilters() {
setState(() {
_filteredUsers = _allUsers.where((user) {
return widget.config.filterFields.every((field) {
final controller = _filterControllers[field.label];
if (controller == null || controller.text.isEmpty) return true;
return field.filter(user, controller.text);
});
}).toList();
});
}
String _getStatusDisplay(Map<String, dynamic> user) {
final status = user['statut'];
if (status == null) return 'Non défini';
switch (status.toString().toLowerCase()) {
case 'actif':
return 'Actif';
case 'en_attente':
return 'En attente';
case 'inactif':
return 'Inactif';
case 'deleted':
return 'Supprimé';
default:
return status.toString();
}
}
Color _getStatusColor(Map<String, dynamic> user) {
final status = user['statut']?.toString().toLowerCase();
switch (status) {
case 'actif':
return Colors.green;
case 'en_attente':
return Colors.orange;
case 'inactif':
return Colors.grey;
case 'supprimé':
return Colors.red;
default:
return Colors.grey;
}
}
void _showUserDetails(Map<String, dynamic> user) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(widget.config.getDisplayName(user)),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Email: ${user['email']}'),
Text('Rôle: ${user['role']}'),
Text('Statut: ${_getStatusDisplay(user)}'),
Text('ID: ${user['id']}'),
if (user['createdAt'] != null)
Text(
'Créé le: ${DateTime.parse(user['createdAt']).toLocal().toString().split(' ')[0]}'),
// Affichage des champs spécifiques selon le type d'utilisateur
...user.entries
.where((e) => ![
'id',
'email',
'role',
'status',
'createdAt',
'updatedAt',
'firstName',
'lastName'
].contains(e.key))
.map((e) => Text('${e.key}: ${e.value}'))
.toList(),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Fermer'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${widget.config.title} (${_filteredUsers.length})',
style: Theme.of(context).textTheme.headlineSmall,
),
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _loadUsers,
tooltip: 'Actualiser',
),
],
),
const SizedBox(height: 16),
_buildFilterSection(),
const SizedBox(height: 16),
Expanded(
child: _buildUsersList(),
),
],
),
);
}
Widget _buildFilterSection() {
return Wrap(
spacing: 16,
runSpacing: 8,
children: widget.config.filterFields.map((field) {
final controller = _filterControllers[field.label]!;
switch (field.type) {
case FilterType.text:
case FilterType.number:
return SizedBox(
width: 250,
child: TextField(
controller: controller,
keyboardType: field.type == FilterType.number
? TextInputType.number
: TextInputType.text,
decoration: InputDecoration(
labelText: field.label,
hintText: field.hint,
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.search),
),
onChanged: (value) => _applyFilters(),
),
);
case FilterType.dropdown:
return SizedBox(
width: 200,
child: DropdownButtonFormField<String>(
decoration: InputDecoration(
labelText: field.label,
border: const OutlineInputBorder(),
),
items: [
const DropdownMenuItem(value: '', child: Text("Tous")),
...?field.options?.map((option) =>
DropdownMenuItem(value: option, child: Text(option))),
],
onChanged: (value) {
controller.text = value ?? '';
_applyFilters();
},
),
);
}
}).toList(),
);
}
Widget _buildUsersList() {
if (_isLoading) {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Chargement...'),
],
),
);
}
if (_error != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 48, color: Colors.red[300]),
const SizedBox(height: 16),
Text(
'Erreur de chargement',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(
_error!,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _loadUsers,
child: const Text('Réessayer'),
),
],
),
);
}
if (_filteredUsers.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.people_outline, size: 48, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
_allUsers.isEmpty ? 'Aucun utilisateur trouvé' : 'Aucun résultat',
style: Theme.of(context).textTheme.titleLarge,
),
if (_allUsers.isNotEmpty) ...[
const SizedBox(height: 8),
const Text('Essayez de modifier vos critères de recherche'),
],
],
),
);
}
return ListView.builder(
itemCount: _filteredUsers.length,
itemBuilder: (context, index) {
final user = _filteredUsers[index];
return Card(
margin: const EdgeInsets.symmetric(vertical: 4),
child: ListTile(
leading: CircleAvatar(
backgroundColor: _getStatusColor(user).withOpacity(0.2),
child: Icon(
widget.config.defaultIcon,
color: _getStatusColor(user),
),
),
title: Text(widget.config.getDisplayName(user)),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(widget.config.getSubtitle(user)),
const SizedBox(height: 4),
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: _getStatusColor(user).withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: _getStatusColor(user)),
),
child: Text(
_getStatusDisplay(user),
style: TextStyle(
color: _getStatusColor(user),
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
],
),
],
),
isThreeLine: true,
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.visibility, color: Colors.blue),
tooltip: "Voir détails",
onPressed: () => _showUserDetails(user),
),
...widget.config.actions
.map(
(action) => IconButton(
icon: Icon(action.icon, color: action.color),
tooltip: action.tooltip,
onPressed: () => action.onPressed(context, user),
),
)
.toList(),
],
),
),
);
},
);
}
}

View File

@ -0,0 +1,172 @@
import 'package:flutter/material.dart';
import 'package:p_tits_pas/services/auth_service.dart';
class DashboardAppBarAdmin extends StatelessWidget implements PreferredSizeWidget {
final int selectedIndex;
final ValueChanged<int> onTabChange;
const DashboardAppBarAdmin({Key? key, required this.selectedIndex, required this.onTabChange}) : super(key: key);
void _Logout(BuildContext context) async {
try {
final authS = AuthService();
await authS.logout();
Navigator.of(context).pushReplacementNamed('/login');
} catch (e) {
print('Erreur lors de la déconnexion: $e');
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Erreur lors de la déconnexion'),
backgroundColor: Colors.red,
),
);
}
}
Future<String> _getUserName() async {
final authS = AuthService();
final userName = await authS.getUserNameById();
return userName ?? 'Admin';
}
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight + 10);
@override
Widget build(BuildContext context) {
final isMobile = MediaQuery.of(context).size.width < 768;
return AppBar(
elevation: 0,
automaticallyImplyLeading: false,
title: Row(
children: [
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, 'Gestionnaires', 0),
// const SizedBox(width: 24),
// _buildNavItem(context, 'Parents', 1),
// const SizedBox(width: 24),
// _buildNavItem(context, 'Assistantes maternelles', 2),
// const SizedBox(width: 24),
// _buildNavItem(context, 'Administrateurs', 4),
],
),
actions: isMobile
? [_buildMobileMenu(context)]
: [
// Nom de l'utilisateur
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Center(
child: FutureBuilder<String>(
future: _getUserName(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Text("Chargement...");
} else if (snapshot.hasError) {
return const Text("Erreur");
} else {
return Text(snapshot.data ?? "Admin");
}
}
),
),
),
// 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 == 4) {
_handleLogout(context);
}
},
itemBuilder: (context) => [
const PopupMenuItem(value: 0, child: Text("Gestionnaires")),
const PopupMenuItem(value: 1, child: Text("Parents")),
const PopupMenuItem(value: 2, child: Text("Assistantes maternelles")),
const PopupMenuItem(value: 3, child: Text("Administrateurs")),
const PopupMenuDivider(),
const PopupMenuItem(value: 4, 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);
_Logout(context);
},
child: const Text('Déconnecter'),
),
],
),
);
}
}

View File

@ -0,0 +1,75 @@
import 'package:flutter/material.dart';
class GestionnaireCard extends StatelessWidget {
final String name;
final String email;
const GestionnaireCard({
Key? key,
required this.name,
required this.email,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 🔹 Infos principales
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(name, style: const TextStyle(fontWeight: FontWeight.bold)),
Text(email, style: const TextStyle(color: Colors.grey)),
],
),
const SizedBox(height: 12),
// 🔹 Attribution à des RPE (dropdown fictif ici)
Row(
children: [
const Text("RPE attribué : "),
const SizedBox(width: 8),
DropdownButton<String>(
value: "RPE 1",
items: const [
DropdownMenuItem(value: "RPE 1", child: Text("RPE 1")),
DropdownMenuItem(value: "RPE 2", child: Text("RPE 2")),
DropdownMenuItem(value: "RPE 3", child: Text("RPE 3")),
],
onChanged: (value) {},
),
],
),
const SizedBox(height: 12),
// 🔹 Boutons d'action
Row(
children: [
TextButton.icon(
onPressed: () {
// Réinitialisation mot de passe
},
icon: const Icon(Icons.lock_reset),
label: const Text("Réinitialiser MDP"),
),
const SizedBox(width: 12),
TextButton.icon(
onPressed: () {
// Suppression du compte
},
icon: const Icon(Icons.delete, color: Colors.red),
label: const Text("Supprimer", style: TextStyle(color: Colors.red)),
),
],
)
],
),
),
);
}
}

View File

@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
import 'package:p_tits_pas/widgets/admin/gestionnaire_card.dart';
class GestionnaireManagementWidget extends StatelessWidget {
const GestionnaireManagementWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 🔹 Barre du haut avec bouton
Row(
children: [
const Expanded(
child: TextField(
decoration: InputDecoration(
hintText: "Rechercher un gestionnaire...",
prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(),
),
),
),
const SizedBox(width: 16),
ElevatedButton.icon(
onPressed: () {
// Rediriger vers la page de création
},
icon: const Icon(Icons.add),
label: const Text("Créer un gestionnaire"),
),
],
),
const SizedBox(height: 24),
// 🔹 Liste des gestionnaires
Expanded(
child: ListView.builder(
itemCount: 5, // À remplacer par liste dynamique
itemBuilder: (context, index) {
return GestionnaireCard(
name: "Dupont $index",
email: "dupont$index@mail.com",
);
},
),
)
],
),
);
}
}

View File

@ -0,0 +1,195 @@
import 'package:flutter/material.dart';
import 'package:p_tits_pas/services/user_service.dart';
import 'package:p_tits_pas/widgets/admin/base_user_management.dart';
class ParentManagementWidget extends StatelessWidget {
const ParentManagementWidget({super.key});
@override
Widget build(BuildContext context) {
return BaseUserManagementWidget(
config: UserDisplayConfig(
title: 'Gestion des Parents',
role: 'parent',
defaultIcon: Icons.person_outline,
filterFields: [
FilterField(
label: 'Rechercher',
hint: 'Nom ou email',
type: FilterType.text,
filter: (user, query) {
final fullName = '${user['prenom'] ?? ''} ${user['nom'] ?? ''}'.toLowerCase();
final email = (user['email'] ?? '').toLowerCase();
return fullName.contains(query.toLowerCase()) ||
email.contains(query.toLowerCase());
},
),
FilterField(
label: 'Statut',
hint: 'Tous',
type: FilterType.dropdown,
options: ['actif', 'en_attente', 'inactif', 'supprimé'],
filter: (user, status) {
if (status.isEmpty) return true;
return user['statut']?.toString().toLowerCase() == status.toLowerCase();
},
),
FilterField(
label: 'Nombre d\'enfants',
hint: 'Minimum',
type: FilterType.number,
filter: (user, query) {
final nombreEnfants = int.tryParse(user['nombreEnfants']?.toString() ?? '0') ?? 0;
final minEnfants = int.tryParse(query) ?? 0;
return nombreEnfants >= minEnfants;
},
),
],
actions: [
const UserAction(
icon: Icons.edit,
color: Colors.orange,
tooltip: 'Modifier',
onPressed: _editParent,
),
const UserAction(
icon: Icons.delete,
color: Colors.red,
tooltip: 'Supprimer',
onPressed: _deleteParent,
),
const UserAction(
icon: Icons.child_care,
color: Colors.purple,
tooltip: 'Voir enfants',
onPressed: _showChildren,
),
],
getDisplayName: (user) => '${user['prenom'] ?? ''} ${user['nom'] ?? ''}',
getSubtitle: (user) {
final email = user['email'] ?? '';
final nombreEnfants = user['nombreEnfants'] ?? user['children']?.length ?? 0;
return '$email\nNombre d\'enfants: $nombreEnfants';
},
),
);
}
static Future<void> _editParent(BuildContext context, Map<String, dynamic> parent) async {
// TODO: Implémenter l'édition
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Fonctionnalité de modification à implémenter'),
backgroundColor: Colors.orange,
),
);
}
static Future<void> _deleteParent(BuildContext context, Map<String, dynamic> parent) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Confirmer la suppression'),
content: Text(
'Êtes-vous sûr de vouloir supprimer le compte de ${parent['prenom']} ${parent['nom']} ?\n\n'
'Cette action supprimera également tous les contrats et données associés.'
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Annuler'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('Supprimer'),
),
],
),
);
if (confirmed == true) {
try {
final userService = UserService();
final success = await userService.deleteUser(parent['id']);
if (success && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${parent['prenom']} ${parent['nom']} supprimé avec succès'),
backgroundColor: Colors.green,
),
);
} else {
throw Exception('Erreur lors de la suppression');
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur: ${e.toString()}'),
backgroundColor: Colors.red,
),
);
}
}
}
}
static Future<void> _showChildren(BuildContext context, Map<String, dynamic> parent) async {
final children = parent['children'] as List<dynamic>? ?? [];
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Enfants de ${parent['prenom']} ${parent['nom']}'),
content: Container(
width: double.maxFinite,
constraints: const BoxConstraints(maxHeight: 400),
child: children.isEmpty
? const Text('Aucun enfant enregistré')
: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: children.map((child) => Card(
child: ListTile(
leading: const Icon(Icons.child_care),
title: Text(child['prenom'] ?? child['firstName'] ?? 'Nom non défini'),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (child['dateNaissance'] != null || child['birthDate'] != null)
Text('Né(e) le: ${child['dateNaissance'] ?? child['birthDate']}'),
if (child['age'] != null)
Text('Âge: ${child['age']} ans'),
],
),
isThreeLine: true,
),
)).toList(),
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Fermer'),
),
if (children.isNotEmpty)
TextButton(
onPressed: () {
Navigator.of(context).pop();
// TODO: Naviguer vers la gestion des enfants
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Navigation vers la gestion des enfants à implémenter'),
),
);
},
child: const Text('Gérer les enfants'),
),
],
),
);
}
}

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,
);
}
}

View File

@ -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()
],
),
);
}
}

View File

@ -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!,
],
),
);
}
}

View File

@ -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,
),
),
);
}
}

View File

@ -0,0 +1,171 @@
import 'package:flutter/material.dart';
import 'package:p_tits_pas/services/auth_service.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);
Future <String> _getUserName() async {
final authS = AuthService();
final userName = await authS.getUserNameById();
return userName ?? 'Bienvenue';
}
void _logout (BuildContext context) {
try {
final authS = AuthService();
authS.logout();
Navigator.of(context).pushReplacementNamed('/login');
} catch (e) {
print('Erreur lors de la déconnexion: $e');
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Erreur lors de la déconnexion'),
backgroundColor: Colors.red,
),
);
}
}
@override
Widget build(BuildContext context) {
final isMobile = MediaQuery.of(context).size.width < 768;
return AppBar(
// backgroundColor: Colors.white,
elevation: 0,
title: Row(
children: [
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
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Center(
child: FutureBuilder<String>(
future: _getUserName(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Text("Chargement...");
} else if (snapshot.hasError) {
return const Text("Erreur");
} else {
return Text(snapshot.data ?? "Admin");
}
}
),
),
),
// 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);
_logout(context);
// TODO: Implémenter la logique de déconnexion
},
child: const Text('Déconnecter'),
),
],
),
);
}
}

View File

@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
import 'package:p_tits_pas/widgets/dashbord_parent/ChildrenSidebarwidget.dart';
import 'package:p_tits_pas/widgets/dashbord_parent/wid_mainContentArea.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
),
),
],
);
}

View File

@ -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,
),
),
],
),
)
],
),
);
}
}

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;
}
}
}

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
frontend/nginx.conf Normal file
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;
}

View File

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

View File

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

View File

@ -169,10 +169,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:

View File

@ -18,7 +18,7 @@ dependencies:
image_picker: ^1.0.7
js: ^0.6.7
url_launcher: ^6.2.4
http: ^1.2.0
http: ^1.2.2
dev_dependencies:
flutter_test:

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 {

View File

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

View File

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

View File

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

View File

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

View File

@ -1 +0,0 @@

View File

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

0
test-hook.txt Normal file
View File