Initialisation du projet geosector complet (web + flutter)

This commit is contained in:
d6soft
2025-05-01 18:59:27 +02:00
commit b5aafc424b
244 changed files with 37296 additions and 0 deletions

View File

@@ -0,0 +1,136 @@
/// Fichier contenant toutes les constantes utilisées dans l'application
/// Centralise les clés, noms de boîtes Hive, et autres constantes
/// pour faciliter la maintenance et éviter les erreurs de frappe
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
class AppKeys {
// Noms des boîtes Hive
static const String usersBoxName = 'users';
static const String operationsBoxName = 'operations';
static const String sectorsBoxName = 'sectors';
static const String passagesBoxName = 'passages';
static const String settingsBoxName = 'settings';
static const String membresBoxName = 'membres';
static const String chatConversationsBoxName = 'chat_conversations';
static const String chatMessagesBoxName = 'chat_messages';
// Rôles utilisateurs
static const int roleUser = 1;
static const int roleAdmin1 = 2;
static const int roleAdmin2 = 4;
static const int roleAdmin3 = 9;
// URLs API
static const String baseApiUrl = 'https://app.geosector.fr/api/geo';
// Endpoints API
static const String loginEndpoint = '/login';
static const String logoutEndpoint = '/logout';
static const String registerEndpoint = '/register';
static const String syncDataEndpoint = '/data/sync';
static const String sectorsEndpoint = '/sectors';
// Durées
static const Duration connectionTimeout = Duration(seconds: 5);
static const Duration receiveTimeout = Duration(seconds: 30);
static const Duration sessionDefaultExpiry = Duration(days: 7);
// Clés API externes
static const String mapboxApiKey =
'pk.eyJ1IjoicHZkNnNvZnQiLCJhIjoiY204dTNhNmd0MGV1ZzJqc2pnNnB0NjYxdSJ9.TA5Mvliyn91Oi01F_2Yuxw'; // À remplacer par votre clé API Mapbox
// Headers
static const String sessionHeader = 'Authorization';
// En-têtes par défaut pour les requêtes API
static const Map<String, String> defaultHeaders = {
'Content-Type': 'application/json',
'X-App-Identifier': 'app.geosector.fr',
'X-Client-Type': kIsWeb ? 'web' : 'mobile',
'Accept': 'application/json',
};
// Civilités
static const Map<int, String> civilites = {
1: 'M.',
2: 'Mme',
};
// Types de règlements
static const Map<int, Map<String, dynamic>> typesReglements = {
0: {
'titre': 'Pas de règlement',
'couleur': 0xFF757575, // Gris foncé
'icon_data': Icons.money_off,
},
1: {
'titre': 'Espèce',
'couleur': 0xFFFFC107, // Jaune foncé (ambre)
'icon_data': Icons.toll,
},
2: {
'titre': 'Chèque',
'couleur': 0xFF8BC34A, // Vert citron
'icon_data': Icons.wallet,
},
3: {
'titre': 'CB',
'couleur': 0xFF00B0FF, // Bleu flashy (bleu clair accent),
'icon_data': Icons.credit_card,
},
};
// Types de passages
static const Map<int, Map<String, dynamic>> typesPassages = {
1: {
'titres': 'Effectués',
'titre': 'Effectué',
'couleur1': 0xFF4CAF50, // Vert success
'couleur2': 0xFF4CAF50, // Vert success
'couleur3': 0xFF4CAF50, // Vert success
'icon_data': Icons.task_alt,
},
2: {
'titres': 'À finaliser',
'titre': 'À finaliser',
'couleur1': 0xFFFFFFFF, // Blanc
'couleur2': 0xFFFF9800, // Orange
'couleur3': 0xFFE65100, // Orange foncé
'icon_data': Icons.refresh,
},
3: {
'titres': 'Refusés',
'titre': 'Refusé',
'couleur1': 0xFFF44336, // Rouge
'couleur2': 0xFFF44336, // Rouge
'couleur3': 0xFFF44336, // Rouge
'icon_data': Icons.block,
},
4: {
'titres': 'Dons',
'titre': 'Don',
'couleur1': 0xFF03A9F4, // Bleu ciel
'couleur2': 0xFF03A9F4, // Bleu ciel
'couleur3': 0xFF03A9F4, // Bleu ciel
'icon_data': Icons.volunteer_activism,
},
5: {
'titres': 'Lots',
'titre': 'Lot',
'couleur1': 0xFF0D47A1, // Bleu foncé
'couleur2': 0xFF0D47A1, // Bleu foncé
'couleur3': 0xFF0D47A1, // Bleu foncé
'icon_data': Icons.layers,
},
6: {
'titres': 'Maisons vides',
'titre': 'Maison vide',
'couleur1': 0xFF9E9E9E, // Gris
'couleur2': 0xFF9E9E9E, // Gris
'couleur3': 0xFF9E9E9E, // Gris
'icon_data': Icons.home_outlined,
},
};
}

View File

@@ -0,0 +1,137 @@
import 'package:hive/hive.dart';
part 'membre_model.g.dart';
@HiveType(typeId: 5) // Utilisation d'un typeId unique
class MembreModel extends HiveObject {
@HiveField(0)
final int id;
@HiveField(1)
final int fkRole;
@HiveField(2)
final int fkTitre;
@HiveField(3)
final String firstName;
@HiveField(4)
final String? sectName;
@HiveField(5)
final DateTime? dateNaissance;
@HiveField(6)
final DateTime? dateEmbauche;
@HiveField(7)
final int chkActive;
@HiveField(8)
final String name;
@HiveField(9)
final String username;
@HiveField(10)
final String email;
MembreModel({
required this.id,
required this.fkRole,
required this.fkTitre,
required this.firstName,
this.sectName,
this.dateNaissance,
this.dateEmbauche,
required this.chkActive,
required this.name,
required this.username,
required this.email,
});
// Factory pour convertir depuis JSON (API)
factory MembreModel.fromJson(Map<String, dynamic> json) {
// Convertir l'ID en int, qu'il soit déjà int ou string
final dynamic rawId = json['id'];
final int id = rawId is String ? int.parse(rawId) : rawId as int;
// Convertir le rôle en int, qu'il soit déjà int ou string
final dynamic rawRole = json['fk_role'];
final int fkRole = rawRole is String ? int.parse(rawRole) : rawRole as int;
// Convertir le titre en int, qu'il soit déjà int ou string
final dynamic rawTitre = json['fk_titre'];
final int fkTitre =
rawTitre is String ? int.parse(rawTitre) : rawTitre as int;
// Convertir le chkActive en int, qu'il soit déjà int ou string
final dynamic rawActive = json['chk_active'];
final int chkActive =
rawActive is String ? int.parse(rawActive) : rawActive as int;
return MembreModel(
id: id,
fkRole: fkRole,
fkTitre: fkTitre,
firstName: json['first_name'] ?? '',
sectName: json['sect_name'],
dateNaissance: json['date_naissance'] != null
? DateTime.parse(json['date_naissance'])
: null,
dateEmbauche: json['date_embauche'] != null
? DateTime.parse(json['date_embauche'])
: null,
chkActive: chkActive,
name: json['name'] ?? '',
username: json['username'] ?? '',
email: json['email'] ?? '',
);
}
// Convertir en JSON pour l'API
Map<String, dynamic> toJson() {
return {
'id': id,
'fk_role': fkRole,
'fk_titre': fkTitre,
'first_name': firstName,
'sect_name': sectName,
'date_naissance': dateNaissance?.toIso8601String(),
'date_embauche': dateEmbauche?.toIso8601String(),
'chk_active': chkActive,
'name': name,
'username': username,
'email': email,
};
}
// Copier avec de nouvelles valeurs
MembreModel copyWith({
int? fkRole,
int? fkTitre,
String? firstName,
String? sectName,
DateTime? dateNaissance,
DateTime? dateEmbauche,
int? chkActive,
String? name,
String? username,
String? email,
}) {
return MembreModel(
id: this.id,
fkRole: fkRole ?? this.fkRole,
fkTitre: fkTitre ?? this.fkTitre,
firstName: firstName ?? this.firstName,
sectName: sectName ?? this.sectName,
dateNaissance: dateNaissance ?? this.dateNaissance,
dateEmbauche: dateEmbauche ?? this.dateEmbauche,
chkActive: chkActive ?? this.chkActive,
name: name ?? this.name,
username: username ?? this.username,
email: email ?? this.email,
);
}
}

View File

@@ -0,0 +1,71 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'membre_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class MembreModelAdapter extends TypeAdapter<MembreModel> {
@override
final int typeId = 5;
@override
MembreModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return MembreModel(
id: fields[0] as int,
fkRole: fields[1] as int,
fkTitre: fields[2] as int,
firstName: fields[3] as String,
sectName: fields[4] as String?,
dateNaissance: fields[5] as DateTime?,
dateEmbauche: fields[6] as DateTime?,
chkActive: fields[7] as int,
name: fields[8] as String,
username: fields[9] as String,
email: fields[10] as String,
);
}
@override
void write(BinaryWriter writer, MembreModel obj) {
writer
..writeByte(11)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.fkRole)
..writeByte(2)
..write(obj.fkTitre)
..writeByte(3)
..write(obj.firstName)
..writeByte(4)
..write(obj.sectName)
..writeByte(5)
..write(obj.dateNaissance)
..writeByte(6)
..write(obj.dateEmbauche)
..writeByte(7)
..write(obj.chkActive)
..writeByte(8)
..write(obj.name)
..writeByte(9)
..write(obj.username)
..writeByte(10)
..write(obj.email);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is MembreModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,85 @@
import 'package:hive/hive.dart';
part 'operation_model.g.dart';
@HiveType(typeId: 1)
class OperationModel extends HiveObject {
@HiveField(0)
final int id;
@HiveField(1)
final String name;
@HiveField(2)
final DateTime dateDebut;
@HiveField(3)
final DateTime dateFin;
@HiveField(4)
DateTime lastSyncedAt;
@HiveField(5)
bool isActive;
@HiveField(6)
bool isSynced;
OperationModel({
required this.id,
required this.name,
required this.dateDebut,
required this.dateFin,
required this.lastSyncedAt,
this.isActive = true,
this.isSynced = false,
});
// Factory pour convertir depuis JSON (API)
factory OperationModel.fromJson(Map<String, dynamic> json) {
// Convertir l'ID en int, qu'il soit déjà int ou string
final dynamic rawId = json['id'];
final int id = rawId is String ? int.parse(rawId) : rawId as int;
return OperationModel(
id: id,
name: json['name'],
dateDebut: DateTime.parse(json['date_deb']),
dateFin: DateTime.parse(json['date_fin']),
lastSyncedAt: DateTime.now(),
isActive: true,
isSynced: true,
);
}
// Convertir en JSON pour l'API
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'date_deb': dateDebut.toIso8601String().split('T')[0], // Format YYYY-MM-DD
'date_fin': dateFin.toIso8601String().split('T')[0], // Format YYYY-MM-DD
'is_active': isActive,
};
}
// Copier avec de nouvelles valeurs
OperationModel copyWith({
String? name,
DateTime? dateDebut,
DateTime? dateFin,
bool? isActive,
bool? isSynced,
DateTime? lastSyncedAt,
}) {
return OperationModel(
id: this.id,
name: name ?? this.name,
dateDebut: dateDebut ?? this.dateDebut,
dateFin: dateFin ?? this.dateFin,
lastSyncedAt: lastSyncedAt ?? this.lastSyncedAt,
isActive: isActive ?? this.isActive,
isSynced: isSynced ?? this.isSynced,
);
}
}

View File

