Initial commit: unionflow-mobile-apps

Application Flutter complète (sans build artifacts).

Signed-off-by: lions dev Team
This commit is contained in:
dahoud
2026-03-15 16:30:08 +00:00
commit d094d6db9c
1790 changed files with 507435 additions and 0 deletions

View File

@@ -0,0 +1,107 @@
import 'package:flutter/material.dart';
import '../tokens/unionflow_colors.dart';
/// Background avec motifs géométriques africains subtils
class AfricanPatternBackground extends StatelessWidget {
final Widget child;
final Color? patternColor;
final double opacity;
const AfricanPatternBackground({
super.key,
required this.child,
this.patternColor,
this.opacity = 0.03,
});
@override
Widget build(BuildContext context) {
return Stack(
children: [
// Background avec motifs
Positioned.fill(
child: IgnorePointer(
child: CustomPaint(
painter: AfricanPatternPainter(
color: (patternColor ?? UnionFlowColors.unionGreen).withOpacity(opacity),
),
),
),
),
// Contenu
child,
],
);
}
}
/// Painter pour dessiner les motifs africains
class AfricanPatternPainter extends CustomPainter {
final Color color;
AfricanPatternPainter({required this.color});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = 2;
final fillPaint = Paint()
..color = color
..style = PaintingStyle.fill;
// Espacement entre les motifs
const double spacing = 80.0;
const double patternSize = 40.0;
// Dessiner la grille de motifs
for (double y = 0; y < size.height + spacing; y += spacing) {
for (double x = 0; x < size.width + spacing; x += spacing) {
final offset = Offset(x, y);
// Alterner entre différents motifs
final patternType = ((x ~/ spacing) + (y ~/ spacing)) % 3;
switch (patternType) {
case 0:
_drawDiamondPattern(canvas, offset, patternSize, paint);
break;
case 1:
_drawTrianglePattern(canvas, offset, patternSize, fillPaint);
break;
case 2:
_drawCirclePattern(canvas, offset, patternSize, paint);
break;
}
}
}
}
void _drawDiamondPattern(Canvas canvas, Offset offset, double size, Paint paint) {
final path = Path()
..moveTo(offset.dx, offset.dy - size / 2)
..lineTo(offset.dx + size / 2, offset.dy)
..lineTo(offset.dx, offset.dy + size / 2)
..lineTo(offset.dx - size / 2, offset.dy)
..close();
canvas.drawPath(path, paint);
}
void _drawTrianglePattern(Canvas canvas, Offset offset, double size, Paint paint) {
final path = Path()
..moveTo(offset.dx, offset.dy - size / 3)
..lineTo(offset.dx + size / 3, offset.dy + size / 3)
..lineTo(offset.dx - size / 3, offset.dy + size / 3)
..close();
canvas.drawPath(path, paint);
}
void _drawCirclePattern(Canvas canvas, Offset offset, double size, Paint paint) {
canvas.drawCircle(offset, size / 4, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

View File

@@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
/// Widget avec animation de fade-in automatique
class AnimatedFadeIn extends StatefulWidget {
final Widget child;
final Duration duration;
final Duration delay;
final Curve curve;
const AnimatedFadeIn({
super.key,
required this.child,
this.duration = const Duration(milliseconds: 600),
this.delay = Duration.zero,
this.curve = Curves.easeOut,
});
@override
State<AnimatedFadeIn> createState() => _AnimatedFadeInState();
}
class _AnimatedFadeInState extends State<AnimatedFadeIn>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: widget.duration,
vsync: this,
);
_animation = CurvedAnimation(
parent: _controller,
curve: widget.curve,
);
Future.delayed(widget.delay, () {
if (mounted) {
_controller.forward();
}
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: _animation,
child: widget.child,
);
}
}

View File

@@ -0,0 +1,74 @@
import 'package:flutter/material.dart';
/// Widget avec animation de slide-in automatique
class AnimatedSlideIn extends StatefulWidget {
final Widget child;
final Duration duration;
final Duration delay;
final Offset begin;
final Curve curve;
const AnimatedSlideIn({
super.key,
required this.child,
this.duration = const Duration(milliseconds: 600),
this.delay = Duration.zero,
this.begin = const Offset(0, 0.3),
this.curve = Curves.easeOut,
});
@override
State<AnimatedSlideIn> createState() => _AnimatedSlideInState();
}
class _AnimatedSlideInState extends State<AnimatedSlideIn>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<Offset> _slideAnimation;
late Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: widget.duration,
vsync: this,
);
_slideAnimation = Tween<Offset>(
begin: widget.begin,
end: Offset.zero,
).animate(CurvedAnimation(
parent: _controller,
curve: widget.curve,
));
_fadeAnimation = CurvedAnimation(
parent: _controller,
curve: widget.curve,
);
Future.delayed(widget.delay, () {
if (mounted) {
_controller.forward();
}
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SlideTransition(
position: _slideAnimation,
child: FadeTransition(
opacity: _fadeAnimation,
child: widget.child,
),
);
}
}

View File

@@ -0,0 +1,109 @@
/// UnionFlow Primary Button - Bouton principal
///
/// Bouton primaire avec la couleur Bleu Roi (#4169E1)
/// Utilisé pour les actions principales (connexion, enregistrer, valider, etc.)
library uf_primary_button;
import 'package:flutter/material.dart';
import '../../unionflow_design_system.dart';
/// Bouton primaire UnionFlow
///
/// Usage:
/// ```dart
/// UFPrimaryButton(
/// label: 'Connexion',
/// onPressed: () => login(),
/// icon: Icons.login,
/// isLoading: false,
/// )
/// ```
class UFPrimaryButton extends StatelessWidget {
/// Texte du bouton
final String label;
/// Callback appelé lors du clic
final VoidCallback? onPressed;
/// Indique si le bouton est en chargement
final bool isLoading;
/// Icône optionnelle à gauche du texte
final IconData? icon;
/// Bouton pleine largeur
final bool isFullWidth;
/// Hauteur personnalisée (optionnel)
final double? height;
/// Couleur de fond personnalisée (optionnel)
final Color? backgroundColor;
/// Couleur du texte/icône personnalisée (optionnel)
final Color? textColor;
const UFPrimaryButton({
super.key,
required this.label,
this.onPressed,
this.isLoading = false,
this.icon,
this.isFullWidth = false,
this.height,
this.backgroundColor,
this.textColor,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: isFullWidth ? double.infinity : null,
height: height ?? SpacingTokens.buttonHeightLarge,
child: ElevatedButton(
onPressed: isLoading ? null : onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: backgroundColor ?? AppColors.primaryGreen,
foregroundColor: textColor ?? Colors.white,
disabledBackgroundColor: (backgroundColor ?? AppColors.primaryGreen).withOpacity(0.5),
disabledForegroundColor: (textColor ?? Colors.white).withOpacity(0.7),
elevation: SpacingTokens.elevationSm,
shadowColor: AppColors.darkBorder.withOpacity(0.1),
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.buttonPaddingHorizontal,
vertical: SpacingTokens.buttonPaddingVertical,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
),
),
child: isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.white,
),
),
)
: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (icon != null) ...[
Icon(icon, size: 20),
const SizedBox(width: SpacingTokens.md),
],
Text(
label,
style: AppTypography.actionText,
),
],
),
),
);
}
}

View File

