Passage en vue mobile du calendrier

This commit is contained in:
2025-05-29 12:41:43 +02:00
parent 77d0d5cc81
commit 004d442e67
4 changed files with 395 additions and 38 deletions

View File

@ -11,6 +11,8 @@ import 'package:intl/date_symbol_data_local.dart';
import 'package:em2rp/views/widgets/calendar_widgets/month_view.dart'; import 'package:em2rp/views/widgets/calendar_widgets/month_view.dart';
import 'package:em2rp/views/widgets/calendar_widgets/week_view.dart'; import 'package:em2rp/views/widgets/calendar_widgets/week_view.dart';
import 'package:em2rp/views/pages/event_add_page.dart'; import 'package:em2rp/views/pages/event_add_page.dart';
import 'package:em2rp/views/widgets/calendar_widgets/mobile_calendar_view.dart';
import 'package:em2rp/utils/colors.dart';
class CalendarPage extends StatefulWidget { class CalendarPage extends StatefulWidget {
const CalendarPage({super.key}); const CalendarPage({super.key});
@ -24,12 +26,40 @@ class _CalendarPageState extends State<CalendarPage> {
DateTime _focusedDay = DateTime.now(); DateTime _focusedDay = DateTime.now();
DateTime? _selectedDay; DateTime? _selectedDay;
EventModel? _selectedEvent; EventModel? _selectedEvent;
bool _calendarCollapsed = false;
int _selectedEventIndex = 0;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
initializeDateFormatting('fr_FR', null); initializeDateFormatting('fr_FR', null);
Future.microtask(() => _loadEvents()); Future.microtask(() => _loadEvents());
// Sélection automatique de l'événement le plus proche de maintenant
WidgetsBinding.instance.addPostFrameCallback((_) {
final eventProvider = Provider.of<EventProvider>(context, listen: false);
final events = eventProvider.events;
if (events.isNotEmpty) {
final now = DateTime.now();
events.sort((a, b) => a.startDateTime.compareTo(b.startDateTime));
int closestIdx = 0;
Duration minDiff = (events[0].startDateTime.difference(now)).abs();
for (int i = 1; i < events.length; i++) {
final diff = (events[i].startDateTime.difference(now)).abs();
if (diff < minDiff) {
minDiff = diff;
closestIdx = i;
}
}
final closestEvent = events[closestIdx];
setState(() {
_selectedDay = DateTime(closestEvent.startDateTime.year,
closestEvent.startDateTime.month, closestEvent.startDateTime.day);
_focusedDay = _selectedDay!;
_selectedEventIndex = 0;
_selectedEvent = closestEvent;
});
}
});
} }
Future<void> _loadEvents() async { Future<void> _loadEvents() async {
@ -69,8 +99,23 @@ class _CalendarPageState extends State<CalendarPage> {
} }
return Scaffold( return Scaffold(
appBar: const CustomAppBar( appBar: CustomAppBar(
title: 'Calendrier', title: _getMonthName(_focusedDay.month),
actions: [
IconButton(
icon: Icon(
_calendarCollapsed
? Icons.keyboard_arrow_down
: Icons.keyboard_arrow_up,
color: AppColors.blanc,
),
onPressed: () {
setState(() {
_calendarCollapsed = !_calendarCollapsed;
});
},
),
],
), ),
drawer: const MainDrawer(currentPage: '/calendar'), drawer: const MainDrawer(currentPage: '/calendar'),
body: isMobile ? _buildMobileLayout() : _buildDesktopLayout(), body: isMobile ? _buildMobileLayout() : _buildDesktopLayout(),
@ -128,37 +173,221 @@ class _CalendarPageState extends State<CalendarPage> {
Widget _buildMobileLayout() { Widget _buildMobileLayout() {
final eventProvider = Provider.of<EventProvider>(context); final eventProvider = Provider.of<EventProvider>(context);
return Column( final eventsForSelectedDay = _selectedDay == null
? []
: eventProvider.events
.where((e) =>
e.startDateTime.year == _selectedDay!.year &&
e.startDateTime.month == _selectedDay!.month &&
e.startDateTime.day == _selectedDay!.day)
.toList()
..sort((a, b) => a.startDateTime.compareTo(b.startDateTime));
final hasEvents = eventsForSelectedDay.isNotEmpty;
final currentEvent =
hasEvents && _selectedEventIndex < eventsForSelectedDay.length
? eventsForSelectedDay[_selectedEventIndex]
: null;
return Stack(
children: [ children: [
// Calendrier // Calendrier + détails en dessous
Expanded( AnimatedPositioned(
child: _buildCalendar(), duration: const Duration(milliseconds: 400),
), curve: Curves.easeInOut,
// Détails de l'événement top: _calendarCollapsed ? -600 : 0, // cache le calendrier en haut
if (_selectedEvent != null) left: 0,
Expanded( right: 0,
child: EventDetails( height: _calendarCollapsed ? 0 : null,
event: _selectedEvent!, child: Container(
selectedDate: _selectedDay, height: MediaQuery.of(context).size.height,
events: eventProvider.events, child: Column(
onSelectEvent: (event, date) { children: [
setState(() { _buildMonthHeader(context),
_selectedEvent = event; if (!_calendarCollapsed)
_selectedDay = date; MobileCalendarView(
}); focusedDay: _focusedDay,
}, selectedDay: _selectedDay,
events: eventProvider.events,
onDaySelected: (day) {
final eventsForDay = eventProvider.events
.where((e) =>
e.startDateTime.year == day.year &&
e.startDateTime.month == day.month &&
e.startDateTime.day == day.day)
.toList()
..sort((a, b) =>
a.startDateTime.compareTo(b.startDateTime));
setState(() {
_selectedDay = day;
_calendarCollapsed = false;
_selectedEventIndex = 0;
_selectedEvent =
eventsForDay.isNotEmpty ? eventsForDay[0] : null;
});
},
),
Expanded(
child: hasEvents
? EventDetails(
event: eventsForSelectedDay[_selectedEventIndex],
selectedDate: _selectedDay,
events: eventsForSelectedDay.cast<EventModel>(),
onSelectEvent: (event, date) {
final idx = eventsForSelectedDay
.indexWhere((e) => e.id == event.id);
setState(() {
_selectedEventIndex = idx >= 0 ? idx : 0;
_selectedEvent = event;
});
},
)
: Center(
child:
Text('Aucun événement ne démarre à cette date')),
),
],
), ),
), ),
if (_selectedEvent == null && _selectedDay != null) ),
Expanded( // Vue détail (prend tout l'espace quand calendrier caché)
child: Center( if (_calendarCollapsed && _selectedDay != null)
child: Text('Aucun événement ne démarre à cette date'), AnimatedPositioned(
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOut,
top: _calendarCollapsed ? 0 : 600,
left: 0,
right: 0,
bottom: 0,
child: Container(
height: MediaQuery.of(context).size.height,
child: Column(
children: [
_buildMonthHeader(context),
Expanded(
child: Stack(
children: [
if (currentEvent != null)
EventDetails(
event: currentEvent,
selectedDate: _selectedDay,
events: eventsForSelectedDay.cast<EventModel>(),
onSelectEvent: (event, date) {
final idx = eventsForSelectedDay
.indexWhere((e) => e.id == event.id);
setState(() {
_selectedEventIndex = idx >= 0 ? idx : 0;
_selectedEvent = event;
});
},
),
if (!hasEvents)
Center(
child:
Text('Aucun événement ne démarre à cette date'),
),
],
),
),
],
),
), ),
), ),
], ],
); );
} }
Widget _buildMonthHeader(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 8, bottom: 8),
child: Row(
children: [
IconButton(
icon: const Icon(Icons.chevron_left,
color: AppColors.rouge, size: 28),
onPressed: () {
setState(() {
_focusedDay =
DateTime(_focusedDay.year, _focusedDay.month - 1, 1);
});
},
),
Expanded(
child: GestureDetector(
onTap: () {
setState(() {
_calendarCollapsed = !_calendarCollapsed;
});
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Text(
_getMonthName(_focusedDay.month),
style: const TextStyle(
color: AppColors.rouge,
fontWeight: FontWeight.bold,
fontSize: 20,
),
),
const SizedBox(width: 6),
Icon(
_calendarCollapsed
? Icons.keyboard_arrow_down
: Icons.keyboard_arrow_up,
color: AppColors.rouge,
size: 26,
),
],
),
),
),
IconButton(
icon: const Icon(Icons.chevron_right,
color: AppColors.rouge, size: 28),
onPressed: () {
setState(() {
_focusedDay =
DateTime(_focusedDay.year, _focusedDay.month + 1, 1);
});
},
),
],
),
);
}
String _getMonthName(int month) {
switch (month) {
case 1:
return 'Janvier';
case 2:
return 'Février';
case 3:
return 'Mars';
case 4:
return 'Avril';
case 5:
return 'Mai';
case 6:
return 'Juin';
case 7:
return 'Juillet';
case 8:
return 'Août';
case 9:
return 'Septembre';
case 10:
return 'Octobre';
case 11:
return 'Novembre';
case 12:
return 'Décembre';
default:
return '';
}
}
Widget _buildCalendar() { Widget _buildCalendar() {
final eventProvider = Provider.of<EventProvider>(context); final eventProvider = Provider.of<EventProvider>(context);

View File

@ -115,16 +115,9 @@ class EventDetails extends StatelessWidget {
EventModel.fromMap(snap.data()!, event.id); EventModel.fromMap(snap.data()!, event.id);
onSelectEvent(updatedEvent, onSelectEvent(updatedEvent,
selectedDate ?? updatedEvent.startDateTime); selectedDate ?? updatedEvent.startDateTime);
// Recharge la liste des événements pour mettre à jour la vue calendrier // Met à jour uniquement l'événement dans le provider (rafraîchissement local et fluide)
final localUserProvider = await Provider.of<EventProvider>(context, listen: false)
Provider.of<LocalUserProvider>(context, listen: false); .updateEvent(updatedEvent);
final userId = localUserProvider.currentUser?.uid;
final canViewAll =
localUserProvider.hasPermission('view_all_users');
if (userId != null) {
await Provider.of<EventProvider>(context, listen: false)
.loadUserEvents(userId, canViewAllEvents: canViewAll);
}
}, },
), ),
), ),

