395 lines
13 KiB
Dart
395 lines
13 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
|
|
? AppColors.rouge.withAlpha(80)
|
|
: AppColors.rouge.withAlpha(26),
|
|
border: Border.all(
|
|
color: AppColors.rouge,
|
|
width: isSelected ? 3 : 1,
|
|
),
|
|
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);
|
|
}
|