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:
@@ -0,0 +1,84 @@
|
||||
/// Chemins des icônes (logos) des moyens de paiement pour listes déroulantes et composants.
|
||||
///
|
||||
/// Les assets sont dans [assets/images/payment_methods/{compagnie}/logo.svg] ou logo.png.
|
||||
/// Utiliser [paymentMethodIconAssetSvg] / [paymentMethodIconAssetPng] ou le widget [PaymentMethodIcon].
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
/// Base path des icônes de moyens de paiement.
|
||||
const String kPaymentMethodIconsBase = 'assets/images/payment_methods';
|
||||
|
||||
/// Retourne le chemin du logo SVG pour un code (ex: WAVE_MONEY).
|
||||
String? paymentMethodIconAssetSvg(String? code) => _pathFor(code, 'logo.svg');
|
||||
|
||||
/// Retourne le chemin du logo PNG pour un code.
|
||||
String? paymentMethodIconAssetPng(String? code) => _pathFor(code, 'logo.png');
|
||||
|
||||
/// Retourne le chemin SVG (rétrocompatibilité).
|
||||
String? paymentMethodIconAsset(String? code) => paymentMethodIconAssetSvg(code);
|
||||
|
||||
String? _pathFor(String? code, String filename) {
|
||||
if (code == null || code.isEmpty) return null;
|
||||
final path = _codeToPath[code.toUpperCase().trim()];
|
||||
if (path != null) return '$kPaymentMethodIconsBase/$path/$filename';
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Mapping code API → sous-dossier asset.
|
||||
const Map<String, String> _codeToPath = {
|
||||
'ESPECES': 'especes',
|
||||
'VIREMENT': 'virement',
|
||||
'CHEQUE': 'cheque',
|
||||
'CARTE_BANCAIRE': 'carte_bancaire',
|
||||
'WAVE_MONEY': 'wave',
|
||||
'ORANGE_MONEY': 'orange_money',
|
||||
'FREE_MONEY': 'free_money',
|
||||
'MTN_MONEY': 'mtn_money',
|
||||
'MOOV_MONEY': 'moov_money',
|
||||
'MOBILE_MONEY': 'mobile_money',
|
||||
'AUTRE': 'autre',
|
||||
};
|
||||
|
||||
/// Liste des codes de méthode de paiement ayant une icône (pour itération).
|
||||
List<String> get paymentMethodCodesWithIcon => _codeToPath.keys.toList();
|
||||
|
||||
/// Widget qui affiche le logo (PNG prioritaire, sinon SVG) pour une méthode de paiement.
|
||||
class PaymentMethodIcon extends StatelessWidget {
|
||||
final String? paymentMethodCode;
|
||||
final double width;
|
||||
final double height;
|
||||
|
||||
const PaymentMethodIcon({
|
||||
super.key,
|
||||
required this.paymentMethodCode,
|
||||
this.width = 24,
|
||||
this.height = 24,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final pngPath = paymentMethodIconAssetPng(paymentMethodCode);
|
||||
final svgPath = paymentMethodIconAssetSvg(paymentMethodCode);
|
||||
if (pngPath == null && svgPath == null) return const SizedBox.shrink();
|
||||
if (pngPath != null) {
|
||||
return Image.asset(
|
||||
pngPath,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (_, __, ___) {
|
||||
if (svgPath != null) {
|
||||
return SvgPicture.asset(svgPath, width: width, height: height);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
);
|
||||
}
|
||||
if (svgPath != null) {
|
||||
return SvgPicture.asset(svgPath, width: width, height: height);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export 'buttons/uf_primary_button.dart';
|
||||
export 'buttons/uf_secondary_button.dart';
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,246 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Design System pour les dashboards avec thème bleu roi et bleu pétrole
|
||||
class DashboardTheme {
|
||||
// === COULEURS PRINCIPALES ===
|
||||
|
||||
/// Bleu roi - Couleur principale
|
||||
static const Color royalBlue = Color(0xFF4169E1);
|
||||
|
||||
/// Bleu pétrole - Couleur secondaire
|
||||
static const Color tealBlue = Color(0xFF008B8B);
|
||||
|
||||
/// Variations du bleu roi
|
||||
static const Color royalBlueLight = Color(0xFF6495ED);
|
||||
static const Color royalBlueDark = Color(0xFF191970);
|
||||
|
||||
/// Variations du bleu pétrole
|
||||
static const Color tealBlueLight = Color(0xFF20B2AA);
|
||||
static const Color tealBlueDark = Color(0xFF006666);
|
||||
|
||||
// === COULEURS FONCTIONNELLES ===
|
||||
|
||||
/// Couleurs de statut
|
||||
static const Color success = Color(0xFF10B981);
|
||||
static const Color warning = Color(0xFFF59E0B);
|
||||
static const Color error = Color(0xFFEF4444);
|
||||
static const Color info = Color(0xFF3B82F6);
|
||||
|
||||
/// Couleurs neutres
|
||||
static const Color white = Color(0xFFFFFFFF);
|
||||
static const Color grey50 = Color(0xFFF9FAFB);
|
||||
static const Color grey100 = Color(0xFFF3F4F6);
|
||||
static const Color grey200 = Color(0xFFE5E7EB);
|
||||
static const Color grey300 = Color(0xFFD1D5DB);
|
||||
static const Color grey400 = Color(0xFF9CA3AF);
|
||||
static const Color grey500 = Color(0xFF6B7280);
|
||||
static const Color grey600 = Color(0xFF4B5563);
|
||||
static const Color grey700 = Color(0xFF374151);
|
||||
static const Color grey800 = Color(0xFF1F2937);
|
||||
static const Color grey900 = Color(0xFF111827);
|
||||
|
||||
// === GRADIENTS ===
|
||||
|
||||
/// Gradient principal (bleu roi vers bleu pétrole)
|
||||
static const LinearGradient primaryGradient = LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [royalBlue, tealBlue],
|
||||
);
|
||||
|
||||
/// Gradient léger pour les cartes
|
||||
static const LinearGradient cardGradient = LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [royalBlueLight, tealBlueLight],
|
||||
stops: [0.0, 1.0],
|
||||
);
|
||||
|
||||
/// Gradient sombre pour les headers
|
||||
static const LinearGradient headerGradient = LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [royalBlueDark, royalBlue],
|
||||
);
|
||||
|
||||
// === OMBRES ===
|
||||
|
||||
/// Ombre légère pour les cartes
|
||||
static const List<BoxShadow> cardShadow = [
|
||||
BoxShadow(
|
||||
color: Color(0x1A000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
];
|
||||
|
||||
/// Ombre plus prononcée pour les éléments flottants
|
||||
static const List<BoxShadow> elevatedShadow = [
|
||||
BoxShadow(
|
||||
color: Color(0x1F000000),
|
||||
blurRadius: 16,
|
||||
offset: Offset(0, 4),
|
||||
),
|
||||
];
|
||||
|
||||
/// Ombre subtile pour les éléments délicats
|
||||
static const List<BoxShadow> subtleShadow = [
|
||||
BoxShadow(
|
||||
color: Color(0x0A000000),
|
||||
blurRadius: 4,
|
||||
offset: Offset(0, 1),
|
||||
),
|
||||
];
|
||||
|
||||
// === BORDURES ===
|
||||
|
||||
/// Rayon de bordure standard
|
||||
static const double borderRadius = 12.0;
|
||||
static const double borderRadiusSmall = 8.0;
|
||||
static const double borderRadiusLarge = 16.0;
|
||||
|
||||
/// Bordures colorées
|
||||
static const BorderSide primaryBorder = BorderSide(
|
||||
color: royalBlue,
|
||||
width: 1.0,
|
||||
);
|
||||
|
||||
static const BorderSide secondaryBorder = BorderSide(
|
||||
color: tealBlue,
|
||||
width: 1.0,
|
||||
);
|
||||
|
||||
// === ESPACEMENTS ===
|
||||
|
||||
static const double spacing2 = 2.0;
|
||||
static const double spacing4 = 4.0;
|
||||
static const double spacing6 = 6.0;
|
||||
static const double spacing8 = 8.0;
|
||||
static const double spacing12 = 12.0;
|
||||
static const double spacing16 = 16.0;
|
||||
static const double spacing20 = 20.0;
|
||||
static const double spacing24 = 24.0;
|
||||
static const double spacing32 = 32.0;
|
||||
static const double spacing48 = 48.0;
|
||||
|
||||
// === STYLES DE TEXTE ===
|
||||
|
||||
/// Titre principal
|
||||
static const TextStyle titleLarge = TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: grey900,
|
||||
height: 1.2,
|
||||
);
|
||||
|
||||
/// Titre de section
|
||||
static const TextStyle titleMedium = TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: grey800,
|
||||
height: 1.3,
|
||||
);
|
||||
|
||||
/// Titre de carte
|
||||
static const TextStyle titleSmall = TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: grey700,
|
||||
height: 1.4,
|
||||
);
|
||||
|
||||
/// Corps de texte
|
||||
static const TextStyle bodyLarge = TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.normal,
|
||||
color: grey700,
|
||||
height: 1.5,
|
||||
);
|
||||
|
||||
/// Corps de texte moyen
|
||||
static const TextStyle bodyMedium = TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.normal,
|
||||
color: grey600,
|
||||
height: 1.4,
|
||||
);
|
||||
|
||||
/// Petit texte
|
||||
static const TextStyle bodySmall = TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.normal,
|
||||
color: grey500,
|
||||
height: 1.3,
|
||||
);
|
||||
|
||||
/// Texte de métrique (gros chiffres)
|
||||
static const TextStyle metricLarge = TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: royalBlue,
|
||||
height: 1.1,
|
||||
);
|
||||
|
||||
/// Texte de métrique moyen
|
||||
static const TextStyle metricMedium = TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: tealBlue,
|
||||
height: 1.2,
|
||||
);
|
||||
|
||||
// === STYLES DE BOUTONS ===
|
||||
|
||||
/// Style de bouton principal
|
||||
static ButtonStyle get primaryButtonStyle => ElevatedButton.styleFrom(
|
||||
backgroundColor: royalBlue,
|
||||
foregroundColor: white,
|
||||
elevation: 2,
|
||||
shadowColor: royalBlue.withOpacity(0.3),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: spacing20,
|
||||
vertical: spacing12,
|
||||
),
|
||||
);
|
||||
|
||||
/// Style de bouton secondaire
|
||||
static ButtonStyle get secondaryButtonStyle => OutlinedButton.styleFrom(
|
||||
foregroundColor: tealBlue,
|
||||
side: const BorderSide(color: tealBlue),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: spacing20,
|
||||
vertical: spacing12,
|
||||
),
|
||||
);
|
||||
|
||||
// === DÉCORATION DE CONTENEURS ===
|
||||
|
||||
/// Décoration de carte standard
|
||||
static BoxDecoration get cardDecoration => BoxDecoration(
|
||||
color: white,
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
boxShadow: cardShadow,
|
||||
);
|
||||
|
||||
/// Décoration de carte avec gradient
|
||||
static BoxDecoration get gradientCardDecoration => BoxDecoration(
|
||||
gradient: cardGradient,
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
boxShadow: cardShadow,
|
||||
);
|
||||
|
||||
/// Décoration de header
|
||||
static BoxDecoration get headerDecoration => const BoxDecoration(
|
||||
gradient: headerGradient,
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(borderRadiusLarge),
|
||||
bottomRight: Radius.circular(borderRadiusLarge),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../tokens/app_colors.dart';
|
||||
import '../tokens/app_typography.dart';
|
||||
|
||||
/// UnionFlow Mobile App - Thème Global
|
||||
/// Utilise la charte stricte (Vert/Blanc/Noir OLED) et force la petite typographie.
|
||||
class AppTheme {
|
||||
|
||||
// --- THÈME CLAIR (Mode Jour) ---
|
||||
static final ThemeData lightTheme = ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.light,
|
||||
primaryColor: AppColors.primaryGreen,
|
||||
scaffoldBackgroundColor: AppColors.lightSurface,
|
||||
colorScheme: const ColorScheme.light(
|
||||
primary: AppColors.primaryGreen,
|
||||
secondary: AppColors.brandGreenLight,
|
||||
surface: AppColors.lightBackground,
|
||||
error: AppColors.error,
|
||||
onPrimary: Colors.white,
|
||||
onSecondary: Colors.white,
|
||||
onSurface: AppColors.textPrimaryLight,
|
||||
onError: Colors.white,
|
||||
),
|
||||
|
||||
// Forcer la typographie standardisée
|
||||
textTheme: const TextTheme(
|
||||
titleMedium: AppTypography.headerSmall,
|
||||
bodyMedium: AppTypography.bodyTextSmall,
|
||||
bodySmall: AppTypography.subtitleSmall,
|
||||
labelLarge: AppTypography.actionText,
|
||||
labelSmall: AppTypography.badgeText,
|
||||
),
|
||||
|
||||
// Personnalisation des AppBar (Garder la minimaliste)
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: AppColors.lightBackground,
|
||||
foregroundColor: AppColors.textPrimaryLight,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
iconTheme: IconThemeData(color: AppColors.textPrimaryLight, size: 20),
|
||||
titleTextStyle: AppTypography.headerSmall,
|
||||
),
|
||||
|
||||
// Boutons par défaut
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
textStyle: AppTypography.actionText,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
minimumSize: const Size(64, 32),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
),
|
||||
),
|
||||
|
||||
// BottomNavigationBar ultra-compacte
|
||||
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
|
||||
backgroundColor: AppColors.lightBackground,
|
||||
selectedItemColor: AppColors.primaryGreen,
|
||||
unselectedItemColor: AppColors.textSecondaryLight,
|
||||
showSelectedLabels: false,
|
||||
showUnselectedLabels: false,
|
||||
elevation: 8,
|
||||
type: BottomNavigationBarType.fixed,
|
||||
),
|
||||
|
||||
dividerTheme: const DividerThemeData(
|
||||
color: AppColors.lightBorder,
|
||||
thickness: 1,
|
||||
space: 1,
|
||||
),
|
||||
);
|
||||
|
||||
// --- THÈME SOMBRE (Mode Nuit OLED) ---
|
||||
static final ThemeData darkTheme = ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.dark,
|
||||
primaryColor: AppColors.primaryGreen,
|
||||
scaffoldBackgroundColor: AppColors.darkBackground, // Noir OLED
|
||||
colorScheme: const ColorScheme.dark(
|
||||
primary: AppColors.primaryGreen,
|
||||
secondary: AppColors.brandGreenLight,
|
||||
surface: AppColors.darkSurface, // Gris très sombre
|
||||
error: AppColors.error,
|
||||
onPrimary: Colors.white,
|
||||
onSecondary: Colors.white,
|
||||
onSurface: AppColors.textPrimaryDark,
|
||||
onError: Colors.white,
|
||||
),
|
||||
|
||||
textTheme: const TextTheme(
|
||||
titleMedium: AppTypography.headerSmall,
|
||||
bodyMedium: AppTypography.bodyTextSmall,
|
||||
bodySmall: AppTypography.subtitleSmall,
|
||||
labelLarge: AppTypography.actionText,
|
||||
labelSmall: AppTypography.badgeText,
|
||||
).apply(
|
||||
bodyColor: AppColors.textPrimaryDark,
|
||||
displayColor: AppColors.textPrimaryDark,
|
||||
),
|
||||
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: AppColors.darkBackground,
|
||||
foregroundColor: AppColors.textPrimaryDark,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
iconTheme: IconThemeData(color: AppColors.textPrimaryDark, size: 20),
|
||||
titleTextStyle: AppTypography.headerSmall, // Remplace titleTextStyle
|
||||
),
|
||||
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
textStyle: AppTypography.actionText,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
minimumSize: const Size(64, 32),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
),
|
||||
),
|
||||
|
||||
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
|
||||
backgroundColor: AppColors.darkBackground,
|
||||
selectedItemColor: AppColors.primaryGreen,
|
||||
unselectedItemColor: AppColors.textSecondaryDark,
|
||||
showSelectedLabels: false,
|
||||
showUnselectedLabels: false,
|
||||
elevation: 8,
|
||||
type: BottomNavigationBarType.fixed,
|
||||
),
|
||||
|
||||
dividerTheme: const DividerThemeData(
|
||||
color: AppColors.darkBorder,
|
||||
thickness: 1,
|
||||
space: 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -85,6 +85,22 @@ class AppThemeSophisticated {
|
||||
);
|
||||
}
|
||||
|
||||
/// Thème sombre (suit le système ou sélection manuelle)
|
||||
static ThemeData get darkTheme {
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.dark,
|
||||
colorScheme: ColorScheme.dark(
|
||||
primary: ColorTokens.primary,
|
||||
onPrimary: Colors.white,
|
||||
surface: const Color(0xFF121212),
|
||||
onSurface: Colors.white,
|
||||
error: ColorTokens.error,
|
||||
),
|
||||
scaffoldBackgroundColor: const Color(0xFF121212),
|
||||
);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// SCHÉMA DE COULEURS
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
@@ -349,7 +365,7 @@ class AppThemeSophisticated {
|
||||
contentTextStyle: TypographyTokens.bodyMedium,
|
||||
);
|
||||
|
||||
/// Configuration des snackbars
|
||||
/// Configuration des snackbars (fixed pour éviter "Floating SnackBar off screen" avec bottomNavigationBar)
|
||||
static final SnackBarThemeData _snackBarTheme = SnackBarThemeData(
|
||||
backgroundColor: ColorTokens.onSurface,
|
||||
contentTextStyle: TypographyTokens.bodyMedium.copyWith(
|
||||
@@ -358,7 +374,7 @@ class AppThemeSophisticated {
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
behavior: SnackBarBehavior.fixed,
|
||||
);
|
||||
|
||||
/// Configuration des puces
|
||||
|
||||
@@ -1,57 +1,107 @@
|
||||
/// UnionFlow Design System - Point d'entrée unique
|
||||
///
|
||||
/// Ce fichier centralise tous les tokens et composants du Design System UnionFlow.
|
||||
/// Importer ce fichier pour accéder à tous les éléments de design.
|
||||
///
|
||||
/// Palette de couleurs: Bleu Roi (#4169E1) + Bleu Pétrole (#2C5F6F)
|
||||
/// Basé sur Material Design 3 et les tendances UI/UX 2024-2025
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// import 'package:unionflow_mobile_apps/shared/design_system/unionflow_design_system.dart';
|
||||
///
|
||||
/// // Utiliser les tokens
|
||||
/// Container(
|
||||
/// color: ColorTokens.primary,
|
||||
/// padding: EdgeInsets.all(SpacingTokens.xl),
|
||||
/// child: Text(
|
||||
/// 'UnionFlow',
|
||||
/// style: TypographyTokens.headlineMedium,
|
||||
/// ),
|
||||
/// );
|
||||
/// ```
|
||||
library unionflow_design_system;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// TOKENS - Valeurs de design fondamentales
|
||||
// IMPORTS de base pour le Design System
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Tokens de couleurs (Bleu Roi + Bleu Pétrole)
|
||||
export 'tokens/color_tokens.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'tokens/app_colors.dart';
|
||||
import 'tokens/app_typography.dart';
|
||||
import 'tokens/spacing_tokens.dart';
|
||||
|
||||
/// Tokens de typographie (Inter, SF Pro Display, JetBrains Mono)
|
||||
export 'tokens/typography_tokens.dart';
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// EXPORTS - Point d'entrée unique (DRY)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Tokens d'espacement (Grille 4px)
|
||||
export 'tokens/app_colors.dart';
|
||||
export 'tokens/app_typography.dart';
|
||||
export 'tokens/spacing_tokens.dart';
|
||||
|
||||
/// Tokens de rayons de bordure
|
||||
export 'tokens/radius_tokens.dart';
|
||||
|
||||
/// Tokens d'ombres standardisés
|
||||
export 'tokens/shadow_tokens.dart';
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// THÈME - Configuration Material Design 3
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Thème sophistiqué (Light + Dark)
|
||||
export 'theme/app_theme_sophisticated.dart';
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// COMPOSANTS - Widgets réutilisables
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Composants (boutons, cards, inputs, etc.)
|
||||
export 'theme/app_theme.dart';
|
||||
export 'components/components.dart';
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// COMPATIBILITÉ - Shims pour les anciens tokens (Migration progressive)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Shim de compatibilité pour ColorTokens
|
||||
class ColorTokens {
|
||||
static const Color primary = AppColors.primaryGreen;
|
||||
static const Color primaryContainer = AppColors.lightSurface;
|
||||
static const Color onPrimary = Colors.white;
|
||||
static const Color onPrimaryContainer = AppColors.textPrimaryLight;
|
||||
static const Color secondary = AppColors.brandGreen;
|
||||
static const Color secondaryContainer = AppColors.lightSurface;
|
||||
static const Color onSecondary = Colors.white;
|
||||
static const Color tertiary = AppColors.brandGreenLight;
|
||||
static const Color tertiaryContainer = AppColors.lightSurface;
|
||||
static const Color onTertiary = Colors.white;
|
||||
static const Color surface = AppColors.lightSurface;
|
||||
static const Color surfaceVariant = AppColors.lightSurface;
|
||||
static const Color background = AppColors.lightBackground;
|
||||
static const Color onSurface = AppColors.textPrimaryLight;
|
||||
static const Color onSurfaceVariant = AppColors.textSecondaryLight;
|
||||
static const Color outline = AppColors.lightBorder;
|
||||
static const Color outlineVariant = AppColors.lightBorder;
|
||||
static const Color error = AppColors.error;
|
||||
static const Color onError = Colors.white;
|
||||
static const Color success = AppColors.success;
|
||||
static const Color onSuccess = Colors.white;
|
||||
static const Color info = Color(0xFF2196F3);
|
||||
static const Color warning = Color(0xFFFFC107);
|
||||
static const Color shadow = Color(0x1A000000);
|
||||
|
||||
static const List<Color> primaryGradient = [
|
||||
AppColors.primaryGreen,
|
||||
AppColors.brandGreenLight,
|
||||
];
|
||||
}
|
||||
|
||||
/// Shim de compatibilité pour ShadowTokens
|
||||
class ShadowTokens {
|
||||
static const List<BoxShadow> sm = [
|
||||
BoxShadow(
|
||||
color: Color(0x1A000000),
|
||||
blurRadius: 4,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
];
|
||||
static const List<BoxShadow> md = [
|
||||
BoxShadow(
|
||||
color: Color(0x26000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 4),
|
||||
),
|
||||
];
|
||||
static const List<BoxShadow> primary = md;
|
||||
}
|
||||
|
||||
/// Shim de compatibilité pour RadiusTokens
|
||||
class RadiusTokens {
|
||||
static const double sm = SpacingTokens.radiusSm;
|
||||
static const double md = SpacingTokens.radiusMd;
|
||||
static const double lg = SpacingTokens.radiusLg;
|
||||
static const double xl = SpacingTokens.radiusXl;
|
||||
static const double circular = SpacingTokens.radiusCircular;
|
||||
static const double round = SpacingTokens.radiusCircular; // Ajouté pour compatibilité
|
||||
}
|
||||
|
||||
/// Shim de compatibilité pour TypographyTokens
|
||||
class TypographyTokens {
|
||||
static const TextStyle displayLarge = AppTypography.headerSmall;
|
||||
static const TextStyle displayMedium = AppTypography.headerSmall;
|
||||
static const TextStyle displaySmall = AppTypography.headerSmall;
|
||||
static const TextStyle headlineLarge = AppTypography.headerSmall;
|
||||
static const TextStyle headlineMedium = AppTypography.headerSmall;
|
||||
static const TextStyle headlineSmall = AppTypography.headerSmall;
|
||||
static const TextStyle titleLarge = AppTypography.headerSmall;
|
||||
static const TextStyle titleMedium = AppTypography.headerSmall;
|
||||
static const TextStyle titleSmall = AppTypography.headerSmall;
|
||||
static const TextStyle bodyLarge = AppTypography.bodyTextSmall;
|
||||
static const TextStyle bodyMedium = AppTypography.bodyTextSmall;
|
||||
static const TextStyle bodySmall = AppTypography.subtitleSmall;
|
||||
static const TextStyle labelLarge = AppTypography.actionText;
|
||||
static const TextStyle labelMedium = AppTypography.badgeText;
|
||||
static const TextStyle labelSmall = AppTypography.badgeText;
|
||||
static const TextStyle buttonLarge = AppTypography.actionText;
|
||||
static const TextStyle cardValue = AppTypography.headerSmall;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/// UnionFlow Design System V2 - Design Signature Original
|
||||
/// Export centralisé de tous les composants et tokens du nouveau design system
|
||||
library unionflow_design_v2;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// TOKENS
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
export 'tokens/unionflow_colors.dart';
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// COMPOSANTS SIGNATURE
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
export 'components/union_balance_card.dart';
|
||||
export 'components/union_progress_card.dart';
|
||||
export 'components/union_action_button.dart';
|
||||
export 'components/union_transaction_tile.dart';
|
||||
export 'components/union_line_chart.dart';
|
||||
export 'components/union_pie_chart.dart';
|
||||
export 'components/union_stat_widget.dart';
|
||||
export 'components/animated_fade_in.dart';
|
||||
export 'components/animated_slide_in.dart';
|
||||
export 'components/union_glass_card.dart';
|
||||
export 'components/african_pattern_background.dart';
|
||||
export 'components/union_unified_account_card.dart';
|
||||
export 'components/union_period_filter.dart';
|
||||
export 'components/union_export_button.dart';
|
||||
export 'components/union_notification_badge.dart';
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../design_system/tokens/app_colors.dart';
|
||||
import '../design_system/tokens/app_typography.dart';
|
||||
|
||||
/// UnionFlow Mobile - Composant DRY : ActionRow
|
||||
/// Centralise les interactions (J'aime, Commenter, Partager, etc.) sous une barre compacte.
|
||||
class ActionRow extends StatelessWidget {
|
||||
final int? likesCount;
|
||||
final int? commentsCount;
|
||||
final VoidCallback? onLike;
|
||||
final VoidCallback? onComment;
|
||||
final VoidCallback? onShare;
|
||||
final bool isLiked; // Permet de teinter l'icône Like
|
||||
|
||||
// Peut être personnalisé pour des actions spécifiques (ex: Payer)
|
||||
final String? customActionLabel;
|
||||
final VoidCallback? onCustomAction;
|
||||
final IconData? customActionIcon;
|
||||
|
||||
const ActionRow({
|
||||
Key? key,
|
||||
this.likesCount,
|
||||
this.commentsCount,
|
||||
this.onLike,
|
||||
this.onComment,
|
||||
this.onShare,
|
||||
this.isLiked = false,
|
||||
this.customActionLabel,
|
||||
this.onCustomAction,
|
||||
this.customActionIcon,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final iconColor = isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// Actions standards (Like/Comment/Share)
|
||||
Row(
|
||||
children: [
|
||||
if (onLike != null)
|
||||
_buildActionIcon(
|
||||
icon: isLiked ? Icons.favorite : Icons.favorite_border,
|
||||
color: isLiked ? AppColors.error : iconColor,
|
||||
count: likesCount,
|
||||
onTap: onLike!,
|
||||
),
|
||||
if (onLike != null && onComment != null) const SizedBox(width: 24),
|
||||
if (onComment != null)
|
||||
_buildActionIcon(
|
||||
icon: Icons.chat_bubble_outline,
|
||||
color: iconColor,
|
||||
count: commentsCount,
|
||||
onTap: onComment!,
|
||||
),
|
||||
if (onComment != null && onShare != null) const SizedBox(width: 24),
|
||||
if (onShare != null)
|
||||
_buildActionIcon(
|
||||
icon: Icons.share_outlined,
|
||||
color: iconColor,
|
||||
onTap: onShare!,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Action personnalisée à droite (ex: Payer la cotisation)
|
||||
if (onCustomAction != null)
|
||||
GestureDetector(
|
||||
onTap: onCustomAction,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primaryGreen.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
if (customActionIcon != null) ...[
|
||||
Icon(customActionIcon, size: 14, color: AppColors.primaryGreen),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
Text(
|
||||
customActionLabel ?? '',
|
||||
style: AppTypography.badgeText.copyWith(color: AppColors.primaryGreen),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionIcon({
|
||||
required IconData icon,
|
||||
required Color color,
|
||||
required VoidCallback onTap,
|
||||
int? count,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 16, color: color),
|
||||
if (count != null && count > 0) ...[
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
count.toString(),
|
||||
style: AppTypography.subtitleSmall.copyWith(color: color),
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -106,7 +106,7 @@ class ConfirmationDialog extends StatelessWidget {
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context, false);
|
||||
onCancel?.call();
|
||||
},
|
||||
child: Text(
|
||||
@@ -119,7 +119,7 @@ class ConfirmationDialog extends StatelessWidget {
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context, true);
|
||||
onConfirm?.call();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../design_system/tokens/app_colors.dart';
|
||||
|
||||
/// UnionFlow Mobile - Composant DRY Centralisé : CoreCard
|
||||
/// Le seul et unique conteneur d'affichage (Posts, Événements, Profils).
|
||||
/// Design : Minimaliste Premium, Bordures ultra-fines, Ombre invisible mais présente.
|
||||
class CoreCard extends StatelessWidget {
|
||||
final Widget child;
|
||||
final EdgeInsetsGeometry padding;
|
||||
final EdgeInsetsGeometry margin;
|
||||
final VoidCallback? onTap;
|
||||
final Color? backgroundColor;
|
||||
|
||||
const CoreCard({
|
||||
Key? key,
|
||||
required this.child,
|
||||
this.padding = const EdgeInsets.all(12.0),
|
||||
this.margin = const EdgeInsets.only(bottom: 10.0),
|
||||
this.onTap,
|
||||
this.backgroundColor,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
margin: margin,
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor ?? (isDark ? const Color(0xFF1A1A1A) : Colors.white),
|
||||
borderRadius: BorderRadius.circular(6.0),
|
||||
border: Border.all(
|
||||
color: isDark ? AppColors.darkBorder.withOpacity(0.5) : AppColors.lightBorder,
|
||||
width: 0.4,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(isDark ? 0.3 : 0.03),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(6.0),
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
import '../design_system/tokens/app_colors.dart';
|
||||
import 'core_card.dart';
|
||||
|
||||
/// UnionFlow Mobile - Composant DRY : CoreShimmer
|
||||
/// Utilise `shimmer` package pour générer des loaders élégants sans textes.
|
||||
class CoreShimmer extends StatelessWidget {
|
||||
final int itemCount;
|
||||
|
||||
const CoreShimmer({
|
||||
Key? key,
|
||||
this.itemCount = 5,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final baseColor = isDark ? Colors.grey[800]! : Colors.grey[300]!;
|
||||
final highlightColor = isDark ? Colors.grey[700]! : Colors.grey[100]!;
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: itemCount,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (_, __) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Shimmer.fromColors(
|
||||
baseColor: baseColor,
|
||||
highlightColor: highlightColor,
|
||||
child: CoreCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
CircleAvatar(radius: 16, backgroundColor: Colors.white),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(width: 100, height: 10, color: Colors.white),
|
||||
SizedBox(height: 4),
|
||||
Container(width: 40, height: 8, color: Colors.white),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
Container(width: double.infinity, height: 10, color: Colors.white),
|
||||
SizedBox(height: 4),
|
||||
Container(width: 250, height: 10, color: Colors.white),
|
||||
SizedBox(height: 4),
|
||||
Container(width: 150, height: 10, color: Colors.white),
|
||||
SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(width: 40, height: 12, color: Colors.white),
|
||||
Container(width: 40, height: 12, color: Colors.white),
|
||||
Container(width: 40, height: 12, color: Colors.white),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../design_system/tokens/app_colors.dart';
|
||||
import '../design_system/tokens/app_typography.dart';
|
||||
|
||||
/// UnionFlow Mobile - Composant DRY : CoreTextField
|
||||
/// Champ de texte minimaliste, fin, sans bordures massives.
|
||||
class CoreTextField extends StatelessWidget {
|
||||
final String hintText;
|
||||
final IconData? prefixIcon;
|
||||
final bool obscureText;
|
||||
final TextEditingController? controller;
|
||||
final TextInputType keyboardType;
|
||||
final String? errorText;
|
||||
|
||||
const CoreTextField({
|
||||
Key? key,
|
||||
required this.hintText,
|
||||
this.prefixIcon,
|
||||
this.obscureText = false,
|
||||
this.controller,
|
||||
this.keyboardType = TextInputType.text,
|
||||
this.errorText,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextField(
|
||||
controller: controller,
|
||||
obscureText: obscureText,
|
||||
keyboardType: keyboardType,
|
||||
style: AppTypography.actionText, // Texte d'entrée assez lisible
|
||||
decoration: InputDecoration(
|
||||
hintText: hintText,
|
||||
hintStyle: AppTypography.subtitleSmall.copyWith(
|
||||
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight,
|
||||
),
|
||||
prefixIcon: prefixIcon != null
|
||||
? Icon(prefixIcon, size: 20, color: AppColors.primaryGreen)
|
||||
: null,
|
||||
filled: true,
|
||||
fillColor: isDark ? AppColors.darkSurface : AppColors.lightSurface,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(
|
||||
color: isDark ? AppColors.darkBorder : AppColors.lightBorder,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(
|
||||
color: isDark ? AppColors.darkBorder : AppColors.lightBorder,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(
|
||||
color: AppColors.primaryGreen,
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
errorText: errorText,
|
||||
errorStyle: AppTypography.badgeText.copyWith(color: AppColors.error),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../design_system/tokens/app_colors.dart';
|
||||
import '../design_system/tokens/app_typography.dart';
|
||||
|
||||
/// UnionFlow Mobile - Composant DRY : DynamicFAB
|
||||
/// Bouton Flottant "Twitter Style" paramétrable pour les actions principales.
|
||||
class DynamicFAB extends StatelessWidget {
|
||||
final VoidCallback onPressed;
|
||||
final IconData icon;
|
||||
final String? label; // Si null, c'est juste un bouton rond. Si texte, c'est un "extended" FAB.
|
||||
|
||||
const DynamicFAB({
|
||||
Key? key,
|
||||
required this.onPressed,
|
||||
required this.icon,
|
||||
this.label,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (label != null) {
|
||||
return FloatingActionButton.extended(
|
||||
onPressed: onPressed,
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 4,
|
||||
icon: Icon(icon, size: 20),
|
||||
label: Text(
|
||||
label!,
|
||||
style: AppTypography.actionText,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return FloatingActionButton(
|
||||
onPressed: onPressed,
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 4,
|
||||
child: Icon(icon, size: 24),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
/// Widget for displaying user-friendly error messages with retry capability
|
||||
library error_display_widget;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../core/error/failures.dart';
|
||||
|
||||
/// Error display widget that shows failures in a user-friendly way
|
||||
class ErrorDisplayWidget extends StatelessWidget {
|
||||
final Failure failure;
|
||||
final VoidCallback? onRetry;
|
||||
final bool showRetryButton;
|
||||
|
||||
const ErrorDisplayWidget({
|
||||
super.key,
|
||||
required this.failure,
|
||||
this.onRetry,
|
||||
this.showRetryButton = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Error icon
|
||||
Icon(
|
||||
_getErrorIcon(),
|
||||
size: 64,
|
||||
color: _getErrorColor(context),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Error title
|
||||
Text(
|
||||
_getErrorTitle(),
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _getErrorColor(context),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Error message
|
||||
Text(
|
||||
failure.getUserMessage(),
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
||||
// Retry button (if retryable and callback provided)
|
||||
if (showRetryButton && failure.isRetryable && onRetry != null) ...[
|
||||
const SizedBox(height: 32),
|
||||
ElevatedButton.icon(
|
||||
onPressed: onRetry,
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Réessayer'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
vertical: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Get appropriate icon for error type
|
||||
IconData _getErrorIcon() {
|
||||
if (failure is NetworkFailure) {
|
||||
return Icons.wifi_off;
|
||||
} else if (failure is UnauthorizedFailure) {
|
||||
return Icons.lock_outline;
|
||||
} else if (failure is ForbiddenFailure) {
|
||||
return Icons.block;
|
||||
} else if (failure is NotFoundFailure) {
|
||||
return Icons.search_off;
|
||||
} else if (failure is ValidationFailure) {
|
||||
return Icons.error_outline;
|
||||
} else if (failure is ServerFailure) {
|
||||
return Icons.cloud_off;
|
||||
} else {
|
||||
return Icons.warning_amber;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get appropriate color for error type
|
||||
Color _getErrorColor(BuildContext context) {
|
||||
if (failure is NetworkFailure) {
|
||||
return Colors.orange;
|
||||
} else if (failure is UnauthorizedFailure) {
|
||||
return Colors.red;
|
||||
} else if (failure is ForbiddenFailure) {
|
||||
return Colors.deepOrange;
|
||||
} else if (failure is ValidationFailure) {
|
||||
return Colors.amber;
|
||||
} else {
|
||||
return Theme.of(context).colorScheme.error;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get appropriate title for error type
|
||||
String _getErrorTitle() {
|
||||
if (failure is NetworkFailure) {
|
||||
return 'Problème de connexion';
|
||||
} else if (failure is UnauthorizedFailure) {
|
||||
return 'Session expirée';
|
||||
} else if (failure is ForbiddenFailure) {
|
||||
return 'Accès refusé';
|
||||
} else if (failure is NotFoundFailure) {
|
||||
return 'Non trouvé';
|
||||
} else if (failure is ValidationFailure) {
|
||||
return 'Données invalides';
|
||||
} else if (failure is ServerFailure) {
|
||||
return 'Erreur serveur';
|
||||
} else {
|
||||
return 'Une erreur est survenue';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compact error banner for inline display
|
||||
class ErrorBanner extends StatelessWidget {
|
||||
final Failure failure;
|
||||
final VoidCallback? onRetry;
|
||||
final VoidCallback? onDismiss;
|
||||
|
||||
const ErrorBanner({
|
||||
super.key,
|
||||
required this.failure,
|
||||
this.onRetry,
|
||||
this.onDismiss,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: _getErrorColor(context).withOpacity(0.1),
|
||||
border: Border.all(
|
||||
color: _getErrorColor(context),
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
_getErrorIcon(),
|
||||
color: _getErrorColor(context),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_getErrorTitle(),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _getErrorColor(context),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
failure.getUserMessage(),
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (failure.isRetryable && onRetry != null)
|
||||
TextButton(
|
||||
onPressed: onRetry,
|
||||
child: const Text('Réessayer'),
|
||||
),
|
||||
if (onDismiss != null)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, size: 20),
|
||||
onPressed: onDismiss,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
IconData _getErrorIcon() {
|
||||
if (failure is NetworkFailure) {
|
||||
return Icons.wifi_off;
|
||||
} else if (failure is UnauthorizedFailure) {
|
||||
return Icons.lock_outline;
|
||||
} else if (failure is ForbiddenFailure) {
|
||||
return Icons.block;
|
||||
} else if (failure is NotFoundFailure) {
|
||||
return Icons.search_off;
|
||||
} else if (failure is ValidationFailure) {
|
||||
return Icons.error_outline;
|
||||
} else if (failure is ServerFailure) {
|
||||
return Icons.cloud_off;
|
||||
} else {
|
||||
return Icons.warning_amber;
|
||||
}
|
||||
}
|
||||
|
||||
Color _getErrorColor(BuildContext context) {
|
||||
if (failure is NetworkFailure) {
|
||||
return Colors.orange;
|
||||
} else if (failure is UnauthorizedFailure) {
|
||||
return Colors.red;
|
||||
} else if (failure is ForbiddenFailure) {
|
||||
return Colors.deepOrange;
|
||||
} else if (failure is ValidationFailure) {
|
||||
return Colors.amber;
|
||||
} else {
|
||||
return Theme.of(context).colorScheme.error;
|
||||
}
|
||||
}
|
||||
|
||||
String _getErrorTitle() {
|
||||
if (failure is NetworkFailure) {
|
||||
return 'Problème de connexion';
|
||||
} else if (failure is UnauthorizedFailure) {
|
||||
return 'Session expirée';
|
||||
} else if (failure is ForbiddenFailure) {
|
||||
return 'Accès refusé';
|
||||
} else if (failure is NotFoundFailure) {
|
||||
return 'Non trouvé';
|
||||
} else if (failure is ValidationFailure) {
|
||||
return 'Données invalides';
|
||||
} else if (failure is ServerFailure) {
|
||||
return 'Erreur serveur';
|
||||
} else {
|
||||
return 'Erreur';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Show error as a SnackBar
|
||||
void showErrorSnackBar(
|
||||
BuildContext context,
|
||||
Failure failure, {
|
||||
VoidCallback? onRetry,
|
||||
}) {
|
||||
final snackBar = SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
Icon(
|
||||
failure is NetworkFailure ? Icons.wifi_off : Icons.error_outline,
|
||||
color: Colors.white,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(failure.getUserMessage()),
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: failure is NetworkFailure ? Colors.orange : Colors.red,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
action: failure.isRetryable && onRetry != null
|
||||
? SnackBarAction(
|
||||
label: 'Réessayer',
|
||||
textColor: Colors.white,
|
||||
onPressed: onRetry,
|
||||
)
|
||||
: null,
|
||||
duration: Duration(seconds: failure.isRetryable ? 6 : 4),
|
||||
);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../design_system/tokens/app_colors.dart';
|
||||
import '../design_system/tokens/app_typography.dart';
|
||||
|
||||
/// UnionFlow Mobile - Composant DRY : InfoBadge
|
||||
/// Indicateur compact pour les statuts ("Payé", "Admin", etc).
|
||||
class InfoBadge extends StatelessWidget {
|
||||
final String text;
|
||||
final Color backgroundColor;
|
||||
final Color textColor;
|
||||
final IconData? icon;
|
||||
|
||||
const InfoBadge({
|
||||
Key? key,
|
||||
required this.text,
|
||||
this.backgroundColor = AppColors.brandGreenLight,
|
||||
this.textColor = Colors.white,
|
||||
this.icon,
|
||||
}) : super(key: key);
|
||||
|
||||
// Factory methods pour les statuts courants
|
||||
factory InfoBadge.success(String text) {
|
||||
return InfoBadge(
|
||||
text: text,
|
||||
backgroundColor: AppColors.success.withOpacity(0.15),
|
||||
textColor: AppColors.success,
|
||||
icon: Icons.check_circle_outline,
|
||||
);
|
||||
}
|
||||
|
||||
factory InfoBadge.error(String text) {
|
||||
return InfoBadge(
|
||||
text: text,
|
||||
backgroundColor: AppColors.error.withOpacity(0.15),
|
||||
textColor: AppColors.error,
|
||||
icon: Icons.error_outline,
|
||||
);
|
||||
}
|
||||
|
||||
factory InfoBadge.neutral(String text) {
|
||||
return InfoBadge(
|
||||
text: text,
|
||||
backgroundColor: AppColors.info.withOpacity(0.15),
|
||||
textColor: AppColors.info,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (icon != null) ...[
|
||||
Icon(icon, size: 10, color: textColor),
|
||||
const SizedBox(width: 2),
|
||||
],
|
||||
Text(
|
||||
text,
|
||||
style: AppTypography.badgeText.copyWith(color: textColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import '../design_system/tokens/app_colors.dart';
|
||||
import '../design_system/tokens/app_typography.dart';
|
||||
|
||||
/// UnionFlow Mobile - Composant DRY : MiniAvatar
|
||||
/// Évite toute répétition de configuration d'image de profil.
|
||||
/// Formats contraints (24px, 32px max).
|
||||
class MiniAvatar extends StatelessWidget {
|
||||
final String? imageUrl;
|
||||
final String fallbackText; // Ex: "JD" pour John Doe
|
||||
final double size;
|
||||
final bool isOnline; // Ajoute une petite pastille verte
|
||||
final Color? backgroundColor;
|
||||
final Color? iconColor;
|
||||
final bool isIcon;
|
||||
|
||||
const MiniAvatar({
|
||||
Key? key,
|
||||
this.imageUrl,
|
||||
required this.fallbackText,
|
||||
this.size = 32.0,
|
||||
this.isOnline = false,
|
||||
this.backgroundColor,
|
||||
this.iconColor,
|
||||
this.isIcon = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: backgroundColor ?? AppColors.primaryGreen.withOpacity(0.1),
|
||||
border: Border.all(
|
||||
color: AppColors.lightBorder,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
child: ClipOval(
|
||||
child: isIcon
|
||||
? _buildIcon()
|
||||
: (imageUrl != null && imageUrl!.isNotEmpty
|
||||
? CachedNetworkImage(
|
||||
imageUrl: imageUrl!,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (context, url) => _buildFallback(),
|
||||
errorWidget: (context, url, error) => _buildFallback(),
|
||||
)
|
||||
: _buildFallback()),
|
||||
),
|
||||
),
|
||||
if (isOnline)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
width: size * 0.3,
|
||||
height: size * 0.3,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.success,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFallback() {
|
||||
return Center(
|
||||
child: Text(
|
||||
fallbackText.toUpperCase(),
|
||||
style: AppTypography.actionText.copyWith(
|
||||
color: iconColor ?? AppColors.primaryGreen,
|
||||
fontSize: size * 0.4,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIcon() {
|
||||
IconData iconData;
|
||||
switch (fallbackText) {
|
||||
case 'people': iconData = Icons.people; break;
|
||||
case 'event': iconData = Icons.event; break;
|
||||
case 'business': iconData = Icons.business; break;
|
||||
case 'settings': iconData = Icons.settings; break;
|
||||
default: iconData = Icons.notifications;
|
||||
}
|
||||
return Center(
|
||||
child: Icon(
|
||||
iconData,
|
||||
color: iconColor ?? AppColors.primaryGreen,
|
||||
size: size * 0.6,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
/// Reusable validated text field with consistent styling
|
||||
library validated_text_field;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// Validated text field with consistent styling and behavior
|
||||
class ValidatedTextField extends StatelessWidget {
|
||||
final TextEditingController? controller;
|
||||
final String? labelText;
|
||||
final String? hintText;
|
||||
final String? helperText;
|
||||
final String? initialValue;
|
||||
final String? Function(String?)? validator;
|
||||
final void Function(String)? onChanged;
|
||||
final void Function(String?)? onSaved;
|
||||
final TextInputType? keyboardType;
|
||||
final TextInputAction? textInputAction;
|
||||
final bool obscureText;
|
||||
final bool enabled;
|
||||
final bool readOnly;
|
||||
final int? maxLines;
|
||||
final int? minLines;
|
||||
final int? maxLength;
|
||||
final Widget? prefixIcon;
|
||||
final Widget? suffixIcon;
|
||||
final List<TextInputFormatter>? inputFormatters;
|
||||
final FocusNode? focusNode;
|
||||
final void Function()? onEditingComplete;
|
||||
final void Function(String)? onFieldSubmitted;
|
||||
final AutovalidateMode? autovalidateMode;
|
||||
final bool showCounter;
|
||||
|
||||
const ValidatedTextField({
|
||||
super.key,
|
||||
this.controller,
|
||||
this.labelText,
|
||||
this.hintText,
|
||||
this.helperText,
|
||||
this.initialValue,
|
||||
this.validator,
|
||||
this.onChanged,
|
||||
this.onSaved,
|
||||
this.keyboardType,
|
||||
this.textInputAction,
|
||||
this.obscureText = false,
|
||||
this.enabled = true,
|
||||
this.readOnly = false,
|
||||
this.maxLines = 1,
|
||||
this.minLines,
|
||||
this.maxLength,
|
||||
this.prefixIcon,
|
||||
this.suffixIcon,
|
||||
this.inputFormatters,
|
||||
this.focusNode,
|
||||
this.onEditingComplete,
|
||||
this.onFieldSubmitted,
|
||||
this.autovalidateMode,
|
||||
this.showCounter = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextFormField(
|
||||
controller: controller,
|
||||
initialValue: initialValue,
|
||||
decoration: InputDecoration(
|
||||
labelText: labelText,
|
||||
hintText: hintText,
|
||||
helperText: helperText,
|
||||
prefixIcon: prefixIcon,
|
||||
suffixIcon: suffixIcon,
|
||||
border: const OutlineInputBorder(),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.shade400,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
focusedBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Colors.blue,
|
||||
width: 2.0,
|
||||
),
|
||||
),
|
||||
errorBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Colors.red,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
focusedErrorBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Colors.red,
|
||||
width: 2.0,
|
||||
),
|
||||
),
|
||||
filled: !enabled,
|
||||
fillColor: !enabled ? Colors.grey.shade100 : null,
|
||||
counterText: showCounter ? null : '',
|
||||
),
|
||||
validator: validator,
|
||||
onChanged: onChanged,
|
||||
onSaved: onSaved,
|
||||
keyboardType: keyboardType,
|
||||
textInputAction: textInputAction,
|
||||
obscureText: obscureText,
|
||||
enabled: enabled,
|
||||
readOnly: readOnly,
|
||||
maxLines: maxLines,
|
||||
minLines: minLines,
|
||||
maxLength: maxLength,
|
||||
inputFormatters: inputFormatters,
|
||||
focusNode: focusNode,
|
||||
onEditingComplete: onEditingComplete,
|
||||
onFieldSubmitted: onFieldSubmitted,
|
||||
autovalidateMode: autovalidateMode,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Validated amount field with currency formatting
|
||||
class ValidatedAmountField extends StatelessWidget {
|
||||
final TextEditingController? controller;
|
||||
final String? labelText;
|
||||
final String? hintText;
|
||||
final String? initialValue;
|
||||
final String? Function(String?)? validator;
|
||||
final void Function(String)? onChanged;
|
||||
final void Function(String?)? onSaved;
|
||||
final bool enabled;
|
||||
final String currencySymbol;
|
||||
final FocusNode? focusNode;
|
||||
|
||||
const ValidatedAmountField({
|
||||
super.key,
|
||||
this.controller,
|
||||
this.labelText,
|
||||
this.hintText,
|
||||
this.initialValue,
|
||||
this.validator,
|
||||
this.onChanged,
|
||||
this.onSaved,
|
||||
this.enabled = true,
|
||||
this.currencySymbol = 'FCFA',
|
||||
this.focusNode,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValidatedTextField(
|
||||
controller: controller,
|
||||
initialValue: initialValue,
|
||||
labelText: labelText,
|
||||
hintText: hintText,
|
||||
helperText: 'Entrez un montant positif (max 2 décimales)',
|
||||
validator: validator,
|
||||
onChanged: onChanged,
|
||||
onSaved: onSaved,
|
||||
enabled: enabled,
|
||||
focusNode: focusNode,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
textInputAction: TextInputAction.next,
|
||||
suffixIcon: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Text(
|
||||
currencySymbol,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,2}')),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Validated dropdown field
|
||||
class ValidatedDropdownField<T> extends StatelessWidget {
|
||||
final T? value;
|
||||
final List<DropdownMenuItem<T>> items;
|
||||
final String? labelText;
|
||||
final String? hintText;
|
||||
final String? helperText;
|
||||
final String? Function(T?)? validator;
|
||||
final void Function(T?)? onChanged;
|
||||
final void Function(T?)? onSaved;
|
||||
final bool enabled;
|
||||
|
||||
const ValidatedDropdownField({
|
||||
super.key,
|
||||
this.value,
|
||||
required this.items,
|
||||
this.labelText,
|
||||
this.hintText,
|
||||
this.helperText,
|
||||
this.validator,
|
||||
this.onChanged,
|
||||
this.onSaved,
|
||||
this.enabled = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DropdownButtonFormField<T>(
|
||||
value: value,
|
||||
items: items,
|
||||
decoration: InputDecoration(
|
||||
labelText: labelText,
|
||||
hintText: hintText,
|
||||
helperText: helperText,
|
||||
border: const OutlineInputBorder(),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.shade400,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
focusedBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Colors.blue,
|
||||
width: 2.0,
|
||||
),
|
||||
),
|
||||
errorBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Colors.red,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
filled: !enabled,
|
||||
fillColor: !enabled ? Colors.grey.shade100 : null,
|
||||
),
|
||||
validator: validator,
|
||||
onChanged: enabled ? onChanged : null,
|
||||
onSaved: onSaved,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Validated date picker field
|
||||
class ValidatedDateField extends StatelessWidget {
|
||||
final DateTime? selectedDate;
|
||||
final String? labelText;
|
||||
final String? hintText;
|
||||
final String? helperText;
|
||||
final String? Function(DateTime?)? validator;
|
||||
final void Function(DateTime)? onChanged;
|
||||
final DateTime? firstDate;
|
||||
final DateTime? lastDate;
|
||||
final bool enabled;
|
||||
|
||||
const ValidatedDateField({
|
||||
super.key,
|
||||
this.selectedDate,
|
||||
this.labelText,
|
||||
this.hintText,
|
||||
this.helperText,
|
||||
this.validator,
|
||||
this.onChanged,
|
||||
this.firstDate,
|
||||
this.lastDate,
|
||||
this.enabled = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: enabled
|
||||
? () async {
|
||||
final date = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: selectedDate ?? DateTime.now(),
|
||||
firstDate: firstDate ?? DateTime(2000),
|
||||
lastDate: lastDate ?? DateTime(2100),
|
||||
);
|
||||
if (date != null && onChanged != null) {
|
||||
onChanged!(date);
|
||||
}
|
||||
}
|
||||
: null,
|
||||
child: InputDecorator(
|
||||
decoration: InputDecoration(
|
||||
labelText: labelText,
|
||||
hintText: hintText,
|
||||
helperText: helperText,
|
||||
suffixIcon: const Icon(Icons.calendar_today),
|
||||
border: const OutlineInputBorder(),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.shade400,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
focusedBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Colors.blue,
|
||||
width: 2.0,
|
||||
),
|
||||
),
|
||||
errorBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Colors.red,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
filled: !enabled,
|
||||
fillColor: !enabled ? Colors.grey.shade100 : null,
|
||||
errorText: validator != null ? validator!(selectedDate) : null,
|
||||
),
|
||||
child: Text(
|
||||
selectedDate != null
|
||||
? '${selectedDate!.day}/${selectedDate!.month}/${selectedDate!.year}'
|
||||
: hintText ?? 'Sélectionner une date',
|
||||
style: TextStyle(
|
||||
color: selectedDate != null ? Colors.black87 : Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user