feat(auth): Supprime l'ancien workflow d'inscription parent et ajoute les assets pour le nouveau workflow

This commit is contained in:
Julien Martin 2025-05-05 12:51:32 +02:00
parent e6d3c41ecc
commit bbdacd68aa
15 changed files with 70 additions and 934 deletions

2
.gitignore vendored
View File

@ -46,6 +46,8 @@ coverage/
*.tmp *.tmp
*.temp *.temp
.cache/ .cache/
Archives/**
Xcf/**
# Release notes # Release notes
CHANGELOG.md CHANGELOG.md

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 KiB

View File

@ -1,73 +0,0 @@
class Child {
final String id;
final String firstName;
final String lastName;
final DateTime? birthDate;
final DateTime? expectedBirthDate;
final String? photoUrl;
final bool hasPhotoConsent;
final DateTime? photoConsentDate;
final String status; // 'unborn', 'active', 'schooled'
final List<String> parentIds;
final bool isMultipleBirth; // true pour jumeaux, triplés, etc.
final DateTime createdAt;
final DateTime updatedAt;
Child({
required this.id,
required this.firstName,
required this.lastName,
this.birthDate,
this.expectedBirthDate,
this.photoUrl,
required this.hasPhotoConsent,
this.photoConsentDate,
required this.status,
required this.parentIds,
required this.isMultipleBirth,
required this.createdAt,
required this.updatedAt,
});
factory Child.fromJson(Map<String, dynamic> json) {
return Child(
id: json['id'],
firstName: json['firstName'],
lastName: json['lastName'],
birthDate: json['birthDate'] != null
? DateTime.parse(json['birthDate'])
: null,
expectedBirthDate: json['expectedBirthDate'] != null
? DateTime.parse(json['expectedBirthDate'])
: null,
photoUrl: json['photoUrl'],
hasPhotoConsent: json['hasPhotoConsent'] ?? false,
photoConsentDate: json['photoConsentDate'] != null
? DateTime.parse(json['photoConsentDate'])
: null,
status: json['status'] ?? 'unborn',
parentIds: List<String>.from(json['parentIds'] ?? []),
isMultipleBirth: json['isMultipleBirth'] ?? false,
createdAt: DateTime.parse(json['createdAt']),
updatedAt: DateTime.parse(json['updatedAt']),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'firstName': firstName,
'lastName': lastName,
'birthDate': birthDate?.toIso8601String(),
'expectedBirthDate': expectedBirthDate?.toIso8601String(),
'photoUrl': photoUrl,
'hasPhotoConsent': hasPhotoConsent,
'photoConsentDate': photoConsentDate?.toIso8601String(),
'status': status,
'parentIds': parentIds,
'isMultipleBirth': isMultipleBirth,
'createdAt': createdAt.toIso8601String(),
'updatedAt': updatedAt.toIso8601String(),
};
}
}

View File

@ -1,63 +0,0 @@
class Parent {
final String id;
final String userId;
final String firstName;
final String lastName;
final String email;
final String phoneNumber;
final String address;
final String city;
final String postalCode;
final List<String> childrenIds;
final DateTime createdAt;
final DateTime updatedAt;
Parent({
required this.id,
required this.userId,
required this.firstName,
required this.lastName,
required this.email,
required this.phoneNumber,
required this.address,
required this.city,
required this.postalCode,
required this.childrenIds,
required this.createdAt,
required this.updatedAt,
});
factory Parent.fromJson(Map<String, dynamic> json) {
return Parent(
id: json['id'],
userId: json['userId'],
firstName: json['firstName'],
lastName: json['lastName'],
email: json['email'],
phoneNumber: json['phoneNumber'],
address: json['address'],
city: json['city'],
postalCode: json['postalCode'],
childrenIds: List<String>.from(json['childrenIds']),
createdAt: DateTime.parse(json['createdAt']),
updatedAt: DateTime.parse(json['updatedAt']),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'userId': userId,
'firstName': firstName,
'lastName': lastName,
'email': email,
'phoneNumber': phoneNumber,
'address': address,
'city': city,
'postalCode': postalCode,
'childrenIds': childrenIds,
'createdAt': createdAt.toIso8601String(),
'updatedAt': updatedAt.toIso8601String(),
};
}
}

View File

@ -1,27 +1,23 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import '../screens/auth/login_screen.dart'; import '../screens/auth/login_screen.dart';
import '../screens/auth/parent_register_screen.dart';
import '../screens/home/home_screen.dart'; import '../screens/home/home_screen.dart';
class AppRouter { class AppRouter {
static const String login = '/login'; static const String login = '/login';
static const String parentRegister = '/parent-register';
static const String home = '/home'; static const String home = '/home';
static Route<dynamic> generateRoute(RouteSettings settings) { static Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) { switch (settings.name) {
case login: case login:
return MaterialPageRoute(builder: (_) => const LoginScreen()); return MaterialPageRoute(builder: (_) => const LoginScreen());
case parentRegister:
return MaterialPageRoute(builder: (_) => const ParentRegisterScreen());
case home: case home:
return MaterialPageRoute(builder: (_) => const HomeScreen()); return MaterialPageRoute(builder: (_) => const HomeScreen());
default: default:
return MaterialPageRoute( return MaterialPageRoute(
builder: (_) => Scaffold( builder: (_) => Scaffold(
body: Center( body: Center(
child: Text('Route non définie: ${settings.name}'), child: Text('Route non définie: ${settings.name}'),
), ),
), ),
); );

View File

@ -197,19 +197,7 @@ class _LoginPageState extends State<LoginPage> {
const SizedBox(height: 10), const SizedBox(height: 10),
// Lien de création de compte // Lien de création de compte
Center( Center(
child: TextButton( child: Container(), // Suppression du bouton 'Créer un compte'
onPressed: () {
Navigator.pushNamed(context, '/parent-register');
},
child: Text(
'Créer un compte',
style: GoogleFonts.merienda(
fontSize: 16,
color: const Color(0xFF2D6A4F),
decoration: TextDecoration.underline,
),
),
),
), ),
const SizedBox(height: 20), // Réduit l'espacement en bas const SizedBox(height: 20), // Réduit l'espacement en bas
], ],

View File

@ -1,752 +0,0 @@
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:go_router/go_router.dart';
import '../../services/auth_service.dart';
import '../../theme/app_theme.dart';
import 'dart:convert';
class ChildData {
final TextEditingController firstNameController = TextEditingController();
final TextEditingController lastNameController = TextEditingController();
DateTime? birthDate;
DateTime? expectedBirthDate;
XFile? photo;
bool hasPhotoConsent = false;
bool isMultipleBirth = false;
bool isUnborn = false;
}
class ParentRegisterScreen extends StatefulWidget {
const ParentRegisterScreen({super.key});
@override
State<ParentRegisterScreen> createState() => _ParentRegisterScreenState();
}
class _ParentRegisterScreenState extends State<ParentRegisterScreen> {
final _formKey = GlobalKey<FormState>();
final _authService = AuthService();
int _currentStep = 0;
bool _isLoading = false;
bool _hasPartner = false;
bool _hasAcceptedCGU = false;
bool _partnerSameAddress = false;
// Contrôleurs pour le parent 1
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _firstNameController = TextEditingController();
final _lastNameController = TextEditingController();
final _phoneController = TextEditingController();
final _addressController = TextEditingController();
final _cityController = TextEditingController();
final _postalCodeController = TextEditingController();
final _presentationController = TextEditingController();
// Contrôleurs pour le parent 2
final _partnerFirstNameController = TextEditingController();
final _partnerLastNameController = TextEditingController();
final _partnerEmailController = TextEditingController();
final _partnerPhoneController = TextEditingController();
final _partnerAddressController = TextEditingController();
final _partnerCityController = TextEditingController();
final _partnerPostalCodeController = TextEditingController();
// Liste des enfants
final List<ChildData> _children = [ChildData()];
final _motivationController = TextEditingController();
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
_firstNameController.dispose();
_lastNameController.dispose();
_phoneController.dispose();
_addressController.dispose();
_cityController.dispose();
_postalCodeController.dispose();
_presentationController.dispose();
_partnerFirstNameController.dispose();
_partnerLastNameController.dispose();
_partnerEmailController.dispose();
_partnerPhoneController.dispose();
_partnerAddressController.dispose();
_partnerCityController.dispose();
_partnerPostalCodeController.dispose();
for (var child in _children) {
child.firstNameController.dispose();
child.lastNameController.dispose();
}
_motivationController.dispose();
super.dispose();
}
Future<void> _pickImage(ChildData child) async {
final ImagePicker picker = ImagePicker();
final XFile? image = await picker.pickImage(source: ImageSource.gallery);
if (image != null) {
setState(() {
child.photo = image;
});
}
}
Future<String?> _uploadImage(XFile image, String userId) async {
// En mode démonstration, on retourne juste un chemin local
return image.path;
}
Future<void> _register() async {
if (!_formKey.currentState!.validate()) return;
setState(() => _isLoading = true);
try {
final List<Map<String, dynamic>> childrenData = [];
for (var child in _children) {
childrenData.add({
'firstName': child.firstNameController.text,
'lastName': child.lastNameController.text,
'birthDate': child.isUnborn ? null : child.birthDate,
'expectedBirthDate': child.isUnborn ? child.expectedBirthDate : null,
'photo': child.photo != null ? base64Encode(child.photo!.readAsBytesSync()) : null,
'hasPhotoConsent': child.hasPhotoConsent,
'isMultipleBirth': child.isMultipleBirth,
});
}
await _authService.registerParent(
email: _emailController.text,
password: _passwordController.text,
firstName: _firstNameController.text,
lastName: _lastNameController.text,
phoneNumber: _phoneController.text,
address: _addressController.text,
city: _cityController.text,
postalCode: _postalCodeController.text,
presentation: _presentationController.text,
hasAcceptedCGU: _hasAcceptedCGU,
partnerFirstName: _hasPartner ? _partnerFirstNameController.text : null,
partnerLastName: _hasPartner ? _partnerLastNameController.text : null,
partnerEmail: _hasPartner ? _partnerEmailController.text : null,
partnerPhoneNumber: _hasPartner ? _partnerPhoneController.text : null,
partnerAddress: _hasPartner
? (_partnerSameAddress
? _addressController.text
: _partnerAddressController.text)
: null,
partnerCity: _hasPartner
? (_partnerSameAddress ? _cityController.text : _partnerCityController.text)
: null,
partnerPostalCode: _hasPartner
? (_partnerSameAddress ? _postalCodeController.text : _partnerPostalCodeController.text)
: null,
children: childrenData,
motivation: _motivationController.text,
);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Inscription réussie ! Votre compte est en attente de validation.'),
backgroundColor: Colors.green,
),
);
context.pop();
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors de l\'inscription: $e'),
backgroundColor: Colors.red,
),
);
}
} finally {
if (mounted) {
setState(() => _isLoading = false);
}
}
}
Widget _buildChildForm(ChildData child, int index) {
return Card(
margin: const EdgeInsets.symmetric(vertical: 8.0),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Enfant ${index + 1}',
style: Theme.of(context).textTheme.titleMedium,
),
if (index > 0)
IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
setState(() {
_children.removeAt(index);
});
},
),
],
),
if (child.photo != null)
CircleAvatar(
radius: 50,
backgroundImage: NetworkImage(child.photo!.path),
),
TextButton(
onPressed: () => _pickImage(child),
child: const Text('Ajouter une photo'),
),
SwitchListTile(
title: const Text('Enfant à naître'),
value: child.isUnborn,
onChanged: (value) => setState(() => child.isUnborn = value),
),
TextFormField(
controller: child.firstNameController,
decoration: const InputDecoration(labelText: 'Prénom de l\'enfant'),
),
TextFormField(
controller: child.lastNameController,
decoration: const InputDecoration(labelText: 'Nom de l\'enfant'),
),
if (!child.isUnborn)
ListTile(
title: const Text('Date de naissance'),
subtitle: Text(child.birthDate != null
? '${child.birthDate!.day}/${child.birthDate!.month}/${child.birthDate!.year}'
: 'Non définie'),
trailing: IconButton(
icon: const Icon(Icons.calendar_today),
onPressed: () async {
final date = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(2000),
lastDate: DateTime.now(),
);
if (date != null) {
setState(() => child.birthDate = date);
}
},
),
),
if (child.isUnborn)
ListTile(
title: const Text('Date prévue'),
subtitle: Text(child.expectedBirthDate != null
? '${child.expectedBirthDate!.day}/${child.expectedBirthDate!.month}/${child.expectedBirthDate!.year}'
: 'Non définie'),
trailing: IconButton(
icon: const Icon(Icons.calendar_today),
onPressed: () async {
final date = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime.now(),
lastDate: DateTime.now().add(const Duration(days: 365)),
);
if (date != null) {
setState(() => child.expectedBirthDate = date);
}
},
),
),
SwitchListTile(
title: const Text('Naissance multiple'),
subtitle: const Text('Jumeaux, triplés, etc.'),
value: child.isMultipleBirth,
onChanged: (value) => setState(() => child.isMultipleBirth = value),
),
if (child.photo != null)
SwitchListTile(
title: const Text('Consentement photo'),
subtitle: const Text('J\'autorise l\'utilisation de la photo de mon enfant'),
value: child.hasPhotoConsent,
onChanged: (value) => setState(() => child.hasPhotoConsent = value),
),
],
),
),
);
}
List<Step> _getSteps() {
return [
// Étape 1 : Parent 1
Step(
title: const Text('Informations parent 1'),
content: Column(
children: [
TextFormField(
controller: _emailController,
decoration: const InputDecoration(labelText: 'Email'),
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre email';
}
return null;
},
),
TextFormField(
controller: _passwordController,
decoration: const InputDecoration(labelText: 'Mot de passe'),
obscureText: true,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer un mot de passe';
}
if (value.length < 6) {
return 'Le mot de passe doit contenir au moins 6 caractères';
}
return null;
},
),
TextFormField(
controller: _firstNameController,
decoration: const InputDecoration(labelText: 'Prénom'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre prénom';
}
return null;
},
),
TextFormField(
controller: _lastNameController,
decoration: const InputDecoration(labelText: 'Nom'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre nom';
}
return null;
},
),
TextFormField(
controller: _phoneController,
decoration: const InputDecoration(labelText: 'Téléphone'),
keyboardType: TextInputType.phone,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre numéro de téléphone';
}
return null;
},
),
TextFormField(
controller: _addressController,
decoration: const InputDecoration(labelText: 'Adresse'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre adresse';
}
return null;
},
),
TextFormField(
controller: _cityController,
decoration: const InputDecoration(labelText: 'Ville'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre ville';
}
return null;
},
),
TextFormField(
controller: _postalCodeController,
decoration: const InputDecoration(labelText: 'Code postal'),
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre code postal';
}
return null;
},
),
TextFormField(
controller: _presentationController,
decoration: const InputDecoration(labelText: 'Présentation'),
maxLines: 3,
),
],
),
isActive: _currentStep >= 0,
),
// Étape 2 : Parent 2
Step(
title: const Text('Parent 2'),
content: Column(
children: [
SwitchListTile(
title: const Text('Ajouter un deuxième parent'),
value: _hasPartner,
onChanged: (value) => setState(() => _hasPartner = value),
),
if (_hasPartner) ...[
SwitchListTile(
title: const Text('Adresse identique au parent 1'),
value: _partnerSameAddress,
onChanged: (value) {
setState(() {
_partnerSameAddress = value;
if (value) {
_partnerAddressController.text = _addressController.text;
_partnerCityController.text = _cityController.text;
_partnerPostalCodeController.text = _postalCodeController.text;
} else {
_partnerAddressController.clear();
_partnerCityController.clear();
_partnerPostalCodeController.clear();
}
});
},
),
TextFormField(
controller: _partnerFirstNameController,
decoration: const InputDecoration(labelText: 'Prénom du deuxième parent'),
validator: (value) {
if (_hasPartner && (value == null || value.isEmpty)) {
return 'Veuillez entrer le prénom';
}
return null;
},
),
TextFormField(
controller: _partnerLastNameController,
decoration: const InputDecoration(labelText: 'Nom du deuxième parent'),
validator: (value) {
if (_hasPartner && (value == null || value.isEmpty)) {
return 'Veuillez entrer le nom';
}
return null;
},
),
TextFormField(
controller: _partnerEmailController,
decoration: const InputDecoration(labelText: 'Email du deuxième parent'),
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (_hasPartner && (value == null || value.isEmpty)) {
return 'Veuillez entrer l\'email';
}
return null;
},
),
TextFormField(
controller: _partnerPhoneController,
decoration: const InputDecoration(labelText: 'Téléphone du deuxième parent'),
keyboardType: TextInputType.phone,
),
if (!_partnerSameAddress) ...[
TextFormField(
controller: _partnerAddressController,
decoration: const InputDecoration(labelText: 'Adresse du deuxième parent'),
validator: (value) {
if (_hasPartner && !_partnerSameAddress && (value == null || value.isEmpty)) {
return 'Veuillez entrer l\'adresse';
}
return null;
},
),
TextFormField(
controller: _partnerCityController,
decoration: const InputDecoration(labelText: 'Ville du deuxième parent'),
validator: (value) {
if (_hasPartner && !_partnerSameAddress && (value == null || value.isEmpty)) {
return 'Veuillez entrer la ville';
}
return null;
},
),
TextFormField(
controller: _partnerPostalCodeController,
decoration: const InputDecoration(labelText: 'Code postal du deuxième parent'),
keyboardType: TextInputType.number,
validator: (value) {
if (_hasPartner && !_partnerSameAddress && (value == null || value.isEmpty)) {
return 'Veuillez entrer le code postal';
}
return null;
},
),
],
],
],
),
isActive: _currentStep >= 1,
),
// Étape 3 : Enfants
Step(
title: const Text('Enfants'),
content: Column(
children: [
..._children.asMap().entries.map((entry) => _buildChildForm(entry.value, entry.key)),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: () {
setState(() {
_children.add(ChildData());
});
},
icon: const Icon(Icons.add),
label: const Text('Ajouter un autre enfant'),
),
],
),
isActive: _currentStep >= 2,
),
// Étape 4 : Description de la situation
Step(
title: const Text('Description de votre situation'),
content: Column(
children: [
TextFormField(
controller: _motivationController,
decoration: const InputDecoration(
labelText: 'Décrivez votre situation',
hintText: 'Expliquez-nous votre situation familiale et vos besoins...',
),
maxLines: 5,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez nous décrire votre situation';
}
return null;
},
),
],
),
isActive: _currentStep >= 3,
),
// Étape 5 : CGU
Step(
title: const Text('Conditions générales'),
content: Column(
children: [
SwitchListTile(
title: const Text('Conditions générales'),
subtitle: const Text('J\'accepte les conditions générales d\'utilisation'),
value: _hasAcceptedCGU,
onChanged: (value) => setState(() => _hasAcceptedCGU = value),
),
if (!_hasAcceptedCGU)
const Text(
'Vous devez accepter les conditions générales pour continuer',
style: TextStyle(color: Colors.red),
),
],
),
isActive: _currentStep >= 4,
),
// Étape 6 : Résumé
Step(
title: const Text('Résumé'),
content: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Veuillez vérifier vos informations avant validation :',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
const SizedBox(height: 16),
// Parent 1
const Text('Parent 1', style: TextStyle(fontWeight: FontWeight.bold)),
ListTile(
title: const Text('Email'),
subtitle: Text(_emailController.text),
trailing: IconButton(
icon: const Icon(Icons.edit),
onPressed: () => setState(() => _currentStep = 0),
),
),
ListTile(
title: const Text('Nom complet'),
subtitle: Text('${_firstNameController.text} ${_lastNameController.text}'),
trailing: IconButton(
icon: const Icon(Icons.edit),
onPressed: () => setState(() => _currentStep = 0),
),
),
ListTile(
title: const Text('Adresse'),
subtitle: Text('${_addressController.text}\n${_postalCodeController.text} ${_cityController.text}'),
trailing: IconButton(
icon: const Icon(Icons.edit),
onPressed: () => setState(() => _currentStep = 0),
),
),
ListTile(
title: const Text('Téléphone'),
subtitle: Text(_phoneController.text),
trailing: IconButton(
icon: const Icon(Icons.edit),
onPressed: () => setState(() => _currentStep = 0),
),
),
if (_presentationController.text.isNotEmpty)
ListTile(
title: const Text('Présentation'),
subtitle: Text(_presentationController.text),
trailing: IconButton(
icon: const Icon(Icons.edit),
onPressed: () => setState(() => _currentStep = 0),
),
),
// Parent 2
if (_hasPartner) ...[
const SizedBox(height: 16),
const Text('Parent 2', style: TextStyle(fontWeight: FontWeight.bold)),
ListTile(
title: const Text('Email'),
subtitle: Text(_partnerEmailController.text),
trailing: IconButton(
icon: const Icon(Icons.edit),
onPressed: () => setState(() => _currentStep = 1),
),
),
ListTile(
title: const Text('Nom complet'),
subtitle: Text('${_partnerFirstNameController.text} ${_partnerLastNameController.text}'),
trailing: IconButton(
icon: const Icon(Icons.edit),
onPressed: () => setState(() => _currentStep = 1),
),
),
ListTile(
title: const Text('Téléphone'),
subtitle: Text(_partnerPhoneController.text),
trailing: IconButton(
icon: const Icon(Icons.edit),
onPressed: () => setState(() => _currentStep = 1),
),
),
ListTile(
title: const Text('Adresse'),
subtitle: _partnerSameAddress
? const Text('Identique au parent 1')
: Text('${_partnerAddressController.text}\n${_partnerPostalCodeController.text} ${_partnerCityController.text}'),
trailing: IconButton(
icon: const Icon(Icons.edit),
onPressed: () => setState(() => _currentStep = 1),
),
),
],
// Enfants
const SizedBox(height: 16),
const Text('Enfants', style: TextStyle(fontWeight: FontWeight.bold)),
..._children.asMap().entries.map((entry) {
final child = entry.value;
return ListTile(
title: Text('Enfant ${entry.key + 1}'),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Prénom : ${child.firstNameController.text} ${child.lastNameController.text}'),
if (child.isUnborn)
Text('Date prévue : ${child.expectedBirthDate?.day}/${child.expectedBirthDate?.month}/${child.expectedBirthDate?.year}')
else
Text('Date de naissance : ${child.birthDate?.day}/${child.birthDate?.month}/${child.birthDate?.year}'),
if (child.isMultipleBirth)
const Text('Naissance multiple'),
],
),
trailing: IconButton(
icon: const Icon(Icons.edit),
onPressed: () => setState(() => _currentStep = 2),
),
);
}),
// Motivation
const SizedBox(height: 16),
const Text('Motivation', style: TextStyle(fontWeight: FontWeight.bold)),
ListTile(
title: const Text('Votre message'),
subtitle: Text(_motivationController.text),
trailing: IconButton(
icon: const Icon(Icons.edit),
onPressed: () => setState(() => _currentStep = 3),
),
),
],
),
),
isActive: _currentStep >= 5,
),
];
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Inscription Parent'),
),
body: Form(
key: _formKey,
child: Stepper(
currentStep: _currentStep,
onStepContinue: () {
if (_currentStep < _getSteps().length - 1) {
setState(() => _currentStep++);
} else if (_hasAcceptedCGU) {
_register();
}
},
onStepCancel: () {
if (_currentStep > 0) {
setState(() => _currentStep--);
} else {
Navigator.pop(context);
}
},
controlsBuilder: (context, details) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Row(
children: [
if (_currentStep > 0)
OutlinedButton(
onPressed: details.onStepCancel,
child: const Text('Retour'),
),
const SizedBox(width: 16),
if (_currentStep < _getSteps().length - 1)
ElevatedButton(
onPressed: details.onStepContinue,
child: const Text('Suivant'),
)
else
ElevatedButton(
onPressed: _hasAcceptedCGU ? details.onStepContinue : null,
child: _isLoading
? const CircularProgressIndicator()
: const Text('S\'inscrire'),
),
],
),
);
},
steps: _getSteps(),
),
),
);
}
}

View File

@ -1,8 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '../models/user.dart'; import '../models/user.dart';
import '../models/parent.dart';
import '../models/child.dart';
class AuthService { class AuthService {
static const String _usersKey = 'users'; static const String _usersKey = 'users';
@ -27,32 +25,6 @@ class AuthService {
throw Exception('Mode démonstration - Inscription désactivée'); throw Exception('Mode démonstration - Inscription désactivée');
} }
// Méthode pour s'inscrire en tant que parent (mode démonstration)
Future<void> registerParent({
required String email,
required String password,
required String firstName,
required String lastName,
required String phoneNumber,
required String address,
required String city,
required String postalCode,
String? presentation,
required bool hasAcceptedCGU,
String? partnerFirstName,
String? partnerLastName,
String? partnerEmail,
String? partnerPhoneNumber,
String? partnerAddress,
String? partnerCity,
String? partnerPostalCode,
required List<Map<String, dynamic>> children,
required String motivation,
}) async {
// En mode démonstration, on ne fait rien
await Future.delayed(const Duration(seconds: 2)); // Simule un délai de traitement
}
// Méthode pour se déconnecter (mode démonstration) // Méthode pour se déconnecter (mode démonstration)
static Future<void> logout() async { static Future<void> logout() async {
// Ne fait rien en mode démonstration // Ne fait rien en mode démonstration

3
ressources/cartes.png Normal file
View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?><Error><Code>AuthenticationFailed</Code><Message>Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature.
RequestId:32300bba-601e-0021-763f-bc1466000000
Time:2025-05-03T15:21:39.1449044Z</Message><AuthenticationErrorDetail>Signed expiry time [Sat, 03 May 2025 15:21:15 GMT] must be after signed start time [Sat, 03 May 2025 15:21:39 GMT]</AuthenticationErrorDetail></Error>

View File

@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>PtitsPas Propositions UI Wizard</title>
<link href="https://fonts.googleapis.com/css2?family=Merienda:wght@600&family=Inter:wght@400;500&display=swap" rel="stylesheet">
<style>
body{background:#fffef9;font-family:Inter,sans-serif;color:#2f2f2f;line-height:1.6;padding:2rem;}
h1,h2{font-family:Merienda,cursive;margin:.5rem 0;}
.grid{display:grid;gap:2rem;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));}
.card{background:#fff;border-radius:18px;box-shadow:0 4px 12px rgba(0,0,0,.05);padding:1.5rem;text-align:center;}
img{max-width:100%;height:auto;border-radius:12px;}
.palette{display:flex;justify-content:center;gap:.5rem;margin-top:.75rem;}
.swatch{width:28px;height:28px;border-radius:50%;}
.code{font-size:.75rem;margin-top:.25rem;}
</style>
</head>
<body>
<h1>Création de compte : idées denchaînement de cartes</h1>
<p>Chaque étape saffiche dans une «carte» pastel. En validant, la carte suivante glisse vers lavant (animation CSS: <code>transform: translateX(-100%)</code> + <code>opacity</code>). Trois styles proposés :</p>
<div class="grid">
<div class="card">
<h2>Style 1 : Watercolor</h2>
<img src="A_digital_graphic_design_image_displays_style_opti.png" alt="watercolor stack"/>
<div class="palette">
<div class="swatch" style="background:#FBC9C4"></div>
<div class="swatch" style="background:#FBD38B"></div>
<div class="swatch" style="background:#A9D8C6"></div>
</div>
<div class="code">#FBC9C4 · #FBD38B · #A9D8C6</div>
<p>Bords arrondis 22px, texture papier sur chaque carte.<br><b>Animation</b> : légère rotation (<em>tilt</em>) pour rappeler un paquet de cartes réaliste.</p>
</div>
<div class="card">
<h2>Style 2 : Minimal pastel</h2>
<img src="A_digital_graphic_design_image_displays_style_opti.png" alt="minimal stack"/>
<div class="palette">
<div class="swatch" style="background:#E3DFFD"></div>
<div class="swatch" style="background:#CFEAE3"></div>
<div class="swatch" style="background:#FFE88A"></div>
</div>
<div class="code">#E3DFFD · #CFEAE3 · #FFE88A</div>
<p>Cartes plates, ombre portée subtile (0 2 8 rgba0,05).<br><b>Animation</b> : slide horizontal + fondu rapide.</p>
</div>
<div class="card">
<h2>Style 3 : Modern vibrant</h2>
<img src="A_digital_graphic_design_image_displays_style_opti.png" alt="modern stack"/>
<div class="palette">
<div class="swatch" style="background:#FB86A2"></div>
<div class="swatch" style="background:#F3D468"></div>
<div class="swatch" style="background:#8AC1E3"></div>
</div>
<div class="code">#FB86A2 · #F3D468 · #8AC1E3</div>
<p>Coins arrondis 12px pour une touche «app mobile».<br><b>Animation</b> : carte sort par la gauche, nouvelle carte zoome légèrement.</p>
</div>
</div>
<footer style="font-size:.8rem;margin-top:2rem">© 2025 PtitsPas maquettes UI</footer>
</body>
</html>