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,105 @@
/// BLoC de gestion de la messagerie
library messaging_bloc;
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:injectable/injectable.dart';
import '../../domain/usecases/get_conversations.dart';
import '../../domain/usecases/get_messages.dart';
import '../../domain/usecases/send_message.dart';
import '../../domain/usecases/send_broadcast.dart';
import 'messaging_event.dart';
import 'messaging_state.dart';
@injectable
class MessagingBloc extends Bloc<MessagingEvent, MessagingState> {
final GetConversations getConversations;
final GetMessages getMessages;
final SendMessage sendMessage;
final SendBroadcast sendBroadcast;
MessagingBloc({
required this.getConversations,
required this.getMessages,
required this.sendMessage,
required this.sendBroadcast,
}) : super(MessagingInitial()) {
on<LoadConversations>(_onLoadConversations);
on<LoadMessages>(_onLoadMessages);
on<SendMessageEvent>(_onSendMessage);
on<SendBroadcastEvent>(_onSendBroadcast);
}
Future<void> _onLoadConversations(
LoadConversations event,
Emitter<MessagingState> emit,
) async {
emit(MessagingLoading());
final result = await getConversations(
organizationId: event.organizationId,
includeArchived: event.includeArchived,
);
result.fold(
(failure) => emit(MessagingError(failure.message)),
(conversations) => emit(ConversationsLoaded(conversations: conversations)),
);
}
Future<void> _onLoadMessages(
LoadMessages event,
Emitter<MessagingState> emit,
) async {
emit(MessagingLoading());
final result = await getMessages(
conversationId: event.conversationId,
limit: event.limit,
beforeMessageId: event.beforeMessageId,
);
result.fold(
(failure) => emit(MessagingError(failure.message)),
(messages) => emit(MessagesLoaded(
conversationId: event.conversationId,
messages: messages,
hasMore: messages.length == (event.limit ?? 50),
)),
);
}
Future<void> _onSendMessage(
SendMessageEvent event,
Emitter<MessagingState> emit,
) async {
final result = await sendMessage(
conversationId: event.conversationId,
content: event.content,
attachments: event.attachments,
priority: event.priority,
);
result.fold(
(failure) => emit(MessagingError(failure.message)),
(message) => emit(MessageSent(message)),
);
}
Future<void> _onSendBroadcast(
SendBroadcastEvent event,
Emitter<MessagingState> emit,
) async {
final result = await sendBroadcast(
organizationId: event.organizationId,
subject: event.subject,
content: event.content,
priority: event.priority,
attachments: event.attachments,
);
result.fold(
(failure) => emit(MessagingError(failure.message)),
(message) => emit(BroadcastSent(message)),
);
}
}

View File

@@ -0,0 +1,118 @@
/// Événements du BLoC Messaging
library messaging_event;
import 'package:equatable/equatable.dart';
import '../../domain/entities/message.dart';
abstract class MessagingEvent extends Equatable {
const MessagingEvent();
@override
List<Object?> get props => [];
}
/// Charger les conversations
class LoadConversations extends MessagingEvent {
final String? organizationId;
final bool includeArchived;
const LoadConversations({
this.organizationId,
this.includeArchived = false,
});
@override
List<Object?> get props => [organizationId, includeArchived];
}
/// Charger les messages d'une conversation
class LoadMessages extends MessagingEvent {
final String conversationId;
final int? limit;
final String? beforeMessageId;
const LoadMessages({
required this.conversationId,
this.limit,
this.beforeMessageId,
});
@override
List<Object?> get props => [conversationId, limit, beforeMessageId];
}
/// Envoyer un message
class SendMessageEvent extends MessagingEvent {
final String conversationId;
final String content;
final List<String>? attachments;
final MessagePriority priority;
const SendMessageEvent({
required this.conversationId,
required this.content,
this.attachments,
this.priority = MessagePriority.normal,
});
@override
List<Object?> get props => [conversationId, content, attachments, priority];
}
/// Envoyer un broadcast
class SendBroadcastEvent extends MessagingEvent {
final String organizationId;
final String subject;
final String content;
final MessagePriority priority;
final List<String>? attachments;
const SendBroadcastEvent({
required this.organizationId,
required this.subject,
required this.content,
this.priority = MessagePriority.normal,
this.attachments,
});
@override
List<Object?> get props => [organizationId, subject, content, priority, attachments];
}
/// Marquer un message comme lu
class MarkMessageAsReadEvent extends MessagingEvent {
final String messageId;
const MarkMessageAsReadEvent(this.messageId);
@override
List<Object?> get props => [messageId];
}
/// Charger le nombre de messages non lus
class LoadUnreadCount extends MessagingEvent {
final String? organizationId;
const LoadUnreadCount({this.organizationId});
@override
List<Object?> get props => [organizationId];
}
/// Créer une nouvelle conversation
class CreateConversationEvent extends MessagingEvent {
final String name;
final List<String> participantIds;
final String? organizationId;
final String? description;
const CreateConversationEvent({
required this.name,
required this.participantIds,
this.organizationId,
this.description,
});
@override
List<Object?> get props => [name, participantIds, organizationId, description];
}