@@ -0,0 +1,59 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'operation_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class OperationModelAdapter extends TypeAdapter<OperationModel> {
@override
final int typeId = 1;
@override
OperationModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return OperationModel(
id: fields[0] as int,
name: fields[1] as String,
dateDebut: fields[2] as DateTime,
dateFin: fields[3] as DateTime,
lastSyncedAt: fields[4] as DateTime,
isActive: fields[5] as bool,
isSynced: fields[6] as bool,
);
}
@override
void write(BinaryWriter writer, OperationModel obj) {
writer
..writeByte(7)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.name)
..writeByte(2)
..write(obj.dateDebut)
..writeByte(3)
..write(obj.dateFin)
..writeByte(4)
..write(obj.lastSyncedAt)
..writeByte(5)
..write(obj.isActive)
..writeByte(6)
..write(obj.isSynced);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is OperationModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,291 @@
import 'package:hive/hive.dart';
part 'passage_model.g.dart';
@HiveType(typeId: 4)
class PassageModel extends HiveObject {
@HiveField(0)
final int id;
@HiveField(1)
final int fkOperation;
@HiveField(2)
final int fkSector;
@HiveField(3)
final int fkUser;
@HiveField(4)
final int fkType;
@HiveField(5)
final String fkAdresse;
@HiveField(6)
final DateTime passedAt;
@HiveField(7)
final String numero;
@HiveField(8)
final String rue;
@HiveField(9)
final String rueBis;
@HiveField(10)
final String ville;
@HiveField(11)
final String residence;
@HiveField(12)
final int fkHabitat;
@HiveField(13)
final String appt;
@HiveField(14)
final String niveau;
@HiveField(15)
final String gpsLat;
@HiveField(16)
final String gpsLng;
@HiveField(17)
final String nomRecu;
@HiveField(18)
final String remarque;
@HiveField(19)
final String montant;
@HiveField(20)
final int fkTypeReglement;
@HiveField(21)
final String emailErreur;
@HiveField(22)
final int nbPassages;
@HiveField(23)
final String name;
@HiveField(24)
final String email;
@HiveField(25)
final String phone;
@HiveField(26)
DateTime lastSyncedAt;
@HiveField(27)
bool isActive;
@HiveField(28)
bool isSynced;
PassageModel({
required this.id,
required this.fkOperation,
required this.fkSector,
required this.fkUser,
required this.fkType,
required this.fkAdresse,
required this.passedAt,
required this.numero,
required this.rue,
this.rueBis = '',
required this.ville,
this.residence = '',
required this.fkHabitat,
this.appt = '',
this.niveau = '',
required this.gpsLat,
required this.gpsLng,
this.nomRecu = '',
this.remarque = '',
required this.montant,
required this.fkTypeReglement,
this.emailErreur = '',
required this.nbPassages,
required this.name,
this.email = '',
this.phone = '',
required this.lastSyncedAt,
this.isActive = true,
this.isSynced = false,
});
// Factory pour convertir depuis JSON (API)
factory PassageModel.fromJson(Map<String, dynamic> json) {
// Convertir l'ID en int, qu'il soit déjà int ou string
final dynamic rawId = json['id'];
final int id = rawId is String ? int.parse(rawId) : rawId as int;
// Convertir les autres champs numériques
final dynamic rawFkOperation = json['fk_operation'];
final int fkOperation = rawFkOperation is String ? int.parse(rawFkOperation) : rawFkOperation as int;
final dynamic rawFkSector = json['fk_sector'];
final int fkSector = rawFkSector is String ? int.parse(rawFkSector) : rawFkSector as int;
final dynamic rawFkUser = json['fk_user'];
final int fkUser = rawFkUser is String ? int.parse(rawFkUser) : rawFkUser as int;
final dynamic rawFkType = json['fk_type'];
final int fkType = rawFkType is String ? int.parse(rawFkType) : rawFkType as int;
final dynamic rawFkHabitat = json['fk_habitat'];
final int fkHabitat = rawFkHabitat is String ? int.parse(rawFkHabitat) : rawFkHabitat as int;
final dynamic rawFkTypeReglement = json['fk_type_reglement'];
final int fkTypeReglement = rawFkTypeReglement is String ? int.parse(rawFkTypeReglement) : rawFkTypeReglement as int;
final dynamic rawNbPassages = json['nb_passages'];
final int nbPassages = rawNbPassages is String ? int.parse(rawNbPassages) : rawNbPassages as int;
// Convertir la date
final DateTime passedAt = DateTime.parse(json['passed_at']);
return PassageModel(
id: id,
fkOperation: fkOperation,
fkSector: fkSector,
fkUser: fkUser,
fkType: fkType,
fkAdresse: json['fk_adresse'] as String,
passedAt: passedAt,
numero: json['numero'] as String,
rue: json['rue'] as String,
rueBis: json['rue_bis'] as String? ?? '',
ville: json['ville'] as String,
residence: json['residence'] as String? ?? '',
fkHabitat: fkHabitat,
appt: json['appt'] as String? ?? '',
niveau: json['niveau'] as String? ?? '',
gpsLat: json['gps_lat'] as String,
gpsLng: json['gps_lng'] as String,
nomRecu: json['nom_recu'] as String? ?? '',
remarque: json['remarque'] as String? ?? '',
montant: json['montant'] as String,
fkTypeReglement: fkTypeReglement,
emailErreur: json['email_erreur'] as String? ?? '',
nbPassages: nbPassages,
name: json['name'] as String,
email: json['email'] as String? ?? '',
phone: json['phone'] as String? ?? '',
lastSyncedAt: DateTime.now(),
isActive: true,
isSynced: true,
);
}
// Convertir en JSON pour l'API
Map<String, dynamic> toJson() {
return {
'id': id,
'fk_operation': fkOperation,
'fk_sector': fkSector,
'fk_user': fkUser,
'fk_type': fkType,
'fk_adresse': fkAdresse,
'passed_at': passedAt.toIso8601String(),
'numero': numero,
'rue': rue,
'rue_bis': rueBis,
'ville': ville,
'residence': residence,
'fk_habitat': fkHabitat,
'appt': appt,
'niveau': niveau,
'gps_lat': gpsLat,
'gps_lng': gpsLng,
'nom_recu': nomRecu,
'remarque': remarque,
'montant': montant,
'fk_type_reglement': fkTypeReglement,
'email_erreur': emailErreur,
'nb_passages': nbPassages,
'name': name,
'email': email,
'phone': phone,
};
}
// Copier avec de nouvelles valeurs
PassageModel copyWith({
int? id,
int? fkOperation,
int? fkSector,
int? fkUser,
int? fkType,
String? fkAdresse,
DateTime? passedAt,
String? numero,
String? rue,
String? rueBis,
String? ville,
String? residence,
int? fkHabitat,
String? appt,
String? niveau,
String? gpsLat,
String? gpsLng,
String? nomRecu,
String? remarque,
String? montant,
int? fkTypeReglement,
String? emailErreur,
int? nbPassages,
String? name,
String? email,
String? phone,
DateTime? lastSyncedAt,
bool? isActive,
bool? isSynced,
}) {
return PassageModel(
id: id ?? this.id,
fkOperation: fkOperation ?? this.fkOperation,
fkSector: fkSector ?? this.fkSector,
fkUser: fkUser ?? this.fkUser,
fkType: fkType ?? this.fkType,
fkAdresse: fkAdresse ?? this.fkAdresse,
passedAt: passedAt ?? this.passedAt,
numero: numero ?? this.numero,
rue: rue ?? this.rue,
rueBis: rueBis ?? this.rueBis,
ville: ville ?? this.ville,
residence: residence ?? this.residence,
fkHabitat: fkHabitat ?? this.fkHabitat,
appt: appt ?? this.appt,
niveau: niveau ?? this.niveau,
gpsLat: gpsLat ?? this.gpsLat,
gpsLng: gpsLng ?? this.gpsLng,
nomRecu: nomRecu ?? this.nomRecu,
remarque: remarque ?? this.remarque,
montant: montant ?? this.montant,
fkTypeReglement: fkTypeReglement ?? this.fkTypeReglement,
emailErreur: emailErreur ?? this.emailErreur,
nbPassages: nbPassages ?? this.nbPassages,
name: name ?? this.name,
email: email ?? this.email,
phone: phone ?? this.phone,
lastSyncedAt: lastSyncedAt ?? this.lastSyncedAt,
isActive: isActive ?? this.isActive,
isSynced: isSynced ?? this.isSynced,
);
}
@override
String toString() {
return 'PassageModel(id: $id, fkOperation: $fkOperation, fkSector: $fkSector, fkUser: $fkUser, fkType: $fkType, adresse: $fkAdresse, ville: $ville, montant: $montant)';
}
}

View File

@@ -0,0 +1,125 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'passage_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class PassageModelAdapter extends TypeAdapter<PassageModel> {
@override
final int typeId = 4;
@override
PassageModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return PassageModel(
id: fields[0] as int,
fkOperation: fields[1] as int,
fkSector: fields[2] as int,
fkUser: fields[3] as int,
fkType: fields[4] as int,
fkAdresse: fields[5] as String,
passedAt: fields[6] as DateTime,
numero: fields[7] as String,
rue: fields[8] as String,
rueBis: fields[9] as String,
ville: fields[10] as String,
residence: fields[11] as String,
fkHabitat: fields[12] as int,
appt: fields[13] as String,
niveau: fields[14] as String,
gpsLat: fields[15] as String,
gpsLng: fields[16] as String,
nomRecu: fields[17] as String,
remarque: fields[18] as String,
montant: fields[19] as String,
fkTypeReglement: fields[20] as int,
emailErreur: fields[21] as String,
nbPassages: fields[22] as int,
name: fields[23] as String,
email: fields[24] as String,
phone: fields[25] as String,
lastSyncedAt: fields[26] as DateTime,
isActive: fields[27] as bool,
isSynced: fields[28] as bool,
);
}
@override
void write(BinaryWriter writer, PassageModel obj) {
writer
..writeByte(29)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.fkOperation)
..writeByte(2)
..write(obj.fkSector)
..writeByte(3)
..write(obj.fkUser)
..writeByte(4)
..write(obj.fkType)
..writeByte(5)
..write(obj.fkAdresse)
..writeByte(6)
..write(obj.passedAt)
..writeByte(7)
..write(obj.numero)
..writeByte(8)
..write(obj.rue)
..writeByte(9)
..write(obj.rueBis)
..writeByte(10)
..write(obj.ville)
..writeByte(11)
..write(obj.residence)
..writeByte(12)
..write(obj.fkHabitat)
..writeByte(13)
..write(obj.appt)
..writeByte(14)
..write(obj.niveau)
..writeByte(15)
..write(obj.gpsLat)
..writeByte(16)
..write(obj.gpsLng)
..writeByte(17)
..write(obj.nomRecu)
..writeByte(18)
..write(obj.remarque)
..writeByte(19)
..write(obj.montant)
..writeByte(20)
..write(obj.fkTypeReglement)
..writeByte(21)
..write(obj.emailErreur)
..writeByte(22)
..write(obj.nbPassages)
..writeByte(23)
..write(obj.name)
..writeByte(24)
..write(obj.email)
..writeByte(25)
..write(obj.phone)
..writeByte(26)
..write(obj.lastSyncedAt)
..writeByte(27)
..write(obj.isActive)
..writeByte(28)
..write(obj.isSynced);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is PassageModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,85 @@
import 'package:hive/hive.dart';
part 'sector_model.g.dart';
@HiveType(typeId: 3)
class SectorModel extends HiveObject {
@HiveField(0)
final int id;
@HiveField(1)
final String libelle;
@HiveField(2)
final String color;
@HiveField(3)
final String sector;
SectorModel({
required this.id,
required this.libelle,
required this.color,
required this.sector,
});
// Factory pour convertir depuis JSON (API)
factory SectorModel.fromJson(Map<String, dynamic> json) {
return SectorModel(
id: json['id'] is String ? int.parse(json['id']) : json['id'] as int,
libelle: json['libelle'] as String,
color: json['color'] as String,
sector: json['sector'] as String,
);
}
// Convertir en JSON pour l'API
Map<String, dynamic> toJson() {
return {
'id': id,
'libelle': libelle,
'color': color,
'sector': sector,
};
}
// Copier avec de nouvelles valeurs
SectorModel copyWith({
int? id,
String? libelle,
String? color,
String? sector,
}) {
return SectorModel(
id: id ?? this.id,
libelle: libelle ?? this.libelle,
color: color ?? this.color,
sector: sector ?? this.sector,
);
}
// Obtenir les coordonnées du secteur sous forme de liste de points
List<List<double>> getCoordinates() {
final List<List<double>> coordinates = [];
// Le format est "lat1/lng1#lat2/lng2#lat3/lng3#..."
final List<String> points = sector.split('#');
for (final String point in points) {
if (point.isEmpty) continue;
final List<String> latLng = point.split('/');
if (latLng.length == 2) {
try {
final double lat = double.parse(latLng[0]);
final double lng = double.parse(latLng[1]);
coordinates.add([lat, lng]);
} catch (e) {
// Ignorer les points mal formatés
}
}
}
return coordinates;
}
}

View File

@@ -0,0 +1,50 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'sector_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class SectorModelAdapter extends TypeAdapter<SectorModel> {
@override
final int typeId = 3;
@override
SectorModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return SectorModel(
id: fields[0] as int,
libelle: fields[1] as String,
color: fields[2] as String,
sector: fields[3] as String,
);
}
@override
void write(BinaryWriter writer, SectorModel obj) {
writer
..writeByte(4)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.libelle)
..writeByte(2)
..write(obj.color)
..writeByte(3)
..write(obj.sector);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is SectorModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,169 @@
import 'package:hive/hive.dart';
part 'user_model.g.dart';
@HiveType(typeId: 0)
class UserModel extends HiveObject {
@HiveField(0)
final int id;
@HiveField(1)
final String email;
@HiveField(2)
String? name;
@HiveField(11)
String? username;
@HiveField(10)
String? firstName;
@HiveField(3)
final int role;
@HiveField(4)
final DateTime createdAt;
@HiveField(5)
DateTime lastSyncedAt;
@HiveField(6)
bool isActive;
@HiveField(7)
bool isSynced;
@HiveField(8)
String? sessionId;
@HiveField(9)
DateTime? sessionExpiry;
@HiveField(12)
String? lastPath;
@HiveField(13)
String? sectName;
@HiveField(14)
String? interface;
UserModel({
required this.id,
required this.email,
this.name,
this.username,
this.firstName,
required this.role,
required this.createdAt,
required this.lastSyncedAt,
this.isActive = true,
this.isSynced = false,
this.sessionId,
this.sessionExpiry,
this.lastPath,
this.sectName,
this.interface,
});
// Factory pour convertir depuis JSON (API)
factory UserModel.fromJson(Map<String, dynamic> json) {
// Convertir l'ID en int, qu'il soit déjà int ou string
final dynamic rawId = json['id'];
final int id = rawId is String ? int.parse(rawId) : rawId as int;
// Convertir le rôle en int, qu'il soit déjà int ou string
final dynamic rawRole = json['role'];
final int role = rawRole is String ? int.parse(rawRole) : rawRole as int;
return UserModel(
id: id,
email: json['email'],
name: json['name'],
username: json['username'],
firstName: json['first_name'],
role: role,
createdAt: DateTime.parse(json['created_at']),
lastSyncedAt: DateTime.now(),
isActive: json['is_active'] ?? true,
isSynced: true,
sessionId: json['session_id'],
sessionExpiry: json['session_expiry'] != null
? DateTime.parse(json['session_expiry'])
: null,
sectName: json['sect_name'],
interface: json['interface'],
);
}
// Convertir en JSON pour l'API
Map<String, dynamic> toJson() {
return {
'id': id,
'email': email,
'name': name,
'username': username,
'first_name': firstName,
'role': role,
'created_at': createdAt.toIso8601String(),
'is_active': isActive,
'session_id': sessionId,
'session_expiry': sessionExpiry?.toIso8601String(),
'last_path': lastPath,
'sect_name': sectName,
'interface': interface,
};
}
// Copier avec de nouvelles valeurs
UserModel copyWith({
String? email,
String? name,
String? username,
String? firstName,
int? role,
bool? isActive,
bool? isSynced,
DateTime? lastSyncedAt,
String? sessionId,
DateTime? sessionExpiry,
String? lastPath,
String? sectName,
String? interface,
}) {
return UserModel(
id: this.id,
email: email ?? this.email,
name: name ?? this.name,
username: username ?? this.username,
firstName: firstName ?? this.firstName,
role: role ?? this.role,
createdAt: this.createdAt,
lastSyncedAt: lastSyncedAt ?? this.lastSyncedAt,
isActive: isActive ?? this.isActive,
isSynced: isSynced ?? this.isSynced,
sessionId: sessionId ?? this.sessionId,
sessionExpiry: sessionExpiry ?? this.sessionExpiry,
lastPath: lastPath ?? this.lastPath,
sectName: sectName ?? this.sectName,
interface: interface ?? this.interface,
);
}
// Vérifier si la session est valide
bool get hasValidSession {
if (sessionId == null || sessionExpiry == null) {
return false;
}
return sessionExpiry!.isAfter(DateTime.now());
}
// Effacer les données de session
UserModel clearSession() {
return copyWith(
sessionId: null,
sessionExpiry: null,
);
}
}

View File

