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 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> _prepareEventsByDay(DateTime weekStart) { List> 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> _assignColumnsToEvents( List> eventsByDay) { return eventsByDay.map((dayEvents) { dayEvents.sort((a, b) => a.start.compareTo(b.start)); List<_PositionedEventWithColumn> result = []; List> 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); }