Passage en vue mobile du calendrier
This commit is contained in:
@ -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/week_view.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 {
|
||||
const CalendarPage({super.key});
|
||||
@ -24,12 +26,40 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
DateTime _focusedDay = DateTime.now();
|
||||
DateTime? _selectedDay;
|
||||
EventModel? _selectedEvent;
|
||||
bool _calendarCollapsed = false;
|
||||
int _selectedEventIndex = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initializeDateFormatting('fr_FR', null);
|
||||
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 {
|
||||
@ -69,8 +99,23 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: const CustomAppBar(
|
||||
title: 'Calendrier',
|
||||
appBar: CustomAppBar(
|
||||
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'),
|
||||
body: isMobile ? _buildMobileLayout() : _buildDesktopLayout(),
|
||||
@ -128,37 +173,221 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
|
||||
Widget _buildMobileLayout() {
|
||||
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: [
|
||||
// Calendrier
|
||||
Expanded(
|
||||
child: _buildCalendar(),
|
||||
),
|
||||
// Détails de l'événement
|
||||
if (_selectedEvent != null)
|
||||
Expanded(
|
||||
child: EventDetails(
|
||||
event: _selectedEvent!,
|
||||
selectedDate: _selectedDay,
|
||||
// Calendrier + détails en dessous
|
||||
AnimatedPositioned(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
curve: Curves.easeInOut,
|
||||
top: _calendarCollapsed ? -600 : 0, // cache le calendrier en haut
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: _calendarCollapsed ? 0 : null,
|
||||
child: Container(
|
||||
height: MediaQuery.of(context).size.height,
|
||||
child: Column(
|
||||
children: [
|
||||
_buildMonthHeader(context),
|
||||
if (!_calendarCollapsed)
|
||||
MobileCalendarView(
|
||||
focusedDay: _focusedDay,
|
||||
selectedDay: _selectedDay,
|
||||
events: eventProvider.events,
|
||||
onSelectEvent: (event, date) {
|
||||
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(() {
|
||||
_selectedEvent = event;
|
||||
_selectedDay = date;
|
||||
_selectedDay = day;
|
||||
_calendarCollapsed = false;
|
||||
_selectedEventIndex = 0;
|
||||
_selectedEvent =
|
||||
eventsForDay.isNotEmpty ? eventsForDay[0] : null;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
if (_selectedEvent == null && _selectedDay != null)
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Text('Aucun événement ne démarre à cette date'),
|
||||
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')),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// Vue détail (prend tout l'espace quand calendrier caché)
|
||||
if (_calendarCollapsed && _selectedDay != null)
|
||||
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() {
|
||||
final eventProvider = Provider.of<EventProvider>(context);
|
||||
|
||||
|
@ -115,16 +115,9 @@ class EventDetails extends StatelessWidget {
|
||||
EventModel.fromMap(snap.data()!, event.id);
|
||||
onSelectEvent(updatedEvent,
|
||||
selectedDate ?? updatedEvent.startDateTime);
|
||||
// Recharge la liste des événements pour mettre à jour la vue calendrier
|
||||
final localUserProvider =
|
||||
Provider.of<LocalUserProvider>(context, listen: false);
|
||||
final userId = localUserProvider.currentUser?.uid;
|
||||
final canViewAll =
|
||||
localUserProvider.hasPermission('view_all_users');
|
||||
if (userId != null) {
|
||||
// Met à jour uniquement l'événement dans le provider (rafraîchissement local et fluide)
|
||||
await Provider.of<EventProvider>(context, listen: false)
|
||||
.loadUserEvents(userId, canViewAllEvents: canViewAll);
|
||||
}
|
||||
.updateEvent(updatedEvent);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -283,10 +283,6 @@ class WeekView extends StatelessWidget {
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(_getStatusIcon(e.event.status),
|
||||
color: _getStatusTextColor(e.event.status),
|
||||
size: 16),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
Reference in New Issue
Block a user