feat: Version 3.6.2 - Correctifs tâches #17-20
- #17: Amélioration gestion des secteurs et statistiques - #18: Optimisation services API et logs - #19: Corrections Flutter widgets et repositories - #20: Fix création passage - détection automatique ope_users.id vs users.id Suppression dossier web/ (migration vers app Flutter) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
594
app/lib/core/data/models/event_stats_model.dart
Normal file
594
app/lib/core/data/models/event_stats_model.dart
Normal file
@@ -0,0 +1,594 @@
|
||||
// Modèles pour les statistiques d'événements (connexions, passages, etc.)
|
||||
//
|
||||
// Ces modèles ne sont PAS stockés dans Hive car les données sont récupérées
|
||||
// à la demande depuis l'API et ne nécessitent pas de persistance locale.
|
||||
|
||||
/// Statistiques d'authentification
|
||||
class AuthStats {
|
||||
final int success;
|
||||
final int failed;
|
||||
final int logout;
|
||||
|
||||
const AuthStats({
|
||||
required this.success,
|
||||
required this.failed,
|
||||
required this.logout,
|
||||
});
|
||||
|
||||
factory AuthStats.fromJson(Map<String, dynamic> json) {
|
||||
return AuthStats(
|
||||
success: _parseInt(json['success']),
|
||||
failed: _parseInt(json['failed']),
|
||||
logout: _parseInt(json['logout']),
|
||||
);
|
||||
}
|
||||
|
||||
int get total => success + failed + logout;
|
||||
}
|
||||
|
||||
/// Statistiques de passages
|
||||
class PassageStats {
|
||||
final int created;
|
||||
final int updated;
|
||||
final int deleted;
|
||||
final double amount;
|
||||
|
||||
const PassageStats({
|
||||
required this.created,
|
||||
required this.updated,
|
||||
required this.deleted,
|
||||
required this.amount,
|
||||
});
|
||||
|
||||
factory PassageStats.fromJson(Map<String, dynamic> json) {
|
||||
return PassageStats(
|
||||
created: _parseInt(json['created']),
|
||||
updated: _parseInt(json['updated']),
|
||||
deleted: _parseInt(json['deleted']),
|
||||
amount: _parseDouble(json['amount']),
|
||||
);
|
||||
}
|
||||
|
||||
int get total => created + updated + deleted;
|
||||
}
|
||||
|
||||
/// Statistiques utilisateurs
|
||||
class UserStats {
|
||||
final int created;
|
||||
final int updated;
|
||||
final int deleted;
|
||||
|
||||
const UserStats({
|
||||
required this.created,
|
||||
required this.updated,
|
||||
required this.deleted,
|
||||
});
|
||||
|
||||
factory UserStats.fromJson(Map<String, dynamic> json) {
|
||||
return UserStats(
|
||||
created: _parseInt(json['created']),
|
||||
updated: _parseInt(json['updated']),
|
||||
deleted: _parseInt(json['deleted']),
|
||||
);
|
||||
}
|
||||
|
||||
int get total => created + updated + deleted;
|
||||
}
|
||||
|
||||
/// Statistiques secteurs
|
||||
class SectorStats {
|
||||
final int created;
|
||||
final int updated;
|
||||
final int deleted;
|
||||
|
||||
const SectorStats({
|
||||
required this.created,
|
||||
required this.updated,
|
||||
required this.deleted,
|
||||
});
|
||||
|
||||
factory SectorStats.fromJson(Map<String, dynamic> json) {
|
||||
return SectorStats(
|
||||
created: _parseInt(json['created']),
|
||||
updated: _parseInt(json['updated']),
|
||||
deleted: _parseInt(json['deleted']),
|
||||
);
|
||||
}
|
||||
|
||||
int get total => created + updated + deleted;
|
||||
}
|
||||
|
||||
/// Statistiques Stripe
|
||||
class StripeStats {
|
||||
final int created;
|
||||
final int success;
|
||||
final int failed;
|
||||
final int cancelled;
|
||||
final double amount;
|
||||
|
||||
const StripeStats({
|
||||
required this.created,
|
||||
required this.success,
|
||||
required this.failed,
|
||||
required this.cancelled,
|
||||
required this.amount,
|
||||
});
|
||||
|
||||
factory StripeStats.fromJson(Map<String, dynamic> json) {
|
||||
return StripeStats(
|
||||
created: _parseInt(json['created']),
|
||||
success: _parseInt(json['success']),
|
||||
failed: _parseInt(json['failed']),
|
||||
cancelled: _parseInt(json['cancelled']),
|
||||
amount: _parseDouble(json['amount']),
|
||||
);
|
||||
}
|
||||
|
||||
int get total => created + success + failed + cancelled;
|
||||
}
|
||||
|
||||
/// Statistiques globales d'une journée
|
||||
class DayStats {
|
||||
final AuthStats auth;
|
||||
final PassageStats passages;
|
||||
final UserStats users;
|
||||
final SectorStats sectors;
|
||||
final StripeStats stripe;
|
||||
|
||||
const DayStats({
|
||||
required this.auth,
|
||||
required this.passages,
|
||||
required this.users,
|
||||
required this.sectors,
|
||||
required this.stripe,
|
||||
});
|
||||
|
||||
factory DayStats.fromJson(Map<String, dynamic> json) {
|
||||
return DayStats(
|
||||
auth: AuthStats.fromJson(json['auth'] ?? {}),
|
||||
passages: PassageStats.fromJson(json['passages'] ?? {}),
|
||||
users: UserStats.fromJson(json['users'] ?? {}),
|
||||
sectors: SectorStats.fromJson(json['sectors'] ?? {}),
|
||||
stripe: StripeStats.fromJson(json['stripe'] ?? {}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Totaux d'une journée
|
||||
class DayTotals {
|
||||
final int events;
|
||||
final int uniqueUsers;
|
||||
|
||||
const DayTotals({
|
||||
required this.events,
|
||||
required this.uniqueUsers,
|
||||
});
|
||||
|
||||
factory DayTotals.fromJson(Map<String, dynamic> json) {
|
||||
return DayTotals(
|
||||
events: _parseInt(json['events']),
|
||||
uniqueUsers: _parseInt(json['unique_users']),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Résumé complet d'une journée (réponse de /stats/summary)
|
||||
class EventSummary {
|
||||
final DateTime date;
|
||||
final DayStats stats;
|
||||
final DayTotals totals;
|
||||
|
||||
const EventSummary({
|
||||
required this.date,
|
||||
required this.stats,
|
||||
required this.totals,
|
||||
});
|
||||
|
||||
factory EventSummary.fromJson(Map<String, dynamic> json) {
|
||||
return EventSummary(
|
||||
date: DateTime.parse(json['date']),
|
||||
stats: DayStats.fromJson(json['stats'] ?? {}),
|
||||
totals: DayTotals.fromJson(json['totals'] ?? {}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Statistiques d'un type d'événement pour une période
|
||||
class EventTypeStats {
|
||||
final int count;
|
||||
final double sumAmount;
|
||||
final int uniqueUsers;
|
||||
|
||||
const EventTypeStats({
|
||||
required this.count,
|
||||
required this.sumAmount,
|
||||
required this.uniqueUsers,
|
||||
});
|
||||
|
||||
factory EventTypeStats.fromJson(Map<String, dynamic> json) {
|
||||
return EventTypeStats(
|
||||
count: _parseInt(json['count']),
|
||||
sumAmount: _parseDouble(json['sum_amount']),
|
||||
uniqueUsers: _parseInt(json['unique_users']),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Données d'une journée dans les stats quotidiennes
|
||||
class DailyStatsEntry {
|
||||
final DateTime date;
|
||||
final Map<String, EventTypeStats> events;
|
||||
final int totalCount;
|
||||
final double totalAmount;
|
||||
|
||||
const DailyStatsEntry({
|
||||
required this.date,
|
||||
required this.events,
|
||||
required this.totalCount,
|
||||
required this.totalAmount,
|
||||
});
|
||||
|
||||
factory DailyStatsEntry.fromJson(Map<String, dynamic> json) {
|
||||
final eventsJson = json['events'] as Map<String, dynamic>? ?? {};
|
||||
final events = <String, EventTypeStats>{};
|
||||
|
||||
for (final entry in eventsJson.entries) {
|
||||
events[entry.key] = EventTypeStats.fromJson(entry.value);
|
||||
}
|
||||
|
||||
final totals = json['totals'] as Map<String, dynamic>? ?? {};
|
||||
|
||||
return DailyStatsEntry(
|
||||
date: DateTime.parse(json['date']),
|
||||
events: events,
|
||||
totalCount: _parseInt(totals['count']),
|
||||
totalAmount: _parseDouble(totals['sum_amount']),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Réponse des stats quotidiennes (/stats/daily)
|
||||
class DailyStats {
|
||||
final DateTime from;
|
||||
final DateTime to;
|
||||
final List<DailyStatsEntry> days;
|
||||
|
||||
const DailyStats({
|
||||
required this.from,
|
||||
required this.to,
|
||||
required this.days,
|
||||
});
|
||||
|
||||
factory DailyStats.fromJson(Map<String, dynamic> json) {
|
||||
final daysJson = json['days'] as List<dynamic>? ?? [];
|
||||
|
||||
return DailyStats(
|
||||
from: DateTime.parse(json['from']),
|
||||
to: DateTime.parse(json['to']),
|
||||
days: daysJson.map((d) => DailyStatsEntry.fromJson(d)).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Données d'une semaine dans les stats hebdomadaires
|
||||
class WeeklyStatsEntry {
|
||||
final DateTime weekStart;
|
||||
final int weekNumber;
|
||||
final int year;
|
||||
final Map<String, EventTypeStats> events;
|
||||
final int totalCount;
|
||||
final double totalAmount;
|
||||
|
||||
const WeeklyStatsEntry({
|
||||
required this.weekStart,
|
||||
required this.weekNumber,
|
||||
required this.year,
|
||||
required this.events,
|
||||
required this.totalCount,
|
||||
required this.totalAmount,
|
||||
});
|
||||
|
||||
factory WeeklyStatsEntry.fromJson(Map<String, dynamic> json) {
|
||||
final eventsJson = json['events'] as Map<String, dynamic>? ?? {};
|
||||
final events = <String, EventTypeStats>{};
|
||||
|
||||
for (final entry in eventsJson.entries) {
|
||||
events[entry.key] = EventTypeStats.fromJson(entry.value);
|
||||
}
|
||||
|
||||
final totals = json['totals'] as Map<String, dynamic>? ?? {};
|
||||
|
||||
return WeeklyStatsEntry(
|
||||
weekStart: DateTime.parse(json['week_start']),
|
||||
weekNumber: _parseInt(json['week_number']),
|
||||
year: _parseInt(json['year']),
|
||||
events: events,
|
||||
totalCount: _parseInt(totals['count']),
|
||||
totalAmount: _parseDouble(totals['sum_amount']),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Réponse des stats hebdomadaires (/stats/weekly)
|
||||
class WeeklyStats {
|
||||
final DateTime from;
|
||||
final DateTime to;
|
||||
final List<WeeklyStatsEntry> weeks;
|
||||
|
||||
const WeeklyStats({
|
||||
required this.from,
|
||||
required this.to,
|
||||
required this.weeks,
|
||||
});
|
||||
|
||||
factory WeeklyStats.fromJson(Map<String, dynamic> json) {
|
||||
final weeksJson = json['weeks'] as List<dynamic>? ?? [];
|
||||
|
||||
return WeeklyStats(
|
||||
from: DateTime.parse(json['from']),
|
||||
to: DateTime.parse(json['to']),
|
||||
weeks: weeksJson.map((w) => WeeklyStatsEntry.fromJson(w)).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Données d'un mois dans les stats mensuelles
|
||||
class MonthlyStatsEntry {
|
||||
final String month; // Format: "2025-01"
|
||||
final int year;
|
||||
final int monthNumber;
|
||||
final Map<String, EventTypeStats> events;
|
||||
final int totalCount;
|
||||
final double totalAmount;
|
||||
|
||||
const MonthlyStatsEntry({
|
||||
required this.month,
|
||||
required this.year,
|
||||
required this.monthNumber,
|
||||
required this.events,
|
||||
required this.totalCount,
|
||||
required this.totalAmount,
|
||||
});
|
||||
|
||||
factory MonthlyStatsEntry.fromJson(Map<String, dynamic> json) {
|
||||
final eventsJson = json['events'] as Map<String, dynamic>? ?? {};
|
||||
final events = <String, EventTypeStats>{};
|
||||
|
||||
for (final entry in eventsJson.entries) {
|
||||
events[entry.key] = EventTypeStats.fromJson(entry.value);
|
||||
}
|
||||
|
||||
final totals = json['totals'] as Map<String, dynamic>? ?? {};
|
||||
|
||||
return MonthlyStatsEntry(
|
||||
month: json['month'] ?? '',
|
||||
year: _parseInt(json['year']),
|
||||
monthNumber: _parseInt(json['month_number']),
|
||||
events: events,
|
||||
totalCount: _parseInt(totals['count']),
|
||||
totalAmount: _parseDouble(totals['sum_amount']),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Réponse des stats mensuelles (/stats/monthly)
|
||||
class MonthlyStats {
|
||||
final int year;
|
||||
final List<MonthlyStatsEntry> months;
|
||||
|
||||
const MonthlyStats({
|
||||
required this.year,
|
||||
required this.months,
|
||||
});
|
||||
|
||||
factory MonthlyStats.fromJson(Map<String, dynamic> json) {
|
||||
final monthsJson = json['months'] as List<dynamic>? ?? [];
|
||||
|
||||
return MonthlyStats(
|
||||
year: _parseInt(json['year']),
|
||||
months: monthsJson.map((m) => MonthlyStatsEntry.fromJson(m)).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Détail d'un événement individuel
|
||||
class EventDetail {
|
||||
final DateTime timestamp;
|
||||
final String event;
|
||||
final String? username;
|
||||
final String? reason;
|
||||
final int? attempt;
|
||||
final String? ip;
|
||||
final String? platform;
|
||||
final String? appVersion;
|
||||
final Map<String, dynamic>? extra;
|
||||
|
||||
const EventDetail({
|
||||
required this.timestamp,
|
||||
required this.event,
|
||||
this.username,
|
||||
this.reason,
|
||||
this.attempt,
|
||||
this.ip,
|
||||
this.platform,
|
||||
this.appVersion,
|
||||
this.extra,
|
||||
});
|
||||
|
||||
factory EventDetail.fromJson(Map<String, dynamic> json) {
|
||||
return EventDetail(
|
||||
timestamp: DateTime.parse(json['timestamp']),
|
||||
event: json['event'] ?? '',
|
||||
username: json['username'],
|
||||
reason: json['reason'],
|
||||
attempt: json['attempt'] != null ? _parseInt(json['attempt']) : null,
|
||||
ip: json['ip'],
|
||||
platform: json['platform'],
|
||||
appVersion: json['app_version'],
|
||||
extra: json,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Pagination pour les détails
|
||||
class EventPagination {
|
||||
final int total;
|
||||
final int limit;
|
||||
final int offset;
|
||||
final bool hasMore;
|
||||
|
||||
const EventPagination({
|
||||
required this.total,
|
||||
required this.limit,
|
||||
required this.offset,
|
||||
required this.hasMore,
|
||||
});
|
||||
|
||||
factory EventPagination.fromJson(Map<String, dynamic> json) {
|
||||
return EventPagination(
|
||||
total: _parseInt(json['total']),
|
||||
limit: _parseInt(json['limit']),
|
||||
offset: _parseInt(json['offset']),
|
||||
hasMore: json['has_more'] == true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Réponse des détails d'événements (/stats/details)
|
||||
class EventDetails {
|
||||
final DateTime date;
|
||||
final List<EventDetail> events;
|
||||
final EventPagination pagination;
|
||||
|
||||
const EventDetails({
|
||||
required this.date,
|
||||
required this.events,
|
||||
required this.pagination,
|
||||
});
|
||||
|
||||
factory EventDetails.fromJson(Map<String, dynamic> json) {
|
||||
final eventsJson = json['events'] as List<dynamic>? ?? [];
|
||||
|
||||
return EventDetails(
|
||||
date: DateTime.parse(json['date']),
|
||||
events: eventsJson.map((e) => EventDetail.fromJson(e)).toList(),
|
||||
pagination: EventPagination.fromJson(json['pagination'] ?? {}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Types d'événements disponibles
|
||||
class EventTypes {
|
||||
final List<String> auth;
|
||||
final List<String> passages;
|
||||
final List<String> sectors;
|
||||
final List<String> users;
|
||||
final List<String> entities;
|
||||
final List<String> operations;
|
||||
final List<String> stripe;
|
||||
|
||||
const EventTypes({
|
||||
required this.auth,
|
||||
required this.passages,
|
||||
required this.sectors,
|
||||
required this.users,
|
||||
required this.entities,
|
||||
required this.operations,
|
||||
required this.stripe,
|
||||
});
|
||||
|
||||
factory EventTypes.fromJson(Map<String, dynamic> json) {
|
||||
return EventTypes(
|
||||
auth: _parseStringList(json['auth']),
|
||||
passages: _parseStringList(json['passages']),
|
||||
sectors: _parseStringList(json['sectors']),
|
||||
users: _parseStringList(json['users']),
|
||||
entities: _parseStringList(json['entities']),
|
||||
operations: _parseStringList(json['operations']),
|
||||
stripe: _parseStringList(json['stripe']),
|
||||
);
|
||||
}
|
||||
|
||||
/// Obtient tous les types d'événements à plat
|
||||
List<String> get allTypes => [
|
||||
...auth,
|
||||
...passages,
|
||||
...sectors,
|
||||
...users,
|
||||
...entities,
|
||||
...operations,
|
||||
...stripe,
|
||||
];
|
||||
|
||||
/// Obtient le libellé français d'un type d'événement
|
||||
static String getLabel(String eventType) {
|
||||
switch (eventType) {
|
||||
// Auth
|
||||
case 'login_success': return 'Connexion réussie';
|
||||
case 'login_failed': return 'Connexion échouée';
|
||||
case 'logout': return 'Déconnexion';
|
||||
// Passages
|
||||
case 'passage_created': return 'Passage créé';
|
||||
case 'passage_updated': return 'Passage modifié';
|
||||
case 'passage_deleted': return 'Passage supprimé';
|
||||
// Sectors
|
||||
case 'sector_created': return 'Secteur créé';
|
||||
case 'sector_updated': return 'Secteur modifié';
|
||||
case 'sector_deleted': return 'Secteur supprimé';
|
||||
// Users
|
||||
case 'user_created': return 'Utilisateur créé';
|
||||
case 'user_updated': return 'Utilisateur modifié';
|
||||
case 'user_deleted': return 'Utilisateur supprimé';
|
||||
// Entities
|
||||
case 'entity_created': return 'Entité créée';
|
||||
case 'entity_updated': return 'Entité modifiée';
|
||||
case 'entity_deleted': return 'Entité supprimée';
|
||||
// Operations
|
||||
case 'operation_created': return 'Opération créée';
|
||||
case 'operation_updated': return 'Opération modifiée';
|
||||
case 'operation_deleted': return 'Opération supprimée';
|
||||
// Stripe
|
||||
case 'stripe_payment_created': return 'Paiement créé';
|
||||
case 'stripe_payment_success': return 'Paiement réussi';
|
||||
case 'stripe_payment_failed': return 'Paiement échoué';
|
||||
case 'stripe_payment_cancelled': return 'Paiement annulé';
|
||||
case 'stripe_terminal_error': return 'Erreur terminal';
|
||||
default: return eventType;
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtient la catégorie d'un type d'événement
|
||||
static String getCategory(String eventType) {
|
||||
if (eventType.startsWith('login') || eventType == 'logout') return 'auth';
|
||||
if (eventType.startsWith('passage')) return 'passages';
|
||||
if (eventType.startsWith('sector')) return 'sectors';
|
||||
if (eventType.startsWith('user')) return 'users';
|
||||
if (eventType.startsWith('entity')) return 'entities';
|
||||
if (eventType.startsWith('operation')) return 'operations';
|
||||
if (eventType.startsWith('stripe')) return 'stripe';
|
||||
return 'other';
|
||||
}
|
||||
}
|
||||
|
||||
// Helpers pour parser les types depuis JSON (gère int/string)
|
||||
int _parseInt(dynamic value) {
|
||||
if (value == null) return 0;
|
||||
if (value is int) return value;
|
||||
if (value is String) return int.tryParse(value) ?? 0;
|
||||
if (value is double) return value.toInt();
|
||||
return 0;
|
||||
}
|
||||
|
||||
double _parseDouble(dynamic value) {
|
||||
if (value == null) return 0.0;
|
||||
if (value is double) return value;
|
||||
if (value is int) return value.toDouble();
|
||||
if (value is String) return double.tryParse(value) ?? 0.0;
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
List<String> _parseStringList(dynamic value) {
|
||||
if (value == null) return [];
|
||||
if (value is List) return value.map((e) => e.toString()).toList();
|
||||
return [];
|
||||
}
|
||||
Reference in New Issue
Block a user