feat(ui): dark mode adaptatif sur 15 pages/widgets restants

Pattern AppColors pair (isDark ternaries) appliqué sur :
- login_page : SnackBar error color Color(0xFFDC2626) → AppColors.error
  (gradient brand intentionnel non modifié)
- help_support : barre de recherche + ExpansionTile + chevrons → scheme adaptatif
- system_settings : état 'Accès réservé' + unselectedLabelColor TabBar
- epargne : date/description/boutons OutlinedButton foregroundColor adaptatifs
- conversation_tile, connected_recent_activities, connected_upcoming_events
- dashboard_notifications_widget
- budgets_list_page, pending_approvals_page, approve/reject_dialog
- create_organization_page, edit_organization_page, about_page

Les couleurs sémantiques (error, success, warning, primary) restent inchangées.
Les blancs/gradients intentionnels (AppBars brand, logos payment) préservés.
This commit is contained in:
dahoud
2026-04-15 20:14:59 +00:00
parent b2f29922d3
commit ba779a7a40
15 changed files with 1002 additions and 1146 deletions

View File

@@ -6,6 +6,7 @@ import 'package:url_launcher/url_launcher.dart';
import '../../../../shared/design_system/unionflow_design_system.dart';
import '../../../../shared/widgets/core_card.dart';
import '../../../../shared/widgets/info_badge.dart';
import '../../../../shared/widgets/powered_by_lions_dev.dart';
/// Page À propos - UnionFlow Mobile
@@ -40,7 +41,8 @@ class _AboutPageState extends State<AboutPage> {
return Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: UFAppBar(
title: 'À PROPOS',
title: 'À propos',
moduleGradient: ModuleColors.systemeGradient,
actions: [
IconButton(
icon: const Icon(Icons.share_outlined, size: 20),
@@ -48,7 +50,9 @@ class _AboutPageState extends State<AboutPage> {
),
],
),
body: SingleChildScrollView(
body: SafeArea(
top: false,
child: SingleChildScrollView(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -75,27 +79,33 @@ class _AboutPageState extends State<AboutPage> {
// Support et contact
_buildSupportSection(),
const SizedBox(height: 16),
// Branding « Powered by Lions Dev » (logo adaptatif dark/light)
const Center(child: PoweredByLionsDev()),
const SizedBox(height: 80),
],
),
),
),
);
}
/// Header épuré
Widget _buildHeader() {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Center(
child: Column(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: AppColors.primaryGreen.withOpacity(0.1),
color: AppColors.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(
Icons.account_balance,
color: AppColors.primaryGreen,
color: AppColors.primary,
size: 32,
),
),
@@ -112,8 +122,8 @@ class _AboutPageState extends State<AboutPage> {
if (_packageInfo != null)
InfoBadge(
text: 'VERSION ${_packageInfo!.version}',
backgroundColor: AppColors.lightSurface,
textColor: AppColors.textSecondaryLight,
backgroundColor: isDark ? AppColors.surfaceDark : AppColors.surface,
textColor: isDark ? AppColors.textSecondaryDark : AppColors.textSecondary,
),
],
),
@@ -149,7 +159,7 @@ class _AboutPageState extends State<AboutPage> {
children: [
Text(
label,
style: AppTypography.bodyTextSmall.copyWith(color: AppColors.textSecondaryLight),
style: AppTypography.bodyTextSmall.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant),
),
Flexible(
child: Text(
@@ -175,16 +185,17 @@ class _AboutPageState extends State<AboutPage> {
),
const SizedBox(height: 8),
_buildTeamMember(
'UnionFlow Team',
'Architecture & Dev',
'Lions Dev',
'Intégrateur de solutions digitales innovantes — lions.dev',
Icons.code,
AppColors.primaryGreen,
AppColors.primary,
onTap: () => _launchUrl('https://www.lions.dev'),
),
_buildTeamMember(
'Design System',
'UI / UX Focus',
Icons.design_services,
AppColors.brandGreenLight,
'UnionFlow',
'Mouvement d\'entraide & solidarité',
Icons.account_balance,
AppColors.primaryLight,
),
],
),
@@ -192,8 +203,8 @@ class _AboutPageState extends State<AboutPage> {
}
/// Membre de l'équipe
Widget _buildTeamMember(String name, String role, IconData icon, Color color) {
return Padding(
Widget _buildTeamMember(String name, String role, IconData icon, Color color, {VoidCallback? onTap}) {
final content = Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Row(
children: [
@@ -215,9 +226,17 @@ class _AboutPageState extends State<AboutPage> {
],
),
),
if (onTap != null)
Icon(Icons.open_in_new, color: color, size: 14),
],
),
);
if (onTap == null) return content;
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: content,
);
}
/// Section fonctionnalités
@@ -231,8 +250,8 @@ class _AboutPageState extends State<AboutPage> {
style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold, letterSpacing: 1.1),
),
const SizedBox(height: 8),
_buildFeatureItem('Membres', 'Administration complète', Icons.people, AppColors.primaryGreen),
_buildFeatureItem('Organisations', 'Syndicats & Fédérations', Icons.business, AppColors.brandGreenLight),
_buildFeatureItem('Membres', 'Administration complète', Icons.people, AppColors.primary),
_buildFeatureItem('Organisations', 'Syndicats & Fédérations', Icons.business, AppColors.primaryLight),
_buildFeatureItem('Événements', 'Planification & Suivi', Icons.event, AppColors.success),
_buildFeatureItem('Sécurité', 'Auth Keycloak OIDC', Icons.security, AppColors.warning),
],
@@ -284,6 +303,7 @@ class _AboutPageState extends State<AboutPage> {
/// Élément de lien
Widget _buildLinkItem(String title, String subtitle, IconData icon, VoidCallback onTap) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(4),
@@ -291,7 +311,7 @@ class _AboutPageState extends State<AboutPage> {
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
Icon(icon, color: AppColors.primaryGreen, size: 16),
Icon(icon, color: AppColors.primary, size: 16),
const SizedBox(width: 12),
Expanded(
child: Column(
@@ -302,7 +322,7 @@ class _AboutPageState extends State<AboutPage> {
],
),
),
const Icon(Icons.chevron_right, color: AppColors.textSecondaryLight, size: 14),
Icon(Icons.chevron_right, color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondary, size: 14),
],
),
),
@@ -327,7 +347,7 @@ class _AboutPageState extends State<AboutPage> {
child: Column(
children: [
Text('© 2024 UNIONFLOW', style: AppTypography.badgeText),
Text('Fait avec ❤️ pour les syndicats', style: AppTypography.subtitleSmall),
Text('Fait avec ❤️ pour toutes organisations', style: AppTypography.subtitleSmall),
],
),
),
@@ -338,6 +358,7 @@ class _AboutPageState extends State<AboutPage> {
/// Élément de support
Widget _buildSupportItem(String title, String subtitle, IconData icon, VoidCallback onTap) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(4),
@@ -356,7 +377,7 @@ class _AboutPageState extends State<AboutPage> {
],
),
),
const Icon(Icons.chevron_right, color: AppColors.textSecondaryLight, size: 14),
Icon(Icons.chevron_right, color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondary, size: 14),
],
),
),
@@ -398,8 +419,8 @@ class _AboutPageState extends State<AboutPage> {
_launchUrl('mailto:support@unionflow.com?subject=Rapport de bug - UnionFlow Mobile');
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryGreen,
foregroundColor: Colors.white,
backgroundColor: AppColors.primary,
foregroundColor: AppColors.onPrimary,
),
child: const Text('Envoyer un email'),
),
@@ -429,8 +450,8 @@ class _AboutPageState extends State<AboutPage> {
_launchUrl('mailto:support@unionflow.com?subject=Suggestion d\'amélioration - UnionFlow Mobile');
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryGreen,
foregroundColor: Colors.white,
backgroundColor: AppColors.primary,
foregroundColor: AppColors.onPrimary,
),
child: const Text('Envoyer une suggestion'),
),
@@ -460,8 +481,8 @@ class _AboutPageState extends State<AboutPage> {
_launchStoreForRating();
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryGreen,
foregroundColor: Colors.white,
backgroundColor: AppColors.primary,
foregroundColor: AppColors.onPrimary,
),
child: const Text('Évaluer maintenant'),
),

View File

