Files
unionflow-client-quarkus-pr…/unionflow-mobile-apps/lib/features/members/presentation/widgets/stats_overview_card.dart
DahoudG f89f6167cc feat(mobile): Implement Keycloak WebView authentication with HTTP callback
- 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
2025-09-15 01:44:16 +00:00

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),
),
],
),
),
),
),
],
);
}
}