/// Page dédiée à l'envoi de commentaires / feedback /// Permet de soumettre des suggestions, signaler des bugs, ou proposer des idées library feedback_page; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:dio/dio.dart'; import '../../../../core/utils/logger.dart'; import '../../../../shared/design_system/unionflow_design_system.dart'; class FeedbackPage extends StatefulWidget { const FeedbackPage({super.key}); @override State createState() => _FeedbackPageState(); } class _FeedbackPageState extends State { final _messageController = TextEditingController(); String _selectedCategory = 'suggestion'; bool _isSending = false; static const _categories = [ _FeedbackCategory('suggestion', 'Suggestion', Icons.lightbulb, AppColors.primaryGreen), _FeedbackCategory('bug', 'Bug / Problème', Icons.bug_report, AppColors.error), _FeedbackCategory('amelioration', 'Amélioration', Icons.trending_up, AppColors.success), _FeedbackCategory('autre', 'Autre', Icons.help_outline, AppColors.brandGreen), ]; @override void dispose() { _messageController.dispose(); super.dispose(); } Future _submitFeedback() async { final message = _messageController.text.trim(); if (message.isEmpty) { _showSnackBar('Veuillez saisir un message.', isError: true); return; } setState(() => _isSending = true); try { await GetIt.I().post( '/api/feedback', data: { 'subject': 'Feedback mobile [$_selectedCategory]', 'message': message, 'categorie': _selectedCategory, }, ); if (mounted) { _messageController.clear(); _showSnackBar('Merci pour votre retour !'); } } catch (e, st) { AppLogger.error('FeedbackPage: envoi feedback échoué', error: e, stackTrace: st); if (mounted) { _showSnackBar('Envoi échoué. Réessayez plus tard.', isError: true); } } finally { if (mounted) setState(() => _isSending = false); } } void _showSnackBar(String message, {bool isError = false}) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), backgroundColor: isError ? AppColors.error : AppColors.success, behavior: SnackBarBehavior.floating, ), ); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Theme.of(context).scaffoldBackgroundColor, body: Column( children: [ _buildHeader(), Expanded( child: SingleChildScrollView( padding: const EdgeInsets.all(12), child: Column( children: [ const SizedBox(height: 8), _buildCategorySection(), const SizedBox(height: 8), _buildMessageSection(), const SizedBox(height: 8), _buildSubmitButton(), const SizedBox(height: 80), ], ), ), ), ], ), ); } Widget _buildHeader() { return Container( margin: const EdgeInsets.symmetric(horizontal: SpacingTokens.sm, vertical: SpacingTokens.xs), padding: const EdgeInsets.all(SpacingTokens.md), decoration: BoxDecoration( gradient: const LinearGradient( colors: [AppColors.brandGreen, AppColors.primaryGreen], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(SpacingTokens.xl), boxShadow: [ BoxShadow( color: AppColors.primaryGreen.withOpacity(0.3), blurRadius: 20, offset: const Offset(0, 8), ), ], ), child: SafeArea( bottom: false, child: Row( children: [ IconButton( onPressed: () => Navigator.of(context).pop(), icon: const Icon(Icons.arrow_back, color: Colors.white), ), Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(8), ), child: const Icon(Icons.feedback, color: Colors.white, size: 20), ), const SizedBox(width: 12), const Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Commentaires', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: Colors.white, ), ), Text( 'Aidez-nous à améliorer UnionFlow', style: TextStyle( fontSize: 14, color: Colors.white70, ), ), ], ), ), ], ), ), ); } Widget _buildCategorySection() { final isDark = Theme.of(context).brightness == Brightness.dark; final textPrimary = isDark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight; final textSecondary = isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight; return Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: isDark ? AppColors.darkSurface : Colors.white, borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.category, color: textSecondary, size: 20), const SizedBox(width: 8), Text( 'Type de retour', style: AppTypography.headerSmall.copyWith(fontWeight: FontWeight.w600, color: textPrimary), ), ], ), const SizedBox(height: 8), Wrap( spacing: 10, runSpacing: 10, children: _categories.map((cat) => _buildCategoryChip(cat)).toList(), ), ], ), ); } Widget _buildCategoryChip(_FeedbackCategory cat) { final isDark = Theme.of(context).brightness == Brightness.dark; final isSelected = _selectedCategory == cat.id; final textSecondary = isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight; return InkWell( onTap: () => setState(() => _selectedCategory = cat.id), borderRadius: BorderRadius.circular(12), child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), decoration: BoxDecoration( color: isSelected ? cat.color.withOpacity(0.12) : (isDark ? AppColors.darkBackground : Colors.grey[50]), borderRadius: BorderRadius.circular(12), border: Border.all( color: isSelected ? cat.color.withOpacity(0.5) : (isDark ? AppColors.darkBorder : Colors.grey[200]!), width: isSelected ? 1.5 : 1, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(cat.icon, size: 18, color: isSelected ? cat.color : textSecondary), const SizedBox(width: 8), Text( cat.label, style: AppTypography.bodyTextSmall.copyWith( fontWeight: FontWeight.w600, color: isSelected ? cat.color : textSecondary, ), ), ], ), ), ); } Widget _buildMessageSection() { final isDark = Theme.of(context).brightness == Brightness.dark; final textPrimary = isDark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight; final textSecondary = isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight; return Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: isDark ? AppColors.darkSurface : Colors.white, borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.edit_note, color: textSecondary, size: 20), const SizedBox(width: 8), Text( 'Votre message', style: AppTypography.headerSmall.copyWith(fontWeight: FontWeight.w600, color: textPrimary), ), ], ), const SizedBox(height: 8), TextField( controller: _messageController, maxLines: 6, style: AppTypography.bodyTextSmall.copyWith(color: textPrimary), decoration: InputDecoration( hintText: 'Décrivez votre suggestion, problème ou idée...', hintStyle: AppTypography.subtitleSmall.copyWith(color: textSecondary), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: isDark ? AppColors.darkBorder : Colors.grey[300]!), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: isDark ? AppColors.darkBorder : Colors.grey[300]!), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: AppColors.primaryGreen, width: 1.5), ), filled: true, fillColor: isDark ? AppColors.darkBackground : Colors.grey[50], alignLabelWithHint: true, ), ), ], ), ); } Widget _buildSubmitButton() { return SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: _isSending ? null : _submitFeedback, icon: _isSending ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), ) : const Icon(Icons.send, color: Colors.white), label: Text( _isSending ? 'Envoi en cours...' : 'Envoyer le commentaire', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white, ), ), style: ElevatedButton.styleFrom( backgroundColor: AppColors.primaryGreen, padding: const EdgeInsets.symmetric(vertical: 10), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), elevation: 2, ), ), ); } } class _FeedbackCategory { final String id; final String label; final IconData icon; final Color color; const _FeedbackCategory(this.id, this.label, this.icon, this.color); }