feat: Version 3.5.2 - Configuration Stripe et gestion des immeubles
- Configuration complète Stripe pour les 3 environnements (DEV/REC/PROD) * DEV: Clés TEST Pierre (mode test) * REC: Clés TEST Client (mode test) * PROD: Clés LIVE Client (mode live) - Ajout de la gestion des bases de données immeubles/bâtiments * Configuration buildings_database pour DEV/REC/PROD * Service BuildingService pour enrichissement des adresses - Optimisations pages et améliorations ergonomie - Mises à jour des dépendances Composer - Nettoyage des fichiers obsolètes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -32,12 +32,12 @@ class AppKeys {
|
||||
// URLs API pour les différents environnements
|
||||
static const String baseApiUrlDev = 'https://dapp.geosector.fr/api';
|
||||
static const String baseApiUrlRec = 'https://rapp.geosector.fr/api';
|
||||
static const String baseApiUrlProd = 'https://app.geosector.fr/api';
|
||||
static const String baseApiUrlProd = 'https://app3.geosector.fr/api';
|
||||
|
||||
// Identifiants d'application pour les différents environnements
|
||||
static const String appIdentifierDev = 'dapp.geosector.fr';
|
||||
static const String appIdentifierRec = 'rapp.geosector.fr';
|
||||
static const String appIdentifierProd = 'app.geosector.fr';
|
||||
static const String appIdentifierProd = 'app3.geosector.fr';
|
||||
|
||||
// Endpoints API
|
||||
static const String loginEndpoint = '/login';
|
||||
@@ -47,7 +47,7 @@ class AppKeys {
|
||||
static const String sectorsEndpoint = '/sectors';
|
||||
|
||||
// Durées
|
||||
static const Duration connectionTimeout = Duration(seconds: 5);
|
||||
static const Duration connectionTimeout = Duration(seconds: 15);
|
||||
static const Duration receiveTimeout = Duration(seconds: 30);
|
||||
static const Duration sessionDefaultExpiry = Duration(days: 7);
|
||||
|
||||
@@ -154,9 +154,9 @@ class AppKeys {
|
||||
2: {
|
||||
'titres': 'À finaliser',
|
||||
'titre': 'À finaliser',
|
||||
'couleur1': 0xFFFFDFC2, // Orange très pâle (nbPassages=0)
|
||||
'couleur2': 0xFFF7A278, // Orange moyen (nbPassages=1)
|
||||
'couleur3': 0xFFE65100, // Orange foncé (nbPassages>1)
|
||||
'couleur1': 0xFFFFFFFF, // Blanc (nbPassages=0)
|
||||
'couleur2': 0xFFFFB978, // Orange moyen (nbPassages=1)
|
||||
'couleur3': 0xFFE66F00, // Orange foncé (nbPassages>1)
|
||||
'icon_data': Icons.refresh,
|
||||
},
|
||||
3: {
|
||||
|
||||
@@ -49,6 +49,9 @@ class AmicaleModel extends HiveObject {
|
||||
@HiveField(14)
|
||||
final String stripeId;
|
||||
|
||||
@HiveField(27)
|
||||
final String? stripeLocationId;
|
||||
|
||||
@HiveField(15)
|
||||
final bool chkDemo;
|
||||
|
||||
@@ -101,6 +104,7 @@ class AmicaleModel extends HiveObject {
|
||||
this.gpsLat = '',
|
||||
this.gpsLng = '',
|
||||
this.stripeId = '',
|
||||
this.stripeLocationId,
|
||||
this.chkDemo = false,
|
||||
this.chkCopieMailRecu = false,
|
||||
this.chkAcceptSms = false,
|
||||
@@ -194,6 +198,7 @@ class AmicaleModel extends HiveObject {
|
||||
gpsLat: json['gps_lat'] ?? '',
|
||||
gpsLng: json['gps_lng'] ?? '',
|
||||
stripeId: json['stripe_id'] ?? '',
|
||||
stripeLocationId: json['stripe_location_id'],
|
||||
chkDemo: chkDemo,
|
||||
chkCopieMailRecu: chkCopieMailRecu,
|
||||
chkAcceptSms: chkAcceptSms,
|
||||
@@ -227,6 +232,7 @@ class AmicaleModel extends HiveObject {
|
||||
'gps_lat': gpsLat,
|
||||
'gps_lng': gpsLng,
|
||||
'stripe_id': stripeId,
|
||||
'stripe_location_id': stripeLocationId,
|
||||
'chk_demo': chkDemo ? 1 : 0,
|
||||
'chk_copie_mail_recu': chkCopieMailRecu ? 1 : 0,
|
||||
'chk_accept_sms': chkAcceptSms ? 1 : 0,
|
||||
@@ -258,6 +264,7 @@ class AmicaleModel extends HiveObject {
|
||||
String? gpsLat,
|
||||
String? gpsLng,
|
||||
String? stripeId,
|
||||
String? stripeLocationId,
|
||||
bool? chkDemo,
|
||||
bool? chkCopieMailRecu,
|
||||
bool? chkAcceptSms,
|
||||
@@ -287,6 +294,7 @@ class AmicaleModel extends HiveObject {
|
||||
gpsLat: gpsLat ?? this.gpsLat,
|
||||
gpsLng: gpsLng ?? this.gpsLng,
|
||||
stripeId: stripeId ?? this.stripeId,
|
||||
stripeLocationId: stripeLocationId ?? this.stripeLocationId,
|
||||
chkDemo: chkDemo ?? this.chkDemo,
|
||||
chkCopieMailRecu: chkCopieMailRecu ?? this.chkCopieMailRecu,
|
||||
chkAcceptSms: chkAcceptSms ?? this.chkAcceptSms,
|
||||
|
||||
@@ -32,6 +32,7 @@ class AmicaleModelAdapter extends TypeAdapter<AmicaleModel> {
|
||||
gpsLat: fields[12] as String,
|
||||
gpsLng: fields[13] as String,
|
||||
stripeId: fields[14] as String,
|
||||
stripeLocationId: fields[27] as String?,
|
||||
chkDemo: fields[15] as bool,
|
||||
chkCopieMailRecu: fields[16] as bool,
|
||||
chkAcceptSms: fields[17] as bool,
|
||||
@@ -50,7 +51,7 @@ class AmicaleModelAdapter extends TypeAdapter<AmicaleModel> {
|
||||
@override
|
||||
void write(BinaryWriter writer, AmicaleModel obj) {
|
||||
writer
|
||||
..writeByte(27)
|
||||
..writeByte(28)
|
||||
..writeByte(0)
|
||||
..write(obj.id)
|
||||
..writeByte(1)
|
||||
@@ -81,6 +82,8 @@ class AmicaleModelAdapter extends TypeAdapter<AmicaleModel> {
|
||||
..write(obj.gpsLng)
|
||||
..writeByte(14)
|
||||
..write(obj.stripeId)
|
||||
..writeByte(27)
|
||||
..write(obj.stripeLocationId)
|
||||
..writeByte(15)
|
||||
..write(obj.chkDemo)
|
||||
..writeByte(16)
|
||||
|
||||
@@ -5,20 +5,24 @@ import 'package:geosector_app/core/data/models/user_model.dart';
|
||||
part 'membre_model.g.dart';
|
||||
|
||||
/// Modèle représentant un membre d'une amicale.
|
||||
///
|
||||
///
|
||||
/// IMPORTANT : Ce modèle représente TOUS les membres d'une amicale,
|
||||
/// pas seulement l'utilisateur connecté. Pour l'utilisateur connecté, voir UserModel.
|
||||
///
|
||||
///
|
||||
/// La box Hive 'membres' contient plusieurs enregistrements (tous les membres de l'amicale).
|
||||
///
|
||||
///
|
||||
/// Relations avec les autres modèles :
|
||||
/// - UserModel : représente uniquement l'utilisateur connecté (current user)
|
||||
/// - UserSectorModel : utilise MembreModel.id pour associer les membres aux secteurs
|
||||
/// ATTENTION : UserSectorModel.id = MembreModel.id (pas UserModel.id)
|
||||
///
|
||||
///
|
||||
/// ⚠️ IMPORTANT : Distinction des IDs
|
||||
/// - id : ID de la table centrale `users` (pour gestion des membres)
|
||||
/// - opeUserId : ID de la table `ope_users` (pour lier avec passages/secteurs de l'opération active)
|
||||
///
|
||||
/// Chaque membre a son propre ID unique qui est utilisé pour :
|
||||
/// - L'attribution aux secteurs (via UserSectorModel)
|
||||
/// - La gestion des passages
|
||||
/// - La gestion des passages (via MembreModel.opeUserId == PassageModel.fkUser)
|
||||
/// - Les statistiques par membre
|
||||
@HiveType(typeId: 5) // Utilisation d'un typeId unique
|
||||
class MembreModel extends HiveObject {
|
||||
@@ -57,6 +61,9 @@ class MembreModel extends HiveObject {
|
||||
@HiveField(14)
|
||||
bool isActive;
|
||||
|
||||
@HiveField(15)
|
||||
int? opeUserId; // ID dans ope_users pour l'opération active
|
||||
|
||||
MembreModel({
|
||||
required this.id,
|
||||
this.fkEntite,
|
||||
@@ -73,6 +80,7 @@ class MembreModel extends HiveObject {
|
||||
this.dateEmbauche,
|
||||
required this.createdAt,
|
||||
required this.isActive,
|
||||
this.opeUserId,
|
||||
});
|
||||
|
||||
// Factory pour convertir depuis JSON (API)
|
||||
@@ -112,6 +120,13 @@ class MembreModel extends HiveObject {
|
||||
}
|
||||
}
|
||||
|
||||
// Convertir ope_user_id en int si présent
|
||||
int? opeUserId;
|
||||
if (json['ope_user_id'] != null) {
|
||||
final dynamic rawOpeUserId = json['ope_user_id'];
|
||||
opeUserId = rawOpeUserId is String ? int.parse(rawOpeUserId) : rawOpeUserId as int;
|
||||
}
|
||||
|
||||
return MembreModel(
|
||||
id: id,
|
||||
fkEntite: fkEntite,
|
||||
@@ -129,6 +144,7 @@ class MembreModel extends HiveObject {
|
||||
createdAt: DateTime.now(), // ← Simplifié car created_at n'existe pas dans l'API
|
||||
// Le champ JSON est 'chk_active' pas 'is_active'
|
||||
isActive: json['chk_active'] == 1 || json['chk_active'] == true,
|
||||
opeUserId: opeUserId,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur parsing MembreModel: $e');
|
||||
@@ -141,6 +157,7 @@ class MembreModel extends HiveObject {
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'ope_user_id': opeUserId,
|
||||
'fk_entite': fkEntite,
|
||||
'fk_role': role, // Changé pour correspondre à l'API
|
||||
'fk_titre': fkTitre,
|
||||
@@ -174,6 +191,7 @@ class MembreModel extends HiveObject {
|
||||
DateTime? dateEmbauche,
|
||||
DateTime? createdAt,
|
||||
bool? isActive,
|
||||
int? opeUserId,
|
||||
}) {
|
||||
return MembreModel(
|
||||
id: id,
|
||||
@@ -191,6 +209,7 @@ class MembreModel extends HiveObject {
|
||||
dateEmbauche: dateEmbauche ?? this.dateEmbauche,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
isActive: isActive ?? this.isActive,
|
||||
opeUserId: opeUserId ?? this.opeUserId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -214,6 +233,7 @@ class MembreModel extends HiveObject {
|
||||
dateNaissance: dateNaissance,
|
||||
dateEmbauche: dateEmbauche,
|
||||
sectName: sectName,
|
||||
opeUserId: opeUserId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -233,6 +253,7 @@ class MembreModel extends HiveObject {
|
||||
dateNaissance: user.dateNaissance,
|
||||
dateEmbauche: user.dateEmbauche,
|
||||
isActive: user.isActive,
|
||||
opeUserId: user.opeUserId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,13 +32,14 @@ class MembreModelAdapter extends TypeAdapter<MembreModel> {
|
||||
dateEmbauche: fields[12] as DateTime?,
|
||||
createdAt: fields[13] as DateTime,
|
||||
isActive: fields[14] as bool,
|
||||
opeUserId: fields[15] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, MembreModel obj) {
|
||||
writer
|
||||
..writeByte(15)
|
||||
..writeByte(16)
|
||||
..writeByte(0)
|
||||
..write(obj.id)
|
||||
..writeByte(1)
|
||||
@@ -68,7 +69,9 @@ class MembreModelAdapter extends TypeAdapter<MembreModel> {
|
||||
..writeByte(13)
|
||||
..write(obj.createdAt)
|
||||
..writeByte(14)
|
||||
..write(obj.isActive);
|
||||
..write(obj.isActive)
|
||||
..writeByte(15)
|
||||
..write(obj.opeUserId);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -95,6 +95,12 @@ class PassageModel extends HiveObject {
|
||||
@HiveField(29)
|
||||
String? stripePaymentId;
|
||||
|
||||
@HiveField(30)
|
||||
String? stripePaymentLinkId;
|
||||
|
||||
@HiveField(31)
|
||||
String? stripePaymentLinkUrl;
|
||||
|
||||
PassageModel({
|
||||
required this.id,
|
||||
required this.fkOperation,
|
||||
@@ -126,6 +132,8 @@ class PassageModel extends HiveObject {
|
||||
this.isActive = true,
|
||||
this.isSynced = false,
|
||||
this.stripePaymentId,
|
||||
this.stripePaymentLinkId,
|
||||
this.stripePaymentLinkUrl,
|
||||
});
|
||||
|
||||
// Factory pour convertir depuis JSON (API)
|
||||
@@ -197,6 +205,8 @@ class PassageModel extends HiveObject {
|
||||
isActive: true,
|
||||
isSynced: true,
|
||||
stripePaymentId: json['stripe_payment_id']?.toString(),
|
||||
stripePaymentLinkId: json['stripe_payment_link_id']?.toString(),
|
||||
stripePaymentLinkUrl: json['stripe_payment_link_url']?.toString(),
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur parsing PassageModel: $e');
|
||||
@@ -235,6 +245,8 @@ class PassageModel extends HiveObject {
|
||||
'email': email,
|
||||
'phone': phone,
|
||||
'stripe_payment_id': stripePaymentId,
|
||||
'stripe_payment_link_id': stripePaymentLinkId,
|
||||
'stripe_payment_link_url': stripePaymentLinkUrl,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -270,6 +282,8 @@ class PassageModel extends HiveObject {
|
||||
bool? isActive,
|
||||
bool? isSynced,
|
||||
String? stripePaymentId,
|
||||
String? stripePaymentLinkId,
|
||||
String? stripePaymentLinkUrl,
|
||||
}) {
|
||||
return PassageModel(
|
||||
id: id ?? this.id,
|
||||
@@ -302,6 +316,8 @@ class PassageModel extends HiveObject {
|
||||
isActive: isActive ?? this.isActive,
|
||||
isSynced: isSynced ?? this.isSynced,
|
||||
stripePaymentId: stripePaymentId ?? this.stripePaymentId,
|
||||
stripePaymentLinkId: stripePaymentLinkId ?? this.stripePaymentLinkId,
|
||||
stripePaymentLinkUrl: stripePaymentLinkUrl ?? this.stripePaymentLinkUrl,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -47,13 +47,15 @@ class PassageModelAdapter extends TypeAdapter<PassageModel> {
|
||||
isActive: fields[27] as bool,
|
||||
isSynced: fields[28] as bool,
|
||||
stripePaymentId: fields[29] as String?,
|
||||
stripePaymentLinkId: fields[30] as String?,
|
||||
stripePaymentLinkUrl: fields[31] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, PassageModel obj) {
|
||||
writer
|
||||
..writeByte(30)
|
||||
..writeByte(32)
|
||||
..writeByte(0)
|
||||
..write(obj.id)
|
||||
..writeByte(1)
|
||||
@@ -113,7 +115,11 @@ class PassageModelAdapter extends TypeAdapter<PassageModel> {
|
||||
..writeByte(28)
|
||||
..write(obj.isSynced)
|
||||
..writeByte(29)
|
||||
..write(obj.stripePaymentId);
|
||||
..write(obj.stripePaymentId)
|
||||
..writeByte(30)
|
||||
..write(obj.stripePaymentLinkId)
|
||||
..writeByte(31)
|
||||
..write(obj.stripePaymentLinkUrl);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
32
app/lib/core/data/models/payment_link_result.dart
Normal file
32
app/lib/core/data/models/payment_link_result.dart
Normal file
@@ -0,0 +1,32 @@
|
||||
/// Résultat de création d'un Payment Link Stripe
|
||||
class PaymentLinkResult {
|
||||
final String paymentLinkId;
|
||||
final String url;
|
||||
final int amount;
|
||||
final int? passageId;
|
||||
|
||||
PaymentLinkResult({
|
||||
required this.paymentLinkId,
|
||||
required this.url,
|
||||
required this.amount,
|
||||
this.passageId,
|
||||
});
|
||||
|
||||
factory PaymentLinkResult.fromJson(Map<String, dynamic> json) {
|
||||
return PaymentLinkResult(
|
||||
paymentLinkId: json['payment_link_id'] as String,
|
||||
url: json['url'] as String,
|
||||
amount: json['amount'] as int,
|
||||
passageId: json['passage_id'] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'payment_link_id': paymentLinkId,
|
||||
'url': url,
|
||||
'amount': amount,
|
||||
'passage_id': passageId,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -74,6 +74,9 @@ class UserModel extends HiveObject {
|
||||
@HiveField(19)
|
||||
DateTime? dateEmbauche;
|
||||
|
||||
@HiveField(20)
|
||||
int? opeUserId; // ID dans ope_users pour l'opération active
|
||||
|
||||
UserModel({
|
||||
required this.id,
|
||||
required this.email,
|
||||
@@ -95,6 +98,7 @@ class UserModel extends HiveObject {
|
||||
this.mobile,
|
||||
this.dateNaissance,
|
||||
this.dateEmbauche,
|
||||
this.opeUserId,
|
||||
});
|
||||
|
||||
// Factory pour convertir depuis JSON (API)
|
||||
@@ -138,6 +142,12 @@ class UserModel extends HiveObject {
|
||||
}
|
||||
}
|
||||
|
||||
// Convertir ope_user_id en int si présent
|
||||
final dynamic rawOpeUserId = json['ope_user_id'];
|
||||
final int? opeUserId = rawOpeUserId != null
|
||||
? (rawOpeUserId is String ? int.parse(rawOpeUserId) : rawOpeUserId as int)
|
||||
: null;
|
||||
|
||||
return UserModel(
|
||||
id: id,
|
||||
email: json['email'] ?? '',
|
||||
@@ -162,6 +172,7 @@ class UserModel extends HiveObject {
|
||||
mobile: json['mobile'],
|
||||
dateNaissance: dateNaissance,
|
||||
dateEmbauche: dateEmbauche,
|
||||
opeUserId: opeUserId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -186,6 +197,7 @@ class UserModel extends HiveObject {
|
||||
'mobile': mobile,
|
||||
'date_naissance': dateNaissance?.toIso8601String(),
|
||||
'date_embauche': dateEmbauche?.toIso8601String(),
|
||||
'ope_user_id': opeUserId,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -209,6 +221,7 @@ class UserModel extends HiveObject {
|
||||
String? mobile,
|
||||
DateTime? dateNaissance,
|
||||
DateTime? dateEmbauche,
|
||||
int? opeUserId,
|
||||
}) {
|
||||
return UserModel(
|
||||
id: id,
|
||||
@@ -231,6 +244,7 @@ class UserModel extends HiveObject {
|
||||
mobile: mobile ?? this.mobile,
|
||||
dateNaissance: dateNaissance ?? this.dateNaissance,
|
||||
dateEmbauche: dateEmbauche ?? this.dateEmbauche,
|
||||
opeUserId: opeUserId ?? this.opeUserId,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -37,13 +37,14 @@ class UserModelAdapter extends TypeAdapter<UserModel> {
|
||||
mobile: fields[17] as String?,
|
||||
dateNaissance: fields[18] as DateTime?,
|
||||
dateEmbauche: fields[19] as DateTime?,
|
||||
opeUserId: fields[20] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, UserModel obj) {
|
||||
writer
|
||||
..writeByte(20)
|
||||
..writeByte(21)
|
||||
..writeByte(0)
|
||||
..write(obj.id)
|
||||
..writeByte(1)
|
||||
@@ -83,7 +84,9 @@ class UserModelAdapter extends TypeAdapter<UserModel> {
|
||||
..writeByte(18)
|
||||
..write(obj.dateNaissance)
|
||||
..writeByte(19)
|
||||
..write(obj.dateEmbauche);
|
||||
..write(obj.dateEmbauche)
|
||||
..writeByte(20)
|
||||
..write(obj.opeUserId);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -6,10 +6,14 @@ part 'user_sector_model.g.dart';
|
||||
///
|
||||
/// Cette classe représente l'association entre un utilisateur et un secteur,
|
||||
/// telle que reçue de l'API dans la réponse users_sectors.
|
||||
///
|
||||
/// ⚠️ IMPORTANT : Distinction des IDs
|
||||
/// - userId : ID de la table centrale `users` (pour gestion des membres)
|
||||
/// - opeUserId : ID de la table `ope_users` (pour lier avec passages/sectors)
|
||||
@HiveType(typeId: 6)
|
||||
class UserSectorModel extends HiveObject {
|
||||
@HiveField(0)
|
||||
final int id; // ID de l'utilisateur
|
||||
final int userId; // ID de l'utilisateur (table centrale users)
|
||||
|
||||
@HiveField(1)
|
||||
final String? firstName;
|
||||
@@ -23,18 +27,23 @@ class UserSectorModel extends HiveObject {
|
||||
@HiveField(4)
|
||||
final String? name;
|
||||
|
||||
@HiveField(5)
|
||||
final int opeUserId; // ID de l'utilisateur dans ope_users (pour lier avec passages)
|
||||
|
||||
UserSectorModel({
|
||||
required this.id,
|
||||
required this.userId,
|
||||
this.firstName,
|
||||
this.sectName,
|
||||
required this.fkSector,
|
||||
this.name,
|
||||
required this.opeUserId,
|
||||
});
|
||||
|
||||
/// Crée un modèle UserSectorModel à partir d'un objet JSON
|
||||
factory UserSectorModel.fromJson(Map<String, dynamic> json) {
|
||||
return UserSectorModel(
|
||||
id: json['id'] is String ? int.parse(json['id']) : json['id'],
|
||||
userId: json['user_id'] is String ? int.parse(json['user_id']) : json['user_id'],
|
||||
opeUserId: json['ope_user_id'] is String ? int.parse(json['ope_user_id']) : json['ope_user_id'],
|
||||
firstName: json['first_name'],
|
||||
sectName: json['sect_name'],
|
||||
fkSector: json['fk_sector'] is String ? int.parse(json['fk_sector']) : json['fk_sector'],
|
||||
@@ -45,7 +54,8 @@ class UserSectorModel extends HiveObject {
|
||||
/// Convertit le modèle en objet JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'user_id': userId,
|
||||
'ope_user_id': opeUserId,
|
||||
'first_name': firstName,
|
||||
'sect_name': sectName,
|
||||
'fk_sector': fkSector,
|
||||
@@ -55,14 +65,16 @@ class UserSectorModel extends HiveObject {
|
||||
|
||||
/// Crée une copie du modèle avec des valeurs potentiellement modifiées
|
||||
UserSectorModel copyWith({
|
||||
int? id,
|
||||
int? userId,
|
||||
int? opeUserId,
|
||||
String? firstName,
|
||||
String? sectName,
|
||||
int? fkSector,
|
||||
String? name,
|
||||
}) {
|
||||
return UserSectorModel(
|
||||
id: id ?? this.id,
|
||||
userId: userId ?? this.userId,
|
||||
opeUserId: opeUserId ?? this.opeUserId,
|
||||
firstName: firstName ?? this.firstName,
|
||||
sectName: sectName ?? this.sectName,
|
||||
fkSector: fkSector ?? this.fkSector,
|
||||
@@ -72,6 +84,6 @@ class UserSectorModel extends HiveObject {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'UserSectorModel(id: $id, firstName: $firstName, sectName: $sectName, fkSector: $fkSector, name: $name)';
|
||||
return 'UserSectorModel(userId: $userId, opeUserId: $opeUserId, firstName: $firstName, sectName: $sectName, fkSector: $fkSector, name: $name)';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,20 +17,21 @@ class UserSectorModelAdapter extends TypeAdapter<UserSectorModel> {
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return UserSectorModel(
|
||||
id: fields[0] as int,
|
||||
userId: fields[0] as int,
|
||||
firstName: fields[1] as String?,
|
||||
sectName: fields[2] as String?,
|
||||
fkSector: fields[3] as int,
|
||||
name: fields[4] as String?,
|
||||
opeUserId: fields[5] as int,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, UserSectorModel obj) {
|
||||
writer
|
||||
..writeByte(5)
|
||||
..writeByte(6)
|
||||
..writeByte(0)
|
||||
..write(obj.id)
|
||||
..write(obj.userId)
|
||||
..writeByte(1)
|
||||
..write(obj.firstName)
|
||||
..writeByte(2)
|
||||
@@ -38,7 +39,9 @@ class UserSectorModelAdapter extends TypeAdapter<UserSectorModel> {
|
||||
..writeByte(3)
|
||||
..write(obj.fkSector)
|
||||
..writeByte(4)
|
||||
..write(obj.name);
|
||||
..write(obj.name)
|
||||
..writeByte(5)
|
||||
..write(obj.opeUserId);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -101,10 +101,10 @@ class PassageRepository extends ChangeNotifier {
|
||||
return _passageBox.values.where((passage) => passage.fkOperation == operationId).toList();
|
||||
}
|
||||
|
||||
// Récupérer les passages par utilisateur
|
||||
List<PassageModel> getPassagesByUser(int userId) {
|
||||
// Récupérer les passages par utilisateur (ope_users.id)
|
||||
List<PassageModel> getPassagesByUser(int opeUserId) {
|
||||
try {
|
||||
return _passageBox.values.where((passage) => passage.fkUser == userId).toList();
|
||||
return _passageBox.values.where((passage) => passage.fkUser == opeUserId).toList();
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors de la récupération des passages par utilisateur: $e');
|
||||
return [];
|
||||
@@ -380,12 +380,12 @@ class PassageRepository extends ChangeNotifier {
|
||||
|
||||
// Vérifier si la requête a été mise en file d'attente
|
||||
if (response.data['queued'] == true) {
|
||||
// Récupérer l'utilisateur actuel
|
||||
final currentUserId = CurrentUserService.instance.userId;
|
||||
// Récupérer l'utilisateur actuel (ope_users.id)
|
||||
final currentOpeUserId = CurrentUserService.instance.opeUserId;
|
||||
|
||||
// Mode offline : mettre à jour localement et marquer comme non synchronisé
|
||||
final offlinePassage = passage.copyWith(
|
||||
fkUser: currentUserId, // Le passage appartient maintenant à l'utilisateur qui l'a modifié
|
||||
fkUser: currentOpeUserId, // Le passage appartient à l'utilisateur qui l'a modifié (ope_users.id)
|
||||
lastSyncedAt: null,
|
||||
isSynced: false,
|
||||
);
|
||||
@@ -418,12 +418,12 @@ class PassageRepository extends ChangeNotifier {
|
||||
|
||||
// Mode online : traitement normal
|
||||
if (response.statusCode == 200) {
|
||||
// Récupérer l'utilisateur actuel
|
||||
final currentUserId = CurrentUserService.instance.userId;
|
||||
// Récupérer l'utilisateur actuel (ope_users.id)
|
||||
final currentOpeUserId = CurrentUserService.instance.opeUserId;
|
||||
|
||||
// Mettre à jour le passage localement avec le user actuel
|
||||
final updatedPassage = passage.copyWith(
|
||||
fkUser: currentUserId, // Le passage appartient maintenant à l'utilisateur qui l'a modifié
|
||||
fkUser: currentOpeUserId, // Le passage appartient à l'utilisateur qui l'a modifié (ope_users.id)
|
||||
lastSyncedAt: DateTime.now(),
|
||||
isSynced: true,
|
||||
);
|
||||
@@ -574,10 +574,10 @@ class PassageRepository extends ChangeNotifier {
|
||||
|
||||
// Calculer les statistiques pour chaque utilisateur
|
||||
for (final entry in passagesByUser.entries) {
|
||||
final userId = entry.key;
|
||||
final opeUserId = entry.key; // ID de l'utilisateur dans ope_users
|
||||
final userPassages = entry.value;
|
||||
|
||||
statsByUser[userId] = {
|
||||
statsByUser[opeUserId] = {
|
||||
'total': userPassages.length,
|
||||
'effectues': userPassages.where((p) => p.fkType == 1).length,
|
||||
'a_finaliser': userPassages.where((p) => p.fkType == 2).length,
|
||||
|
||||
@@ -301,17 +301,20 @@ class SectorRepository extends ChangeNotifier {
|
||||
}
|
||||
|
||||
// Mettre à jour un secteur via l'API
|
||||
Future<Map<String, dynamic>> updateSector(SectorModel sector, {List<int>? users}) async {
|
||||
Future<Map<String, dynamic>> updateSector(SectorModel sector, {List<int>? users, int? chkAdressesChange}) async {
|
||||
try {
|
||||
// Préparer les données à envoyer
|
||||
final Map<String, dynamic> requestData = {
|
||||
...sector.toJson(),
|
||||
};
|
||||
|
||||
|
||||
// Ajouter les utilisateurs si fournis
|
||||
if (users != null) {
|
||||
requestData['users'] = users;
|
||||
}
|
||||
|
||||
// Ajouter le paramètre chk_adresses_change (par défaut 1 si non spécifié)
|
||||
requestData['chk_adresses_change'] = chkAdressesChange ?? 1;
|
||||
|
||||
final response = await ApiService.instance.put(
|
||||
'${AppKeys.sectorsEndpoint}/${sector.id}',
|
||||
@@ -339,19 +342,19 @@ class SectorRepository extends ChangeNotifier {
|
||||
final SectorModel updatedSector = SectorModel.fromJson(responseData['sector']);
|
||||
await saveSector(updatedSector);
|
||||
}
|
||||
|
||||
// Traiter les passages retournés s'ils existent
|
||||
if (responseData['passages_sector'] != null) {
|
||||
|
||||
// Traiter les passages retournés UNIQUEMENT si chk_adresses_change = 1
|
||||
if ((chkAdressesChange ?? 1) == 1 && responseData['passages_sector'] != null) {
|
||||
try {
|
||||
final passagesData = responseData['passages_sector'] as List<dynamic>;
|
||||
debugPrint('Traitement de ${passagesData.length} passages après UPDATE');
|
||||
|
||||
debugPrint('Traitement de ${passagesData.length} passages après UPDATE (chk_adresses_change=1)');
|
||||
|
||||
// Utiliser PassageRepository pour traiter les passages
|
||||
final passageRepository = PassageRepository();
|
||||
|
||||
|
||||
// Vider d'abord tous les passages du secteur
|
||||
await _deleteAllPassagesOfSector(sector.id);
|
||||
|
||||
|
||||
// Puis sauvegarder tous les passages retournés
|
||||
final List<PassageModel> passagesToSave = [];
|
||||
for (final passageData in passagesData) {
|
||||
@@ -363,7 +366,7 @@ class SectorRepository extends ChangeNotifier {
|
||||
debugPrint('Erreur lors du traitement d\'un passage: $e');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (passagesToSave.isNotEmpty) {
|
||||
await passageRepository.savePassages(passagesToSave);
|
||||
debugPrint('${passagesToSave.length} passages sauvegardés après UPDATE');
|
||||
@@ -371,6 +374,8 @@ class SectorRepository extends ChangeNotifier {
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors du traitement des passages: $e');
|
||||
}
|
||||
} else if ((chkAdressesChange ?? 1) == 0) {
|
||||
debugPrint('⏭️ Passages ignorés (chk_adresses_change=0) - les passages existants sont conservés');
|
||||
}
|
||||
|
||||
// Traiter les users_sectors retournés s'ils existent
|
||||
|
||||
@@ -646,8 +646,8 @@ class UserRepository extends ChangeNotifier {
|
||||
// === SYNCHRONISATION ET REFRESH ===
|
||||
|
||||
/// Rafraîchir la session (soft login)
|
||||
/// Utilise un refresh partiel si la dernière sync date de moins de 24h
|
||||
/// Sinon fait un refresh complet
|
||||
/// NOTE: Les endpoints /session/refresh/all et /session/refresh/partial ont été retirés
|
||||
/// Cette méthode maintient la session locale sans faire d'appel API
|
||||
Future<bool> refreshSession() async {
|
||||
try {
|
||||
debugPrint('🔄 Début du refresh de session...');
|
||||
@@ -658,7 +658,7 @@ class UserRepository extends ChangeNotifier {
|
||||
return false;
|
||||
}
|
||||
|
||||
// NOUVEAU : Vérifier la connexion internet avant de faire des appels API
|
||||
// Vérifier la connexion internet avant de faire des appels API
|
||||
final hasConnection = await ApiService.instance.hasInternetConnection();
|
||||
if (!hasConnection) {
|
||||
debugPrint('📵 Pas de connexion internet - refresh annulé');
|
||||
@@ -671,147 +671,17 @@ class UserRepository extends ChangeNotifier {
|
||||
_startAutoRefreshTimer();
|
||||
}
|
||||
|
||||
// Récupérer la dernière date de sync depuis settings
|
||||
DateTime? lastSync;
|
||||
try {
|
||||
if (Hive.isBoxOpen(AppKeys.settingsBoxName)) {
|
||||
final settingsBox = Hive.box(AppKeys.settingsBoxName);
|
||||
final lastSyncString = settingsBox.get('last_sync') as String?;
|
||||
if (lastSyncString != null) {
|
||||
lastSync = DateTime.parse(lastSyncString);
|
||||
debugPrint('📅 Dernière sync: ${lastSync.toIso8601String()}');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Erreur lecture last_sync: $e');
|
||||
}
|
||||
// NOTE: Les endpoints de refresh ont été retirés
|
||||
// La session locale est maintenue mais aucune synchronisation avec le serveur n'est effectuée
|
||||
debugPrint('ℹ️ Refresh de session désactivé (endpoints retirés)');
|
||||
|
||||
// Déterminer si on fait un refresh partiel ou complet
|
||||
// Refresh partiel si:
|
||||
// - On a une date de dernière sync
|
||||
// - Cette date est de moins de 24h
|
||||
final now = DateTime.now();
|
||||
final shouldPartialRefresh = lastSync != null &&
|
||||
now.difference(lastSync).inHours < 24;
|
||||
|
||||
if (shouldPartialRefresh) {
|
||||
debugPrint('⚡ Refresh partiel (dernière sync < 24h)');
|
||||
|
||||
try {
|
||||
// Appel API pour refresh partiel
|
||||
final response = await ApiService.instance.refreshSessionPartial(lastSync);
|
||||
|
||||
if (response.data != null && response.data['status'] == 'success') {
|
||||
// Traiter uniquement les données modifiées
|
||||
await _processPartialRefreshData(response.data);
|
||||
|
||||
// Mettre à jour last_sync
|
||||
await _saveLastSyncTimestamp(now);
|
||||
|
||||
debugPrint('✅ Refresh partiel réussi');
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Erreur refresh partiel: $e');
|
||||
|
||||
// Vérifier si c'est une erreur d'authentification
|
||||
if (_isAuthenticationError(e)) {
|
||||
debugPrint('🔒 Erreur d\'authentification détectée - nettoyage de la session locale');
|
||||
await _clearInvalidSession();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Sinon, on tente un refresh complet
|
||||
debugPrint('Tentative de refresh complet...');
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh complet
|
||||
debugPrint('🔄 Refresh complet des données...');
|
||||
|
||||
try {
|
||||
final response = await ApiService.instance.refreshSessionAll();
|
||||
|
||||
if (response.data != null && response.data['status'] == 'success') {
|
||||
// Traiter toutes les données comme un login
|
||||
await DataLoadingService.instance.processLoginData(response.data);
|
||||
|
||||
// Mettre à jour last_sync
|
||||
await _saveLastSyncTimestamp(now);
|
||||
|
||||
debugPrint('✅ Refresh complet réussi');
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur refresh complet: $e');
|
||||
|
||||
// Vérifier si c'est une erreur d'authentification
|
||||
if (_isAuthenticationError(e)) {
|
||||
debugPrint('🔒 Session invalide côté serveur - nettoyage de la session locale');
|
||||
await _clearInvalidSession();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur générale refresh session: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Traiter les données d'un refresh partiel
|
||||
Future<void> _processPartialRefreshData(Map<String, dynamic> data) async {
|
||||
try {
|
||||
debugPrint('📦 Traitement des données partielles...');
|
||||
|
||||
// Traiter les secteurs modifiés
|
||||
if (data['sectors'] != null && data['sectors'] is List) {
|
||||
final sectorsBox = Hive.box<SectorModel>(AppKeys.sectorsBoxName);
|
||||
for (final sectorData in data['sectors']) {
|
||||
final sector = SectorModel.fromJson(sectorData);
|
||||
await sectorsBox.put(sector.id, sector);
|
||||
}
|
||||
debugPrint('✅ ${data['sectors'].length} secteurs mis à jour');
|
||||
}
|
||||
|
||||
// Traiter les passages modifiés
|
||||
if (data['passages'] != null && data['passages'] is List) {
|
||||
final passagesBox = Hive.box<PassageModel>(AppKeys.passagesBoxName);
|
||||
for (final passageData in data['passages']) {
|
||||
final passage = PassageModel.fromJson(passageData);
|
||||
await passagesBox.put(passage.id, passage);
|
||||
}
|
||||
debugPrint('✅ ${data['passages'].length} passages mis à jour');
|
||||
}
|
||||
|
||||
// Traiter les opérations modifiées
|
||||
if (data['operations'] != null && data['operations'] is List) {
|
||||
final operationsBox = Hive.box<OperationModel>(AppKeys.operationsBoxName);
|
||||
for (final operationData in data['operations']) {
|
||||
final operation = OperationModel.fromJson(operationData);
|
||||
await operationsBox.put(operation.id, operation);
|
||||
}
|
||||
debugPrint('✅ ${data['operations'].length} opérations mises à jour');
|
||||
}
|
||||
|
||||
// Traiter les membres modifiés
|
||||
if (data['membres'] != null && data['membres'] is List) {
|
||||
final membresBox = Hive.box<MembreModel>(AppKeys.membresBoxName);
|
||||
for (final membreData in data['membres']) {
|
||||
final membre = MembreModel.fromJson(membreData);
|
||||
await membresBox.put(membre.id, membre);
|
||||
}
|
||||
debugPrint('✅ ${data['membres'].length} membres mis à jour');
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur traitement données partielles: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Sauvegarder le timestamp de la dernière sync
|
||||
Future<void> _saveLastSyncTimestamp(DateTime timestamp) async {
|
||||
try {
|
||||
@@ -825,55 +695,6 @@ class UserRepository extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// Vérifie si l'erreur est une erreur d'authentification (401, 403)
|
||||
/// Retourne false pour les erreurs 404 (route non trouvée)
|
||||
bool _isAuthenticationError(dynamic error) {
|
||||
final errorMessage = error.toString().toLowerCase();
|
||||
|
||||
// Si c'est une erreur 404, ce n'est pas une erreur d'authentification
|
||||
// C'est juste que la route n'existe pas encore côté API
|
||||
if (errorMessage.contains('404') || errorMessage.contains('not found')) {
|
||||
debugPrint('⚠️ Route API non trouvée (404) - en attente de l\'implémentation côté serveur');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Vérifier les vraies erreurs d'authentification
|
||||
return errorMessage.contains('401') ||
|
||||
errorMessage.contains('403') ||
|
||||
errorMessage.contains('unauthorized') ||
|
||||
errorMessage.contains('forbidden') ||
|
||||
errorMessage.contains('session expired') ||
|
||||
errorMessage.contains('authentication failed');
|
||||
}
|
||||
|
||||
/// Nettoie la session locale invalide
|
||||
Future<void> _clearInvalidSession() async {
|
||||
try {
|
||||
debugPrint('🗑️ Nettoyage de la session invalide...');
|
||||
|
||||
// Arrêter le timer de refresh
|
||||
_stopAutoRefreshTimer();
|
||||
|
||||
// Nettoyer les données de session
|
||||
await CurrentUserService.instance.clearUser();
|
||||
await CurrentAmicaleService.instance.clearAmicale();
|
||||
|
||||
// Nettoyer les IDs dans settings
|
||||
if (Hive.isBoxOpen(AppKeys.settingsBoxName)) {
|
||||
final settingsBox = Hive.box(AppKeys.settingsBoxName);
|
||||
await settingsBox.delete('current_user_id');
|
||||
await settingsBox.delete('current_amicale_id');
|
||||
await settingsBox.delete('last_sync');
|
||||
}
|
||||
|
||||
// Supprimer le sessionId de l'API
|
||||
ApiService.instance.setSessionId(null);
|
||||
|
||||
debugPrint('✅ Session locale nettoyée suite à erreur d\'authentification');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur lors du nettoyage de session: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// === TIMER DE REFRESH AUTOMATIQUE ===
|
||||
|
||||
@@ -1078,8 +899,14 @@ class UserRepository extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
// Convertir ope_user_id en int si présent
|
||||
final dynamic rawOpeUserId = userData['ope_user_id'];
|
||||
final int? opeUserId = rawOpeUserId != null
|
||||
? (rawOpeUserId is String ? int.parse(rawOpeUserId) : rawOpeUserId as int)
|
||||
: null;
|
||||
|
||||
debugPrint(
|
||||
'✅ Données traitées - id: $id, role: $role, fkEntite: $fkEntite');
|
||||
'✅ Données traitées - id: $id, role: $role, fkEntite: $fkEntite, opeUserId: $opeUserId');
|
||||
|
||||
// Créer un utilisateur avec toutes les données disponibles
|
||||
return UserModel(
|
||||
@@ -1103,6 +930,7 @@ class UserRepository extends ChangeNotifier {
|
||||
mobile: userData['mobile'],
|
||||
dateNaissance: dateNaissance,
|
||||
dateEmbauche: dateEmbauche,
|
||||
opeUserId: opeUserId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,8 +145,8 @@ class ApiService {
|
||||
// Détermine l'environnement actuel (DEV, REC, PROD) en fonction de l'URL
|
||||
String _determineEnvironment() {
|
||||
if (!kIsWeb) {
|
||||
// En mode non-web, utiliser l'environnement de développement par défaut
|
||||
return 'DEV';
|
||||
// En mode non-web (iOS/Android), utiliser l'environnement de PRODUCTION par défaut
|
||||
return 'PROD';
|
||||
}
|
||||
|
||||
final currentUrl = html.window.location.href.toLowerCase();
|
||||
@@ -196,6 +196,12 @@ class ApiService {
|
||||
return _baseUrl;
|
||||
}
|
||||
|
||||
// Obtenir l'URL frontend (sans /api) - utile pour les redirections Stripe
|
||||
String getFrontendUrl() {
|
||||
// Retirer le /api de l'URL pour obtenir l'URL frontend
|
||||
return _baseUrl.replaceAll('/api', '');
|
||||
}
|
||||
|
||||
// Obtenir l'identifiant d'application actuel (utile pour le débogage)
|
||||
String getCurrentAppIdentifier() {
|
||||
return _appIdentifier;
|
||||
@@ -208,8 +214,8 @@ class ApiService {
|
||||
return _connectivityService!.isConnected;
|
||||
}
|
||||
// Fallback sur la vérification directe
|
||||
final connectivityResult = await (Connectivity().checkConnectivity());
|
||||
return connectivityResult != ConnectivityResult.none;
|
||||
final connectivityResults = await (Connectivity().checkConnectivity());
|
||||
return !connectivityResults.contains(ConnectivityResult.none);
|
||||
}
|
||||
|
||||
// Met une requête en file d'attente pour envoi ultérieur
|
||||
@@ -385,7 +391,8 @@ class ApiService {
|
||||
|
||||
// Limiter le nombre de tentatives
|
||||
if (request.retryCount >= 5) {
|
||||
debugPrint('⚠️ Nombre maximum de tentatives atteint (5) - Passage à la requête suivante');
|
||||
debugPrint('⚠️ Nombre maximum de tentatives atteint (5) - Suppression de la requête');
|
||||
await box.delete(request.key);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -590,7 +597,7 @@ class ApiService {
|
||||
return await _dio.post(path, data: requestData);
|
||||
} on DioException catch (e) {
|
||||
// Si erreur réseau, mettre en file d'attente
|
||||
if (e.type == DioExceptionType.connectionTimeout ||
|
||||
if (e.type == DioExceptionType.connectionTimeout ||
|
||||
e.type == DioExceptionType.connectionError ||
|
||||
e.type == DioExceptionType.unknown) {
|
||||
await _queueRequest(
|
||||
@@ -629,12 +636,12 @@ class ApiService {
|
||||
data: {'queued': true},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
return await _dio.get(path, queryParameters: queryParameters);
|
||||
} on DioException catch (e) {
|
||||
// Si erreur réseau, mettre en file d'attente
|
||||
if (e.type == DioExceptionType.connectionTimeout ||
|
||||
if (e.type == DioExceptionType.connectionTimeout ||
|
||||
e.type == DioExceptionType.connectionError ||
|
||||
e.type == DioExceptionType.unknown) {
|
||||
await _queueRequest(
|
||||
@@ -655,6 +662,19 @@ class ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
// Méthode GET sans queue (pour les refresh de session)
|
||||
// Ne met JAMAIS la requête en file d'attente, échoue directement
|
||||
Future<Response> getWithoutQueue(String path, {Map<String, dynamic>? queryParameters}) async {
|
||||
try {
|
||||
return await _dio.get(path, queryParameters: queryParameters);
|
||||
} on DioException catch (e) {
|
||||
throw ApiException.fromDioException(e);
|
||||
} catch (e) {
|
||||
if (e is ApiException) rethrow;
|
||||
throw ApiException('Erreur inattendue lors de la requête GET', originalError: e);
|
||||
}
|
||||
}
|
||||
|
||||
// Méthode PUT générique
|
||||
Future<Response> put(String path, {dynamic data, String? tempId}) async {
|
||||
// Vérifier la connectivité
|
||||
@@ -684,7 +704,7 @@ class ApiService {
|
||||
return await _dio.put(path, data: requestData);
|
||||
} on DioException catch (e) {
|
||||
// Si erreur réseau, mettre en file d'attente
|
||||
if (e.type == DioExceptionType.connectionTimeout ||
|
||||
if (e.type == DioExceptionType.connectionTimeout ||
|
||||
e.type == DioExceptionType.connectionError ||
|
||||
e.type == DioExceptionType.unknown) {
|
||||
await _queueRequest(
|
||||
@@ -728,7 +748,7 @@ class ApiService {
|
||||
return await _dio.delete(path);
|
||||
} on DioException catch (e) {
|
||||
// Si erreur réseau, mettre en file d'attente
|
||||
if (e.type == DioExceptionType.connectionTimeout ||
|
||||
if (e.type == DioExceptionType.connectionTimeout ||
|
||||
e.type == DioExceptionType.connectionError ||
|
||||
e.type == DioExceptionType.unknown) {
|
||||
await _queueRequest(
|
||||
@@ -1068,70 +1088,6 @@ class ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
// === MÉTHODES DE REFRESH DE SESSION ===
|
||||
|
||||
/// Rafraîchit toutes les données de session (pour F5, démarrage)
|
||||
/// Retourne les mêmes données qu'un login normal
|
||||
Future<Response> refreshSessionAll() async {
|
||||
try {
|
||||
debugPrint('🔄 Refresh complet de session');
|
||||
|
||||
// Vérifier qu'on a bien un token/session
|
||||
if (_sessionId == null) {
|
||||
throw ApiException('Pas de session active pour le refresh');
|
||||
}
|
||||
|
||||
final response = await post('/session/refresh/all');
|
||||
|
||||
// Traiter la réponse comme un login
|
||||
final data = response.data as Map<String, dynamic>?;
|
||||
if (data != null && data['status'] == 'success') {
|
||||
// Si nouveau session_id dans la réponse, le mettre à jour
|
||||
if (data.containsKey('session_id')) {
|
||||
final newSessionId = data['session_id'];
|
||||
if (newSessionId != null) {
|
||||
setSessionId(newSessionId);
|
||||
}
|
||||
}
|
||||
|
||||
// Collecter et envoyer les informations du device après refresh réussi
|
||||
debugPrint('📱 Collecte des informations device après refresh de session...');
|
||||
DeviceInfoService.instance.collectAndSendDeviceInfo().then((_) {
|
||||
debugPrint('✅ Informations device collectées et envoyées (refresh)');
|
||||
}).catchError((error) {
|
||||
debugPrint('⚠️ Erreur lors de l\'envoi des infos device (refresh): $error');
|
||||
// Ne pas bloquer le refresh si l'envoi des infos device échoue
|
||||
});
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur refresh complet: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Rafraîchit partiellement les données modifiées depuis lastSync
|
||||
/// Ne retourne que les données modifiées (delta)
|
||||
Future<Response> refreshSessionPartial(DateTime lastSync) async {
|
||||
try {
|
||||
debugPrint('🔄 Refresh partiel depuis: ${lastSync.toIso8601String()}');
|
||||
|
||||
// Vérifier qu'on a bien un token/session
|
||||
if (_sessionId == null) {
|
||||
throw ApiException('Pas de session active pour le refresh');
|
||||
}
|
||||
|
||||
final response = await post('/session/refresh/partial', data: {
|
||||
'last_sync': lastSync.toIso8601String(),
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur refresh partiel: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// Déconnexion
|
||||
Future<void> logout() async {
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
// ⚠️ AUTO-GENERATED FILE - DO NOT EDIT MANUALLY
|
||||
// This file is automatically generated by deploy-app.sh script
|
||||
// Last update: 2025-11-09 12:39:26
|
||||
// Source: ../VERSION file
|
||||
//
|
||||
// GEOSECTOR App Version Service
|
||||
// Provides application version and build information without external dependencies
|
||||
|
||||
class AppInfoService {
|
||||
static PackageInfo? _packageInfo;
|
||||
|
||||
static Future<void> initialize() async {
|
||||
_packageInfo = await PackageInfo.fromPlatform();
|
||||
}
|
||||
|
||||
static String get version => _packageInfo?.version ?? '0.0.0';
|
||||
static String get buildNumber => _packageInfo?.buildNumber ?? '0';
|
||||
// Version number (format: x.x.x)
|
||||
static const String version = '3.5.2';
|
||||
|
||||
// Build number (version without dots: xxx)
|
||||
static const String buildNumber = '352';
|
||||
|
||||
// Full version string (format: vx.x.x+xxx)
|
||||
static String get fullVersion => 'v$version+$buildNumber';
|
||||
}
|
||||
|
||||
// Application name
|
||||
static const String appName = 'GeoSector';
|
||||
|
||||
// Package name
|
||||
static const String packageName = 'fr.geosector.app3';
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
/// Service qui gère la surveillance de l'état de connectivité de l'appareil
|
||||
class ConnectivityService extends ChangeNotifier {
|
||||
final Connectivity _connectivity = Connectivity();
|
||||
late StreamSubscription<ConnectivityResult> _connectivitySubscription;
|
||||
late StreamSubscription<List<ConnectivityResult>> _connectivitySubscription;
|
||||
|
||||
List<ConnectivityResult> _connectionStatus = [ConnectivityResult.none];
|
||||
bool _isInitialized = false;
|
||||
@@ -87,12 +87,12 @@ class ConnectivityService extends ChangeNotifier {
|
||||
_connectionStatus = [ConnectivityResult.wifi]; // Valeur par défaut pour le web
|
||||
} else {
|
||||
final result = await _connectivity.checkConnectivity();
|
||||
_connectionStatus = [result];
|
||||
_connectionStatus = result;
|
||||
}
|
||||
|
||||
// S'abonner aux changements de connectivité
|
||||
_connectivitySubscription = _connectivity.onConnectivityChanged.listen((ConnectivityResult result) {
|
||||
_updateConnectionStatus([result]);
|
||||
_connectivitySubscription = _connectivity.onConnectivityChanged.listen((List<ConnectivityResult> results) {
|
||||
_updateConnectionStatus(results);
|
||||
});
|
||||
_isInitialized = true;
|
||||
} catch (e) {
|
||||
@@ -146,9 +146,8 @@ class ConnectivityService extends ChangeNotifier {
|
||||
} else {
|
||||
// Version mobile - utiliser l'API standard
|
||||
final result = await _connectivity.checkConnectivity();
|
||||
final results = [result];
|
||||
_updateConnectionStatus(results);
|
||||
return results;
|
||||
_updateConnectionStatus(result);
|
||||
return result;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors de la vérification de la connectivité: $e');
|
||||
|
||||
@@ -21,6 +21,7 @@ class CurrentUserService extends ChangeNotifier {
|
||||
bool get isLoggedIn => _currentUser?.hasValidSession ?? false;
|
||||
int get userRole => _currentUser?.role ?? 0;
|
||||
int? get userId => _currentUser?.id;
|
||||
int? get opeUserId => _currentUser?.opeUserId; // ID dans ope_users pour l'opération active
|
||||
String? get userEmail => _currentUser?.email;
|
||||
String? get userName => _currentUser?.name;
|
||||
String? get userFirstName => _currentUser?.firstName;
|
||||
|
||||
@@ -448,9 +448,9 @@ class DataLoadingService extends ChangeNotifier {
|
||||
for (final userSectorData in userSectorsList) {
|
||||
try {
|
||||
final userSector = UserSectorModel.fromJson(userSectorData);
|
||||
final key = '${userSector.id}_${userSector.fkSector}';
|
||||
final key = '${userSector.opeUserId}_${userSector.fkSector}';
|
||||
await _userSectorBox.put(key, userSector);
|
||||
debugPrint('✅ Association sauvegardée: ${userSector.firstName} ${userSector.name} (ID: ${userSector.id}) -> Secteur ${userSector.fkSector}');
|
||||
debugPrint('✅ Association sauvegardée: ${userSector.firstName} ${userSector.name} (userId: ${userSector.userId}, opeUserId: ${userSector.opeUserId}) -> Secteur ${userSector.fkSector}');
|
||||
count++;
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Erreur traitement association: $e');
|
||||
@@ -467,7 +467,7 @@ class DataLoadingService extends ChangeNotifier {
|
||||
final key = _userSectorBox.keyAt(i);
|
||||
final value = _userSectorBox.getAt(i);
|
||||
if (value != null) {
|
||||
debugPrint(' - [$key]: ${value.firstName} ${value.name} (ID: ${value.id}) -> Secteur ${value.fkSector}');
|
||||
debugPrint(' - [$key]: ${value.firstName} ${value.name} (userId: ${value.userId}, opeUserId: ${value.opeUserId}) -> Secteur ${value.fkSector}');
|
||||
}
|
||||
}
|
||||
if (_userSectorBox.length > 5) {
|
||||
|
||||
@@ -3,13 +3,12 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:battery_plus/battery_plus.dart';
|
||||
import 'package:nfc_manager/nfc_manager.dart';
|
||||
import 'package:network_info_plus/network_info_plus.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
import 'api_service.dart';
|
||||
import 'current_user_service.dart';
|
||||
import 'app_info_service.dart'; // Remplace package_info_plus
|
||||
import '../constants/app_keys.dart';
|
||||
|
||||
class DeviceInfoService {
|
||||
@@ -18,7 +17,6 @@ class DeviceInfoService {
|
||||
|
||||
final DeviceInfoPlugin _deviceInfo = DeviceInfoPlugin();
|
||||
final Battery _battery = Battery();
|
||||
final NetworkInfo _networkInfo = NetworkInfo();
|
||||
|
||||
Future<Map<String, dynamic>> collectDeviceInfo() async {
|
||||
final deviceData = <String, dynamic>{};
|
||||
@@ -27,8 +25,8 @@ class DeviceInfoService {
|
||||
// Informations réseau et IP (IPv4 uniquement)
|
||||
deviceData['device_ip_local'] = await _getLocalIpAddress();
|
||||
deviceData['device_ip_public'] = await _getPublicIpAddress();
|
||||
deviceData['device_wifi_name'] = await _networkInfo.getWifiName();
|
||||
deviceData['device_wifi_bssid'] = await _networkInfo.getWifiBSSID();
|
||||
deviceData['device_wifi_name'] = null; // ❌ Supprimé network_info_plus (13/10/2025)
|
||||
deviceData['device_wifi_bssid'] = null; // ❌ Supprimé network_info_plus (13/10/2025)
|
||||
|
||||
// Informations batterie
|
||||
final batteryLevel = await _battery.batteryLevel;
|
||||
@@ -120,13 +118,7 @@ class DeviceInfoService {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Méthode 1 : Via network_info_plus (retourne généralement IPv4)
|
||||
String? wifiIP = await _networkInfo.getWifiIP();
|
||||
if (wifiIP != null && wifiIP.isNotEmpty && _isIPv4(wifiIP)) {
|
||||
return wifiIP;
|
||||
}
|
||||
|
||||
// Méthode 2 : Via NetworkInterface avec filtre IPv4 strict
|
||||
// Via NetworkInterface avec filtre IPv4 strict
|
||||
for (var interface in await NetworkInterface.list()) {
|
||||
for (var addr in interface.addresses) {
|
||||
// Vérifier explicitement IPv4 et non loopback
|
||||
@@ -252,9 +244,8 @@ class DeviceInfoService {
|
||||
final deviceData = await collectDeviceInfo();
|
||||
|
||||
// 2. Ajouter les infos de l'app
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
deviceData['app_version'] = packageInfo.version;
|
||||
deviceData['app_build'] = packageInfo.buildNumber;
|
||||
deviceData['app_version'] = AppInfoService.version;
|
||||
deviceData['app_build'] = AppInfoService.buildNumber;
|
||||
|
||||
// 3. Sauvegarder dans Hive Settings
|
||||
await _saveToHiveSettings(deviceData);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:io';
|
||||
import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
@@ -379,7 +380,13 @@ class HiveService {
|
||||
final config = _boxConfigs[i];
|
||||
|
||||
try {
|
||||
await _createSingleBox(config);
|
||||
// Ajouter un timeout de 10 secondes pour éviter les blocages infinis
|
||||
await _createSingleBox(config).timeout(
|
||||
const Duration(seconds: 10),
|
||||
onTimeout: () {
|
||||
throw TimeoutException('Timeout lors de la création de la box ${config.name}');
|
||||
},
|
||||
);
|
||||
debugPrint('✅ Box ${config.name} créée (${i + 1}/${_boxConfigs.length})');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur création ${config.name}: $e');
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:geosector_app/core/services/api_service.dart';
|
||||
import 'package:geosector_app/core/data/models/amicale_model.dart';
|
||||
import 'package:geosector_app/core/data/models/payment_link_result.dart';
|
||||
|
||||
/// Service pour gérer Stripe Connect dans l'application
|
||||
class StripeConnectService {
|
||||
@@ -11,65 +12,79 @@ class StripeConnectService {
|
||||
StripeConnectService({required this.apiService});
|
||||
|
||||
/// Créer un compte Stripe Connect pour une amicale
|
||||
/// Retourne l'URL d'onboarding ou null si erreur
|
||||
/// ✅ VERSION OPTIMISÉE : 1 seule requête qui crée tout
|
||||
/// - Compte Stripe Connect
|
||||
/// - Location Terminal
|
||||
/// - Lien d'onboarding
|
||||
Future<String?> createStripeAccount(AmicaleModel amicale) async {
|
||||
try {
|
||||
debugPrint('🏢 Création compte Stripe pour ${amicale.name}...');
|
||||
debugPrint(' Amicale ID: ${amicale.id}');
|
||||
|
||||
// 1. Créer le compte Stripe Connect via notre API
|
||||
final createResponse = await apiService.post(
|
||||
|
||||
// URLs de retour après onboarding
|
||||
final baseUrl = apiService.getFrontendUrl();
|
||||
final returnUrl = Uri.encodeFull('$baseUrl/stripe/success');
|
||||
final refreshUrl = Uri.encodeFull('$baseUrl/stripe/refresh');
|
||||
|
||||
debugPrint(' Return URL: $returnUrl');
|
||||
debugPrint(' Refresh URL: $refreshUrl');
|
||||
|
||||
// ✅ UNE SEULE REQUÊTE qui crée :
|
||||
// 1. Le compte Stripe Connect
|
||||
// 2. La Location Terminal
|
||||
// 3. Le lien d'onboarding
|
||||
final response = await apiService.post(
|
||||
'/stripe/accounts',
|
||||
data: {'fk_entite': amicale.id},
|
||||
data: {
|
||||
'fk_entite': amicale.id,
|
||||
'return_url': returnUrl,
|
||||
'refresh_url': refreshUrl,
|
||||
},
|
||||
);
|
||||
|
||||
debugPrint(' Response status: ${createResponse.statusCode}');
|
||||
debugPrint(' Response data: ${createResponse.data}');
|
||||
|
||||
if (createResponse.statusCode != 200 && createResponse.statusCode != 201) {
|
||||
final error = createResponse.data?['message'] ?? 'Erreur création compte';
|
||||
|
||||
debugPrint(' Response status: ${response.statusCode}');
|
||||
debugPrint(' Response data: ${response.data}');
|
||||
|
||||
if (response.statusCode != 200 && response.statusCode != 201) {
|
||||
final error = response.data?['message'] ?? 'Erreur création compte';
|
||||
debugPrint('❌ Erreur création compte: $error');
|
||||
throw Exception(error);
|
||||
}
|
||||
|
||||
// Récupérer les données de la réponse
|
||||
final responseData = createResponse.data;
|
||||
final accountId = responseData?['account_id'];
|
||||
final isExisting = responseData?['existing'] ?? false;
|
||||
final chargesEnabled = responseData?['charges_enabled'] ?? false;
|
||||
final payoutsEnabled = responseData?['payouts_enabled'] ?? false;
|
||||
|
||||
|
||||
// Récupérer toutes les données de la réponse
|
||||
final data = response.data;
|
||||
final accountId = data['account_id'];
|
||||
final locationId = data['location_id'];
|
||||
final onboardingUrl = data['onboarding_url'];
|
||||
final isExisting = data['existing'] ?? false;
|
||||
final chargesEnabled = data['charges_enabled'] ?? false;
|
||||
final payoutsEnabled = data['payouts_enabled'] ?? false;
|
||||
|
||||
if (accountId == null) {
|
||||
throw Exception('account_id non retourné par l\'API');
|
||||
}
|
||||
|
||||
|
||||
debugPrint('✅ Compte Stripe créé: $accountId');
|
||||
debugPrint('✅ Location Terminal créée: $locationId');
|
||||
|
||||
if (isExisting) {
|
||||
debugPrint(' Compte existant détecté, account_id: $accountId');
|
||||
|
||||
// Si le compte est déjà complètement configuré, pas besoin d'onboarding
|
||||
debugPrint(' Compte existant détecté');
|
||||
|
||||
// Si le compte est déjà complètement configuré
|
||||
if (chargesEnabled && payoutsEnabled) {
|
||||
debugPrint('✅ Compte déjà configuré et actif');
|
||||
return null; // Pas besoin de lien d'onboarding
|
||||
}
|
||||
debugPrint(' Compte existant mais configuration incomplète, génération du lien...');
|
||||
} else {
|
||||
debugPrint('✅ Nouveau compte créé: $accountId');
|
||||
debugPrint(' Compte existant mais configuration incomplète');
|
||||
}
|
||||
|
||||
// 2. Créer la Location pour Terminal/Tap to Pay
|
||||
try {
|
||||
await apiService.post(
|
||||
'/stripe/locations',
|
||||
data: {'fk_entite': amicale.id},
|
||||
);
|
||||
debugPrint('✅ Location Terminal créée');
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Erreur création Location (non bloquant): $e');
|
||||
|
||||
if (onboardingUrl == null || onboardingUrl.isEmpty) {
|
||||
throw Exception('onboarding_url non retourné par l\'API');
|
||||
}
|
||||
|
||||
// 3. Obtenir le lien d'onboarding
|
||||
return await getOnboardingLink(accountId);
|
||||
|
||||
|
||||
debugPrint('✅ Lien onboarding généré');
|
||||
return onboardingUrl;
|
||||
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur StripeConnect: $e');
|
||||
return null;
|
||||
@@ -77,12 +92,14 @@ class StripeConnectService {
|
||||
}
|
||||
|
||||
/// Obtenir le lien d'onboarding pour finaliser la configuration
|
||||
/// ⚠️ DÉPRÉCIÉ : L'onboarding URL est maintenant retournée par createStripeAccount
|
||||
/// Conservé pour compatibilité temporaire
|
||||
Future<String?> getOnboardingLink(String accountId) async {
|
||||
try {
|
||||
debugPrint('📋 Génération du lien d\'onboarding pour account: $accountId');
|
||||
|
||||
// URLs de retour après onboarding
|
||||
const baseUrl = 'https://app.geo.dev'; // À adapter selon l'environnement
|
||||
// URLs de retour après onboarding - utilise l'environnement détecté
|
||||
final baseUrl = apiService.getFrontendUrl();
|
||||
final returnUrl = Uri.encodeFull('$baseUrl/stripe/success');
|
||||
final refreshUrl = Uri.encodeFull('$baseUrl/stripe/refresh');
|
||||
|
||||
@@ -124,6 +141,7 @@ class StripeConnectService {
|
||||
return StripeAccountStatus(
|
||||
hasAccount: data['has_account'] ?? false,
|
||||
accountId: data['account_id'],
|
||||
locationId: data['location_id'],
|
||||
chargesEnabled: data['charges_enabled'] ?? false,
|
||||
payoutsEnabled: data['payouts_enabled'] ?? false,
|
||||
onboardingCompleted: data['onboarding_completed'] ?? false,
|
||||
@@ -149,6 +167,49 @@ class StripeConnectService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Créer un Payment Link pour paiement par QRcode
|
||||
Future<PaymentLinkResult?> createPaymentLink({
|
||||
required int amountInCents,
|
||||
required int passageId,
|
||||
String? description,
|
||||
Map<String, dynamic>? metadata,
|
||||
}) async {
|
||||
try {
|
||||
debugPrint('💰 Création Payment Link pour ${amountInCents / 100}€...');
|
||||
debugPrint(' Passage ID: $passageId');
|
||||
|
||||
final response = await apiService.post(
|
||||
'/stripe/payment-links',
|
||||
data: {
|
||||
'amount': amountInCents,
|
||||
'currency': 'eur',
|
||||
'description': description ?? 'Calendrier pompiers',
|
||||
'passage_id': passageId,
|
||||
'metadata': metadata ?? {},
|
||||
},
|
||||
);
|
||||
|
||||
debugPrint(' Response status: ${response.statusCode}');
|
||||
debugPrint(' Response data: ${response.data}');
|
||||
|
||||
if (response.statusCode != 200 && response.statusCode != 201) {
|
||||
final error = response.data?['message'] ?? 'Erreur création Payment Link';
|
||||
debugPrint('❌ Erreur création Payment Link: $error');
|
||||
throw Exception(error);
|
||||
}
|
||||
|
||||
final result = PaymentLinkResult.fromJson(response.data);
|
||||
debugPrint('✅ Payment Link créé: ${result.paymentLinkId}');
|
||||
debugPrint(' URL: ${result.url}');
|
||||
|
||||
return result;
|
||||
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur PaymentLink: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Lancer le processus d'onboarding dans un navigateur externe
|
||||
Future<bool> launchOnboarding(String url) async {
|
||||
try {
|
||||
@@ -190,13 +251,15 @@ class StripeConnectService {
|
||||
class StripeAccountStatus {
|
||||
final bool hasAccount;
|
||||
final String? accountId;
|
||||
final String? locationId;
|
||||
final bool chargesEnabled;
|
||||
final bool payoutsEnabled;
|
||||
final bool onboardingCompleted;
|
||||
|
||||
|
||||
StripeAccountStatus({
|
||||
required this.hasAccount,
|
||||
this.accountId,
|
||||
this.locationId,
|
||||
this.chargesEnabled = false,
|
||||
this.payoutsEnabled = false,
|
||||
this.onboardingCompleted = false,
|
||||
|
||||
@@ -54,6 +54,18 @@ class StripeTapToPayService {
|
||||
}
|
||||
|
||||
_stripeAccountId = amicale.stripeId;
|
||||
_locationId = amicale.stripeLocationId;
|
||||
|
||||
// Vérifier que la Location existe
|
||||
if (_locationId == null || _locationId!.isEmpty) {
|
||||
debugPrint('❌ Aucune Location Stripe Terminal configurée pour cette amicale');
|
||||
debugPrint(' La Location doit être créée lors de l\'onboarding Stripe Connect');
|
||||
_paymentStatusController.add(TapToPayStatus(
|
||||
type: TapToPayStatusType.error,
|
||||
message: 'Location Stripe non configurée',
|
||||
));
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. Vérifier la compatibilité de l'appareil
|
||||
_deviceCompatible = DeviceInfoService.instance.canUseTapToPay();
|
||||
@@ -66,9 +78,6 @@ class StripeTapToPayService {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4. Récupérer la configuration depuis l'API
|
||||
await _fetchConfiguration();
|
||||
|
||||
_isInitialized = true;
|
||||
debugPrint('✅ Tap to Pay initialisé avec succès');
|
||||
|
||||
@@ -92,20 +101,6 @@ class StripeTapToPayService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Récupère la configuration depuis l'API
|
||||
Future<void> _fetchConfiguration() async {
|
||||
try {
|
||||
final response = await ApiService.instance.get('/api/stripe/configuration');
|
||||
|
||||
_locationId = response.data['location_id'];
|
||||
|
||||
debugPrint('✅ Configuration récupérée - Location: $_locationId');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur récupération config: $e');
|
||||
throw Exception('Impossible de récupérer la configuration Stripe');
|
||||
}
|
||||
}
|
||||
|
||||
/// Crée un PaymentIntent pour un paiement Tap to Pay
|
||||
Future<PaymentIntentResult?> createPaymentIntent({
|
||||
required int amountInCents,
|
||||
@@ -130,7 +125,7 @@ class StripeTapToPayService {
|
||||
final passageId = metadata?['passage_id'] ?? '0';
|
||||
|
||||
final response = await ApiService.instance.post(
|
||||
'/api/stripe/payments/create-intent',
|
||||
'/stripe/payments/create-intent',
|
||||
data: {
|
||||
'amount': amountInCents,
|
||||
'currency': 'eur',
|
||||
@@ -220,7 +215,7 @@ class StripeTapToPayService {
|
||||
|
||||
// Notifier le serveur du succès
|
||||
await ApiService.instance.post(
|
||||
'/api/stripe/payments/confirm',
|
||||
'/stripe/payments/confirm',
|
||||
data: {
|
||||
'payment_intent_id': paymentIntent.paymentIntentId,
|
||||
'amount': paymentIntent.amount,
|
||||
@@ -258,7 +253,7 @@ class StripeTapToPayService {
|
||||
Future<void> cancelPayment(String paymentIntentId) async {
|
||||
try {
|
||||
await ApiService.instance.post(
|
||||
'/api/stripe/payments/cancel',
|
||||
'/stripe/payments/cancel',
|
||||
data: {
|
||||
'payment_intent_id': paymentIntentId,
|
||||
},
|
||||
|
||||
@@ -23,9 +23,9 @@ class SyncService {
|
||||
void _initConnectivityListener() {
|
||||
_connectivitySubscription = Connectivity()
|
||||
.onConnectivityChanged
|
||||
.listen((ConnectivityResult result) {
|
||||
// Vérifier si la connexion est disponible
|
||||
if (result != ConnectivityResult.none) {
|
||||
.listen((List<ConnectivityResult> results) {
|
||||
// Vérifier si la liste contient au moins un type de connexion autre que 'none'
|
||||
if (results.any((result) => result != ConnectivityResult.none)) {
|
||||
// Lorsque la connexion est rétablie, déclencher une synchronisation
|
||||
syncAll();
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ class AppTheme {
|
||||
// Ombres
|
||||
static List<BoxShadow> cardShadow = [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 3),
|
||||
@@ -72,7 +72,7 @@ class AppTheme {
|
||||
|
||||
static List<BoxShadow> buttonShadow = [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.1),
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 5,
|
||||
offset: const Offset(0, 2),
|
||||
@@ -158,14 +158,14 @@ class AppTheme {
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(borderRadiusMedium),
|
||||
borderSide: BorderSide(
|
||||
color: textLightColor.withValues(alpha: 0.1),
|
||||
color: textLightColor.withOpacity(0.1),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(borderRadiusMedium),
|
||||
borderSide: BorderSide(
|
||||
color: textLightColor.withValues(alpha: 0.1),
|
||||
color: textLightColor.withOpacity(0.1),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
@@ -176,7 +176,7 @@ class AppTheme {
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: spacingM, vertical: spacingM),
|
||||
),
|
||||
cardTheme: CardThemeData(
|
||||
cardTheme: CardTheme(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(borderRadiusXL),
|
||||
@@ -255,14 +255,14 @@ class AppTheme {
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(borderRadiusMedium),
|
||||
borderSide: BorderSide(
|
||||
color: textDarkColor.withValues(alpha: 0.1),
|
||||
color: textDarkColor.withOpacity(0.1),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(borderRadiusMedium),
|
||||
borderSide: BorderSide(
|
||||
color: textDarkColor.withValues(alpha: 0.1),
|
||||
color: textDarkColor.withOpacity(0.1),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
@@ -273,7 +273,7 @@ class AppTheme {
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: spacingM, vertical: spacingM),
|
||||
),
|
||||
cardTheme: CardThemeData(
|
||||
cardTheme: CardTheme(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(borderRadiusXL),
|
||||
@@ -281,7 +281,7 @@ class AppTheme {
|
||||
color: const Color(0xFF1F2937),
|
||||
),
|
||||
dividerTheme: DividerThemeData(
|
||||
color: textDarkColor.withValues(alpha: 0.1),
|
||||
color: textDarkColor.withOpacity(0.1),
|
||||
thickness: 1,
|
||||
space: spacingM,
|
||||
),
|
||||
@@ -361,7 +361,7 @@ class AppTheme {
|
||||
),
|
||||
bodySmall: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
color: textColor.withValues(alpha: 0.7),
|
||||
color: textColor.withOpacity(0.7),
|
||||
fontSize: 12 * scaleFactor,
|
||||
),
|
||||
|
||||
@@ -374,12 +374,12 @@ class AppTheme {
|
||||
),
|
||||
labelMedium: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
color: textColor.withValues(alpha: 0.7),
|
||||
color: textColor.withOpacity(0.7),
|
||||
fontSize: 12 * scaleFactor,
|
||||
),
|
||||
labelSmall: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
color: textColor.withValues(alpha: 0.7),
|
||||
color: textColor.withOpacity(0.7),
|
||||
fontSize: 11 * scaleFactor,
|
||||
),
|
||||
);
|
||||
@@ -399,10 +399,10 @@ class AppTheme {
|
||||
titleSmall: TextStyle(fontFamily: 'Inter', color: textColor, fontSize: 14, fontWeight: FontWeight.w600),
|
||||
bodyLarge: TextStyle(fontFamily: 'Inter', color: textColor, fontSize: 16, fontWeight: FontWeight.w500),
|
||||
bodyMedium: TextStyle(fontFamily: 'Inter', color: textColor, fontSize: 14, fontWeight: FontWeight.w500),
|
||||
bodySmall: TextStyle(fontFamily: 'Inter', color: textColor.withValues(alpha: 0.7), fontSize: 12),
|
||||
bodySmall: TextStyle(fontFamily: 'Inter', color: textColor.withOpacity(0.7), fontSize: 12),
|
||||
labelLarge: TextStyle(fontFamily: 'Inter', color: textColor, fontSize: 14, fontWeight: FontWeight.w600),
|
||||
labelMedium: TextStyle(fontFamily: 'Inter', color: textColor.withValues(alpha: 0.7), fontSize: 12),
|
||||
labelSmall: TextStyle(fontFamily: 'Inter', color: textColor.withValues(alpha: 0.7), fontSize: 11),
|
||||
labelMedium: TextStyle(fontFamily: 'Inter', color: textColor.withOpacity(0.7), fontSize: 12),
|
||||
labelSmall: TextStyle(fontFamily: 'Inter', color: textColor.withOpacity(0.7), fontSize: 11),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -224,7 +224,7 @@ class ApiException implements Exception {
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
color: Colors.black.withOpacity(0.3),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user