300 lines
7.6 KiB
Dart
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: 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),
|
|
],
|
|
),
|
|
SizedBox(height: DesignSystem.spacingMd),
|
|
_buildShimmer(80, 32),
|
|
SizedBox(height: DesignSystem.spacingSm),
|
|
_buildShimmer(120, 16),
|
|
if (widget.subtitle != null) ...[
|
|
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(),
|
|
SizedBox(height: DesignSystem.spacingSm),
|
|
_buildTitle(),
|
|
if (widget.subtitle != null) ...[
|
|
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: 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,
|
|
),
|
|
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;
|
|
}
|
|
}
|
|
}
|