@@ -0,0 +1,80 @@
/// UnionFlow Secondary Button - Bouton secondaire
///
/// Bouton secondaire avec la couleur Indigo (#6366F1)
/// Utilisé pour les actions secondaires (annuler, retour, etc.)
library uf_secondary_button;
import 'package:flutter/material.dart';
import '../../unionflow_design_system.dart';
/// Bouton secondaire UnionFlow
class UFSecondaryButton extends StatelessWidget {
final String label;
final VoidCallback? onPressed;
final bool isLoading;
final IconData? icon;
final bool isFullWidth;
final double? height;
const UFSecondaryButton({
super.key,
required this.label,
this.onPressed,
this.isLoading = false,
this.icon,
this.isFullWidth = false,
this.height,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: isFullWidth ? double.infinity : null,
height: height ?? SpacingTokens.buttonHeightLarge,
child: ElevatedButton(
onPressed: isLoading ? null : onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.brandGreen,
foregroundColor: Colors.white,
disabledBackgroundColor: AppColors.brandGreen.withOpacity(0.5),
disabledForegroundColor: Colors.white.withOpacity(0.7),
elevation: SpacingTokens.elevationSm,
shadowColor: AppColors.darkBorder.withOpacity(0.1),
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.buttonPaddingHorizontal,
vertical: SpacingTokens.buttonPaddingVertical,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
),
),
child: isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.white,
),
),
)
: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (icon != null) ...[
Icon(icon, size: 20),
const SizedBox(width: SpacingTokens.md),
],
Text(
label,
style: AppTypography.actionText,
),
],
),
),
);
}
}

View File

@@ -0,0 +1,158 @@
import 'package:flutter/material.dart';
import '../../unionflow_design_system.dart';
/// Card standardisé UnionFlow
///
/// Composant Card unifié avec 3 styles prédéfinis :
/// - elevated : Card avec ombre (par défaut)
/// - outlined : Card avec bordure
/// - filled : Card avec fond coloré
///
/// Usage:
/// ```dart
/// UFCard(
/// child: Text('Contenu'),
/// )
///
/// UFCard.outlined(
/// child: Text('Contenu'),
/// )
///
/// UFCard.filled(
/// color: ColorTokens.primary,
/// child: Text('Contenu'),
/// )
/// ```
class UFCard extends StatelessWidget {
final Widget child;
final EdgeInsets? padding;
final EdgeInsets? margin;
final VoidCallback? onTap;
final VoidCallback? onLongPress;
final UFCardStyle style;
final Color? color;
final Color? borderColor;
final double? borderWidth;
final double? elevation;
final double? borderRadius;
/// Card avec ombre (style par défaut)
const UFCard({
super.key,
required this.child,
this.padding,
this.margin,
this.onTap,
this.onLongPress,
this.color,
this.elevation,
this.borderRadius,
}) : style = UFCardStyle.elevated,
borderColor = null,
borderWidth = null;
/// Card avec bordure
const UFCard.outlined({
super.key,
required this.child,
this.padding,
this.margin,
this.onTap,
this.onLongPress,
this.color,
this.borderColor,
this.borderWidth,
this.borderRadius,
}) : style = UFCardStyle.outlined,
elevation = null;
/// Card avec fond coloré
const UFCard.filled({
super.key,
required this.child,
this.padding,
this.margin,
this.onTap,
this.onLongPress,
required this.color,
this.borderRadius,
}) : style = UFCardStyle.filled,
borderColor = null,
borderWidth = null,
elevation = null;
@override
Widget build(BuildContext context) {
final effectivePadding = padding ?? const EdgeInsets.all(SpacingTokens.cardPadding);
final effectiveMargin = margin ?? EdgeInsets.zero;
final effectiveBorderRadius = borderRadius ?? SpacingTokens.radiusLg;
Widget content = Container(
padding: effectivePadding,
decoration: _getDecoration(effectiveBorderRadius),
child: child,
);
if (onTap != null || onLongPress != null) {
content = InkWell(
onTap: onTap,
onLongPress: onLongPress,
borderRadius: BorderRadius.circular(effectiveBorderRadius),
child: content,
);
}
return Container(
margin: effectiveMargin,
child: content,
);
}
BoxDecoration _getDecoration(double radius) {
switch (style) {
case UFCardStyle.elevated:
return BoxDecoration(
color: color ?? AppColors.lightSurface,
borderRadius: BorderRadius.circular(radius),
boxShadow: elevation != null
? [
BoxShadow(
color: AppColors.darkBorder.withOpacity(0.1),
blurRadius: elevation!,
offset: const Offset(0, 2),
),
]
: ShadowTokens.sm,
);
case UFCardStyle.outlined:
return BoxDecoration(
color: color ?? AppColors.lightSurface,
borderRadius: BorderRadius.circular(radius),
border: Border.all(
color: borderColor ?? AppColors.lightBorder,
width: borderWidth ?? 1.0,
),
);
case UFCardStyle.filled:
return BoxDecoration(
color: color ?? AppColors.lightSurface,
borderRadius: BorderRadius.circular(radius),
);
}
}
}
/// Styles de Card disponibles
enum UFCardStyle {
/// Card avec ombre
elevated,
/// Card avec bordure
outlined,
/// Card avec fond coloré
filled,
}

View File

@@ -0,0 +1,89 @@
/// UnionFlow Info Card - Card d'information générique
///
/// Card blanche avec titre, icône et contenu personnalisable
library uf_info_card;
import 'package:flutter/material.dart';
import '../../unionflow_design_system.dart';
/// Card d'information générique
///
/// Usage:
/// ```dart
/// UFInfoCard(
/// title: 'État du système',
/// icon: Icons.health_and_safety,
/// iconColor: ColorTokens.primary,
/// trailing: Container(...), // Badge ou autre widget
/// child: Column(...), // Contenu de la card
/// )
/// ```
class UFInfoCard extends StatelessWidget {
/// Titre de la card
final String title;
/// Icône du titre
final IconData icon;
/// Couleur de l'icône (par défaut: primary)
final Color? iconColor;
/// Widget à droite du titre (badge, bouton, etc.)
final Widget? trailing;
/// Contenu de la card
final Widget child;
/// Padding de la card (par défaut: xl)
final EdgeInsets? padding;
const UFInfoCard({
super.key,
required this.title,
required this.icon,
this.iconColor,
this.trailing,
required this.child,
this.padding,
});
@override
Widget build(BuildContext context) {
final effectiveIconColor = iconColor ?? AppColors.primaryGreen;
final effectivePadding = padding ?? const EdgeInsets.all(SpacingTokens.xl);
return Container(
padding: effectivePadding,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
boxShadow: ShadowTokens.sm,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header avec titre et trailing
Row(
children: [
Icon(icon, color: effectiveIconColor, size: 20),
const SizedBox(width: SpacingTokens.md),
Expanded(
child: Text(
title,
style: AppTypography.headerSmall.copyWith(
color: AppColors.textPrimaryLight,
),
),
),
if (trailing != null) trailing!,
],
),
const SizedBox(height: SpacingTokens.xl),
// Contenu
child,
],
),
);
}
}

View File

@@ -0,0 +1,75 @@
/// UnionFlow Metric Card - Card de métrique système
///
/// Card compacte pour afficher une métrique système (CPU, RAM, etc.)
library uf_metric_card;
import 'package:flutter/material.dart';
import '../../unionflow_design_system.dart';
/// Card de métrique système
///
/// Usage:
/// ```dart
/// UFMetricCard(
/// label: 'CPU',
/// value: '23.5%',
/// icon: Icons.memory,
/// color: ColorTokens.success,
/// )
/// ```
class UFMetricCard extends StatelessWidget {
/// Label de la métrique (ex: "CPU")
final String label;
/// Valeur de la métrique (ex: "23.5%")
final String value;
/// Icône représentant la métrique
final IconData icon;
/// Couleur de la métrique (optionnel)
final Color? color;
const UFMetricCard({
super.key,
required this.label,
required this.value,
required this.icon,
this.color,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(SpacingTokens.md),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
),
child: Column(
children: [
Icon(icon, color: Colors.white, size: 16),
const SizedBox(height: SpacingTokens.sm),
Text(
value,
style: AppTypography.badgeText.copyWith(
fontWeight: FontWeight.bold,
color: Colors.white,
),
textAlign: TextAlign.center,
),
Text(
label,
style: AppTypography.badgeText.copyWith(
fontSize: 9,
color: Colors.white.withOpacity(0.8),
),
textAlign: TextAlign.center,
),
],
),
);
}
}

