first commit

This commit is contained in:
DahoudG
2025-08-20 21:00:35 +00:00
commit b2a23bdf89
583 changed files with 243074 additions and 0 deletions

View File

@@ -0,0 +1,69 @@
import 'package:flutter/material.dart';
import '../../../../shared/theme/app_theme.dart';
import 'welcome_screen.dart';
class AuthWrapper extends StatefulWidget {
const AuthWrapper({super.key});
@override
State<AuthWrapper> createState() => _AuthWrapperState();
}
class _AuthWrapperState extends State<AuthWrapper> {
bool _isLoading = true;
bool _isAuthenticated = false;
@override
void initState() {
super.initState();
_checkAuthenticationStatus();
}
Future<void> _checkAuthenticationStatus() async {
// Simulation de vérification d'authentification
// En production : vérifier le token JWT, SharedPreferences, etc.
await Future.delayed(const Duration(milliseconds: 500));
setState(() {
_isLoading = false;
// Pour le moment, toujours false (pas d'utilisateur connecté)
_isAuthenticated = false;
});
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return _buildLoadingScreen();
}
if (_isAuthenticated) {
// TODO: Retourner vers la navigation principale
return _buildLoadingScreen(); // Temporaire
} else {
return const WelcomeScreen();
}
}
Widget _buildLoadingScreen() {
return Scaffold(
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
AppTheme.primaryColor,
AppTheme.primaryDark,
],
),
),
child: const Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
),
),
);
}
}

View File

@@ -0,0 +1,489 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../shared/theme/app_theme.dart';
import '../../../../shared/widgets/custom_text_field.dart';
import '../../../../shared/widgets/loading_button.dart';
class ForgotPasswordScreen extends StatefulWidget {
const ForgotPasswordScreen({super.key});
@override
State<ForgotPasswordScreen> createState() => _ForgotPasswordScreenState();
}
class _ForgotPasswordScreenState extends State<ForgotPasswordScreen>
with TickerProviderStateMixin {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
late AnimationController _fadeController;
late AnimationController _slideController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
bool _isLoading = false;
bool _emailSent = false;
@override
void initState() {
super.initState();
_initializeAnimations();
_startAnimations();
}
void _initializeAnimations() {
_fadeController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_slideController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _fadeController,
curve: Curves.easeInOut,
));
_slideAnimation = Tween<Offset>(
begin: const Offset(0, 0.3),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _slideController,
curve: Curves.easeOutCubic,
));
}
void _startAnimations() async {
await Future.delayed(const Duration(milliseconds: 100));
_fadeController.forward();
_slideController.forward();
}
@override
void dispose() {
_emailController.dispose();
_fadeController.dispose();
_slideController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.backgroundLight,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: AppTheme.textPrimary),
onPressed: () => Navigator.of(context).pop(),
),
),
body: SafeArea(
child: AnimatedBuilder(
animation: _fadeAnimation,
builder: (context, child) {
return Opacity(
opacity: _fadeAnimation.value,
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: SlideTransition(
position: _slideAnimation,
child: _emailSent ? _buildSuccessView() : _buildFormView(),
),
),
);
},
),
),
);
}
Widget _buildFormView() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: 40),
_buildInstructions(),
const SizedBox(height: 32),
_buildForm(),
const SizedBox(height: 32),
_buildSendButton(),
const SizedBox(height: 24),
_buildBackToLogin(),
],
);
}
Widget _buildSuccessView() {
return Column(
children: [
const SizedBox(height: 60),
// Icône de succès
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: AppTheme.successColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(60),
border: Border.all(
color: AppTheme.successColor.withOpacity(0.3),
width: 2,
),
),
child: const Icon(
Icons.mark_email_read_rounded,
size: 60,
color: AppTheme.successColor,
),
),
const SizedBox(height: 32),
// Titre de succès
const Text(
'Email envoyé !',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
// Message de succès
Text(
'Nous avons envoyé un lien de réinitialisation à :',
style: TextStyle(
fontSize: 16,
color: AppTheme.textSecondary,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
_emailController.text,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.primaryColor,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
// Instructions
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppTheme.infoColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppTheme.infoColor.withOpacity(0.2),
),
),
child: Column(
children: [
const Icon(
Icons.info_outline,
color: AppTheme.infoColor,
size: 24,
),
const SizedBox(height: 12),
const Text(
'Instructions',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 8),
Text(
'1. Vérifiez votre boîte email (et vos spams)\n'
'2. Cliquez sur le lien de réinitialisation\n'
'3. Créez un nouveau mot de passe sécurisé',
style: TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
height: 1.5,
),
textAlign: TextAlign.center,
),
],
),
),
const SizedBox(height: 32),
// Boutons d'action
Column(
children: [
LoadingButton(
onPressed: _handleResendEmail,
text: 'Renvoyer l\'email',
width: double.infinity,
height: 48,
backgroundColor: AppTheme.secondaryColor,
),
const SizedBox(height: 12),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text(
'Retour à la connexion',
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
),
),
),
],
),
],
);
}
Widget _buildHeader() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Icône
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: AppTheme.warningColor,
borderRadius: BorderRadius.circular(15),
),
child: const Icon(
Icons.lock_reset_rounded,
color: Colors.white,
size: 30,
),
),
const SizedBox(height: 24),
// Titre
const Text(
'Mot de passe oublié ?',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 8),
// Sous-titre
Text(
'Pas de problème ! Nous allons vous aider à le récupérer.',
style: TextStyle(
fontSize: 16,
color: AppTheme.textSecondary,
),
),
],
);
}
Widget _buildInstructions() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.05),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppTheme.primaryColor.withOpacity(0.1),
),
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
),
child: const Icon(
Icons.email_outlined,
color: AppTheme.primaryColor,
size: 20,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Comment ça marche ?',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 4),
Text(
'Saisissez votre email et nous vous enverrons un lien sécurisé pour réinitialiser votre mot de passe.',
style: TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
height: 1.4,
),
),
],
),
),
],
),
);
}
Widget _buildForm() {
return Form(
key: _formKey,
child: CustomTextField(
controller: _emailController,
label: 'Adresse email',
hintText: 'votre.email@exemple.com',
prefixIcon: Icons.email_outlined,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.done,
validator: _validateEmail,
onFieldSubmitted: (_) => _handleSendResetEmail(),
autofocus: true,
),
);
}
Widget _buildSendButton() {
return LoadingButton(
onPressed: _handleSendResetEmail,
isLoading: _isLoading,
text: 'Envoyer le lien de réinitialisation',
width: double.infinity,
height: 56,
);
}
Widget _buildBackToLogin() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Vous vous souvenez de votre mot de passe ? ',
style: TextStyle(
color: AppTheme.textSecondary,
fontSize: 14,
),
),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text(
'Se connecter',
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
),
],
);
}
String? _validateEmail(String? value) {
if (value == null || value.isEmpty) {
return 'Veuillez saisir votre adresse email';
}
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
return 'Veuillez saisir une adresse email valide';
}
return null;
}
Future<void> _handleSendResetEmail() async {
if (!_formKey.currentState!.validate()) {
return;
}
setState(() {
_isLoading = true;
});
try {
// Simulation d'envoi d'email
await Future.delayed(const Duration(seconds: 2));
// Vibration de succès
HapticFeedback.lightImpact();
// Transition vers la vue de succès
setState(() {
_emailSent = true;
_isLoading = false;
});
} catch (e) {
// Gestion d'erreur
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors de l\'envoi: ${e.toString()}'),
backgroundColor: AppTheme.errorColor,
behavior: SnackBarBehavior.floating,
),
);
setState(() {
_isLoading = false;
});
}
}
}
Future<void> _handleResendEmail() async {
try {
// Simulation de renvoi d'email
await Future.delayed(const Duration(seconds: 1));
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Email renvoyé avec succès !'),
backgroundColor: AppTheme.successColor,
behavior: SnackBarBehavior.floating,
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors du renvoi: ${e.toString()}'),
backgroundColor: AppTheme.errorColor,
behavior: SnackBarBehavior.floating,
),
);
}
}
}
}

View File

@@ -0,0 +1,321 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../core/auth/bloc/auth_bloc.dart';
import '../../../../core/auth/bloc/auth_event.dart';
import '../../../../core/auth/models/auth_state.dart';
import '../../../../core/auth/models/login_request.dart';
import '../../../../shared/theme/app_theme.dart';
import '../../../../shared/widgets/buttons/buttons.dart';
import '../widgets/login_form.dart';
import '../widgets/login_header.dart';
import '../widgets/login_footer.dart';
/// Écran de connexion avec interface sophistiquée
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage>
with TickerProviderStateMixin {
late AnimationController _animationController;
late AnimationController _shakeController;
late Animation<double> _fadeAnimation;
late Animation<double> _slideAnimation;
late Animation<double> _shakeAnimation;
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _obscurePassword = true;
bool _rememberMe = false;
bool _isLoading = false;
@override
void initState() {
super.initState();
_setupAnimations();
_startEntryAnimation();
}
void _setupAnimations() {
_animationController = AnimationController(
duration: const Duration(milliseconds: 1200),
vsync: this,
);
_shakeController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: const Interval(0.0, 0.6, curve: Curves.easeOut),
));
_slideAnimation = Tween<double>(
begin: 50.0,
end: 0.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: const Interval(0.2, 0.8, curve: Curves.easeOut),
));
_shakeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _shakeController,
curve: Curves.elasticInOut,
));
}
void _startEntryAnimation() {
Future.delayed(const Duration(milliseconds: 100), () {
if (mounted) {
_animationController.forward();
}
});
}
@override
void dispose() {
_animationController.dispose();
_shakeController.dispose();
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.backgroundLight,
body: BlocListener<AuthBloc, AuthState>(
listener: _handleAuthStateChange,
child: SafeArea(
child: AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return FadeTransition(
opacity: _fadeAnimation,
child: Transform.translate(
offset: Offset(0, _slideAnimation.value),
child: _buildLoginContent(),
),
);
},
),
),
),
);
}
Widget _buildLoginContent() {
return SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
children: [
const SizedBox(height: 60),
// Header avec logo et titre
LoginHeader(
onAnimationComplete: () {},
),
const SizedBox(height: 60),
// Formulaire de connexion
AnimatedBuilder(
animation: _shakeAnimation,
builder: (context, child) {
return Transform.translate(
offset: Offset(
_shakeAnimation.value * 10 *
(1 - _shakeAnimation.value) *
(1 - _shakeAnimation.value),
0,
),
child: LoginForm(
formKey: _formKey,
emailController: _emailController,
passwordController: _passwordController,
obscurePassword: _obscurePassword,
rememberMe: _rememberMe,
isLoading: _isLoading,
onObscureToggle: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
HapticFeedback.selectionClick();
},
onRememberMeToggle: (value) {
setState(() {
_rememberMe = value;
});
HapticFeedback.selectionClick();
},
onSubmit: _handleLogin,
),
),
),
),
const SizedBox(height: 40),
// Footer avec liens et informations
const LoginFooter(),
const SizedBox(height: 20),
],
),
);
}
void _handleAuthStateChange(BuildContext context, AuthState state) {
setState(() {
_isLoading = state.isLoading;
});
if (state.status == AuthStatus.authenticated) {
// Connexion réussie - navigation gérée par l'app principal
_showSuccessMessage();
HapticFeedback.heavyImpact();
} else if (state.status == AuthStatus.error) {
// Erreur de connexion
_handleLoginError(state.errorMessage ?? 'Erreur inconnue');
} else if (state.status == AuthStatus.unauthenticated && state.errorMessage != null) {
// Échec de connexion
_handleLoginError(state.errorMessage!);
}
}
void _handleLogin() {
if (!_formKey.currentState!.validate()) {
_triggerShakeAnimation();
HapticFeedback.mediumImpact();
return;
}
final email = _emailController.text.trim();
final password = _passwordController.text;
if (email.isEmpty || password.isEmpty) {
_showErrorMessage('Veuillez remplir tous les champs');
_triggerShakeAnimation();
return;
}
// Déclencher la connexion
final loginRequest = LoginRequest(
email: email,
password: password,
rememberMe: _rememberMe,
);
context.read<AuthBloc>().add(AuthLoginRequested(loginRequest));
// Feedback haptique
HapticFeedback.lightImpact();
}
void _handleLoginError(String errorMessage) {
_showErrorMessage(errorMessage);
_triggerShakeAnimation();
HapticFeedback.mediumImpact();
// Effacer l'erreur après affichage
Future.delayed(const Duration(seconds: 3), () {
if (mounted) {
context.read<AuthBloc>().add(const AuthErrorCleared());
}
});
}
void _triggerShakeAnimation() {
_shakeController.reset();
_shakeController.forward();
}
void _showSuccessMessage() {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
Icon(
Icons.check_circle,
color: Colors.white,
size: 24,
),
const SizedBox(width: 12),
const Text(
'Connexion réussie !',
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16,
),
),
],
),
backgroundColor: AppTheme.successColor,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
margin: const EdgeInsets.all(16),
duration: const Duration(seconds: 2),
),
);
}
void _showErrorMessage(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
Icon(
Icons.error_outline,
color: Colors.white,
size: 24,
),
const SizedBox(width: 12),
Expanded(
child: Text(
message,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
),
],
),
backgroundColor: AppTheme.errorColor,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
margin: const EdgeInsets.all(16),
duration: const Duration(seconds: 4),
action: SnackBarAction(
label: 'OK',
textColor: Colors.white,
onPressed: () {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
},
),
),
);
}
}

View File

@@ -0,0 +1,478 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../core/auth/bloc/temp_auth_bloc.dart';
import '../../../../core/auth/bloc/auth_event.dart';
import '../../../../core/auth/models/auth_state.dart';
import '../../../../core/auth/models/login_request.dart';
import '../../../../shared/theme/app_theme.dart';
import '../widgets/login_header.dart';
import '../widgets/login_footer.dart';
/// Écran de connexion temporaire simplifié
class TempLoginPage extends StatefulWidget {
const TempLoginPage({super.key});
@override
State<TempLoginPage> createState() => _TempLoginPageState();
}
class _TempLoginPageState extends State<TempLoginPage>
with TickerProviderStateMixin {
late AnimationController _animationController;
late AnimationController _shakeController;
late Animation<double> _fadeAnimation;
late Animation<double> _slideAnimation;
late Animation<double> _shakeAnimation;
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController(text: 'admin@unionflow.dev');
final _passwordController = TextEditingController(text: 'admin123');
bool _obscurePassword = true;
bool _rememberMe = false;
bool _isLoading = false;
@override
void initState() {
super.initState();
_setupAnimations();
_startEntryAnimation();
}
void _setupAnimations() {
_animationController = AnimationController(
duration: const Duration(milliseconds: 1200),
vsync: this,
);
_shakeController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: const Interval(0.0, 0.6, curve: Curves.easeOut),
));
_slideAnimation = Tween<double>(
begin: 50.0,
end: 0.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: const Interval(0.2, 0.8, curve: Curves.easeOut),
));
_shakeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _shakeController,
curve: Curves.elasticInOut,
));
}
void _startEntryAnimation() {
Future.delayed(const Duration(milliseconds: 100), () {
if (mounted) {
_animationController.forward();
}
});
}
@override
void dispose() {
_animationController.dispose();
_shakeController.dispose();
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.backgroundLight,
body: BlocListener<TempAuthBloc, AuthState>(
listener: _handleAuthStateChange,
child: SafeArea(
child: AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return FadeTransition(
opacity: _fadeAnimation,
child: Transform.translate(
offset: Offset(0, _slideAnimation.value),
child: _buildLoginContent(),
),
);
},
),
),
),
);
}
Widget _buildLoginContent() {
return SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
children: [
const SizedBox(height: 60),
// Header avec logo et titre
const LoginHeader(),
const SizedBox(height: 60),
// Formulaire de connexion
AnimatedBuilder(
animation: _shakeAnimation,
builder: (context, child) {
return Transform.translate(
offset: Offset(
_shakeAnimation.value * 10 *
(1 - _shakeAnimation.value) *
(1 - _shakeAnimation.value),
0,
),
child: _buildLoginForm(),
);
},
),
const SizedBox(height: 40),
// Footer avec liens et informations
const LoginFooter(),
const SizedBox(height: 20),
],
),
);
}
Widget _buildLoginForm() {
return Form(
key: _formKey,
child: Column(
children: [
// Champ email
_buildEmailField(),
const SizedBox(height: 20),
// Champ mot de passe
_buildPasswordField(),
const SizedBox(height: 16),
// Options
_buildOptionsRow(),
const SizedBox(height: 32),
// Bouton de connexion
_buildLoginButton(),
],
),
);
}
Widget _buildEmailField() {
return TextFormField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
enabled: !_isLoading,
decoration: InputDecoration(
labelText: 'Adresse email',
hintText: 'votre.email@exemple.com',
prefixIcon: Icon(
Icons.email_outlined,
color: AppTheme.primaryColor,
),
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide(
color: AppTheme.primaryColor,
width: 2,
),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez saisir votre email';
}
if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(value)) {
return 'Format d\'email invalide';
}
return null;
},
);
}
Widget _buildPasswordField() {
return TextFormField(
controller: _passwordController,
obscureText: _obscurePassword,
textInputAction: TextInputAction.done,
enabled: !_isLoading,
onFieldSubmitted: (_) => _handleLogin(),
decoration: InputDecoration(
labelText: 'Mot de passe',
hintText: 'Saisissez votre mot de passe',
prefixIcon: Icon(
Icons.lock_outlined,
color: AppTheme.primaryColor,
),
suffixIcon: IconButton(
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
HapticFeedback.selectionClick();
},
icon: Icon(
_obscurePassword
? Icons.visibility_outlined
: Icons.visibility_off_outlined,
color: AppTheme.primaryColor,
),
),
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide(
color: AppTheme.primaryColor,
width: 2,
),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez saisir votre mot de passe';
}
if (value.length < 6) {
return 'Le mot de passe doit contenir au moins 6 caractères';
}
return null;
},
);
}
Widget _buildOptionsRow() {
return Row(
children: [
// Se souvenir de moi
Expanded(
child: GestureDetector(
onTap: () {
setState(() {
_rememberMe = !_rememberMe;
});
HapticFeedback.selectionClick();
},
child: Row(
children: [
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: _rememberMe
? AppTheme.primaryColor
: AppTheme.textSecondary,
width: 2,
),
color: _rememberMe
? AppTheme.primaryColor
: Colors.transparent,
),
child: _rememberMe
? const Icon(
Icons.check,
size: 14,
color: Colors.white,
)
: null,
),
const SizedBox(width: 8),
Text(
'Se souvenir de moi',
style: TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
// Compte de test
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: AppTheme.infoColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'Compte de test',
style: TextStyle(
fontSize: 12,
color: AppTheme.infoColor,
fontWeight: FontWeight.w600,
),
),
),
],
);
}
Widget _buildLoginButton() {
return SizedBox(
width: double.infinity,
height: 56,
child: ElevatedButton(
onPressed: _isLoading ? null : _handleLogin,
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
elevation: 4,
),
child: _isLoading
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.login, size: 20),
const SizedBox(width: 8),
const Text(
'Se connecter',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
],
),
),
);
}
void _handleAuthStateChange(BuildContext context, AuthState state) {
setState(() {
_isLoading = state.isLoading;
});
if (state.status == AuthStatus.authenticated) {
_showSuccessMessage();
HapticFeedback.heavyImpact();
} else if (state.status == AuthStatus.error) {
_handleLoginError(state.errorMessage ?? 'Erreur inconnue');
}
}
void _handleLogin() {
if (!_formKey.currentState!.validate()) {
_triggerShakeAnimation();
HapticFeedback.mediumImpact();
return;
}
final email = _emailController.text.trim();
final password = _passwordController.text;
final loginRequest = LoginRequest(
email: email,
password: password,
rememberMe: _rememberMe,
);
context.read<TempAuthBloc>().add(AuthLoginRequested(loginRequest));
HapticFeedback.lightImpact();
}
void _handleLoginError(String errorMessage) {
_showErrorMessage(errorMessage);
_triggerShakeAnimation();
HapticFeedback.mediumImpact();
}
void _triggerShakeAnimation() {
_shakeController.reset();
_shakeController.forward();
}
void _showSuccessMessage() {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Row(
children: [
Icon(Icons.check_circle, color: Colors.white),
SizedBox(width: 12),
Text('Connexion réussie !'),
],
),
backgroundColor: AppTheme.successColor,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
);
}
void _showErrorMessage(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.error_outline, color: Colors.white),
const SizedBox(width: 12),
Expanded(child: Text(message)),
],
),
backgroundColor: AppTheme.errorColor,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
);
}
}

