Refacto et clean

This commit is contained in:
2025-05-18 20:34:57 +02:00
parent 62c6125d8c
commit 6adc90ecfe
13 changed files with 830 additions and 792 deletions

View File

@ -6,16 +6,16 @@ class ResetPasswordPage extends StatefulWidget {
final String actionCode;
const ResetPasswordPage({
Key? key,
super.key,
required this.email,
required this.actionCode,
}) : super(key: key);
});
@override
_ResetPasswordPageState createState() => _ResetPasswordPageState();
ResetPasswordPageState createState() => ResetPasswordPageState();
}
class _ResetPasswordPageState extends State<ResetPasswordPage> {
class ResetPasswordPageState extends State<ResetPasswordPage> {
final _formKey = GlobalKey<FormState>();
final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController();

View File

@ -0,0 +1,115 @@
import 'package:em2rp/models/event_model.dart';
class CalendarUtils {
static String getDayName(int weekday) {
switch (weekday) {
case DateTime.monday:
return 'Lundi';
case DateTime.tuesday:
return 'Mardi';
case DateTime.wednesday:
return 'Mercredi';
case DateTime.thursday:
return 'Jeudi';
case DateTime.friday:
return 'Vendredi';
case DateTime.saturday:
return 'Samedi';
case DateTime.sunday:
return 'Dimanche';
default:
return '';
}
}
static String getShortDayName(int weekday) {
switch (weekday) {
case DateTime.monday:
return 'Lun';
case DateTime.tuesday:
return 'Mar';
case DateTime.wednesday:
return 'Mer';
case DateTime.thursday:
return 'Jeu';
case DateTime.friday:
return 'Ven';
case DateTime.saturday:
return 'Sam';
case DateTime.sunday:
return 'Dim';
default:
return '';
}
}
static 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 '';
}
}
static String getMonthYearString(DateTime weekStart, DateTime weekEnd) {
if (weekStart.month == weekEnd.month) {
return '${getMonthName(weekStart.month)} ${weekStart.year}';
} else {
return '${getMonthName(weekStart.month)} - ${getMonthName(weekEnd.month)} ${weekEnd.year}';
}
}
static bool isMultiDayEvent(EventModel event) {
return event.startDateTime.day != event.endDateTime.day ||
event.startDateTime.month != event.endDateTime.month ||
event.startDateTime.year != event.endDateTime.year;
}
static int calculateTotalDays(EventModel event) {
final startDate = DateTime(event.startDateTime.year,
event.startDateTime.month, event.startDateTime.day);
final endDate = DateTime(
event.endDateTime.year, event.endDateTime.month, event.endDateTime.day);
return endDate.difference(startDate).inDays + 1;
}
static int calculateDayNumber(DateTime startDate, DateTime currentDay) {
final start = DateTime(startDate.year, startDate.month, startDate.day);
final current = DateTime(currentDay.year, currentDay.month, currentDay.day);
return current.difference(start).inDays + 1;
}
static List<EventModel> getEventsForDay(
DateTime day, List<EventModel> events) {
final dayStart = DateTime(day.year, day.month, day.day, 0, 0);
final dayEnd = DateTime(day.year, day.month, day.day, 23, 59, 59);
return events.where((event) {
return !(event.endDateTime.isBefore(dayStart) ||
event.startDateTime.isAfter(dayEnd));
}).toList();
}
}

View File