View File

@@ -0,0 +1,141 @@
/// UnionFlow Stat Card - Card de statistiques
///
/// Card affichant une statistique avec icône, titre, valeur et sous-titre optionnel
/// Utilisé dans le dashboard pour afficher les métriques clés
library uf_stat_card;
import 'package:flutter/material.dart';
import '../../unionflow_design_system.dart';
/// Card de statistiques UnionFlow
///
/// Usage:
/// ```dart
/// UFStatCard(
/// title: 'Membres',
/// value: '142',
/// icon: Icons.people,
/// iconColor: ColorTokens.primary,
/// subtitle: '+5 ce mois',
/// onTap: () => navigateToMembers(),
/// )
/// ```
class UFStatCard extends StatelessWidget {
/// Titre de la statistique (ex: "Membres")
final String title;
/// Valeur de la statistique (ex: "142")
final String value;
/// Icône représentant la statistique
final IconData icon;
/// Couleur de l'icône (par défaut: primary)
final Color? iconColor;
/// Sous-titre optionnel (ex: "+5 ce mois")
final String? subtitle;
/// Callback appelé lors du clic sur la card
final VoidCallback? onTap;
/// Couleur de fond de l'icône (par défaut: iconColor avec opacité)
final Color? iconBackgroundColor;
const UFStatCard({
super.key,
required this.title,
required this.value,
required this.icon,
this.iconColor,
this.subtitle,
this.onTap,
this.iconBackgroundColor,
});
@override
Widget build(BuildContext context) {
final effectiveIconColor = iconColor ?? AppColors.primaryGreen;
final effectiveIconBgColor = iconBackgroundColor ??
effectiveIconColor.withOpacity(0.1);
return Card(
elevation: SpacingTokens.elevationSm,
shadowColor: AppColors.darkBorder.withOpacity(0.1),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
child: Padding(
padding: const EdgeInsets.all(SpacingTokens.cardPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// Header avec icône et flèche
Row(
children: [
// Icône avec background coloré
Container(
padding: const EdgeInsets.all(SpacingTokens.md),
decoration: BoxDecoration(
color: effectiveIconBgColor,
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
),
child: Icon(
icon,
color: effectiveIconColor,
size: 24,
),
),
const Spacer(),
// Flèche si cliquable
if (onTap != null)
const Icon(
Icons.arrow_forward_ios,
size: 16,
color: AppColors.textSecondaryLight,
),
],
),
const SizedBox(height: SpacingTokens.lg),
// Titre
Text(
title,
style: AppTypography.badgeText.copyWith(
color: AppColors.textSecondaryLight,
),
),
const SizedBox(height: SpacingTokens.sm),
// Valeur
Text(
value,
style: AppTypography.headerSmall.copyWith(
color: AppColors.textPrimaryLight,
),
),
// Sous-titre optionnel
if (subtitle != null) ...[
const SizedBox(height: SpacingTokens.sm),
Text(
subtitle!,
style: AppTypography.subtitleSmall.copyWith(
color: AppColors.textSecondaryLight,
),
),
],
],
),
),
),
);
}
}

View File

@@ -0,0 +1,43 @@
/// UnionFlow Components - Export centralisé
///
/// Ce fichier exporte tous les composants réutilisables du Design System
library components;
// ═══════════════════════════════════════════════════════════════════════════
// BOUTONS
// ═══════════════════════════════════════════════════════════════════════════
export 'buttons/uf_primary_button.dart';
export 'buttons/uf_secondary_button.dart';
// ═══════════════════════════════════════════════════════════════════════════
// CARDS & CONTAINERS
// ═══════════════════════════════════════════════════════════════════════════
export 'cards/uf_card.dart';
export 'cards/uf_stat_card.dart';
export 'cards/uf_info_card.dart';
export 'cards/uf_metric_card.dart';
export 'uf_container.dart';
// ═══════════════════════════════════════════════════════════════════════════
// INPUTS
// ═══════════════════════════════════════════════════════════════════════════
export 'inputs/uf_switch_tile.dart';
export 'inputs/uf_dropdown_tile.dart';
// ═══════════════════════════════════════════════════════════════════════════
// HEADERS & APPBAR
// ═══════════════════════════════════════════════════════════════════════════
export 'uf_header.dart';
export 'uf_page_header.dart';
export 'uf_app_bar.dart';
// Composants supplémentaires à exporter quand créés :
// export 'buttons/uf_outline_button.dart';
// export 'buttons/uf_text_button.dart';
// export 'cards/uf_event_card.dart';
// export 'inputs/uf_text_field.dart';

View File

