feat: WebSocket temps réel + Finance Workflow + corrections
- Task #6: WebSocket /ws/dashboard + Kafka events (5 topics) * Backend: KafkaEventProducer, KafkaEventConsumer * Mobile: WebSocketService (reconnection, heartbeat, typed events) * DashboardBloc: Auto-refresh depuis WebSocket events - Finance Workflow: approbations + budgets (backend + mobile) * Backend: entities, services, resources, migrations Flyway V6 * Mobile: features finance_workflow complète avec BLoC - Corrections DI: interfaces IRepository partout * IProfileRepository, IOrganizationRepository, IMembreRepository * GetIt configuré avec @injectable - Spec-Kit: constitution + templates mis à jour * .specify/memory/constitution.md enrichie * Templates agent, plan, spec, tasks, checklist - Nettoyage: fichiers temporaires supprimés Signed-off-by: lions dev Team
This commit is contained in:
@@ -1,17 +1,45 @@
|
||||
/// BLoC pour la gestion des organisations
|
||||
/// BLoC pour la gestion des organisations (Clean Architecture)
|
||||
library organizations_bloc;
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import '../data/models/organization_model.dart';
|
||||
import '../data/services/organization_service.dart';
|
||||
import '../domain/usecases/get_organizations.dart';
|
||||
import '../domain/usecases/get_organization_by_id.dart';
|
||||
import '../domain/usecases/create_organization.dart' as uc;
|
||||
import '../domain/usecases/update_organization.dart' as uc;
|
||||
import '../domain/usecases/delete_organization.dart' as uc;
|
||||
import '../domain/usecases/get_organization_members.dart';
|
||||
import '../domain/usecases/update_organization_config.dart';
|
||||
import '../domain/repositories/organization_repository.dart';
|
||||
import 'organizations_event.dart';
|
||||
import 'organizations_state.dart';
|
||||
|
||||
/// BLoC principal pour la gestion des organisations
|
||||
@injectable
|
||||
class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
|
||||
final OrganizationService _organizationService;
|
||||
final GetOrganizations _getOrganizations;
|
||||
final GetOrganizationById _getOrganizationById;
|
||||
final uc.CreateOrganization _createOrganization;
|
||||
final uc.UpdateOrganization _updateOrganization;
|
||||
final uc.DeleteOrganization _deleteOrganization;
|
||||
final GetOrganizationMembers _getOrganizationMembers;
|
||||
final UpdateOrganizationConfig _updateOrganizationConfig;
|
||||
final IOrganizationRepository _repository; // Pour méthodes non-couvertes (activate, suspend, search, stats)
|
||||
final OrganizationService _organizationService; // Pour helpers (sort, filter local)
|
||||
|
||||
OrganizationsBloc(this._organizationService) : super(const OrganizationsInitial()) {
|
||||
OrganizationsBloc(
|
||||
this._getOrganizations,
|
||||
this._getOrganizationById,
|
||||
this._createOrganization,
|
||||
this._updateOrganization,
|
||||
this._deleteOrganization,
|
||||
this._getOrganizationMembers,
|
||||
this._updateOrganizationConfig,
|
||||
this._repository,
|
||||
this._organizationService,
|
||||
) : super(const OrganizationsInitial()) {
|
||||
// Enregistrement des handlers d'événements
|
||||
on<LoadOrganizations>(_onLoadOrganizations);
|
||||
on<LoadMoreOrganizations>(_onLoadMoreOrganizations);
|
||||
@@ -42,18 +70,39 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
|
||||
emit(const OrganizationsLoading());
|
||||
}
|
||||
|
||||
final organizations = await _organizationService.getOrganizations(
|
||||
page: event.page,
|
||||
size: event.size,
|
||||
recherche: event.recherche,
|
||||
);
|
||||
List<OrganizationModel> listToUse;
|
||||
List<String>? filterIds;
|
||||
if (event.useMesOnly) {
|
||||
// Admin d'organisation : endpoint backend /mes retourne uniquement ses organisations
|
||||
listToUse = await _repository.getMesOrganisations();
|
||||
filterIds = listToUse.map((o) => o.id).whereType<String>().toList();
|
||||
} else {
|
||||
final organizations = await _getOrganizations(
|
||||
page: event.page,
|
||||
size: event.size,
|
||||
recherche: event.recherche,
|
||||
);
|
||||
if (event.filterOrganizationIds != null &&
|
||||
event.filterOrganizationIds!.isNotEmpty) {
|
||||
final allowedIds = event.filterOrganizationIds!.toSet();
|
||||
listToUse = organizations
|
||||
.where((org) => org.id != null && allowedIds.contains(org.id))
|
||||
.toList();
|
||||
filterIds = event.filterOrganizationIds;
|
||||
} else {
|
||||
listToUse = organizations;
|
||||
filterIds = null;
|
||||
}
|
||||
}
|
||||
|
||||
emit(OrganizationsLoaded(
|
||||
organizations: organizations,
|
||||
filteredOrganizations: organizations,
|
||||
hasReachedMax: organizations.length < event.size,
|
||||
organizations: listToUse,
|
||||
filteredOrganizations: listToUse,
|
||||
hasReachedMax: event.useMesOnly || listToUse.length < event.size,
|
||||
currentPage: event.page,
|
||||
currentSearch: event.recherche,
|
||||
filterOrganizationIds: filterIds,
|
||||
useMesOnly: event.useMesOnly,
|
||||
));
|
||||
} catch (e) {
|
||||
emit(OrganizationsError(
|
||||
@@ -77,13 +126,21 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
|
||||
|
||||
try {
|
||||
final nextPage = currentState.currentPage + 1;
|
||||
final newOrganizations = await _organizationService.getOrganizations(
|
||||
final newOrganizations = await _getOrganizations(
|
||||
page: nextPage,
|
||||
size: 20,
|
||||
recherche: currentState.currentSearch,
|
||||
);
|
||||
|
||||
final allOrganizations = [...currentState.organizations, ...newOrganizations];
|
||||
var allOrganizations = [...currentState.organizations, ...newOrganizations];
|
||||
// Réappliquer le filtre orgAdmin si présent
|
||||
if (currentState.filterOrganizationIds != null &&
|
||||
currentState.filterOrganizationIds!.isNotEmpty) {
|
||||
final allowedIds = currentState.filterOrganizationIds!.toSet();
|
||||
allOrganizations = allOrganizations
|
||||
.where((org) => org.id != null && allowedIds.contains(org.id))
|
||||
.toList();
|
||||
}
|
||||
final filteredOrganizations = _applyCurrentFilters(allOrganizations, currentState);
|
||||
|
||||
emit(currentState.copyWith(
|
||||
@@ -137,7 +194,7 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
|
||||
));
|
||||
|
||||
// Puis recherche serveur pour plus de résultats
|
||||
final serverResults = await _organizationService.getOrganizations(
|
||||
final serverResults = await _getOrganizations(
|
||||
page: 0,
|
||||
size: 50,
|
||||
recherche: event.query,
|
||||
@@ -169,7 +226,7 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
|
||||
emit(const OrganizationsLoading());
|
||||
|
||||
try {
|
||||
final organizations = await _organizationService.searchOrganizations(
|
||||
final organizations = await _repository.searchOrganizations(
|
||||
nom: event.nom,
|
||||
type: event.type,
|
||||
statut: event.statut,
|
||||
@@ -204,7 +261,7 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
|
||||
emit(OrganizationLoading(event.id));
|
||||
|
||||
try {
|
||||
final organization = await _organizationService.getOrganizationById(event.id);
|
||||
final organization = await _getOrganizationById(event.id);
|
||||
if (organization != null) {
|
||||
emit(OrganizationLoaded(organization));
|
||||
} else {
|
||||
@@ -226,7 +283,7 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
|
||||
emit(const OrganizationCreating());
|
||||
|
||||
try {
|
||||
final createdOrganization = await _organizationService.createOrganization(event.organization);
|
||||
final createdOrganization = await _createOrganization(event.organization);
|
||||
emit(OrganizationCreated(createdOrganization));
|
||||
|
||||
// Recharger la liste si elle était déjà chargée
|
||||
@@ -249,7 +306,7 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
|
||||
emit(OrganizationUpdating(event.id));
|
||||
|
||||
try {
|
||||
final updatedOrganization = await _organizationService.updateOrganization(
|
||||
final updatedOrganization = await _updateOrganization(
|
||||
event.id,
|
||||
event.organization,
|
||||
);
|
||||
@@ -284,7 +341,7 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
|
||||
emit(OrganizationDeleting(event.id));
|
||||
|
||||
try {
|
||||
await _organizationService.deleteOrganization(event.id);
|
||||
await _deleteOrganization(event.id);
|
||||
emit(OrganizationDeleted(event.id));
|
||||
|
||||
// Retirer de la liste si elle était déjà chargée
|
||||
@@ -313,7 +370,7 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
|
||||
emit(OrganizationActivating(event.id));
|
||||
|
||||
try {
|
||||
final activatedOrganization = await _organizationService.activateOrganization(event.id);
|
||||
final activatedOrganization = await _repository.activateOrganization(event.id);
|
||||
emit(OrganizationActivated(activatedOrganization));
|
||||
|
||||
// Mettre à jour la liste si elle était déjà chargée
|
||||
@@ -345,7 +402,7 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
|
||||
emit(OrganizationSuspending(event.id));
|
||||
|
||||
try {
|
||||
final suspendedOrganization = await _organizationService.suspendOrganization(event.id);
|
||||
final suspendedOrganization = await _repository.suspendOrganization(event.id);
|
||||
emit(OrganizationSuspended(suspendedOrganization));
|
||||
|
||||
// Mettre à jour la liste si elle était déjà chargée
|
||||
@@ -454,7 +511,7 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
|
||||
emit(const OrganizationsStatsLoading());
|
||||
|
||||
try {
|
||||
final stats = await _organizationService.getOrganizationsStats();
|
||||
final stats = await _repository.getOrganizationsStats();
|
||||
emit(OrganizationsStatsLoaded(stats));
|
||||
} catch (e) {
|
||||
emit(const OrganizationsStatsError('Erreur lors du chargement des statistiques'));
|
||||
@@ -483,7 +540,15 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
|
||||
RefreshOrganizations event,
|
||||
Emitter<OrganizationsState> emit,
|
||||
) {
|
||||
add(const LoadOrganizations(refresh: true));
|
||||
final currentState = state;
|
||||
if (currentState is OrganizationsLoaded && currentState.useMesOnly) {
|
||||
add(const LoadOrganizations(refresh: true, useMesOnly: true));
|
||||
} else {
|
||||
final filterIds = currentState is OrganizationsLoaded
|
||||
? currentState.filterOrganizationIds
|
||||
: null;
|
||||
add(LoadOrganizations(refresh: true, filterOrganizationIds: filterIds));
|
||||
}
|
||||
}
|
||||
|
||||
/// Remet à zéro l'état
|
||||
|
||||
@@ -18,16 +18,22 @@ class LoadOrganizations extends OrganizationsEvent {
|
||||
final int size;
|
||||
final String? recherche;
|
||||
final bool refresh;
|
||||
/// Filtre par IDs d'organisations (ex. pour orgAdmin : uniquement son organisation)
|
||||
final List<String>? filterOrganizationIds;
|
||||
/// Si true, appelle GET /api/organisations/mes (uniquement les orgs du membre connecté)
|
||||
final bool useMesOnly;
|
||||
|
||||
const LoadOrganizations({
|
||||
this.page = 0,
|
||||
this.size = 20,
|
||||
this.recherche,
|
||||
this.refresh = false,
|
||||
this.filterOrganizationIds,
|
||||
this.useMesOnly = false,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [page, size, recherche, refresh];
|
||||
List<Object?> get props => [page, size, recherche, refresh, filterOrganizationIds, useMesOnly];
|
||||
}
|
||||
|
||||
/// Événement pour charger plus d'organisations (pagination)
|
||||
|
||||
@@ -44,6 +44,10 @@ class OrganizationsLoaded extends OrganizationsState {
|
||||
final OrganizationSortType? sortType;
|
||||
final bool sortAscending;
|
||||
final Map<String, dynamic>? stats;
|
||||
/// Filtre appliqué (ex. orgAdmin : uniquement ses organisations)
|
||||
final List<String>? filterOrganizationIds;
|
||||
/// True si la liste provient de GET /mes (admin d'organisation)
|
||||
final bool useMesOnly;
|
||||
|
||||
const OrganizationsLoaded({
|
||||
required this.organizations,
|
||||
@@ -56,6 +60,8 @@ class OrganizationsLoaded extends OrganizationsState {
|
||||
this.sortType,
|
||||
this.sortAscending = true,
|
||||
this.stats,
|
||||
this.filterOrganizationIds,
|
||||
this.useMesOnly = false,
|
||||
});
|
||||
|
||||
/// Copie avec modifications
|
||||
@@ -70,6 +76,8 @@ class OrganizationsLoaded extends OrganizationsState {
|
||||
OrganizationSortType? sortType,
|
||||
bool? sortAscending,
|
||||
Map<String, dynamic>? stats,
|
||||
List<String>? filterOrganizationIds,
|
||||
bool? useMesOnly,
|
||||
bool clearSearch = false,
|
||||
bool clearStatusFilter = false,
|
||||
bool clearTypeFilter = false,
|
||||
@@ -86,6 +94,8 @@ class OrganizationsLoaded extends OrganizationsState {
|
||||
sortType: clearSort ? null : (sortType ?? this.sortType),
|
||||
sortAscending: sortAscending ?? this.sortAscending,
|
||||
stats: stats ?? this.stats,
|
||||
filterOrganizationIds: filterOrganizationIds ?? this.filterOrganizationIds,
|
||||
useMesOnly: useMesOnly ?? this.useMesOnly,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -130,6 +140,8 @@ class OrganizationsLoaded extends OrganizationsState {
|
||||
sortType,
|
||||
sortAscending,
|
||||
stats,
|
||||
filterOrganizationIds,
|
||||
useMesOnly,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -213,6 +213,10 @@ class OrganizationModel extends Equatable {
|
||||
@JsonKey(name: 'nombreAdministrateurs')
|
||||
final int nombreAdministrateurs;
|
||||
|
||||
/// Nombre d'événements (fourni par l'API si disponible)
|
||||
@JsonKey(name: 'nombreEvenements')
|
||||
final int? nombreEvenements;
|
||||
|
||||
/// Budget annuel
|
||||
@JsonKey(name: 'budgetAnnuel')
|
||||
final double? budgetAnnuel;
|
||||
@@ -280,6 +284,7 @@ class OrganizationModel extends Equatable {
|
||||
this.logo,
|
||||
this.nombreMembres = 0,
|
||||
this.nombreAdministrateurs = 0,
|
||||
this.nombreEvenements,
|
||||
this.budgetAnnuel,
|
||||
this.devise = 'XOF',
|
||||
this.cotisationObligatoire = false,
|
||||
@@ -323,6 +328,7 @@ class OrganizationModel extends Equatable {
|
||||
String? logo,
|
||||
int? nombreMembres,
|
||||
int? nombreAdministrateurs,
|
||||
int? nombreEvenements,
|
||||
double? budgetAnnuel,
|
||||
String? devise,
|
||||
bool? cotisationObligatoire,
|
||||
@@ -357,6 +363,7 @@ class OrganizationModel extends Equatable {
|
||||
logo: logo ?? this.logo,
|
||||
nombreMembres: nombreMembres ?? this.nombreMembres,
|
||||
nombreAdministrateurs: nombreAdministrateurs ?? this.nombreAdministrateurs,
|
||||
nombreEvenements: nombreEvenements ?? this.nombreEvenements,
|
||||
budgetAnnuel: budgetAnnuel ?? this.budgetAnnuel,
|
||||
devise: devise ?? this.devise,
|
||||
cotisationObligatoire: cotisationObligatoire ?? this.cotisationObligatoire,
|
||||
@@ -414,6 +421,7 @@ class OrganizationModel extends Equatable {
|
||||
logo,
|
||||
nombreMembres,
|
||||
nombreAdministrateurs,
|
||||
nombreEvenements,
|
||||
budgetAnnuel,
|
||||
devise,
|
||||
cotisationObligatoire,
|
||||
|
||||
@@ -34,6 +34,7 @@ OrganizationModel _$OrganizationModelFromJson(Map<String, dynamic> json) =>
|
||||
nombreMembres: (json['nombreMembres'] as num?)?.toInt() ?? 0,
|
||||
nombreAdministrateurs:
|
||||
(json['nombreAdministrateurs'] as num?)?.toInt() ?? 0,
|
||||
nombreEvenements: (json['nombreEvenements'] as num?)?.toInt(),
|
||||
budgetAnnuel: (json['budgetAnnuel'] as num?)?.toDouble(),
|
||||
devise: json['devise'] as String? ?? 'XOF',
|
||||
cotisationObligatoire: json['cotisationObligatoire'] as bool? ?? false,
|
||||
@@ -75,6 +76,7 @@ Map<String, dynamic> _$OrganizationModelToJson(OrganizationModel instance) =>
|
||||
'logo': instance.logo,
|
||||
'nombreMembres': instance.nombreMembres,
|
||||
'nombreAdministrateurs': instance.nombreAdministrateurs,
|
||||
'nombreEvenements': instance.nombreEvenements,
|
||||
'budgetAnnuel': instance.budgetAnnuel,
|
||||
'devise': instance.devise,
|
||||
'cotisationObligatoire': instance.cotisationObligatoire,
|
||||
|
||||
@@ -1,59 +1,20 @@
|
||||
/// Repository pour la gestion des organisations
|
||||
/// Interface avec l'API backend OrganizationResource
|
||||
library organization_repository;
|
||||
library organization_repository_impl;
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:unionflow_mobile_apps/core/network/api_client.dart';
|
||||
import '../../domain/repositories/organization_repository.dart';
|
||||
import '../models/organization_model.dart';
|
||||
|
||||
/// Interface du repository des organisations
|
||||
abstract class OrganizationRepository {
|
||||
/// Récupère la liste des organisations avec pagination
|
||||
Future<List<OrganizationModel>> getOrganizations({
|
||||
int page = 0,
|
||||
int size = 20,
|
||||
String? recherche,
|
||||
});
|
||||
|
||||
/// Récupère une organisation par son ID
|
||||
Future<OrganizationModel?> getOrganizationById(String id);
|
||||
|
||||
/// Crée une nouvelle organisation
|
||||
Future<OrganizationModel> createOrganization(OrganizationModel organization);
|
||||
|
||||
/// Met à jour une organisation
|
||||
Future<OrganizationModel> updateOrganization(String id, OrganizationModel organization);
|
||||
|
||||
/// Supprime une organisation
|
||||
Future<void> deleteOrganization(String id);
|
||||
|
||||
/// Active une organisation
|
||||
Future<OrganizationModel> activateOrganization(String id);
|
||||
|
||||
/// Suspend une organisation
|
||||
Future<OrganizationModel> suspendOrganization(String id);
|
||||
|
||||
/// Recherche avancée d'organisations
|
||||
Future<List<OrganizationModel>> searchOrganizations({
|
||||
String? nom,
|
||||
TypeOrganization? type,
|
||||
StatutOrganization? statut,
|
||||
String? ville,
|
||||
String? region,
|
||||
String? pays,
|
||||
int page = 0,
|
||||
int size = 20,
|
||||
});
|
||||
|
||||
/// Récupère les statistiques des organisations
|
||||
Future<Map<String, dynamic>> getOrganizationsStats();
|
||||
}
|
||||
|
||||
/// Implémentation du repository des organisations
|
||||
class OrganizationRepositoryImpl implements OrganizationRepository {
|
||||
final Dio _dio;
|
||||
@LazySingleton(as: IOrganizationRepository)
|
||||
class OrganizationRepositoryImpl implements IOrganizationRepository {
|
||||
final ApiClient _apiClient;
|
||||
static const String _baseUrl = '/api/organisations';
|
||||
|
||||
OrganizationRepositoryImpl(this._dio);
|
||||
OrganizationRepositoryImpl(this._apiClient);
|
||||
|
||||
@override
|
||||
Future<List<OrganizationModel>> getOrganizations({
|
||||
@@ -71,12 +32,13 @@ class OrganizationRepositoryImpl implements OrganizationRepository {
|
||||
queryParams['recherche'] = recherche;
|
||||
}
|
||||
|
||||
final response = await _dio.get(
|
||||
final response = await _apiClient.get(
|
||||
_baseUrl,
|
||||
queryParameters: queryParams,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
// Le backend retourne directement une liste [...]
|
||||
final List<dynamic> data = response.data as List<dynamic>;
|
||||
return data
|
||||
.map((json) => OrganizationModel.fromJson(json as Map<String, dynamic>))
|
||||
@@ -91,10 +53,29 @@ class OrganizationRepositoryImpl implements OrganizationRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<OrganizationModel>> getMesOrganisations() async {
|
||||
try {
|
||||
const String path = '$_baseUrl/mes';
|
||||
final response = await _apiClient.get(path);
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> data = response.data as List<dynamic>;
|
||||
return data
|
||||
.map((json) => OrganizationModel.fromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
throw Exception('Erreur lors de la récupération de mes organisations: ${response.statusCode}');
|
||||
} on DioException catch (e) {
|
||||
throw Exception('Erreur réseau lors de la récupération de mes organisations: ${e.message}');
|
||||
} catch (e) {
|
||||
throw Exception('Erreur inattendue lors de la récupération de mes organisations: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<OrganizationModel?> getOrganizationById(String id) async {
|
||||
try {
|
||||
final response = await _dio.get('$_baseUrl/$id');
|
||||
final response = await _apiClient.get('$_baseUrl/$id');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return OrganizationModel.fromJson(response.data as Map<String, dynamic>);
|
||||
@@ -116,7 +97,7 @@ class OrganizationRepositoryImpl implements OrganizationRepository {
|
||||
@override
|
||||
Future<OrganizationModel> createOrganization(OrganizationModel organization) async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
final response = await _apiClient.post(
|
||||
_baseUrl,
|
||||
data: organization.toJson(),
|
||||
);
|
||||
@@ -144,7 +125,7 @@ class OrganizationRepositoryImpl implements OrganizationRepository {
|
||||
@override
|
||||
Future<OrganizationModel> updateOrganization(String id, OrganizationModel organization) async {
|
||||
try {
|
||||
final response = await _dio.put(
|
||||
final response = await _apiClient.put(
|
||||
'$_baseUrl/$id',
|
||||
data: organization.toJson(),
|
||||
);
|
||||
@@ -172,7 +153,7 @@ class OrganizationRepositoryImpl implements OrganizationRepository {
|
||||
@override
|
||||
Future<void> deleteOrganization(String id) async {
|
||||
try {
|
||||
final response = await _dio.delete('$_baseUrl/$id');
|
||||
final response = await _apiClient.delete('$_baseUrl/$id');
|
||||
|
||||
if (response.statusCode != 200 && response.statusCode != 204) {
|
||||
throw Exception('Erreur lors de la suppression de l\'organisation: ${response.statusCode}');
|
||||
@@ -195,7 +176,7 @@ class OrganizationRepositoryImpl implements OrganizationRepository {
|
||||
@override
|
||||
Future<OrganizationModel> activateOrganization(String id) async {
|
||||
try {
|
||||
final response = await _dio.post('$_baseUrl/$id/activer');
|
||||
final response = await _apiClient.post('$_baseUrl/$id/activer');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return OrganizationModel.fromJson(response.data as Map<String, dynamic>);
|
||||
@@ -215,7 +196,7 @@ class OrganizationRepositoryImpl implements OrganizationRepository {
|
||||
@override
|
||||
Future<OrganizationModel> suspendOrganization(String id) async {
|
||||
try {
|
||||
final response = await _dio.post('$_baseUrl/$id/suspendre');
|
||||
final response = await _apiClient.post('$_baseUrl/$id/suspendre');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return OrganizationModel.fromJson(response.data as Map<String, dynamic>);
|
||||
@@ -256,12 +237,13 @@ class OrganizationRepositoryImpl implements OrganizationRepository {
|
||||
if (region?.isNotEmpty == true) queryParams['region'] = region;
|
||||
if (pays?.isNotEmpty == true) queryParams['pays'] = pays;
|
||||
|
||||
final response = await _dio.get(
|
||||
final response = await _apiClient.get(
|
||||
'$_baseUrl/recherche',
|
||||
queryParameters: queryParams,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
// Le backend retourne directement une liste [...]
|
||||
final List<dynamic> data = response.data as List<dynamic>;
|
||||
return data
|
||||
.map((json) => OrganizationModel.fromJson(json as Map<String, dynamic>))
|
||||
@@ -276,10 +258,62 @@ class OrganizationRepositoryImpl implements OrganizationRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Map<String, dynamic>>> getOrganizationMembers(String organizationId) async {
|
||||
try {
|
||||
final response = await _apiClient.get('$_baseUrl/$organizationId/membres');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> data = response.data as List<dynamic>;
|
||||
return data.map((e) => Map<String, dynamic>.from(e as Map)).toList();
|
||||
} else {
|
||||
throw Exception('Erreur lors de la récupération des membres: ${response.statusCode}');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
if (e.response?.statusCode == 404) {
|
||||
throw Exception('Organisation non trouvée');
|
||||
}
|
||||
throw Exception('Erreur réseau lors de la récupération des membres: ${e.message}');
|
||||
} catch (e) {
|
||||
throw Exception('Erreur inattendue lors de la récupération des membres: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<OrganizationModel> updateOrganizationConfig(
|
||||
String id,
|
||||
Map<String, dynamic> config,
|
||||
) async {
|
||||
try {
|
||||
final response = await _apiClient.put(
|
||||
'$_baseUrl/$id/configuration',
|
||||
data: config,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return OrganizationModel.fromJson(response.data as Map<String, dynamic>);
|
||||
} else {
|
||||
throw Exception('Erreur lors de la mise à jour de la configuration: ${response.statusCode}');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
if (e.response?.statusCode == 404) {
|
||||
throw Exception('Organisation non trouvée');
|
||||
} else if (e.response?.statusCode == 400) {
|
||||
final errorData = e.response?.data;
|
||||
if (errorData is Map<String, dynamic> && errorData.containsKey('error')) {
|
||||
throw Exception('Configuration invalide: ${errorData['error']}');
|
||||
}
|
||||
}
|
||||
throw Exception('Erreur réseau lors de la mise à jour de la configuration: ${e.message}');
|
||||
} catch (e) {
|
||||
throw Exception('Erreur inattendue lors de la mise à jour de la configuration: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> getOrganizationsStats() async {
|
||||
try {
|
||||
final response = await _dio.get('$_baseUrl/statistiques');
|
||||
final response = await _apiClient.get('$_baseUrl/statistiques');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return response.data as Map<String, dynamic>;
|
||||
@@ -292,5 +326,4 @@ class OrganizationRepositoryImpl implements OrganizationRepository {
|
||||
throw Exception('Erreur inattendue lors de la récupération des statistiques: $e');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
/// Service pour la gestion des organisations
|
||||
/// Couche de logique métier entre le repository et l'interface utilisateur
|
||||
/// Helpers pour tri, filtrage local et recherche
|
||||
library organization_service;
|
||||
|
||||
import 'package:injectable/injectable.dart';
|
||||
import '../models/organization_model.dart';
|
||||
import '../repositories/organization_repository.dart';
|
||||
import '../../domain/repositories/organization_repository.dart';
|
||||
|
||||
/// Service de gestion des organisations
|
||||
/// Service de gestion des organisations (helpers uniquement)
|
||||
@injectable
|
||||
class OrganizationService {
|
||||
final OrganizationRepository _repository;
|
||||
final IOrganizationRepository _repository;
|
||||
|
||||
OrganizationService(this._repository);
|
||||
|
||||
@@ -28,6 +30,15 @@ class OrganizationService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Récupère les organisations du membre connecté (pour admin d'organisation)
|
||||
Future<List<OrganizationModel>> getMesOrganisations() async {
|
||||
try {
|
||||
return await _repository.getMesOrganisations();
|
||||
} catch (e) {
|
||||
throw Exception('Erreur lors de la récupération de mes organisations: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Récupère une organisation par son ID
|
||||
Future<OrganizationModel?> getOrganizationById(String id) async {
|
||||
if (id.isEmpty) {
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
/// Configuration de l'injection de dépendances pour le module Organizations
|
||||
library organizations_di;
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import '../data/repositories/organization_repository.dart';
|
||||
import '../data/services/organization_service.dart';
|
||||
import '../bloc/organizations_bloc.dart';
|
||||
|
||||
/// Configuration des dépendances du module Organizations
|
||||
class OrganizationsDI {
|
||||
static final GetIt _getIt = GetIt.instance;
|
||||
|
||||
/// Enregistre toutes les dépendances du module
|
||||
static void registerDependencies() {
|
||||
// Repository
|
||||
_getIt.registerLazySingleton<OrganizationRepository>(
|
||||
() => OrganizationRepositoryImpl(_getIt<Dio>()),
|
||||
);
|
||||
|
||||
// Service
|
||||
_getIt.registerLazySingleton<OrganizationService>(
|
||||
() => OrganizationService(_getIt<OrganizationRepository>()),
|
||||
);
|
||||
|
||||
// BLoC - Factory pour permettre plusieurs instances
|
||||
_getIt.registerFactory<OrganizationsBloc>(
|
||||
() => OrganizationsBloc(_getIt<OrganizationService>()),
|
||||
);
|
||||
}
|
||||
|
||||
/// Nettoie les dépendances du module
|
||||
static void unregisterDependencies() {
|
||||
if (_getIt.isRegistered<OrganizationsBloc>()) {
|
||||
_getIt.unregister<OrganizationsBloc>();
|
||||
}
|
||||
if (_getIt.isRegistered<OrganizationService>()) {
|
||||
_getIt.unregister<OrganizationService>();
|
||||
}
|
||||
if (_getIt.isRegistered<OrganizationRepository>()) {
|
||||
_getIt.unregister<OrganizationRepository>();
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtient une instance du BLoC
|
||||
static OrganizationsBloc getOrganizationsBloc() {
|
||||
return _getIt<OrganizationsBloc>();
|
||||
}
|
||||
|
||||
/// Obtient une instance du service
|
||||
static OrganizationService getOrganizationService() {
|
||||
return _getIt<OrganizationService>();
|
||||
}
|
||||
|
||||
/// Obtient une instance du repository
|
||||
static OrganizationRepository getOrganizationRepository() {
|
||||
return _getIt<OrganizationRepository>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/// Interface du repository des organisations (Clean Architecture - Domain Layer)
|
||||
library organization_repository;
|
||||
|
||||
import '../../data/models/organization_model.dart';
|
||||
|
||||
/// Interface du repository pour la gestion des organisations
|
||||
/// Contrat défini dans la couche Domain, implémenté dans la couche Data
|
||||
abstract class IOrganizationRepository {
|
||||
/// Récupère la liste des organisations avec pagination
|
||||
Future<List<OrganizationModel>> getOrganizations({
|
||||
int page = 0,
|
||||
int size = 20,
|
||||
String? recherche,
|
||||
});
|
||||
|
||||
/// Récupère les organisations du membre connecté (pour OrgAdmin)
|
||||
Future<List<OrganizationModel>> getMesOrganisations();
|
||||
|
||||
/// Récupère une organisation par son ID
|
||||
Future<OrganizationModel?> getOrganizationById(String id);
|
||||
|
||||
/// Crée une nouvelle organisation (SuperAdmin)
|
||||
Future<OrganizationModel> createOrganization(OrganizationModel organization);
|
||||
|
||||
/// Met à jour une organisation (OrgAdmin)
|
||||
Future<OrganizationModel> updateOrganization(String id, OrganizationModel organization);
|
||||
|
||||
/// Supprime une organisation (SuperAdmin)
|
||||
Future<void> deleteOrganization(String id);
|
||||
|
||||
/// Active une organisation
|
||||
Future<OrganizationModel> activateOrganization(String id);
|
||||
|
||||
/// Suspend une organisation
|
||||
Future<OrganizationModel> suspendOrganization(String id);
|
||||
|
||||
/// Recherche avancée d'organisations
|
||||
Future<List<OrganizationModel>> searchOrganizations({
|
||||
String? nom,
|
||||
TypeOrganization? type,
|
||||
StatutOrganization? statut,
|
||||
String? ville,
|
||||
String? region,
|
||||
String? pays,
|
||||
int page = 0,
|
||||
int size = 20,
|
||||
});
|
||||
|
||||
/// Récupère les membres d'une organisation (GET /api/organisations/{id}/membres)
|
||||
Future<List<Map<String, dynamic>>> getOrganizationMembers(String organizationId);
|
||||
|
||||
/// Met à jour la configuration d'une organisation (PUT /api/organisations/{id}/configuration)
|
||||
/// Configuration: logo, couleurs, préférences, modules activés, etc.
|
||||
Future<OrganizationModel> updateOrganizationConfig(
|
||||
String id,
|
||||
Map<String, dynamic> config,
|
||||
);
|
||||
|
||||
/// Récupère les statistiques des organisations
|
||||
Future<Map<String, dynamic>> getOrganizationsStats();
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/// Use Case: Créer une nouvelle organisation
|
||||
library create_organization;
|
||||
|
||||
import 'package:injectable/injectable.dart';
|
||||
import '../../data/models/organization_model.dart';
|
||||
import '../repositories/organization_repository.dart';
|
||||
|
||||
/// Crée une nouvelle organisation (SuperAdmin uniquement)
|
||||
@injectable
|
||||
class CreateOrganization {
|
||||
final IOrganizationRepository _repository;
|
||||
|
||||
CreateOrganization(this._repository);
|
||||
|
||||
/// Exécute le use case
|
||||
/// [organization] : Modèle de l'organisation à créer
|
||||
/// Retourne l'organisation créée avec son ID
|
||||
/// Lève une exception en cas d'erreur (données invalides, conflit)
|
||||
Future<OrganizationModel> call(OrganizationModel organization) async {
|
||||
return _repository.createOrganization(organization);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/// Use Case: Supprimer une organisation
|
||||
library delete_organization;
|
||||
|
||||
import 'package:injectable/injectable.dart';
|
||||
import '../repositories/organization_repository.dart';
|
||||
|
||||
/// Supprime une organisation (SuperAdmin uniquement)
|
||||
@injectable
|
||||
class DeleteOrganization {
|
||||
final IOrganizationRepository _repository;
|
||||
|
||||
DeleteOrganization(this._repository);
|
||||
|
||||
/// Exécute le use case
|
||||
/// [id] : Identifiant de l'organisation à supprimer
|
||||
/// Lève une exception si organisation non trouvée ou suppression impossible
|
||||
/// Note: Peut être un soft delete selon l'implémentation backend
|
||||
Future<void> call(String id) async {
|
||||
return _repository.deleteOrganization(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/// Use Case: Récupérer une organisation par ID
|
||||
library get_organization_by_id;
|
||||
|
||||
import 'package:injectable/injectable.dart';
|
||||
import '../../data/models/organization_model.dart';
|
||||
import '../repositories/organization_repository.dart';
|
||||
|
||||
/// Récupère le détail d'une organisation par son identifiant
|
||||
@injectable
|
||||
class GetOrganizationById {
|
||||
final IOrganizationRepository _repository;
|
||||
|
||||
GetOrganizationById(this._repository);
|
||||
|
||||
/// Exécute le use case
|
||||
/// [id] : Identifiant de l'organisation
|
||||
/// Retourne l'organisation ou null si non trouvée
|
||||
Future<OrganizationModel?> call(String id) async {
|
||||
return _repository.getOrganizationById(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/// Use Case: Récupérer les membres d'une organisation
|
||||
library get_organization_members;
|
||||
|
||||
import 'package:injectable/injectable.dart';
|
||||
import '../repositories/organization_repository.dart';
|
||||
|
||||
/// Récupère la liste des membres d'une organisation
|
||||
@injectable
|
||||
class GetOrganizationMembers {
|
||||
final IOrganizationRepository _repository;
|
||||
|
||||
GetOrganizationMembers(this._repository);
|
||||
|
||||
/// Exécute le use case
|
||||
/// [organizationId] : Identifiant de l'organisation
|
||||
/// Retourne une liste de membres (Map avec id, nom, prenom, role, etc.)
|
||||
/// Endpoint: GET /api/organisations/{id}/membres
|
||||
Future<List<Map<String, dynamic>>> call(String organizationId) async {
|
||||
return _repository.getOrganizationMembers(organizationId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/// Use Case: Récupérer la liste des organisations
|
||||
library get_organizations;
|
||||
|
||||
import 'package:injectable/injectable.dart';
|
||||
import '../../data/models/organization_model.dart';
|
||||
import '../repositories/organization_repository.dart';
|
||||
|
||||
/// Récupère la liste paginée des organisations
|
||||
@injectable
|
||||
class GetOrganizations {
|
||||
final IOrganizationRepository _repository;
|
||||
|
||||
GetOrganizations(this._repository);
|
||||
|
||||
/// Exécute le use case
|
||||
/// [page] : Numéro de page (défaut: 0)
|
||||
/// [size] : Taille de la page (défaut: 20)
|
||||
/// [recherche] : Terme de recherche optionnel
|
||||
/// Retourne une liste d'organisations
|
||||
Future<List<OrganizationModel>> call({
|
||||
int page = 0,
|
||||
int size = 20,
|
||||
String? recherche,
|
||||
}) async {
|
||||
return _repository.getOrganizations(
|
||||
page: page,
|
||||
size: size,
|
||||
recherche: recherche,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/// Use Case: Mettre à jour une organisation
|
||||
library update_organization;
|
||||
|
||||
import 'package:injectable/injectable.dart';
|
||||
import '../../data/models/organization_model.dart';
|
||||
import '../repositories/organization_repository.dart';
|
||||
|
||||
/// Met à jour une organisation existante (OrgAdmin ou SuperAdmin)
|
||||
@injectable
|
||||
class UpdateOrganization {
|
||||
final IOrganizationRepository _repository;
|
||||
|
||||
UpdateOrganization(this._repository);
|
||||
|
||||
/// Exécute le use case
|
||||
/// [id] : Identifiant de l'organisation
|
||||
/// [organization] : Modèle avec les données mises à jour
|
||||
/// Retourne l'organisation mise à jour
|
||||
/// Lève une exception si organisation non trouvée ou données invalides
|
||||
Future<OrganizationModel> call(String id, OrganizationModel organization) async {
|
||||
return _repository.updateOrganization(id, organization);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/// Use Case: Mettre à jour la configuration d'une organisation
|
||||
library update_organization_config;
|
||||
|
||||
import 'package:injectable/injectable.dart';
|
||||
import '../../data/models/organization_model.dart';
|
||||
import '../repositories/organization_repository.dart';
|
||||
|
||||
/// Met à jour la configuration spécifique d'une organisation
|
||||
/// (logo, couleurs, modules activés, préférences, etc.)
|
||||
@injectable
|
||||
class UpdateOrganizationConfig {
|
||||
final IOrganizationRepository _repository;
|
||||
|
||||
UpdateOrganizationConfig(this._repository);
|
||||
|
||||
/// Exécute le use case
|
||||
/// [id] : Identifiant de l'organisation
|
||||
/// [config] : Configuration à mettre à jour
|
||||
/// Exemple: { "logo": "url", "couleurPrimaire": "#FF5733", "modulesActifs": ["finance", "events"] }
|
||||
/// Retourne l'organisation avec la configuration mise à jour
|
||||
/// Endpoint: PUT /api/organisations/{id}/configuration
|
||||
Future<OrganizationModel> call(String id, Map<String, dynamic> config) async {
|
||||
return _repository.updateOrganizationConfig(id, config);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import '../../data/models/organization_model.dart';
|
||||
import '../../bloc/organizations_bloc.dart';
|
||||
import '../../bloc/organizations_event.dart';
|
||||
import '../../bloc/organizations_state.dart';
|
||||
import 'edit_organization_page.dart';
|
||||
|
||||
/// Page de détail d'une organisation avec design system cohérent
|
||||
class OrganizationDetailPage extends StatefulWidget {
|
||||
@@ -387,7 +388,7 @@ class _OrganizationDetailPageState extends State<OrganizationDetailPage> {
|
||||
child: _buildStatItem(
|
||||
icon: Icons.event,
|
||||
label: 'Événements',
|
||||
value: '0', // Nécessite endpoint stats par organisation
|
||||
value: (organization.nombreEvenements ?? 0).toString(),
|
||||
color: const Color(0xFF10B981),
|
||||
),
|
||||
),
|
||||
@@ -590,7 +591,7 @@ class _OrganizationDetailPageState extends State<OrganizationDetailPage> {
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => _showEditDialog(),
|
||||
onPressed: () => _showEditDialog(organization),
|
||||
icon: const Icon(Icons.edit),
|
||||
label: const Text('Modifier'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
@@ -725,11 +726,36 @@ class _OrganizationDetailPageState extends State<OrganizationDetailPage> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Affiche le dialog d'édition
|
||||
void _showEditDialog() {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Édition - À implémenter')),
|
||||
);
|
||||
/// Ouvre la page d'édition ou le dialog selon le contexte
|
||||
void _showEditDialog([OrganizationModel? organization]) {
|
||||
if (organization == null) {
|
||||
final state = context.read<OrganizationsBloc>().state;
|
||||
if (state is OrganizationLoaded) {
|
||||
organization = state.organization;
|
||||
}
|
||||
}
|
||||
if (organization == null || !context.mounted) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Chargement de l\'organisation en cours...')),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
final org = organization;
|
||||
final bloc = context.read<OrganizationsBloc>();
|
||||
Navigator.of(context).push<void>(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => BlocProvider.value(
|
||||
value: bloc,
|
||||
child: EditOrganizationPage(organization: org),
|
||||
),
|
||||
),
|
||||
).then((_) {
|
||||
if (context.mounted) {
|
||||
bloc.add(LoadOrganizationById(widget.organizationId));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Affiche la confirmation de suppression
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,10 +3,15 @@ library organisations_page_wrapper;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../di/organizations_di.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import '../../../authentication/data/models/user_role.dart';
|
||||
import '../../../authentication/presentation/bloc/auth_bloc.dart';
|
||||
import '../../bloc/organizations_bloc.dart';
|
||||
import '../../bloc/organizations_event.dart';
|
||||
import 'organizations_page.dart';
|
||||
|
||||
final _getIt = GetIt.instance;
|
||||
|
||||
/// Wrapper qui fournit le BLoC pour la page des organisations
|
||||
class OrganizationsPageWrapper extends StatelessWidget {
|
||||
const OrganizationsPageWrapper({super.key});
|
||||
@@ -14,7 +19,15 @@ class OrganizationsPageWrapper extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<OrganizationsBloc>(
|
||||
create: (context) => OrganizationsDI.getOrganizationsBloc(),
|
||||
create: (context) {
|
||||
final bloc = _getIt<OrganizationsBloc>();
|
||||
// Admin d'organisation : ne charger que son/ses organisation(s)
|
||||
final authState = context.read<AuthBloc>().state;
|
||||
final useMesOnly = authState is AuthAuthenticated &&
|
||||
authState.effectiveRole == UserRole.orgAdmin;
|
||||
bloc.add(LoadOrganizations(useMesOnly: useMesOnly));
|
||||
return bloc;
|
||||
},
|
||||
child: const OrganizationsPage(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -209,7 +209,7 @@ class _CreateOrganizationDialogState extends State<CreateOrganizationDialog> {
|
||||
labelText: 'Site web',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.language),
|
||||
hintText: 'https://www.exemple.com',
|
||||
hintText: 'https://',
|
||||
),
|
||||
keyboardType: TextInputType.url,
|
||||
),
|
||||
|
||||
@@ -308,7 +308,7 @@ class _EditOrganizationDialogState extends State<EditOrganizationDialog> {
|
||||
labelText: 'Site web',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.language),
|
||||
hintText: 'https://www.exemple.com',
|
||||
hintText: 'https://',
|
||||
),
|
||||
keyboardType: TextInputType.url,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user