475 lines
13 KiB
Dart
475 lines
13 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import 'package:intl/intl.dart';
|
|
import '../../../../core/di/injection.dart';
|
|
import '../../../../core/models/membre_model.dart';
|
|
import '../../../../core/models/cotisation_model.dart';
|
|
import '../../../../shared/theme/app_theme.dart';
|
|
|
|
import '../bloc/membres_bloc.dart';
|
|
import '../bloc/membres_event.dart';
|
|
import '../bloc/membres_state.dart';
|
|
import '../widgets/membre_info_section.dart';
|
|
import '../widgets/membre_stats_section.dart';
|
|
import '../widgets/membre_cotisations_section.dart';
|
|
import '../widgets/membre_actions_section.dart';
|
|
import '../widgets/membre_delete_dialog.dart';
|
|
import 'membre_edit_page.dart';
|
|
|
|
/// Page de détails complète d'un membre
|
|
class MembreDetailsPage extends StatefulWidget {
|
|
const MembreDetailsPage({
|
|
super.key,
|
|
required this.membreId,
|
|
this.membre,
|
|
});
|
|
|
|
final String membreId;
|
|
final MembreModel? membre;
|
|
|
|
@override
|
|
State<MembreDetailsPage> createState() => _MembreDetailsPageState();
|
|
}
|
|
|
|
class _MembreDetailsPageState extends State<MembreDetailsPage>
|
|
with SingleTickerProviderStateMixin {
|
|
late MembresBloc _membresBloc;
|
|
late TabController _tabController;
|
|
|
|
MembreModel? _currentMembre;
|
|
List<CotisationModel> _cotisations = [];
|
|
bool _isLoadingCotisations = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_membresBloc = getIt<MembresBloc>();
|
|
_tabController = TabController(length: 3, vsync: this);
|
|
_currentMembre = widget.membre;
|
|
|
|
// Charger les détails du membre si pas fourni
|
|
if (_currentMembre == null) {
|
|
_membresBloc.add(LoadMembreById(widget.membreId));
|
|
}
|
|
|
|
// Charger les cotisations du membre
|
|
_loadMemberCotisations();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_tabController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _loadMemberCotisations() async {
|
|
setState(() {
|
|
_isLoadingCotisations = true;
|
|
});
|
|
|
|
try {
|
|
// TODO: Implémenter le chargement des cotisations via le repository
|
|
// final cotisations = await getIt<CotisationRepository>()
|
|
// .getCotisationsByMembre(widget.membreId);
|
|
// setState(() {
|
|
// _cotisations = cotisations;
|
|
// });
|
|
|
|
// Simulation temporaire
|
|
await Future.delayed(const Duration(seconds: 1));
|
|
setState(() {
|
|
_cotisations = _generateMockCotisations();
|
|
});
|
|
} catch (e) {
|
|
// Gérer l'erreur
|
|
debugPrint('Erreur lors du chargement des cotisations: $e');
|
|
} finally {
|
|
setState(() {
|
|
_isLoadingCotisations = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
List<CotisationModel> _generateMockCotisations() {
|
|
// Données de test temporaires
|
|
return [
|
|
CotisationModel(
|
|
id: '1',
|
|
numeroReference: 'COT-2025-001',
|
|
membreId: widget.membreId,
|
|
typeCotisation: 'MENSUELLE',
|
|
periode: 'Janvier 2025',
|
|
montantDu: 25000,
|
|
montantPaye: 25000,
|
|
codeDevise: 'XOF',
|
|
statut: 'PAYEE',
|
|
dateEcheance: DateTime(2025, 1, 31),
|
|
datePaiement: DateTime(2025, 1, 15),
|
|
annee: 2025,
|
|
recurrente: true,
|
|
nombreRappels: 0,
|
|
dateCreation: DateTime(2025, 1, 1),
|
|
),
|
|
CotisationModel(
|
|
id: '2',
|
|
numeroReference: 'COT-2025-002',
|
|
membreId: widget.membreId,
|
|
typeCotisation: 'MENSUELLE',
|
|
periode: 'Février 2025',
|
|
montantDu: 25000,
|
|
montantPaye: 0,
|
|
codeDevise: 'XOF',
|
|
statut: 'EN_ATTENTE',
|
|
dateEcheance: DateTime(2025, 2, 28),
|
|
annee: 2025,
|
|
recurrente: true,
|
|
nombreRappels: 1,
|
|
dateCreation: DateTime(2025, 2, 1),
|
|
),
|
|
];
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return BlocProvider.value(
|
|
value: _membresBloc,
|
|
child: Scaffold(
|
|
backgroundColor: AppTheme.backgroundLight,
|
|
body: BlocConsumer<MembresBloc, MembresState>(
|
|
listener: (context, state) {
|
|
if (state is MembreLoaded) {
|
|
setState(() {
|
|
_currentMembre = state.membre;
|
|
});
|
|
} else if (state is MembresError) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(state.message),
|
|
backgroundColor: AppTheme.errorColor,
|
|
),
|
|
);
|
|
}
|
|
},
|
|
builder: (context, state) {
|
|
if (state is MembresLoading && _currentMembre == null) {
|
|
return const Scaffold(
|
|
body: Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
CircularProgressIndicator(),
|
|
SizedBox(height: 16),
|
|
Text('Chargement des détails...'),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
if (state is MembresError && _currentMembre == null) {
|
|
return Scaffold(
|
|
body: Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.error, size: 64, color: AppTheme.errorColor),
|
|
SizedBox(height: 16),
|
|
Text(state.message),
|
|
SizedBox(height: 16),
|
|
ElevatedButton(
|
|
onPressed: () => _membresBloc.add(LoadMembreById(widget.membreId)),
|
|
child: Text('Réessayer'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
if (_currentMembre == null) {
|
|
return const Scaffold(
|
|
body: Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.person_off, size: 64),
|
|
SizedBox(height: 16),
|
|
Text('Membre non trouvé'),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
return _buildContent();
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildContent() {
|
|
return NestedScrollView(
|
|
headerSliverBuilder: (context, innerBoxIsScrolled) {
|
|
return [
|
|
_buildAppBar(innerBoxIsScrolled),
|
|
_buildMemberHeader(),
|
|
_buildTabBar(),
|
|
];
|
|
},
|
|
body: TabBarView(
|
|
controller: _tabController,
|
|
children: [
|
|
_buildInfoTab(),
|
|
_buildCotisationsTab(),
|
|
_buildStatsTab(),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildAppBar(bool innerBoxIsScrolled) {
|
|
return SliverAppBar(
|
|
expandedHeight: 0,
|
|
floating: true,
|
|
pinned: true,
|
|
backgroundColor: AppTheme.primaryColor,
|
|
foregroundColor: Colors.white,
|
|
title: Text(
|
|
_currentMembre?.nomComplet ?? 'Détails du membre',
|
|
style: const TextStyle(
|
|
fontWeight: FontWeight.w600,
|
|
fontSize: 18,
|
|
),
|
|
),
|
|
actions: [
|
|
IconButton(
|
|
icon: const Icon(Icons.edit),
|
|
onPressed: _editMember,
|
|
tooltip: 'Modifier',
|
|
),
|
|
PopupMenuButton<String>(
|
|
onSelected: _handleMenuAction,
|
|
itemBuilder: (context) => [
|
|
const PopupMenuItem(
|
|
value: 'call',
|
|
child: ListTile(
|
|
leading: Icon(Icons.phone),
|
|
title: Text('Appeler'),
|
|
contentPadding: EdgeInsets.zero,
|
|
),
|
|
),
|
|
const PopupMenuItem(
|
|
value: 'message',
|
|
child: ListTile(
|
|
leading: Icon(Icons.message),
|
|
title: Text('Message'),
|
|
contentPadding: EdgeInsets.zero,
|
|
),
|
|
),
|
|
const PopupMenuItem(
|
|
value: 'export',
|
|
child: ListTile(
|
|
leading: Icon(Icons.download),
|
|
title: Text('Exporter'),
|
|
contentPadding: EdgeInsets.zero,
|
|
),
|
|
),
|
|
const PopupMenuItem(
|
|
value: 'delete',
|
|
child: ListTile(
|
|
leading: Icon(Icons.delete, color: Colors.red),
|
|
title: Text('Supprimer', style: TextStyle(color: Colors.red)),
|
|
contentPadding: EdgeInsets.zero,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildMemberHeader() {
|
|
return SliverToBoxAdapter(
|
|
child: Container(
|
|
color: AppTheme.primaryColor,
|
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 24),
|
|
child: MembreInfoSection(
|
|
membre: _currentMembre!,
|
|
showActions: false,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildTabBar() {
|
|
return SliverPersistentHeader(
|
|
pinned: true,
|
|
delegate: _TabBarDelegate(
|
|
TabBar(
|
|
controller: _tabController,
|
|
labelColor: AppTheme.primaryColor,
|
|
unselectedLabelColor: AppTheme.textSecondary,
|
|
indicatorColor: AppTheme.primaryColor,
|
|
indicatorWeight: 3,
|
|
tabs: const [
|
|
Tab(text: 'Informations'),
|
|
Tab(text: 'Cotisations'),
|
|
Tab(text: 'Statistiques'),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildInfoTab() {
|
|
return SingleChildScrollView(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
children: [
|
|
MembreInfoSection(
|
|
membre: _currentMembre!,
|
|
showActions: true,
|
|
onEdit: _editMember,
|
|
onCall: _callMember,
|
|
onMessage: _messageMember,
|
|
),
|
|
const SizedBox(height: 16),
|
|
MembreActionsSection(
|
|
membre: _currentMembre!,
|
|
onEdit: _editMember,
|
|
onDelete: _deleteMember,
|
|
onExport: _exportMember,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildCotisationsTab() {
|
|
return MembreCotisationsSection(
|
|
membre: _currentMembre!,
|
|
cotisations: _cotisations,
|
|
isLoading: _isLoadingCotisations,
|
|
onRefresh: _loadMemberCotisations,
|
|
);
|
|
}
|
|
|
|
Widget _buildStatsTab() {
|
|
return SingleChildScrollView(
|
|
padding: const EdgeInsets.all(16),
|
|
child: MembreStatsSection(
|
|
membre: _currentMembre!,
|
|
cotisations: _cotisations,
|
|
),
|
|
);
|
|
}
|
|
|
|
void _editMember() async {
|
|
if (widget.membre == null) return;
|
|
|
|
final result = await Navigator.of(context).push(
|
|
MaterialPageRoute(
|
|
builder: (context) => MembreEditPage(membre: widget.membre!),
|
|
),
|
|
);
|
|
|
|
// Si le membre a été modifié avec succès, recharger les données
|
|
if (result == true) {
|
|
_loadMemberCotisations();
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Membre modifié avec succès !'),
|
|
backgroundColor: AppTheme.successColor,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
void _callMember() {
|
|
// TODO: Implémenter l'appel
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Appel - À implémenter')),
|
|
);
|
|
}
|
|
|
|
void _messageMember() {
|
|
// TODO: Implémenter l'envoi de message
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Message - À implémenter')),
|
|
);
|
|
}
|
|
|
|
void _deleteMember() async {
|
|
if (widget.membre == null) return;
|
|
|
|
final result = await showDialog<bool>(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (context) => MembreDeleteDialog(membre: widget.membre!),
|
|
);
|
|
|
|
// Si le membre a été supprimé/désactivé avec succès
|
|
if (result == true && mounted) {
|
|
// Retourner à la liste des membres
|
|
Navigator.of(context).pop();
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Membre traité avec succès !'),
|
|
backgroundColor: AppTheme.successColor,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
void _exportMember() {
|
|
// TODO: Implémenter l'export
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Export - À implémenter')),
|
|
);
|
|
}
|
|
|
|
void _handleMenuAction(String action) {
|
|
switch (action) {
|
|
case 'call':
|
|
_callMember();
|
|
break;
|
|
case 'message':
|
|
_messageMember();
|
|
break;
|
|
case 'export':
|
|
_exportMember();
|
|
break;
|
|
case 'delete':
|
|
_deleteMember();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
class _TabBarDelegate extends SliverPersistentHeaderDelegate {
|
|
const _TabBarDelegate(this.tabBar);
|
|
|
|
final TabBar tabBar;
|
|
|
|
@override
|
|
double get minExtent => tabBar.preferredSize.height;
|
|
|
|
@override
|
|
double get maxExtent => tabBar.preferredSize.height;
|
|
|
|
@override
|
|
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
|
|
return Container(
|
|
color: Colors.white,
|
|
child: tabBar,
|
|
);
|
|
}
|
|
|
|
@override
|
|
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
|
|
return false;
|
|
}
|
|
}
|