@ -27,8 +27,6 @@ class LoginViewModel extends ChangeNotifier {
notifyListeners();
try {
final userCredential = await localAuthProvider.signInWithEmailAndPassword(
emailController.text.trim(), passwordController.text);
print('User signed in');
// Attendre que les données utilisateur soient chargées

View File

@ -4,16 +4,15 @@ import 'package:flutter/material.dart';
import 'package:em2rp/widgets/custom_app_bar.dart';
import 'package:em2rp/views/widgets/nav/main_drawer.dart';
import 'package:provider/provider.dart';
import 'package:em2rp/utils/colors.dart';
import 'package:table_calendar/table_calendar.dart';
import 'package:em2rp/models/event_model.dart';
import 'package:em2rp/widgets/event_details.dart';
import 'package:latlong2/latlong.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:em2rp/views/widgets/calendar_widgets/month_view.dart';
import 'package:em2rp/views/widgets/calendar_widgets/week_view.dart';
class CalendarPage extends StatefulWidget {
const CalendarPage({Key? key}) : super(key: key);
const CalendarPage({super.key});
@override
State<CalendarPage> createState() => _CalendarPageState();
@ -51,7 +50,6 @@ class _CalendarPageState extends State<CalendarPage> {
@override
Widget build(BuildContext context) {
final localAuthProvider = Provider.of<LocalUserProvider>(context);
final eventProvider = Provider.of<EventProvider>(context);
final isMobile = MediaQuery.of(context).size.width < 600;
@ -111,41 +109,30 @@ class _CalendarPageState extends State<CalendarPage> {
}
Widget _buildCalendar() {
return LayoutBuilder(
builder: (context, constraints) {
if (_calendarFormat == CalendarFormat.week) {
return _buildWeekView(constraints);
} else {
return _buildMonthView(constraints);
}
},
);
}
final eventProvider = Provider.of<EventProvider>(context);
Widget _buildMonthView(BoxConstraints constraints) {
// Calculer la hauteur des lignes en fonction de la hauteur disponible
// Ajustement pour les mois à 6 semaines
final rowHeight = (constraints.maxHeight - 100) /
6; // Augmenté de 80 à 100 pour donner plus d'espace
return Container(
height: constraints.maxHeight,
padding:
const EdgeInsets.all(8), // Réduit de 16 à 8 pour gagner de l'espace
child: TableCalendar(
firstDay: DateTime.utc(2020, 1, 1),
lastDay: DateTime.utc(2030, 12, 31),
if (_calendarFormat == CalendarFormat.week) {
return WeekView(
focusedDay: _focusedDay,
events: eventProvider.events,
onWeekChange: _changeWeek,
onEventSelected: (event) {
setState(() {
_selectedEvent = event;
});
},
onSwitchToMonth: () {
setState(() {
_calendarFormat = CalendarFormat.month;
});
},
);
} else {
return MonthView(
focusedDay: _focusedDay,
selectedDay: _selectedDay,
calendarFormat: _calendarFormat,
startingDayOfWeek: StartingDayOfWeek.monday,
locale: 'fr_FR',
availableCalendarFormats: const {
CalendarFormat.month: 'Mois',
CalendarFormat.week: 'Semaine',
},
selectedDayPredicate: (day) {
return isSameDay(_selectedDay, day);
},
events: eventProvider.events,
onDaySelected: (selectedDay, focusedDay) {
setState(() {
_selectedDay = selectedDay;
@ -158,714 +145,16 @@ class _CalendarPageState extends State<CalendarPage> {
});
},
onPageChanged: (focusedDay) {
_focusedDay = focusedDay;
setState(() {
_focusedDay = focusedDay;
});
},
calendarStyle: CalendarStyle(
defaultDecoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(4),
),
selectedDecoration: BoxDecoration(
color: AppColors.rouge,
border: Border.all(color: AppColors.rouge),
borderRadius: BorderRadius.circular(4),
),
todayDecoration: BoxDecoration(
color: AppColors.rouge.withOpacity(0.1),
border: Border.all(color: AppColors.rouge),
borderRadius: BorderRadius.circular(4),
),
outsideDecoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(4),
),
outsideDaysVisible: false,
cellMargin: EdgeInsets.zero,
cellPadding: EdgeInsets.zero,
),
rowHeight: rowHeight,
headerStyle: HeaderStyle(
formatButtonVisible: true,
titleCentered: true,
formatButtonShowsNext: false,
formatButtonDecoration: BoxDecoration(
color: AppColors.rouge,
borderRadius: BorderRadius.circular(16),
),
formatButtonTextStyle: const TextStyle(color: Colors.white),
leftChevronIcon:
const Icon(Icons.chevron_left, color: AppColors.rouge),
rightChevronIcon:
const Icon(Icons.chevron_right, color: AppColors.rouge),
headerPadding: const EdgeInsets.symmetric(vertical: 8),
titleTextStyle: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
calendarBuilders: CalendarBuilders(
dowBuilder: (context, day) {
return Center(
child: Text(
_getDayName(day.weekday),
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
);
},
defaultBuilder: (context, day, focusedDay) {
final events = _getEventsForDay(day);
return Container(
margin: const EdgeInsets.all(4),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(4),
),
child: Stack(
children: [
// Numéro du jour en haut à gauche
Positioned(
top: 4,
left: 4,
child: Text(
day.day.toString(),
style: TextStyle(
color:
isSameDay(day, _selectedDay) ? Colors.white : null,
),
),
),
// Badge du nombre d'événements en haut à droite
if (events.isNotEmpty)
Positioned(
top: 4,
right: 4,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: isSameDay(day, _selectedDay)
? Colors.white
: AppColors.rouge,
borderRadius: BorderRadius.circular(10),
),
child: Text(
events.length.toString(),
style: TextStyle(
color: isSameDay(day, _selectedDay)
? AppColors.rouge
: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
),
// Liste des événements en dessous
if (events.isNotEmpty)
Positioned(
bottom: 2,
left: 2,
right: 2,
top: 28,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: events
.map((event) => GestureDetector(
onTap: () {
setState(() {
_selectedEvent = event;
_selectedDay = day;
});
},
child: Container(
margin: const EdgeInsets.only(bottom: 2),
padding: const EdgeInsets.symmetric(
horizontal: 4, vertical: 2),
decoration: BoxDecoration(
color: AppColors.rouge.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
event.name,
style: const TextStyle(
fontSize: 12,
color: AppColors.rouge,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (_isMultiDayEvent(event))
Text(
'Jour ${_calculateDayNumber(event.startDateTime, day)}/${_calculateTotalDays(event)}',
style: const TextStyle(
fontSize: 10,
color: AppColors.rouge,
),
maxLines: 1,
),
],
),
),
))
.toList(),
),
),
),
],
),
);
},
selectedBuilder: (context, day, focusedDay) {
final events = _getEventsForDay(day);
return Container(
margin: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: AppColors.rouge,
border: Border.all(color: AppColors.rouge),
borderRadius: BorderRadius.circular(4),
),
child: Stack(
children: [
// Numéro du jour en haut à gauche
Positioned(
top: 4,
left: 4,
child: Text(
day.day.toString(),
style: const TextStyle(color: Colors.white),
),
),
// Badge du nombre d'événements en haut à droite
if (events.isNotEmpty)
Positioned(
top: 4,
right: 4,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
),
child: Text(
events.length.toString(),
style: const TextStyle(
color: AppColors.rouge,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
),
// Liste des événements en dessous
if (events.isNotEmpty)
Positioned(
bottom: 2,
left: 2,
right: 2,
top: 28,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: events
.map((event) => GestureDetector(
onTap: () {
setState(() {
_selectedEvent = event;
});
},
child: Container(
margin: const EdgeInsets.only(bottom: 2),
padding: const EdgeInsets.symmetric(
horizontal: 4, vertical: 2),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(4),
),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
event.name,
style: const TextStyle(
fontSize: 12,
color: Colors.white,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (_isMultiDayEvent(event))
Text(
'Jour ${_calculateDayNumber(event.startDateTime, day)}/${_calculateTotalDays(event)}',
style: const TextStyle(
fontSize: 10,
color: Colors.white,
),
maxLines: 1,
),
],
),
),
))
.toList(),
),
),
),
],
),
);
},
),
),
);
}
Widget _buildWeekView(BoxConstraints constraints) {
final weekStart =
_focusedDay.subtract(Duration(days: _focusedDay.weekday - 1));
final weekEnd = weekStart.add(const Duration(days: 6));
// Ajustement de la hauteur pour éviter l'overflow
double availableHeight =
constraints.maxHeight - 80; // Réserver de l'espace pour les en-têtes
final hourHeight = availableHeight / 24;
final dayWidth = (constraints.maxWidth - 50) / 7;
// Préparer les événements par jour (en tenant compte des multi-jours)
List<List<_PositionedEvent>> eventsByDay = List.generate(7, (i) => []);
for (final event in Provider.of<EventProvider>(context).events) {
// Pour chaque jour de la semaine
for (int i = 0; i < 7; i++) {
final day = weekStart.add(Duration(days: i));
final dayStart = DateTime(day.year, day.month, day.day, 0, 0);
final dayEnd = DateTime(day.year, day.month, day.day, 23, 59, 59);
// Si l'événement recouvre ce jour
if (!(event.endDateTime.isBefore(dayStart) ||
event.startDateTime.isAfter(dayEnd))) {
// Tronquer les heures de début/fin si besoin
final start = event.startDateTime.isBefore(dayStart)
? dayStart
: event.startDateTime;
final end =
event.endDateTime.isAfter(dayEnd) ? dayEnd : event.endDateTime;
eventsByDay[i].add(_PositionedEvent(event, start, end));
}
}
}
// Pour chaque jour, calculer les "colonnes" d'événements qui se chevauchent
List<List<_PositionedEventWithColumn>> eventsWithColumnsByDay = [];
for (final dayEvents in eventsByDay) {
final columns = _assignColumns(dayEvents);
eventsWithColumnsByDay.add(columns);
}
return Column(
children: [
// Barre d'en-tête semaine
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: const Icon(Icons.chevron_left),
onPressed: () => _changeWeek(-1),
),
Text(
_getMonthYearString(weekStart, weekEnd),
style:
const TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
),
Row(
children: [
TextButton(
onPressed: () {
setState(() {
_calendarFormat = CalendarFormat.month;
});
},
style: TextButton.styleFrom(
backgroundColor: AppColors.rouge,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 8),
),
child: const Text('Semaine'),
),
IconButton(
icon: const Icon(Icons.chevron_right),
onPressed: () => _changeWeek(1),
),
],
),
],
),
),
// En-tête avec les jours
SizedBox(
height: 40,
child: Row(
children: [
Container(
width: 50,
color: Colors.transparent,
),
...List.generate(7, (index) {
final day = weekStart.add(Duration(days: index));
return Container(
width: dayWidth,
decoration: BoxDecoration(
border: Border(
right: BorderSide(
color: index < 6
? Colors.grey.shade300
: Colors.transparent,
width: 1,
),
),
),
child: Stack(
children: [
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(_getDayName(day.weekday),
style: const TextStyle(
fontWeight: FontWeight.bold)),
Text('${day.day}',
style: const TextStyle(fontSize: 13)),
],
),
),
if (_getEventsForDay(day).isNotEmpty)
Positioned(
top: 4,
right: 4,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: AppColors.rouge,
borderRadius: BorderRadius.circular(10),
),
child: Text(
_getEventsForDay(day).length.toString(),
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
);
}),
],
),
),
// Grille des heures + jours
Expanded(
child: SingleChildScrollView(
child: SizedBox(
height: 24 * hourHeight,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Colonne des heures
Column(
children: List.generate(24, (index) {
return Container(
width: 50,
height: hourHeight,
alignment: Alignment.topRight,
padding: const EdgeInsets.only(right: 4),
child: Text(
'${index.toString().padLeft(2, '0')}:00',
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 12,
),
),
);
}),
),
// Grille des jours
Expanded(
child: Stack(
children: [
// Lignes horizontales
Column(
children: List.generate(24, (index) {
return Container(
height: hourHeight,
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Colors.grey.shade300,
width: 0.5,
),
),
),
);
}),
),
// Bordures verticales entre jours
Positioned.fill(
child: Row(
children: List.generate(7, (i) {
return Container(
width: dayWidth,
decoration: BoxDecoration(
border: Border(
right: BorderSide(
color: i < 6
? Colors.grey.shade300
: Colors.transparent,
width: 1,
),
),
),
);
}),
),
),
// Événements (chevauchements et multi-jours)
...List.generate(7, (dayIdx) {
final dayEvents = eventsWithColumnsByDay[dayIdx];
return Stack(
children: dayEvents.map((e) {
final startHour =
e.start.hour + e.start.minute / 60;
final endHour = e.end.hour + e.end.minute / 60;
final duration = endHour - startHour;
final width = dayWidth / e.totalColumns;
return Positioned(
left: dayIdx * dayWidth + e.column * width,
top: startHour * hourHeight,
width: width,
height: duration * hourHeight,
child: GestureDetector(
onTap: () {
setState(() {
_selectedEvent = e.event;
_selectedDay =
weekStart.add(Duration(days: dayIdx));
});
},
child: Container(
margin: const EdgeInsets.all(2),
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: AppColors.rouge.withOpacity(0.2),
border:
Border.all(color: AppColors.rouge),
borderRadius: BorderRadius.circular(4),
),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
e.event.name,
style: const TextStyle(
color: AppColors.rouge,
fontSize: 12,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (_isMultiDayEvent(e.event))
Text(
'Jour ${_calculateDayNumber(e.event.startDateTime, weekStart.add(Duration(days: dayIdx)))}/${_calculateTotalDays(e.event)}',
style: const TextStyle(
color: AppColors.rouge,
fontSize: 10,
),
maxLines: 1,
),
],
),
),
),
);
}).toList(),
);
}),
],
),
),
],
),
),
),
),
],
);
}
// Méthode pour récupérer les événements pour un jour donné (inclut les multi-jours)
List<EventModel> _getEventsForDay(DateTime day) {
final dayStart = DateTime(day.year, day.month, day.day, 0, 0);
final dayEnd = DateTime(day.year, day.month, day.day, 23, 59, 59);
return Provider.of<EventProvider>(context).events.where((event) {
return !(event.endDateTime.isBefore(dayStart) ||
event.startDateTime.isAfter(dayEnd));
}).toList();
}
// Méthodes pour gérer les événements multi-jours
bool _isMultiDayEvent(EventModel event) {
return event.startDateTime.day != event.endDateTime.day ||
event.startDateTime.month != event.endDateTime.month ||
event.startDateTime.year != event.endDateTime.year;
}
int _calculateTotalDays(EventModel event) {
final startDate = DateTime(event.startDateTime.year,
event.startDateTime.month, event.startDateTime.day);
final endDate = DateTime(
event.endDateTime.year, event.endDateTime.month, event.endDateTime.day);
return endDate.difference(startDate).inDays + 1;
}
int _calculateDayNumber(DateTime startDate, DateTime currentDay) {
final start = DateTime(startDate.year, startDate.month, startDate.day);
final current = DateTime(currentDay.year, currentDay.month, currentDay.day);
return current.difference(start).inDays + 1;
}
String _getDayName(int weekday) {
switch (weekday) {
case DateTime.monday:
return 'Lun';
case DateTime.tuesday:
return 'Mar';
case DateTime.wednesday:
return 'Mer';
case DateTime.thursday:
return 'Jeu';
case DateTime.friday:
return 'Ven';
case DateTime.saturday:
return 'Sam';
case DateTime.sunday:
return 'Dim';
default:
return '';
}
}
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 '';
}
}
String _getMonthYearString(DateTime weekStart, DateTime weekEnd) {
if (weekStart.month == weekEnd.month) {
return '${_getMonthName(weekStart.month)} ${weekStart.year}';
} else {
return '${_getMonthName(weekStart.month)} - ${_getMonthName(weekEnd.month)} ${weekEnd.year}';
onEventSelected: (event) {
setState(() {
_selectedEvent = event;
});
},
);
}
}
}
class _PositionedEvent {
final EventModel event;
final DateTime start;
final DateTime end;
_PositionedEvent(this.event, this.start, this.end);
}
class _PositionedEventWithColumn extends _PositionedEvent {
final int column;
final int totalColumns;
_PositionedEventWithColumn(EventModel event, DateTime start, DateTime end,
this.column, this.totalColumns)
: super(event, start, end);
}
List<_PositionedEventWithColumn> _assignColumns(List<_PositionedEvent> events) {
// Algorithme simple :
// - Trier par heure de début
// - Pour chaque événement, trouver la première colonne libre
// - Attribuer le nombre total de colonnes pour le groupe de chevauchement
events.sort((a, b) => a.start.compareTo(b.start));
List<_PositionedEventWithColumn> result = [];
List<List<_PositionedEventWithColumn>> columns = [];
for (final e in events) {
bool placed = false;
for (int col = 0; col < columns.length; col++) {
if (columns[col].isEmpty || !(_overlap(columns[col].last, e))) {
columns[col]
.add(_PositionedEventWithColumn(e.event, e.start, e.end, col, 0));
placed = true;
break;
}
}
if (!placed) {
columns.add([
_PositionedEventWithColumn(e.event, e.start, e.end, columns.length, 0)
]);
}
}
// Mettre à jour le nombre total de colonnes pour chaque événement
int totalCols = columns.length;
for (final col in columns) {
for (final e in col) {
result.add(_PositionedEventWithColumn(
e.event, e.start, e.end, e.column, totalCols));
}
}
return result;
}
bool _overlap(_PositionedEvent a, _PositionedEvent b) {
return a.end.isAfter(b.start) && a.start.isBefore(b.end);
}

