feat(init): mise en place initiale de P'titsPas - Documentation: CDC complet, sécifications techniques SSS-001, charte graphique, évolutions - Backend: structure NestJS avec controllers/services/routes, config Prisma - Frontend: app Flutter avec structure MVC, thème et Firebase - Changement de nom: SuperNounou devient P'titsPas

This commit is contained in:
Julien Martin
2025-04-30 10:38:47 +02:00
parent d5015b9c42
commit 9321430818
61 changed files with 9251 additions and 125 deletions
+22
View File
@@ -0,0 +1,22 @@
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, kIsWeb, TargetPlatform;
class DefaultFirebaseOptions {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
return web;
}
// Handle other platforms if needed
return web;
}
static const FirebaseOptions web = FirebaseOptions(
apiKey: 'AIzaSyDXVQr3rlBPXhk-dYB6DyQF_JYrHKxXwrk',
appId: '1:654650461516:web:7a9e7c84c26c5a3a7c0e2c',
messagingSenderId: '654650461516',
projectId: 'petitspas-dev',
authDomain: 'petitspas-dev.firebaseapp.com',
storageBucket: 'petitspas-dev.appspot.com',
);
}
+37 -47
View File
@@ -3,62 +3,52 @@ import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:google_fonts/google_fonts.dart';
import 'theme/app_theme.dart';
import 'screens/auth/login_screen.dart';
import 'screens/auth/register_screen.dart';
import 'screens/auth/parent_register_screen.dart';
import 'screens/home/home_screen.dart';
void main() {
runApp(const MyApp());
runApp(
ChangeNotifierProvider(
create: (_) => AppTheme(),
child: const MyApp(),
),
);
}
final _router = GoRouter(
initialLocation: '/login',
routes: [
GoRoute(
path: '/login',
builder: (context, state) => const LoginScreen(),
),
GoRoute(
path: '/register',
builder: (context, state) => const RegisterScreen(),
),
GoRoute(
path: '/parent-register',
builder: (context, state) => const ParentRegisterScreen(),
),
GoRoute(
path: '/home',
builder: (context, state) => const HomeScreen(),
),
],
);
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'P\'titsPas',
theme: AppTheme.lightTheme,
routerConfig: GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomePage(),
),
],
),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('P\'titsPas'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
'assets/images/logo.png',
width: 200,
height: 200,
),
const SizedBox(height: 24),
Text(
'Bienvenue sur P\'titsPas',
style: Theme.of(context).textTheme.displayMedium,
),
const SizedBox(height: 16),
Text(
'La plateforme de gestion de la garde d\'enfants',
style: Theme.of(context).textTheme.bodyLarge,
),
],
),
return Consumer<AppTheme>(
builder: (context, appTheme, _) => MaterialApp.router(
title: 'P\'titsPas',
theme: appTheme.lightTheme,
routerConfig: _router,
),
);
}
+73
View File
@@ -0,0 +1,73 @@
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(),
};
}
}
+63
View File
@@ -0,0 +1,63 @@
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(),
};
}
}
+35
View File
@@ -0,0 +1,35 @@
class AppUser {
final String id;
final String email;
final String role;
final DateTime createdAt;
final DateTime updatedAt;
AppUser({
required this.id,
required this.email,
required this.role,
required this.createdAt,
required this.updatedAt,
});
factory AppUser.fromJson(Map<String, dynamic> json) {
return 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),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'email': email,
'role': role,
'createdAt': createdAt.toIso8601String(),
'updatedAt': updatedAt.toIso8601String(),
};
}
}
+33
View File
@@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
import '../screens/auth/login_screen.dart';
import '../screens/auth/register_screen.dart';
import '../screens/auth/parent_register_screen.dart';
import '../screens/home/home_screen.dart';
class AppRouter {
static const String login = '/login';
static const String register = '/register';
static const String parentRegister = '/parent-register';
static const String home = '/home';
static Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case login:
return MaterialPageRoute(builder: (_) => const LoginScreen());
case register:
return MaterialPageRoute(builder: (_) => const RegisterScreen());
case parentRegister:
return MaterialPageRoute(builder: (_) => const ParentRegisterScreen());
case home:
return MaterialPageRoute(builder: (_) => const HomeScreen());
default:
return MaterialPageRoute(
builder: (_) => Scaffold(
body: Center(
child: Text('Route non définie: ${settings.name}'),
),
),
);
}
}
}
+124
View File
@@ -0,0 +1,124 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../../services/auth_service.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _isLoading = false;
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
Future<void> _login() async {
if (_formKey.currentState!.validate()) {
setState(() => _isLoading = true);
try {
await AuthService.login(
_emailController.text,
_passwordController.text,
);
if (mounted) {
context.go('/home');
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors de la connexion: $e'),
backgroundColor: Colors.red,
),
);
}
} finally {
if (mounted) {
setState(() => _isLoading = false);
}
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Connexion'),
),
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextFormField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre email';
}
if (!value.contains('@')) {
return 'Veuillez entrer un email valide';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _passwordController,
decoration: const InputDecoration(
labelText: 'Mot de passe',
border: OutlineInputBorder(),
),
obscureText: true,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre mot de passe';
}
return null;
},
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _isLoading ? null : _login,
child: _isLoading
? const CircularProgressIndicator()
: const Text('Se connecter'),
),
const SizedBox(height: 16),
TextButton(
onPressed: () => context.go('/register'),
child: const Text('Créer un compte'),
),
const SizedBox(height: 8),
TextButton(
onPressed: () => context.go('/parent-register'),
child: const Text('Créer un compte parent'),
),
],
),
),
),
),
);
}
}
@@ -0,0 +1,756 @@
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';
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) {
String? childPhotoUrl;
if (child.photo != null) {
childPhotoUrl = await _uploadImage(child.photo!, _emailController.text);
}
childrenData.add({
'firstName': child.firstNameController.text,
'lastName': child.lastNameController.text,
'birthDate': child.isUnborn ? null : child.birthDate,
'expectedBirthDate': child.isUnborn ? child.expectedBirthDate : null,
'photoUrl': childPhotoUrl,
'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(),
),
),
);
}
}
@@ -0,0 +1,157 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../../services/auth_service.dart';
class RegisterScreen extends StatefulWidget {
const RegisterScreen({super.key});
@override
State<RegisterScreen> createState() => _RegisterScreenState();
}
class _RegisterScreenState extends State<RegisterScreen> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _firstNameController = TextEditingController();
final _lastNameController = TextEditingController();
bool _isLoading = false;
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
_firstNameController.dispose();
_lastNameController.dispose();
super.dispose();
}
Future<void> _register() async {
if (_formKey.currentState!.validate()) {
setState(() => _isLoading = true);
try {
await AuthService.register(
email: _emailController.text,
password: _passwordController.text,
firstName: _firstNameController.text,
lastName: _lastNameController.text,
role: 'user',
);
if (mounted) {
context.go('/login');
}
} 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);
}
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Inscription'),
),
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextFormField(
controller: _firstNameController,
decoration: const InputDecoration(
labelText: 'Prénom',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre prénom';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _lastNameController,
decoration: const InputDecoration(
labelText: 'Nom',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre nom';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre email';
}
if (!value.contains('@')) {
return 'Veuillez entrer un email valide';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _passwordController,
decoration: const InputDecoration(
labelText: 'Mot de passe',
border: OutlineInputBorder(),
),
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;
},
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _isLoading ? null : _register,
child: _isLoading
? const CircularProgressIndicator()
: const Text('S\'inscrire'),
),
const SizedBox(height: 16),
TextButton(
onPressed: () => context.go('/login'),
child: const Text('Déjà un compte ? Se connecter'),
),
],
),
),
),
),
);
}
}
@@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('P\'titsPas'),
),
body: const Center(
child: Text('Bienvenue sur P\'titsPas'),
),
);
}
}
+70
View File
@@ -0,0 +1,70 @@
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/user.dart';
import '../models/parent.dart';
import '../models/child.dart';
class AuthService {
static const String _usersKey = 'users';
static const String _parentsKey = 'parents';
static const String _childrenKey = 'children';
// Méthode pour se connecter (mode démonstration)
static Future<AppUser> login(String email, String password) async {
await Future.delayed(const Duration(seconds: 1)); // Simule un délai de traitement
throw Exception('Mode démonstration - Connexion désactivée');
}
// Méthode pour s'inscrire (mode démonstration)
static Future<AppUser> register({
required String email,
required String password,
required String firstName,
required String lastName,
required String role,
}) async {
await Future.delayed(const Duration(seconds: 1)); // Simule un délai de traitement
throw Exception('Mode démonstration - Inscription désactivée');
}
// 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)
static Future<void> logout() async {
// Ne fait rien en mode démonstration
}
// Méthode pour vérifier si l'utilisateur est connecté (mode démonstration)
static Future<bool> isLoggedIn() async {
return false; // Toujours non connecté en mode démonstration
}
// Méthode pour récupérer l'utilisateur connecté (mode démonstration)
static Future<AppUser?> getCurrentUser() async {
return null; // Aucun utilisateur en mode démonstration
}
}
+94 -33
View File
@@ -1,49 +1,86 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class AppTheme {
static const Color primaryColor = Color(0xFF2B6CB0);
static const Color secondaryColor = Color(0xFFF7FAFC);
static const Color backgroundColor = Colors.white;
static const Color textColor = Color(0xFF2D3748);
enum ThemeType {
defaultTheme,
pastelTheme,
// Ajouter d'autres thèmes ici
}
static ThemeData get lightTheme {
class AppTheme extends ChangeNotifier {
static final AppTheme _instance = AppTheme._internal();
factory AppTheme() => _instance;
AppTheme._internal();
// Thème par défaut (P'titsPas)
static const Color _defaultPrimaryColor = Color(0xFF2B6CB0);
static const Color _defaultSecondaryColor = Color(0xFFF7FAFC);
static const Color _defaultBackgroundColor = Color(0xFFFFFFFF);
static const Color _defaultTextColor = Color(0xFF000000);
static const Color _defaultErrorColor = Color(0xFFF4A28C);
static const Color _defaultWarningColor = Color(0xFFF2D269);
static const Color _defaultSuccessColor = Color(0xFF4CAF50);
// Thème Pastel
static const Color _pastelPrimaryColor = Color(0xFF8AD0C8); // Turquoise
static const Color _pastelSecondaryColor = Color(0xFFC6A3D8); // Violet Pastel
static const Color _pastelBackgroundColor = Color(0xFFFFFEF9); // Ivoire BG
static const Color _pastelTextColor = Color(0xFF2F2F2F); // Encre
static const Color _pastelErrorColor = Color(0xFFFFB4AB); // Rose Pastel
static const Color _pastelWarningColor = Color(0xFFFFE4B5); // Jaune Pastel
static const Color _pastelSuccessColor = Color(0xFFA5D6A7); // Vert Pastel
// Configuration du thème actuel
ThemeType _currentTheme = ThemeType.defaultTheme;
ThemeType get currentTheme => _currentTheme;
// Getters pour les couleurs du thème actuel
Color get primaryColor => _currentTheme == ThemeType.defaultTheme ? _defaultPrimaryColor : _pastelPrimaryColor;
Color get secondaryColor => _currentTheme == ThemeType.defaultTheme ? _defaultSecondaryColor : _pastelSecondaryColor;
Color get backgroundColor => _currentTheme == ThemeType.defaultTheme ? _defaultBackgroundColor : _pastelBackgroundColor;
Color get textColor => _currentTheme == ThemeType.defaultTheme ? _defaultTextColor : _pastelTextColor;
Color get errorColor => _currentTheme == ThemeType.defaultTheme ? _defaultErrorColor : _pastelErrorColor;
Color get warningColor => _currentTheme == ThemeType.defaultTheme ? _defaultWarningColor : _pastelWarningColor;
Color get successColor => _currentTheme == ThemeType.defaultTheme ? _defaultSuccessColor : _pastelSuccessColor;
// Méthode pour changer de thème
void setTheme(ThemeType theme) {
_currentTheme = theme;
notifyListeners();
}
// Thème Material 3
ThemeData get lightTheme {
return ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.light(
primary: primaryColor,
secondary: secondaryColor,
background: backgroundColor,
error: errorColor,
tertiary: warningColor,
onPrimary: Colors.white,
onSecondary: Colors.white,
onBackground: textColor,
onError: Colors.white,
onTertiary: textColor,
),
scaffoldBackgroundColor: backgroundColor,
textTheme: TextTheme(
displayLarge: GoogleFonts.comfortaa(
fontSize: 32,
fontWeight: FontWeight.bold,
color: textColor,
),
displayMedium: GoogleFonts.comfortaa(
fontSize: 24,
fontWeight: FontWeight.bold,
color: textColor,
),
bodyLarge: GoogleFonts.roboto(
fontSize: 16,
color: textColor,
),
bodyMedium: GoogleFonts.roboto(
fontSize: 14,
color: textColor,
),
displayLarge: _getTitleStyle(32),
displayMedium: _getTitleStyle(28),
displaySmall: _getTitleStyle(24),
headlineMedium: _getTitleStyle(20),
headlineSmall: _getTitleStyle(18),
titleLarge: _getTitleStyle(16),
bodyLarge: _getBodyStyle(16),
bodyMedium: _getBodyStyle(14),
bodySmall: _getBodyStyle(12),
),
appBarTheme: AppBarTheme(
backgroundColor: primaryColor,
foregroundColor: Colors.white,
elevation: 0,
titleTextStyle: GoogleFonts.comfortaa(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
titleTextStyle: _getTitleStyle(20).copyWith(color: Colors.white),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
@@ -56,19 +93,43 @@ class AppTheme {
),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: secondaryColor.withOpacity(0.5),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: primaryColor.withOpacity(0.5)),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: primaryColor.withOpacity(0.3)),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: primaryColor),
borderSide: BorderSide(color: primaryColor, width: 2),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
snackBarTheme: SnackBarThemeData(
backgroundColor: errorColor,
contentTextStyle: _getBodyStyle(14).copyWith(color: Colors.white),
),
);
}
// Méthodes privées pour la typographie
TextStyle _getTitleStyle(double fontSize) {
return GoogleFonts.merienda(
fontSize: fontSize,
fontWeight: FontWeight.w600,
color: textColor,
);
}
TextStyle _getBodyStyle(double fontSize) {
return GoogleFonts.merriweather(
fontSize: fontSize,
fontWeight: FontWeight.w300,
color: textColor,
);
}
}