Compare commits
41
Commits
813fdb8449
...
stable
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d32d956b0e | ||
|
|
1fca0cf132 | ||
|
|
b16dd4b55c | ||
|
|
8682421453 | ||
|
|
31bd8c3175 | ||
|
|
c94f2cf0d5 | ||
|
|
dfe7daed14 | ||
|
|
111935e451 | ||
|
|
ae3292a7fc | ||
|
|
8e8c6d79b1 | ||
|
|
6752dc97b4 | ||
|
|
31857ec891 | ||
|
|
ca7ef862da | ||
|
|
11aa66feff | ||
|
|
358eefdab3 | ||
|
|
d23f3c9f4f | ||
|
|
1834eb8c79 | ||
|
|
0386785f81 | ||
|
|
c43f55bed6 | ||
|
|
0c48a5c06f | ||
|
|
be8b1f23ed | ||
|
|
18b270eaa3 | ||
|
|
68e4f54814 | ||
|
|
6794190916 | ||
|
|
790761d576 | ||
|
|
930097f87d | ||
|
|
18af5c9034 | ||
|
|
10bf2553e7 | ||
|
|
678f4219b5 | ||
|
|
5295e8ec72 | ||
|
|
480f4a9396 | ||
|
|
6bf0932da8 | ||
|
|
2f1740b35f | ||
|
|
fd97e68dd9 | ||
|
|
9b007fe490 | ||
|
|
7ecb99963c | ||
|
|
39814c76b1 | ||
|
|
b956f94ad2 | ||
|
|
b18d5c8a9e | ||
|
|
45bd8a9ef1 | ||
|
|
b6c70a52ac |
@@ -0,0 +1,18 @@
|
||||
# Fins de ligne : toujours LF dans le dépôt (évite les conflits Linux/Windows)
|
||||
* text=auto eol=lf
|
||||
|
||||
# Fichiers binaires : pas de conversion
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.ico binary
|
||||
*.webp binary
|
||||
*.pdf binary
|
||||
*.woff binary
|
||||
*.woff2 binary
|
||||
*.ttf binary
|
||||
*.eot binary
|
||||
|
||||
# Scripts shell : toujours LF
|
||||
*.sh text eol=lf
|
||||
@@ -21,3 +21,6 @@ JWT_EXPIRATION_TIME=7d
|
||||
|
||||
# Environnement
|
||||
NODE_ENV=development
|
||||
|
||||
# Log de chaque appel API (mode debug) — mettre à true pour tracer les requêtes front
|
||||
# LOG_API_REQUESTS=true
|
||||
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
# Test POST /auth/register/am (ticket #90)
|
||||
# Usage: ./scripts/test-register-am.sh [BASE_URL]
|
||||
# Exemple: ./scripts/test-register-am.sh https://app.ptits-pas.fr/api/v1
|
||||
# ./scripts/test-register-am.sh http://localhost:3000/api/v1
|
||||
|
||||
BASE_URL="${1:-http://localhost:3000/api/v1}"
|
||||
echo "Testing POST $BASE_URL/auth/register/am"
|
||||
echo "---"
|
||||
|
||||
curl -s -w "\n\nHTTP %{http_code}\n" -X POST "$BASE_URL/auth/register/am" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "marie.dupont.test@ptits-pas.fr",
|
||||
"prenom": "Marie",
|
||||
"nom": "DUPONT",
|
||||
"telephone": "0612345678",
|
||||
"adresse": "1 rue Test",
|
||||
"code_postal": "75001",
|
||||
"ville": "Paris",
|
||||
"consentement_photo": true,
|
||||
"nir": "123456789012345",
|
||||
"numero_agrement": "AGR-2024-001",
|
||||
"capacite_accueil": 4,
|
||||
"acceptation_cgu": true,
|
||||
"acceptation_privacy": true
|
||||
}'
|
||||
@@ -0,0 +1,69 @@
|
||||
import {
|
||||
CallHandler,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
NestInterceptor,
|
||||
} from '@nestjs/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import { tap } from 'rxjs/operators';
|
||||
import { Request } from 'express';
|
||||
|
||||
/** Clés à masquer dans les logs (corps de requête) */
|
||||
const SENSITIVE_KEYS = [
|
||||
'password',
|
||||
'smtp_password',
|
||||
'token',
|
||||
'accessToken',
|
||||
'refreshToken',
|
||||
'secret',
|
||||
];
|
||||
|
||||
function maskBody(body: unknown): unknown {
|
||||
if (body === null || body === undefined) return body;
|
||||
if (typeof body !== 'object') return body;
|
||||
const out: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(body)) {
|
||||
const lower = key.toLowerCase();
|
||||
const isSensitive = SENSITIVE_KEYS.some((s) => lower.includes(s));
|
||||
out[key] = isSensitive ? '***' : value;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class LogRequestInterceptor implements NestInterceptor {
|
||||
private readonly enabled: boolean;
|
||||
|
||||
constructor() {
|
||||
this.enabled =
|
||||
process.env.LOG_API_REQUESTS === 'true' ||
|
||||
process.env.LOG_API_REQUESTS === '1';
|
||||
}
|
||||
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
|
||||
if (!this.enabled) return next.handle();
|
||||
|
||||
const http = context.switchToHttp();
|
||||
const req = http.getRequest<Request>();
|
||||
const { method, url, body, query } = req;
|
||||
const hasBody = body && Object.keys(body).length > 0;
|
||||
|
||||
const logLine = [
|
||||
`[API] ${method} ${url}`,
|
||||
Object.keys(query || {}).length ? `query=${JSON.stringify(query)}` : '',
|
||||
hasBody ? `body=${JSON.stringify(maskBody(body))}` : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
console.log(logLine);
|
||||
|
||||
return next.handle().pipe(
|
||||
tap({
|
||||
next: () => {
|
||||
// Optionnel: log du statut en fin de requête (si besoin plus tard)
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
+6
-5
@@ -1,17 +1,18 @@
|
||||
import { NestFactory, Reflector } from '@nestjs/core';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { SwaggerModule } from '@nestjs/swagger/dist/swagger-module';
|
||||
import { DocumentBuilder } from '@nestjs/swagger';
|
||||
import { AuthGuard } from './common/guards/auth.guard';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { RolesGuard } from './common/guards/roles.guard';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { LogRequestInterceptor } from './common/interceptors/log-request.interceptor';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule,
|
||||
{ logger: ['error', 'warn', 'log', 'debug', 'verbose'] });
|
||||
|
||||
|
||||
// Log de chaque appel API si LOG_API_REQUESTS=true (mode debug)
|
||||
app.useGlobalInterceptors(new LogRequestInterceptor());
|
||||
|
||||
// Configuration CORS pour autoriser les requêtes depuis localhost (dev) et production
|
||||
app.enableCors({
|
||||
origin: true, // Autorise toutes les origines (dev) - à restreindre en prod
|
||||
|
||||
@@ -53,8 +53,7 @@ export class ConfigController {
|
||||
// @Roles('super_admin')
|
||||
async completeSetup(@Request() req: any) {
|
||||
try {
|
||||
// TODO: Récupérer l'ID utilisateur depuis le JWT
|
||||
const userId = req.user?.id || 'system';
|
||||
const userId = req.user?.id ?? null;
|
||||
|
||||
await this.configService.markSetupCompleted(userId);
|
||||
|
||||
|
||||
@@ -259,10 +259,10 @@ export class AppConfigService implements OnModuleInit {
|
||||
|
||||
/**
|
||||
* Marquer la configuration initiale comme terminée
|
||||
* @param userId ID de l'utilisateur qui termine la configuration
|
||||
* @param userId ID de l'utilisateur qui termine la configuration (null si non authentifié)
|
||||
*/
|
||||
async markSetupCompleted(userId: string): Promise<void> {
|
||||
await this.set('setup_completed', 'true', userId);
|
||||
async markSetupCompleted(userId: string | null): Promise<void> {
|
||||
await this.set('setup_completed', 'true', userId ?? undefined);
|
||||
this.logger.log('✅ Configuration initiale marquée comme terminée');
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ export class AssistantesMaternellesController {
|
||||
return this.assistantesMaternellesService.create(dto);
|
||||
}
|
||||
|
||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
|
||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Récupérer la liste des nounous' })
|
||||
@ApiResponse({ status: 200, description: 'Liste des nounous' })
|
||||
|
||||
@@ -3,8 +3,8 @@ import { LoginDto } from './dto/login.dto';
|
||||
import { AuthService } from './auth.service';
|
||||
import { Public } from 'src/common/decorators/public.decorator';
|
||||
import { RegisterDto } from './dto/register.dto';
|
||||
import { RegisterParentDto } from './dto/register-parent.dto';
|
||||
import { RegisterParentCompletDto } from './dto/register-parent-complet.dto';
|
||||
import { RegisterAMCompletDto } from './dto/register-am-complet.dto';
|
||||
import { ChangePasswordRequiredDto } from './dto/change-password.dto';
|
||||
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { AuthGuard } from 'src/common/guards/auth.guard';
|
||||
@@ -53,12 +53,16 @@ export class AuthController {
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Post('register/parent/legacy')
|
||||
@ApiOperation({ summary: '[OBSOLÈTE] Inscription Parent (étape 1/6 uniquement)' })
|
||||
@ApiResponse({ status: 201, description: 'Inscription réussie' })
|
||||
@Post('register/am')
|
||||
@ApiOperation({
|
||||
summary: 'Inscription Assistante Maternelle COMPLÈTE',
|
||||
description: 'Crée User AM + entrée assistantes_maternelles (identité + infos pro + photo + CGU) en une transaction',
|
||||
})
|
||||
@ApiResponse({ status: 201, description: 'Inscription réussie - Dossier en attente de validation' })
|
||||
@ApiResponse({ status: 400, description: 'Données invalides ou CGU non acceptées' })
|
||||
@ApiResponse({ status: 409, description: 'Email déjà utilisé' })
|
||||
async registerParentLegacy(@Body() dto: RegisterParentDto) {
|
||||
return this.authService.registerParent(dto);
|
||||
async inscrireAMComplet(@Body() dto: RegisterAMCompletDto) {
|
||||
return this.authService.inscrireAMComplet(dto);
|
||||
}
|
||||
|
||||
@Public()
|
||||
|
||||
@@ -8,11 +8,12 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { Users } from 'src/entities/users.entity';
|
||||
import { Parents } from 'src/entities/parents.entity';
|
||||
import { Children } from 'src/entities/children.entity';
|
||||
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
|
||||
import { AppConfigModule } from 'src/modules/config';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Users, Parents, Children]),
|
||||
TypeOrmModule.forFeature([Users, Parents, Children, AssistanteMaternelle]),
|
||||
forwardRef(() => UserModule),
|
||||
AppConfigModule,
|
||||
JwtModule.registerAsync({
|
||||
|
||||
@@ -13,13 +13,14 @@ import * as crypto from 'crypto';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import { RegisterDto } from './dto/register.dto';
|
||||
import { RegisterParentDto } from './dto/register-parent.dto';
|
||||
import { RegisterParentCompletDto } from './dto/register-parent-complet.dto';
|
||||
import { RegisterAMCompletDto } from './dto/register-am-complet.dto';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { RoleType, StatutUtilisateurType, Users } from 'src/entities/users.entity';
|
||||
import { Parents } from 'src/entities/parents.entity';
|
||||
import { Children, StatutEnfantType } from 'src/entities/children.entity';
|
||||
import { ParentsChildren } from 'src/entities/parents_children.entity';
|
||||
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
|
||||
import { LoginDto } from './dto/login.dto';
|
||||
import { AppConfigService } from 'src/modules/config/config.service';
|
||||
|
||||
@@ -116,7 +117,7 @@ export class AuthService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Inscription utilisateur OBSOLÈTE - Utiliser registerParent() ou registerAM()
|
||||
* Inscription utilisateur OBSOLÈTE - Utiliser inscrireParentComplet() ou registerAM()
|
||||
* @deprecated
|
||||
*/
|
||||
async register(registerDto: RegisterDto) {
|
||||
@@ -157,125 +158,6 @@ export class AuthService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Inscription Parent (étape 1/6 du workflow CDC)
|
||||
* SANS mot de passe - Token de création MDP généré
|
||||
*/
|
||||
async registerParent(dto: RegisterParentDto) {
|
||||
// 1. Vérifier que l'email n'existe pas
|
||||
const exists = await this.usersService.findByEmailOrNull(dto.email);
|
||||
if (exists) {
|
||||
throw new ConflictException('Un compte avec cet email existe déjà');
|
||||
}
|
||||
|
||||
// 2. Vérifier l'email du co-parent s'il existe
|
||||
if (dto.co_parent_email) {
|
||||
const coParentExists = await this.usersService.findByEmailOrNull(dto.co_parent_email);
|
||||
if (coParentExists) {
|
||||
throw new ConflictException('L\'email du co-parent est déjà utilisé');
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Récupérer la durée d'expiration du token depuis la config
|
||||
const tokenExpiryDays = await this.appConfigService.get<number>(
|
||||
'password_reset_token_expiry_days',
|
||||
7,
|
||||
);
|
||||
|
||||
// 4. Générer les tokens de création de mot de passe
|
||||
const tokenCreationMdp = crypto.randomUUID();
|
||||
const tokenExpiration = new Date();
|
||||
tokenExpiration.setDate(tokenExpiration.getDate() + tokenExpiryDays);
|
||||
|
||||
// 5. Transaction : Créer Parent 1 + Parent 2 (si existe) + entités parents
|
||||
const result = await this.usersRepo.manager.transaction(async (manager) => {
|
||||
// Créer Parent 1
|
||||
const parent1 = manager.create(Users, {
|
||||
email: dto.email,
|
||||
prenom: dto.prenom,
|
||||
nom: dto.nom,
|
||||
role: RoleType.PARENT,
|
||||
statut: StatutUtilisateurType.EN_ATTENTE,
|
||||
telephone: dto.telephone,
|
||||
adresse: dto.adresse,
|
||||
code_postal: dto.code_postal,
|
||||
ville: dto.ville,
|
||||
token_creation_mdp: tokenCreationMdp,
|
||||
token_creation_mdp_expire_le: tokenExpiration,
|
||||
});
|
||||
|
||||
const savedParent1 = await manager.save(Users, parent1);
|
||||
|
||||
// Créer Parent 2 si renseigné
|
||||
let savedParent2: Users | null = null;
|
||||
let tokenCoParent: string | null = null;
|
||||
|
||||
if (dto.co_parent_email && dto.co_parent_prenom && dto.co_parent_nom) {
|
||||
tokenCoParent = crypto.randomUUID();
|
||||
const tokenExpirationCoParent = new Date();
|
||||
tokenExpirationCoParent.setDate(tokenExpirationCoParent.getDate() + tokenExpiryDays);
|
||||
|
||||
const parent2 = manager.create(Users, {
|
||||
email: dto.co_parent_email,
|
||||
prenom: dto.co_parent_prenom,
|
||||
nom: dto.co_parent_nom,
|
||||
role: RoleType.PARENT,
|
||||
statut: StatutUtilisateurType.EN_ATTENTE,
|
||||
telephone: dto.co_parent_telephone,
|
||||
adresse: dto.co_parent_meme_adresse ? dto.adresse : dto.co_parent_adresse,
|
||||
code_postal: dto.co_parent_meme_adresse ? dto.code_postal : dto.co_parent_code_postal,
|
||||
ville: dto.co_parent_meme_adresse ? dto.ville : dto.co_parent_ville,
|
||||
token_creation_mdp: tokenCoParent,
|
||||
token_creation_mdp_expire_le: tokenExpirationCoParent,
|
||||
});
|
||||
|
||||
savedParent2 = await manager.save(Users, parent2);
|
||||
}
|
||||
|
||||
// Créer l'entité métier Parents pour Parent 1
|
||||
const parentEntity = manager.create(Parents, {
|
||||
user_id: savedParent1.id,
|
||||
});
|
||||
parentEntity.user = savedParent1;
|
||||
if (savedParent2) {
|
||||
parentEntity.co_parent = savedParent2;
|
||||
}
|
||||
|
||||
await manager.save(Parents, parentEntity);
|
||||
|
||||
// Créer l'entité métier Parents pour Parent 2 (si existe)
|
||||
if (savedParent2) {
|
||||
const coParentEntity = manager.create(Parents, {
|
||||
user_id: savedParent2.id,
|
||||
});
|
||||
coParentEntity.user = savedParent2;
|
||||
coParentEntity.co_parent = savedParent1;
|
||||
|
||||
await manager.save(Parents, coParentEntity);
|
||||
}
|
||||
|
||||
return {
|
||||
parent1: savedParent1,
|
||||
parent2: savedParent2,
|
||||
tokenCreationMdp,
|
||||
tokenCoParent,
|
||||
};
|
||||
});
|
||||
|
||||
// 6. TODO: Envoyer email avec lien de création de MDP
|
||||
// await this.mailService.sendPasswordCreationEmail(result.parent1, result.tokenCreationMdp);
|
||||
// if (result.parent2 && result.tokenCoParent) {
|
||||
// await this.mailService.sendPasswordCreationEmail(result.parent2, result.tokenCoParent);
|
||||
// }
|
||||
|
||||
return {
|
||||
message: 'Inscription réussie. Un email de validation vous a été envoyé.',
|
||||
parent_id: result.parent1.id,
|
||||
co_parent_id: result.parent2?.id,
|
||||
statut: StatutUtilisateurType.EN_ATTENTE,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Inscription Parent COMPLÈTE - Workflow CDC 6 étapes en 1 transaction
|
||||
* Gère : Parent 1 + Parent 2 (opt) + Enfants + Présentation + CGU
|
||||
@@ -432,6 +314,82 @@ export class AuthService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Inscription Assistante Maternelle COMPLÈTE - Un seul endpoint (identité + pro + photo + CGU)
|
||||
* Crée User (role AM) + entrée assistantes_maternelles, token création MDP
|
||||
*/
|
||||
async inscrireAMComplet(dto: RegisterAMCompletDto) {
|
||||
if (!dto.acceptation_cgu || !dto.acceptation_privacy) {
|
||||
throw new BadRequestException(
|
||||
"L'acceptation des CGU et de la politique de confidentialité est obligatoire",
|
||||
);
|
||||
}
|
||||
|
||||
const existe = await this.usersService.findByEmailOrNull(dto.email);
|
||||
if (existe) {
|
||||
throw new ConflictException('Un compte avec cet email existe déjà');
|
||||
}
|
||||
|
||||
const joursExpirationToken = await this.appConfigService.get<number>(
|
||||
'password_reset_token_expiry_days',
|
||||
7,
|
||||
);
|
||||
const tokenCreationMdp = crypto.randomUUID();
|
||||
const dateExpiration = new Date();
|
||||
dateExpiration.setDate(dateExpiration.getDate() + joursExpirationToken);
|
||||
|
||||
let urlPhoto: string | null = null;
|
||||
if (dto.photo_base64 && dto.photo_filename) {
|
||||
urlPhoto = await this.sauvegarderPhotoDepuisBase64(dto.photo_base64, dto.photo_filename);
|
||||
}
|
||||
|
||||
const dateConsentementPhoto =
|
||||
dto.consentement_photo ? new Date() : undefined;
|
||||
|
||||
const resultat = await this.usersRepo.manager.transaction(async (manager) => {
|
||||
const user = manager.create(Users, {
|
||||
email: dto.email,
|
||||
prenom: dto.prenom,
|
||||
nom: dto.nom,
|
||||
role: RoleType.ASSISTANTE_MATERNELLE,
|
||||
statut: StatutUtilisateurType.EN_ATTENTE,
|
||||
telephone: dto.telephone,
|
||||
adresse: dto.adresse,
|
||||
code_postal: dto.code_postal,
|
||||
ville: dto.ville,
|
||||
token_creation_mdp: tokenCreationMdp,
|
||||
token_creation_mdp_expire_le: dateExpiration,
|
||||
photo_url: urlPhoto ?? undefined,
|
||||
consentement_photo: dto.consentement_photo,
|
||||
date_consentement_photo: dateConsentementPhoto,
|
||||
date_naissance: dto.date_naissance ? new Date(dto.date_naissance) : undefined,
|
||||
});
|
||||
const userEnregistre = await manager.save(Users, user);
|
||||
|
||||
const amRepo = manager.getRepository(AssistanteMaternelle);
|
||||
const am = amRepo.create({
|
||||
user_id: userEnregistre.id,
|
||||
approval_number: dto.numero_agrement,
|
||||
nir: dto.nir,
|
||||
max_children: dto.capacite_accueil,
|
||||
biography: dto.biographie,
|
||||
residence_city: dto.ville ?? undefined,
|
||||
agreement_date: dto.date_agrement ? new Date(dto.date_agrement) : undefined,
|
||||
available: true,
|
||||
});
|
||||
await amRepo.save(am);
|
||||
|
||||
return { user: userEnregistre };
|
||||
});
|
||||
|
||||
return {
|
||||
message:
|
||||
'Inscription réussie. Votre dossier est en attente de validation par un gestionnaire.',
|
||||
user_id: resultat.user.id,
|
||||
statut: StatutUtilisateurType.EN_ATTENTE,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sauvegarde une photo depuis base64 vers le système de fichiers
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import {
|
||||
IsEmail,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsBoolean,
|
||||
IsInt,
|
||||
Min,
|
||||
Max,
|
||||
MinLength,
|
||||
MaxLength,
|
||||
Matches,
|
||||
IsDateString,
|
||||
} from 'class-validator';
|
||||
|
||||
export class RegisterAMCompletDto {
|
||||
// ============================================
|
||||
// ÉTAPE 1 : IDENTITÉ (Obligatoire)
|
||||
// ============================================
|
||||
|
||||
@ApiProperty({ example: 'marie.dupont@ptits-pas.fr' })
|
||||
@IsEmail({}, { message: 'Email invalide' })
|
||||
@IsNotEmpty({ message: "L'email est requis" })
|
||||
email: string;
|
||||
|
||||
@ApiProperty({ example: 'Marie' })
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: 'Le prénom est requis' })
|
||||
@MinLength(2, { message: 'Le prénom doit contenir au moins 2 caractères' })
|
||||
@MaxLength(100)
|
||||
prenom: string;
|
||||
|
||||
@ApiProperty({ example: 'DUPONT' })
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: 'Le nom est requis' })
|
||||
@MinLength(2, { message: 'Le nom doit contenir au moins 2 caractères' })
|
||||
@MaxLength(100)
|
||||
nom: string;
|
||||
|
||||
@ApiProperty({ example: '0689567890' })
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: 'Le téléphone est requis' })
|
||||
@Matches(/^(\+33|0)[1-9](\d{2}){4}$/, {
|
||||
message: 'Le numéro de téléphone doit être valide (ex: 0689567890 ou +33689567890)',
|
||||
})
|
||||
telephone: string;
|
||||
|
||||
@ApiProperty({ example: '5 Avenue du Général de Gaulle', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
adresse?: string;
|
||||
|
||||
@ApiProperty({ example: '95870', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(10)
|
||||
code_postal?: string;
|
||||
|
||||
@ApiProperty({ example: 'Bezons', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(150)
|
||||
ville?: string;
|
||||
|
||||
// ============================================
|
||||
// ÉTAPE 2 : PHOTO + INFOS PRO
|
||||
// ============================================
|
||||
|
||||
@ApiProperty({
|
||||
example: 'data:image/jpeg;base64,/9j/4AAQ...',
|
||||
required: false,
|
||||
description: 'Photo de profil en base64',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
photo_base64?: string;
|
||||
|
||||
@ApiProperty({ example: 'photo_profil.jpg', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
photo_filename?: string;
|
||||
|
||||
@ApiProperty({ example: true, description: 'Consentement utilisation photo' })
|
||||
@IsBoolean()
|
||||
@IsNotEmpty({ message: 'Le consentement photo est requis' })
|
||||
consentement_photo: boolean;
|
||||
|
||||
@ApiProperty({ example: '2024-01-15', required: false, description: 'Date de naissance' })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
date_naissance?: string;
|
||||
|
||||
@ApiProperty({ example: 'Paris', required: false, description: 'Ville de naissance' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
lieu_naissance_ville?: string;
|
||||
|
||||
@ApiProperty({ example: 'France', required: false, description: 'Pays de naissance' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
lieu_naissance_pays?: string;
|
||||
|
||||
@ApiProperty({ example: '123456789012345', description: 'NIR 15 chiffres' })
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: 'Le NIR est requis' })
|
||||
@Matches(/^\d{15}$/, { message: 'Le NIR doit contenir exactement 15 chiffres' })
|
||||
nir: string;
|
||||
|
||||
@ApiProperty({ example: 'AGR-2024-12345', description: "Numéro d'agrément" })
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: "Le numéro d'agrément est requis" })
|
||||
@MaxLength(50)
|
||||
numero_agrement: string;
|
||||
|
||||
@ApiProperty({ example: '2024-06-01', required: false, description: "Date d'obtention de l'agrément" })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
date_agrement?: string;
|
||||
|
||||
@ApiProperty({ example: 4, description: 'Capacité d\'accueil (nombre d\'enfants)', minimum: 1, maximum: 10 })
|
||||
@IsInt()
|
||||
@Min(1, { message: 'La capacité doit être au moins 1' })
|
||||
@Max(10, { message: 'La capacité ne peut pas dépasser 10' })
|
||||
capacite_accueil: number;
|
||||
|
||||
// ============================================
|
||||
// ÉTAPE 3 : PRÉSENTATION (Optionnel)
|
||||
// ============================================
|
||||
|
||||
@ApiProperty({
|
||||
example: 'Assistante maternelle expérimentée, accueil bienveillant...',
|
||||
required: false,
|
||||
description: 'Présentation / biographie (max 2000 caractères)',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(2000, { message: 'La présentation ne peut pas dépasser 2000 caractères' })
|
||||
biographie?: string;
|
||||
|
||||
// ============================================
|
||||
// ÉTAPE 4 : ACCEPTATION CGU (Obligatoire)
|
||||
// ============================================
|
||||
|
||||
@ApiProperty({ example: true, description: "Acceptation des CGU" })
|
||||
@IsBoolean()
|
||||
@IsNotEmpty({ message: "L'acceptation des CGU est requise" })
|
||||
acceptation_cgu: boolean;
|
||||
|
||||
@ApiProperty({ example: true, description: 'Acceptation de la Politique de confidentialité' })
|
||||
@IsBoolean()
|
||||
@IsNotEmpty({ message: "L'acceptation de la politique de confidentialité est requise" })
|
||||
acceptation_privacy: boolean;
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import {
|
||||
IsEmail,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsDateString,
|
||||
IsEnum,
|
||||
MinLength,
|
||||
MaxLength,
|
||||
Matches,
|
||||
} from 'class-validator';
|
||||
import { SituationFamilialeType } from 'src/entities/users.entity';
|
||||
|
||||
export class RegisterParentDto {
|
||||
// === Informations obligatoires ===
|
||||
@ApiProperty({ example: 'claire.martin@ptits-pas.fr' })
|
||||
@IsEmail({}, { message: 'Email invalide' })
|
||||
@IsNotEmpty({ message: 'L\'email est requis' })
|
||||
email: string;
|
||||
|
||||
@ApiProperty({ example: 'Claire' })
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: 'Le prénom est requis' })
|
||||
@MinLength(2, { message: 'Le prénom doit contenir au moins 2 caractères' })
|
||||
@MaxLength(100, { message: 'Le prénom ne peut pas dépasser 100 caractères' })
|
||||
prenom: string;
|
||||
|
||||
@ApiProperty({ example: 'MARTIN' })
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: 'Le nom est requis' })
|
||||
@MinLength(2, { message: 'Le nom doit contenir au moins 2 caractères' })
|
||||
@MaxLength(100, { message: 'Le nom ne peut pas dépasser 100 caractères' })
|
||||
nom: string;
|
||||
|
||||
@ApiProperty({ example: '0689567890' })
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: 'Le téléphone est requis' })
|
||||
@Matches(/^(\+33|0)[1-9](\d{2}){4}$/, {
|
||||
message: 'Le numéro de téléphone doit être valide (ex: 0689567890 ou +33689567890)',
|
||||
})
|
||||
telephone: string;
|
||||
|
||||
// === Informations optionnelles ===
|
||||
@ApiProperty({ example: '5 Avenue du Général de Gaulle', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
adresse?: string;
|
||||
|
||||
@ApiProperty({ example: '95870', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(10)
|
||||
code_postal?: string;
|
||||
|
||||
@ApiProperty({ example: 'Bezons', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(150)
|
||||
ville?: string;
|
||||
|
||||
// === Informations co-parent (optionnel) ===
|
||||
@ApiProperty({ example: 'thomas.martin@ptits-pas.fr', required: false })
|
||||
@IsOptional()
|
||||
@IsEmail({}, { message: 'Email du co-parent invalide' })
|
||||
co_parent_email?: string;
|
||||
|
||||
@ApiProperty({ example: 'Thomas', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
co_parent_prenom?: string;
|
||||
|
||||
@ApiProperty({ example: 'MARTIN', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
co_parent_nom?: string;
|
||||
|
||||
@ApiProperty({ example: '0612345678', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Matches(/^(\+33|0)[1-9](\d{2}){4}$/, {
|
||||
message: 'Le numéro de téléphone du co-parent doit être valide',
|
||||
})
|
||||
co_parent_telephone?: string;
|
||||
|
||||
@ApiProperty({ example: 'true', description: 'Le co-parent habite à la même adresse', required: false })
|
||||
@IsOptional()
|
||||
co_parent_meme_adresse?: boolean;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
co_parent_adresse?: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
co_parent_code_postal?: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
co_parent_ville?: string;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import { UpdateParentsDto } from '../user/dto/update_parent.dto';
|
||||
export class ParentsController {
|
||||
constructor(private readonly parentsService: ParentsService) {}
|
||||
|
||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
|
||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
|
||||
@Get()
|
||||
@ApiResponse({ status: 200, type: [Parents], description: 'Liste des parents' })
|
||||
@ApiResponse({ status: 403, description: 'Accès refusé !' })
|
||||
|
||||
@@ -35,7 +35,7 @@ export class GestionnairesController {
|
||||
return this.gestionnairesService.create(dto);
|
||||
}
|
||||
|
||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
|
||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
|
||||
@ApiOperation({ summary: 'Liste des gestionnaires' })
|
||||
@ApiResponse({ status: 200, description: 'Liste des gestionnaires : ', type: [Users] })
|
||||
@Get()
|
||||
|
||||
@@ -3,9 +3,13 @@ import { GestionnairesService } from './gestionnaires.service';
|
||||
import { GestionnairesController } from './gestionnaires.controller';
|
||||
import { Users } from 'src/entities/users.entity';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AuthModule } from 'src/routes/auth/auth.module';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Users])],
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Users]),
|
||||
AuthModule,
|
||||
],
|
||||
controllers: [GestionnairesController],
|
||||
providers: [GestionnairesService],
|
||||
})
|
||||
|
||||
@@ -28,7 +28,7 @@ export class UserController {
|
||||
|
||||
// Lister tous les utilisateurs (super_admin uniquement)
|
||||
@Get()
|
||||
@Roles(RoleType.SUPER_ADMIN)
|
||||
@Roles(RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR)
|
||||
@ApiOperation({ summary: 'Lister tous les utilisateurs' })
|
||||
findAll() {
|
||||
return this.userService.findAll();
|
||||
|
||||
@@ -9,6 +9,7 @@ import { ParentsModule } from '../parents/parents.module';
|
||||
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
|
||||
import { AssistantesMaternellesModule } from '../assistantes_maternelles/assistantes_maternelles.module';
|
||||
import { Parents } from 'src/entities/parents.entity';
|
||||
import { GestionnairesModule } from './gestionnaires/gestionnaires.module';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature(
|
||||
@@ -20,6 +21,7 @@ import { Parents } from 'src/entities/parents.entity';
|
||||
]), forwardRef(() => AuthModule),
|
||||
ParentsModule,
|
||||
AssistantesMaternellesModule,
|
||||
GestionnairesModule,
|
||||
],
|
||||
controllers: [UserController],
|
||||
providers: [UserService],
|
||||
|
||||
+6
-3
@@ -80,12 +80,15 @@ CREATE INDEX idx_utilisateurs_token_creation_mdp
|
||||
CREATE TABLE assistantes_maternelles (
|
||||
id_utilisateur UUID PRIMARY KEY REFERENCES utilisateurs(id) ON DELETE CASCADE,
|
||||
numero_agrement VARCHAR(50),
|
||||
date_agrement DATE NOT NULL, -- Obligatoire selon CDC v1.3
|
||||
nir_chiffre CHAR(15),
|
||||
nb_max_enfants INT,
|
||||
place_disponible INT,
|
||||
biographie TEXT,
|
||||
disponible BOOLEAN DEFAULT true
|
||||
disponible BOOLEAN DEFAULT true,
|
||||
ville_residence VARCHAR(100),
|
||||
date_agrement DATE,
|
||||
annee_experience SMALLINT,
|
||||
specialite VARCHAR(100),
|
||||
place_disponible INT
|
||||
);
|
||||
|
||||
-- ==========================================================
|
||||
|
||||
@@ -41,6 +41,16 @@ docker compose -f docker-compose.dev.yml down -v
|
||||
---
|
||||
|
||||
|
||||
## Réinitialiser la BDD et charger les données de test (dashboard admin)
|
||||
|
||||
Depuis la **racine du projet** (ptitspas-app, où se trouve `docker-compose.yml`) :
|
||||
|
||||
```bash
|
||||
./scripts/reset-and-seed-db.sh
|
||||
```
|
||||
|
||||
Ce script : arrête les conteneurs, supprime le volume Postgres, redémarre la base (le schéma est recréé via `BDD.sql`), puis exécute `database/seed/03_seed_test_data.sql`. Tu obtiens un super_admin (`admin@ptits-pas.fr`) plus 9 comptes de test (1 admin, 1 gestionnaire, 2 AM, 5 parents) avec **mot de passe : `password`**. Idéal pour développer le ticket #92 (dashboard admin).
|
||||
|
||||
## Importation automatique des données de test
|
||||
|
||||
Les données de test (CSV) sont automatiquement importées dans la base au démarrage du conteneur Docker grâce aux scripts présents dans le dossier `migrations/`.
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
-- ============================================================
|
||||
-- 03_seed_test_data.sql : Données de test complètes (dashboard admin)
|
||||
-- Aligné sur utilisateurs-test-complet.json
|
||||
-- Mot de passe universel : password (bcrypt)
|
||||
-- À exécuter après BDD.sql (init DB)
|
||||
-- ============================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- Hash bcrypt pour "password" (10 rounds)
|
||||
|
||||
-- ========== UTILISATEURS (1 admin + 1 gestionnaire + 2 AM + 5 parents) ==========
|
||||
-- On garde admin@ptits-pas.fr (super_admin) déjà créé par BDD.sql
|
||||
|
||||
INSERT INTO utilisateurs (id, email, password, prenom, nom, role, statut, telephone, adresse, ville, code_postal, profession, situation_familiale, date_naissance, consentement_photo)
|
||||
VALUES
|
||||
('a0000001-0001-0001-0001-000000000001', 'sophie.bernard@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'Sophie', 'BERNARD', 'administrateur', 'actif', '0678123456', '12 Avenue Gabriel Péri', 'Bezons', '95870', 'Responsable administrative', 'marie', '1978-03-15', false),
|
||||
('a0000002-0002-0002-0002-000000000002', 'lucas.moreau@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'Lucas', 'MOREAU', 'gestionnaire', 'actif', '0687234567', '8 Rue Jean Jaurès', 'Bezons', '95870', 'Gestionnaire des placements', 'celibataire', '1985-09-22', false),
|
||||
('a0000003-0003-0003-0003-000000000003', 'marie.dubois@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'Marie', 'DUBOIS', 'assistante_maternelle', 'actif', '0696345678', '25 Rue de la République', 'Bezons', '95870', 'Assistante maternelle', 'marie', '1980-06-08', true),
|
||||
('a0000004-0004-0004-0004-000000000004', 'fatima.elmansouri@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'Fatima', 'EL MANSOURI', 'assistante_maternelle', 'actif', '0675456789', '17 Boulevard Aristide Briand', 'Bezons', '95870', 'Assistante maternelle', 'marie', '1975-11-12', true),
|
||||
('a0000005-0005-0005-0005-000000000005', 'claire.martin@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'Claire', 'MARTIN', 'parent', 'actif', '0689567890', '5 Avenue du Général de Gaulle', 'Bezons', '95870', 'Infirmière', 'marie', '1990-04-03', false),
|
||||
('a0000006-0006-0006-0006-000000000006', 'thomas.martin@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'Thomas', 'MARTIN', 'parent', 'actif', '0678456789', '5 Avenue du Général de Gaulle', 'Bezons', '95870', 'Ingénieur', 'marie', '1988-07-18', false),
|
||||
('a0000007-0007-0007-0007-000000000007', 'amelie.durand@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'Amélie', 'DURAND', 'parent', 'actif', '0667788990', '23 Rue Victor Hugo', 'Bezons', '95870', 'Comptable', 'divorce', '1987-12-14', false),
|
||||
('a0000008-0008-0008-0008-000000000008', 'julien.rousseau@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'Julien', 'ROUSSEAU', 'parent', 'actif', '0656677889', '14 Rue Pasteur', 'Bezons', '95870', 'Commercial', 'divorce', '1985-08-29', false),
|
||||
('a0000009-0009-0009-0009-000000000009', 'david.lecomte@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'David', 'LECOMTE', 'parent', 'actif', '0645566778', '31 Rue Émile Zola', 'Bezons', '95870', 'Développeur web', 'parent_isole', '1992-10-07', false)
|
||||
ON CONFLICT (email) DO NOTHING;
|
||||
|
||||
-- ========== PARENTS (avec co-parent pour le couple Martin) ==========
|
||||
INSERT INTO parents (id_utilisateur, id_co_parent)
|
||||
VALUES
|
||||
('a0000005-0005-0005-0005-000000000005', 'a0000006-0006-0006-0006-000000000006'),
|
||||
('a0000006-0006-0006-0006-000000000006', 'a0000005-0005-0005-0005-000000000005'),
|
||||
('a0000007-0007-0007-0007-000000000007', NULL),
|
||||
('a0000008-0008-0008-0008-000000000008', NULL),
|
||||
('a0000009-0009-0009-0009-000000000009', NULL)
|
||||
ON CONFLICT (id_utilisateur) DO NOTHING;
|
||||
|
||||
-- ========== ASSISTANTES MATERNELLES ==========
|
||||
INSERT INTO assistantes_maternelles (id_utilisateur, numero_agrement, nir_chiffre, nb_max_enfants, biographie, date_agrement, ville_residence, disponible, place_disponible)
|
||||
VALUES
|
||||
('a0000003-0003-0003-0003-000000000003', 'AGR-2019-095001', '280069512345671', 4, 'Assistante maternelle agréée depuis 2019. Spécialité bébés 0-18 mois. Accueil bienveillant et cadre sécurisant. 2 places disponibles.', '2019-09-01', 'Bezons', true, 2),
|
||||
('a0000004-0004-0004-0004-000000000004', 'AGR-2017-095002', '275119512345672', 3, 'Assistante maternelle expérimentée. Spécialité 1-3 ans. Accueil à la journée. 1 place disponible.', '2017-06-15', 'Bezons', true, 1)
|
||||
ON CONFLICT (id_utilisateur) DO NOTHING;
|
||||
|
||||
-- ========== ENFANTS ==========
|
||||
INSERT INTO enfants (id, prenom, nom, genre, date_naissance, statut, est_multiple)
|
||||
VALUES
|
||||
('e0000001-0001-0001-0001-000000000001', 'Emma', 'MARTIN', 'F', '2023-02-15', 'actif', true),
|
||||
('e0000002-0002-0002-0002-000000000002', 'Noah', 'MARTIN', 'H', '2023-02-15', 'actif', true),
|
||||
('e0000003-0003-0003-0003-000000000003', 'Léa', 'MARTIN', 'F', '2023-02-15', 'actif', true),
|
||||
('e0000004-0004-0004-0004-000000000004', 'Chloé', 'ROUSSEAU', 'F', '2022-04-20', 'actif', false),
|
||||
('e0000005-0005-0005-0005-000000000005', 'Hugo', 'ROUSSEAU', 'H', '2024-03-10', 'actif', false),
|
||||
('e0000006-0006-0006-0006-000000000006', 'Maxime', 'LECOMTE', 'H', '2023-04-15', 'actif', false)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ========== ENFANTS_PARENTS (liaison N:N) ==========
|
||||
-- Martin (Claire + Thomas) -> Emma, Noah, Léa
|
||||
INSERT INTO enfants_parents (id_parent, id_enfant)
|
||||
VALUES
|
||||
('a0000005-0005-0005-0005-000000000005', 'e0000001-0001-0001-0001-000000000001'),
|
||||
('a0000005-0005-0005-0005-000000000005', 'e0000002-0002-0002-0002-000000000002'),
|
||||
('a0000005-0005-0005-0005-000000000005', 'e0000003-0003-0003-0003-000000000003'),
|
||||
('a0000006-0006-0006-0006-000000000006', 'e0000001-0001-0001-0001-000000000001'),
|
||||
('a0000006-0006-0006-0006-000000000006', 'e0000002-0002-0002-0002-000000000002'),
|
||||
('a0000006-0006-0006-0006-000000000006', 'e0000003-0003-0003-0003-000000000003'),
|
||||
('a0000007-0007-0007-0007-000000000007', 'e0000004-0004-0004-0004-000000000004'),
|
||||
('a0000007-0007-0007-0007-000000000007', 'e0000005-0005-0005-0005-000000000005'),
|
||||
('a0000008-0008-0008-0008-000000000008', 'e0000004-0004-0004-0004-000000000004'),
|
||||
('a0000008-0008-0008-0008-000000000008', 'e0000005-0005-0005-0005-000000000005'),
|
||||
('a0000009-0009-0009-0009-000000000009', 'e0000006-0006-0006-0006-000000000006')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
COMMIT;
|
||||
@@ -55,6 +55,8 @@ services:
|
||||
JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET}
|
||||
JWT_REFRESH_EXPIRES: ${JWT_REFRESH_EXPIRES}
|
||||
NODE_ENV: ${NODE_ENV}
|
||||
LOG_API_REQUESTS: ${LOG_API_REQUESTS:-false}
|
||||
CONFIG_ENCRYPTION_KEY: ${CONFIG_ENCRYPTION_KEY}
|
||||
depends_on:
|
||||
- database
|
||||
labels:
|
||||
|
||||
@@ -27,6 +27,7 @@ Ce fichier sert d'index pour naviguer dans toute la documentation du projet.
|
||||
- [**23 - Liste des Tickets**](./23_LISTE-TICKETS.md) - 61 tickets Phase 1 détaillés
|
||||
- [**24 - Décisions Projet**](./24_DECISIONS-PROJET.md) - Décisions architecturales et fonctionnelles
|
||||
- [**25 - Backlog Phase 2**](./25_PHASE-2-BACKLOG.md) - Fonctionnalités techniques reportées
|
||||
- [**26 - API Gitea**](./26_GITEA-API.md) - Procédure d'utilisation de l'API Gitea (issues, PR, branches, labels)
|
||||
|
||||
### Administration (À créer)
|
||||
- [**30 - Guide d'administration**](./30_ADMIN.md) - Gestion des utilisateurs, accès PgAdmin, logs
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# Ticket #14 – Note pour modifications backend
|
||||
|
||||
**Contexte :** Première connexion admin → panneau Paramètres, déblocage après clic sur « Sauvegarder ». Le front appelle `POST /api/v1/configuration/setup/complete` au clic sur Sauvegarder.
|
||||
|
||||
## Problème
|
||||
|
||||
Erreur renvoyée par le back :
|
||||
`invalid input syntax for type uuid: "system"`
|
||||
|
||||
- Le controller fait `const userId = req.user?.id || 'system'` puis `markSetupCompleted(userId)`.
|
||||
- Le service `set()` fait `config.modifiePar = { id: userId }` ; la colonne `modifie_par` est une FK UUID vers `users`.
|
||||
- La chaîne `"system"` n’est pas un UUID valide → erreur PostgreSQL.
|
||||
|
||||
## Modifications à apporter au backend
|
||||
|
||||
**Option A – Accepter l’absence d’utilisateur (recommandé si la route peut être appelée sans JWT)**
|
||||
|
||||
1. **`config.controller.ts`** (route `completeSetup`)
|
||||
- Remplacer :
|
||||
`const userId = req.user?.id || 'system';`
|
||||
- Par :
|
||||
`const userId = req.user?.id ?? null;`
|
||||
|
||||
2. **`config.service.ts`** (`markSetupCompleted`)
|
||||
- Changer la signature :
|
||||
`async markSetupCompleted(userId: string | null): Promise<void>`
|
||||
- Et appeler :
|
||||
`await this.set('setup_completed', 'true', userId ?? undefined);`
|
||||
- Dans `set()`, ne pas remplir `modifiePar` quand `userId` est absent (déjà le cas si `if (userId)`).
|
||||
|
||||
**Option B – Imposer un utilisateur authentifié**
|
||||
|
||||
- Activer le guard JWT (et éventuellement RolesGuard) sur `POST /configuration/setup/complete` pour que `req.user` soit toujours défini, et garder `userId = req.user.id` (plus de fallback `'system'`).
|
||||
|
||||
---
|
||||
|
||||
Une fois le back modifié, le flux « Sauvegarder » → déblocage des panneaux fonctionne sans erreur.
|
||||
@@ -1,7 +1,7 @@
|
||||
# 🔧 Documentation Technique - Configuration Système On-Premise
|
||||
|
||||
**Version** : 1.0
|
||||
**Date** : 25 Novembre 2025
|
||||
**Version** : 1.1
|
||||
**Date** : 9 Février 2026
|
||||
**Auteur** : Équipe PtitsPas
|
||||
**Référence** : Architecture On-Premise
|
||||
|
||||
@@ -78,7 +78,7 @@ L'application P'titsPas est déployée **on-premise** chez différentes collecti
|
||||
2. **ConfigService** : Cache en mémoire + chiffrement
|
||||
3. **ConfigAPI** : Endpoints REST pour CRUD
|
||||
4. **Guard Setup** : Redirection forcée si config incomplète
|
||||
5. **Interface Admin** : Formulaire de configuration
|
||||
5. **Interface Admin** : Panneau Paramètres (3 sections) dans le dashboard, première config + accès permanent
|
||||
|
||||
---
|
||||
|
||||
@@ -304,93 +304,69 @@ export class ConfigService {
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant SA as Super Admin
|
||||
participant App as Application
|
||||
participant Guard as SetupGuard
|
||||
participant Op as Opérateur
|
||||
participant App as Application (Dashboard)
|
||||
participant API as ConfigAPI
|
||||
participant DB as PostgreSQL
|
||||
participant SMTP as Serveur SMTP
|
||||
|
||||
SA->>App: Première connexion
|
||||
App->>Guard: Vérifier setup_completed
|
||||
Guard->>DB: SELECT valeur FROM configuration<br/>WHERE cle='setup_completed'
|
||||
DB-->>Guard: 'false'
|
||||
Op->>App: Première connexion (admin)
|
||||
App->>API: GET /configuration/setup/status
|
||||
API->>DB: setup_completed ?
|
||||
DB-->>API: false
|
||||
API-->>App: setupCompleted: false
|
||||
|
||||
Guard-->>App: Redirection forcée vers<br/>/admin/setup
|
||||
App->>App: Affiche panneau Configuration<br/>et bloque les autres onglets
|
||||
|
||||
SA->>SA: Remplit formulaire config<br/>(SMTP, app, sécurité)
|
||||
Op->>Op: Remplit les 3 sections<br/>(Email, Personnalisation, Avancé)
|
||||
|
||||
SA->>App: Clic "Tester la connexion SMTP"
|
||||
Op->>App: Clic "Tester la connexion SMTP"
|
||||
App->>API: POST /api/v1/configuration/test-smtp
|
||||
API->>SMTP: Test connexion
|
||||
|
||||
alt Test SMTP OK
|
||||
SMTP-->>API: ✅ Connexion réussie
|
||||
API->>SA: Envoi email de test
|
||||
API->>Op: Envoi email de test
|
||||
API-->>App: ✅ Test réussi
|
||||
App-->>SA: Message: "Email de test envoyé"
|
||||
App-->>Op: Message: "Email de test envoyé"
|
||||
else Test SMTP KO
|
||||
SMTP-->>API: ❌ Erreur connexion
|
||||
API-->>App: ❌ Erreur détaillée
|
||||
App-->>SA: Message: "Erreur: vérifiez les paramètres"
|
||||
App-->>Op: Message: "Erreur: vérifiez les paramètres"
|
||||
end
|
||||
|
||||
SA->>App: Clic "Sauvegarder"
|
||||
Op->>App: Clic "Sauvegarder et terminer la configuration"
|
||||
App->>API: PATCH /api/v1/configuration/bulk<br/>{smtp_host, smtp_port, ...}
|
||||
API->>DB: UPDATE configuration SET valeur=...
|
||||
API-->>App: OK
|
||||
|
||||
API->>DB: BEGIN TRANSACTION
|
||||
API->>DB: UPDATE configuration SET valeur=...<br/>FOR EACH key
|
||||
API->>DB: UPDATE configuration<br/>SET valeur='true'<br/>WHERE cle='setup_completed'
|
||||
API->>DB: COMMIT
|
||||
|
||||
App->>API: POST /api/v1/configuration/setup/complete
|
||||
API->>DB: SET setup_completed = true
|
||||
API->>API: Recharger cache ConfigService
|
||||
API-->>App: OK
|
||||
|
||||
API-->>App: ✅ Configuration sauvegardée
|
||||
App-->>SA: Redirection vers /admin/dashboard
|
||||
|
||||
SA->>App: Accès complet à l'application
|
||||
App->>App: Débloque la navigation<br/>Message succès
|
||||
Op->>App: Accès complet au dashboard
|
||||
```
|
||||
|
||||
### Étapes détaillées
|
||||
|
||||
#### 1. Détection configuration incomplète
|
||||
|
||||
**Guard** : `SetupGuard` (NestJS)
|
||||
**Backend** : Le `SetupGuard` (NestJS) vérifie `setup_completed`. Si false, il autorise l’accès au dashboard et aux APIs configuration (pas de redirection vers une page dédiée). Le **frontend** appelle `GET /configuration/setup/status` au chargement du dashboard admin ; si `setupCompleted === false`, il affiche directement le **panneau Paramètres** et désactive les autres onglets jusqu’à sauvegarde.
|
||||
|
||||
**Guard** (exemple) : exemption des routes login + dashboard + APIs configuration.
|
||||
|
||||
```typescript
|
||||
@Injectable()
|
||||
export class SetupGuard implements CanActivate {
|
||||
constructor(private configService: ConfigService) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const setupCompleted = this.configService.get('setup_completed', false);
|
||||
|
||||
// Exemptions
|
||||
const exemptedRoutes = ['/auth/login', '/admin/setup', '/api/v1/configuration'];
|
||||
if (exemptedRoutes.some(route => request.url.includes(route))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Si setup non complété, bloquer
|
||||
if (!setupCompleted) {
|
||||
throw new HttpException(
|
||||
'Configuration initiale requise',
|
||||
HttpStatus.TEMPORARY_REDIRECT,
|
||||
{ location: '/admin/setup' }
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Exemptions : /auth/login, /api/v1/configuration, routes dashboard admin
|
||||
// Si setup non complété : pas de redirection HTTP ; le frontend gère l’affichage du panneau Config et le blocage des onglets.
|
||||
```
|
||||
|
||||
#### 2. Formulaire Setup (Frontend)
|
||||
#### 2. Panneau Paramètres (Frontend)
|
||||
|
||||
**3 onglets** :
|
||||
**Une seule page avec 3 sections** (blocs successifs, pas d’onglets dans le formulaire) :
|
||||
|
||||
##### Onglet 1 : Configuration Email 📧
|
||||
##### Section 1 : Configuration Email 📧
|
||||
|
||||
| Champ | Type | Valeur par défaut | Obligatoire |
|
||||
|-------|------|-------------------|-------------|
|
||||
@@ -405,7 +381,7 @@ export class SetupGuard implements CanActivate {
|
||||
|
||||
**Bouton** : "🧪 Tester la connexion SMTP"
|
||||
|
||||
##### Onglet 2 : Personnalisation 🎨
|
||||
##### Section 2 : Personnalisation 🎨
|
||||
|
||||
| Champ | Type | Valeur par défaut | Obligatoire |
|
||||
|-------|------|-------------------|-------------|
|
||||
@@ -413,7 +389,7 @@ export class SetupGuard implements CanActivate {
|
||||
| URL de l'application | URL | `https://app.ptits-pas.fr` | ✅ |
|
||||
| Logo | File (PNG/JPG) | Logo par défaut | ❌ |
|
||||
|
||||
##### Onglet 3 : Paramètres avancés ⚙️
|
||||
##### Section 3 : Paramètres avancés ⚙️
|
||||
|
||||
| Champ | Type | Valeur par défaut | Obligatoire |
|
||||
|-------|------|-------------------|-------------|
|
||||
@@ -421,7 +397,7 @@ export class SetupGuard implements CanActivate {
|
||||
| Durée session JWT (heures) | Number | `24` | ✅ |
|
||||
| Taille max upload (MB) | Number | `5` | ✅ |
|
||||
|
||||
**Bouton** : "💾 Sauvegarder et terminer la configuration"
|
||||
**Bouton** : "💾 Sauvegarder et terminer la configuration" (première config) ou "💾 Enregistrer" (accès permanent).
|
||||
|
||||
---
|
||||
|
||||
@@ -536,60 +512,39 @@ Content-Type: application/json
|
||||
|
||||
## 💻 Interface Admin
|
||||
|
||||
### Écran Setup Initial
|
||||
### Panneau Paramètres / Configuration (unique)
|
||||
|
||||
Un **seul panneau** dans le dashboard admin, avec **3 sections** affichées sur une même page (défilement si besoin). Pas d’onglets dans le formulaire.
|
||||
|
||||
- **Première configuration** (au déploiement, `setup_completed === false`) : l’opérateur arrive sur le dashboard ; le panneau Configuration est affiché par défaut et les **autres onglets sont bloqués** jusqu’à clic sur « Sauvegarder et terminer la configuration » (PATCH bulk + POST setup/complete).
|
||||
- **Accès permanent** : même panneau accessible via l’onglet « Configuration » / « Paramètres » du dashboard ; pas de blocage, simple modification et enregistrement (PATCH bulk).
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 🚀 Configuration Initiale - P'titsPas │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Bienvenue ! Configurez votre installation P'titsPas │
|
||||
│ │
|
||||
│ [ 📧 Email ] [ 🎨 Personnalisation ] [ ⚙️ Avancé ] │
|
||||
│ Dashboard Admin [ Gestionnaires ] [ Parents ] ... │
|
||||
│ [ Configuration ] ← onglet actif │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 📧 Configuration Email (SMTP) │
|
||||
│ Serveur SMTP * [_________________________________] │
|
||||
│ Port * [____] Sécurité [▼] ☐ Auth requise │
|
||||
│ Utilisateur [__________] Mot de passe [__________] │
|
||||
│ Nom expéditeur * [__________] Email * [__________] │
|
||||
│ [ 🧪 Tester la connexion SMTP ] │
|
||||
│ │
|
||||
│ Serveur SMTP * │
|
||||
│ [_____________________________________________] │
|
||||
│ Ex: mail.mairie-bezons.fr, smtp.gmail.com │
|
||||
│ │
|
||||
│ Port SMTP * │
|
||||
│ [_____] 25 (standard), 465 (SSL), 587 (STARTTLS) │
|
||||
│ │
|
||||
│ Sécurité * │
|
||||
│ [ ▼ Aucune ] STARTTLS SSL/TLS │
|
||||
│ │
|
||||
│ ☐ Authentification requise │
|
||||
│ │
|
||||
│ Utilisateur SMTP │
|
||||
│ [_____________________________________________] │
|
||||
│ │
|
||||
│ Mot de passe SMTP │
|
||||
│ [_____________________________________________] │
|
||||
│ │
|
||||
│ Nom de l'expéditeur * │
|
||||
│ [_____________________________________________] │
|
||||
│ Ex: P'titsPas - Mairie de Bezons │
|
||||
│ │
|
||||
│ Email expéditeur * │
|
||||
│ [_____________________________________________] │
|
||||
│ Ex: noreply@mairie-bezons.fr │
|
||||
│ │
|
||||
│ [ 🧪 Tester la connexion SMTP ] │
|
||||
│ │
|
||||
│ ───────────────────────────────────────────────── │
|
||||
│ │
|
||||
│ [ ← Précédent ] [ Suivant → ] │
|
||||
│ ───────────────────────────────────────────────── │
|
||||
│ 🎨 Personnalisation │
|
||||
│ Nom application * [__________] URL * [__________] │
|
||||
│ Logo [ Choisir un fichier ] │
|
||||
│ ───────────────────────────────────────────────── │
|
||||
│ ⚙️ Paramètres avancés │
|
||||
│ Durée token MDP (jours) [__] JWT (h) [__] Upload MB [__] │
|
||||
│ │
|
||||
│ [ 💾 Sauvegarder et terminer la configuration ] │
|
||||
│ (ou « Enregistrer » si config déjà complétée) │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Écran Paramètres (accès permanent)
|
||||
|
||||
Identique au Setup Initial, mais accessible depuis le menu admin :
|
||||
- Menu Admin → Paramètres → Configuration Système
|
||||
|
||||
---
|
||||
|
||||
## 📋 Exemples de configuration
|
||||
@@ -706,7 +661,7 @@ PORT=3000
|
||||
|
||||
---
|
||||
|
||||
**Dernière mise à jour** : 25 Novembre 2025
|
||||
**Version** : 1.0
|
||||
**Dernière mise à jour** : 9 Février 2026
|
||||
**Version** : 1.1
|
||||
**Statut** : ✅ Document validé
|
||||
|
||||
|
||||
+230
-119
@@ -1,9 +1,36 @@
|
||||
# 🎫 Liste Complète des Tickets - Projet P'titsPas
|
||||
|
||||
**Version** : 1.0
|
||||
**Date** : 25 Novembre 2025
|
||||
**Version** : 1.4
|
||||
**Date** : 9 Février 2026
|
||||
**Auteur** : Équipe PtitsPas
|
||||
**Estimation totale** : ~173h
|
||||
**Estimation totale** : ~184h
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Liste des tickets Gitea
|
||||
|
||||
**Les numéros de section dans ce document = numéros d’issues Gitea.** Ticket #14 dans le doc = issue Gitea #14, etc. Source : dépôt `jmartin/petitspas` (état au 9 février 2026).
|
||||
|
||||
| Gitea # | Titre (dépôt) | Statut |
|
||||
|--------|----------------|--------|
|
||||
| 3 | [BDD] Ajout champs manquants conformité CDC | ✅ Fermé |
|
||||
| 4 | [BDD] Ajout table/champ présentation dossier parent | ✅ Fermé |
|
||||
| 5 | [BDD] Ajout gestion tokens création mot de passe | ✅ Fermé |
|
||||
| 6 | [BDD] Ajout champ genre obligatoire enfants | ✅ Fermé |
|
||||
| 7 | [BDD] Supprimer champs obsolètes | ✅ Fermé |
|
||||
| 8 | [BDD] Table configuration système | ✅ Fermé |
|
||||
| 9 | [BDD] Tables documents légaux & acceptations | ✅ Fermé |
|
||||
| 10 | [Backend] Service Configuration | ✅ Fermé |
|
||||
| 11 | [Backend] API Configuration | ✅ Fermé |
|
||||
| 12 | [Backend] Guard Configuration Initiale | ✅ Fermé |
|
||||
| 13 | [Backend] Adaptation MailService pour config dynamique | ✅ Fermé |
|
||||
| 14 | [Frontend] Panneau Paramètres / Configuration (première config + accès permanent) | Ouvert |
|
||||
| 15 | [Frontend] Écran Paramètres (accès permanent) | Ouvert |
|
||||
| 16 | [Doc] Documentation configuration on-premise | Ouvert |
|
||||
| 17–88 | (voir sections ci‑dessous ; #82, #78, #79, #81, #83 ; #86, #87, #88 fermés en doublon) | — |
|
||||
| 92 | [Frontend] Dashboard Admin - Données réelles et branchement API | Ouvert |
|
||||
|
||||
*Gitea #1 et #2 = anciens tickets de test (fermés). Liste complète : https://git.ptits-pas.fr/jmartin/petitspas/issues*
|
||||
|
||||
---
|
||||
|
||||
@@ -16,17 +43,17 @@
|
||||
| **P0** | 7 tickets | ~5h | Amendements BDD (BLOQUANT) |
|
||||
| **P1** | 7 tickets | ~22h | Configuration système (BLOQUANT) |
|
||||
| **P2** | 18 tickets | ~50h | Backend métier |
|
||||
| **P3** | 17 tickets | ~52h | Frontend |
|
||||
| **P3** | 22 tickets | ~71h | Frontend |
|
||||
| **P4** | 4 tickets | ~24h | Tests & Documentation |
|
||||
| **CRITIQUES** | 6 tickets | ~13h | Upload, Logs, Infra, CDC |
|
||||
| **JURIDIQUE** | 1 ticket | ~8h | Rédaction CGU/Privacy |
|
||||
| **TOTAL** | **62 tickets** | **~181h** | |
|
||||
| **TOTAL** | **65 tickets** | **~184h** | |
|
||||
|
||||
---
|
||||
|
||||
## 🔴 PRIORITÉ 0 : Amendements Base de Données (BLOQUANT)
|
||||
|
||||
### Ticket #1 : [BDD] Ajout champs manquants conformité CDC
|
||||
### Ticket #3 : [BDD] Ajout champs manquants conformité CDC
|
||||
**Estimation** : 1h
|
||||
**Labels** : `bdd`, `p0-bloquant`, `cdc`
|
||||
|
||||
@@ -44,7 +71,7 @@ Ajouter les champs manquants dans la base de données pour être conforme au Cah
|
||||
|
||||
---
|
||||
|
||||
### Ticket #2 : [BDD] Ajout table/champ présentation dossier parent
|
||||
### Ticket #4 : [BDD] Ajout table/champ présentation dossier parent
|
||||
**Estimation** : 30min
|
||||
**Labels** : `bdd`, `p0-bloquant`, `cdc`
|
||||
|
||||
@@ -58,7 +85,7 @@ Ajouter un champ pour stocker la présentation du dossier parent (étape 4 de l'
|
||||
|
||||
---
|
||||
|
||||
### Ticket #3 : [BDD] Ajout gestion tokens création mot de passe ✅
|
||||
### Ticket #5 : [BDD] Ajout gestion tokens création mot de passe ✅
|
||||
**Estimation** : 30min
|
||||
**Labels** : `bdd`, `p0-bloquant`, `security`
|
||||
**Statut** : ✅ TERMINÉ (Fermé le 2025-11-28)
|
||||
@@ -74,7 +101,7 @@ Ajouter les champs nécessaires pour gérer les tokens de création de mot de pa
|
||||
|
||||
---
|
||||
|
||||
### Ticket #4 : [BDD] Ajout champ genre obligatoire enfants ✅
|
||||
### Ticket #6 : [BDD] Ajout champ genre obligatoire enfants ✅
|
||||
**Estimation** : 30min
|
||||
**Labels** : `bdd`, `p0-bloquant`, `cdc`
|
||||
**Statut** : ✅ TERMINÉ (Fermé le 2025-11-28)
|
||||
@@ -89,7 +116,7 @@ Ajouter le champ `genre` obligatoire (H/F) dans la table `enfants`.
|
||||
|
||||
---
|
||||
|
||||
### Ticket #5 : [BDD] Supprimer champs obsolètes
|
||||
### Ticket #7 : [BDD] Supprimer champs obsolètes
|
||||
**Estimation** : 30min
|
||||
**Labels** : `bdd`, `p0-bloquant`, `cleanup`
|
||||
|
||||
@@ -106,7 +133,7 @@ Supprimer les champs obsolètes identifiés lors de l'audit.
|
||||
|
||||
---
|
||||
|
||||
### Ticket #6 : [BDD] Table configuration système
|
||||
### Ticket #8 : [BDD] Table configuration système
|
||||
**Estimation** : 1h
|
||||
**Labels** : `bdd`, `p0-bloquant`, `on-premise`
|
||||
|
||||
@@ -124,7 +151,7 @@ Créer la table `configuration` pour stocker les paramètres système (SMTP, app
|
||||
|
||||
---
|
||||
|
||||
### Ticket #7 : [BDD] Tables documents légaux & acceptations ✅
|
||||
### Ticket #9 : [BDD] Tables documents légaux & acceptations ✅
|
||||
**Estimation** : 2h
|
||||
**Labels** : `bdd`, `p0-bloquant`, `rgpd`, `juridique`
|
||||
**Statut** : ✅ TERMINÉ (Fermé le 2025-11-30 - Ticket #68 sur Gitea)
|
||||
@@ -147,7 +174,7 @@ Créer les tables pour gérer les versions des documents légaux (CGU/Privacy) e
|
||||
|
||||
## 🟠 PRIORITÉ 1 : Configuration Système (BLOQUANT)
|
||||
|
||||
### Ticket #8 : [Backend] Service Configuration
|
||||
### Ticket #10 : [Backend] Service Configuration
|
||||
**Estimation** : 4h
|
||||
**Labels** : `backend`, `p1-bloquant`, `on-premise`
|
||||
|
||||
@@ -168,7 +195,7 @@ Créer le service de configuration avec cache en mémoire et chiffrement AES-256
|
||||
|
||||
---
|
||||
|
||||
### Ticket #9 : [Backend] API Configuration
|
||||
### Ticket #11 : [Backend] API Configuration
|
||||
**Estimation** : 3h
|
||||
**Labels** : `backend`, `p1-bloquant`, `on-premise`
|
||||
|
||||
@@ -187,26 +214,27 @@ Créer les endpoints REST pour gérer la configuration système.
|
||||
|
||||
---
|
||||
|
||||
### Ticket #10 : [Backend] Guard Configuration Initiale
|
||||
### Ticket #12 : [Backend] Guard Configuration Initiale
|
||||
**Estimation** : 2h
|
||||
**Labels** : `backend`, `p1-bloquant`, `on-premise`
|
||||
|
||||
**Description** :
|
||||
Créer un Guard/Middleware qui détecte si la configuration initiale est incomplète et force la redirection.
|
||||
Créer un Guard/Middleware qui détecte si la configuration initiale est incomplète. Le frontend affiche alors directement le panneau Configuration du dashboard et bloque la navigation jusqu'à sauvegarde (pas de page dédiée `/admin/setup`).
|
||||
|
||||
**Tâches** :
|
||||
- [ ] Créer `SetupGuard`
|
||||
- [ ] Vérifier `setup_completed` dans ConfigService
|
||||
- [ ] Redirection forcée vers `/admin/setup` si false
|
||||
- [ ] Exemption pour routes publiques (login, register)
|
||||
- [ ] Exemption pour route `/admin/setup`
|
||||
- [ ] Si false : autoriser accès au dashboard et aux APIs configuration (le frontend gère l’affichage du panneau Config et le blocage des onglets)
|
||||
- [ ] Exemption pour routes publiques (login, register) et pour les APIs `/api/v1/configuration`
|
||||
- [ ] Tests unitaires
|
||||
|
||||
**Référence** : [21_CONFIGURATION-SYSTEME.md](./21_CONFIGURATION-SYSTEME.md#workflow-setup-initial)
|
||||
|
||||
*Issue Gitea #86 fermée en doublon ; ce ticket (#12) est la référence.*
|
||||
|
||||
---
|
||||
|
||||
### Ticket #11 : [Backend] Adaptation MailService pour config dynamique
|
||||
### Ticket #13 : [Backend] Adaptation MailService pour config dynamique
|
||||
**Estimation** : 3h
|
||||
**Labels** : `backend`, `p1-bloquant`, `on-premise`, `email`
|
||||
|
||||
@@ -224,44 +252,45 @@ Adapter le service email pour utiliser la configuration dynamique depuis la BDD
|
||||
|
||||
---
|
||||
|
||||
### Ticket #12 : [Frontend] Écran Configuration Initiale (Setup Wizard)
|
||||
**Estimation** : 6h
|
||||
### Ticket #14 : [Frontend] Panneau Paramètres / Configuration (première config + accès permanent)
|
||||
**Estimation** : 5h
|
||||
**Labels** : `frontend`, `p1-bloquant`, `on-premise`
|
||||
|
||||
**Description** :
|
||||
Créer l'écran de configuration initiale (Setup Wizard) accessible à la première connexion du super admin.
|
||||
Un seul panneau **Paramètres / Configuration** dans le dashboard admin, avec **3 sections** sur une même page (pas d’onglets dédiés au formulaire : Email, Personnalisation, Avancé). Utilisé à la fois pour la **première configuration** (au déploiement, par un opérateur) et pour l’**accès permanent** (menu ou onglet Configuration). Lorsque `setup_completed` est false, le dashboard affiche directement ce panneau et **bloque la navigation** (autres onglets désactivés) jusqu’à sauvegarde.
|
||||
|
||||
**Tâches** :
|
||||
- [ ] Page `/admin/setup` (accessible uniquement si config incomplète)
|
||||
- [ ] Formulaire multi-onglets (Email / Application / Avancé)
|
||||
- [ ] Onglet 1 : Configuration Email (SMTP, auth, expéditeur)
|
||||
- [ ] Onglet 2 : Personnalisation (nom app, URL, logo)
|
||||
- [ ] Onglet 3 : Paramètres avancés (durées tokens, upload max)
|
||||
- [ ] Panneau Configuration dans le dashboard admin (onglet ou entrée de menu dédiée)
|
||||
- [ ] Une seule page avec 3 sections : **Email (SMTP)** ; **Personnalisation** (nom app, URL, logo) ; **Avancé** (durées token MDP, JWT, taille max upload)
|
||||
- [ ] Bouton "Tester la connexion SMTP" (appel API + feedback)
|
||||
- [ ] Validation côté client
|
||||
- [ ] Sauvegarde (appel API `PATCH /configuration/bulk`)
|
||||
- [ ] Message succès + redirection dashboard
|
||||
- [ ] Sauvegarde : `PATCH /configuration/bulk` puis `POST /configuration/setup/complete` si première config
|
||||
- [ ] Si `setup_completed` false au chargement : afficher ce panneau par défaut et bloquer les autres onglets jusqu’à sauvegarde
|
||||
- [ ] Message succès ; après première config, déblocage de la navigation
|
||||
|
||||
**Référence** : [21_CONFIGURATION-SYSTEME.md](./21_CONFIGURATION-SYSTEME.md#interface-admin)
|
||||
|
||||
*Issue Gitea #87 fermée en doublon de #14.*
|
||||
|
||||
---
|
||||
|
||||
### Ticket #13 : [Frontend] Écran Paramètres (accès permanent)
|
||||
**Estimation** : 2h
|
||||
### Ticket #15 : [Frontend] Écran Paramètres (accès permanent) / Intégration panneau
|
||||
**Estimation** : 1h
|
||||
**Labels** : `frontend`, `p1-bloquant`, `on-premise`
|
||||
|
||||
**Description** :
|
||||
Créer l'écran de paramètres accessible depuis le menu admin (même interface que Setup Wizard).
|
||||
S’assurer que le panneau Paramètres (décrit en #14) est accessible en permanence depuis le dashboard admin (onglet « Configuration » ou « Paramètres »). Même interface que pour la première config ; affichage des valeurs actuelles, modification et sauvegarde sans blocage de navigation.
|
||||
|
||||
**Tâches** :
|
||||
- [ ] Page `/admin/parametres` (accessible depuis menu admin)
|
||||
- [ ] Même interface que Setup Wizard
|
||||
- [ ] Affichage valeurs actuelles
|
||||
- [ ] Modification et sauvegarde
|
||||
- [ ] Onglet ou entrée menu « Configuration » / « Paramètres » dans le dashboard admin pointant vers le même panneau que #14
|
||||
- [ ] Chargement des valeurs actuelles (GET `/configuration` ou par catégorie)
|
||||
- [ ] Modification et sauvegarde (PATCH bulk) sans appel à `setup/complete`
|
||||
|
||||
*Issue Gitea #88 fermée en doublon ; ce ticket (#15) est la référence.*
|
||||
|
||||
---
|
||||
|
||||
### Ticket #14 : [Doc] Documentation configuration on-premise
|
||||
### Ticket #16 : [Doc] Documentation configuration on-premise
|
||||
**Estimation** : 2h
|
||||
**Labels** : `documentation`, `p1-bloquant`, `on-premise`
|
||||
|
||||
@@ -280,9 +309,14 @@ Rédiger la documentation pour aider les collectivités à configurer l'applicat
|
||||
|
||||
---
|
||||
|
||||
### Ticket #86 / #88 : Doublons fermés
|
||||
*#86* fermé en doublon de **#12** (Guard). *#88* fermé en doublon de **#15** (Intégration panneau). Voir les tickets #12, #14 et #15 pour le travail à faire.
|
||||
|
||||
---
|
||||
|
||||
## 🟢 PRIORITÉ 2 : Backend - Authentification & Gestion Comptes
|
||||
|
||||
### Ticket #15 : [Backend] API Création gestionnaire
|
||||
### Ticket #17 : [Backend] API Création gestionnaire
|
||||
**Estimation** : 3h
|
||||
**Labels** : `backend`, `p2`, `auth`
|
||||
|
||||
@@ -302,7 +336,7 @@ Créer l'endpoint pour permettre au super admin de créer des gestionnaires.
|
||||
|
||||
---
|
||||
|
||||
### Ticket #16 : [Backend] API Inscription Parent (étape 1 - Parent 1)
|
||||
### Ticket #18 : [Backend] API Inscription Parent (étape 1 - Parent 1)
|
||||
**Estimation** : 4h
|
||||
**Labels** : `backend`, `p2`, `auth`, `cdc`
|
||||
|
||||
@@ -322,7 +356,7 @@ Créer l'endpoint d'inscription Parent (étape 1/6 : informations Parent 1).
|
||||
|
||||
---
|
||||
|
||||
### Ticket #17 : [Backend] API Inscription Parent (étape 2 - Parent 2)
|
||||
### Ticket #19 : [Backend] API Inscription Parent (étape 2 - Parent 2)
|
||||
**Estimation** : 2h
|
||||
**Labels** : `backend`, `p2`, `auth`, `cdc`
|
||||
|
||||
@@ -409,7 +443,7 @@ Finalisation de l'inscription parent (présentation, CGU, récapitulatif - inté
|
||||
|
||||
---
|
||||
|
||||
### Ticket #22 : [Backend] API Création mot de passe
|
||||
### Ticket #24 : [Backend] API Création mot de passe
|
||||
**Estimation** : 3h
|
||||
**Labels** : `backend`, `p2`, `auth`, `security`
|
||||
|
||||
@@ -428,7 +462,7 @@ Créer les endpoints pour permettre aux utilisateurs de créer leur mot de passe
|
||||
|
||||
---
|
||||
|
||||
### Ticket #23 : [Backend] API Liste comptes en attente
|
||||
### Ticket #25 : [Backend] API Liste comptes en attente
|
||||
**Estimation** : 2h
|
||||
**Labels** : `backend`, `p2`, `gestionnaire`
|
||||
|
||||
@@ -446,7 +480,7 @@ Créer les endpoints pour lister les comptes en attente de validation.
|
||||
|
||||
---
|
||||
|
||||
### Ticket #24 : [Backend] API Validation/Refus comptes
|
||||
### Ticket #26 : [Backend] API Validation/Refus comptes
|
||||
**Estimation** : 3h
|
||||
**Labels** : `backend`, `p2`, `gestionnaire`
|
||||
|
||||
@@ -464,7 +498,7 @@ Créer les endpoints pour valider ou refuser les comptes en attente.
|
||||
|
||||
---
|
||||
|
||||
### Ticket #25 : [Backend] Service Email - Installation Nodemailer
|
||||
### Ticket #27 : [Backend] Service Email - Installation Nodemailer
|
||||
**Estimation** : 2h
|
||||
**Labels** : `backend`, `p2`, `email`
|
||||
|
||||
@@ -479,7 +513,7 @@ Installer et configurer Nodemailer pour l'envoi d'emails.
|
||||
|
||||
---
|
||||
|
||||
### Ticket #26 : [Backend] Templates Email - Validation
|
||||
### Ticket #28 : [Backend] Templates Email - Validation
|
||||
**Estimation** : 3h
|
||||
**Labels** : `backend`, `p2`, `email`
|
||||
|
||||
@@ -500,7 +534,7 @@ Créer les templates d'emails pour la validation des comptes (avec lien créatio
|
||||
|
||||
---
|
||||
|
||||
### Ticket #27 : [Backend] Templates Email - Refus
|
||||
### Ticket #29 : [Backend] Templates Email - Refus
|
||||
**Estimation** : 1h
|
||||
**Labels** : `backend`, `p2`, `email`
|
||||
|
||||
@@ -515,7 +549,7 @@ Créer le template d'email pour le refus de compte.
|
||||
|
||||
---
|
||||
|
||||
### Ticket #28 : [Backend] Connexion - Vérification statut
|
||||
### Ticket #30 : [Backend] Connexion - Vérification statut
|
||||
**Estimation** : 2h
|
||||
**Labels** : `backend`, `p2`, `auth`
|
||||
|
||||
@@ -533,7 +567,7 @@ Modifier l'endpoint de connexion pour bloquer les comptes en attente ou suspendu
|
||||
|
||||
---
|
||||
|
||||
### Ticket #29 : [Backend] Changement MDP obligatoire première connexion
|
||||
### Ticket #31 : [Backend] Changement MDP obligatoire première connexion
|
||||
**Estimation** : 2h
|
||||
**Labels** : `backend`, `p2`, `auth`, `security`
|
||||
|
||||
@@ -548,7 +582,7 @@ Implémenter le changement de mot de passe obligatoire pour les gestionnaires/ad
|
||||
|
||||
---
|
||||
|
||||
### Ticket #30 : [Backend] Service Documents Légaux
|
||||
### Ticket #32 : [Backend] Service Documents Légaux
|
||||
**Estimation** : 4h
|
||||
**Labels** : `backend`, `p2`, `juridique`, `rgpd`
|
||||
|
||||
@@ -570,7 +604,7 @@ Créer le service de gestion des documents légaux (CGU/Privacy) avec versioning
|
||||
|
||||
---
|
||||
|
||||
### Ticket #31 : [Backend] API Documents Légaux
|
||||
### Ticket #33 : [Backend] API Documents Légaux
|
||||
**Estimation** : 3h
|
||||
**Labels** : `backend`, `p2`, `juridique`, `rgpd`
|
||||
|
||||
@@ -590,7 +624,7 @@ Créer les endpoints REST pour gérer les documents légaux.
|
||||
|
||||
---
|
||||
|
||||
### Ticket #32 : [Backend] Traçabilité acceptations documents
|
||||
### Ticket #34 : [Backend] Traçabilité acceptations documents
|
||||
**Estimation** : 2h
|
||||
**Labels** : `backend`, `p2`, `rgpd`
|
||||
|
||||
@@ -609,7 +643,7 @@ Enregistrer les acceptations de documents légaux lors de l'inscription (traçab
|
||||
|
||||
## 🟢 PRIORITÉ 3 : Frontend - Interfaces
|
||||
|
||||
### Ticket #33 : [Frontend] Écran Création Gestionnaire
|
||||
### Ticket #35 : [Frontend] Écran Création Gestionnaire
|
||||
**Estimation** : 3h
|
||||
**Labels** : `frontend`, `p3`, `auth`
|
||||
|
||||
@@ -624,14 +658,6 @@ Créer l'écran de création de gestionnaire (super admin uniquement).
|
||||
|
||||
---
|
||||
|
||||
### Ticket #34 : [Réservé - Non utilisé]
|
||||
|
||||
---
|
||||
|
||||
### Ticket #35 : [Réservé - Non utilisé]
|
||||
|
||||
---
|
||||
|
||||
### Ticket #36 : [Frontend] Inscription Parent - Étape 1 (Parent 1) ✅
|
||||
**Estimation** : 3h
|
||||
**Labels** : `frontend`, `p3`, `auth`, `cdc`
|
||||
@@ -668,85 +694,90 @@ Créer le formulaire d'inscription parent - étape 2/6 (informations Parent 2 op
|
||||
|
||||
---
|
||||
|
||||
### Ticket #38 : [Frontend] Inscription Parent - Étape 3 (Enfants)
|
||||
**Estimation** : 4h
|
||||
### Ticket #38 : [Frontend] Inscription Parent - Étape 3 (Enfants) ✅
|
||||
**Estimation** : 4h
|
||||
**Labels** : `frontend`, `p3`, `auth`, `cdc`, `upload`
|
||||
**Statut** : ✅ TERMINÉ (Fermé le 2026-02-07)
|
||||
|
||||
**Description** :
|
||||
Créer le formulaire d'inscription parent - étape 3/6 (informations enfants).
|
||||
|
||||
**Tâches** :
|
||||
- [ ] Question "Enfant déjà né ?"
|
||||
- [ ] Formulaire enfant (prénom, date, genre H/F obligatoire)
|
||||
- [ ] Upload photo (si né)
|
||||
- [ ] Validation taille (5MB)
|
||||
- [ ] Bouton "Ajouter un autre enfant"
|
||||
- [ ] Navigation vers étape 4
|
||||
- [x] Question "Enfant déjà né ?"
|
||||
- [x] Formulaire enfant (prénom, date, genre H/F obligatoire)
|
||||
- [x] Upload photo (si né)
|
||||
- [x] Validation taille (5MB)
|
||||
- [x] Bouton "Ajouter un autre enfant"
|
||||
- [x] Navigation vers étape 4
|
||||
|
||||
---
|
||||
|
||||
### Ticket #39 : [Frontend] Inscription Parent - Étapes 4-6 (Finalisation)
|
||||
**Estimation** : 4h
|
||||
### Ticket #39 : [Frontend] Inscription Parent - Étapes 4-6 (Finalisation) ✅
|
||||
**Estimation** : 4h
|
||||
**Labels** : `frontend`, `p3`, `auth`, `cdc`
|
||||
**Statut** : ✅ TERMINÉ (Fermé le 2026-02-07)
|
||||
|
||||
**Description** :
|
||||
Créer les étapes finales de l'inscription parent (présentation, CGU, récapitulatif).
|
||||
|
||||
**Tâches** :
|
||||
- [ ] Étape 4 : Textarea présentation
|
||||
- [ ] Étape 5 : Checkbox CGU + liens PDF
|
||||
- [ ] Étape 6 : Récapitulatif complet
|
||||
- [ ] Bouton "Modifier" / "Valider"
|
||||
- [ ] Appel API final
|
||||
- [ ] Message confirmation
|
||||
- [x] Étape 4 : Textarea présentation
|
||||
- [x] Étape 5 : Checkbox CGU + liens PDF
|
||||
- [x] Étape 6 : Récapitulatif complet
|
||||
- [x] Bouton "Modifier" / "Valider"
|
||||
- [x] Appel API final
|
||||
- [x] Message confirmation
|
||||
|
||||
---
|
||||
|
||||
### Ticket #40 : [Frontend] Inscription AM - Panneau 1 (Identité)
|
||||
**Estimation** : 3h
|
||||
### Ticket #40 : [Frontend] Inscription AM - Panneau 1 (Identité) ✅
|
||||
**Estimation** : 3h
|
||||
**Labels** : `frontend`, `p3`, `auth`, `cdc`, `upload`
|
||||
**Statut** : ✅ TERMINÉ (Fermé le 2026-02-07)
|
||||
|
||||
**Description** :
|
||||
Créer le formulaire d'inscription AM - panneau 1/5 (identité).
|
||||
|
||||
**Tâches** :
|
||||
- [ ] Formulaire identité
|
||||
- [ ] Upload photo
|
||||
- [ ] Checkbox consentement photo
|
||||
- [ ] Pas de champ mot de passe
|
||||
- [ ] Navigation vers panneau 2
|
||||
- [x] Formulaire identité
|
||||
- [x] Upload photo
|
||||
- [x] Checkbox consentement photo
|
||||
- [x] Pas de champ mot de passe
|
||||
- [x] Navigation vers panneau 2
|
||||
|
||||
---
|
||||
|
||||
### Ticket #41 : [Frontend] Inscription AM - Panneau 2 (Infos pro)
|
||||
**Estimation** : 3h
|
||||
### Ticket #41 : [Frontend] Inscription AM - Panneau 2 (Infos pro) ✅
|
||||
**Estimation** : 3h
|
||||
**Labels** : `frontend`, `p3`, `auth`, `cdc`
|
||||
**Statut** : ✅ TERMINÉ (Fermé le 2026-02-07)
|
||||
|
||||
**Description** :
|
||||
Créer le formulaire d'inscription AM - panneau 2/5 (informations professionnelles).
|
||||
|
||||
**Tâches** :
|
||||
- [ ] Formulaire infos pro
|
||||
- [ ] Champ NIR (15 chiffres, validation)
|
||||
- [ ] Date/lieu naissance
|
||||
- [ ] Agrément + date obtention
|
||||
- [ ] Navigation vers présentation
|
||||
- [x] Formulaire infos pro
|
||||
- [x] Champ NIR (15 chiffres, validation)
|
||||
- [x] Date/lieu naissance
|
||||
- [x] Agrément + date obtention
|
||||
- [x] Navigation vers présentation
|
||||
|
||||
---
|
||||
|
||||
### Ticket #42 : [Frontend] Inscription AM - Finalisation
|
||||
**Estimation** : 3h
|
||||
### Ticket #42 : [Frontend] Inscription AM - Finalisation ✅
|
||||
**Estimation** : 3h
|
||||
**Labels** : `frontend`, `p3`, `auth`, `cdc`
|
||||
**Statut** : ✅ TERMINÉ (Fermé le 2026-02-07)
|
||||
|
||||
**Description** :
|
||||
Créer les étapes finales de l'inscription AM (présentation, CGU, récapitulatif).
|
||||
|
||||
**Tâches** :
|
||||
- [ ] Textarea présentation
|
||||
- [ ] Checkbox CGU + liens PDF
|
||||
- [ ] Récapitulatif (NIR masqué)
|
||||
- [ ] Appel API final
|
||||
- [ ] Message confirmation
|
||||
- [x] Textarea présentation
|
||||
- [x] Checkbox CGU + liens PDF
|
||||
- [x] Récapitulatif (NIR masqué)
|
||||
- [x] Appel API final
|
||||
- [x] Message confirmation
|
||||
|
||||
---
|
||||
|
||||
@@ -863,6 +894,30 @@ Créer l'écran de gestion des documents légaux (CGU/Privacy) pour l'admin.
|
||||
|
||||
---
|
||||
|
||||
### Ticket #92 : [Frontend] Dashboard Admin - Données réelles et branchement API
|
||||
**Estimation** : 8h
|
||||
**Labels** : `frontend`, `p3`, `admin`
|
||||
|
||||
**Description** :
|
||||
Le dashboard admin (onglets Gestionnaires | Parents | Assistantes maternelles | Administrateurs) affiche actuellement des données en dur (mock). Remplacer par des appels API pour afficher les vrais utilisateurs et permettre les actions de gestion (voir, modifier, valider/refuser). Référence : [90_AUDIT.md](./90_AUDIT.md).
|
||||
|
||||
**Fichiers concernés** :
|
||||
- `gestionnaire_management_widget.dart` — liste actuellement 5 cartes "Dupont" en dur
|
||||
- `parent_managmant_widget.dart` — 2 parents simulés
|
||||
- `assistante_maternelle_management_widget.dart` — 2 AM simulées
|
||||
|
||||
**Tâches** :
|
||||
- [ ] S'assurer que les endpoints backend existent (liste users par rôle)
|
||||
- [ ] Onglet Gestionnaires : appel API, affichage dynamique, recherche, lien "Créer gestionnaire"
|
||||
- [ ] Onglet Parents : appel API, affichage dynamique, recherche/filtres, actions Voir/Modifier/Valider/Refuser
|
||||
- [ ] Onglet Assistantes maternelles : appel API, affichage dynamique, filtres, actions
|
||||
- [ ] Onglet Administrateurs : liste ou placeholder documenté
|
||||
- [ ] Gestion états (chargement, erreur, liste vide) et rafraîchissement après actions
|
||||
|
||||
**Références** : #44, #45, #46 (dashboard Gestionnaire), #25, #26 (API liste/validation), #17, #35 (création gestionnaire)
|
||||
|
||||
---
|
||||
|
||||
### Ticket #50 : [Frontend] Affichage dynamique CGU lors inscription
|
||||
**Estimation** : 2h
|
||||
**Labels** : `frontend`, `p3`, `juridique`
|
||||
@@ -913,6 +968,56 @@ Créer une infrastructure générique pour gérer les formulaires en modes multi
|
||||
|
||||
---
|
||||
|
||||
### Ticket #79 : [Frontend] Renommer "Nanny" en "Assistante Maternelle" (AM) ✅
|
||||
**Estimation** : 2h
|
||||
**Labels** : `frontend`, `p3`, `refactoring`, `cdc`
|
||||
**Statut** : ✅ TERMINÉ (Fermé le 2026-02-07)
|
||||
|
||||
**Description** :
|
||||
Renommage complet de "Nanny" en "AM" dans le frontend pour cohérence avec le CDC.
|
||||
|
||||
**Tâches** :
|
||||
- [x] Modèles : nanny_registration_data.dart -> am_registration_data.dart
|
||||
- [x] Écrans : nanny_register_*.dart -> am_register_*.dart
|
||||
- [x] Routes : /nanny-register -> /am-register
|
||||
- [x] Suppression fichiers obsolètes
|
||||
|
||||
---
|
||||
|
||||
### Ticket #81 : [Frontend] Corrections suite refactoring widgets ✅
|
||||
**Estimation** : 2h
|
||||
**Labels** : `frontend`, `p3`, `bugfix`
|
||||
**Statut** : ✅ TERMINÉ (Fermé le 2026-02-07)
|
||||
|
||||
**Description** :
|
||||
Corrections et ajustements suite au refactoring des widgets.
|
||||
|
||||
**Corrections :**
|
||||
- [x] Erreurs compilation (updateParent1/2, updateIdentityInfo)
|
||||
- [x] Routes obsolètes supprimées
|
||||
- [x] Toggles Parent Step 2 (2 côte à côte)
|
||||
- [x] Positionnement éléments dans cartes
|
||||
|
||||
---
|
||||
|
||||
### Ticket #83 : [Frontend] Adapter RegisterChoiceScreen pour mobile ✅
|
||||
**Estimation** : 3h
|
||||
**Labels** : `frontend`, `p3`, `responsive`, `ux`
|
||||
**Statut** : ✅ TERMINÉ (Fermé le 2026-01-27)
|
||||
|
||||
**Description** :
|
||||
Adapter l'écran de choix Parent/AM pour une meilleure expérience mobile et cohérence avec les autres écrans.
|
||||
|
||||
**Tâches :**
|
||||
- [x] Implémentation responsive avec LayoutBuilder (mobile < 900px)
|
||||
- [x] Mode mobile : titre au-dessus, carte pleine largeur (ratio 2/3), boutons verticaux
|
||||
- [x] Mode desktop : chevron haut-gauche, layout texte/carte côte à côte
|
||||
- [x] Extraction logique carte dans ChoiceCardWidget réutilisable
|
||||
- [x] Bouton "Précédent" mobile avec CustomNavigationButton + HoverReliefWidget
|
||||
- [x] Tailles icônes augmentées (140px mobile, 170px desktop)
|
||||
|
||||
---
|
||||
|
||||
## 🔵 PRIORITÉ 4 : Tests & Documentation
|
||||
|
||||
### Ticket #52 : [Tests] Tests unitaires Backend
|
||||
@@ -930,7 +1035,7 @@ Créer les tests unitaires pour tous les services et controllers backend.
|
||||
|
||||
---
|
||||
|
||||
### Ticket #50 : [Tests] Tests intégration Backend
|
||||
### Ticket #53 : [Tests] Tests intégration Backend
|
||||
**Estimation** : 5h
|
||||
**Labels** : `tests`, `p4`, `backend`
|
||||
|
||||
@@ -945,7 +1050,7 @@ Créer les tests d'intégration pour les workflows complets.
|
||||
|
||||
---
|
||||
|
||||
### Ticket #51 : [Tests] Tests E2E Frontend
|
||||
### Ticket #54 : [Tests] Tests E2E Frontend
|
||||
**Estimation** : 8h
|
||||
**Labels** : `tests`, `p4`, `frontend`
|
||||
|
||||
@@ -961,7 +1066,7 @@ Créer les tests end-to-end pour les parcours utilisateurs.
|
||||
|
||||
---
|
||||
|
||||
### Ticket #52 : [Doc] Documentation API OpenAPI/Swagger
|
||||
### Ticket #55 : [Doc] Documentation API OpenAPI/Swagger
|
||||
**Estimation** : 3h
|
||||
**Labels** : `documentation`, `p4`, `api`
|
||||
|
||||
@@ -978,7 +1083,7 @@ Générer et documenter l'API avec Swagger/OpenAPI.
|
||||
|
||||
## ⚠️ CRITIQUES : Upload, Emails, Infra, Doc
|
||||
|
||||
### Ticket #53 : [Backend] Service Upload & Stockage fichiers
|
||||
### Ticket #56 : [Backend] Service Upload & Stockage fichiers
|
||||
**Estimation** : 3h
|
||||
**Labels** : `backend`, `critique`, `upload`
|
||||
|
||||
@@ -1009,7 +1114,7 @@ Créer l'endpoint sécurisé pour télécharger les photos.
|
||||
|
||||
---
|
||||
|
||||
### Ticket #55 : [Backend] Service Logging (Winston)
|
||||
### Ticket #58 : [Backend] Service Logging (Winston)
|
||||
**Estimation** : 3h
|
||||
**Labels** : `backend`, `critique`, `monitoring`
|
||||
|
||||
@@ -1028,7 +1133,7 @@ Mettre en place un système de logs centralisé avec Winston pour faciliter le d
|
||||
|
||||
---
|
||||
|
||||
### Ticket #56 : [Frontend] Écran Logs Admin (optionnel v1.1)
|
||||
### Ticket #51 (réf.) : [Frontend] Écran Logs Admin (optionnel v1.1)
|
||||
**Estimation** : 4h
|
||||
**Labels** : `frontend`, `p3`, `monitoring`, `admin`
|
||||
|
||||
@@ -1046,7 +1151,7 @@ Créer un écran pour consulter les logs depuis l'interface admin (optionnel Pha
|
||||
|
||||
---
|
||||
|
||||
### Ticket #57 : [Infra] Volume Docker pour uploads
|
||||
### Ticket #59 : [Infra] Volume Docker pour uploads
|
||||
**Estimation** : 30min
|
||||
**Labels** : `infra`, `critique`, `docker`
|
||||
|
||||
@@ -1060,7 +1165,7 @@ Ajouter un volume Docker pour persister les fichiers uploadés.
|
||||
|
||||
---
|
||||
|
||||
### Ticket #58 : [Infra] Volume Docker pour documents légaux
|
||||
### Ticket #60 : [Infra] Volume Docker pour documents légaux
|
||||
**Estimation** : 30min
|
||||
**Labels** : `infra`, `critique`, `docker`
|
||||
|
||||
@@ -1074,7 +1179,7 @@ Ajouter un volume Docker pour persister les documents légaux (CGU/Privacy).
|
||||
|
||||
---
|
||||
|
||||
### Ticket #59 : [Doc] Guide installation & configuration
|
||||
### Ticket #61 : [Doc] Guide installation & configuration
|
||||
**Estimation** : 3h
|
||||
**Labels** : `documentation`, `critique`, `on-premise`
|
||||
|
||||
@@ -1093,7 +1198,7 @@ Rédiger le guide complet d'installation et de configuration pour les collectivi
|
||||
|
||||
## 📚 JURIDIQUE & CDC
|
||||
|
||||
### Ticket #60 : [Doc] Amendement CDC v1.4 - Suppression SMS
|
||||
### Ticket #62 : [Doc] Amendement CDC v1.4 - Suppression SMS
|
||||
**Estimation** : 30min
|
||||
**Labels** : `documentation`, `cdc`
|
||||
|
||||
@@ -1110,7 +1215,7 @@ Amender le Cahier des Charges pour supprimer la mention des notifications SMS (p
|
||||
|
||||
---
|
||||
|
||||
### Ticket #61 : [Doc] Rédaction CGU/Privacy génériques v1
|
||||
### Ticket #63 : [Doc] Rédaction CGU/Privacy génériques v1
|
||||
**Estimation** : 8h
|
||||
**Labels** : `documentation`, `juridique`, `rgpd`
|
||||
|
||||
@@ -1130,36 +1235,42 @@ Rédiger les documents légaux génériques (CGU et Politique de confidentialit
|
||||
|
||||
## 📊 Résumé final
|
||||
|
||||
**Total** : 61 tickets
|
||||
**Estimation** : ~173h de développement
|
||||
**Total** : 65 tickets
|
||||
**Estimation** : ~184h de développement
|
||||
|
||||
### Par priorité
|
||||
- **P0 (Bloquant BDD)** : 7 tickets (~5h)
|
||||
- **P1 (Bloquant Config)** : 7 tickets (~22h)
|
||||
- **P2 (Backend)** : 18 tickets (~50h)
|
||||
- **P3 (Frontend)** : 18 tickets (~60h) ← +1 ticket logs admin, +1 ticket refonte formulaires
|
||||
- **P3 (Frontend)** : 22 tickets (~71h) ← +1 mobile RegisterChoice
|
||||
- **P4 (Tests/Doc)** : 4 tickets (~24h)
|
||||
- **Critiques** : 6 tickets (~13h) ← -2 email, +1 logs, +1 CDC
|
||||
- **Critiques** : 6 tickets (~13h)
|
||||
- **Juridique** : 1 ticket (~8h)
|
||||
|
||||
### Par domaine
|
||||
- **BDD** : 7 tickets
|
||||
- **Backend** : 23 tickets ← +1 logs
|
||||
- **Frontend** : 18 tickets ← +1 logs admin, +1 refonte formulaires
|
||||
- **Backend** : 23 tickets
|
||||
- **Frontend** : 22 tickets ← +1 mobile RegisterChoice
|
||||
- **Tests** : 3 tickets
|
||||
- **Documentation** : 5 tickets ← +1 amendement CDC
|
||||
- **Documentation** : 5 tickets
|
||||
- **Infra** : 2 tickets
|
||||
- **Juridique** : 1 ticket
|
||||
|
||||
### Modifications par rapport à la version initiale
|
||||
- ✅ **v1.4** : Numéros de section du doc = numéros Gitea (Ticket #n = issue #n). Tableau et sections renumérotés. Doublons #86, #87, #88 fermés sur Gitea (#86→#12, #87→#14, #88→#15) ; tickets sources #12, #14, #15 mis à jour (doc + body Gitea).
|
||||
- ✅ **Concept v1.3** : Configuration initiale = un seul panneau Paramètres (3 sections) dans le dashboard ; plus de page dédiée « Setup Wizard » ; navigation bloquée jusqu’à sauvegarde au premier déploiement. Tickets #10, #12, #13 alignés.
|
||||
- ❌ **Supprimé** : Tickets "Renvoyer email validation" (backend + frontend) - Pas prioritaire
|
||||
- ✅ **Ajouté** : Ticket #55 "Service Logging Winston" - Monitoring essentiel
|
||||
- ✅ **Ajouté** : Ticket #56 "Écran Logs Admin" - Optionnel Phase 1.1
|
||||
- ✅ **Ajouté** : Ticket #78 "Refonte Infrastructure Formulaires" - Harmonisation UI/UX
|
||||
- ✅ **Ajouté** : Ticket #79 "Renommer Nanny en AM" - Cohérence CDC
|
||||
- ✅ **Ajouté** : Ticket #81 "Corrections refactoring" - Bugfixes
|
||||
- ✅ **Ajouté** : Ticket #83 "RegisterChoiceScreen Mobile" - Responsive UX
|
||||
- ✅ **Fermé** : Ticket #82 "Écran Login mobile" - Merge develop + master
|
||||
|
||||
---
|
||||
|
||||
**Dernière mise à jour** : 25 Novembre 2025
|
||||
**Version** : 1.0
|
||||
**Statut** : ✅ Prêt pour création dans Gitea
|
||||
**Dernière mise à jour** : 9 Février 2026
|
||||
**Version** : 1.4
|
||||
**Statut** : ✅ Aligné avec le dépôt Gitea
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 📋 Décisions Projet - P'titsPas
|
||||
|
||||
**Version** : 1.0
|
||||
**Date** : 25 Novembre 2025
|
||||
**Version** : 1.1
|
||||
**Date** : 9 Février 2026
|
||||
**Auteur** : Équipe PtitsPas
|
||||
|
||||
---
|
||||
@@ -49,7 +49,7 @@ ptitspas-app/
|
||||
|
||||
**Solution technique** :
|
||||
- Table `configuration` en BDD (clé/valeur)
|
||||
- Setup Wizard à la première connexion
|
||||
- Panneau Paramètres (3 sections) dans le dashboard à la première connexion ; navigation bloquée jusqu'à sauvegarde
|
||||
- Configuration SMTP dynamique
|
||||
- Personnalisation (nom app, logo, URL)
|
||||
|
||||
@@ -559,10 +559,11 @@ docs/
|
||||
| Date | Version | Modifications |
|
||||
|------|---------|---------------|
|
||||
| 25/11/2025 | 1.0 | Création du document - Toutes les décisions initiales |
|
||||
| 09/02/2026 | 1.1 | Configuration initiale : un seul panneau Paramètres (3 sections) dans le dashboard, plus de Setup Wizard dédié ; navigation bloquée jusqu'à sauvegarde |
|
||||
|
||||
---
|
||||
|
||||
**Dernière mise à jour** : 25 Novembre 2025
|
||||
**Version** : 1.0
|
||||
**Dernière mise à jour** : 9 Février 2026
|
||||
**Version** : 1.1
|
||||
**Statut** : ✅ Document validé
|
||||
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
# Procédure – Utilisation de l'API Gitea
|
||||
|
||||
## 1. Contexte
|
||||
|
||||
- **Instance** : https://git.ptits-pas.fr
|
||||
- **API de base** : `https://git.ptits-pas.fr/api/v1`
|
||||
- **Projet P'titsPas** : dépôt `jmartin/petitspas` (owner = `jmartin`, repo = `petitspas`)
|
||||
|
||||
## 2. Authentification
|
||||
|
||||
### 2.1 Token
|
||||
|
||||
Le token est défini dans l'environnement (ex. `~/.bashrc`) :
|
||||
|
||||
```bash
|
||||
export GITEA_TOKEN="<votre_token>"
|
||||
```
|
||||
|
||||
Pour l'utiliser dans les commandes :
|
||||
|
||||
```bash
|
||||
source ~/.bashrc # ou : . ~/.bashrc
|
||||
# Puis utiliser $GITEA_TOKEN dans les curl
|
||||
```
|
||||
|
||||
### 2.2 En-tête HTTP
|
||||
|
||||
Toutes les requêtes API doivent envoyer le token :
|
||||
|
||||
```bash
|
||||
-H "Authorization: token $GITEA_TOKEN"
|
||||
```
|
||||
|
||||
Exemple :
|
||||
|
||||
```bash
|
||||
curl -s -H "Authorization: token $GITEA_TOKEN" \
|
||||
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas"
|
||||
```
|
||||
|
||||
## 3. Endpoints utiles
|
||||
|
||||
### 3.1 Dépôt (repository)
|
||||
|
||||
| Action | Méthode | URL |
|
||||
|---------------|---------|-----|
|
||||
| Infos dépôt | GET | `/repos/{owner}/{repo}` |
|
||||
| Liste dépôts | GET | `/repos/search?q=petitspas` |
|
||||
|
||||
Exemple – infos du dépôt :
|
||||
|
||||
```bash
|
||||
curl -s -H "Authorization: token $GITEA_TOKEN" \
|
||||
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas" | jq .
|
||||
```
|
||||
|
||||
### 3.2 Issues (tickets)
|
||||
|
||||
| Action | Méthode | URL |
|
||||
|------------------|---------|-----|
|
||||
| Liste des issues | GET | `/repos/{owner}/{repo}/issues` |
|
||||
| Détail d'une issue | GET | `/repos/{owner}/{repo}/issues/{index}` |
|
||||
| Créer une issue | POST | `/repos/{owner}/{repo}/issues` |
|
||||
| Modifier une issue | PATCH | `/repos/{owner}/{repo}/issues/{index}` |
|
||||
| Fermer une issue | PATCH | (même URL, `state: "closed"`) |
|
||||
|
||||
**Paramètres GET utiles pour la liste :**
|
||||
|
||||
- `state` : `open` ou `closed`
|
||||
- `labels` : filtre par label (ex. `frontend`)
|
||||
- `page`, `limit` : pagination
|
||||
|
||||
Exemples :
|
||||
|
||||
```bash
|
||||
# Toutes les issues ouvertes
|
||||
curl -s -H "Authorization: token $GITEA_TOKEN" \
|
||||
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues?state=open" | jq .
|
||||
|
||||
# Issues ouvertes avec label "frontend"
|
||||
curl -s -H "Authorization: token $GITEA_TOKEN" \
|
||||
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues?state=open" | \
|
||||
jq '.[] | select(.labels[].name == "frontend") | {number, title, state}'
|
||||
|
||||
# Détail de l'issue #47
|
||||
curl -s -H "Authorization: token $GITEA_TOKEN" \
|
||||
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues/47" | jq .
|
||||
|
||||
# Fermer l'issue #31
|
||||
curl -s -X PATCH -H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"state":"closed"}' \
|
||||
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues/31"
|
||||
|
||||
# Créer une issue
|
||||
curl -s -X POST -H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"title":"Titre du ticket","body":"Description","labels":[1]}' \
|
||||
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues"
|
||||
```
|
||||
|
||||
### 3.3 Pull requests
|
||||
|
||||
| Action | Méthode | URL |
|
||||
|---------------|---------|-----|
|
||||
| Liste des PR | GET | `/repos/{owner}/{repo}/pulls` |
|
||||
| Détail d'une PR | GET | `/repos/{owner}/{repo}/pulls/{index}` |
|
||||
| Créer une PR | POST | `/repos/{owner}/{repo}/pulls` |
|
||||
| Fusionner une PR | POST | `/repos/{owner}/{repo}/pulls/{index}/merge` |
|
||||
|
||||
Exemples :
|
||||
|
||||
```bash
|
||||
# Liste des PR ouvertes
|
||||
curl -s -H "Authorization: token $GITEA_TOKEN" \
|
||||
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/pulls?state=open" | jq .
|
||||
|
||||
# Créer une PR (head = branche source, base = branche cible)
|
||||
curl -s -X POST -H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"head":"develop","base":"master","title":"Titre de la PR"}' \
|
||||
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/pulls"
|
||||
```
|
||||
|
||||
### 3.4 Branches
|
||||
|
||||
| Action | Méthode | URL |
|
||||
|---------------|---------|-----|
|
||||
| Liste des branches | GET | `/repos/{owner}/{repo}/branches` |
|
||||
|
||||
```bash
|
||||
curl -s -H "Authorization: token $GITEA_TOKEN" \
|
||||
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/branches" | jq '.[].name'
|
||||
```
|
||||
|
||||
### 3.5 Webhooks
|
||||
|
||||
| Action | Méthode | URL |
|
||||
|---------------|---------|-----|
|
||||
| Liste webhooks | GET | `/repos/{owner}/{repo}/hooks` |
|
||||
| Créer webhook | POST | `/repos/{owner}/{repo}/hooks` |
|
||||
|
||||
### 3.6 Labels
|
||||
|
||||
| Action | Méthode | URL |
|
||||
|---------------|---------|-----|
|
||||
| Liste des labels | GET | `/repos/{owner}/{repo}/labels` |
|
||||
|
||||
```bash
|
||||
curl -s -H "Authorization: token $GITEA_TOKEN" \
|
||||
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/labels" | jq '.[] | {id, name}'
|
||||
```
|
||||
|
||||
## 4. Résumé des URLs pour P'titsPas
|
||||
|
||||
Remplacer `{owner}` par `jmartin` et `{repo}` par `petitspas` :
|
||||
|
||||
| Ressource | URL |
|
||||
|------------------|-----|
|
||||
| Dépôt | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas` |
|
||||
| Issues | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues` |
|
||||
| Issue #n | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues/{n}` |
|
||||
| Pull requests | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/pulls` |
|
||||
| Branches | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/branches` |
|
||||
| Labels | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/labels` |
|
||||
|
||||
## 5. Documentation officielle
|
||||
|
||||
- Swagger / OpenAPI : https://docs.gitea.com/api
|
||||
- Référence selon la version de Gitea installée (ex. 1.21, 1.25).
|
||||
|
||||
## 6. Dépannage
|
||||
|
||||
- **401 Unauthorized** : vérifier le token et l'en-tête `Authorization: token <TOKEN>`.
|
||||
- **404** : vérifier owner/repo et l'URL (sensible à la casse).
|
||||
- **422 / body invalide** : pour POST/PATCH, envoyer `Content-Type: application/json` et un JSON valide.
|
||||
@@ -0,0 +1,63 @@
|
||||
# Note Backend - Activation du module Gestionnaires (Ticket #92)
|
||||
|
||||
## Problème
|
||||
L'endpoint `GET /api/v1/gestionnaires` renvoie une erreur **404 Not Found**.
|
||||
Cela est dû au fait que le `GestionnairesModule` n'est pas importé dans l'arbre des modules de l'application (via `UserModule` ou `AppModule`).
|
||||
|
||||
## Solution de contournement actuelle (Frontend)
|
||||
Le frontend utilise actuellement l'endpoint générique `/api/v1/users` et filtre les résultats côté client pour ne garder que les utilisateurs ayant le rôle `gestionnaire`.
|
||||
*Fichier concerné : `frontend/lib/services/user_service.dart`*
|
||||
|
||||
## Correctif Backend à appliquer
|
||||
Pour activer proprement l'endpoint dédié, il faut effectuer les modifications suivantes dans le backend :
|
||||
|
||||
### 1. Importer le module dans `UserModule`
|
||||
Fichier : `backend/src/routes/user/user.module.ts`
|
||||
|
||||
Ajouter `GestionnairesModule` dans les imports.
|
||||
|
||||
```typescript
|
||||
import { GestionnairesModule } from './gestionnaires/gestionnaires.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
// ... autres imports
|
||||
GestionnairesModule, // <--- AJOUTER ICI
|
||||
],
|
||||
// ...
|
||||
})
|
||||
export class UserModule { }
|
||||
```
|
||||
|
||||
### 2. Ajouter AuthModule dans `GestionnairesModule`
|
||||
Fichier : `backend/src/routes/user/gestionnaires/gestionnaires.module.ts`
|
||||
|
||||
Le contrôleur utilise `AuthGuard`, qui dépend de `JwtService` fourni par `AuthModule`.
|
||||
|
||||
```typescript
|
||||
import { AuthModule } from 'src/routes/auth/auth.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Users]),
|
||||
AuthModule // <--- AJOUTER ICI
|
||||
],
|
||||
controllers: [GestionnairesController],
|
||||
providers: [GestionnairesService],
|
||||
})
|
||||
export class GestionnairesModule { }
|
||||
```
|
||||
|
||||
## Après application du correctif
|
||||
Une fois ces modifications backend effectuées :
|
||||
1. Redémarrer le serveur backend.
|
||||
2. Modifier le frontend (`frontend/lib/services/user_service.dart`) pour utiliser à nouveau l'endpoint dédié :
|
||||
```dart
|
||||
static Future<List<AppUser>> getGestionnaires() async {
|
||||
final response = await http.get(
|
||||
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.gestionnaires}'),
|
||||
headers: await _headers(),
|
||||
);
|
||||
// ...
|
||||
}
|
||||
```
|
||||
@@ -61,7 +61,8 @@ changement_mdp_obligatoire: true (première connexion)
|
||||
|
||||
| # | Ticket | Description | Effort |
|
||||
|---|--------|-------------|--------|
|
||||
| **#14** | Setup Wizard | Écran configuration initiale (SMTP, app) | 4-5h |
|
||||
| **#12** | Panneau Paramètres / Configuration | Une page avec 3 sections (Email, Personnalisation, Avancé) ; première config = affichage direct + navigation bloquée jusqu'à sauvegarde | 5h |
|
||||
| **#13** | Intégration panneau au dashboard | Onglet Configuration, accès permanent, même interface | 1h |
|
||||
| **#47** | Changement MDP Obligatoire | Modale bloquante après login si flag=true | 1-2h |
|
||||
| **#35** | Création Gestionnaire | Formulaire création gestionnaire | 2-3h |
|
||||
|
||||
@@ -256,11 +257,11 @@ Configuration des appels API avec intercepteurs.
|
||||
## Recommandation de démarrage
|
||||
|
||||
1. **Ticket #47** - Modale changement MDP obligatoire (simple, rapide)
|
||||
2. **Ticket #14** - Setup Wizard (écran configuration initiale)
|
||||
2. **Tickets #12 + #13** - Panneau Paramètres (3 sections) : première config + onglet Configuration dans le dashboard
|
||||
3. **Ticket #35** - Formulaire création gestionnaire
|
||||
|
||||
Ces 3 tickets complètent le **workflow d'initialisation** !
|
||||
Ces tickets complètent le **workflow d'initialisation** !
|
||||
|
||||
---
|
||||
|
||||
*Dernière mise à jour : 27 janvier 2026*
|
||||
*Dernière mise à jour : 9 février 2026*
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
Point tickets frontend (API Gitea) - 27/01/2026
|
||||
================================================
|
||||
|
||||
Issues avec label "frontend" : 20 (ouvertes: 12, fermees: 8)
|
||||
|
||||
Num | Etat | Titre
|
||||
----+--------+--------------------------------------------------------
|
||||
35 | open | [Frontend] Écran Création Gestionnaire
|
||||
36 | closed | [Frontend] Inscription Parent - Étape 1 (Parent 1)
|
||||
37 | closed | [Frontend] Inscription Parent - Étape 2 (Parent 2)
|
||||
38 | closed | [Frontend] Inscription Parent - Étape 3 (Enfants)
|
||||
39 | closed | [Frontend] Inscription Parent - Étapes 4-6 (Finalisatio
|
||||
40 | closed | [Frontend] Inscription AM - Panneau 1 (Identité)
|
||||
41 | closed | [Frontend] Inscription AM - Panneau 2 (Infos pro)
|
||||
42 | closed | [Frontend] Inscription AM - Finalisation
|
||||
43 | open | [Frontend] Écran Création Mot de Passe
|
||||
44 | open | [Frontend] Dashboard Gestionnaire - Structure
|
||||
45 | open | [Frontend] Dashboard Gestionnaire - Liste Parents
|
||||
46 | open | [Frontend] Dashboard Gestionnaire - Liste AM
|
||||
47 | open | [Frontend] Écran Changement MDP Obligatoire
|
||||
48 | open | [Frontend] Gestion Erreurs & Messages
|
||||
49 | open | [Frontend] Écran Gestion Documents Légaux (Admin)
|
||||
50 | open | [Frontend] Affichage dynamique CGU lors inscription
|
||||
51 | open | [Frontend] Écran Logs Admin (optionnel v1.1)
|
||||
54 | open | [Tests] Tests E2E Frontend
|
||||
82 | closed | [Frontend] Adapter �cran Login pour mobile
|
||||
83 | closed | [Frontend] Adapter �cran Choix Inscription pour mobile
|
||||
|
||||
Suivi doc 23_LISTE-TICKETS (Gitea #73,78,79,81,82,83):
|
||||
#73 closed labels=[]
|
||||
#78 closed labels=[]
|
||||
#79 closed labels=[]
|
||||
#81 closed labels=[]
|
||||
#82 closed (écran Login mobile)
|
||||
#83 closed labels=['frontend', 'p3', 'phase-1', 'ux']
|
||||
@@ -0,0 +1,176 @@
|
||||
# Procédure – Utilisation de l’API Gitea
|
||||
|
||||
## 1. Contexte
|
||||
|
||||
- **Instance** : https://git.ptits-pas.fr
|
||||
- **API de base** : `https://git.ptits-pas.fr/api/v1`
|
||||
- **Projet P'titsPas** : dépôt `jmartin/petitspas` (owner = `jmartin`, repo = `petitspas`)
|
||||
|
||||
## 2. Authentification
|
||||
|
||||
### 2.1 Token
|
||||
|
||||
Le token est défini dans l’environnement (ex. `~/.bashrc`) :
|
||||
|
||||
```bash
|
||||
export GITEA_TOKEN="<votre_token>"
|
||||
```
|
||||
|
||||
Pour l’utiliser dans les commandes :
|
||||
|
||||
```bash
|
||||
source ~/.bashrc # ou : . ~/.bashrc
|
||||
# Puis utiliser $GITEA_TOKEN dans les curl
|
||||
```
|
||||
|
||||
### 2.2 En-tête HTTP
|
||||
|
||||
Toutes les requêtes API doivent envoyer le token :
|
||||
|
||||
```bash
|
||||
-H "Authorization: token $GITEA_TOKEN"
|
||||
```
|
||||
|
||||
Exemple :
|
||||
|
||||
```bash
|
||||
curl -s -H "Authorization: token $GITEA_TOKEN" \
|
||||
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas"
|
||||
```
|
||||
|
||||
## 3. Endpoints utiles
|
||||
|
||||
### 3.1 Dépôt (repository)
|
||||
|
||||
| Action | Méthode | URL |
|
||||
|---------------|---------|-----|
|
||||
| Infos dépôt | GET | `/repos/{owner}/{repo}` |
|
||||
| Liste dépôts | GET | `/repos/search?q=petitspas` |
|
||||
|
||||
Exemple – infos du dépôt :
|
||||
|
||||
```bash
|
||||
curl -s -H "Authorization: token $GITEA_TOKEN" \
|
||||
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas" | jq .
|
||||
```
|
||||
|
||||
### 3.2 Issues (tickets)
|
||||
|
||||
| Action | Méthode | URL |
|
||||
|------------------|---------|-----|
|
||||
| Liste des issues | GET | `/repos/{owner}/{repo}/issues` |
|
||||
| Détail d’une issue | GET | `/repos/{owner}/{repo}/issues/{index}` |
|
||||
| Créer une issue | POST | `/repos/{owner}/{repo}/issues` |
|
||||
| Modifier une issue | PATCH | `/repos/{owner}/{repo}/issues/{index}` |
|
||||
| Fermer une issue | PATCH | (même URL, `state: "closed"`) |
|
||||
|
||||
**Paramètres GET utiles pour la liste :**
|
||||
|
||||
- `state` : `open` ou `closed`
|
||||
- `labels` : filtre par label (ex. `frontend`)
|
||||
- `page`, `limit` : pagination
|
||||
|
||||
Exemples :
|
||||
|
||||
```bash
|
||||
# Toutes les issues ouvertes
|
||||
curl -s -H "Authorization: token $GITEA_TOKEN" \
|
||||
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues?state=open" | jq .
|
||||
|
||||
# Issues ouvertes avec label "frontend"
|
||||
curl -s -H "Authorization: token $GITEA_TOKEN" \
|
||||
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues?state=open" | \
|
||||
jq '.[] | select(.labels[].name == "frontend") | {number, title, state}'
|
||||
|
||||
# Détail de l’issue #47
|
||||
curl -s -H "Authorization: token $GITEA_TOKEN" \
|
||||
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues/47" | jq .
|
||||
|
||||
# Fermer l’issue #31
|
||||
curl -s -X PATCH -H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"state":"closed"}' \
|
||||
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues/31"
|
||||
|
||||
# Créer une issue
|
||||
curl -s -X POST -H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"title":"Titre du ticket","body":"Description","labels":[1]}' \
|
||||
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues"
|
||||
```
|
||||
|
||||
### 3.3 Pull requests
|
||||
|
||||
| Action | Méthode | URL |
|
||||
|---------------|---------|-----|
|
||||
| Liste des PR | GET | `/repos/{owner}/{repo}/pulls` |
|
||||
| Détail d’une PR | GET | `/repos/{owner}/{repo}/pulls/{index}` |
|
||||
| Créer une PR | POST | `/repos/{owner}/{repo}/pulls` |
|
||||
| Fusionner une PR | POST | `/repos/{owner}/{repo}/pulls/{index}/merge` |
|
||||
|
||||
Exemples :
|
||||
|
||||
```bash
|
||||
# Liste des PR ouvertes
|
||||
curl -s -H "Authorization: token $GITEA_TOKEN" \
|
||||
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/pulls?state=open" | jq .
|
||||
|
||||
# Créer une PR (head = branche source, base = branche cible)
|
||||
curl -s -X POST -H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"head":"develop","base":"master","title":"Titre de la PR"}' \
|
||||
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/pulls"
|
||||
```
|
||||
|
||||
### 3.4 Branches
|
||||
|
||||
| Action | Méthode | URL |
|
||||
|---------------|---------|-----|
|
||||
| Liste des branches | GET | `/repos/{owner}/{repo}/branches` |
|
||||
|
||||
```bash
|
||||
curl -s -H "Authorization: token $GITEA_TOKEN" \
|
||||
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/branches" | jq '.[].name'
|
||||
```
|
||||
|
||||
### 3.5 Webhooks
|
||||
|
||||
| Action | Méthode | URL |
|
||||
|---------------|---------|-----|
|
||||
| Liste webhooks | GET | `/repos/{owner}/{repo}/hooks` |
|
||||
| Créer webhook | POST | `/repos/{owner}/{repo}/hooks` |
|
||||
|
||||
### 3.6 Labels
|
||||
|
||||
| Action | Méthode | URL |
|
||||
|---------------|---------|-----|
|
||||
| Liste des labels | GET | `/repos/{owner}/{repo}/labels` |
|
||||
|
||||
```bash
|
||||
curl -s -H "Authorization: token $GITEA_TOKEN" \
|
||||
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/labels" | jq '.[] | {id, name}'
|
||||
```
|
||||
|
||||
## 4. Résumé des URLs pour P'titsPas
|
||||
|
||||
Remplacer `{owner}` par `jmartin` et `{repo}` par `petitspas` :
|
||||
|
||||
| Ressource | URL |
|
||||
|------------------|-----|
|
||||
| Dépôt | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas` |
|
||||
| Issues | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues` |
|
||||
| Issue #n | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues/{n}` |
|
||||
| Pull requests | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/pulls` |
|
||||
| Branches | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/branches` |
|
||||
| Labels | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/labels` |
|
||||
|
||||
## 5. Documentation officielle
|
||||
|
||||
- Swagger / OpenAPI : https://docs.gitea.com/api
|
||||
- Référence selon la version de Gitea installée (ex. 1.21, 1.25).
|
||||
|
||||
## 6. Dépannage
|
||||
|
||||
- **401 Unauthorized** : vérifier le token et l’en-tête `Authorization: token <TOKEN>`.
|
||||
- **404** : vérifier owner/repo et l’URL (sensible à la casse).
|
||||
- **422 / body invalide** : pour POST/PATCH, envoyer `Content-Type: application/json` et un JSON valide.
|
||||
@@ -0,0 +1,115 @@
|
||||
# Statut de l'application P'titsPas
|
||||
|
||||
**Date du point** : 8 février 2026
|
||||
|
||||
---
|
||||
|
||||
## 1. Environnement de production
|
||||
|
||||
| Élément | Statut | Détail |
|
||||
|--------|--------|--------|
|
||||
| **URL** | OK | https://app.ptits-pas.fr |
|
||||
| **Frontend** | 200 | Flutter Web, Nginx |
|
||||
| **API** | 200 | NestJS, préfixe `/api/v1` |
|
||||
| **Base de données** | OK | PostgreSQL 17 |
|
||||
| **PgAdmin** | OK | https://app.ptits-pas.fr/pgadmin |
|
||||
|
||||
### Conteneurs Docker
|
||||
|
||||
| Service | Image | État |
|
||||
|---------|--------|------|
|
||||
| ptitspas-frontend | ptitspas-app-frontend | Up (recréé récemment) |
|
||||
| ptitspas-backend | ptitspas-app-backend | Up ~26h |
|
||||
| ptitspas-postgres | postgres:17 | Up ~28h |
|
||||
| ptitspas-pgadmin | dpage/pgadmin4 | Up ~28h |
|
||||
|
||||
---
|
||||
|
||||
## 2. Dépôt Git
|
||||
|
||||
- **Branche déployée** : `master`
|
||||
- **Derniers commits** :
|
||||
- `10bf255` – fix(ui): renforcer ombre boutons Parents/AM sur mobile
|
||||
- `678f421` – docs: ticket #82 fermé (écran Login mobile)
|
||||
- `5295e8e` – Merge develop: login mobile, formulaire sous slogan par ratio
|
||||
- `6bf0932` – docs: Index, doc API Gitea, script fermeture issue
|
||||
- `2f1740b` – docs: ticket #83 RegisterChoiceScreen Mobile (terminé)
|
||||
|
||||
- **Branches actives** : `master`, `develop`, diverses `feature/*` (inscription, config, documents légaux, etc.)
|
||||
|
||||
---
|
||||
|
||||
## 3. Déploiement (hook Gitea)
|
||||
|
||||
| Élément | Statut |
|
||||
|--------|--------|
|
||||
| **Webhook** | Opérationnel (`hooks.ptits-pas.fr/hooks/petitspas-deploy`) |
|
||||
| **Déclencheur** | Push sur `master`, dépôt `petitspas` |
|
||||
| **Script** | Monté depuis l’hôte (verrou + sans Prisma) |
|
||||
| **Dernier déploiement** | 08/02/2026 18:18:26 – Succès |
|
||||
|
||||
Un seul déploiement à la fois (verrou) ; plus d’étape Prisma dans le script.
|
||||
|
||||
---
|
||||
|
||||
## 4. Fonctionnalités livrées
|
||||
|
||||
### Backend (API)
|
||||
|
||||
- Auth : login, refresh, profil, **changement MDP obligatoire** (first login)
|
||||
- Configuration : setup status, bulk, test SMTP, catégories
|
||||
- Documents légaux : actifs, versions, upload, activation, téléchargement
|
||||
- Inscription : parents (workflow complet), enfants (CRUD)
|
||||
- Compte super_admin par défaut (seed BDD) : `admin@ptits-pas.fr` / `4dm1n1strateur`
|
||||
|
||||
### Frontend
|
||||
|
||||
- **Formulaires d’inscription** : compatibles **desktop et mobile**
|
||||
- Choix d’inscription (Parents / Assistante maternelle) – responsive
|
||||
- Inscription Parent : étapes 1 à 5 (infos parent 1 & 2, enfants, présentation, CGU, récap)
|
||||
- Inscription AM : étapes 1 à 4 (identité, pro, présentation, récap)
|
||||
- **Login** : écran adapté mobile (formulaire sous slogan selon ratio)
|
||||
- Modale **changement de mot de passe obligatoire** après première connexion si `changement_mdp_obligatoire`
|
||||
- CORS configuré (localhost + prod)
|
||||
|
||||
### Base de données
|
||||
|
||||
- Schéma database-first (BDD.sql)
|
||||
- Tables : utilisateurs, configuration, documents_legaux, acceptations_documents, enfants, etc.
|
||||
- Champs tokens création MDP, genre enfants, configuration système
|
||||
|
||||
---
|
||||
|
||||
## 5. Tickets / Priorités (résumé)
|
||||
|
||||
- **Liste détaillée** : `docs/23_LISTE-TICKETS.md`
|
||||
- **Récent fermé** : #82 (Login mobile), #83 (RegisterChoiceScreen mobile), #73, #78, #79, #81
|
||||
- **P0 (BDD)** : quelques amendements ouverts (champs CDC, présentation dossier, etc.)
|
||||
- **P1** : configuration système (panneau Paramètres, 3 sections, première config + accès permanent)
|
||||
- **P2/P3** : backend métier et frontend (dashboards, écrans création MDP, etc.)
|
||||
|
||||
---
|
||||
|
||||
## 6. Documentation utile
|
||||
|
||||
| Fichier | Usage |
|
||||
|---------|--------|
|
||||
| `00_INDEX.md` | Index de la doc |
|
||||
| `01_CAHIER-DES-CHARGES.md` | CDC v1.3 |
|
||||
| `11_API.md` | Endpoints API |
|
||||
| `20_WORKFLOW-CREATION-COMPTE.md` | Workflow création compte |
|
||||
| `23_LISTE-TICKETS.md` | Liste des tickets |
|
||||
| `BRIEFING-FRONTEND.md` | Brief frontend, accès Git, tickets prioritaires |
|
||||
| `PROCEDURE-API-GITEA.md` | Utilisation API Gitea (issues, PR, token) |
|
||||
|
||||
---
|
||||
|
||||
## 7. Synthèse
|
||||
|
||||
L’application est **en production** sur https://app.ptits-pas.fr avec :
|
||||
|
||||
- Frontend et API accessibles et répondant en 200.
|
||||
- Déploiement automatique sur push `master` avec script à jour (verrou, sans Prisma).
|
||||
- Formulaires d’inscription (Parents et AM) **responsive desktop et mobile**.
|
||||
- Login et changement de mot de passe obligatoire opérationnels.
|
||||
- Prochaines priorités : P0 BDD si besoin, P1 panneau Paramètres / Configuration (tickets #12, #13), puis dashboards et workflows métier (P2/P3).
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 304 KiB |
@@ -19,6 +19,8 @@ import '../screens/auth/am_register_step2_screen.dart';
|
||||
import '../screens/auth/am_register_step3_screen.dart';
|
||||
import '../screens/auth/am_register_step4_screen.dart';
|
||||
import '../screens/home/home_screen.dart';
|
||||
import '../screens/administrateurs/admin_dashboardScreen.dart';
|
||||
import '../screens/home/parent_screen/ParentDashboardScreen.dart';
|
||||
import '../screens/unknown_screen.dart';
|
||||
|
||||
// --- Provider Instances ---
|
||||
@@ -47,6 +49,18 @@ class AppRouter {
|
||||
path: '/home',
|
||||
builder: (BuildContext context, GoRouterState state) => const HomeScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/admin-dashboard',
|
||||
builder: (BuildContext context, GoRouterState state) => const AdminDashboardScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/parent-dashboard',
|
||||
builder: (BuildContext context, GoRouterState state) => const ParentDashboardScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/am-dashboard',
|
||||
builder: (BuildContext context, GoRouterState state) => const HomeScreen(),
|
||||
),
|
||||
|
||||
// --- Parent Registration Flow ---
|
||||
ShellRoute(
|
||||
|
||||
@@ -6,7 +6,7 @@ class Env {
|
||||
);
|
||||
|
||||
// Construit une URL vers l'API v1 à partir d'un chemin (commençant par '/')
|
||||
static String apiV1(String path) => "${apiBaseUrl}/api/v1$path";
|
||||
static String apiV1(String path) => '$apiBaseUrl/api/v1$path';
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import 'package:p_tits_pas/models/user.dart';
|
||||
|
||||
class AssistanteMaternelleModel {
|
||||
final AppUser user;
|
||||
final String? approvalNumber;
|
||||
final String? residenceCity;
|
||||
final int? maxChildren;
|
||||
final int? placesAvailable;
|
||||
|
||||
AssistanteMaternelleModel({
|
||||
required this.user,
|
||||
this.approvalNumber,
|
||||
this.residenceCity,
|
||||
this.maxChildren,
|
||||
this.placesAvailable,
|
||||
});
|
||||
|
||||
factory AssistanteMaternelleModel.fromJson(Map<String, dynamic> json) {
|
||||
final userJson = json['user'] ?? json;
|
||||
final user = AppUser.fromJson(userJson);
|
||||
|
||||
return AssistanteMaternelleModel(
|
||||
user: user,
|
||||
approvalNumber: json['numero_agrement'] as String?,
|
||||
residenceCity: json['ville_residence'] as String?,
|
||||
maxChildren: json['nb_max_enfants'] as int?,
|
||||
placesAvailable: json['place_disponible'] as int?,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class NannyRegistrationData extends ChangeNotifier {
|
||||
// Step 1: Identity Info
|
||||
String firstName = '';
|
||||
String lastName = '';
|
||||
String streetAddress = ''; // Nouveau pour N° et Rue
|
||||
String postalCode = ''; // Nouveau
|
||||
String city = ''; // Nouveau
|
||||
String phone = '';
|
||||
String email = '';
|
||||
String password = '';
|
||||
// String? photoPath; // Déplacé ou géré à l'étape 2
|
||||
// bool photoConsent = false; // Déplacé ou géré à l'étape 2
|
||||
|
||||
// Step 2: Professional Info
|
||||
String? photoPath; // Ajouté pour l'étape 2
|
||||
bool photoConsent = false; // Ajouté pour l'étape 2
|
||||
DateTime? dateOfBirth;
|
||||
String birthCity = ''; // Nouveau
|
||||
String birthCountry = ''; // Nouveau
|
||||
// String placeOfBirth = ''; // Remplacé par birthCity et birthCountry
|
||||
String nir = ''; // Numéro de Sécurité Sociale
|
||||
String agrementNumber = ''; // Numéro d'agrément
|
||||
int? capacity; // Number of children the nanny can look after
|
||||
|
||||
// Step 3: Presentation & CGU
|
||||
String presentationText = '';
|
||||
bool cguAccepted = false;
|
||||
|
||||
// --- Methods to update data and notify listeners ---
|
||||
|
||||
void updateIdentityInfo({
|
||||
String? firstName,
|
||||
String? lastName,
|
||||
String? streetAddress, // Modifié
|
||||
String? postalCode, // Nouveau
|
||||
String? city, // Nouveau
|
||||
String? phone,
|
||||
String? email,
|
||||
String? password,
|
||||
}) {
|
||||
this.firstName = firstName ?? this.firstName;
|
||||
this.lastName = lastName ?? this.lastName;
|
||||
this.streetAddress = streetAddress ?? this.streetAddress; // Modifié
|
||||
this.postalCode = postalCode ?? this.postalCode; // Nouveau
|
||||
this.city = city ?? this.city; // Nouveau
|
||||
this.phone = phone ?? this.phone;
|
||||
this.email = email ?? this.email;
|
||||
this.password = password ?? this.password;
|
||||
// if (photoPath != null || this.photoPath != null) { // Supprimé de l'étape 1
|
||||
// this.photoPath = photoPath;
|
||||
// }
|
||||
// this.photoConsent = photoConsent ?? this.photoConsent; // Supprimé de l'étape 1
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void updateProfessionalInfo({
|
||||
String? photoPath,
|
||||
bool? photoConsent,
|
||||
DateTime? dateOfBirth,
|
||||
String? birthCity, // Nouveau
|
||||
String? birthCountry, // Nouveau
|
||||
// String? placeOfBirth, // Remplacé
|
||||
String? nir,
|
||||
String? agrementNumber,
|
||||
int? capacity,
|
||||
}) {
|
||||
// Allow setting photoPath to null explicitly
|
||||
if (photoPath != null || this.photoPath != null) {
|
||||
this.photoPath = photoPath;
|
||||
}
|
||||
this.photoConsent = photoConsent ?? this.photoConsent;
|
||||
this.dateOfBirth = dateOfBirth ?? this.dateOfBirth;
|
||||
this.birthCity = birthCity ?? this.birthCity; // Nouveau
|
||||
this.birthCountry = birthCountry ?? this.birthCountry; // Nouveau
|
||||
// this.placeOfBirth = placeOfBirth ?? this.placeOfBirth; // Remplacé
|
||||
this.nir = nir ?? this.nir;
|
||||
this.agrementNumber = agrementNumber ?? this.agrementNumber;
|
||||
this.capacity = capacity ?? this.capacity;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void updatePresentationAndCgu({
|
||||
String? presentationText,
|
||||
bool? cguAccepted,
|
||||
}) {
|
||||
this.presentationText = presentationText ?? this.presentationText;
|
||||
this.cguAccepted = cguAccepted ?? this.cguAccepted;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// --- Getters for validation or display ---
|
||||
bool get isStep1Complete =>
|
||||
firstName.isNotEmpty &&
|
||||
lastName.isNotEmpty &&
|
||||
streetAddress.isNotEmpty && // Modifié
|
||||
postalCode.isNotEmpty && // Nouveau
|
||||
city.isNotEmpty && // Nouveau
|
||||
phone.isNotEmpty &&
|
||||
email.isNotEmpty &&
|
||||
password.isNotEmpty;
|
||||
|
||||
bool get isStep2Complete =>
|
||||
// photoConsent is mandatory if a photo is system-required, otherwise optional.
|
||||
// For now, let's assume if photoPath is present, consent should ideally be true.
|
||||
// Or, make consent always mandatory if photo section exists.
|
||||
// Based on new mockup, photo is present, so consent might be implicitly or explicitly needed.
|
||||
(photoPath != null ? photoConsent == true : true) && // Ajuster selon la logique de consentement désirée
|
||||
dateOfBirth != null &&
|
||||
birthCity.isNotEmpty &&
|
||||
birthCountry.isNotEmpty &&
|
||||
nir.isNotEmpty && // Basic check, could add validation
|
||||
agrementNumber.isNotEmpty &&
|
||||
capacity != null && capacity! > 0;
|
||||
|
||||
bool get isStep3Complete =>
|
||||
// presentationText is optional as per CDC (message au gestionnaire)
|
||||
cguAccepted;
|
||||
|
||||
bool get isRegistrationComplete =>
|
||||
isStep1Complete && isStep2Complete && isStep3Complete;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'NannyRegistrationData('
|
||||
'firstName: $firstName, lastName: $lastName, '
|
||||
'streetAddress: $streetAddress, postalCode: $postalCode, city: $city, '
|
||||
'phone: $phone, email: $email, '
|
||||
// 'photoPath: $photoPath, photoConsent: $photoConsent, ' // Commenté car déplacé/modifié
|
||||
'dateOfBirth: $dateOfBirth, birthCity: $birthCity, birthCountry: $birthCountry, '
|
||||
'nir: $nir, agrementNumber: $agrementNumber, capacity: $capacity, '
|
||||
'photoPath (step2): $photoPath, photoConsent (step2): $photoConsent, '
|
||||
'presentationText: $presentationText, cguAccepted: $cguAccepted)';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:p_tits_pas/models/user.dart';
|
||||
|
||||
class ParentModel {
|
||||
final AppUser user;
|
||||
final int childrenCount;
|
||||
|
||||
ParentModel({required this.user, this.childrenCount = 0});
|
||||
|
||||
factory ParentModel.fromJson(Map<String, dynamic> json) {
|
||||
final userJson = json['user'] ?? json;
|
||||
final user = AppUser.fromJson(userJson);
|
||||
final children = json['parentChildren'] as List?;
|
||||
return ParentModel(
|
||||
user: user,
|
||||
childrenCount: children?.length ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,14 @@ class AppUser {
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final bool changementMdpObligatoire;
|
||||
final String? nom;
|
||||
final String? prenom;
|
||||
final String? statut;
|
||||
final String? telephone;
|
||||
final String? photoUrl;
|
||||
final String? adresse;
|
||||
final String? ville;
|
||||
final String? codePostal;
|
||||
|
||||
AppUser({
|
||||
required this.id,
|
||||
@@ -13,6 +21,14 @@ class AppUser {
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.changementMdpObligatoire = false,
|
||||
this.nom,
|
||||
this.prenom,
|
||||
this.statut,
|
||||
this.telephone,
|
||||
this.photoUrl,
|
||||
this.adresse,
|
||||
this.ville,
|
||||
this.codePostal,
|
||||
});
|
||||
|
||||
factory AppUser.fromJson(Map<String, dynamic> json) {
|
||||
@@ -20,9 +36,26 @@ class AppUser {
|
||||
id: json['id'] as String,
|
||||
email: json['email'] as String,
|
||||
role: json['role'] as String,
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
||||
changementMdpObligatoire: json['changement_mdp_obligatoire'] as bool? ?? false,
|
||||
createdAt: json['cree_le'] != null
|
||||
? DateTime.parse(json['cree_le'] as String)
|
||||
: (json['createdAt'] != null
|
||||
? DateTime.parse(json['createdAt'] as String)
|
||||
: DateTime.now()),
|
||||
updatedAt: json['modifie_le'] != null
|
||||
? DateTime.parse(json['modifie_le'] as String)
|
||||
: (json['updatedAt'] != null
|
||||
? DateTime.parse(json['updatedAt'] as String)
|
||||
: DateTime.now()),
|
||||
changementMdpObligatoire:
|
||||
json['changement_mdp_obligatoire'] as bool? ?? false,
|
||||
nom: json['nom'] as String?,
|
||||
prenom: json['prenom'] as String?,
|
||||
statut: json['statut'] as String?,
|
||||
telephone: json['telephone'] as String?,
|
||||
photoUrl: json['photo_url'] as String?,
|
||||
adresse: json['adresse'] as String?,
|
||||
ville: json['ville'] as String?,
|
||||
codePostal: json['code_postal'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -34,6 +67,16 @@ class AppUser {
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
'updatedAt': updatedAt.toIso8601String(),
|
||||
'changement_mdp_obligatoire': changementMdpObligatoire,
|
||||
'nom': nom,
|
||||
'prenom': prenom,
|
||||
'statut': statut,
|
||||
'telephone': telephone,
|
||||
'photo_url': photoUrl,
|
||||
'adresse': adresse,
|
||||
'ville': ville,
|
||||
'code_postal': codePostal,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
String get fullName => '${prenom ?? ''} ${nom ?? ''}'.trim();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:p_tits_pas/services/configuration_service.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/admin/admin_management_widget.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/parametres_panel.dart';
|
||||
import 'package:p_tits_pas/widgets/app_footer.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/dashboard_admin.dart';
|
||||
|
||||
@@ -9,20 +12,55 @@ class AdminDashboardScreen extends StatefulWidget {
|
||||
const AdminDashboardScreen({super.key});
|
||||
|
||||
@override
|
||||
_AdminDashboardScreenState createState() => _AdminDashboardScreenState();
|
||||
State<AdminDashboardScreen> createState() => _AdminDashboardScreenState();
|
||||
}
|
||||
|
||||
class _AdminDashboardScreenState extends State<AdminDashboardScreen> {
|
||||
int selectedIndex = 0;
|
||||
bool? _setupCompleted;
|
||||
int mainTabIndex = 0;
|
||||
int subIndex = 0;
|
||||
|
||||
void onTabChange(int index) {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadSetupStatus();
|
||||
}
|
||||
|
||||
Future<void> _loadSetupStatus() async {
|
||||
try {
|
||||
final completed = await ConfigurationService.getSetupStatus();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_setupCompleted = completed;
|
||||
if (!completed) mainTabIndex = 1;
|
||||
});
|
||||
} catch (e) {
|
||||
if (mounted) setState(() {
|
||||
_setupCompleted = false;
|
||||
mainTabIndex = 1;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void onMainTabChange(int index) {
|
||||
setState(() {
|
||||
selectedIndex = index;
|
||||
mainTabIndex = index;
|
||||
});
|
||||
}
|
||||
|
||||
void onSubTabChange(int index) {
|
||||
setState(() {
|
||||
subIndex = index;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_setupCompleted == null) {
|
||||
return const Scaffold(
|
||||
body: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
return Scaffold(
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(60.0),
|
||||
@@ -33,13 +71,19 @@ class _AdminDashboardScreenState extends State<AdminDashboardScreen> {
|
||||
),
|
||||
),
|
||||
child: DashboardAppBarAdmin(
|
||||
selectedIndex: selectedIndex,
|
||||
onTabChange: onTabChange,
|
||||
selectedIndex: mainTabIndex,
|
||||
onTabChange: onMainTabChange,
|
||||
setupCompleted: _setupCompleted!,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
if (mainTabIndex == 0)
|
||||
DashboardUserManagementSubBar(
|
||||
selectedSubIndex: subIndex,
|
||||
onSubTabChange: onSubTabChange,
|
||||
),
|
||||
Expanded(
|
||||
child: _getBody(),
|
||||
),
|
||||
@@ -50,17 +94,20 @@ class _AdminDashboardScreenState extends State<AdminDashboardScreen> {
|
||||
}
|
||||
|
||||
Widget _getBody() {
|
||||
switch (selectedIndex) {
|
||||
if (mainTabIndex == 1) {
|
||||
return ParametresPanel(redirectToLoginAfterSave: !_setupCompleted!);
|
||||
}
|
||||
switch (subIndex) {
|
||||
case 0:
|
||||
return const GestionnaireManagementWidget();
|
||||
return const GestionnaireManagementWidget();
|
||||
case 1:
|
||||
return const ParentManagementWidget();
|
||||
case 2:
|
||||
return const AssistanteMaternelleManagementWidget();
|
||||
case 3:
|
||||
return const Center(child: Text("👨💼 Administrateurs"));
|
||||
return const AdminManagementWidget();
|
||||
default:
|
||||
return const Center(child: Text("Page non trouvée"));
|
||||
return const Center(child: Text('Page non trouvée'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:p_tits_pas/services/bug_report_service.dart';
|
||||
@@ -17,7 +16,7 @@ class LoginScreen extends StatefulWidget {
|
||||
State<LoginScreen> createState() => _LoginPageState();
|
||||
}
|
||||
|
||||
class _LoginPageState extends State<LoginScreen> {
|
||||
class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _emailController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
@@ -25,13 +24,28 @@ class _LoginPageState extends State<LoginScreen> {
|
||||
bool _isLoading = false;
|
||||
String? _errorMessage;
|
||||
|
||||
static const double _mobileBreakpoint = 900.0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_emailController.dispose();
|
||||
_passwordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeMetrics() {
|
||||
super.didChangeMetrics();
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
String? _validateEmail(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Veuillez entrer votre email';
|
||||
@@ -102,36 +116,42 @@ class _LoginPageState extends State<LoginScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Redirige l'utilisateur selon son rôle
|
||||
/// Redirige l'utilisateur selon son rôle (GoRouter : context.go).
|
||||
void _redirectUserByRole(String role) {
|
||||
setState(() => _isLoading = false);
|
||||
switch (role.toLowerCase()) {
|
||||
case 'super_admin':
|
||||
case 'administrateur':
|
||||
case 'gestionnaire':
|
||||
Navigator.pushReplacementNamed(context, '/admin-dashboard');
|
||||
context.go('/admin-dashboard');
|
||||
break;
|
||||
case 'parent':
|
||||
Navigator.pushReplacementNamed(context, '/parent-dashboard');
|
||||
context.go('/parent-dashboard');
|
||||
break;
|
||||
case 'assistante_maternelle':
|
||||
Navigator.pushReplacementNamed(context, '/am-dashboard');
|
||||
context.go('/am-dashboard');
|
||||
break;
|
||||
default:
|
||||
Navigator.pushReplacementNamed(context, '/home');
|
||||
context.go('/home');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isMobile = MediaQuery.of(context).size.width < _mobileBreakpoint;
|
||||
if (isMobile) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
body: _buildMobileLayout(context),
|
||||
);
|
||||
}
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
body: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
// Version desktop (web)
|
||||
if (kIsWeb) {
|
||||
final w = constraints.maxWidth;
|
||||
final h = constraints.maxHeight;
|
||||
|
||||
return FutureBuilder(
|
||||
final w = constraints.maxWidth;
|
||||
final h = constraints.maxHeight;
|
||||
return FutureBuilder(
|
||||
future: _getImageDimensions(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
@@ -289,18 +309,19 @@ class _LoginPageState extends State<LoginScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
// Pied de page
|
||||
// Pied de page (Wrap pour éviter overflow sur petite largeur)
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
decoration: BoxDecoration(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
_FooterLink(
|
||||
text: 'Contact support',
|
||||
@@ -349,17 +370,207 @@ class _LoginPageState extends State<LoginScreen> {
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Version mobile (à implémenter)
|
||||
return const Center(
|
||||
child: Text('Version mobile à implémenter'),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Dimensions de river_logo_mobile.png (à mettre à jour si l'asset change).
|
||||
static const int _riverLogoMobileWidth = 600;
|
||||
static const int _riverLogoMobileHeight = 1080;
|
||||
/// Fraction de la hauteur de l'image où se termine visuellement le slogan (0 = haut, 1 = bas).
|
||||
static const double _sloganEndFraction = 0.42;
|
||||
static const double _gapBelowSlogan = 12.0;
|
||||
|
||||
Widget _buildMobileLayout(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final h = constraints.maxHeight;
|
||||
final w = constraints.maxWidth;
|
||||
final imageAspectRatio = _riverLogoMobileHeight / _riverLogoMobileWidth;
|
||||
final formTop = w * imageAspectRatio * _sloganEndFraction + _gapBelowSlogan;
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Image.asset(
|
||||
'assets/images/paper2.png',
|
||||
fit: BoxFit.cover,
|
||||
repeat: ImageRepeat.repeat,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: h * 1.2,
|
||||
child: OverflowBox(
|
||||
alignment: Alignment.topCenter,
|
||||
minWidth: w,
|
||||
maxWidth: w,
|
||||
minHeight: 0,
|
||||
maxHeight: h * 2.5,
|
||||
child: Image.asset(
|
||||
'assets/images/river_logo_mobile.png',
|
||||
width: w,
|
||||
fit: BoxFit.fitWidth,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: formTop,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 400),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
CustomAppTextField(
|
||||
controller: _emailController,
|
||||
labelText: 'Email',
|
||||
showLabel: false,
|
||||
hintText: 'Votre adresse email',
|
||||
validator: _validateEmail,
|
||||
style: CustomAppTextFieldStyle.lavande,
|
||||
fieldHeight: 48,
|
||||
fieldWidth: double.infinity,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
CustomAppTextField(
|
||||
controller: _passwordController,
|
||||
labelText: 'Mot de passe',
|
||||
showLabel: false,
|
||||
hintText: 'Votre mot de passe',
|
||||
obscureText: true,
|
||||
validator: _validatePassword,
|
||||
style: CustomAppTextFieldStyle.jaune,
|
||||
fieldHeight: 48,
|
||||
fieldWidth: double.infinity,
|
||||
),
|
||||
if (_errorMessage != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: Colors.red.shade300),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.error_outline, color: Colors.red.shade700, size: 20),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_errorMessage!,
|
||||
style: GoogleFonts.merienda(fontSize: 12, color: Colors.red.shade700),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
_isLoading
|
||||
? const CircularProgressIndicator()
|
||||
: ImageButton(
|
||||
bg: 'assets/images/bg_green.png',
|
||||
width: double.infinity,
|
||||
height: 44,
|
||||
text: 'Se connecter',
|
||||
textColor: const Color(0xFF2D6A4F),
|
||||
onPressed: _handleLogin,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextButton(
|
||||
onPressed: () { /* TODO */ },
|
||||
child: Text(
|
||||
'Mot de passe oublié ?',
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: 14,
|
||||
color: const Color(0xFF2D6A4F),
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => context.go('/register-choice'),
|
||||
child: Text(
|
||||
'Créer un compte',
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: 16,
|
||||
color: const Color(0xFF2D6A4F),
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12, top: 8),
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
runSpacing: 6,
|
||||
spacing: 4,
|
||||
children: [
|
||||
_FooterLink(
|
||||
text: 'Contact support',
|
||||
fontSize: 11,
|
||||
onTap: () async {
|
||||
final uri = Uri(scheme: 'mailto', path: 'support@supernounou.local');
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri);
|
||||
} else if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Impossible d\'ouvrir le client mail', style: GoogleFonts.merienda())),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
_FooterLink(
|
||||
text: 'Signaler un bug',
|
||||
fontSize: 11,
|
||||
onTap: () => _showBugReportDialog(context),
|
||||
),
|
||||
_FooterLink(
|
||||
text: 'Mentions légales',
|
||||
fontSize: 11,
|
||||
onTap: () => context.go('/legal'),
|
||||
),
|
||||
_FooterLink(
|
||||
text: 'Politique de confidentialité',
|
||||
fontSize: 11,
|
||||
onTap: () => context.go('/privacy'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showBugReportDialog(BuildContext context) {
|
||||
final TextEditingController controller = TextEditingController();
|
||||
|
||||
@@ -471,10 +682,12 @@ class ImageDimensions {
|
||||
class _FooterLink extends StatelessWidget {
|
||||
final String text;
|
||||
final VoidCallback onTap;
|
||||
final double fontSize;
|
||||
|
||||
const _FooterLink({
|
||||
required this.text,
|
||||
required this.onTap,
|
||||
this.fontSize = 14.0,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -482,11 +695,11 @@ class _FooterLink extends StatelessWidget {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
padding: EdgeInsets.symmetric(horizontal: fontSize > 12 ? 8.0 : 4.0),
|
||||
child: Text(
|
||||
text,
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: 14,
|
||||
fontSize: fontSize,
|
||||
color: Colors.black87,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class NannyRegisterConfirmationScreen extends StatelessWidget {
|
||||
const NannyRegisterConfirmationScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Inscription Soumise'),
|
||||
automaticallyImplyLeading: false, // Remove back button
|
||||
),
|
||||
body: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.check_circle_outline, color: Colors.green, size: 80),
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
'Votre demande d\'inscription a été soumise avec succès !',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
const Text(
|
||||
'Votre compte est en attente de validation par un gestionnaire. Vous recevrez une notification par e-mail une fois votre compte activé.',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
// Navigate back to the login screen
|
||||
context.go('/login');
|
||||
},
|
||||
child: const Text('Retour à la connexion'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@ class ApiConfig {
|
||||
// Auth endpoints
|
||||
static const String login = '/auth/login';
|
||||
static const String register = '/auth/register';
|
||||
static const String registerParent = '/auth/register/parent';
|
||||
static const String registerAM = '/auth/register/am';
|
||||
static const String refreshToken = '/auth/refresh';
|
||||
static const String authMe = '/auth/me';
|
||||
static const String changePasswordRequired = '/auth/change-password-required';
|
||||
@@ -13,6 +15,16 @@ class ApiConfig {
|
||||
static const String users = '/users';
|
||||
static const String userProfile = '/users/profile';
|
||||
static const String userChildren = '/users/children';
|
||||
static const String gestionnaires = '/gestionnaires';
|
||||
static const String parents = '/parents';
|
||||
static const String assistantesMaternelles = '/assistantes-maternelles';
|
||||
|
||||
// Configuration (admin)
|
||||
static const String configuration = '/configuration';
|
||||
static const String configurationSetupStatus = '/configuration/setup/status';
|
||||
static const String configurationSetupComplete = '/configuration/setup/complete';
|
||||
static const String configurationTestSmtp = '/configuration/test-smtp';
|
||||
static const String configurationBulk = '/configuration/bulk';
|
||||
|
||||
// Dashboard endpoints
|
||||
static const String dashboard = '/dashboard';
|
||||
|
||||
@@ -23,13 +23,15 @@ class AuthService {
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
final data = jsonDecode(response.body);
|
||||
|
||||
// Stocker les tokens
|
||||
await TokenService.saveToken(data['accessToken']);
|
||||
await TokenService.saveRefreshToken(data['refreshToken']);
|
||||
|
||||
// Récupérer le profil utilisateur pour avoir toutes les infos
|
||||
final user = await _fetchUserProfile(data['accessToken']);
|
||||
// API renvoie access_token / refresh_token (snake_case)
|
||||
final accessToken = data['access_token'] as String? ?? data['accessToken'] as String?;
|
||||
final refreshToken = data['refresh_token'] as String? ?? data['refreshToken'] as String?;
|
||||
if (accessToken == null) throw Exception('Token absent dans la réponse serveur');
|
||||
|
||||
await TokenService.saveToken(accessToken);
|
||||
await TokenService.saveRefreshToken(refreshToken ?? '');
|
||||
|
||||
final user = await _fetchUserProfile(accessToken);
|
||||
|
||||
// Stocker l'utilisateur en cache
|
||||
await _saveCurrentUser(user);
|
||||
@@ -80,8 +82,9 @@ class AuthService {
|
||||
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.changePasswordRequired}'),
|
||||
headers: ApiConfig.authHeaders(token),
|
||||
body: jsonEncode({
|
||||
'currentPassword': currentPassword,
|
||||
'newPassword': newPassword,
|
||||
'mot_de_passe_actuel': currentPassword,
|
||||
'nouveau_mot_de_passe': newPassword,
|
||||
'confirmation_mot_de_passe': newPassword,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'api/api_config.dart';
|
||||
import 'api/tokenService.dart';
|
||||
|
||||
/// Réponse GET /configuration (liste complète)
|
||||
class ConfigItem {
|
||||
final String cle;
|
||||
final String? valeur;
|
||||
final String type;
|
||||
final String? categorie;
|
||||
final String? description;
|
||||
|
||||
ConfigItem({
|
||||
required this.cle,
|
||||
this.valeur,
|
||||
required this.type,
|
||||
this.categorie,
|
||||
this.description,
|
||||
});
|
||||
|
||||
factory ConfigItem.fromJson(Map<String, dynamic> json) {
|
||||
return ConfigItem(
|
||||
cle: json['cle'] as String,
|
||||
valeur: json['valeur'] as String?,
|
||||
type: json['type'] as String? ?? 'string',
|
||||
categorie: json['categorie'] as String?,
|
||||
description: json['description'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Réponse GET /configuration/:category (objet clé -> { value, type, description })
|
||||
class ConfigValueItem {
|
||||
final dynamic value;
|
||||
final String type;
|
||||
final String? description;
|
||||
|
||||
ConfigValueItem({required this.value, required this.type, this.description});
|
||||
|
||||
factory ConfigValueItem.fromJson(Map<String, dynamic> json) {
|
||||
return ConfigValueItem(
|
||||
value: json['value'],
|
||||
type: json['type'] as String? ?? 'string',
|
||||
description: json['description'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ConfigurationService {
|
||||
static Future<Map<String, String>> _headers() async {
|
||||
final token = await TokenService.getToken();
|
||||
return token != null
|
||||
? ApiConfig.authHeaders(token)
|
||||
: Map<String, String>.from(ApiConfig.headers);
|
||||
}
|
||||
|
||||
static String? _toStr(dynamic v) {
|
||||
if (v == null) return null;
|
||||
if (v is String) return v;
|
||||
return v.toString();
|
||||
}
|
||||
|
||||
/// GET /api/v1/configuration/setup/status
|
||||
static Future<bool> getSetupStatus() async {
|
||||
final response = await http.get(
|
||||
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.configurationSetupStatus}'),
|
||||
headers: await _headers(),
|
||||
);
|
||||
if (response.statusCode != 200) return true;
|
||||
final data = jsonDecode(response.body);
|
||||
final val = data['data']?['setupCompleted'];
|
||||
if (val is bool) return val;
|
||||
if (val is String) return val.toLowerCase() == 'true' || val == '1';
|
||||
if (val is int) return val == 1;
|
||||
return true; // Par défaut on considère configuré pour ne pas bloquer
|
||||
}
|
||||
|
||||
/// GET /api/v1/configuration (toutes les configs)
|
||||
static Future<List<ConfigItem>> getAll() async {
|
||||
final response = await http.get(
|
||||
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.configuration}'),
|
||||
headers: await _headers(),
|
||||
);
|
||||
if (response.statusCode != 200) {
|
||||
final err = jsonDecode(response.body) as Map<String, dynamic>?;
|
||||
throw Exception(_toStr(err?['message']) ?? 'Erreur chargement configuration');
|
||||
}
|
||||
final data = jsonDecode(response.body);
|
||||
final list = data['data'] as List<dynamic>? ?? [];
|
||||
return list.map((e) => ConfigItem.fromJson(e as Map<String, dynamic>)).toList();
|
||||
}
|
||||
|
||||
/// GET /api/v1/configuration/:category
|
||||
static Future<Map<String, ConfigValueItem>> getByCategory(String category) async {
|
||||
final response = await http.get(
|
||||
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.configuration}/$category'),
|
||||
headers: await _headers(),
|
||||
);
|
||||
if (response.statusCode != 200) {
|
||||
final err = jsonDecode(response.body) as Map<String, dynamic>?;
|
||||
throw Exception(_toStr(err?['message']) ?? 'Erreur chargement configuration');
|
||||
}
|
||||
final data = jsonDecode(response.body);
|
||||
final map = data['data'] as Map<String, dynamic>? ?? {};
|
||||
return map.map((k, v) => MapEntry(k, ConfigValueItem.fromJson(v as Map<String, dynamic>)));
|
||||
}
|
||||
|
||||
/// PATCH /api/v1/configuration/bulk
|
||||
static Future<void> updateBulk(Map<String, dynamic> body) async {
|
||||
final response = await http.patch(
|
||||
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.configurationBulk}'),
|
||||
headers: await _headers(),
|
||||
body: jsonEncode(body),
|
||||
);
|
||||
if (response.statusCode != 200 && response.statusCode != 201) {
|
||||
final err = jsonDecode(response.body) as Map<String, dynamic>?;
|
||||
final msg = err != null ? (_toStr(err['error']) ?? _toStr(err['message'])) : null;
|
||||
throw Exception(msg ?? 'Erreur lors de la sauvegarde');
|
||||
}
|
||||
}
|
||||
|
||||
/// POST /api/v1/configuration/test-smtp
|
||||
static Future<String> testSmtp(String testEmail) async {
|
||||
final response = await http.post(
|
||||
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.configurationTestSmtp}'),
|
||||
headers: await _headers(),
|
||||
body: jsonEncode({'testEmail': testEmail}),
|
||||
);
|
||||
final data = jsonDecode(response.body) as Map<String, dynamic>?;
|
||||
if ((response.statusCode == 200 || response.statusCode == 201) && (data?['success'] == true)) {
|
||||
return _toStr(data?['message']) ?? 'Test SMTP réussi.';
|
||||
}
|
||||
final msg = data != null ? (_toStr(data['error']) ?? _toStr(data['message'])) : null;
|
||||
throw Exception(msg ?? 'Échec du test SMTP');
|
||||
}
|
||||
|
||||
/// POST /api/v1/configuration/setup/complete (après première config)
|
||||
static Future<void> completeSetup() async {
|
||||
final response = await http.post(
|
||||
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.configurationSetupComplete}'),
|
||||
headers: await _headers(),
|
||||
);
|
||||
if (response.statusCode != 200 && response.statusCode != 201) {
|
||||
final err = jsonDecode(response.body) as Map<String, dynamic>?;
|
||||
final msg = err != null ? (_toStr(err['error']) ?? _toStr(err['message'])) : null;
|
||||
throw Exception(msg ?? 'Erreur finalisation configuration');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:p_tits_pas/models/user.dart';
|
||||
import 'package:p_tits_pas/models/parent_model.dart';
|
||||
import 'package:p_tits_pas/models/assistante_maternelle_model.dart';
|
||||
import 'package:p_tits_pas/services/api/api_config.dart';
|
||||
import 'package:p_tits_pas/services/api/tokenService.dart';
|
||||
|
||||
class UserService {
|
||||
static Future<Map<String, String>> _headers() async {
|
||||
final token = await TokenService.getToken();
|
||||
return token != null
|
||||
? ApiConfig.authHeaders(token)
|
||||
: Map<String, String>.from(ApiConfig.headers);
|
||||
}
|
||||
|
||||
static String? _toStr(dynamic v) {
|
||||
if (v == null) return null;
|
||||
if (v is String) return v;
|
||||
return v.toString();
|
||||
}
|
||||
|
||||
// Récupérer la liste des gestionnaires (endpoint dédié)
|
||||
static Future<List<AppUser>> getGestionnaires() async {
|
||||
final response = await http.get(
|
||||
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.gestionnaires}'),
|
||||
headers: await _headers(),
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
final err = jsonDecode(response.body) as Map<String, dynamic>?;
|
||||
throw Exception(_toStr(err?['message']) ?? 'Erreur chargement gestionnaires');
|
||||
}
|
||||
|
||||
final List<dynamic> data = jsonDecode(response.body);
|
||||
return data.map((e) => AppUser.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
// Récupérer la liste des parents
|
||||
static Future<List<ParentModel>> getParents() async {
|
||||
final response = await http.get(
|
||||
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.parents}'),
|
||||
headers: await _headers(),
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
final err = jsonDecode(response.body) as Map<String, dynamic>?;
|
||||
throw Exception(_toStr(err?['message']) ?? 'Erreur chargement parents');
|
||||
}
|
||||
|
||||
final List<dynamic> data = jsonDecode(response.body);
|
||||
return data.map((e) => ParentModel.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
// Récupérer la liste des assistantes maternelles
|
||||
static Future<List<AssistanteMaternelleModel>> getAssistantesMaternelles() async {
|
||||
final response = await http.get(
|
||||
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.assistantesMaternelles}'),
|
||||
headers: await _headers(),
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
final err = jsonDecode(response.body) as Map<String, dynamic>?;
|
||||
throw Exception(_toStr(err?['message']) ?? 'Erreur chargement AM');
|
||||
}
|
||||
|
||||
final List<dynamic> data = jsonDecode(response.body);
|
||||
return data.map((e) => AssistanteMaternelleModel.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
// Récupérer la liste des administrateurs (via /users filtré ou autre)
|
||||
// Pour l'instant on va utiliser /users et filtrer côté client si on est super admin
|
||||
static Future<List<AppUser>> getAdministrateurs() async {
|
||||
// TODO: Endpoint dédié ou filtrage
|
||||
// En attendant, on retourne une liste vide ou on tente /users
|
||||
try {
|
||||
final response = await http.get(
|
||||
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.users}'),
|
||||
headers: await _headers(),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> data = jsonDecode(response.body);
|
||||
return data
|
||||
.map((e) => AppUser.fromJson(e))
|
||||
.where((u) => u.role == 'administrateur' || u.role == 'super_admin')
|
||||
.toList();
|
||||
}
|
||||
} catch (e) {
|
||||
print('Erreur chargement admins: $e');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:p_tits_pas/models/user.dart';
|
||||
import 'package:p_tits_pas/services/user_service.dart';
|
||||
|
||||
class AdminManagementWidget extends StatefulWidget {
|
||||
const AdminManagementWidget({super.key});
|
||||
|
||||
@override
|
||||
State<AdminManagementWidget> createState() => _AdminManagementWidgetState();
|
||||
}
|
||||
|
||||
class _AdminManagementWidgetState extends State<AdminManagementWidget> {
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
List<AppUser> _admins = [];
|
||||
List<AppUser> _filteredAdmins = [];
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadAdmins();
|
||||
_searchController.addListener(_onSearchChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadAdmins() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
});
|
||||
try {
|
||||
final list = await UserService.getAdministrateurs();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_admins = list;
|
||||
_filteredAdmins = list;
|
||||
_isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_error = e.toString();
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _onSearchChanged() {
|
||||
final query = _searchController.text.toLowerCase();
|
||||
setState(() {
|
||||
_filteredAdmins = _admins.where((u) {
|
||||
final name = u.fullName.toLowerCase();
|
||||
final email = u.email.toLowerCase();
|
||||
return name.contains(query) || email.contains(query);
|
||||
}).toList();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: "Rechercher un administrateur...",
|
||||
prefixIcon: Icon(Icons.search),
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
// TODO: Créer admin
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text("Créer un admin"),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
if (_isLoading)
|
||||
const Center(child: CircularProgressIndicator())
|
||||
else if (_error != null)
|
||||
Center(child: Text('Erreur: $_error', style: const TextStyle(color: Colors.red)))
|
||||
else if (_filteredAdmins.isEmpty)
|
||||
const Center(child: Text("Aucun administrateur trouvé."))
|
||||
else
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: _filteredAdmins.length,
|
||||
itemBuilder: (context, index) {
|
||||
final user = _filteredAdmins[index];
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
child: Text(user.fullName.isNotEmpty
|
||||
? user.fullName[0].toUpperCase()
|
||||
: 'A'),
|
||||
),
|
||||
title: Text(user.fullName.isNotEmpty
|
||||
? user.fullName
|
||||
: 'Sans nom'),
|
||||
subtitle: Text(user.email),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit),
|
||||
onPressed: () {},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,72 +1,142 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:p_tits_pas/models/assistante_maternelle_model.dart';
|
||||
import 'package:p_tits_pas/services/user_service.dart';
|
||||
|
||||
class AssistanteMaternelleManagementWidget extends StatelessWidget {
|
||||
class AssistanteMaternelleManagementWidget extends StatefulWidget {
|
||||
const AssistanteMaternelleManagementWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final assistantes = [
|
||||
{
|
||||
"nom": "Marie Dupont",
|
||||
"numeroAgrement": "AG123456",
|
||||
"zone": "Paris 14",
|
||||
"capacite": 3,
|
||||
},
|
||||
{
|
||||
"nom": "Claire Martin",
|
||||
"numeroAgrement": "AG654321",
|
||||
"zone": "Lyon 7",
|
||||
"capacite": 2,
|
||||
},
|
||||
];
|
||||
State<AssistanteMaternelleManagementWidget> createState() =>
|
||||
_AssistanteMaternelleManagementWidgetState();
|
||||
}
|
||||
|
||||
class _AssistanteMaternelleManagementWidgetState
|
||||
extends State<AssistanteMaternelleManagementWidget> {
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
List<AssistanteMaternelleModel> _assistantes = [];
|
||||
List<AssistanteMaternelleModel> _filteredAssistantes = [];
|
||||
|
||||
final TextEditingController _zoneController = TextEditingController();
|
||||
final TextEditingController _capacityController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadAssistantes();
|
||||
_zoneController.addListener(_filter);
|
||||
_capacityController.addListener(_filter);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_zoneController.dispose();
|
||||
_capacityController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadAssistantes() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
});
|
||||
try {
|
||||
final list = await UserService.getAssistantesMaternelles();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_assistantes = list;
|
||||
_filter();
|
||||
_isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_error = e.toString();
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _filter() {
|
||||
final zoneQuery = _zoneController.text.toLowerCase();
|
||||
final capacityQuery = int.tryParse(_capacityController.text);
|
||||
|
||||
setState(() {
|
||||
_filteredAssistantes = _assistantes.where((am) {
|
||||
final matchesZone = zoneQuery.isEmpty ||
|
||||
(am.residenceCity?.toLowerCase().contains(zoneQuery) ?? false);
|
||||
final matchesCapacity = capacityQuery == null ||
|
||||
(am.maxChildren != null && am.maxChildren! >= capacityQuery);
|
||||
return matchesZone && matchesCapacity;
|
||||
}).toList();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 🔎 Zone de filtre
|
||||
_buildFilterSection(),
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 🔎 Zone de filtre
|
||||
_buildFilterSection(),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
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
|
||||
},
|
||||
// 📋 Liste des assistantes
|
||||
if (_isLoading)
|
||||
const Center(child: CircularProgressIndicator())
|
||||
else if (_error != null)
|
||||
Center(child: Text('Erreur: $_error', style: const TextStyle(color: Colors.red)))
|
||||
else if (_filteredAssistantes.isEmpty)
|
||||
const Center(child: Text("Aucune assistante maternelle trouvée."))
|
||||
else
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: _filteredAssistantes.length,
|
||||
itemBuilder: (context, index) {
|
||||
final assistante = _filteredAssistantes[index];
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundImage: assistante.user.photoUrl != null
|
||||
? NetworkImage(assistante.user.photoUrl!)
|
||||
: null,
|
||||
child: assistante.user.photoUrl == null
|
||||
? const Icon(Icons.face)
|
||||
: null,
|
||||
),
|
||||
title: Text(assistante.user.fullName.isNotEmpty
|
||||
? assistante.user.fullName
|
||||
: 'Sans nom'),
|
||||
subtitle: Text(
|
||||
"N° Agrément : ${assistante.approvalNumber ?? 'N/A'}\nZone : ${assistante.residenceCity ?? 'N/A'} | Capacité : ${assistante.maxChildren ?? 0}"),
|
||||
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
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () {
|
||||
// TODO: Ajouter suppression
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -78,26 +148,23 @@ class AssistanteMaternelleManagementWidget extends StatelessWidget {
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: TextField(
|
||||
controller: _zoneController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Zone géographique",
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.location_on),
|
||||
),
|
||||
onChanged: (value) {
|
||||
// TODO: Ajouter logique de filtrage par zone
|
||||
},
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: TextField(
|
||||
controller: _capacityController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Capacité minimum",
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: (value) {
|
||||
// TODO: Ajouter logique de filtrage par capacité
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,47 +1,51 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:p_tits_pas/services/auth_service.dart';
|
||||
|
||||
/// Barre du dashboard admin : onglets Gestion des utilisateurs | Paramètres + déconnexion.
|
||||
class DashboardAppBarAdmin extends StatelessWidget implements PreferredSizeWidget {
|
||||
final int selectedIndex;
|
||||
final ValueChanged<int> onTabChange;
|
||||
final bool setupCompleted;
|
||||
|
||||
const DashboardAppBarAdmin({Key? key, required this.selectedIndex, required this.onTabChange}) : super(key: key);
|
||||
const DashboardAppBarAdmin({
|
||||
Key? key,
|
||||
required this.selectedIndex,
|
||||
required this.onTabChange,
|
||||
this.setupCompleted = true,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight + 10);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isMobile = MediaQuery.of(context).size.width < 768;
|
||||
return AppBar(
|
||||
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,
|
||||
const SizedBox(width: 24),
|
||||
Image.asset(
|
||||
'assets/images/logo.png',
|
||||
height: 40,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildNavItem(context, 'Gestion des utilisateurs', 0, enabled: setupCompleted),
|
||||
const SizedBox(width: 24),
|
||||
_buildNavItem(context, 'Paramètres', 1, enabled: true),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
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', 3),
|
||||
],
|
||||
),
|
||||
actions: isMobile
|
||||
? [_buildMobileMenu(context)]
|
||||
: [
|
||||
// Nom de l'utilisateur
|
||||
actions: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Center(
|
||||
@@ -55,8 +59,6 @@ class DashboardAppBarAdmin extends StatelessWidget implements PreferredSizeWidge
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Bouton déconnexion
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 16),
|
||||
child: TextButton(
|
||||
@@ -72,51 +74,33 @@ class DashboardAppBarAdmin extends StatelessWidget implements PreferredSizeWidge
|
||||
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 _buildNavItem(BuildContext context, String title, int index, {bool enabled = true}) {
|
||||
final bool isActive = index == selectedIndex;
|
||||
return InkWell(
|
||||
onTap: enabled ? () => onTabChange(index) : null,
|
||||
child: Opacity(
|
||||
opacity: enabled ? 1.0 : 0.5,
|
||||
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")),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -132,9 +116,10 @@ class DashboardAppBarAdmin extends StatelessWidget implements PreferredSizeWidge
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
// TODO: Implémenter la logique de déconnexion
|
||||
await AuthService.logout();
|
||||
if (context.mounted) context.go('/login');
|
||||
},
|
||||
child: const Text('Déconnecter'),
|
||||
),
|
||||
@@ -142,4 +127,65 @@ class DashboardAppBarAdmin extends StatelessWidget implements PreferredSizeWidge
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sous-barre : Gestionnaires | Parents | Assistantes maternelles | Administrateurs.
|
||||
class DashboardUserManagementSubBar extends StatelessWidget {
|
||||
final int selectedSubIndex;
|
||||
final ValueChanged<int> onSubTabChange;
|
||||
|
||||
const DashboardUserManagementSubBar({
|
||||
Key? key,
|
||||
required this.selectedSubIndex,
|
||||
required this.onSubTabChange,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
border: Border(bottom: BorderSide(color: Colors.grey.shade300)),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 6),
|
||||
child: Center(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildSubNavItem(context, 'Gestionnaires', 0),
|
||||
const SizedBox(width: 16),
|
||||
_buildSubNavItem(context, 'Parents', 1),
|
||||
const SizedBox(width: 16),
|
||||
_buildSubNavItem(context, 'Assistantes maternelles', 2),
|
||||
const SizedBox(width: 16),
|
||||
_buildSubNavItem(context, 'Administrateurs', 3),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSubNavItem(BuildContext context, String title, int index) {
|
||||
final bool isActive = index == selectedSubIndex;
|
||||
return InkWell(
|
||||
onTap: () => onSubTabChange(index),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: isActive ? const Color(0xFF9CC5C0) : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: isActive ? null : Border.all(color: Colors.black26),
|
||||
),
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: isActive ? Colors.white : Colors.black87,
|
||||
fontWeight: isActive ? FontWeight.w600 : FontWeight.normal,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,70 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:p_tits_pas/models/user.dart';
|
||||
import 'package:p_tits_pas/services/user_service.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/gestionnaire_card.dart';
|
||||
|
||||
class GestionnaireManagementWidget extends StatelessWidget {
|
||||
class GestionnaireManagementWidget extends StatefulWidget {
|
||||
const GestionnaireManagementWidget({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<GestionnaireManagementWidget> createState() =>
|
||||
_GestionnaireManagementWidgetState();
|
||||
}
|
||||
|
||||
class _GestionnaireManagementWidgetState
|
||||
extends State<GestionnaireManagementWidget> {
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
List<AppUser> _gestionnaires = [];
|
||||
List<AppUser> _filteredGestionnaires = [];
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadGestionnaires();
|
||||
_searchController.addListener(_onSearchChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadGestionnaires() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
});
|
||||
try {
|
||||
final list = await UserService.getGestionnaires();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_gestionnaires = list;
|
||||
_filteredGestionnaires = list;
|
||||
_isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_error = e.toString();
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _onSearchChanged() {
|
||||
final query = _searchController.text.toLowerCase();
|
||||
setState(() {
|
||||
_filteredGestionnaires = _gestionnaires.where((u) {
|
||||
final name = u.fullName.toLowerCase();
|
||||
final email = u.email.toLowerCase();
|
||||
return name.contains(query) || email.contains(query);
|
||||
}).toList();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
@@ -14,9 +75,10 @@ class GestionnaireManagementWidget extends StatelessWidget {
|
||||
// 🔹 Barre du haut avec bouton
|
||||
Row(
|
||||
children: [
|
||||
const Expanded(
|
||||
Expanded(
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
controller: _searchController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: "Rechercher un gestionnaire...",
|
||||
prefixIcon: Icon(Icons.search),
|
||||
border: OutlineInputBorder(),
|
||||
@@ -26,7 +88,7 @@ class GestionnaireManagementWidget extends StatelessWidget {
|
||||
const SizedBox(width: 16),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
// Rediriger vers la page de création
|
||||
// TODO: Rediriger vers la page de création
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text("Créer un gestionnaire"),
|
||||
@@ -36,17 +98,25 @@ class GestionnaireManagementWidget extends StatelessWidget {
|
||||
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",
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
if (_isLoading)
|
||||
const Center(child: CircularProgressIndicator())
|
||||
else if (_error != null)
|
||||
Center(child: Text('Erreur: $_error', style: const TextStyle(color: Colors.red)))
|
||||
else if (_filteredGestionnaires.isEmpty)
|
||||
const Center(child: Text("Aucun gestionnaire trouvé."))
|
||||
else
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: _filteredGestionnaires.length,
|
||||
itemBuilder: (context, index) {
|
||||
final user = _filteredGestionnaires[index];
|
||||
return GestionnaireCard(
|
||||
name: user.fullName.isNotEmpty ? user.fullName : "Sans nom",
|
||||
email: user.email,
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,428 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:p_tits_pas/services/configuration_service.dart';
|
||||
|
||||
/// Panneau Paramètres admin : Email (SMTP), Personnalisation, Avancé.
|
||||
class ParametresPanel extends StatefulWidget {
|
||||
/// Si true, après sauvegarde on redirige vers le login (première config). Sinon on reste sur la page.
|
||||
final bool redirectToLoginAfterSave;
|
||||
|
||||
const ParametresPanel({super.key, this.redirectToLoginAfterSave = false});
|
||||
|
||||
@override
|
||||
State<ParametresPanel> createState() => _ParametresPanelState();
|
||||
}
|
||||
|
||||
class _ParametresPanelState extends State<ParametresPanel> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
bool _isLoading = true;
|
||||
String? _loadError;
|
||||
bool _isSaving = false;
|
||||
String? _message;
|
||||
|
||||
final Map<String, TextEditingController> _controllers = {};
|
||||
bool _smtpSecure = false;
|
||||
bool _smtpAuthRequired = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_createControllers();
|
||||
_loadConfiguration();
|
||||
}
|
||||
|
||||
void _createControllers() {
|
||||
final keys = [
|
||||
'smtp_host', 'smtp_port', 'smtp_user', 'smtp_password',
|
||||
'email_from_name', 'email_from_address',
|
||||
'app_name', 'app_url', 'app_logo_url',
|
||||
'password_reset_token_expiry_days', 'jwt_expiry_hours', 'max_upload_size_mb',
|
||||
];
|
||||
for (final k in keys) {
|
||||
_controllers[k] = TextEditingController();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadConfiguration() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_loadError = null;
|
||||
});
|
||||
try {
|
||||
final list = await ConfigurationService.getAll();
|
||||
if (!mounted) return;
|
||||
for (final item in list) {
|
||||
final c = _controllers[item.cle];
|
||||
if (c != null && item.valeur != null && item.valeur != '***********') {
|
||||
c.text = item.valeur!;
|
||||
}
|
||||
if (item.cle == 'smtp_secure') {
|
||||
_smtpSecure = item.valeur == 'true';
|
||||
}
|
||||
if (item.cle == 'smtp_auth_required') {
|
||||
_smtpAuthRequired = item.valeur == 'true';
|
||||
}
|
||||
}
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_loadError = e.toString().replaceAll('Exception: ', '');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (final c in _controllers.values) {
|
||||
c.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Map<String, dynamic> _buildPayload() {
|
||||
final payload = <String, dynamic>{};
|
||||
payload['smtp_host'] = _controllers['smtp_host']!.text.trim();
|
||||
final port = int.tryParse(_controllers['smtp_port']!.text.trim());
|
||||
if (port != null) payload['smtp_port'] = port;
|
||||
payload['smtp_secure'] = _smtpSecure;
|
||||
payload['smtp_auth_required'] = _smtpAuthRequired;
|
||||
payload['smtp_user'] = _controllers['smtp_user']!.text.trim();
|
||||
final pwd = _controllers['smtp_password']!.text.trim();
|
||||
if (pwd.isNotEmpty && pwd != '***********') payload['smtp_password'] = pwd;
|
||||
payload['email_from_name'] = _controllers['email_from_name']!.text.trim();
|
||||
payload['email_from_address'] = _controllers['email_from_address']!.text.trim();
|
||||
payload['app_name'] = _controllers['app_name']!.text.trim();
|
||||
payload['app_url'] = _controllers['app_url']!.text.trim();
|
||||
payload['app_logo_url'] = _controllers['app_logo_url']!.text.trim();
|
||||
final tokenDays = int.tryParse(_controllers['password_reset_token_expiry_days']!.text.trim());
|
||||
if (tokenDays != null) payload['password_reset_token_expiry_days'] = tokenDays;
|
||||
final jwtHours = int.tryParse(_controllers['jwt_expiry_hours']!.text.trim());
|
||||
if (jwtHours != null) payload['jwt_expiry_hours'] = jwtHours;
|
||||
final maxMb = int.tryParse(_controllers['max_upload_size_mb']!.text.trim());
|
||||
if (maxMb != null) payload['max_upload_size_mb'] = maxMb;
|
||||
return payload;
|
||||
}
|
||||
|
||||
/// Sauvegarde en base sans completeSetup (utilisé avant test SMTP).
|
||||
Future<void> _saveBulkOnly() async {
|
||||
await ConfigurationService.updateBulk(_buildPayload());
|
||||
}
|
||||
|
||||
/// Sauvegarde la config, marque le setup comme terminé. Si première config, redirige vers le login.
|
||||
Future<void> _save() async {
|
||||
final redirectAfter = widget.redirectToLoginAfterSave;
|
||||
setState(() {
|
||||
_message = null;
|
||||
_isSaving = true;
|
||||
});
|
||||
try {
|
||||
await ConfigurationService.updateBulk(_buildPayload());
|
||||
if (!mounted) return;
|
||||
await ConfigurationService.completeSetup();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isSaving = false;
|
||||
_message = 'Configuration enregistrée.';
|
||||
});
|
||||
if (!mounted) return;
|
||||
if (redirectAfter) {
|
||||
GoRouter.of(context).go('/login');
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isSaving = false;
|
||||
_message = e.toString().replaceAll('Exception: ', '');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _testSmtp() async {
|
||||
final email = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (ctx) {
|
||||
final c = TextEditingController();
|
||||
return AlertDialog(
|
||||
title: const Text('Tester la connexion SMTP'),
|
||||
content: TextField(
|
||||
controller: c,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Email pour recevoir le test',
|
||||
hintText: 'admin@example.com',
|
||||
),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
final t = c.text.trim();
|
||||
if (t.isNotEmpty) Navigator.pop(ctx, t);
|
||||
},
|
||||
child: const Text('Envoyer'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
if (email == null || !mounted) return;
|
||||
setState(() => _message = null);
|
||||
try {
|
||||
await _saveBulkOnly();
|
||||
if (!mounted) return;
|
||||
final msg = await ConfigurationService.testSmtp(email);
|
||||
if (!mounted) return;
|
||||
setState(() => _message = msg);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() => _message = e.toString().replaceAll('Exception: ', ''));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (_loadError != null) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(_loadError!, style: TextStyle(color: Colors.red.shade700)),
|
||||
const SizedBox(height: 16),
|
||||
FilledButton(
|
||||
onPressed: _loadConfiguration,
|
||||
child: const Text('Réessayer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final isSuccess = _message != null &&
|
||||
(_message!.startsWith('Configuration') || _message!.startsWith('Connexion'));
|
||||
|
||||
return Form(
|
||||
key: _formKey,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 720),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (_message != null) ...[
|
||||
_MessageBanner(message: _message!, isSuccess: isSuccess),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
_buildSectionCard(
|
||||
context,
|
||||
icon: Icons.email_outlined,
|
||||
title: 'Configuration Email (SMTP)',
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildField('smtp_host', 'Serveur SMTP', hint: 'mail.example.com'),
|
||||
const SizedBox(height: 14),
|
||||
_buildField('smtp_port', 'Port SMTP', keyboard: TextInputType.number, hint: '25, 465, 587'),
|
||||
const SizedBox(height: 14),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 14),
|
||||
child: Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: _smtpSecure,
|
||||
onChanged: (v) => setState(() => _smtpSecure = v ?? false),
|
||||
activeColor: const Color(0xFF9CC5C0),
|
||||
),
|
||||
const Text('SSL/TLS (secure)'),
|
||||
const SizedBox(width: 24),
|
||||
Checkbox(
|
||||
value: _smtpAuthRequired,
|
||||
onChanged: (v) => setState(() => _smtpAuthRequired = v ?? false),
|
||||
activeColor: const Color(0xFF9CC5C0),
|
||||
),
|
||||
const Text('Authentification requise'),
|
||||
],
|
||||
),
|
||||
),
|
||||
_buildField('smtp_user', 'Utilisateur SMTP'),
|
||||
const SizedBox(height: 14),
|
||||
_buildField('smtp_password', 'Mot de passe SMTP', obscure: true),
|
||||
const SizedBox(height: 14),
|
||||
_buildField('email_from_name', 'Nom expéditeur'),
|
||||
const SizedBox(height: 14),
|
||||
_buildField('email_from_address', 'Email expéditeur', hint: 'no-reply@example.com'),
|
||||
const SizedBox(height: 18),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: _isSaving ? null : _testSmtp,
|
||||
icon: const Icon(Icons.send_outlined, size: 18),
|
||||
label: const Text('Tester la connexion SMTP'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: const Color(0xFF2D6A4F),
|
||||
side: const BorderSide(color: Color(0xFF9CC5C0)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_buildSectionCard(
|
||||
context,
|
||||
icon: Icons.palette_outlined,
|
||||
title: 'Personnalisation',
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildField('app_name', 'Nom de l\'application'),
|
||||
const SizedBox(height: 14),
|
||||
_buildField('app_url', 'URL de l\'application', hint: 'https://app.example.com'),
|
||||
const SizedBox(height: 14),
|
||||
_buildField('app_logo_url', 'URL du logo', hint: '/assets/logo.png'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_buildSectionCard(
|
||||
context,
|
||||
icon: Icons.settings_outlined,
|
||||
title: 'Paramètres avancés',
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildField('password_reset_token_expiry_days', 'Validité token MDP (jours)', keyboard: TextInputType.number),
|
||||
const SizedBox(height: 14),
|
||||
_buildField('jwt_expiry_hours', 'Validité session JWT (heures)', keyboard: TextInputType.number),
|
||||
const SizedBox(height: 14),
|
||||
_buildField('max_upload_size_mb', 'Taille max upload (MB)', keyboard: TextInputType.number),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
SizedBox(
|
||||
height: 48,
|
||||
child: FilledButton(
|
||||
onPressed: _isSaving ? null : _save,
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF9CC5C0),
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: _isSaving
|
||||
? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
|
||||
: const Text('Sauvegarder la configuration'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionCard(BuildContext context, {required IconData icon, required String title, required Widget child}) {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(icon, size: 22, color: const Color(0xFF9CC5C0)),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: const Color(0xFF2D6A4F),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
child,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildField(String key, String label, {bool obscure = false, TextInputType? keyboard, String? hint}) {
|
||||
final c = _controllers[key];
|
||||
if (c == null) return const SizedBox.shrink();
|
||||
return TextFormField(
|
||||
controller: c,
|
||||
obscureText: obscure,
|
||||
keyboardType: keyboard,
|
||||
enabled: !_isSaving,
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
hintText: hint,
|
||||
border: const OutlineInputBorder(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MessageBanner extends StatelessWidget {
|
||||
final String message;
|
||||
final bool isSuccess;
|
||||
|
||||
const _MessageBanner({required this.message, required this.isSuccess});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: isSuccess ? Colors.green.shade50 : Colors.red.shade50,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: isSuccess ? Colors.green.shade200 : Colors.red.shade200,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
isSuccess ? Icons.check_circle_outline : Icons.error_outline,
|
||||
size: 22,
|
||||
color: isSuccess ? Colors.green.shade700 : Colors.red.shade700,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
message,
|
||||
style: TextStyle(
|
||||
color: isSuccess ? Colors.green.shade900 : Colors.red.shade900,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,83 +1,149 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:p_tits_pas/models/parent_model.dart';
|
||||
import 'package:p_tits_pas/services/user_service.dart';
|
||||
|
||||
class ParentManagementWidget extends StatelessWidget {
|
||||
class ParentManagementWidget extends StatefulWidget {
|
||||
const ParentManagementWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 🔁 Simulation de données parents
|
||||
final parents = [
|
||||
{
|
||||
"nom": "Jean Dupuis",
|
||||
"email": "jean.dupuis@email.com",
|
||||
"statut": "Actif",
|
||||
"enfants": 2,
|
||||
},
|
||||
{
|
||||
"nom": "Lucie Morel",
|
||||
"email": "lucie.morel@email.com",
|
||||
"statut": "En attente",
|
||||
"enfants": 1,
|
||||
},
|
||||
];
|
||||
State<ParentManagementWidget> createState() => _ParentManagementWidgetState();
|
||||
}
|
||||
|
||||
class _ParentManagementWidgetState extends State<ParentManagementWidget> {
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
List<ParentModel> _parents = [];
|
||||
List<ParentModel> _filteredParents = [];
|
||||
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
String? _selectedStatus;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadParents();
|
||||
_searchController.addListener(_filter);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadParents() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
});
|
||||
try {
|
||||
final list = await UserService.getParents();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_parents = list;
|
||||
_filter(); // Apply initial filter (if any)
|
||||
_isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_error = e.toString();
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _filter() {
|
||||
final query = _searchController.text.toLowerCase();
|
||||
setState(() {
|
||||
_filteredParents = _parents.where((p) {
|
||||
final matchesName = p.user.fullName.toLowerCase().contains(query) ||
|
||||
p.user.email.toLowerCase().contains(query);
|
||||
final matchesStatus = _selectedStatus == null ||
|
||||
_selectedStatus == 'Tous' ||
|
||||
(p.user.statut?.toLowerCase() == _selectedStatus?.toLowerCase());
|
||||
|
||||
// Mapping simple pour le statut affiché vs backend
|
||||
// Backend: en_attente, actif, suspendu
|
||||
// Dropdown: En attente, Actif, Suspendu
|
||||
|
||||
return matchesName && matchesStatus;
|
||||
}).toList();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
||||
_buildSearchSection(),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: parents.length,
|
||||
itemBuilder: (context, index) {
|
||||
final parent = parents[index];
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.person_outline),
|
||||
title: Text(parent['nom'].toString()),
|
||||
subtitle: Text(
|
||||
"${parent['email']}\nStatut : ${parent['statut']} | Enfants : ${parent['enfants']}",
|
||||
),
|
||||
isThreeLine: true,
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.visibility),
|
||||
tooltip: "Voir dossier",
|
||||
onPressed: () {
|
||||
// TODO: Voir le statut du dossier
|
||||
},
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSearchSection(),
|
||||
const SizedBox(height: 16),
|
||||
if (_isLoading)
|
||||
const Center(child: CircularProgressIndicator())
|
||||
else if (_error != null)
|
||||
Center(child: Text('Erreur: $_error', style: const TextStyle(color: Colors.red)))
|
||||
else if (_filteredParents.isEmpty)
|
||||
const Center(child: Text("Aucun parent trouvé."))
|
||||
else
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: _filteredParents.length,
|
||||
itemBuilder: (context, index) {
|
||||
final parent = _filteredParents[index];
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundImage: parent.user.photoUrl != null
|
||||
? NetworkImage(parent.user.photoUrl!)
|
||||
: null,
|
||||
child: parent.user.photoUrl == null
|
||||
? const Icon(Icons.person)
|
||||
: null,
|
||||
),
|
||||
title: Text(parent.user.fullName.isNotEmpty
|
||||
? parent.user.fullName
|
||||
: 'Sans nom'),
|
||||
subtitle: Text(
|
||||
"${parent.user.email}\nStatut : ${parent.user.statut ?? 'Inconnu'} | Enfants : ${parent.childrenCount}",
|
||||
),
|
||||
isThreeLine: true,
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.visibility),
|
||||
tooltip: "Voir dossier",
|
||||
onPressed: () {
|
||||
// TODO: Voir le statut du dossier
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit),
|
||||
tooltip: "Modifier",
|
||||
onPressed: () {
|
||||
// TODO: Modifier parent
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
tooltip: "Supprimer",
|
||||
onPressed: () {
|
||||
// TODO: Supprimer compte
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit),
|
||||
tooltip: "Modifier",
|
||||
onPressed: () {
|
||||
// TODO: Modifier parent
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
tooltip: "Supprimer",
|
||||
onPressed: () {
|
||||
// TODO: Supprimer compte
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -89,13 +155,12 @@ class ParentManagementWidget extends StatelessWidget {
|
||||
SizedBox(
|
||||
width: 220,
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Nom du parent",
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.search),
|
||||
),
|
||||
onChanged: (value) {
|
||||
// TODO: Ajouter logique de recherche
|
||||
},
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
@@ -105,13 +170,18 @@ class ParentManagementWidget extends StatelessWidget {
|
||||
labelText: "Statut",
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
value: _selectedStatus,
|
||||
items: const [
|
||||
DropdownMenuItem(value: "Actif", child: Text("Actif")),
|
||||
DropdownMenuItem(value: "En attente", child: Text("En attente")),
|
||||
DropdownMenuItem(value: "Supprimé", child: Text("Supprimé")),
|
||||
DropdownMenuItem(value: null, child: Text("Tous")),
|
||||
DropdownMenuItem(value: "actif", child: Text("Actif")),
|
||||
DropdownMenuItem(value: "en_attente", child: Text("En attente")),
|
||||
DropdownMenuItem(value: "suspendu", child: Text("Suspendu")),
|
||||
],
|
||||
onChanged: (value) {
|
||||
// TODO: Ajouter logique de filtrage
|
||||
setState(() {
|
||||
_selectedStatus = value;
|
||||
_filter();
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -196,7 +196,7 @@ class _ChangePasswordDialogState extends State<ChangePasswordDialog> {
|
||||
hintText: 'Retapez le nouveau mot de passe',
|
||||
obscureText: true,
|
||||
validator: _validateConfirmPassword,
|
||||
style: CustomAppTextFieldStyle.lavande,
|
||||
style: CustomAppTextFieldStyle.jaune,
|
||||
fieldHeight: 53,
|
||||
fieldWidth: double.infinity,
|
||||
enabled: !_isLoading,
|
||||
|
||||
@@ -55,8 +55,14 @@ class ChoiceCardWidget extends StatelessWidget {
|
||||
required bool isMobile,
|
||||
}) {
|
||||
final Color baseRoseColor = Colors.pink.shade300;
|
||||
final Color initialShadow = baseRoseColor.withAlpha(90);
|
||||
final Color hoverShadow = baseRoseColor.withAlpha(130);
|
||||
final Color initialShadow = isMobile
|
||||
? Colors.black.withOpacity(0.45)
|
||||
: baseRoseColor.withAlpha(90);
|
||||
final Color hoverShadow = isMobile
|
||||
? Colors.black.withOpacity(0.5)
|
||||
: baseRoseColor.withAlpha(130);
|
||||
final double initialElevation = isMobile ? 14.0 : 4.0;
|
||||
final double hoverElevation = isMobile ? 18.0 : 8.0;
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -64,6 +70,8 @@ class ChoiceCardWidget extends StatelessWidget {
|
||||
HoverReliefWidget(
|
||||
onPressed: onPressed,
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
initialElevation: initialElevation,
|
||||
hoverElevation: hoverElevation,
|
||||
initialShadowColor: initialShadow,
|
||||
hoverShadowColor: hoverShadow,
|
||||
child: Padding(
|
||||
|
||||
@@ -25,11 +25,13 @@ class CustomAppTextField extends StatefulWidget {
|
||||
final IconData? suffixIcon;
|
||||
final double labelFontSize;
|
||||
final double inputFontSize;
|
||||
final bool showLabel;
|
||||
|
||||
const CustomAppTextField({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.labelText,
|
||||
this.showLabel = true,
|
||||
this.hintText = '',
|
||||
this.fieldWidth = 300.0,
|
||||
this.fieldHeight = 53.0,
|
||||
@@ -73,15 +75,17 @@ class _CustomAppTextFieldState extends State<CustomAppTextField> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
widget.labelText,
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: widget.labelFontSize,
|
||||
color: Colors.black87,
|
||||
fontWeight: FontWeight.w500,
|
||||
if (widget.showLabel) ...[
|
||||
Text(
|
||||
widget.labelText,
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: widget.labelFontSize,
|
||||
color: Colors.black87,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
const SizedBox(height: 6),
|
||||
],
|
||||
SizedBox(
|
||||
width: widget.fieldWidth,
|
||||
height: dynamicFieldHeight,
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Créer l’issue #84 (correctifs modale MDP) via l’API Gitea
|
||||
|
||||
1. Définir un token valide :
|
||||
`export GITEA_TOKEN="votre_token"`
|
||||
ou créer `.gitea-token` à la racine du projet avec le token seul.
|
||||
|
||||
2. Créer l’issue :
|
||||
```bash
|
||||
cd /chemin/vers/PetitsPas
|
||||
curl -s -X POST \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @scripts/issue-84-payload.json \
|
||||
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues"
|
||||
```
|
||||
|
||||
3. En cas de succès (HTTP 201), la réponse JSON contient le numéro de l’issue créée.
|
||||
|
||||
Payload utilisé : `scripts/issue-84-payload.json` (titre + corps depuis `scripts/issue-84-body.txt`).
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
# Crée une issue Gitea via l'API.
|
||||
# Usage: GITEA_TOKEN=xxx ./scripts/create-gitea-issue.sh
|
||||
# Ou: mettre le token dans .gitea-token à la racine du projet.
|
||||
|
||||
set -e
|
||||
BASE_URL="${GITEA_URL:-https://git.ptits-pas.fr/api/v1}"
|
||||
REPO="jmartin/petitspas"
|
||||
|
||||
if [ -z "$GITEA_TOKEN" ]; then
|
||||
if [ -f .gitea-token ]; then
|
||||
GITEA_TOKEN=$(cat .gitea-token)
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$GITEA_TOKEN" ]; then
|
||||
echo "Définir GITEA_TOKEN ou créer .gitea-token avec votre token Gitea."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TITLE="$1"
|
||||
BODY="$2"
|
||||
if [ -z "$TITLE" ]; then
|
||||
echo "Usage: $0 \"Titre de l'issue\" \"Corps (optionnel)\""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build JSON (escape body for JSON)
|
||||
BODY_ESC=$(echo "$BODY" | jq -Rs . 2>/dev/null || echo "null")
|
||||
if [ "$BODY_ESC" = "null" ] || [ -z "$BODY" ]; then
|
||||
PAYLOAD=$(jq -n --arg t "$TITLE" '{title: $t}')
|
||||
else
|
||||
PAYLOAD=$(jq -n --arg t "$TITLE" --arg b "$BODY" '{title: $t, body: $b}')
|
||||
fi
|
||||
|
||||
RESP=$(curl -s -w "\n%{http_code}" -X POST \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$PAYLOAD" \
|
||||
"$BASE_URL/repos/$REPO/issues")
|
||||
HTTP_CODE=$(echo "$RESP" | tail -1)
|
||||
BODY_RESP=$(echo "$RESP" | sed '$d')
|
||||
|
||||
if [ "$HTTP_CODE" = "201" ]; then
|
||||
ISSUE_NUM=$(echo "$BODY_RESP" | jq -r .number)
|
||||
echo "Issue #$ISSUE_NUM créée."
|
||||
echo "$BODY_RESP" | jq .
|
||||
else
|
||||
echo "Erreur HTTP $HTTP_CODE: $BODY_RESP"
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env bash
|
||||
# Poste un commentaire sur une issue Gitea puis la ferme.
|
||||
# Usage: GITEA_TOKEN=xxx ./scripts/gitea-close-issue-with-comment.sh <numéro> "Commentaire"
|
||||
# Ou: mettre le token dans .gitea-token à la racine du projet.
|
||||
# Exemple: ./scripts/gitea-close-issue-with-comment.sh 15 "Livré : panneau Paramètres opérationnel."
|
||||
|
||||
set -e
|
||||
ISSUE="${1:?Usage: $0 <numéro_issue> \"Commentaire\"}"
|
||||
COMMENT="${2:?Usage: $0 <numéro_issue> \"Commentaire\"}"
|
||||
BASE_URL="${GITEA_URL:-https://git.ptits-pas.fr/api/v1}"
|
||||
REPO="jmartin/petitspas"
|
||||
|
||||
if [ -z "$GITEA_TOKEN" ]; then
|
||||
if [ -f .gitea-token ]; then
|
||||
GITEA_TOKEN=$(cat .gitea-token)
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$GITEA_TOKEN" ]; then
|
||||
echo "Définir GITEA_TOKEN ou créer .gitea-token avec votre token Gitea."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 1) Poster le commentaire
|
||||
echo "Ajout du commentaire sur l'issue #$ISSUE..."
|
||||
# Échapper pour JSON (guillemets et backslash)
|
||||
COMMENT_ESC=$(printf '%s' "$COMMENT" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\r//g')
|
||||
PAYLOAD="{\"body\":\"$COMMENT_ESC\"}"
|
||||
RESP=$(curl -s -w "\n%{http_code}" -X POST \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$PAYLOAD" \
|
||||
"$BASE_URL/repos/$REPO/issues/$ISSUE/comments")
|
||||
HTTP_CODE=$(echo "$RESP" | tail -1)
|
||||
BODY=$(echo "$RESP" | sed '$d')
|
||||
|
||||
if [ "$HTTP_CODE" != "201" ]; then
|
||||
echo "Erreur HTTP $HTTP_CODE lors du commentaire: $BODY"
|
||||
exit 1
|
||||
fi
|
||||
echo "Commentaire ajouté."
|
||||
|
||||
# 2) Fermer l'issue
|
||||
echo "Fermeture de l'issue #$ISSUE..."
|
||||
RESP2=$(curl -s -w "\n%{http_code}" -X PATCH \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"state":"closed"}' \
|
||||
"$BASE_URL/repos/$REPO/issues/$ISSUE")
|
||||
HTTP_CODE2=$(echo "$RESP2" | tail -1)
|
||||
BODY2=$(echo "$RESP2" | sed '$d')
|
||||
|
||||
if [ "$HTTP_CODE2" = "200" ] || [ "$HTTP_CODE2" = "201" ]; then
|
||||
echo "Issue #$ISSUE fermée."
|
||||
else
|
||||
echo "Erreur HTTP $HTTP_CODE2: $BODY2"
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
# Ferme une issue Gitea via l'API.
|
||||
# Usage: GITEA_TOKEN=votre_token ./scripts/gitea-close-issue.sh [numéro]
|
||||
# Exemple: GITEA_TOKEN=xxx ./scripts/gitea-close-issue.sh 83
|
||||
|
||||
set -e
|
||||
ISSUE="${1:-83}"
|
||||
BASE_URL="${GITEA_URL:-https://git.ptits-pas.fr/api/v1}"
|
||||
REPO="jmartin/petitspas"
|
||||
|
||||
if [ -z "$GITEA_TOKEN" ]; then
|
||||
if [ -f .gitea-token ]; then
|
||||
GITEA_TOKEN=$(cat .gitea-token)
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$GITEA_TOKEN" ]; then
|
||||
echo "Définir GITEA_TOKEN ou créer .gitea-token avec votre token Gitea."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Fermeture de l'issue #$ISSUE..."
|
||||
RESP=$(curl -s -w "\n%{http_code}" -X PATCH \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"state":"closed"}' \
|
||||
"$BASE_URL/repos/$REPO/issues/$ISSUE")
|
||||
HTTP_CODE=$(echo "$RESP" | tail -1)
|
||||
BODY=$(echo "$RESP" | sed '$d')
|
||||
|
||||
if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "201" ]; then
|
||||
echo "Issue #$ISSUE fermée."
|
||||
else
|
||||
echo "Erreur HTTP $HTTP_CODE: $BODY"
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,14 @@
|
||||
Correctifs et améliorations de la modale de changement de mot de passe obligatoire affichée à la première connexion admin.
|
||||
|
||||
**Périmètre :**
|
||||
- Ajustements visuels / UX de la modale (ChangePasswordDialog)
|
||||
- Cohérence charte graphique, espacements, lisibilité
|
||||
- Comportement (validation, messages d'erreur, fermeture)
|
||||
- Lien de test en debug sur l'écran login (« Test modale MDP ») pour faciliter les réglages
|
||||
|
||||
**Tâches :**
|
||||
- [ ] Revoir le design de la modale (relief, bordures, couleurs)
|
||||
- [ ] Vérifier les champs (MDP actuel, nouveau, confirmation) et validations
|
||||
- [ ] Ajuster les textes et messages d'erreur
|
||||
- [ ] Tester sur mobile et desktop
|
||||
- [ ] Retirer ou conditionner le lien « Test modale MDP » en production si besoin
|
||||
@@ -0,0 +1 @@
|
||||
{"title": "[Frontend] Bug – Correctifs modale Changement MDP (première connexion admin)", "body": "Correctifs et améliorations de la modale de changement de mot de passe obligatoire affichée à la première connexion admin.\n\n**Périmètre :**\n- Ajustements visuels / UX de la modale (ChangePasswordDialog)\n- Cohérence charte graphique, espacements, lisibilité\n- Comportement (validation, messages d'erreur, fermeture)\n- Lien de test en debug sur l'écran login (« Test modale MDP ») pour faciliter les réglages\n\n**Tâches :**\n- [ ] Revoir le design de la modale (relief, bordures, couleurs)\n- [ ] Vérifier les champs (MDP actuel, nouveau, confirmation) et validations\n- [ ] Ajuster les textes et messages d'erreur\n- [ ] Tester sur mobile et desktop\n- [ ] Retirer ou conditionner le lien « Test modale MDP » en production si besoin\n"}
|
||||
Executable
+61
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
# ============================================================
|
||||
# reset-and-seed-db.sh : Réinitialise la BDD et injecte les données de test
|
||||
# Usage : depuis la racine du projet ptitspas-app
|
||||
# ./scripts/reset-and-seed-db.sh
|
||||
# ============================================================
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
echo "=== Réinitialisation BDD + seed données de test ==="
|
||||
echo "Projet : $PROJECT_ROOT"
|
||||
echo ""
|
||||
|
||||
# 1) Arrêter les conteneurs et supprimer le volume Postgres
|
||||
echo "[1/4] Arrêt des conteneurs et suppression du volume Postgres..."
|
||||
docker compose down -v 2>/dev/null || docker-compose down -v 2>/dev/null || true
|
||||
|
||||
# 2) Démarrer uniquement la base
|
||||
echo "[2/4] Démarrage du conteneur database..."
|
||||
docker compose up -d database 2>/dev/null || docker-compose up -d database 2>/dev/null
|
||||
|
||||
# 3) Attendre que Postgres soit prêt
|
||||
echo "[3/4] Attente du démarrage de Postgres..."
|
||||
for i in {1..30}; do
|
||||
if docker exec ptitspas-postgres pg_isready -U admin -d ptitpas_db 2>/dev/null; then
|
||||
echo " Postgres prêt."
|
||||
break
|
||||
fi
|
||||
if [ "$i" -eq 30 ]; then
|
||||
echo "Erreur : Postgres ne répond pas après 30 tentatives."
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Petit délai supplémentaire pour la fin de l'init (BDD.sql)
|
||||
sleep 2
|
||||
|
||||
# 4) Exécuter le seed des données de test
|
||||
echo "[4/4] Exécution du seed (03_seed_test_data.sql)..."
|
||||
docker exec -i ptitspas-postgres psql -U admin -d ptitpas_db < database/seed/03_seed_test_data.sql
|
||||
|
||||
echo ""
|
||||
echo "=== Terminé ==="
|
||||
echo "Comptes de test (mot de passe : password) :"
|
||||
echo " - admin@ptits-pas.fr (super_admin, créé par BDD.sql)"
|
||||
echo " - sophie.bernard@ptits-pas.fr (administrateur)"
|
||||
echo " - lucas.moreau@ptits-pas.fr (gestionnaire)"
|
||||
echo " - marie.dubois@ptits-pas.fr (assistante maternelle)"
|
||||
echo " - fatima.elmansouri@ptits-pas.fr (assistante maternelle)"
|
||||
echo " - claire.martin@ptits-pas.fr (parent)"
|
||||
echo " - thomas.martin@ptits-pas.fr (parent)"
|
||||
echo " - amelie.durand@ptits-pas.fr (parent)"
|
||||
echo " - julien.rousseau@ptits-pas.fr (parent)"
|
||||
echo " - david.lecomte@ptits-pas.fr (parent)"
|
||||
echo ""
|
||||
echo "Tu peux redémarrer le backend/frontend si besoin : docker compose up -d"
|
||||
Reference in New Issue
Block a user