@@ -0,0 +1,83 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class UserModelAdapter extends TypeAdapter<UserModel> {
@override
final int typeId = 0;
@override
UserModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return UserModel(
id: fields[0] as int,
email: fields[1] as String,
name: fields[2] as String?,
username: fields[11] as String?,
firstName: fields[10] as String?,
role: fields[3] as int,
createdAt: fields[4] as DateTime,
lastSyncedAt: fields[5] as DateTime,
isActive: fields[6] as bool,
isSynced: fields[7] as bool,
sessionId: fields[8] as String?,
sessionExpiry: fields[9] as DateTime?,
lastPath: fields[12] as String?,
sectName: fields[13] as String?,
interface: fields[14] as String?,
);
}
@override
void write(BinaryWriter writer, UserModel obj) {
writer
..writeByte(15)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.email)
..writeByte(2)
..write(obj.name)
..writeByte(11)
..write(obj.username)
..writeByte(10)
..write(obj.firstName)
..writeByte(3)
..write(obj.role)
..writeByte(4)
..write(obj.createdAt)
..writeByte(5)
..write(obj.lastSyncedAt)
..writeByte(6)
..write(obj.isActive)
..writeByte(7)
..write(obj.isSynced)
..writeByte(8)
..write(obj.sessionId)
..writeByte(9)
..write(obj.sessionExpiry)
..writeByte(12)
..write(obj.lastPath)
..writeByte(13)
..write(obj.sectName)
..writeByte(14)
..write(obj.interface);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is UserModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,208 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
import 'package:geosector_app/core/services/api_service.dart';
import 'package:geosector_app/core/data/models/membre_model.dart';
class MembreRepository extends ChangeNotifier {
// Utilisation de getters lazy pour n'accéder à la boîte que lorsque nécessaire
Box<MembreModel> get _membreBox =>
Hive.box<MembreModel>(AppKeys.membresBoxName);
final ApiService _apiService;
bool _isLoading = false;
MembreRepository(this._apiService);
// Getters
bool get isLoading => _isLoading;
List<MembreModel> get membres => getAllMembres();
// Méthode pour vérifier si une boîte est ouverte et l'ouvrir si nécessaire
Future<void> _ensureBoxIsOpen() async {
try {
if (!Hive.isBoxOpen(AppKeys.membresBoxName)) {
debugPrint('Ouverture de la boîte ${AppKeys.membresBoxName}...');
await Hive.openBox<MembreModel>(AppKeys.membresBoxName);
debugPrint('Boîte ${AppKeys.membresBoxName} ouverte avec succès');
}
} catch (e) {
debugPrint(
'Erreur lors de l\'ouverture de la boîte ${AppKeys.membresBoxName}: $e');
throw Exception(
'Impossible d\'ouvrir la boîte ${AppKeys.membresBoxName}: $e');
}
}
// Récupérer tous les membres
List<MembreModel> getAllMembres() {
try {
return _membreBox.values.toList();
} catch (e) {
debugPrint('Erreur lors de la récupération des membres: $e');
return [];
}
}
// Récupérer un membre par son ID
MembreModel? getMembreById(int id) {
try {
return _membreBox.get(id);
} catch (e) {
debugPrint('Erreur lors de la récupération du membre: $e');
return null;
}
}
// Créer ou mettre à jour un membre
Future<MembreModel> saveMembre(MembreModel membre) async {
await _ensureBoxIsOpen();
await _membreBox.put(membre.id, membre);
notifyListeners();
return membre;
}
// Supprimer un membre
Future<void> deleteMembre(int id) async {
await _ensureBoxIsOpen();
await _membreBox.delete(id);
notifyListeners();
}
// Récupérer les membres depuis l'API (uniquement pour l'interface admin)
Future<List<MembreModel>> fetchMembresFromApi() async {
_isLoading = true;
notifyListeners();
try {
final hasConnection = await _apiService.hasInternetConnection();
if (!hasConnection) {
debugPrint(
'Pas de connexion Internet, utilisation des données locales');
return getAllMembres();
}
// Endpoint à adapter selon votre API
final response = await _apiService.get('/membres');
final List<dynamic> membresData = response.data['membres'];
// Vider la boîte avant d'ajouter les nouveaux membres
await _ensureBoxIsOpen();
await _membreBox.clear();
final List<MembreModel> membres = [];
for (var membreData in membresData) {
try {
final membre = MembreModel.fromJson(membreData);
await _membreBox.put(membre.id, membre);
membres.add(membre);
} catch (e) {
debugPrint('Erreur lors du traitement d\'un membre: $e');
continue;
}
}
notifyListeners();
return membres;
} catch (e) {
debugPrint(
'Erreur lors de la récupération des membres depuis l\'API: $e');
return getAllMembres();
} finally {
_isLoading = false;
notifyListeners();
}
}
// Créer un membre via l'API
Future<MembreModel?> createMembreViaApi(MembreModel membre) async {
_isLoading = true;
notifyListeners();
try {
final hasConnection = await _apiService.hasInternetConnection();
if (!hasConnection) {
debugPrint('Pas de connexion Internet, impossible de créer le membre');
return null;
}
// Endpoint à adapter selon votre API
final response =
await _apiService.post('/membres', data: membre.toJson());
final membreData = response.data['membre'];
final newMembre = MembreModel.fromJson(membreData);
await saveMembre(newMembre);
return newMembre;
} catch (e) {
debugPrint('Erreur lors de la création du membre via l\'API: $e');
return null;
} finally {
_isLoading = false;
notifyListeners();
}
}
// Mettre à jour un membre via l'API
Future<MembreModel?> updateMembreViaApi(MembreModel membre) async {
_isLoading = true;
notifyListeners();
try {
final hasConnection = await _apiService.hasInternetConnection();
if (!hasConnection) {
debugPrint(
'Pas de connexion Internet, impossible de mettre à jour le membre');
return null;
}
// Endpoint à adapter selon votre API
final response =
await _apiService.put('/membres/${membre.id}', data: membre.toJson());
final membreData = response.data['membre'];
final updatedMembre = MembreModel.fromJson(membreData);
await saveMembre(updatedMembre);
return updatedMembre;
} catch (e) {
debugPrint('Erreur lors de la mise à jour du membre via l\'API: $e');
return null;
} finally {
_isLoading = false;
notifyListeners();
}
}
// Supprimer un membre via l'API
Future<bool> deleteMembreViaApi(int id) async {
_isLoading = true;
notifyListeners();
try {
final hasConnection = await _apiService.hasInternetConnection();
if (!hasConnection) {
debugPrint(
'Pas de connexion Internet, impossible de supprimer le membre');
return false;
}
// Endpoint à adapter selon votre API
await _apiService.delete('/membres/$id');
// Supprimer localement
await deleteMembre(id);
return true;
} catch (e) {
debugPrint('Erreur lors de la suppression du membre via l\'API: $e');
return false;
} finally {
_isLoading = false;
notifyListeners();
}
}
}

View File

@@ -0,0 +1,215 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:geosector_app/core/data/models/operation_model.dart';
import 'package:geosector_app/core/services/api_service.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
class OperationRepository extends ChangeNotifier {
// Utiliser un getter lazy pour n'accéder à la boîte que lorsque nécessaire
// et vérifier qu'elle est ouverte avant accès
Box<OperationModel> get _operationBox {
_ensureBoxIsOpen();
return Hive.box<OperationModel>(AppKeys.operationsBoxName);
}
// Méthode pour vérifier si la boîte est ouverte et l'ouvrir si nécessaire
Future<void> _ensureBoxIsOpen() async {
final boxName = AppKeys.operationsBoxName;
if (!Hive.isBoxOpen(boxName)) {
debugPrint('Ouverture de la boîte $boxName dans OperationRepository...');
await Hive.openBox<OperationModel>(boxName);
}
}
final ApiService _apiService;
bool _isLoading = false;
OperationRepository(this._apiService);
// Getters
bool get isLoading => _isLoading;
List<OperationModel> get operations => getAllOperations();
// Récupérer toutes les opérations
List<OperationModel> getAllOperations() {
return _operationBox.values.toList();
}
// Récupérer une opération par son ID
OperationModel? getOperationById(int id) {
return _operationBox.get(id);
}
// Sauvegarder une opération
Future<void> saveOperation(OperationModel operation) async {
await _operationBox.put(operation.id, operation);
notifyListeners();
}
// Supprimer une opération
Future<void> deleteOperation(int id) async {
await _operationBox.delete(id);
notifyListeners();
}
// Créer ou mettre à jour des opérations à partir des données de l'API
Future<void> processOperationsFromApi(List<dynamic> operationsData) async {
_isLoading = true;
notifyListeners();
try {
for (var operationData in operationsData) {
final operationJson = operationData as Map<String, dynamic>;
final operationId = operationJson['id'] is String
? int.parse(operationJson['id'])
: operationJson['id'] as int;
// Vérifier si l'opération existe déjà
OperationModel? existingOperation = getOperationById(operationId);
if (existingOperation == null) {
// Créer une nouvelle opération
final newOperation = OperationModel.fromJson(operationJson);
await saveOperation(newOperation);
} else {
// Mettre à jour l'opération existante
final updatedOperation = existingOperation.copyWith(
name: operationJson['name'],
dateDebut: DateTime.parse(operationJson['date_deb']),
dateFin: DateTime.parse(operationJson['date_fin']),
lastSyncedAt: DateTime.now(),
isSynced: true,
);
await saveOperation(updatedOperation);
}
}
} catch (e) {
debugPrint('Erreur lors du traitement des opérations: $e');
} finally {
_isLoading = false;
notifyListeners();
}
}
// Créer une opération
Future<bool> createOperation(String name, DateTime dateDebut, DateTime dateFin) async {
_isLoading = true;
notifyListeners();
try {
// Préparer les données pour l'API
final Map<String, dynamic> data = {
'name': name,
'date_deb': dateDebut.toIso8601String().split('T')[0], // Format YYYY-MM-DD
'date_fin': dateFin.toIso8601String().split('T')[0], // Format YYYY-MM-DD
};
// Appeler l'API pour créer l'opération
final response = await _apiService.post('/operations', data: data);
if (response.statusCode == 201 || response.statusCode == 200) {
// Récupérer l'ID de la nouvelle opération
final operationId = response.data['id'] is String
? int.parse(response.data['id'])
: response.data['id'] as int;
// Créer l'opération localement
final newOperation = OperationModel(
id: operationId,
name: name,
dateDebut: dateDebut,
dateFin: dateFin,
lastSyncedAt: DateTime.now(),
isActive: true,
isSynced: true,
);
await saveOperation(newOperation);
return true;
}
return false;
} catch (e) {
debugPrint('Erreur lors de la création de l\'opération: $e');
return false;
} finally {
_isLoading = false;
notifyListeners();
}
}
// Mettre à jour une opération
Future<bool> updateOperation(int id, {String? name, DateTime? dateDebut, DateTime? dateFin, bool? isActive}) async {
_isLoading = true;
notifyListeners();
try {
// Récupérer l'opération existante
final existingOperation = getOperationById(id);
if (existingOperation == null) {
return false;
}
// Préparer les données pour l'API
final Map<String, dynamic> data = {
'id': id,
'name': name ?? existingOperation.name,
'date_deb': (dateDebut ?? existingOperation.dateDebut).toIso8601String().split('T')[0],
'date_fin': (dateFin ?? existingOperation.dateFin).toIso8601String().split('T')[0],
'is_active': isActive ?? existingOperation.isActive,
};
// Appeler l'API pour mettre à jour l'opération
final response = await _apiService.put('/operations/$id', data: data);
if (response.statusCode == 200) {
// Mettre à jour l'opération localement
final updatedOperation = existingOperation.copyWith(
name: name,
dateDebut: dateDebut,
dateFin: dateFin,
isActive: isActive,
lastSyncedAt: DateTime.now(),
isSynced: true,
);
await saveOperation(updatedOperation);
return true;
}
return false;
} catch (e) {
debugPrint('Erreur lors de la mise à jour de l\'opération: $e');
return false;
} finally {
_isLoading = false;
notifyListeners();
}
}
// Supprimer une opération via l'API
Future<bool> deleteOperationViaApi(int id) async {
_isLoading = true;
notifyListeners();
try {
// Appeler l'API pour supprimer l'opération
final response = await _apiService.delete('/operations/$id');
if (response.statusCode == 200 || response.statusCode == 204) {
// Supprimer l'opération localement
await deleteOperation(id);
return true;
}
return false;
} catch (e) {
debugPrint('Erreur lors de la suppression de l\'opération: $e');
return false;
} finally {
_isLoading = false;
notifyListeners();
}
}
}

View File

