Version propre - Dashboard enhanced
This commit is contained in:
@@ -0,0 +1,299 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user