View File

@@ -0,0 +1,517 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../shared/theme/app_theme.dart';
import '../../../../shared/widgets/custom_text_field.dart';
import '../../../../shared/widgets/loading_button.dart';
import '../../../navigation/presentation/pages/main_navigation.dart';
import 'forgot_password_screen.dart';
import 'register_screen.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen>
with TickerProviderStateMixin {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
late AnimationController _fadeController;
late AnimationController _slideController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
bool _isLoading = false;
bool _obscurePassword = true;
bool _rememberMe = false;
@override
void initState() {
super.initState();
_initializeAnimations();
_startAnimations();
}
void _initializeAnimations() {
_fadeController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_slideController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _fadeController,
curve: Curves.easeInOut,
));
_slideAnimation = Tween<Offset>(
begin: const Offset(0, 0.3),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _slideController,
curve: Curves.easeOutCubic,
));
}
void _startAnimations() async {
await Future.delayed(const Duration(milliseconds: 100));
_fadeController.forward();
_slideController.forward();
}
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
_fadeController.dispose();
_slideController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.backgroundLight,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: AppTheme.textPrimary),
onPressed: () => Navigator.of(context).pop(),
),
),
body: SafeArea(
child: AnimatedBuilder(
animation: _fadeAnimation,
builder: (context, child) {
return Opacity(
opacity: _fadeAnimation.value,
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: SlideTransition(
position: _slideAnimation,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: 40),
_buildLoginForm(),
const SizedBox(height: 24),
_buildForgotPassword(),
const SizedBox(height: 32),
_buildLoginButton(),
const SizedBox(height: 24),
_buildDivider(),
const SizedBox(height: 24),
_buildSocialLogin(),
const SizedBox(height: 32),
_buildSignUpLink(),
],
),
),
),
);
},
),
),
);
}
Widget _buildHeader() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Logo petit
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: AppTheme.primaryColor,
borderRadius: BorderRadius.circular(15),
),
child: const Icon(
Icons.groups_rounded,
color: Colors.white,
size: 30,
),
),
const SizedBox(height: 24),
// Titre
const Text(
'Bienvenue !',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 8),
// Sous-titre
Text(
'Connectez-vous à votre compte UnionFlow',
style: TextStyle(
fontSize: 16,
color: AppTheme.textSecondary,
),
),
],
);
}
Widget _buildLoginForm() {
return Form(
key: _formKey,
child: Column(
children: [
// Champ Email
CustomTextField(
controller: _emailController,
label: 'Adresse email',
hintText: 'votre.email@exemple.com',
prefixIcon: Icons.email_outlined,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
validator: _validateEmail,
onFieldSubmitted: (_) => FocusScope.of(context).nextFocus(),
),
const SizedBox(height: 16),
// Champ Mot de passe
CustomTextField(
controller: _passwordController,
label: 'Mot de passe',
hintText: 'Votre mot de passe',
prefixIcon: Icons.lock_outline,
obscureText: _obscurePassword,
textInputAction: TextInputAction.done,
validator: _validatePassword,
onFieldSubmitted: (_) => _handleLogin(),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword ? Icons.visibility_off : Icons.visibility,
color: AppTheme.textHint,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
),
const SizedBox(height: 16),
// Remember me
Row(
children: [
Checkbox(
value: _rememberMe,
onChanged: (value) {
setState(() {
_rememberMe = value ?? false;
});
},
activeColor: AppTheme.primaryColor,
),
const Text(
'Se souvenir de moi',
style: TextStyle(
color: AppTheme.textSecondary,
fontSize: 14,
),
),
],
),
],
),
);
}
Widget _buildForgotPassword() {
return Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () => _navigateToForgotPassword(),
child: const Text(
'Mot de passe oublié ?',
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
),
),
),
);
}
Widget _buildLoginButton() {
return LoadingButton(
onPressed: _handleLogin,
isLoading: _isLoading,
text: 'Se connecter',
width: double.infinity,
height: 56,
);
}
Widget _buildDivider() {
return Row(
children: [
const Expanded(child: Divider()),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'ou',
style: TextStyle(
color: AppTheme.textHint,
fontSize: 14,
),
),
),
const Expanded(child: Divider()),
],
);
}
Widget _buildSocialLogin() {
return Column(
children: [
// Google Login
SizedBox(
width: double.infinity,
height: 56,
child: OutlinedButton.icon(
onPressed: () => _handleGoogleLogin(),
icon: Image.asset(
'assets/icons/google.png',
width: 20,
height: 20,
errorBuilder: (context, error, stackTrace) => const Icon(
Icons.g_mobiledata,
color: Colors.red,
size: 20,
),
),
label: const Text('Continuer avec Google'),
style: OutlinedButton.styleFrom(
foregroundColor: AppTheme.textPrimary,
side: const BorderSide(color: AppTheme.borderColor),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
const SizedBox(height: 12),
// Microsoft Login
SizedBox(
width: double.infinity,
height: 56,
child: OutlinedButton.icon(
onPressed: () => _handleMicrosoftLogin(),
icon: const Icon(
Icons.business,
color: Color(0xFF00A4EF),
size: 20,
),
label: const Text('Continuer avec Microsoft'),
style: OutlinedButton.styleFrom(
foregroundColor: AppTheme.textPrimary,
side: const BorderSide(color: AppTheme.borderColor),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
],
);
}
Widget _buildSignUpLink() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Pas encore de compte ? ',
style: TextStyle(
color: AppTheme.textSecondary,
fontSize: 14,
),
),
TextButton(
onPressed: () => _navigateToRegister(),
child: const Text(
'S\'inscrire',
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
),
],
);
}
String? _validateEmail(String? value) {
if (value == null || value.isEmpty) {
return 'Veuillez saisir votre adresse email';
}
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
return 'Veuillez saisir une adresse email valide';
}
return null;
}
String? _validatePassword(String? value) {
if (value == null || value.isEmpty) {
return 'Veuillez saisir votre mot de passe';
}
if (value.length < 6) {
return 'Le mot de passe doit contenir au moins 6 caractères';
}
return null;
}
Future<void> _handleLogin() async {
if (!_formKey.currentState!.validate()) {
return;
}
setState(() {
_isLoading = true;
});
try {
// Simulation d'authentification
await Future.delayed(const Duration(seconds: 2));
// Vibration de succès
HapticFeedback.lightImpact();
// Navigation vers le dashboard
if (mounted) {
Navigator.of(context).pushReplacement(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const MainNavigation(),
transitionDuration: const Duration(milliseconds: 600),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeInOut,
)),
child: child,
),
);
},
),
);
}
} catch (e) {
// Gestion d'erreur
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur de connexion: ${e.toString()}'),
backgroundColor: AppTheme.errorColor,
behavior: SnackBarBehavior.floating,
),
);
}
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
void _handleGoogleLogin() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Connexion Google - En cours de développement'),
backgroundColor: AppTheme.infoColor,
behavior: SnackBarBehavior.floating,
),
);
}
void _handleMicrosoftLogin() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Connexion Microsoft - En cours de développement'),
backgroundColor: AppTheme.infoColor,
behavior: SnackBarBehavior.floating,
),
);
}
void _navigateToForgotPassword() {
Navigator.of(context).push(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const ForgotPasswordScreen(),
transitionDuration: const Duration(milliseconds: 400),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeInOut,
)),
child: child,
);
},
),
);
}
void _navigateToRegister() {
Navigator.of(context).pushReplacement(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const RegisterScreen(),
transitionDuration: const Duration(milliseconds: 400),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeInOut,
)),
child: child,
);
},
),
);
}
}

View File

@@ -0,0 +1,624 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../shared/theme/app_theme.dart';
import '../../../../shared/widgets/custom_text_field.dart';
import '../../../../shared/widgets/loading_button.dart';
import 'login_screen.dart';
class RegisterScreen extends StatefulWidget {
const RegisterScreen({super.key});
@override
State<RegisterScreen> createState() => _RegisterScreenState();
}
class _RegisterScreenState extends State<RegisterScreen>
with TickerProviderStateMixin {
final _formKey = GlobalKey<FormState>();
final _firstNameController = TextEditingController();
final _lastNameController = TextEditingController();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
late AnimationController _fadeController;
late AnimationController _slideController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
bool _isLoading = false;
bool _obscurePassword = true;
bool _obscureConfirmPassword = true;
bool _acceptTerms = false;
bool _acceptNewsletter = false;
@override
void initState() {
super.initState();
_initializeAnimations();
_startAnimations();
}
void _initializeAnimations() {
_fadeController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_slideController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _fadeController,
curve: Curves.easeInOut,
));
_slideAnimation = Tween<Offset>(
begin: const Offset(0, 0.3),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _slideController,
curve: Curves.easeOutCubic,
));
}
void _startAnimations() async {
await Future.delayed(const Duration(milliseconds: 100));
_fadeController.forward();
_slideController.forward();
}
@override
void dispose() {
_firstNameController.dispose();
_lastNameController.dispose();
_emailController.dispose();
_passwordController.dispose();
_confirmPasswordController.dispose();
_fadeController.dispose();
_slideController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.backgroundLight,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: AppTheme.textPrimary),
onPressed: () => Navigator.of(context).pop(),
),
),
body: SafeArea(
child: AnimatedBuilder(
animation: _fadeAnimation,
builder: (context, child) {
return Opacity(
opacity: _fadeAnimation.value,
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: SlideTransition(
position: _slideAnimation,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: 32),
_buildRegistrationForm(),
const SizedBox(height: 24),
_buildTermsAndConditions(),
const SizedBox(height: 32),
_buildRegisterButton(),
const SizedBox(height: 24),
_buildLoginLink(),
],
),
),
),
);
},
),
),
);
}
Widget _buildHeader() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Logo petit
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: AppTheme.primaryColor,
borderRadius: BorderRadius.circular(15),
),
child: const Icon(
Icons.person_add_rounded,
color: Colors.white,
size: 30,
),
),
const SizedBox(height: 24),
// Titre
const Text(
'Créer un compte',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 8),
// Sous-titre
Text(
'Rejoignez UnionFlow et gérez votre association',
style: TextStyle(
fontSize: 16,
color: AppTheme.textSecondary,
),
),
],
);
}
Widget _buildRegistrationForm() {
return Form(
key: _formKey,
child: Column(
children: [
// Nom et Prénom
Row(
children: [
Expanded(
child: CustomTextField(
controller: _firstNameController,
label: 'Prénom',
hintText: 'Jean',
prefixIcon: Icons.person_outline,
textInputAction: TextInputAction.next,
validator: _validateFirstName,
onFieldSubmitted: (_) => FocusScope.of(context).nextFocus(),
),
),
const SizedBox(width: 16),
Expanded(
child: CustomTextField(
controller: _lastNameController,
label: 'Nom',
hintText: 'Dupont',
prefixIcon: Icons.person_outline,
textInputAction: TextInputAction.next,
validator: _validateLastName,
onFieldSubmitted: (_) => FocusScope.of(context).nextFocus(),
),
),
],
),
const SizedBox(height: 16),
// Email
CustomTextField(
controller: _emailController,
label: 'Adresse email',
hintText: 'jean.dupont@exemple.com',
prefixIcon: Icons.email_outlined,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
validator: _validateEmail,
onFieldSubmitted: (_) => FocusScope.of(context).nextFocus(),
),
const SizedBox(height: 16),
// Mot de passe
CustomTextField(
controller: _passwordController,
label: 'Mot de passe',
hintText: 'Minimum 8 caractères',
prefixIcon: Icons.lock_outline,
obscureText: _obscurePassword,
textInputAction: TextInputAction.next,
validator: _validatePassword,
onFieldSubmitted: (_) => FocusScope.of(context).nextFocus(),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword ? Icons.visibility_off : Icons.visibility,
color: AppTheme.textHint,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
),
const SizedBox(height: 16),
// Confirmer mot de passe
CustomTextField(
controller: _confirmPasswordController,
label: 'Confirmer le mot de passe',
hintText: 'Retapez votre mot de passe',
prefixIcon: Icons.lock_outline,
obscureText: _obscureConfirmPassword,
textInputAction: TextInputAction.done,
validator: _validateConfirmPassword,
onFieldSubmitted: (_) => _handleRegister(),
suffixIcon: IconButton(
icon: Icon(
_obscureConfirmPassword ? Icons.visibility_off : Icons.visibility,
color: AppTheme.textHint,
),
onPressed: () {
setState(() {
_obscureConfirmPassword = !_obscureConfirmPassword;
});
},
),
),
const SizedBox(height: 16),
// Indicateur de force du mot de passe
_buildPasswordStrengthIndicator(),
],
),
);
}
Widget _buildPasswordStrengthIndicator() {
final password = _passwordController.text;
final strength = _calculatePasswordStrength(password);
Color strengthColor;
String strengthText;
if (strength < 0.3) {
strengthColor = AppTheme.errorColor;
strengthText = 'Faible';
} else if (strength < 0.7) {
strengthColor = AppTheme.warningColor;
strengthText = 'Moyen';
} else {
strengthColor = AppTheme.successColor;
strengthText = 'Fort';
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Force du mot de passe',
style: TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
if (password.isNotEmpty)
Text(
strengthText,
style: TextStyle(
fontSize: 12,
color: strengthColor,
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: 8),
Container(
height: 4,
decoration: BoxDecoration(
color: AppTheme.borderColor,
borderRadius: BorderRadius.circular(2),
),
child: FractionallySizedBox(
alignment: Alignment.centerLeft,
widthFactor: password.isEmpty ? 0 : strength,
child: Container(
decoration: BoxDecoration(
color: strengthColor,
borderRadius: BorderRadius.circular(2),
),
),
),
),
],
);
}
double _calculatePasswordStrength(String password) {
if (password.isEmpty) return 0.0;
double strength = 0.0;
// Longueur
if (password.length >= 8) strength += 0.25;
if (password.length >= 12) strength += 0.25;
// Contient des minuscules
if (password.contains(RegExp(r'[a-z]'))) strength += 0.15;
// Contient des majuscules
if (password.contains(RegExp(r'[A-Z]'))) strength += 0.15;
// Contient des chiffres
if (password.contains(RegExp(r'[0-9]'))) strength += 0.1;
// Contient des caractères spéciaux
if (password.contains(RegExp(r'[!@#$%^&*(),.?":{}|<>]'))) strength += 0.1;
return strength.clamp(0.0, 1.0);
}
Widget _buildTermsAndConditions() {
return Column(
children: [
// Accepter les conditions
Row(
children: [
Checkbox(
value: _acceptTerms,
onChanged: (value) {
setState(() {
_acceptTerms = value ?? false;
});
},
activeColor: AppTheme.primaryColor,
),
Expanded(
child: RichText(
text: TextSpan(
style: const TextStyle(
color: AppTheme.textSecondary,
fontSize: 14,
),
children: [
const TextSpan(text: 'J\'accepte les '),
TextSpan(
text: 'Conditions d\'utilisation',
style: const TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
decoration: TextDecoration.underline,
),
),
const TextSpan(text: ' et la '),
TextSpan(
text: 'Politique de confidentialité',
style: const TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
decoration: TextDecoration.underline,
),
),
],
),
),
),
],
),
// Newsletter (optionnel)
Row(
children: [
Checkbox(
value: _acceptNewsletter,
onChanged: (value) {
setState(() {
_acceptNewsletter = value ?? false;
});
},
activeColor: AppTheme.primaryColor,
),
const Expanded(
child: Text(
'Je souhaite recevoir des actualités et conseils par email (optionnel)',
style: TextStyle(
color: AppTheme.textSecondary,
fontSize: 14,
),
),
),
],
),
],
);
}
Widget _buildRegisterButton() {
return LoadingButton(
onPressed: _acceptTerms ? _handleRegister : null,
isLoading: _isLoading,
text: 'Créer mon compte',
width: double.infinity,
height: 56,
enabled: _acceptTerms,
);
}
Widget _buildLoginLink() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Déjà un compte ? ',
style: TextStyle(
color: AppTheme.textSecondary,
fontSize: 14,
),
),
TextButton(
onPressed: () => _navigateToLogin(),
child: const Text(
'Se connecter',
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
),
],
);
}
String? _validateFirstName(String? value) {
if (value == null || value.isEmpty) {
return 'Veuillez saisir votre prénom';
}
if (value.length < 2) {
return 'Le prénom doit contenir au moins 2 caractères';
}
return null;
}
String? _validateLastName(String? value) {
if (value == null || value.isEmpty) {
return 'Veuillez saisir votre nom';
}
if (value.length < 2) {
return 'Le nom doit contenir au moins 2 caractères';
}
return null;
}
String? _validateEmail(String? value) {
if (value == null || value.isEmpty) {
return 'Veuillez saisir votre adresse email';
}
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
return 'Veuillez saisir une adresse email valide';
}
return null;
}
String? _validatePassword(String? value) {
if (value == null || value.isEmpty) {
return 'Veuillez saisir un mot de passe';
}
if (value.length < 8) {
return 'Le mot de passe doit contenir au moins 8 caractères';
}
if (!value.contains(RegExp(r'[A-Z]'))) {
return 'Le mot de passe doit contenir au moins une majuscule';
}
if (!value.contains(RegExp(r'[a-z]'))) {
return 'Le mot de passe doit contenir au moins une minuscule';
}
if (!value.contains(RegExp(r'[0-9]'))) {
return 'Le mot de passe doit contenir au moins un chiffre';
}
return null;
}
String? _validateConfirmPassword(String? value) {
if (value == null || value.isEmpty) {
return 'Veuillez confirmer votre mot de passe';
}
if (value != _passwordController.text) {
return 'Les mots de passe ne correspondent pas';
}
return null;
}
Future<void> _handleRegister() async {
if (!_formKey.currentState!.validate()) {
return;
}
if (!_acceptTerms) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Veuillez accepter les conditions d\'utilisation'),
backgroundColor: AppTheme.errorColor,
behavior: SnackBarBehavior.floating,
),
);
return;
}
setState(() {
_isLoading = true;
});
try {
// Simulation d'inscription
await Future.delayed(const Duration(seconds: 2));
// Vibration de succès
HapticFeedback.lightImpact();
// Afficher message de succès
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Compte créé avec succès ! Vérifiez votre email.'),
backgroundColor: AppTheme.successColor,
behavior: SnackBarBehavior.floating,
),
);
// Navigation vers l'écran de connexion
_navigateToLogin();
}
} catch (e) {
// Gestion d'erreur
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors de la création du compte: ${e.toString()}'),
backgroundColor: AppTheme.errorColor,
behavior: SnackBarBehavior.floating,
),
);
}
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
void _navigateToLogin() {
Navigator.of(context).pushReplacement(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const LoginScreen(),
transitionDuration: const Duration(milliseconds: 400),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(-1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeInOut,
)),
child: child,
);
},
),
);
}
}

