1135 lines
34 KiB
Dart
1135 lines
34 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:wtpe/pages/role_navigator.dart';
|
|
import 'package:wtpe/pages/session_manager.dart';
|
|
import 'pages/splash_screen.dart';
|
|
import 'dart:convert';
|
|
import 'package:http/http.dart' as http;
|
|
import '../widgets/responsive_helper.dart';
|
|
import 'services/connectivity_service.dart';
|
|
|
|
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
|
|
|
// =================== MODÈLE SERVICE (INCHANGÉ) ===================
|
|
class WortisService {
|
|
final int? rang;
|
|
final String id;
|
|
final String name;
|
|
final String description;
|
|
final String secteurActivite;
|
|
final String typeService;
|
|
final String icon;
|
|
final String? banner;
|
|
final bool status;
|
|
final String? linkView;
|
|
|
|
WortisService({
|
|
required this.id,
|
|
this.rang,
|
|
required this.name,
|
|
required this.description,
|
|
required this.secteurActivite,
|
|
required this.typeService,
|
|
required this.icon,
|
|
this.banner,
|
|
required this.status,
|
|
this.linkView,
|
|
});
|
|
|
|
factory WortisService.fromJson(Map<String, dynamic> json) {
|
|
return WortisService(
|
|
id: json['_id'] ?? '',
|
|
name: json['name'] ?? '',
|
|
description: json['description'] ?? '',
|
|
secteurActivite: json['SecteurActivite'] ?? '',
|
|
typeService: json['Type_Service'] ?? '',
|
|
icon: json['icon'] ?? 'apps',
|
|
banner: json['banner'],
|
|
status: json['status'] ?? false,
|
|
linkView: json['link_view'],
|
|
);
|
|
}
|
|
|
|
IconData get flutterIcon {
|
|
switch (icon.toLowerCase()) {
|
|
case 'movie':
|
|
return Icons.movie;
|
|
case 'phone':
|
|
return Icons.phone;
|
|
case 'wifi':
|
|
return Icons.wifi;
|
|
case 'flash_on':
|
|
return Icons.flash_on;
|
|
case 'water_drop':
|
|
return Icons.water_drop;
|
|
case 'account_balance_wallet':
|
|
return Icons.account_balance_wallet;
|
|
case 'tv':
|
|
return Icons.tv;
|
|
case 'security':
|
|
return Icons.security;
|
|
case 'send':
|
|
return Icons.send;
|
|
default:
|
|
return Icons.apps;
|
|
}
|
|
}
|
|
|
|
Color get sectorColor {
|
|
switch (secteurActivite.toLowerCase()) {
|
|
case 'billetterie':
|
|
return Color(0xFF9C27B0);
|
|
case 'mobile money':
|
|
return Color(0xFFFF9800);
|
|
case 'télécommunications':
|
|
case 'telecommunication':
|
|
return Color(0xFF2196F3);
|
|
case 'électricité & eau':
|
|
case 'electricite':
|
|
return Color(0xFF4CAF50);
|
|
case 'services financiers':
|
|
return Color(0xFFFF6600);
|
|
case 'transport':
|
|
return Color(0xFF607D8B);
|
|
default:
|
|
return Color(0xFF006699);
|
|
}
|
|
}
|
|
|
|
get logo => null;
|
|
}
|
|
|
|
// =================== MODÈLE UTILISATEUR ÉTENDU ===================
|
|
class User {
|
|
final String id;
|
|
final String nom;
|
|
final String agentId;
|
|
final String role;
|
|
final DateTime dateCreation;
|
|
final String type; // 'agent' ou 'enterprise_member'
|
|
final Enterprise? enterprise; // Nullable pour les agents individuels
|
|
|
|
User({
|
|
required this.id,
|
|
required this.nom,
|
|
required this.agentId,
|
|
required this.role,
|
|
required this.dateCreation,
|
|
this.type = 'agent',
|
|
this.enterprise,
|
|
});
|
|
|
|
factory User.fromJson(Map<String, dynamic> json) {
|
|
return User(
|
|
id: json['id'] ?? '',
|
|
nom: json['nom'] ?? '',
|
|
agentId: json['agent_id'] ?? '',
|
|
role: json['role'] ?? 'agent',
|
|
dateCreation:
|
|
json['date_creation'] != null
|
|
? DateTime.parse(json['date_creation'])
|
|
: DateTime.now(),
|
|
type: json['type'] ?? 'agent',
|
|
enterprise:
|
|
json['enterprise'] != null
|
|
? Enterprise.fromJson(json['enterprise'])
|
|
: null,
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
'id': id,
|
|
'nom': nom,
|
|
'agent_id': agentId,
|
|
'role': role,
|
|
'date_creation': dateCreation.toIso8601String(),
|
|
'type': type,
|
|
'enterprise': enterprise?.toJson(),
|
|
};
|
|
}
|
|
|
|
bool get isEnterpriseMember => type == 'enterprise_member';
|
|
bool get isIndividualAgent => type == 'agent';
|
|
}
|
|
|
|
// ===== Dans main.dart - Mettre à jour la classe Enterprise =====
|
|
|
|
// ===== CORRIGER LA CLASSE Enterprise =====
|
|
|
|
class Enterprise {
|
|
final String id;
|
|
final String nomEntreprise;
|
|
final String? numeroRegistreCommerce;
|
|
final String? adresse;
|
|
final String? telephone;
|
|
final String? email;
|
|
final double soldeEntreprise;
|
|
final List<String> membresIds;
|
|
final DateTime dateCreation;
|
|
final int nombreMembres;
|
|
final String? domaineActivite;
|
|
|
|
// AJOUTER ces propriétés manquantes
|
|
final String enterpriseId; // Pour compatibilité
|
|
final String status;
|
|
|
|
Enterprise({
|
|
required this.id,
|
|
required this.nomEntreprise,
|
|
this.numeroRegistreCommerce,
|
|
this.adresse,
|
|
this.telephone,
|
|
this.email,
|
|
required this.soldeEntreprise,
|
|
required this.membresIds,
|
|
required this.dateCreation,
|
|
required this.nombreMembres,
|
|
this.domaineActivite,
|
|
String? enterpriseId, // NOUVEAU
|
|
String? status, // NOUVEAU
|
|
}) : enterpriseId = enterpriseId ?? id,
|
|
status = status ?? 'active';
|
|
|
|
factory Enterprise.fromJson(Map<String, dynamic> json) {
|
|
return Enterprise(
|
|
id: json['_id'] ?? json['id'] ?? '',
|
|
nomEntreprise: json['nom_entreprise'] ?? json['nomEntreprise'] ?? '',
|
|
numeroRegistreCommerce:
|
|
json['numero_registre_commerce'] ?? json['numeroRegistreCommerce'],
|
|
adresse: json['adresse'],
|
|
telephone: json['telephone'],
|
|
email: json['email'],
|
|
soldeEntreprise:
|
|
(json['solde_entreprise'] ?? json['soldeEntreprise'] ?? 0.0)
|
|
.toDouble(),
|
|
membresIds: List<String>.from(
|
|
json['membres_ids'] ?? json['membresIds'] ?? [],
|
|
),
|
|
dateCreation:
|
|
json['date_creation'] != null
|
|
? DateTime.parse(json['date_creation'])
|
|
: json['dateCreation'] != null
|
|
? DateTime.parse(json['dateCreation'])
|
|
: DateTime.now(),
|
|
nombreMembres:
|
|
json['nombre_membres'] ??
|
|
json['nombreMembres'] ??
|
|
(json['membres_ids'] as List?)?.length ??
|
|
(json['membresIds'] as List?)?.length ??
|
|
1,
|
|
domaineActivite:
|
|
json['domaine_activite'] ??
|
|
json['domaineActivite'] ??
|
|
json['secteur_activite'] ??
|
|
json['secteurActivite'],
|
|
enterpriseId: json['enterprise_id'] ?? json['enterpriseId'], // NOUVEAU
|
|
status: json['status'] ?? 'active', // NOUVEAU
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
'id': id,
|
|
'enterprise_id': enterpriseId,
|
|
'nom_entreprise': nomEntreprise,
|
|
'numero_registre_commerce': numeroRegistreCommerce,
|
|
'adresse': adresse,
|
|
'telephone': telephone,
|
|
'email': email,
|
|
'solde_entreprise': soldeEntreprise,
|
|
'membres_ids': membresIds,
|
|
'date_creation': dateCreation.toIso8601String(),
|
|
'nombre_membres': nombreMembres,
|
|
'domaine_activite': domaineActivite,
|
|
'status': status,
|
|
};
|
|
}
|
|
|
|
Enterprise copyWith({
|
|
String? id,
|
|
String? nomEntreprise,
|
|
String? numeroRegistreCommerce,
|
|
String? adresse,
|
|
String? telephone,
|
|
String? email,
|
|
double? soldeEntreprise,
|
|
List<String>? membresIds,
|
|
DateTime? dateCreation,
|
|
int? nombreMembres,
|
|
String? domaineActivite,
|
|
String? enterpriseId,
|
|
String? status,
|
|
}) {
|
|
return Enterprise(
|
|
id: id ?? this.id,
|
|
nomEntreprise: nomEntreprise ?? this.nomEntreprise,
|
|
numeroRegistreCommerce:
|
|
numeroRegistreCommerce ?? this.numeroRegistreCommerce,
|
|
adresse: adresse ?? this.adresse,
|
|
telephone: telephone ?? this.telephone,
|
|
email: email ?? this.email,
|
|
soldeEntreprise: soldeEntreprise ?? this.soldeEntreprise,
|
|
membresIds: membresIds ?? this.membresIds,
|
|
dateCreation: dateCreation ?? this.dateCreation,
|
|
nombreMembres: nombreMembres ?? this.nombreMembres,
|
|
domaineActivite: domaineActivite ?? this.domaineActivite,
|
|
enterpriseId: enterpriseId ?? this.enterpriseId,
|
|
status: status ?? this.status,
|
|
);
|
|
}
|
|
}
|
|
|
|
// =================== SERVICE API (INCHANGÉ) ===================
|
|
class WortisApiService {
|
|
static const String baseUrl = 'https://api.live.wortis.cg/tpe';
|
|
|
|
static final WortisApiService _instance = WortisApiService._internal();
|
|
factory WortisApiService() => _instance;
|
|
WortisApiService._internal();
|
|
|
|
Future<List<WortisService>> getServices() async {
|
|
try {
|
|
final response = await http
|
|
.get(
|
|
Uri.parse('$baseUrl/get_services_tpe'),
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
},
|
|
)
|
|
.timeout(Duration(seconds: 30));
|
|
|
|
if (response.statusCode == 200) {
|
|
final Map<String, dynamic> jsonData = json.decode(response.body);
|
|
final List<dynamic> servicesJson = jsonData['all_services'] ?? [];
|
|
|
|
return servicesJson
|
|
.map((serviceJson) => WortisService.fromJson(serviceJson))
|
|
.toList();
|
|
} else {
|
|
throw Exception('Erreur API: ${response.statusCode}');
|
|
}
|
|
} catch (e) {
|
|
print('Erreur lors de la récupération des services: $e');
|
|
throw Exception('Impossible de récupérer les services: $e');
|
|
}
|
|
}
|
|
|
|
Map<String, List<WortisService>> groupServicesBySector(
|
|
List<WortisService> services,
|
|
) {
|
|
Map<String, List<WortisService>> grouped = {};
|
|
|
|
for (var service in services) {
|
|
String sector = service.secteurActivite;
|
|
if (grouped[sector] == null) {
|
|
grouped[sector] = [];
|
|
}
|
|
grouped[sector]!.add(service);
|
|
}
|
|
|
|
return grouped;
|
|
}
|
|
}
|
|
|
|
// =================== CONTROLLER SERVICES (INCHANGÉ) ===================
|
|
class ServicesController extends ChangeNotifier {
|
|
final WortisApiService _apiService = WortisApiService();
|
|
|
|
List<WortisService> _allServices = [];
|
|
Map<String, List<WortisService>> _servicesBySector = {};
|
|
bool _isLoading = false;
|
|
String? _error;
|
|
String _searchQuery = '';
|
|
String _selectedSector = 'Tous';
|
|
|
|
List<WortisService> get allServices => _allServices;
|
|
Map<String, List<WortisService>> get servicesBySector => _servicesBySector;
|
|
bool get isLoading => _isLoading;
|
|
String? get error => _error;
|
|
String get searchQuery => _searchQuery;
|
|
String get selectedSector => _selectedSector;
|
|
|
|
List<String> get sectors => ['Tous', ..._servicesBySector.keys];
|
|
|
|
List<WortisService> get filteredServices {
|
|
List<WortisService> services;
|
|
|
|
if (_selectedSector == 'Tous') {
|
|
services = _allServices;
|
|
} else {
|
|
services = _servicesBySector[_selectedSector] ?? [];
|
|
}
|
|
|
|
if (_searchQuery.isEmpty) {
|
|
return services;
|
|
}
|
|
|
|
return services.where((service) {
|
|
final name = service.name.toLowerCase();
|
|
final description = service.description.toLowerCase();
|
|
final sector = service.secteurActivite.toLowerCase();
|
|
final query = _searchQuery.toLowerCase();
|
|
|
|
return name.contains(query) ||
|
|
description.contains(query) ||
|
|
sector.contains(query);
|
|
}).toList();
|
|
}
|
|
|
|
Future<void> loadServices() async {
|
|
_isLoading = true;
|
|
_error = null;
|
|
notifyListeners();
|
|
|
|
try {
|
|
_allServices = await _apiService.getServices();
|
|
_servicesBySector = _apiService.groupServicesBySector(_allServices);
|
|
_error = null;
|
|
} catch (e) {
|
|
_error = e.toString();
|
|
_allServices = [];
|
|
_servicesBySector = {};
|
|
} finally {
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
Future<void> refreshServices() async {
|
|
await loadServices();
|
|
}
|
|
|
|
void updateSearchQuery(String query) {
|
|
_searchQuery = query;
|
|
notifyListeners();
|
|
}
|
|
|
|
void selectSector(String sector) {
|
|
_selectedSector = sector;
|
|
notifyListeners();
|
|
}
|
|
|
|
void clearSearch() {
|
|
_searchQuery = '';
|
|
notifyListeners();
|
|
}
|
|
|
|
List<WortisService> get activeServices {
|
|
return _allServices.where((service) => service.status).toList();
|
|
}
|
|
|
|
Map<String, int> get servicesCountBySector {
|
|
Map<String, int> count = {};
|
|
for (var entry in _servicesBySector.entries) {
|
|
count[entry.key] = entry.value.length;
|
|
}
|
|
return count;
|
|
}
|
|
}
|
|
|
|
// =================== AUTH CONTROLLER AVEC SUPPORT ENTREPRISE ===================
|
|
class AuthController extends ChangeNotifier {
|
|
bool _isLoading = false;
|
|
bool _isLoggedIn = false;
|
|
String? _agentId;
|
|
String? _agentName;
|
|
String? _pinkey;
|
|
String? _role;
|
|
String? _errorMessage;
|
|
String? _token;
|
|
double _balance = 0.0;
|
|
Map<String, dynamic>? _currentUser;
|
|
DateTime? _lastLoginDate;
|
|
String? _lastWorkingEndpoint;
|
|
|
|
// ===== NOUVEAUX ATTRIBUTS POUR ENTREPRISE =====
|
|
String? _accountType; // 'agent' ou 'enterprise_member'
|
|
Enterprise? _enterprise;
|
|
double? _enterpriseBalance;
|
|
String? _balanceSource; // 'personal' ou 'enterprise'
|
|
|
|
static const String baseUrl = 'https://api.live.wortis.cg/tpe';
|
|
|
|
static const Map<String, String> apiHeaders = {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
};
|
|
|
|
// Getters existants
|
|
bool get isLoading => _isLoading;
|
|
bool get isLoggedIn => _isLoggedIn;
|
|
String? get agentId => _agentId;
|
|
String? get pinkey => _pinkey;
|
|
String? get agentName => _agentName;
|
|
String? get role => _role;
|
|
String? get errorMessage => _errorMessage;
|
|
double get balance => _balance;
|
|
Map<String, dynamic>? get currentUser => _currentUser;
|
|
DateTime? get lastLoginDate => _lastLoginDate;
|
|
|
|
// ===== NOUVEAUX GETTERS POUR ENTREPRISE =====
|
|
String get accountType => _accountType ?? 'agent';
|
|
Enterprise? get enterprise => _enterprise;
|
|
double? get enterpriseBalance => _enterpriseBalance;
|
|
String get balanceSource => _balanceSource ?? 'personal';
|
|
bool get isEnterpriseMember => _accountType == 'enterprise_member';
|
|
bool get isIndividualAgent => _accountType == 'agent';
|
|
|
|
// Retourne le solde à utiliser selon le type de compte
|
|
double get activeBalance {
|
|
if (isEnterpriseMember && _enterpriseBalance != null) {
|
|
return _enterpriseBalance!;
|
|
}
|
|
return _balance;
|
|
}
|
|
|
|
void _setLoading(bool loading) {
|
|
_isLoading = loading;
|
|
notifyListeners();
|
|
}
|
|
|
|
void _setError(String? error) {
|
|
_errorMessage = error;
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<bool> testApiConnection() async {
|
|
try {
|
|
final response = await http
|
|
.get(Uri.parse('$baseUrl/get_services_tpe'), headers: apiHeaders)
|
|
.timeout(Duration(seconds: 5));
|
|
|
|
return response.statusCode == 200;
|
|
} catch (e) {
|
|
print('Erreur test connexion API: $e');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
Future<Map<String, dynamic>> _loginWithApi(
|
|
String agentId,
|
|
String pin,
|
|
bool isSessionLogin,
|
|
) async {
|
|
List<String> possibleEndpoints = [
|
|
'$baseUrl/login',
|
|
'$baseUrl/auth/login',
|
|
'$baseUrl/auth_agent',
|
|
'$baseUrl/authenticate',
|
|
'$baseUrl/agent/login',
|
|
'$baseUrl/tpe/auth',
|
|
];
|
|
|
|
if (_lastWorkingEndpoint != null) {
|
|
possibleEndpoints.remove(_lastWorkingEndpoint);
|
|
possibleEndpoints.insert(0, _lastWorkingEndpoint!);
|
|
}
|
|
|
|
Map<String, dynamic> body;
|
|
if (isSessionLogin) {
|
|
print('Connexion avec token de session');
|
|
String? savedPin = await SessionManager().getPIN();
|
|
body = {'agent_id': agentId, 'pin': savedPin ?? pin};
|
|
} else {
|
|
print('Connexion avec PIN');
|
|
body = {'agent_id': agentId, 'pin': pin};
|
|
}
|
|
|
|
for (String endpoint in possibleEndpoints) {
|
|
try {
|
|
print('Tentative avec endpoint: $endpoint');
|
|
|
|
final response = await http
|
|
.post(
|
|
Uri.parse(endpoint),
|
|
headers: apiHeaders,
|
|
body: jsonEncode(body),
|
|
)
|
|
.timeout(Duration(seconds: 8));
|
|
|
|
print('Réponse API ($endpoint): ${response.statusCode}');
|
|
|
|
if (response.statusCode == 200) {
|
|
final data = jsonDecode(response.body);
|
|
_lastWorkingEndpoint = endpoint;
|
|
print('✅ Connexion réussie avec $endpoint');
|
|
return data;
|
|
} else if (response.statusCode == 404) {
|
|
print('Endpoint $endpoint non trouvé');
|
|
continue;
|
|
} else if (response.statusCode == 401 || response.statusCode == 403) {
|
|
print('Authentification échouée sur $endpoint');
|
|
return {'success': false, 'message': 'Identifiants incorrects'};
|
|
} else {
|
|
print('Erreur $endpoint: ${response.statusCode}');
|
|
continue;
|
|
}
|
|
} catch (e) {
|
|
print('Erreur réseau $endpoint: $e');
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return {
|
|
'success': false,
|
|
'message': 'Service d\'authentification indisponible',
|
|
};
|
|
}
|
|
|
|
Future<bool> login(
|
|
String agentId,
|
|
String pin, [
|
|
bool isSessionLogin = false,
|
|
]) async {
|
|
_setLoading(true);
|
|
_setError(null);
|
|
|
|
try {
|
|
print('🔐 Tentative de connexion pour: $agentId');
|
|
|
|
bool apiConnected = await testApiConnection();
|
|
|
|
if (apiConnected) {
|
|
print('✅ API disponible, connexion en ligne');
|
|
final response = await _loginWithApi(agentId, pin, isSessionLogin);
|
|
|
|
if (response['success'] == true ||
|
|
response.containsKey('user') ||
|
|
response.containsKey('agent_id') ||
|
|
response.containsKey('token')) {
|
|
await _setUserData(response, agentId);
|
|
|
|
final session = SessionManager();
|
|
await session.saveSession(
|
|
_agentId!,
|
|
token: _token,
|
|
role: _role,
|
|
pinkey: pin,
|
|
);
|
|
|
|
// Log selon le type de compte
|
|
if (isEnterpriseMember) {
|
|
print('✅ Connexion membre entreprise réussie');
|
|
print(' - Entreprise: ${_enterprise?.nomEntreprise}');
|
|
print(' - Solde entreprise: ${_enterpriseBalance} FCFA');
|
|
print(' - Solde personnel: $_balance FCFA');
|
|
} else {
|
|
print('✅ Connexion agent individuel réussie');
|
|
print(' - Solde: $_balance FCFA');
|
|
}
|
|
|
|
_setLoading(false);
|
|
return true;
|
|
} else {
|
|
_setError(response['message'] ?? 'Identifiants incorrects');
|
|
print('❌ Erreur API: ${response['message']}');
|
|
}
|
|
} else {
|
|
print('⚠️ API non disponible');
|
|
_setError('Service temporairement indisponible');
|
|
}
|
|
} catch (e) {
|
|
_setError('Erreur de connexion: ${e.toString()}');
|
|
print('❌ Exception login: $e');
|
|
}
|
|
|
|
_setLoading(false);
|
|
return false;
|
|
}
|
|
|
|
/// ===== MÉTHODE AMÉLIORÉE POUR GÉRER LES DONNÉES ENTREPRISE =====
|
|
Future<void> _setUserData(
|
|
Map<String, dynamic> response,
|
|
String agentId,
|
|
) async {
|
|
_isLoggedIn = true;
|
|
_lastLoginDate = DateTime.now();
|
|
|
|
try {
|
|
if (response.containsKey('user') && response['user'] != null) {
|
|
_currentUser = response['user'];
|
|
_agentId = _currentUser?['agent_id']?.toString() ?? agentId;
|
|
_agentName = _currentUser?['nom']?.toString() ?? 'Agent $_agentId';
|
|
_pinkey = _currentUser?['pin']?.toString();
|
|
_role = _currentUser?['role']?.toString() ?? 'agent';
|
|
_balance = _parseDouble(_currentUser?['solde']) ?? 0.0;
|
|
_token =
|
|
_currentUser?['token']?.toString() ?? response['token']?.toString();
|
|
|
|
// Gestion du type de compte
|
|
_accountType = _currentUser?['type']?.toString() ?? 'agent';
|
|
_balanceSource =
|
|
_accountType == 'enterprise_member' ? 'enterprise' : 'personal';
|
|
|
|
// Extraction des données entreprise
|
|
if (_currentUser?['enterprise'] != null) {
|
|
try {
|
|
_enterprise = Enterprise.fromJson(_currentUser!['enterprise']);
|
|
_enterpriseBalance = _enterprise?.soldeEntreprise;
|
|
|
|
print('📊 Données entreprise chargées:');
|
|
print(' - ID: ${_enterprise?.enterpriseId}');
|
|
print(' - Nom: ${_enterprise?.nomEntreprise}');
|
|
print(' - Solde: ${_enterpriseBalance} FCFA');
|
|
print(' - Membres: ${_enterprise?.nombreMembres}');
|
|
print(
|
|
' - Secteur: ${_enterprise?.domaineActivite ?? "Non spécifié"}',
|
|
);
|
|
} catch (e) {
|
|
print('⚠️ Erreur parsing entreprise: $e');
|
|
_enterprise = null;
|
|
_enterpriseBalance = null;
|
|
}
|
|
}
|
|
} else {
|
|
_agentId = response['agent_id']?.toString() ?? agentId;
|
|
_agentName = response['nom']?.toString() ?? 'Agent $_agentId';
|
|
_pinkey = response['pin']?.toString();
|
|
_role = response['role']?.toString() ?? 'agent';
|
|
_balance = _parseDouble(response['solde']) ?? 0.0;
|
|
_token = response['token']?.toString();
|
|
_accountType = response['type']?.toString() ?? 'agent';
|
|
|
|
// Gestion entreprise au niveau racine
|
|
if (response['enterprise'] != null) {
|
|
try {
|
|
_enterprise = Enterprise.fromJson(response['enterprise']);
|
|
_enterpriseBalance = _enterprise?.soldeEntreprise;
|
|
_balanceSource = 'enterprise';
|
|
} catch (e) {
|
|
print('⚠️ Erreur parsing entreprise (racine): $e');
|
|
}
|
|
}
|
|
|
|
_currentUser = {
|
|
'agent_id': _agentId,
|
|
'nom': _agentName,
|
|
'pin': _pinkey,
|
|
'role': _role,
|
|
'solde': _balance,
|
|
'token': _token,
|
|
'type': _accountType,
|
|
'enterprise': _enterprise?.toJson(),
|
|
};
|
|
}
|
|
|
|
// Valeurs par défaut si non définies
|
|
_agentId ??= agentId;
|
|
_agentName ??= 'Agent $_agentId';
|
|
_role ??= 'agent';
|
|
_accountType ??= 'agent';
|
|
_token ??= 'temp_token_${DateTime.now().millisecondsSinceEpoch}';
|
|
} catch (e) {
|
|
print('❌ Erreur configuration données utilisateur: $e');
|
|
print('Stack trace: ${StackTrace.current}');
|
|
|
|
// Fallback sécurisé
|
|
_agentId = agentId;
|
|
_agentName = 'Agent $agentId';
|
|
_role = 'agent';
|
|
_balance = 0.0;
|
|
_accountType = 'agent';
|
|
_token = 'fallback_token_${DateTime.now().millisecondsSinceEpoch}';
|
|
_enterprise = null;
|
|
_enterpriseBalance = null;
|
|
|
|
_currentUser = {
|
|
'agent_id': _agentId,
|
|
'nom': _agentName,
|
|
'role': _role,
|
|
'solde': _balance,
|
|
'token': _token,
|
|
'type': _accountType,
|
|
};
|
|
}
|
|
}
|
|
|
|
double? _parseDouble(dynamic value) {
|
|
if (value == null) return null;
|
|
if (value is double) return value;
|
|
if (value is int) return value.toDouble();
|
|
if (value is String) {
|
|
try {
|
|
return double.parse(value);
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
Future<bool> checkLoginStatus(BuildContext context) async {
|
|
_setLoading(true);
|
|
|
|
try {
|
|
final session = SessionManager();
|
|
bool hasSession = await session.isLoggedIn();
|
|
|
|
if (hasSession) {
|
|
String? savedUserId = await session.getUserId();
|
|
String? savedToken = await session.getToken();
|
|
String? savedRole = await session.getRole();
|
|
String? savedPin = await session.getPIN();
|
|
|
|
if (savedUserId != null && savedPin != null && savedRole != null) {
|
|
print(
|
|
'Session trouvée pour $savedUserId, tentative de reconnexion...',
|
|
);
|
|
|
|
bool reconnected = await login(savedUserId, savedPin, true);
|
|
|
|
if (reconnected) {
|
|
print('✅ Reconnexion automatique réussie');
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
RoleNavigator.navigateByRole(context, _role!);
|
|
});
|
|
|
|
_setLoading(false);
|
|
return true;
|
|
} else {
|
|
print('❌ Échec de la reconnexion, suppression de la session');
|
|
await session.clearSession();
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
print('❌ Erreur vérification session: $e');
|
|
}
|
|
|
|
_setLoading(false);
|
|
return false;
|
|
}
|
|
|
|
/// ===== NOUVELLE MÉTHODE: RAFRAÎCHIR TOUS LES SOLDES =====
|
|
Future<void> refreshBalance() async {
|
|
if (_agentId != null) {
|
|
try {
|
|
final response = await http.get(
|
|
Uri.parse('$baseUrl/agent/$_agentId/balance'),
|
|
headers: apiHeaders,
|
|
);
|
|
|
|
if (response.statusCode == 200) {
|
|
final data = jsonDecode(response.body);
|
|
if (data['success']) {
|
|
// Solde personnel
|
|
_balance = _parseDouble(data['solde']) ?? 0.0;
|
|
|
|
// Solde entreprise si applicable
|
|
if (data['enterprise'] != null) {
|
|
_enterpriseBalance = _parseDouble(
|
|
data['enterprise']['solde_entreprise'],
|
|
);
|
|
}
|
|
|
|
notifyListeners();
|
|
print(
|
|
'✅ Soldes mis à jour - Personnel: $_balance, Entreprise: $_enterpriseBalance',
|
|
);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
print('❌ Erreur actualisation solde: $e');
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> logout() async {
|
|
print('Déconnexion de $_agentName');
|
|
|
|
_isLoggedIn = false;
|
|
_agentId = null;
|
|
_agentName = null;
|
|
_pinkey = null;
|
|
_role = null;
|
|
_balance = 0.0;
|
|
_errorMessage = null;
|
|
_currentUser = null;
|
|
_lastLoginDate = null;
|
|
_token = null;
|
|
|
|
// Réinitialiser les données entreprise
|
|
_accountType = null;
|
|
_enterprise = null;
|
|
_enterpriseBalance = null;
|
|
_balanceSource = null;
|
|
|
|
await SessionManager().clearSession();
|
|
notifyListeners();
|
|
}
|
|
|
|
/// ===== MÉTHODES AMÉLIORÉES POUR GÉRER LES SOLDES =====
|
|
void updateBalance(double newBalance) {
|
|
if (isEnterpriseMember) {
|
|
// Mise à jour du solde entreprise
|
|
if (newBalance != _enterpriseBalance) {
|
|
_enterpriseBalance = newBalance;
|
|
if (_enterprise != null) {
|
|
_enterprise = _enterprise!.copyWith(soldeEntreprise: newBalance);
|
|
}
|
|
notifyListeners();
|
|
}
|
|
} else {
|
|
// Mise à jour du solde personnel
|
|
if (newBalance != _balance) {
|
|
_balance = newBalance;
|
|
if (_currentUser != null) {
|
|
_currentUser!['solde'] = newBalance;
|
|
}
|
|
notifyListeners();
|
|
}
|
|
}
|
|
}
|
|
|
|
void debitBalance(double amount) {
|
|
if (amount <= 0) return;
|
|
|
|
if (isEnterpriseMember && _enterpriseBalance != null) {
|
|
// Débiter le solde entreprise
|
|
if (amount <= _enterpriseBalance!) {
|
|
_enterpriseBalance = _enterpriseBalance! - amount;
|
|
notifyListeners();
|
|
}
|
|
} else {
|
|
// Débiter le solde personnel
|
|
if (amount <= _balance) {
|
|
_balance -= amount;
|
|
if (_currentUser != null) {
|
|
_currentUser!['solde'] = _balance;
|
|
}
|
|
notifyListeners();
|
|
}
|
|
}
|
|
}
|
|
|
|
void creditBalance(double amount) {
|
|
if (amount <= 0) return;
|
|
|
|
if (isEnterpriseMember && _enterpriseBalance != null) {
|
|
// Créditer le solde entreprise
|
|
_enterpriseBalance = _enterpriseBalance! + amount;
|
|
notifyListeners();
|
|
} else {
|
|
// Créditer le solde personnel
|
|
_balance += amount;
|
|
if (_currentUser != null) {
|
|
_currentUser!['solde'] = _balance;
|
|
}
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
bool hasSufficientBalance(double requiredAmount) {
|
|
return activeBalance >= requiredAmount;
|
|
}
|
|
|
|
void clearError() {
|
|
_setError(null);
|
|
}
|
|
|
|
/// ===== NOUVELLE MÉTHODE: OBTENIR LES INFOS DE BALANCE =====
|
|
Map<String, dynamic> getBalanceInfo() {
|
|
return {
|
|
'account_type': accountType,
|
|
'is_enterprise_member': isEnterpriseMember,
|
|
'personal_balance': _balance,
|
|
'enterprise_balance': _enterpriseBalance,
|
|
'active_balance': activeBalance,
|
|
'balance_source': balanceSource,
|
|
'enterprise_name': _enterprise?.nomEntreprise,
|
|
'enterprise_id': _enterprise?.enterpriseId,
|
|
};
|
|
}
|
|
}
|
|
|
|
// =================== MAIN APP ===================
|
|
void main() async {
|
|
WidgetsFlutterBinding.ensureInitialized();
|
|
|
|
await SystemChrome.setPreferredOrientations([
|
|
DeviceOrientation.portraitUp,
|
|
DeviceOrientation.portraitDown,
|
|
]);
|
|
|
|
ResponsiveHelper.enableAllOrientations();
|
|
|
|
SystemChrome.setSystemUIOverlayStyle(
|
|
SystemUiOverlayStyle(
|
|
statusBarColor: Colors.transparent,
|
|
statusBarIconBrightness: Brightness.light,
|
|
systemNavigationBarColor: Color(0xFF006699),
|
|
systemNavigationBarIconBrightness: Brightness.light,
|
|
),
|
|
);
|
|
|
|
ConnectivityService.setNavigatorKey(navigatorKey);
|
|
|
|
runApp(WortisApp());
|
|
}
|
|
|
|
class WortisApp extends StatefulWidget {
|
|
static const Color primaryColor = Color(0xFF006699);
|
|
static const Color secondaryColor = Color(0xFF0088CC);
|
|
static const Color accentColor = Color(0xFFFF6B35);
|
|
static const Color backgroundColor = Color(0xFFF8FAFC);
|
|
static const Color surfaceColor = Colors.white;
|
|
static const Color errorColor = Color(0xFFE53E3E);
|
|
|
|
const WortisApp({super.key});
|
|
|
|
@override
|
|
State<WortisApp> createState() => _WortisAppState();
|
|
}
|
|
|
|
class _WortisAppState extends State<WortisApp> with WidgetsBindingObserver {
|
|
final ConnectivityService _connectivityService = ConnectivityService();
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
WidgetsBinding.instance.addObserver(this);
|
|
|
|
print('✅ WortisApp initialisé avec support entreprise');
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
print('🌐 Démarrage de la surveillance de connectivité');
|
|
_connectivityService.startMonitoring();
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_connectivityService.dispose();
|
|
WidgetsBinding.instance.removeObserver(this);
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
|
super.didChangeAppLifecycleState(state);
|
|
|
|
if (state == AppLifecycleState.resumed) {
|
|
print('📱 App au premier plan, vérification de connectivité');
|
|
_connectivityService.checkConnectivity();
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return MultiProvider(
|
|
providers: [
|
|
ChangeNotifierProvider(create: (_) => AuthController()),
|
|
ChangeNotifierProvider(create: (_) => ServicesController()),
|
|
ChangeNotifierProvider<ConnectivityService>.value(
|
|
value: _connectivityService,
|
|
),
|
|
],
|
|
child: MaterialApp(
|
|
title: 'Wortis Agent',
|
|
debugShowCheckedModeBanner: false,
|
|
navigatorKey: navigatorKey,
|
|
theme: _buildAppTheme(),
|
|
home: SplashScreen(),
|
|
builder: (context, child) {
|
|
return MediaQuery(
|
|
data: MediaQuery.of(
|
|
context,
|
|
).copyWith(textScaler: TextScaler.linear(1.0)),
|
|
child: child!,
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
ThemeData _buildAppTheme() {
|
|
return ThemeData(
|
|
primarySwatch: _createMaterialColor(WortisApp.primaryColor),
|
|
primaryColor: WortisApp.primaryColor,
|
|
scaffoldBackgroundColor: WortisApp.backgroundColor,
|
|
|
|
appBarTheme: AppBarTheme(
|
|
backgroundColor: WortisApp.primaryColor,
|
|
foregroundColor: Colors.white,
|
|
elevation: 0,
|
|
centerTitle: true,
|
|
titleTextStyle: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.white,
|
|
),
|
|
systemOverlayStyle: SystemUiOverlayStyle.light,
|
|
),
|
|
|
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: WortisApp.primaryColor,
|
|
foregroundColor: Colors.white,
|
|
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
elevation: 2,
|
|
textStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
|
),
|
|
),
|
|
|
|
inputDecorationTheme: InputDecorationTheme(
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
borderSide: BorderSide(color: Colors.grey.shade300),
|
|
),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
borderSide: BorderSide(color: WortisApp.primaryColor, width: 2),
|
|
),
|
|
errorBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
borderSide: BorderSide(color: WortisApp.errorColor),
|
|
),
|
|
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
filled: true,
|
|
fillColor: WortisApp.surfaceColor,
|
|
),
|
|
|
|
snackBarTheme: SnackBarThemeData(
|
|
backgroundColor: Colors.grey.shade800,
|
|
contentTextStyle: TextStyle(color: Colors.white),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
behavior: SnackBarBehavior.floating,
|
|
),
|
|
|
|
// cardTheme: CardTheme(
|
|
// elevation: 2,
|
|
// shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
// clipBehavior: Clip.antiAlias,
|
|
// ),
|
|
visualDensity: VisualDensity.adaptivePlatformDensity,
|
|
useMaterial3: true,
|
|
);
|
|
}
|
|
|
|
MaterialColor _createMaterialColor(Color color) {
|
|
List strengths = <double>[.05];
|
|
Map<int, Color> swatch = <int, Color>{};
|
|
final int r = color.red, g = color.green, b = color.blue;
|
|
|
|
for (int i = 1; i < 10; i++) {
|
|
strengths.add(0.1 * i);
|
|
}
|
|
|
|
for (var strength in strengths) {
|
|
final double ds = 0.5 - strength;
|
|
swatch[(strength * 1000).round()] = Color.fromRGBO(
|
|
r + ((ds < 0 ? r : (255 - r)) * ds).round(),
|
|
g + ((ds < 0 ? g : (255 - g)) * ds).round(),
|
|
b + ((ds < 0 ? b : (255 - b)) * ds).round(),
|
|
1,
|
|
);
|
|
}
|
|
return MaterialColor(color.value, swatch);
|
|
}
|
|
}
|