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:
ElPoyo
2026-03-12 21:14:44 +01:00
parent 6737ad80e4
commit ecf4a5cede
11 changed files with 434 additions and 221 deletions

View File

@@ -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(() {

View File

@@ -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();
}
}

View File

@@ -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 ||