@@ -0,0 +1,97 @@
/// UnionFlow Dropdown Tile - Ligne de paramètre avec dropdown
///
/// Tile avec titre et dropdown pour les paramètres
library uf_dropdown_tile;
import 'package:flutter/material.dart';
import '../../unionflow_design_system.dart';
/// Tile de paramètre avec dropdown
///
/// Usage:
/// ```dart
/// UFDropdownTile<String>(
/// title: 'Niveau de log',
/// value: 'INFO',
/// items: ['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR'],
/// onChanged: (value) => setState(() => _logLevel = value),
/// )
/// ```
class UFDropdownTile<T> extends StatelessWidget {
/// Titre du paramètre
final String title;
/// Valeur actuelle
final T value;
/// Liste des options
final List<T> items;
/// Callback appelé lors du changement
final ValueChanged<T?>? onChanged;
/// Couleur de fond (par défaut: surfaceVariant)
final Color? backgroundColor;
/// Fonction pour afficher le texte d'un item (par défaut: toString())
final String Function(T)? itemBuilder;
const UFDropdownTile({
super.key,
required this.title,
required this.value,
required this.items,
this.onChanged,
this.backgroundColor,
this.itemBuilder,
});
@override
Widget build(BuildContext context) {
final effectiveBgColor = backgroundColor ?? AppColors.lightSurface;
final effectiveItemBuilder = itemBuilder ?? (item) => item.toString();
return Container(
margin: const EdgeInsets.only(bottom: SpacingTokens.lg),
padding: const EdgeInsets.all(SpacingTokens.lg),
decoration: BoxDecoration(
color: effectiveBgColor,
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
),
child: Row(
children: [
Expanded(
child: Text(
title,
style: AppTypography.bodyTextSmall.copyWith(
fontWeight: FontWeight.w600,
color: AppColors.textPrimaryLight,
),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: SpacingTokens.lg),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
border: Border.all(color: AppColors.lightBorder),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<T>(
value: value,
onChanged: onChanged,
items: items.map((item) {
return DropdownMenuItem<T>(
value: item,
child: Text(effectiveItemBuilder(item)),
);
}).toList(),
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,88 @@
/// UnionFlow Switch Tile - Ligne de paramètre avec switch
///
/// Tile avec titre, description et switch pour les paramètres
library uf_switch_tile;
import 'package:flutter/material.dart';
import '../../unionflow_design_system.dart';
/// Tile de paramètre avec switch
///
/// Usage:
/// ```dart
/// UFSwitchTile(
/// title: 'Notifications',
/// subtitle: 'Activer les notifications push',
/// value: true,
/// onChanged: (value) => setState(() => _notifications = value),
/// )
/// ```
class UFSwitchTile extends StatelessWidget {
/// Titre du paramètre
final String title;
/// Description du paramètre
final String subtitle;
/// Valeur actuelle du switch
final bool value;
/// Callback appelé lors du changement
final ValueChanged<bool>? onChanged;
/// Couleur de fond (par défaut: surfaceVariant)
final Color? backgroundColor;
const UFSwitchTile({
super.key,
required this.title,
required this.subtitle,
required this.value,
this.onChanged,
this.backgroundColor,
});
@override
Widget build(BuildContext context) {
final effectiveBgColor = backgroundColor ?? AppColors.lightSurface;
return Container(
margin: const EdgeInsets.only(bottom: SpacingTokens.lg),
padding: const EdgeInsets.all(SpacingTokens.lg),
decoration: BoxDecoration(
color: effectiveBgColor,
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: AppTypography.bodyTextSmall.copyWith(
fontWeight: FontWeight.w600,
color: AppColors.textPrimaryLight,
),
),
Text(
subtitle,
style: AppTypography.subtitleSmall.copyWith(
color: AppColors.textSecondaryLight,
),
),
],
),
),
Switch(
value: value,
onChanged: onChanged,
activeColor: AppColors.primaryGreen,
),
],
),
);
}
}

View File

@@ -0,0 +1,103 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../unionflow_design_system.dart';
/// AppBar standardisé UnionFlow
///
/// Composant AppBar unifié pour toutes les pages de détail/formulaire.
/// Garantit la cohérence visuelle et l'expérience utilisateur.
///
/// Si [mergeLeadingWithTitle] est true et que la route peut être quittée,
/// le bouton retour et le titre sont fusionnés en une seule ligne (retour
/// toujours visible avec la même couleur que le titre).
class UFAppBar extends StatelessWidget implements PreferredSizeWidget {
final String title;
final List<Widget>? actions;
final Widget? leading;
final bool automaticallyImplyLeading;
/// Fusionne le bouton retour et le titre en une seule zone (retour visible, même couleur que le titre).
final bool mergeLeadingWithTitle;
final PreferredSizeWidget? bottom;
final Color? backgroundColor;
final Color? foregroundColor;
final double elevation;
const UFAppBar({
super.key,
required this.title,
this.actions,
this.leading,
this.automaticallyImplyLeading = true,
this.mergeLeadingWithTitle = false,
this.bottom,
this.backgroundColor,
this.foregroundColor,
this.elevation = 0,
});
@override
Widget build(BuildContext context) {
final canPop = ModalRoute.of(context)?.canPop ?? false;
final fg = foregroundColor ?? Colors.white;
final useMergedTitle = mergeLeadingWithTitle && canPop;
final isTransparent = backgroundColor == Colors.transparent ||
(backgroundColor != null && backgroundColor!.opacity < 0.1);
return AppBar(
title: useMergedTitle
? Row(
children: [
Material(
color: isTransparent ? Colors.black26 : Colors.transparent,
borderRadius: BorderRadius.circular(20),
child: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.of(context).pop(),
color: fg,
tooltip: 'Retour',
style: IconButton.styleFrom(
foregroundColor: fg,
),
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
title,
style: AppTypography.headerSmall.copyWith(
color: fg,
fontWeight: FontWeight.w600,
),
overflow: TextOverflow.ellipsis,
),
),
],
)
: Text(title),
backgroundColor: backgroundColor ?? AppColors.primaryGreen,
foregroundColor: fg,
elevation: elevation,
leading: useMergedTitle ? null : leading,
automaticallyImplyLeading: useMergedTitle ? false : automaticallyImplyLeading,
actions: actions,
bottom: bottom,
systemOverlayStyle: const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.light,
statusBarBrightness: Brightness.dark,
),
centerTitle: false,
titleTextStyle: AppTypography.headerSmall.copyWith(
color: fg,
fontWeight: FontWeight.w600,
),
);
}
@override
Size get preferredSize => Size.fromHeight(
kToolbarHeight + (bottom?.preferredSize.height ?? 0.0),
);
}

View File

@@ -0,0 +1,2 @@
export 'buttons/uf_primary_button.dart';
export 'buttons/uf_secondary_button.dart';

View File

@@ -0,0 +1,138 @@
import 'package:flutter/material.dart';
import '../unionflow_design_system.dart';
/// Container standardisé UnionFlow
///
/// Composant Container unifié avec styles prédéfinis.
/// Garantit la cohérence des espacements, rayons et ombres.
///
/// Usage:
/// ```dart
/// UFContainer(
/// child: Text('Contenu'),
/// )
///
/// UFContainer.rounded(
/// color: ColorTokens.primary,
/// child: Text('Contenu'),
/// )
///
/// UFContainer.elevated(
/// child: Text('Contenu'),
/// )
/// ```
class UFContainer extends StatelessWidget {
final Widget child;
final Color? color;
final EdgeInsets? padding;
final EdgeInsets? margin;
final double? width;
final double? height;
final AlignmentGeometry? alignment;
final BoxConstraints? constraints;
final Gradient? gradient;
final double borderRadius;
final Border? border;
final List<BoxShadow>? boxShadow;
/// Container standard
const UFContainer({
super.key,
required this.child,
this.color,
this.padding,
this.margin,
this.width,
this.height,
this.alignment,
this.constraints,
this.gradient,
this.border,
this.boxShadow,
}) : borderRadius = SpacingTokens.radiusMd;
/// Container avec coins arrondis
const UFContainer.rounded({
super.key,
required this.child,
this.color,
this.padding,
this.margin,
this.width,
this.height,
this.alignment,
this.constraints,
this.gradient,
this.border,
this.boxShadow,
}) : borderRadius = SpacingTokens.radiusLg;
/// Container très arrondi
const UFContainer.extraRounded({
super.key,
required this.child,
this.color,
this.padding,
this.margin,
this.width,
this.height,
this.alignment,
this.constraints,
this.gradient,
this.border,
this.boxShadow,
}) : borderRadius = SpacingTokens.radiusXl;
/// Container avec ombre
UFContainer.elevated({
super.key,
required this.child,
this.color,
this.padding,
this.margin,
this.width,
this.height,
this.alignment,
this.constraints,
this.gradient,
this.border,
}) : borderRadius = SpacingTokens.radiusLg,
boxShadow = ShadowTokens.sm;
/// Container circulaire
const UFContainer.circular({
super.key,
required this.child,
this.color,
this.padding,
this.margin,
this.width,
this.height,
this.alignment,
this.constraints,
this.gradient,
this.border,
this.boxShadow,
}) : borderRadius = SpacingTokens.radiusCircular;
@override
Widget build(BuildContext context) {
return Container(
width: width,
height: height,
padding: padding,
margin: margin,
alignment: alignment,
constraints: constraints,
decoration: BoxDecoration(
color: gradient == null ? (color ?? AppColors.lightSurface) : null,
gradient: gradient,
borderRadius: BorderRadius.circular(borderRadius),
border: border,
boxShadow: boxShadow,
),
child: child,
);
}
}

View File

@@ -0,0 +1,127 @@
import 'package:flutter/material.dart';
import '../unionflow_design_system.dart';
/// Header harmonisé UnionFlow
///
/// Composant header standardisé pour toutes les pages de l'application.
/// Garantit la cohérence visuelle et l'expérience utilisateur.
class UFHeader extends StatelessWidget {
final String title;
final String? subtitle;
final IconData icon;
final List<Widget>? actions;
final VoidCallback? onNotificationTap;
final VoidCallback? onSettingsTap;
final bool showActions;
const UFHeader({
super.key,
required this.title,
this.subtitle,
required this.icon,
this.actions,
this.onNotificationTap,
this.onSettingsTap,
this.showActions = true,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(SpacingTokens.xl),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [AppColors.primaryGreen, AppColors.brandGreenLight],
),
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
boxShadow: ShadowTokens.primary,
),
child: Row(
children: [
// Icône et contenu principal
Container(
padding: const EdgeInsets.all(SpacingTokens.sm),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
),
child: Icon(
icon,
color: Colors.white,
size: 24,
),
),
const SizedBox(width: SpacingTokens.lg),
// Titre et sous-titre
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: AppTypography.headerSmall.copyWith(
color: Colors.white,
),
),
if (subtitle != null) ...[
const SizedBox(height: SpacingTokens.xs),
Text(
subtitle!,
style: AppTypography.subtitleSmall.copyWith(
color: Colors.white.withOpacity(0.8),
),
),
],
],
),
),
// Actions
if (showActions) _buildActions(),
],
),
);
}
Widget _buildActions() {
if (actions != null) {
return Row(children: actions!);
}
return Row(
children: [
if (onNotificationTap != null)
Container(
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(SpacingTokens.radiusSm),
),
child: IconButton(
onPressed: onNotificationTap,
icon: const Icon(
Icons.notifications_outlined,
color: Colors.white,
),
),
),
if (onNotificationTap != null && onSettingsTap != null)
const SizedBox(width: SpacingTokens.sm),
if (onSettingsTap != null)
Container(
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(SpacingTokens.radiusSm),
),
child: IconButton(
onPressed: onSettingsTap,
icon: const Icon(
Icons.settings_outlined,
color: Colors.white,
),
),
),
],
);
}
}

