feat: mise à jour de la version à 1.1.18 et amélioration de la page calendrier avec ajout de la fonctionnalité de rafraîchissement des événements
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:em2rp/providers/local_user_provider.dart';
|
||||
import 'package:em2rp/providers/event_provider.dart';
|
||||
import 'package:em2rp/utils/performance_monitor.dart';
|
||||
@@ -24,13 +26,22 @@ class CalendarPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _CalendarPageState extends State<CalendarPage> {
|
||||
static const double _minDetailsPaneFraction = 0.25;
|
||||
static const double _maxDetailsPaneFraction = 0.5;
|
||||
static const double _desktopResizeHandleWidth = 12;
|
||||
static const double _minCalendarPaneWidth = 480;
|
||||
static const double _minDetailsPaneWidth = 320;
|
||||
|
||||
CalendarFormat _calendarFormat = CalendarFormat.month;
|
||||
DateTime _focusedDay = DateTime.now();
|
||||
DateTime? _selectedDay;
|
||||
EventModel? _selectedEvent;
|
||||
bool _calendarCollapsed = false;
|
||||
int _selectedEventIndex = 0;
|
||||
String? _selectedUserId; // Filtre par utilisateur (null = tous les événements)
|
||||
String?
|
||||
_selectedUserId; // Filtre par utilisateur (null = tous les événements)
|
||||
bool _isRefreshing = false;
|
||||
double _detailsPaneFraction = 0.35;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -46,13 +57,15 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
Future<void> _loadCurrentMonthEvents() async {
|
||||
PerformanceMonitor.start('CalendarPage.loadCurrentMonthEvents');
|
||||
|
||||
final localAuthProvider = Provider.of<LocalUserProvider>(context, listen: false);
|
||||
final localAuthProvider =
|
||||
Provider.of<LocalUserProvider>(context, listen: false);
|
||||
final eventProvider = Provider.of<EventProvider>(context, listen: false);
|
||||
final userId = localAuthProvider.uid;
|
||||
final canViewAllEvents = localAuthProvider.hasPermission('view_all_events');
|
||||
|
||||
if (userId != null) {
|
||||
print('[CalendarPage] Loading events for ${_focusedDay.year}-${_focusedDay.month}');
|
||||
print(
|
||||
'[CalendarPage] Loading events for ${_focusedDay.year}-${_focusedDay.month}');
|
||||
|
||||
await eventProvider.loadMonthEvents(
|
||||
userId,
|
||||
@@ -79,6 +92,19 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
PerformanceMonitor.end('CalendarPage.loadCurrentMonthEvents');
|
||||
}
|
||||
|
||||
/// Vide le cache et recharge les événements du mois courant
|
||||
Future<void> _refreshEvents() async {
|
||||
if (_isRefreshing) return;
|
||||
setState(() => _isRefreshing = true);
|
||||
try {
|
||||
final eventProvider = Provider.of<EventProvider>(context, listen: false);
|
||||
eventProvider.clearAllCache();
|
||||
await _loadCurrentMonthEvents();
|
||||
} finally {
|
||||
if (mounted) setState(() => _isRefreshing = false);
|
||||
}
|
||||
}
|
||||
|
||||
/// Charge les événements de manière asynchrone et sélectionne l'événement approprié
|
||||
/// DEPRECATED: Utiliser _loadCurrentMonthEvents à la place
|
||||
Future<void> _loadEventsAsync() async {
|
||||
@@ -107,9 +133,10 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
final todayEvents = events.where((e) {
|
||||
final start = e.startDateTime;
|
||||
return start.year == now.year &&
|
||||
start.month == now.month &&
|
||||
start.day == now.day;
|
||||
}).toList()..sort((a, b) => a.startDateTime.compareTo(b.startDateTime));
|
||||
start.month == now.month &&
|
||||
start.day == now.day;
|
||||
}).toList()
|
||||
..sort((a, b) => a.startDateTime.compareTo(b.startDateTime));
|
||||
|
||||
EventModel? selected;
|
||||
DateTime? selectedDay;
|
||||
@@ -121,7 +148,8 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
// Chercher le prochain événement à venir
|
||||
final futureEvents = events
|
||||
.where((e) => e.startDateTime.isAfter(now))
|
||||
.toList()..sort((a, b) => a.startDateTime.compareTo(b.startDateTime));
|
||||
.toList()
|
||||
..sort((a, b) => a.startDateTime.compareTo(b.startDateTime));
|
||||
|
||||
if (futureEvents.isNotEmpty) {
|
||||
selected = futureEvents[0];
|
||||
@@ -186,21 +214,98 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
});
|
||||
}
|
||||
|
||||
double _clampDetailsPaneFraction(double fraction, double totalWidth) {
|
||||
if (totalWidth <= 0) {
|
||||
return fraction.clamp(_minDetailsPaneFraction, _maxDetailsPaneFraction);
|
||||
}
|
||||
|
||||
final minFractionFromPixels = _minDetailsPaneWidth / totalWidth;
|
||||
final maxFractionFromPixels =
|
||||
(totalWidth - _desktopResizeHandleWidth - _minCalendarPaneWidth) /
|
||||
totalWidth;
|
||||
|
||||
final minFraction =
|
||||
math.max(_minDetailsPaneFraction, minFractionFromPixels);
|
||||
final maxFraction =
|
||||
math.min(_maxDetailsPaneFraction, maxFractionFromPixels);
|
||||
|
||||
if (maxFraction < minFraction) {
|
||||
return fraction.clamp(_minDetailsPaneFraction, _maxDetailsPaneFraction);
|
||||
}
|
||||
|
||||
return fraction.clamp(minFraction, maxFraction);
|
||||
}
|
||||
|
||||
Widget _buildDesktopDetailsPane(List<EventModel> filteredEvents) {
|
||||
if (_selectedEvent != null) {
|
||||
return EventDetails(
|
||||
event: _selectedEvent!,
|
||||
selectedDate: _selectedDay,
|
||||
events: filteredEvents,
|
||||
onSelectEvent: (event, date) {
|
||||
setState(() {
|
||||
_selectedEvent = event;
|
||||
_selectedDay = date;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return Center(
|
||||
child: _selectedDay != null
|
||||
? const Text('Aucun événement ne démarre à cette date')
|
||||
: const Text('Sélectionnez un événement pour voir les détails'),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDesktopResizeHandle(double totalWidth) {
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.resizeLeftRight,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onHorizontalDragUpdate: (details) {
|
||||
setState(() {
|
||||
_detailsPaneFraction = _clampDetailsPaneFraction(
|
||||
_detailsPaneFraction - (details.delta.dx / totalWidth),
|
||||
totalWidth,
|
||||
);
|
||||
});
|
||||
},
|
||||
child: SizedBox(
|
||||
width: _desktopResizeHandleWidth,
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 4,
|
||||
margin: const EdgeInsets.symmetric(vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final eventProvider = Provider.of<EventProvider>(context);
|
||||
final localUserProvider = Provider.of<LocalUserProvider>(context);
|
||||
final canCreateEvents = localUserProvider.hasPermission('create_events');
|
||||
final canViewAllUserEvents = localUserProvider.hasPermission('view_all_user_events');
|
||||
final canViewAllUserEvents =
|
||||
localUserProvider.hasPermission('view_all_user_events');
|
||||
final isMobile = MediaQuery.of(context).size.width < 600;
|
||||
|
||||
// Appliquer le filtre utilisateur si actif
|
||||
final filteredEvents = _getFilteredEvents(eventProvider.events);
|
||||
|
||||
// Debug logs
|
||||
print('[CalendarPage.build] Total events: ${eventProvider.events.length}, Filtered: ${filteredEvents.length}');
|
||||
print(
|
||||
'[CalendarPage.build] Total events: ${eventProvider.events.length}, Filtered: ${filteredEvents.length}');
|
||||
if (eventProvider.events.isNotEmpty) {
|
||||
print('[CalendarPage.build] First event: ${eventProvider.events.first.name} at ${eventProvider.events.first.startDateTime}');
|
||||
print(
|
||||
'[CalendarPage.build] First event: ${eventProvider.events.first.name} at ${eventProvider.events.first.startDateTime}');
|
||||
}
|
||||
|
||||
if (eventProvider.isLoading) {
|
||||
@@ -214,6 +319,26 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
return Scaffold(
|
||||
appBar: CustomAppBar(
|
||||
title: "Calendrier",
|
||||
actions: [
|
||||
if (_isRefreshing)
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12),
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh, color: Colors.white),
|
||||
tooltip: 'Mettre à jour les événements',
|
||||
onPressed: _refreshEvents,
|
||||
),
|
||||
],
|
||||
),
|
||||
drawer: const MainDrawer(currentPage: '/calendar'),
|
||||
body: Column(
|
||||
@@ -247,7 +372,9 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
),
|
||||
// Corps du calendrier
|
||||
Expanded(
|
||||
child: isMobile ? _buildMobileLayout(filteredEvents) : _buildDesktopLayout(filteredEvents),
|
||||
child: isMobile
|
||||
? _buildMobileLayout(filteredEvents)
|
||||
: _buildDesktopLayout(filteredEvents),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -271,36 +398,30 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
}
|
||||
|
||||
Widget _buildDesktopLayout(List<EventModel> filteredEvents) {
|
||||
return Row(
|
||||
children: [
|
||||
// Calendrier (65% de la largeur)
|
||||
Expanded(
|
||||
flex: 65,
|
||||
child: _buildCalendar(filteredEvents),
|
||||
),
|
||||
// Détails de l'événement (35% de la largeur)
|
||||
Expanded(
|
||||
flex: 35,
|
||||
child: _selectedEvent != null
|
||||
? EventDetails(
|
||||
event: _selectedEvent!,
|
||||
selectedDate: _selectedDay,
|
||||
events: filteredEvents,
|
||||
onSelectEvent: (event, date) {
|
||||
setState(() {
|
||||
_selectedEvent = event;
|
||||
_selectedDay = date;
|
||||
});
|
||||
},
|
||||
)
|
||||
: Center(
|
||||
child: _selectedDay != null
|
||||
? Text('Aucun événement ne démarre à cette date')
|
||||
: const Text(
|
||||
'Sélectionnez un événement pour voir les détails'),
|
||||
),
|
||||
),
|
||||
],
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final totalWidth = constraints.maxWidth;
|
||||
final detailsPaneFraction =
|
||||
_clampDetailsPaneFraction(_detailsPaneFraction, totalWidth);
|
||||
final detailsWidth = totalWidth * detailsPaneFraction;
|
||||
final calendarWidth =
|
||||
totalWidth - _desktopResizeHandleWidth - detailsWidth;
|
||||
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: calendarWidth,
|
||||
child: _buildCalendar(filteredEvents),
|
||||
),
|
||||
_buildDesktopResizeHandle(totalWidth),
|
||||
SizedBox(
|
||||
width: detailsWidth,
|
||||
child: _buildDesktopDetailsPane(filteredEvents),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -341,19 +462,23 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
if (details.primaryVelocity != null) {
|
||||
if (details.primaryVelocity! < -200) {
|
||||
// Swipe gauche : mois suivant
|
||||
final newMonth = DateTime(_focusedDay.year, _focusedDay.month + 1, 1);
|
||||
final newMonth =
|
||||
DateTime(_focusedDay.year, _focusedDay.month + 1, 1);
|
||||
setState(() {
|
||||
_focusedDay = newMonth;
|
||||
});
|
||||
print('[CalendarPage] Month changed to ${newMonth.year}-${newMonth.month}');
|
||||
print(
|
||||
'[CalendarPage] Month changed to ${newMonth.year}-${newMonth.month}');
|
||||
_loadCurrentMonthEvents();
|
||||
} else if (details.primaryVelocity! > 200) {
|
||||
// Swipe droite : mois précédent
|
||||
final newMonth = DateTime(_focusedDay.year, _focusedDay.month - 1, 1);
|
||||
final newMonth =
|
||||
DateTime(_focusedDay.year, _focusedDay.month - 1, 1);
|
||||
setState(() {
|
||||
_focusedDay = newMonth;
|
||||
});
|
||||
print('[CalendarPage] Month changed to ${newMonth.year}-${newMonth.month}');
|
||||
print(
|
||||
'[CalendarPage] Month changed to ${newMonth.year}-${newMonth.month}');
|
||||
_loadCurrentMonthEvents();
|
||||
}
|
||||
}
|
||||
@@ -385,7 +510,8 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
setState(() {
|
||||
_focusedDay = newMonth;
|
||||
});
|
||||
print('[CalendarPage] Month changed to ${newMonth.year}-${newMonth.month}');
|
||||
print(
|
||||
'[CalendarPage] Month changed to ${newMonth.year}-${newMonth.month}');
|
||||
_loadCurrentMonthEvents();
|
||||
} else if (details.primaryVelocity! > 200) {
|
||||
// Swipe droite : mois précédent
|
||||
@@ -394,7 +520,8 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
setState(() {
|
||||
_focusedDay = newMonth;
|
||||
});
|
||||
print('[CalendarPage] Month changed to ${newMonth.year}-${newMonth.month}');
|
||||
print(
|
||||
'[CalendarPage] Month changed to ${newMonth.year}-${newMonth.month}');
|
||||
_loadCurrentMonthEvents();
|
||||
}
|
||||
}
|
||||
@@ -557,11 +684,13 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
icon: const Icon(Icons.chevron_left,
|
||||
color: AppColors.rouge, size: 28),
|
||||
onPressed: () {
|
||||
final newMonth = DateTime(_focusedDay.year, _focusedDay.month - 1, 1);
|
||||
final newMonth =
|
||||
DateTime(_focusedDay.year, _focusedDay.month - 1, 1);
|
||||
setState(() {
|
||||
_focusedDay = newMonth;
|
||||
});
|
||||
print('[CalendarPage] Month changed to ${newMonth.year}-${newMonth.month}');
|
||||
print(
|
||||
'[CalendarPage] Month changed to ${newMonth.year}-${newMonth.month}');
|
||||
_loadCurrentMonthEvents();
|
||||
},
|
||||
),
|
||||
@@ -600,11 +729,13 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
icon: const Icon(Icons.chevron_right,
|
||||
color: AppColors.rouge, size: 28),
|
||||
onPressed: () {
|
||||
final newMonth = DateTime(_focusedDay.year, _focusedDay.month + 1, 1);
|
||||
final newMonth =
|
||||
DateTime(_focusedDay.year, _focusedDay.month + 1, 1);
|
||||
setState(() {
|
||||
_focusedDay = newMonth;
|
||||
});
|
||||
print('[CalendarPage] Month changed to ${newMonth.year}-${newMonth.month}');
|
||||
print(
|
||||
'[CalendarPage] Month changed to ${newMonth.year}-${newMonth.month}');
|
||||
_loadCurrentMonthEvents();
|
||||
},
|
||||
),
|
||||
@@ -721,7 +852,7 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
onPageChanged: (focusedDay) {
|
||||
// Détecter si on a changé de mois
|
||||
final monthChanged = focusedDay.year != _focusedDay.year ||
|
||||
focusedDay.month != _focusedDay.month;
|
||||
focusedDay.month != _focusedDay.month;
|
||||
|
||||
setState(() {
|
||||
_focusedDay = focusedDay;
|
||||
@@ -729,7 +860,8 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
|
||||
// Charger les événements du nouveau mois si nécessaire
|
||||
if (monthChanged) {
|
||||
print('[CalendarPage] Month changed to ${focusedDay.year}-${focusedDay.month}');
|
||||
print(
|
||||
'[CalendarPage] Month changed to ${focusedDay.year}-${focusedDay.month}');
|
||||
_loadCurrentMonthEvents();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -26,6 +26,16 @@ class _EventStatusButtonState extends State<EventStatusButton> {
|
||||
EventStatus? _optimisticStatus;
|
||||
final DataService _dataService = DataService(FirebaseFunctionsApiService());
|
||||
|
||||
@override
|
||||
void didUpdateWidget(EventStatusButton oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
// Réinitialiser le statut optimiste si on affiche un nouvel événement
|
||||
if (oldWidget.event.id != widget.event.id) {
|
||||
_optimisticStatus = null;
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _changeStatus(EventStatus newStatus) async {
|
||||
if ((widget.event.status == newStatus) || _loading) return;
|
||||
setState(() {
|
||||
|
||||
@@ -5,6 +5,11 @@ import 'package:em2rp/models/event_model.dart';
|
||||
import 'package:em2rp/utils/calendar_utils.dart';
|
||||
|
||||
class MonthView extends StatelessWidget {
|
||||
static const double _calendarPadding = 8.0;
|
||||
static const double _headerHeight = 52.0;
|
||||
static const double _headerVerticalPadding = 16.0;
|
||||
static const double _daysOfWeekHeight = 16.0;
|
||||
|
||||
final DateTime focusedDay;
|
||||
final DateTime? selectedDay;
|
||||
final CalendarFormat calendarFormat;
|
||||
@@ -30,11 +35,17 @@ class MonthView extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final rowHeight = (constraints.maxHeight - 100) / 6;
|
||||
final rowCount = _computeRowCount(focusedDay);
|
||||
final availableHeight = constraints.maxHeight -
|
||||
(_calendarPadding * 2) -
|
||||
_headerHeight -
|
||||
_headerVerticalPadding -
|
||||
_daysOfWeekHeight;
|
||||
final rowHeight = availableHeight / rowCount;
|
||||
|
||||
return Container(
|
||||
height: constraints.maxHeight,
|
||||
padding: const EdgeInsets.all(8),
|
||||
padding: const EdgeInsets.all(_calendarPadding),
|
||||
child: TableCalendar(
|
||||
firstDay: DateTime.utc(2020, 1, 1),
|
||||
lastDay: DateTime.utc(2030, 12, 31),
|
||||
@@ -42,6 +53,7 @@ class MonthView extends StatelessWidget {
|
||||
calendarFormat: calendarFormat,
|
||||
startingDayOfWeek: StartingDayOfWeek.monday,
|
||||
locale: 'fr_FR',
|
||||
daysOfWeekHeight: _daysOfWeekHeight,
|
||||
availableCalendarFormats: const {
|
||||
CalendarFormat.month: 'Mois',
|
||||
CalendarFormat.week: 'Semaine',
|
||||
@@ -132,10 +144,9 @@ class MonthView extends StatelessWidget {
|
||||
|
||||
Widget _buildDayCell(DateTime day, bool isSelected, {bool isToday = false}) {
|
||||
final dayEvents = CalendarUtils.getEventsForDay(day, events);
|
||||
final statusCounts = _getStatusCounts(dayEvents);
|
||||
final textColor =
|
||||
isSelected ? Colors.white : (isToday ? AppColors.rouge : null);
|
||||
final badgeColor = isSelected ? Colors.white : AppColors.rouge;
|
||||
final badgeTextColor = isSelected ? AppColors.rouge : Colors.white;
|
||||
|
||||
BoxDecoration decoration;
|
||||
if (isSelected) {
|
||||
@@ -161,56 +172,125 @@ class MonthView extends StatelessWidget {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(4),
|
||||
decoration: decoration,
|
||||
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: Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
day.day.toString(),
|
||||
style: TextStyle(color: textColor),
|
||||
),
|
||||
child: Text(
|
||||
dayEvents.length.toString(),
|
||||
style: TextStyle(
|
||||
color: badgeTextColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: Wrap(
|
||||
spacing: 4,
|
||||
runSpacing: 2,
|
||||
alignment: WrapAlignment.end,
|
||||
children: _buildStatusBadges(statusCounts),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (dayEvents.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: dayEvents
|
||||
.map((event) => _buildEventItem(event, isSelected, day))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Map<EventStatus, int> _getStatusCounts(List<EventModel> dayEvents) {
|
||||
final counts = <EventStatus, int>{
|
||||
EventStatus.confirmed: 0,
|
||||
EventStatus.waitingForApproval: 0,
|
||||
EventStatus.canceled: 0,
|
||||
};
|
||||
|
||||
for (final event in dayEvents) {
|
||||
counts[event.status] = (counts[event.status] ?? 0) + 1;
|
||||
}
|
||||
|
||||
return counts;
|
||||
}
|
||||
|
||||
List<Widget> _buildStatusBadges(Map<EventStatus, int> statusCounts) {
|
||||
final badges = <Widget>[];
|
||||
|
||||
void addBadge({
|
||||
required EventStatus status,
|
||||
required Color backgroundColor,
|
||||
required Color textColor,
|
||||
required String tooltipLabel,
|
||||
}) {
|
||||
final count = statusCounts[status] ?? 0;
|
||||
if (count <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
badges.add(
|
||||
Tooltip(
|
||||
message: '$count $tooltipLabel',
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: Text(
|
||||
count.toString(),
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
addBadge(
|
||||
status: EventStatus.confirmed,
|
||||
backgroundColor: Colors.green,
|
||||
textColor: Colors.white,
|
||||
tooltipLabel:
|
||||
'validé${(statusCounts[EventStatus.confirmed] ?? 0) > 1 ? 's' : ''}',
|
||||
);
|
||||
addBadge(
|
||||
status: EventStatus.waitingForApproval,
|
||||
backgroundColor: Colors.amber,
|
||||
textColor: Colors.black,
|
||||
tooltipLabel: 'en attente',
|
||||
);
|
||||
addBadge(
|
||||
status: EventStatus.canceled,
|
||||
backgroundColor: Colors.red,
|
||||
textColor: Colors.white,
|
||||
tooltipLabel:
|
||||
'annulé${(statusCounts[EventStatus.canceled] ?? 0) > 1 ? 's' : ''}',
|
||||
);
|
||||
|
||||
return badges;
|
||||
}
|
||||
|
||||
Widget _buildEventItem(
|
||||
EventModel event, bool isSelected, DateTime currentDay) {
|
||||
Color color;
|
||||
@@ -228,7 +308,6 @@ class MonthView extends StatelessWidget {
|
||||
icon = Icons.close;
|
||||
break;
|
||||
case EventStatus.waitingForApproval:
|
||||
default:
|
||||
color = Colors.amber;
|
||||
textColor = Colors.black;
|
||||
icon = Icons.hourglass_empty;
|
||||
@@ -243,7 +322,8 @@ class MonthView extends StatelessWidget {
|
||||
margin: const EdgeInsets.only(bottom: 2),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? color.withAlpha(220) : color.withOpacity(0.18),
|
||||
color:
|
||||
isSelected ? color.withAlpha(220) : color.withValues(alpha: 0.18),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Row(
|
||||
@@ -282,4 +362,13 @@ class MonthView extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Calcule le nombre de rangées affichées pour le mois de [focusedDay]
|
||||
/// (calendrier commençant le lundi : offset = weekday - 1)
|
||||
int _computeRowCount(DateTime focusedDay) {
|
||||
final firstOfMonth = DateTime(focusedDay.year, focusedDay.month, 1);
|
||||
final daysInMonth = DateTime(focusedDay.year, focusedDay.month + 1, 0).day;
|
||||
final offset = (firstOfMonth.weekday - 1) % 7; // 0 = lundi, 6 = dimanche
|
||||
return ((daysInMonth + offset) / 7).ceil();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,8 @@ import 'package:em2rp/models/equipment_model.dart';
|
||||
import 'package:em2rp/models/container_model.dart';
|
||||
import 'package:em2rp/providers/equipment_provider.dart';
|
||||
import 'package:em2rp/providers/container_provider.dart';
|
||||
import 'package:em2rp/services/data_service.dart';
|
||||
import 'package:em2rp/services/api_service.dart';
|
||||
import 'package:em2rp/utils/colors.dart';
|
||||
import 'package:em2rp/views/widgets/event/equipment_selection_dialog.dart';
|
||||
import 'package:em2rp/services/event_availability_service.dart';
|
||||
|
||||
/// Section pour afficher et gérer le matériel assigné à un événement
|
||||
class EventAssignedEquipmentSection extends StatefulWidget {
|
||||
@@ -37,8 +34,6 @@ class EventAssignedEquipmentSection extends StatefulWidget {
|
||||
|
||||
class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSection> {
|
||||
bool get _canAddMaterial => widget.startDate != null && widget.endDate != null;
|
||||
final EventAvailabilityService _availabilityService = EventAvailabilityService();
|
||||
final DataService _dataService = DataService(FirebaseFunctionsApiService());
|
||||
final Map<String, EquipmentModel> _equipmentCache = {};
|
||||
final Map<String, ContainerModel> _containerCache = {};
|
||||
bool _isLoading = true;
|
||||
@@ -66,97 +61,53 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
||||
final equipmentProvider = context.read<EquipmentProvider>();
|
||||
final containerProvider = context.read<ContainerProvider>();
|
||||
|
||||
// 🔧 FIX: Si on a un eventId, utiliser getEventWithDetails pour charger les données complètes
|
||||
if (widget.eventId != null && widget.eventId!.isNotEmpty) {
|
||||
DebugLog.info('[EventAssignedEquipmentSection] Loading event with details: ${widget.eventId}');
|
||||
DebugLog.info('[EventAssignedEquipmentSection] Loading caches from assigned lists');
|
||||
|
||||
final result = await _dataService.getEventWithDetails(widget.eventId!);
|
||||
final equipmentsMap = result['equipments'] as Map<String, dynamic>;
|
||||
final containersMap = result['containers'] as Map<String, dynamic>;
|
||||
// Toujours partir des données locales du formulaire pour éviter les décalages visuels.
|
||||
final equipmentIds = widget.assignedEquipment.map((eq) => eq.equipmentId).toList();
|
||||
final containers = await containerProvider.getContainersByIds(widget.assignedContainers);
|
||||
|
||||
DebugLog.info('[EventAssignedEquipmentSection] Loaded ${equipmentsMap.length} equipments and ${containersMap.length} containers with details');
|
||||
final childEquipmentIds = <String>[];
|
||||
for (final container in containers) {
|
||||
childEquipmentIds.addAll(container.equipmentIds);
|
||||
}
|
||||
|
||||
// Construire les caches à partir des données reçues
|
||||
_equipmentCache.clear();
|
||||
_containerCache.clear();
|
||||
final allEquipmentIds = <String>{...equipmentIds, ...childEquipmentIds}.toList();
|
||||
final equipment = await equipmentProvider.getEquipmentsByIds(allEquipmentIds);
|
||||
|
||||
// Remplir le cache d'équipements
|
||||
equipmentsMap.forEach((id, data) {
|
||||
try {
|
||||
_equipmentCache[id] = EquipmentModel.fromMap(data as Map<String, dynamic>, id);
|
||||
} catch (e) {
|
||||
DebugLog.error('[EventAssignedEquipmentSection] Error parsing equipment $id', e);
|
||||
}
|
||||
});
|
||||
_equipmentCache.clear();
|
||||
_containerCache.clear();
|
||||
|
||||
// Remplir le cache de containers
|
||||
containersMap.forEach((id, data) {
|
||||
try {
|
||||
_containerCache[id] = ContainerModel.fromMap(data as Map<String, dynamic>, id);
|
||||
} catch (e) {
|
||||
DebugLog.error('[EventAssignedEquipmentSection] Error parsing container $id', e);
|
||||
}
|
||||
});
|
||||
for (final eq in widget.assignedEquipment) {
|
||||
final equipmentItem = equipment.firstWhere(
|
||||
(e) => e.id == eq.equipmentId,
|
||||
orElse: () => EquipmentModel(
|
||||
id: eq.equipmentId,
|
||||
name: 'Équipement inconnu',
|
||||
category: EquipmentCategory.other,
|
||||
status: EquipmentStatus.available,
|
||||
maintenanceIds: [],
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
),
|
||||
);
|
||||
_equipmentCache[eq.equipmentId] = equipmentItem;
|
||||
}
|
||||
|
||||
DebugLog.info('[EventAssignedEquipmentSection] Caches populated: ${_equipmentCache.length} equipments, ${_containerCache.length} containers');
|
||||
|
||||
} else {
|
||||
// Mode création d'événement : charger via les providers
|
||||
DebugLog.info('[EventAssignedEquipmentSection] Loading via providers (creation mode)');
|
||||
|
||||
// Extraire les IDs des équipements assignés
|
||||
final equipmentIds = widget.assignedEquipment
|
||||
.map((eq) => eq.equipmentId)
|
||||
.toList();
|
||||
|
||||
// Charger les conteneurs
|
||||
final containers = await containerProvider.getContainersByIds(widget.assignedContainers);
|
||||
|
||||
// Extraire les IDs des équipements enfants des containers
|
||||
final childEquipmentIds = <String>[];
|
||||
for (var container in containers) {
|
||||
childEquipmentIds.addAll(container.equipmentIds);
|
||||
}
|
||||
|
||||
// Combiner les IDs des équipements assignés + enfants des containers
|
||||
final allEquipmentIds = <String>{...equipmentIds, ...childEquipmentIds}.toList();
|
||||
|
||||
// Charger TOUS les équipements nécessaires
|
||||
final equipment = await equipmentProvider.getEquipmentsByIds(allEquipmentIds);
|
||||
|
||||
// Créer le cache des équipements
|
||||
for (var eq in widget.assignedEquipment) {
|
||||
final equipmentItem = equipment.firstWhere(
|
||||
(e) => e.id == eq.equipmentId,
|
||||
orElse: () => EquipmentModel(
|
||||
id: eq.equipmentId,
|
||||
name: 'Équipement inconnu',
|
||||
category: EquipmentCategory.other,
|
||||
status: EquipmentStatus.available,
|
||||
maintenanceIds: [],
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
),
|
||||
);
|
||||
_equipmentCache[eq.equipmentId] = equipmentItem;
|
||||
}
|
||||
|
||||
// Créer le cache des conteneurs
|
||||
for (var containerId in widget.assignedContainers) {
|
||||
final container = containers.firstWhere(
|
||||
(c) => c.id == containerId,
|
||||
orElse: () => ContainerModel(
|
||||
id: containerId,
|
||||
name: 'Conteneur inconnu',
|
||||
type: ContainerType.flightCase,
|
||||
status: EquipmentStatus.available,
|
||||
equipmentIds: [],
|
||||
updatedAt: DateTime.now(),
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
);
|
||||
_containerCache[containerId] = container;
|
||||
}
|
||||
for (final containerId in widget.assignedContainers) {
|
||||
final container = containers.firstWhere(
|
||||
(c) => c.id == containerId,
|
||||
orElse: () => ContainerModel(
|
||||
id: containerId,
|
||||
name: 'Conteneur inconnu',
|
||||
type: ContainerType.flightCase,
|
||||
status: EquipmentStatus.available,
|
||||
equipmentIds: [],
|
||||
updatedAt: DateTime.now(),
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
);
|
||||
_containerCache[containerId] = container;
|
||||
}
|
||||
} catch (e) {
|
||||
DebugLog.error('[EventAssignedEquipmentSection] Error loading equipment and containers', e);
|
||||
@@ -262,9 +213,6 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
||||
|
||||
// Notifier le changement
|
||||
widget.onChanged(updatedEquipment, updatedContainers);
|
||||
|
||||
// Recharger le cache
|
||||
await _loadEquipmentAndContainers();
|
||||
}
|
||||
|
||||
void _removeEquipment(String equipmentId) {
|
||||
@@ -519,7 +467,14 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
||||
|
||||
Widget _buildContainerItem(ContainerModel? container) {
|
||||
if (container == null) {
|
||||
return const SizedBox.shrink();
|
||||
return const Card(
|
||||
margin: EdgeInsets.only(bottom: 8),
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.inventory_2, color: Colors.grey),
|
||||
title: Text('Conteneur inconnu'),
|
||||
subtitle: Text('Données du conteneur indisponibles'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Card(
|
||||
@@ -609,7 +564,24 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
||||
|
||||
Widget _buildEquipmentItem(EquipmentModel? equipment, EventEquipment eventEq) {
|
||||
if (equipment == null) {
|
||||
return const SizedBox.shrink();
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
child: ListTile(
|
||||
leading: const CircleAvatar(
|
||||
backgroundColor: Color(0xFFE0E0E0),
|
||||
child: Icon(Icons.inventory_2, color: Colors.grey),
|
||||
),
|
||||
title: Text(
|
||||
eventEq.equipmentId,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
subtitle: const Text('Équipement indisponible dans le cache local'),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.delete, color: Colors.red),
|
||||
onPressed: () => _removeEquipment(eventEq.equipmentId),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final isConsumable = equipment.category == EquipmentCategory.consumable ||
|
||||
|
||||
Reference in New Issue
Block a user