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:
@@ -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(() {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
Reference in New Issue
Block a user