View File

@@ -0,0 +1,237 @@
import 'package:flutter/material.dart';
import '../unionflow_design_system.dart';
/// Header de page compact et moderne
///
/// Composant header minimaliste pour les pages principales du BottomNavigationBar.
/// Design épuré sans gradient lourd, optimisé pour l'espace.
///
/// Usage:
/// ```dart
/// UFPageHeader(
/// title: 'Membres',
/// icon: Icons.people,
/// actions: [
/// IconButton(icon: Icon(Icons.add), onPressed: () => Navigator.pop(context)),
/// ],
/// )
/// ```
class UFPageHeader extends StatelessWidget {
final String title;
final IconData icon;
final List<Widget>? actions;
final Color? iconColor;
final bool showDivider;
const UFPageHeader({
super.key,
required this.title,
required this.icon,
this.actions,
this.iconColor,
this.showDivider = true,
});
@override
Widget build(BuildContext context) {
final effectiveIconColor = iconColor ?? AppColors.primaryGreen;
return Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.lg,
vertical: SpacingTokens.md,
),
child: Row(
children: [
// Icône
Container(
padding: const EdgeInsets.all(SpacingTokens.sm),
decoration: BoxDecoration(
color: effectiveIconColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
),
child: Icon(
icon,
color: effectiveIconColor,
size: 20,
),
),
const SizedBox(width: SpacingTokens.md),
// Titre
Expanded(
child: Text(
title,
style: AppTypography.headerSmall.copyWith(
color: AppColors.textPrimaryLight,
fontWeight: FontWeight.w600,
),
),
),
// Actions
if (actions != null) ...actions!,
],
),
),
// Divider optionnel
if (showDivider)
const Divider(
height: 1,
thickness: 1,
color: AppColors.lightBorder,
),
],
);
}
}
/// Header de page avec statistiques
///
/// Header compact avec des métriques KPI intégrées.
class UFPageHeaderWithStats extends StatelessWidget {
final String title;
final IconData icon;
final List<UFHeaderStat> stats;
final List<Widget>? actions;
final Color? iconColor;
const UFPageHeaderWithStats({
super.key,
required this.title,
required this.icon,
required this.stats,
this.actions,
this.iconColor,
});
@override
Widget build(BuildContext context) {
final effectiveIconColor = iconColor ?? AppColors.primaryGreen;
return Column(
children: [
// Titre et actions
Padding(
padding: const EdgeInsets.fromLTRB(
SpacingTokens.lg,
SpacingTokens.md,
SpacingTokens.lg,
SpacingTokens.sm,
),
child: Row(
children: [
// Icône
Container(
padding: const EdgeInsets.all(SpacingTokens.sm),
decoration: BoxDecoration(
color: effectiveIconColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
),
child: Icon(
icon,
color: effectiveIconColor,
size: 20,
),
),
const SizedBox(width: SpacingTokens.md),
// Titre
Expanded(
child: Text(
title,
style: AppTypography.headerSmall.copyWith(
color: AppColors.textPrimaryLight,
fontWeight: FontWeight.w600,
),
),
),
// Actions
if (actions != null) ...actions!,
],
),
),
// Statistiques
Padding(
padding: const EdgeInsets.fromLTRB(
SpacingTokens.lg,
0,
SpacingTokens.lg,
SpacingTokens.md,
),
child: Row(
children: stats.map((stat) {
final isLast = stat == stats.last;
return Expanded(
child: Padding(
padding: EdgeInsets.only(
right: isLast ? 0 : SpacingTokens.sm,
),
child: _buildStatItem(stat),
),
);
}).toList(),
),
),
// Divider
const Divider(
height: 1,
thickness: 1,
color: AppColors.lightBorder,
),
],
);
}
Widget _buildStatItem(UFHeaderStat stat) {
final effectiveColor = stat.color ?? AppColors.primaryGreen;
return UFContainer.rounded(
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.md,
vertical: SpacingTokens.sm,
),
color: effectiveColor.withOpacity(0.05),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
stat.value,
style: AppTypography.headerSmall.copyWith(
color: effectiveColor,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: SpacingTokens.xs),
Text(
stat.label,
style: AppTypography.badgeText.copyWith(
color: AppColors.textSecondaryLight,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
);
}
}
/// Statistique pour UFPageHeaderWithStats
class UFHeaderStat {
final String label;
final String value;
final Color? color;
const UFHeaderStat({
required this.label,
required this.value,
this.color,
});
}

View File

@@ -0,0 +1,81 @@
import 'package:flutter/material.dart';
import '../tokens/unionflow_colors.dart';
/// Bouton d'action rapide UnionFlow
class UnionActionButton extends StatelessWidget {
final IconData icon;
final String label;
final VoidCallback onTap;
final Color? backgroundColor;
final Color? iconColor;
const UnionActionButton({
super.key,
required this.icon,
required this.label,
required this.onTap,
this.backgroundColor,
this.iconColor,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12),
decoration: BoxDecoration(
color: backgroundColor ?? UnionFlowColors.unionGreenPale,
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: (backgroundColor ?? UnionFlowColors.unionGreenPale)
.withOpacity(0.2),
width: 1,
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
size: 28,
color: iconColor ?? UnionFlowColors.unionGreen,
),
const SizedBox(height: 8),
Text(
label,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: UnionFlowColors.textPrimary,
),
textAlign: TextAlign.center,
),
],
),
),
);
}
}
/// Grid d'actions rapides
class UnionActionGrid extends StatelessWidget {
final List<UnionActionButton> actions;
const UnionActionGrid({
super.key,
required this.actions,
});
@override
Widget build(BuildContext context) {
return Row(
children: [
for (int i = 0; i < actions.length; i++) ...[
Expanded(child: actions[i]),
if (i < actions.length - 1) const SizedBox(width: 12),
],
],
);
}
}

View File

@@ -0,0 +1,98 @@
import 'package:flutter/material.dart';
import '../tokens/unionflow_colors.dart';
/// Card de balance UnionFlow - Affichage élégant du solde principal
class UnionBalanceCard extends StatelessWidget {
final String label;
final String amount;
final String? trend;
final bool? isTrendPositive;
final VoidCallback? onTap;
const UnionBalanceCard({
super.key,
required this.label,
required this.amount,
this.trend,
this.isTrendPositive,
this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: UnionFlowColors.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: UnionFlowColors.softShadow,
// Bordure dorée subtile en haut
border: const Border(
top: BorderSide(
color: UnionFlowColors.gold,
width: 3,
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Label
Text(
label.toUpperCase(),
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: UnionFlowColors.textSecondary,
letterSpacing: 0.8,
),
),
const SizedBox(height: 8),
// Montant principal
Text(
amount,
style: const TextStyle(
fontSize: 32,
fontWeight: FontWeight.w700,
color: UnionFlowColors.unionGreen,
height: 1.2,
),
),
// Trend (optionnel)
if (trend != null) ...[
const SizedBox(height: 10),
Row(
children: [
Icon(
isTrendPositive == true
? Icons.trending_up
: Icons.trending_down,
size: 16,
color: isTrendPositive == true
? UnionFlowColors.success
: UnionFlowColors.error,
),
const SizedBox(width: 4),
Text(
trend!,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: isTrendPositive == true
? UnionFlowColors.success
: UnionFlowColors.error,
),
),
],
),
],
],
),
),
);
}
}

