573 lines
18 KiB
Dart
573 lines
18 KiB
Dart
/// Dialogue de création de cotisation
|
|
library create_cotisation_dialog;
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import 'package:intl/intl.dart';
|
|
import '../../bloc/cotisations_bloc.dart';
|
|
import '../../bloc/cotisations_event.dart';
|
|
import '../../data/models/cotisation_model.dart';
|
|
import '../../../members/bloc/membres_bloc.dart';
|
|
import '../../../members/bloc/membres_event.dart';
|
|
import '../../../members/bloc/membres_state.dart';
|
|
import '../../../members/data/models/membre_complete_model.dart';
|
|
|
|
class CreateCotisationDialog extends StatefulWidget {
|
|
const CreateCotisationDialog({super.key});
|
|
|
|
@override
|
|
State<CreateCotisationDialog> createState() => _CreateCotisationDialogState();
|
|
}
|
|
|
|
class _CreateCotisationDialogState extends State<CreateCotisationDialog> {
|
|
final _formKey = GlobalKey<FormState>();
|
|
final _montantController = TextEditingController();
|
|
final _descriptionController = TextEditingController();
|
|
final _searchController = TextEditingController();
|
|
|
|
MembreCompletModel? _selectedMembre;
|
|
TypeCotisation _selectedType = TypeCotisation.annuelle;
|
|
DateTime _dateEcheance = DateTime.now().add(const Duration(days: 30));
|
|
int _annee = DateTime.now().year;
|
|
int? _mois;
|
|
int? _trimestre;
|
|
int? _semestre;
|
|
List<MembreCompletModel> _membresDisponibles = [];
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
context.read<MembresBloc>().add(const LoadActiveMembres());
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_montantController.dispose();
|
|
_descriptionController.dispose();
|
|
_searchController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Dialog(
|
|
child: Container(
|
|
width: MediaQuery.of(context).size.width * 0.9,
|
|
constraints: const BoxConstraints(maxHeight: 600),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
_buildHeader(),
|
|
Expanded(
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Form(
|
|
key: _formKey,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildSectionTitle('Membre'),
|
|
const SizedBox(height: 12),
|
|
_buildMembreSelector(),
|
|
const SizedBox(height: 16),
|
|
|
|
_buildSectionTitle('Type de cotisation'),
|
|
const SizedBox(height: 12),
|
|
_buildTypeDropdown(),
|
|
const SizedBox(height: 12),
|
|
_buildPeriodeFields(),
|
|
const SizedBox(height: 16),
|
|
|
|
_buildSectionTitle('Montant'),
|
|
const SizedBox(height: 12),
|
|
_buildMontantField(),
|
|
const SizedBox(height: 16),
|
|
|
|
_buildSectionTitle('Échéance'),
|
|
const SizedBox(height: 12),
|
|
_buildDateEcheanceField(),
|
|
const SizedBox(height: 16),
|
|
|
|
_buildSectionTitle('Description (optionnel)'),
|
|
const SizedBox(height: 12),
|
|
_buildDescriptionField(),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
_buildActionButtons(),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildHeader() {
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: const BoxDecoration(
|
|
color: Color(0xFFEF4444),
|
|
borderRadius: BorderRadius.only(
|
|
topLeft: Radius.circular(4),
|
|
topRight: Radius.circular(4),
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
const Icon(Icons.add_card, color: Colors.white),
|
|
const SizedBox(width: 12),
|
|
const Text(
|
|
'Créer une cotisation',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const Spacer(),
|
|
IconButton(
|
|
icon: const Icon(Icons.close, color: Colors.white),
|
|
onPressed: () => Navigator.pop(context),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSectionTitle(String title) {
|
|
return Text(
|
|
title,
|
|
style: const TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
color: Color(0xFFEF4444),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildMembreSelector() {
|
|
return BlocBuilder<MembresBloc, MembresState>(
|
|
builder: (context, state) {
|
|
if (state is MembresLoaded) {
|
|
_membresDisponibles = state.membres;
|
|
}
|
|
|
|
if (_selectedMembre != null) {
|
|
return _buildSelectedMembre();
|
|
}
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildSearchField(),
|
|
const SizedBox(height: 12),
|
|
if (_membresDisponibles.isNotEmpty) _buildMembresList(),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildSearchField() {
|
|
return TextFormField(
|
|
controller: _searchController,
|
|
decoration: InputDecoration(
|
|
labelText: 'Rechercher un membre *',
|
|
border: const OutlineInputBorder(),
|
|
prefixIcon: const Icon(Icons.search),
|
|
suffixIcon: _searchController.text.isNotEmpty
|
|
? IconButton(
|
|
icon: const Icon(Icons.clear),
|
|
onPressed: () {
|
|
_searchController.clear();
|
|
context.read<MembresBloc>().add(const LoadActiveMembres());
|
|
},
|
|
)
|
|
: null,
|
|
),
|
|
onChanged: (value) {
|
|
if (value.isNotEmpty) {
|
|
context.read<MembresBloc>().add(LoadMembres(recherche: value));
|
|
} else {
|
|
context.read<MembresBloc>().add(const LoadActiveMembres());
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildMembresList() {
|
|
return Container(
|
|
constraints: const BoxConstraints(maxHeight: 200),
|
|
decoration: BoxDecoration(
|
|
border: Border.all(color: Colors.grey[300]!),
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
child: ListView.builder(
|
|
shrinkWrap: true,
|
|
itemCount: _membresDisponibles.length,
|
|
itemBuilder: (context, index) {
|
|
final membre = _membresDisponibles[index];
|
|
return ListTile(
|
|
leading: CircleAvatar(child: Text(membre.initiales)),
|
|
title: Text(membre.nomComplet),
|
|
subtitle: Text(membre.email),
|
|
onTap: () => setState(() => _selectedMembre = membre),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSelectedMembre() {
|
|
return Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.green[50],
|
|
border: Border.all(color: Colors.green[300]!),
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
CircleAvatar(child: Text(_selectedMembre!.initiales)),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
_selectedMembre!.nomComplet,
|
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
Text(
|
|
_selectedMembre!.email,
|
|
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.close, color: Colors.red),
|
|
onPressed: () => setState(() => _selectedMembre = null),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildTypeDropdown() {
|
|
return DropdownButtonFormField<TypeCotisation>(
|
|
value: _selectedType,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Type *',
|
|
border: OutlineInputBorder(),
|
|
prefixIcon: Icon(Icons.category),
|
|
),
|
|
items: TypeCotisation.values.map((type) {
|
|
return DropdownMenuItem(
|
|
value: type,
|
|
child: Text(_getTypeLabel(type)),
|
|
);
|
|
}).toList(),
|
|
onChanged: (value) {
|
|
setState(() {
|
|
_selectedType = value!;
|
|
_updatePeriodeFields();
|
|
});
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildMontantField() {
|
|
return TextFormField(
|
|
controller: _montantController,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Montant *',
|
|
border: OutlineInputBorder(),
|
|
prefixIcon: Icon(Icons.attach_money),
|
|
suffixText: 'XOF',
|
|
),
|
|
keyboardType: TextInputType.number,
|
|
validator: (value) {
|
|
if (value == null || value.isEmpty) {
|
|
return 'Le montant est obligatoire';
|
|
}
|
|
final montant = double.tryParse(value);
|
|
if (montant == null || montant <= 0) {
|
|
return 'Le montant doit être supérieur à 0';
|
|
}
|
|
return null;
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildPeriodeFields() {
|
|
switch (_selectedType) {
|
|
case TypeCotisation.mensuelle:
|
|
return Row(
|
|
children: [
|
|
Expanded(
|
|
child: DropdownButtonFormField<int>(
|
|
value: _mois,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Mois *',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
items: List.generate(12, (index) {
|
|
final mois = index + 1;
|
|
return DropdownMenuItem(
|
|
value: mois,
|
|
child: Text(_getNomMois(mois)),
|
|
);
|
|
}).toList(),
|
|
onChanged: (value) => setState(() => _mois = value),
|
|
validator: (value) => value == null ? 'Le mois est obligatoire' : null,
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: TextFormField(
|
|
initialValue: _annee.toString(),
|
|
decoration: const InputDecoration(
|
|
labelText: 'Année *',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
keyboardType: TextInputType.number,
|
|
onChanged: (value) => _annee = int.tryParse(value) ?? DateTime.now().year,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
|
|
case TypeCotisation.trimestrielle:
|
|
return Row(
|
|
children: [
|
|
Expanded(
|
|
child: DropdownButtonFormField<int>(
|
|
value: _trimestre,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Trimestre *',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
items: const [
|
|
DropdownMenuItem(value: 1, child: Text('T1 (Jan-Mar)')),
|
|
DropdownMenuItem(value: 2, child: Text('T2 (Avr-Juin)')),
|
|
DropdownMenuItem(value: 3, child: Text('T3 (Juil-Sep)')),
|
|
DropdownMenuItem(value: 4, child: Text('T4 (Oct-Déc)')),
|
|
],
|
|
onChanged: (value) => setState(() => _trimestre = value),
|
|
validator: (value) => value == null ? 'Le trimestre est obligatoire' : null,
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: TextFormField(
|
|
initialValue: _annee.toString(),
|
|
decoration: const InputDecoration(
|
|
labelText: 'Année *',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
keyboardType: TextInputType.number,
|
|
onChanged: (value) => _annee = int.tryParse(value) ?? DateTime.now().year,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
|
|
case TypeCotisation.semestrielle:
|
|
return Row(
|
|
children: [
|
|
Expanded(
|
|
child: DropdownButtonFormField<int>(
|
|
value: _semestre,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Semestre *',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
items: const [
|
|
DropdownMenuItem(value: 1, child: Text('S1 (Jan-Juin)')),
|
|
DropdownMenuItem(value: 2, child: Text('S2 (Juil-Déc)')),
|
|
],
|
|
onChanged: (value) => setState(() => _semestre = value),
|
|
validator: (value) => value == null ? 'Le semestre est obligatoire' : null,
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: TextFormField(
|
|
initialValue: _annee.toString(),
|
|
decoration: const InputDecoration(
|
|
labelText: 'Année *',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
keyboardType: TextInputType.number,
|
|
onChanged: (value) => _annee = int.tryParse(value) ?? DateTime.now().year,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
|
|
case TypeCotisation.annuelle:
|
|
case TypeCotisation.exceptionnelle:
|
|
return TextFormField(
|
|
initialValue: _annee.toString(),
|
|
decoration: const InputDecoration(
|
|
labelText: 'Année *',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
keyboardType: TextInputType.number,
|
|
onChanged: (value) => _annee = int.tryParse(value) ?? DateTime.now().year,
|
|
);
|
|
}
|
|
}
|
|
|
|
Widget _buildDateEcheanceField() {
|
|
return InkWell(
|
|
onTap: () => _selectDateEcheance(context),
|
|
child: InputDecorator(
|
|
decoration: const InputDecoration(
|
|
labelText: 'Date d\'échéance *',
|
|
border: OutlineInputBorder(),
|
|
prefixIcon: Icon(Icons.calendar_today),
|
|
),
|
|
child: Text(DateFormat('dd/MM/yyyy').format(_dateEcheance)),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildDescriptionField() {
|
|
return TextFormField(
|
|
controller: _descriptionController,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Description',
|
|
border: OutlineInputBorder(),
|
|
prefixIcon: Icon(Icons.notes),
|
|
),
|
|
maxLines: 3,
|
|
);
|
|
}
|
|
|
|
Widget _buildActionButtons() {
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[100],
|
|
border: Border(top: BorderSide(color: Colors.grey[300]!)),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: const Text('Annuler'),
|
|
),
|
|
const SizedBox(width: 12),
|
|
ElevatedButton(
|
|
onPressed: _submitForm,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFFEF4444),
|
|
foregroundColor: Colors.white,
|
|
),
|
|
child: const Text('Créer la cotisation'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
String _getTypeLabel(TypeCotisation type) {
|
|
switch (type) {
|
|
case TypeCotisation.annuelle:
|
|
return 'Annuelle';
|
|
case TypeCotisation.mensuelle:
|
|
return 'Mensuelle';
|
|
case TypeCotisation.trimestrielle:
|
|
return 'Trimestrielle';
|
|
case TypeCotisation.semestrielle:
|
|
return 'Semestrielle';
|
|
case TypeCotisation.exceptionnelle:
|
|
return 'Exceptionnelle';
|
|
}
|
|
}
|
|
|
|
String _getNomMois(int mois) {
|
|
const moisFr = [
|
|
'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin',
|
|
'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'
|
|
];
|
|
return (mois >= 1 && mois <= 12) ? moisFr[mois - 1] : 'Mois $mois';
|
|
}
|
|
|
|
void _updatePeriodeFields() {
|
|
_mois = null;
|
|
_trimestre = null;
|
|
_semestre = null;
|
|
|
|
final now = DateTime.now();
|
|
switch (_selectedType) {
|
|
case TypeCotisation.mensuelle:
|
|
_mois = now.month;
|
|
break;
|
|
case TypeCotisation.trimestrielle:
|
|
_trimestre = ((now.month - 1) ~/ 3) + 1;
|
|
break;
|
|
case TypeCotisation.semestrielle:
|
|
_semestre = now.month <= 6 ? 1 : 2;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
Future<void> _selectDateEcheance(BuildContext context) async {
|
|
final DateTime? picked = await showDatePicker(
|
|
context: context,
|
|
initialDate: _dateEcheance,
|
|
firstDate: DateTime.now(),
|
|
lastDate: DateTime.now().add(const Duration(days: 365 * 2)),
|
|
);
|
|
if (picked != null && picked != _dateEcheance) {
|
|
setState(() => _dateEcheance = picked);
|
|
}
|
|
}
|
|
|
|
void _submitForm() {
|
|
if (_formKey.currentState!.validate()) {
|
|
if (_selectedMembre == null) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Veuillez sélectionner un membre'),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
final cotisation = CotisationModel(
|
|
membreId: _selectedMembre!.id!,
|
|
membreNom: _selectedMembre!.nom,
|
|
membrePrenom: _selectedMembre!.prenom,
|
|
type: _selectedType,
|
|
montant: double.parse(_montantController.text),
|
|
dateEcheance: _dateEcheance,
|
|
annee: _annee,
|
|
mois: _mois,
|
|
trimestre: _trimestre,
|
|
semestre: _semestre,
|
|
description: _descriptionController.text.isNotEmpty ? _descriptionController.text : null,
|
|
statut: StatutCotisation.nonPayee,
|
|
);
|
|
|
|
context.read<CotisationsBloc>().add(CreateCotisation(cotisation: cotisation));
|
|
Navigator.pop(context);
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Cotisation créée avec succès'),
|
|
backgroundColor: Colors.green,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|