View File

@@ -0,0 +1,400 @@
import 'package:flutter/material.dart';
import '../../../../shared/theme/app_theme.dart';
import 'login_screen.dart';
import 'register_screen.dart';
class WelcomeScreen extends StatefulWidget {
const WelcomeScreen({super.key});
@override
State<WelcomeScreen> createState() => _WelcomeScreenState();
}
class _WelcomeScreenState extends State<WelcomeScreen>
with TickerProviderStateMixin {
late AnimationController _fadeController;
late AnimationController _slideController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
@override
void initState() {
super.initState();
_initializeAnimations();
_startAnimations();
}
void _initializeAnimations() {
_fadeController = AnimationController(
duration: const Duration(milliseconds: 1200),
vsync: this,
);
_slideController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _fadeController,
curve: Curves.easeInOut,
));
_slideAnimation = Tween<Offset>(
begin: const Offset(0, 0.5),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _slideController,
curve: Curves.easeOutCubic,
));
}
void _startAnimations() async {
await Future.delayed(const Duration(milliseconds: 200));
_fadeController.forward();
await Future.delayed(const Duration(milliseconds: 300));
_slideController.forward();
}
@override
void dispose() {
_fadeController.dispose();
_slideController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
AppTheme.primaryColor,
AppTheme.primaryDark,
const Color(0xFF0D47A1),
],
),
),
child: SafeArea(
child: AnimatedBuilder(
animation: _fadeAnimation,
builder: (context, child) {
return Opacity(
opacity: _fadeAnimation.value,
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
children: [
// Header avec logo
Expanded(
flex: 3,
child: SlideTransition(
position: _slideAnimation,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Logo principal
Container(
width: 140,
height: 140,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(35),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 25,
offset: const Offset(0, 12),
),
],
),
child: const Icon(
Icons.groups_rounded,
size: 70,
color: AppTheme.primaryColor,
),
),
const SizedBox(height: 32),
// Titre principal
const Text(
'UnionFlow',
style: TextStyle(
fontSize: 42,
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: 1.5,
),
),
const SizedBox(height: 16),
// Sous-titre
Text(
'Gestion moderne d\'associations\net de mutuelles',
style: TextStyle(
fontSize: 18,
color: Colors.white.withOpacity(0.9),
fontWeight: FontWeight.w300,
height: 1.4,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
// Points forts
_buildFeatureHighlights(),
],
),
),
),
// Boutons d'action
Expanded(
flex: 2,
child: SlideTransition(
position: _slideAnimation,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Bouton Connexion
SizedBox(
width: double.infinity,
height: 56,
child: ElevatedButton(
onPressed: () => _navigateToLogin(),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: AppTheme.primaryColor,
elevation: 8,
shadowColor: Colors.black.withOpacity(0.3),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.login, size: 20),
SizedBox(width: 8),
Text(
'Se connecter',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
const SizedBox(height: 16),
// Bouton Inscription
SizedBox(
width: double.infinity,
height: 56,
child: OutlinedButton(
onPressed: () => _navigateToRegister(),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.white,
side: const BorderSide(
color: Colors.white,
width: 2,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.person_add, size: 20),
SizedBox(width: 8),
Text(
'Créer un compte',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
const SizedBox(height: 24),
// Lien mode démo
TextButton(
onPressed: () => _navigateToDemo(),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.visibility,
size: 16,
color: Colors.white.withOpacity(0.8),
),
const SizedBox(width: 6),
Text(
'Découvrir en mode démo',
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
),
),
),
// Footer
Padding(
padding: const EdgeInsets.only(top: 20),
child: Column(
children: [
Text(
'Version 1.0.0 • Sécurisé et confidentiel',
style: TextStyle(
color: Colors.white.withOpacity(0.6),
fontSize: 12,
),
),
const SizedBox(height: 8),
Text(
'© 2024 Lions Club International',
style: TextStyle(
color: Colors.white.withOpacity(0.5),
fontSize: 10,
),
),
],
),
),
],
),
),
);
},
),
),
),
);
}
Widget _buildFeatureHighlights() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.white.withOpacity(0.2),
width: 1,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildFeatureItem(Icons.security, 'Sécurisé'),
_buildFeatureItem(Icons.analytics, 'Analytique'),
_buildFeatureItem(Icons.cloud_sync, 'Synchronisé'),
],
),
);
}
Widget _buildFeatureItem(IconData icon, String label) {
return Column(
children: [
Icon(
icon,
color: Colors.white,
size: 20,
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
],
);
}
void _navigateToLogin() {
Navigator.of(context).push(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const LoginScreen(),
transitionDuration: const Duration(milliseconds: 400),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeInOut,
)),
child: child,
);
},
),
);
}
void _navigateToRegister() {
Navigator.of(context).push(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const RegisterScreen(),
transitionDuration: const Duration(milliseconds: 400),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeInOut,
)),
child: child,
);
},
),
);
}
void _navigateToDemo() {
// TODO: Implémenter la navigation vers le mode démo
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Mode démo - En cours de développement'),
backgroundColor: AppTheme.primaryColor,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
);
}
}

View File

@@ -0,0 +1,362 @@
import 'package:flutter/material.dart';
import '../../../../shared/theme/app_theme.dart';
/// Pied de page de la connexion avec informations et liens
class LoginFooter extends StatelessWidget {
const LoginFooter({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
// Séparateur
_buildDivider(),
const SizedBox(height: 24),
// Informations sur l'application
_buildAppInfo(),
const SizedBox(height: 20),
// Liens utiles
_buildUsefulLinks(context),
const SizedBox(height: 20),
// Version et copyright
_buildVersionInfo(),
],
);
}
Widget _buildDivider() {
return Row(
children: [
Expanded(
child: Container(
height: 1,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
Colors.transparent,
AppTheme.textSecondary.withOpacity(0.3),
Colors.transparent,
],
),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Icon(
Icons.star,
size: 16,
color: AppTheme.textSecondary.withOpacity(0.5),
),
),
Expanded(
child: Container(
height: 1,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
Colors.transparent,
AppTheme.textSecondary.withOpacity(0.3),
Colors.transparent,
],
),
),
),
),
],
);
}
Widget _buildAppInfo() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppTheme.backgroundLight,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppTheme.textSecondary.withOpacity(0.1),
),
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.security,
size: 20,
color: AppTheme.successColor,
),
const SizedBox(width: 8),
Text(
'Connexion sécurisée',
style: TextStyle(
fontSize: 14,
color: AppTheme.successColor,
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: 8),
Text(
'Vos données sont protégées par un cryptage de niveau bancaire',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
],
),
);
}
Widget _buildUsefulLinks(BuildContext context) {
return Wrap(
spacing: 20,
runSpacing: 12,
alignment: WrapAlignment.center,
children: [
_buildLinkButton(
icon: Icons.help_outline,
label: 'Aide',
onTap: () => _showHelpDialog(context),
),
_buildLinkButton(
icon: Icons.info_outline,
label: 'À propos',
onTap: () => _showAboutDialog(context),
),
_buildLinkButton(
icon: Icons.privacy_tip_outlined,
label: 'Confidentialité',
onTap: () => _showPrivacyDialog(context),
),
],
);
}
Widget _buildLinkButton({
required IconData icon,
required String label,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: AppTheme.textSecondary.withOpacity(0.2),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
size: 16,
color: AppTheme.textSecondary,
),
const SizedBox(width: 6),
Text(
label,
style: TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
fontWeight: FontWeight.w500,
),
),
],
),
),
);
}
Widget _buildVersionInfo() {
return Column(
children: [
Text(
'UnionFlow Mobile v1.0.0',
style: TextStyle(
fontSize: 12,
color: AppTheme.textSecondary.withOpacity(0.7),
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
Text(
'© 2025 Lions Dev Team. Tous droits réservés.',
style: TextStyle(
fontSize: 10,
color: AppTheme.textSecondary.withOpacity(0.5),
),
),
],
);
}
void _showHelpDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
title: Row(
children: [
Icon(
Icons.help_outline,
color: AppTheme.infoColor,
),
const SizedBox(width: 12),
const Text('Aide'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHelpItem(
'Connexion',
'Utilisez votre email et mot de passe fournis par votre association.',
),
const SizedBox(height: 12),
_buildHelpItem(
'Mot de passe oublié',
'Contactez votre administrateur pour réinitialiser votre mot de passe.',
),
const SizedBox(height: 12),
_buildHelpItem(
'Problèmes techniques',
'Vérifiez votre connexion internet et réessayez.',
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(
'Fermer',
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
),
),
),
],
),
);
}
void _showAboutDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
title: Row(
children: [
Icon(
Icons.info_outline,
color: AppTheme.primaryColor,
),
const SizedBox(width: 12),
const Text('À propos'),
],
),
content: const Text(
'UnionFlow est une solution complète de gestion d\'associations développée par Lions Dev Team.\n\n'
'Cette application mobile vous permet de gérer vos membres, cotisations, événements et bien plus encore, où que vous soyez.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(
'Fermer',
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
),
),
),
],
),
);
}
void _showPrivacyDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
title: Row(
children: [
Icon(
Icons.privacy_tip_outlined,
color: AppTheme.warningColor,
),
const SizedBox(width: 12),
const Text('Confidentialité'),
],
),
content: const Text(
'Nous respectons votre vie privée. Toutes vos données sont stockées de manière sécurisée et ne sont jamais partagées avec des tiers.\n\n'
'Les données sont chiffrées en transit et au repos selon les standards de sécurité les plus élevés.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(
'Compris',
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
),
),
),
],
),
);
}
Widget _buildHelpItem(String title, String description) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 4),
Text(
description,
style: TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
],
);
}
}

View File

@@ -0,0 +1,437 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../shared/theme/app_theme.dart';
import '../../../../shared/widgets/buttons/buttons.dart';
/// Formulaire de connexion sophistiqué avec validation
class LoginForm extends StatefulWidget {
final GlobalKey<FormState> formKey;
final TextEditingController emailController;
final TextEditingController passwordController;
final bool obscurePassword;
final bool rememberMe;
final bool isLoading;
final VoidCallback onObscureToggle;
final ValueChanged<bool> onRememberMeToggle;
final VoidCallback onSubmit;
const LoginForm({
super.key,
required this.formKey,
required this.emailController,
required this.passwordController,
required this.obscurePassword,
required this.rememberMe,
required this.isLoading,
required this.onObscureToggle,
required this.onRememberMeToggle,
required this.onSubmit,
});
@override
State<LoginForm> createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm>
with TickerProviderStateMixin {
late AnimationController _fieldAnimationController;
late List<Animation<Offset>> _fieldAnimations;
final FocusNode _emailFocusNode = FocusNode();
final FocusNode _passwordFocusNode = FocusNode();
bool _emailHasFocus = false;
bool _passwordHasFocus = false;
@override
void initState() {
super.initState();
_setupAnimations();
_setupFocusListeners();
_startFieldAnimations();
}
void _setupAnimations() {
_fieldAnimationController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_fieldAnimations = List.generate(4, (index) {
return Tween<Offset>(
begin: const Offset(0, 1),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _fieldAnimationController,
curve: Interval(
index * 0.2,
(index * 0.2) + 0.6,
curve: Curves.easeOut,
),
));
});
}
void _setupFocusListeners() {
_emailFocusNode.addListener(() {
setState(() {
_emailHasFocus = _emailFocusNode.hasFocus;
});
});
_passwordFocusNode.addListener(() {
setState(() {
_passwordHasFocus = _passwordFocusNode.hasFocus;
});
});
}
void _startFieldAnimations() {
Future.delayed(const Duration(milliseconds: 200), () {
if (mounted) {
_fieldAnimationController.forward();
}
});
}
@override
void dispose() {
_fieldAnimationController.dispose();
_emailFocusNode.dispose();
_passwordFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Form(
key: widget.formKey,
child: Column(
children: [
// Champ email
SlideTransition(
position: _fieldAnimations[0],
child: _buildEmailField(),
),
const SizedBox(height: 20),
// Champ mot de passe
SlideTransition(
position: _fieldAnimations[1],
child: _buildPasswordField(),
),
const SizedBox(height: 16),
// Options (Se souvenir de moi, Mot de passe oublié)
SlideTransition(
position: _fieldAnimations[2],
child: _buildOptionsRow(),
),
const SizedBox(height: 32),
// Bouton de connexion
SlideTransition(
position: _fieldAnimations[3],
child: _buildLoginButton(),
),
],
),
);
}
Widget _buildEmailField() {
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
boxShadow: _emailHasFocus ? [
BoxShadow(
color: AppTheme.primaryColor.withOpacity(0.2),
blurRadius: 12,
offset: const Offset(0, 4),
),
] : [],
),
child: TextFormField(
controller: widget.emailController,
focusNode: _emailFocusNode,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
enabled: !widget.isLoading,
onFieldSubmitted: (_) {
FocusScope.of(context).requestFocus(_passwordFocusNode);
},
decoration: InputDecoration(
labelText: 'Adresse email',
hintText: 'votre.email@exemple.com',
prefixIcon: AnimatedContainer(
duration: const Duration(milliseconds: 200),
child: Icon(
Icons.email_outlined,
color: _emailHasFocus
? AppTheme.primaryColor
: AppTheme.textSecondary,
),
),
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide(
color: AppTheme.primaryColor,
width: 2,
),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide(
color: AppTheme.errorColor,
width: 2,
),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide(
color: AppTheme.errorColor,
width: 2,
),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez saisir votre email';
}
if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(value)) {
return 'Format d\'email invalide';
}
return null;
},
),
);
}
Widget _buildPasswordField() {
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
boxShadow: _passwordHasFocus ? [
BoxShadow(
color: AppTheme.primaryColor.withOpacity(0.2),
blurRadius: 12,
offset: const Offset(0, 4),
),
] : [],
),
child: TextFormField(
controller: widget.passwordController,
focusNode: _passwordFocusNode,
obscureText: widget.obscurePassword,
textInputAction: TextInputAction.done,
enabled: !widget.isLoading,
onFieldSubmitted: (_) => widget.onSubmit(),
decoration: InputDecoration(
labelText: 'Mot de passe',
hintText: 'Saisissez votre mot de passe',
prefixIcon: AnimatedContainer(
duration: const Duration(milliseconds: 200),
child: Icon(
Icons.lock_outlined,
color: _passwordHasFocus
? AppTheme.primaryColor
: AppTheme.textSecondary,
),
),
suffixIcon: IconButton(
onPressed: widget.onObscureToggle,
icon: AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: Icon(
widget.obscurePassword
? Icons.visibility_outlined
: Icons.visibility_off_outlined,
key: ValueKey(widget.obscurePassword),
color: _passwordHasFocus
? AppTheme.primaryColor
: AppTheme.textSecondary,
),
),
),
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide(
color: AppTheme.primaryColor,
width: 2,
),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide(
color: AppTheme.errorColor,
width: 2,
),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide(
color: AppTheme.errorColor,
width: 2,
),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez saisir votre mot de passe';
}
if (value.length < 6) {
return 'Le mot de passe doit contenir au moins 6 caractères';
}
return null;
},
),
);
}
Widget _buildOptionsRow() {
return Row(
children: [
// Se souvenir de moi
Expanded(
child: GestureDetector(
onTap: () => widget.onRememberMeToggle(!widget.rememberMe),
child: Row(
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: 20,
height: 20,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: widget.rememberMe
? AppTheme.primaryColor
: AppTheme.textSecondary,
width: 2,
),
color: widget.rememberMe
? AppTheme.primaryColor
: Colors.transparent,
),
child: widget.rememberMe
? Icon(
Icons.check,
size: 14,
color: Colors.white,
)
: null,
),
const SizedBox(width: 8),
Text(
'Se souvenir de moi',
style: TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
// Mot de passe oublié
TextButton(
onPressed: widget.isLoading ? null : () {
HapticFeedback.selectionClick();
_showForgotPasswordDialog();
},
child: Text(
'Mot de passe oublié ?',
style: TextStyle(
fontSize: 14,
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
),
),
),
],
);
}
Widget _buildLoginButton() {
return SizedBox(
width: double.infinity,
height: 56,
child: widget.isLoading
? QuickButtons.primary(
text: '',
onPressed: () {},
loading: true,
)
: QuickButtons.primary(
text: 'Se connecter',
icon: Icons.login,
onPressed: widget.onSubmit,
size: ButtonSize.large,
),
);
}
void _showForgotPasswordDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
title: Row(
children: [
Icon(
Icons.help_outline,
color: AppTheme.primaryColor,
),
const SizedBox(width: 12),
const Text('Mot de passe oublié'),
],
),
content: const Text(
'Pour récupérer votre mot de passe, veuillez contacter votre administrateur ou utiliser la fonction de récupération sur l\'interface web.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(
'Compris',
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,259 @@
import 'package:flutter/material.dart';
import '../../../../shared/theme/app_theme.dart';
/// En-tête de la page de connexion avec logo et animation
class LoginHeader extends StatefulWidget {
final VoidCallback? onAnimationComplete;
const LoginHeader({
super.key,
this.onAnimationComplete,
});
@override
State<LoginHeader> createState() => _LoginHeaderState();
}
class _LoginHeaderState extends State<LoginHeader>
with TickerProviderStateMixin {
late AnimationController _logoController;
late AnimationController _textController;
late Animation<double> _logoScaleAnimation;
late Animation<double> _logoRotationAnimation;
late Animation<double> _textFadeAnimation;
late Animation<Offset> _textSlideAnimation;
@override
void initState() {
super.initState();
_setupAnimations();
_startAnimations();
}
void _setupAnimations() {
_logoController = AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: this,
);
_textController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_logoScaleAnimation = Tween<double>(
begin: 0.5,
end: 1.0,
).animate(CurvedAnimation(
parent: _logoController,
curve: Curves.elasticOut,
));
_logoRotationAnimation = Tween<double>(
begin: -0.1,
end: 0.0,
).animate(CurvedAnimation(
parent: _logoController,
curve: Curves.easeOut,
));
_textFadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _textController,
curve: Curves.easeOut,
));
_textSlideAnimation = Tween<Offset>(
begin: const Offset(0, 0.5),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _textController,
curve: Curves.easeOut,
));
}
void _startAnimations() {
_logoController.forward().then((_) {
_textController.forward().then((_) {
widget.onAnimationComplete?.call();
});
});
}
@override
void dispose() {
_logoController.dispose();
_textController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
// Logo animé
AnimatedBuilder(
animation: _logoController,
builder: (context, child) {
return Transform.scale(
scale: _logoScaleAnimation.value,
child: Transform.rotate(
angle: _logoRotationAnimation.value,
child: _buildLogo(),
),
);
},
),
const SizedBox(height: 32),
// Texte animé
AnimatedBuilder(
animation: _textController,
builder: (context, child) {
return FadeTransition(
opacity: _textFadeAnimation,
child: SlideTransition(
position: _textSlideAnimation,
child: _buildWelcomeText(),
),
);
},
),
],
);
}
Widget _buildLogo() {
return Container(
width: 120,
height: 120,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
AppTheme.primaryColor,
AppTheme.secondaryColor,
],
),
borderRadius: BorderRadius.circular(30),
boxShadow: [
BoxShadow(
color: AppTheme.primaryColor.withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: Stack(
alignment: Alignment.center,
children: [
// Effet de brillance
Container(
width: 100,
height: 100,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.white.withOpacity(0.2),
Colors.transparent,
],
),
borderRadius: BorderRadius.circular(25),
),
),
// Icône ou texte du logo
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.group,
size: 48,
color: Colors.white,
),
const SizedBox(height: 4),
Text(
'UF',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: 2,
),
),
],
),
],
),
);
}
Widget _buildWelcomeText() {
return Column(
children: [
// Titre principal
ShaderMask(
shaderCallback: (bounds) => LinearGradient(
colors: [
AppTheme.primaryColor,
AppTheme.secondaryColor,
],
).createShader(bounds),
child: Text(
'UnionFlow',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: 1.2,
),
),
),
const SizedBox(height: 8),
// Sous-titre
Text(
'Gestion d\'associations',
style: TextStyle(
fontSize: 16,
color: AppTheme.textSecondary,
fontWeight: FontWeight.w500,
letterSpacing: 0.5,
),
),
const SizedBox(height: 24),
// Message de bienvenue
Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: AppTheme.primaryColor.withOpacity(0.2),
width: 1,
),
),
child: Text(
'Connectez-vous pour accéder à votre espace',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
color: AppTheme.primaryColor,
fontWeight: FontWeight.w500,
),
),
),
],
);
}
}

