import 'package:em2rp/models/event_model.dart'; import 'package:em2rp/models/event_statistics_models.dart'; import 'package:flutter/material.dart'; class EventStatisticsService { const EventStatisticsService(); static const double _taxRatio = 1.2; EventStatisticsSummary buildSummary({ required List events, required EventStatisticsFilter filter, required Map eventTypeNames, }) { final filteredEvents = events.where((event) => _matchesFilter(event, filter)).toList(); if (filteredEvents.isEmpty) { return EventStatisticsSummary.empty; } var validatedEvents = 0; var pendingEvents = 0; var canceledEvents = 0; var validatedAmount = 0.0; var pendingAmount = 0.0; var canceledAmount = 0.0; var baseAmount = 0.0; var optionsAmount = 0.0; final eventAmounts = []; final byType = {}; final optionStats = {}; for (final event in filteredEvents) { final base = _toHtAmount(event.basePrice); final optionTotal = _computeOptionsTotal(event); final amount = base + optionTotal; final isValidated = event.status == EventStatus.confirmed; eventAmounts.add(amount); baseAmount += base; optionsAmount += optionTotal; switch (event.status) { case EventStatus.confirmed: validatedEvents += 1; validatedAmount += amount; break; case EventStatus.waitingForApproval: pendingEvents += 1; pendingAmount += amount; break; case EventStatus.canceled: canceledEvents += 1; canceledAmount += amount; break; } final eventTypeId = event.eventTypeId; final eventTypeName = eventTypeNames[eventTypeId] ?? 'Type inconnu'; final typeAccumulator = byType.putIfAbsent( eventTypeId, () => _EventTypeAccumulator( eventTypeId: eventTypeId, eventTypeName: eventTypeName), ); typeAccumulator.totalEvents += 1; typeAccumulator.totalAmount += amount; switch (event.status) { case EventStatus.confirmed: typeAccumulator.validatedAmount += amount; break; case EventStatus.waitingForApproval: typeAccumulator.pendingAmount += amount; break; case EventStatus.canceled: typeAccumulator.canceledAmount += amount; break; } for (final rawOption in event.options) { final optionPrice = _toHtAmount(_toDouble(rawOption['price'])); final optionQuantity = _toInt(rawOption['quantity'], fallback: 1); if (optionPrice == 0) { continue; } final optionId = (rawOption['id'] ?? rawOption['code'] ?? rawOption['name'] ?? 'option') .toString(); final optionLabel = _buildOptionLabel(rawOption, optionId); final optionAmount = optionPrice * optionQuantity; final optionAccumulator = optionStats.putIfAbsent( optionId, () => _OptionAccumulator(optionKey: optionId, optionLabel: optionLabel), ); optionAccumulator.usageCount += 1; if (isValidated) { optionAccumulator.validatedUsageCount += 1; } optionAccumulator.quantity += optionQuantity; optionAccumulator.totalAmount += optionAmount; } } final byEventType = byType.values .map((accumulator) => EventTypeStatistics( eventTypeId: accumulator.eventTypeId, eventTypeName: accumulator.eventTypeName, totalEvents: accumulator.totalEvents, totalAmount: accumulator.totalAmount, validatedAmount: accumulator.validatedAmount, pendingAmount: accumulator.pendingAmount, canceledAmount: accumulator.canceledAmount, )) .toList() ..sort((a, b) => b.totalAmount.compareTo(a.totalAmount)); final topOptions = optionStats.values .map((accumulator) => OptionStatistics( optionKey: accumulator.optionKey, optionLabel: accumulator.optionLabel, usageCount: accumulator.usageCount, validatedUsageCount: accumulator.validatedUsageCount, quantity: accumulator.quantity, totalAmount: accumulator.totalAmount, )) .toList() ..sort((a, b) { final validatedComparison = b.validatedUsageCount.compareTo(a.validatedUsageCount); if (validatedComparison != 0) { return validatedComparison; } return b.totalAmount.compareTo(a.totalAmount); }); return EventStatisticsSummary( totalEvents: filteredEvents.length, validatedEvents: validatedEvents, pendingEvents: pendingEvents, canceledEvents: canceledEvents, totalAmount: validatedAmount + pendingAmount + canceledAmount, validatedAmount: validatedAmount, pendingAmount: pendingAmount, canceledAmount: canceledAmount, baseAmount: baseAmount, optionsAmount: optionsAmount, medianAmount: _computeMedian(eventAmounts), byEventType: byEventType, topOptions: topOptions.take(8).toList(), ); } bool _matchesFilter(EventModel event, EventStatisticsFilter filter) { if (!_overlapsRange(event, filter.period)) { return false; } if (!filter.selectedStatuses.contains(event.status)) { return false; } if (filter.eventTypeIds.isNotEmpty && !filter.eventTypeIds.contains(event.eventTypeId)) { return false; } return true; } bool _overlapsRange(EventModel event, DateTimeRange range) { return !event.endDateTime.isBefore(range.start) && !event.startDateTime.isAfter(range.end); } double _computeOptionsTotal(EventModel event) { return event.options.fold(0.0, (sum, option) { final optionPrice = _toHtAmount(_toDouble(option['price'])); final optionQuantity = _toInt(option['quantity'], fallback: 1); return sum + (optionPrice * optionQuantity); }); } double _toHtAmount(double storedAmount) { return storedAmount / _taxRatio; } double _toDouble(dynamic value) { if (value == null) { return 0.0; } if (value is num) { return value.toDouble(); } return double.tryParse(value.toString()) ?? 0.0; } int _toInt(dynamic value, {int fallback = 0}) { if (value == null) { return fallback; } if (value is int) { return value; } if (value is num) { return value.toInt(); } return int.tryParse(value.toString()) ?? fallback; } String _buildOptionLabel(Map option, String fallback) { final code = (option['code'] ?? '').toString().trim(); final name = (option['name'] ?? '').toString().trim(); if (code.isNotEmpty && name.isNotEmpty) { return '$code - $name'; } if (name.isNotEmpty) { return name; } if (code.isNotEmpty) { return code; } return fallback; } double _computeMedian(List values) { if (values.isEmpty) { return 0.0; } final sorted = [...values]..sort(); final middleIndex = sorted.length ~/ 2; if (sorted.length.isOdd) { return sorted[middleIndex]; } return (sorted[middleIndex - 1] + sorted[middleIndex]) / 2; } } class _EventTypeAccumulator { final String eventTypeId; final String eventTypeName; int totalEvents = 0; double totalAmount = 0.0; double validatedAmount = 0.0; double pendingAmount = 0.0; double canceledAmount = 0.0; _EventTypeAccumulator({ required this.eventTypeId, required this.eventTypeName, }); } class _OptionAccumulator { final String optionKey; final String optionLabel; int usageCount = 0; int validatedUsageCount = 0; int quantity = 0; double totalAmount = 0.0; _OptionAccumulator({ required this.optionKey, required this.optionLabel, }); }