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,177 @@
/// Dialog pour approuver une transaction
library approve_dialog;
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
import '../../../../core/validation/validators.dart';
import '../../../../shared/design_system/unionflow_design_system.dart';
import '../../domain/entities/transaction_approval.dart';
import '../bloc/approval_bloc.dart';
import '../bloc/approval_event.dart';
class ApproveDialog extends StatefulWidget {
final TransactionApproval approval;
const ApproveDialog({
super.key,
required this.approval,
});
@override
State<ApproveDialog> createState() => _ApproveDialogState();
}
class _ApproveDialogState extends State<ApproveDialog> {
final _commentController = TextEditingController();
final _formKey = GlobalKey<FormState>();
@override
void dispose() {
_commentController.dispose();
super.dispose();
}
String _getTransactionTypeLabel(TransactionType type) {
switch (type) {
case TransactionType.contribution:
return 'Cotisation';
case TransactionType.deposit:
return 'Dépôt';
case TransactionType.withdrawal:
return 'Retrait';
case TransactionType.transfer:
return 'Transfert';
case TransactionType.solidarity:
return 'Solidarité';
case TransactionType.event:
return 'Événement';
case TransactionType.other:
return 'Autre';
}
}
@override
Widget build(BuildContext context) {
final currencyFormat = NumberFormat.currency(symbol: widget.approval.currency);
return AlertDialog(
title: const Text('Approuver la transaction'),
content: SingleChildScrollView(
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Confirmez-vous l\'approbation de cette transaction ?',
style: AppTypography.bodyTextSmall,
),
const SizedBox(height: SpacingTokens.md),
Container(
padding: const EdgeInsets.all(SpacingTokens.md),
decoration: BoxDecoration(
color: AppColors.lightBackground,
borderRadius: BorderRadius.circular(SpacingTokens.radiusSm),
border: Border.all(color: AppColors.lightBorder),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow(
'Type',
_getTransactionTypeLabel(widget.approval.transactionType),
),
const SizedBox(height: SpacingTokens.sm),
_buildInfoRow(
'Montant',
currencyFormat.format(widget.approval.amount),
valueStyle: AppTypography.actionText.copyWith(
color: AppColors.primaryGreen,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: SpacingTokens.sm),
_buildInfoRow(
'Demandeur',
widget.approval.requesterName,
),
const SizedBox(height: SpacingTokens.sm),
_buildInfoRow(
'Date',
DateFormat('dd/MM/yyyy HH:mm').format(widget.approval.createdAt),
),
],
),
),
const SizedBox(height: SpacingTokens.md),
TextFormField(
controller: _commentController,
decoration: const InputDecoration(
labelText: 'Commentaire (optionnel)',
hintText: 'Ajouter un commentaire...',
border: OutlineInputBorder(),
helperText: 'Maximum 500 caractères',
),
maxLines: 3,
maxLength: 500,
validator: FinanceValidators.approvalComment(),
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Annuler'),
),
ElevatedButton.icon(
onPressed: () {
if (_formKey.currentState!.validate()) {
context.read<ApprovalBloc>().add(
ApproveTransactionEvent(
approvalId: widget.approval.id,
comment: _commentController.text.trim().isEmpty
? null
: _commentController.text.trim(),
),
);
Navigator.of(context).pop();
}
},
icon: const Icon(Icons.check),
label: const Text('Approuver'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.success,
foregroundColor: Colors.white,
),
),
],
);
}
Widget _buildInfoRow(String label, String value, {TextStyle? valueStyle}) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 90,
child: Text(
'$label :',
style: AppTypography.subtitleSmall.copyWith(
fontWeight: FontWeight.w600,
),
),
),
Expanded(
child: Text(
value,
style: valueStyle ?? AppTypography.bodyTextSmall,
),
),
],
);
}
}

View File