View File

@@ -0,0 +1,675 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import '../../../../shared/theme/app_theme.dart';
class DashboardPage extends StatelessWidget {
const DashboardPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.backgroundLight,
appBar: AppBar(
title: const Text('Tableau de bord'),
backgroundColor: AppTheme.primaryColor,
elevation: 0,
actions: [
IconButton(
icon: const Icon(Icons.notifications_outlined),
onPressed: () {},
),
IconButton(
icon: const Icon(Icons.settings_outlined),
onPressed: () {},
),
],
),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Message de bienvenue
_buildWelcomeSection(context),
const SizedBox(height: 24),
// Cartes KPI principales
_buildKPICards(context),
const SizedBox(height: 24),
// Graphiques et statistiques
_buildChartsSection(context),
const SizedBox(height: 24),
// Actions rapides
_buildQuickActions(context),
const SizedBox(height: 24),
// Activités récentes
_buildRecentActivities(context),
],
),
),
),
);
}
Widget _buildWelcomeSection(BuildContext context) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [AppTheme.primaryColor, AppTheme.primaryLight],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Bonjour !',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
'Voici un aperçu de votre association',
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 16,
),
),
],
),
),
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(30),
),
child: const Icon(
Icons.dashboard,
color: Colors.white,
size: 30,
),
),
],
),
);
}
Widget _buildKPICards(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Indicateurs clés',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildKPICard(
context,
'Membres',
'1,247',
'+5.2%',
Icons.people,
AppTheme.primaryColor,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildKPICard(
context,
'Revenus',
'€45,890',
'+12.8%',
Icons.euro,
AppTheme.successColor,
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildKPICard(
context,
'Événements',
'23',
'+3',
Icons.event,
AppTheme.accentColor,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildKPICard(
context,
'Cotisations',
'89.5%',
'+2.1%',
Icons.payments,
AppTheme.infoColor,
),
),
],
),
],
);
}
Widget _buildKPICard(
BuildContext context,
String title,
String value,
String change,
IconData icon,
Color color,
) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
icon,
color: color,
size: 20,
),
),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppTheme.successColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
change,
style: const TextStyle(
color: AppTheme.successColor,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
],
),
const SizedBox(height: 12),
Text(
value,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 4),
Text(
title,
style: const TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
),
),
],
),
);
}
Widget _buildChartsSection(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Analyses',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildLineChart(context),
),
const SizedBox(width: 12),
Expanded(
child: _buildPieChart(context),
),
],
),
],
);
}
Widget _buildLineChart(BuildContext context) {
return Container(
height: 200,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Évolution des membres',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 16),
Expanded(
child: LineChart(
LineChartData(
gridData: const FlGridData(show: false),
titlesData: const FlTitlesData(show: false),
borderData: FlBorderData(show: false),
lineBarsData: [
LineChartBarData(
spots: const [
FlSpot(0, 1000),
FlSpot(1, 1050),
FlSpot(2, 1100),
FlSpot(3, 1180),
FlSpot(4, 1247),
],
color: AppTheme.primaryColor,
barWidth: 3,
isStrokeCapRound: true,
dotData: const FlDotData(show: false),
belowBarData: BarAreaData(
show: true,
color: AppTheme.primaryColor.withOpacity(0.1),
),
),
],
),
),
),
],
),
);
}
Widget _buildPieChart(BuildContext context) {
return Container(
height: 200,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Répartition des membres',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 16),
Expanded(
child: PieChart(
PieChartData(
sectionsSpace: 0,
centerSpaceRadius: 40,
sections: [
PieChartSectionData(
color: AppTheme.primaryColor,
value: 45,
title: '45%',
radius: 50,
titleStyle: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
PieChartSectionData(
color: AppTheme.secondaryColor,
value: 30,
title: '30%',
radius: 50,
titleStyle: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
PieChartSectionData(
color: AppTheme.accentColor,
value: 25,
title: '25%',
radius: 50,
titleStyle: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
),
),
],
),
);
}
Widget _buildQuickActions(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Actions rapides',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildActionCard(
context,
'Nouveau membre',
'Ajouter un membre',
Icons.person_add,
AppTheme.primaryColor,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildActionCard(
context,
'Créer événement',
'Organiser un événement',
Icons.event_available,
AppTheme.secondaryColor,
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildActionCard(
context,
'Suivi cotisations',
'Gérer les cotisations',
Icons.payment,
AppTheme.accentColor,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildActionCard(
context,
'Rapports',
'Générer des rapports',
Icons.analytics,
AppTheme.infoColor,
),
),
],
),
],
);
}
Widget _buildActionCard(
BuildContext context,
String title,
String subtitle,
IconData icon,
Color color,
) {
return InkWell(
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('$title - En cours de développement'),
backgroundColor: color,
),
);
},
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withOpacity(0.2)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
icon,
color: color,
size: 24,
),
),
const SizedBox(height: 12),
Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 4),
Text(
subtitle,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
textAlign: TextAlign.center,
),
],
),
),
);
}
Widget _buildRecentActivities(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Activités récentes',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
TextButton(
onPressed: () {},
child: const Text('Voir tout'),
),
],
),
const SizedBox(height: 16),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
_buildActivityItem(
'Nouveau membre inscrit',
'Marie Dupont a rejoint l\'association',
Icons.person_add,
AppTheme.successColor,
'Il y a 2h',
),
const Divider(height: 1),
_buildActivityItem(
'Cotisation reçue',
'Jean Martin a payé sa cotisation annuelle',
Icons.payment,
AppTheme.primaryColor,
'Il y a 4h',
),
const Divider(height: 1),
_buildActivityItem(
'Événement créé',
'Assemblée générale 2024 programmée',
Icons.event,
AppTheme.accentColor,
'Hier',
),
],
),
),
],
);
}
Widget _buildActivityItem(
String title,
String description,
IconData icon,
Color color,
String time,
) {
return Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
icon,
color: color,
size: 16,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 2),
Text(
description,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
],
),
),
Text(
time,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textHint,
),
),
],
),
);
}
}

View File

@@ -0,0 +1,485 @@
import 'package:flutter/material.dart';
import '../../../../shared/theme/app_theme.dart';
import '../widgets/kpi_card.dart';
import '../widgets/clickable_kpi_card.dart';
import '../widgets/chart_card.dart';
import '../widgets/activity_feed.dart';
import '../widgets/quick_actions_grid.dart';
import '../widgets/navigation_cards.dart';
class EnhancedDashboard extends StatefulWidget {
final Function(int)? onNavigateToTab;
const EnhancedDashboard({
super.key,
this.onNavigateToTab,
});
@override
State<EnhancedDashboard> createState() => _EnhancedDashboardState();
}
class _EnhancedDashboardState extends State<EnhancedDashboard> {
final PageController _pageController = PageController();
int _currentPage = 0;
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.backgroundLight,
body: CustomScrollView(
slivers: [
_buildAppBar(),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildWelcomeCard(),
const SizedBox(height: 24),
_buildKPISection(),
const SizedBox(height: 24),
_buildChartsSection(),
const SizedBox(height: 24),
NavigationCards(
onNavigateToTab: widget.onNavigateToTab,
),
const SizedBox(height: 24),
const QuickActionsGrid(),
const SizedBox(height: 24),
const ActivityFeed(),
const SizedBox(height: 24),
],
),
),
),
],
),
);
}
Widget _buildAppBar() {
return SliverAppBar(
expandedHeight: 120,
floating: false,
pinned: true,
backgroundColor: AppTheme.primaryColor,
flexibleSpace: FlexibleSpaceBar(
title: const Text(
'Tableau de bord',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
background: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [AppTheme.primaryColor, AppTheme.primaryDark],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
),
),
actions: [
IconButton(
icon: const Icon(Icons.notifications_outlined),
onPressed: () => _showNotifications(),
),
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () => _refreshData(),
),
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert),
onSelected: _handleMenuSelection,
itemBuilder: (context) => [
const PopupMenuItem(
value: 'settings',
child: Row(
children: [
Icon(Icons.settings),
SizedBox(width: 8),
Text('Paramètres'),
],
),
),
const PopupMenuItem(
value: 'export',
child: Row(
children: [
Icon(Icons.download),
SizedBox(width: 8),
Text('Exporter'),
],
),
),
const PopupMenuItem(
value: 'help',
child: Row(
children: [
Icon(Icons.help),
SizedBox(width: 8),
Text('Aide'),
],
),
),
],
),
],
);
}
Widget _buildWelcomeCard() {
return Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [AppTheme.primaryColor, AppTheme.primaryLight],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: AppTheme.primaryColor.withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Bonjour !',
style: TextStyle(
color: Colors.white,
fontSize: 28,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
'Découvrez les dernières statistiques de votre association',
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 16,
),
),
const SizedBox(height: 16),
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.trending_up,
color: Colors.white,
size: 16,
),
const SizedBox(width: 4),
Text(
'+12% ce mois',
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
],
),
),
],
),
],
),
),
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(40),
),
child: const Icon(
Icons.dashboard_rounded,
color: Colors.white,
size: 40,
),
),
],
),
);
}
Widget _buildKPISection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Indicateurs clés',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
TextButton.icon(
onPressed: () {},
icon: const Icon(Icons.analytics, size: 16),
label: const Text('Analyse détaillée'),
),
],
),
const SizedBox(height: 16),
SizedBox(
height: 180,
child: PageView(
controller: _pageController,
onPageChanged: (index) {
setState(() {
_currentPage = index;
});
},
children: [
_buildKPIPage1(),
_buildKPIPage2(),
],
),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildPageIndicator(0),
const SizedBox(width: 8),
_buildPageIndicator(1),
],
),
],
);
}
Widget _buildKPIPage1() {
return Row(
children: [
Expanded(
child: ClickableKPICard(
title: 'Membres actifs',
value: '1,247',
change: '+5.2%',
icon: Icons.people,
color: AppTheme.secondaryColor,
actionText: 'Gérer',
onTap: () => widget.onNavigateToTab?.call(1),
),
),
const SizedBox(width: 12),
Expanded(
child: ClickableKPICard(
title: 'Revenus mensuel',
value: '€45,890',
change: '+12.8%',
icon: Icons.euro,
color: AppTheme.successColor,
actionText: 'Finances',
onTap: () => _showFinancesMessage(),
),
),
],
);
}
Widget _buildKPIPage2() {
return Row(
children: [
Expanded(
child: ClickableKPICard(
title: 'Événements',
value: '23',
change: '+3',
icon: Icons.event,
color: AppTheme.warningColor,
actionText: 'Planifier',
onTap: () => widget.onNavigateToTab?.call(3),
),
),
const SizedBox(width: 12),
Expanded(
child: ClickableKPICard(
title: 'Taux cotisation',
value: '89.5%',
change: '+2.1%',
icon: Icons.payments,
color: AppTheme.accentColor,
actionText: 'Gérer',
onTap: () => widget.onNavigateToTab?.call(2),
),
),
],
);
}
Widget _buildPageIndicator(int index) {
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
width: _currentPage == index ? 20 : 8,
height: 8,
decoration: BoxDecoration(
color: _currentPage == index
? AppTheme.primaryColor
: AppTheme.primaryColor.withOpacity(0.3),
borderRadius: BorderRadius.circular(4),
),
);
}
Widget _buildChartsSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Analyses et tendances',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 16),
ChartCard(
title: 'Évolution des membres',
subtitle: 'Croissance sur 6 mois',
chart: const MembershipChart(),
onTap: () => widget.onNavigateToTab?.call(1),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: ChartCard(
title: 'Répartition',
subtitle: 'Par catégorie',
chart: const CategoryChart(),
onTap: () => widget.onNavigateToTab?.call(1),
),
),
const SizedBox(width: 12),
Expanded(
child: ChartCard(
title: 'Revenus',
subtitle: 'Évolution mensuelle',
chart: const RevenueChart(),
onTap: () => _showFinancesMessage(),
),
),
],
),
],
);
}
void _showNotifications() {
showModalBottomSheet(
context: context,
builder: (context) => Container(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Notifications',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
ListTile(
leading: const Icon(Icons.warning, color: AppTheme.warningColor),
title: const Text('3 cotisations en retard'),
subtitle: const Text('Nécessite votre attention'),
onTap: () {},
),
ListTile(
leading: const Icon(Icons.event, color: AppTheme.accentColor),
title: const Text('Assemblée générale'),
subtitle: const Text('Dans 5 jours'),
onTap: () {},
),
ListTile(
leading: const Icon(Icons.check_circle, color: AppTheme.successColor),
title: const Text('Rapport mensuel'),
subtitle: const Text('Prêt à être envoyé'),
onTap: () {},
),
],
),
),
);
}
void _refreshData() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Données actualisées'),
backgroundColor: AppTheme.successColor,
behavior: SnackBarBehavior.floating,
),
);
}
void _handleMenuSelection(String value) {
switch (value) {
case 'settings':
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Paramètres - En développement')),
);
break;
case 'export':
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Export - En développement')),
);
break;
case 'help':
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Aide - En développement')),
);
break;
}
}
void _showFinancesMessage() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Module Finances - Prochainement disponible'),
backgroundColor: AppTheme.successColor,
behavior: SnackBarBehavior.floating,
),
);
}
}

View File

