Initial commit: unionflow-mobile-apps

Application Flutter complète (sans build artifacts).

Signed-off-by: lions dev Team
This commit is contained in:
dahoud
2026-03-15 16:30:08 +00:00
commit d094d6db9c
1790 changed files with 507435 additions and 0 deletions

View File

@@ -0,0 +1,175 @@
/// Dialog de création d'une demande d'adhésion
library create_adhesion_dialog;
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
import '../../../../core/utils/logger.dart';
import '../../bloc/adhesions_bloc.dart';
import '../../data/models/adhesion_model.dart';
import '../../../organizations/data/models/organization_model.dart';
import '../../../organizations/domain/repositories/organization_repository.dart';
import '../../../members/data/models/membre_complete_model.dart';
import '../../../profile/domain/repositories/profile_repository.dart';
class CreateAdhesionDialog extends StatefulWidget {
final VoidCallback onCreated;
const CreateAdhesionDialog({super.key, required this.onCreated});
@override
State<CreateAdhesionDialog> createState() => _CreateAdhesionDialogState();
}
class _CreateAdhesionDialogState extends State<CreateAdhesionDialog> {
final _fraisController = TextEditingController();
String? _organisationId;
bool _loading = false;
bool _isInitLoading = true;
List<OrganizationModel> _organisations = [];
MembreCompletModel? _me;
@override
void initState() {
super.initState();
_loadInitialData();
}
Future<void> _loadInitialData() async {
try {
final user = await GetIt.instance<IProfileRepository>().getMe();
final orgRepo = GetIt.instance<IOrganizationRepository>();
final list = await orgRepo.getOrganizations(page: 0, size: 100);
if (mounted) {
setState(() {
_me = user;
_organisations = list;
_isInitLoading = false;
});
}
} catch (e, st) {
AppLogger.error('CreateAdhesionDialog: chargement profil/organisations échoué', error: e, stackTrace: st);
if (mounted) {
setState(() {
_isInitLoading = false;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Impossible de charger le profil ou les organisations. Réessayez.')),
);
}
}
}
@override
void dispose() {
_fraisController.dispose();
super.dispose();
}
void _submit() {
if (_me == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Profil non chargé, veuillez réessayer')),
);
return;
}
if (_organisationId == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Veuillez sélectionner un membre et une organisation')),
);
return;
}
final frais = double.tryParse(_fraisController.text.replaceAll(',', '.'));
if (frais == null || frais <= 0) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Frais d\'adhésion invalides')),
);
return;
}
setState(() => _loading = true);
final adhesion = AdhesionModel(
membreId: _me!.id,
organisationId: _organisationId,
fraisAdhesion: frais,
codeDevise: 'XOF',
dateDemande: DateTime.now(),
);
context.read<AdhesionsBloc>().add(CreateAdhesion(adhesion));
widget.onCreated();
if (mounted) {
setState(() => _loading = false);
Navigator.of(context).pop();
}
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Nouvelle demande d\'adhésion'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (_isInitLoading)
const CircularProgressIndicator()
else if (_me != null)
TextFormField(
initialValue: '${_me!.prenom} ${_me!.nom}',
decoration: const InputDecoration(
labelText: 'Membre',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.person),
),
enabled: false,
)
else
const Text('Impossible de récupérer votre profil', style: TextStyle(color: Colors.red)),
const SizedBox(height: 16),
DropdownButtonFormField<String>(
value: _organisationId,
isExpanded: true,
decoration: const InputDecoration(
labelText: 'Organisation',
border: OutlineInputBorder(),
),
items: _organisations
.map((o) => DropdownMenuItem<String>(
value: o.id,
child: Text(o.nom, overflow: TextOverflow.ellipsis, maxLines: 1),
))
.toList(),
onChanged: _loading ? null : (v) => setState(() => _organisationId = v),
),
const SizedBox(height: 16),
TextField(
controller: _fraisController,
decoration: const InputDecoration(
labelText: 'Frais d\'adhésion (FCFA)',
border: OutlineInputBorder(),
),
keyboardType: const TextInputType.numberWithOptions(decimal: true),
enabled: !_loading,
),
],
),
),
actions: [
TextButton(
onPressed: _loading ? null : () => Navigator.of(context).pop(),
child: const Text('Annuler'),
),
FilledButton(
onPressed: _loading ? null : _submit,
child: _loading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Créer'),
),
],
);
}
}

View File

