Files
dahoud 55f84da49a feat(ui): RefreshIndicator + AlwaysScrollable + dark mode sur 14 pages
RefreshIndicator ajouté (dispatche les events BLoC appropriés) :
- adhesion_detail, adhesions_page, demande_aide_detail, demandes_aide_page
- event_detail, organization_detail, org_selector, org_types
- user_management_detail, reports (TabBarView), logs (Dashboard tab)
- profile (onglet Perso), backup (3 onglets), notifications

Fixes associés :
- AlwaysScrollableScrollPhysics sur tous les scroll widgets
  (permet pull-to-refresh même si contenu < écran)
- Empty states des listes : wrappés dans SingleChildScrollView pour refresh
- Dark mode adaptatif sur textes/surfaces/borders hardcodés
- backup_page : bouton retour ajouté dans le header gradient
- org_types : chevron/star/border adaptatifs
- reports : couleurs placeholders graphique + chevrons
2026-04-15 20:13:50 +00:00

450 lines
13 KiB
Dart

/// Page de détails d'un événement
library event_detail_page;
import 'package:flutter/material.dart';
import '../../../../shared/design_system/tokens/module_colors.dart';
import '../../../../shared/design_system/tokens/color_tokens.dart';
import '../../../../shared/design_system/tokens/app_colors.dart';
import '../../../../shared/design_system/components/uf_app_bar.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../core/di/injection.dart';
import '../../bloc/evenements_bloc.dart';
import '../../bloc/evenements_state.dart';
import '../../data/models/evenement_model.dart';
import '../../domain/repositories/evenement_repository.dart';
import '../widgets/inscription_event_dialog.dart';
import '../widgets/edit_event_dialog.dart';
class EventDetailPage extends StatefulWidget {
final EvenementModel evenement;
const EventDetailPage({
super.key,
required this.evenement,
});
@override
State<EventDetailPage> createState() => _EventDetailPageState();
}
class _EventDetailPageState extends State<EventDetailPage> {
bool? _isInscrit;
@override
void initState() {
super.initState();
_loadInscriptionStatus();
}
Future<void> _loadInscriptionStatus() async {
final id = widget.evenement.id?.toString();
if (id == null || id.isEmpty) {
if (mounted) setState(() => _isInscrit = false);
return;
}
try {
final repo = getIt<IEvenementRepository>();
final value = await repo.getInscriptionStatus(id);
if (mounted) setState(() => _isInscrit = value);
} catch (_) {
if (mounted) setState(() => _isInscrit = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: UFAppBar(
title: "Détails de l'événement",
moduleGradient: ModuleColors.evenementsGradient,
actions: [
IconButton(
icon: const Icon(Icons.edit),
onPressed: () => _showEditDialog(context),
),
],
),
body: SafeArea(
top: false,
child: BlocBuilder<EvenementsBloc, EvenementsState>(
builder: (context, state) {
return RefreshIndicator(
color: ModuleColors.evenements,
onRefresh: _loadInscriptionStatus,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
_buildInfoSection(),
_buildDescriptionSection(),
if (widget.evenement.lieu != null) _buildLocationSection(),
_buildParticipantsSection(),
const SizedBox(height: 80), // Espace pour le bouton flottant
],
),
), // SingleChildScrollView
); // RefreshIndicator
},
),
),
floatingActionButton: _buildInscriptionButton(context),
);
}
Widget _buildHeader() {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: ModuleColors.evenementsGradient,
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Text(
_getTypeLabel(widget.evenement.type),
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 12),
Text(
widget.evenement.titre,
style: const TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: _getStatutColor(widget.evenement.statut),
borderRadius: BorderRadius.circular(4),
),
child: Text(
_getStatutLabel(widget.evenement.statut),
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
],
),
],
),
);
}
Widget _buildInfoSection() {
return Container(
padding: const EdgeInsets.all(10),
child: Column(
children: [
_buildInfoRow(
Icons.calendar_today,
'Date de début',
_formatDate(widget.evenement.dateDebut),
),
const Divider(),
_buildInfoRow(
Icons.event,
'Date de fin',
_formatDate(widget.evenement.dateFin),
),
if (widget.evenement.maxParticipants != null) ...[
const Divider(),
_buildInfoRow(
Icons.people,
'Places',
'${widget.evenement.participantsActuels} / ${widget.evenement.maxParticipants}',
),
],
if (widget.evenement.organisateurNom != null) ...[
const Divider(),
_buildInfoRow(
Icons.person,
'Organisateur',
widget.evenement.organisateurNom!,
),
],
],
),
);
}
Widget _buildInfoRow(IconData icon, String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Icon(icon, color: ModuleColors.evenements, size: 20),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 2),
Text(
value,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
),
);
}
Widget _buildDescriptionSection() {
if (widget.evenement.description == null) return const SizedBox.shrink();
return Container(
padding: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Description',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
widget.evenement.description!,
style: const TextStyle(fontSize: 14, height: 1.5),
),
],
),
);
}
Widget _buildLocationSection() {
return Container(
padding: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Lieu',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Row(
children: [
const Icon(Icons.location_on, color: ModuleColors.evenements),
const SizedBox(width: 8),
Expanded(
child: Text(
widget.evenement.lieu!,
style: const TextStyle(fontSize: 14),
),
),
],
),
],
),
);
}
Widget _buildParticipantsSection() {
return Container(
padding: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Participants',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
Text(
'${widget.evenement.participantsActuels} inscrits',
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: const Row(
children: [
Icon(Icons.info_outline, size: 20),
SizedBox(width: 8),
Expanded(
child: Text(
'La liste des participants est visible uniquement pour les organisateurs',
style: TextStyle(fontSize: 12),
),
),
],
),
),
],
),
);
}
Widget _buildInscriptionButton(BuildContext context) {
final isInscrit = _isInscrit ?? false;
final placesRestantes = (widget.evenement.maxParticipants ?? 0) -
widget.evenement.participantsActuels;
final isComplet = placesRestantes <= 0 && widget.evenement.maxParticipants != null;
if (!isComplet) {
return FloatingActionButton.extended(
onPressed: () => _showInscriptionDialog(context, isInscrit),
backgroundColor: ModuleColors.evenements,
icon: const Icon(Icons.check),
label: const Text('S\'inscrire'),
);
} else {
return const FloatingActionButton.extended(
onPressed: null,
backgroundColor: AppColors.textTertiary,
icon: Icon(Icons.block),
label: Text('Complet'),
);
}
}
void _showInscriptionDialog(BuildContext context, bool isInscrit) {
showDialog(
context: context,
builder: (context) => BlocProvider.value(
value: context.read<EvenementsBloc>(),
child: InscriptionEventDialog(
evenement: widget.evenement,
isInscrit: isInscrit,
),
),
).then((_) => _loadInscriptionStatus());
}
void _showEditDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => BlocProvider.value(
value: context.read<EvenementsBloc>(),
child: EditEventDialog(evenement: widget.evenement),
),
);
}
String _formatDate(DateTime date) {
final months = [
'janvier', 'février', 'mars', 'avril', 'mai', 'juin',
'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre'
];
return '${date.day} ${months[date.month - 1]} ${date.year} à ${date.hour}:${date.minute.toString().padLeft(2, '0')}';
}
String _getTypeLabel(TypeEvenement type) {
switch (type) {
case TypeEvenement.assembleeGenerale:
return 'Assemblée Générale';
case TypeEvenement.reunion:
return 'Réunion';
case TypeEvenement.formation:
return 'Formation';
case TypeEvenement.conference:
return 'Conférence';
case TypeEvenement.atelier:
return 'Atelier';
case TypeEvenement.seminaire:
return 'Séminaire';
case TypeEvenement.evenementSocial:
return 'Événement Social';
case TypeEvenement.manifestation:
return 'Manifestation';
case TypeEvenement.celebration:
return 'Célébration';
case TypeEvenement.autre:
return 'Autre';
}
}
String _getStatutLabel(StatutEvenement statut) {
switch (statut) {
case StatutEvenement.planifie:
return 'Planifié';
case StatutEvenement.confirme:
return 'Confirmé';
case StatutEvenement.enCours:
return 'En cours';
case StatutEvenement.termine:
return 'Terminé';
case StatutEvenement.annule:
return 'Annulé';
case StatutEvenement.reporte:
return 'Reporté';
}
}
Color _getStatutColor(StatutEvenement statut) {
switch (statut) {
case StatutEvenement.planifie:
return ColorTokens.info;
case StatutEvenement.confirme:
return ColorTokens.success;
case StatutEvenement.enCours:
return ColorTokens.warningLight;
case StatutEvenement.termine:
return ColorTokens.textSecondary;
case StatutEvenement.annule:
return ColorTokens.error;
case StatutEvenement.reporte:
return ColorTokens.warning;
}
}
}