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 'dart:io';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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/group.dart';
|
||||||
import '../../models/expense.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 {
|
class AddExpenseDialog extends StatefulWidget {
|
||||||
|
/// The group to which the expense belongs.
|
||||||
final Group group;
|
final Group group;
|
||||||
|
/// The user creating or editing the expense.
|
||||||
final user_state.UserModel currentUser;
|
final user_state.UserModel currentUser;
|
||||||
|
/// The expense to edit (null for new expense).
|
||||||
final Expense? expenseToEdit;
|
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({
|
const AddExpenseDialog({
|
||||||
super.key,
|
super.key,
|
||||||
required this.group,
|
required this.group,
|
||||||
@@ -27,38 +96,52 @@ class AddExpenseDialog extends StatefulWidget {
|
|||||||
State<AddExpenseDialog> createState() => _AddExpenseDialogState();
|
State<AddExpenseDialog> createState() => _AddExpenseDialogState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// State for AddExpenseDialog.
|
||||||
|
///
|
||||||
|
/// Handles form logic, validation, image picking, split calculation, and submission.
|
||||||
class _AddExpenseDialogState extends State<AddExpenseDialog> {
|
class _AddExpenseDialogState extends State<AddExpenseDialog> {
|
||||||
|
/// Form key for validating the expense form.
|
||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
/// Controller for the expense description field.
|
||||||
final _descriptionController = TextEditingController();
|
final _descriptionController = TextEditingController();
|
||||||
|
/// Controller for the expense amount field.
|
||||||
final _amountController = TextEditingController();
|
final _amountController = TextEditingController();
|
||||||
|
/// The selected date for the expense.
|
||||||
late DateTime _selectedDate;
|
late DateTime _selectedDate;
|
||||||
|
/// The selected category for the expense.
|
||||||
late ExpenseCategory _selectedCategory;
|
late ExpenseCategory _selectedCategory;
|
||||||
|
/// The selected currency for the expense.
|
||||||
late ExpenseCurrency _selectedCurrency;
|
late ExpenseCurrency _selectedCurrency;
|
||||||
|
/// The user ID of the payer.
|
||||||
late String _paidById;
|
late String _paidById;
|
||||||
|
/// Map of userId to split amount for each participant.
|
||||||
final Map<String, double> _splits = {};
|
final Map<String, double> _splits = {};
|
||||||
|
/// The selected receipt image file, if any.
|
||||||
File? _receiptImage;
|
File? _receiptImage;
|
||||||
|
/// Whether the dialog is currently submitting data.
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
|
/// Whether the expense is split equally among participants.
|
||||||
bool _splitEqually = true;
|
bool _splitEqually = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
// Initialize form fields and splits based on whether editing or creating
|
||||||
_selectedDate = widget.expenseToEdit?.date ?? DateTime.now();
|
_selectedDate = widget.expenseToEdit?.date ?? DateTime.now();
|
||||||
_selectedCategory = widget.expenseToEdit?.category ?? ExpenseCategory.other;
|
_selectedCategory = widget.expenseToEdit?.category ?? ExpenseCategory.other;
|
||||||
_selectedCurrency = widget.expenseToEdit?.currency ?? ExpenseCurrency.eur;
|
_selectedCurrency = widget.expenseToEdit?.currency ?? ExpenseCurrency.eur;
|
||||||
_paidById = widget.expenseToEdit?.paidById ?? widget.currentUser.id;
|
_paidById = widget.expenseToEdit?.paidById ?? widget.currentUser.id;
|
||||||
|
|
||||||
if (widget.expenseToEdit != null) {
|
if (widget.expenseToEdit != null) {
|
||||||
|
// Editing: pre-fill fields and splits
|
||||||
_descriptionController.text = widget.expenseToEdit!.description;
|
_descriptionController.text = widget.expenseToEdit!.description;
|
||||||
_amountController.text = widget.expenseToEdit!.amount.toString();
|
_amountController.text = widget.expenseToEdit!.amount.toString();
|
||||||
|
|
||||||
for (final split in widget.expenseToEdit!.splits) {
|
for (final split in widget.expenseToEdit!.splits) {
|
||||||
_splits[split.userId] = split.amount;
|
_splits[split.userId] = split.amount;
|
||||||
}
|
}
|
||||||
_splitEqually = false;
|
_splitEqually = false;
|
||||||
} else {
|
} else {
|
||||||
// Initialiser avec tous les membres sélectionnés
|
// Creating: initialize splits for all group members
|
||||||
for (final member in widget.group.members) {
|
for (final member in widget.group.members) {
|
||||||
_splits[member.userId] = 0;
|
_splits[member.userId] = 0;
|
||||||
}
|
}
|
||||||
@@ -67,11 +150,16 @@ class _AddExpenseDialogState extends State<AddExpenseDialog> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
// Dispose controllers to free resources
|
||||||
_descriptionController.dispose();
|
_descriptionController.dispose();
|
||||||
_amountController.dispose();
|
_amountController.dispose();
|
||||||
super.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 {
|
Future<void> _pickImage() async {
|
||||||
final picker = ImagePicker();
|
final picker = ImagePicker();
|
||||||
final pickedFile = await picker.pickImage(
|
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() {
|
void _calculateSplits() {
|
||||||
if (!_splitEqually) return;
|
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 {
|
Future<void> _submit() async {
|
||||||
if (!_formKey.currentState!.validate()) return;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Dialog(
|
return Dialog(
|
||||||
|
|||||||
@@ -1,9 +1,25 @@
|
|||||||
|
/// This file defines the `BalancesTab` widget, which is responsible for displaying a list of user balances
|
||||||
|
/// in a group. Each balance is represented as a card, showing the user's name, the amount they have paid,
|
||||||
|
/// the amount they owe, and their overall balance status (to pay, to receive, or balanced).
|
||||||
|
///
|
||||||
|
/// The widget handles both light and dark themes and provides a fallback UI for empty balance lists.
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../../models/user_balance.dart';
|
import '../../models/user_balance.dart';
|
||||||
|
|
||||||
|
/// A stateless widget that displays a list of user balances in a group.
|
||||||
|
///
|
||||||
|
/// The `BalancesTab` widget takes a list of `UserBalance` objects and renders them as cards in a scrollable list.
|
||||||
|
/// Each card provides detailed information about the user's financial status within the group.
|
||||||
|
///
|
||||||
|
/// If the list of balances is empty, a placeholder message is displayed.
|
||||||
class BalancesTab extends StatelessWidget {
|
class BalancesTab extends StatelessWidget {
|
||||||
|
/// The list of user balances to display.
|
||||||
final List<UserBalance> balances;
|
final List<UserBalance> balances;
|
||||||
|
|
||||||
|
/// Creates a `BalancesTab` widget.
|
||||||
|
///
|
||||||
|
/// The [balances] parameter must not be null.
|
||||||
const BalancesTab({
|
const BalancesTab({
|
||||||
super.key,
|
super.key,
|
||||||
required this.balances,
|
required this.balances,
|
||||||
@@ -11,12 +27,14 @@ class BalancesTab extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
// Check if the balances list is empty and display a placeholder message if true.
|
||||||
if (balances.isEmpty) {
|
if (balances.isEmpty) {
|
||||||
return const Center(
|
return const Center(
|
||||||
child: Text('Aucune balance à afficher'),
|
child: Text('Aucune balance à afficher'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render the list of balances as a scrollable list.
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
itemCount: balances.length,
|
itemCount: balances.length,
|
||||||
@@ -27,13 +45,20 @@ class BalancesTab extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Builds a card widget to display a single user's balance information.
|
||||||
|
///
|
||||||
|
/// The card includes the user's name, their total paid and owed amounts, and their balance status.
|
||||||
|
/// The card's appearance adapts to the app's current theme (light or dark).
|
||||||
Widget _buildBalanceCard(BuildContext context, UserBalance balance) {
|
Widget _buildBalanceCard(BuildContext context, UserBalance balance) {
|
||||||
|
// Determine if the app is in dark mode.
|
||||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
|
// Define variables for the balance's color, icon, and status text.
|
||||||
Color balanceColor;
|
Color balanceColor;
|
||||||
IconData balanceIcon;
|
IconData balanceIcon;
|
||||||
String balanceText;
|
String balanceText;
|
||||||
|
|
||||||
|
// Determine the balance status and corresponding UI elements.
|
||||||
if (balance.shouldReceive) {
|
if (balance.shouldReceive) {
|
||||||
balanceColor = Colors.green;
|
balanceColor = Colors.green;
|
||||||
balanceIcon = Icons.arrow_downward;
|
balanceIcon = Icons.arrow_downward;
|
||||||
@@ -48,12 +73,14 @@ class BalancesTab extends StatelessWidget {
|
|||||||
balanceText = 'Équilibré';
|
balanceText = 'Équilibré';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build and return the card widget.
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
|
// Display the user's initial in a circular avatar.
|
||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
radius: 24,
|
radius: 24,
|
||||||
backgroundColor: isDark ? Colors.grey[800] : Colors.grey[200],
|
backgroundColor: isDark ? Colors.grey[800] : Colors.grey[200],
|
||||||
@@ -68,10 +95,12 @@ class BalancesTab extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
|
// Display the user's name and financial details.
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
// User's name.
|
||||||
Text(
|
Text(
|
||||||
balance.userName,
|
balance.userName,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
@@ -80,6 +109,7 @@ class BalancesTab extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
|
// User's total paid and owed amounts.
|
||||||
Text(
|
Text(
|
||||||
'Payé: ${balance.totalPaid.toStringAsFixed(2)} € • Doit: ${balance.totalOwed.toStringAsFixed(2)} €',
|
'Payé: ${balance.totalPaid.toStringAsFixed(2)} € • Doit: ${balance.totalOwed.toStringAsFixed(2)} €',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@@ -90,13 +120,16 @@ class BalancesTab extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// Display the user's balance status and amount.
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
|
// Icon indicating the balance status.
|
||||||
Icon(balanceIcon, size: 16, color: balanceColor),
|
Icon(balanceIcon, size: 16, color: balanceColor),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
|
// User's absolute balance amount.
|
||||||
Text(
|
Text(
|
||||||
'${balance.absoluteBalance.toStringAsFixed(2)} €',
|
'${balance.absoluteBalance.toStringAsFixed(2)} €',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@@ -107,6 +140,7 @@ class BalancesTab extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
// Text indicating the balance status (e.g., "À recevoir").
|
||||||
Text(
|
Text(
|
||||||
balanceText,
|
balanceText,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
/// This file defines the `ExpenseDetailDialog` widget, which provides a detailed view of a specific expense
|
||||||
|
/// within a group. It allows users to view expense details, such as the amount, payer, date, and splits, and
|
||||||
|
/// perform actions like editing, deleting, or archiving the expense.
|
||||||
|
///
|
||||||
|
/// The dialog is highly interactive and adapts its UI based on the current user's permissions and the state
|
||||||
|
/// of the expense. It also integrates with BLoC for state management and supports features like receipt display
|
||||||
|
/// and split payment marking.
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
@@ -10,10 +18,20 @@ import '../../models/expense.dart';
|
|||||||
import '../../models/group.dart';
|
import '../../models/group.dart';
|
||||||
import 'add_expense_dialog.dart';
|
import 'add_expense_dialog.dart';
|
||||||
|
|
||||||
|
/// A stateless widget that displays detailed information about a specific expense.
|
||||||
|
///
|
||||||
|
/// The `ExpenseDetailDialog` widget shows the expense's amount, payer, date, splits, and receipt. It also
|
||||||
|
/// provides actions for editing, deleting, or archiving the expense, depending on the current user's permissions.
|
||||||
class ExpenseDetailDialog extends StatelessWidget {
|
class ExpenseDetailDialog extends StatelessWidget {
|
||||||
|
/// The expense to display details for.
|
||||||
final Expense expense;
|
final Expense expense;
|
||||||
|
|
||||||
|
/// The group to which the expense belongs.
|
||||||
final Group group;
|
final Group group;
|
||||||
|
|
||||||
|
/// Creates an `ExpenseDetailDialog` widget.
|
||||||
|
///
|
||||||
|
/// The [expense] and [group] parameters must not be null.
|
||||||
const ExpenseDetailDialog({
|
const ExpenseDetailDialog({
|
||||||
super.key,
|
super.key,
|
||||||
required this.expense,
|
required this.expense,
|
||||||
@@ -22,11 +40,13 @@ class ExpenseDetailDialog extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
// Formatters for displaying dates and times.
|
||||||
final dateFormat = DateFormat('dd MMMM yyyy');
|
final dateFormat = DateFormat('dd MMMM yyyy');
|
||||||
final timeFormat = DateFormat('HH:mm');
|
final timeFormat = DateFormat('HH:mm');
|
||||||
|
|
||||||
return BlocBuilder<UserBloc, user_state.UserState>(
|
return BlocBuilder<UserBloc, user_state.UserState>(
|
||||||
builder: (context, userState) {
|
builder: (context, userState) {
|
||||||
|
// Determine the current user and their permissions.
|
||||||
final currentUser = userState is user_state.UserLoaded ? userState.user : null;
|
final currentUser = userState is user_state.UserLoaded ? userState.user : null;
|
||||||
final canEdit = currentUser?.id == expense.paidById;
|
final canEdit = currentUser?.id == expense.paidById;
|
||||||
|
|
||||||
@@ -39,6 +59,7 @@ class ExpenseDetailDialog extends StatelessWidget {
|
|||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
actions: [
|
actions: [
|
||||||
if (canEdit) ...[
|
if (canEdit) ...[
|
||||||
|
// Edit button.
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.edit),
|
icon: const Icon(Icons.edit),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -46,11 +67,13 @@ class ExpenseDetailDialog extends StatelessWidget {
|
|||||||
_showEditDialog(context, currentUser!);
|
_showEditDialog(context, currentUser!);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
// Delete button.
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.delete, color: Colors.red),
|
icon: const Icon(Icons.delete, color: Colors.red),
|
||||||
onPressed: () => _confirmDelete(context),
|
onPressed: () => _confirmDelete(context),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
// Close button.
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.close),
|
icon: const Icon(Icons.close),
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
@@ -60,7 +83,7 @@ class ExpenseDetailDialog extends StatelessWidget {
|
|||||||
body: ListView(
|
body: ListView(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
children: [
|
children: [
|
||||||
// En-tête avec icône
|
// Header with icon and description.
|
||||||
Center(
|
Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -99,7 +122,7 @@ class ExpenseDetailDialog extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// Montant
|
// Amount card.
|
||||||
Card(
|
Card(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
@@ -127,11 +150,11 @@ class ExpenseDetailDialog extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Informations
|
// Information rows.
|
||||||
_buildInfoRow(Icons.person, 'Payé par', expense.paidByName),
|
_buildInfoRow(Icons.person, 'Payé par', expense.paidByName),
|
||||||
_buildInfoRow(Icons.calendar_today, 'Date', dateFormat.format(expense.date)),
|
_buildInfoRow(Icons.calendar_today, 'Date', dateFormat.format(expense.date)),
|
||||||
_buildInfoRow(Icons.access_time, 'Heure', timeFormat.format(expense.createdAt)),
|
_buildInfoRow(Icons.access_time, 'Heure', timeFormat.format(expense.createdAt)),
|
||||||
|
|
||||||
if (expense.isEdited && expense.editedAt != null)
|
if (expense.isEdited && expense.editedAt != null)
|
||||||
_buildInfoRow(
|
_buildInfoRow(
|
||||||
Icons.edit,
|
Icons.edit,
|
||||||
@@ -143,7 +166,7 @@ class ExpenseDetailDialog extends StatelessWidget {
|
|||||||
const Divider(),
|
const Divider(),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
// Divisions
|
// Splits section.
|
||||||
const Text(
|
const Text(
|
||||||
'Répartition',
|
'Répartition',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@@ -156,7 +179,7 @@ class ExpenseDetailDialog extends StatelessWidget {
|
|||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Reçu
|
// Receipt section.
|
||||||
if (expense.receiptUrl != null) ...[
|
if (expense.receiptUrl != null) ...[
|
||||||
const Divider(),
|
const Divider(),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
@@ -188,7 +211,7 @@ class ExpenseDetailDialog extends StatelessWidget {
|
|||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Bouton archiver
|
// Archive button.
|
||||||
if (!expense.isArchived && canEdit)
|
if (!expense.isArchived && canEdit)
|
||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
onPressed: () => _confirmArchive(context),
|
onPressed: () => _confirmArchive(context),
|
||||||
@@ -204,6 +227,7 @@ class ExpenseDetailDialog extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Builds a row displaying an icon, a label, and a value.
|
||||||
Widget _buildInfoRow(IconData icon, String label, String value) {
|
Widget _buildInfoRow(IconData icon, String label, String value) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
@@ -231,6 +255,10 @@ class ExpenseDetailDialog extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Builds a tile displaying details about a split in the expense.
|
||||||
|
///
|
||||||
|
/// The tile shows the user's name, the split amount, and whether the split is paid. If the current user
|
||||||
|
/// is responsible for the split and it is unpaid, a button is provided to mark it as paid.
|
||||||
Widget _buildSplitTile(BuildContext context, ExpenseSplit split) {
|
Widget _buildSplitTile(BuildContext context, ExpenseSplit split) {
|
||||||
return BlocBuilder<UserBloc, user_state.UserState>(
|
return BlocBuilder<UserBloc, user_state.UserState>(
|
||||||
builder: (context, userState) {
|
builder: (context, userState) {
|
||||||
@@ -283,6 +311,7 @@ class ExpenseDetailDialog extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shows a dialog for editing the expense.
|
||||||
void _showEditDialog(BuildContext context, user_state.UserModel currentUser) {
|
void _showEditDialog(BuildContext context, user_state.UserModel currentUser) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -297,6 +326,7 @@ class ExpenseDetailDialog extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shows a confirmation dialog for deleting the expense.
|
||||||
void _confirmDelete(BuildContext context) {
|
void _confirmDelete(BuildContext context) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -324,6 +354,7 @@ class ExpenseDetailDialog extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shows a confirmation dialog for archiving the expense.
|
||||||
void _confirmArchive(BuildContext context) {
|
void _confirmArchive(BuildContext context) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
|
|||||||
Reference in New Issue
Block a user