437 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			437 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| 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;
 | |
|   final Function(DateTime) onDaySelected;
 | |
|   final EventModel? selectedEvent;
 | |
| 
 | |
|   const WeekView({
 | |
|     super.key,
 | |
|     required this.focusedDay,
 | |
|     required this.events,
 | |
|     required this.onWeekChange,
 | |
|     required this.onEventSelected,
 | |
|     required this.onSwitchToMonth,
 | |
|     required this.onDaySelected,
 | |
|     required this.selectedEvent,
 | |
|   });
 | |
| 
 | |
|   @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;
 | |
|               final isSelected =
 | |
|                   selectedEvent != null && e.event.id == selectedEvent!.id;
 | |
| 
 | |
|               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: isSelected
 | |
|                           ? _getStatusColor(e.event.status).withAlpha(220)
 | |
|                           : _getStatusColor(e.event.status).withOpacity(0.18),
 | |
|                       border: Border.all(
 | |
|                         color: _getStatusColor(e.event.status),
 | |
|                         width: isSelected ? 3 : 1,
 | |
|                       ),
 | |
|                       borderRadius: BorderRadius.circular(4),
 | |
|                     ),
 | |
|                     child: Row(
 | |
|                       crossAxisAlignment: CrossAxisAlignment.center,
 | |
|                       children: [
 | |
|                         Expanded(
 | |
|                           child: Column(
 | |
|                             crossAxisAlignment: CrossAxisAlignment.start,
 | |
|                             children: [
 | |
|                               Text(
 | |
|                                 e.event.name,
 | |
|                                 style: TextStyle(
 | |
|                                   color: _getStatusTextColor(e.event.status),
 | |
|                                   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: TextStyle(
 | |
|                                     color: _getStatusTextColor(e.event.status),
 | |
|                                     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);
 | |
|   }
 | |
| 
 | |
|   Color _getStatusColor(EventStatus status) {
 | |
|     switch (status) {
 | |
|       case EventStatus.confirmed:
 | |
|         return Colors.green;
 | |
|       case EventStatus.canceled:
 | |
|         return Colors.red;
 | |
|       case EventStatus.waitingForApproval:
 | |
|       default:
 | |
|         return Colors.amber;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   Color _getStatusTextColor(EventStatus status) {
 | |
|     switch (status) {
 | |
|       case EventStatus.confirmed:
 | |
|       case EventStatus.canceled:
 | |
|         return Colors.white;
 | |
|       case EventStatus.waitingForApproval:
 | |
|       default:
 | |
|         return Colors.black;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   IconData _getStatusIcon(EventStatus status) {
 | |
|     switch (status) {
 | |
|       case EventStatus.confirmed:
 | |
|         return Icons.check;
 | |
|       case EventStatus.canceled:
 | |
|         return Icons.close;
 | |
|       case EventStatus.waitingForApproval:
 | |
|       default:
 | |
|         return Icons.hourglass_empty;
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| 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);
 | |
| }
 |