Files
unionflow-server-impl-quarkus/unionflow-mobile-apps/lib/features/members/presentation/widgets/dashboard_stat_card.dart
2025-09-15 20:15:34 +00:00

300 lines
7.6 KiB
Dart

import 'package:flutter/material.dart';
import '../../../../shared/theme/app_theme.dart';
import '../../../../shared/theme/design_system.dart';
/// Card statistique professionnelle avec design basé sur le nombre d'or
class DashboardStatCard extends StatefulWidget {
const DashboardStatCard({
super.key,
required this.title,
required this.value,
required this.icon,
required this.color,
this.trend,
this.subtitle,
this.onTap,
this.isLoading = false,
});
final String title;
final String value;
final IconData icon;
final Color color;
final String? trend;
final String? subtitle;
final VoidCallback? onTap;
final bool isLoading;
@override
State<DashboardStatCard> createState() => _DashboardStatCardState();
}
class _DashboardStatCardState extends State<DashboardStatCard>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
late Animation<double> _fadeAnimation;
bool _isHovered = false;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: DesignSystem.animationMedium,
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: DesignSystem.animationCurveEnter,
));
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: DesignSystem.animationCurve,
));
_animationController.forward();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: FadeTransition(
opacity: _fadeAnimation,
child: _buildCard(context),
),
);
},
);
}
Widget _buildCard(BuildContext context) {
return MouseRegion(
onEnter: (_) => _setHovered(true),
onExit: (_) => _setHovered(false),
child: GestureDetector(
onTap: widget.onTap,
child: AnimatedContainer(
duration: DesignSystem.animationFast,
curve: DesignSystem.animationCurve,
padding: const EdgeInsets.all(DesignSystem.spacingLg),
decoration: BoxDecoration(
color: AppTheme.surfaceLight,
borderRadius: BorderRadius.circular(DesignSystem.radiusLg),
boxShadow: _isHovered ? DesignSystem.shadowCardHover : DesignSystem.shadowCard,
border: Border.all(
color: widget.color.withOpacity(0.1),
width: 1,
),
),
child: widget.isLoading ? _buildLoadingState() : _buildContent(),
),
),
);
}
Widget _buildLoadingState() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildShimmer(40, 40, isCircular: true),
if (widget.trend != null) _buildShimmer(60, 24, radius: 12),
],
),
const SizedBox(height: DesignSystem.spacingMd),
_buildShimmer(80, 32),
const SizedBox(height: DesignSystem.spacingSm),
_buildShimmer(120, 16),
if (widget.subtitle != null) ...[
const SizedBox(height: DesignSystem.spacingXs),
_buildShimmer(100, 14),
],
],
);
}
Widget _buildShimmer(double width, double height, {double? radius, bool isCircular = false}) {
return Container(
width: width,
height: height,
decoration: BoxDecoration(
color: AppTheme.textHint.withOpacity(0.1),
borderRadius: isCircular
? BorderRadius.circular(height / 2)
: BorderRadius.circular(radius ?? DesignSystem.radiusSm),
),
);
}
Widget _buildContent() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
SizedBox(height: DesignSystem.goldenHeight(DesignSystem.spacingLg)),
_buildValue(),
const SizedBox(height: DesignSystem.spacingSm),
_buildTitle(),
if (widget.subtitle != null) ...[
const SizedBox(height: DesignSystem.spacingXs),
_buildSubtitle(),
],
],
);
}
Widget _buildHeader() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildIconContainer(),
if (widget.trend != null) _buildTrendBadge(),
],
);
}
Widget _buildIconContainer() {
return Container(
width: DesignSystem.goldenWidth(32),
height: 32,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
widget.color.withOpacity(0.15),
widget.color.withOpacity(0.05),
],
),
borderRadius: BorderRadius.circular(DesignSystem.radiusMd),
border: Border.all(
color: widget.color.withOpacity(0.2),
width: 1,
),
),
child: Icon(
widget.icon,
color: widget.color,
size: 20,
),
);
}
Widget _buildTrendBadge() {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: DesignSystem.spacingSm,
vertical: DesignSystem.spacingXs,
),
decoration: BoxDecoration(
color: _getTrendColor().withOpacity(0.1),
borderRadius: BorderRadius.circular(DesignSystem.radiusXl),
border: Border.all(
color: _getTrendColor().withOpacity(0.2),
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
_getTrendIcon(),
color: _getTrendColor(),
size: 14,
),
const SizedBox(width: DesignSystem.spacing2xs),
Text(
widget.trend!,
style: DesignSystem.labelSmall.copyWith(
color: _getTrendColor(),
fontWeight: FontWeight.w600,
),
),
],
),
);
}
Widget _buildValue() {
return Text(
widget.value,
style: DesignSystem.displayMedium.copyWith(
color: widget.color,
fontWeight: FontWeight.w800,
fontSize: 28,
),
);
}
Widget _buildTitle() {
return Text(
widget.title,
style: DesignSystem.labelLarge.copyWith(
color: AppTheme.textSecondary,
fontWeight: FontWeight.w500,
),
);
}
Widget _buildSubtitle() {
return Text(
widget.subtitle!,
style: DesignSystem.labelMedium.copyWith(
color: AppTheme.textHint,
),
);
}
void _setHovered(bool hovered) {
if (mounted) {
setState(() {
_isHovered = hovered;
});
}
}
Color _getTrendColor() {
if (widget.trend == null) return AppTheme.textSecondary;
if (widget.trend!.startsWith('+')) {
return AppTheme.successColor;
} else if (widget.trend!.startsWith('-')) {
return AppTheme.errorColor;
} else {
return AppTheme.warningColor;
}
}
IconData _getTrendIcon() {
if (widget.trend == null) return Icons.trending_flat;
if (widget.trend!.startsWith('+')) {
return Icons.trending_up;
} else if (widget.trend!.startsWith('-')) {
return Icons.trending_down;
} else {
return Icons.trending_flat;
}
}
}