View File

@ -11,7 +11,7 @@ import 'package:em2rp/models/role_model.dart';
import 'package:em2rp/widgets/custom_app_bar.dart';
class UserManagementPage extends StatefulWidget {
const UserManagementPage({Key? key}) : super(key: key);
const UserManagementPage({super.key});
@override
State<UserManagementPage> createState() => _UserManagementPageState();
@ -21,8 +21,11 @@ class _UserManagementPageState extends State<UserManagementPage> {
@override
void initState() {
super.initState();
Future.microtask(
() => Provider.of<UsersProvider>(context, listen: false).fetchUsers());
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
Provider.of<UsersProvider>(context, listen: false).fetchUsers();
}
});
}
@override
@ -242,24 +245,29 @@ class _UserManagementPageState extends State<UserManagementPage> {
profilePhotoUrl: '',
);
final scaffoldMessenger = ScaffoldMessenger.of(context);
await Provider.of<UsersProvider>(context, listen: false)
.createUserWithEmailInvite(newUser);
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Invitation envoyée avec succès'),
backgroundColor: Colors.green,
),
);
if (context.mounted) {
Navigator.pop(context);
scaffoldMessenger.showSnackBar(
const SnackBar(
content: Text('Invitation envoyée avec succès'),
backgroundColor: Colors.green,
),
);
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Erreur lors de la création: ${e.toString()}'),
backgroundColor: Colors.red,
),
);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Erreur lors de la création: ${e.toString()}'),
backgroundColor: Colors.red,
),
);
}
}
},
style: ElevatedButton.styleFrom(

View File

@ -0,0 +1,238 @@
import 'package:flutter/material.dart';
import 'package:table_calendar/table_calendar.dart';
import 'package:em2rp/utils/colors.dart';
import 'package:em2rp/models/event_model.dart';
import 'package:em2rp/utils/calendar_utils.dart';
class MonthView extends StatelessWidget {
final DateTime focusedDay;
final DateTime? selectedDay;
final CalendarFormat calendarFormat;
final Function(DateTime, DateTime) onDaySelected;
final Function(CalendarFormat) onFormatChanged;
final Function(DateTime) onPageChanged;
final List<EventModel> events;
final Function(EventModel) onEventSelected;
const MonthView({
super.key,
required this.focusedDay,
required this.selectedDay,
required this.calendarFormat,
required this.onDaySelected,
required this.onFormatChanged,
required this.onPageChanged,
required this.events,
required this.onEventSelected,
});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final rowHeight = (constraints.maxHeight - 100) / 6;
return Container(
height: constraints.maxHeight,
padding: const EdgeInsets.all(8),
child: TableCalendar(
firstDay: DateTime.utc(2020, 1, 1),
lastDay: DateTime.utc(2030, 12, 31),
focusedDay: focusedDay,
calendarFormat: calendarFormat,
startingDayOfWeek: StartingDayOfWeek.monday,
locale: 'fr_FR',
availableCalendarFormats: const {
CalendarFormat.month: 'Mois',
CalendarFormat.week: 'Semaine',
},
selectedDayPredicate: (day) => isSameDay(selectedDay, day),
onDaySelected: onDaySelected,
onFormatChanged: onFormatChanged,
onPageChanged: onPageChanged,
calendarStyle: _buildCalendarStyle(),
rowHeight: rowHeight,
headerStyle: _buildHeaderStyle(),
calendarBuilders: _buildCalendarBuilders(),
),
);
},
);
}
CalendarStyle _buildCalendarStyle() {
return CalendarStyle(
defaultDecoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(4),
),
selectedDecoration: BoxDecoration(
color: AppColors.rouge,
border: Border.all(color: AppColors.rouge),
borderRadius: BorderRadius.circular(4),
),
todayDecoration: BoxDecoration(
color: AppColors.rouge.withAlpha(26),
border: Border.all(color: AppColors.rouge),
borderRadius: BorderRadius.circular(4),
),
outsideDecoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(4),
),
outsideDaysVisible: false,
cellMargin: EdgeInsets.zero,
cellPadding: EdgeInsets.zero,
);
}
HeaderStyle _buildHeaderStyle() {
return HeaderStyle(
formatButtonVisible: true,
titleCentered: true,
formatButtonShowsNext: false,
formatButtonDecoration: BoxDecoration(
color: AppColors.rouge,
borderRadius: BorderRadius.circular(16),
),
formatButtonTextStyle: const TextStyle(color: Colors.white),
leftChevronIcon: const Icon(Icons.chevron_left, color: AppColors.rouge),
rightChevronIcon: const Icon(Icons.chevron_right, color: AppColors.rouge),
headerPadding: const EdgeInsets.symmetric(vertical: 8),
titleTextStyle: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
);
}
CalendarBuilders _buildCalendarBuilders() {
return CalendarBuilders(
dowBuilder: (context, day) {
return Center(
child: Text(
CalendarUtils.getShortDayName(day.weekday),
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
);
},
defaultBuilder: (context, day, focusedDay) {
return _buildDayCell(day, false);
},
selectedBuilder: (context, day, focusedDay) {
return _buildDayCell(day, true);
},
);
}
Widget _buildDayCell(DateTime day, bool isSelected) {
final dayEvents = CalendarUtils.getEventsForDay(day, events);
final textColor = isSelected ? Colors.white : null;
final badgeColor = isSelected ? Colors.white : AppColors.rouge;
final badgeTextColor = isSelected ? AppColors.rouge : Colors.white;
return Container(
margin: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: isSelected ? AppColors.rouge : null,
border: Border.all(
color: isSelected ? AppColors.rouge : Colors.grey.shade300,
),
borderRadius: BorderRadius.circular(4),
),
child: Stack(
children: [
Positioned(
top: 4,
left: 4,
child: Text(
day.day.toString(),
style: TextStyle(color: textColor),
),
),
if (dayEvents.isNotEmpty)
Positioned(
top: 4,
right: 4,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: badgeColor,
borderRadius: BorderRadius.circular(10),
),
child: Text(
dayEvents.length.toString(),
style: TextStyle(
color: badgeTextColor,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
),
if (dayEvents.isNotEmpty)
Positioned(
bottom: 2,
left: 2,
right: 2,
top: 28,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: dayEvents
.map((event) => _buildEventItem(event, isSelected, day))
.toList(),
),
),
),
],
),
);
}
Widget _buildEventItem(
EventModel event, bool isSelected, DateTime currentDay) {
return GestureDetector(
onTap: () {
onDaySelected(currentDay, currentDay);
onEventSelected(event);
},
child: Container(
margin: const EdgeInsets.only(bottom: 2),
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
decoration: BoxDecoration(
color: isSelected
? Colors.white.withAlpha(51)
: AppColors.rouge.withAlpha(26),
borderRadius: BorderRadius.circular(4),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
event.name,
style: TextStyle(
fontSize: 12,
color: isSelected ? Colors.white : AppColors.rouge,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (CalendarUtils.isMultiDayEvent(event))
Text(
'Jour ${CalendarUtils.calculateDayNumber(event.startDateTime, event.startDateTime)}/${CalendarUtils.calculateTotalDays(event)}',
style: TextStyle(
fontSize: 10,
color: isSelected ? Colors.white : AppColors.rouge,
),
maxLines: 1,
),
],
),
),
);
}
}

