Initial commit: unionflow-mobile-apps

Application Flutter complète (sans build artifacts).

Signed-off-by: lions dev Team
This commit is contained in:
dahoud
2026-03-15 16:30:08 +00:00
commit d094d6db9c
1790 changed files with 507435 additions and 0 deletions

View File

@@ -0,0 +1,146 @@
import 'package:dio/dio.dart';
import 'package:injectable/injectable.dart';
import 'package:unionflow_mobile_apps/core/network/api_client.dart';
import 'package:unionflow_mobile_apps/core/utils/logger.dart';
import '../../domain/entities/network_item.dart';
/// Repository pour la recherche réseau (membres + organisations).
/// Délègue la recherche au backend Quarkus.
@lazySingleton
class NetworkRepository {
final ApiClient _apiClient;
NetworkRepository(this._apiClient);
List<dynamic> _parseListResponse(dynamic data) {
if (data is List) return data;
if (data is Map && data.containsKey('content')) {
final content = data['content'];
return content is List ? content : [];
}
return [];
}
/// Recherche de membres (GET /api/membres/recherche?q=...)
Future<List<NetworkItem>> searchMembers(String query, {int page = 0, int size = 20}) async {
if (query.trim().isEmpty) return [];
try {
final response = await _apiClient.get(
'/api/membres/recherche',
queryParameters: {'q': query.trim(), 'page': page, 'size': size},
);
final data = _parseListResponse(response.data);
return data.map((json) => _memberFromJson(json as Map<String, dynamic>)).toList();
} on DioException catch (e) {
if (e.response?.statusCode == 400) return [];
rethrow;
}
}
/// Recherche d'organisations (GET /api/organisations/recherche?nom=...)
Future<List<NetworkItem>> searchOrganizations(String query, {int page = 0, int size = 20}) async {
if (query.trim().isEmpty) return [];
try {
final response = await _apiClient.get(
'/api/organisations/recherche',
queryParameters: {'nom': query.trim(), 'page': page, 'size': size},
);
final data = _parseListResponse(response.data);
return data.map((json) => _organisationFromJson(json as Map<String, dynamic>)).toList();
} on DioException catch (e) {
if (e.response?.statusCode == 400) return [];
rethrow;
}
}
/// Recherche globale : membres + organisations (deux appels parallèles).
/// Si [followedIds] est fourni, les membres dont l'id est dans le set auront [isConnected: true].
Future<List<NetworkItem>> search(String query, {int page = 0, int size = 10, Set<String>? followedIds}) async {
if (query.trim().isEmpty) return [];
try {
final results = await Future.wait([
searchMembers(query, page: page, size: size),
searchOrganizations(query, page: page, size: size),
]);
final members = results[0].map((m) {
if (followedIds != null && followedIds.contains(m.id)) return m.copyWith(isConnected: true);
return m;
}).toList();
final orgs = results[1];
return [...members, ...orgs];
} catch (e) {
rethrow;
}
}
/// Liste des ids des membres suivis par l'utilisateur connecté (GET /api/membres/me/suivis).
Future<List<String>> getFollowedIds() async {
try {
final response = await _apiClient.get('/api/membres/me/suivis');
if (response.statusCode != 200) return [];
final data = response.data;
if (data is! List) return [];
return data.map((e) => e.toString()).toList();
} on DioException catch (e) {
if (e.response?.statusCode == 401 || e.response?.statusCode == 403) return [];
AppLogger.error('NetworkRepository: getFollowedIds échoué', error: e);
rethrow;
}
}
/// Suivre un membre (POST /api/membres/{id}/suivre). Retourne true si following après l'appel.
Future<bool> follow(String memberId) async {
try {
final response = await _apiClient.post('/api/membres/$memberId/suivre');
if (response.statusCode == 200 && response.data is Map) {
return (response.data as Map)['following'] == true;
}
return false;
} on DioException catch (e, st) {
AppLogger.error('NetworkRepository: follow échoué', error: e, stackTrace: st);
rethrow;
}
}
/// Ne plus suivre un membre (DELETE /api/membres/{id}/suivre). Retourne false (plus suivi).
Future<bool> unfollow(String memberId) async {
try {
final response = await _apiClient.delete('/api/membres/$memberId/suivre');
if (response.statusCode == 200 && response.data is Map) {
return (response.data as Map)['following'] == true;
}
return false;
} on DioException catch (e, st) {
AppLogger.error('NetworkRepository: unfollow échoué', error: e, stackTrace: st);
rethrow;
}
}
static NetworkItem _memberFromJson(Map<String, dynamic> json) {
final id = json['id']?.toString() ?? '';
final prenom = json['prenom']?.toString() ?? '';
final nom = json['nom']?.toString() ?? '';
final name = '$prenom $nom'.trim().isEmpty ? (json['numeroMembre']?.toString() ?? id) : '$prenom $nom'.trim();
return NetworkItem(
id: id,
name: name,
subtitle: json['profession']?.toString() ?? json['statutCompteLibelle']?.toString(),
avatarUrl: null,
type: 'Member',
isConnected: false,
);
}
static NetworkItem _organisationFromJson(Map<String, dynamic> json) {
final id = json['id']?.toString() ?? '';
return NetworkItem(
id: id,
name: json['nom']?.toString() ?? json['nomCourt']?.toString() ?? 'Organisation',
subtitle: json['typeOrganisationLibelle']?.toString() ?? json['statutLibelle']?.toString(),
avatarUrl: null,
type: 'Organization',
isConnected: false,
);
}
}

View File

