Initial commit: unionflow-mobile-apps
Application Flutter complète (sans build artifacts). Signed-off-by: lions dev Team
This commit is contained in:
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user