@@ -0,0 +1,381 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:geosector_app/core/data/models/passage_model.dart';
import 'package:geosector_app/core/services/api_service.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
class PassageRepository extends ChangeNotifier {
// Utiliser un getter lazy pour n'accéder à la boîte que lorsque nécessaire
// et vérifier qu'elle est ouverte avant accès
Box<PassageModel> get _passageBox {
_ensureBoxIsOpen();
return Hive.box<PassageModel>(AppKeys.passagesBoxName);
}
// Méthode pour vérifier si la boîte est ouverte et l'ouvrir si nécessaire
Future<void> _ensureBoxIsOpen() async {
final boxName = AppKeys.passagesBoxName;
if (!Hive.isBoxOpen(boxName)) {
debugPrint('Ouverture de la boîte $boxName dans PassageRepository...');
await Hive.openBox<PassageModel>(boxName);
}
}
final ApiService _apiService;
bool _isLoading = false;
PassageRepository(this._apiService);
// Getters
bool get isLoading => _isLoading;
List<PassageModel> get passages => getAllPassages();
// Récupérer tous les passages
List<PassageModel> getAllPassages() {
return _passageBox.values.toList();
}
// Récupérer un passage par son ID
PassageModel? getPassageById(int id) {
return _passageBox.get(id);
}
// Récupérer les passages par secteur
List<PassageModel> getPassagesBySector(int sectorId) {
return _passageBox.values
.where((passage) => passage.fkSector == sectorId)
.toList();
}
// Récupérer les passages par opération
List<PassageModel> getPassagesByOperation(int operationId) {
return _passageBox.values
.where((passage) => passage.fkOperation == operationId)
.toList();
}
// Récupérer les passages par type
List<PassageModel> getPassagesByType(int typeId) {
return _passageBox.values
.where((passage) => passage.fkType == typeId)
.toList();
}
// Récupérer les passages par type de règlement
List<PassageModel> getPassagesByPaymentType(int paymentTypeId) {
return _passageBox.values
.where((passage) => passage.fkTypeReglement == paymentTypeId)
.toList();
}
// Sauvegarder un passage
Future<void> savePassage(PassageModel passage) async {
await _passageBox.put(passage.id, passage);
notifyListeners();
}
// Supprimer un passage
Future<void> deletePassage(int id) async {
await _passageBox.delete(id);
notifyListeners();
}
// Traiter les passages reçus de l'API
Future<void> processPassagesFromApi(List<dynamic> passagesData) async {
_isLoading = true;
notifyListeners();
try {
for (var passageData in passagesData) {
final passageJson = passageData as Map<String, dynamic>;
final passageId = passageJson['id'] is String
? int.parse(passageJson['id'])
: passageJson['id'] as int;
// Vérifier si le passage existe déjà
PassageModel? existingPassage = getPassageById(passageId);
if (existingPassage == null) {
// Créer un nouveau passage
final newPassage = PassageModel.fromJson(passageJson);
await savePassage(newPassage);
} else {
// Mettre à jour le passage existant avec les nouvelles données
final updatedPassage = PassageModel.fromJson(passageJson).copyWith(
lastSyncedAt: DateTime.now(),
isActive: existingPassage.isActive,
isSynced: true,
);
await savePassage(updatedPassage);
}
}
} catch (e) {
debugPrint('Erreur lors du traitement des passages: $e');
} finally {
_isLoading = false;
notifyListeners();
}
}
// Créer un nouveau passage
Future<bool> createPassage({
required int fkOperation,
required int fkSector,
required int fkUser,
required int fkType,
required String fkAdresse,
required DateTime passedAt,
required String numero,
required String rue,
String rueBis = '',
required String ville,
String residence = '',
required int fkHabitat,
String appt = '',
String niveau = '',
required String gpsLat,
required String gpsLng,
String nomRecu = '',
String remarque = '',
required String montant,
required int fkTypeReglement,
String name = '',
String email = '',
String phone = '',
}) async {
_isLoading = true;
notifyListeners();
try {
// Préparer les données pour l'API
final Map<String, dynamic> data = {
'fk_operation': fkOperation,
'fk_sector': fkSector,
'fk_user': fkUser,
'fk_type': fkType,
'fk_adresse': fkAdresse,
'passed_at': passedAt.toIso8601String(),
'numero': numero,
'rue': rue,
'rue_bis': rueBis,
'ville': ville,
'residence': residence,
'fk_habitat': fkHabitat,
'appt': appt,
'niveau': niveau,
'gps_lat': gpsLat,
'gps_lng': gpsLng,
'nom_recu': nomRecu,
'remarque': remarque,
'montant': montant,
'fk_type_reglement': fkTypeReglement,
'name': name,
'email': email,
'phone': phone,
};
// Appeler l'API pour créer le passage
final response = await _apiService.post('/passages', data: data);
if (response.statusCode == 201 || response.statusCode == 200) {
// Récupérer l'ID du nouveau passage
final passageId = response.data['id'] is String
? int.parse(response.data['id'])
: response.data['id'] as int;
// Créer le modèle local
final newPassage = PassageModel(
id: passageId,
fkOperation: fkOperation,
fkSector: fkSector,
fkUser: fkUser,
fkType: fkType,
fkAdresse: fkAdresse,
passedAt: passedAt,
numero: numero,
rue: rue,
rueBis: rueBis,
ville: ville,
residence: residence,
fkHabitat: fkHabitat,
appt: appt,
niveau: niveau,
gpsLat: gpsLat,
gpsLng: gpsLng,
nomRecu: nomRecu,
remarque: remarque,
montant: montant,
fkTypeReglement: fkTypeReglement,
nbPassages: 1, // Par défaut pour un nouveau passage
name: name,
email: email,
phone: phone,
lastSyncedAt: DateTime.now(),
isActive: true,
isSynced: true,
);
await savePassage(newPassage);
return true;
} else {
debugPrint('Erreur lors de la création du passage: ${response.statusMessage}');
return false;
}
} catch (e) {
debugPrint('Erreur lors de la création du passage: $e');
return false;
} finally {
_isLoading = false;
notifyListeners();
}
}
// Mettre à jour un passage existant
Future<bool> updatePassage(PassageModel passage) async {
_isLoading = true;
notifyListeners();
try {
// Préparer les données pour l'API
final Map<String, dynamic> data = passage.toJson();
// Appeler l'API pour mettre à jour le passage
final response = await _apiService.put('/passages/${passage.id}', data: data);
if (response.statusCode == 200) {
// Mettre à jour le modèle local
final updatedPassage = passage.copyWith(
lastSyncedAt: DateTime.now(),
isSynced: true,
);
await savePassage(updatedPassage);
return true;
} else {
debugPrint('Erreur lors de la mise à jour du passage: ${response.statusMessage}');
// Marquer comme non synchronisé mais sauvegarder localement
final updatedPassage = passage.copyWith(
lastSyncedAt: DateTime.now(),
isSynced: false,
);
await savePassage(updatedPassage);
return false;
}
} catch (e) {
debugPrint('Erreur lors de la mise à jour du passage: $e');
// Marquer comme non synchronisé mais sauvegarder localement
final updatedPassage = passage.copyWith(
lastSyncedAt: DateTime.now(),
isSynced: false,
);
await savePassage(updatedPassage);
return false;
} finally {
_isLoading = false;
notifyListeners();
}
}
// Synchroniser tous les passages non synchronisés
Future<void> syncUnsyncedPassages() async {
try {
final hasConnection = await _apiService.hasInternetConnection();
if (!hasConnection) {
return;
}
final unsyncedPassages = _passageBox.values.where((passage) => !passage.isSynced).toList();
if (unsyncedPassages.isEmpty) {
return;
}
_isLoading = true;
notifyListeners();
for (final passage in unsyncedPassages) {
try {
if (passage.id < 0) {
// Nouveau passage créé localement, à envoyer à l'API
await createPassage(
fkOperation: passage.fkOperation,
fkSector: passage.fkSector,
fkUser: passage.fkUser,
fkType: passage.fkType,
fkAdresse: passage.fkAdresse,
passedAt: passage.passedAt,
numero: passage.numero,
rue: passage.rue,
rueBis: passage.rueBis,
ville: passage.ville,
residence: passage.residence,
fkHabitat: passage.fkHabitat,
appt: passage.appt,
niveau: passage.niveau,
gpsLat: passage.gpsLat,
gpsLng: passage.gpsLng,
nomRecu: passage.nomRecu,
remarque: passage.remarque,
montant: passage.montant,
fkTypeReglement: passage.fkTypeReglement,
name: passage.name,
email: passage.email,
phone: passage.phone,
);
// Supprimer l'ancien passage avec ID temporaire
await deletePassage(passage.id);
} else {
// Passage existant à mettre à jour
await updatePassage(passage);
}
} catch (e) {
debugPrint('Erreur lors de la synchronisation du passage ${passage.id}: $e');
}
}
} catch (e) {
debugPrint('Erreur lors de la synchronisation des passages: $e');
} finally {
_isLoading = false;
notifyListeners();
}
}
// Récupérer les passages depuis l'API
Future<void> fetchPassages() async {
try {
final hasConnection = await _apiService.hasInternetConnection();
if (!hasConnection) {
return;
}
_isLoading = true;
notifyListeners();
final response = await _apiService.get('/passages');
if (response.statusCode == 200) {
final List<dynamic> passagesData = response.data;
await processPassagesFromApi(passagesData);
}
} catch (e) {
debugPrint('Erreur lors de la récupération des passages: $e');
} finally {
_isLoading = false;
notifyListeners();
}
}
// Vider tous les passages
Future<void> clearAllPassages() async {
await _passageBox.clear();
notifyListeners();
}
}

View File

@@ -0,0 +1,149 @@
import 'package:hive/hive.dart';
import 'package:geosector_app/core/data/models/sector_model.dart';
import 'package:geosector_app/core/services/api_service.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
class SectorRepository {
final ApiService _apiService;
SectorRepository(this._apiService);
// Utiliser un getter lazy pour n'accéder à la boîte que lorsque nécessaire
// et vérifier qu'elle est ouverte avant accès
Box<SectorModel> get _sectorsBox {
_ensureBoxIsOpen();
return Hive.box<SectorModel>(AppKeys.sectorsBoxName);
}
// Méthode pour vérifier si la boîte est ouverte et l'ouvrir si nécessaire
Future<void> _ensureBoxIsOpen() async {
final boxName = AppKeys.sectorsBoxName;
if (!Hive.isBoxOpen(boxName)) {
print('Ouverture de la boîte $boxName dans SectorRepository...');
await Hive.openBox<SectorModel>(boxName);
}
}
// Récupérer tous les secteurs depuis la base de données locale
List<SectorModel> getAllSectors() {
return _sectorsBox.values.toList();
}
// Récupérer un secteur par son ID
SectorModel? getSectorById(int id) {
try {
return _sectorsBox.values.firstWhere(
(sector) => sector.id == id,
);
} catch (e) {
return null;
}
}
// Sauvegarder les secteurs dans la base de données locale
Future<void> saveSectors(List<SectorModel> sectors) async {
// Vider la box avant d'ajouter les nouveaux secteurs
await _sectorsBox.clear();
// Ajouter les nouveaux secteurs
for (final sector in sectors) {
await _sectorsBox.put(sector.id, sector);
}
}
// Ajouter ou mettre à jour un secteur
Future<void> saveSector(SectorModel sector) async {
await _sectorsBox.put(sector.id, sector);
}
// Supprimer un secteur
Future<void> deleteSector(int id) async {
await _sectorsBox.delete(id);
}
// Récupérer les secteurs depuis l'API
Future<List<SectorModel>> fetchSectorsFromApi() async {
try {
final response = await _apiService.get(AppKeys.sectorsEndpoint);
final Map<String, dynamic> responseData = response as Map<String, dynamic>;
if (responseData['status'] == 'success' && responseData['sectors'] != null) {
final List<dynamic> sectorsJson = responseData['sectors'];
final List<SectorModel> sectors = sectorsJson
.map((json) => SectorModel.fromJson(json))
.toList();
// Sauvegarder les secteurs localement
await saveSectors(sectors);
return sectors;
}
return [];
} catch (e) {
// En cas d'erreur, retourner les secteurs locaux
return getAllSectors();
}
}
// Créer un nouveau secteur via l'API
Future<SectorModel?> createSector(SectorModel sector) async {
try {
final response = await _apiService.post(
AppKeys.sectorsEndpoint,
data: sector.toJson(),
);
final Map<String, dynamic> responseData = response as Map<String, dynamic>;
if (responseData['status'] == 'success' && responseData['sector'] != null) {
final SectorModel newSector = SectorModel.fromJson(responseData['sector']);
await saveSector(newSector);
return newSector;
}
return null;
} catch (e) {
return null;
}
}
// Mettre à jour un secteur via l'API
Future<SectorModel?> updateSector(SectorModel sector) async {
try {
final response = await _apiService.put(
'${AppKeys.sectorsEndpoint}/${sector.id}',
data: sector.toJson(),
);
final Map<String, dynamic> responseData = response as Map<String, dynamic>;
if (responseData['status'] == 'success' && responseData['sector'] != null) {
final SectorModel updatedSector = SectorModel.fromJson(responseData['sector']);
await saveSector(updatedSector);
return updatedSector;
}
return null;
} catch (e) {
return null;
}
}
// Supprimer un secteur via l'API
Future<bool> deleteSectorFromApi(int id) async {
try {
final response = await _apiService.delete(
'${AppKeys.sectorsEndpoint}/$id',
);
final Map<String, dynamic> responseData = response as Map<String, dynamic>;
if (responseData['status'] == 'success') {
await deleteSector(id);
return true;
}
return false;
} catch (e) {
return false;
}
}
}

View File