@@ -0,0 +1,513 @@
/// Dialog pour créer un budget
library create_budget_dialog;
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../core/validation/validators.dart';
import '../../../../shared/design_system/unionflow_design_system.dart';
import '../../../../shared/widgets/validated_text_field.dart';
import '../../domain/entities/budget.dart';
import '../bloc/budget_bloc.dart';
import '../bloc/budget_event.dart';
class CreateBudgetDialog extends StatefulWidget {
final String organizationId;
const CreateBudgetDialog({
super.key,
required this.organizationId,
});
@override
State<CreateBudgetDialog> createState() => _CreateBudgetDialogState();
}
class _CreateBudgetDialogState extends State<CreateBudgetDialog> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _descriptionController = TextEditingController();
final _yearController = TextEditingController(
text: DateTime.now().year.toString(),
);
BudgetPeriod _selectedPeriod = BudgetPeriod.annual;
int? _selectedMonth;
// Budget lines
final List<_BudgetLineData> _budgetLines = [];
@override
void dispose() {
_nameController.dispose();
_descriptionController.dispose();
_yearController.dispose();
super.dispose();
}
void _addBudgetLine() {
setState(() {
_budgetLines.add(_BudgetLineData());
});
}
void _removeBudgetLine(int index) {
setState(() {
_budgetLines.removeAt(index);
});
}
void _submitForm() {
if (_formKey.currentState!.validate()) {
// Validate at least one budget line
if (_budgetLines.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Veuillez ajouter au moins une ligne budgétaire'),
backgroundColor: Colors.red,
),
);
return;
}
// Build budget lines
final lines = _budgetLines.map((line) {
return BudgetLine(
id: DateTime.now().millisecondsSinceEpoch.toString(),
category: line.category!,
name: line.nameController.text.trim(),
description: line.descriptionController.text.trim(),
amountPlanned: double.parse(line.amountController.text.trim()),
amountRealized: 0.0,
);
}).toList();
// Dispatch create budget event
context.read<BudgetBloc>().add(
CreateBudgetEvent(
name: _nameController.text.trim(),
description: _descriptionController.text.trim().isEmpty
? null
: _descriptionController.text.trim(),
organizationId: widget.organizationId,
period: _selectedPeriod,
year: int.parse(_yearController.text.trim()),
month: _selectedMonth,
lines: lines,
),
);
Navigator.of(context).pop();
}
}
@override
Widget build(BuildContext context) {
return Dialog(
child: Container(
constraints: const BoxConstraints(maxWidth: 600),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Header
Container(
padding: const EdgeInsets.all(SpacingTokens.md),
decoration: BoxDecoration(
color: AppColors.primaryGreen,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(SpacingTokens.radiusMd),
topRight: Radius.circular(SpacingTokens.radiusMd),
),
),
child: Row(
children: [
const Icon(Icons.account_balance, color: Colors.white),
const SizedBox(width: SpacingTokens.sm),
const Text(
'Créer un budget',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const Spacer(),
IconButton(
icon: const Icon(Icons.close, color: Colors.white),
onPressed: () => Navigator.of(context).pop(),
),
],
),
),
// Content
Flexible(
child: SingleChildScrollView(
padding: const EdgeInsets.all(SpacingTokens.md),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Budget name
ValidatedTextField(
controller: _nameController,
labelText: 'Nom du budget *',
hintText: 'Ex: Budget annuel 2026',
validator: FinanceValidators.budgetName(),
textInputAction: TextInputAction.next,
),
const SizedBox(height: SpacingTokens.md),
// Description
ValidatedTextField(
controller: _descriptionController,
labelText: 'Description',
hintText: 'Description du budget...',
validator: FinanceValidators.budgetDescription(),
maxLines: 3,
textInputAction: TextInputAction.next,
),
const SizedBox(height: SpacingTokens.md),
// Period and Year
Row(
children: [
Expanded(
child: ValidatedDropdownField<BudgetPeriod>(
value: _selectedPeriod,
labelText: 'Période *',
items: BudgetPeriod.values.map((period) {
return DropdownMenuItem(
value: period,
child: Text(_getPeriodLabel(period)),
);
}).toList(),
validator: (value) {
if (value == null) {
return 'Période requise';
}
return null;
},
onChanged: (value) {
setState(() {
_selectedPeriod = value!;
if (_selectedPeriod != BudgetPeriod.monthly) {
_selectedMonth = null;
}
});
},
),
),
const SizedBox(width: SpacingTokens.sm),
Expanded(
child: ValidatedTextField(
controller: _yearController,
labelText: 'Année *',
hintText: 'Ex: 2026',
validator: FinanceValidators.fiscalYear(),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
),
),
],
),
// Month (if monthly period)
if (_selectedPeriod == BudgetPeriod.monthly) ...[
const SizedBox(height: SpacingTokens.md),
ValidatedDropdownField<int>(
value: _selectedMonth,
labelText: 'Mois *',
items: List.generate(12, (index) {
final month = index + 1;
return DropdownMenuItem(
value: month,
child: Text(_getMonthLabel(month)),
);
}),
validator: (value) {
if (_selectedPeriod == BudgetPeriod.monthly &&
value == null) {
return 'Mois requis pour budget mensuel';
}
return null;
},
onChanged: (value) {
setState(() {
_selectedMonth = value;
});
},
),
],
const SizedBox(height: SpacingTokens.lg),
// Budget lines section
Row(
children: [
Text(
'Lignes budgétaires',
style: AppTypography.headerSmall.copyWith(
fontWeight: FontWeight.bold,
),
),
const Spacer(),
ElevatedButton.icon(
onPressed: _addBudgetLine,
icon: const Icon(Icons.add, size: 18),
label: const Text('Ajouter'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryGreen,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.md,
vertical: SpacingTokens.sm,
),
),
),
],
),
const SizedBox(height: SpacingTokens.sm),
// Budget lines list
if (_budgetLines.isEmpty)
Container(
padding: const EdgeInsets.all(SpacingTokens.lg),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius:
BorderRadius.circular(SpacingTokens.radiusSm),
border: Border.all(color: Colors.grey.shade300),
),
child: const Center(
child: Text(
'Aucune ligne budgétaire.\nCliquez sur "Ajouter" pour commencer.',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey),
),
),
)
else
..._budgetLines.asMap().entries.map((entry) {
final index = entry.key;
final line = entry.value;
return _BudgetLineWidget(
key: ValueKey(line.id),
lineData: line,
onRemove: () => _removeBudgetLine(index),
);
}),
],
),
),
),
),
// Actions
Container(
padding: const EdgeInsets.all(SpacingTokens.md),
decoration: BoxDecoration(
color: Colors.grey.shade50,
border: Border(
top: BorderSide(color: Colors.grey.shade300),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Annuler'),
),
const SizedBox(width: SpacingTokens.sm),
ElevatedButton.icon(
onPressed: _submitForm,
icon: const Icon(Icons.check),
label: const Text('Créer le budget'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.success,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.lg,
vertical: SpacingTokens.md,
),
),
),
],
),
),
],
),
),
);
}
String _getPeriodLabel(BudgetPeriod period) {
switch (period) {
case BudgetPeriod.monthly:
return 'Mensuel';
case BudgetPeriod.quarterly:
return 'Trimestriel';
case BudgetPeriod.semiannual:
return 'Semestriel';
case BudgetPeriod.annual:
return 'Annuel';
}
}
String _getMonthLabel(int month) {
const months = [
'Janvier',
'Février',
'Mars',
'Avril',
'Mai',
'Juin',
'Juillet',
'Août',
'Septembre',
'Octobre',
'Novembre',
'Décembre'
];
return months[month - 1];
}
}
/// Budget line data holder
class _BudgetLineData {
final String id;
final nameController = TextEditingController();
final descriptionController = TextEditingController();
final amountController = TextEditingController();
BudgetCategory? category;
_BudgetLineData() : id = DateTime.now().millisecondsSinceEpoch.toString();
void dispose() {
nameController.dispose();
descriptionController.dispose();
amountController.dispose();
}
}
/// Budget line widget
class _BudgetLineWidget extends StatefulWidget {
final _BudgetLineData lineData;
final VoidCallback onRemove;
const _BudgetLineWidget({
super.key,
required this.lineData,
required this.onRemove,
});
@override
State<_BudgetLineWidget> createState() => _BudgetLineWidgetState();
}
class _BudgetLineWidgetState extends State<_BudgetLineWidget> {
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.only(bottom: SpacingTokens.md),
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(SpacingTokens.md),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.receipt_long, color: AppColors.primaryGreen),
const SizedBox(width: SpacingTokens.sm),
const Text(
'Ligne budgétaire',
style: TextStyle(fontWeight: FontWeight.bold),
),
const Spacer(),
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: widget.onRemove,
tooltip: 'Supprimer',
),
],
),
const SizedBox(height: SpacingTokens.md),
// Category
ValidatedDropdownField<BudgetCategory>(
value: widget.lineData.category,
labelText: 'Catégorie *',
items: BudgetCategory.values.map((category) {
return DropdownMenuItem(
value: category,
child: Text(_getCategoryLabel(category)),
);
}).toList(),
validator: (value) {
if (value == null) {
return 'Catégorie requise';
}
return null;
},
onChanged: (value) {
setState(() {
widget.lineData.category = value;
});
},
),
const SizedBox(height: SpacingTokens.sm),
// Name
ValidatedTextField(
controller: widget.lineData.nameController,
labelText: 'Nom *',
hintText: 'Ex: Cotisations mensuelles',
validator: FinanceValidators.budgetLineName(),
textInputAction: TextInputAction.next,
),
const SizedBox(height: SpacingTokens.sm),
// Amount
ValidatedAmountField(
controller: widget.lineData.amountController,
labelText: 'Montant prévu *',
hintText: '0.00',
validator: FinanceValidators.amount(min: 0.01),
),
const SizedBox(height: SpacingTokens.sm),
// Description
ValidatedTextField(
controller: widget.lineData.descriptionController,
labelText: 'Description',
hintText: 'Description de la ligne...',
validator: FinanceValidators.budgetDescription(),
maxLines: 2,
),
],
),
),
);
}
String _getCategoryLabel(BudgetCategory category) {
switch (category) {
case BudgetCategory.contributions:
return 'Cotisations';
case BudgetCategory.savings:
return 'Épargne';
case BudgetCategory.solidarity:
return 'Solidarité';
case BudgetCategory.events:
return 'Événements';
case BudgetCategory.operational:
return 'Opérationnel';
case BudgetCategory.investments:
return 'Investissements';
case BudgetCategory.other:
return 'Autre';
}
}
}