View File

@ -0,0 +1,383 @@
import 'package:flutter/material.dart';
import 'package:em2rp/utils/colors.dart';
import 'package:em2rp/models/event_model.dart';
import 'package:em2rp/utils/calendar_utils.dart';
class WeekView extends StatelessWidget {
final DateTime focusedDay;
final List<EventModel> events;
final Function(int) onWeekChange;
final Function(EventModel) onEventSelected;
final Function() onSwitchToMonth;
const WeekView({
super.key,
required this.focusedDay,
required this.events,
required this.onWeekChange,
required this.onEventSelected,
required this.onSwitchToMonth,
});
@override
Widget build(BuildContext context) {
final weekStart =
focusedDay.subtract(Duration(days: focusedDay.weekday - 1));
final weekEnd = weekStart.add(const Duration(days: 6));
return Column(
children: [
_buildWeekHeader(weekStart, weekEnd),
Expanded(
child: LayoutBuilder(
builder: (context, constraints) {
final availableHeight = constraints.maxHeight - 80;
final hourHeight = availableHeight / 24;
final dayWidth = (constraints.maxWidth - 50) / 7;
return SingleChildScrollView(
child: SizedBox(
height: 24 * hourHeight,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHourColumn(hourHeight),
Expanded(
child: _buildWeekGrid(
weekStart,
hourHeight,
dayWidth,
constraints,
),
),
],
),
),
);
},
),
),
],
);
}
Widget _buildWeekHeader(DateTime weekStart, DateTime weekEnd) {
return Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: const Icon(Icons.chevron_left),
onPressed: () => onWeekChange(-1),
),
Text(
CalendarUtils.getMonthYearString(weekStart, weekEnd),
style:
const TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
),
Row(
children: [
TextButton(
onPressed: onSwitchToMonth,
style: TextButton.styleFrom(
backgroundColor: AppColors.rouge,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 8),
),
child: const Text('Semaine'),
),
IconButton(
icon: const Icon(Icons.chevron_right),
onPressed: () => onWeekChange(1),
),
],
),
],
),
),
_buildDaysHeader(weekStart),
],
);
}
Widget _buildDaysHeader(DateTime weekStart) {
return SizedBox(
height: 40,
child: Row(
children: [
Container(
width: 50,
color: Colors.transparent,
),
...List.generate(7, (index) {
final day = weekStart.add(Duration(days: index));
return Expanded(
child: Container(
decoration: BoxDecoration(
border: Border(
right: BorderSide(
color:
index < 6 ? Colors.grey.shade300 : Colors.transparent,
width: 1,
),
),
),
child: Stack(
children: [
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
CalendarUtils.getShortDayName(day.weekday),
style: const TextStyle(fontWeight: FontWeight.bold),
),
Text(
'${day.day}',
style: const TextStyle(fontSize: 13),
),
],
),
),
if (CalendarUtils.getEventsForDay(day, events).isNotEmpty)
Positioned(
top: 4,
right: 4,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: AppColors.rouge,
borderRadius: BorderRadius.circular(10),
),
child: Text(
CalendarUtils.getEventsForDay(day, events)
.length
.toString(),
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
);
}),
],
),
);
}
Widget _buildHourColumn(double hourHeight) {
return Column(
children: List.generate(24, (index) {
return Container(
width: 50,
height: hourHeight,
alignment: Alignment.topRight,
padding: const EdgeInsets.only(right: 4),
child: Text(
'${index.toString().padLeft(2, '0')}:00',
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 12,
),
),
);
}),
);
}
Widget _buildWeekGrid(
DateTime weekStart,
double hourHeight,
double dayWidth,
BoxConstraints constraints,
) {
final eventsByDay = _prepareEventsByDay(weekStart);
final eventsWithColumnsByDay = _assignColumnsToEvents(eventsByDay);
return Stack(
children: [
// Lignes horizontales
Column(
children: List.generate(24, (index) {
return Container(
height: hourHeight,
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Colors.grey.shade300,
width: 0.5,
),
),
),
);
}),
),
// Bordures verticales entre jours
Positioned.fill(
child: Row(
children: List.generate(7, (i) {
return Container(
width: dayWidth,
decoration: BoxDecoration(
border: Border(
right: BorderSide(
color: i < 6 ? Colors.grey.shade300 : Colors.transparent,
width: 1,
),
),
),
);
}),
),
),
// Événements
...List.generate(7, (dayIdx) {
final dayEvents = eventsWithColumnsByDay[dayIdx];
return Stack(
children: dayEvents.map((e) {
final startHour = e.start.hour + e.start.minute / 60;
final endHour = e.end.hour + e.end.minute / 60;
final duration = endHour - startHour;
final width = dayWidth / e.totalColumns;
return Positioned(
left: dayIdx * dayWidth + e.column * width,
top: startHour * hourHeight,
width: width,
height: duration * hourHeight,
child: GestureDetector(
onTap: () => onEventSelected(e.event),
child: Container(
margin: const EdgeInsets.all(2),
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: AppColors.rouge.withAlpha(26),
border: Border.all(color: AppColors.rouge),
borderRadius: BorderRadius.circular(4),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
e.event.name,
style: const TextStyle(
color: AppColors.rouge,
fontSize: 12,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (CalendarUtils.isMultiDayEvent(e.event))
Text(
'Jour ${CalendarUtils.calculateDayNumber(e.event.startDateTime, weekStart.add(Duration(days: dayIdx)))}/${CalendarUtils.calculateTotalDays(e.event)}',
style: const TextStyle(
color: AppColors.rouge,
fontSize: 10,
),
maxLines: 1,
),
],
),
),
),
);
}).toList(),
);
}),
],
);
}
List<List<_PositionedEvent>> _prepareEventsByDay(DateTime weekStart) {
List<List<_PositionedEvent>> eventsByDay = List.generate(7, (i) => []);
for (final event in events) {
for (int i = 0; i < 7; i++) {
final day = weekStart.add(Duration(days: i));
final dayStart = DateTime(day.year, day.month, day.day, 0, 0);
final dayEnd = DateTime(day.year, day.month, day.day, 23, 59, 59);
if (!(event.endDateTime.isBefore(dayStart) ||
event.startDateTime.isAfter(dayEnd))) {
final start = event.startDateTime.isBefore(dayStart)
? dayStart
: event.startDateTime;
final end =
event.endDateTime.isAfter(dayEnd) ? dayEnd : event.endDateTime;
eventsByDay[i].add(_PositionedEvent(event, start, end));
}
}
}
return eventsByDay;
}
List<List<_PositionedEventWithColumn>> _assignColumnsToEvents(
List<List<_PositionedEvent>> eventsByDay) {
return eventsByDay.map((dayEvents) {
dayEvents.sort((a, b) => a.start.compareTo(b.start));
List<_PositionedEventWithColumn> result = [];
List<List<_PositionedEventWithColumn>> columns = [];
for (final e in dayEvents) {
bool placed = false;
for (int col = 0; col < columns.length; col++) {
if (columns[col].isEmpty || !_overlap(columns[col].last, e)) {
columns[col].add(
_PositionedEventWithColumn(e.event, e.start, e.end, col, 0));
placed = true;
break;
}
}
if (!placed) {
columns.add([
_PositionedEventWithColumn(
e.event, e.start, e.end, columns.length, 0)
]);
}
}
int totalCols = columns.length;
for (final col in columns) {
for (final e in col) {
result.add(_PositionedEventWithColumn(
e.event, e.start, e.end, e.column, totalCols));
}
}
return result;
}).toList();
}
bool _overlap(_PositionedEvent a, _PositionedEvent b) {
return a.end.isAfter(b.start) && a.start.isBefore(b.end);
}
}
class _PositionedEvent {
final EventModel event;
final DateTime start;
final DateTime end;
_PositionedEvent(this.event, this.start, this.end);
}
class _PositionedEventWithColumn extends _PositionedEvent {
final int column;
final int totalColumns;
_PositionedEventWithColumn(
super.event, super.start, super.end, this.column, this.totalColumns);
}

