petitspas/frontend/lib/screens/auth/parent_register_step5_screen.dart
Julien Martin fdd1e06e77 [#101] [Frontend] Inscription parent — API, soumission et validation
Squash merge de develop vers master.

Livrables principaux (ticket #101 et mise au point associée) :
- Branchement du formulaire d'inscription parent sur POST /api/v1/auth/register/parent
- Payload DTO (parents, enfants, photos base64, CGU) et services Auth
- Parcours gestionnaire : cartes dossiers, wizard validation famille, images authentifiées
- Scripts d'inscription test (Martin, Durand/Rousseau, Lecomte) ; .gitignore .cursor/

Inclut également les ajustements develop fusionnés dans ce lot (inscription AM, champs relais, etc.).

Closes #101

Made-with: Cursor
2026-04-11 18:07:24 +02:00

310 lines
12 KiB
Dart

import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';
import 'package:go_router/go_router.dart';
import 'dart:math' as math;
import '../../models/user_registration_data.dart';
import '../../models/card_assets.dart';
import '../../config/display_config.dart';
import '../../widgets/hover_relief_widget.dart';
import '../../widgets/image_button.dart';
import '../../widgets/custom_navigation_button.dart';
import '../../widgets/personal_info_form_screen.dart';
import '../../widgets/child_card_widget.dart';
import '../../widgets/presentation_form_screen.dart';
import '../../services/auth_service.dart';
class ParentRegisterStep5Screen extends StatefulWidget {
const ParentRegisterStep5Screen({super.key});
@override
_ParentRegisterStep5ScreenState createState() => _ParentRegisterStep5ScreenState();
}
class _ParentRegisterStep5ScreenState extends State<ParentRegisterStep5Screen> {
bool _isSubmitting = false;
Future<void> _submitRegistration(BuildContext context, UserRegistrationData data) async {
if (_isSubmitting) return;
setState(() => _isSubmitting = true);
try {
await AuthService.registerParent(data);
if (!context.mounted) return;
_showSuccessModal(context);
} catch (e) {
if (!context.mounted) return;
final msg = e.toString().replaceAll('Exception: ', '');
await showDialog<void>(
context: context,
builder: (ctx) => AlertDialog(
title: Text('Envoi impossible', style: GoogleFonts.merienda(fontWeight: FontWeight.bold)),
content: Text(msg, style: GoogleFonts.merienda(fontSize: 14)),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: Text('OK', style: GoogleFonts.merienda(fontWeight: FontWeight.bold)),
),
],
),
);
} finally {
if (mounted) setState(() => _isSubmitting = false);
}
}
@override
Widget build(BuildContext context) {
final registrationData = Provider.of<UserRegistrationData>(context);
final screenSize = MediaQuery.of(context).size;
final config = DisplayConfig.fromContext(context, mode: DisplayMode.readonly);
return Scaffold(
body: Stack(
children: [
Positioned.fill(
child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeatY),
),
Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 40.0),
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: config.isMobile ? 0 : screenSize.width / 4.0
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text('Étape 5/5', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)),
const SizedBox(height: 20),
Text('Récapitulatif de votre demande', style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87), textAlign: TextAlign.center),
const SizedBox(height: 30),
// Carte Parent 1
_buildParent1(context, registrationData),
const SizedBox(height: 20),
// Carte Parent 2 (si présent)
if (registrationData.parent2 != null) ...[
_buildParent2(context, registrationData),
const SizedBox(height: 20),
],
// Cartes Enfants
...registrationData.children.asMap().entries.map((entry) =>
Column(
children: [
_buildChildCard(context, entry.value, entry.key),
const SizedBox(height: 20),
],
)
),
// Carte Motivation
_buildMotivation(context, registrationData),
const SizedBox(height: 40),
// Boutons Mobile (Retour + Soumettre) ou Bouton Soumettre Desktop
if (config.isMobile)
Padding(
padding: EdgeInsets.symmetric(horizontal: screenSize.width * 0.05),
child: Row(
children: [
Expanded(
child: HoverReliefWidget(
child: CustomNavigationButton(
text: 'Précédent',
style: NavigationButtonStyle.purple,
onPressed: () {
if (context.canPop()) {
context.pop();
} else {
context.go('/parent-register-step4');
}
},
width: double.infinity,
height: 50,
fontSize: 16,
),
),
),
const SizedBox(width: 16),
Expanded(
child: HoverReliefWidget(
child: CustomNavigationButton(
text: _isSubmitting ? 'Envoi…' : 'Soumettre',
style: NavigationButtonStyle.green,
onPressed: _isSubmitting
? () {}
: () => _submitRegistration(context, registrationData),
width: double.infinity,
height: 50,
fontSize: 16,
),
),
),
],
),
)
else
_isSubmitting
? const Padding(
padding: EdgeInsets.all(16),
child: CircularProgressIndicator(),
)
: ImageButton(
bg: 'assets/images/bg_green.png',
text: 'Soumettre ma demande',
textColor: const Color(0xFF2D6A4F),
width: 350,
height: 50,
fontSize: 18,
onPressed: () => _submitRegistration(context, registrationData),
),
],
),
),
),
),
// Chevrons desktop uniquement
if (!config.isMobile)
Positioned(
top: screenSize.height / 2 - 20,
left: 40,
child: IconButton(
icon: Transform(
alignment: Alignment.center,
transform: Matrix4.rotationY(math.pi),
child: Image.asset('assets/images/chevron_right.png', height: 40),
),
onPressed: () {
if (context.canPop()) {
context.pop();
} else {
context.go('/parent-register-step4');
}
},
tooltip: 'Retour',
),
),
],
),
);
}
Widget _buildParent1(BuildContext context, UserRegistrationData data) {
return PersonalInfoFormScreen(
mode: DisplayMode.readonly,
embedContentOnly: true,
stepText: '',
title: 'Informations du Parent Principal',
cardColor: CardColorHorizontal.peach,
initialData: PersonalInfoData(
firstName: data.parent1.firstName,
lastName: data.parent1.lastName,
phone: data.parent1.phone,
email: data.parent1.email,
address: data.parent1.address,
postalCode: data.parent1.postalCode,
city: data.parent1.city,
),
onSubmit: (d, {hasSecondPerson, sameAddress}) {},
previousRoute: '',
onEdit: () => context.go('/parent-register-step1'),
);
}
Widget _buildParent2(BuildContext context, UserRegistrationData data) {
if (data.parent2 == null) return const SizedBox();
return PersonalInfoFormScreen(
mode: DisplayMode.readonly,
embedContentOnly: true,
stepText: '',
title: 'Informations du Deuxième Parent',
cardColor: CardColorHorizontal.blue,
initialData: PersonalInfoData(
firstName: data.parent2!.firstName,
lastName: data.parent2!.lastName,
phone: data.parent2!.phone,
email: data.parent2!.email,
address: data.parent2!.address,
postalCode: data.parent2!.postalCode,
city: data.parent2!.city,
),
onSubmit: (d, {hasSecondPerson, sameAddress}) {},
previousRoute: '',
onEdit: () => context.go('/parent-register-step2'),
);
}
Widget _buildChildCard(BuildContext context, ChildData child, int index) {
// Note: Le titre est maintenant intégré dans la carte ChildCardWidget en mode readonly
return Column(
children: [
ChildCardWidget(
key: ValueKey('child_readonly_$index'),
childData: child,
childIndex: index,
mode: DisplayMode.readonly,
onPickImage: () {},
onClearImage: () {},
onDateSelect: () {},
onFirstNameChanged: (v) {},
onLastNameChanged: (v) {},
onGenreChanged: (v) {},
onTogglePhotoConsent: (v) {},
onToggleIsUnborn: (v) {},
onRemove: () {},
canBeRemoved: false,
onEdit: () => context.go('/parent-register-step3', extra: {'childIndex': index}),
),
],
);
}
Widget _buildMotivation(BuildContext context, UserRegistrationData data) {
return PresentationFormScreen(
mode: DisplayMode.readonly,
embedContentOnly: true,
stepText: '',
title: 'Votre Motivation',
cardColor: CardColorHorizontal.green, // Changé de pink à green
textFieldHint: '',
initialText: data.motivationText,
initialCguAccepted: data.cguAccepted,
previousRoute: '',
onSubmit: (t, c) {},
onEdit: () => context.go('/parent-register-step4'),
);
}
void _showSuccessModal(BuildContext context) {
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (BuildContext dialogContext) {
return AlertDialog(
title: Text(
'Demande enregistrée',
style: GoogleFonts.merienda(fontWeight: FontWeight.bold),
),
content: Text(
'Votre dossier a bien été pris en compte. Un gestionnaire le validera bientôt.',
style: GoogleFonts.merienda(fontSize: 14),
),
actions: <Widget>[
TextButton(
child: Text('OK', style: GoogleFonts.merienda(fontWeight: FontWeight.bold)),
onPressed: () {
Navigator.of(dialogContext).pop();
context.go('/login');
},
),
],
);
},
);
}
}