feat: WebSocket temps réel + Finance Workflow + corrections

- Task #6: WebSocket /ws/dashboard + Kafka events (5 topics)
  * Backend: KafkaEventProducer, KafkaEventConsumer
  * Mobile: WebSocketService (reconnection, heartbeat, typed events)
  * DashboardBloc: Auto-refresh depuis WebSocket events

- Finance Workflow: approbations + budgets (backend + mobile)
  * Backend: entities, services, resources, migrations Flyway V6
  * Mobile: features finance_workflow complète avec BLoC

- Corrections DI: interfaces IRepository partout
  * IProfileRepository, IOrganizationRepository, IMembreRepository
  * GetIt configuré avec @injectable

- Spec-Kit: constitution + templates mis à jour
  * .specify/memory/constitution.md enrichie
  * Templates agent, plan, spec, tasks, checklist

- Nettoyage: fichiers temporaires supprimés

Signed-off-by: lions dev Team
This commit is contained in:
dahoud
2026-03-15 02:12:17 +00:00
parent bbc409de9d
commit e8ad874015
635 changed files with 58160 additions and 20674 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

@@ -5,9 +5,7 @@
library uf_primary_button;
import 'package:flutter/material.dart';
import '../../tokens/color_tokens.dart';
import '../../tokens/spacing_tokens.dart';
import '../../tokens/typography_tokens.dart';
import '../../unionflow_design_system.dart';
/// Bouton primaire UnionFlow
///
@@ -38,6 +36,12 @@ class UFPrimaryButton extends StatelessWidget {
/// 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,
@@ -47,6 +51,8 @@ class UFPrimaryButton extends StatelessWidget {
this.icon,
this.isFullWidth = false,
this.height,
this.backgroundColor,
this.textColor,
});
@override
@@ -57,12 +63,12 @@ class UFPrimaryButton extends StatelessWidget {
child: ElevatedButton(
onPressed: isLoading ? null : onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: ColorTokens.primary, // Bleu roi
foregroundColor: ColorTokens.onPrimary, // Blanc
disabledBackgroundColor: ColorTokens.primary.withOpacity(0.5),
disabledForegroundColor: ColorTokens.onPrimary.withOpacity(0.7),
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: ColorTokens.shadow,
shadowColor: AppColors.darkBorder.withOpacity(0.1),
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.buttonPaddingHorizontal,
vertical: SpacingTokens.buttonPaddingVertical,
@@ -78,7 +84,7 @@ class UFPrimaryButton extends StatelessWidget {
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
ColorTokens.onPrimary,
Colors.white,
),
),
)
@@ -92,7 +98,7 @@ class UFPrimaryButton extends StatelessWidget {
],
Text(
label,
style: TypographyTokens.buttonLarge,
style: AppTypography.actionText,
),
],
),

View File

@@ -5,9 +5,7 @@
library uf_secondary_button;
import 'package:flutter/material.dart';
import '../../tokens/color_tokens.dart';
import '../../tokens/spacing_tokens.dart';
import '../../tokens/typography_tokens.dart';
import '../../unionflow_design_system.dart';
/// Bouton secondaire UnionFlow
class UFSecondaryButton extends StatelessWidget {
@@ -36,12 +34,12 @@ class UFSecondaryButton extends StatelessWidget {
child: ElevatedButton(
onPressed: isLoading ? null : onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: ColorTokens.secondary, // Indigo
foregroundColor: ColorTokens.onSecondary, // Blanc
disabledBackgroundColor: ColorTokens.secondary.withOpacity(0.5),
disabledForegroundColor: ColorTokens.onSecondary.withOpacity(0.7),
backgroundColor: AppColors.brandGreen,
foregroundColor: Colors.white,
disabledBackgroundColor: AppColors.brandGreen.withOpacity(0.5),
disabledForegroundColor: Colors.white.withOpacity(0.7),
elevation: SpacingTokens.elevationSm,
shadowColor: ColorTokens.shadow,
shadowColor: AppColors.darkBorder.withOpacity(0.1),
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.buttonPaddingHorizontal,
vertical: SpacingTokens.buttonPaddingVertical,
@@ -57,7 +55,7 @@ class UFSecondaryButton extends StatelessWidget {
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
ColorTokens.onSecondary,
Colors.white,
),
),
)
@@ -71,7 +69,7 @@ class UFSecondaryButton extends StatelessWidget {
],
Text(
label,
style: TypographyTokens.buttonLarge,
style: AppTypography.actionText,
),
],
),