View File

@ -7,7 +7,7 @@ class BigLeftImageWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
color: AppColors.gris.withOpacity(0.1),
color: AppColors.gris.withAlpha(26),
child: ClipRRect(
borderRadius: BorderRadius.zero,
child: Image.asset(

View File

@ -15,12 +15,12 @@ class _ProfilePictureSelectorState extends State<ProfilePictureSelector> {
bool _isHovering = false;
Future<void> _pickAndUploadImage() async {
if (!context.mounted) return;
final provider = Provider.of<LocalUserProvider>(context, listen: false);
final ImagePicker picker = ImagePicker();
final XFile? image = await picker.pickImage(source: ImageSource.gallery);
if (image != null) {
// Envoie l'image au provider
await Provider.of<LocalUserProvider>(context, listen: false)
.changeProfilePicture(image);
if (image != null && context.mounted) {
await provider.changeProfilePicture(image);
}
}

View File

@ -13,9 +13,9 @@ class MainDrawer extends StatelessWidget {
final String currentPage;
const MainDrawer({
Key? key,
super.key,
required this.currentPage,
}) : super(key: key);
});
@override
Widget build(BuildContext context) {
@ -33,7 +33,7 @@ class MainDrawer extends StatelessWidget {
image: const AssetImage('assets/EM2_NsurB.jpg'),
fit: BoxFit.cover,
colorFilter: ColorFilter.mode(
AppColors.noir.withOpacity(0.4),
AppColors.noir.withAlpha(102),
BlendMode.darken,
),
),
@ -88,7 +88,8 @@ class MainDrawer extends StatelessWidget {
Navigator.pop(context);
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const CalendarPage()),
MaterialPageRoute(
builder: (context) => const CalendarPage()),
);
},
),

View File

@ -10,11 +10,11 @@ class UserCard extends StatelessWidget {
static const double _desktopMaxWidth = 280;
const UserCard({
Key? key,
super.key,
required this.user,
required this.onEdit,
required this.onDelete,
}) : super(key: key);
});
@override
Widget build(BuildContext context) {

View File

@ -3,25 +3,33 @@ import 'package:provider/provider.dart';
import 'package:em2rp/providers/local_user_provider.dart';
import 'package:em2rp/utils/colors.dart';
class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
class CustomAppBar extends StatefulWidget implements PreferredSizeWidget {
final String title;
final List<Widget>? actions;
final bool showLogoutButton;
const CustomAppBar({
Key? key,
super.key,
required this.title,
this.actions,
this.showLogoutButton = true,
}) : super(key: key);
});
@override
State<CustomAppBar> createState() => _CustomAppBarState();
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}
class _CustomAppBarState extends State<CustomAppBar> {
@override
Widget build(BuildContext context) {
return AppBar(
title: Text(title),
title: Text(widget.title),
backgroundColor: AppColors.rouge,
actions: [
if (showLogoutButton)
if (widget.showLogoutButton)
IconButton(
icon: const Icon(Icons.logout, color: AppColors.blanc),
onPressed: () async {
@ -45,21 +53,19 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
),
);
if (shouldLogout == true) {
if (shouldLogout == true && context.mounted) {
// Déconnexion
await Provider.of<LocalUserProvider>(context, listen: false)
.signOut();
final provider =
Provider.of<LocalUserProvider>(context, listen: false);
await provider.signOut();
if (context.mounted) {
Navigator.of(context).pushReplacementNamed('/login');
}
}
},
),
if (actions != null) ...actions!,
if (widget.actions != null) ...widget.actions!,
],
);
}
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}

View File

@ -7,9 +7,9 @@ class EventDetails extends StatelessWidget {
final EventModel event;
const EventDetails({
Key? key,
super.key,
required this.event,
}) : super(key: key);
});
@override
Widget build(BuildContext context) {