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
450 lines
13 KiB
Dart
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;
|
|
}
|
|
}
|
|
}
|
|
|