@@ -0,0 +1,958 @@
import 'dart:async';
import 'dart:io';
import 'dart:js' as js;
import 'package:geosector_app/core/services/hive_web_fix.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:path_provider/path_provider.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
import 'package:geosector_app/core/services/api_service.dart';
import 'package:geosector_app/core/services/sync_service.dart';
import 'package:geosector_app/core/data/models/user_model.dart';
import 'package:geosector_app/core/data/models/operation_model.dart';
import 'package:geosector_app/core/data/models/sector_model.dart';
import 'package:geosector_app/core/data/models/passage_model.dart';
import 'package:geosector_app/core/data/models/membre_model.dart';
import 'package:geosector_app/core/repositories/operation_repository.dart';
import 'package:geosector_app/core/repositories/sector_repository.dart';
import 'package:geosector_app/core/repositories/passage_repository.dart';
import 'package:geosector_app/chat/models/conversation_model.dart';
import 'package:geosector_app/chat/models/message_model.dart';
class UserRepository extends ChangeNotifier {
// Utilisation de getters lazy pour n'accéder aux boîtes que lorsque nécessaire
Box<UserModel> get _userBox => Hive.box<UserModel>(AppKeys.usersBoxName);
// Getters pour les autres boîtes qui vérifient si elles sont ouvertes avant accès
Box<OperationModel> get _operationBox {
_ensureBoxIsOpen(AppKeys.operationsBoxName);
return Hive.box<OperationModel>(AppKeys.operationsBoxName);
}
Box<SectorModel> get _sectorBox {
_ensureBoxIsOpen(AppKeys.sectorsBoxName);
return Hive.box<SectorModel>(AppKeys.sectorsBoxName);
}
// Méthode pour initialiser les boîtes après connexion
Future<void> _initializeBoxes() async {
debugPrint('Initialisation des boîtes Hive nécessaires...');
await _ensureBoxIsOpen(AppKeys.operationsBoxName);
await _ensureBoxIsOpen(AppKeys.sectorsBoxName);
await _ensureBoxIsOpen(AppKeys.passagesBoxName);
await _ensureBoxIsOpen(AppKeys.membresBoxName);
// Les boîtes de chat sont déjà initialisées au démarrage
await _ensureBoxIsOpen(AppKeys.chatConversationsBoxName);
await _ensureBoxIsOpen(AppKeys.chatMessagesBoxName);
debugPrint('Toutes les boîtes Hive sont maintenant ouvertes');
}
final ApiService _apiService;
final SyncService? _syncService;
final OperationRepository? _operationRepository;
final SectorRepository? _sectorRepository;
final PassageRepository? _passageRepository;
bool _isLoading = false;
UserRepository(this._apiService,
{SyncService? syncService,
OperationRepository? operationRepository,
SectorRepository? sectorRepository,
PassageRepository? passageRepository})
: _syncService = syncService,
_operationRepository = operationRepository,
_sectorRepository = sectorRepository,
_passageRepository = passageRepository {
// Initialiser la session si un utilisateur est déjà connecté
final currentUser = getCurrentUser();
if (currentUser != null && currentUser.sessionId != null) {
setSessionId(currentUser.sessionId);
}
}
// Getters
bool get isLoading => _isLoading;
bool get isLoggedIn => getCurrentUser() != null;
// Vérifie si l'utilisateur a un rôle administrateur (2, 4 ou 9)
bool isAdmin() {
final user = getCurrentUser();
if (user == null) return false;
final String interface = user.interface ?? 'user';
return interface == 'admin';
}
int? get userId => getCurrentUser()?.id;
UserModel? get currentUser => getCurrentUser();
// Récupérer l'utilisateur actuellement connecté
UserModel? getCurrentUser() {
try {
// Chercher un utilisateur avec une session active
final activeUsers = _userBox.values
.where((user) =>
user.sessionId != null && // Vérifier que sessionId n'est pas null
user.sessionId!
.isNotEmpty && // Vérifier que sessionId n'est pas vide
user.sessionExpiry != null &&
user.sessionExpiry!.isAfter(DateTime.now()))
.toList();
return activeUsers.isNotEmpty ? activeUsers.first : null;
} catch (e) {
debugPrint('Erreur lors de la récupération de l\'utilisateur actuel: $e');
return null;
}
}
// Mettre à jour le chemin de la page actuelle pour l'utilisateur connecté
Future<void> updateLastPath(String path) async {
final currentUser = getCurrentUser();
if (currentUser != null) {
final updatedUser = currentUser.copyWith(lastPath: path);
await saveUser(updatedUser);
}
}
// Récupérer le dernier chemin visité par l'utilisateur
String? getLastPath() {
final currentUser = getCurrentUser();
return currentUser?.lastPath;
}
// Configurer la session dans l'API
void setSessionId(String? sessionId) {
_apiService.setSessionId(sessionId);
}
// Login API PHP
Future<Map<String, dynamic>> loginAPI(String username, String password,
{String type = 'admin'}) async {
try {
return await _apiService.login(username, password, type: type);
} catch (e) {
debugPrint('Erreur login API: $e');
rethrow;
}
}
// Register API PHP - Uniquement pour les administrateurs
Future<Map<String, dynamic>> registerAPI(String email, String name,
String amicaleName, String postalCode, String cityName) async {
try {
final Map<String, dynamic> data = {
'email': email,
'name': name,
'amicale_name': amicaleName,
'postal_code': postalCode,
'city_name': cityName
};
final response =
await _apiService.post(AppKeys.registerEndpoint, data: data);
return response.data;
} catch (e) {
debugPrint('Erreur register API: $e');
rethrow;
}
}
// Logout API PHP
Future<void> logoutAPI() async {
try {
await _apiService.logout();
} catch (e) {
debugPrint('Erreur logout API: $e');
rethrow;
}
}
// Méthode d'inscription (uniquement pour les administrateurs)
Future<bool> register(String email, String password, String name,
String amicaleName, String postalCode, String cityName) async {
_isLoading = true;
notifyListeners();
try {
// Enregistrer l'administrateur via l'API
final apiResult =
await registerAPI(email, name, amicaleName, postalCode, cityName);
// Créer l'administrateur local
final int userId = apiResult['user_id'] is String
? int.parse(apiResult['user_id'])
: apiResult['user_id'];
final now = DateTime.now();
final newAdmin = UserModel(
id: userId,
email: email,
name: name,
role: AppKeys.roleAdmin2,
createdAt: now,
lastSyncedAt: now,
isActive: true,
isSynced: true,
sessionId: apiResult['session_id'],
sessionExpiry: DateTime.parse(apiResult['session_expiry']),
);
// Sauvegarder dans le repository local
await saveUser(newAdmin);
// Configurer la session dans l'API
setSessionId(newAdmin.sessionId);
notifyListeners();
return true;
} catch (e) {
debugPrint('Erreur d\'inscription: $e');
return false;
} finally {
_isLoading = false;
notifyListeners();
}
}
// Login complet
Future<bool> login(String username, String password,
{String type = 'admin'}) async {
_isLoading = true;
notifyListeners();
try {
debugPrint('Début du processus de connexion pour: $username');
// Supprimer les références aux boîtes non définies dans AppKeys
// pour éviter les erreurs de suppression de boîtes non référencées
final nonDefinedBoxes = ['auth', 'locations', 'messages'];
for (final boxName in nonDefinedBoxes) {
try {
if (Hive.isBoxOpen(boxName)) {
debugPrint('Fermeture de la boîte non référencée: $boxName');
await Hive.box(boxName).close();
}
// Supprimer la boîte du disque
await Hive.deleteBoxFromDisk(boxName);
debugPrint('Nettoyage: Box $boxName supprimée');
} catch (e) {
debugPrint(
'Erreur lors de la suppression de la boîte non référencée $boxName: $e');
}
}
// S'assurer que toutes les Hive boxes sont vides avant de se connecter
// Vider toutes les boîtes Hive SAUF la boîte des utilisateurs
debugPrint('Nettoyage des données existantes avant connexion...');
// Sur le web, utiliser notre méthode sécurisée pour nettoyer les boîtes Hive
if (kIsWeb) {
await HiveWebFix.safeCleanHiveBoxes(
excludeBoxes: [AppKeys.usersBoxName]);
}
// Sur iOS, nettoyer les fichiers Hive directement
else if (!kIsWeb && Platform.isIOS) {
await _cleanHiveFilesOnIOS();
}
// Sur Android, nettoyer les fichiers Hive directement
else if (!kIsWeb && Platform.isAndroid) {
await _cleanHiveFilesOnAndroid();
}
// Nettoyer les boîtes sans les fermer
await _clearAndRecreateBoxes();
// Initialiser les boîtes nécessaires avant d'appeler l'API
// Cela garantit que toutes les boîtes sont ouvertes avant le traitement des données
await _initializeBoxes();
// Appeler l'API
debugPrint('Appel de l\'API de connexion (type: $type)...');
final apiResult = await loginAPI(username, password, type: type);
// Vérifier le statut de la réponse
final status = apiResult['status'] as String?;
final message = apiResult['message'] as String?;
// Si le statut n'est pas 'success', retourner false
if (status != 'success') {
debugPrint('Échec de connexion: $message');
return false;
}
debugPrint('Connexion réussie, traitement des données...');
// [Reste de la méthode login inchangé...]
return true;
} catch (e) {
debugPrint('Erreur de connexion: $e');
return false;
} finally {
_isLoading = false;
notifyListeners();
}
}
// Méthode pour vérifier si une boîte est ouverte et l'ouvrir si nécessaire
Future<void> _ensureBoxIsOpen(String boxName) async {
try {
if (!Hive.isBoxOpen(boxName)) {
debugPrint('Ouverture de la boîte $boxName...');
if (boxName == AppKeys.passagesBoxName) {
await Hive.openBox<PassageModel>(boxName);
} else if (boxName == AppKeys.operationsBoxName) {
await Hive.openBox<OperationModel>(boxName);
} else if (boxName == AppKeys.sectorsBoxName) {
await Hive.openBox<SectorModel>(boxName);
} else if (boxName == AppKeys.usersBoxName) {
await Hive.openBox<UserModel>(boxName);
} else if (boxName == AppKeys.membresBoxName) {
await Hive.openBox<MembreModel>(boxName);
} else if (boxName == AppKeys.settingsBoxName) {
await Hive.openBox(boxName);
} else if (boxName == AppKeys.chatConversationsBoxName) {
await Hive.openBox<ConversationModel>(boxName);
} else if (boxName == AppKeys.chatMessagesBoxName) {
await Hive.openBox<MessageModel>(boxName);
} else {
await Hive.openBox(boxName);
}
// Boîte ouverte avec succès
} else {
// La boîte est déjà ouverte
}
} catch (e) {
debugPrint('Erreur lors de l\'ouverture de la boîte $boxName: $e');
throw Exception('Impossible d\'ouvrir la boîte $boxName: $e');
}
}
// Méthode pour vider et recréer toutes les boîtes Hive sauf la boîte des utilisateurs
Future<void> _clearAndRecreateBoxes() async {
try {
debugPrint('Début de la suppression complète des données Hive...');
// Supprimer les références aux boîtes non définies dans AppKeys
// pour éviter les erreurs de suppression de boîtes non référencées
final nonDefinedBoxes = ['auth', 'locations', 'messages'];
for (final boxName in nonDefinedBoxes) {
try {
if (Hive.isBoxOpen(boxName)) {
debugPrint('Fermeture de la boîte non référencée: $boxName');
await Hive.box(boxName).close();
}
// Supprimer la boîte du disque
await Hive.deleteBoxFromDisk(boxName);
debugPrint('Nettoyage: Box $boxName supprimée');
} catch (e) {
debugPrint(
'Erreur lors de la suppression de la boîte non référencée $boxName: $e');
}
}
// Sur le web, utiliser notre méthode sécurisée pour nettoyer les boîtes Hive
if (kIsWeb) {
await HiveWebFix.safeCleanHiveBoxes(
excludeBoxes: [AppKeys.usersBoxName]);
}
// Sur iOS, nettoyer les fichiers Hive directement
else if (Platform.isIOS) {
await _cleanHiveFilesOnIOS();
}
// Sur Android, nettoyer les fichiers Hive directement
else if (Platform.isAndroid) {
await _cleanHiveFilesOnAndroid();
}
// Liste des noms de boîtes à supprimer
final boxesToDelete = [
AppKeys.passagesBoxName,
AppKeys.operationsBoxName,
AppKeys.sectorsBoxName,
AppKeys.chatConversationsBoxName,
AppKeys.chatMessagesBoxName,
];
// Vider chaque boîte sans la fermer
for (final boxName in boxesToDelete) {
try {
debugPrint('Nettoyage de la boîte: $boxName');
// Vérifier si la boîte est déjà ouverte
if (Hive.isBoxOpen(boxName)) {
// Vider la boîte sans la fermer
debugPrint('Boîte $boxName déjà ouverte, vidage sans fermeture');
if (boxName == AppKeys.passagesBoxName) {
await Hive.box<PassageModel>(boxName).clear();
} else if (boxName == AppKeys.operationsBoxName) {
await Hive.box<OperationModel>(boxName).clear();
} else if (boxName == AppKeys.sectorsBoxName) {
await Hive.box<SectorModel>(boxName).clear();
} else if (boxName == AppKeys.chatConversationsBoxName) {
await Hive.box<ConversationModel>(boxName).clear();
} else if (boxName == AppKeys.chatMessagesBoxName) {
await Hive.box<MessageModel>(boxName).clear();
}
} else {
// Supprimer la boîte du disque si elle n'est pas ouverte
debugPrint('Boîte $boxName non ouverte, suppression du disque');
await Hive.deleteBoxFromDisk(boxName);
}
} catch (e) {
debugPrint('Erreur lors du nettoyage de la boîte $boxName: $e');
// Tenter de supprimer la boîte du disque en cas d'erreur
try {
await Hive.deleteBoxFromDisk(boxName);
} catch (deleteError) {
debugPrint(
'Impossible de supprimer la boîte $boxName: $deleteError');
}
}
}
// Attendre un court instant pour s'assurer que les opérations de suppression sont terminées
await Future.delayed(const Duration(milliseconds: 500));
// Recréer les boîtes avec la méthode sécurisée
debugPrint('Recréation des boîtes Hive...');
// Utiliser notre méthode pour s'assurer que les boîtes sont ouvertes
try {
// Passages
await _ensureBoxIsOpen(AppKeys.passagesBoxName);
// Opérations
await _ensureBoxIsOpen(AppKeys.operationsBoxName);
// Secteurs
await _ensureBoxIsOpen(AppKeys.sectorsBoxName);
// Chat
await _ensureBoxIsOpen(AppKeys.chatConversationsBoxName);
await _ensureBoxIsOpen(AppKeys.chatMessagesBoxName);
// Vérifier l'intégrité des boîtes après recréation
await _verifyHiveBoxesIntegrity();
} catch (e) {
debugPrint('Erreur lors de la recréation des boîtes Hive: $e');
// Tentative de récupération sur erreur
if (kIsWeb) {
debugPrint('Tentative de récupération sur le web...');
await HiveWebFix.resetHiveCompletely();
// Réessayer d'ouvrir les boîtes
await _ensureBoxIsOpen(AppKeys.passagesBoxName);
await _ensureBoxIsOpen(AppKeys.operationsBoxName);
await _ensureBoxIsOpen(AppKeys.sectorsBoxName);
await _ensureBoxIsOpen(AppKeys.chatConversationsBoxName);
await _ensureBoxIsOpen(AppKeys.chatMessagesBoxName);
}
}
} catch (e) {
debugPrint('Erreur lors de la réinitialisation des boîtes Hive: $e');
}
}
// Méthode pour vérifier l'intégrité des boîtes Hive après recréation
Future<void> _verifyHiveBoxesIntegrity() async {
try {
debugPrint('Vérification de l\'intégrité des boîtes Hive...');
// Liste des boîtes à vérifier avec leur type
final boxesToCheck = [
{'name': AppKeys.passagesBoxName, 'type': 'passage'},
{'name': AppKeys.operationsBoxName, 'type': 'operation'},
{'name': AppKeys.sectorsBoxName, 'type': 'sector'},
{'name': AppKeys.chatConversationsBoxName, 'type': 'conversation'},
{'name': AppKeys.chatMessagesBoxName, 'type': 'message'},
];
// Vérifier chaque boîte
for (final boxInfo in boxesToCheck) {
final boxName = boxInfo['name'] as String;
final boxType = boxInfo['type'] as String;
try {
if (Hive.isBoxOpen(boxName)) {
// Utiliser une approche spécifique au type pour éviter les erreurs de typage
Box box;
try {
if (boxType == 'passage') {
box = Hive.box<PassageModel>(boxName);
} else if (boxType == 'operation') {
box = Hive.box<OperationModel>(boxName);
} else if (boxType == 'sector') {
box = Hive.box<SectorModel>(boxName);
} else if (boxType == 'conversation') {
box = Hive.box<ConversationModel>(boxName);
} else if (boxType == 'message') {
box = Hive.box<MessageModel>(boxName);
} else {
box = Hive.box(boxName);
}
final count = box.length;
debugPrint('Boîte $boxName: $count éléments');
// Si la boîte contient des éléments, c'est anormal après recréation
if (count > 0) {
debugPrint(
'ATTENTION: La boîte $boxName contient encore des données après recréation');
// Essayer de vider la boîte une dernière fois
await box.clear();
debugPrint('Vidage forcé de la boîte $boxName effectué');
}
} catch (typeError) {
debugPrint(
'Erreur de typage lors de la vérification de $boxName: $typeError');
// Tentative alternative sans typage spécifique
try {
box = Hive.box(boxName);
final count = box.length;
debugPrint('Boîte $boxName (sans typage): $count éléments');
if (count > 0) {
await box.clear();
debugPrint(
'Vidage forcé de la boîte $boxName (sans typage) effectué');
}
} catch (e2) {
debugPrint(
'Impossible de vérifier la boîte $boxName même sans typage: $e2');
}
}
} else {
debugPrint(
'Boîte $boxName non ouverte, impossible de vérifier l\'intégrité');
}
} catch (e) {
debugPrint('Erreur lors de la vérification de la boîte $boxName: $e');
}
}
debugPrint('Vérification d\'intégrité terminée');
} catch (e) {
debugPrint(
'Erreur lors de la vérification d\'intégrité des boîtes Hive: $e');
}
}
// Méthode spéciale pour nettoyer IndexedDB sur le web
Future<void> _clearIndexedDB() async {
if (kIsWeb) {
try {
debugPrint('Nettoyage complet d\'IndexedDB sur le web...');
// Utiliser JavaScript pour nettoyer IndexedDB
js.context.callMethod('eval', [
'''
var request = indexedDB.deleteDatabase("geosector_app");
request.onsuccess = function() { console.log("IndexedDB nettoyé avec succès"); };
request.onerror = function() { console.log("Erreur lors du nettoyage d\'IndexedDB"); };
'''
]);
await Future.delayed(const Duration(milliseconds: 500));
debugPrint('Nettoyage d\'IndexedDB terminé');
} catch (e) {
debugPrint('Erreur lors du nettoyage d\'IndexedDB: $e');
}
}
}
// Méthode spéciale pour nettoyer les fichiers Hive sur iOS
Future<void> _cleanHiveFilesOnIOS() async {
if (!kIsWeb && Platform.isIOS) {
try {
debugPrint('Nettoyage des fichiers Hive sur iOS...');
final appDir = await getApplicationDocumentsDirectory();
final hiveDir = Directory('${appDir.path}/hive');
if (await hiveDir.exists()) {
debugPrint('Suppression du répertoire Hive: ${hiveDir.path}');
// Exclure le dossier des utilisateurs pour conserver les informations de session
final entries = await hiveDir.list().toList();
for (var entry in entries) {
final name = entry.path.split('/').last;
// Ne pas supprimer la boîte des utilisateurs
if (!name.contains(AppKeys.usersBoxName)) {
debugPrint('Suppression de: ${entry.path}');
if (entry is Directory) {
await entry.delete(recursive: true);
} else if (entry is File) {
await entry.delete();
}
}
}
debugPrint('Nettoyage des fichiers Hive sur iOS terminé');
} else {
debugPrint('Répertoire Hive non trouvé');
}
} catch (e) {
debugPrint('Erreur lors du nettoyage des fichiers Hive sur iOS: $e');
}
}
}
// Méthode spéciale pour nettoyer les fichiers Hive sur Android
Future<void> _cleanHiveFilesOnAndroid() async {
if (!kIsWeb && Platform.isAndroid) {
try {
debugPrint('Nettoyage des fichiers Hive sur Android...');
final appDir = await getApplicationDocumentsDirectory();
final hiveDir = Directory('${appDir.path}');
if (await hiveDir.exists()) {
debugPrint('Recherche des fichiers Hive dans: ${hiveDir.path}');
// Sur Android, les fichiers Hive sont directement dans le répertoire de l'application
final entries = await hiveDir.list().toList();
int filesDeleted = 0;
for (var entry in entries) {
final name = entry.path.split('/').last;
// Ne supprimer que les fichiers Hive, mais pas la boîte des utilisateurs
if (name.endsWith('.hive') &&
!name.contains(AppKeys.usersBoxName)) {
debugPrint('Suppression du fichier Hive: ${entry.path}');
if (entry is File) {
await entry.delete();
filesDeleted++;
// Supprimer également les fichiers lock associés
final lockFile = File('${entry.path}.lock');
if (await lockFile.exists()) {
await lockFile.delete();
debugPrint('Suppression du fichier lock: ${lockFile.path}');
}
}
}
}
debugPrint(
'Nettoyage des fichiers Hive sur Android terminé. $filesDeleted fichiers supprimés.');
} else {
debugPrint('Répertoire d\'application non trouvé');
}
} catch (e) {
debugPrint(
'Erreur lors du nettoyage des fichiers Hive sur Android: $e');
}
}
}
// Logout complet
Future<bool> logout() async {
_isLoading = true;
notifyListeners();
try {
debugPrint('Début du processus de déconnexion...');
// S'assurer que la boîte des utilisateurs est ouverte avant tout
await _ensureBoxIsOpen(AppKeys.usersBoxName);
// Supprimer les références aux boîtes non définies dans AppKeys
final nonDefinedBoxes = ['auth', 'locations', 'messages'];
for (final boxName in nonDefinedBoxes) {
try {
if (Hive.isBoxOpen(boxName)) {
debugPrint('Fermeture de la boîte non référencée: $boxName');
await Hive.box(boxName).close();
}
// Supprimer la boîte du disque
await Hive.deleteBoxFromDisk(boxName);
debugPrint('Nettoyage: Box $boxName supprimée');
} catch (e) {
debugPrint(
'Erreur lors de la suppression de la boîte non référencée $boxName: $e');
}
}
// Récupérer l'utilisateur actuel avant de nettoyer les données
final currentUser = getCurrentUser();
if (currentUser == null) {
debugPrint('Aucun utilisateur connecté, déconnexion terminée');
return true;
}
debugPrint('Déconnexion de l\'utilisateur: ${currentUser.email}');
// Appeler l'API pour déconnecter la session
if (currentUser.sessionId != null) {
debugPrint('Déconnexion de la session API...');
await logoutAPI();
}
// Effacer la session de l'utilisateur
debugPrint('Mise à jour de l\'utilisateur pour effacer la session...');
final updatedUser = currentUser.copyWith(
sessionId: null,
sessionExpiry: null,
lastPath:
null, // Réinitialiser le chemin pour revenir à l'écran de connexion
);
// Sauvegarder l'utilisateur sans session
await saveUser(updatedUser);
// Effacer la session de l'API
setSessionId(null);
// Maintenant, nettoyer les données
debugPrint('Nettoyage des données...');
// Sur le web, utiliser notre méthode sécurisée pour nettoyer les boîtes Hive
if (kIsWeb) {
await HiveWebFix.safeCleanHiveBoxes(
excludeBoxes: [AppKeys.usersBoxName]);
}
// Sur iOS, nettoyer les fichiers Hive directement
else if (!kIsWeb && Platform.isIOS) {
await _cleanHiveFilesOnIOS();
}
// Sur Android, nettoyer les fichiers Hive directement
else if (!kIsWeb && Platform.isAndroid) {
await _cleanHiveFilesOnAndroid();
}
// Vider les boîtes sans les fermer, y compris les boîtes de chat
debugPrint('Suppression des données Hive...');
await _clearAndRecreateBoxes();
// Vider spécifiquement les boîtes de chat si elles sont ouvertes
try {
if (Hive.isBoxOpen(AppKeys.chatConversationsBoxName)) {
await Hive.box<ConversationModel>(AppKeys.chatConversationsBoxName).clear();
debugPrint('Boîte conversations vidée');
}
if (Hive.isBoxOpen(AppKeys.chatMessagesBoxName)) {
await Hive.box<MessageModel>(AppKeys.chatMessagesBoxName).clear();
debugPrint('Boîte messages vidée');
}
} catch (e) {
debugPrint('Erreur lors du vidage des boîtes de chat: $e');
}
debugPrint('Déconnexion terminée avec succès');
notifyListeners();
return true;
} catch (e) {
debugPrint('Erreur de déconnexion: $e');
return false;
} finally {
_isLoading = false;
notifyListeners();
}
}
// Obtenir tous les utilisateurs locaux
List<UserModel> getAllUsers() {
return _userBox.values.toList();
}
// Obtenir un utilisateur par son ID
UserModel? getUserById(int id) {
return _userBox.get(id);
}
// Obtenir un utilisateur par son email
UserModel? getUserByEmail(String email) {
try {
return _userBox.values.firstWhere(
(user) => user.email == email,
);
} catch (e) {
return null; // Utilisateur non trouvé
}
}
// Créer ou mettre à jour un utilisateur localement
Future<UserModel> saveUser(UserModel user) async {
await _userBox.put(user.id, user);
notifyListeners(); // Notifier les changements pour mettre à jour l'UI
return user;
}
// Supprimer un utilisateur localement
Future<void> deleteUser(String id) async {
await _userBox.delete(id);
}
// Créer un nouvel utilisateur localement et tenter de le synchroniser
Future<UserModel> createUser({
required String email,
required String name,
required int role,
}) async {
// Générer un ID numérique temporaire (timestamp)
final int tempId = DateTime.now().millisecondsSinceEpoch;
final now = DateTime.now();
final user = UserModel(
id: tempId,
email: email,
name: name,
role: role,
createdAt: now,
lastSyncedAt: now,
isSynced: false,
);
await _userBox.put(user.id, user);
// Tenter de synchroniser si possible
await syncUser(user);
return user;
}
// Synchroniser un utilisateur spécifique avec le serveur
Future<UserModel> syncUser(UserModel user) async {
try {
final hasConnection = await _apiService.hasInternetConnection();
if (!hasConnection) {
return user;
}
UserModel syncedUser;
if (!user.isSynced) {
// Si l'utilisateur n'est pas encore synchronisé, le créer sur le serveur
syncedUser = await _apiService.createUser(user);
} else {
// Sinon, mettre à jour les informations
syncedUser = await _apiService.updateUser(user);
}
// Mettre à jour l'utilisateur local avec les informations du serveur
final updatedUser = syncedUser.copyWith(
isSynced: true,
lastSyncedAt: DateTime.now(),
);
await _userBox.put(updatedUser.id, updatedUser);
return updatedUser;
} catch (e) {
// En cas d'erreur, garder l'utilisateur local tel quel
return user;
}
}
// Synchroniser tous les utilisateurs non synchronisés
Future<void> syncAllUsers() async {
try {
final hasConnection = await _apiService.hasInternetConnection();
if (!hasConnection) {
return;
}
final unsyncedUsers =
_userBox.values.where((user) => !user.isSynced).toList();
if (unsyncedUsers.isEmpty) {
return;
}
// Synchroniser en batch
final result = await _apiService.syncData(users: unsyncedUsers);
// Mettre à jour les utilisateurs locaux
if (result['users'] != null) {
for (final userData in result['users']) {
final syncedUser = UserModel.fromJson(userData);
await _userBox.put(
syncedUser.id,
syncedUser.copyWith(
isSynced: true,
lastSyncedAt: DateTime.now(),
),
);
}
}
} catch (e) {
// Gérer les erreurs de synchronisation
print('Erreur de synchronisation des utilisateurs: $e');
}
}
// Rafraîchir les données depuis le serveur
Future<void> refreshFromServer() async {
try {
final hasConnection = await _apiService.hasInternetConnection();
if (!hasConnection) {
return;
}
// Récupérer tous les utilisateurs du serveur
final serverUsers = await _apiService.getUsers();
// Mettre à jour la base locale
for (final serverUser in serverUsers) {
final updatedUser = serverUser.copyWith(
isSynced: true,
lastSyncedAt: DateTime.now(),
);
await _userBox.put(updatedUser.id, updatedUser);
}
} catch (e) {
// Gérer les erreurs
print('Erreur lors du rafraîchissement des données: $e');
}
}
// Synchroniser les données utilisateur
Future<void> syncUserData() async {
if (_syncService != null && currentUser != null) {
await _syncService!.syncUserData(currentUser!.id);
}
}
// Récupérer la dernière opération active (avec isActive == true)
OperationModel? getCurrentOperation() {
try {
// Récupérer toutes les opérations
final operations = _operationBox.values.toList();
// Filtrer pour ne garder que les opérations actives
final activeOperations = operations.where((op) => op.isActive).toList();
// Si aucune opération active n'est trouvée, retourner null
if (activeOperations.isEmpty) {
return operations.isNotEmpty ? operations.last : null;
}
// Retourner la dernière opération active
return activeOperations.last;
} catch (e) {
debugPrint('Erreur lors de la récupération de l\'opération actuelle: $e');
return null;
}
}
// Récupérer tous les secteurs de l'utilisateur
List<SectorModel> getUserSectors() {
try {
return _sectorBox.values.toList();
} catch (e) {
debugPrint('Erreur lors de la récupération des secteurs: $e');
return [];
}
}
// Récupérer un secteur par son ID
SectorModel? getSectorById(int id) {
try {
return _sectorBox.get(id);
} catch (e) {
debugPrint('Erreur lors de la récupération du secteur: $e');
return null;
}
}
}