View File

@@ -0,0 +1,168 @@
import 'package:flutter/material.dart';
import '../tokens/unionflow_colors.dart';
/// Type d'export disponible
enum ExportType {
pdf('PDF', Icons.picture_as_pdf),
excel('Excel', Icons.table_chart),
csv('CSV', Icons.description);
final String label;
final IconData icon;
const ExportType(this.label, this.icon);
}
/// Bouton d'export avec options
class UnionExportButton extends StatelessWidget {
final Function(ExportType) onExport;
final bool isLoading;
const UnionExportButton({
super.key,
required this.onExport,
this.isLoading = false,
});
@override
Widget build(BuildContext context) {
return PopupMenuButton<ExportType>(
icon: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
gradient: UnionFlowColors.primaryGradient,
borderRadius: BorderRadius.circular(12),
boxShadow: UnionFlowColors.greenGlowShadow,
),
child: isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: const Icon(
Icons.download,
color: Colors.white,
size: 20,
),
),
itemBuilder: (context) => ExportType.values.map((type) {
return PopupMenuItem<ExportType>(
value: type,
child: Row(
children: [
Icon(
type.icon,
size: 20,
color: UnionFlowColors.unionGreen,
),
const SizedBox(width: 12),
Text(
'Exporter en ${type.label}',
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: UnionFlowColors.textPrimary,
),
),
],
),
);
}).toList(),
onSelected: onExport,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 8,
color: UnionFlowColors.surface,
);
}
}
/// Dialog pour confirmer l'export
class ExportConfirmDialog extends StatelessWidget {
final ExportType exportType;
final VoidCallback onConfirm;
final String? message;
const ExportConfirmDialog({
super.key,
required this.exportType,
required this.onConfirm,
this.message,
});
@override
Widget build(BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
backgroundColor: UnionFlowColors.surface,
title: Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: UnionFlowColors.unionGreen.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
exportType.icon,
color: UnionFlowColors.unionGreen,
size: 24,
),
),
const SizedBox(width: 12),
Text(
'Exporter en ${exportType.label}',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: UnionFlowColors.textPrimary,
),
),
],
),
content: Text(
message ?? 'Voulez-vous exporter le rapport au format ${exportType.label}?',
style: const TextStyle(
fontSize: 14,
color: UnionFlowColors.textSecondary,
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text(
'Annuler',
style: TextStyle(
color: UnionFlowColors.textSecondary,
fontWeight: FontWeight.w600,
),
),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
onConfirm();
},
style: ElevatedButton.styleFrom(
backgroundColor: UnionFlowColors.unionGreen,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 0,
),
child: const Text(
'Confirmer',
style: TextStyle(fontWeight: FontWeight.w700),
),
),
],
);
}
}

View File

@@ -0,0 +1,65 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import '../tokens/unionflow_colors.dart';
/// Card avec effet glassmorphism
class UnionGlassCard extends StatelessWidget {
final Widget child;
final EdgeInsetsGeometry? padding;
final EdgeInsetsGeometry? margin;
final double? borderRadius;
final VoidCallback? onTap;
const UnionGlassCard({
super.key,
required this.child,
this.padding,
this.margin,
this.borderRadius,
this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
margin: margin,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(borderRadius ?? 16),
border: Border.all(
color: Colors.white.withOpacity(0.2),
width: 1.5,
),
boxShadow: [
BoxShadow(
color: UnionFlowColors.unionGreen.withOpacity(0.1),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(borderRadius ?? 16),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Container(
padding: padding ?? const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.white.withOpacity(0.2),
Colors.white.withOpacity(0.1),
],
),
),
child: child,
),
),
),
),
);
}
}

View File

@@ -0,0 +1,216 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import '../tokens/unionflow_colors.dart';
/// Graphique en ligne UnionFlow - Pour afficher l'évolution temporelle
class UnionLineChart extends StatelessWidget {
final List<FlSpot> spots;
final String title;
final String? subtitle;
final Color? lineColor;
final Color? gradientStartColor;
final Color? gradientEndColor;
const UnionLineChart({
super.key,
required this.spots,
required this.title,
this.subtitle,
this.lineColor,
this.gradientStartColor,
this.gradientEndColor,
});
/// Calcule maxY de manière sécurisée pour éviter NaN, Infinity ou 0
double _calculateSafeMaxY() {
if (spots.isEmpty) return 100.0;
final maxValue = spots.map((e) => e.y).reduce((a, b) => a > b ? a : b);
// Si maxValue est invalide (NaN, Infinity) ou trop petit
if (maxValue.isNaN || maxValue.isInfinite || maxValue <= 0) {
return 100.0;
}
return maxValue * 1.2;
}
/// Calcule maxX de manière sécurisée
double _calculateSafeMaxX() {
if (spots.isEmpty) return 11.0; // 12 mois - 1
return spots.length.toDouble() - 1;
}
/// Calcule l'intervalle de grille approprié basé sur maxY
double _calculateGridInterval() {
final maxY = _calculateSafeMaxY();
// Calculer un intervalle qui donne environ 4-6 lignes de grille
final baseInterval = maxY / 5;
if (baseInterval == 0) return 20.0; // Fallback si maxY est trop petit
// Arrondir à un nombre "propre" (puissance de 10)
final magnitude = pow(10.0, (log(baseInterval) / log(10.0)).floor()).toDouble();
final normalized = baseInterval / magnitude;
// Arrondir vers le haut au multiple de 1, 2 ou 5 le plus proche
double roundedInterval;
if (normalized <= 1) {
roundedInterval = 1;
} else if (normalized <= 2) {
roundedInterval = 2;
} else if (normalized <= 5) {
roundedInterval = 5;
} else {
roundedInterval = 10;
}
return roundedInterval * magnitude;
}
@override
Widget build(BuildContext context) {
final effectiveLineColor = lineColor ?? UnionFlowColors.unionGreen;
final effectiveGradientStart = gradientStartColor ?? UnionFlowColors.unionGreen.withOpacity(0.3);
final effectiveGradientEnd = gradientEndColor ?? UnionFlowColors.unionGreen.withOpacity(0.0);
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: UnionFlowColors.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: UnionFlowColors.softShadow,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: UnionFlowColors.textPrimary,
),
),
if (subtitle != null) ...[
const SizedBox(height: 4),
Text(
subtitle!,
style: const TextStyle(
fontSize: 11,
color: UnionFlowColors.textSecondary,
),
),
],
const SizedBox(height: 20),
// Chart
SizedBox(
height: 180,
child: LineChart(
LineChartData(
gridData: FlGridData(
show: true,
drawVerticalLine: false,
horizontalInterval: _calculateGridInterval(),
getDrawingHorizontalLine: (value) {
return FlLine(
color: UnionFlowColors.border.withOpacity(0.2),
strokeWidth: 1,
);
},
),
titlesData: FlTitlesData(
show: true,
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
interval: 1,
getTitlesWidget: (value, meta) {
const months = ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Jun', 'Jul', 'Aoû', 'Sep', 'Oct', 'Nov', 'Déc'];
if (value.toInt() >= 0 && value.toInt() < months.length) {
return Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
months[value.toInt()],
style: const TextStyle(
fontSize: 10,
color: UnionFlowColors.textTertiary,
),
),
);
}
return const Text('');
},
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 40,
getTitlesWidget: (value, meta) {
return Text(
'${(value / 1000).toStringAsFixed(0)}K',
style: const TextStyle(
fontSize: 10,
color: UnionFlowColors.textTertiary,
),
);
},
),
),
),
borderData: FlBorderData(show: false),
minX: 0,
maxX: _calculateSafeMaxX(),
minY: 0,
maxY: _calculateSafeMaxY(),
lineBarsData: [
LineChartBarData(
spots: spots,
isCurved: true,
color: effectiveLineColor,
barWidth: 3,
isStrokeCapRound: true,
dotData: FlDotData(
show: true,
getDotPainter: (spot, percent, barData, index) {
return FlDotCirclePainter(
radius: 4,
color: UnionFlowColors.surface,
strokeWidth: 2,
strokeColor: effectiveLineColor,
);
},
),
belowBarData: BarAreaData(
show: true,
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
effectiveGradientStart,
effectiveGradientEnd,
],
),
),
),
],
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,213 @@
import 'package:flutter/material.dart';
import '../tokens/unionflow_colors.dart';
/// Badge de notification avec compteur
class UnionNotificationBadge extends StatelessWidget {
final int count;
final Widget child;
final Color? badgeColor;
final bool showZero;
const UnionNotificationBadge({
super.key,
required this.count,
required this.child,
this.badgeColor,
this.showZero = false,
});
@override
Widget build(BuildContext context) {
final shouldShow = count > 0 || showZero;
return Stack(
clipBehavior: Clip.none,
children: [
child,
if (shouldShow)
Positioned(
right: -6,
top: -6,
child: AnimatedScale(
duration: const Duration(milliseconds: 300),
scale: 1.0,
curve: Curves.elasticOut,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: badgeColor ?? UnionFlowColors.error,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: UnionFlowColors.surface,
width: 2,
),
boxShadow: [
BoxShadow(
color: (badgeColor ?? UnionFlowColors.error)
.withOpacity(0.4),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
constraints: const BoxConstraints(
minWidth: 18,
minHeight: 18,
),
child: Text(
count > 99 ? '99+' : count.toString(),
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.w700,
),
textAlign: TextAlign.center,
),
),
),
),
],
);
}
}
/// Widget de notification en temps réel (toast)
class UnionNotificationToast extends StatelessWidget {
final String title;
final String message;
final IconData icon;
final Color color;
final VoidCallback? onTap;
const UnionNotificationToast({
super.key,
required this.title,
required this.message,
this.icon = Icons.notifications_active,
this.color = UnionFlowColors.info,
this.onTap,
});
static void show(
BuildContext context, {
required String title,
required String message,
IconData icon = Icons.notifications_active,
Color color = UnionFlowColors.info,
VoidCallback? onTap,
}) {
final overlay = Overlay.of(context);
late OverlayEntry entry;
entry = OverlayEntry(
builder: (context) => Positioned(
top: 60,
left: 16,
right: 16,
child: Material(
color: Colors.transparent,
child: TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
builder: (context, value, child) {
return Transform.translate(
offset: Offset(0, -20 * (1 - value)),
child: Opacity(
opacity: value,
child: child,
),
);
},
child: UnionNotificationToast(
title: title,
message: message,
icon: icon,
color: color,
onTap: () {
entry.remove();
onTap?.call();
},
),
),
),
),
);
overlay.insert(entry);
// Auto-dismiss après 4 secondes
Future.delayed(const Duration(seconds: 4), () {
entry.remove();
});
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: UnionFlowColors.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: UnionFlowColors.mediumShadow,
border: Border(
left: BorderSide(
color: color,
width: 4,
),
),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, color: color, size: 24),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: UnionFlowColors.textPrimary,
),
),
const SizedBox(height: 4),
Text(
message,
style: const TextStyle(
fontSize: 12,
color: UnionFlowColors.textSecondary,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
const SizedBox(width: 8),
Icon(
Icons.chevron_right,
size: 18,
color: UnionFlowColors.textTertiary,
),
],
),
),
);
}
}

