feat(communication): module messagerie unifié + contact policies + blocages

Aligné avec le backend MessagingResource :
- Nouveau module communication (conversations, messages, participants)
- Respect des ContactPolicy (qui peut parler à qui par rôle)
- Gestion MemberBlock (blocages individuels)
- UI : conversations list, conversation detail, broadcast, tiles
- BLoC : MessagingBloc avec events (envoyer, démarrer conversation rôle, etc.)
This commit is contained in:
dahoud
2026-04-15 20:26:35 +00:00
parent 07b8488714
commit 45dcd2171e
23 changed files with 2096 additions and 1588 deletions

View File

@@ -0,0 +1,27 @@
/// Modèle de données ContactPolicy v4 avec désérialisation JSON
library contact_policy_model;
import '../../domain/entities/contact_policy.dart';
/// Modèle ContactPolicy v4
class ContactPolicyModel extends ContactPolicy {
const ContactPolicyModel({
super.id,
super.organisationId,
required super.typePolitique,
super.autoriserMembreVersMembre,
super.autoriserMembreVersRole,
super.autoriserNotesVocales,
});
factory ContactPolicyModel.fromJson(Map<String, dynamic> json) {
return ContactPolicyModel(
id: json['id']?.toString(),
organisationId: json['organisationId']?.toString(),
typePolitique: json['typePolitique']?.toString() ?? 'OUVERT',
autoriserMembreVersMembre: json['autoriserMembreVersMembre'] == true,
autoriserMembreVersRole: json['autoriserMembreVersRole'] == true,
autoriserNotesVocales: json['autoriserNotesVocales'] == true,
);
}
}

View File