View File

@@ -0,0 +1,204 @@
import 'dart:async';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:geosector_app/core/data/models/user_model.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
import 'package:retry/retry.dart';
class ApiService {
final Dio _dio = Dio();
final String _baseUrl = AppKeys.baseApiUrl;
String? _sessionId;
ApiService() {
_dio.options.baseUrl = _baseUrl;
_dio.options.connectTimeout = AppKeys.connectionTimeout;
_dio.options.receiveTimeout = AppKeys.receiveTimeout;
_dio.options.headers.addAll(AppKeys.defaultHeaders);
// Ajouter des intercepteurs pour l'authentification par session
_dio.interceptors.add(InterceptorsWrapper(onRequest: (options, handler) {
// Ajouter le session_id comme token Bearer aux en-têtes si disponible
if (_sessionId != null) {
options.headers[AppKeys.sessionHeader] = 'Bearer $_sessionId';
}
return handler.next(options);
}, onError: (DioException error, handler) {
// Gérer les erreurs d'authentification (401)
if (error.response?.statusCode == 401) {
// Session expirée ou invalide
_sessionId = null;
}
return handler.next(error);
}));
}
// Définir l'ID de session
void setSessionId(String? sessionId) {
_sessionId = sessionId;
}
// Vérifier la connectivité réseau
Future<bool> hasInternetConnection() async {
final connectivityResult = await (Connectivity().checkConnectivity());
return connectivityResult != ConnectivityResult.none;
}
// Méthode POST générique
Future<Response> post(String path, {dynamic data}) async {
try {
return await _dio.post(path, data: data);
} catch (e) {
rethrow;
}
}
// Méthode GET générique
Future<Response> get(String path, {Map<String, dynamic>? queryParameters}) async {
try {
return await _dio.get(path, queryParameters: queryParameters);
} catch (e) {
rethrow;
}
}
// Méthode PUT générique
Future<Response> put(String path, {dynamic data}) async {
try {
return await _dio.put(path, data: data);
} catch (e) {
rethrow;
}
}
// Méthode DELETE générique
Future<Response> delete(String path) async {
try {
return await _dio.delete(path);
} catch (e) {
rethrow;
}
}
// Authentification avec PHP session
Future<Map<String, dynamic>> login(String username, String password, {String type = 'admin'}) async {
try {
final response = await _dio.post(AppKeys.loginEndpoint, data: {
'username': username,
'password': password,
'type': type, // Ajouter le type de connexion (user ou admin)
});
// Vérifier la structure de la réponse
final data = response.data as Map<String, dynamic>;
final status = data['status'] as String?;
// Afficher le message en cas d'erreur
if (status != 'success') {
final message = data['message'] as String?;
debugPrint('Erreur d\'authentification: $message');
}
// Si le statut est 'success', récupérer le session_id
if (status == 'success' && data.containsKey('session_id')) {
final sessionId = data['session_id'];
// Définir la session pour les futures requêtes
if (sessionId != null) {
setSessionId(sessionId);
}
}
return data;
} catch (e) {
rethrow;
}
}
// Déconnexion
Future<void> logout() async {
try {
if (_sessionId != null) {
await _dio.post(AppKeys.logoutEndpoint);
_sessionId = null;
}
} catch (e) {
// Même en cas d'erreur, on réinitialise la session
_sessionId = null;
rethrow;
}
}
// Utilisateurs
Future<List<UserModel>> getUsers() async {
try {
final response = await retry(
() => _dio.get('/users'),
retryIf: (e) => e is SocketException || e is TimeoutException,
);
return (response.data as List)
.map((json) => UserModel.fromJson(json))
.toList();
} catch (e) {
// Gérer les erreurs
rethrow;
}
}
Future<UserModel> getUserById(int id) async {
try {
final response = await _dio.get('/users/$id');
return UserModel.fromJson(response.data);
} catch (e) {
rethrow;
}
}
Future<UserModel> createUser(UserModel user) async {
try {
final response = await _dio.post('/users', data: user.toJson());
return UserModel.fromJson(response.data);
} catch (e) {
rethrow;
}
}
Future<UserModel> updateUser(UserModel user) async {
try {
final response = await _dio.put('/users/${user.id}', data: user.toJson());
return UserModel.fromJson(response.data);
} catch (e) {
rethrow;
}
}
Future<void> deleteUser(String id) async {
try {
await _dio.delete('/users/$id');
} catch (e) {
rethrow;
}
}
// Espace réservé pour les futures méthodes de gestion des profils
// Espace réservé pour les futures méthodes de gestion des données
// Synchronisation en batch
Future<Map<String, dynamic>> syncData({
List<UserModel>? users,
}) async {
try {
final Map<String, dynamic> payload = {
if (users != null) 'users': users.map((u) => u.toJson()).toList(),
};
final response = await _dio.post('/sync', data: payload);
return response.data;
} catch (e) {
rethrow;
}
}
}

