Compare commits

...

2 Commits

Author SHA1 Message Date
004d442e67 Passage en vue mobile du calendrier 2025-05-29 12:41:43 +02:00
77d0d5cc81 bouton qui fonctionne 2025-05-28 23:21:33 +02:00
4 changed files with 424 additions and 49 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/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,
events: eventProvider.events,
onSelectEvent: (event, date) {
setState(() {
_selectedEvent = event;
_selectedDay = date;
});
},
// 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,
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(
child: 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);

View File

@ -106,6 +106,18 @@ class EventDetails extends StatelessWidget {
.collection('events')
.doc(event.id)
.update({'status': eventStatusToString(newStatus)});
// Recharge l'événement depuis Firestore et notifie le parent
final snap = await FirebaseFirestore.instance
.collection('events')
.doc(event.id)
.get();
final updatedEvent =
EventModel.fromMap(snap.data()!, event.id);
onSelectEvent(updatedEvent,
selectedDate ?? updatedEvent.startDateTime);
// Met à jour uniquement l'événement dans le provider (rafraîchissement local et fluide)
await Provider.of<EventProvider>(context, listen: false)
.updateEvent(updatedEvent);
},
),
),
@ -628,44 +640,43 @@ class _FirestoreStatusButton extends StatefulWidget {
final String eventId;
final EventStatus currentStatus;
final Future<void> Function(EventStatus) onStatusChanged;
const _FirestoreStatusButton(
{required this.eventId,
required this.currentStatus,
required this.onStatusChanged});
const _FirestoreStatusButton({
required this.eventId,
required this.currentStatus,
required this.onStatusChanged,
});
@override
State<_FirestoreStatusButton> createState() => _FirestoreStatusButtonState();
}
class _FirestoreStatusButtonState extends State<_FirestoreStatusButton> {
late EventStatus _status;
bool _loading = false;
@override
void initState() {
super.initState();
_status = widget.currentStatus;
Future<void> changerStatut(EventStatus nouveau) async {
if (widget.currentStatus == nouveau) return;
setState(() => _loading = true);
await widget.onStatusChanged(nouveau);
setState(() => _loading = false);
}
Future<void> changerStatut(EventStatus nouveau) async {
if (_status == nouveau) return;
setState(() => _loading = true);
await FirebaseFirestore.instance
.collection('events')
.doc(widget.eventId)
.update({'status': eventStatusToString(nouveau)});
setState(() {
_status = nouveau;
_loading = false;
});
@override
void didUpdateWidget(covariant _FirestoreStatusButton oldWidget) {
super.didUpdateWidget(oldWidget);
// Si l'événement change, on arrête le loading (sécurité UX)
if (oldWidget.eventId != widget.eventId ||
oldWidget.currentStatus != widget.currentStatus) {
if (_loading) setState(() => _loading = false);
}
}
@override
Widget build(BuildContext context) {
final status = widget.currentStatus;
String texte;
Color couleurFond;
List<Widget> enfants = [];
switch (_status) {
switch (status) {
case EventStatus.waitingForApproval:
texte = "En Attente";
couleurFond = Colors.yellow.shade600;

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(
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,