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:
@@ -0,0 +1,70 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
import 'package:unionflow_mobile_apps/core/network/api_client.dart';
|
||||
import '../../presentation/bloc/notification_state.dart';
|
||||
|
||||
/// Repository pour l'onglet Notifications (flux DRY).
|
||||
/// Utilise ApiClient et mappe vers NotificationItem.
|
||||
@lazySingleton
|
||||
class NotificationFeedRepository {
|
||||
final ApiClient _apiClient;
|
||||
|
||||
NotificationFeedRepository(this._apiClient);
|
||||
|
||||
/// Récupère le membre connecté (GET /api/membres/me) pour obtenir l'id.
|
||||
Future<String> _getMembreId() async {
|
||||
final response = await _apiClient.get('/api/membres/me');
|
||||
final id = response.data['id']?.toString();
|
||||
if (id == null || id.isEmpty) {
|
||||
throw Exception('Membre connecté introuvable');
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
/// Liste des notifications du membre connecté (GET /api/notifications/membre/{membreId}).
|
||||
Future<List<NotificationItem>> getNotifications() async {
|
||||
try {
|
||||
final membreId = await _getMembreId();
|
||||
final response = await _apiClient.get('/api/notifications/membre/$membreId');
|
||||
final List<dynamic> data = response.data is List ? response.data as List : [];
|
||||
return data
|
||||
.map((json) => _itemFromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
} on DioException catch (e) {
|
||||
if (e.response?.statusCode == 404) return [];
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Marque une notification comme lue (POST /api/notifications/{id}/marquer-lue).
|
||||
Future<void> markAsRead(String id) async {
|
||||
await _apiClient.post('/api/notifications/$id/marquer-lue');
|
||||
}
|
||||
|
||||
static NotificationItem _itemFromJson(Map<String, dynamic> json) {
|
||||
final id = json['id']?.toString() ?? '';
|
||||
final sujet = json['sujet']?.toString() ?? 'Notification';
|
||||
final corps = json['corps']?.toString() ?? '';
|
||||
final dateEnvoi = json['dateEnvoi']?.toString();
|
||||
final dateLecture = json['dateLecture']?.toString();
|
||||
final date = dateEnvoi != null
|
||||
? DateTime.tryParse(dateEnvoi) ?? DateTime.now()
|
||||
: DateTime.now();
|
||||
final isRead = dateLecture != null || json['statut']?.toString() == 'LUE';
|
||||
final type = (json['typeNotification']?.toString() ?? 'SYSTEME').toLowerCase();
|
||||
final category = type.contains('cotisation') || type.contains('finance')
|
||||
? 'finance'
|
||||
: type.contains('event') || type.contains('evenement')
|
||||
? 'event'
|
||||
: 'system';
|
||||
return NotificationItem(
|
||||
id: id,
|
||||
title: sujet,
|
||||
body: corps,
|
||||
date: date,
|
||||
isRead: isRead,
|
||||
category: category,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,15 @@
|
||||
library notification_repository;
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import '../../../../core/network/api_client.dart';
|
||||
import '../models/notification_model.dart';
|
||||
|
||||
/// Interface du repository des notifications
|
||||
abstract class NotificationRepository {
|
||||
/// Notifications du membre connecté (GET /api/notifications/me)
|
||||
Future<List<NotificationModel>> getMesNotifications();
|
||||
Future<List<NotificationModel>> getMesNonLues();
|
||||
Future<List<NotificationModel>> getNotificationsByMembre(String membreId);
|
||||
Future<List<NotificationModel>> getNonLuesByMembre(String membreId);
|
||||
Future<NotificationModel?> getNotificationById(String id);
|
||||
@@ -12,16 +17,49 @@ abstract class NotificationRepository {
|
||||
}
|
||||
|
||||
/// Implémentation via /api/notifications
|
||||
@LazySingleton(as: NotificationRepository)
|
||||
class NotificationRepositoryImpl implements NotificationRepository {
|
||||
final Dio _dio;
|
||||
final ApiClient _apiClient;
|
||||
static const String _baseUrl = '/api/notifications';
|
||||
|
||||
NotificationRepositoryImpl(this._dio);
|
||||
NotificationRepositoryImpl(this._apiClient);
|
||||
|
||||
@override
|
||||
Future<List<NotificationModel>> getMesNotifications() async {
|
||||
try {
|
||||
final response = await _apiClient.get('$_baseUrl/me');
|
||||
if (response.statusCode == 200) {
|
||||
final data = response.data;
|
||||
final list = data is List ? data : (data['content'] as List? ?? []);
|
||||
return list.map((e) => NotificationModel.fromJson(e as Map<String, dynamic>)).toList();
|
||||
}
|
||||
return [];
|
||||
} on DioException catch (e) {
|
||||
if (e.response?.statusCode == 404) return [];
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<NotificationModel>> getMesNonLues() async {
|
||||
try {
|
||||
final response = await _apiClient.get('$_baseUrl/me/non-lues');
|
||||
if (response.statusCode == 200) {
|
||||
final data = response.data;
|
||||
final list = data is List ? data : (data['content'] as List? ?? []);
|
||||
return list.map((e) => NotificationModel.fromJson(e as Map<String, dynamic>)).toList();
|
||||
}
|
||||
return [];
|
||||
} on DioException catch (e) {
|
||||
if (e.response?.statusCode == 404) return [];
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<NotificationModel>> getNotificationsByMembre(String membreId) async {
|
||||
try {
|
||||
final response = await _dio.get('$_baseUrl/membre/$membreId');
|
||||
final response = await _apiClient.get('$_baseUrl/membre/$membreId');
|
||||
if (response.statusCode == 200) {
|
||||
final data = response.data;
|
||||
final list = data is List ? data : (data['content'] as List? ?? []);
|
||||
@@ -39,7 +77,7 @@ class NotificationRepositoryImpl implements NotificationRepository {
|
||||
@override
|
||||
Future<List<NotificationModel>> getNonLuesByMembre(String membreId) async {
|
||||
try {
|
||||
final response = await _dio.get('$_baseUrl/membre/$membreId/non-lues');
|
||||
final response = await _apiClient.get('$_baseUrl/membre/$membreId/non-lues');
|
||||
if (response.statusCode == 200) {
|
||||
final data = response.data;
|
||||
final list = data is List ? data : (data['content'] as List? ?? []);
|
||||
@@ -57,7 +95,7 @@ class NotificationRepositoryImpl implements NotificationRepository {
|
||||
@override
|
||||
Future<NotificationModel?> getNotificationById(String id) async {
|
||||
try {
|
||||
final response = await _dio.get('$_baseUrl/$id');
|
||||
final response = await _apiClient.get('$_baseUrl/$id');
|
||||
if (response.statusCode == 200) {
|
||||
return NotificationModel.fromJson(response.data as Map<String, dynamic>);
|
||||
}
|
||||
@@ -70,6 +108,6 @@ class NotificationRepositoryImpl implements NotificationRepository {
|
||||
|
||||
@override
|
||||
Future<void> marquerCommeLue(String id) async {
|
||||
await _dio.post('$_baseUrl/$id/marquer-lue');
|
||||
await _apiClient.post('$_baseUrl/$id/marquer-lue');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
library notifications_di;
|
||||
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import '../data/repositories/notification_repository.dart';
|
||||
import '../presentation/bloc/notifications_bloc.dart';
|
||||
|
||||
class NotificationsDI {
|
||||
static final GetIt _getIt = GetIt.instance;
|
||||
|
||||
static void register() {
|
||||
_getIt.registerLazySingleton<NotificationRepository>(
|
||||
() => NotificationRepositoryImpl(_getIt<Dio>()),
|
||||
);
|
||||
|
||||
_getIt.registerFactory<NotificationsBloc>(
|
||||
() => NotificationsBloc(_getIt<NotificationRepository>()),
|
||||
);
|
||||
}
|
||||
|
||||
static void unregister() {
|
||||
if (_getIt.isRegistered<NotificationsBloc>()) _getIt.unregister<NotificationsBloc>();
|
||||
if (_getIt.isRegistered<NotificationRepository>()) _getIt.unregister<NotificationRepository>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
import '../../../../core/utils/logger.dart';
|
||||
import '../../data/repositories/notification_feed_repository.dart';
|
||||
import 'notification_event.dart';
|
||||
import 'notification_state.dart';
|
||||
|
||||
@injectable
|
||||
class NotificationBloc extends Bloc<NotificationEvent, NotificationState> {
|
||||
final NotificationFeedRepository _repository;
|
||||
|
||||
NotificationBloc(this._repository) : super(NotificationInitial()) {
|
||||
on<LoadNotificationsRequested>(_onLoadNotificationsRequested);
|
||||
on<NotificationMarkedAsRead>(_onNotificationMarkedAsRead);
|
||||
}
|
||||
|
||||
Future<void> _onLoadNotificationsRequested(LoadNotificationsRequested event, Emitter<NotificationState> emit) async {
|
||||
emit(NotificationLoading());
|
||||
try {
|
||||
final items = await _repository.getNotifications();
|
||||
emit(NotificationLoaded(items: items));
|
||||
} catch (e, st) {
|
||||
AppLogger.error('NotificationBloc: chargement notifications échoué', error: e, stackTrace: st);
|
||||
emit(NotificationError('Erreur de chargement: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onNotificationMarkedAsRead(NotificationMarkedAsRead event, Emitter<NotificationState> emit) async {
|
||||
if (state is NotificationLoaded) {
|
||||
final currentState = state as NotificationLoaded;
|
||||
try {
|
||||
await _repository.markAsRead(event.id);
|
||||
final updatedItems = currentState.items.map((item) {
|
||||
if (item.id == event.id) {
|
||||
return NotificationItem(
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
body: item.body,
|
||||
date: item.date,
|
||||
isRead: true,
|
||||
category: item.category,
|
||||
);
|
||||
}
|
||||
return item;
|
||||
}).toList();
|
||||
emit(NotificationLoaded(items: updatedItems));
|
||||
} catch (e, st) {
|
||||
AppLogger.error('NotificationBloc: marquer comme lu échoué', error: e, stackTrace: st);
|
||||
emit(NotificationError('Impossible de marquer comme lu'));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
abstract class NotificationEvent extends Equatable {
|
||||
const NotificationEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class LoadNotificationsRequested extends NotificationEvent {}
|
||||
|
||||
class NotificationMarkedAsRead extends NotificationEvent {
|
||||
final String id;
|
||||
const NotificationMarkedAsRead(this.id);
|
||||
|
||||
@override
|
||||
List<Object> get props => [id];
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class NotificationItem extends Equatable {
|
||||
final String id;
|
||||
final String title;
|
||||
final String body;
|
||||
final DateTime date;
|
||||
final bool isRead;
|
||||
final String category; // 'finance', 'event', 'system'
|
||||
|
||||
const NotificationItem({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.body,
|
||||
required this.date,
|
||||
this.isRead = false,
|
||||
required this.category,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id, title, body, date, isRead, category];
|
||||
}
|
||||
|
||||
abstract class NotificationState extends Equatable {
|
||||
const NotificationState();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class NotificationInitial extends NotificationState {}
|
||||
|
||||
class NotificationLoading extends NotificationState {}
|
||||
|
||||
class NotificationLoaded extends NotificationState {
|
||||
final List<NotificationItem> items;
|
||||
|
||||
const NotificationLoaded({required this.items});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [items];
|
||||
}
|
||||
|
||||
class NotificationError extends NotificationState {
|
||||
final String message;
|
||||
const NotificationError(this.message);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
library notifications_bloc;
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import '../../../../core/utils/logger.dart';
|
||||
import '../../data/models/notification_model.dart';
|
||||
import '../../data/repositories/notification_repository.dart';
|
||||
|
||||
part 'notifications_event.dart';
|
||||
part 'notifications_state.dart';
|
||||
|
||||
@injectable
|
||||
class NotificationsBloc extends Bloc<NotificationsEvent, NotificationsState> {
|
||||
final NotificationRepository _repository;
|
||||
|
||||
@@ -24,7 +27,9 @@ class NotificationsBloc extends Bloc<NotificationsEvent, NotificationsState> {
|
||||
) async {
|
||||
try {
|
||||
emit(const NotificationsLoading());
|
||||
final notifications = await _repository.getNotificationsByMembre(event.membreId);
|
||||
final notifications = (event.membreId != null && event.membreId!.isNotEmpty)
|
||||
? await _repository.getNotificationsByMembre(event.membreId!)
|
||||
: await _repository.getMesNotifications();
|
||||
final nonLues = notifications.where((n) => !n.estLue).length;
|
||||
emit(NotificationsLoaded(notifications: notifications, nonLuesCount: nonLues));
|
||||
} on DioException catch (e) {
|
||||
@@ -64,8 +69,9 @@ class NotificationsBloc extends Bloc<NotificationsEvent, NotificationsState> {
|
||||
final nonLues = updated.where((n) => !n.estLue).length;
|
||||
emit(NotificationMarkedAsRead(notifications: updated, nonLuesCount: nonLues));
|
||||
}
|
||||
} catch (e) {
|
||||
// Echec silencieux : ne pas bloquer l'UI
|
||||
} catch (e, st) {
|
||||
AppLogger.error('NotificationsBloc: marquer comme lu échoué', error: e, stackTrace: st);
|
||||
emit(NotificationsError('Impossible de marquer comme lu'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,9 +8,10 @@ abstract class NotificationsEvent extends Equatable {
|
||||
}
|
||||
|
||||
class LoadNotifications extends NotificationsEvent {
|
||||
final String membreId;
|
||||
/// Si null ou vide, utilise GET /api/notifications/me (membre connecté).
|
||||
final String? membreId;
|
||||
final bool onlyUnread;
|
||||
const LoadNotifications({required this.membreId, this.onlyUnread = false});
|
||||
const LoadNotifications({this.membreId, this.onlyUnread = false});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [membreId, onlyUnread];
|
||||
@@ -25,8 +26,8 @@ class MarkNotificationAsRead extends NotificationsEvent {
|
||||
}
|
||||
|
||||
class RefreshNotifications extends NotificationsEvent {
|
||||
final String membreId;
|
||||
const RefreshNotifications(this.membreId);
|
||||
final String? membreId;
|
||||
const RefreshNotifications([this.membreId]);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [membreId];
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../authentication/presentation/bloc/auth_bloc.dart';
|
||||
import '../../../members/presentation/pages/members_page_wrapper.dart';
|
||||
import '../../../events/presentation/pages/events_page_wrapper.dart';
|
||||
import '../../../organizations/presentation/pages/organizations_page.dart';
|
||||
import '../bloc/notifications_bloc.dart';
|
||||
import '../../data/models/notification_model.dart';
|
||||
import '../../../../shared/design_system/unionflow_design_system.dart';
|
||||
import '../../../../shared/widgets/core_card.dart';
|
||||
import '../../../../shared/widgets/mini_avatar.dart';
|
||||
import '../../../../shared/widgets/info_badge.dart';
|
||||
import '../../../../shared/design_system/components/uf_app_bar.dart';
|
||||
import '../../../../shared/design_system/components/uf_buttons.dart';
|
||||
|
||||
/// Page Notifications - UnionFlow Mobile
|
||||
///
|
||||
@@ -41,7 +50,7 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
final authState = context.read<AuthBloc>().state;
|
||||
if (authState is AuthAuthenticated) {
|
||||
context.read<NotificationsBloc>().add(
|
||||
LoadNotifications(membreId: authState.user.id),
|
||||
const LoadNotifications(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -94,92 +103,65 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
/// Header harmonisé avec le design system
|
||||
Widget _buildHeader() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(12),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF6C5CE7), Color(0xFF5A4FCF)],
|
||||
gradient: LinearGradient(
|
||||
colors: [AppColors.brandGreen, AppColors.primaryGreen],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF6C5CE7).withOpacity(0.3),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
color: Color(0x1A000000),
|
||||
blurRadius: 10,
|
||||
offset: Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.notifications,
|
||||
Icons.notifications_none,
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Notifications',
|
||||
'NOTIFICATIONS',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
letterSpacing: 1.1,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Gérer vos notifications et préférences',
|
||||
'Restez connecté à votre réseau',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
fontSize: 11,
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: IconButton(
|
||||
onPressed: () => _markAllAsRead(),
|
||||
icon: const Icon(
|
||||
Icons.done_all,
|
||||
color: Colors.white,
|
||||
),
|
||||
tooltip: 'Tout marquer comme lu',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: IconButton(
|
||||
onPressed: () => _showNotificationSettings(),
|
||||
icon: const Icon(
|
||||
Icons.settings,
|
||||
color: Colors.white,
|
||||
),
|
||||
tooltip: 'Paramètres',
|
||||
),
|
||||
),
|
||||
],
|
||||
IconButton(
|
||||
onPressed: () => _markAllAsRead(),
|
||||
icon: const Icon(Icons.done_all, color: Colors.white, size: 20),
|
||||
tooltip: 'Tout marquer comme lu',
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -189,42 +171,24 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
/// Barre d'onglets
|
||||
Widget _buildTabBar() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 12),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
color: AppColors.lightSurface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
labelColor: const Color(0xFF6C5CE7),
|
||||
unselectedLabelColor: Colors.grey[600],
|
||||
indicatorColor: const Color(0xFF6C5CE7),
|
||||
indicatorWeight: 3,
|
||||
indicatorSize: TabBarIndicatorSize.tab,
|
||||
labelStyle: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
unselectedLabelStyle: const TextStyle(
|
||||
fontWeight: FontWeight.normal,
|
||||
fontSize: 14,
|
||||
labelColor: AppColors.primaryGreen,
|
||||
unselectedLabelColor: AppColors.textSecondaryLight,
|
||||
indicator: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: AppColors.primaryGreen.withOpacity(0.1),
|
||||
),
|
||||
labelStyle: AppTypography.actionText.copyWith(fontSize: 12),
|
||||
unselectedLabelStyle: AppTypography.bodyTextSmall.copyWith(fontSize: 12),
|
||||
tabs: const [
|
||||
Tab(
|
||||
icon: Icon(Icons.inbox),
|
||||
text: 'Notifications',
|
||||
),
|
||||
Tab(
|
||||
icon: Icon(Icons.tune),
|
||||
text: 'Préférences',
|
||||
),
|
||||
Tab(text: 'FLUX'),
|
||||
Tab(text: 'RÉGLAGES'),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -249,68 +213,49 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
|
||||
/// Section filtres
|
||||
Widget _buildFiltersSection() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 12),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.filter_list,
|
||||
color: Colors.grey[600],
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Filtres',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey[800],
|
||||
'FILTRER PAR :',
|
||||
style: AppTypography.subtitleSmall.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.1,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Switch(
|
||||
value: _showOnlyUnread,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_showOnlyUnread = value;
|
||||
});
|
||||
},
|
||||
activeColor: const Color(0xFF6C5CE7),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Non lues uniquement',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
'NON LUES',
|
||||
style: AppTypography.badgeText.copyWith(fontSize: 9),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
SizedBox(
|
||||
height: 24,
|
||||
child: Switch(
|
||||
value: _showOnlyUnread,
|
||||
onChanged: (value) => setState(() => _showOnlyUnread = value),
|
||||
activeColor: AppColors.primaryGreen,
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: _filters.map((filter) {
|
||||
final isSelected = _selectedFilter == filter;
|
||||
return _buildFilterChip(filter, isSelected);
|
||||
}).toList(),
|
||||
const SizedBox(height: 8),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: _filters.map((filter) {
|
||||
final isSelected = _selectedFilter == filter;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: _buildFilterChip(filter, isSelected),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -320,28 +265,23 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
/// Chip de filtre
|
||||
Widget _buildFilterChip(String label, bool isSelected) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_selectedFilter = label;
|
||||
});
|
||||
},
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
onTap: () => setState(() => _selectedFilter = label),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? const Color(0xFF6C5CE7) : Colors.grey[50],
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: isSelected ? AppColors.primaryGreen.withOpacity(0.1) : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(
|
||||
color: isSelected ? const Color(0xFF6C5CE7) : Colors.grey[300]!,
|
||||
width: 1,
|
||||
color: isSelected ? AppColors.primaryGreen : AppColors.lightBorder,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: isSelected ? Colors.white : Colors.grey[700],
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 13,
|
||||
label.toUpperCase(),
|
||||
style: AppTypography.badgeText.copyWith(
|
||||
color: isSelected ? AppColors.primaryGreen : AppColors.textSecondaryLight,
|
||||
fontSize: 9,
|
||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -372,36 +312,22 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF6C5CE7).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.notifications_none,
|
||||
size: 48,
|
||||
color: Color(0xFF6C5CE7),
|
||||
),
|
||||
const Icon(
|
||||
Icons.notifications_none_outlined,
|
||||
size: 40,
|
||||
color: AppColors.textSecondaryLight,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Aucune notification',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1F2937),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'AUCUNE NOTIFICATION',
|
||||
style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_showOnlyUnread
|
||||
? 'Toutes vos notifications ont été lues'
|
||||
: 'Vous n\'avez aucune notification pour le moment',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
: 'Votre flux est à jour.',
|
||||
style: AppTypography.subtitleSmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
@@ -415,168 +341,97 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
final type = notification['type'] as String;
|
||||
final color = _getNotificationColor(type);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: isRead ? null : Border.all(
|
||||
color: const Color(0xFF6C5CE7).withOpacity(0.3),
|
||||
width: 2,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
return CoreCard(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.all(12),
|
||||
onTap: () => _handleNotificationTap(notification),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MiniAvatar(
|
||||
fallbackText: _getNotificationIconSource(type),
|
||||
size: 32,
|
||||
backgroundColor: isRead ? AppColors.lightSurface : color.withOpacity(0.1),
|
||||
iconColor: isRead ? AppColors.textSecondaryLight : color,
|
||||
isIcon: true,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
notification['title'].toString().toUpperCase(),
|
||||
style: AppTypography.actionText.copyWith(
|
||||
fontSize: 11,
|
||||
color: isRead ? AppColors.textSecondaryLight : AppColors.textPrimaryLight,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
notification['time'],
|
||||
style: AppTypography.subtitleSmall.copyWith(fontSize: 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
notification['message'],
|
||||
style: AppTypography.bodyTextSmall.copyWith(
|
||||
color: isRead ? AppColors.textSecondaryLight : AppColors.textPrimaryLight,
|
||||
fontWeight: isRead ? FontWeight.normal : FontWeight.w500,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (!isRead) ...[
|
||||
const SizedBox(height: 4),
|
||||
InfoBadge(
|
||||
text: 'NOUVEAU',
|
||||
backgroundColor: AppColors.primaryGreen.withOpacity(0.1),
|
||||
textColor: AppColors.primaryGreen,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuButton<String>(
|
||||
onSelected: (action) => _handleNotificationAction(notification, action),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: isRead ? 'mark_unread' : 'mark_read',
|
||||
child: Text(isRead ? 'Marquer non lu' : 'Marquer lu', style: AppTypography.bodyTextSmall),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Text('Supprimer', style: AppTypography.bodyTextSmall.copyWith(color: AppColors.error)),
|
||||
),
|
||||
],
|
||||
child: const Icon(Icons.more_vert, size: 14, color: AppColors.textSecondaryLight),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () => _handleNotificationTap(notification),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Icône et indicateur
|
||||
Stack(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
_getNotificationIcon(type),
|
||||
color: color,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
if (!isRead)
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF6C5CE7),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Contenu
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
notification['title'],
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: isRead ? FontWeight.w500 : FontWeight.w600,
|
||||
color: isRead ? Colors.grey[700] : const Color(0xFF1F2937),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
notification['time'],
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[500],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
notification['message'],
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey[600],
|
||||
height: 1.3,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (notification['actionText'] != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
notification['actionText'],
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: color,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Menu actions
|
||||
PopupMenuButton<String>(
|
||||
onSelected: (action) => _handleNotificationAction(notification, action),
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: isRead ? 'mark_unread' : 'mark_read',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
isRead ? Icons.mark_email_unread : Icons.mark_email_read,
|
||||
size: 18,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(isRead ? 'Marquer non lu' : 'Marquer comme lu'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.delete,
|
||||
size: 18,
|
||||
color: Colors.red,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text('Supprimer', style: TextStyle(color: Colors.red)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
child: Icon(
|
||||
Icons.more_vert,
|
||||
color: Colors.grey[400],
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getNotificationIconSource(String type) {
|
||||
switch (type) {
|
||||
case 'Membres': return 'people';
|
||||
case 'Événements': return 'event';
|
||||
case 'Organisations': return 'business';
|
||||
case 'Système': return 'settings';
|
||||
default: return 'notifications';
|
||||
}
|
||||
}
|
||||
|
||||
/// Onglet préférences
|
||||
Widget _buildPreferencesTab() {
|
||||
return SingleChildScrollView(
|
||||
@@ -587,18 +442,18 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
|
||||
// Notifications push
|
||||
_buildPreferenceSection(
|
||||
'Notifications push',
|
||||
'NOTIFICATIONS PUSH',
|
||||
'Recevoir des notifications sur votre appareil',
|
||||
Icons.notifications_active,
|
||||
Icons.notifications_active_outlined,
|
||||
[
|
||||
_buildPreferenceItem(
|
||||
'Activer les notifications',
|
||||
'ACTIVER LES NOTIFICATIONS',
|
||||
'Recevoir toutes les notifications',
|
||||
true,
|
||||
(value) => _updatePreference('push_enabled', value),
|
||||
),
|
||||
_buildPreferenceItem(
|
||||
'Sons et vibrations',
|
||||
'SONS ET VIBRATIONS',
|
||||
'Alertes sonores et vibrations',
|
||||
true,
|
||||
(value) => _updatePreference('sound_enabled', value),
|
||||
@@ -683,19 +538,8 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
IconData icon,
|
||||
List<Widget> items,
|
||||
) {
|
||||
return Container(
|
||||
return CoreCard(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -703,35 +547,28 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color: Colors.grey[600],
|
||||
size: 20,
|
||||
color: AppColors.primaryGreen,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey[800],
|
||||
),
|
||||
style: AppTypography.actionText.copyWith(fontSize: 12),
|
||||
),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
style: AppTypography.subtitleSmall.copyWith(fontSize: 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 12),
|
||||
...items,
|
||||
],
|
||||
),
|
||||
@@ -746,7 +583,7 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
Function(bool) onChanged,
|
||||
) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -755,26 +592,23 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1F2937),
|
||||
),
|
||||
style: AppTypography.bodyTextSmall.copyWith(fontWeight: FontWeight.w500),
|
||||
),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
style: AppTypography.subtitleSmall.copyWith(fontSize: 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Switch(
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
activeColor: const Color(0xFF6C5CE7),
|
||||
SizedBox(
|
||||
height: 24,
|
||||
child: Switch(
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
activeColor: AppColors.primaryGreen,
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -827,15 +661,15 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
Color _getNotificationColor(String type) {
|
||||
switch (type) {
|
||||
case 'Membres':
|
||||
return const Color(0xFF6C5CE7);
|
||||
return AppColors.primaryGreen;
|
||||
case 'Événements':
|
||||
return const Color(0xFF00B894);
|
||||
case 'Organisations':
|
||||
return const Color(0xFF0984E3);
|
||||
return AppColors.primaryGreen;
|
||||
case 'Système':
|
||||
return const Color(0xFFE17055);
|
||||
return AppColors.warning;
|
||||
default:
|
||||
return Colors.grey;
|
||||
return AppColors.textSecondaryLight;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -866,17 +700,23 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
);
|
||||
}
|
||||
|
||||
// Action selon le type
|
||||
// Action selon le type : navigation vers l'écran concerné
|
||||
final type = notification['type'] as String;
|
||||
switch (type) {
|
||||
case 'Membres':
|
||||
_showSuccessSnackBar('Navigation vers la gestion des membres');
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(builder: (_) => const MembersPageWrapper()),
|
||||
);
|
||||
break;
|
||||
case 'Événements':
|
||||
_showSuccessSnackBar('Navigation vers les événements');
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(builder: (_) => const EventsPageWrapper()),
|
||||
);
|
||||
break;
|
||||
case 'Organisations':
|
||||
_showSuccessSnackBar('Navigation vers les organisations');
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(builder: (_) => const OrganizationsPage()),
|
||||
);
|
||||
break;
|
||||
case 'Système':
|
||||
_showSystemNotificationDialog(notification);
|
||||
@@ -910,32 +750,28 @@ class _NotificationsPageState extends State<NotificationsPage>
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Marquer tout comme lu'),
|
||||
content: const Text(
|
||||
'Êtes-vous sûr de vouloir marquer toutes les notifications comme lues ?',
|
||||
title: Text('Vider le flux', style: AppTypography.headerSmall),
|
||||
content: Text(
|
||||
'Voulez-vous marquer toutes les notifications comme lues ?',
|
||||
style: AppTypography.bodyTextSmall,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Annuler'),
|
||||
child: Text('ANNULER', style: AppTypography.actionText.copyWith(color: AppColors.textSecondaryLight)),
|
||||
),
|
||||
ElevatedButton(
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
setState(() {
|
||||
// Marquer toutes les notifications comme lues
|
||||
final notifications = _getFilteredNotifications();
|
||||
for (var notification in notifications) {
|
||||
notification['isRead'] = true;
|
||||
}
|
||||
});
|
||||
_showSuccessSnackBar('Toutes les notifications ont été marquées comme lues');
|
||||
_showSuccessSnackBar('Flux marqué comme lu');
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF6C5CE7),
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Confirmer'),
|
||||
child: Text('CONFIRMER', style: AppTypography.actionText.copyWith(color: AppColors.primaryGreen)),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -3,6 +3,9 @@ library notifications_page_wrapper;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../../../core/utils/logger.dart';
|
||||
import '../../data/repositories/notification_repository.dart';
|
||||
import '../bloc/notifications_bloc.dart';
|
||||
import 'notifications_page.dart';
|
||||
|
||||
@@ -13,8 +16,20 @@ class NotificationsPageWrapper extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<NotificationsBloc>(
|
||||
create: (_) => GetIt.instance<NotificationsBloc>(),
|
||||
create: (_) => _getOrCreateNotificationsBloc(),
|
||||
child: const NotificationsPage(),
|
||||
);
|
||||
}
|
||||
|
||||
static NotificationsBloc _getOrCreateNotificationsBloc() {
|
||||
try {
|
||||
if (GetIt.instance.isRegistered<NotificationsBloc>()) {
|
||||
return GetIt.instance<NotificationsBloc>();
|
||||
}
|
||||
} catch (e, st) {
|
||||
AppLogger.error('NotificationsPageWrapper: résolution NotificationsBloc échouée', error: e, stackTrace: st);
|
||||
}
|
||||
final repo = getIt<NotificationRepository>();
|
||||
return NotificationsBloc(repo);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user