- Ajout système complet de gestion des secteurs avec contours géographiques - Import des contours départementaux depuis GeoJSON - API REST pour la gestion des secteurs (/api/sectors) - Service de géolocalisation pour déterminer les secteurs - Migration base de données avec tables x_departements_contours et sectors_adresses - Interface Flutter pour visualisation et gestion des secteurs - Ajout thème sombre dans l'application - Corrections diverses et optimisations 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
246 lines
11 KiB
Dart
Executable File
246 lines
11 KiB
Dart
Executable File
import 'package:flutter/material.dart';
|
|
import 'package:geosector_app/core/theme/app_theme.dart';
|
|
|
|
/// Widget pour afficher les messages d'une conversation
|
|
class ChatMessages extends StatelessWidget {
|
|
final List<Map<String, dynamic>> messages;
|
|
final int currentUserId;
|
|
final Function(Map<String, dynamic>) onReply;
|
|
|
|
const ChatMessages({
|
|
super.key,
|
|
required this.messages,
|
|
required this.currentUserId,
|
|
required this.onReply,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return messages.isEmpty
|
|
? const Center(
|
|
child: Text('Aucun message dans cette conversation'),
|
|
)
|
|
: ListView.builder(
|
|
padding: const EdgeInsets.all(AppTheme.spacingM),
|
|
itemCount: messages.length,
|
|
reverse:
|
|
false, // Afficher les messages du plus ancien au plus récent
|
|
itemBuilder: (context, index) {
|
|
final message = messages[index];
|
|
final isCurrentUser = message['senderId'] == currentUserId;
|
|
final hasReply = message['replyTo'] != null;
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: AppTheme.spacingM),
|
|
child: Column(
|
|
crossAxisAlignment: isCurrentUser
|
|
? CrossAxisAlignment.end
|
|
: CrossAxisAlignment.start,
|
|
children: [
|
|
// Afficher le message auquel on répond
|
|
if (hasReply) ...[
|
|
Container(
|
|
margin: EdgeInsets.only(
|
|
left: isCurrentUser ? 0 : 40,
|
|
right: isCurrentUser ? 40 : 0,
|
|
bottom: 4,
|
|
),
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[200],
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Réponse à ${message['replyTo']['senderName']}',
|
|
style: const TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 12,
|
|
color: AppTheme.primaryColor,
|
|
),
|
|
),
|
|
Text(
|
|
message['replyTo']['message'],
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.grey[600],
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
|
|
// Message principal
|
|
Row(
|
|
mainAxisAlignment: isCurrentUser
|
|
? MainAxisAlignment.end
|
|
: MainAxisAlignment.start,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Avatar (seulement pour les messages des autres)
|
|
if (!isCurrentUser)
|
|
CircleAvatar(
|
|
radius: 16,
|
|
backgroundColor:
|
|
AppTheme.primaryColor.withOpacity(0.2),
|
|
backgroundImage: message['avatar'] != null
|
|
? AssetImage(message['avatar'] as String)
|
|
: null,
|
|
child: message['avatar'] == null
|
|
? Text(
|
|
message['senderName'].isNotEmpty
|
|
? message['senderName'][0].toUpperCase()
|
|
: '',
|
|
style: const TextStyle(
|
|
color: AppTheme.primaryColor,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 12,
|
|
),
|
|
)
|
|
: null,
|
|
),
|
|
|
|
const SizedBox(width: 8),
|
|
|
|
// Contenu du message
|
|
Flexible(
|
|
child: Column(
|
|
crossAxisAlignment: isCurrentUser
|
|
? CrossAxisAlignment.end
|
|
: CrossAxisAlignment.start,
|
|
children: [
|
|
// Nom de l'expéditeur (seulement pour les messages des autres)
|
|
if (!isCurrentUser)
|
|
Padding(
|
|
padding:
|
|
const EdgeInsets.only(left: 4, bottom: 2),
|
|
child: Text(
|
|
message['senderName'],
|
|
style: const TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 12,
|
|
),
|
|
),
|
|
),
|
|
|
|
// Bulle de message
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 12,
|
|
vertical: 8,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: isCurrentUser
|
|
? AppTheme.primaryColor
|
|
: Colors.white,
|
|
borderRadius: BorderRadius.circular(16),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 3,
|
|
offset: const Offset(0, 1),
|
|
),
|
|
],
|
|
),
|
|
child: Text(
|
|
message['message'],
|
|
style: TextStyle(
|
|
color: isCurrentUser
|
|
? Colors.white
|
|
: Colors.black87,
|
|
),
|
|
),
|
|
),
|
|
|
|
// Heure et statut
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 4, left: 4),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
_formatTime(message['time']),
|
|
style: TextStyle(
|
|
fontSize: 10,
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
const SizedBox(width: 4),
|
|
if (isCurrentUser)
|
|
Icon(
|
|
message['isRead']
|
|
? Icons.done_all
|
|
: Icons.done,
|
|
size: 12,
|
|
color: message['isRead']
|
|
? Colors.blue
|
|
: Colors.grey[600],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(width: 8),
|
|
|
|
// Menu d'actions (seulement pour les messages des autres)
|
|
if (!isCurrentUser)
|
|
PopupMenuButton<String>(
|
|
icon: Icon(
|
|
Icons.more_vert,
|
|
size: 16,
|
|
color: Colors.grey[600],
|
|
),
|
|
padding: EdgeInsets.zero,
|
|
itemBuilder: (context) => [
|
|
const PopupMenuItem<String>(
|
|
value: 'reply',
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.reply, size: 16),
|
|
SizedBox(width: 8),
|
|
Text('Répondre'),
|
|
],
|
|
),
|
|
),
|
|
const PopupMenuItem<String>(
|
|
value: 'copy',
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.content_copy, size: 16),
|
|
SizedBox(width: 8),
|
|
Text('Copier'),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
onSelected: (value) {
|
|
if (value == 'reply') {
|
|
onReply(message);
|
|
} else if (value == 'copy') {
|
|
// Copier le message
|
|
}
|
|
},
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
// Formater l'heure du message
|
|
String _formatTime(DateTime time) {
|
|
return '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}';
|
|
}
|
|
}
|