View File

@@ -112,12 +112,12 @@ class UFCard extends StatelessWidget {
switch (style) {
case UFCardStyle.elevated:
return BoxDecoration(
color: color ?? ColorTokens.surface,
color: color ?? AppColors.lightSurface,
borderRadius: BorderRadius.circular(radius),
boxShadow: elevation != null
? [
BoxShadow(
color: ColorTokens.shadow,
color: AppColors.darkBorder.withOpacity(0.1),
blurRadius: elevation!,
offset: const Offset(0, 2),
),
@@ -127,17 +127,17 @@ class UFCard extends StatelessWidget {
case UFCardStyle.outlined:
return BoxDecoration(
color: color ?? ColorTokens.surface,
color: color ?? AppColors.lightSurface,
borderRadius: BorderRadius.circular(radius),
border: Border.all(
color: borderColor ?? ColorTokens.outline,
color: borderColor ?? AppColors.lightBorder,
width: borderWidth ?? 1.0,
),
);
case UFCardStyle.filled:
return BoxDecoration(
color: color ?? ColorTokens.surfaceContainer,
color: color ?? AppColors.lightSurface,
borderRadius: BorderRadius.circular(radius),
);
}

View File

@@ -4,10 +4,7 @@
library uf_info_card;
import 'package:flutter/material.dart';
import '../../tokens/color_tokens.dart';
import '../../tokens/spacing_tokens.dart';
import '../../tokens/typography_tokens.dart';
import '../../tokens/shadow_tokens.dart';
import '../../unionflow_design_system.dart';
/// Card d'information générique
///
@@ -52,13 +49,13 @@ class UFInfoCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final effectiveIconColor = iconColor ?? ColorTokens.primary;
final effectiveIconColor = iconColor ?? AppColors.primaryGreen;
final effectivePadding = padding ?? const EdgeInsets.all(SpacingTokens.xl);
return Container(
padding: effectivePadding,
decoration: BoxDecoration(
color: ColorTokens.surface,
color: Colors.white,
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
boxShadow: ShadowTokens.sm,
),
@@ -73,8 +70,8 @@ class UFInfoCard extends StatelessWidget {
Expanded(
child: Text(
title,
style: TypographyTokens.titleMedium.copyWith(
color: ColorTokens.onSurface,
style: AppTypography.headerSmall.copyWith(
color: AppColors.textPrimaryLight,
),
),
),

View File

@@ -5,8 +5,7 @@ library uf_metric_card;
import 'package:flutter/material.dart';
import '../../tokens/spacing_tokens.dart';
import '../../tokens/typography_tokens.dart';
import '../../unionflow_design_system.dart';
/// Card de métrique système
///
@@ -54,7 +53,7 @@ class UFMetricCard extends StatelessWidget {
const SizedBox(height: SpacingTokens.sm),
Text(
value,
style: TypographyTokens.labelSmall.copyWith(
style: AppTypography.badgeText.copyWith(
fontWeight: FontWeight.bold,
color: Colors.white,
),
@@ -62,7 +61,7 @@ class UFMetricCard extends StatelessWidget {
),
Text(
label,
style: TypographyTokens.labelSmall.copyWith(
style: AppTypography.badgeText.copyWith(
fontSize: 9,
color: Colors.white.withOpacity(0.8),
),

View File

@@ -5,9 +5,7 @@
library uf_stat_card;
import 'package:flutter/material.dart';
import '../../tokens/color_tokens.dart';
import '../../tokens/spacing_tokens.dart';
import '../../tokens/typography_tokens.dart';
import '../../unionflow_design_system.dart';
/// Card de statistiques UnionFlow
///
@@ -57,13 +55,13 @@ class UFStatCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final effectiveIconColor = iconColor ?? ColorTokens.primary;
final effectiveIconColor = iconColor ?? AppColors.primaryGreen;
final effectiveIconBgColor = iconBackgroundColor ??
effectiveIconColor.withOpacity(0.1);
return Card(
elevation: SpacingTokens.elevationSm,
shadowColor: ColorTokens.shadow,
shadowColor: AppColors.darkBorder.withOpacity(0.1),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
),
@@ -98,7 +96,7 @@ class UFStatCard extends StatelessWidget {
const Icon(
Icons.arrow_forward_ios,
size: 16,
color: ColorTokens.onSurfaceVariant,
color: AppColors.textSecondaryLight,
),
],
),
@@ -108,8 +106,8 @@ class UFStatCard extends StatelessWidget {
// Titre
Text(
title,
style: TypographyTokens.labelLarge.copyWith(
color: ColorTokens.onSurfaceVariant,
style: AppTypography.badgeText.copyWith(
color: AppColors.textSecondaryLight,
),
),
@@ -118,8 +116,8 @@ class UFStatCard extends StatelessWidget {
// Valeur
Text(
value,
style: TypographyTokens.cardValue.copyWith(
color: ColorTokens.onSurface,
style: AppTypography.headerSmall.copyWith(
color: AppColors.textPrimaryLight,
),
),
@@ -128,8 +126,8 @@ class UFStatCard extends StatelessWidget {
const SizedBox(height: SpacingTokens.sm),
Text(
subtitle!,
style: TypographyTokens.bodySmall.copyWith(
color: ColorTokens.onSurfaceVariant,
style: AppTypography.subtitleSmall.copyWith(
color: AppColors.textSecondaryLight,
),
),
],

View File

@@ -4,9 +4,7 @@
library uf_dropdown_tile;
import 'package:flutter/material.dart';
import '../../tokens/color_tokens.dart';
import '../../tokens/spacing_tokens.dart';
import '../../tokens/typography_tokens.dart';
import '../../unionflow_design_system.dart';
/// Tile de paramètre avec dropdown
///
@@ -50,7 +48,7 @@ class UFDropdownTile<T> extends StatelessWidget {
@override
Widget build(BuildContext context) {
final effectiveBgColor = backgroundColor ?? ColorTokens.surfaceVariant;
final effectiveBgColor = backgroundColor ?? AppColors.lightSurface;
final effectiveItemBuilder = itemBuilder ?? (item) => item.toString();
return Container(
@@ -65,18 +63,18 @@ class UFDropdownTile<T> extends StatelessWidget {
Expanded(
child: Text(
title,
style: TypographyTokens.bodyMedium.copyWith(
style: AppTypography.bodyTextSmall.copyWith(
fontWeight: FontWeight.w600,
color: ColorTokens.onSurface,
color: AppColors.textPrimaryLight,
),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: SpacingTokens.lg),
decoration: BoxDecoration(
color: ColorTokens.surface,
color: Colors.white,
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
border: Border.all(color: ColorTokens.outline),
border: Border.all(color: AppColors.lightBorder),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<T>(

View File

@@ -4,9 +4,7 @@
library uf_switch_tile;
import 'package:flutter/material.dart';
import '../../tokens/color_tokens.dart';
import '../../tokens/spacing_tokens.dart';
import '../../tokens/typography_tokens.dart';
import '../../unionflow_design_system.dart';
/// Tile de paramètre avec switch
///
@@ -46,7 +44,7 @@ class UFSwitchTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
final effectiveBgColor = backgroundColor ?? ColorTokens.surfaceVariant;
final effectiveBgColor = backgroundColor ?? AppColors.lightSurface;
return Container(
margin: const EdgeInsets.only(bottom: SpacingTokens.lg),
@@ -63,15 +61,15 @@ class UFSwitchTile extends StatelessWidget {
children: [
Text(
title,
style: TypographyTokens.bodyMedium.copyWith(
style: AppTypography.bodyTextSmall.copyWith(
fontWeight: FontWeight.w600,
color: ColorTokens.onSurface,
color: AppColors.textPrimaryLight,
),
),
Text(
subtitle,
style: TypographyTokens.bodySmall.copyWith(
color: ColorTokens.onSurfaceVariant,
style: AppTypography.subtitleSmall.copyWith(
color: AppColors.textSecondaryLight,
),
),
],
@@ -80,7 +78,7 @@ class UFSwitchTile extends StatelessWidget {
Switch(
value: value,
onChanged: onChanged,
activeColor: ColorTokens.primary,
activeColor: AppColors.primaryGreen,
),
],
),

View File

@@ -3,14 +3,20 @@ 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;
@@ -22,6 +28,7 @@ class UFAppBar extends StatelessWidget implements PreferredSizeWidget {
this.actions,
this.leading,
this.automaticallyImplyLeading = true,
this.mergeLeadingWithTitle = false,
this.bottom,
this.backgroundColor,
this.foregroundColor,
@@ -30,23 +37,59 @@ class UFAppBar extends StatelessWidget implements PreferredSizeWidget {
@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: Text(title),
backgroundColor: backgroundColor ?? ColorTokens.primary,
foregroundColor: foregroundColor ?? ColorTokens.onPrimary,
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: leading,
automaticallyImplyLeading: automaticallyImplyLeading,
leading: useMergedTitle ? null : leading,
automaticallyImplyLeading: useMergedTitle ? false : automaticallyImplyLeading,
actions: actions,
bottom: bottom,
systemOverlayStyle: const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.light, // Icônes claires sur fond bleu
statusBarBrightness: Brightness.dark, // Pour iOS
statusBarIconBrightness: Brightness.light,
statusBarBrightness: Brightness.dark,
),
centerTitle: false,
titleTextStyle: TypographyTokens.titleLarge.copyWith(
color: foregroundColor ?? ColorTokens.onPrimary,
titleTextStyle: AppTypography.headerSmall.copyWith(
color: fg,
fontWeight: FontWeight.w600,
),
);

View File

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

View File

@@ -125,7 +125,7 @@ class UFContainer extends StatelessWidget {
alignment: alignment,
constraints: constraints,
decoration: BoxDecoration(
color: gradient == null ? (color ?? ColorTokens.surface) : null,
color: gradient == null ? (color ?? AppColors.lightSurface) : null,
gradient: gradient,
borderRadius: BorderRadius.circular(borderRadius),
border: border,

View File

@@ -31,7 +31,7 @@ class UFHeader extends StatelessWidget {
padding: const EdgeInsets.all(SpacingTokens.xl),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: ColorTokens.primaryGradient,
colors: [AppColors.primaryGreen, AppColors.brandGreenLight],
),
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
boxShadow: ShadowTokens.primary,
@@ -42,12 +42,12 @@ class UFHeader extends StatelessWidget {
Container(
padding: const EdgeInsets.all(SpacingTokens.sm),
decoration: BoxDecoration(
color: ColorTokens.onPrimary.withOpacity(0.2),
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
),
child: Icon(
icon,
color: ColorTokens.onPrimary,
color: Colors.white,
size: 24,
),
),
@@ -60,16 +60,16 @@ class UFHeader extends StatelessWidget {
children: [
Text(
title,
style: TypographyTokens.titleLarge.copyWith(
color: ColorTokens.onPrimary,
style: AppTypography.headerSmall.copyWith(
color: Colors.white,
),
),
if (subtitle != null) ...[
const SizedBox(height: SpacingTokens.xs),
Text(
subtitle!,
style: TypographyTokens.bodySmall.copyWith(
color: ColorTokens.onPrimary.withOpacity(0.8),
style: AppTypography.subtitleSmall.copyWith(
color: Colors.white.withOpacity(0.8),
),
),
],
@@ -94,14 +94,14 @@ class UFHeader extends StatelessWidget {
if (onNotificationTap != null)
Container(
decoration: BoxDecoration(
color: ColorTokens.onPrimary.withOpacity(0.2),
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(SpacingTokens.radiusSm),
),
child: IconButton(
onPressed: onNotificationTap,
icon: const Icon(
Icons.notifications_outlined,
color: ColorTokens.onPrimary,
color: Colors.white,
),
),
),
@@ -110,14 +110,14 @@ class UFHeader extends StatelessWidget {
if (onSettingsTap != null)
Container(
decoration: BoxDecoration(
color: ColorTokens.onPrimary.withOpacity(0.2),
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(SpacingTokens.radiusSm),
),
child: IconButton(
onPressed: onSettingsTap,
icon: const Icon(
Icons.settings_outlined,
color: ColorTokens.onPrimary,
color: Colors.white,
),
),
),

View File

@@ -12,7 +12,7 @@ import '../unionflow_design_system.dart';
/// title: 'Membres',
/// icon: Icons.people,
/// actions: [
/// IconButton(icon: Icon(Icons.add), onPressed: () {}),
/// IconButton(icon: Icon(Icons.add), onPressed: () => Navigator.pop(context)),
/// ],
/// )
/// ```
@@ -34,7 +34,7 @@ class UFPageHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
final effectiveIconColor = iconColor ?? ColorTokens.primary;
final effectiveIconColor = iconColor ?? AppColors.primaryGreen;
return Column(
children: [
@@ -64,8 +64,8 @@ class UFPageHeader extends StatelessWidget {
Expanded(
child: Text(
title,
style: TypographyTokens.titleLarge.copyWith(
color: ColorTokens.onSurface,
style: AppTypography.headerSmall.copyWith(
color: AppColors.textPrimaryLight,
fontWeight: FontWeight.w600,
),
),
@@ -79,10 +79,10 @@ class UFPageHeader extends StatelessWidget {
// Divider optionnel
if (showDivider)
Divider(
const Divider(
height: 1,
thickness: 1,
color: ColorTokens.outline.withOpacity(0.1),
color: AppColors.lightBorder,
),
],
);
@@ -92,18 +92,6 @@ class UFPageHeader extends StatelessWidget {
/// Header de page avec statistiques
///
/// Header compact avec des métriques KPI intégrées.
///
/// Usage:
/// ```dart
/// UFPageHeaderWithStats(
/// title: 'Membres',
/// icon: Icons.people,
/// stats: [
/// UFHeaderStat(label: 'Total', value: '142'),
/// UFHeaderStat(label: 'Actifs', value: '128'),
/// ],
/// )
/// ```
class UFPageHeaderWithStats extends StatelessWidget {
final String title;
final IconData icon;
@@ -122,7 +110,7 @@ class UFPageHeaderWithStats extends StatelessWidget {
@override
Widget build(BuildContext context) {
final effectiveIconColor = iconColor ?? ColorTokens.primary;
final effectiveIconColor = iconColor ?? AppColors.primaryGreen;
return Column(
children: [
@@ -155,8 +143,8 @@ class UFPageHeaderWithStats extends StatelessWidget {
Expanded(
child: Text(
title,
style: TypographyTokens.titleLarge.copyWith(
color: ColorTokens.onSurface,
style: AppTypography.headerSmall.copyWith(
color: AppColors.textPrimaryLight,
fontWeight: FontWeight.w600,
),
),
@@ -192,37 +180,38 @@ class UFPageHeaderWithStats extends StatelessWidget {
),
// Divider
Divider(
const Divider(
height: 1,
thickness: 1,
color: ColorTokens.outline.withOpacity(0.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: (stat.color ?? ColorTokens.primary).withOpacity(0.05),
color: effectiveColor.withOpacity(0.05),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
stat.value,
style: TypographyTokens.titleMedium.copyWith(
color: stat.color ?? ColorTokens.primary,
style: AppTypography.headerSmall.copyWith(
color: effectiveColor,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: SpacingTokens.xs),
Text(
stat.label,
style: TypographyTokens.labelSmall.copyWith(
color: ColorTokens.onSurfaceVariant,
style: AppTypography.badgeText.copyWith(
color: AppColors.textSecondaryLight,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,

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,
),
),
],
),
);
}
}