566 lines
18 KiB
Dart
566 lines
18 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import '../../../../core/di/injection.dart';
|
|
import '../../../../core/models/cotisation_model.dart';
|
|
import '../../../../core/models/membre_model.dart';
|
|
import '../../../../shared/theme/app_theme.dart';
|
|
import '../../../../shared/widgets/custom_text_field.dart';
|
|
import '../../../../shared/widgets/loading_button.dart';
|
|
|
|
import '../bloc/cotisations_bloc.dart';
|
|
import '../bloc/cotisations_event.dart';
|
|
import '../bloc/cotisations_state.dart';
|
|
|
|
/// Page de création d'une nouvelle cotisation
|
|
class CotisationCreatePage extends StatefulWidget {
|
|
final MembreModel? membre; // Membre pré-sélectionné (optionnel)
|
|
|
|
const CotisationCreatePage({
|
|
super.key,
|
|
this.membre,
|
|
});
|
|
|
|
@override
|
|
State<CotisationCreatePage> createState() => _CotisationCreatePageState();
|
|
}
|
|
|
|
class _CotisationCreatePageState extends State<CotisationCreatePage> {
|
|
final _formKey = GlobalKey<FormState>();
|
|
late CotisationsBloc _cotisationsBloc;
|
|
|
|
// Contrôleurs de champs
|
|
final _montantController = TextEditingController();
|
|
final _descriptionController = TextEditingController();
|
|
final _periodeController = TextEditingController();
|
|
|
|
// Valeurs sélectionnées
|
|
String _typeCotisation = 'MENSUELLE';
|
|
DateTime _dateEcheance = DateTime.now().add(const Duration(days: 30));
|
|
MembreModel? _membreSelectionne;
|
|
|
|
// Options disponibles
|
|
final List<String> _typesCotisation = [
|
|
'MENSUELLE',
|
|
'TRIMESTRIELLE',
|
|
'SEMESTRIELLE',
|
|
'ANNUELLE',
|
|
'EXCEPTIONNELLE',
|
|
];
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_cotisationsBloc = getIt<CotisationsBloc>();
|
|
_membreSelectionne = widget.membre;
|
|
|
|
// Pré-remplir la période selon le type
|
|
_updatePeriodeFromType();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_montantController.dispose();
|
|
_descriptionController.dispose();
|
|
_periodeController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _updatePeriodeFromType() {
|
|
final now = DateTime.now();
|
|
String periode;
|
|
|
|
switch (_typeCotisation) {
|
|
case 'MENSUELLE':
|
|
periode = '${_getMonthName(now.month)} ${now.year}';
|
|
break;
|
|
case 'TRIMESTRIELLE':
|
|
final trimestre = ((now.month - 1) ~/ 3) + 1;
|
|
periode = 'T$trimestre ${now.year}';
|
|
break;
|
|
case 'SEMESTRIELLE':
|
|
final semestre = now.month <= 6 ? 1 : 2;
|
|
periode = 'S$semestre ${now.year}';
|
|
break;
|
|
case 'ANNUELLE':
|
|
periode = '${now.year}';
|
|
break;
|
|
case 'EXCEPTIONNELLE':
|
|
periode = 'Exceptionnelle ${now.day}/${now.month}/${now.year}';
|
|
break;
|
|
default:
|
|
periode = '${now.month}/${now.year}';
|
|
}
|
|
|
|
_periodeController.text = periode;
|
|
}
|
|
|
|
String _getMonthName(int month) {
|
|
const months = [
|
|
'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin',
|
|
'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'
|
|
];
|
|
return months[month - 1];
|
|
}
|
|
|
|
void _onTypeChanged(String? newType) {
|
|
if (newType != null) {
|
|
setState(() {
|
|
_typeCotisation = newType;
|
|
_updatePeriodeFromType();
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<void> _selectDate() async {
|
|
final picked = await showDatePicker(
|
|
context: context,
|
|
initialDate: _dateEcheance,
|
|
firstDate: DateTime.now(),
|
|
lastDate: DateTime.now().add(const Duration(days: 365)),
|
|
locale: const Locale('fr', 'FR'),
|
|
);
|
|
|
|
if (picked != null) {
|
|
setState(() {
|
|
_dateEcheance = picked;
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<void> _selectMembre() async {
|
|
// TODO: Implémenter la sélection de membre
|
|
// Pour l'instant, afficher un message
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Fonctionnalité de sélection de membre à implémenter'),
|
|
backgroundColor: AppTheme.infoColor,
|
|
),
|
|
);
|
|
}
|
|
|
|
void _createCotisation() {
|
|
if (!_formKey.currentState!.validate()) {
|
|
return;
|
|
}
|
|
|
|
if (_membreSelectionne == null) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Veuillez sélectionner un membre'),
|
|
backgroundColor: AppTheme.errorColor,
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
final montant = double.tryParse(_montantController.text.replaceAll(' ', ''));
|
|
if (montant == null || montant <= 0) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Veuillez saisir un montant valide'),
|
|
backgroundColor: AppTheme.errorColor,
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Créer la cotisation
|
|
final cotisation = CotisationModel(
|
|
id: '', // Sera généré par le backend
|
|
numeroReference: '', // Sera généré par le backend
|
|
membreId: _membreSelectionne!.id ?? '',
|
|
nomMembre: _membreSelectionne!.nomComplet,
|
|
typeCotisation: _typeCotisation,
|
|
montantDu: montant,
|
|
montantPaye: 0.0,
|
|
dateEcheance: _dateEcheance,
|
|
statut: 'EN_ATTENTE',
|
|
description: _descriptionController.text.trim(),
|
|
periode: _periodeController.text.trim(),
|
|
annee: _dateEcheance.year,
|
|
mois: _dateEcheance.month,
|
|
codeDevise: 'XOF',
|
|
recurrente: _typeCotisation != 'EXCEPTIONNELLE',
|
|
nombreRappels: 0,
|
|
dateCreation: DateTime.now(),
|
|
dateModification: DateTime.now(),
|
|
);
|
|
|
|
_cotisationsBloc.add(CreateCotisation(cotisation));
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return BlocProvider.value(
|
|
value: _cotisationsBloc,
|
|
child: Scaffold(
|
|
backgroundColor: AppTheme.backgroundLight,
|
|
appBar: AppBar(
|
|
title: const Text('Nouvelle Cotisation'),
|
|
backgroundColor: AppTheme.accentColor,
|
|
foregroundColor: Colors.white,
|
|
elevation: 0,
|
|
),
|
|
body: BlocListener<CotisationsBloc, CotisationsState>(
|
|
listener: (context, state) {
|
|
if (state is CotisationCreated) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Cotisation créée avec succès'),
|
|
backgroundColor: AppTheme.successColor,
|
|
),
|
|
);
|
|
Navigator.of(context).pop(true);
|
|
} else if (state is CotisationsError) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(state.message),
|
|
backgroundColor: AppTheme.errorColor,
|
|
),
|
|
);
|
|
}
|
|
},
|
|
child: Form(
|
|
key: _formKey,
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Sélection du membre
|
|
_buildMembreSection(),
|
|
const SizedBox(height: 24),
|
|
|
|
// Type de cotisation
|
|
_buildTypeSection(),
|
|
const SizedBox(height: 24),
|
|
|
|
// Montant
|
|
_buildMontantSection(),
|
|
const SizedBox(height: 24),
|
|
|
|
// Période et échéance
|
|
_buildPeriodeSection(),
|
|
const SizedBox(height: 24),
|
|
|
|
// Description
|
|
_buildDescriptionSection(),
|
|
const SizedBox(height: 32),
|
|
|
|
// Bouton de création
|
|
_buildCreateButton(),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildMembreSection() {
|
|
return Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'Membre',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppTheme.textPrimary,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
if (_membreSelectionne != null)
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: AppTheme.accentColor.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: AppTheme.accentColor.withOpacity(0.3)),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
CircleAvatar(
|
|
backgroundColor: AppTheme.accentColor,
|
|
child: Text(
|
|
_membreSelectionne!.nomComplet.substring(0, 1).toUpperCase(),
|
|
style: const TextStyle(color: Colors.white),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
_membreSelectionne!.nomComplet,
|
|
style: const TextStyle(
|
|
fontWeight: FontWeight.w600,
|
|
color: AppTheme.textPrimary,
|
|
),
|
|
),
|
|
Text(
|
|
_membreSelectionne!.telephone.isNotEmpty
|
|
? _membreSelectionne!.telephone
|
|
: 'Pas de téléphone',
|
|
style: const TextStyle(
|
|
fontSize: 12,
|
|
color: AppTheme.textSecondary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.change_circle),
|
|
onPressed: _selectMembre,
|
|
color: AppTheme.accentColor,
|
|
),
|
|
],
|
|
),
|
|
)
|
|
else
|
|
ElevatedButton.icon(
|
|
onPressed: _selectMembre,
|
|
icon: const Icon(Icons.person_add),
|
|
label: const Text('Sélectionner un membre'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: AppTheme.accentColor,
|
|
foregroundColor: Colors.white,
|
|
minimumSize: const Size(double.infinity, 48),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildTypeSection() {
|
|
return Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'Type de cotisation',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppTheme.textPrimary,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
DropdownButtonFormField<String>(
|
|
value: _typeCotisation,
|
|
decoration: const InputDecoration(
|
|
border: OutlineInputBorder(),
|
|
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
),
|
|
items: _typesCotisation.map((type) {
|
|
return DropdownMenuItem(
|
|
value: type,
|
|
child: Text(_getTypeLabel(type)),
|
|
);
|
|
}).toList(),
|
|
onChanged: _onTypeChanged,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
String _getTypeLabel(String type) {
|
|
switch (type) {
|
|
case 'MENSUELLE': return 'Mensuelle';
|
|
case 'TRIMESTRIELLE': return 'Trimestrielle';
|
|
case 'SEMESTRIELLE': return 'Semestrielle';
|
|
case 'ANNUELLE': return 'Annuelle';
|
|
case 'EXCEPTIONNELLE': return 'Exceptionnelle';
|
|
default: return type;
|
|
}
|
|
}
|
|
|
|
Widget _buildMontantSection() {
|
|
return Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'Montant',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppTheme.textPrimary,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
CustomTextField(
|
|
controller: _montantController,
|
|
label: 'Montant (XOF)',
|
|
keyboardType: TextInputType.number,
|
|
inputFormatters: [
|
|
FilteringTextInputFormatter.digitsOnly,
|
|
TextInputFormatter.withFunction((oldValue, newValue) {
|
|
// Formater avec des espaces pour les milliers
|
|
final text = newValue.text.replaceAll(' ', '');
|
|
if (text.isEmpty) return newValue;
|
|
|
|
final number = int.tryParse(text);
|
|
if (number == null) return oldValue;
|
|
|
|
final formatted = number.toString().replaceAllMapped(
|
|
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
|
|
(Match m) => '${m[1]} ',
|
|
);
|
|
|
|
return TextEditingValue(
|
|
text: formatted,
|
|
selection: TextSelection.collapsed(offset: formatted.length),
|
|
);
|
|
}),
|
|
],
|
|
validator: (value) {
|
|
if (value == null || value.isEmpty) {
|
|
return 'Veuillez saisir un montant';
|
|
}
|
|
final montant = double.tryParse(value.replaceAll(' ', ''));
|
|
if (montant == null || montant <= 0) {
|
|
return 'Veuillez saisir un montant valide';
|
|
}
|
|
return null;
|
|
},
|
|
suffixIcon: const Icon(Icons.attach_money),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildPeriodeSection() {
|
|
return Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'Période et échéance',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppTheme.textPrimary,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
CustomTextField(
|
|
controller: _periodeController,
|
|
label: 'Période',
|
|
validator: (value) {
|
|
if (value == null || value.isEmpty) {
|
|
return 'Veuillez saisir une période';
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
const SizedBox(height: 16),
|
|
InkWell(
|
|
onTap: _selectDate,
|
|
child: Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
border: Border.all(color: Colors.grey.shade300),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
const Icon(Icons.calendar_today, color: AppTheme.accentColor),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'Date d\'échéance',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: AppTheme.textSecondary,
|
|
),
|
|
),
|
|
Text(
|
|
'${_dateEcheance.day}/${_dateEcheance.month}/${_dateEcheance.year}',
|
|
style: const TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w500,
|
|
color: AppTheme.textPrimary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const Icon(Icons.arrow_drop_down),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildDescriptionSection() {
|
|
return Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'Description (optionnelle)',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppTheme.textPrimary,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
CustomTextField(
|
|
controller: _descriptionController,
|
|
label: 'Description de la cotisation',
|
|
maxLines: 3,
|
|
maxLength: 500,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildCreateButton() {
|
|
return BlocBuilder<CotisationsBloc, CotisationsState>(
|
|
builder: (context, state) {
|
|
final isLoading = state is CotisationsLoading;
|
|
|
|
return LoadingButton(
|
|
onPressed: isLoading ? null : _createCotisation,
|
|
isLoading: isLoading,
|
|
text: 'Créer la cotisation',
|
|
backgroundColor: AppTheme.accentColor,
|
|
textColor: Colors.white,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|