- Replace flutter_appauth with custom WebView implementation to resolve deep link issues - Add KeycloakWebViewAuthService with integrated WebView for seamless authentication - Configure Android manifest for HTTP cleartext traffic support - Add network security config for development environment (192.168.1.11) - Update Keycloak client to use HTTP callback endpoint (http://192.168.1.11:8080/auth/callback) - Remove obsolete keycloak_auth_service.dart and temporary scripts - Clean up dependencies and regenerate injection configuration - Tested successfully on multiple Android devices (Xiaomi 2201116TG, SM A725F) BREAKING CHANGE: Authentication flow now uses WebView instead of external browser - Users will see Keycloak login page within the app instead of browser redirect - Resolves ERR_CLEARTEXT_NOT_PERMITTED and deep link state management issues - Maintains full OIDC compliance with PKCE flow and secure token storage Technical improvements: - WebView with custom navigation delegate for callback handling - Automatic token extraction and user info parsing from JWT - Proper error handling and user feedback - Consistent authentication state management across app lifecycle
392 lines
10 KiB
Dart
392 lines
10 KiB
Dart
import 'package:equatable/equatable.dart';
|
|
import 'package:json_annotation/json_annotation.dart';
|
|
|
|
part 'evenement_model.g.dart';
|
|
|
|
/// Modèle de données pour un événement UnionFlow
|
|
/// Aligné avec l'entité Evenement du serveur API
|
|
@JsonSerializable()
|
|
class EvenementModel extends Equatable {
|
|
/// ID unique de l'événement
|
|
final String? id;
|
|
|
|
/// Titre de l'événement
|
|
final String titre;
|
|
|
|
/// Description détaillée
|
|
final String? description;
|
|
|
|
/// Date et heure de début
|
|
@JsonKey(name: 'dateDebut')
|
|
final DateTime dateDebut;
|
|
|
|
/// Date et heure de fin
|
|
@JsonKey(name: 'dateFin')
|
|
final DateTime? dateFin;
|
|
|
|
/// Lieu de l'événement
|
|
final String? lieu;
|
|
|
|
/// Adresse complète
|
|
final String? adresse;
|
|
|
|
/// Type d'événement
|
|
@JsonKey(name: 'typeEvenement')
|
|
final TypeEvenement typeEvenement;
|
|
|
|
/// Statut de l'événement
|
|
final StatutEvenement statut;
|
|
|
|
/// Capacité maximale
|
|
@JsonKey(name: 'capaciteMax')
|
|
final int? capaciteMax;
|
|
|
|
/// Prix de participation
|
|
final double? prix;
|
|
|
|
/// Inscription requise
|
|
@JsonKey(name: 'inscriptionRequise')
|
|
final bool inscriptionRequise;
|
|
|
|
/// Date limite d'inscription
|
|
@JsonKey(name: 'dateLimiteInscription')
|
|
final DateTime? dateLimiteInscription;
|
|
|
|
/// Instructions particulières
|
|
@JsonKey(name: 'instructionsParticulieres')
|
|
final String? instructionsParticulieres;
|
|
|
|
/// Contact organisateur
|
|
@JsonKey(name: 'contactOrganisateur')
|
|
final String? contactOrganisateur;
|
|
|
|
/// Matériel requis
|
|
@JsonKey(name: 'materielRequis')
|
|
final String? materielRequis;
|
|
|
|
/// Visible au public
|
|
@JsonKey(name: 'visiblePublic')
|
|
final bool visiblePublic;
|
|
|
|
/// Événement actif
|
|
final bool actif;
|
|
|
|
/// Créé par
|
|
@JsonKey(name: 'creePar')
|
|
final String? creePar;
|
|
|
|
/// Date de création
|
|
@JsonKey(name: 'dateCreation')
|
|
final DateTime? dateCreation;
|
|
|
|
/// Modifié par
|
|
@JsonKey(name: 'modifiePar')
|
|
final String? modifiePar;
|
|
|
|
/// Date de modification
|
|
@JsonKey(name: 'dateModification')
|
|
final DateTime? dateModification;
|
|
|
|
/// Organisation associée (ID)
|
|
@JsonKey(name: 'organisationId')
|
|
final String? organisationId;
|
|
|
|
/// Organisateur (ID)
|
|
@JsonKey(name: 'organisateurId')
|
|
final String? organisateurId;
|
|
|
|
const EvenementModel({
|
|
this.id,
|
|
required this.titre,
|
|
this.description,
|
|
required this.dateDebut,
|
|
this.dateFin,
|
|
this.lieu,
|
|
this.adresse,
|
|
required this.typeEvenement,
|
|
required this.statut,
|
|
this.capaciteMax,
|
|
this.prix,
|
|
required this.inscriptionRequise,
|
|
this.dateLimiteInscription,
|
|
this.instructionsParticulieres,
|
|
this.contactOrganisateur,
|
|
this.materielRequis,
|
|
required this.visiblePublic,
|
|
required this.actif,
|
|
this.creePar,
|
|
this.dateCreation,
|
|
this.modifiePar,
|
|
this.dateModification,
|
|
this.organisationId,
|
|
this.organisateurId,
|
|
});
|
|
|
|
/// Factory pour créer depuis JSON
|
|
factory EvenementModel.fromJson(Map<String, dynamic> json) =>
|
|
_$EvenementModelFromJson(json);
|
|
|
|
/// Convertir vers JSON
|
|
Map<String, dynamic> toJson() => _$EvenementModelToJson(this);
|
|
|
|
/// Copie avec modifications
|
|
EvenementModel copyWith({
|
|
String? id,
|
|
String? titre,
|
|
String? description,
|
|
DateTime? dateDebut,
|
|
DateTime? dateFin,
|
|
String? lieu,
|
|
String? adresse,
|
|
TypeEvenement? typeEvenement,
|
|
StatutEvenement? statut,
|
|
int? capaciteMax,
|
|
double? prix,
|
|
bool? inscriptionRequise,
|
|
DateTime? dateLimiteInscription,
|
|
String? instructionsParticulieres,
|
|
String? contactOrganisateur,
|
|
String? materielRequis,
|
|
bool? visiblePublic,
|
|
bool? actif,
|
|
String? creePar,
|
|
DateTime? dateCreation,
|
|
String? modifiePar,
|
|
DateTime? dateModification,
|
|
String? organisationId,
|
|
String? organisateurId,
|
|
}) {
|
|
return EvenementModel(
|
|
id: id ?? this.id,
|
|
titre: titre ?? this.titre,
|
|
description: description ?? this.description,
|
|
dateDebut: dateDebut ?? this.dateDebut,
|
|
dateFin: dateFin ?? this.dateFin,
|
|
lieu: lieu ?? this.lieu,
|
|
adresse: adresse ?? this.adresse,
|
|
typeEvenement: typeEvenement ?? this.typeEvenement,
|
|
statut: statut ?? this.statut,
|
|
capaciteMax: capaciteMax ?? this.capaciteMax,
|
|
prix: prix ?? this.prix,
|
|
inscriptionRequise: inscriptionRequise ?? this.inscriptionRequise,
|
|
dateLimiteInscription: dateLimiteInscription ?? this.dateLimiteInscription,
|
|
instructionsParticulieres: instructionsParticulieres ?? this.instructionsParticulieres,
|
|
contactOrganisateur: contactOrganisateur ?? this.contactOrganisateur,
|
|
materielRequis: materielRequis ?? this.materielRequis,
|
|
visiblePublic: visiblePublic ?? this.visiblePublic,
|
|
actif: actif ?? this.actif,
|
|
creePar: creePar ?? this.creePar,
|
|
dateCreation: dateCreation ?? this.dateCreation,
|
|
modifiePar: modifiePar ?? this.modifiePar,
|
|
dateModification: dateModification ?? this.dateModification,
|
|
organisationId: organisationId ?? this.organisationId,
|
|
organisateurId: organisateurId ?? this.organisateurId,
|
|
);
|
|
}
|
|
|
|
/// Méthodes utilitaires
|
|
|
|
/// Vérifie si l'événement est à venir
|
|
bool get estAVenir => dateDebut.isAfter(DateTime.now());
|
|
|
|
/// Vérifie si l'événement est en cours
|
|
bool get estEnCours {
|
|
final maintenant = DateTime.now();
|
|
return dateDebut.isBefore(maintenant) &&
|
|
(dateFin?.isAfter(maintenant) ?? false);
|
|
}
|
|
|
|
/// Vérifie si l'événement est terminé
|
|
bool get estTermine {
|
|
final maintenant = DateTime.now();
|
|
return dateFin?.isBefore(maintenant) ?? dateDebut.isBefore(maintenant);
|
|
}
|
|
|
|
/// Vérifie si les inscriptions sont ouvertes
|
|
bool get inscriptionsOuvertes {
|
|
if (!inscriptionRequise) return false;
|
|
if (dateLimiteInscription == null) return estAVenir;
|
|
return dateLimiteInscription!.isAfter(DateTime.now()) && estAVenir;
|
|
}
|
|
|
|
/// Durée de l'événement
|
|
Duration? get duree {
|
|
if (dateFin == null) return null;
|
|
return dateFin!.difference(dateDebut);
|
|
}
|
|
|
|
/// Formatage de la durée
|
|
String get dureeFormatee {
|
|
final d = duree;
|
|
if (d == null) return 'Non spécifiée';
|
|
|
|
if (d.inDays > 0) {
|
|
return '${d.inDays} jour${d.inDays > 1 ? 's' : ''}';
|
|
} else if (d.inHours > 0) {
|
|
return '${d.inHours}h${d.inMinutes.remainder(60) > 0 ? '${d.inMinutes.remainder(60)}' : ''}';
|
|
} else {
|
|
return '${d.inMinutes} min';
|
|
}
|
|
}
|
|
|
|
@override
|
|
List<Object?> get props => [
|
|
id,
|
|
titre,
|
|
description,
|
|
dateDebut,
|
|
dateFin,
|
|
lieu,
|
|
adresse,
|
|
typeEvenement,
|
|
statut,
|
|
capaciteMax,
|
|
prix,
|
|
inscriptionRequise,
|
|
dateLimiteInscription,
|
|
instructionsParticulieres,
|
|
contactOrganisateur,
|
|
materielRequis,
|
|
visiblePublic,
|
|
actif,
|
|
creePar,
|
|
dateCreation,
|
|
modifiePar,
|
|
dateModification,
|
|
organisationId,
|
|
organisateurId,
|
|
];
|
|
}
|
|
|
|
/// Types d'événements disponibles
|
|
@JsonEnum()
|
|
enum TypeEvenement {
|
|
@JsonValue('ASSEMBLEE_GENERALE')
|
|
assembleeGenerale,
|
|
@JsonValue('REUNION')
|
|
reunion,
|
|
@JsonValue('FORMATION')
|
|
formation,
|
|
@JsonValue('CONFERENCE')
|
|
conference,
|
|
@JsonValue('ATELIER')
|
|
atelier,
|
|
@JsonValue('SEMINAIRE')
|
|
seminaire,
|
|
@JsonValue('EVENEMENT_SOCIAL')
|
|
evenementSocial,
|
|
@JsonValue('MANIFESTATION')
|
|
manifestation,
|
|
@JsonValue('CELEBRATION')
|
|
celebration,
|
|
@JsonValue('AUTRE')
|
|
autre,
|
|
}
|
|
|
|
/// Extension pour les libellés des types
|
|
extension TypeEvenementExtension on TypeEvenement {
|
|
String get libelle {
|
|
switch (this) {
|
|
case TypeEvenement.assembleeGenerale:
|
|
return 'Assemblée Générale';
|
|
case TypeEvenement.reunion:
|
|
return 'Réunion';
|
|
case TypeEvenement.formation:
|
|
return 'Formation';
|
|
case TypeEvenement.conference:
|
|
return 'Conférence';
|
|
case TypeEvenement.atelier:
|
|
return 'Atelier';
|
|
case TypeEvenement.seminaire:
|
|
return 'Séminaire';
|
|
case TypeEvenement.evenementSocial:
|
|
return 'Événement Social';
|
|
case TypeEvenement.manifestation:
|
|
return 'Manifestation';
|
|
case TypeEvenement.celebration:
|
|
return 'Célébration';
|
|
case TypeEvenement.autre:
|
|
return 'Autre';
|
|
}
|
|
}
|
|
|
|
String get icone {
|
|
switch (this) {
|
|
case TypeEvenement.assembleeGenerale:
|
|
return '🏛️';
|
|
case TypeEvenement.reunion:
|
|
return '👥';
|
|
case TypeEvenement.formation:
|
|
return '📚';
|
|
case TypeEvenement.conference:
|
|
return '🎤';
|
|
case TypeEvenement.atelier:
|
|
return '🔧';
|
|
case TypeEvenement.seminaire:
|
|
return '🎓';
|
|
case TypeEvenement.evenementSocial:
|
|
return '🎉';
|
|
case TypeEvenement.manifestation:
|
|
return '📢';
|
|
case TypeEvenement.celebration:
|
|
return '🎊';
|
|
case TypeEvenement.autre:
|
|
return '📅';
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Statuts d'événements disponibles
|
|
@JsonEnum()
|
|
enum StatutEvenement {
|
|
@JsonValue('PLANIFIE')
|
|
planifie,
|
|
@JsonValue('CONFIRME')
|
|
confirme,
|
|
@JsonValue('EN_COURS')
|
|
enCours,
|
|
@JsonValue('TERMINE')
|
|
termine,
|
|
@JsonValue('ANNULE')
|
|
annule,
|
|
@JsonValue('REPORTE')
|
|
reporte,
|
|
}
|
|
|
|
/// Extension pour les libellés des statuts
|
|
extension StatutEvenementExtension on StatutEvenement {
|
|
String get libelle {
|
|
switch (this) {
|
|
case StatutEvenement.planifie:
|
|
return 'Planifié';
|
|
case StatutEvenement.confirme:
|
|
return 'Confirmé';
|
|
case StatutEvenement.enCours:
|
|
return 'En cours';
|
|
case StatutEvenement.termine:
|
|
return 'Terminé';
|
|
case StatutEvenement.annule:
|
|
return 'Annulé';
|
|
case StatutEvenement.reporte:
|
|
return 'Reporté';
|
|
}
|
|
}
|
|
|
|
String get couleur {
|
|
switch (this) {
|
|
case StatutEvenement.planifie:
|
|
return '#FFA500'; // Orange
|
|
case StatutEvenement.confirme:
|
|
return '#4CAF50'; // Vert
|
|
case StatutEvenement.enCours:
|
|
return '#2196F3'; // Bleu
|
|
case StatutEvenement.termine:
|
|
return '#9E9E9E'; // Gris
|
|
case StatutEvenement.annule:
|
|
return '#F44336'; // Rouge
|
|
case StatutEvenement.reporte:
|
|
return '#FF9800'; // Orange foncé
|
|
}
|
|
}
|
|
}
|