View File

@@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
import 'package:geosector_app/core/repositories/user_repository.dart';
import 'package:geosector_app/presentation/widgets/loading_overlay.dart';
/// Service qui gère les opérations d'authentification avec affichage d'un overlay de chargement
class AuthService {
final UserRepository _userRepository;
AuthService(this._userRepository);
/// Méthode de connexion avec affichage d'un overlay de chargement
Future<bool> login(BuildContext context, String username, String password,
{String type = 'admin'}) async {
return await LoadingOverlay.show(
context: context,
spinnerSize: 80.0, // Spinner plus grand
strokeWidth: 6.0, // Trait plus épais
future: _userRepository.login(username, password, type: type),
);
}
/// Méthode de déconnexion avec affichage d'un overlay de chargement
Future<bool> logout(BuildContext context) async {
return await LoadingOverlay.show(
context: context,
spinnerSize: 80.0, // Spinner plus grand
strokeWidth: 6.0, // Trait plus épais
future: _userRepository.logout(),
);
}
/// Vérifie si un utilisateur est connecté
bool isLoggedIn() {
return _userRepository.isLoggedIn;
}
/// Vérifie si l'utilisateur connecté est un administrateur
bool isAdmin() {
return _userRepository.isAdmin();
}
}

View File

@@ -0,0 +1,157 @@
import 'dart:async';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/material.dart';
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<List<ConnectivityResult>> _connectivitySubscription;
List<ConnectivityResult> _connectionStatus = [ConnectivityResult.none];
bool _isInitialized = false;
/// Indique si l'appareil est connecté à Internet
bool get isConnected {
// Vérifie si la liste contient au moins un type de connexion autre que 'none'
return _connectionStatus.any((result) => result != ConnectivityResult.none);
}
/// Indique si l'appareil est connecté via WiFi
bool get isWifi => _connectionStatus.contains(ConnectivityResult.wifi);
/// Indique si l'appareil est connecté via données mobiles (4G, 5G, etc.)
bool get isMobile => _connectionStatus.contains(ConnectivityResult.mobile);
/// Retourne le type de connexion actuel (WiFi, données mobiles, etc.)
List<ConnectivityResult> get connectionStatus => _connectionStatus;
/// Retourne le premier type de connexion actif (pour compatibilité avec l'ancien code)
ConnectivityResult get primaryConnectionStatus {
// Retourne le premier type de connexion qui n'est pas 'none', ou 'none' si tous sont 'none'
return _connectionStatus.firstWhere(
(result) => result != ConnectivityResult.none,
orElse: () => ConnectivityResult.none
);
}
/// Obtient une description textuelle du type de connexion
String get connectionType {
// Si aucune connexion n'est disponible
if (!isConnected) {
return 'Aucune connexion';
}
// Utiliser le premier type de connexion actif
ConnectivityResult primaryStatus = primaryConnectionStatus;
switch (primaryStatus) {
case ConnectivityResult.wifi:
return 'WiFi';
case ConnectivityResult.mobile:
return 'Données mobiles';
case ConnectivityResult.ethernet:
return 'Ethernet';
case ConnectivityResult.bluetooth:
return 'Bluetooth';
case ConnectivityResult.vpn:
return 'VPN';
case ConnectivityResult.none:
return 'Aucune connexion';
default:
return 'Inconnu';
}
}
/// Constructeur du service de connectivité
ConnectivityService() {
_initConnectivity();
}
/// Initialise le service et commence à écouter les changements de connectivité
Future<void> _initConnectivity() async {
if (_isInitialized) return;
try {
// En version web, on considère par défaut que la connexion est disponible
// car la vérification de connectivité est moins fiable sur le web
if (kIsWeb) {
_connectionStatus = [ConnectivityResult.wifi]; // Valeur par défaut pour le web
} else {
_connectionStatus = await _connectivity.checkConnectivity();
}
// S'abonner aux changements de connectivité
_connectivitySubscription = _connectivity.onConnectivityChanged.listen(_updateConnectionStatus);
_isInitialized = true;
} catch (e) {
debugPrint('Erreur lors de l\'initialisation du service de connectivité: $e');
// En cas d'erreur en version web, on suppose que la connexion est disponible
// car l'application web ne peut pas fonctionner sans connexion de toute façon
if (kIsWeb) {
_connectionStatus = [ConnectivityResult.wifi];
_isInitialized = true;
}
}
notifyListeners();
}
/// Met à jour l'état de la connexion lorsqu'il change
void _updateConnectionStatus(List<ConnectivityResult> results) {
// Vérifier si la liste des résultats a changé
bool hasChanged = false;
// Si les listes ont des longueurs différentes, elles sont différentes
if (_connectionStatus.length != results.length) {
hasChanged = true;
} else {
// Vérifier si les éléments sont différents
for (int i = 0; i < _connectionStatus.length; i++) {
if (i >= results.length || _connectionStatus[i] != results[i]) {
hasChanged = true;
break;
}
}
}
if (hasChanged) {
_connectionStatus = results;
notifyListeners();
}
}
/// Vérifie manuellement l'état actuel de la connexion
Future<List<ConnectivityResult>> checkConnectivity() async {
try {
// En version web, on considère par défaut que la connexion est disponible
if (kIsWeb) {
// En version web, on peut tenter de faire une requête réseau légère pour vérifier la connectivité
// mais pour l'instant, on suppose que la connexion est disponible
final results = [ConnectivityResult.wifi];
_updateConnectionStatus(results);
return results;
} else {
// Version mobile - utiliser l'API standard
final results = await _connectivity.checkConnectivity();
_updateConnectionStatus(results);
return results;
}
} catch (e) {
debugPrint('Erreur lors de la vérification de la connectivité: $e');
// En cas d'erreur, on conserve l'état actuel
return _connectionStatus;
}
}
@override
void dispose() {
try {
_connectivitySubscription.cancel();
} catch (e) {
debugPrint('Erreur lors de l\'annulation de l\'abonnement de connectivité: $e');
}
super.dispose();
}
}

View File

@@ -0,0 +1,182 @@
import 'dart:async';
import 'dart:js' as js;
import 'package:flutter/foundation.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
/// Service pour gérer les problèmes spécifiques à Hive en version web
class HiveWebFix {
/// Nettoie en toute sécurité les boîtes Hive en version web
/// Cette méthode est plus sûre que de supprimer directement IndexedDB
static Future<void> safeCleanHiveBoxes({List<String>? excludeBoxes}) async {
if (!kIsWeb) return;
try {
debugPrint(
'HiveWebFix: Nettoyage sécurisé des boîtes Hive en version web');
// Liste des boîtes à nettoyer
final boxesToClean = [
AppKeys.operationsBoxName,
AppKeys.sectorsBoxName,
AppKeys.passagesBoxName,
];
// Exclure certaines boîtes si spécifié
if (excludeBoxes != null) {
boxesToClean.removeWhere((box) => excludeBoxes.contains(box));
}
// Nettoyer chaque boîte individuellement au lieu de supprimer IndexedDB
for (final boxName in boxesToClean) {
try {
if (Hive.isBoxOpen(boxName)) {
debugPrint('HiveWebFix: Nettoyage de la boîte $boxName');
final box = Hive.box(boxName);
await box.clear();
debugPrint('HiveWebFix: Boîte $boxName nettoyée avec succès');
} else {
debugPrint(
'HiveWebFix: La boîte $boxName n\'est pas ouverte, ouverture temporaire');
final box = await Hive.openBox(boxName);
await box.clear();
await box.close();
debugPrint('HiveWebFix: Boîte $boxName nettoyée et fermée');
}
} catch (e) {
debugPrint(
'HiveWebFix: Erreur lors du nettoyage de la boîte $boxName: $e');
}
}
debugPrint('HiveWebFix: Nettoyage sécurisé terminé');
} catch (e) {
debugPrint('HiveWebFix: Erreur lors du nettoyage sécurisé: $e');
}
}
/// Vérifie l'intégrité des boîtes Hive et tente de les réparer si nécessaire
static Future<bool> checkAndRepairHiveBoxes() async {
if (!kIsWeb) return true;
try {
debugPrint('HiveWebFix: Vérification de l\'intégrité des boîtes Hive');
// Vérifier si IndexedDB est accessible
final isIndexedDBAvailable = js.context.hasProperty('indexedDB');
if (!isIndexedDBAvailable) {
debugPrint(
'HiveWebFix: IndexedDB n\'est pas disponible dans ce navigateur');
return false;
}
// Liste des boîtes essentielles
final essentialBoxes = [
AppKeys.usersBoxName,
AppKeys.settingsBoxName,
];
// Vérifier chaque boîte essentielle
for (final boxName in essentialBoxes) {
try {
if (!Hive.isBoxOpen(boxName)) {
debugPrint(
'HiveWebFix: Ouverture de la boîte essentielle $boxName');
await Hive.openBox(boxName);
}
// Vérifier si la boîte est accessible
final box = Hive.box(boxName);
// Tenter une opération simple pour vérifier l'intégrité
final length = box.length;
debugPrint(
'HiveWebFix: Boîte $boxName accessible avec $length éléments');
} catch (e) {
debugPrint('HiveWebFix: Erreur d\'accès à la boîte $boxName: $e');
// Tenter de réparer en réinitialisant Hive
try {
debugPrint(
'HiveWebFix: Tentative de réparation de la boîte $boxName');
// Fermer la boîte si elle est ouverte
if (Hive.isBoxOpen(boxName)) {
await Hive.box(boxName).close();
}
// Réouvrir la boîte
await Hive.openBox(boxName);
debugPrint('HiveWebFix: Boîte $boxName réparée avec succès');
} catch (repairError) {
debugPrint(
'HiveWebFix: Échec de la réparation de la boîte $boxName: $repairError');
return false;
}
}
}
debugPrint('HiveWebFix: Toutes les boîtes essentielles sont intègres');
return true;
} catch (e) {
debugPrint('HiveWebFix: Erreur lors de la vérification d\'intégrité: $e');
return false;
}
}
/// Réinitialise complètement Hive en cas de problème grave
/// À utiliser en dernier recours car cela supprimera toutes les données
static Future<void> resetHiveCompletely() async {
if (!kIsWeb) return;
try {
debugPrint('HiveWebFix: Réinitialisation complète de Hive');
// Fermer toutes les boîtes ouvertes
final boxesToClose = [
AppKeys.usersBoxName,
AppKeys.operationsBoxName,
AppKeys.sectorsBoxName,
AppKeys.passagesBoxName,
AppKeys.settingsBoxName,
];
for (final boxName in boxesToClose) {
if (Hive.isBoxOpen(boxName)) {
debugPrint('HiveWebFix: Fermeture de la boîte $boxName');
await Hive.box(boxName).close();
}
}
// Supprimer IndexedDB avec une approche plus sûre
js.context.callMethod('eval', [
'''
(function() {
return new Promise(function(resolve, reject) {
var request = indexedDB.deleteDatabase("geosector_app");
request.onsuccess = function() {
console.log("IndexedDB nettoyé avec succès");
resolve(true);
};
request.onerror = function(event) {
console.log("Erreur lors du nettoyage d'IndexedDB", event);
reject(event);
};
});
})();
'''
]);
// Attendre un peu pour s'assurer que la suppression est terminée
await Future.delayed(const Duration(milliseconds: 500));
// Réinitialiser Hive
await Hive.initFlutter();
// Réenregistrer les adaptateurs
// Note: Cette partie devrait être gérée par le code principal de l'application
debugPrint('HiveWebFix: Réinitialisation complète terminée');
} catch (e) {
debugPrint('HiveWebFix: Erreur lors de la réinitialisation complète: $e');
}
}
}

View File