View File

@@ -0,0 +1,111 @@
import 'package:flutter/material.dart';
import '../tokens/unionflow_colors.dart';
/// Filtre de période pour le dashboard
enum PeriodFilter {
today('Aujourd\'hui'),
week('Cette semaine'),
month('Ce mois'),
quarter('Ce trimestre'),
year('Cette année'),
custom('Personnalisé');
final String label;
const PeriodFilter(this.label);
}
/// Widget de sélection de période
class UnionPeriodFilter extends StatelessWidget {
final PeriodFilter selectedPeriod;
final Function(PeriodFilter) onPeriodChanged;
final bool showCustom;
const UnionPeriodFilter({
super.key,
required this.selectedPeriod,
required this.onPeriodChanged,
this.showCustom = false,
});
@override
Widget build(BuildContext context) {
final periods = showCustom
? PeriodFilter.values
: PeriodFilter.values.where((p) => p != PeriodFilter.custom).toList();
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: UnionFlowColors.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: UnionFlowColors.softShadow,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(
Icons.calendar_today,
size: 16,
color: UnionFlowColors.unionGreen,
),
SizedBox(width: 8),
Text(
'Période',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: UnionFlowColors.textPrimary,
),
),
],
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: periods.map((period) {
final isSelected = selectedPeriod == period;
return GestureDetector(
onTap: () => onPeriodChanged(period),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 8,
),
decoration: BoxDecoration(
gradient: isSelected
? UnionFlowColors.primaryGradient
: null,
color: isSelected
? null
: UnionFlowColors.surfaceVariant,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: isSelected
? UnionFlowColors.unionGreen
: UnionFlowColors.border,
width: isSelected ? 1.5 : 1,
),
),
child: Text(
period.label,
style: TextStyle(
fontSize: 12,
fontWeight: isSelected ? FontWeight.w700 : FontWeight.w600,
color: isSelected
? Colors.white
: UnionFlowColors.textSecondary,
),
),
),
);
}).toList(),
),
],
),
);
}
}

View File

@@ -0,0 +1,92 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import '../tokens/unionflow_colors.dart';
/// Graphique circulaire UnionFlow - Pour afficher des répartitions
class UnionPieChart extends StatelessWidget {
final List<PieChartSectionData> sections;
final String title;
final String? subtitle;
final double? centerSpaceRadius;
const UnionPieChart({
super.key,
required this.sections,
required this.title,
this.subtitle,
this.centerSpaceRadius,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: UnionFlowColors.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: UnionFlowColors.softShadow,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: UnionFlowColors.textPrimary,
),
),
if (subtitle != null) ...[
const SizedBox(height: 4),
Text(
subtitle!,
style: const TextStyle(
fontSize: 11,
color: UnionFlowColors.textSecondary,
),
),
],
const SizedBox(height: 20),
// Chart
SizedBox(
height: 180,
child: PieChart(
PieChartData(
sectionsSpace: 2,
centerSpaceRadius: centerSpaceRadius ?? 50,
sections: sections,
),
),
),
],
),
);
}
}
/// Helper pour créer des sections de pie chart
class UnionPieChartSection {
static PieChartSectionData create({
required double value,
required Color color,
required String title,
double radius = 50,
bool showTitle = true,
}) {
return PieChartSectionData(
color: color,
value: value,
title: showTitle ? title : '',
radius: radius,
titleStyle: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: Colors.white,
),
badgeWidget: null,
);
}
}

View File