View File

@@ -0,0 +1,99 @@
/// États du BLoC Messaging
library messaging_state;
import 'package:equatable/equatable.dart';
import '../../domain/entities/conversation.dart';
import '../../domain/entities/message.dart';
abstract class MessagingState extends Equatable {
const MessagingState();
@override
List<Object?> get props => [];
}
/// État initial
class MessagingInitial extends MessagingState {}
/// Chargement en cours
class MessagingLoading extends MessagingState {}
/// Conversations chargées
class ConversationsLoaded extends MessagingState {
final List<Conversation> conversations;
final int unreadCount;
const ConversationsLoaded({
required this.conversations,
this.unreadCount = 0,
});
@override
List<Object?> get props => [conversations, unreadCount];
}
/// Messages d'une conversation chargés
class MessagesLoaded extends MessagingState {
final String conversationId;
final List<Message> messages;
final bool hasMore;
const MessagesLoaded({
required this.conversationId,
required this.messages,
this.hasMore = false,
});
@override
List<Object?> get props => [conversationId, messages, hasMore];
}
/// Message envoyé avec succès
class MessageSent extends MessagingState {
final Message message;
const MessageSent(this.message);
@override
List<Object?> get props => [message];
}
/// Broadcast envoyé avec succès
class BroadcastSent extends MessagingState {
final Message message;
const BroadcastSent(this.message);
@override
List<Object?> get props => [message];
}
/// Conversation créée
class ConversationCreated extends MessagingState {
final Conversation conversation;
const ConversationCreated(this.conversation);
@override
List<Object?> get props => [conversation];
}
/// Compteur de non lus chargé
class UnreadCountLoaded extends MessagingState {
final int count;
const UnreadCountLoaded(this.count);
@override
List<Object?> get props => [count];
}
/// Erreur
class MessagingError extends MessagingState {
final String message;
const MessagingError(this.message);
@override
List<Object?> get props => [message];
}

View File

@@ -0,0 +1,150 @@
/// Page liste des conversations
library conversations_page;
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../core/di/injection_container.dart';
import '../../../../shared/design_system/unionflow_design_system.dart';
import '../bloc/messaging_bloc.dart';
import '../bloc/messaging_event.dart';
import '../bloc/messaging_state.dart';
import '../widgets/conversation_tile.dart';
class ConversationsPage extends StatelessWidget {
final String? organizationId;
const ConversationsPage({
super.key,
this.organizationId,
});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => sl<MessagingBloc>()
..add(LoadConversations(organizationId: organizationId)),
child: Scaffold(
backgroundColor: ColorTokens.background,
appBar: const UFAppBar(
title: 'MESSAGES',
automaticallyImplyLeading: true,
),
body: BlocBuilder<MessagingBloc, MessagingState>(
builder: (context, state) {
if (state is MessagingLoading) {
return const Center(child: CircularProgressIndicator());
}
if (state is MessagingError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
size: 64,
color: AppColors.error,
),
const SizedBox(height: SpacingTokens.md),
Text(
'Erreur',
style: AppTypography.headerSmall,
),
const SizedBox(height: SpacingTokens.sm),
Text(
state.message,
style: AppTypography.bodyTextSmall,
textAlign: TextAlign.center,
),
const SizedBox(height: SpacingTokens.lg),
UFPrimaryButton(
label: 'Réessayer',
onPressed: () {
context.read<MessagingBloc>().add(
LoadConversations(organizationId: organizationId),
);
},
),
],
),
);
}
if (state is ConversationsLoaded) {
final conversations = state.conversations;
if (conversations.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.chat_bubble_outline,
size: 64,
color: AppColors.textSecondaryLight,
),
const SizedBox(height: SpacingTokens.md),
Text(
'Aucune conversation',
style: AppTypography.headerSmall.copyWith(
color: AppColors.textSecondaryLight,
),
),
const SizedBox(height: SpacingTokens.sm),
Text(
'Commencez une nouvelle conversation',
style: AppTypography.bodyTextSmall.copyWith(
color: AppColors.textSecondaryLight,
),
),
],
),
);
}
return RefreshIndicator(
onRefresh: () async {
context.read<MessagingBloc>().add(
LoadConversations(organizationId: organizationId),
);
},
child: ListView.separated(
padding: const EdgeInsets.all(SpacingTokens.md),
itemCount: conversations.length,
separatorBuilder: (_, __) => const SizedBox(height: SpacingTokens.sm),
itemBuilder: (context, index) {
final conversation = conversations[index];
return ConversationTile(
conversation: conversation,
onTap: () {
// Navigation vers la page de chat
// TODO: Implémenter navigation
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Ouvrir conversation: ${conversation.name}'),
),
);
},
);
},
),
);
}
return const SizedBox.shrink();
},
),
floatingActionButton: FloatingActionButton(
backgroundColor: AppColors.primaryGreen,
onPressed: () {
// TODO: Ouvrir dialogue nouvelle conversation
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Nouvelle conversation (à implémenter)')),
);
},
child: const Icon(Icons.add, color: Colors.white),
),
),
);
}
}