@@ -0,0 +1,154 @@
/// Dialog pour enregistrer un paiement sur une adhésion
library paiement_adhesion_dialog;
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../shared/constants/payment_method_assets.dart';
import '../../bloc/adhesions_bloc.dart';
class PaiementAdhesionDialog extends StatefulWidget {
final String adhesionId;
final double montantRestant;
final VoidCallback onPaid;
const PaiementAdhesionDialog({
super.key,
required this.adhesionId,
required this.montantRestant,
required this.onPaid,
});
@override
State<PaiementAdhesionDialog> createState() => _PaiementAdhesionDialogState();
}
class _PaiementAdhesionDialogState extends State<PaiementAdhesionDialog> {
final _montantController = TextEditingController();
final _refController = TextEditingController();
String? _methode;
bool _loading = false;
@override
void initState() {
super.initState();
_montantController.text = widget.montantRestant.toStringAsFixed(0);
}
@override
void dispose() {
_montantController.dispose();
_refController.dispose();
super.dispose();
}
List<DropdownMenuItem<String>> _buildPaymentMethodItems() {
const codes = ['ESPECES', 'VIREMENT', 'WAVE_MONEY', 'ORANGE_MONEY', 'CHEQUE'];
const labels = {'ESPECES': 'Espèces', 'VIREMENT': 'Virement', 'WAVE_MONEY': 'Wave Money', 'ORANGE_MONEY': 'Orange Money', 'CHEQUE': 'Chèque'};
return codes.map((code) {
final label = labels[code] ?? code;
return DropdownMenuItem<String>(
value: code,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
PaymentMethodIcon(paymentMethodCode: code, width: 24, height: 24),
const SizedBox(width: 12),
Text(label),
],
),
);
}).toList();
}
void _submit() {
final montant = double.tryParse(_montantController.text.replaceAll(',', '.'));
if (montant == null || montant <= 0) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Montant invalide')),
);
return;
}
if (montant > widget.montantRestant) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Le montant ne peut pas dépasser le restant dû')),
);
return;
}
setState(() => _loading = true);
context.read<AdhesionsBloc>().add(
EnregistrerPaiementAdhesion(
widget.adhesionId,
montantPaye: montant,
methodePaiement: _methode,
referencePaiement: _refController.text.trim().isEmpty
? null
: _refController.text.trim(),
),
);
widget.onPaid();
if (mounted) {
setState(() => _loading = false);
Navigator.of(context).pop();
}
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Enregistrer un paiement'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Restant dû : ${widget.montantRestant.toStringAsFixed(0)} FCFA'),
const SizedBox(height: 16),
TextField(
controller: _montantController,
decoration: const InputDecoration(
labelText: 'Montant payé (FCFA)',
border: OutlineInputBorder(),
),
keyboardType: const TextInputType.numberWithOptions(decimal: true),
enabled: !_loading,
),
const SizedBox(height: 12),
DropdownButtonFormField<String>(
value: _methode,
decoration: const InputDecoration(
labelText: 'Méthode de paiement',
border: OutlineInputBorder(),
),
items: _buildPaymentMethodItems(),
onChanged: _loading ? null : (v) => setState(() => _methode = v),
),
const SizedBox(height: 12),
TextField(
controller: _refController,
decoration: const InputDecoration(
labelText: 'Référence (optionnel)',
border: OutlineInputBorder(),
),
enabled: !_loading,
),
],
),
),
actions: [
TextButton(
onPressed: _loading ? null : () => Navigator.of(context).pop(),
child: const Text('Annuler'),
),
FilledButton(
onPressed: _loading ? null : _submit,
child: _loading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Enregistrer'),
),
],
);
}
}

View File

@@ -0,0 +1,102 @@
/// Dialog pour rejeter une adhésion (saisie du motif)
library rejet_adhesion_dialog;
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../bloc/adhesions_bloc.dart';
class RejetAdhesionDialog extends StatefulWidget {
final String adhesionId;
final VoidCallback onRejected;
const RejetAdhesionDialog({
super.key,
required this.adhesionId,
required this.onRejected,
});
@override
State<RejetAdhesionDialog> createState() => _RejetAdhesionDialogState();
}
class _RejetAdhesionDialogState extends State<RejetAdhesionDialog> {
final _controller = TextEditingController();
bool _loading = false;
bool _rejectSent = false;
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _submit() {
final motif = _controller.text.trim();
if (motif.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Veuillez saisir un motif de rejet')),
);
return;
}
setState(() {
_loading = true;
_rejectSent = true;
});
context.read<AdhesionsBloc>().add(RejeterAdhesion(widget.adhesionId, motif));
}
@override
Widget build(BuildContext context) {
return BlocListener<AdhesionsBloc, AdhesionsState>(
listenWhen: (_, state) => _rejectSent && (state.status == AdhesionsStatus.loaded || state.status == AdhesionsStatus.error),
listener: (context, state) {
if (!_rejectSent || !mounted) return;
if (state.status == AdhesionsStatus.error) {
setState(() {
_loading = false;
_rejectSent = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.message ?? 'Erreur lors du rejet')),
);
return;
}
if (state.status == AdhesionsStatus.loaded) {
setState(() => _rejectSent = false);
widget.onRejected();
Navigator.of(context).pop();
}
},
child: AlertDialog(
title: const Text('Rejeter la demande'),
content: TextField(
controller: _controller,
decoration: const InputDecoration(
labelText: 'Motif du rejet',
hintText: 'Saisir le motif...',
border: OutlineInputBorder(),
),
maxLines: 3,
enabled: !_loading,
),
actions: [
TextButton(
onPressed: _loading ? null : () => Navigator.of(context).pop(),
child: const Text('Annuler'),
),
FilledButton(
onPressed: _loading ? null : _submit,
style: FilledButton.styleFrom(backgroundColor: Colors.red),
child: _loading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Rejeter'),
),
],
),
);
}
}