@@ -0,0 +1,218 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../../../../shared/theme/app_theme.dart';
class ActivityFeed extends StatelessWidget {
const ActivityFeed({super.key});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 15,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Activités récentes',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
TextButton(
onPressed: () {},
child: const Text('Voir tout'),
),
],
),
),
..._getActivities().map((activity) => _buildActivityItem(activity)),
],
),
);
}
Widget _buildActivityItem(ActivityItem activity) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
decoration: const BoxDecoration(
border: Border(
top: BorderSide(color: AppTheme.borderColor, width: 0.5),
),
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: activity.color.withOpacity(0.15),
borderRadius: BorderRadius.circular(20),
),
child: Icon(
activity.icon,
color: activity.color,
size: 20,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
activity.title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 4),
Text(
activity.description,
style: const TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
),
),
const SizedBox(height: 8),
Row(
children: [
Icon(
Icons.access_time,
size: 14,
color: AppTheme.textHint,
),
const SizedBox(width: 4),
Text(
_formatTime(activity.timestamp),
style: const TextStyle(
fontSize: 12,
color: AppTheme.textHint,
),
),
],
),
],
),
),
if (activity.actionRequired)
Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
color: AppTheme.errorColor,
shape: BoxShape.circle,
),
),
],
),
);
}
List<ActivityItem> _getActivities() {
final now = DateTime.now();
return [
ActivityItem(
title: 'Nouveau membre inscrit',
description: 'Marie Dupont a rejoint l\'association',
icon: Icons.person_add,
color: AppTheme.successColor,
timestamp: now.subtract(const Duration(hours: 2)),
actionRequired: false,
),
ActivityItem(
title: 'Cotisation en retard',
description: 'Pierre Martin - Cotisation échue depuis 5 jours',
icon: Icons.warning,
color: AppTheme.warningColor,
timestamp: now.subtract(const Duration(hours: 4)),
actionRequired: true,
),
ActivityItem(
title: 'Paiement reçu',
description: 'Jean Dubois - Cotisation annuelle 2024',
icon: Icons.payment,
color: AppTheme.primaryColor,
timestamp: now.subtract(const Duration(hours: 6)),
actionRequired: false,
),
ActivityItem(
title: 'Événement créé',
description: 'Assemblée générale 2024 - 15 mars 2024',
icon: Icons.event,
color: AppTheme.accentColor,
timestamp: now.subtract(const Duration(days: 1)),
actionRequired: false,
),
ActivityItem(
title: 'Mise à jour profil',
description: 'Sophie Bernard a modifié ses informations',
icon: Icons.edit,
color: AppTheme.infoColor,
timestamp: now.subtract(const Duration(days: 1, hours: 3)),
actionRequired: false,
),
ActivityItem(
title: 'Nouveau document',
description: 'Procès-verbal ajouté aux archives',
icon: Icons.file_upload,
color: AppTheme.secondaryColor,
timestamp: now.subtract(const Duration(days: 2)),
actionRequired: false,
),
];
}
String _formatTime(DateTime timestamp) {
final now = DateTime.now();
final difference = now.difference(timestamp);
if (difference.inMinutes < 60) {
return 'Il y a ${difference.inMinutes} min';
} else if (difference.inHours < 24) {
return 'Il y a ${difference.inHours}h';
} else if (difference.inDays == 1) {
return 'Hier';
} else if (difference.inDays < 7) {
return 'Il y a ${difference.inDays} jours';
} else {
return DateFormat('dd/MM/yyyy').format(timestamp);
}
}
}
class ActivityItem {
final String title;
final String description;
final IconData icon;
final Color color;
final DateTime timestamp;
final bool actionRequired;
ActivityItem({
required this.title,
required this.description,
required this.icon,
required this.color,
required this.timestamp,
this.actionRequired = false,
});
}

View File

@@ -0,0 +1,335 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import '../../../../shared/theme/app_theme.dart';
class ChartCard extends StatelessWidget {
final String title;
final Widget chart;
final String? subtitle;
final VoidCallback? onTap;
const ChartCard({
super.key,
required this.title,
required this.chart,
this.subtitle,
this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 15,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
if (subtitle != null) ...[
const SizedBox(height: 4),
Text(
subtitle!,
style: const TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
),
),
],
],
),
),
if (onTap != null)
const Icon(
Icons.arrow_forward_ios,
size: 16,
color: AppTheme.textHint,
),
],
),
const SizedBox(height: 20),
SizedBox(
height: 200,
child: chart,
),
],
),
),
);
}
}
class MembershipChart extends StatelessWidget {
const MembershipChart({super.key});
@override
Widget build(BuildContext context) {
return LineChart(
LineChartData(
gridData: FlGridData(
show: true,
drawVerticalLine: false,
horizontalInterval: 200,
getDrawingHorizontalLine: (value) {
return FlLine(
color: AppTheme.borderColor.withOpacity(0.5),
strokeWidth: 1,
);
},
),
titlesData: FlTitlesData(
show: true,
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: 200,
getTitlesWidget: (value, meta) {
return Text(
value.toInt().toString(),
style: const TextStyle(
color: AppTheme.textHint,
fontSize: 12,
),
);
},
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) {
const months = ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Jun'];
if (value.toInt() < months.length) {
return Text(
months[value.toInt()],
style: const TextStyle(
color: AppTheme.textHint,
fontSize: 12,
),
);
}
return const Text('');
},
),
),
),
borderData: FlBorderData(show: false),
minX: 0,
maxX: 5,
minY: 800,
maxY: 1400,
lineBarsData: [
LineChartBarData(
spots: const [
FlSpot(0, 1000),
FlSpot(1, 1050),
FlSpot(2, 1100),
FlSpot(3, 1180),
FlSpot(4, 1220),
FlSpot(5, 1247),
],
color: AppTheme.primaryColor,
barWidth: 3,
isStrokeCapRound: true,
dotData: FlDotData(
show: true,
getDotPainter: (spot, percent, barData, index) {
return FlDotCirclePainter(
radius: 4,
color: AppTheme.primaryColor,
strokeWidth: 2,
strokeColor: Colors.white,
);
},
),
belowBarData: BarAreaData(
show: true,
gradient: LinearGradient(
colors: [
AppTheme.primaryColor.withOpacity(0.3),
AppTheme.primaryColor.withOpacity(0.1),
AppTheme.primaryColor.withOpacity(0.0),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
),
],
),
);
}
}
class CategoryChart extends StatelessWidget {
const CategoryChart({super.key});
@override
Widget build(BuildContext context) {
return PieChart(
PieChartData(
sectionsSpace: 4,
centerSpaceRadius: 50,
sections: [
PieChartSectionData(
color: AppTheme.primaryColor,
value: 45,
title: 'Actifs\n45%',
radius: 60,
titleStyle: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
PieChartSectionData(
color: AppTheme.secondaryColor,
value: 30,
title: 'Retraités\n30%',
radius: 60,
titleStyle: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
PieChartSectionData(
color: AppTheme.accentColor,
value: 25,
title: 'Étudiants\n25%',
radius: 60,
titleStyle: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
);
}
}
class RevenueChart extends StatelessWidget {
const RevenueChart({super.key});
@override
Widget build(BuildContext context) {
return BarChart(
BarChartData(
alignment: BarChartAlignment.spaceAround,
maxY: 15000,
barTouchData: BarTouchData(enabled: false),
titlesData: FlTitlesData(
show: true,
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: 5000,
getTitlesWidget: (value, meta) {
return Text(
'${(value / 1000).toInt()}k€',
style: const TextStyle(
color: AppTheme.textHint,
fontSize: 12,
),
);
},
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) {
const months = ['J', 'F', 'M', 'A', 'M', 'J'];
if (value.toInt() < months.length) {
return Text(
months[value.toInt()],
style: const TextStyle(
color: AppTheme.textHint,
fontSize: 12,
),
);
}
return const Text('');
},
),
),
),
borderData: FlBorderData(show: false),
gridData: FlGridData(
show: true,
drawVerticalLine: false,
horizontalInterval: 5000,
getDrawingHorizontalLine: (value) {
return FlLine(
color: AppTheme.borderColor.withOpacity(0.5),
strokeWidth: 1,
);
},
),
barGroups: [
_buildBarGroup(0, 8000, AppTheme.primaryColor),
_buildBarGroup(1, 9500, AppTheme.primaryColor),
_buildBarGroup(2, 7800, AppTheme.primaryColor),
_buildBarGroup(3, 11200, AppTheme.primaryColor),
_buildBarGroup(4, 13500, AppTheme.primaryColor),
_buildBarGroup(5, 12800, AppTheme.primaryColor),
],
),
);
}
BarChartGroupData _buildBarGroup(int x, double y, Color color) {
return BarChartGroupData(
x: x,
barRods: [
BarChartRodData(
toY: y,
color: color,
width: 16,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(4),
topRight: Radius.circular(4),
),
),
],
);
}
}

View File

@@ -0,0 +1,252 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../shared/theme/app_theme.dart';
import '../../../../core/utils/responsive_utils.dart';
class ClickableKPICard extends StatefulWidget {
final String title;
final String value;
final String change;
final IconData icon;
final Color color;
final bool isPositiveChange;
final VoidCallback? onTap;
final String? actionText;
const ClickableKPICard({
super.key,
required this.title,
required this.value,
required this.change,
required this.icon,
required this.color,
this.isPositiveChange = true,
this.onTap,
this.actionText,
});
@override
State<ClickableKPICard> createState() => _ClickableKPICardState();
}
class _ClickableKPICardState extends State<ClickableKPICard>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 150),
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 1.0,
end: 0.95,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// Initialiser ResponsiveUtils
ResponsiveUtils.init(context);
return AnimatedBuilder(
animation: _scaleAnimation,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: widget.onTap != null ? _handleTap : null,
onTapDown: widget.onTap != null ? (_) => _animationController.forward() : null,
onTapUp: widget.onTap != null ? (_) => _animationController.reverse() : null,
onTapCancel: widget.onTap != null ? () => _animationController.reverse() : null,
borderRadius: ResponsiveUtils.borderRadius(4),
child: Container(
padding: ResponsiveUtils.paddingAll(5),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: ResponsiveUtils.borderRadius(4),
border: widget.onTap != null
? Border.all(
color: widget.color.withOpacity(0.2),
width: ResponsiveUtils.adaptive(
small: 1,
medium: 1.5,
large: 2,
),
)
: null,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 3.5.sp,
offset: Offset(0, 1.hp),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// Icône et indicateur de changement
Flexible(
child: Row(
children: [
Container(
padding: ResponsiveUtils.paddingAll(2.5),
decoration: BoxDecoration(
color: widget.color.withOpacity(0.15),
borderRadius: ResponsiveUtils.borderRadius(2.5),
),
child: Icon(
widget.icon,
color: widget.color,
size: ResponsiveUtils.iconSize(5),
),
),
const Spacer(),
_buildChangeIndicator(),
],
),
),
SizedBox(height: 2.hp),
// Valeur principale
Flexible(
child: Text(
widget.value,
style: TextStyle(
fontSize: ResponsiveUtils.adaptive(
small: 4.5.fs,
medium: 4.2.fs,
large: 4.fs,
),
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
SizedBox(height: 0.5.hp),
// Titre et action
Flexible(
child: Row(
children: [
Expanded(
child: Text(
widget.title,
style: TextStyle(
fontSize: ResponsiveUtils.adaptive(
small: 3.fs,
medium: 2.8.fs,
large: 2.6.fs,
),
color: AppTheme.textSecondary,
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
if (widget.onTap != null) ...[
SizedBox(width: 1.5.wp),
Flexible(
child: Container(
padding: ResponsiveUtils.paddingSymmetric(
horizontal: 1.5,
vertical: 0.3,
),
decoration: BoxDecoration(
color: widget.color.withOpacity(0.1),
borderRadius: ResponsiveUtils.borderRadius(2.5),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
widget.actionText ?? 'Voir',
style: TextStyle(
color: widget.color,
fontSize: 2.5.fs,
fontWeight: FontWeight.w600,
),
),
SizedBox(width: 0.5.wp),
Icon(
Icons.arrow_forward_ios,
size: ResponsiveUtils.iconSize(2),
color: widget.color,
),
],
),
),
),
],
],
),
),
],
),
),
),
),
);
},
);
}
Widget _buildChangeIndicator() {
final changeColor = widget.isPositiveChange
? AppTheme.successColor
: AppTheme.errorColor;
final changeIcon = widget.isPositiveChange
? Icons.trending_up
: Icons.trending_down;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: changeColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
changeIcon,
size: 16,
color: changeColor,
),
const SizedBox(width: 4),
Text(
widget.change,
style: TextStyle(
color: changeColor,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
],
),
);
}
void _handleTap() {
HapticFeedback.lightImpact();
widget.onTap?.call();
}
}

View File

@@ -0,0 +1,116 @@
import 'package:flutter/material.dart';
import '../../../../shared/theme/app_theme.dart';
class KPICard extends StatelessWidget {
final String title;
final String value;
final String change;
final IconData icon;
final Color color;
final bool isPositiveChange;
const KPICard({
super.key,
required this.title,
required this.value,
required this.change,
required this.icon,
required this.color,
this.isPositiveChange = true,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 15,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.15),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
icon,
color: color,
size: 24,
),
),
const Spacer(),
_buildChangeIndicator(),
],
),
const SizedBox(height: 20),
Text(
value,
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 8),
Text(
title,
style: const TextStyle(
fontSize: 16,
color: AppTheme.textSecondary,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
Widget _buildChangeIndicator() {
final changeColor = isPositiveChange
? AppTheme.successColor
: AppTheme.errorColor;
final changeIcon = isPositiveChange
? Icons.trending_up
: Icons.trending_down;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: changeColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
changeIcon,
size: 16,
color: changeColor,
),
const SizedBox(width: 4),
Text(
change,
style: TextStyle(
color: changeColor,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
],
),
);
}
}

View File

@@ -0,0 +1,281 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../shared/theme/app_theme.dart';
import '../../../../core/utils/responsive_utils.dart';
class NavigationCards extends StatelessWidget {
final Function(int)? onNavigateToTab;
const NavigationCards({
super.key,
this.onNavigateToTab,
});
@override
Widget build(BuildContext context) {
ResponsiveUtils.init(context);
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 15,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.all(20),
child: Row(
children: [
Icon(
Icons.dashboard_customize,
color: AppTheme.primaryColor,
size: 20,
),
SizedBox(width: 8),
Text(
'Accès rapide aux modules',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
],
),
),
Padding(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 20),
child: GridView.count(
crossAxisCount: 2,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 1.1,
children: [
_buildNavigationCard(
context,
title: 'Membres',
subtitle: '1,247 membres',
icon: Icons.people_rounded,
color: AppTheme.secondaryColor,
onTap: () => _navigateToModule(context, 1, 'Membres'),
badge: '+5 cette semaine',
),
_buildNavigationCard(
context,
title: 'Cotisations',
subtitle: '89.5% à jour',
icon: Icons.payment_rounded,
color: AppTheme.accentColor,
onTap: () => _navigateToModule(context, 2, 'Cotisations'),
badge: '15 en retard',
badgeColor: AppTheme.warningColor,
),
_buildNavigationCard(
context,
title: 'Événements',
subtitle: '3 à venir',
icon: Icons.event_rounded,
color: AppTheme.warningColor,
onTap: () => _navigateToModule(context, 3, 'Événements'),
badge: 'AG dans 5 jours',
),
_buildNavigationCard(
context,
title: 'Finances',
subtitle: '€45,890',
icon: Icons.account_balance_rounded,
color: AppTheme.primaryColor,
onTap: () => _navigateToModule(context, 4, 'Finances'),
badge: '+12.8% ce mois',
badgeColor: AppTheme.successColor,
),
],
),
),
],
),
);
}
Widget _buildNavigationCard(
BuildContext context, {
required String title,
required String subtitle,
required IconData icon,
required Color color,
required VoidCallback onTap,
String? badge,
Color? badgeColor,
}) {
return Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
HapticFeedback.lightImpact();
onTap();
},
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border.all(
color: color.withOpacity(0.2),
width: 1,
),
borderRadius: BorderRadius.circular(12),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
color.withOpacity(0.05),
color.withOpacity(0.02),
],
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header avec icône et badge
Row(
children: [
Flexible(
child: Container(
width: ResponsiveUtils.iconSize(8),
height: ResponsiveUtils.iconSize(8),
decoration: BoxDecoration(
color: color.withOpacity(0.15),
borderRadius: BorderRadius.circular(ResponsiveUtils.iconSize(4)),
),
child: Icon(
icon,
color: color,
size: ResponsiveUtils.iconSize(4.5),
),
),
),
const Spacer(),
if (badge != null)
Flexible(
child: Container(
padding: ResponsiveUtils.paddingSymmetric(
horizontal: 1.5,
vertical: 0.3,
),
decoration: BoxDecoration(
color: (badgeColor ?? AppTheme.successColor).withOpacity(0.1),
borderRadius: ResponsiveUtils.borderRadius(2),
border: Border.all(
color: (badgeColor ?? AppTheme.successColor).withOpacity(0.3),
width: 0.5,
),
),
child: Text(
badge,
style: TextStyle(
color: badgeColor ?? AppTheme.successColor,
fontSize: 2.5.fs,
fontWeight: FontWeight.w600,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
],
),
const Spacer(),
// Contenu principal
Text(
title,
style: TextStyle(
fontSize: ResponsiveUtils.adaptive(
small: 4.fs,
medium: 3.8.fs,
large: 3.6.fs,
),
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 1.hp),
Text(
subtitle,
style: TextStyle(
fontSize: ResponsiveUtils.adaptive(
small: 3.2.fs,
medium: 3.fs,
large: 2.8.fs,
),
color: AppTheme.textSecondary,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
// Flèche d'action
Row(
children: [
Text(
'Gérer',
style: TextStyle(
fontSize: 12,
color: color,
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: 4),
Icon(
Icons.arrow_forward_ios,
size: 12,
color: color,
),
],
),
],
),
),
),
);
}
void _navigateToModule(BuildContext context, int tabIndex, String moduleName) {
// Si onNavigateToTab est fourni, l'utiliser pour naviguer vers l'onglet
if (onNavigateToTab != null) {
onNavigateToTab!(tabIndex);
} else {
// Sinon, afficher un message temporaire
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Navigation vers $moduleName'),
backgroundColor: AppTheme.primaryColor,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
action: SnackBarAction(
label: 'OK',
textColor: Colors.white,
onPressed: () {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
},
),
),
);
}
}
}

View File

