Files
unionflow-mobile-apps/lib/features/settings/presentation/pages/feedback_page.dart
dahoud 744faa3a9c feat(features): refontes onboarding/organizations/profile/reports/settings/solidarity
- onboarding : datasource souscription, models formule/status, bloc complet
- organizations : bloc orgs + switcher + types bloc, models, pages edit/create
- profile : bloc complet avec change password, delete account, preferences
- reports : bloc avec DashboardReports + ScheduleReports + GenerateReport
- settings : language, privacy, feedback pages
- solidarity : bloc complet demandes d'aide (CRUD, approuver, rejeter)
2026-04-15 20:27:12 +00:00

310 lines
12 KiB
Dart

/// Page dédiée à l'envoi de commentaires / feedback
library feedback_page;
import 'package:flutter/material.dart';
import '../../../../core/di/injection.dart';
import '../../../../core/network/api_client.dart';
import '../../../../core/utils/logger.dart';
import '../../../../shared/design_system/components/uf_app_bar.dart';
import '../../../../shared/design_system/unionflow_design_system.dart';
import '../../../../shared/widgets/core_card.dart';
// ─────────────────────────────────────────────────────────────────────────────
// Données statiques
// ─────────────────────────────────────────────────────────────────────────────
const _kCategories = [
_FeedbackCategory('suggestion', 'Suggestion', Icons.lightbulb_outline, AppColors.primary),
_FeedbackCategory('bug', 'Bug / Problème', Icons.bug_report_outlined, AppColors.error),
_FeedbackCategory('amelioration', 'Amélioration', Icons.trending_up, AppColors.success),
_FeedbackCategory('autre', 'Autre', Icons.help_outline, AppColors.primaryDark),
];
const _kMaxLength = 1000;
// ─────────────────────────────────────────────────────────────────────────────
// Page
// ─────────────────────────────────────────────────────────────────────────────
class FeedbackPage extends StatefulWidget {
const FeedbackPage({super.key});
@override
State<FeedbackPage> createState() => _FeedbackPageState();
}
class _FeedbackPageState extends State<FeedbackPage> {
final _messageController = TextEditingController();
String _selectedCategory = 'suggestion';
bool _isSending = false;
int _charCount = 0;
@override
void initState() {
super.initState();
_messageController.addListener(
() => setState(() => _charCount = _messageController.text.length),
);
}
@override
void dispose() {
_messageController.dispose();
super.dispose();
}
Future<void> _submitFeedback() async {
final message = _messageController.text.trim();
if (message.isEmpty) {
_showSnackBar('Veuillez saisir un message.', isError: true);
return;
}
setState(() => _isSending = true);
try {
final cat = _kCategories.firstWhere((c) => c.id == _selectedCategory);
await getIt<ApiClient>().post(
'/api/feedback',
data: {
'subject': '[${cat.label}] Feedback mobile',
'message': message,
},
);
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,
appBar: UFAppBar(
title: 'Commentaires',
moduleGradient: ModuleColors.supportGradient,
),
body: SafeArea(
top: false,
child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
children: [
const SizedBox(height: 4),
_buildCategorySection(),
const SizedBox(height: 8),
_buildMessageSection(),
const SizedBox(height: 12),
_buildSubmitButton(),
const SizedBox(height: 80),
],
),
),
);
}
// ── Section catégories ────────────────────────────────────────────────────
Widget _buildCategorySection() {
final scheme = Theme.of(context).colorScheme;
return CoreCard(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.category_outlined, color: scheme.onSurfaceVariant, size: 16),
const SizedBox(width: 8),
Text(
'TYPE DE RETOUR',
style: AppTypography.subtitleSmall.copyWith(
fontWeight: FontWeight.bold,
letterSpacing: 1.1,
color: scheme.onSurfaceVariant,
),
),
],
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: _kCategories.map(_buildCategoryChip).toList(),
),
],
),
);
}
Widget _buildCategoryChip(_FeedbackCategory cat) {
final scheme = Theme.of(context).colorScheme;
final isSelected = _selectedCategory == cat.id;
return InkWell(
onTap: () => setState(() => _selectedCategory = cat.id),
borderRadius: BorderRadius.circular(8),
child: AnimatedContainer(
duration: const Duration(milliseconds: 180),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: isSelected ? cat.color.withOpacity(0.1) : scheme.surface,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isSelected ? cat.color.withOpacity(0.5) : scheme.outlineVariant,
width: isSelected ? 1.5 : 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
cat.icon,
size: 15,
color: isSelected ? cat.color : scheme.onSurfaceVariant,
),
const SizedBox(width: 6),
Text(
cat.label,
style: AppTypography.actionText.copyWith(
fontSize: 12,
fontWeight: FontWeight.w600,
color: isSelected ? cat.color : scheme.onSurfaceVariant,
),
),
],
),
),
);
}
// ── Section message ────────────────────────────────────────────────────────
Widget _buildMessageSection() {
final scheme = Theme.of(context).colorScheme;
final isNearLimit = _charCount > _kMaxLength * 0.85;
return CoreCard(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.edit_note_outlined, color: scheme.onSurfaceVariant, size: 16),
const SizedBox(width: 8),
Text(
'VOTRE MESSAGE',
style: AppTypography.subtitleSmall.copyWith(
fontWeight: FontWeight.bold,
letterSpacing: 1.1,
color: scheme.onSurfaceVariant,
),
),
const Spacer(),
Text(
'$_charCount / $_kMaxLength',
style: AppTypography.subtitleSmall.copyWith(
fontSize: 10,
color: isNearLimit ? AppColors.error : scheme.onSurfaceVariant,
fontWeight: isNearLimit ? FontWeight.bold : FontWeight.normal,
),
),
],
),
const SizedBox(height: 12),
TextField(
controller: _messageController,
maxLines: 7,
maxLength: _kMaxLength,
buildCounter: (_, {required currentLength, required isFocused, maxLength}) =>
const SizedBox.shrink(),
style: AppTypography.bodyTextSmall.copyWith(
color: scheme.onSurface,
fontSize: 13,
),
decoration: InputDecoration(
hintText: 'Décrivez votre suggestion, problème ou idée...',
hintStyle: AppTypography.subtitleSmall.copyWith(
color: scheme.onSurfaceVariant,
),
filled: true,
fillColor: scheme.surfaceContainerHighest.withOpacity(0.4),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: scheme.outlineVariant),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: scheme.outlineVariant),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: ModuleColors.support, width: 1.5),
),
contentPadding: const EdgeInsets.all(12),
),
),
],
),
);
}
// ── Bouton envoi ──────────────────────────────────────────────────────────
Widget _buildSubmitButton() {
return SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: _isSending ? null : _submitFeedback,
icon: _isSending
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
)
: const Icon(Icons.send_rounded, color: Colors.white, size: 16),
label: Text(
_isSending ? 'Envoi en cours...' : 'Envoyer le commentaire',
style: AppTypography.actionText.copyWith(
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
style: ElevatedButton.styleFrom(
backgroundColor: ModuleColors.support,
disabledBackgroundColor: ModuleColors.support.withOpacity(0.5),
padding: const EdgeInsets.symmetric(vertical: 10),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
elevation: 0,
),
),
);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Modèle de catégorie
// ─────────────────────────────────────────────────────────────────────────────
class _FeedbackCategory {
final String id;
final String label;
final IconData icon;
final Color color;
const _FeedbackCategory(this.id, this.label, this.icon, this.color);
}