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:
2026-01-16 14:11:15 +01:00
parent 7b78037175
commit 232940b1eb
196 changed files with 8483 additions and 7966 deletions

View 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 [];
}