View File

@@ -0,0 +1,173 @@
/// Dialog pour rejeter une transaction
library reject_dialog;
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
import '../../../../core/validation/validators.dart';
import '../../../../shared/design_system/unionflow_design_system.dart';
import '../../domain/entities/transaction_approval.dart';
import '../bloc/approval_bloc.dart';
import '../bloc/approval_event.dart';
class RejectDialog extends StatefulWidget {
final TransactionApproval approval;
const RejectDialog({
super.key,
required this.approval,
});
@override
State<RejectDialog> createState() => _RejectDialogState();
}
class _RejectDialogState extends State<RejectDialog> {
final _reasonController = TextEditingController();
final _formKey = GlobalKey<FormState>();
@override
void dispose() {
_reasonController.dispose();
super.dispose();
}
String _getTransactionTypeLabel(TransactionType type) {
switch (type) {
case TransactionType.contribution:
return 'Cotisation';
case TransactionType.deposit:
return 'Dépôt';
case TransactionType.withdrawal:
return 'Retrait';
case TransactionType.transfer:
return 'Transfert';
case TransactionType.solidarity:
return 'Solidarité';
case TransactionType.event:
return 'Événement';
case TransactionType.other:
return 'Autre';
}
}
@override
Widget build(BuildContext context) {
final currencyFormat = NumberFormat.currency(symbol: widget.approval.currency);
return AlertDialog(
title: const Text('Rejeter la transaction'),
content: SingleChildScrollView(
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Vous êtes sur le point de rejeter cette transaction.',
style: AppTypography.bodyTextSmall.copyWith(
color: AppColors.error,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: SpacingTokens.md),
Container(
padding: const EdgeInsets.all(SpacingTokens.md),
decoration: BoxDecoration(
color: AppColors.lightBackground,
borderRadius: BorderRadius.circular(SpacingTokens.radiusSm),
border: Border.all(color: AppColors.lightBorder),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow(
'Type',
_getTransactionTypeLabel(widget.approval.transactionType),
),
const SizedBox(height: SpacingTokens.sm),
_buildInfoRow(
'Montant',
currencyFormat.format(widget.approval.amount),
valueStyle: AppTypography.actionText.copyWith(
color: AppColors.primaryGreen,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: SpacingTokens.sm),
_buildInfoRow(
'Demandeur',
widget.approval.requesterName,
),
],
),
),
const SizedBox(height: SpacingTokens.md),
TextFormField(
controller: _reasonController,
decoration: const InputDecoration(
labelText: 'Raison du rejet *',
hintText: 'Expliquez la raison du rejet...',
border: OutlineInputBorder(),
helperText: 'Minimum 10 caractères, maximum 500',
),
maxLines: 4,
maxLength: 500,
validator: FinanceValidators.rejectionReason(),
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Annuler'),
),
ElevatedButton.icon(
onPressed: () {
if (_formKey.currentState!.validate()) {
context.read<ApprovalBloc>().add(
RejectTransactionEvent(
approvalId: widget.approval.id,
reason: _reasonController.text.trim(),
),
);
Navigator.of(context).pop();
}
},
icon: const Icon(Icons.close),
label: const Text('Rejeter'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.error,
foregroundColor: Colors.white,
),
),
],
);
}
Widget _buildInfoRow(String label, String value, {TextStyle? valueStyle}) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 90,
child: Text(
'$label :',
style: AppTypography.subtitleSmall.copyWith(
fontWeight: FontWeight.w600,
),
),
),
Expanded(
child: Text(
value,
style: valueStyle ?? AppTypography.bodyTextSmall,
),
),
],
);
}
}