Files
unionflow-mobile-apps/lib/features/help/presentation/pages/help_support_page.dart
dahoud ba779a7a40 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.
2026-04-15 20:14:59 +00:00

712 lines
22 KiB
Dart

import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../../shared/design_system/tokens/app_colors.dart';
import '../../../../shared/design_system/unionflow_design_system.dart';
import '../../../../shared/widgets/core_card.dart';
/// Page Aide & Support - UnionFlow Mobile
///
/// Page complète d'aide avec FAQ, guides, support technique,
/// et ressources pour les utilisateurs.
class HelpSupportPage extends StatefulWidget {
const HelpSupportPage({super.key});
@override
State<HelpSupportPage> createState() => _HelpSupportPageState();
}
class _HelpSupportPageState extends State<HelpSupportPage> {
final TextEditingController _searchController = TextEditingController();
String _searchQuery = '';
int _selectedCategoryIndex = 0;
final List<String> _categories = [
'Tout',
'Connexion',
'Membres',
'Organisations',
'Événements',
'Technique',
];
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: UFAppBar(
title: 'Aide & Support',
moduleGradient: ModuleColors.supportGradient,
),
body: SafeArea(
top: false,
child: SingleChildScrollView(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header harmonisé
_buildHeader(),
const SizedBox(height: 8),
// Barre de recherche
_buildSearchSection(),
const SizedBox(height: 8),
// Actions rapides
_buildQuickActionsSection(),
const SizedBox(height: 8),
// Catégories FAQ
_buildCategoriesSection(),
const SizedBox(height: 8),
// FAQ
_buildFAQSection(),
const SizedBox(height: 8),
// Guides et tutoriels
_buildGuidesSection(),
const SizedBox(height: 8),
// Contact support
_buildContactSection(),
const SizedBox(height: 80),
],
),
),
),
);
}
/// Header épuré
Widget _buildHeader() {
return Center(
child: Column(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: AppColors.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.help_outline,
color: AppColors.primary,
size: 32,
),
),
const SizedBox(height: 8),
Text(
'COMMENT POUVONS-NOUS VOUS AIDER ?',
style: AppTypography.headerSmall.copyWith(fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
const SizedBox(height: 4),
Text(
'Documentation, FAQ et support technique',
style: AppTypography.subtitleSmall,
),
],
),
);
}
/// 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.primary, size: 18),
const SizedBox(width: 8),
Text(
'RECHERCHER DANS L\'AIDE',
style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold, letterSpacing: 1.1),
),
],
),
const SizedBox(height: 12),
Container(
decoration: BoxDecoration(
color: bgSearch,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: borderSearch),
),
child: TextField(
controller: _searchController,
onChanged: (value) => setState(() => _searchQuery = value),
style: AppTypography.bodyTextSmall,
decoration: InputDecoration(
hintText: 'Une question, un mot-clé...',
hintStyle: AppTypography.subtitleSmall,
prefixIcon: Icon(Icons.search, color: iconColor, size: 18),
suffixIcon: _searchQuery.isNotEmpty
? IconButton(
onPressed: () {
_searchController.clear();
setState(() => _searchQuery = '');
},
icon: Icon(Icons.clear, color: iconColor, size: 18),
)
: null,
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
),
),
],
),
);
}
/// Section actions rapides
Widget _buildQuickActionsSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 4, bottom: 12),
child: Text(
'ACTIONS RAPIDES',
style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold, letterSpacing: 1.1),
),
),
Row(
children: [
Expanded(
child: _buildQuickActionCard(
'CHAT',
'Support Direct',
Icons.chat_bubble_outline,
AppColors.primary,
() => _startLiveChat(),
),
),
const SizedBox(width: 12),
Expanded(
child: _buildQuickActionCard(
'BUG',
'Signaler',
Icons.bug_report_outlined,
AppColors.error,
() => _reportBug(),
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildQuickActionCard(
'IDÉE',
'Suggérer',
Icons.lightbulb_outline,
AppColors.warning,
() => _requestFeature(),
),
),
const SizedBox(width: 12),
Expanded(
child: _buildQuickActionCard(
'EMAIL',
'Support Tech',
Icons.mark_email_read_outlined,
AppColors.info,
() => _contactByEmail(),
),
),
],
),
],
);
}
/// Carte d'action rapide
Widget _buildQuickActionCard(String title, String subtitle, IconData icon, Color color, VoidCallback onTap) {
return CoreCard(
padding: const EdgeInsets.all(12),
onTap: onTap,
child: Column(
children: [
Icon(icon, color: color, size: 24),
const SizedBox(height: 8),
Text(
title,
style: AppTypography.actionText.copyWith(fontSize: 11),
textAlign: TextAlign.center,
),
Text(
subtitle,
style: AppTypography.subtitleSmall.copyWith(fontSize: 9),
textAlign: TextAlign.center,
),
],
),
);
}
/// Section catégories
Widget _buildCategoriesSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 4, bottom: 8),
child: Text(
'PAR CATÉGORIE',
style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold, letterSpacing: 1.1),
),
),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: _categories.asMap().entries.map((entry) {
final isSelected = _selectedCategoryIndex == entry.key;
return Padding(
padding: const EdgeInsets.only(right: 8),
child: _buildCategoryChip(entry.value, isSelected, () {
setState(() => _selectedCategoryIndex = entry.key);
}),
);
}).toList(),
),
),
],
);
}
/// Chip de catégorie
Widget _buildCategoryChip(String label, bool isSelected, VoidCallback onTap) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(4),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: isSelected ? AppColors.primary.withOpacity(0.1) : Colors.transparent,
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: isSelected ? AppColors.primary : AppColors.border,
),
),
child: Text(
label.toUpperCase(),
style: AppTypography.badgeText.copyWith(
color: isSelected ? AppColors.primary : AppColors.textSecondary,
fontSize: 9,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
),
),
);
}
/// Section FAQ
Widget _buildFAQSection() {
final faqs = _getFilteredFAQs();
if (faqs.isEmpty) return const SizedBox.shrink();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 4, bottom: 12),
child: Text(
'QUESTIONS FRÉQUENTES',
style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold, letterSpacing: 1.1),
),
),
...faqs.map((faq) => Padding(
padding: const EdgeInsets.only(bottom: 8),
child: _buildFAQItem(faq),
)),
],
);
}
/// Élément FAQ
Widget _buildFAQItem(Map<String, dynamic> faq) {
return CoreCard(
padding: EdgeInsets.zero,
child: ExpansionTile(
title: Text(
faq['question'],
style: AppTypography.actionText.copyWith(fontSize: 12),
),
leading: Icon(
faq['icon'] as IconData,
color: AppColors.primary,
size: 18,
),
iconColor: AppColors.primary,
collapsedIconColor: Theme.of(context).brightness == Brightness.dark
? AppColors.textSecondaryDark
: AppColors.textSecondary,
shape: const RoundedRectangleBorder(side: BorderSide.none),
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: Text(
faq['answer'],
style: AppTypography.subtitleSmall.copyWith(fontSize: 11, height: 1.4),
),
),
],
),
);
}
/// Section guides
Widget _buildGuidesSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 4, bottom: 12),
child: Text(
'GUIDES & TUTORIELS',
style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold, letterSpacing: 1.1),
),
),
_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.primary, () => _openGuide('members')),
_buildGuideItem('Organisations', 'Structures & Syndicats', Icons.business_outlined, AppColors.info, () => _openGuide('organizations')),
],
);
}
/// Élément de guide
Widget _buildGuideItem(String title, String description, IconData icon, Color color, VoidCallback onTap) {
return CoreCard(
margin: const EdgeInsets.only(bottom: 8),
onTap: onTap,
child: Row(
children: [
Icon(icon, color: color, size: 20),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: AppTypography.actionText.copyWith(fontSize: 12)),
Text(description, style: AppTypography.subtitleSmall.copyWith(fontSize: 10)),
],
),
),
Icon(
Icons.chevron_right,
color: Theme.of(context).brightness == Brightness.dark
? AppColors.textSecondaryDark
: AppColors.textSecondary,
size: 16,
),
],
),
);
}
/// Section contact
Widget _buildContactSection() {
final isDark = Theme.of(context).brightness == Brightness.dark;
return CoreCard(
backgroundColor: AppColors.primary, // Correction: color -> backgroundColor
child: Column(
children: [
const Icon(Icons.headset_mic_outlined, color: Colors.white, size: 32),
const SizedBox(height: 12),
Text(
'BESOIN D\'AIDE SUPPLÉMENTAIRE ?',
style: AppTypography.headerSmall.copyWith(color: Colors.white, fontSize: 14),
textAlign: TextAlign.center,
),
const SizedBox(height: 4),
Text(
'Disponible lun-ven, 9h-18h',
style: AppTypography.subtitleSmall.copyWith(color: Colors.white.withOpacity(0.8)),
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: UFPrimaryButton(
label: 'EMAIL', // Correction: text -> label
onPressed: () => _contactByEmail(),
backgroundColor: isDark ? AppColors.surfaceDark : AppColors.surface,
textColor: AppColors.primary,
),
),
const SizedBox(width: 12),
Expanded(
child: UFPrimaryButton(
label: 'CHAT', // Correction: text -> label
onPressed: () => _startLiveChat(),
backgroundColor: Colors.white.withOpacity(0.2),
textColor: AppColors.onPrimary,
),
),
],
),
],
),
);
}
/// Obtenir les FAQs filtrées
List<Map<String, dynamic>> _getFilteredFAQs() {
final allFAQs = [
{
'question': 'Comment me connecter à UnionFlow ?',
'answer': 'Utilisez vos identifiants fournis par votre organisation. La connexion se fait via Keycloak pour une sécurité optimale.',
'category': 'Connexion',
'icon': Icons.login,
},
{
'question': 'Comment ajouter un nouveau membre ?',
'answer': 'Allez dans la section Membres, cliquez sur le bouton + et remplissez les informations requises. Vous devez avoir les permissions appropriées.',
'category': 'Membres',
'icon': Icons.person_add,
},
{
'question': 'Comment créer une nouvelle organisation ?',
'answer': 'Dans la section Organisations, utilisez le bouton "Nouvelle organisation" et suivez les étapes du formulaire.',
'category': 'Organisations',
'icon': Icons.business,
},
{
'question': 'Comment planifier un événement ?',
'answer': 'Accédez à la section Événements, cliquez sur "Nouvel événement" et configurez les détails, date, lieu et participants.',
'category': 'Événements',
'icon': Icons.event,
},
{
'question': 'L\'application ne se synchronise pas',
'answer': 'Vérifiez votre connexion internet. Si le problème persiste, déconnectez-vous et reconnectez-vous.',
'category': 'Technique',
'icon': Icons.sync_problem,
},
{
'question': 'Comment modifier mes informations personnelles ?',
'answer': 'Allez dans Plus > Profil pour modifier vos informations personnelles et préférences.',
'category': 'Tout',
'icon': Icons.edit,
},
];
if (_selectedCategoryIndex == 0) return allFAQs; // Tout
final selectedCategory = _categories[_selectedCategoryIndex];
return allFAQs.where((faq) => faq['category'] == selectedCategory).toList();
}
// ==================== MÉTHODES D'ACTION ====================
/// Démarrer un chat en direct
void _startLiveChat() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Chat en direct'),
content: const Text(
'Contacter le support par email pour toute question. '
'Notre équipe vous répondra dans les meilleurs délais.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Fermer'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
_contactByEmail();
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
foregroundColor: AppColors.onPrimary,
),
child: const Text('Envoyer un email'),
),
],
),
);
}
/// Signaler un bug
void _reportBug() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Signaler un bug'),
content: const Text(
'Décrivez le problème rencontré et les étapes pour le reproduire. '
'Notre équipe technique vous répondra rapidement.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
_launchUrl('mailto:support@unionflow.com?subject=Rapport de bug - UnionFlow Mobile');
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.error,
foregroundColor: AppColors.onError,
),
child: const Text('Signaler'),
),
],
),
);
}
/// Demander une fonctionnalité
void _requestFeature() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Demander une fonctionnalité'),
content: const Text(
'Partagez vos idées d\'amélioration ! '
'Nous étudions toutes les suggestions pour améliorer UnionFlow.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
_launchUrl('mailto:support@unionflow.com?subject=Demande de fonctionnalité - UnionFlow Mobile');
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
foregroundColor: AppColors.onPrimary,
),
child: const Text('Envoyer'),
),
],
),
);
}
/// Contacter par email
void _contactByEmail() {
_launchUrl('mailto:support@unionflow.com?subject=Support UnionFlow Mobile');
}
/// Ouvrir un guide
void _openGuide(String guideId) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Guide'),
content: Text(
'Consultez notre documentation en ligne pour le guide "$guideId".',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Fermer'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
_launchUrl('https://docs.unionflow.com/$guideId');
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
foregroundColor: AppColors.onPrimary,
),
child: const Text('Voir en ligne'),
),
],
),
);
}
/// Afficher la visite guidée
void _showHelpTour() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Visite guidée'),
content: const Text(
'Parcourez les onglets de l\'application pour découvrir les fonctionnalités. '
'En cas de question, contactez le support par email.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Fermer'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
_contactByEmail();
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
foregroundColor: AppColors.onPrimary,
),
child: const Text('Contacter le support'),
),
],
),
);
}
/// Lancer une URL
Future<void> _launchUrl(String url) async {
try {
final uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
} else {
_showErrorSnackBar('Impossible d\'ouvrir le lien');
}
} catch (e) {
_showErrorSnackBar('Erreur lors de l\'ouverture du lien');
}
}
/// Afficher un message d'erreur
void _showErrorSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: AppColors.error,
behavior: SnackBarBehavior.floating,
),
);
}
/// Afficher un message de succès
void _showSuccessSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: AppColors.success,
behavior: SnackBarBehavior.floating,
),
);
}
}