@@ -0,0 +1,100 @@
import 'package:flutter/material.dart';
import '../tokens/unionflow_colors.dart';
/// Card de progression UnionFlow avec barre de progrès élégante
class UnionProgressCard extends StatelessWidget {
final String title;
final double progress; // 0.0 à 1.0
final String subtitle;
final Color? progressColor;
final VoidCallback? onTap;
const UnionProgressCard({
super.key,
required this.title,
required this.progress,
required this.subtitle,
this.progressColor,
this.onTap,
});
@override
Widget build(BuildContext context) {
final effectiveColor = progressColor ?? UnionFlowColors.gold;
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: UnionFlowColors.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: UnionFlowColors.softShadow,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Title
Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: UnionFlowColors.textPrimary,
),
),
const SizedBox(height: 12),
// Progress bar
Stack(
children: [
// Background track
Container(
height: 14,
decoration: BoxDecoration(
color: UnionFlowColors.border,
borderRadius: BorderRadius.circular(20),
),
),
// Progress fill avec gradient
FractionallySizedBox(
widthFactor: progress.clamp(0.0, 1.0),
child: Container(
height: 14,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
effectiveColor,
effectiveColor.withOpacity(0.8),
],
),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: effectiveColor.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
),
),
],
),
const SizedBox(height: 8),
// Subtitle
Text(
subtitle,
style: const TextStyle(
fontSize: 12,
color: UnionFlowColors.textSecondary,
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,105 @@
import 'package:flutter/material.dart';
import '../tokens/unionflow_colors.dart';
/// Widget de statistique compacte avec icône et tendance
class UnionStatWidget extends StatelessWidget {
final String label;
final String value;
final IconData icon;
final Color color;
final String? trend;
final bool? isTrendUp;
const UnionStatWidget({
super.key,
required this.label,
required this.value,
required this.icon,
required this.color,
this.trend,
this.isTrendUp,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: UnionFlowColors.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: UnionFlowColors.softShadow,
border: Border(
left: BorderSide(
color: color,
width: 4,
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Icon
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, size: 20, color: color),
),
const SizedBox(height: 12),
// Value
Text(
value,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w700,
color: color,
),
),
const SizedBox(height: 4),
// Label
Text(
label,
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: UnionFlowColors.textSecondary,
),
),
// Trend
if (trend != null) ...[
const SizedBox(height: 8),
Row(
children: [
Icon(
isTrendUp == true
? Icons.trending_up
: Icons.trending_down,
size: 14,
color: isTrendUp == true
? UnionFlowColors.success
: UnionFlowColors.error,
),
const SizedBox(width: 4),
Text(
trend!,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: isTrendUp == true
? UnionFlowColors.success
: UnionFlowColors.error,
),
),
],
),
],
],
),
);
}
}

View File

@@ -0,0 +1,199 @@
import 'package:flutter/material.dart';
import '../tokens/unionflow_colors.dart';
/// Tuile de transaction UnionFlow
class UnionTransactionTile extends StatelessWidget {
final String name;
final String amount;
final String status;
final String? date;
final VoidCallback? onTap;
const UnionTransactionTile({
super.key,
required this.name,
required this.amount,
required this.status,
this.date,
this.onTap,
});
Color _getStatusColor() {
switch (status.toLowerCase()) {
case 'confirmé':
case 'confirmed':
return UnionFlowColors.success;
case 'en attente':
case 'pending':
return UnionFlowColors.warning;
case 'échoué':
case 'failed':
return UnionFlowColors.error;
default:
return UnionFlowColors.textSecondary;
}
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(
color: UnionFlowColors.border,
width: 1,
),
),
),
child: Row(
children: [
// Avatar avec initiale
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
gradient: UnionFlowColors.primaryGradient,
shape: BoxShape.circle,
),
alignment: Alignment.center,
child: Text(
name.isNotEmpty ? name[0].toUpperCase() : '?',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w700,
fontSize: 16,
),
),
),
const SizedBox(width: 12),
// Nom et date
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: UnionFlowColors.textPrimary,
),
),
if (date != null) ...[
const SizedBox(height: 2),
Text(
date!,
style: const TextStyle(
fontSize: 11,
color: UnionFlowColors.textTertiary,
),
),
],
],
),
),
// Montant et status
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
amount,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: UnionFlowColors.unionGreen,
),
),
const SizedBox(height: 2),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: _getStatusColor().withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
status,
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
color: _getStatusColor(),
),
),
),
],
),
],
),
),
);
}
}
/// Liste de transactions dans une card
class UnionTransactionCard extends StatelessWidget {
final String title;
final List<UnionTransactionTile> transactions;
final VoidCallback? onSeeAll;
const UnionTransactionCard({
super.key,
required this.title,
required this.transactions,
this.onSeeAll,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: UnionFlowColors.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: UnionFlowColors.softShadow,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: UnionFlowColors.textPrimary,
),
),
if (onSeeAll != null)
GestureDetector(
onTap: onSeeAll,
child: const Text(
'Voir tout',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: UnionFlowColors.unionGreen,
),
),
),
],
),
const SizedBox(height: 12),
// Transactions
...transactions,
],
),
);
}
}

View File

@@ -0,0 +1,217 @@
import 'package:flutter/material.dart';
import '../tokens/unionflow_colors.dart';
/// Carte premium affichant la vue unifiée "Compte Adhérent" du membre.
class UnionUnifiedAccountCard extends StatelessWidget {
final String numeroMembre;
final String organisationNom;
final String soldeTotal;
final String capaciteEmprunt;
final String epargneBloquee;
final double engagementRate; // 0.0 to 1.0
final VoidCallback? onDetailsTap;
const UnionUnifiedAccountCard({
super.key,
required this.numeroMembre,
required this.organisationNom,
required this.soldeTotal,
required this.capaciteEmprunt,
required this.epargneBloquee,
required this.engagementRate,
this.onDetailsTap,
});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
UnionFlowColors.unionGreen,
UnionFlowColors.unionGreen.withOpacity(0.85),
],
),
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: UnionFlowColors.unionGreen.withOpacity(0.35),
offset: const Offset(0, 10),
blurRadius: 20,
),
],
),
child: Stack(
children: [
// Pattern décoratif subtil (fond)
Positioned(
right: -20,
bottom: -20,
child: Icon(
Icons.account_balance_wallet,
size: 150,
color: Colors.white.withOpacity(0.1),
),
),
Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// En-tête : Organisation + Numéro
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
organisationNom.toUpperCase(),
style: const TextStyle(
color: Colors.white70,
fontSize: 10,
fontWeight: FontWeight.w700,
letterSpacing: 1.2,
),
),
const SizedBox(height: 4),
Text(
'COMPTE ADHÉRENT',
style: TextStyle(
color: Colors.white.withOpacity(0.95),
fontSize: 14,
fontWeight: FontWeight.w800,
),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.white30),
),
child: Text(
numeroMembre,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w700,
fontFamily: 'Courier', // Look like a card number
),
),
),
],
),
const SizedBox(height: 32),
// Solde Total Disponible
const Text(
'Solde Total Disponible',
style: TextStyle(
color: Colors.white70,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
FittedBox(
fit: BoxFit.scaleDown,
child: Text(
soldeTotal,
style: const TextStyle(
color: Colors.white,
fontSize: 36,
fontWeight: FontWeight.w800,
letterSpacing: -0.5,
),
),
),
const SizedBox(height: 32),
// Grille de détails
Row(
children: [
_buildSubStat('Capacité Emprunt', capaciteEmprunt, Icons.rocket_launch, UnionFlowColors.gold),
const SizedBox(width: 16),
_buildSubStat('Épargne Bloquée', epargneBloquee, Icons.lock_clock, Colors.white60),
],
),
const SizedBox(height: 20),
// Barre d'engagement
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Taux d\'engagement (Cotisations)',
style: TextStyle(color: Colors.white70, fontSize: 10, fontWeight: FontWeight.w600),
),
Text(
'${(engagementRate * 100).toInt()}%',
style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.w800),
),
],
),
const SizedBox(height: 8),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: engagementRate,
backgroundColor: Colors.white10,
valueColor: const AlwaysStoppedAnimation<Color>(UnionFlowColors.gold),
minHeight: 6,
),
),
],
),
],
),
),
],
),
);
}
Widget _buildSubStat(String label, String value, IconData icon, Color iconColor) {
return Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, size: 12, color: iconColor),
const SizedBox(width: 4),
Text(
label,
style: const TextStyle(color: Colors.white70, fontSize: 10, fontWeight: FontWeight.w600),
),
],
),
const SizedBox(height: 4),
Text(
value,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w700,
),
),
],
),
);
}
}