View File

@ -0,0 +1,139 @@
import 'package:flutter/material.dart';
import 'package:em2rp/models/event_model.dart';
class MobileCalendarView extends StatelessWidget {
final DateTime focusedDay;
final DateTime? selectedDay;
final List<EventModel> events;
final void Function(DateTime) onDaySelected;
const MobileCalendarView({
super.key,
required this.focusedDay,
required this.selectedDay,
required this.events,
required this.onDaySelected,
});
@override
Widget build(BuildContext context) {
final daysInMonth =
DateUtils.getDaysInMonth(focusedDay.year, focusedDay.month);
final firstDayOfMonth = DateTime(focusedDay.year, focusedDay.month, 1);
final firstWeekday = firstDayOfMonth.weekday;
final days = <DateTime>[];
// Ajoute les jours vides avant le 1er du mois (pour aligner sur le bon jour de la semaine)
for (int i = 1; i < firstWeekday; i++) {
days.add(DateTime(0)); // jour vide
}
// Ajoute les jours du mois
for (int i = 0; i < daysInMonth; i++) {
days.add(DateTime(focusedDay.year, focusedDay.month, i + 1));
}
// Complète la dernière semaine si besoin
while (days.length % 7 != 0) {
days.add(DateTime(0));
}
return Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(7, (i) {
const daysShort = ['L', 'M', 'M', 'J', 'V', 'S', 'D'];
return Expanded(
child: Center(
child: Text(daysShort[i],
style: const TextStyle(fontWeight: FontWeight.bold)),
),
);
}),
),
),
...List.generate(days.length ~/ 7, (week) {
return Row(
children: List.generate(7, (i) {
final day = days[week * 7 + i];
if (day.year == 0) {
return const Expanded(child: SizedBox.shrink());
}
final isSelected =
selectedDay != null && _isSameDay(day, selectedDay!);
final eventsForDay = events
.where((e) => _isSameDay(e.startDateTime, day))
.toList();
return Expanded(
child: GestureDetector(
onTap: () => onDaySelected(day),
child: Container(
color: Colors.transparent,
padding: const EdgeInsets.symmetric(vertical: 6),
child: Column(
children: [
Container(
decoration: isSelected
? BoxDecoration(
color: Theme.of(context)
.colorScheme
.primary
.withOpacity(0.15),
shape: BoxShape.circle,
)
: null,
padding: const EdgeInsets.all(4),
child: Text(
'${day.day}',
style: TextStyle(
fontWeight: FontWeight.w600,
color: isSelected
? Theme.of(context).colorScheme.primary
: null,
),
),
),
const SizedBox(height: 2),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: eventsForDay
.map((event) => Container(
margin: const EdgeInsets.symmetric(
horizontal: 1),
width: 6,
height: 6,
decoration: BoxDecoration(
color: _getColorForStatus(event.status),
shape: BoxShape.circle,
),
))
.toList(),
),
],
),
),
),
);
}),
);
}),
],
);
}
bool _isSameDay(DateTime a, DateTime b) {
return a.year == b.year && a.month == b.month && a.day == b.day;
}
Color _getColorForStatus(EventStatus status) {
switch (status) {
case EventStatus.confirmed:
return Colors.green;
case EventStatus.canceled:
return Colors.red;
case EventStatus.waitingForApproval:
default:
return Colors.amber;
}
}
}

View File

@ -283,10 +283,6 @@ class WeekView extends StatelessWidget {
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Icon(_getStatusIcon(e.event.status),
color: _getStatusTextColor(e.event.status),
size: 16),
const SizedBox(width: 4),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,