Files
unionflow-server-api/unionflow-mobile-apps/lib/features/notifications/presentation/widgets/notification_card_widget.dart
2025-09-17 17:54:06 +00:00

431 lines
15 KiB
Dart

import 'package:flutter/material.dart';
import '../../../../core/widgets/unified_card.dart';
import '../../../../core/theme/app_colors.dart';
import '../../../../core/theme/app_text_styles.dart';
import '../../../../core/utils/date_formatter.dart';
import '../../domain/entities/notification.dart';
/// Widget de carte pour afficher une notification
class NotificationCardWidget extends StatelessWidget {
final NotificationEntity notification;
final VoidCallback? onTap;
final VoidCallback? onMarkAsRead;
final VoidCallback? onMarkAsImportant;
final VoidCallback? onArchive;
final VoidCallback? onDelete;
final Function(ActionNotification)? onActionTap;
const NotificationCardWidget({
super.key,
required this.notification,
this.onTap,
this.onMarkAsRead,
this.onMarkAsImportant,
this.onArchive,
this.onDelete,
this.onActionTap,
});
@override
Widget build(BuildContext context) {
final isUnread = !notification.estLue;
final isImportant = notification.estImportante;
final isExpired = notification.isExpiree;
return UnifiedCard(
variant: isUnread ? UnifiedCardVariant.elevated : UnifiedCardVariant.outlined,
onTap: onTap,
child: Container(
decoration: BoxDecoration(
border: isUnread
? Border.left(
color: _getTypeColor(),
width: 4,
)
: null,
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// En-tête avec icône, titre et actions
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Icône du type de notification
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: _getTypeColor().withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
_getTypeIcon(),
color: _getTypeColor(),
size: 20,
),
),
const SizedBox(width: 12),
// Titre et métadonnées
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
notification.titre,
style: AppTextStyles.titleSmall.copyWith(
fontWeight: isUnread ? FontWeight.w600 : FontWeight.w500,
color: isExpired
? AppColors.onSurface.withOpacity(0.6)
: AppColors.onSurface,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
// Badges de statut
if (isImportant) ...[
const SizedBox(width: 8),
Icon(
Icons.star,
color: AppColors.warning,
size: 16,
),
],
if (isUnread) ...[
const SizedBox(width: 8),
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: AppColors.primary,
shape: BoxShape.circle,
),
),
],
],
),
const SizedBox(height: 4),
// Métadonnées (expéditeur, date)
Row(
children: [
if (notification.expediteurNom != null) ...[
Text(
notification.expediteurNom!,
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.onSurface.withOpacity(0.7),
fontWeight: FontWeight.w500,
),
),
Text(
'',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.onSurface.withOpacity(0.5),
),
),
],
Text(
notification.tempsEcoule,
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.onSurface.withOpacity(0.7),
),
),
if (isExpired) ...[
Text(
' • Expirée',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.error,
fontWeight: FontWeight.w500,
),
),
],
],
),
],
),
),
// Menu d'actions
PopupMenuButton<String>(
icon: Icon(
Icons.more_vert,
color: AppColors.onSurface.withOpacity(0.6),
size: 20,
),
onSelected: (value) => _handleMenuAction(value),
itemBuilder: (context) => [
if (!notification.estLue)
const PopupMenuItem(
value: 'mark_read',
child: ListTile(
leading: Icon(Icons.mark_email_read, size: 20),
title: Text('Marquer comme lu'),
contentPadding: EdgeInsets.zero,
),
),
PopupMenuItem(
value: 'mark_important',
child: ListTile(
leading: Icon(
notification.estImportante ? Icons.star : Icons.star_border,
size: 20,
),
title: Text(
notification.estImportante
? 'Retirer des importantes'
: 'Marquer comme importante',
),
contentPadding: EdgeInsets.zero,
),
),
if (!notification.estArchivee)
const PopupMenuItem(
value: 'archive',
child: ListTile(
leading: Icon(Icons.archive, size: 20),
title: Text('Archiver'),
contentPadding: EdgeInsets.zero,
),
),
const PopupMenuDivider(),
const PopupMenuItem(
value: 'delete',
child: ListTile(
leading: Icon(Icons.delete, size: 20, color: Colors.red),
title: Text('Supprimer', style: TextStyle(color: Colors.red)),
contentPadding: EdgeInsets.zero,
),
),
],
),
],
),
const SizedBox(height: 12),
// Message de la notification
Text(
notification.messageAffichage,
style: AppTextStyles.bodyMedium.copyWith(
color: isExpired
? AppColors.onSurface.withOpacity(0.6)
: AppColors.onSurface.withOpacity(0.8),
),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
// Image de la notification (si présente)
if (notification.imageUrl != null) ...[
const SizedBox(height: 12),
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
notification.imageUrl!,
height: 120,
width: double.infinity,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) => Container(
height: 120,
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.image_not_supported,
color: AppColors.onSurface.withOpacity(0.5),
),
),
),
),
],
// Actions rapides
if (notification.hasActionsRapides) ...[
const SizedBox(height: 16),
Wrap(
spacing: 8,
runSpacing: 8,
children: notification.actionsRapidesActives
.take(3) // Limite à 3 actions pour éviter l'encombrement
.map((action) => _buildActionButton(action))
.toList(),
),
],
// Tags (si présents)
if (notification.tags != null && notification.tags!.isNotEmpty) ...[
const SizedBox(height: 12),
Wrap(
spacing: 6,
runSpacing: 6,
children: notification.tags!
.take(3) // Limite à 3 tags
.map((tag) => Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: AppColors.surfaceVariant,
borderRadius: BorderRadius.circular(12),
),
child: Text(
tag,
style: AppTextStyles.labelSmall.copyWith(
color: AppColors.onSurfaceVariant,
),
),
))
.toList(),
),
],
],
),
),
),
);
}
Widget _buildActionButton(ActionNotification action) {
return OutlinedButton.icon(
onPressed: () => onActionTap?.call(action),
icon: Icon(
_getActionIcon(action.icone),
size: 16,
),
label: Text(
action.libelle,
style: AppTextStyles.labelMedium,
),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
foregroundColor: action.couleur != null
? Color(int.parse(action.couleur!.replaceFirst('#', '0xFF')))
: AppColors.primary,
side: BorderSide(
color: action.couleur != null
? Color(int.parse(action.couleur!.replaceFirst('#', '0xFF')))
: AppColors.primary,
),
),
);
}
Color _getTypeColor() {
try {
return Color(int.parse(notification.couleurType.replaceFirst('#', '0xFF')));
} catch (e) {
return AppColors.primary;
}
}
IconData _getTypeIcon() {
switch (notification.typeNotification.icone) {
case 'event':
return Icons.event;
case 'payment':
return Icons.payment;
case 'help':
return Icons.help;
case 'person_add':
return Icons.person_add;
case 'cake':
return Icons.cake;
case 'campaign':
return Icons.campaign;
case 'mail':
return Icons.mail;
case 'system_update':
return Icons.system_update;
case 'build':
return Icons.build;
case 'schedule':
return Icons.schedule;
case 'event_busy':
return Icons.event_busy;
case 'check_circle':
return Icons.check_circle;
case 'paid':
return Icons.paid;
case 'error':
return Icons.error;
case 'thumb_up':
return Icons.thumb_up;
case 'volunteer_activism':
return Icons.volunteer_activism;
case 'groups':
return Icons.groups;
case 'alternate_email':
return Icons.alternate_email;
default:
return Icons.notifications;
}
}
IconData _getActionIcon(String? iconeName) {
if (iconeName == null) return Icons.touch_app;
switch (iconeName) {
case 'visibility':
return Icons.visibility;
case 'event_available':
return Icons.event_available;
case 'directions':
return Icons.directions;
case 'payment':
return Icons.payment;
case 'schedule':
return Icons.schedule;
case 'receipt':
return Icons.receipt;
case 'person':
return Icons.person;
case 'message':
return Icons.message;
case 'phone':
return Icons.phone;
case 'reply':
return Icons.reply;
default:
return Icons.touch_app;
}
}
void _handleMenuAction(String action) {
switch (action) {
case 'mark_read':
onMarkAsRead?.call();
break;
case 'mark_important':
onMarkAsImportant?.call();
break;
case 'archive':
onArchive?.call();
break;
case 'delete':
onDelete?.call();
break;
}
}
}