Files
EM2_ERP/em2rp/lib/views/widgets/calendar_widgets/month_view.dart

375 lines
12 KiB
Dart

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 {
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;
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 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(_calendarPadding),
child: TableCalendar(
firstDay: DateTime.utc(2020, 1, 1),
lastDay: DateTime.utc(2030, 12, 31),
focusedDay: focusedDay,
calendarFormat: calendarFormat,
startingDayOfWeek: StartingDayOfWeek.monday,
locale: 'fr_FR',
daysOfWeekHeight: _daysOfWeekHeight,
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);
},
todayBuilder: (context, day, focusedDay) {
return _buildDayCell(day, false, isToday: true);
},
);
}
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);
BoxDecoration decoration;
if (isSelected) {
decoration = BoxDecoration(
color: AppColors.rouge,
border: Border.all(color: AppColors.rouge),
borderRadius: BorderRadius.circular(4),
);
} else if (isToday) {
decoration = BoxDecoration(
color: AppColors.rouge.withAlpha(26),
border: Border.all(color: AppColors.rouge),
borderRadius: BorderRadius.circular(4),
);
} else {
decoration = BoxDecoration(
color: null,
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(4),
);
}
return Container(
margin: const EdgeInsets.all(4),
decoration: decoration,
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),
),
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(),
),
),
),
],
],
),
),
);
}
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;
Color textColor;
IconData icon;
switch (event.status) {
case EventStatus.confirmed:
color = Colors.green;
textColor = Colors.white;
icon = Icons.check;
break;
case EventStatus.canceled:
color = Colors.red;
textColor = Colors.white;
icon = Icons.close;
break;
case EventStatus.waitingForApproval:
color = Colors.amber;
textColor = Colors.black;
icon = Icons.hourglass_empty;
break;
}
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 ? color.withAlpha(220) : color.withValues(alpha: 0.18),
borderRadius: BorderRadius.circular(4),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(icon, color: textColor, size: 16),
const SizedBox(width: 4),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
event.name,
style: TextStyle(
fontSize: 12,
color: textColor,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (CalendarUtils.isMultiDayEvent(event))
Text(
'Jour ${CalendarUtils.calculateDayNumber(event.startDateTime, currentDay)}/${CalendarUtils.calculateTotalDays(event)}',
style: TextStyle(
fontSize: 10,
color: textColor,
),
maxLines: 1,
),
],
),
),
],
),
),
);
}
/// 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();
}
}