@@ -0,0 +1,164 @@
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:geolocator/geolocator.dart';
import 'package:latlong2/latlong.dart';
/// Service de géolocalisation pour gérer les permissions et l'accès à la position
class LocationService {
/// Vérifie si les services de localisation sont activés
static Future<bool> isLocationServiceEnabled() async {
// En version web, on considère que les services de localisation sont toujours activés
// car la vérification est gérée différemment par le navigateur
if (kIsWeb) {
return true;
}
return await Geolocator.isLocationServiceEnabled();
}
/// Vérifie et demande les permissions de localisation
/// Retourne true si l'autorisation est accordée, false sinon
static Future<bool> checkAndRequestPermission() async {
// En version web, on considère que les permissions sont toujours accordées
// car la gestion des permissions est différente et gérée par le navigateur
if (kIsWeb) {
return true;
}
try {
// Vérifier si les services de localisation sont activés
bool serviceEnabled = await isLocationServiceEnabled();
if (!serviceEnabled) {
// Les services de localisation ne sont pas activés, on ne peut pas demander la permission
return false;
}
// Vérifier le statut actuel de la permission
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
// Demander la permission
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
// La permission a été refusée
return false;
}
}
if (permission == LocationPermission.deniedForever) {
// La permission a été refusée définitivement
return false;
}
// La permission est accordée (whileInUse ou always)
return true;
} catch (e) {
debugPrint('Erreur lors de la vérification des permissions de localisation: $e');
// En cas d'erreur, on retourne false pour être sûr
return false;
}
}
/// Obtient la position actuelle de l'utilisateur
/// Retourne null si la position ne peut pas être obtenue
static Future<LatLng?> getCurrentPosition() async {
try {
// En version web, la géolocalisation fonctionne différemment
// et peut être bloquée par le navigateur si le site n'est pas en HTTPS
if (kIsWeb) {
try {
Position position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
);
return LatLng(position.latitude, position.longitude);
} catch (e) {
debugPrint('Erreur lors de l\'obtention de la position en version web: $e');
// En version web, en cas d'erreur, on peut retourner une position par défaut
// ou null selon les besoins de l'application
return null;
}
}
// Version mobile
// Vérifier si l'autorisation est accordée
bool hasPermission = await checkAndRequestPermission();
if (!hasPermission) {
return null;
}
// Obtenir la position actuelle
Position position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
);
return LatLng(position.latitude, position.longitude);
} catch (e) {
debugPrint('Erreur lors de l\'obtention de la position: $e');
return null;
}
}
/// Vérifie si l'application peut accéder à la position de l'utilisateur
/// Retourne un message d'erreur si l'accès n'est pas possible, null sinon
static Future<String?> getLocationErrorMessage() async {
// En version web, on considère qu'il n'y a pas d'erreur de localisation
// car la gestion des permissions est gérée par le navigateur
if (kIsWeb) {
return null;
}
try {
// Vérifier si les services de localisation sont activés
bool serviceEnabled = await isLocationServiceEnabled();
if (!serviceEnabled) {
return 'Les services de localisation sont désactivés. Veuillez les activer dans les paramètres de votre appareil.';
}
// Vérifier le statut actuel de la permission
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
return 'L\'accès à la localisation a été refusé. Cette application ne peut pas fonctionner sans cette autorisation.';
}
if (permission == LocationPermission.deniedForever) {
return 'L\'accès à la localisation a été définitivement refusé. Veuillez l\'autoriser dans les paramètres de votre appareil.';
}
return null; // Pas d'erreur
} catch (e) {
debugPrint('Erreur lors de la vérification des erreurs de localisation: $e');
// En cas d'erreur, on retourne null pour ne pas bloquer l'application
return null;
}
}
/// Ouvre les paramètres de l'application pour permettre à l'utilisateur de modifier les autorisations
static Future<void> openAppSettings() async {
// En version web, cette fonctionnalité n'est pas disponible
if (kIsWeb) {
debugPrint('Ouverture des paramètres de l\'application non disponible en version web');
return;
}
try {
await Geolocator.openAppSettings();
} catch (e) {
debugPrint('Erreur lors de l\'ouverture des paramètres de l\'application: $e');
}
}
/// Ouvre les paramètres de localisation de l'appareil
static Future<void> openLocationSettings() async {
// En version web, cette fonctionnalité n'est pas disponible
if (kIsWeb) {
debugPrint('Ouverture des paramètres de localisation non disponible en version web');
return;
}
try {
await Geolocator.openLocationSettings();
} catch (e) {
debugPrint('Erreur lors de l\'ouverture des paramètres de localisation: $e');
}
}
}

View File

@@ -0,0 +1,194 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
import 'package:geosector_app/core/repositories/passage_repository.dart';
import 'package:geosector_app/core/repositories/user_repository.dart';
/// Service pour charger et filtrer les données de passages
class PassageDataService {
final PassageRepository passageRepository;
final UserRepository userRepository;
PassageDataService({
required this.passageRepository,
required this.userRepository,
});
/// Charge les données de passage depuis Hive
///
/// [daysToShow] : Nombre de jours à afficher
/// [excludePassageTypes] : Types de passages à exclure
/// [userId] : ID de l'utilisateur pour filtrer les passages (null = utilisateur actuel)
/// [showAllPassages] : Si vrai, n'applique aucun filtrage par utilisateur
List<Map<String, dynamic>> loadPassageData({
required int daysToShow,
List<int> excludePassageTypes = const [2],
int? userId,
bool showAllPassages = false,
}) {
// Récupérer tous les passages
final passages = passageRepository.getAllPassages();
// Filtrer les passages pour exclure ceux avec fkType dans la liste d'exclusion
final filteredPassages = passages
.where((p) => !excludePassageTypes.contains(p.fkType))
.toList();
if (filteredPassages.isEmpty) {
return [];
}
// Déterminer si on filtre par utilisateur ou si on prend tous les passages
final passagesToUse = showAllPassages
? filteredPassages
: _filterPassagesByUser(filteredPassages, userId);
if (passagesToUse.isEmpty) {
debugPrint('Aucun passage trouvé après filtrage');
return [];
}
// Trouver la date du passage le plus récent
passagesToUse.sort((a, b) => b.passedAt.compareTo(a.passedAt));
final DateTime referenceDate = passagesToUse.first.passedAt;
debugPrint(
'Date de référence pour le graphique: ${DateFormat('dd/MM/yyyy').format(referenceDate)}');
// Définir la date de début (N jours avant la date de référence)
final startDate = referenceDate.subtract(Duration(days: daysToShow - 1));
debugPrint(
'Date de début pour le graphique: ${DateFormat('dd/MM/yyyy').format(startDate)}');
debugPrint(
'Plage de dates du graphique: ${DateFormat('dd/MM/yyyy').format(startDate)} - ${DateFormat('dd/MM/yyyy').format(referenceDate)}');
// Regrouper les passages par date et type
final Map<String, Map<int, int>> passagesByDateAndType = {};
// Initialiser le dictionnaire avec les N derniers jours
for (int i = daysToShow - 1; i >= 0; i--) {
final date = referenceDate.subtract(Duration(days: i));
final dateStr =
'${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
passagesByDateAndType[dateStr] = {};
}
// Ajouter tous les types de passage possibles pour chaque date
for (final dateStr in passagesByDateAndType.keys) {
for (final typeId in AppKeys.typesPassages.keys) {
// Exclure les types dans la liste d'exclusion
if (!excludePassageTypes.contains(typeId)) {
passagesByDateAndType[dateStr]![typeId] = 0;
}
}
}
// Parcourir les passages et les regrouper par date et type
for (final passage in passagesToUse) {
if (passage.passedAt
.isAfter(startDate.subtract(const Duration(days: 1))) &&
passage.passedAt
.isBefore(referenceDate.add(const Duration(days: 1)))) {
final dateStr =
'${passage.passedAt.year}-${passage.passedAt.month.toString().padLeft(2, '0')}-${passage.passedAt.day.toString().padLeft(2, '0')}';
final typeId = passage.fkType;
// Vérifier que le type n'est pas exclu
if (!excludePassageTypes.contains(typeId)) {
// Si la date existe dans notre dictionnaire, mettre à jour le compteur
if (passagesByDateAndType.containsKey(dateStr)) {
if (!passagesByDateAndType[dateStr]!.containsKey(typeId)) {
passagesByDateAndType[dateStr]![typeId] = 0;
}
passagesByDateAndType[dateStr]![typeId] =
(passagesByDateAndType[dateStr]![typeId] ?? 0) + 1;
}
}
}
}
// Convertir les données au format attendu par le graphique
final List<Map<String, dynamic>> result = [];
passagesByDateAndType.forEach((dateStr, typesCounts) {
typesCounts.forEach((typeId, count) {
result.add({
'date': dateStr,
'type_passage': typeId,
'nb': count,
});
});
});
return result;
}
/// Filtre les passages par utilisateur
List<dynamic> _filterPassagesByUser(List<dynamic> passages, int? userId) {
// Récupérer l'ID de l'utilisateur actuel si nécessaire
final int? currentUserId = userId ?? userRepository.getCurrentUser()?.id;
// Filtrer les passages pour l'utilisateur actuel
final userPassages = passages
.where((p) => currentUserId == null || p.fkUser == currentUserId)
.toList();
if (userPassages.isEmpty) {
debugPrint('Aucun passage trouvé pour l\'utilisateur $currentUserId');
}
return userPassages;
}
/// Charge et prépare les données pour le graphique en camembert
///
/// [excludePassageTypes] : Types de passages à exclure
/// [userId] : ID de l'utilisateur pour filtrer les passages (null = utilisateur actuel)
/// [showAllPassages] : Si vrai, n'applique aucun filtrage par utilisateur
Map<int, int> loadPassageDataForPieChart({
List<int> excludePassageTypes = const [2],
int? userId,
bool showAllPassages = false,
}) {
// Récupérer tous les passages
final passages = passageRepository.getAllPassages();
// Filtrer les passages pour exclure ceux avec fkType dans la liste d'exclusion
final filteredPassages = passages
.where((p) => !excludePassageTypes.contains(p.fkType))
.toList();
if (filteredPassages.isEmpty) {
return {};
}
// Déterminer si on filtre par utilisateur ou si on prend tous les passages
final passagesToUse = showAllPassages
? filteredPassages
: _filterPassagesByUser(filteredPassages, userId);
if (passagesToUse.isEmpty) {
debugPrint('Aucun passage trouvé après filtrage');
return {};
}
// Compter les passages par type
final Map<int, int> passagesByType = {};
// Initialiser les compteurs pour tous les types de passage
for (final typeId in AppKeys.typesPassages.keys) {
// Exclure les types dans la liste d'exclusion
if (!excludePassageTypes.contains(typeId)) {
passagesByType[typeId] = 0;
}
}
// Compter les passages par type
for (final passage in passagesToUse) {
final typeId = passage.fkType;
if (!excludePassageTypes.contains(typeId)) {
passagesByType[typeId] = (passagesByType[typeId] ?? 0) + 1;
}
}
return passagesByType;
}
}

View File

@@ -0,0 +1,96 @@
import 'dart:async';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:geosector_app/core/repositories/user_repository.dart';
class SyncService {
final UserRepository _userRepository;
StreamSubscription? _connectivitySubscription;
Timer? _periodicSyncTimer;
bool _isSyncing = false;
final Duration _syncInterval = const Duration(minutes: 15);
SyncService({
required UserRepository userRepository,
}) : _userRepository = userRepository {
_initConnectivityListener();
_initPeriodicSync();
}
// Initialiser l'écouteur de connectivité
void _initConnectivityListener() {
_connectivitySubscription = Connectivity()
.onConnectivityChanged
.listen((List<ConnectivityResult> results) {
// Vérifier si au moins un type de connexion est disponible
if (results.any((result) => result != ConnectivityResult.none)) {
// Lorsque la connexion est rétablie, déclencher une synchronisation
syncAll();
}
});
}
// Initialiser la synchronisation périodique
void _initPeriodicSync() {
_periodicSyncTimer = Timer.periodic(_syncInterval, (timer) {
syncAll();
});
}
// Synchroniser toutes les données
Future<void> syncAll() async {
if (_isSyncing) return;
_isSyncing = true;
try {
// Synchroniser les utilisateurs
await _userRepository.syncAllUsers();
} catch (e) {
// Gérer les erreurs de synchronisation
print('Erreur lors de la synchronisation: $e');
} finally {
_isSyncing = false;
}
}
// Synchroniser uniquement les données d'un utilisateur spécifique
Future<void> syncUserData(int userId) async {
try {
// Cette méthode pourrait être étendue à l'avenir pour synchroniser d'autres données utilisateur
await _userRepository.refreshFromServer();
} catch (e) {
print('Erreur lors de la synchronisation des données utilisateur: $e');
}
}
// Forcer le rafraîchissement depuis le serveur
Future<void> forceRefresh() async {
if (_isSyncing) return;
_isSyncing = true;
try {
// Rafraîchir depuis le serveur
await _userRepository.refreshFromServer();
} catch (e) {
print('Erreur lors du rafraîchissement forcé: $e');
} finally {
_isSyncing = false;
}
}
// Obtenir l'état de synchronisation
Map<String, dynamic> getSyncStatus() {
return {
'isSyncing': _isSyncing,
};
}
// Nettoyer les ressources
void dispose() {
_connectivitySubscription?.cancel();
_periodicSyncTimer?.cancel();
}
}

View File

@@ -0,0 +1,122 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class AppTheme {
// Nouvelles couleurs du thème
static const Color primaryColor = Color(0xFF2E4057); // Bleu foncé/gris
static const Color secondaryColor = Color(0xFF048BA8); // Bleu turquoise
static const Color accentColor = Color(0xFFF18F01); // Orange
static const Color backgroundLightColor = Color(0xFFF9FAFB);
static const Color backgroundDarkColor = Color(0xFF111827);
static const Color textLightColor = Color(0xFF1F2937);
static const Color textDarkColor = Color(0xFFF9FAFB);
// Thème clair
static ThemeData get lightTheme {
return ThemeData(
useMaterial3: true,
brightness: Brightness.light,
colorScheme: ColorScheme.light(
primary: primaryColor,
secondary: secondaryColor,
tertiary: accentColor,
background: backgroundLightColor,
surface: Colors.white,
onPrimary: Colors.white,
onSecondary: Colors.white,
onBackground: textLightColor,
onSurface: textLightColor,
),
textTheme: GoogleFonts.poppinsTextTheme(ThemeData.light().textTheme),
appBarTheme: const AppBarTheme(
backgroundColor: primaryColor,
foregroundColor: Colors.white,
elevation: 0,
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: primaryColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: Colors.grey[100],
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: primaryColor, width: 2),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
),
cardTheme: CardTheme(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
);
}
// Thème sombre
static ThemeData get darkTheme {
return ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
colorScheme: ColorScheme.dark(
primary: primaryColor,
secondary: secondaryColor,
tertiary: accentColor,
background: backgroundDarkColor,
surface: const Color(0xFF1F2937),
onPrimary: Colors.white,
onSecondary: Colors.white,
onBackground: textDarkColor,
onSurface: textDarkColor,
),
textTheme: GoogleFonts.poppinsTextTheme(ThemeData.dark().textTheme),
appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFF1F2937),
foregroundColor: Colors.white,
elevation: 0,
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: primaryColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: const Color(0xFF374151),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: primaryColor, width: 2),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
),
cardTheme: CardTheme(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
color: const Color(0xFF1F2937),
),
);
}
}