Compare commits
No commits in common. "master" and "feature/FRONT-05b" have entirely different histories.
master
...
feature/FR
@ -1,4 +0,0 @@
|
|||||||
# 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
1
.gitignore
vendored
@ -52,4 +52,3 @@ Xcf/**
|
|||||||
# Release notes
|
# Release notes
|
||||||
CHANGELOG.md
|
CHANGELOG.md
|
||||||
Ressources/
|
Ressources/
|
||||||
.env
|
|
||||||
|
|||||||
@ -1,62 +0,0 @@
|
|||||||
# 🎨 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
Normal file
3320
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
36
backend/package.json
Normal file
36
backend/package.json
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"name": "petitspas-backend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Backend pour l'application P'titsPas",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
|
||||||
|
"build": "tsc",
|
||||||
|
"test": "jest",
|
||||||
|
"init-admin": "ts-node src/scripts/initAdmin.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nestjs/common": "^11.1.0",
|
||||||
|
"@prisma/client": "^6.7.0",
|
||||||
|
"@types/jsonwebtoken": "^9.0.9",
|
||||||
|
"bcrypt": "^5.1.1",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"helmet": "^7.1.0",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"morgan": "^1.10.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bcrypt": "^5.0.2",
|
||||||
|
"@types/cors": "^2.8.17",
|
||||||
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/helmet": "^4.0.0",
|
||||||
|
"@types/morgan": "^1.9.9",
|
||||||
|
"@types/node": "^20.11.19",
|
||||||
|
"prisma": "^6.7.0",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"ts-node-dev": "^2.0.0",
|
||||||
|
"typescript": "^5.3.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
108
backend/prisma/schema.prisma
Normal file
108
backend/prisma/schema.prisma
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
// This is your Prisma schema file,
|
||||||
|
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||||
|
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "postgresql"
|
||||||
|
url = env("DATABASE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modèle pour les parents
|
||||||
|
model Parent {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
email String @unique
|
||||||
|
password String
|
||||||
|
firstName String
|
||||||
|
lastName String
|
||||||
|
phoneNumber String?
|
||||||
|
address String?
|
||||||
|
status AccountStatus @default(PENDING)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
children Child[]
|
||||||
|
contracts Contract[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modèle pour les enfants
|
||||||
|
model Child {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
firstName String
|
||||||
|
dateOfBirth DateTime
|
||||||
|
photoUrl String?
|
||||||
|
photoConsent Boolean @default(false)
|
||||||
|
isMultiple Boolean @default(false)
|
||||||
|
isUnborn Boolean @default(false)
|
||||||
|
parentId String
|
||||||
|
parent Parent @relation(fields: [parentId], references: [id])
|
||||||
|
contracts Contract[]
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modèle pour les contrats
|
||||||
|
model Contract {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
parentId String
|
||||||
|
childId String
|
||||||
|
startDate DateTime
|
||||||
|
endDate DateTime?
|
||||||
|
status ContractStatus @default(ACTIVE)
|
||||||
|
parent Parent @relation(fields: [parentId], references: [id])
|
||||||
|
child Child @relation(fields: [childId], references: [id])
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modèle pour les thèmes
|
||||||
|
model Theme {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
name String @unique
|
||||||
|
primaryColor String
|
||||||
|
secondaryColor String
|
||||||
|
backgroundColor String
|
||||||
|
textColor String
|
||||||
|
isActive Boolean @default(false)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
appSettings AppSettings[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modèle pour les paramètres de l'application
|
||||||
|
model AppSettings {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
currentThemeId String
|
||||||
|
currentTheme Theme @relation(fields: [currentThemeId], references: [id])
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@index([currentThemeId])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modèle pour les administrateurs
|
||||||
|
model Admin {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
email String @unique
|
||||||
|
password String
|
||||||
|
firstName String
|
||||||
|
lastName String
|
||||||
|
passwordChanged Boolean @default(false)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enums
|
||||||
|
enum AccountStatus {
|
||||||
|
PENDING
|
||||||
|
VALIDATED
|
||||||
|
REJECTED
|
||||||
|
SUSPENDED
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ContractStatus {
|
||||||
|
ACTIVE
|
||||||
|
ENDED
|
||||||
|
CANCELLED
|
||||||
|
}
|
||||||
18
backend/src/admin/admin.controller.ts
Normal file
18
backend/src/admin/admin.controller.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Controller, Post, Body, UseGuards, Req } from '@nestjs/common';
|
||||||
|
import { AdminService } from './admin.service';
|
||||||
|
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||||
|
|
||||||
|
@Controller('admin')
|
||||||
|
export class AdminController {
|
||||||
|
constructor(private readonly adminService: AdminService) {}
|
||||||
|
|
||||||
|
@Post('change-password')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
async changePassword(
|
||||||
|
@Req() req,
|
||||||
|
@Body('oldPassword') oldPassword: string,
|
||||||
|
@Body('newPassword') newPassword: string,
|
||||||
|
) {
|
||||||
|
return this.adminService.changePassword(req.user.id, oldPassword, newPassword);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
backend/src/admin/admin.module.ts
Normal file
18
backend/src/admin/admin.module.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { AdminController } from './admin.controller';
|
||||||
|
import { AdminService } from './admin.service';
|
||||||
|
import { PrismaModule } from '../prisma/prisma.module';
|
||||||
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
PrismaModule,
|
||||||
|
JwtModule.register({
|
||||||
|
secret: process.env.JWT_SECRET,
|
||||||
|
signOptions: { expiresIn: '1d' },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
controllers: [AdminController],
|
||||||
|
providers: [AdminService],
|
||||||
|
})
|
||||||
|
export class AdminModule {}
|
||||||
40
backend/src/admin/admin.service.ts
Normal file
40
backend/src/admin/admin.service.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
|
import { JwtService } from '@nestjs/jwt';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
import * as bcrypt from 'bcrypt';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AdminService {
|
||||||
|
constructor(
|
||||||
|
private prisma: PrismaService,
|
||||||
|
private jwtService: JwtService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async changePassword(adminId: string, oldPassword: string, newPassword: string) {
|
||||||
|
// Récupérer l'administrateur
|
||||||
|
const admin = await this.prisma.admin.findUnique({
|
||||||
|
where: { id: adminId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!admin) {
|
||||||
|
throw new UnauthorizedException('Administrateur non trouvé');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier l'ancien mot de passe
|
||||||
|
const isPasswordValid = await bcrypt.compare(oldPassword, admin.password);
|
||||||
|
if (!isPasswordValid) {
|
||||||
|
throw new UnauthorizedException('Ancien mot de passe incorrect');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hasher le nouveau mot de passe
|
||||||
|
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
||||||
|
|
||||||
|
// Mettre à jour le mot de passe
|
||||||
|
await this.prisma.admin.update({
|
||||||
|
where: { id: adminId },
|
||||||
|
data: { password: hashedPassword },
|
||||||
|
});
|
||||||
|
|
||||||
|
return { message: 'Mot de passe modifié avec succès' };
|
||||||
|
}
|
||||||
|
}
|
||||||
17
backend/src/app.module.ts
Normal file
17
backend/src/app.module.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { PrismaModule } from './prisma/prisma.module';
|
||||||
|
import { AuthModule } from './auth/auth.module';
|
||||||
|
import { AdminModule } from './admin/admin.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
isGlobal: true,
|
||||||
|
}),
|
||||||
|
PrismaModule,
|
||||||
|
AuthModule,
|
||||||
|
AdminModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
72
backend/src/controllers/theme.controller.ts
Normal file
72
backend/src/controllers/theme.controller.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { ThemeService, ThemeData } from '../services/theme.service';
|
||||||
|
|
||||||
|
export class ThemeController {
|
||||||
|
// Créer un nouveau thème
|
||||||
|
static async createTheme(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const themeData: ThemeData = req.body;
|
||||||
|
const theme = await ThemeService.createTheme(themeData);
|
||||||
|
res.status(201).json(theme);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: 'Erreur lors de la création du thème' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupérer tous les thèmes
|
||||||
|
static async getAllThemes(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const themes = await ThemeService.getAllThemes();
|
||||||
|
res.json(themes);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: 'Erreur lors de la récupération des thèmes' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupérer le thème actif
|
||||||
|
static async getActiveTheme(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const theme = await ThemeService.getActiveTheme();
|
||||||
|
if (!theme) {
|
||||||
|
return res.status(404).json({ error: 'Aucun thème actif trouvé' });
|
||||||
|
}
|
||||||
|
res.json(theme);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: 'Erreur lors de la récupération du thème actif' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activer un thème
|
||||||
|
static async activateTheme(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { themeId } = req.params;
|
||||||
|
const theme = await ThemeService.activateTheme(themeId);
|
||||||
|
res.json(theme);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: 'Erreur lors de l\'activation du thème' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mettre à jour un thème
|
||||||
|
static async updateTheme(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { themeId } = req.params;
|
||||||
|
const themeData: Partial<ThemeData> = req.body;
|
||||||
|
const theme = await ThemeService.updateTheme(themeId, themeData);
|
||||||
|
res.json(theme);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: 'Erreur lors de la mise à jour du thème' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supprimer un thème
|
||||||
|
static async deleteTheme(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { themeId } = req.params;
|
||||||
|
await ThemeService.deleteTheme(themeId);
|
||||||
|
res.status(204).send();
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: 'Erreur lors de la suppression du thème' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
backend/src/index.ts
Normal file
28
backend/src/index.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import cors from 'cors';
|
||||||
|
import helmet from 'helmet';
|
||||||
|
import morgan from 'morgan';
|
||||||
|
import themeRoutes from './routes/theme.routes';
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const port = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
app.use(cors());
|
||||||
|
app.use(helmet());
|
||||||
|
app.use(morgan('dev'));
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Routes
|
||||||
|
app.use('/api/themes', themeRoutes);
|
||||||
|
|
||||||
|
// Gestion des erreurs
|
||||||
|
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
console.error(err.stack);
|
||||||
|
res.status(500).json({ error: 'Une erreur est survenue' });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Démarrage du serveur
|
||||||
|
app.listen(port, () => {
|
||||||
|
console.log(`Serveur démarré sur le port ${port}`);
|
||||||
|
});
|
||||||
95
backend/src/routes/auth.ts
Normal file
95
backend/src/routes/auth.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
import * as bcrypt from 'bcrypt';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
|
||||||
|
|
||||||
|
// Route de connexion
|
||||||
|
router.post('/login', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { email, password } = req.body;
|
||||||
|
|
||||||
|
// Vérifier les identifiants
|
||||||
|
const admin = await prisma.admin.findUnique({
|
||||||
|
where: { email }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!admin) {
|
||||||
|
return res.status(401).json({ error: 'Identifiants invalides' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier le mot de passe
|
||||||
|
const validPassword = await bcrypt.compare(password, admin.password);
|
||||||
|
if (!validPassword) {
|
||||||
|
return res.status(401).json({ error: 'Identifiants invalides' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier si le mot de passe doit être changé
|
||||||
|
if (!admin.passwordChanged) {
|
||||||
|
return res.status(403).json({
|
||||||
|
error: 'Changement de mot de passe requis',
|
||||||
|
requiresPasswordChange: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Générer le token JWT
|
||||||
|
const token = jwt.sign(
|
||||||
|
{
|
||||||
|
id: admin.id,
|
||||||
|
email: admin.email,
|
||||||
|
role: 'admin'
|
||||||
|
},
|
||||||
|
JWT_SECRET,
|
||||||
|
{ expiresIn: '24h' }
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({ token });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors de la connexion:', error);
|
||||||
|
res.status(500).json({ error: 'Erreur serveur' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Route de changement de mot de passe
|
||||||
|
router.post('/change-password', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { email, currentPassword, newPassword } = req.body;
|
||||||
|
|
||||||
|
// Vérifier l'administrateur
|
||||||
|
const admin = await prisma.admin.findUnique({
|
||||||
|
where: { email }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!admin) {
|
||||||
|
return res.status(404).json({ error: 'Administrateur non trouvé' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier l'ancien mot de passe
|
||||||
|
const validPassword = await bcrypt.compare(currentPassword, admin.password);
|
||||||
|
if (!validPassword) {
|
||||||
|
return res.status(401).json({ error: 'Mot de passe actuel incorrect' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hasher le nouveau mot de passe
|
||||||
|
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
||||||
|
|
||||||
|
// Mettre à jour le mot de passe
|
||||||
|
await prisma.admin.update({
|
||||||
|
where: { id: admin.id },
|
||||||
|
data: {
|
||||||
|
password: hashedPassword,
|
||||||
|
passwordChanged: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ message: 'Mot de passe changé avec succès' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors du changement de mot de passe:', error);
|
||||||
|
res.status(500).json({ error: 'Erreur serveur' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
14
backend/src/routes/theme.routes.ts
Normal file
14
backend/src/routes/theme.routes.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import { ThemeController } from '../controllers/theme.controller';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// Routes pour les thèmes
|
||||||
|
router.post('/', ThemeController.createTheme);
|
||||||
|
router.get('/', ThemeController.getAllThemes);
|
||||||
|
router.get('/active', ThemeController.getActiveTheme);
|
||||||
|
router.put('/:themeId/activate', ThemeController.activateTheme);
|
||||||
|
router.put('/:themeId', ThemeController.updateTheme);
|
||||||
|
router.delete('/:themeId', ThemeController.deleteTheme);
|
||||||
|
|
||||||
|
export default router;
|
||||||
39
backend/src/scripts/initAdmin.ts
Normal file
39
backend/src/scripts/initAdmin.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
import * as bcrypt from 'bcrypt';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
// Vérifier si l'administrateur existe déjà
|
||||||
|
const existingAdmin = await prisma.admin.findUnique({
|
||||||
|
where: { email: 'administrateur@ptitspas.fr' }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existingAdmin) {
|
||||||
|
// Hasher le mot de passe
|
||||||
|
const hashedPassword = await bcrypt.hash('password', 10);
|
||||||
|
|
||||||
|
// Créer l'administrateur
|
||||||
|
await prisma.admin.create({
|
||||||
|
data: {
|
||||||
|
email: 'administrateur@ptitspas.fr',
|
||||||
|
password: hashedPassword,
|
||||||
|
firstName: 'Administrateur',
|
||||||
|
lastName: 'P\'titsPas',
|
||||||
|
passwordChanged: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ Administrateur créé avec succès');
|
||||||
|
} else {
|
||||||
|
console.log('ℹ️ L\'administrateur existe déjà');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Erreur lors de la création de l\'administrateur:', error);
|
||||||
|
} finally {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
77
backend/src/services/theme.service.ts
Normal file
77
backend/src/services/theme.service.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
export interface ThemeData {
|
||||||
|
name: string;
|
||||||
|
primaryColor: string;
|
||||||
|
secondaryColor: string;
|
||||||
|
backgroundColor: string;
|
||||||
|
textColor: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ThemeService {
|
||||||
|
// Créer un nouveau thème
|
||||||
|
static async createTheme(data: ThemeData) {
|
||||||
|
return prisma.theme.create({
|
||||||
|
data: {
|
||||||
|
...data,
|
||||||
|
isActive: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupérer tous les thèmes
|
||||||
|
static async getAllThemes() {
|
||||||
|
return prisma.theme.findMany();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupérer le thème actif
|
||||||
|
static async getActiveTheme() {
|
||||||
|
const settings = await prisma.appSettings.findFirst({
|
||||||
|
include: {
|
||||||
|
currentTheme: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return settings?.currentTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activer un thème
|
||||||
|
static async activateTheme(themeId: string) {
|
||||||
|
// Désactiver tous les thèmes
|
||||||
|
await prisma.theme.updateMany({
|
||||||
|
where: { isActive: true },
|
||||||
|
data: { isActive: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Activer le thème sélectionné
|
||||||
|
const updatedTheme = await prisma.theme.update({
|
||||||
|
where: { id: themeId },
|
||||||
|
data: { isActive: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mettre à jour les paramètres de l'application
|
||||||
|
await prisma.appSettings.upsert({
|
||||||
|
where: { id: '1' },
|
||||||
|
update: { currentThemeId: themeId },
|
||||||
|
create: { id: '1', currentThemeId: themeId },
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mettre à jour un thème
|
||||||
|
static async updateTheme(themeId: string, data: Partial<ThemeData>) {
|
||||||
|
return prisma.theme.update({
|
||||||
|
where: { id: themeId },
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supprimer un thème
|
||||||
|
static async deleteTheme(themeId: string) {
|
||||||
|
return prisma.theme.delete({
|
||||||
|
where: { id: themeId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
28
backend/tsconfig.json
Normal file
28
backend/tsconfig.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2018",
|
||||||
|
"module": "commonjs",
|
||||||
|
"lib": ["es2018", "esnext.asynciterable"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"removeComments": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"strictFunctionTypes": true,
|
||||||
|
"noImplicitThis": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"baseUrl": "."
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules"],
|
||||||
|
"include": ["./src/**/*.ts"]
|
||||||
|
}
|
||||||
@ -1,21 +0,0 @@
|
|||||||
# 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
6
frontend/.gitignore
vendored
@ -43,9 +43,3 @@ app.*.map.json
|
|||||||
/android/app/debug
|
/android/app/debug
|
||||||
/android/app/profile
|
/android/app/profile
|
||||||
/android/app/release
|
/android/app/release
|
||||||
|
|
||||||
|
|
||||||
# Fichiers générés automatiquement par Flutter pour l'enregistrement des plugins
|
|
||||||
**/GeneratedPluginRegistrant.java
|
|
||||||
**/generated_plugin_registrant.*
|
|
||||||
**/generated_plugins.cmake
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
# Stage builder
|
|
||||||
FROM ghcr.io/cirruslabs/flutter:3.19.0 AS builder
|
|
||||||
WORKDIR /app
|
|
||||||
COPY pubspec.* ./
|
|
||||||
RUN flutter pub get
|
|
||||||
COPY . .
|
|
||||||
RUN flutter build web --release
|
|
||||||
|
|
||||||
# Stage production
|
|
||||||
FROM nginx:alpine
|
|
||||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
|
||||||
COPY --from=builder /app/build/web /usr/share/nginx/html
|
|
||||||
|
|
||||||
EXPOSE 80
|
|
||||||
|
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
package io.flutter.plugins;
|
||||||
|
|
||||||
|
import androidx.annotation.Keep;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import io.flutter.Log;
|
||||||
|
|
||||||
|
import io.flutter.embedding.engine.FlutterEngine;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generated file. Do not edit.
|
||||||
|
* This file is generated by the Flutter tool based on the
|
||||||
|
* plugins that support the Android platform.
|
||||||
|
*/
|
||||||
|
@Keep
|
||||||
|
public final class GeneratedPluginRegistrant {
|
||||||
|
private static final String TAG = "GeneratedPluginRegistrant";
|
||||||
|
public static void registerWith(@NonNull FlutterEngine flutterEngine) {
|
||||||
|
try {
|
||||||
|
flutterEngine.getPlugins().add(new io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin());
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error registering plugin flutter_plugin_android_lifecycle, io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin", e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
flutterEngine.getPlugins().add(new io.flutter.plugins.imagepicker.ImagePickerPlugin());
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error registering plugin image_picker_android, io.flutter.plugins.imagepicker.ImagePickerPlugin", e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
flutterEngine.getPlugins().add(new io.flutter.plugins.pathprovider.PathProviderPlugin());
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error registering plugin path_provider_android, io.flutter.plugins.pathprovider.PathProviderPlugin", e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
flutterEngine.getPlugins().add(new io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin());
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error registering plugin shared_preferences_android, io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin", e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
flutterEngine.getPlugins().add(new io.flutter.plugins.urllauncher.UrlLauncherPlugin());
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error registering plugin url_launcher_android, io.flutter.plugins.urllauncher.UrlLauncherPlugin", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,14 +0,0 @@
|
|||||||
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";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,138 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:p_tits_pas/models/m_dashbord/assistant_model.dart';
|
|
||||||
import 'package:p_tits_pas/models/m_dashbord/child_model.dart';
|
|
||||||
import 'package:p_tits_pas/models/m_dashbord/contract_model.dart';
|
|
||||||
import 'package:p_tits_pas/models/m_dashbord/conversation_model.dart';
|
|
||||||
import 'package:p_tits_pas/models/m_dashbord/event_model.dart';
|
|
||||||
import 'package:p_tits_pas/models/m_dashbord/notification_model.dart';
|
|
||||||
import 'package:p_tits_pas/services/dashboardService.dart';
|
|
||||||
|
|
||||||
class ParentDashboardController extends ChangeNotifier {
|
|
||||||
final DashboardService _dashboardService;
|
|
||||||
|
|
||||||
ParentDashboardController(this._dashboardService);
|
|
||||||
|
|
||||||
// État des données
|
|
||||||
List<ChildModel> _children = [];
|
|
||||||
String? _selectedChildId;
|
|
||||||
AssistantModel? _selectedAssistant;
|
|
||||||
List<EventModel> _upcomingEvents = [];
|
|
||||||
List<ContractModel> _contracts = [];
|
|
||||||
List<ConversationModel> _conversations = [];
|
|
||||||
List<NotificationModel> _notifications = [];
|
|
||||||
|
|
||||||
// État de chargement
|
|
||||||
bool _isLoading = false;
|
|
||||||
String? _error;
|
|
||||||
|
|
||||||
// Getters
|
|
||||||
List<ChildModel> get children => _children;
|
|
||||||
String? get selectedChildId => _selectedChildId;
|
|
||||||
ChildModel? get selectedChild => _children.where((c) => c.id == _selectedChildId).firstOrNull;
|
|
||||||
AssistantModel? get selectedAssistant => _selectedAssistant;
|
|
||||||
List<EventModel> get upcomingEvents => _upcomingEvents;
|
|
||||||
List<ContractModel> get contracts => _contracts;
|
|
||||||
List<ConversationModel> get conversations => _conversations;
|
|
||||||
List<NotificationModel> get notifications => _notifications;
|
|
||||||
bool get isLoading => _isLoading;
|
|
||||||
String? get error => _error;
|
|
||||||
|
|
||||||
// Initialisation du dashboard
|
|
||||||
Future<void> initDashboard() async {
|
|
||||||
_isLoading = true;
|
|
||||||
_error = null;
|
|
||||||
notifyListeners();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Future.wait([
|
|
||||||
_loadChildren(),
|
|
||||||
_loadUpcomingEvents(),
|
|
||||||
_loadContracts(),
|
|
||||||
_loadConversations(),
|
|
||||||
_loadNotifications(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Sélectionner le premier enfant par défaut
|
|
||||||
if (_children.isNotEmpty && _selectedChildId == null) {
|
|
||||||
await selectChild(_children.first.id);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
_error = 'Erreur lors du chargement du tableau de bord: $e';
|
|
||||||
} finally {
|
|
||||||
_isLoading = false;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sélection d'un enfant
|
|
||||||
Future<void> selectChild(String childId) async {
|
|
||||||
_selectedChildId = childId;
|
|
||||||
notifyListeners();
|
|
||||||
|
|
||||||
// Charger les données spécifiques à cet enfant
|
|
||||||
await _loadChildSpecificData(childId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Afficher le modal d'ajout d'enfant
|
|
||||||
void showAddChildModal() {
|
|
||||||
// Logique pour ouvrir le modal d'ajout d'enfant
|
|
||||||
// Sera implémentée dans le ticket FRONT-09
|
|
||||||
}
|
|
||||||
|
|
||||||
// Méthodes privées de chargement des données
|
|
||||||
Future<void> _loadChildren() async {
|
|
||||||
_children = await _dashboardService.getChildren();
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _loadChildSpecificData(String childId) async {
|
|
||||||
try {
|
|
||||||
// Charger l'assistante maternelle associée à cet enfant
|
|
||||||
_selectedAssistant = await _dashboardService.getAssistantForChild(childId);
|
|
||||||
|
|
||||||
// Filtrer les événements et contrats pour cet enfant
|
|
||||||
_upcomingEvents = await _dashboardService.getEventsForChild(childId);
|
|
||||||
_contracts = await _dashboardService.getContractsForChild(childId);
|
|
||||||
|
|
||||||
notifyListeners();
|
|
||||||
} catch (e) {
|
|
||||||
_error = 'Erreur lors du chargement des données pour l\'enfant: $e';
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _loadUpcomingEvents() async {
|
|
||||||
_upcomingEvents = await _dashboardService.getUpcomingEvents();
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _loadContracts() async {
|
|
||||||
_contracts = await _dashboardService.getContracts();
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _loadConversations() async {
|
|
||||||
_conversations = await _dashboardService.getConversations();
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _loadNotifications() async {
|
|
||||||
_notifications = await _dashboardService.getNotifications();
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Méthodes d'action
|
|
||||||
Future<void> markNotificationAsRead(String notificationId) async {
|
|
||||||
try {
|
|
||||||
await _dashboardService.markNotificationAsRead(notificationId);
|
|
||||||
await _loadNotifications(); // Recharger les notifications
|
|
||||||
} catch (e) {
|
|
||||||
_error = 'Erreur lors du marquage de la notification: $e';
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> refreshDashboard() async {
|
|
||||||
await initDashboard();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,7 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart'; // Import pour la localisation
|
||||||
|
// import 'package:provider/provider.dart'; // Supprimer Provider
|
||||||
import 'navigation/app_router.dart';
|
import 'navigation/app_router.dart';
|
||||||
|
// import 'theme/app_theme.dart'; // Supprimer AppTheme
|
||||||
|
// import 'theme/theme_provider.dart'; // Supprimer ThemeProvider
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
runApp(const MyApp()); // Exécution simple
|
runApp(const MyApp()); // Exécution simple
|
||||||
|
|||||||
@ -1,51 +0,0 @@
|
|||||||
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,
|
|
||||||
}
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
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,
|
|
||||||
}
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
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,
|
|
||||||
}
|
|
||||||
@ -1,66 +0,0 @@
|
|||||||
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,
|
|
||||||
}
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@ -1,14 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:p_tits_pas/models/am_user_registration_data.dart';
|
import 'package:p_tits_pas/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_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_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_step3_sceen.dart';
|
||||||
import 'package:p_tits_pas/screens/auth/am/am_register_step4_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/login_screen.dart';
|
||||||
import '../screens/auth/register_choice_screen.dart';
|
import '../screens/auth/register_choice_screen.dart';
|
||||||
import '../screens/auth/parent/parent_register_step1_screen.dart';
|
import '../screens/auth/parent/parent_register_step1_screen.dart';
|
||||||
@ -16,13 +12,12 @@ import '../screens/auth/parent/parent_register_step2_screen.dart';
|
|||||||
import '../screens/auth/parent/parent_register_step3_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_step4_screen.dart';
|
||||||
import '../screens/auth/parent/parent_register_step5_screen.dart';
|
import '../screens/auth/parent/parent_register_step5_screen.dart';
|
||||||
|
import '../screens/home/home_screen.dart';
|
||||||
import '../models/parent_user_registration_data.dart';
|
import '../models/parent_user_registration_data.dart';
|
||||||
|
|
||||||
class AppRouter {
|
class AppRouter {
|
||||||
static const String login = '/login';
|
static const String login = '/login';
|
||||||
static const String registerChoice = '/register-choice';
|
static const String registerChoice = '/register-choice';
|
||||||
static const String legal = '/legal';
|
|
||||||
static const String privacy = '/privacy';
|
|
||||||
static const String parentRegisterStep1 = '/parent-register/step1';
|
static const String parentRegisterStep1 = '/parent-register/step1';
|
||||||
static const String parentRegisterStep2 = '/parent-register/step2';
|
static const String parentRegisterStep2 = '/parent-register/step2';
|
||||||
static const String parentRegisterStep3 = '/parent-register/step3';
|
static const String parentRegisterStep3 = '/parent-register/step3';
|
||||||
@ -33,9 +28,7 @@ class AppRouter {
|
|||||||
static const String amRegisterStep2 = '/am-register/step2';
|
static const String amRegisterStep2 = '/am-register/step2';
|
||||||
static const String amRegisterStep3 = '/am-register/step3';
|
static const String amRegisterStep3 = '/am-register/step3';
|
||||||
static const String amRegisterStep4 = '/am-register/step4';
|
static const String amRegisterStep4 = '/am-register/step4';
|
||||||
static const String parentDashboard = '/parent-dashboard';
|
static const String home = '/home';
|
||||||
static const String admin_dashboard = '/admin_dashboard';
|
|
||||||
static const String findNanny = '/find-nanny';
|
|
||||||
|
|
||||||
static Route<dynamic> generateRoute(RouteSettings settings) {
|
static Route<dynamic> generateRoute(RouteSettings settings) {
|
||||||
Widget screen;
|
Widget screen;
|
||||||
@ -55,16 +48,8 @@ class AppRouter {
|
|||||||
screen = const RegisterChoiceScreen();
|
screen = const RegisterChoiceScreen();
|
||||||
slideTransition = true;
|
slideTransition = true;
|
||||||
break;
|
break;
|
||||||
case legal:
|
|
||||||
screen = const LegalPage();
|
|
||||||
slideTransition = true;
|
|
||||||
break;
|
|
||||||
case privacy:
|
|
||||||
screen = const PrivacyPage();
|
|
||||||
slideTransition = true;
|
|
||||||
break;
|
|
||||||
case parentRegisterStep1:
|
case parentRegisterStep1:
|
||||||
screen = ParentRegisterStep1Screen();
|
screen = const ParentRegisterStep1Screen();
|
||||||
slideTransition = true;
|
slideTransition = true;
|
||||||
break;
|
break;
|
||||||
case parentRegisterStep2:
|
case parentRegisterStep2:
|
||||||
@ -127,14 +112,8 @@ class AppRouter {
|
|||||||
}
|
}
|
||||||
slideTransition = true;
|
slideTransition = true;
|
||||||
break;
|
break;
|
||||||
case parentDashboard:
|
case home:
|
||||||
screen = const ParentDashboardScreen();
|
screen = const HomeScreen();
|
||||||
break;
|
|
||||||
case admin_dashboard:
|
|
||||||
screen = const AdminDashboardScreen();
|
|
||||||
break;
|
|
||||||
case findNanny:
|
|
||||||
screen = const FindNannyScreen();
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
screen = Scaffold(
|
screen = Scaffold(
|
||||||
|
|||||||
@ -1,86 +0,0 @@
|
|||||||
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"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
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'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -2,10 +2,9 @@ import 'dart:async';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
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:url_launcher/url_launcher.dart';
|
||||||
import 'package:p_tits_pas/services/bug_report_service.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/image_button.dart';
|
||||||
import '../../widgets/custom_app_text_field.dart';
|
import '../../widgets/custom_app_text_field.dart';
|
||||||
|
|
||||||
@ -20,8 +19,6 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
final _emailController = TextEditingController();
|
final _emailController = TextEditingController();
|
||||||
final _passwordController = TextEditingController();
|
final _passwordController = TextEditingController();
|
||||||
final AuthService _authService = AuthService();
|
|
||||||
bool _isLoading = false;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@ -50,89 +47,6 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
return null;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@ -140,8 +54,7 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
body: LayoutBuilder(
|
body: LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
// Version desktop (web)
|
// Version desktop (web)
|
||||||
|
if (kIsWeb) {
|
||||||
// if (kIsWeb) {
|
|
||||||
final w = constraints.maxWidth;
|
final w = constraints.maxWidth;
|
||||||
final h = constraints.maxHeight;
|
final h = constraints.maxHeight;
|
||||||
|
|
||||||
@ -154,8 +67,7 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
|
|
||||||
final imageDimensions = snapshot.data!;
|
final imageDimensions = snapshot.data!;
|
||||||
final imageHeight = h;
|
final imageHeight = h;
|
||||||
final imageWidth = imageHeight *
|
final imageWidth = imageHeight * (imageDimensions.width / imageDimensions.height);
|
||||||
(imageDimensions.width / imageDimensions.height);
|
|
||||||
final remainingWidth = w - imageWidth;
|
final remainingWidth = w - imageWidth;
|
||||||
final leftMargin = remainingWidth / 4;
|
final leftMargin = remainingWidth / 4;
|
||||||
|
|
||||||
@ -184,10 +96,10 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
Positioned(
|
Positioned(
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
width: w * 0.6, // 60% de la largeur 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
|
height: h * 0.5, // 50% de la hauteur de l'écran
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.all(w * 0.02), // 2% de padding
|
padding: EdgeInsets.all(w * 0.02), // 2% de padding
|
||||||
child: Form(
|
child: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -206,7 +118,6 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
style: CustomAppTextFieldStyle.lavande,
|
style: CustomAppTextFieldStyle.lavande,
|
||||||
fieldHeight: 53,
|
fieldHeight: 53,
|
||||||
fieldWidth: double.infinity,
|
fieldWidth: double.infinity,
|
||||||
enabled: !_isLoading,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 20),
|
const SizedBox(width: 20),
|
||||||
@ -220,7 +131,6 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
style: CustomAppTextFieldStyle.jaune,
|
style: CustomAppTextFieldStyle.jaune,
|
||||||
fieldHeight: 53,
|
fieldHeight: 53,
|
||||||
fieldWidth: double.infinity,
|
fieldWidth: double.infinity,
|
||||||
enabled: !_isLoading,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -228,21 +138,17 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
// Bouton centré
|
// Bouton centré
|
||||||
Center(
|
Center(
|
||||||
child: _isLoading
|
child: ImageButton(
|
||||||
? const SizedBox(
|
|
||||||
width: 300,
|
|
||||||
height: 40,
|
|
||||||
child: Center(
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: ImageButton(
|
|
||||||
bg: 'assets/images/btn_green.png',
|
bg: 'assets/images/btn_green.png',
|
||||||
width: 300,
|
width: 300,
|
||||||
height: 40,
|
height: 40,
|
||||||
text: 'Se connecter',
|
text: 'Se connecter',
|
||||||
textColor: const Color(0xFF2D6A4F),
|
textColor: const Color(0xFF2D6A4F),
|
||||||
onPressed: _handleLogin,
|
onPressed: () {
|
||||||
|
if (_formKey.currentState?.validate() ?? false) {
|
||||||
|
// TODO: Implémenter la logique de connexion
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
@ -267,8 +173,7 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
Center(
|
Center(
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.pushNamed(
|
Navigator.pushNamed(context, '/register-choice');
|
||||||
context, '/register-choice');
|
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
'Créer un compte',
|
'Créer un compte',
|
||||||
@ -280,8 +185,7 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(height: 20), // Réduit l'espacement en bas
|
||||||
height: 20), // Réduit l'espacement en bas
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -347,12 +251,12 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
// }
|
}
|
||||||
|
|
||||||
// Version mobile (à implémenter)
|
// Version mobile (à implémenter)
|
||||||
// return const Center(
|
return const Center(
|
||||||
// child: Text('Version mobile à implémenter'),
|
child: Text('Version mobile à implémenter'),
|
||||||
// );
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -394,7 +298,14 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
if (controller.text.trim().isEmpty) {
|
if (controller.text.trim().isEmpty) {
|
||||||
_showErrorSnackBar('Veuillez décrire le problème');
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'Veuillez décrire le problème',
|
||||||
|
style: GoogleFonts.merienda(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -402,11 +313,25 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
await BugReportService.sendReport(controller.text);
|
await BugReportService.sendReport(controller.text);
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
_showSuccessSnackBar('Rapport envoyé avec succès');
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'Rapport envoyé avec succès',
|
||||||
|
style: GoogleFonts.merienda(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
_showErrorSnackBar('Erreur lors de l\'envoi du rapport');
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'Erreur lors de l\'envoi du rapport',
|
||||||
|
style: GoogleFonts.merienda(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -471,4 +396,4 @@ class _FooterLink extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -14,4 +14,4 @@ class HomeScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,242 +0,0 @@
|
|||||||
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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
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"),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
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',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,71 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,162 +1,42 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:p_tits_pas/services/api/api_config.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:p_tits_pas/services/api/tokenService.dart';
|
|
||||||
import '../models/user.dart';
|
import '../models/user.dart';
|
||||||
import 'package:http/http.dart' as http;
|
|
||||||
|
|
||||||
|
|
||||||
class AuthService {
|
class AuthService {
|
||||||
final String baseUrl = ApiConfig.baseUrl;
|
static const String _usersKey = 'users';
|
||||||
|
static const String _parentsKey = 'parents';
|
||||||
|
static const String _childrenKey = 'children';
|
||||||
|
|
||||||
//login
|
// Méthode pour se connecter (mode démonstration)
|
||||||
Future<Map<String, dynamic>> login(String email, String password) async {
|
static Future<AppUser> login(String email, String password) async {
|
||||||
try {
|
await Future.delayed(const Duration(seconds: 1)); // Simule un délai de traitement
|
||||||
final response = await http.post(
|
throw Exception('Mode démonstration - Connexion désactivée');
|
||||||
Uri.parse('$baseUrl${ApiConfig.login}'),
|
|
||||||
headers: ApiConfig.headers,
|
|
||||||
body: jsonEncode({
|
|
||||||
'email': email,
|
|
||||||
'password': password
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
if (response.statusCode == 201) {
|
|
||||||
final data = jsonDecode(response.body);
|
|
||||||
|
|
||||||
await TokenService.saveToken(data['access_token']);
|
|
||||||
await TokenService.saveRefreshToken(data['refresh_token']);
|
|
||||||
final role = _extractRoleFromToken(data['access_token']);
|
|
||||||
await TokenService.saveRole(role);
|
|
||||||
|
|
||||||
return data;
|
|
||||||
} else {
|
|
||||||
throw Exception('Failed to login: ${response.body}');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
throw Exception('Failed to login: $e');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String _extractRoleFromToken(String token) {
|
// Méthode pour s'inscrire (mode démonstration)
|
||||||
try {
|
static Future<AppUser> register({
|
||||||
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 email,
|
||||||
required String password,
|
required String password,
|
||||||
required String firstName,
|
required String firstName,
|
||||||
required String lastName,
|
required String lastName,
|
||||||
required String role,
|
required String role,
|
||||||
}) async {
|
}) async {
|
||||||
final response = await http.post(
|
await Future.delayed(const Duration(seconds: 1)); // Simule un délai de traitement
|
||||||
Uri.parse('$baseUrl${ApiConfig.register}'),
|
throw Exception('Mode démonstration - Inscription désactivée');
|
||||||
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');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> getUserId() async {
|
// Méthode pour se déconnecter (mode démonstration)
|
||||||
final token = await TokenService.getToken();
|
static Future<void> logout() async {
|
||||||
if (token == null) return '';
|
// Ne fait rien en mode démonstration
|
||||||
|
|
||||||
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 '';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> getUserNameById() async {
|
// Méthode pour vérifier si l'utilisateur est connecté (mode démonstration)
|
||||||
final userid = await getUserId();
|
static Future<bool> isLoggedIn() async {
|
||||||
final token = await TokenService.getToken();
|
return false; // Toujours non connecté 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Méthode pour récupérer l'utilisateur connecté (mode démonstration)
|
||||||
|
static Future<AppUser?> getCurrentUser() async {
|
||||||
|
return null; // Aucun utilisateur en mode démonstration
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,9 +1,8 @@
|
|||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:p_tits_pas/config/env.dart';
|
|
||||||
|
|
||||||
class BugReportService {
|
class BugReportService {
|
||||||
static final String _apiUrl = Env.apiV1('/bug-reports');
|
static const String _apiUrl = 'https://api.supernounou.local/bug-reports';
|
||||||
|
|
||||||
static Future<void> sendReport(String description) async {
|
static Future<void> sendReport(String description) async {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -1,202 +0,0 @@
|
|||||||
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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,105 +0,0 @@
|
|||||||
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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,70 +0,0 @@
|
|||||||
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),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,132 +0,0 @@
|
|||||||
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']}')),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,278 +0,0 @@
|
|||||||
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é
|
|
||||||
// },
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ],
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
@ -1,424 +0,0 @@
|
|||||||
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(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,172 +0,0 @@
|
|||||||
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'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,75 +0,0 @@
|
|||||||
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)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,54 +0,0 @@
|
|||||||
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",
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,195 +0,0 @@
|
|||||||
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'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,209 +0,0 @@
|
|||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
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()
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
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!,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,203 +0,0 @@
|
|||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,171 +0,0 @@
|
|||||||
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'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
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 l’enfant sélectionné si besoin
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,94 +0,0 @@
|
|||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,326 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,207 +0,0 @@
|
|||||||
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}';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name ynov.ptits-pas.fr;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
root /usr/share/nginx/html;
|
|
||||||
index index.html;
|
|
||||||
try_files $uri $uri/ /index.html;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Gestion des erreurs
|
|
||||||
error_page 404 /index.html;
|
|
||||||
}
|
|
||||||
40
frontend/public/ptitspas-login/login.html
Normal file
40
frontend/public/ptitspas-login/login.html
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>P'titsPas - Connexion</title>
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Merienda:wght@400;600&display=swap" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<img src="assets/river.png" alt="" class="river" aria-hidden="true">
|
||||||
|
|
||||||
|
<main class="login-container">
|
||||||
|
<img src="assets/logo.png" alt="P'titsPas" class="logo">
|
||||||
|
<h1 class="slogan">Grandir pas à pas, sereinement</h1>
|
||||||
|
|
||||||
|
<form class="login-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">Adresse e-mail</label>
|
||||||
|
<input type="email" id="email" name="email"
|
||||||
|
placeholder="Votre adresse e-mail"
|
||||||
|
aria-label="Saisissez votre adresse e-mail"
|
||||||
|
required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Mot de passe</label>
|
||||||
|
<input type="password" id="password" name="password"
|
||||||
|
placeholder="Votre mot de passe"
|
||||||
|
aria-label="Saisissez votre mot de passe"
|
||||||
|
required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="login-button">
|
||||||
|
Se connecter
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
133
frontend/public/ptitspas-login/styles.css
Normal file
133
frontend/public/ptitspas-login/styles.css
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
/* Reset et styles de base */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Merienda', cursive;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Décor de fond */
|
||||||
|
.river {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 60vw;
|
||||||
|
opacity: 0.15;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Container principal */
|
||||||
|
.login-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 480px;
|
||||||
|
padding: 2rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Logo */
|
||||||
|
.logo {
|
||||||
|
max-width: 220px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Slogan */
|
||||||
|
.slogan {
|
||||||
|
font-family: 'Merienda', cursive;
|
||||||
|
text-align: center;
|
||||||
|
color: #3a3a3a;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Formulaire */
|
||||||
|
.login-form {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Labels */
|
||||||
|
label {
|
||||||
|
color: #3a3a3a;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Champs de saisie */
|
||||||
|
input {
|
||||||
|
height: 80px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 30px;
|
||||||
|
padding: 0 1.2rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-family: inherit;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="email"] {
|
||||||
|
background-image: url('assets/field_email.png');
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="password"] {
|
||||||
|
background-image: url('assets/field_password.png');
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bouton de connexion */
|
||||||
|
.login-button {
|
||||||
|
height: 80px;
|
||||||
|
background-image: url('assets/btn_green.png');
|
||||||
|
background-size: cover;
|
||||||
|
border: none;
|
||||||
|
border-radius: 40px;
|
||||||
|
color: #ffffff;
|
||||||
|
font-family: "Merienda", cursive;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: filter 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button:hover {
|
||||||
|
filter: brightness(1.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Media queries pour mobile */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.river {
|
||||||
|
width: 40vw;
|
||||||
|
opacity: 0.10;
|
||||||
|
clip-path: inset(0 0 20% 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
max-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slogan {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -169,10 +169,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: http
|
name: http
|
||||||
sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007
|
sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.0"
|
version: "1.3.0"
|
||||||
http_parser:
|
http_parser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -18,7 +18,7 @@ dependencies:
|
|||||||
image_picker: ^1.0.7
|
image_picker: ^1.0.7
|
||||||
js: ^0.6.7
|
js: ^0.6.7
|
||||||
url_launcher: ^6.2.4
|
url_launcher: ^6.2.4
|
||||||
http: ^1.2.2
|
http: ^1.2.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
@ -7,8 +7,8 @@
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:p_tits_pas/main.dart';
|
|
||||||
|
|
||||||
|
import 'package:petitspas/main.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
||||||
|
|||||||
17
frontend/windows/flutter/generated_plugin_registrant.cc
Normal file
17
frontend/windows/flutter/generated_plugin_registrant.cc
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
//
|
||||||
|
// 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"));
|
||||||
|
}
|
||||||
15
frontend/windows/flutter/generated_plugin_registrant.h
Normal file
15
frontend/windows/flutter/generated_plugin_registrant.h
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
//
|
||||||
|
// 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_
|
||||||
25
frontend/windows/flutter/generated_plugins.cmake
Normal file
25
frontend/windows/flutter/generated_plugins.cmake
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
#
|
||||||
|
# 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)
|
||||||
55
lib/main.dart
Normal file
55
lib/main.dart
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:p_tits_pas/screens/auth/login_screen.dart';
|
||||||
|
import 'package:p_tits_pas/screens/auth/register_choice_screen.dart';
|
||||||
|
import 'package:p_tits_pas/screens/auth/parent_register_step1_screen.dart';
|
||||||
|
import 'package:p_tits_pas/screens/auth/parent_register_step2_screen.dart';
|
||||||
|
import 'package:p_tits_pas/screens/auth/parent_register_step3_screen.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// TODO: Initialiser SharedPreferences, Provider, etc.
|
||||||
|
runApp(const MyApp());
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyApp extends StatelessWidget {
|
||||||
|
const MyApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
title: 'P\'titsPas',
|
||||||
|
theme: ThemeData(
|
||||||
|
primarySwatch: Colors.blue, // TODO: Utiliser la palette de la charte graphique
|
||||||
|
textTheme: GoogleFonts.merriweatherTextTheme(
|
||||||
|
Theme.of(context).textTheme,
|
||||||
|
),
|
||||||
|
visualDensity: VisualDensity.adaptivePlatformDensity,
|
||||||
|
),
|
||||||
|
// Gestionnaire de routes initial (simple pour l'instant)
|
||||||
|
initialRoute: '/', // Ou '/login' selon le point d'entrée désiré
|
||||||
|
routes: {
|
||||||
|
'/': (context) => const LoginScreen(), // Exemple, pourrait être RegisterChoiceScreen aussi
|
||||||
|
'/login': (context) => const LoginScreen(),
|
||||||
|
'/register-choice': (context) => const RegisterChoiceScreen(),
|
||||||
|
'/parent-register/step1': (context) => const ParentRegisterStep1Screen(),
|
||||||
|
'/parent-register/step2': (context) => const ParentRegisterStep2Screen(),
|
||||||
|
'/parent-register/step3': (context) => const ParentRegisterStep3Screen(),
|
||||||
|
// TODO: Ajouter les autres routes (step 4, etc., dashboard...)
|
||||||
|
},
|
||||||
|
// Gestion des routes inconnues
|
||||||
|
onUnknownRoute: (settings) {
|
||||||
|
return MaterialPageRoute(
|
||||||
|
builder: (context) => Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: Text(
|
||||||
|
'Route inconnue :\n${settings.name}',
|
||||||
|
style: GoogleFonts.merriweather(fontSize: 20, color: Colors.red),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
1
lib/screens/auth/login_screen.dart
Normal file
1
lib/screens/auth/login_screen.dart
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
22
lib/screens/auth/parent_register_step3_screen.dart
Normal file
22
lib/screens/auth/parent_register_step3_screen.dart
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
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,
|
||||||
|
),
|
||||||
Loading…
x
Reference in New Issue
Block a user