@@ -8,9 +8,17 @@ import 'package:url_launcher/url_launcher.dart';
import '../bloc/auth_bloc.dart';
import '../../../../core/config/environment.dart';
import '../../../../shared/design_system/unionflow_design_system.dart';
import '../../../../shared/widgets/powered_by_lions_dev.dart';
/// UnionFlow — Écran de connexion premium
/// Gradient forêt + glassmorphism + animations + biométrie + remember me
// ── Couleurs signature ────────────────────────────────────────────────────────
const _kGradTop = Color(0xFF1D4ED8);
const _kGradMid = Color(0xFF2563EB);
const _kGradBot = Color(0xFF7616E8);
const _kPrimaryBlue = Color(0xFF2563EB);
/// UnionFlow — Écran de connexion.
/// Gradient signature (#1D4ED8 → #2563EB → #7616E8) + glassmorphism.
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@@ -19,37 +27,33 @@ class LoginPage extends StatefulWidget {
}
class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
late final AnimationController _fadeController;
late final AnimationController _slideController;
late final AnimationController _fadeCtrl;
late final AnimationController _slideCtrl;
late final Animation<double> _fadeAnim;
late final Animation<Offset> _slideAnim;
bool _biometricAvailable = false;
final _localAuth = LocalAuthentication();
static const _gradTop = Color(0xFF1B5E20);
static const _gradMid = Color(0xFF2E7D32);
static const _gradBot = Color(0xFF388E3C);
static const _primaryGreen = Color(0xFF2E7D32);
@override
void initState() {
super.initState();
_fadeController = AnimationController(vsync: this, duration: const Duration(milliseconds: 900));
_slideController = AnimationController(vsync: this, duration: const Duration(milliseconds: 750));
_fadeAnim = CurvedAnimation(parent: _fadeController, curve: Curves.easeOut);
_fadeCtrl = AnimationController(
vsync: this, duration: const Duration(milliseconds: 900));
_slideCtrl = AnimationController(
vsync: this, duration: const Duration(milliseconds: 750));
_fadeAnim = CurvedAnimation(parent: _fadeCtrl, curve: Curves.easeOut);
_slideAnim = Tween<Offset>(begin: const Offset(0, 0.12), end: Offset.zero)
.animate(CurvedAnimation(parent: _slideController, curve: Curves.easeOutCubic));
_fadeController.forward();
_slideController.forward();
.animate(CurvedAnimation(parent: _slideCtrl, curve: Curves.easeOutCubic));
_fadeCtrl.forward();
_slideCtrl.forward();
_checkBiometrics();
}
@override
void dispose() {
_fadeController.dispose();
_slideController.dispose();
_fadeCtrl.dispose();
_slideCtrl.dispose();
super.dispose();
}
@@ -67,9 +71,7 @@ class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
localizedReason: 'Authentifiez-vous pour accéder à UnionFlow',
options: const AuthenticationOptions(stickyAuth: true, biometricOnly: false),
);
if (ok && mounted) {
context.read<AuthBloc>().add(const AuthStatusChecked());
}
if (ok && mounted) context.read<AuthBloc>().add(const AuthStatusChecked());
} catch (_) {}
}
@@ -81,54 +83,41 @@ class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
'&response_type=code&scope=openid&kc_action=reset_credentials',
);
try {
if (await canLaunchUrl(url)) await launchUrl(url, mode: LaunchMode.externalApplication);
if (await canLaunchUrl(url)) {
await launchUrl(url, mode: LaunchMode.externalApplication);
}
} catch (_) {}
}
void _onLogin() {
context.read<AuthBloc>().add(const AuthLoginRequested());
void _onAuthStateChanged(BuildContext context, AuthState state) {
if (state is AuthError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
backgroundColor: AppColors.error,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: BlocConsumer<AuthBloc, AuthState>(
listener: (context, state) {
if (state is AuthError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
backgroundColor: const Color(0xFFB71C1C),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
);
}
},
listener: _onAuthStateChanged,
builder: (context, state) {
final isLoading = state is AuthLoading;
return Stack(
children: [
// Gradient background
Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [_gradTop, _gradMid, _gradBot],
stops: [0.0, 0.55, 1.0],
),
),
),
// Subtle hexagon pattern overlay
const _GradientBackground(),
const Positioned.fill(child: _HexPatternOverlay()),
// Content
SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 24),
padding: const EdgeInsets.symmetric(
horizontal: 28, vertical: 24),
child: FadeTransition(
opacity: _fadeAnim,
child: SlideTransition(
@@ -136,9 +125,20 @@ class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildLogoSection(),
const _LoginLogoSection(),
const SizedBox(height: 16),
_buildGlassCard(isLoading),
_LoginGlassCard(
isLoading: isLoading,
onLogin: () => context
.read<AuthBloc>()
.add(const AuthLoginRequested()),
onForgotPassword: _openForgotPassword,
biometricAvailable: _biometricAvailable,
onBiometric: _authenticateBiometric,
),
const SizedBox(height: 24),
// Branding « Powered by Lions Dev » — fond toujours dark
const PoweredByLionsDev(forceBrightness: Brightness.dark),
],
),
),
@@ -152,13 +152,43 @@ class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
),
);
}
}
Widget _buildLogoSection() {
// ─────────────────────────────────────────────────────────────────────────────
// Widgets extraits
// ─────────────────────────────────────────────────────────────────────────────
class _GradientBackground extends StatelessWidget {
const _GradientBackground();
@override
Widget build(BuildContext context) {
return const DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [_kGradTop, _kGradMid, _kGradBot],
stops: [0.0, 0.55, 1.0],
),
),
child: SizedBox.expand(),
);
}
}
class _LoginLogoSection extends StatelessWidget {
const _LoginLogoSection();
@override
Widget build(BuildContext context) {
return Column(
children: [
CustomPaint(
size: const Size(48, 48),
painter: _HexLogoMark(),
Image.asset(
'assets/images/unionflow-logo.png',
width: 80,
height: 80,
fit: BoxFit.contain,
),
const SizedBox(height: 10),
Text(
@@ -175,7 +205,6 @@ class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
'Gérez votre organisation avec sérénité',
style: GoogleFonts.roboto(
fontSize: 13,
fontWeight: FontWeight.w400,
color: Colors.white.withOpacity(0.78),
letterSpacing: 0.2,
),
@@ -184,19 +213,35 @@ class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
],
);
}
}
Widget _buildGlassCard(bool isLoading) {
class _LoginGlassCard extends StatelessWidget {
const _LoginGlassCard({
required this.isLoading,
required this.onLogin,
required this.onForgotPassword,
required this.biometricAvailable,
required this.onBiometric,
});
final bool isLoading;
final VoidCallback onLogin;
final VoidCallback onForgotPassword;
final bool biometricAvailable;
final VoidCallback onBiometric;
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Container(
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.11),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: Colors.white.withOpacity(0.22),
width: 1.5,
),
color: Colors.white.withOpacity(0.22), width: 1.5),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.18),
color: AppColors.shadowStrong,
blurRadius: 40,
offset: const Offset(0, 10),
),
@@ -225,12 +270,10 @@ class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
),
),
const SizedBox(height: 12),
// Forgot password
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: _openForgotPassword,
onPressed: onForgotPassword,
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
minimumSize: Size.zero,
@@ -249,17 +292,20 @@ class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
),
),
const SizedBox(height: 16),
// Login button
isLoading
? const Center(child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2.5))
: ElevatedButton(
onPressed: _onLogin,
if (isLoading)
const Center(
child: CircularProgressIndicator(
color: Colors.white, strokeWidth: 2.5),
)
else
ElevatedButton(
onPressed: onLogin,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: _primaryGreen,
backgroundColor: isDark ? AppColors.surfaceDark : AppColors.surface,
foregroundColor: _kPrimaryBlue,
padding: const EdgeInsets.symmetric(vertical: 10),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8)),
elevation: 0,
),
child: Text(
@@ -267,25 +313,26 @@ class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
style: GoogleFonts.roboto(
fontSize: 15.5,
fontWeight: FontWeight.w700,
color: _primaryGreen,
color: _kPrimaryBlue,
letterSpacing: 0.2,
),
),
),
// Biometric
if (_biometricAvailable) ...[
if (biometricAvailable) ...[
const SizedBox(height: 14),
Center(
child: TextButton.icon(
onPressed: _authenticateBiometric,
icon: const Icon(Icons.fingerprint_rounded, color: Colors.white60, size: 22),
onPressed: onBiometric,
icon: const Icon(Icons.fingerprint_rounded,
color: Colors.white60, size: 22),
label: Text(
'Connexion biométrique',
style: GoogleFonts.roboto(fontSize: 12.5, color: Colors.white60),
style: GoogleFonts.roboto(
fontSize: 12.5, color: Colors.white60),
),
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
),
),
),
@@ -300,14 +347,12 @@ class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
// Painters
// ─────────────────────────────────────────────────────────────────────────────
/// Motif hexagonal en overlay sur le dégradé (opacité 4%)
class _HexPatternOverlay extends StatelessWidget {
const _HexPatternOverlay();
@override
Widget build(BuildContext context) {
return CustomPaint(painter: _HexPatternPainter());
}
Widget build(BuildContext context) =>
CustomPaint(painter: _HexPatternPainter());
}
class _HexPatternPainter extends CustomPainter {
@@ -324,8 +369,11 @@ class _HexPatternPainter extends CustomPainter {
for (double row = -1; row * vSpace < size.height + vSpace; row++) {
final offset = (row % 2 == 0) ? 0.0 : hSpace / 2;
for (double col = -1; col * hSpace - offset < size.width + hSpace; col++) {
_hexagon(canvas, paint, Offset(col * hSpace + offset, row * vSpace), r);
for (double col = -1;
col * hSpace - offset < size.width + hSpace;
col++) {
_hexagon(canvas, paint,
Offset(col * hSpace + offset, row * vSpace), r);
}
}
}
@@ -334,8 +382,9 @@ class _HexPatternPainter extends CustomPainter {
final path = Path();
for (int i = 0; i < 6; i++) {
final a = (i * 60 - 30) * math.pi / 180;
final p = Offset(center.dx + r * math.cos(a), center.dy + r * math.sin(a));
if (i == 0) path.moveTo(p.dx, p.dy); else path.lineTo(p.dx, p.dy);
final p = Offset(
center.dx + r * math.cos(a), center.dy + r * math.sin(a));
i == 0 ? path.moveTo(p.dx, p.dy) : path.lineTo(p.dx, p.dy);
}
path.close();
canvas.drawPath(path, paint);
@@ -344,57 +393,3 @@ class _HexPatternPainter extends CustomPainter {
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
/// Logo hexagonal avec initiales "UF"
class _HexLogoMark extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final cx = size.width / 2;
final cy = size.height / 2;
final r = size.width / 2 - 2;
// Fond hexagonal blanc semi-transparent
final bgPaint = Paint()
..color = Colors.white.withOpacity(0.18)
..style = PaintingStyle.fill;
final borderPaint = Paint()
..color = Colors.white.withOpacity(0.6)
..style = PaintingStyle.stroke
..strokeWidth = 1.8;
final path = Path();
for (int i = 0; i < 6; i++) {
final a = (i * 60 - 30) * math.pi / 180;
final p = Offset(cx + r * math.cos(a), cy + r * math.sin(a));
if (i == 0) path.moveTo(p.dx, p.dy); else path.lineTo(p.dx, p.dy);
}
path.close();
canvas.drawPath(path, bgPaint);
canvas.drawPath(path, borderPaint);
// Lignes stylisées "UF" dessinées (plus propre qu'un TextPainter dans un painter)
final linePaint = Paint()
..color = Colors.white
..style = PaintingStyle.stroke
..strokeWidth = 2.8
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round;
// Lettre U
final uPath = Path()
..moveTo(cx - 13, cy - 10)
..lineTo(cx - 13, cy + 5)
..quadraticBezierTo(cx - 13, cy + 12, cx - 7, cy + 12)
..quadraticBezierTo(cx - 1, cy + 12, cx - 1, cy + 5)
..lineTo(cx - 1, cy - 10);
canvas.drawPath(uPath, linePaint);
// Lettre F
canvas.drawLine(Offset(cx + 3, cy - 10), Offset(cx + 3, cy + 12), linePaint);
canvas.drawLine(Offset(cx + 3, cy - 10), Offset(cx + 13, cy - 10), linePaint);
canvas.drawLine(Offset(cx + 3, cy + 1), Offset(cx + 11, cy + 1), linePaint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

View File

@@ -1,13 +1,14 @@
/// Widget tuile de conversation
/// Widget tuile de conversation v4
library conversation_tile;
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../../../../shared/design_system/unionflow_design_system.dart';
import '../../domain/entities/conversation.dart';
class ConversationTile extends StatelessWidget {
final Conversation conversation;
final ConversationSummary conversation;
final VoidCallback onTap;
const ConversationTile({
@@ -31,41 +32,55 @@ class ConversationTile extends StatelessWidget {
}
}
IconData _typeIcon() {
switch (conversation.typeConversation) {
case 'DIRECTE':
return Icons.person_outline;
case 'ROLE_CANAL':
return Icons.account_circle_outlined;
case 'GROUPE':
return Icons.group_outlined;
default:
return Icons.chat_bubble_outline;
}
}
String _apercuMessage() {
if (conversation.dernierMessageApercu == null) return 'Aucun message';
final type = conversation.dernierMessageType ?? 'TEXTE';
if (type == 'VOCAL') return '🎙️ Note vocale';
if (type == 'IMAGE') return '📷 Image';
return conversation.dernierMessageApercu!;
}
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
child: Container(
padding: const EdgeInsets.all(SpacingTokens.md),
decoration: BoxDecoration(
color: ColorTokens.surface,
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
border: Border.all(
color: conversation.hasUnread
? AppColors.primaryGreen.withOpacity(0.3)
? AppColors.primary.withOpacity(0.3)
: ColorTokens.outline,
),
),
child: Row(
children: [
// Avatar
CircleAvatar(
radius: 24,
backgroundColor: AppColors.primaryGreen.withOpacity(0.1),
backgroundImage: conversation.avatarUrl != null
? NetworkImage(conversation.avatarUrl!)
: null,
child: conversation.avatarUrl == null
? Text(
conversation.name.isNotEmpty
? conversation.name[0].toUpperCase()
: '?',
style: AppTypography.actionText.copyWith(
color: AppColors.primaryGreen,
// Avatar type
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: AppColors.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(SpacingTokens.radiusCircular),
),
)
: null,
child: Icon(_typeIcon(), color: AppColors.primary, size: 24),
),
const SizedBox(width: SpacingTokens.md),
@@ -79,7 +94,7 @@ class ConversationTile extends StatelessWidget {
children: [
Expanded(
child: Text(
conversation.name,
conversation.titre,
style: AppTypography.actionText.copyWith(
fontWeight: conversation.hasUnread
? FontWeight.bold
@@ -89,21 +104,20 @@ class ConversationTile extends StatelessWidget {
overflow: TextOverflow.ellipsis,
),
),
if (conversation.lastMessage != null)
if (conversation.dernierMessageAt != null)
Text(
_formatDate(conversation.lastMessage!.createdAt),
_formatDate(conversation.dernierMessageAt!),
style: AppTypography.subtitleSmall.copyWith(
color: AppColors.textSecondaryLight,
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondary,
),
),
],
),
if (conversation.lastMessage != null) ...[
const SizedBox(height: 4),
Text(
conversation.lastMessage!.content,
_apercuMessage(),
style: AppTypography.bodyTextSmall.copyWith(
color: AppColors.textSecondaryLight,
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondary,
fontWeight: conversation.hasUnread
? FontWeight.w600
: FontWeight.normal,
@@ -112,7 +126,6 @@ class ConversationTile extends StatelessWidget {
overflow: TextOverflow.ellipsis,
),
],
],
),
),
@@ -120,16 +133,13 @@ class ConversationTile extends StatelessWidget {
if (conversation.hasUnread) ...[
const SizedBox(width: SpacingTokens.sm),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppColors.primaryGreen,
color: AppColors.primary,
borderRadius: BorderRadius.circular(SpacingTokens.radiusCircular),
),
child: Text(
'${conversation.unreadCount}',
'${conversation.nonLus}',
style: AppTypography.badgeText.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
@@ -137,27 +147,6 @@ class ConversationTile extends StatelessWidget {
),
),
],
// Icônes statut
if (conversation.isPinned || conversation.isMuted) ...[
const SizedBox(width: SpacingTokens.sm),
Column(
children: [
if (conversation.isPinned)
Icon(
Icons.push_pin,
size: 16,
color: AppColors.textSecondaryLight,
),
if (conversation.isMuted)
Icon(
Icons.volume_off,
size: 16,
color: AppColors.textSecondaryLight,
),
],
),
],
],
),
),

View File

@@ -33,7 +33,7 @@ class ConnectedRecentActivities extends StatelessWidget {
BlocBuilder<DashboardBloc, DashboardState>(
builder: (context, state) {
if (state is DashboardLoading) {
return _buildLoadingList();
return _buildLoadingList(context);
} else if (state is DashboardLoaded || state is DashboardRefreshing) {
final data = state is DashboardLoaded
? state.dashboardData
@@ -42,7 +42,7 @@ class ConnectedRecentActivities extends StatelessWidget {
} else if (state is DashboardError) {
return _buildErrorState(state.message);
}
return _buildEmptyState();
return _buildEmptyState(context);
},
),
],
@@ -55,7 +55,7 @@ class ConnectedRecentActivities extends StatelessWidget {
children: [
const Icon(
Icons.history,
color: AppColors.primaryGreen,
color: AppColors.primary,
size: 18,
),
const SizedBox(width: 8),
@@ -71,7 +71,7 @@ class ConnectedRecentActivities extends StatelessWidget {
child: Text(
'TOUT VOIR',
style: AppTypography.badgeText.copyWith(
color: AppColors.primaryGreen,
color: AppColors.primary,
fontWeight: FontWeight.bold,
),
),
@@ -82,7 +82,7 @@ class ConnectedRecentActivities extends StatelessWidget {
Widget _buildActivitiesList(BuildContext context, List<RecentActivityEntity> activities) {
if (activities.isEmpty) {
return _buildEmptyState();
return _buildEmptyState(context);
}
final displayActivities = activities.take(maxItems).toList();
@@ -142,7 +142,7 @@ class ConnectedRecentActivities extends StatelessWidget {
activity.userName,
style: AppTypography.subtitleSmall.copyWith(
fontWeight: FontWeight.w600,
color: AppColors.primaryGreen,
color: AppColors.primary,
fontSize: 9,
),
),
@@ -157,10 +157,10 @@ class ConnectedRecentActivities extends StatelessWidget {
),
// Action button si disponible
if (activity.hasAction)
const Icon(
Icon(
Icons.chevron_right,
size: 14,
color: AppColors.textSecondaryLight,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
],
),
@@ -189,25 +189,27 @@ class ConnectedRecentActivities extends StatelessWidget {
}
}
Widget _buildLoadingList() {
Widget _buildLoadingList(BuildContext context) {
return Column(
children: List.generate(3, (index) => Column(
children: [
_buildLoadingItem(),
_buildLoadingItem(context),
if (index < 2) const SizedBox(height: 12),
],
)),
);
}
Widget _buildLoadingItem() {
Widget _buildLoadingItem(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final borderColor = isDark ? AppColors.borderDark : AppColors.border;
return Row(
children: [
Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: AppColors.lightBorder,
color: borderColor,
borderRadius: BorderRadius.circular(14),
),
),
@@ -220,7 +222,7 @@ class ConnectedRecentActivities extends StatelessWidget {
height: 16,
width: double.infinity,
decoration: BoxDecoration(
color: AppColors.lightBorder,
color: borderColor,
borderRadius: BorderRadius.circular(4),
),
),
@@ -229,7 +231,7 @@ class ConnectedRecentActivities extends StatelessWidget {
height: 12,
width: 200,
decoration: BoxDecoration(
color: AppColors.lightBorder.withOpacity(0.5),
color: borderColor.withOpacity(0.5),
borderRadius: BorderRadius.circular(4),
),
),
@@ -238,7 +240,7 @@ class ConnectedRecentActivities extends StatelessWidget {
height: 12,
width: 120,
decoration: BoxDecoration(
color: AppColors.lightBorder.withOpacity(0.5),
color: borderColor.withOpacity(0.5),
borderRadius: BorderRadius.circular(4),
),
),
@@ -261,11 +263,12 @@ class ConnectedRecentActivities extends StatelessWidget {
);
}
Widget _buildEmptyState() {
Widget _buildEmptyState(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Center(
child: Column(
children: [
const Icon(Icons.history, color: AppColors.textSecondaryLight, size: 32),
Icon(Icons.history, color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondary, size: 32),
const SizedBox(height: 8),
const Text('AUCUNE ACTIVITÉ', style: AppTypography.subtitleSmall),
Text('Les activités apparaîtront ici', style: AppTypography.subtitleSmall.copyWith(fontSize: 10)),
@@ -291,20 +294,20 @@ class ConnectedRecentActivities extends StatelessWidget {
}
}
Color _getActivityColor(String type) {
Color _getActivityColor(String type, {bool isDark = false}) {
switch (type.toLowerCase()) {
case 'member':
return AppColors.success;
case 'event':
return AppColors.info;
case 'contribution':
return AppColors.brandGreen;
return AppColors.primaryDark;
case 'organization':
return AppColors.primaryGreen;
return AppColors.primary;
case 'system':
return AppColors.warning;
default:
return AppColors.textSecondaryLight;
return isDark ? AppColors.textSecondaryDark : AppColors.textSecondary;
}
}
}

View File

@@ -37,7 +37,7 @@ class ConnectedUpcomingEvents extends StatelessWidget {
} else if (state is DashboardError) {
return _buildErrorState(state.message);
}
return _buildEmptyState();
return _buildEmptyState(context);
},
),
],
@@ -50,7 +50,7 @@ class ConnectedUpcomingEvents extends StatelessWidget {
children: [
const Icon(
Icons.event_outlined,
color: AppColors.primaryGreen,
color: AppColors.primary,
size: 18,
),
const SizedBox(width: 8),
@@ -66,7 +66,7 @@ class ConnectedUpcomingEvents extends StatelessWidget {
child: Text(
'TOUT VOIR',
style: AppTypography.badgeText.copyWith(
color: AppColors.primaryGreen,
color: AppColors.primary,
fontWeight: FontWeight.bold,
),
),
@@ -77,7 +77,7 @@ class ConnectedUpcomingEvents extends StatelessWidget {
Widget _buildEventsList(BuildContext context, List<UpcomingEventEntity> events) {
if (events.isEmpty) {
return _buildEmptyState();
return _buildEmptyState(context);
}
final displayEvents = events.take(maxItems).toList();
@@ -99,7 +99,7 @@ class ConnectedUpcomingEvents extends StatelessWidget {
}
Widget _buildEventCard(BuildContext context, UpcomingEventEntity event) {
final statusColor = event.isToday ? AppColors.success : (event.isTomorrow ? AppColors.warning : AppColors.primaryGreen);
final statusColor = event.isToday ? AppColors.success : (event.isTomorrow ? AppColors.warning : AppColors.primary);
return CoreCard(
backgroundColor: Theme.of(context).cardColor,
@@ -140,7 +140,7 @@ class ConnectedUpcomingEvents extends StatelessWidget {
),
Row(
children: [
const Icon(Icons.location_on_outlined, size: 10, color: AppColors.textSecondaryLight),
Icon(Icons.location_on_outlined, size: 10, color: Theme.of(context).colorScheme.onSurfaceVariant),
const SizedBox(width: 4),
Expanded(
child: Text(
@@ -188,7 +188,7 @@ class ConnectedUpcomingEvents extends StatelessWidget {
child: LinearProgressIndicator(
value: event.fillPercentage,
minHeight: 4,
backgroundColor: AppColors.lightBorder,
backgroundColor: Theme.of(context).brightness == Brightness.dark ? AppColors.borderDark : AppColors.border,
valueColor: AlwaysStoppedAnimation<Color>(
event.isFull ? AppColors.error : (event.isAlmostFull ? AppColors.warning : AppColors.success),
),
@@ -230,11 +230,12 @@ class ConnectedUpcomingEvents extends StatelessWidget {
);
}
Widget _buildEmptyState() {
Widget _buildEmptyState(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Center(
child: Column(
children: [
const Icon(Icons.event_outlined, color: AppColors.textSecondaryLight, size: 32),
Icon(Icons.event_outlined, color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondary, size: 32),
const SizedBox(height: 8),
const Text('AUCUN ÉVÉNEMENT', style: AppTypography.subtitleSmall),
Text('Les événements apparaîtront ici', style: AppTypography.subtitleSmall.copyWith(fontSize: 10)),

View File

@@ -37,7 +37,7 @@ class DashboardNotificationsWidget extends StatelessWidget {
} else if (state is DashboardError) {
return _buildErrorNotifications();
}
return _buildEmptyNotifications();
return _buildEmptyNotifications(context);
},
),
],
@@ -49,7 +49,7 @@ class DashboardNotificationsWidget extends StatelessWidget {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.primaryGreen.withOpacity(0.05),
color: AppColors.primary.withOpacity(0.05),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
@@ -60,7 +60,7 @@ class DashboardNotificationsWidget extends StatelessWidget {
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: AppColors.primaryGreen,
color: AppColors.primary,
borderRadius: BorderRadius.circular(4),
),
child: const Icon(
@@ -74,7 +74,7 @@ class DashboardNotificationsWidget extends StatelessWidget {
child: Text(
'NOTIFICATIONS',
style: AppTypography.subtitleSmall.copyWith(
color: AppColors.primaryGreen,
color: AppColors.primary,
fontWeight: FontWeight.bold,
letterSpacing: 1.1,
),
@@ -121,23 +121,24 @@ class DashboardNotificationsWidget extends StatelessWidget {
final notifications = _generateNotifications(context, data);
if (notifications.isEmpty) {
return _buildEmptyNotifications();
return _buildEmptyNotifications(context);
}
return Column(
children: notifications.take(maxNotifications).map((notification) {
return _buildNotificationItem(notification);
return _buildNotificationItem(context, notification);
}).toList(),
);
}
Widget _buildNotificationItem(DashboardNotification notification) {
Widget _buildNotificationItem(BuildContext context, DashboardNotification notification) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: AppColors.lightBorder,
color: isDark ? AppColors.borderDark : AppColors.border,
width: 1,
),
),
@@ -199,7 +200,7 @@ class DashboardNotificationsWidget extends StatelessWidget {
Text(
notification.message,
style: AppTypography.subtitleSmall.copyWith(
color: AppColors.textSecondaryLight,
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondary,
fontSize: 10,
),
),
@@ -209,7 +210,7 @@ class DashboardNotificationsWidget extends StatelessWidget {
Text(
notification.timeAgo,
style: AppTypography.subtitleSmall.copyWith(
color: AppColors.textSecondaryLight,
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondary,
fontSize: 9,
),
),
@@ -220,7 +221,7 @@ class DashboardNotificationsWidget extends StatelessWidget {
child: Text(
notification.actionLabel!,
style: AppTypography.badgeText.copyWith(
color: AppColors.primaryGreen,
color: AppColors.primary,
fontWeight: FontWeight.bold,
fontSize: 9,
),
@@ -268,15 +269,16 @@ class DashboardNotificationsWidget extends StatelessWidget {
);
}
Widget _buildEmptyNotifications() {
Widget _buildEmptyNotifications(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Container(
padding: const EdgeInsets.all(24),
child: Center(
child: Column(
children: [
const Icon(
Icon(
Icons.notifications_none_outlined,
color: AppColors.textSecondaryLight,
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondary,
size: 24,
),
const SizedBox(height: 8),
@@ -359,7 +361,7 @@ class DashboardNotificationsWidget extends StatelessWidget {
title: 'Nouvelles activités',
message: '${data.recentActivitiesCount} activités récentes',
icon: Icons.fiber_new_outlined,
color: AppColors.brandGreen,
color: AppColors.primaryDark,
timeAgo: '15min',
isUrgent: false,
actionLabel: 'Voir',

View File

@@ -173,28 +173,28 @@ class _EpargnePageState extends State<EpargnePage> {
'VUE D\'ENSEMBLE',
style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold),
),
const Icon(Icons.account_balance_wallet, color: AppColors.primaryGreen, size: 24),
const Icon(Icons.account_balance_wallet, color: AppColors.primary, size: 24),
],
),
const SizedBox(height: SpacingTokens.md),
Text(
'${total.toStringAsFixed(0)} XOF',
style: AppTypography.headerSmall.copyWith(fontSize: 24, color: AppColors.primaryGreen),
style: AppTypography.headerSmall.copyWith(fontSize: 24, color: AppColors.primary),
),
const SizedBox(height: SpacingTokens.xs),
Text(
'Solde disponible total • ${_comptes.length} compte${_comptes.length > 1 ? 's' : ''}',
style: AppTypography.bodyTextSmall.copyWith(color: AppColors.textSecondaryLight),
style: AppTypography.bodyTextSmall.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant),
),
if (_comptes.any((c) => c.soldeBloque > 0)) ...[
const SizedBox(height: SpacingTokens.xs),
Row(
children: [
Icon(Icons.shield_outlined, size: 12, color: Colors.amber.shade700),
Icon(Icons.shield_outlined, size: 12, color: AppColors.warning),
const SizedBox(width: 4),
Text(
'Certains fonds sont sous surveillance LCB-FT',
style: AppTypography.bodyTextSmall.copyWith(fontSize: 10, color: Colors.amber.shade700),
style: AppTypography.bodyTextSmall.copyWith(fontSize: 10, color: AppColors.warning),
),
],
),
@@ -234,12 +234,17 @@ class _EpargnePageState extends State<EpargnePage> {
if (typeLibelle != null)
Text(
typeLibelle,
style: AppTypography.subtitleSmall.copyWith(color: AppColors.textSecondaryLight),
style: AppTypography.subtitleSmall.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant),
),
if (dateStr != null)
Text(
dateStr,
style: AppTypography.bodyTextSmall.copyWith(fontSize: 10, color: AppColors.textSecondaryLight),
style: AppTypography.bodyTextSmall.copyWith(
fontSize: 10,
color: Theme.of(context).brightness == Brightness.dark
? AppColors.textSecondaryDark
: AppColors.textSecondary,
),
),
],
),
@@ -252,16 +257,16 @@ class _EpargnePageState extends State<EpargnePage> {
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3),
decoration: BoxDecoration(
color: Colors.amber.withOpacity(0.15),
color: AppColors.warningContainer,
borderRadius: BorderRadius.circular(6),
border: Border.all(color: Colors.amber.shade700, width: 1),
border: Border.all(color: AppColors.warning, width: 1),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.shield_outlined, size: 12, color: Colors.amber.shade700),
Icon(Icons.shield_outlined, size: 12, color: AppColors.warning),
const SizedBox(width: 3),
Text('LCB-FT', style: TextStyle(fontSize: 9, fontWeight: FontWeight.bold, color: Colors.amber.shade700)),
Text('LCB-FT', style: TextStyle(fontSize: 9, fontWeight: FontWeight.bold, color: AppColors.warning)),
],
),
),
@@ -270,7 +275,7 @@ class _EpargnePageState extends State<EpargnePage> {
if (c.statut != null)
InfoBadge(
text: c.statut!,
backgroundColor: c.statut == 'ACTIF' ? AppColors.success : AppColors.textSecondaryLight,
backgroundColor: c.statut == 'ACTIF' ? AppColors.success : AppColors.textSecondary,
),
],
),
@@ -284,7 +289,7 @@ class _EpargnePageState extends State<EpargnePage> {
Text('SOLDE ACTUEL', style: AppTypography.subtitleSmall.copyWith(fontSize: 8, fontWeight: FontWeight.bold)),
Text(
'${c.soldeActuel.toStringAsFixed(0)} XOF',
style: AppTypography.headerSmall.copyWith(fontSize: 14, color: AppColors.primaryGreen),
style: AppTypography.headerSmall.copyWith(fontSize: 14, color: AppColors.primary),
),
],
),
@@ -305,7 +310,7 @@ class _EpargnePageState extends State<EpargnePage> {
const SizedBox(height: SpacingTokens.sm),
Text(
c.description!,
style: AppTypography.bodyTextSmall.copyWith(fontSize: 11, color: AppColors.textSecondaryLight),
style: AppTypography.bodyTextSmall.copyWith(fontSize: 11, color: AppColors.textSecondary),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
@@ -321,8 +326,8 @@ class _EpargnePageState extends State<EpargnePage> {
onPressed: () => _openDepot(c),
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: SpacingTokens.sm),
backgroundColor: AppColors.primaryGreen.withOpacity(0.1),
foregroundColor: AppColors.primaryGreen,
backgroundColor: AppColors.primary.withOpacity(0.1),
foregroundColor: AppColors.primary,
),
child: const Text('Dépôt', style: TextStyle(fontSize: 12)),
),
@@ -333,8 +338,8 @@ class _EpargnePageState extends State<EpargnePage> {
onPressed: soldeDispo > 0 ? () => _openRetrait(c) : null,
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: SpacingTokens.sm),
backgroundColor: AppColors.primaryGreen.withOpacity(0.1),
foregroundColor: AppColors.primaryGreen,
backgroundColor: AppColors.primary.withOpacity(0.1),
foregroundColor: AppColors.primary,
),
child: const Text('Retrait', style: TextStyle(fontSize: 12)),
),
@@ -346,8 +351,8 @@ class _EpargnePageState extends State<EpargnePage> {
onPressed: soldeDispo > 0 ? () => _openTransfert(c) : null,
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: SpacingTokens.sm),
backgroundColor: AppColors.primaryGreen.withOpacity(0.1),
foregroundColor: AppColors.primaryGreen,
backgroundColor: AppColors.primary.withOpacity(0.1),
foregroundColor: AppColors.primary,
),
child: const Text('Transférer', style: TextStyle(fontSize: 12)),
),
@@ -365,7 +370,9 @@ class _EpargnePageState extends State<EpargnePage> {
label: const Text('Détail', style: TextStyle(fontSize: 12)),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: SpacingTokens.sm),
foregroundColor: AppColors.textPrimaryLight,
foregroundColor: Theme.of(context).brightness == Brightness.dark
? AppColors.textPrimaryDark
: AppColors.textPrimary,
),
),
),
@@ -377,7 +384,9 @@ class _EpargnePageState extends State<EpargnePage> {
label: const Text('Historique', style: TextStyle(fontSize: 12)),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: SpacingTokens.sm),
foregroundColor: AppColors.textPrimaryLight,
foregroundColor: Theme.of(context).brightness == Brightness.dark
? AppColors.textPrimaryDark
: AppColors.textPrimary,
),
),
),
@@ -424,17 +433,17 @@ class _EpargnePageState extends State<EpargnePage> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.savings_outlined, size: 64, color: AppColors.textSecondaryLight),
Icon(Icons.savings_outlined, size: 64, color: Theme.of(context).colorScheme.onSurfaceVariant),
const SizedBox(height: SpacingTokens.lg),
Text(
'Aucun compte épargne',
style: AppTypography.actionText.copyWith(color: AppColors.textSecondaryLight),
style: AppTypography.actionText.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant),
textAlign: TextAlign.center,
),
const SizedBox(height: SpacingTokens.sm),
Text(
'Votre organisation peut ouvrir un compte épargne pour vous. Contactez-la pour en bénéficier.',
style: AppTypography.bodyTextSmall.copyWith(color: AppColors.textSecondaryLight),
style: AppTypography.bodyTextSmall.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant),
textAlign: TextAlign.center,
),
],
@@ -459,19 +468,18 @@ class _EpargnePageState extends State<EpargnePage> {
Widget build(BuildContext context) {
final showFab = _canCreateCompte(context);
return Scaffold(
backgroundColor: AppColors.background,
appBar: const UFAppBar(
title: 'COMPTES ÉPARGNE',
backgroundColor: AppColors.surface,
foregroundColor: AppColors.textPrimaryLight,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: UFAppBar(
title: 'Comptes Épargne',
moduleGradient: ModuleColors.epargneGradient,
),
body: _buildBodyContent(),
floatingActionButton: showFab
? FloatingActionButton(
onPressed: _openCreerCompte,
tooltip: 'Créer un compte épargne pour un membre',
backgroundColor: AppColors.primaryGreen,
foregroundColor: Colors.white,
backgroundColor: AppColors.primary,
foregroundColor: AppColors.onPrimary,
child: const Icon(Icons.add),
)
: null,

View File

@@ -38,9 +38,10 @@ class _BudgetsListView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: ColorTokens.background,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: UFAppBar(
title: 'BUDGETS',
title: 'Budgets',
moduleGradient: ModuleColors.financeWorkflowGradient,
automaticallyImplyLeading: true,
actions: [
IconButton(
@@ -57,7 +58,9 @@ class _BudgetsListView extends StatelessWidget {
),
],
),
body: BlocConsumer<BudgetBloc, BudgetState>(
body: SafeArea(
top: false,
child: BlocConsumer<BudgetBloc, BudgetState>(
listener: (context, state) {
if (state is BudgetCreated) {
ScaffoldMessenger.of(context).showSnackBar(
@@ -98,13 +101,14 @@ class _BudgetsListView extends StatelessWidget {
return const SizedBox.shrink();
},
),
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () {
SnackbarHelper.showNotImplemented(context, 'Création de budget');
},
icon: const Icon(Icons.add),
label: const Text('Nouveau budget'),
backgroundColor: AppColors.primaryGreen,
backgroundColor: AppColors.primary,
),
);
}
@@ -143,7 +147,7 @@ class _BudgetsListView extends StatelessWidget {
Widget _buildFilterChips(BuildContext context, BudgetsLoaded state) {
return Container(
padding: const EdgeInsets.all(SpacingTokens.md),
color: AppColors.lightBackground,
color: Theme.of(context).scaffoldBackgroundColor,
child: Wrap(
spacing: SpacingTokens.sm,
runSpacing: SpacingTokens.sm,
@@ -178,6 +182,10 @@ class _BudgetsListView extends StatelessWidget {
}
Widget _buildEmptyState(String message) {
return Builder(
builder: (context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final textSecondary = isDark ? AppColors.textSecondaryDark : AppColors.textSecondary;
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -185,21 +193,25 @@ class _BudgetsListView extends StatelessWidget {
Icon(
Icons.account_balance_wallet_outlined,
size: 48,
color: AppColors.textSecondaryLight.withOpacity(0.5),
color: textSecondary.withOpacity(0.5),
),
const SizedBox(height: SpacingTokens.lg),
Text(
message,
style: AppTypography.headerSmall.copyWith(
color: AppColors.textSecondaryLight,
color: textSecondary,
),
),
],
),
);
},
);
}
Widget _buildErrorState(BuildContext context, String message) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final textSecondary = isDark ? AppColors.textSecondaryDark : AppColors.textSecondary;
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -213,7 +225,7 @@ class _BudgetsListView extends StatelessWidget {
Text(
message,
style: AppTypography.headerSmall.copyWith(
color: AppColors.textSecondaryLight,
color: textSecondary,
),
textAlign: TextAlign.center,
),
@@ -274,14 +286,14 @@ class _BudgetCard extends StatelessWidget {
}
}
Color _getStatusColor(BudgetStatus status) {
Color _getStatusColor(BudgetStatus status, {bool isDark = false}) {
switch (status) {
case BudgetStatus.draft:
return AppColors.textSecondaryLight;
return isDark ? AppColors.textSecondaryDark : AppColors.textSecondary;
case BudgetStatus.active:
return AppColors.brandGreen;
return AppColors.primaryDark;
case BudgetStatus.closed:
return AppColors.textSecondaryLight;
return isDark ? AppColors.textSecondaryDark : AppColors.textSecondary;
case BudgetStatus.cancelled:
return AppColors.error;
}
@@ -302,17 +314,20 @@ class _BudgetCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final currencyFormat = NumberFormat.currency(symbol: budget.currency);
return Container(
padding: const EdgeInsets.all(SpacingTokens.md),
decoration: BoxDecoration(
color: Colors.white,
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
border: Border.all(color: AppColors.lightBorder),
border: Border.all(
color: isDark ? AppColors.borderDark : AppColors.border,
),
boxShadow: const [
BoxShadow(
color: Color(0x0A000000),
color: AppColors.shadow,
blurRadius: 8,
offset: Offset(0, 2),
),
@@ -335,13 +350,13 @@ class _BudgetCard extends StatelessWidget {
vertical: 4,
),
decoration: BoxDecoration(
color: _getStatusColor(budget.status).withOpacity(0.1),
color: _getStatusColor(budget.status, isDark: isDark).withOpacity(0.1),
borderRadius: BorderRadius.circular(SpacingTokens.radiusSm),
),
child: Text(
_getStatusLabel(budget.status),
style: AppTypography.badgeText.copyWith(
color: _getStatusColor(budget.status),
color: _getStatusColor(budget.status, isDark: isDark),
fontWeight: FontWeight.bold,
),
),
@@ -368,7 +383,7 @@ class _BudgetCard extends StatelessWidget {
Text(
currencyFormat.format(budget.totalPlanned),
style: AppTypography.headerSmall.copyWith(
color: AppColors.primaryGreen,
color: AppColors.primary,
fontWeight: FontWeight.bold,
),
),
@@ -389,7 +404,7 @@ class _BudgetCard extends StatelessWidget {
style: AppTypography.headerSmall.copyWith(
color: budget.isOverBudget
? AppColors.error
: AppColors.brandGreen,
: AppColors.primaryDark,
fontWeight: FontWeight.bold,
),
),
@@ -401,10 +416,10 @@ class _BudgetCard extends StatelessWidget {
const SizedBox(height: SpacingTokens.sm),
LinearProgressIndicator(
value: budget.realizationRate / 100,
backgroundColor: AppColors.lightBorder,
backgroundColor: isDark ? AppColors.borderDark : AppColors.border,
color: budget.isOverBudget
? AppColors.error
: AppColors.brandGreen,
: AppColors.primaryDark,
),
const SizedBox(height: SpacingTokens.xs),
Text(

View File

@@ -39,9 +39,10 @@ class _PendingApprovalsView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: ColorTokens.background,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: UFAppBar(
title: 'APPROBATIONS EN ATTENTE',
title: 'Approbations en attente',
moduleGradient: ModuleColors.financeWorkflowGradient,
automaticallyImplyLeading: true,
actions: [
IconButton(
@@ -155,6 +156,7 @@ class _PendingApprovalsView extends StatelessWidget {
}
Widget _buildErrorState(BuildContext context, String message) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -168,7 +170,7 @@ class _PendingApprovalsView extends StatelessWidget {
Text(
message,
style: AppTypography.headerSmall.copyWith(
color: AppColors.textSecondaryLight,
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondary,
),
textAlign: TextAlign.center,
),
@@ -188,6 +190,10 @@ class _PendingApprovalsView extends StatelessWidget {
}
Widget _buildEmptyState() {
return Builder(
builder: (context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final textSecondary = isDark ? AppColors.textSecondaryDark : AppColors.textSecondary;
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -201,19 +207,21 @@ class _PendingApprovalsView extends StatelessWidget {
Text(
'Aucune approbation en attente',
style: AppTypography.headerSmall.copyWith(
color: AppColors.textSecondaryLight,
color: textSecondary,
),
),
const SizedBox(height: SpacingTokens.sm),
Text(
'Toutes les transactions sont approuvées',
style: AppTypography.bodyTextSmall.copyWith(
color: AppColors.textSecondaryLight,
color: textSecondary,
),
),
],
),
);
},
);
}
}
@@ -248,12 +256,12 @@ class _ApprovalCard extends StatelessWidget {
}
}
Color _getLevelColor(ApprovalLevel level) {
Color _getLevelColor(ApprovalLevel level, {bool isDark = false}) {
switch (level) {
case ApprovalLevel.none:
return AppColors.textSecondaryLight;
return isDark ? AppColors.textSecondaryDark : AppColors.textSecondary;
case ApprovalLevel.level1:
return AppColors.brandGreen;
return AppColors.primaryDark;
case ApprovalLevel.level2:
return AppColors.warning;
case ApprovalLevel.level3:
@@ -263,17 +271,18 @@ class _ApprovalCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final currencyFormat = NumberFormat.currency(symbol: approval.currency);
return Container(
padding: const EdgeInsets.all(SpacingTokens.md),
decoration: BoxDecoration(
color: Colors.white,
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
border: Border.all(color: AppColors.lightBorder),
border: Border.all(color: isDark ? AppColors.borderDark : AppColors.border),
boxShadow: const [
BoxShadow(
color: Color(0x0A000000),
color: AppColors.shadow,
blurRadius: 8,
offset: Offset(0, 2),
),
@@ -296,13 +305,13 @@ class _ApprovalCard extends StatelessWidget {
vertical: 4,
),
decoration: BoxDecoration(
color: _getLevelColor(approval.requiredLevel).withOpacity(0.1),
color: _getLevelColor(approval.requiredLevel, isDark: isDark).withOpacity(0.1),
borderRadius: BorderRadius.circular(SpacingTokens.radiusSm),
),
child: Text(
'Niveau ${approval.requiredApprovals}',
style: AppTypography.badgeText.copyWith(
color: _getLevelColor(approval.requiredLevel),
color: _getLevelColor(approval.requiredLevel, isDark: isDark),
fontWeight: FontWeight.bold,
),
),
@@ -313,7 +322,7 @@ class _ApprovalCard extends StatelessWidget {
Text(
currencyFormat.format(approval.amount),
style: AppTypography.headerSmall.copyWith(
color: AppColors.primaryGreen,
color: AppColors.primary,
fontWeight: FontWeight.bold,
),
),
@@ -323,7 +332,7 @@ class _ApprovalCard extends StatelessWidget {
Icon(
Icons.person_outline,
size: 16,
color: AppColors.textSecondaryLight,
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondary,
),
const SizedBox(width: 4),
Expanded(
@@ -340,7 +349,7 @@ class _ApprovalCard extends StatelessWidget {
Icon(
Icons.access_time,
size: 16,
color: AppColors.textSecondaryLight,
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondary,
),
const SizedBox(width: 4),
Text(

View File

@@ -53,6 +53,7 @@ class _ApproveDialogState extends State<ApproveDialog> {
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final currencyFormat = NumberFormat.currency(symbol: widget.approval.currency);
return AlertDialog(
@@ -72,9 +73,9 @@ class _ApproveDialogState extends State<ApproveDialog> {
Container(
padding: const EdgeInsets.all(SpacingTokens.md),
decoration: BoxDecoration(
color: AppColors.lightBackground,
color: isDark ? AppColors.surfaceDark : AppColors.surface,
borderRadius: BorderRadius.circular(SpacingTokens.radiusSm),
border: Border.all(color: AppColors.lightBorder),
border: Border.all(color: isDark ? AppColors.borderDark : AppColors.border),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -88,7 +89,7 @@ class _ApproveDialogState extends State<ApproveDialog> {
'Montant',
currencyFormat.format(widget.approval.amount),
valueStyle: AppTypography.actionText.copyWith(
color: AppColors.primaryGreen,
color: AppColors.primary,
fontWeight: FontWeight.bold,
),
),
@@ -145,7 +146,7 @@ class _ApproveDialogState extends State<ApproveDialog> {
label: const Text('Approuver'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.success,
foregroundColor: Colors.white,
foregroundColor: AppColors.onPrimary,
),
),
],

View File

@@ -53,6 +53,7 @@ class _RejectDialogState extends State<RejectDialog> {
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final currencyFormat = NumberFormat.currency(symbol: widget.approval.currency);
return AlertDialog(
@@ -75,9 +76,9 @@ class _RejectDialogState extends State<RejectDialog> {
Container(
padding: const EdgeInsets.all(SpacingTokens.md),
decoration: BoxDecoration(
color: AppColors.lightBackground,
color: isDark ? AppColors.surfaceDark : AppColors.surface,
borderRadius: BorderRadius.circular(SpacingTokens.radiusSm),
border: Border.all(color: AppColors.lightBorder),
border: Border.all(color: isDark ? AppColors.borderDark : AppColors.border),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -91,7 +92,7 @@ class _RejectDialogState extends State<RejectDialog> {
'Montant',
currencyFormat.format(widget.approval.amount),
valueStyle: AppTypography.actionText.copyWith(
color: AppColors.primaryGreen,
color: AppColors.primary,
fontWeight: FontWeight.bold,
),
),
@@ -141,7 +142,7 @@ class _RejectDialogState extends State<RejectDialog> {
label: const Text('Rejeter'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.error,
foregroundColor: Colors.white,
foregroundColor: AppColors.onError,
),
),
],

View File

@@ -39,8 +39,13 @@ class _HelpSupportPageState extends State<HelpSupportPage> {
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: const UFAppBar(title: 'AIDE & SUPPORT'),
body: SingleChildScrollView(
appBar: UFAppBar(
title: 'Aide & Support',
moduleGradient: ModuleColors.supportGradient,
),
body: SafeArea(
top: false,
child: SingleChildScrollView(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -75,6 +80,7 @@ class _HelpSupportPageState extends State<HelpSupportPage> {
],
),
),
),
);
}
@@ -86,12 +92,12 @@ class _HelpSupportPageState extends State<HelpSupportPage> {
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: AppColors.primaryGreen.withOpacity(0.1),
color: AppColors.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.help_outline,
color: AppColors.primaryGreen,
color: AppColors.primary,
size: 32,
),
),
@@ -113,13 +119,17 @@ class _HelpSupportPageState extends State<HelpSupportPage> {
/// Section de recherche
Widget _buildSearchSection() {
final isDark = Theme.of(context).brightness == Brightness.dark;
final bgSearch = isDark ? AppColors.surfaceVariantDark : AppColors.surface;
final borderSearch = isDark ? AppColors.borderDark : AppColors.border;
final iconColor = isDark ? AppColors.textSecondaryDark : AppColors.textSecondary;
return CoreCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.search, color: AppColors.primaryGreen, size: 18),
const Icon(Icons.search, color: AppColors.primary, size: 18),
const SizedBox(width: 8),
Text(
'RECHERCHER DANS L\'AIDE',
@@ -130,9 +140,9 @@ class _HelpSupportPageState extends State<HelpSupportPage> {
const SizedBox(height: 12),
Container(
decoration: BoxDecoration(
color: AppColors.lightSurface,
color: bgSearch,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppColors.lightBorder),
border: Border.all(color: borderSearch),
),
child: TextField(
controller: _searchController,
@@ -141,14 +151,14 @@ class _HelpSupportPageState extends State<HelpSupportPage> {
decoration: InputDecoration(
hintText: 'Une question, un mot-clé...',
hintStyle: AppTypography.subtitleSmall,
prefixIcon: const Icon(Icons.search, color: AppColors.textSecondaryLight, size: 18),
prefixIcon: Icon(Icons.search, color: iconColor, size: 18),
suffixIcon: _searchQuery.isNotEmpty
? IconButton(
onPressed: () {
_searchController.clear();
setState(() => _searchQuery = '');
},
icon: const Icon(Icons.clear, color: AppColors.textSecondaryLight, size: 18),
icon: Icon(Icons.clear, color: iconColor, size: 18),
)
: null,
border: InputBorder.none,
@@ -180,7 +190,7 @@ class _HelpSupportPageState extends State<HelpSupportPage> {
'CHAT',
'Support Direct',
Icons.chat_bubble_outline,
AppColors.primaryGreen,
AppColors.primary,
() => _startLiveChat(),
),
),
@@ -286,16 +296,16 @@ class _HelpSupportPageState extends State<HelpSupportPage> {
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: isSelected ? AppColors.primaryGreen.withOpacity(0.1) : Colors.transparent,
color: isSelected ? AppColors.primary.withOpacity(0.1) : Colors.transparent,
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: isSelected ? AppColors.primaryGreen : AppColors.lightBorder,
color: isSelected ? AppColors.primary : AppColors.border,
),
),
child: Text(
label.toUpperCase(),
style: AppTypography.badgeText.copyWith(
color: isSelected ? AppColors.primaryGreen : AppColors.textSecondaryLight,
color: isSelected ? AppColors.primary : AppColors.textSecondary,
fontSize: 9,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
@@ -338,11 +348,13 @@ class _HelpSupportPageState extends State<HelpSupportPage> {
),
leading: Icon(
faq['icon'] as IconData,
color: AppColors.primaryGreen,
color: AppColors.primary,
size: 18,
),
iconColor: AppColors.primaryGreen,
collapsedIconColor: AppColors.textSecondaryLight,
iconColor: AppColors.primary,
collapsedIconColor: Theme.of(context).brightness == Brightness.dark
? AppColors.textSecondaryDark
: AppColors.textSecondary,
shape: const RoundedRectangleBorder(side: BorderSide.none),
children: [
Padding(
@@ -370,7 +382,7 @@ class _HelpSupportPageState extends State<HelpSupportPage> {
),
),
_buildGuideItem('Introduction', 'Démarrer avec UnionFlow', Icons.play_circle_outline, AppColors.success, () => _openGuide('getting-started')),
_buildGuideItem('Membres', 'Gérer vos adhérents', Icons.people_outline, AppColors.primaryGreen, () => _openGuide('members')),
_buildGuideItem('Membres', 'Gérer vos adhérents', Icons.people_outline, AppColors.primary, () => _openGuide('members')),
_buildGuideItem('Organisations', 'Structures & Syndicats', Icons.business_outlined, AppColors.info, () => _openGuide('organizations')),
],
);
@@ -394,7 +406,13 @@ class _HelpSupportPageState extends State<HelpSupportPage> {
],
),
),
const Icon(Icons.chevron_right, color: AppColors.textSecondaryLight, size: 16),
Icon(
Icons.chevron_right,
color: Theme.of(context).brightness == Brightness.dark
? AppColors.textSecondaryDark
: AppColors.textSecondary,
size: 16,
),
],
),
);
@@ -402,8 +420,9 @@ class _HelpSupportPageState extends State<HelpSupportPage> {
/// Section contact
Widget _buildContactSection() {
final isDark = Theme.of(context).brightness == Brightness.dark;
return CoreCard(
backgroundColor: AppColors.primaryGreen, // Correction: color -> backgroundColor
backgroundColor: AppColors.primary, // Correction: color -> backgroundColor
child: Column(
children: [
const Icon(Icons.headset_mic_outlined, color: Colors.white, size: 32),
@@ -426,8 +445,8 @@ class _HelpSupportPageState extends State<HelpSupportPage> {
child: UFPrimaryButton(
label: 'EMAIL', // Correction: text -> label
onPressed: () => _contactByEmail(),
backgroundColor: Colors.white,
textColor: AppColors.primaryGreen,
backgroundColor: isDark ? AppColors.surfaceDark : AppColors.surface,
textColor: AppColors.primary,
),
),
const SizedBox(width: 12),
@@ -436,7 +455,7 @@ class _HelpSupportPageState extends State<HelpSupportPage> {
label: 'CHAT', // Correction: text -> label
onPressed: () => _startLiveChat(),
backgroundColor: Colors.white.withOpacity(0.2),
textColor: Colors.white,
textColor: AppColors.onPrimary,
),
),
],
@@ -516,8 +535,8 @@ class _HelpSupportPageState extends State<HelpSupportPage> {
_contactByEmail();
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryGreen,
foregroundColor: Colors.white,
backgroundColor: AppColors.primary,
foregroundColor: AppColors.onPrimary,
),
child: const Text('Envoyer un email'),
),
@@ -548,7 +567,7 @@ class _HelpSupportPageState extends State<HelpSupportPage> {
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.error,
foregroundColor: Colors.white,
foregroundColor: AppColors.onError,
),
child: const Text('Signaler'),
),
@@ -578,8 +597,8 @@ class _HelpSupportPageState extends State<HelpSupportPage> {
_launchUrl('mailto:support@unionflow.com?subject=Demande de fonctionnalité - UnionFlow Mobile');
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryGreen,
foregroundColor: Colors.white,
backgroundColor: AppColors.primary,
foregroundColor: AppColors.onPrimary,
),
child: const Text('Envoyer'),
),
@@ -613,8 +632,8 @@ class _HelpSupportPageState extends State<HelpSupportPage> {
_launchUrl('https://docs.unionflow.com/$guideId');
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryGreen,
foregroundColor: Colors.white,
backgroundColor: AppColors.primary,
foregroundColor: AppColors.onPrimary,
),
child: const Text('Voir en ligne'),
),
@@ -644,8 +663,8 @@ class _HelpSupportPageState extends State<HelpSupportPage> {
_contactByEmail();
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryGreen,
foregroundColor: Colors.white,
backgroundColor: AppColors.primary,
foregroundColor: AppColors.onPrimary,
),
child: const Text('Contacter le support'),
),

View File

@@ -10,6 +10,8 @@ import '../../bloc/organizations_state.dart';
import '../../bloc/org_types_bloc.dart';
import '../../domain/entities/type_reference_entity.dart';
import '../../../../shared/design_system/tokens/app_colors.dart';
import '../../../../shared/design_system/tokens/module_colors.dart';
import '../../../../shared/design_system/components/uf_app_bar.dart';
import '../../../../core/di/injection_container.dart';
const List<String> _devises = ['XOF', 'XAF', 'EUR', 'USD', 'GBP', 'CAD', 'CHF', 'MAD', 'GHS', 'NGN', 'CDF', 'KES'];
@@ -106,12 +108,10 @@ class _CreateOrganizationPageState extends State<CreateOrganizationPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.lightBackground,
appBar: AppBar(
backgroundColor: AppColors.primaryGreen,
foregroundColor: Colors.white,
title: const Text('Nouvelle Organisation'),
elevation: 0,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: UFAppBar(
title: 'Nouvelle Organisation',
moduleGradient: ModuleColors.organisationsGradient,
actions: [
TextButton(
onPressed: _isFormValid() ? _saveOrganisation : null,
@@ -119,7 +119,9 @@ class _CreateOrganizationPageState extends State<CreateOrganizationPage> {
),
],
),
body: BlocListener<OrganizationsBloc, OrganizationsState>(
body: SafeArea(
top: false,
child: BlocListener<OrganizationsBloc, OrganizationsState>(
listener: (context, state) {
if (state is OrganizationCreated) {
ScaffoldMessenger.of(context).showSnackBar(
@@ -128,7 +130,7 @@ class _CreateOrganizationPageState extends State<CreateOrganizationPage> {
Navigator.of(context).pop(true);
} else if (state is OrganizationsError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.message), backgroundColor: Colors.red),
SnackBar(content: Text(state.message), backgroundColor: AppColors.error),
);
}
},
@@ -161,20 +163,21 @@ class _CreateOrganizationPageState extends State<CreateOrganizationPage> {
),
),
),
),
);
}
Widget _buildSection(String title, IconData icon, List<Widget> children) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(8)),
decoration: BoxDecoration(color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(8)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(children: [
Icon(icon, size: 16, color: AppColors.primaryGreen),
Icon(icon, size: 16, color: AppColors.primary),
const SizedBox(width: 6),
Text(title, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold, color: AppColors.primaryGreen)),
Text(title, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold, color: AppColors.primary)),
]),
const SizedBox(height: 10),
...children,
@@ -237,9 +240,18 @@ class _CreateOrganizationPageState extends State<CreateOrganizationPage> {
onTap: () => _pickDateFondation(context),
child: InputDecorator(
decoration: const InputDecoration(labelText: 'Date de fondation', border: OutlineInputBorder(), prefixIcon: Icon(Icons.cake)),
child: Text(
child: Builder(
builder: (ctx) {
final isDark = Theme.of(ctx).brightness == Brightness.dark;
return Text(
_dateFondation != null ? _formatDate(_dateFondation) : 'Sélectionner une date',
style: TextStyle(color: _dateFondation != null ? AppColors.textPrimaryLight : AppColors.textSecondaryLight),
style: TextStyle(
color: _dateFondation != null
? (isDark ? AppColors.textPrimaryDark : AppColors.textPrimary)
: (isDark ? AppColors.textSecondaryDark : AppColors.textSecondary),
),
);
},
),
),
),
@@ -355,7 +367,7 @@ class _CreateOrganizationPageState extends State<CreateOrganizationPage> {
subtitle: const Text('Visible par tous les utilisateurs', style: TextStyle(fontSize: 12)),
value: _organisationPublique,
onChanged: (v) => setState(() => _organisationPublique = v),
activeColor: AppColors.primaryGreen,
activeColor: AppColors.primary,
),
SwitchListTile(
dense: true,
@@ -364,7 +376,7 @@ class _CreateOrganizationPageState extends State<CreateOrganizationPage> {
subtitle: const Text('Les demandes d\'adhésion sont ouvertes', style: TextStyle(fontSize: 12)),
value: _accepteNouveauxMembres,
onChanged: (v) => setState(() => _accepteNouveauxMembres = v),
activeColor: AppColors.primaryGreen,
activeColor: AppColors.primary,
),
];
@@ -392,7 +404,7 @@ class _CreateOrganizationPageState extends State<CreateOrganizationPage> {
title: const Text('Cotisation obligatoire', style: TextStyle(fontSize: 14)),
value: _cotisationObligatoire,
onChanged: (v) => setState(() => _cotisationObligatoire = v),
activeColor: AppColors.primaryGreen,
activeColor: AppColors.primary,
),
if (_cotisationObligatoire) ...[
const SizedBox(height: 8),
@@ -451,8 +463,8 @@ class _CreateOrganizationPageState extends State<CreateOrganizationPage> {
icon: const Icon(Icons.save),
label: const Text('Créer l\'organisation'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryGreen,
foregroundColor: Colors.white,
backgroundColor: AppColors.primary,
foregroundColor: AppColors.onPrimary,
padding: const EdgeInsets.symmetric(vertical: 10),
textStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
),
@@ -466,7 +478,7 @@ class _CreateOrganizationPageState extends State<CreateOrganizationPage> {
icon: const Icon(Icons.cancel),
label: const Text('Annuler'),
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.textSecondaryLight,
foregroundColor: Theme.of(context).brightness == Brightness.dark ? AppColors.textSecondaryDark : AppColors.textSecondary,
padding: const EdgeInsets.symmetric(vertical: 10),
),
),

View File

@@ -10,6 +10,8 @@ import '../../bloc/organizations_state.dart';
import '../../bloc/org_types_bloc.dart';
import '../../domain/entities/type_reference_entity.dart';
import '../../../../shared/design_system/tokens/app_colors.dart';
import '../../../../shared/design_system/tokens/module_colors.dart';
import '../../../../shared/design_system/components/uf_app_bar.dart';
import '../../../../core/di/injection_container.dart';
const List<String> _devisesEdit = ['XOF', 'XAF', 'EUR', 'USD', 'GBP', 'CAD', 'CHF', 'MAD', 'GHS', 'NGN', 'CDF', 'KES'];
@@ -185,12 +187,10 @@ class _EditOrganizationPageState extends State<EditOrganizationPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.lightBackground,
appBar: AppBar(
backgroundColor: AppColors.primaryGreen,
foregroundColor: Colors.white,
title: const Text('Modifier Organisation'),
elevation: 0,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: UFAppBar(
title: 'Modifier Organisation',
moduleGradient: ModuleColors.organisationsGradient,
actions: [
TextButton(
onPressed: _hasChanges() ? _saveChanges : null,
@@ -212,7 +212,7 @@ class _EditOrganizationPageState extends State<EditOrganizationPage> {
Navigator.of(context).pop(true);
} else if (state is OrganizationsError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.message), backgroundColor: Colors.red),
SnackBar(content: Text(state.message), backgroundColor: AppColors.error),
);
}
},
@@ -254,14 +254,14 @@ class _EditOrganizationPageState extends State<EditOrganizationPage> {
Widget _buildSection(String title, IconData icon, List<Widget> children) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(8)),
decoration: BoxDecoration(color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(8)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(children: [
Icon(icon, size: 16, color: AppColors.primaryGreen),
Icon(icon, size: 16, color: AppColors.primary),
const SizedBox(width: 6),
Text(title, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold, color: AppColors.primaryGreen)),
Text(title, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold, color: AppColors.primary)),
]),
const SizedBox(height: 10),
...children,
@@ -326,9 +326,18 @@ class _EditOrganizationPageState extends State<EditOrganizationPage> {
onTap: () => _pickDateFondation(context),
child: InputDecorator(
decoration: const InputDecoration(labelText: 'Date de fondation', border: OutlineInputBorder(), prefixIcon: Icon(Icons.cake)),
child: Text(
child: Builder(
builder: (ctx) {
final isDark = Theme.of(ctx).brightness == Brightness.dark;
return Text(
_dateFondation != null ? _formatDate(_dateFondation) : 'Sélectionner une date',
style: TextStyle(color: _dateFondation != null ? AppColors.textPrimaryLight : AppColors.textSecondaryLight),
style: TextStyle(
color: _dateFondation != null
? (isDark ? AppColors.textPrimaryDark : AppColors.textPrimary)
: (isDark ? AppColors.textSecondaryDark : AppColors.textSecondary),
),
);
},
),
),
),
@@ -452,7 +461,7 @@ class _EditOrganizationPageState extends State<EditOrganizationPage> {
subtitle: const Text('Visible par tous les utilisateurs', style: TextStyle(fontSize: 12)),
value: _organisationPublique,
onChanged: (v) => setState(() => _organisationPublique = v),
activeColor: AppColors.primaryGreen,
activeColor: AppColors.primary,
),
SwitchListTile(
dense: true,
@@ -461,7 +470,7 @@ class _EditOrganizationPageState extends State<EditOrganizationPage> {
subtitle: const Text('Les demandes d\'adhésion sont ouvertes', style: TextStyle(fontSize: 12)),
value: _accepteNouveauxMembres,
onChanged: (v) => setState(() => _accepteNouveauxMembres = v),
activeColor: AppColors.primaryGreen,
activeColor: AppColors.primary,
),
];
@@ -490,7 +499,7 @@ class _EditOrganizationPageState extends State<EditOrganizationPage> {
title: const Text('Cotisation obligatoire', style: TextStyle(fontSize: 14)),
value: _cotisationObligatoire,
onChanged: (v) => setState(() => _cotisationObligatoire = v),
activeColor: AppColors.primaryGreen,
activeColor: AppColors.primary,
),
if (_cotisationObligatoire) ...[
const SizedBox(height: 8),
@@ -559,12 +568,15 @@ class _EditOrganizationPageState extends State<EditOrganizationPage> {
];
Widget _buildReadOnlyRow(IconData icon, String label, String value) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final textSecondary = isDark ? AppColors.textSecondaryDark : AppColors.textSecondary;
final textPrimary = isDark ? AppColors.textPrimaryDark : AppColors.textPrimary;
return Row(children: [
Icon(icon, size: 18, color: AppColors.textSecondaryLight),
Icon(icon, size: 18, color: textSecondary),
const SizedBox(width: 10),
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(label, style: const TextStyle(fontSize: 11, color: AppColors.textSecondaryLight)),
Text(value, style: const TextStyle(fontSize: 13, color: AppColors.textPrimaryLight, fontWeight: FontWeight.w600)),
Text(label, style: TextStyle(fontSize: 11, color: textSecondary)),
Text(value, style: TextStyle(fontSize: 13, color: textPrimary, fontWeight: FontWeight.w600)),
])),
]);
}
@@ -578,8 +590,8 @@ class _EditOrganizationPageState extends State<EditOrganizationPage> {
icon: const Icon(Icons.save),
label: const Text('Enregistrer les modifications'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryGreen,
foregroundColor: Colors.white,
backgroundColor: AppColors.primary,
foregroundColor: AppColors.onPrimary,
padding: const EdgeInsets.symmetric(vertical: 10),
textStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
),
@@ -592,7 +604,7 @@ class _EditOrganizationPageState extends State<EditOrganizationPage> {
onPressed: _showDiscardDialog,
icon: const Icon(Icons.cancel),
label: const Text('Annuler'),
style: OutlinedButton.styleFrom(foregroundColor: AppColors.textSecondaryLight, padding: const EdgeInsets.symmetric(vertical: 10)),
style: OutlinedButton.styleFrom(foregroundColor: Theme.of(context).brightness == Brightness.dark ? AppColors.textSecondaryDark : AppColors.textSecondary, padding: const EdgeInsets.symmetric(vertical: 10)),
),
),
]);
@@ -681,8 +693,8 @@ class _EditOrganizationPageState extends State<EditOrganizationPage> {
TextButton(onPressed: () => Navigator.of(ctx).pop(), child: const Text('Continuer l\'édition')),
ElevatedButton(
onPressed: () { Navigator.of(ctx).pop(); Navigator.of(context).pop(); },
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
child: const Text('Abandonner', style: TextStyle(color: Colors.white)),
style: ElevatedButton.styleFrom(backgroundColor: AppColors.error),
child: const Text('Abandonner', style: TextStyle(color: AppColors.onPrimary)),
),
],
),