@@ -1,70 +1,110 @@
/// Model de données Conversation avec sérialisation JSON
/// Modèles de données Conversation v4 avec sérialisation JSON
library conversation_model;
import 'package:json_annotation/json_annotation.dart';
import '../../domain/entities/conversation.dart';
import '../../domain/entities/message.dart';
import 'message_model.dart';
part 'conversation_model.g.dart';
@JsonSerializable(explicitToJson: true)
class ConversationModel extends Conversation {
@JsonKey(
fromJson: _messageFromJson,
toJson: _messageToJson,
)
@override
final Message? lastMessage;
const ConversationModel({
/// Modèle de résumé de conversation (liste)
class ConversationSummaryModel extends ConversationSummary {
const ConversationSummaryModel({
required super.id,
required super.name,
super.description,
required super.type,
required super.participantIds,
super.organizationId,
this.lastMessage,
super.unreadCount,
super.isMuted,
super.isPinned,
super.isArchived,
required super.createdAt,
super.updatedAt,
super.avatarUrl,
super.metadata,
}) : super(lastMessage: lastMessage);
required super.typeConversation,
required super.titre,
required super.statut,
super.dernierMessageApercu,
super.dernierMessageType,
super.dernierMessageAt,
super.nonLus,
super.organisationId,
});
static Message? _messageFromJson(Map<String, dynamic>? json) =>
json == null ? null : MessageModel.fromJson(json);
static Map<String, dynamic>? _messageToJson(Message? message) =>
message == null ? null : MessageModel.fromEntity(message).toJson();
factory ConversationModel.fromJson(Map<String, dynamic> json) =>
_$ConversationModelFromJson(json);
Map<String, dynamic> toJson() => _$ConversationModelToJson(this);
factory ConversationModel.fromEntity(Conversation conversation) {
return ConversationModel(
id: conversation.id,
name: conversation.name,
description: conversation.description,
type: conversation.type,
participantIds: conversation.participantIds,
organizationId: conversation.organizationId,
lastMessage: conversation.lastMessage,
unreadCount: conversation.unreadCount,
isMuted: conversation.isMuted,
isPinned: conversation.isPinned,
isArchived: conversation.isArchived,
createdAt: conversation.createdAt,
updatedAt: conversation.updatedAt,
avatarUrl: conversation.avatarUrl,
metadata: conversation.metadata,
factory ConversationSummaryModel.fromJson(Map<String, dynamic> json) {
return ConversationSummaryModel(
id: json['id']?.toString() ?? '',
typeConversation: json['typeConversation']?.toString() ?? 'DIRECTE',
titre: json['titre']?.toString() ?? '',
statut: json['statut']?.toString() ?? 'ACTIVE',
dernierMessageApercu: json['dernierMessageApercu']?.toString(),
dernierMessageType: json['dernierMessageType']?.toString(),
dernierMessageAt: json['dernierMessageAt'] != null
? DateTime.tryParse(json['dernierMessageAt'].toString())
: null,
nonLus: _parseInt(json['nonLus']),
organisationId: json['organisationId']?.toString(),
);
}
Conversation toEntity() => this;
}
/// Modèle de participant
class ConversationParticipantModel extends ConversationParticipant {
const ConversationParticipantModel({
required super.membreId,
super.prenom,
super.nom,
super.roleDansConversation,
super.luJusqua,
});
factory ConversationParticipantModel.fromJson(Map<String, dynamic> json) {
return ConversationParticipantModel(
membreId: json['membreId']?.toString() ?? '',
prenom: json['prenom']?.toString(),
nom: json['nom']?.toString(),
roleDansConversation: json['roleDansConversation']?.toString(),
luJusqua: json['luJusqua'] != null
? DateTime.tryParse(json['luJusqua'].toString())
: null,
);
}
}
/// Modèle de conversation complète (détail)
class ConversationModel extends Conversation {
const ConversationModel({
required super.id,
required super.typeConversation,
required super.titre,
required super.statut,
super.organisationId,
super.organisationNom,
super.dateCreation,
super.nombreMessages,
super.participants,
super.messages,
super.nonLus,
super.roleCible,
});
factory ConversationModel.fromJson(Map<String, dynamic> json) {
final participantsJson = json['participants'] as List<dynamic>? ?? [];
final messagesJson = json['messages'] as List<dynamic>? ?? [];
return ConversationModel(
id: json['id']?.toString() ?? '',
typeConversation: json['typeConversation']?.toString() ?? 'DIRECTE',
titre: json['titre']?.toString() ?? '',
statut: json['statut']?.toString() ?? 'ACTIVE',
organisationId: json['organisationId']?.toString(),
organisationNom: json['organisationNom']?.toString(),
dateCreation: json['dateCreation'] != null
? DateTime.tryParse(json['dateCreation'].toString())
: null,
nombreMessages: _parseInt(json['nombreMessages']),
participants: participantsJson
.map((p) => ConversationParticipantModel.fromJson(p as Map<String, dynamic>))
.toList(),
messages: messagesJson
.map((m) => MessageModel.fromJson(m as Map<String, dynamic>))
.toList(),
nonLus: _parseInt(json['nonLus']),
roleCible: json['roleCible']?.toString(),
);
}
}
int _parseInt(dynamic value) {
if (value == null) return 0;
if (value is int) return value;
if (value is double) return value.toInt();
return int.tryParse(value.toString()) ?? 0;
}

View File

@@ -1,57 +1,2 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'conversation_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
ConversationModel _$ConversationModelFromJson(Map<String, dynamic> json) =>
ConversationModel(
id: json['id'] as String,
name: json['name'] as String,
description: json['description'] as String?,
type: $enumDecode(_$ConversationTypeEnumMap, json['type']),
participantIds: (json['participantIds'] as List<dynamic>)
.map((e) => e as String)
.toList(),
organizationId: json['organizationId'] as String?,
lastMessage: ConversationModel._messageFromJson(
json['lastMessage'] as Map<String, dynamic>?),
unreadCount: (json['unreadCount'] as num?)?.toInt() ?? 0,
isMuted: json['isMuted'] as bool? ?? false,
isPinned: json['isPinned'] as bool? ?? false,
isArchived: json['isArchived'] as bool? ?? false,
createdAt: DateTime.parse(json['createdAt'] as String),
updatedAt: json['updatedAt'] == null
? null
: DateTime.parse(json['updatedAt'] as String),
avatarUrl: json['avatarUrl'] as String?,
metadata: json['metadata'] as Map<String, dynamic>?,
);
Map<String, dynamic> _$ConversationModelToJson(ConversationModel instance) =>
<String, dynamic>{
'id': instance.id,
'name': instance.name,
'description': instance.description,
'type': _$ConversationTypeEnumMap[instance.type]!,
'participantIds': instance.participantIds,
'organizationId': instance.organizationId,
'unreadCount': instance.unreadCount,
'isMuted': instance.isMuted,
'isPinned': instance.isPinned,
'isArchived': instance.isArchived,
'createdAt': instance.createdAt.toIso8601String(),
'updatedAt': instance.updatedAt?.toIso8601String(),
'avatarUrl': instance.avatarUrl,
'metadata': instance.metadata,
'lastMessage': ConversationModel._messageToJson(instance.lastMessage),
};
const _$ConversationTypeEnumMap = {
ConversationType.individual: 'individual',
ConversationType.group: 'group',
ConversationType.broadcast: 'broadcast',
ConversationType.announcement: 'announcement',
};
// Modèles v4 : désérialisation manuelle, code generation non utilisé.

View File

@@ -1,83 +1,48 @@
/// Model de données Message avec sérialisation JSON
/// Modèle de données Message v4 avec sérialisation JSON
library message_model;
import 'package:json_annotation/json_annotation.dart';
import '../../domain/entities/message.dart';
part 'message_model.g.dart';
@JsonSerializable(explicitToJson: true)
/// Modèle Message v4
class MessageModel extends Message {
const MessageModel({
required super.id,
required super.conversationId,
required super.senderId,
required super.senderName,
super.senderAvatar,
required super.content,
required super.type,
required super.status,
super.priority,
required super.recipientIds,
super.recipientRoles,
super.organizationId,
required super.createdAt,
super.readAt,
super.metadata,
super.attachments,
super.isEdited,
super.editedAt,
super.isDeleted,
required super.typeMessage,
super.contenu,
super.urlFichier,
super.dureeAudio,
super.supprime,
super.expediteurId,
super.expediteurNom,
super.expediteurPrenom,
super.messageParentId,
super.messageParentApercu,
super.dateEnvoi,
});
factory MessageModel.fromJson(Map<String, dynamic> json) =>
_$MessageModelFromJson(json);
Map<String, dynamic> toJson() => _$MessageModelToJson(this);
factory MessageModel.fromEntity(Message message) {
factory MessageModel.fromJson(Map<String, dynamic> json) {
return MessageModel(
id: message.id,
conversationId: message.conversationId,
senderId: message.senderId,
senderName: message.senderName,
senderAvatar: message.senderAvatar,
content: message.content,
type: message.type,
status: message.status,
priority: message.priority,
recipientIds: message.recipientIds,
recipientRoles: message.recipientRoles,
organizationId: message.organizationId,
createdAt: message.createdAt,
readAt: message.readAt,
metadata: message.metadata,
attachments: message.attachments,
isEdited: message.isEdited,
editedAt: message.editedAt,
isDeleted: message.isDeleted,
id: json['id']?.toString() ?? '',
typeMessage: json['typeMessage']?.toString() ?? 'TEXTE',
contenu: json['contenu']?.toString(),
urlFichier: json['urlFichier']?.toString(),
dureeAudio: _parseInt(json['dureeAudio']),
supprime: json['supprime'] == true,
expediteurId: json['expediteurId']?.toString(),
expediteurNom: json['expediteurNom']?.toString(),
expediteurPrenom: json['expediteurPrenom']?.toString(),
messageParentId: json['messageParentId']?.toString(),
messageParentApercu: json['messageParentApercu']?.toString(),
dateEnvoi: json['dateEnvoi'] != null
? DateTime.tryParse(json['dateEnvoi'].toString())
: null,
);
}
Message toEntity() => Message(
id: id,
conversationId: conversationId,
senderId: senderId,
senderName: senderName,
senderAvatar: senderAvatar,
content: content,
type: type,
status: status,
priority: priority,
recipientIds: recipientIds,
recipientRoles: recipientRoles,
organizationId: organizationId,
createdAt: createdAt,
readAt: readAt,
metadata: metadata,
attachments: attachments,
isEdited: isEdited,
editedAt: editedAt,
isDeleted: isDeleted,
);
}
int? _parseInt(dynamic value) {
if (value == null) return null;
if (value is int) return value;
if (value is double) return value.toInt();
return int.tryParse(value.toString());
}

View File

@@ -1,84 +1,2 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'message_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
MessageModel _$MessageModelFromJson(Map<String, dynamic> json) => MessageModel(
id: json['id'] as String,
conversationId: json['conversationId'] as String,
senderId: json['senderId'] as String,
senderName: json['senderName'] as String,
senderAvatar: json['senderAvatar'] as String?,
content: json['content'] as String,
type: $enumDecode(_$MessageTypeEnumMap, json['type']),
status: $enumDecode(_$MessageStatusEnumMap, json['status']),
priority:
$enumDecodeNullable(_$MessagePriorityEnumMap, json['priority']) ??
MessagePriority.normal,
recipientIds: (json['recipientIds'] as List<dynamic>)
.map((e) => e as String)
.toList(),
recipientRoles: (json['recipientRoles'] as List<dynamic>?)
?.map((e) => e as String)
.toList(),
organizationId: json['organizationId'] as String?,
createdAt: DateTime.parse(json['createdAt'] as String),
readAt: json['readAt'] == null
? null
: DateTime.parse(json['readAt'] as String),
metadata: json['metadata'] as Map<String, dynamic>?,
attachments: (json['attachments'] as List<dynamic>?)
?.map((e) => e as String)
.toList(),
isEdited: json['isEdited'] as bool? ?? false,
editedAt: json['editedAt'] == null
? null
: DateTime.parse(json['editedAt'] as String),
isDeleted: json['isDeleted'] as bool? ?? false,
);
Map<String, dynamic> _$MessageModelToJson(MessageModel instance) =>
<String, dynamic>{
'id': instance.id,
'conversationId': instance.conversationId,
'senderId': instance.senderId,
'senderName': instance.senderName,
'senderAvatar': instance.senderAvatar,
'content': instance.content,
'type': _$MessageTypeEnumMap[instance.type]!,
'status': _$MessageStatusEnumMap[instance.status]!,
'priority': _$MessagePriorityEnumMap[instance.priority]!,
'recipientIds': instance.recipientIds,
'recipientRoles': instance.recipientRoles,
'organizationId': instance.organizationId,
'createdAt': instance.createdAt.toIso8601String(),
'readAt': instance.readAt?.toIso8601String(),
'metadata': instance.metadata,
'attachments': instance.attachments,
'isEdited': instance.isEdited,
'editedAt': instance.editedAt?.toIso8601String(),
'isDeleted': instance.isDeleted,
};
const _$MessageTypeEnumMap = {
MessageType.individual: 'individual',
MessageType.broadcast: 'broadcast',
MessageType.targeted: 'targeted',
MessageType.system: 'system',
};
const _$MessageStatusEnumMap = {
MessageStatus.sent: 'sent',
MessageStatus.delivered: 'delivered',
MessageStatus.read: 'read',
MessageStatus.failed: 'failed',
};
const _$MessagePriorityEnumMap = {
MessagePriority.normal: 'normal',
MessagePriority.high: 'high',
MessagePriority.urgent: 'urgent',
};
// Modèles v4 : désérialisation manuelle, code generation non utilisé.