@@ -0,0 +1,214 @@
import 'package:flutter/material.dart';
import '../../../../shared/theme/app_theme.dart';
import '../../../../core/utils/responsive_utils.dart';
class QuickActionsGrid extends StatelessWidget {
const QuickActionsGrid({super.key});
@override
Widget build(BuildContext context) {
ResponsiveUtils.init(context);
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 15,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.all(20),
child: Text(
'Actions rapides',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 20),
child: GridView.count(
crossAxisCount: 2,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: 1.2,
children: _getQuickActions(context),
),
),
],
),
);
}
List<Widget> _getQuickActions(BuildContext context) {
final actions = [
QuickAction(
title: 'Nouveau membre',
description: 'Ajouter un membre',
icon: Icons.person_add,
color: AppTheme.primaryColor,
onTap: () => _showAction(context, 'Nouveau membre'),
),
QuickAction(
title: 'Créer événement',
description: 'Organiser un événement',
icon: Icons.event_available,
color: AppTheme.secondaryColor,
onTap: () => _showAction(context, 'Créer événement'),
),
QuickAction(
title: 'Suivi cotisations',
description: 'Gérer les cotisations',
icon: Icons.payment,
color: AppTheme.accentColor,
onTap: () => _showAction(context, 'Suivi cotisations'),
),
QuickAction(
title: 'Rapports',
description: 'Générer des rapports',
icon: Icons.analytics,
color: AppTheme.infoColor,
onTap: () => _showAction(context, 'Rapports'),
),
QuickAction(
title: 'Messages',
description: 'Envoyer des notifications',
icon: Icons.message,
color: AppTheme.warningColor,
onTap: () => _showAction(context, 'Messages'),
),
QuickAction(
title: 'Documents',
description: 'Gérer les documents',
icon: Icons.folder,
color: Color(0xFF9C27B0),
onTap: () => _showAction(context, 'Documents'),
),
];
return actions.map((action) => _buildActionCard(action)).toList();
}
Widget _buildActionCard(QuickAction action) {
return Material(
color: Colors.transparent,
child: InkWell(
onTap: action.onTap,
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border.all(
color: action.color.withOpacity(0.2),
width: 1,
),
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Container(
width: ResponsiveUtils.iconSize(12),
height: ResponsiveUtils.iconSize(12),
decoration: BoxDecoration(
color: action.color.withOpacity(0.15),
borderRadius: BorderRadius.circular(ResponsiveUtils.iconSize(6)),
),
child: Icon(
action.icon,
color: action.color,
size: ResponsiveUtils.iconSize(6),
),
),
),
SizedBox(height: 2.hp),
Flexible(
child: Text(
action.title,
style: TextStyle(
fontSize: ResponsiveUtils.adaptive(
small: 3.5.fs,
medium: 3.2.fs,
large: 3.fs,
),
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
SizedBox(height: 0.5.hp),
Flexible(
child: Text(
action.description,
style: TextStyle(
fontSize: ResponsiveUtils.adaptive(
small: 2.8.fs,
medium: 2.6.fs,
large: 2.4.fs,
),
color: AppTheme.textSecondary,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
),
);
}
void _showAction(BuildContext context, String actionName) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('$actionName - En cours de développement'),
backgroundColor: AppTheme.primaryColor,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
action: SnackBarAction(
label: 'OK',
textColor: Colors.white,
onPressed: () {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
},
),
),
);
}
}
class QuickAction {
final String title;
final String description;
final IconData icon;
final Color color;
final VoidCallback onTap;
QuickAction({
required this.title,
required this.description,
required this.icon,
required this.color,
required this.onTap,
});
}

View File

@@ -0,0 +1,627 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../shared/theme/app_theme.dart';
import '../../../../core/utils/responsive_utils.dart';
import '../widgets/sophisticated_member_card.dart';
import '../widgets/members_search_bar.dart';
import '../widgets/members_filter_sheet.dart';
class MembersListPage extends StatefulWidget {
const MembersListPage({super.key});
@override
State<MembersListPage> createState() => _MembersListPageState();
}
class _MembersListPageState extends State<MembersListPage>
with TickerProviderStateMixin {
late TabController _tabController;
final TextEditingController _searchController = TextEditingController();
final ScrollController _scrollController = ScrollController();
String _searchQuery = '';
String _selectedFilter = 'Tous';
bool _isSearchActive = false;
final List<Map<String, dynamic>> _members = [
{
'id': '1',
'firstName': 'Jean',
'lastName': 'Dupont',
'email': 'jean.dupont@email.com',
'phone': '+33 6 12 34 56 78',
'role': 'Président',
'status': 'Actif',
'joinDate': '2022-01-15',
'lastActivity': '2024-08-15',
'cotisationStatus': 'À jour',
'avatar': null,
'category': 'Bureau',
},
{
'id': '2',
'firstName': 'Marie',
'lastName': 'Martin',
'email': 'marie.martin@email.com',
'phone': '+33 6 98 76 54 32',
'role': 'Secrétaire',
'status': 'Actif',
'joinDate': '2022-03-20',
'lastActivity': '2024-08-14',
'cotisationStatus': 'À jour',
'avatar': null,
'category': 'Bureau',
},
{
'id': '3',
'firstName': 'Pierre',
'lastName': 'Dubois',
'email': 'pierre.dubois@email.com',
'phone': '+33 6 55 44 33 22',
'role': 'Trésorier',
'status': 'Actif',
'joinDate': '2022-02-10',
'lastActivity': '2024-08-13',
'cotisationStatus': 'En retard',
'avatar': null,
'category': 'Bureau',
},
{
'id': '4',
'firstName': 'Sophie',
'lastName': 'Leroy',
'email': 'sophie.leroy@email.com',
'phone': '+33 6 11 22 33 44',
'role': 'Membre',
'status': 'Actif',
'joinDate': '2023-05-12',
'lastActivity': '2024-08-12',
'cotisationStatus': 'À jour',
'avatar': null,
'category': 'Membres',
},
{
'id': '5',
'firstName': 'Thomas',
'lastName': 'Roux',
'email': 'thomas.roux@email.com',
'phone': '+33 6 77 88 99 00',
'role': 'Membre',
'status': 'Inactif',
'joinDate': '2021-09-08',
'lastActivity': '2024-07-20',
'cotisationStatus': 'En retard',
'avatar': null,
'category': 'Membres',
},
{
'id': '6',
'firstName': 'Emma',
'lastName': 'Moreau',
'email': 'emma.moreau@email.com',
'phone': '+33 6 66 77 88 99',
'role': 'Responsable événements',
'status': 'Actif',
'joinDate': '2023-01-25',
'lastActivity': '2024-08-16',
'cotisationStatus': 'À jour',
'avatar': null,
'category': 'Responsables',
},
];
@override
void initState() {
super.initState();
_tabController = TabController(length: 4, vsync: this);
}
@override
void dispose() {
_tabController.dispose();
_searchController.dispose();
_scrollController.dispose();
super.dispose();
}
List<Map<String, dynamic>> get _filteredMembers {
return _members.where((member) {
final matchesSearch = _searchQuery.isEmpty ||
member['firstName'].toLowerCase().contains(_searchQuery.toLowerCase()) ||
member['lastName'].toLowerCase().contains(_searchQuery.toLowerCase()) ||
member['email'].toLowerCase().contains(_searchQuery.toLowerCase()) ||
member['role'].toLowerCase().contains(_searchQuery.toLowerCase());
final matchesFilter = _selectedFilter == 'Tous' ||
(_selectedFilter == 'Actifs' && member['status'] == 'Actif') ||
(_selectedFilter == 'Inactifs' && member['status'] == 'Inactif') ||
(_selectedFilter == 'Bureau' && member['category'] == 'Bureau') ||
(_selectedFilter == 'En retard' && member['cotisationStatus'] == 'En retard');
return matchesSearch && matchesFilter;
}).toList();
}
@override
Widget build(BuildContext context) {
ResponsiveUtils.init(context);
return Scaffold(
backgroundColor: AppTheme.backgroundLight,
body: NestedScrollView(
controller: _scrollController,
headerSliverBuilder: (context, innerBoxIsScrolled) {
return [
_buildAppBar(innerBoxIsScrolled),
_buildTabBar(),
];
},
body: TabBarView(
controller: _tabController,
children: [
_buildMembersList(),
_buildMembersList(filter: 'Bureau'),
_buildMembersList(filter: 'Responsables'),
_buildMembersList(filter: 'Membres'),
],
),
),
);
}
Widget _buildAppBar(bool innerBoxIsScrolled) {
return SliverAppBar(
expandedHeight: _isSearchActive ? 250 : 180,
floating: false,
pinned: true,
backgroundColor: AppTheme.secondaryColor,
flexibleSpace: FlexibleSpaceBar(
title: AnimatedOpacity(
opacity: innerBoxIsScrolled ? 1.0 : 0.0,
duration: const Duration(milliseconds: 200),
child: const Text(
'Membres',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
background: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [AppTheme.secondaryColor, AppTheme.secondaryColor.withOpacity(0.8)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: SafeArea(
child: Column(
children: [
// Titre principal quand l'AppBar est étendu
if (!innerBoxIsScrolled)
Padding(
padding: const EdgeInsets.only(top: 60),
child: Text(
'Membres',
style: const TextStyle(
color: Colors.white,
fontSize: 28,
fontWeight: FontWeight.bold,
),
),
),
// Contenu principal
Expanded(
child: Padding(
padding: ResponsiveUtils.paddingOnly(
left: 4,
top: 2,
right: 4,
bottom: 2,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
if (_isSearchActive) ...[
Flexible(
child: MembersSearchBar(
controller: _searchController,
onChanged: (value) {
setState(() {
_searchQuery = value;
});
},
onClear: () {
setState(() {
_searchQuery = '';
_searchController.clear();
});
},
),
),
SizedBox(height: 2.hp),
],
Flexible(
child: _buildStatsRow(),
),
],
),
),
),
],
),
),
),
),
actions: [
IconButton(
icon: Icon(_isSearchActive ? Icons.search_off : Icons.search),
onPressed: () {
setState(() {
_isSearchActive = !_isSearchActive;
if (!_isSearchActive) {
_searchController.clear();
_searchQuery = '';
}
});
},
),
IconButton(
icon: const Icon(Icons.filter_list),
onPressed: _showFilterSheet,
),
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert),
onSelected: _handleMenuSelection,
itemBuilder: (context) => [
const PopupMenuItem(
value: 'export',
child: Row(
children: [
Icon(Icons.download),
SizedBox(width: 8),
Text('Exporter'),
],
),
),
const PopupMenuItem(
value: 'import',
child: Row(
children: [
Icon(Icons.upload),
SizedBox(width: 8),
Text('Importer'),
],
),
),
const PopupMenuItem(
value: 'stats',
child: Row(
children: [
Icon(Icons.analytics),
SizedBox(width: 8),
Text('Statistiques'),
],
),
),
],
),
],
);
}
Widget _buildTabBar() {
return SliverPersistentHeader(
delegate: _TabBarDelegate(
TabBar(
controller: _tabController,
labelColor: AppTheme.secondaryColor,
unselectedLabelColor: AppTheme.textSecondary,
indicatorColor: AppTheme.secondaryColor,
indicatorWeight: 3,
labelStyle: const TextStyle(fontWeight: FontWeight.w600),
tabs: const [
Tab(text: 'Tous'),
Tab(text: 'Bureau'),
Tab(text: 'Responsables'),
Tab(text: 'Membres'),
],
),
),
pinned: true,
);
}
Widget _buildStatsRow() {
final activeCount = _members.where((m) => m['status'] == 'Actif').length;
final latePayments = _members.where((m) => m['cotisationStatus'] == 'En retard').length;
return Row(
children: [
_buildStatCard(
title: 'Total',
value: '${_members.length}',
icon: Icons.people,
color: Colors.white,
),
const SizedBox(width: 8),
_buildStatCard(
title: 'Actifs',
value: '$activeCount',
icon: Icons.check_circle,
color: AppTheme.successColor,
),
const SizedBox(width: 8),
_buildStatCard(
title: 'En retard',
value: '$latePayments',
icon: Icons.warning,
color: AppTheme.warningColor,
),
],
);
}
Widget _buildStatCard({
required String title,
required String value,
required IconData icon,
required Color color,
}) {
return Expanded(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Colors.white.withOpacity(0.3),
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
color: color,
size: ResponsiveUtils.iconSize(4),
),
SizedBox(width: 1.5.wp),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
child: Text(
value,
style: TextStyle(
color: Colors.white,
fontSize: ResponsiveUtils.adaptive(
small: 3.5.fs,
medium: 3.2.fs,
large: 3.fs,
),
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
Flexible(
child: Text(
title,
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: ResponsiveUtils.adaptive(
small: 2.8.fs,
medium: 2.6.fs,
large: 2.4.fs,
),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
],
),
),
);
}
Widget _buildMembersList({String? filter}) {
List<Map<String, dynamic>> members = _filteredMembers;
if (filter != null) {
members = members.where((member) => member['category'] == filter).toList();
}
if (members.isEmpty) {
return _buildEmptyState();
}
return RefreshIndicator(
onRefresh: _refreshMembers,
color: AppTheme.secondaryColor,
child: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: members.length,
itemBuilder: (context, index) {
final member = members[index];
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: SophisticatedMemberCard(
member: member,
onTap: () => _showMemberDetails(member),
onEdit: () => _editMember(member),
compact: false,
),
);
},
),
);
}
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.people_outline,
size: 80,
color: AppTheme.textHint,
),
const SizedBox(height: 16),
const Text(
'Aucun membre trouvé',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppTheme.textSecondary,
),
),
const SizedBox(height: 8),
const Text(
'Modifiez vos critères de recherche ou ajoutez de nouveaux membres',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
color: AppTheme.textHint,
),
),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: _addMember,
icon: const Icon(Icons.person_add),
label: const Text('Ajouter un membre'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.secondaryColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
),
],
),
);
}
void _showFilterSheet() {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => MembersFilterSheet(
selectedFilter: _selectedFilter,
onFilterChanged: (filter) {
setState(() {
_selectedFilter = filter;
});
},
),
);
}
void _handleMenuSelection(String value) {
switch (value) {
case 'export':
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Export des membres - En développement'),
backgroundColor: AppTheme.secondaryColor,
),
);
break;
case 'import':
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Import des membres - En développement'),
backgroundColor: AppTheme.secondaryColor,
),
);
break;
case 'stats':
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Statistiques détaillées - En développement'),
backgroundColor: AppTheme.secondaryColor,
),
);
break;
}
}
Future<void> _refreshMembers() async {
await Future.delayed(const Duration(seconds: 1));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Liste des membres actualisée'),
backgroundColor: AppTheme.successColor,
behavior: SnackBarBehavior.floating,
),
);
}
void _showMemberDetails(Map<String, dynamic> member) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Détails de ${member['firstName']} ${member['lastName']} - En développement'),
backgroundColor: AppTheme.secondaryColor,
behavior: SnackBarBehavior.floating,
),
);
}
void _editMember(Map<String, dynamic> member) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Édition de ${member['firstName']} ${member['lastName']} - En développement'),
backgroundColor: AppTheme.accentColor,
behavior: SnackBarBehavior.floating,
),
);
}
void _addMember() {
HapticFeedback.lightImpact();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Ajouter un membre - En développement'),
backgroundColor: AppTheme.secondaryColor,
behavior: SnackBarBehavior.floating,
),
);
}
}
class _TabBarDelegate extends SliverPersistentHeaderDelegate {
final TabBar tabBar;
_TabBarDelegate(this.tabBar);
@override
double get minExtent => tabBar.preferredSize.height;
@override
double get maxExtent => tabBar.preferredSize.height;
@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
return Container(
color: Colors.white,
child: tabBar,
);
}
@override
bool shouldRebuild(_TabBarDelegate oldDelegate) {
return false;
}
}

View File

@@ -0,0 +1,427 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../shared/theme/app_theme.dart';
class MemberCard extends StatefulWidget {
final Map<String, dynamic> member;
final VoidCallback? onTap;
final VoidCallback? onEdit;
const MemberCard({
super.key,
required this.member,
this.onTap,
this.onEdit,
});
@override
State<MemberCard> createState() => _MemberCardState();
}
class _MemberCardState extends State<MemberCard>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 150),
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 1.0,
end: 0.98,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _scaleAnimation,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: widget.onTap != null ? _handleTap : null,
onTapDown: widget.onTap != null ? (_) => _animationController.forward() : null,
onTapUp: widget.onTap != null ? (_) => _animationController.reverse() : null,
onTapCancel: widget.onTap != null ? () => _animationController.reverse() : null,
borderRadius: BorderRadius.circular(16),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 15,
offset: const Offset(0, 4),
),
],
border: Border.all(
color: _getStatusColor().withOpacity(0.2),
width: 1,
),
),
child: Column(
children: [
Row(
children: [
_buildAvatar(),
const SizedBox(width: 16),
Expanded(
child: _buildMemberInfo(),
),
_buildStatusBadge(),
],
),
const SizedBox(height: 16),
_buildMemberDetails(),
const SizedBox(height: 12),
_buildActionButtons(),
],
),
),
),
),
);
},
);
}
Widget _buildAvatar() {
return Container(
width: 60,
height: 60,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
_getStatusColor(),
_getStatusColor().withOpacity(0.7),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(30),
boxShadow: [
BoxShadow(
color: _getStatusColor().withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: widget.member['avatar'] != null
? ClipRRect(
borderRadius: BorderRadius.circular(30),
child: Image.network(
widget.member['avatar'],
fit: BoxFit.cover,
),
)
: Center(
child: Text(
_getInitials(),
style: const TextStyle(
color: Colors.white,
fontSize: 22,
fontWeight: FontWeight.bold,
),
),
),
);
}
Widget _buildMemberInfo() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${widget.member['firstName']} ${widget.member['lastName']}',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 4),
Text(
widget.member['role'],
style: TextStyle(
fontSize: 14,
color: _getStatusColor(),
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 2),
Text(
widget.member['email'],
style: const TextStyle(
fontSize: 13,
color: AppTheme.textSecondary,
),
overflow: TextOverflow.ellipsis,
),
],
);
}
Widget _buildStatusBadge() {
final isActive = widget.member['status'] == 'Actif';
final color = isActive ? AppTheme.successColor : AppTheme.textHint;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: color.withOpacity(0.3),
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(4),
),
),
const SizedBox(width: 6),
Text(
widget.member['status'],
style: TextStyle(
color: color,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
],
),
);
}
Widget _buildMemberDetails() {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppTheme.backgroundLight,
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
_buildDetailRow(
icon: Icons.phone,
label: 'Téléphone',
value: widget.member['phone'],
color: AppTheme.infoColor,
),
const SizedBox(height: 8),
_buildDetailRow(
icon: Icons.calendar_today,
label: 'Adhésion',
value: _formatDate(widget.member['joinDate']),
color: AppTheme.primaryColor,
),
const SizedBox(height: 8),
_buildDetailRow(
icon: Icons.payment,
label: 'Cotisation',
value: widget.member['cotisationStatus'],
color: _getCotisationColor(),
),
],
),
);
}
Widget _buildDetailRow({
required IconData icon,
required String label,
required String value,
required Color color,
}) {
return Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
),
child: Icon(
icon,
size: 16,
color: color,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
Text(
value,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: label == 'Cotisation' ? color : AppTheme.textPrimary,
),
),
],
),
),
],
);
}
Widget _buildActionButtons() {
return Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: _callMember,
icon: const Icon(Icons.phone, size: 16),
label: const Text('Appeler'),
style: OutlinedButton.styleFrom(
foregroundColor: AppTheme.infoColor,
side: BorderSide(color: AppTheme.infoColor.withOpacity(0.5)),
padding: const EdgeInsets.symmetric(vertical: 8),
),
),
),
const SizedBox(width: 12),
Expanded(
child: OutlinedButton.icon(
onPressed: _emailMember,
icon: const Icon(Icons.email, size: 16),
label: const Text('Email'),
style: OutlinedButton.styleFrom(
foregroundColor: AppTheme.primaryColor,
side: BorderSide(color: AppTheme.primaryColor.withOpacity(0.5)),
padding: const EdgeInsets.symmetric(vertical: 8),
),
),
),
const SizedBox(width: 12),
Container(
decoration: BoxDecoration(
color: AppTheme.secondaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: IconButton(
onPressed: widget.onEdit,
icon: const Icon(Icons.edit, size: 18),
color: AppTheme.secondaryColor,
padding: const EdgeInsets.all(8),
constraints: const BoxConstraints(
minWidth: 40,
minHeight: 40,
),
),
),
],
);
}
String _getInitials() {
final firstName = widget.member['firstName'] as String;
final lastName = widget.member['lastName'] as String;
return '${firstName.isNotEmpty ? firstName[0] : ''}${lastName.isNotEmpty ? lastName[0] : ''}'.toUpperCase();
}
Color _getStatusColor() {
switch (widget.member['role']) {
case 'Président':
return AppTheme.primaryColor;
case 'Secrétaire':
return AppTheme.secondaryColor;
case 'Trésorier':
return AppTheme.accentColor;
case 'Responsable événements':
return AppTheme.warningColor;
default:
return AppTheme.infoColor;
}
}
Color _getCotisationColor() {
switch (widget.member['cotisationStatus']) {
case 'À jour':
return AppTheme.successColor;
case 'En retard':
return AppTheme.errorColor;
case 'Exempt':
return AppTheme.infoColor;
default:
return AppTheme.textSecondary;
}
}
String _formatDate(String dateString) {
try {
final date = DateTime.parse(dateString);
final months = [
'Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Jun',
'Jul', 'Aoû', 'Sep', 'Oct', 'Nov', 'Déc'
];
return '${date.day} ${months[date.month - 1]} ${date.year}';
} catch (e) {
return dateString;
}
}
void _handleTap() {
HapticFeedback.selectionClick();
widget.onTap?.call();
}
void _callMember() {
HapticFeedback.lightImpact();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Appel vers ${widget.member['phone']} - En développement'),
backgroundColor: AppTheme.infoColor,
behavior: SnackBarBehavior.floating,
),
);
}
void _emailMember() {
HapticFeedback.lightImpact();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Email vers ${widget.member['email']} - En développement'),
backgroundColor: AppTheme.primaryColor,
behavior: SnackBarBehavior.floating,
),
);
}
}

