- Replace flutter_appauth with custom WebView implementation to resolve deep link issues - Add KeycloakWebViewAuthService with integrated WebView for seamless authentication - Configure Android manifest for HTTP cleartext traffic support - Add network security config for development environment (192.168.1.11) - Update Keycloak client to use HTTP callback endpoint (http://192.168.1.11:8080/auth/callback) - Remove obsolete keycloak_auth_service.dart and temporary scripts - Clean up dependencies and regenerate injection configuration - Tested successfully on multiple Android devices (Xiaomi 2201116TG, SM A725F) BREAKING CHANGE: Authentication flow now uses WebView instead of external browser - Users will see Keycloak login page within the app instead of browser redirect - Resolves ERR_CLEARTEXT_NOT_PERMITTED and deep link state management issues - Maintains full OIDC compliance with PKCE flow and secure token storage Technical improvements: - WebView with custom navigation delegate for callback handling - Automatic token extraction and user info parsing from JWT - Proper error handling and user feedback - Consistent authentication state management across app lifecycle
282 lines
7.5 KiB
Dart
282 lines
7.5 KiB
Dart
import 'package:flutter/material.dart';
|
|
import '../../../../shared/theme/app_theme.dart';
|
|
import '../../../../shared/theme/design_system.dart';
|
|
|
|
/// Card de vue d'ensemble des statistiques avec design professionnel
|
|
class StatsOverviewCard extends StatefulWidget {
|
|
const StatsOverviewCard({
|
|
super.key,
|
|
required this.stats,
|
|
this.onTap,
|
|
});
|
|
|
|
final Map<String, dynamic> stats;
|
|
final VoidCallback? onTap;
|
|
|
|
@override
|
|
State<StatsOverviewCard> createState() => _StatsOverviewCardState();
|
|
}
|
|
|
|
class _StatsOverviewCardState extends State<StatsOverviewCard>
|
|
with SingleTickerProviderStateMixin {
|
|
late AnimationController _animationController;
|
|
late Animation<double> _fadeAnimation;
|
|
late Animation<Offset> _slideAnimation;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_animationController = AnimationController(
|
|
duration: DesignSystem.animationMedium,
|
|
vsync: this,
|
|
);
|
|
|
|
_fadeAnimation = Tween<double>(
|
|
begin: 0.0,
|
|
end: 1.0,
|
|
).animate(CurvedAnimation(
|
|
parent: _animationController,
|
|
curve: DesignSystem.animationCurve,
|
|
));
|
|
|
|
_slideAnimation = Tween<Offset>(
|
|
begin: const Offset(0, 0.3),
|
|
end: Offset.zero,
|
|
).animate(CurvedAnimation(
|
|
parent: _animationController,
|
|
curve: DesignSystem.animationCurveEnter,
|
|
));
|
|
|
|
_animationController.forward();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_animationController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return AnimatedBuilder(
|
|
animation: _animationController,
|
|
builder: (context, child) {
|
|
return SlideTransition(
|
|
position: _slideAnimation,
|
|
child: FadeTransition(
|
|
opacity: _fadeAnimation,
|
|
child: _buildCard(),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildCard() {
|
|
return Container(
|
|
padding: const EdgeInsets.all(DesignSystem.spacingLg),
|
|
decoration: BoxDecoration(
|
|
gradient: DesignSystem.primaryGradient,
|
|
borderRadius: BorderRadius.circular(DesignSystem.radiusLg),
|
|
boxShadow: DesignSystem.shadowCard,
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildHeader(),
|
|
const SizedBox(height: DesignSystem.spacingLg),
|
|
_buildMainStats(),
|
|
const SizedBox(height: DesignSystem.spacingLg),
|
|
_buildSecondaryStats(),
|
|
const SizedBox(height: DesignSystem.spacingMd),
|
|
_buildProgressIndicator(),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildHeader() {
|
|
return Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Vue d\'ensemble',
|
|
style: DesignSystem.titleLarge.copyWith(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
const SizedBox(height: DesignSystem.spacingXs),
|
|
Text(
|
|
'Statistiques générales',
|
|
style: DesignSystem.bodyMedium.copyWith(
|
|
color: Colors.white.withOpacity(0.9),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
Container(
|
|
padding: const EdgeInsets.all(DesignSystem.spacingSm),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withOpacity(0.2),
|
|
borderRadius: BorderRadius.circular(DesignSystem.radiusMd),
|
|
),
|
|
child: const Icon(
|
|
Icons.analytics,
|
|
color: Colors.white,
|
|
size: 24,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildMainStats() {
|
|
return Row(
|
|
children: [
|
|
Expanded(
|
|
child: _buildStatItem(
|
|
'Total Membres',
|
|
widget.stats['totalMembres'].toString(),
|
|
Icons.people,
|
|
Colors.white,
|
|
),
|
|
),
|
|
const SizedBox(width: DesignSystem.spacingLg),
|
|
Expanded(
|
|
child: _buildStatItem(
|
|
'Membres Actifs',
|
|
widget.stats['membresActifs'].toString(),
|
|
Icons.person,
|
|
Colors.white,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildSecondaryStats() {
|
|
return Row(
|
|
children: [
|
|
Expanded(
|
|
child: _buildStatItem(
|
|
'Nouveaux ce mois',
|
|
widget.stats['nouveauxCeMois'].toString(),
|
|
Icons.person_add,
|
|
Colors.white.withOpacity(0.9),
|
|
isSecondary: true,
|
|
),
|
|
),
|
|
const SizedBox(width: DesignSystem.spacingLg),
|
|
Expanded(
|
|
child: _buildStatItem(
|
|
'Taux d\'activité',
|
|
'${widget.stats['tauxActivite']}%',
|
|
Icons.trending_up,
|
|
Colors.white.withOpacity(0.9),
|
|
isSecondary: true,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildStatItem(
|
|
String label,
|
|
String value,
|
|
IconData icon,
|
|
Color color, {
|
|
bool isSecondary = false,
|
|
}) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
icon,
|
|
color: color,
|
|
size: isSecondary ? 16 : 20,
|
|
),
|
|
const SizedBox(width: DesignSystem.spacingXs),
|
|
Text(
|
|
label,
|
|
style: (isSecondary ? DesignSystem.labelMedium : DesignSystem.labelLarge).copyWith(
|
|
color: color,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: DesignSystem.spacingXs),
|
|
Text(
|
|
value,
|
|
style: (isSecondary ? DesignSystem.headlineMedium : DesignSystem.displayMedium).copyWith(
|
|
color: color,
|
|
fontWeight: FontWeight.w800,
|
|
fontSize: isSecondary ? 20 : 32,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildProgressIndicator() {
|
|
final tauxActivite = widget.stats['tauxActivite'] as int;
|
|
final progress = tauxActivite / 100.0;
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
'Engagement communautaire',
|
|
style: DesignSystem.labelMedium.copyWith(
|
|
color: Colors.white.withOpacity(0.9),
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
Text(
|
|
'$tauxActivite%',
|
|
style: DesignSystem.labelMedium.copyWith(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: DesignSystem.spacingXs),
|
|
Container(
|
|
height: 6,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withOpacity(0.2),
|
|
borderRadius: BorderRadius.circular(DesignSystem.radiusXs),
|
|
),
|
|
child: FractionallySizedBox(
|
|
alignment: Alignment.centerLeft,
|
|
widthFactor: progress,
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(DesignSystem.radiusXs),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.white.withOpacity(0.3),
|
|
blurRadius: 4,
|
|
offset: const Offset(0, 1),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|