View File

@@ -0,0 +1,166 @@
/// Widget tuile de conversation
library conversation_tile;
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../../../../shared/design_system/unionflow_design_system.dart';
import '../../domain/entities/conversation.dart';
class ConversationTile extends StatelessWidget {
final Conversation conversation;
final VoidCallback onTap;
const ConversationTile({
super.key,
required this.conversation,
required this.onTap,
});
String _formatDate(DateTime date) {
final now = DateTime.now();
final difference = now.difference(date);
if (difference.inDays == 0) {
return DateFormat('HH:mm').format(date);
} else if (difference.inDays == 1) {
return 'Hier';
} else if (difference.inDays < 7) {
return DateFormat('EEEE', 'fr_FR').format(date);
} else {
return DateFormat('dd/MM/yy').format(date);
}
}
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
child: Container(
padding: const EdgeInsets.all(SpacingTokens.md),
decoration: BoxDecoration(
color: ColorTokens.surface,
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
border: Border.all(
color: conversation.hasUnread
? AppColors.primaryGreen.withOpacity(0.3)
: ColorTokens.outline,
),
),
child: Row(
children: [
// Avatar
CircleAvatar(
radius: 24,
backgroundColor: AppColors.primaryGreen.withOpacity(0.1),
backgroundImage: conversation.avatarUrl != null
? NetworkImage(conversation.avatarUrl!)
: null,
child: conversation.avatarUrl == null
? Text(
conversation.name.isNotEmpty
? conversation.name[0].toUpperCase()
: '?',
style: AppTypography.actionText.copyWith(
color: AppColors.primaryGreen,
),
)
: null,
),
const SizedBox(width: SpacingTokens.md),
// Contenu
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
conversation.name,
style: AppTypography.actionText.copyWith(
fontWeight: conversation.hasUnread
? FontWeight.bold
: FontWeight.normal,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
if (conversation.lastMessage != null)
Text(
_formatDate(conversation.lastMessage!.createdAt),
style: AppTypography.subtitleSmall.copyWith(
color: AppColors.textSecondaryLight,
),
),
],
),
if (conversation.lastMessage != null) ...[
const SizedBox(height: 4),
Text(
conversation.lastMessage!.content,
style: AppTypography.bodyTextSmall.copyWith(
color: AppColors.textSecondaryLight,
fontWeight: conversation.hasUnread
? FontWeight.w600
: FontWeight.normal,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
],
),
),
// Badge non lus
if (conversation.hasUnread) ...[
const SizedBox(width: SpacingTokens.sm),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: AppColors.primaryGreen,
borderRadius: BorderRadius.circular(SpacingTokens.radiusCircular),
),
child: Text(
'${conversation.unreadCount}',
style: AppTypography.badgeText.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
],
// Icônes statut
if (conversation.isPinned || conversation.isMuted) ...[
const SizedBox(width: SpacingTokens.sm),
Column(
children: [
if (conversation.isPinned)
Icon(
Icons.push_pin,
size: 16,
color: AppColors.textSecondaryLight,
),
if (conversation.isMuted)
Icon(
Icons.volume_off,
size: 16,
color: AppColors.textSecondaryLight,
),
],
),
],
],
),
),
);
}
}