View File

@@ -0,0 +1,377 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../shared/theme/app_theme.dart';
class MembersFilterSheet extends StatefulWidget {
final String selectedFilter;
final Function(String) onFilterChanged;
const MembersFilterSheet({
super.key,
required this.selectedFilter,
required this.onFilterChanged,
});
@override
State<MembersFilterSheet> createState() => _MembersFilterSheetState();
}
class _MembersFilterSheetState extends State<MembersFilterSheet>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _slideAnimation;
late Animation<double> _fadeAnimation;
String _tempSelectedFilter = '';
final List<Map<String, dynamic>> _filterOptions = [
{
'value': 'Tous',
'label': 'Tous les membres',
'icon': Icons.people,
'color': AppTheme.primaryColor,
'description': 'Afficher tous les membres',
},
{
'value': 'Actifs',
'label': 'Membres actifs',
'icon': Icons.check_circle,
'color': AppTheme.successColor,
'description': 'Membres avec un statut actif',
},
{
'value': 'Inactifs',
'label': 'Membres inactifs',
'icon': Icons.pause_circle,
'color': AppTheme.textHint,
'description': 'Membres avec un statut inactif',
},
{
'value': 'Bureau',
'label': 'Membres du bureau',
'icon': Icons.star,
'color': AppTheme.warningColor,
'description': 'Président, secrétaire, trésorier',
},
{
'value': 'En retard',
'label': 'Cotisations en retard',
'icon': Icons.warning,
'color': AppTheme.errorColor,
'description': 'Membres avec cotisations impayées',
},
];
@override
void initState() {
super.initState();
_tempSelectedFilter = widget.selectedFilter;
_animationController = AnimationController(
duration: const Duration(milliseconds: 400),
vsync: this,
);
_slideAnimation = Tween<double>(
begin: 1.0,
end: 0.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeOutCubic,
));
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: const Interval(0.2, 1.0, curve: Curves.easeOut),
));
_animationController.forward();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Container(
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.5 * _fadeAnimation.value),
),
child: Align(
alignment: Alignment.bottomCenter,
child: Transform.translate(
offset: Offset(0, MediaQuery.of(context).size.height * _slideAnimation.value),
child: Container(
width: double.infinity,
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.7,
),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(24),
topRight: Radius.circular(24),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildHandle(),
_buildHeader(),
Flexible(child: _buildFilterOptions()),
_buildActionButtons(),
],
),
),
),
),
);
},
);
}
Widget _buildHandle() {
return Container(
margin: const EdgeInsets.only(top: 12, bottom: 8),
width: 40,
height: 4,
decoration: BoxDecoration(
color: AppTheme.textHint.withOpacity(0.3),
borderRadius: BorderRadius.circular(2),
),
);
}
Widget _buildHeader() {
return Padding(
padding: const EdgeInsets.fromLTRB(24, 16, 24, 8),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppTheme.secondaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
Icons.filter_list,
color: AppTheme.secondaryColor,
size: 20,
),
),
const SizedBox(width: 12),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Filtrer les membres',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
Text(
'Sélectionnez un critère de filtrage',
style: TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
),
),
],
),
),
IconButton(
onPressed: _closeSheet,
icon: Icon(
Icons.close,
color: AppTheme.textHint,
),
),
],
),
);
}
Widget _buildFilterOptions() {
return ListView.separated(
shrinkWrap: true,
padding: const EdgeInsets.fromLTRB(24, 16, 24, 0),
itemCount: _filterOptions.length,
separatorBuilder: (context, index) => const SizedBox(height: 12),
itemBuilder: (context, index) {
final option = _filterOptions[index];
final isSelected = _tempSelectedFilter == option['value'];
return _buildFilterOption(
option: option,
isSelected: isSelected,
onTap: () => _selectFilter(option['value']),
);
},
);
}
Widget _buildFilterOption({
required Map<String, dynamic> option,
required bool isSelected,
required VoidCallback onTap,
}) {
return Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
HapticFeedback.selectionClick();
onTap();
},
borderRadius: BorderRadius.circular(16),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isSelected
? option['color'].withOpacity(0.1)
: Colors.transparent,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isSelected
? option['color']
: AppTheme.textHint.withOpacity(0.2),
width: isSelected ? 2 : 1,
),
),
child: Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: option['color'].withOpacity(isSelected ? 0.2 : 0.1),
borderRadius: BorderRadius.circular(24),
),
child: Icon(
option['icon'],
color: option['color'],
size: 24,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
option['label'],
style: TextStyle(
fontSize: 16,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
color: isSelected ? option['color'] : AppTheme.textPrimary,
),
),
const SizedBox(height: 2),
Text(
option['description'],
style: const TextStyle(
fontSize: 13,
color: AppTheme.textSecondary,
),
),
],
),
),
AnimatedOpacity(
opacity: isSelected ? 1.0 : 0.0,
duration: const Duration(milliseconds: 200),
child: Icon(
Icons.check_circle,
color: option['color'],
size: 24,
),
),
],
),
),
),
);
}
Widget _buildActionButtons() {
return Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: AppTheme.backgroundLight,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
child: Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: _resetFilter,
style: OutlinedButton.styleFrom(
foregroundColor: AppTheme.textSecondary,
side: BorderSide(color: AppTheme.textHint.withOpacity(0.5)),
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: const Text('Réinitialiser'),
),
),
const SizedBox(width: 16),
Expanded(
flex: 2,
child: ElevatedButton(
onPressed: _applyFilter,
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.secondaryColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
elevation: 0,
),
child: const Text(
'Appliquer',
style: TextStyle(fontWeight: FontWeight.w600),
),
),
),
],
),
);
}
void _selectFilter(String filter) {
setState(() {
_tempSelectedFilter = filter;
});
}
void _resetFilter() {
setState(() {
_tempSelectedFilter = 'Tous';
});
}
void _applyFilter() {
widget.onFilterChanged(_tempSelectedFilter);
_closeSheet();
}
void _closeSheet() {
_animationController.reverse().then((_) {
if (mounted) {
Navigator.of(context).pop();
}
});
}
}

View File

@@ -0,0 +1,133 @@
import 'package:flutter/material.dart';
import '../../../../shared/theme/app_theme.dart';
class MembersSearchBar extends StatefulWidget {
final TextEditingController controller;
final Function(String) onChanged;
final VoidCallback onClear;
const MembersSearchBar({
super.key,
required this.controller,
required this.onChanged,
required this.onClear,
});
@override
State<MembersSearchBar> createState() => _MembersSearchBarState();
}
class _MembersSearchBarState extends State<MembersSearchBar>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
bool _hasText = false;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
widget.controller.addListener(_onTextChanged);
_animationController.forward();
}
@override
void dispose() {
widget.controller.removeListener(_onTextChanged);
_animationController.dispose();
super.dispose();
}
void _onTextChanged() {
final hasText = widget.controller.text.isNotEmpty;
if (hasText != _hasText) {
setState(() {
_hasText = hasText;
});
}
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _fadeAnimation,
builder: (context, child) {
return Opacity(
opacity: _fadeAnimation.value,
child: Transform.translate(
offset: Offset(0, 20 * (1 - _fadeAnimation.value)),
child: Container(
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.9),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: TextField(
controller: widget.controller,
onChanged: widget.onChanged,
style: const TextStyle(
fontSize: 16,
color: AppTheme.textPrimary,
),
decoration: InputDecoration(
hintText: 'Rechercher un membre...',
hintStyle: TextStyle(
color: AppTheme.textHint,
fontSize: 16,
),
prefixIcon: Icon(
Icons.search,
color: AppTheme.secondaryColor,
size: 24,
),
suffixIcon: _hasText
? AnimatedOpacity(
opacity: _hasText ? 1.0 : 0.0,
duration: const Duration(milliseconds: 200),
child: IconButton(
icon: Icon(
Icons.clear,
color: AppTheme.textHint,
size: 20,
),
onPressed: widget.onClear,
),
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
),
filled: true,
fillColor: Colors.transparent,
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
),
),
),
),
);
},
);
}
}

View File

@@ -0,0 +1,544 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../shared/theme/app_theme.dart';
import '../../../../shared/widgets/cards/sophisticated_card.dart';
import '../../../../shared/widgets/avatars/sophisticated_avatar.dart';
import '../../../../shared/widgets/badges/status_badge.dart';
import '../../../../shared/widgets/badges/count_badge.dart';
import '../../../../shared/widgets/buttons/buttons.dart';
class SophisticatedMemberCard extends StatefulWidget {
final Map<String, dynamic> member;
final VoidCallback? onTap;
final VoidCallback? onEdit;
final VoidCallback? onMessage;
final VoidCallback? onCall;
final bool showActions;
final bool compact;
const SophisticatedMemberCard({
super.key,
required this.member,
this.onTap,
this.onEdit,
this.onMessage,
this.onCall,
this.showActions = true,
this.compact = false,
});
@override
State<SophisticatedMemberCard> createState() => _SophisticatedMemberCardState();
}
class _SophisticatedMemberCardState extends State<SophisticatedMemberCard>
with TickerProviderStateMixin {
late AnimationController _expandController;
late AnimationController _actionController;
late Animation<double> _expandAnimation;
late Animation<double> _actionAnimation;
bool _isExpanded = false;
@override
void initState() {
super.initState();
_expandController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_actionController = AnimationController(
duration: const Duration(milliseconds: 200),
vsync: this,
);
_expandAnimation = CurvedAnimation(
parent: _expandController,
curve: Curves.easeInOut,
);
_actionAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _actionController,
curve: Curves.elasticOut,
));
_actionController.forward();
}
@override
void dispose() {
_expandController.dispose();
_actionController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SophisticatedCard(
variant: CardVariant.elevated,
size: widget.compact ? CardSize.compact : CardSize.standard,
onTap: widget.onTap,
margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 4),
child: Column(
children: [
_buildMainContent(),
AnimatedBuilder(
animation: _expandAnimation,
builder: (context, child) {
return ClipRect(
child: Align(
alignment: Alignment.topCenter,
heightFactor: _expandAnimation.value,
child: child,
),
);
},
child: _buildExpandedContent(),
),
],
),
);
}
Widget _buildMainContent() {
return Row(
children: [
_buildAvatar(),
const SizedBox(width: 16),
Expanded(child: _buildMemberInfo()),
_buildTrailingActions(),
],
);
}
Widget _buildAvatar() {
final roleColor = _getRoleColor();
final isOnline = widget.member['status'] == 'Actif';
return SophisticatedAvatar(
initials: _getInitials(),
size: widget.compact ? AvatarSize.medium : AvatarSize.large,
variant: AvatarVariant.gradient,
backgroundColor: roleColor,
showOnlineStatus: true,
isOnline: isOnline,
badge: _buildRoleBadge(),
onTap: () => _toggleExpanded(),
);
}
Widget _buildRoleBadge() {
final role = widget.member['role'] as String;
if (role == 'Président' || role == 'Secrétaire' || role == 'Trésorier') {
return CountBadge(
count: 1,
backgroundColor: AppTheme.warningColor,
size: 16,
suffix: '',
);
}
return const SizedBox.shrink();
}
Widget _buildMemberInfo() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
'${widget.member['firstName']} ${widget.member['lastName']}',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
_buildStatusBadge(),
],
),
const SizedBox(height: 4),
_buildRoleChip(),
if (!widget.compact) ...[
const SizedBox(height: 8),
_buildQuickInfo(),
],
],
);
}
Widget _buildStatusBadge() {
final status = widget.member['status'] as String;
final cotisationStatus = widget.member['cotisationStatus'] as String;
if (cotisationStatus == 'En retard') {
return StatusBadge(
text: 'Retard',
type: BadgeType.error,
size: BadgeSize.small,
variant: BadgeVariant.ghost,
icon: Icons.warning,
);
}
return StatusBadge(
text: status,
type: status == 'Actif' ? BadgeType.success : BadgeType.neutral,
size: BadgeSize.small,
variant: BadgeVariant.ghost,
);
}
Widget _buildRoleChip() {
final role = widget.member['role'] as String;
final roleColor = _getRoleColor();
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: roleColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: roleColor.withOpacity(0.3),
width: 1,
),
),
child: Text(
role,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: roleColor,
),
),
);
}
Widget _buildQuickInfo() {
return Row(
children: [
Expanded(
child: _buildInfoItem(
Icons.email_outlined,
widget.member['email'],
AppTheme.infoColor,
),
),
const SizedBox(width: 16),
_buildInfoItem(
Icons.phone_outlined,
_formatPhone(widget.member['phone']),
AppTheme.successColor,
),
],
);
}
Widget _buildInfoItem(IconData icon, String text, Color color) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 14, color: color),
const SizedBox(width: 4),
Flexible(
child: Text(
text,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
overflow: TextOverflow.ellipsis,
),
),
],
);
}
Widget _buildTrailingActions() {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedBuilder(
animation: _actionAnimation,
builder: (context, child) {
return Transform.scale(
scale: _actionAnimation.value,
child: IconButton(
onPressed: _toggleExpanded,
icon: AnimatedRotation(
turns: _isExpanded ? 0.5 : 0.0,
duration: const Duration(milliseconds: 300),
child: const Icon(Icons.expand_more),
),
iconSize: 20,
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
style: IconButton.styleFrom(
backgroundColor: AppTheme.backgroundLight,
foregroundColor: AppTheme.textSecondary,
),
),
);
},
),
if (widget.compact) ...[
const SizedBox(height: 4),
_buildQuickActionButton(),
],
],
);
}
Widget _buildQuickActionButton() {
return QuickButtons.iconGhost(
icon: Icons.edit,
onPressed: widget.onEdit ?? _editMember,
size: 32,
color: _getRoleColor(),
tooltip: 'Modifier',
);
}
Widget _buildExpandedContent() {
return Container(
margin: const EdgeInsets.only(top: 16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppTheme.backgroundLight,
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
_buildDetailedInfo(),
if (widget.showActions) ...[
const SizedBox(height: 16),
_buildActionButtons(),
],
],
),
);
}
Widget _buildDetailedInfo() {
return Column(
children: [
_buildDetailRow(
'Adhésion',
_formatDate(widget.member['joinDate']),
Icons.calendar_today,
AppTheme.primaryColor,
),
const SizedBox(height: 12),
_buildDetailRow(
'Dernière activité',
_formatDate(widget.member['lastActivity']),
Icons.access_time,
AppTheme.infoColor,
),
const SizedBox(height: 12),
_buildDetailRow(
'Cotisation',
widget.member['cotisationStatus'],
Icons.payment,
_getCotisationColor(),
),
],
);
}
Widget _buildDetailRow(String label, String value, IconData icon, Color color) {
return Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
),
child: Icon(icon, size: 16, color: color),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
Text(
value,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: label == 'Cotisation' ? color : AppTheme.textPrimary,
),
),
],
),
),
],
);
}
Widget _buildActionButtons() {
return Row(
children: [
Expanded(
child: QuickButtons.outline(
text: 'Appeler',
icon: Icons.phone,
onPressed: widget.onCall ?? _callMember,
size: ButtonSize.small,
color: AppTheme.successColor,
),
),
const SizedBox(width: 8),
Expanded(
child: QuickButtons.outline(
text: 'Message',
icon: Icons.message,
onPressed: widget.onMessage ?? _messageMember,
size: ButtonSize.small,
color: AppTheme.infoColor,
),
),
const SizedBox(width: 8),
Expanded(
child: QuickButtons.outline(
text: 'Modifier',
icon: Icons.edit,
onPressed: widget.onEdit ?? _editMember,
size: ButtonSize.small,
color: AppTheme.warningColor,
),
),
],
);
}
void _toggleExpanded() {
setState(() {
_isExpanded = !_isExpanded;
if (_isExpanded) {
_expandController.forward();
} else {
_expandController.reverse();
}
});
HapticFeedback.selectionClick();
}
String _getInitials() {
final firstName = widget.member['firstName'] as String;
final lastName = widget.member['lastName'] as String;
return '${firstName.isNotEmpty ? firstName[0] : ''}${lastName.isNotEmpty ? lastName[0] : ''}'.toUpperCase();
}
Color _getRoleColor() {
switch (widget.member['role']) {
case 'Président':
return AppTheme.primaryColor;
case 'Secrétaire':
return AppTheme.secondaryColor;
case 'Trésorier':
return AppTheme.accentColor;
case 'Responsable événements':
return AppTheme.warningColor;
default:
return AppTheme.infoColor;
}
}
Color _getCotisationColor() {
switch (widget.member['cotisationStatus']) {
case 'À jour':
return AppTheme.successColor;
case 'En retard':
return AppTheme.errorColor;
case 'Exempt':
return AppTheme.infoColor;
default:
return AppTheme.textSecondary;
}
}
String _formatDate(String dateString) {
try {
final date = DateTime.parse(dateString);
final now = DateTime.now();
final difference = now.difference(date);
if (difference.inDays < 1) {
return 'Aujourd\'hui';
} else if (difference.inDays < 7) {
return 'Il y a ${difference.inDays} jour${difference.inDays > 1 ? 's' : ''}';
} else if (difference.inDays < 30) {
final weeks = (difference.inDays / 7).floor();
return 'Il y a $weeks semaine${weeks > 1 ? 's' : ''}';
} else {
final months = [
'Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Jun',
'Jul', 'Aoû', 'Sep', 'Oct', 'Nov', 'Déc'
];
return '${date.day} ${months[date.month - 1]} ${date.year}';
}
} catch (e) {
return dateString;
}
}
String _formatPhone(String phone) {
if (phone.length >= 10) {
return '${phone.substring(0, 3)} ${phone.substring(3, 5)} ${phone.substring(5, 7)} ${phone.substring(7, 9)} ${phone.substring(9)}';
}
return phone;
}
void _callMember() {
HapticFeedback.lightImpact();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Appel vers ${widget.member['firstName']} ${widget.member['lastName']}'),
backgroundColor: AppTheme.successColor,
behavior: SnackBarBehavior.floating,
),
);
}
void _messageMember() {
HapticFeedback.lightImpact();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Message vers ${widget.member['firstName']} ${widget.member['lastName']}'),
backgroundColor: AppTheme.infoColor,
behavior: SnackBarBehavior.floating,
),
);
}
void _editMember() {
HapticFeedback.lightImpact();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Modification de ${widget.member['firstName']} ${widget.member['lastName']}'),
backgroundColor: AppTheme.warningColor,
behavior: SnackBarBehavior.floating,
),
);
}
}

