Files
unionflow-mobile-apps/lib/features/communication/presentation/pages/broadcast_page.dart
dahoud 45dcd2171e 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.)
2026-04-15 20:26:35 +00:00

213 lines
7.3 KiB
Dart

/// Page contacter le bureau — Communication UnionFlow v4
///
/// Remplace l'ancien broadcast par un canal vers le rôle PRESIDENT.
library broadcast_page;
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../shared/design_system/unionflow_design_system.dart';
import '../../../../shared/widgets/core_card.dart';
import '../bloc/messaging_bloc.dart';
import '../bloc/messaging_event.dart';
import '../bloc/messaging_state.dart';
import 'conversation_detail_page.dart';
/// Page pour contacter le bureau (canal rôle PRESIDENT)
class BroadcastPage extends StatefulWidget {
final String organizationId;
const BroadcastPage({
super.key,
required this.organizationId,
});
@override
State<BroadcastPage> createState() => _BroadcastPageState();
}
class _BroadcastPageState extends State<BroadcastPage> {
final _messageController = TextEditingController();
String _selectedRole = 'PRESIDENT';
bool _isSending = false;
static const _roles = [
('PRESIDENT', 'Président'),
('TRESORIER', 'Trésorier'),
('SECRETAIRE', 'Secrétaire'),
('VICE_PRESIDENT', 'Vice-Président'),
('CONSEILLER', 'Conseiller'),
];
@override
void dispose() {
_messageController.dispose();
super.dispose();
}
void _submit() {
final message = _messageController.text.trim();
if (message.isEmpty || _isSending) return;
setState(() => _isSending = true);
context.read<MessagingBloc>().add(
DemarrerConversationRole(
roleCible: _selectedRole,
organisationId: widget.organizationId,
premierMessage: message,
),
);
}
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
return BlocListener<MessagingBloc, MessagingState>(
listener: (context, state) {
if (state is ConversationCreee) {
setState(() => _isSending = false);
// Naviguer vers la conversation créée
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => BlocProvider.value(
value: context.read<MessagingBloc>(),
child: ConversationDetailPage(conversationId: state.conversation.id),
),
),
);
}
if (state is MessagingError) {
setState(() => _isSending = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
backgroundColor: ColorTokens.error,
),
);
}
},
child: Scaffold(
backgroundColor: scheme.surface,
appBar: UFAppBar(
title: 'Contacter le bureau',
moduleGradient: ModuleColors.communicationGradient,
automaticallyImplyLeading: true,
),
body: SafeArea(
top: false,
child: SingleChildScrollView(
padding: const EdgeInsets.all(SpacingTokens.md),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Info
CoreCard(
child: Row(
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: ModuleColors.communication.withOpacity(0.15),
borderRadius: BorderRadius.circular(SpacingTokens.radiusSm),
),
child: const Icon(
Icons.account_circle_outlined,
color: ModuleColors.communication,
size: 24,
),
),
const SizedBox(width: SpacingTokens.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Contacter un responsable', style: AppTypography.actionText),
const SizedBox(height: 2),
Text(
'Votre message sera transmis au titulaire du rôle choisi',
style: AppTypography.subtitleSmall,
),
],
),
),
],
),
),
const SizedBox(height: SpacingTokens.lg),
// Sélection du rôle
Text('Destinataire', style: AppTypography.actionText),
const SizedBox(height: SpacingTokens.sm),
Wrap(
spacing: SpacingTokens.sm,
runSpacing: SpacingTokens.sm,
children: _roles.map((role) {
final isSelected = _selectedRole == role.$1;
return ChoiceChip(
label: Text(role.$2),
selected: isSelected,
selectedColor: ModuleColors.communication.withOpacity(0.2),
labelStyle: AppTypography.bodyTextSmall.copyWith(
color: isSelected ? ModuleColors.communication : null,
fontWeight: isSelected ? FontWeight.w600 : null,
),
onSelected: (_) => setState(() => _selectedRole = role.$1),
);
}).toList(),
),
const SizedBox(height: SpacingTokens.lg),
// Message
Text('Message', style: AppTypography.actionText),
const SizedBox(height: SpacingTokens.sm),
TextField(
controller: _messageController,
maxLines: 6,
minLines: 3,
decoration: InputDecoration(
hintText: 'Écrivez votre message...',
hintStyle: AppTypography.bodyTextSmall.copyWith(
color: scheme.onSurfaceVariant,
),
filled: true,
fillColor: scheme.surfaceContainerHighest.withOpacity(0.3),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
borderSide: BorderSide(color: scheme.outlineVariant),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
borderSide: BorderSide(color: scheme.outlineVariant),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
borderSide: const BorderSide(color: ModuleColors.communication, width: 2),
),
),
style: AppTypography.bodyMedium,
),
const SizedBox(height: SpacingTokens.xl),
SizedBox(
width: double.infinity,
child: UFPrimaryButton(
label: _isSending ? 'Envoi en cours...' : 'Envoyer le message',
onPressed: _isSending ? null : _submit,
icon: _isSending ? null : Icons.send_rounded,
),
),
],
),
),
),
),
);
}
}