@@ -0,0 +1,41 @@
import 'package:equatable/equatable.dart';
/// Entité représentant un membre ou une organisation dans la recherche réseau.
class NetworkItem extends Equatable {
final String id;
final String name;
final String? subtitle;
final String? avatarUrl;
final String type; // 'Member', 'Organization'
final bool isConnected;
const NetworkItem({
required this.id,
required this.name,
this.subtitle,
this.avatarUrl,
required this.type,
this.isConnected = false,
});
NetworkItem copyWith({
String? id,
String? name,
String? subtitle,
String? avatarUrl,
String? type,
bool? isConnected,
}) {
return NetworkItem(
id: id ?? this.id,
name: name ?? this.name,
subtitle: subtitle ?? this.subtitle,
avatarUrl: avatarUrl ?? this.avatarUrl,
type: type ?? this.type,
isConnected: isConnected ?? this.isConnected,
);
}
@override
List<Object?> get props => [id, name, subtitle, avatarUrl, type, isConnected];
}

View File

@@ -0,0 +1,83 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:injectable/injectable.dart';
import '../../../../core/utils/logger.dart';
import '../../data/repositories/network_repository.dart';
import 'network_event.dart';
import 'network_state.dart';
@injectable
class NetworkBloc extends Bloc<NetworkEvent, NetworkState> {
final NetworkRepository _repository;
NetworkBloc(this._repository) : super(NetworkInitial()) {
on<LoadNetworkRequested>(_onLoadNetworkRequested);
on<SearchNetworkRequested>(_onSearchNetworkRequested);
on<ToggleFollowRequested>(_onToggleFollowRequested);
}
Future<void> _onToggleFollowRequested(ToggleFollowRequested event, Emitter<NetworkState> emit) async {
if (state is! NetworkLoaded) return;
final current = state as NetworkLoaded;
NetworkItem? item;
for (final i in current.items) {
if (i.id == event.itemId) {
item = i;
break;
}
}
if (item == null) return;
// Seuls les membres (type Member) sont persistés côté backend ; Organisation reste local.
if (item.type != 'Member') {
final items = current.items.map((i) {
if (i.id == event.itemId) return i.copyWith(isConnected: !i.isConnected);
return i;
}).toList();
emit(NetworkLoaded(items: items, currentQuery: current.currentQuery));
return;
}
try {
final bool newFollowing = item.isConnected
? await _repository.unfollow(event.itemId)
: await _repository.follow(event.itemId);
final items = current.items.map((i) {
if (i.id == event.itemId) return i.copyWith(isConnected: newFollowing);
return i;
}).toList();
emit(NetworkLoaded(items: items, currentQuery: current.currentQuery));
} catch (e, st) {
AppLogger.error('NetworkBloc: toggle follow échoué', error: e, stackTrace: st);
emit(const NetworkError('Impossible de mettre à jour le suivi. Réessayez.'));
}
}
Future<void> _onLoadNetworkRequested(LoadNetworkRequested event, Emitter<NetworkState> emit) async {
emit(NetworkLoading());
try {
final followedIds = await _repository.getFollowedIds();
final items = await _repository.search('', followedIds: followedIds.toSet());
emit(NetworkLoaded(items: items, currentQuery: ''));
} catch (e, st) {
AppLogger.error('NetworkBloc: chargement réseau échoué', error: e, stackTrace: st);
emit(NetworkError('Erreur chargement réseau : $e'));
}
}
Future<void> _onSearchNetworkRequested(SearchNetworkRequested event, Emitter<NetworkState> emit) async {
emit(NetworkLoading());
try {
if (event.query.trim().isEmpty) {
final followedIds = await _repository.getFollowedIds();
final items = await _repository.search('', followedIds: followedIds.toSet());
emit(NetworkLoaded(items: items, currentQuery: ''));
return;
}
final followedIds = await _repository.getFollowedIds();
final items = await _repository.search(event.query, followedIds: followedIds.toSet());
emit(NetworkLoaded(items: items, currentQuery: event.query));
} catch (e, st) {
AppLogger.error('NetworkBloc: recherche réseau échouée', error: e, stackTrace: st);
emit(NetworkError('Erreur de recherche : $e'));
}
}
}

View File

@@ -0,0 +1,29 @@
import 'package:equatable/equatable.dart';
abstract class NetworkEvent extends Equatable {
const NetworkEvent();
@override
List<Object> get props => [];
}
class LoadNetworkRequested extends NetworkEvent {}
class SearchNetworkRequested extends NetworkEvent {
final String query;
const SearchNetworkRequested(this.query);
@override
List<Object> get props => [query];
}
/// Bascule Suivre / Ne plus suivre pour un item (membre ou organisation).
class ToggleFollowRequested extends NetworkEvent {
final String itemId;
const ToggleFollowRequested(this.itemId);
@override
List<Object> get props => [itemId];
}

View File

@@ -0,0 +1,38 @@
import 'package:equatable/equatable.dart';
import '../../domain/entities/network_item.dart';
export '../../domain/entities/network_item.dart';
abstract class NetworkState extends Equatable {
const NetworkState();
@override
List<Object?> get props => [];
}
class NetworkInitial extends NetworkState {}
class NetworkLoading extends NetworkState {}
class NetworkLoaded extends NetworkState {
final List<NetworkItem> items;
final String currentQuery;
const NetworkLoaded({
required this.items,
this.currentQuery = '',
});
@override
List<Object?> get props => [items, currentQuery];
}
class NetworkError extends NetworkState {
final String message;
const NetworkError(this.message);
@override
List<Object?> get props => [message];
}