View File

@@ -0,0 +1,406 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../shared/theme/app_theme.dart';
import '../../../../shared/widgets/coming_soon_page.dart';
import '../../../../shared/widgets/buttons/buttons.dart';
import '../../../dashboard/presentation/pages/enhanced_dashboard.dart';
import '../../../members/presentation/pages/members_list_page.dart';
import '../widgets/custom_bottom_nav_bar.dart';
class MainNavigation extends StatefulWidget {
const MainNavigation({super.key});
@override
State<MainNavigation> createState() => _MainNavigationState();
}
class _MainNavigationState extends State<MainNavigation>
with TickerProviderStateMixin {
int _currentIndex = 0;
late PageController _pageController;
late AnimationController _fabAnimationController;
late Animation<double> _fabAnimation;
@override
void initState() {
super.initState();
_pageController = PageController();
_fabAnimationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_fabAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _fabAnimationController,
curve: Curves.easeInOut,
));
_fabAnimationController.forward();
}
@override
void dispose() {
_pageController.dispose();
_fabAnimationController.dispose();
super.dispose();
}
final List<NavigationTab> _tabs = [
NavigationTab(
title: 'Tableau de bord',
icon: Icons.dashboard_outlined,
activeIcon: Icons.dashboard,
color: AppTheme.primaryColor,
),
NavigationTab(
title: 'Membres',
icon: Icons.people_outline,
activeIcon: Icons.people,
color: AppTheme.secondaryColor,
),
NavigationTab(
title: 'Cotisations',
icon: Icons.payment_outlined,
activeIcon: Icons.payment,
color: AppTheme.accentColor,
),
NavigationTab(
title: 'Événements',
icon: Icons.event_outlined,
activeIcon: Icons.event,
color: AppTheme.warningColor,
),
NavigationTab(
title: 'Plus',
icon: Icons.more_horiz_outlined,
activeIcon: Icons.menu,
color: AppTheme.infoColor,
),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: PageView(
controller: _pageController,
onPageChanged: _onPageChanged,
children: [
EnhancedDashboard(
onNavigateToTab: _onTabTapped,
),
_buildMembresPage(),
_buildCotisationsPage(),
_buildEventsPage(),
_buildMorePage(),
],
),
bottomNavigationBar: CustomBottomNavBar(
currentIndex: _currentIndex,
tabs: _tabs,
onTap: _onTabTapped,
),
floatingActionButton: _buildFloatingActionButton(),
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
);
}
Widget _buildFloatingActionButton() {
// Afficher le FAB seulement sur certains onglets
if (_currentIndex == 1 || _currentIndex == 2 || _currentIndex == 3) {
return ScaleTransition(
scale: _fabAnimation,
child: QuickButtons.fab(
onPressed: _onFabPressed,
icon: _getFabIcon(),
variant: FABVariant.gradient,
size: FABSize.regular,
tooltip: _getFabTooltip(),
),
);
}
return const SizedBox.shrink();
}
IconData _getFabIcon() {
switch (_currentIndex) {
case 1: // Membres
return Icons.person_add;
case 2: // Cotisations
return Icons.add_card;
case 3: // Événements
return Icons.add_circle_outline;
default:
return Icons.add;
}
}
String _getFabTooltip() {
switch (_currentIndex) {
case 1: // Membres
return 'Ajouter un membre';
case 2: // Cotisations
return 'Nouvelle cotisation';
case 3: // Événements
return 'Créer un événement';
default:
return 'Ajouter';
}
}
void _onPageChanged(int index) {
setState(() {
_currentIndex = index;
});
// Animation du FAB
if (index == 1 || index == 2 || index == 3) {
_fabAnimationController.forward();
} else {
_fabAnimationController.reverse();
}
// Vibration légère
HapticFeedback.selectionClick();
}
void _onTabTapped(int index) {
if (_currentIndex != index) {
setState(() {
_currentIndex = index;
});
_pageController.animateToPage(
index,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
}
void _onFabPressed() {
HapticFeedback.lightImpact();
String action;
switch (_currentIndex) {
case 1:
action = 'Ajouter un membre';
break;
case 2:
action = 'Nouvelle cotisation';
break;
case 3:
action = 'Créer un événement';
break;
default:
action = 'Action';
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('$action - En cours de développement'),
backgroundColor: _tabs[_currentIndex].color,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
action: SnackBarAction(
label: 'OK',
textColor: Colors.white,
onPressed: () {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
},
),
),
);
}
Widget _buildMembresPage() {
return MembersListPage();
}
Widget _buildCotisationsPage() {
return ComingSoonPage(
title: 'Module Cotisations',
description: 'Suivi et gestion des cotisations avec paiements automatiques',
icon: Icons.payment_rounded,
color: AppTheme.accentColor,
features: [
'Tableau de bord des cotisations',
'Relances automatiques par email/SMS',
'Paiements en ligne sécurisés',
'Génération de reçus automatique',
'Suivi des retards de paiement',
'Rapports financiers détaillés',
],
);
}
Widget _buildEventsPage() {
return ComingSoonPage(
title: 'Module Événements',
description: 'Organisation et gestion d\'événements avec calendrier intégré',
icon: Icons.event_rounded,
color: AppTheme.warningColor,
features: [
'Calendrier interactif des événements',
'Gestion des inscriptions en ligne',
'Envoi d\'invitations automatiques',
'Suivi de la participation',
'Gestion des lieux et ressources',
'Sondages et feedback post-événement',
],
);
}
Widget _buildMorePage() {
return Scaffold(
backgroundColor: AppTheme.backgroundLight,
appBar: AppBar(
title: const Text('Plus'),
backgroundColor: AppTheme.infoColor,
elevation: 0,
automaticallyImplyLeading: false,
actions: [
IconButton(
icon: const Icon(Icons.settings),
onPressed: () {},
),
],
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
_buildMoreSection(
'Gestion',
[
_buildMoreItem(Icons.analytics, 'Rapports', 'Génération de rapports'),
_buildMoreItem(Icons.account_balance, 'Finances', 'Tableau de bord financier'),
_buildMoreItem(Icons.message, 'Communications', 'Messages et notifications'),
_buildMoreItem(Icons.folder, 'Documents', 'Gestion documentaire'),
],
),
const SizedBox(height: 24),
_buildMoreSection(
'Paramètres',
[
_buildMoreItem(Icons.person, 'Mon profil', 'Informations personnelles'),
_buildMoreItem(Icons.notifications, 'Notifications', 'Préférences de notification'),
_buildMoreItem(Icons.security, 'Sécurité', 'Mot de passe et sécurité'),
_buildMoreItem(Icons.language, 'Langue', 'Changer la langue'),
],
),
const SizedBox(height: 24),
_buildMoreSection(
'Support',
[
_buildMoreItem(Icons.help, 'Aide', 'Centre d\'aide et FAQ'),
_buildMoreItem(Icons.contact_support, 'Contact', 'Nous contacter'),
_buildMoreItem(Icons.info, 'À propos', 'Informations sur l\'application'),
_buildMoreItem(Icons.logout, 'Déconnexion', 'Se déconnecter', isDestructive: true),
],
),
],
),
);
}
Widget _buildMoreSection(String title, List<Widget> items) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 4, bottom: 12),
child: Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: items,
),
),
],
);
}
Widget _buildMoreItem(IconData icon, String title, String subtitle, {bool isDestructive = false}) {
return ListTile(
leading: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: (isDestructive ? AppTheme.errorColor : AppTheme.primaryColor).withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
),
child: Icon(
icon,
color: isDestructive ? AppTheme.errorColor : AppTheme.primaryColor,
size: 20,
),
),
title: Text(
title,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: isDestructive ? AppTheme.errorColor : AppTheme.textPrimary,
),
),
subtitle: Text(
subtitle,
style: const TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
),
),
trailing: Icon(
Icons.arrow_forward_ios,
size: 16,
color: AppTheme.textHint,
),
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('$title - En cours de développement'),
backgroundColor: isDestructive ? AppTheme.errorColor : AppTheme.primaryColor,
behavior: SnackBarBehavior.floating,
),
);
},
);
}
}
class NavigationTab {
final String title;
final IconData icon;
final IconData activeIcon;
final Color color;
NavigationTab({
required this.title,
required this.icon,
required this.activeIcon,
required this.color,
});
}

View File

@@ -0,0 +1,211 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../shared/theme/app_theme.dart';
import '../pages/main_navigation.dart';
class CustomBottomNavBar extends StatefulWidget {
final int currentIndex;
final List<NavigationTab> tabs;
final Function(int) onTap;
const CustomBottomNavBar({
super.key,
required this.currentIndex,
required this.tabs,
required this.onTap,
});
@override
State<CustomBottomNavBar> createState() => _CustomBottomNavBarState();
}
class _CustomBottomNavBarState extends State<CustomBottomNavBar>
with TickerProviderStateMixin {
late List<AnimationController> _animationControllers;
late List<Animation<double>> _scaleAnimations;
late List<Animation<Color?>> _colorAnimations;
@override
void initState() {
super.initState();
_initializeAnimations();
}
void _initializeAnimations() {
_animationControllers = List.generate(
widget.tabs.length,
(index) => AnimationController(
duration: const Duration(milliseconds: 200),
vsync: this,
),
);
_scaleAnimations = _animationControllers
.map((controller) => Tween<double>(
begin: 1.0,
end: 1.2,
).animate(CurvedAnimation(
parent: controller,
curve: Curves.easeInOut,
)))
.toList();
_colorAnimations = _animationControllers
.map((controller) => ColorTween(
begin: AppTheme.textHint,
end: AppTheme.primaryColor,
).animate(CurvedAnimation(
parent: controller,
curve: Curves.easeInOut,
)))
.toList();
// Animation initiale pour l'onglet sélectionné
if (widget.currentIndex < _animationControllers.length) {
_animationControllers[widget.currentIndex].forward();
}
}
@override
void didUpdateWidget(CustomBottomNavBar oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.currentIndex != widget.currentIndex) {
// Reverse animation for old tab
if (oldWidget.currentIndex < _animationControllers.length) {
_animationControllers[oldWidget.currentIndex].reverse();
}
// Forward animation for new tab
if (widget.currentIndex < _animationControllers.length) {
_animationControllers[widget.currentIndex].forward();
}
}
}
@override
void dispose() {
for (var controller in _animationControllers) {
controller.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 20,
offset: const Offset(0, -5),
),
],
),
child: SafeArea(
child: Container(
height: 70,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: List.generate(
widget.tabs.length,
(index) => _buildNavItem(index),
),
),
),
),
);
}
Widget _buildNavItem(int index) {
final tab = widget.tabs[index];
final isSelected = index == widget.currentIndex;
return Expanded(
child: GestureDetector(
onTap: () => _handleTap(index),
behavior: HitTestBehavior.opaque,
child: AnimatedBuilder(
animation: _animationControllers[index],
builder: (context, child) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Icône avec animation
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: isSelected
? tab.color.withOpacity(0.15)
: Colors.transparent,
borderRadius: BorderRadius.circular(16),
),
child: Transform.scale(
scale: _scaleAnimations[index].value,
child: Icon(
isSelected ? tab.activeIcon : tab.icon,
size: 20,
color: isSelected ? tab.color : AppTheme.textHint,
),
),
),
const SizedBox(height: 2),
// Label avec animation
AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 200),
style: TextStyle(
fontSize: 11,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
color: isSelected ? tab.color : AppTheme.textHint,
),
child: Text(
tab.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
// Indicateur de sélection
AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: isSelected ? 16 : 0,
height: 2,
margin: const EdgeInsets.only(top: 2),
decoration: BoxDecoration(
color: tab.color,
borderRadius: BorderRadius.circular(1),
),
),
],
),
);
},
),
),
);
}
void _handleTap(int index) {
// Vibration tactile
HapticFeedback.selectionClick();
// Animation de pression
_animationControllers[index].forward().then((_) {
if (mounted && index != widget.currentIndex) {
_animationControllers[index].reverse();
}
});
// Callback
widget.onTap(index);
}
}

View File

@@ -0,0 +1,306 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../shared/theme/app_theme.dart';
class SplashScreen extends StatefulWidget {
const SplashScreen({super.key});
@override
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen>
with TickerProviderStateMixin {
late AnimationController _logoController;
late AnimationController _progressController;
late AnimationController _textController;
late Animation<double> _logoScaleAnimation;
late Animation<double> _logoOpacityAnimation;
late Animation<double> _progressAnimation;
late Animation<double> _textOpacityAnimation;
@override
void initState() {
super.initState();
_initializeAnimations();
_startSplashSequence();
}
void _initializeAnimations() {
// Animation du logo
_logoController = AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: this,
);
_logoScaleAnimation = Tween<double>(
begin: 0.5,
end: 1.0,
).animate(CurvedAnimation(
parent: _logoController,
curve: Curves.elasticOut,
));
_logoOpacityAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _logoController,
curve: const Interval(0.0, 0.6, curve: Curves.easeIn),
));
// Animation de la barre de progression
_progressController = AnimationController(
duration: const Duration(milliseconds: 2000),
vsync: this,
);
_progressAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _progressController,
curve: Curves.easeInOut,
));
// Animation du texte
_textController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_textOpacityAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _textController,
curve: Curves.easeIn,
));
}
void _startSplashSequence() async {
// Configuration de la barre de statut
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.light,
),
);
// Séquence d'animations
await Future.delayed(const Duration(milliseconds: 300));
_logoController.forward();
await Future.delayed(const Duration(milliseconds: 500));
_textController.forward();
await Future.delayed(const Duration(milliseconds: 300));
_progressController.forward();
// Attendre la fin de toutes les animations + temps de chargement
await Future.delayed(const Duration(milliseconds: 2000));
// Le splash screen sera remplacé automatiquement par l'AppWrapper
// basé sur l'état d'authentification
}
@override
void dispose() {
_logoController.dispose();
_progressController.dispose();
_textController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
AppTheme.primaryColor,
AppTheme.primaryDark,
const Color(0xFF0D47A1),
],
),
),
child: SafeArea(
child: Column(
children: [
Expanded(
flex: 3,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Logo animé
AnimatedBuilder(
animation: _logoController,
builder: (context, child) {
return Transform.scale(
scale: _logoScaleAnimation.value,
child: Opacity(
opacity: _logoOpacityAnimation.value,
child: Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(30),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: const Icon(
Icons.groups_rounded,
size: 60,
color: AppTheme.primaryColor,
),
),
),
);
},
),
const SizedBox(height: 32),
// Titre animé
AnimatedBuilder(
animation: _textController,
builder: (context, child) {
return Opacity(
opacity: _textOpacityAnimation.value,
child: Column(
children: [
const Text(
'UnionFlow',
style: TextStyle(
fontSize: 36,
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: 1.2,
),
),
const SizedBox(height: 8),
Text(
'Gestion d\'associations professionnelle',
style: TextStyle(
fontSize: 16,
color: Colors.white.withOpacity(0.9),
fontWeight: FontWeight.w300,
),
textAlign: TextAlign.center,
),
],
),
);
},
),
],
),
),
),
// Section de chargement
Expanded(
flex: 1,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Barre de progression animée
Container(
width: 200,
margin: const EdgeInsets.symmetric(horizontal: 40),
child: Column(
children: [
AnimatedBuilder(
animation: _progressController,
builder: (context, child) {
return LinearProgressIndicator(
value: _progressAnimation.value,
backgroundColor: Colors.white.withOpacity(0.2),
valueColor: const AlwaysStoppedAnimation<Color>(
Colors.white,
),
minHeight: 3,
);
},
),
const SizedBox(height: 16),
AnimatedBuilder(
animation: _progressController,
builder: (context, child) {
return Text(
'${(_progressAnimation.value * 100).toInt()}%',
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 14,
fontWeight: FontWeight.w500,
),
);
},
),
],
),
),
const SizedBox(height: 24),
// Texte de chargement
AnimatedBuilder(
animation: _textController,
builder: (context, child) {
return Opacity(
opacity: _textOpacityAnimation.value,
child: Text(
'Initialisation...',
style: TextStyle(
color: Colors.white.withOpacity(0.7),
fontSize: 14,
),
),
);
},
),
],
),
),
// Footer
Padding(
padding: const EdgeInsets.only(bottom: 40),
child: Column(
children: [
Text(
'Version 1.0.0',
style: TextStyle(
color: Colors.white.withOpacity(0.6),
fontSize: 12,
),
),
const SizedBox(height: 8),
Text(
'© 2024 Lions Club International',
style: TextStyle(
color: Colors.white.withOpacity(0.5),
fontSize: 10,
),
),
],
),
),
],
),
),
),
);
}
}