feat: Enhance documentation for AddExpenseDialog, BalancesTab, and ExpenseDetailDialog with detailed comments and usage examples
This commit is contained in:
@@ -1,3 +1,60 @@
|
||||
/// AddExpenseDialog
|
||||
/// =================
|
||||
///
|
||||
/// A comprehensive dialog widget for creating or editing an expense within a group.
|
||||
/// This dialog supports multi-currency, receipt image upload, flexible splitting (equal or custom),
|
||||
/// and integrates with the ExpenseBloc for state management.
|
||||
///
|
||||
/// ## Features
|
||||
/// - Form for entering expense details: description, amount, currency, category, date, payer, splits
|
||||
/// - Receipt image upload with file size validation (max 5MB)
|
||||
/// - Split expense equally or custom among group members
|
||||
/// - Multi-currency support with automatic conversion to EUR
|
||||
/// - Category selection with icons
|
||||
/// - Date picker for expense date
|
||||
/// - Paid by selection from group members
|
||||
/// - Real-time split calculation and validation
|
||||
/// - Form validation and error feedback
|
||||
/// - Loading state during submission
|
||||
/// - Integrates with ExpenseBloc for create/update actions
|
||||
/// - Handles both creation and editing of expenses
|
||||
///
|
||||
/// ## Usage Example
|
||||
/// ```dart
|
||||
/// showDialog(
|
||||
/// context: context,
|
||||
/// builder: (context) => AddExpenseDialog(
|
||||
/// group: group,
|
||||
/// currentUser: user,
|
||||
/// expenseToEdit: existingExpense, // null for new expense
|
||||
/// ),
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// ## State Management
|
||||
/// - Uses ExpenseBloc for dispatching CreateExpense and UpdateExpense events
|
||||
/// - Reads exchange rates from ExpensesLoaded state for currency conversion
|
||||
///
|
||||
/// ## Validation
|
||||
/// - Description: required
|
||||
/// - Amount: required, positive number
|
||||
/// - Splits: at least one participant, total must match amount
|
||||
/// - Receipt: optional, max 5MB
|
||||
///
|
||||
/// ## Accessibility
|
||||
/// - All form fields have labels and icons
|
||||
/// - Keyboard and screen reader friendly
|
||||
///
|
||||
/// ## Dependencies
|
||||
/// - flutter_bloc
|
||||
/// - image_picker
|
||||
/// - intl
|
||||
/// - Custom models: Expense, ExpenseSplit, Group
|
||||
///
|
||||
/// ## See also
|
||||
/// - Expense
|
||||
/// - Group
|
||||
/// - ExpenseBloc
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -11,11 +68,23 @@ import '../../blocs/user/user_state.dart' as user_state;
|
||||
import '../../models/group.dart';
|
||||
import '../../models/expense.dart';
|
||||
|
||||
/// A dialog for creating or editing an expense in a group.
|
||||
///
|
||||
/// Accepts the group, current user, and optionally an expense to edit.
|
||||
/// Shows a form for all expense details, supports receipt upload, and flexible splitting.
|
||||
class AddExpenseDialog extends StatefulWidget {
|
||||
/// The group to which the expense belongs.
|
||||
final Group group;
|
||||
/// The user creating or editing the expense.
|
||||
final user_state.UserModel currentUser;
|
||||
/// The expense to edit (null for new expense).
|
||||
final Expense? expenseToEdit;
|
||||
|
||||
/// Creates an AddExpenseDialog.
|
||||
///
|
||||
/// [group] is the group for the expense.
|
||||
/// [currentUser] is the user creating/editing.
|
||||
/// [expenseToEdit] is the expense to edit, or null for new.
|
||||
const AddExpenseDialog({
|
||||
super.key,
|
||||
required this.group,
|
||||
@@ -27,38 +96,52 @@ class AddExpenseDialog extends StatefulWidget {
|
||||
State<AddExpenseDialog> createState() => _AddExpenseDialogState();
|
||||
}
|
||||
|
||||
/// State for AddExpenseDialog.
|
||||
///
|
||||
/// Handles form logic, validation, image picking, split calculation, and submission.
|
||||
class _AddExpenseDialogState extends State<AddExpenseDialog> {
|
||||
/// Form key for validating the expense form.
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
/// Controller for the expense description field.
|
||||
final _descriptionController = TextEditingController();
|
||||
/// Controller for the expense amount field.
|
||||
final _amountController = TextEditingController();
|
||||
|
||||
/// The selected date for the expense.
|
||||
late DateTime _selectedDate;
|
||||
/// The selected category for the expense.
|
||||
late ExpenseCategory _selectedCategory;
|
||||
/// The selected currency for the expense.
|
||||
late ExpenseCurrency _selectedCurrency;
|
||||
/// The user ID of the payer.
|
||||
late String _paidById;
|
||||
/// Map of userId to split amount for each participant.
|
||||
final Map<String, double> _splits = {};
|
||||
/// The selected receipt image file, if any.
|
||||
File? _receiptImage;
|
||||
/// Whether the dialog is currently submitting data.
|
||||
bool _isLoading = false;
|
||||
/// Whether the expense is split equally among participants.
|
||||
bool _splitEqually = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Initialize form fields and splits based on whether editing or creating
|
||||
_selectedDate = widget.expenseToEdit?.date ?? DateTime.now();
|
||||
_selectedCategory = widget.expenseToEdit?.category ?? ExpenseCategory.other;
|
||||
_selectedCurrency = widget.expenseToEdit?.currency ?? ExpenseCurrency.eur;
|
||||
_paidById = widget.expenseToEdit?.paidById ?? widget.currentUser.id;
|
||||
|
||||
if (widget.expenseToEdit != null) {
|
||||
// Editing: pre-fill fields and splits
|
||||
_descriptionController.text = widget.expenseToEdit!.description;
|
||||
_amountController.text = widget.expenseToEdit!.amount.toString();
|
||||
|
||||
for (final split in widget.expenseToEdit!.splits) {
|
||||
_splits[split.userId] = split.amount;
|
||||
}
|
||||
_splitEqually = false;
|
||||
} else {
|
||||
// Initialiser avec tous les membres sélectionnés
|
||||
// Creating: initialize splits for all group members
|
||||
for (final member in widget.group.members) {
|
||||
_splits[member.userId] = 0;
|
||||
}
|
||||
@@ -67,11 +150,16 @@ class _AddExpenseDialogState extends State<AddExpenseDialog> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Dispose controllers to free resources
|
||||
_descriptionController.dispose();
|
||||
_amountController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Opens the image picker and validates the selected image (max 5MB).
|
||||
///
|
||||
/// If valid, sets [_receiptImage] to the selected file.
|
||||
/// Shows an error message if the file is too large.
|
||||
Future<void> _pickImage() async {
|
||||
final picker = ImagePicker();
|
||||
final pickedFile = await picker.pickImage(
|
||||
@@ -102,6 +190,9 @@ class _AddExpenseDialogState extends State<AddExpenseDialog> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates splits for equal division among selected members.
|
||||
///
|
||||
/// Updates [_splits] map with equal amounts for each selected participant.
|
||||
void _calculateSplits() {
|
||||
if (!_splitEqually) return;
|
||||
|
||||
@@ -119,6 +210,12 @@ class _AddExpenseDialogState extends State<AddExpenseDialog> {
|
||||
});
|
||||
}
|
||||
|
||||
/// Validates the form and submits the expense.
|
||||
///
|
||||
/// - Checks all form fields and splits
|
||||
/// - Converts amount to EUR using exchange rates
|
||||
/// - Dispatches CreateExpense or UpdateExpense event to ExpenseBloc
|
||||
/// - Shows success or error feedback
|
||||
Future<void> _submit() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
@@ -211,6 +308,9 @@ class _AddExpenseDialogState extends State<AddExpenseDialog> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the dialog UI with all form fields and controls.
|
||||
///
|
||||
/// Returns a Dialog widget containing the expense form.
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
|
||||
Reference in New Issue
Block a user