- Epargne: badge LCB-FT (bouclier ambre) sur comptes avec fonds bloques + note recap - EpargneDetail: historique pagine (page/size), affichage soldeAvant/soldeApres/motif dans chaque transaction, bouton "Charger plus" - TransactionEpargneRepository: getByCompte accepte page et size, gere reponse paginee Spring (content[]) - MessagingDatasource: markMessageAsRead silencieuse (pas d'endpoint unitaire), getUnreadCount somme unreadCount des conversations - OrganizationDetail: _memberCount charge le vrai nombre depuis GET /membres/count, affiche la valeur reelle au lieu de nombreMembres (toujours 0)
322 lines
10 KiB
Dart
322 lines
10 KiB
Dart
/// Datasource distant pour la communication (API)
|
|
library messaging_remote_datasource;
|
|
|
|
import 'dart:convert';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|
import 'package:injectable/injectable.dart';
|
|
import '../../../../core/config/environment.dart';
|
|
import '../../../../core/error/exceptions.dart';
|
|
import '../models/message_model.dart';
|
|
import '../models/conversation_model.dart';
|
|
import '../../domain/entities/message.dart';
|
|
|
|
@lazySingleton
|
|
class MessagingRemoteDatasource {
|
|
final http.Client client;
|
|
final FlutterSecureStorage secureStorage;
|
|
|
|
MessagingRemoteDatasource({
|
|
required this.client,
|
|
required this.secureStorage,
|
|
});
|
|
|
|
/// Headers HTTP avec authentification
|
|
Future<Map<String, String>> _getHeaders() async {
|
|
final token = await secureStorage.read(key: 'kc_access');
|
|
return {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
if (token != null) 'Authorization': 'Bearer $token',
|
|
};
|
|
}
|
|
|
|
// === CONVERSATIONS ===
|
|
|
|
Future<List<ConversationModel>> getConversations({
|
|
String? organizationId,
|
|
bool includeArchived = false,
|
|
}) async {
|
|
final uri = Uri.parse('${AppConfig.apiBaseUrl}/api/conversations')
|
|
.replace(queryParameters: {
|
|
if (organizationId != null) 'organisationId': organizationId,
|
|
'includeArchived': includeArchived.toString(),
|
|
});
|
|
|
|
final response = await client.get(uri, headers: await _getHeaders());
|
|
|
|
if (response.statusCode == 200) {
|
|
final List<dynamic> jsonList = json.decode(response.body);
|
|
return jsonList
|
|
.map((json) => ConversationModel.fromJson(json))
|
|
.toList();
|
|
} else if (response.statusCode == 401) {
|
|
throw UnauthorizedException();
|
|
} else {
|
|
throw ServerException('Erreur lors de la récupération des conversations');
|
|
}
|
|
}
|
|
|
|
Future<ConversationModel> getConversationById(String conversationId) async {
|
|
final uri = Uri.parse(
|
|
'${AppConfig.apiBaseUrl}/api/conversations/$conversationId');
|
|
|
|
final response = await client.get(uri, headers: await _getHeaders());
|
|
|
|
if (response.statusCode == 200) {
|
|
return ConversationModel.fromJson(json.decode(response.body));
|
|
} else if (response.statusCode == 404) {
|
|
throw NotFoundException('Conversation non trouvée');
|
|
} else if (response.statusCode == 401) {
|
|
throw UnauthorizedException();
|
|
} else {
|
|
throw ServerException('Erreur lors de la récupération de la conversation');
|
|
}
|
|
}
|
|
|
|
Future<ConversationModel> createConversation({
|
|
required String name,
|
|
required List<String> participantIds,
|
|
String? organizationId,
|
|
String? description,
|
|
}) async {
|
|
final uri =
|
|
Uri.parse('${AppConfig.apiBaseUrl}/api/conversations');
|
|
|
|
final body = json.encode({
|
|
'name': name,
|
|
'participantIds': participantIds,
|
|
'type': 'GROUP', // Default to GROUP for multi-participant conversations
|
|
if (organizationId != null) 'organisationId': organizationId,
|
|
if (description != null) 'description': description,
|
|
});
|
|
|
|
final response = await client.post(
|
|
uri,
|
|
headers: await _getHeaders(),
|
|
body: body,
|
|
);
|
|
|
|
if (response.statusCode == 201 || response.statusCode == 200) {
|
|
return ConversationModel.fromJson(json.decode(response.body));
|
|
} else if (response.statusCode == 401) {
|
|
throw UnauthorizedException();
|
|
} else {
|
|
throw ServerException('Erreur lors de la création de la conversation');
|
|
}
|
|
}
|
|
|
|
// === MESSAGES ===
|
|
|
|
Future<List<MessageModel>> getMessages({
|
|
required String conversationId,
|
|
int? limit,
|
|
String? beforeMessageId,
|
|
}) async {
|
|
final uri = Uri.parse('${AppConfig.apiBaseUrl}/api/messages')
|
|
.replace(queryParameters: {
|
|
'conversationId': conversationId,
|
|
if (limit != null) 'limit': limit.toString(),
|
|
// beforeMessageId not supported by backend yet, omit
|
|
});
|
|
|
|
final response = await client.get(uri, headers: await _getHeaders());
|
|
|
|
if (response.statusCode == 200) {
|
|
final List<dynamic> jsonList = json.decode(response.body);
|
|
return jsonList.map((json) => MessageModel.fromJson(json)).toList();
|
|
} else if (response.statusCode == 401) {
|
|
throw UnauthorizedException();
|
|
} else {
|
|
throw ServerException('Erreur lors de la récupération des messages');
|
|
}
|
|
}
|
|
|
|
Future<MessageModel> sendMessage({
|
|
required String conversationId,
|
|
required String content,
|
|
List<String>? attachments,
|
|
MessagePriority priority = MessagePriority.normal,
|
|
}) async {
|
|
final uri = Uri.parse('${AppConfig.apiBaseUrl}/api/messages');
|
|
|
|
final body = json.encode({
|
|
'conversationId': conversationId,
|
|
'content': content,
|
|
if (attachments != null) 'attachments': attachments,
|
|
'priority': priority.name.toUpperCase(),
|
|
});
|
|
|
|
final response = await client.post(
|
|
uri,
|
|
headers: await _getHeaders(),
|
|
body: body,
|
|
);
|
|
|
|
if (response.statusCode == 201 || response.statusCode == 200) {
|
|
return MessageModel.fromJson(json.decode(response.body));
|
|
} else if (response.statusCode == 401) {
|
|
throw UnauthorizedException();
|
|
} else {
|
|
throw ServerException('Erreur lors de l\'envoi du message');
|
|
}
|
|
}
|
|
|
|
Future<MessageModel> sendBroadcast({
|
|
required String organizationId,
|
|
required String subject,
|
|
required String content,
|
|
MessagePriority priority = MessagePriority.normal,
|
|
List<String>? attachments,
|
|
}) async {
|
|
final uri = Uri.parse('${AppConfig.apiBaseUrl}/api/messaging/broadcast');
|
|
|
|
final body = json.encode({
|
|
'organizationId': organizationId,
|
|
'subject': subject,
|
|
'content': content,
|
|
'priority': priority.name,
|
|
if (attachments != null) 'attachments': attachments,
|
|
});
|
|
|
|
final response = await client.post(
|
|
uri,
|
|
headers: await _getHeaders(),
|
|
body: body,
|
|
);
|
|
|
|
if (response.statusCode == 201 || response.statusCode == 200) {
|
|
return MessageModel.fromJson(json.decode(response.body));
|
|
} else if (response.statusCode == 401) {
|
|
throw UnauthorizedException();
|
|
} else if (response.statusCode == 403) {
|
|
throw ForbiddenException('Permission insuffisante pour envoyer un broadcast');
|
|
} else {
|
|
throw ServerException('Erreur lors de l\'envoi du broadcast');
|
|
}
|
|
}
|
|
|
|
// === CONVERSATION ACTIONS ===
|
|
|
|
Future<void> archiveConversation(String conversationId, {bool archive = true}) async {
|
|
final uri = Uri.parse(
|
|
'${AppConfig.apiBaseUrl}/api/conversations/$conversationId/archive')
|
|
.replace(queryParameters: {'archive': archive.toString()});
|
|
|
|
final response = await client.put(uri, headers: await _getHeaders());
|
|
|
|
if (response.statusCode != 200 && response.statusCode != 204) {
|
|
if (response.statusCode == 401) {
|
|
throw UnauthorizedException();
|
|
} else {
|
|
throw ServerException('Erreur lors de l\'archivage de la conversation');
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> markConversationAsRead(String conversationId) async {
|
|
final uri = Uri.parse(
|
|
'${AppConfig.apiBaseUrl}/api/conversations/$conversationId/mark-read');
|
|
|
|
final response = await client.put(uri, headers: await _getHeaders());
|
|
|
|
if (response.statusCode != 200 && response.statusCode != 204) {
|
|
if (response.statusCode == 401) {
|
|
throw UnauthorizedException();
|
|
} else {
|
|
throw ServerException('Erreur lors du marquage de la conversation comme lue');
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> toggleMuteConversation(String conversationId) async {
|
|
final uri = Uri.parse(
|
|
'${AppConfig.apiBaseUrl}/api/conversations/$conversationId/toggle-mute');
|
|
|
|
final response = await client.put(uri, headers: await _getHeaders());
|
|
|
|
if (response.statusCode != 200 && response.statusCode != 204) {
|
|
if (response.statusCode == 401) {
|
|
throw UnauthorizedException();
|
|
} else {
|
|
throw ServerException('Erreur lors du toggle mute de la conversation');
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> togglePinConversation(String conversationId) async {
|
|
final uri = Uri.parse(
|
|
'${AppConfig.apiBaseUrl}/api/conversations/$conversationId/toggle-pin');
|
|
|
|
final response = await client.put(uri, headers: await _getHeaders());
|
|
|
|
if (response.statusCode != 200 && response.statusCode != 204) {
|
|
if (response.statusCode == 401) {
|
|
throw UnauthorizedException();
|
|
} else {
|
|
throw ServerException('Erreur lors du toggle pin de la conversation');
|
|
}
|
|
}
|
|
}
|
|
|
|
// === MESSAGE ACTIONS ===
|
|
|
|
Future<MessageModel> editMessage({
|
|
required String messageId,
|
|
required String newContent,
|
|
}) async {
|
|
final uri = Uri.parse('${AppConfig.apiBaseUrl}/api/messages/$messageId');
|
|
|
|
final body = json.encode({'content': newContent});
|
|
|
|
final response = await client.put(
|
|
uri,
|
|
headers: await _getHeaders(),
|
|
body: body,
|
|
);
|
|
|
|
if (response.statusCode == 200) {
|
|
return MessageModel.fromJson(json.decode(response.body));
|
|
} else if (response.statusCode == 401) {
|
|
throw UnauthorizedException();
|
|
} else if (response.statusCode == 404) {
|
|
throw NotFoundException('Message non trouvé');
|
|
} else {
|
|
throw ServerException('Erreur lors de l\'édition du message');
|
|
}
|
|
}
|
|
|
|
Future<void> deleteMessage(String messageId) async {
|
|
final uri = Uri.parse('${AppConfig.apiBaseUrl}/api/messages/$messageId');
|
|
|
|
final response = await client.delete(uri, headers: await _getHeaders());
|
|
|
|
if (response.statusCode != 200 && response.statusCode != 204) {
|
|
if (response.statusCode == 401) {
|
|
throw UnauthorizedException();
|
|
} else if (response.statusCode == 404) {
|
|
throw NotFoundException('Message non trouvé');
|
|
} else {
|
|
throw ServerException('Erreur lors de la suppression du message');
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> markMessageAsRead(String messageId) async {
|
|
// Backend has no per-message read endpoint — use markConversationAsRead
|
|
if (AppConfig.enableLogging) {
|
|
debugPrint('[Messaging] markMessageAsRead ignored (no per-message endpoint), messageId=$messageId');
|
|
}
|
|
}
|
|
|
|
Future<int> getUnreadCount({String? organizationId}) async {
|
|
try {
|
|
final conversations = await getConversations(organizationId: organizationId);
|
|
return conversations.fold<int>(0, (sum, c) => sum + c.unreadCount);
|
|
} catch (_) {
|
|
return 0;
|
|
}
|
|
}
|
|
}
|