diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index d4a4263..7b0e425 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -15,7 +15,7 @@
{
final ErrorService _errorService;
/// Constructor for BalanceBloc.
- ///
+ ///
/// Initializes the bloc with required repositories and optional services.
/// Sets up event handlers for balance-related operations.
- ///
+ ///
/// Args:
/// [balanceRepository]: Repository for balance data operations
/// [expenseRepository]: Repository for expense data operations
@@ -61,7 +62,12 @@ class BalanceBloc extends Bloc {
BalanceService? balanceService,
ErrorService? errorService,
}) : _balanceRepository = balanceRepository,
- _balanceService = balanceService ?? BalanceService(balanceRepository: balanceRepository, expenseRepository: expenseRepository),
+ _balanceService =
+ balanceService ??
+ BalanceService(
+ balanceRepository: balanceRepository,
+ expenseRepository: expenseRepository,
+ ),
_errorService = errorService ?? ErrorService(),
super(BalanceInitial()) {
on(_onLoadGroupBalance);
@@ -70,11 +76,11 @@ class BalanceBloc extends Bloc {
}
/// Handles [LoadGroupBalances] events.
- ///
+ ///
/// Loads and calculates user balances for a specific group along with
/// optimal settlement recommendations. This provides a complete overview
/// of who owes money to whom and the most efficient payment strategy.
- ///
+ ///
/// Args:
/// [event]: The LoadGroupBalances event containing the group ID
/// [emit]: State emitter function
@@ -83,18 +89,22 @@ class BalanceBloc extends Bloc {
Emitter emit,
) async {
try {
- emit(BalanceLoading());
-
+ // Emit empty state initially to avoid infinite spinner
+ emit(const GroupBalancesLoaded(balances: [], settlements: []));
+
// Calculate group user balances
- final userBalances = await _balanceRepository.calculateGroupUserBalances(event.groupId);
-
+ final userBalances = await _balanceRepository.calculateGroupUserBalances(
+ event.groupId,
+ );
+
// Calculate optimal settlements
- final settlements = await _balanceService.calculateOptimalSettlements(event.groupId);
-
- emit(GroupBalancesLoaded(
- balances: userBalances,
- settlements: settlements,
- ));
+ final settlements = await _balanceService.calculateOptimalSettlements(
+ event.groupId,
+ );
+
+ emit(
+ GroupBalancesLoaded(balances: userBalances, settlements: settlements),
+ );
} catch (e) {
_errorService.logError('BalanceBloc', 'Error loading balance: $e');
emit(BalanceError(e.toString()));
@@ -102,11 +112,11 @@ class BalanceBloc extends Bloc {
}
/// Handles [RefreshBalance] events.
- ///
+ ///
/// Refreshes the balance data for a group while trying to maintain the current
/// state when possible to provide a smoother user experience. Only shows loading
/// state if there's no existing balance data.
- ///
+ ///
/// Args:
/// [event]: The RefreshBalance event containing the group ID
/// [emit]: State emitter function
@@ -119,17 +129,20 @@ class BalanceBloc extends Bloc {
if (state is! GroupBalancesLoaded) {
emit(BalanceLoading());
}
-
+
// Calculate group user balances
- final userBalances = await _balanceRepository.calculateGroupUserBalances(event.groupId);
-
+ final userBalances = await _balanceRepository.calculateGroupUserBalances(
+ event.groupId,
+ );
+
// Calculate optimal settlements
- final settlements = await _balanceService.calculateOptimalSettlements(event.groupId);
-
- emit(GroupBalancesLoaded(
- balances: userBalances,
- settlements: settlements,
- ));
+ final settlements = await _balanceService.calculateOptimalSettlements(
+ event.groupId,
+ );
+
+ emit(
+ GroupBalancesLoaded(balances: userBalances, settlements: settlements),
+ );
} catch (e) {
_errorService.logError('BalanceBloc', 'Error refreshing balance: $e');
emit(BalanceError(e.toString()));
@@ -137,11 +150,11 @@ class BalanceBloc extends Bloc {
}
/// Handles [MarkSettlementAsCompleted] events.
- ///
+ ///
/// Records a settlement transaction between two users, marking that
/// a debt has been paid. This updates the balance calculations and
/// automatically refreshes the group balance data to reflect the change.
- ///
+ ///
/// Args:
/// [event]: The MarkSettlementAsCompleted event containing settlement details
/// [emit]: State emitter function
@@ -156,9 +169,9 @@ class BalanceBloc extends Bloc {
toUserId: event.toUserId,
amount: event.amount,
);
-
+
emit(const BalanceOperationSuccess('Settlement marked as completed'));
-
+
// Reload balance after settlement
add(RefreshBalance(event.groupId));
} catch (e) {
@@ -166,4 +179,4 @@ class BalanceBloc extends Bloc {
emit(BalanceError(e.toString()));
}
}
-}
\ No newline at end of file
+}
diff --git a/lib/blocs/expense/expense_bloc.dart b/lib/blocs/expense/expense_bloc.dart
index 9670c2e..0b48977 100644
--- a/lib/blocs/expense/expense_bloc.dart
+++ b/lib/blocs/expense/expense_bloc.dart
@@ -7,7 +7,7 @@ import 'expense_event.dart';
import 'expense_state.dart';
/// BLoC for managing expense operations and state.
-///
+///
/// This BLoC handles expense-related operations including loading expenses,
/// creating new expenses, updating existing ones, deleting expenses, and
/// managing expense splits. It coordinates with the expense repository and
@@ -15,18 +15,18 @@ import 'expense_state.dart';
class ExpenseBloc extends Bloc {
/// Repository for expense data operations.
final ExpenseRepository _expenseRepository;
-
+
/// Service for expense business logic and validation.
final ExpenseService _expenseService;
-
+
/// Service for error handling and logging.
final ErrorService _errorService;
-
+
/// Subscription to the expenses stream for real-time updates.
StreamSubscription? _expensesSubscription;
/// Creates a new [ExpenseBloc] with required dependencies.
- ///
+ ///
/// [expenseRepository] is required for data operations.
/// [expenseService] and [errorService] have default implementations if not provided.
ExpenseBloc({
@@ -34,10 +34,11 @@ class ExpenseBloc extends Bloc {
ExpenseService? expenseService,
ErrorService? errorService,
}) : _expenseRepository = expenseRepository,
- _expenseService = expenseService ?? ExpenseService(expenseRepository: expenseRepository),
+ _expenseService =
+ expenseService ??
+ ExpenseService(expenseRepository: expenseRepository),
_errorService = errorService ?? ErrorService(),
super(ExpenseInitial()) {
-
on(_onLoadExpensesByGroup);
on(_onExpensesUpdated);
on(_onCreateExpense);
@@ -48,7 +49,7 @@ class ExpenseBloc extends Bloc {
}
/// Handles [LoadExpensesByGroup] events.
- ///
+ ///
/// Sets up a stream subscription to receive real-time updates for expenses
/// in the specified group. Cancels any existing subscription before creating a new one.
Future _onLoadExpensesByGroup(
@@ -56,15 +57,18 @@ class ExpenseBloc extends Bloc {
Emitter emit,
) async {
try {
- emit(ExpenseLoading());
-
+ // Emit empty state initially to avoid infinite spinner
+ // The stream will update with actual data when available
+ emit(const ExpensesLoaded(expenses: []));
+
await _expensesSubscription?.cancel();
-
+
_expensesSubscription = _expenseRepository
.getExpensesStream(event.groupId)
.listen(
(expenses) => add(ExpensesUpdated(expenses)),
- onError: (error) => add(ExpensesUpdated([], error: error.toString())),
+ onError: (error) =>
+ add(ExpensesUpdated([], error: error.toString())),
);
} catch (e) {
_errorService.logError('ExpenseBloc', 'Error loading expenses: $e');
@@ -73,10 +77,10 @@ class ExpenseBloc extends Bloc {
}
/// Handles [ExpensesUpdated] events.
- ///
+ ///
/// Processes real-time updates from the expense stream, either emitting
/// the updated expense list or an error state if the stream encountered an error.
- ///
+ ///
/// Args:
/// [event]: The ExpensesUpdated event containing expenses or error information
/// [emit]: State emitter function
@@ -92,11 +96,11 @@ class ExpenseBloc extends Bloc {
}
/// Handles [CreateExpense] events.
- ///
+ ///
/// Creates a new expense with validation and optional receipt image upload.
/// Uses the expense service to handle business logic and validation,
/// including currency conversion and split calculations.
- ///
+ ///
/// Args:
/// [event]: The CreateExpense event containing expense data and optional receipt
/// [emit]: State emitter function
@@ -105,7 +109,10 @@ class ExpenseBloc extends Bloc {
Emitter emit,
) async {
try {
- await _expenseService.createExpenseWithValidation(event.expense, event.receiptImage);
+ await _expenseService.createExpenseWithValidation(
+ event.expense,
+ event.receiptImage,
+ );
emit(const ExpenseOperationSuccess('Expense created successfully'));
} catch (e) {
_errorService.logError('ExpenseBloc', 'Error creating expense: $e');
@@ -114,11 +121,11 @@ class ExpenseBloc extends Bloc {
}
/// Handles [UpdateExpense] events.
- ///
+ ///
/// Updates an existing expense with validation and optional new receipt image.
/// Uses the expense service to handle business logic, validation, and
/// recalculation of splits if expense details change.
- ///
+ ///
/// Args:
/// [event]: The UpdateExpense event containing updated expense data and optional new receipt
/// [emit]: State emitter function
@@ -127,7 +134,10 @@ class ExpenseBloc extends Bloc {
Emitter emit,
) async {
try {
- await _expenseService.updateExpenseWithValidation(event.expense, event.newReceiptImage);
+ await _expenseService.updateExpenseWithValidation(
+ event.expense,
+ event.newReceiptImage,
+ );
emit(const ExpenseOperationSuccess('Expense updated successfully'));
} catch (e) {
_errorService.logError('ExpenseBloc', 'Error updating expense: $e');
@@ -136,10 +146,10 @@ class ExpenseBloc extends Bloc {
}
/// Handles [DeleteExpense] events.
- ///
+ ///
/// Permanently deletes an expense from the database. This action
/// cannot be undone and will affect group balance calculations.
- ///
+ ///
/// Args:
/// [event]: The DeleteExpense event containing the expense ID to delete
/// [emit]: State emitter function
@@ -157,11 +167,11 @@ class ExpenseBloc extends Bloc {
}
/// Handles [MarkSplitAsPaid] events.
- ///
+ ///
/// Marks a user's portion of an expense split as paid, updating the
/// expense's split information and affecting balance calculations.
/// This helps track who has settled their portion of shared expenses.
- ///
+ ///
/// Args:
/// [event]: The MarkSplitAsPaid event containing expense ID and user ID
/// [emit]: State emitter function
@@ -179,11 +189,11 @@ class ExpenseBloc extends Bloc {
}
/// Handles [ArchiveExpense] events.
- ///
+ ///
/// Archives an expense, moving it out of the active expense list
/// while preserving it for historical records and audit purposes.
/// Archived expenses are not included in current balance calculations.
- ///
+ ///
/// Args:
/// [event]: The ArchiveExpense event containing the expense ID to archive
/// [emit]: State emitter function
@@ -201,7 +211,7 @@ class ExpenseBloc extends Bloc {
}
/// Cleans up resources when the bloc is closed.
- ///
+ ///
/// Cancels the expense stream subscription to prevent memory leaks
/// and ensure proper disposal of resources.
@override
@@ -209,4 +219,4 @@ class ExpenseBloc extends Bloc {
_expensesSubscription?.cancel();
return super.close();
}
-}
\ No newline at end of file
+}
diff --git a/lib/components/account/add_expense_dialog.dart b/lib/components/account/add_expense_dialog.dart
index f50c6fc..0fe0699 100644
--- a/lib/components/account/add_expense_dialog.dart
+++ b/lib/components/account/add_expense_dialog.dart
@@ -56,6 +56,7 @@
/// - Group
/// - ExpenseBloc
library;
+
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -76,8 +77,10 @@ import '../../models/expense.dart';
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;
@@ -103,27 +106,40 @@ class AddExpenseDialog extends StatefulWidget {
class _AddExpenseDialogState extends State {
/// Form key for validating the expense form.
final _formKey = GlobalKey();
+
/// 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 _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;
+ /// Whether the existing receipt has been removed.
+ bool _receiptRemoved = false;
+
@override
void initState() {
super.initState();
@@ -172,7 +188,7 @@ class _AddExpenseDialogState extends State {
if (pickedFile != null) {
final file = File(pickedFile.path);
final fileSize = await file.length();
-
+
if (fileSize > 5 * 1024 * 1024) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
@@ -199,11 +215,11 @@ class _AddExpenseDialogState extends State {
final amount = double.tryParse(_amountController.text) ?? 0;
final selectedMembers = _splits.entries.where((e) => e.value >= 0).toList();
-
+
if (selectedMembers.isEmpty) return;
final splitAmount = amount / selectedMembers.length;
-
+
setState(() {
for (final entry in selectedMembers) {
_splits[entry.key] = splitAmount;
@@ -221,17 +237,14 @@ class _AddExpenseDialogState extends State {
if (!_formKey.currentState!.validate()) return;
final amount = double.parse(_amountController.text);
- final selectedSplits = _splits.entries
- .where((e) => e.value > 0)
- .map((e) {
- final member = widget.group.members.firstWhere((m) => m.userId == e.key);
- return ExpenseSplit(
- userId: e.key,
- userName: member.firstName,
- amount: e.value,
- );
- })
- .toList();
+ final selectedSplits = _splits.entries.where((e) => e.value > 0).map((e) {
+ final member = widget.group.members.firstWhere((m) => m.userId == e.key);
+ return ExpenseSplit(
+ userId: e.key,
+ userName: member.firstName,
+ amount: e.value,
+ );
+ }).toList();
if (selectedSplits.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
@@ -248,11 +261,15 @@ class _AddExpenseDialogState extends State {
try {
// Convertir en EUR
final amountInEur = context.read().state is ExpensesLoaded
- ? (context.read().state as ExpensesLoaded)
- .exchangeRates[_selectedCurrency]! * amount
+ ? ((context.read().state as ExpensesLoaded)
+ .exchangeRates[_selectedCurrency.code] ??
+ 1.0) *
+ amount
: amount;
- final payer = widget.group.members.firstWhere((m) => m.userId == _paidById);
+ final payer = widget.group.members.firstWhere(
+ (m) => m.userId == _paidById,
+ );
final expense = Expense(
id: widget.expenseToEdit?.id ?? '',
@@ -266,29 +283,29 @@ class _AddExpenseDialogState extends State {
paidByName: payer.firstName,
splits: selectedSplits,
date: _selectedDate,
- receiptUrl: widget.expenseToEdit?.receiptUrl,
+ receiptUrl: _receiptRemoved ? null : widget.expenseToEdit?.receiptUrl,
createdAt: widget.expenseToEdit?.createdAt ?? DateTime.now(),
);
if (widget.expenseToEdit == null) {
- context.read().add(CreateExpense(
- expense: expense,
- receiptImage: _receiptImage,
- ));
+ context.read().add(
+ CreateExpense(expense: expense, receiptImage: _receiptImage),
+ );
} else {
- context.read().add(UpdateExpense(
- expense: expense,
- newReceiptImage: _receiptImage,
- ));
+ context.read().add(
+ UpdateExpense(expense: expense, newReceiptImage: _receiptImage),
+ );
}
if (mounted) {
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
- content: Text(widget.expenseToEdit == null
- ? 'Dépense ajoutée'
- : 'Dépense modifiée'),
+ content: Text(
+ widget.expenseToEdit == null
+ ? 'Dépense ajoutée'
+ : 'Dépense modifiée',
+ ),
backgroundColor: Colors.green,
),
);
@@ -296,10 +313,7 @@ class _AddExpenseDialogState extends State {
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(
- content: Text('Erreur: $e'),
- backgroundColor: Colors.red,
- ),
+ SnackBar(content: Text('Erreur: $e'), backgroundColor: Colors.red),
);
}
} finally {
@@ -314,284 +328,425 @@ class _AddExpenseDialogState extends State {
/// Returns a Dialog widget containing the expense form.
@override
Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ final isDark = theme.brightness == Brightness.dark;
+
return Dialog(
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
+ backgroundColor: isDark ? theme.scaffoldBackgroundColor : Colors.white,
+ insetPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
child: Container(
- constraints: const BoxConstraints(maxWidth: 600, maxHeight: 700),
- child: Scaffold(
- appBar: AppBar(
- title: Text(widget.expenseToEdit == null
- ? 'Nouvelle dépense'
- : 'Modifier la dépense'),
- automaticallyImplyLeading: false,
- actions: [
- IconButton(
- icon: const Icon(Icons.close),
- onPressed: () => Navigator.of(context).pop(),
+ constraints: const BoxConstraints(maxWidth: 500, maxHeight: 800),
+ child: Column(
+ children: [
+ // Header
+ Padding(
+ padding: const EdgeInsets.fromLTRB(24, 24, 16, 16),
+ child: Row(
+ children: [
+ Expanded(
+ child: Text(
+ widget.expenseToEdit == null
+ ? 'Nouvelle dépense'
+ : 'Modifier la dépense',
+ style: const TextStyle(
+ fontSize: 20,
+ fontWeight: FontWeight.bold,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ ),
+ IconButton(
+ icon: const Icon(Icons.close),
+ onPressed: () => Navigator.of(context).pop(),
+ padding: EdgeInsets.zero,
+ constraints: const BoxConstraints(),
+ ),
+ ],
),
- ],
- ),
- body: Form(
- key: _formKey,
- child: ListView(
- padding: const EdgeInsets.all(16),
- children: [
- // Description
- TextFormField(
- controller: _descriptionController,
- decoration: const InputDecoration(
- labelText: 'Description',
- hintText: 'Ex: Restaurant, Essence...',
- border: OutlineInputBorder(),
- prefixIcon: Icon(Icons.description),
- ),
- validator: (value) {
- if (value == null || value.trim().isEmpty) {
- return 'Veuillez entrer une description';
- }
- return null;
- },
- ),
- const SizedBox(height: 16),
+ ),
+ const Divider(height: 1),
- // Montant et devise
- Row(
+ // Form Content
+ Expanded(
+ child: Form(
+ key: _formKey,
+ child: ListView(
+ padding: const EdgeInsets.all(24),
children: [
- Expanded(
- flex: 2,
- child: TextFormField(
- controller: _amountController,
- decoration: const InputDecoration(
- labelText: 'Montant',
- border: OutlineInputBorder(),
- prefixIcon: Icon(Icons.euro),
+ // Description
+ TextFormField(
+ controller: _descriptionController,
+ decoration: InputDecoration(
+ labelText: 'Description',
+ hintText: 'Ex: Restaurant, Essence...',
+ prefixIcon: const Icon(Icons.description_outlined),
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(8),
),
- keyboardType: const TextInputType.numberWithOptions(decimal: true),
- onChanged: (_) => _calculateSplits(),
- validator: (value) {
- if (value == null || value.isEmpty) {
- return 'Requis';
- }
- if (double.tryParse(value) == null || double.parse(value) <= 0) {
- return 'Montant invalide';
- }
- return null;
- },
- ),
- ),
- const SizedBox(width: 8),
- Expanded(
- child: DropdownButtonFormField(
- initialValue: _selectedCurrency,
- decoration: const InputDecoration(
- labelText: 'Devise',
- border: OutlineInputBorder(),
+ contentPadding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 16,
),
- items: ExpenseCurrency.values.map((currency) {
- return DropdownMenuItem(
- value: currency,
- child: Text(currency.code),
- );
- }).toList(),
- onChanged: (value) {
- if (value != null) {
- setState(() => _selectedCurrency = value);
- }
- },
),
+ validator: (value) {
+ if (value == null || value.trim().isEmpty) {
+ return 'Requis';
+ }
+ return null;
+ },
),
- ],
- ),
- const SizedBox(height: 16),
+ const SizedBox(height: 16),
- // Catégorie
- DropdownButtonFormField(
- initialValue: _selectedCategory,
- decoration: const InputDecoration(
- labelText: 'Catégorie',
- border: OutlineInputBorder(),
- prefixIcon: Icon(Icons.category),
- ),
- items: ExpenseCategory.values.map((category) {
- return DropdownMenuItem(
- value: category,
- child: Row(
- children: [
- Icon(category.icon, size: 20),
- const SizedBox(width: 8),
- Text(category.displayName),
- ],
- ),
- );
- }).toList(),
- onChanged: (value) {
- if (value != null) {
- setState(() => _selectedCategory = value);
- }
- },
- ),
- const SizedBox(height: 16),
-
- // Date
- ListTile(
- leading: const Icon(Icons.calendar_today),
- title: const Text('Date'),
- subtitle: Text(DateFormat('dd/MM/yyyy').format(_selectedDate)),
- shape: RoundedRectangleBorder(
- side: BorderSide(color: Colors.grey[300]!),
- borderRadius: BorderRadius.circular(4),
- ),
- onTap: () async {
- final date = await showDatePicker(
- context: context,
- initialDate: _selectedDate,
- firstDate: DateTime(2020),
- lastDate: DateTime.now().add(const Duration(days: 365)),
- );
- if (date != null) {
- setState(() => _selectedDate = date);
- }
- },
- ),
- const SizedBox(height: 16),
-
- // Payé par
- DropdownButtonFormField(
- initialValue: _paidById,
- decoration: const InputDecoration(
- labelText: 'Payé par',
- border: OutlineInputBorder(),
- prefixIcon: Icon(Icons.person),
- ),
- items: widget.group.members.map((member) {
- return DropdownMenuItem(
- value: member.userId,
- child: Text(member.firstName),
- );
- }).toList(),
- onChanged: (value) {
- if (value != null) {
- setState(() => _paidById = value);
- }
- },
- ),
- const SizedBox(height: 16),
-
- // Division
- Card(
- child: Padding(
- padding: const EdgeInsets.all(12),
- child: Column(
+ // Montant et Devise
+ Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Row(
- children: [
- const Text(
- 'Division',
- style: TextStyle(
- fontSize: 16,
- fontWeight: FontWeight.bold,
+ Expanded(
+ flex: 2,
+ child: TextFormField(
+ controller: _amountController,
+ decoration: InputDecoration(
+ labelText: 'Montant',
+ prefixIcon: const Icon(Icons.euro_symbol),
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(8),
+ ),
+ contentPadding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 16,
),
),
+ keyboardType: const TextInputType.numberWithOptions(
+ decimal: true,
+ ),
+ onChanged: (_) => _calculateSplits(),
+ validator: (value) {
+ if (value == null || value.isEmpty) {
+ return 'Requis';
+ }
+ if (double.tryParse(value) == null ||
+ double.parse(value) <= 0) {
+ return 'Invalide';
+ }
+ return null;
+ },
+ ),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: DropdownButtonFormField(
+ initialValue: _selectedCurrency,
+ decoration: InputDecoration(
+ labelText: 'Devise',
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(8),
+ ),
+ contentPadding: const EdgeInsets.symmetric(
+ horizontal: 12,
+ vertical: 16,
+ ),
+ ),
+ items: ExpenseCurrency.values.map((currency) {
+ return DropdownMenuItem(
+ value: currency,
+ child: Text(currency.code),
+ );
+ }).toList(),
+ onChanged: (value) {
+ if (value != null) {
+ setState(() => _selectedCurrency = value);
+ }
+ },
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 16),
+
+ // Catégorie
+ DropdownButtonFormField(
+ initialValue: _selectedCategory,
+ decoration: InputDecoration(
+ labelText: 'Catégorie',
+ prefixIcon: Icon(_selectedCategory.icon),
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(8),
+ ),
+ contentPadding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 16,
+ ),
+ ),
+ items: ExpenseCategory.values.map((category) {
+ return DropdownMenuItem(
+ value: category,
+ child: Text(category.displayName),
+ );
+ }).toList(),
+ onChanged: (value) {
+ if (value != null) {
+ setState(() => _selectedCategory = value);
+ }
+ },
+ ),
+ const SizedBox(height: 16),
+
+ // Date
+ InkWell(
+ onTap: () async {
+ final date = await showDatePicker(
+ context: context,
+ initialDate: _selectedDate,
+ firstDate: DateTime(2020),
+ lastDate: DateTime.now().add(
+ const Duration(days: 365),
+ ),
+ );
+ if (date != null) setState(() => _selectedDate = date);
+ },
+ borderRadius: BorderRadius.circular(8),
+ child: InputDecorator(
+ decoration: InputDecoration(
+ labelText: 'Date',
+ prefixIcon: const Icon(Icons.calendar_today_outlined),
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(8),
+ ),
+ contentPadding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 16,
+ ),
+ ),
+ child: Text(
+ DateFormat('dd/MM/yyyy').format(_selectedDate),
+ style: theme.textTheme.bodyLarge,
+ ),
+ ),
+ ),
+ const SizedBox(height: 16),
+
+ // Payé par
+ DropdownButtonFormField(
+ initialValue: _paidById,
+ decoration: InputDecoration(
+ labelText: 'Payé par',
+ prefixIcon: const Icon(Icons.person_outline),
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(8),
+ ),
+ contentPadding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 16,
+ ),
+ ),
+ items: widget.group.members.map((member) {
+ return DropdownMenuItem(
+ value: member.userId,
+ child: Text(member.firstName),
+ );
+ }).toList(),
+ onChanged: (value) {
+ if (value != null) setState(() => _paidById = value);
+ },
+ ),
+ const SizedBox(height: 24),
+
+ // Division Section
+ Container(
+ decoration: BoxDecoration(
+ color: isDark ? Colors.grey[800] : Colors.grey[50],
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(color: theme.dividerColor),
+ ),
+ padding: const EdgeInsets.all(16),
+ child: Column(
+ children: [
+ Row(
+ children: [
+ const Text(
+ 'Division',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const Spacer(),
+ Text(
+ 'Égale',
+ style: TextStyle(
+ color: isDark
+ ? Colors.grey[400]
+ : Colors.grey[600],
+ ),
+ ),
+ const SizedBox(width: 8),
+ Switch(
+ value: _splitEqually,
+ onChanged: (value) {
+ setState(() {
+ _splitEqually = value;
+ if (value) _calculateSplits();
+ });
+ },
+ activeThumbColor: theme.colorScheme.primary,
+ ),
+ ],
+ ),
+ const Divider(),
+ ...widget.group.members.map((member) {
+ final isSelected =
+ _splits.containsKey(member.userId) &&
+ _splits[member.userId]! >= 0;
+
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 4),
+ child: Row(
+ children: [
+ Expanded(
+ child: Text(
+ member.firstName,
+ style: const TextStyle(fontSize: 16),
+ ),
+ ),
+ if (!_splitEqually && isSelected)
+ SizedBox(
+ width: 100,
+ child: TextFormField(
+ initialValue: _splits[member.userId]
+ ?.toStringAsFixed(2),
+ decoration: const InputDecoration(
+ isDense: true,
+ contentPadding: EdgeInsets.symmetric(
+ horizontal: 8,
+ vertical: 8,
+ ),
+ border: OutlineInputBorder(),
+ suffixText: '€',
+ ),
+ keyboardType:
+ const TextInputType.numberWithOptions(
+ decimal: true,
+ ),
+ onChanged: (value) {
+ final amount =
+ double.tryParse(value) ?? 0;
+ setState(
+ () =>
+ _splits[member.userId] = amount,
+ );
+ },
+ ),
+ ),
+ Checkbox(
+ value: isSelected,
+ onChanged: (value) {
+ setState(() {
+ if (value == true) {
+ _splits[member.userId] = 0;
+ if (_splitEqually) _calculateSplits();
+ } else {
+ _splits[member.userId] = -1;
+ }
+ });
+ },
+ activeColor: theme.colorScheme.primary,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(4),
+ ),
+ ),
+ ],
+ ),
+ );
+ }),
+ ],
+ ),
+ ),
+ const SizedBox(height: 16),
+
+ // Reçu (Optional - keeping simple for now as per design focus)
+ if (_receiptImage != null ||
+ (widget.expenseToEdit?.receiptUrl != null &&
+ !_receiptRemoved))
+ Container(
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ border: Border.all(color: theme.dividerColor),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Row(
+ children: [
+ const Icon(Icons.receipt_long, color: Colors.green),
+ const SizedBox(width: 8),
+ const Text('Reçu joint'),
const Spacer(),
- Text(_splitEqually ? 'Égale' : 'Personnalisée'),
- Switch(
- value: _splitEqually,
- onChanged: (value) {
- setState(() {
- _splitEqually = value;
- if (value) _calculateSplits();
- });
- },
+ IconButton(
+ icon: const Icon(Icons.close),
+ onPressed: () => setState(() {
+ _receiptImage = null;
+ _receiptRemoved = true;
+ }),
),
],
),
- const Divider(),
- ...widget.group.members.map((member) {
- final isSelected = _splits.containsKey(member.userId) &&
- _splits[member.userId]! >= 0;
-
- return CheckboxListTile(
- title: Text(member.firstName),
- subtitle: _splitEqually || !isSelected
- ? null
- : TextFormField(
- initialValue: _splits[member.userId]?.toStringAsFixed(2),
- decoration: const InputDecoration(
- labelText: 'Montant',
- isDense: true,
- ),
- keyboardType: const TextInputType.numberWithOptions(decimal: true),
- onChanged: (value) {
- final amount = double.tryParse(value) ?? 0;
- setState(() => _splits[member.userId] = amount);
- },
- ),
- value: isSelected,
- onChanged: (value) {
- setState(() {
- if (value == true) {
- _splits[member.userId] = 0;
- if (_splitEqually) _calculateSplits();
- } else {
- _splits[member.userId] = -1;
- }
- });
- },
- );
- }),
- ],
- ),
- ),
- ),
- const SizedBox(height: 16),
-
- // Reçu
- ListTile(
- leading: const Icon(Icons.receipt),
- title: Text(_receiptImage != null || widget.expenseToEdit?.receiptUrl != null
- ? 'Reçu ajouté'
- : 'Ajouter un reçu'),
- subtitle: _receiptImage != null
- ? const Text('Nouveau reçu sélectionné')
- : null,
- trailing: IconButton(
- icon: const Icon(Icons.add_photo_alternate),
- onPressed: _pickImage,
- ),
- shape: RoundedRectangleBorder(
- side: BorderSide(color: Colors.grey[300]!),
- borderRadius: BorderRadius.circular(4),
- ),
- ),
- const SizedBox(height: 24),
-
- // Boutons
- Row(
- children: [
- Expanded(
- child: OutlinedButton(
- onPressed: _isLoading ? null : () => Navigator.of(context).pop(),
- child: const Text('Annuler'),
+ )
+ else
+ OutlinedButton.icon(
+ onPressed: _pickImage,
+ icon: const Icon(Icons.camera_alt_outlined),
+ label: const Text('Ajouter un reçu'),
+ style: OutlinedButton.styleFrom(
+ padding: const EdgeInsets.symmetric(vertical: 12),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(8),
+ ),
+ ),
),
- ),
- const SizedBox(width: 16),
- Expanded(
- child: ElevatedButton(
- onPressed: _isLoading ? null : _submit,
- child: _isLoading
- ? const SizedBox(
- height: 20,
- width: 20,
- child: CircularProgressIndicator(strokeWidth: 2),
- )
- : Text(widget.expenseToEdit == null ? 'Ajouter' : 'Modifier'),
- ),
- ),
],
),
- ],
+ ),
),
- ),
+
+ // Bottom Button
+ Padding(
+ padding: const EdgeInsets.all(24),
+ child: SizedBox(
+ width: double.infinity,
+ child: ElevatedButton(
+ onPressed: _isLoading ? null : _submit,
+ style: ElevatedButton.styleFrom(
+ padding: const EdgeInsets.symmetric(vertical: 16),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ backgroundColor: theme.colorScheme.primary,
+ foregroundColor: Colors.white,
+ elevation: 0,
+ ),
+ child: _isLoading
+ ? const SizedBox(
+ height: 20,
+ width: 20,
+ child: CircularProgressIndicator(
+ strokeWidth: 2,
+ valueColor: AlwaysStoppedAnimation(
+ Colors.white,
+ ),
+ ),
+ )
+ : Text(
+ widget.expenseToEdit == null
+ ? 'Ajouter'
+ : 'Enregistrer',
+ style: const TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ],
),
),
);
diff --git a/lib/components/account/expenses_tab.dart b/lib/components/account/expenses_tab.dart
index 2270436..4598e94 100644
--- a/lib/components/account/expenses_tab.dart
+++ b/lib/components/account/expenses_tab.dart
@@ -7,11 +7,13 @@ import 'expense_detail_dialog.dart';
class ExpensesTab extends StatelessWidget {
final List expenses;
final Group group;
+ final String currentUserId;
const ExpensesTab({
super.key,
required this.expenses,
required this.group,
+ required this.currentUserId,
});
@override
@@ -48,95 +50,157 @@ class ExpensesTab extends StatelessWidget {
}
Widget _buildExpenseCard(BuildContext context, Expense expense) {
- final isDark = Theme.of(context).brightness == Brightness.dark;
- final dateFormat = DateFormat('dd/MM/yyyy');
+ final theme = Theme.of(context);
+ final isDark = theme.brightness == Brightness.dark;
+ final dateFormat = DateFormat('dd/MM');
- return Card(
+ // Logique pour déterminer l'impact sur l'utilisateur
+ bool isPayer = expense.paidById == currentUserId;
+ double amountToDisplay = expense.amount;
+ bool isPositive = isPayer;
+
+ // Si je suis le payeur, je suis en positif (on me doit de l'argent)
+ // Si je ne suis pas le payeur, je suis en négatif (je dois de l'argent)
+ // Note: Pour être précis, il faudrait calculer ma part exacte, mais pour l'instant
+ // on affiche le total avec la couleur indiquant si j'ai payé ou non.
+
+ final amountColor = isPositive ? Colors.green : Colors.red;
+ final prefix = isPositive ? '+' : '-';
+
+ return Container(
margin: const EdgeInsets.only(bottom: 12),
- child: InkWell(
- onTap: () => _showExpenseDetail(context, expense),
- borderRadius: BorderRadius.circular(12),
- child: Padding(
- padding: const EdgeInsets.all(16),
- child: Row(
- children: [
- Container(
- width: 48,
- height: 48,
- decoration: BoxDecoration(
- color: isDark ? Colors.blue[900] : Colors.blue[100],
- borderRadius: BorderRadius.circular(12),
+ decoration: BoxDecoration(
+ color: isDark ? theme.cardColor : Colors.white,
+ borderRadius: BorderRadius.circular(16),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withValues(alpha: 0.05),
+ blurRadius: 8,
+ offset: const Offset(0, 2),
+ ),
+ ],
+ ),
+ child: Material(
+ color: Colors.transparent,
+ child: InkWell(
+ onTap: () => _showExpenseDetail(context, expense),
+ borderRadius: BorderRadius.circular(16),
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: Row(
+ children: [
+ // Icone circulaire
+ Container(
+ width: 48,
+ height: 48,
+ decoration: BoxDecoration(
+ color: _getCategoryColor(
+ expense.category,
+ ).withValues(alpha: 0.2),
+ shape: BoxShape.circle,
+ ),
+ child: Icon(
+ expense.category.icon,
+ color: _getCategoryColor(expense.category),
+ size: 24,
+ ),
),
- child: Icon(
- expense.category.icon,
- color: Colors.blue,
+ const SizedBox(width: 16),
+
+ // Détails
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ expense.description,
+ style: const TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.w600,
+ ),
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ const SizedBox(height: 4),
+ Row(
+ children: [
+ Text(
+ isPayer
+ ? 'Payé par vous'
+ : 'Payé par ${expense.paidByName}',
+ style: TextStyle(
+ fontSize: 13,
+ color: isDark
+ ? Colors.grey[400]
+ : Colors.grey[600],
+ ),
+ ),
+ Text(
+ ' • ${dateFormat.format(expense.date)}',
+ style: TextStyle(
+ fontSize: 13,
+ color: isDark
+ ? Colors.grey[500]
+ : Colors.grey[500],
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
),
- ),
- const SizedBox(width: 16),
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
+
+ // Montant
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
- expense.description,
- style: const TextStyle(
+ '$prefix${amountToDisplay.toStringAsFixed(2)} ${expense.currency.symbol}',
+ style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
+ color: amountColor,
),
),
- const SizedBox(height: 4),
- Text(
- 'Payé par ${expense.paidByName}',
- style: TextStyle(
- fontSize: 14,
- color: isDark ? Colors.grey[400] : Colors.grey[600],
+ if (expense.currency != ExpenseCurrency.eur)
+ Text(
+ 'Total ${expense.amountInEur.toStringAsFixed(2)} €',
+ style: TextStyle(
+ fontSize: 11,
+ color: isDark ? Colors.grey[500] : Colors.grey[400],
+ ),
),
- ),
- Text(
- dateFormat.format(expense.date),
- style: TextStyle(
- fontSize: 12,
- color: isDark ? Colors.grey[500] : Colors.grey[500],
- ),
- ),
],
),
- ),
- Column(
- crossAxisAlignment: CrossAxisAlignment.end,
- children: [
- Text(
- '${expense.amount.toStringAsFixed(2)} ${expense.currency.symbol}',
- style: const TextStyle(
- fontSize: 16,
- fontWeight: FontWeight.bold,
- color: Colors.green,
- ),
- ),
- if (expense.currency != ExpenseCurrency.eur)
- Text(
- '${expense.amountInEur.toStringAsFixed(2)} €',
- style: TextStyle(
- fontSize: 12,
- color: isDark ? Colors.grey[400] : Colors.grey[600],
- ),
- ),
- ],
- ),
- ],
+ ],
+ ),
),
),
),
);
}
+ Color _getCategoryColor(ExpenseCategory category) {
+ switch (category) {
+ case ExpenseCategory.restaurant:
+ return Colors.orange;
+ case ExpenseCategory.transport:
+ return Colors.blue;
+ case ExpenseCategory.accommodation:
+ return Colors.purple;
+ case ExpenseCategory.entertainment:
+ return Colors.pink;
+ case ExpenseCategory.shopping:
+ return Colors.teal;
+ case ExpenseCategory.other:
+ return Colors.grey;
+ }
+ }
+
void _showExpenseDetail(BuildContext context, Expense expense) {
showDialog(
context: context,
- builder: (context) => ExpenseDetailDialog(
- expense: expense,
- group: group,
- ),
+ builder: (context) => ExpenseDetailDialog(expense: expense, group: group),
);
}
}
diff --git a/lib/components/account/group_expenses_page.dart b/lib/components/account/group_expenses_page.dart
index 71f8e42..9d84cc5 100644
--- a/lib/components/account/group_expenses_page.dart
+++ b/lib/components/account/group_expenses_page.dart
@@ -13,7 +13,8 @@ import '../../models/group.dart';
import 'add_expense_dialog.dart';
import 'balances_tab.dart';
import 'expenses_tab.dart';
-import 'settlements_tab.dart';
+import '../../models/user_balance.dart';
+import '../../models/expense.dart';
class GroupExpensesPage extends StatefulWidget {
final Account account;
@@ -31,13 +32,14 @@ class GroupExpensesPage extends StatefulWidget {
class _GroupExpensesPageState extends State
with SingleTickerProviderStateMixin {
-
late TabController _tabController;
-
+ ExpenseCategory? _selectedCategory;
+ String? _selectedPayerId;
+
@override
void initState() {
super.initState();
- _tabController = TabController(length: 3, vsync: this);
+ _tabController = TabController(length: 2, vsync: this);
_loadData();
}
@@ -50,39 +52,41 @@ class _GroupExpensesPageState extends State
void _loadData() {
// Charger les dépenses du groupe
context.read().add(LoadExpensesByGroup(widget.group.id));
-
+
// Charger les balances du groupe
context.read().add(LoadGroupBalances(widget.group.id));
}
@override
Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ final isDarkMode = theme.brightness == Brightness.dark;
+
return Scaffold(
+ backgroundColor: isDarkMode
+ ? theme.scaffoldBackgroundColor
+ : Colors.grey[50],
appBar: AppBar(
- title: Text(widget.account.name),
- backgroundColor: Theme.of(context).primaryColor,
- foregroundColor: Colors.white,
- elevation: 0,
- bottom: TabBar(
- controller: _tabController,
- indicatorColor: Colors.white,
- labelColor: Colors.white,
- unselectedLabelColor: Colors.white70,
- tabs: const [
- Tab(
- icon: Icon(Icons.balance),
- text: 'Balances',
- ),
- Tab(
- icon: Icon(Icons.receipt_long),
- text: 'Dépenses',
- ),
- Tab(
- icon: Icon(Icons.payment),
- text: 'Règlements',
- ),
- ],
+ title: const Text(
+ 'Dépenses du voyage',
+ style: TextStyle(fontWeight: FontWeight.bold),
),
+ centerTitle: true,
+ backgroundColor: Colors.transparent,
+ elevation: 0,
+ foregroundColor: theme.colorScheme.onSurface,
+ leading: IconButton(
+ icon: const Icon(Icons.arrow_back),
+ onPressed: () => Navigator.pop(context),
+ ),
+ actions: [
+ IconButton(
+ icon: const Icon(Icons.filter_list),
+ onPressed: () {
+ _showFilterDialog();
+ },
+ ),
+ ],
),
body: MultiBlocListener(
listeners: [
@@ -103,56 +107,107 @@ class _GroupExpensesPageState extends State
backgroundColor: Colors.red,
),
);
+ } else if (state is ExpensesLoaded) {
+ // Rafraîchir les balances quand les dépenses changent (ex: via stream)
+ context.read().add(
+ RefreshBalance(widget.group.id),
+ );
}
},
),
],
- child: TabBarView(
- controller: _tabController,
+ child: Column(
children: [
- // Onglet Balances
+ // Summary Card
BlocBuilder(
builder: (context, state) {
- if (state is BalanceLoading) {
- return const Center(child: CircularProgressIndicator());
- } else if (state is GroupBalancesLoaded) {
- return BalancesTab(balances: state.balances);
- } else if (state is BalanceError) {
- return _buildErrorState('Erreur lors du chargement des balances: ${state.message}');
+ if (state is GroupBalancesLoaded) {
+ return _buildSummaryCard(state.balances, isDarkMode);
}
- return _buildEmptyState('Aucune balance disponible');
+ return const SizedBox.shrink();
},
),
-
- // Onglet Dépenses
- BlocBuilder(
- builder: (context, state) {
- if (state is ExpenseLoading) {
- return const Center(child: CircularProgressIndicator());
- } else if (state is ExpensesLoaded) {
- return ExpensesTab(
- expenses: state.expenses,
- group: widget.group,
- );
- } else if (state is ExpenseError) {
- return _buildErrorState('Erreur lors du chargement des dépenses: ${state.message}');
- }
- return _buildEmptyState('Aucune dépense trouvée');
- },
+
+ // Tabs
+ Container(
+ decoration: BoxDecoration(
+ border: Border(
+ bottom: BorderSide(color: theme.dividerColor, width: 1),
+ ),
+ ),
+ child: TabBar(
+ controller: _tabController,
+ labelColor: theme.colorScheme.primary,
+ unselectedLabelColor: theme.colorScheme.onSurface.withValues(
+ alpha: 0.6,
+ ),
+ indicatorColor: theme.colorScheme.primary,
+ indicatorWeight: 3,
+ labelStyle: const TextStyle(fontWeight: FontWeight.bold),
+ tabs: const [
+ Tab(text: 'Toutes les dépenses'),
+ Tab(text: 'Mes soldes'),
+ ],
+ ),
),
-
- // Onglet Règlements
- BlocBuilder(
- builder: (context, state) {
- if (state is BalanceLoading) {
- return const Center(child: CircularProgressIndicator());
- } else if (state is GroupBalancesLoaded) {
- return SettlementsTab(settlements: state.settlements);
- } else if (state is BalanceError) {
- return _buildErrorState('Erreur lors du chargement des règlements: ${state.message}');
- }
- return _buildEmptyState('Aucun règlement nécessaire');
- },
+
+ // Tab View
+ Expanded(
+ child: TabBarView(
+ controller: _tabController,
+ children: [
+ // Onglet Dépenses
+ BlocBuilder(
+ builder: (context, state) {
+ if (state is ExpenseLoading) {
+ return const Center(child: CircularProgressIndicator());
+ } else if (state is ExpensesLoaded) {
+ final userState = context.read().state;
+ final currentUserId = userState is user_state.UserLoaded
+ ? userState.user.id
+ : '';
+
+ var filteredExpenses = state.expenses;
+
+ if (_selectedCategory != null) {
+ filteredExpenses = filteredExpenses
+ .where((e) => e.category == _selectedCategory)
+ .toList();
+ }
+
+ if (_selectedPayerId != null) {
+ filteredExpenses = filteredExpenses
+ .where((e) => e.paidById == _selectedPayerId)
+ .toList();
+ }
+
+ return ExpensesTab(
+ expenses: filteredExpenses,
+ group: widget.group,
+ currentUserId: currentUserId,
+ );
+ } else if (state is ExpenseError) {
+ return _buildErrorState('Erreur: ${state.message}');
+ }
+ return _buildEmptyState('Aucune dépense trouvée');
+ },
+ ),
+
+ // Onglet Balances (Combiné)
+ BlocBuilder(
+ builder: (context, state) {
+ if (state is BalanceLoading) {
+ return const Center(child: CircularProgressIndicator());
+ } else if (state is GroupBalancesLoaded) {
+ return BalancesTab(balances: state.balances);
+ } else if (state is BalanceError) {
+ return _buildErrorState('Erreur: ${state.message}');
+ }
+ return _buildEmptyState('Aucune balance disponible');
+ },
+ ),
+ ],
+ ),
),
],
),
@@ -160,8 +215,109 @@ class _GroupExpensesPageState extends State
floatingActionButton: FloatingActionButton(
onPressed: _showAddExpenseDialog,
heroTag: "add_expense_fab",
+ backgroundColor: Colors.blue,
+ foregroundColor: Colors.white,
+ elevation: 4,
+ shape: const CircleBorder(),
tooltip: 'Ajouter une dépense',
- child: const Icon(Icons.add),
+ child: const Icon(Icons.add, size: 32),
+ ),
+ );
+ }
+
+ Widget _buildSummaryCard(List balances, bool isDarkMode) {
+ // Trouver la balance de l'utilisateur courant
+ final userState = context.read().state;
+ double myBalance = 0;
+
+ if (userState is user_state.UserLoaded) {
+ final myBalanceObj = balances.firstWhere(
+ (b) => b.userId == userState.user.id,
+ orElse: () => const UserBalance(
+ userId: '',
+ userName: '',
+ totalPaid: 0,
+ totalOwed: 0,
+ balance: 0,
+ ),
+ );
+ myBalance = myBalanceObj.balance;
+ }
+
+ final isPositive = myBalance >= 0;
+ final color = isPositive ? Colors.green : Colors.red;
+ final amountStr = '${myBalance.abs().toStringAsFixed(2)} €';
+
+ return Container(
+ margin: const EdgeInsets.all(16),
+ padding: const EdgeInsets.all(20),
+ decoration: BoxDecoration(
+ color: isDarkMode ? Colors.grey[800] : Colors.white,
+ borderRadius: BorderRadius.circular(16),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withValues(alpha: 0.05),
+ blurRadius: 10,
+ offset: const Offset(0, 4),
+ ),
+ ],
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ 'Votre solde total',
+ style: TextStyle(
+ color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
+ fontSize: 14,
+ ),
+ ),
+ const SizedBox(height: 8),
+ Row(
+ children: [
+ Text(
+ isPositive ? 'On vous doit ' : 'Vous devez ',
+ style: TextStyle(
+ color: color,
+ fontSize: 24,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ Text(
+ amountStr,
+ style: TextStyle(
+ color: color,
+ fontSize: 24,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 8),
+ Text(
+ isPositive
+ ? 'Vous êtes en positif sur ce voyage.'
+ : 'Vous êtes en négatif sur ce voyage.',
+ style: TextStyle(
+ color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
+ fontSize: 14,
+ ),
+ ),
+ const SizedBox(height: 12),
+ InkWell(
+ onTap: () {
+ _tabController.animateTo(1); // Aller à l'onglet Balances
+ },
+ child: Text(
+ 'Voir le détail des soldes',
+ style: TextStyle(
+ color: Colors.blue[400],
+ fontWeight: FontWeight.w500,
+ fontSize: 14,
+ ),
+ ),
+ ),
+ ],
),
);
}
@@ -171,11 +327,7 @@ class _GroupExpensesPageState extends State
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
- Icon(
- Icons.error_outline,
- size: 80,
- color: Colors.red[300],
- ),
+ Icon(Icons.error_outline, size: 80, color: Colors.red[300]),
const SizedBox(height: 16),
Text(
'Erreur',
@@ -190,10 +342,7 @@ class _GroupExpensesPageState extends State
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Text(
message,
- style: TextStyle(
- fontSize: 16,
- color: Colors.grey[600],
- ),
+ style: TextStyle(fontSize: 16, color: Colors.grey[600]),
textAlign: TextAlign.center,
),
),
@@ -213,11 +362,7 @@ class _GroupExpensesPageState extends State
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
- Icon(
- Icons.info_outline,
- size: 80,
- color: Colors.grey[400],
- ),
+ Icon(Icons.info_outline, size: 80, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
'Aucune donnée',
@@ -230,10 +375,7 @@ class _GroupExpensesPageState extends State
const SizedBox(height: 8),
Text(
message,
- style: TextStyle(
- fontSize: 16,
- color: Colors.grey[500],
- ),
+ style: TextStyle(fontSize: 16, color: Colors.grey[500]),
textAlign: TextAlign.center,
),
],
@@ -243,14 +385,12 @@ class _GroupExpensesPageState extends State
void _showAddExpenseDialog() {
final userState = context.read().state;
-
+
if (userState is user_state.UserLoaded) {
showDialog(
context: context,
- builder: (context) => AddExpenseDialog(
- group: widget.group,
- currentUser: userState.user,
- ),
+ builder: (context) =>
+ AddExpenseDialog(group: widget.group, currentUser: userState.user),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
@@ -261,4 +401,96 @@ class _GroupExpensesPageState extends State
);
}
}
-}
\ No newline at end of file
+
+ void _showFilterDialog() {
+ showDialog(
+ context: context,
+ builder: (context) {
+ return StatefulBuilder(
+ builder: (context, setState) {
+ return AlertDialog(
+ title: const Text('Filtrer les dépenses'),
+ content: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ DropdownButtonFormField(
+ // ignore: deprecated_member_use
+ value: _selectedCategory,
+ decoration: const InputDecoration(
+ labelText: 'Catégorie',
+ border: OutlineInputBorder(),
+ ),
+ items: [
+ const DropdownMenuItem(
+ value: null,
+ child: Text('Toutes'),
+ ),
+ ...ExpenseCategory.values.map((category) {
+ return DropdownMenuItem(
+ value: category,
+ child: Text(category.displayName),
+ );
+ }),
+ ],
+ onChanged: (value) {
+ setState(() => _selectedCategory = value);
+ },
+ ),
+ const SizedBox(height: 16),
+ DropdownButtonFormField(
+ // ignore: deprecated_member_use
+ value: _selectedPayerId,
+ decoration: const InputDecoration(
+ labelText: 'Payé par',
+ border: OutlineInputBorder(),
+ ),
+ items: [
+ const DropdownMenuItem(
+ value: null,
+ child: Text('Tous'),
+ ),
+ ...widget.group.members.map((member) {
+ return DropdownMenuItem(
+ value: member.userId,
+ child: Text(member.firstName),
+ );
+ }),
+ ],
+ onChanged: (value) {
+ setState(() => _selectedPayerId = value);
+ },
+ ),
+ ],
+ ),
+ actions: [
+ TextButton(
+ onPressed: () {
+ setState(() {
+ _selectedCategory = null;
+ _selectedPayerId = null;
+ });
+ // Also update parent state
+ this.setState(() {
+ _selectedCategory = null;
+ _selectedPayerId = null;
+ });
+ Navigator.pop(context);
+ },
+ child: const Text('Réinitialiser'),
+ ),
+ ElevatedButton(
+ onPressed: () {
+ // Update parent state
+ this.setState(() {});
+ Navigator.pop(context);
+ },
+ child: const Text('Appliquer'),
+ ),
+ ],
+ );
+ },
+ );
+ },
+ );
+ }
+}
diff --git a/lib/components/activities/activities_page.dart b/lib/components/activities/activities_page.dart
index b067f1c..f88487f 100644
--- a/lib/components/activities/activities_page.dart
+++ b/lib/components/activities/activities_page.dart
@@ -8,6 +8,8 @@ import '../../models/activity.dart';
import '../../services/activity_cache_service.dart';
import '../loading/laoding_content.dart';
+import '../../blocs/user/user_bloc.dart';
+import '../../blocs/user/user_state.dart';
class ActivitiesPage extends StatefulWidget {
final Trip trip;
@@ -32,8 +34,7 @@ class _ActivitiesPageState extends State
List _tripActivities = [];
List _approvedActivities = [];
bool _isLoadingTripActivities = false;
- int _totalGoogleActivitiesRequested =
- 0; // Compteur pour les recherches progressives
+
bool _autoReloadInProgress =
false; // Protection contre les rechargements en boucle
int _lastAutoReloadTriggerCount =
@@ -999,9 +1000,11 @@ class _ActivitiesPageState extends State
}
void _voteForActivity(String activityId, int vote) {
- // TODO: Récupérer l'ID utilisateur actuel
- // Pour l'instant, on utilise l'ID du créateur du voyage pour que le vote compte
- final userId = widget.trip.createdBy;
+ // Récupérer l'ID utilisateur actuel depuis le UserBloc
+ final userState = context.read().state;
+ final userId = userState is UserLoaded
+ ? userState.user.id
+ : widget.trip.createdBy;
// Vérifier si l'activité existe dans la liste locale pour vérifier le vote
// (car l'objet activity passé peut venir d'une liste filtrée ou autre)
@@ -1122,7 +1125,7 @@ class _ActivitiesPageState extends State
6; // Activités actuelles + ce qui manque + buffer de 6
// Mettre à jour le compteur et recharger avec le nouveau total
- _totalGoogleActivitiesRequested = newTotalToRequest;
+
_loadMoreGoogleActivitiesWithTotal(newTotalToRequest);
// Libérer le verrou après un délai
@@ -1135,7 +1138,6 @@ class _ActivitiesPageState extends State
}
void _searchGoogleActivities() {
- _totalGoogleActivitiesRequested = 6; // Reset du compteur
_autoReloadInProgress = false; // Reset des protections
_lastAutoReloadTriggerCount = 0;
@@ -1166,7 +1168,6 @@ class _ActivitiesPageState extends State
}
void _resetAndSearchGoogleActivities() {
- _totalGoogleActivitiesRequested = 6; // Reset du compteur
_autoReloadInProgress = false; // Reset des protections
_lastAutoReloadTriggerCount = 0;
@@ -1203,8 +1204,6 @@ class _ActivitiesPageState extends State
final currentCount = currentState.searchResults.length;
final newTotal = currentCount + 6;
- _totalGoogleActivitiesRequested = newTotal;
-
// Utiliser les coordonnées pré-géolocalisées du voyage si disponibles
if (widget.trip.hasCoordinates) {
context.read().add(
diff --git a/lib/components/activities/add_activity_bottom_sheet.dart b/lib/components/activities/add_activity_bottom_sheet.dart
index dd86c6f..ca2f39d 100644
--- a/lib/components/activities/add_activity_bottom_sheet.dart
+++ b/lib/components/activities/add_activity_bottom_sheet.dart
@@ -57,7 +57,7 @@ class _AddActivityBottomSheetState extends State {
height: 4,
margin: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
- color: theme.colorScheme.onSurface.withOpacity(0.3),
+ color: theme.colorScheme.onSurface.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(2),
),
),
diff --git a/lib/components/group/chat_group_content.dart b/lib/components/group/chat_group_content.dart
index 1b2d5c0..d587a39 100644
--- a/lib/components/group/chat_group_content.dart
+++ b/lib/components/group/chat_group_content.dart
@@ -12,7 +12,7 @@ import '../../models/message.dart';
import '../../repositories/group_repository.dart';
/// Chat group content widget for group messaging functionality.
-///
+///
/// This widget provides a complete chat interface for group members to
/// communicate within a travel group. Features include:
/// - Real-time message loading and sending
@@ -20,7 +20,7 @@ import '../../repositories/group_repository.dart';
/// - Message reactions (like/unlike)
/// - Scroll-to-bottom functionality
/// - Message status indicators
-///
+///
/// The widget integrates with MessageBloc for state management and
/// handles various message operations through the bloc pattern.
class ChatGroupContent extends StatefulWidget {
@@ -28,13 +28,10 @@ class ChatGroupContent extends StatefulWidget {
final Group group;
/// Creates a chat group content widget.
- ///
+ ///
/// Args:
/// [group]: The group object containing group details and ID
- const ChatGroupContent({
- super.key,
- required this.group,
- });
+ const ChatGroupContent({super.key, required this.group});
@override
State createState() => _ChatGroupContentState();
@@ -43,16 +40,16 @@ class ChatGroupContent extends StatefulWidget {
class _ChatGroupContentState extends State {
/// Controller for the message input field
final _messageController = TextEditingController();
-
+
/// Controller for managing scroll position in the message list
final _scrollController = ScrollController();
-
+
/// Currently selected message for editing (null if not editing)
Message? _editingMessage;
/// Repository pour gérer les groupes
final _groupRepository = GroupRepository();
-
+
/// Subscription pour écouter les changements des membres du groupe
late StreamSubscription> _membersSubscription;
@@ -61,18 +58,20 @@ class _ChatGroupContentState extends State {
super.initState();
// Load messages when the widget initializes
context.read().add(LoadMessages(widget.group.id));
-
+
// Écouter les changements des membres du groupe
- _membersSubscription = _groupRepository.watchGroupMembers(widget.group.id).listen((updatedMembers) {
- if (mounted) {
- setState(() {
- widget.group.members.clear();
- widget.group.members.addAll(updatedMembers);
+ _membersSubscription = _groupRepository
+ .watchGroupMembers(widget.group.id)
+ .listen((updatedMembers) {
+ if (mounted) {
+ setState(() {
+ widget.group.members.clear();
+ widget.group.members.addAll(updatedMembers);
+ });
+ }
});
- }
- });
}
-
+
@override
void dispose() {
_messageController.dispose();
@@ -82,11 +81,11 @@ class _ChatGroupContentState extends State {
}
/// Sends a new message or updates an existing message.
- ///
+ ///
/// Handles both sending new messages and editing existing ones based
/// on the current editing state. Validates input and clears the input
/// field after successful submission.
- ///
+ ///
/// Args:
/// [currentUser]: The user sending or editing the message
void _sendMessage(user_state.UserModel currentUser) {
@@ -96,33 +95,33 @@ class _ChatGroupContentState extends State {
if (_editingMessage != null) {
// Edit mode - update existing message
context.read().add(
- UpdateMessage(
- groupId: widget.group.id,
- messageId: _editingMessage!.id,
- newText: messageText,
- ),
- );
+ UpdateMessage(
+ groupId: widget.group.id,
+ messageId: _editingMessage!.id,
+ newText: messageText,
+ ),
+ );
_cancelEdit();
} else {
// Send mode - create new message
context.read().add(
- SendMessage(
- groupId: widget.group.id,
- text: messageText,
- senderId: currentUser.id,
- senderName: currentUser.prenom,
- ),
- );
+ SendMessage(
+ groupId: widget.group.id,
+ text: messageText,
+ senderId: currentUser.id,
+ senderName: currentUser.prenom,
+ ),
+ );
}
_messageController.clear();
}
/// Initiates editing mode for a selected message.
- ///
+ ///
/// Sets the message as the currently editing message and populates
/// the input field with the message text for modification.
- ///
+ ///
/// Args:
/// [message]: The message to edit
void _editMessage(Message message) {
@@ -133,7 +132,7 @@ class _ChatGroupContentState extends State {
}
/// Cancels the current editing operation.
- ///
+ ///
/// Resets the editing state and clears the input field,
/// returning to normal message sending mode.
void _cancelEdit() {
@@ -144,46 +143,43 @@ class _ChatGroupContentState extends State {
}
/// Deletes a message from the group chat.
- ///
+ ///
/// Sends a delete event to the MessageBloc to remove the specified
/// message from the group's message history.
- ///
+ ///
/// Args:
/// [messageId]: The ID of the message to delete
void _deleteMessage(String messageId) {
context.read().add(
- DeleteMessage(
- groupId: widget.group.id,
- messageId: messageId,
- ),
- );
+ DeleteMessage(groupId: widget.group.id, messageId: messageId),
+ );
}
void _reactToMessage(String messageId, String userId, String reaction) {
context.read().add(
- ReactToMessage(
- groupId: widget.group.id,
- messageId: messageId,
- userId: userId,
- reaction: reaction,
- ),
- );
+ ReactToMessage(
+ groupId: widget.group.id,
+ messageId: messageId,
+ userId: userId,
+ reaction: reaction,
+ ),
+ );
}
void _removeReaction(String messageId, String userId) {
context.read().add(
- RemoveReaction(
- groupId: widget.group.id,
- messageId: messageId,
- userId: userId,
- ),
- );
+ RemoveReaction(
+ groupId: widget.group.id,
+ messageId: messageId,
+ userId: userId,
+ ),
+ );
}
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
-
+
return BlocBuilder(
builder: (context, userState) {
if (userState is! user_state.UserLoaded) {
@@ -203,7 +199,10 @@ class _ChatGroupContentState extends State {
Text(widget.group.name, style: const TextStyle(fontSize: 18)),
Text(
'${widget.group.members.length} membre${widget.group.members.length > 1 ? 's' : ''}',
- style: const TextStyle(fontSize: 12, fontWeight: FontWeight.normal),
+ style: const TextStyle(
+ fontSize: 12,
+ fontWeight: FontWeight.normal,
+ ),
),
],
),
@@ -255,7 +254,8 @@ class _ChatGroupContentState extends State {
itemBuilder: (context, index) {
final message = state.messages[index];
final isMe = message.senderId == currentUser.id;
- final showDate = index == 0 ||
+ final showDate =
+ index == 0 ||
!_isSameDay(
state.messages[index - 1].timestamp,
message.timestamp,
@@ -263,8 +263,14 @@ class _ChatGroupContentState extends State {
return Column(
children: [
- if (showDate) _buildDateSeparator(message.timestamp),
- _buildMessageBubble(message, isMe, isDark, currentUser.id),
+ if (showDate)
+ _buildDateSeparator(message.timestamp),
+ _buildMessageBubble(
+ message,
+ isMe,
+ isDark,
+ currentUser.id,
+ ),
],
);
},
@@ -280,14 +286,15 @@ class _ChatGroupContentState extends State {
if (_editingMessage != null)
Container(
color: isDark ? Colors.blue[900] : Colors.blue[100],
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 8,
+ ),
child: Row(
children: [
const Icon(Icons.edit, size: 20),
const SizedBox(width: 8),
- const Expanded(
- child: Text('Modification du message'),
- ),
+ const Expanded(child: Text('Modification du message')),
IconButton(
icon: const Icon(Icons.close),
onPressed: _cancelEdit,
@@ -315,11 +322,13 @@ class _ChatGroupContentState extends State {
child: TextField(
controller: _messageController,
decoration: InputDecoration(
- hintText: _editingMessage != null
- ? 'Modifier le message...'
+ hintText: _editingMessage != null
+ ? 'Modifier le message...'
: 'Écrire un message...',
filled: true,
- fillColor: isDark ? Colors.grey[850] : Colors.grey[100],
+ fillColor: isDark
+ ? Colors.grey[850]
+ : Colors.grey[100],
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(24),
borderSide: BorderSide.none,
@@ -336,9 +345,13 @@ class _ChatGroupContentState extends State {
const SizedBox(width: 8),
IconButton(
onPressed: () => _sendMessage(currentUser),
- icon: Icon(_editingMessage != null ? Icons.check : Icons.send),
+ icon: Icon(
+ _editingMessage != null ? Icons.check : Icons.send,
+ ),
style: IconButton.styleFrom(
- backgroundColor: Theme.of(context).colorScheme.primary,
+ backgroundColor: Theme.of(
+ context,
+ ).colorScheme.primary,
foregroundColor: Colors.white,
padding: const EdgeInsets.all(12),
),
@@ -361,27 +374,17 @@ class _ChatGroupContentState extends State {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
- Icon(
- Icons.chat_bubble_outline,
- size: 80,
- color: Colors.grey[400],
- ),
+ Icon(Icons.chat_bubble_outline, size: 80, color: Colors.grey[400]),
const SizedBox(height: 16),
const Text(
'Aucun message',
- style: TextStyle(
- fontSize: 20,
- fontWeight: FontWeight.bold,
- ),
+ style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(
'Commencez la conversation !',
textAlign: TextAlign.center,
- style: TextStyle(
- fontSize: 14,
- color: Colors.grey[600],
- ),
+ style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
],
),
@@ -389,10 +392,15 @@ class _ChatGroupContentState extends State {
);
}
- Widget _buildMessageBubble(Message message, bool isMe, bool isDark, String currentUserId) {
+ Widget _buildMessageBubble(
+ Message message,
+ bool isMe,
+ bool isDark,
+ String currentUserId,
+ ) {
final Color bubbleColor;
final Color textColor;
-
+
if (isMe) {
bubbleColor = isDark ? const Color(0xFF1E3A5F) : const Color(0xFF90CAF9);
textColor = isDark ? Colors.white : Colors.black87;
@@ -402,42 +410,48 @@ class _ChatGroupContentState extends State {
}
// Trouver le membre qui a envoyé le message pour récupérer son pseudo actuel
- final senderMember = widget.group.members.firstWhere(
- (m) => m.userId == message.senderId,
- orElse: () => null as dynamic,
- ) as dynamic;
+ final senderMember =
+ widget.group.members.firstWhere(
+ (m) => m.userId == message.senderId,
+ orElse: () => null as dynamic,
+ )
+ as dynamic;
// Utiliser le pseudo actuel du membre, ou le senderName en fallback
- final displayName = senderMember != null ? senderMember.pseudo : message.senderName;
+ final displayName = senderMember != null
+ ? senderMember.pseudo
+ : message.senderName;
return Align(
alignment: isMe ? Alignment.centerRight : Alignment.centerLeft,
child: GestureDetector(
- onLongPress: () => _showMessageOptions(context, message, isMe, currentUserId),
+ onLongPress: () =>
+ _showMessageOptions(context, message, isMe, currentUserId),
child: Padding(
- padding: EdgeInsets.symmetric(
- horizontal: 8,
- vertical: 4,
- ),
+ padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Row(
- mainAxisAlignment: isMe ? MainAxisAlignment.end : MainAxisAlignment.start,
+ mainAxisAlignment: isMe
+ ? MainAxisAlignment.end
+ : MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
// Avatar du sender (seulement pour les autres messages)
if (!isMe) ...[
CircleAvatar(
radius: 16,
- backgroundImage: (senderMember != null &&
- senderMember.profilePictureUrl != null &&
- senderMember.profilePictureUrl!.isNotEmpty)
+ backgroundImage:
+ (senderMember != null &&
+ senderMember.profilePictureUrl != null &&
+ senderMember.profilePictureUrl!.isNotEmpty)
? NetworkImage(senderMember.profilePictureUrl!)
: null,
- child: (senderMember == null ||
- senderMember.profilePictureUrl == null ||
- senderMember.profilePictureUrl!.isEmpty)
+ child:
+ (senderMember == null ||
+ senderMember.profilePictureUrl == null ||
+ senderMember.profilePictureUrl!.isEmpty)
? Text(
- displayName.isNotEmpty
- ? displayName[0].toUpperCase()
+ displayName.isNotEmpty
+ ? displayName[0].toUpperCase()
: '?',
style: const TextStyle(fontSize: 12),
)
@@ -445,10 +459,13 @@ class _ChatGroupContentState extends State {
),
const SizedBox(width: 8),
],
-
+
Container(
margin: const EdgeInsets.symmetric(vertical: 4),
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 10,
+ ),
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.65,
),
@@ -462,7 +479,9 @@ class _ChatGroupContentState extends State {
),
),
child: Column(
- crossAxisAlignment: isMe ? CrossAxisAlignment.end : CrossAxisAlignment.start,
+ crossAxisAlignment: isMe
+ ? CrossAxisAlignment.end
+ : CrossAxisAlignment.start,
children: [
if (!isMe) ...[
Text(
@@ -476,11 +495,17 @@ class _ChatGroupContentState extends State {
const SizedBox(height: 4),
],
Text(
- message.isDeleted ? 'a supprimé un message' : message.text,
+ message.isDeleted
+ ? 'a supprimé un message'
+ : message.text,
style: TextStyle(
fontSize: 15,
- color: message.isDeleted ? textColor.withValues(alpha: 0.5) : textColor,
- fontStyle: message.isDeleted ? FontStyle.italic : FontStyle.normal,
+ color: message.isDeleted
+ ? textColor.withValues(alpha: 0.5)
+ : textColor,
+ fontStyle: message.isDeleted
+ ? FontStyle.italic
+ : FontStyle.normal,
),
),
const SizedBox(height: 4),
@@ -528,7 +553,7 @@ class _ChatGroupContentState extends State {
List _buildReactionChips(Message message, String currentUserId) {
final reactionCounts = >{};
-
+
// Grouper les réactions par emoji
message.reactions.forEach((userId, emoji) {
reactionCounts.putIfAbsent(emoji, () => []).add(userId);
@@ -550,7 +575,7 @@ class _ChatGroupContentState extends State {
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
- color: hasReacted
+ color: hasReacted
? Colors.blue.withValues(alpha: 0.3)
: Colors.grey.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(12),
@@ -565,7 +590,10 @@ class _ChatGroupContentState extends State {
const SizedBox(width: 2),
Text(
'${userIds.length}',
- style: const TextStyle(fontSize: 10, fontWeight: FontWeight.bold),
+ style: const TextStyle(
+ fontSize: 10,
+ fontWeight: FontWeight.bold,
+ ),
),
],
),
@@ -574,7 +602,12 @@ class _ChatGroupContentState extends State {
}).toList();
}
- void _showMessageOptions(BuildContext context, Message message, bool isMe, String currentUserId) {
+ void _showMessageOptions(
+ BuildContext context,
+ Message message,
+ bool isMe,
+ String currentUserId,
+ ) {
showModalBottomSheet(
context: context,
builder: (context) => SafeArea(
@@ -609,7 +642,10 @@ class _ChatGroupContentState extends State {
),
ListTile(
leading: const Icon(Icons.delete, color: Colors.red),
- title: const Text('Supprimer', style: TextStyle(color: Colors.red)),
+ title: const Text(
+ 'Supprimer',
+ style: TextStyle(color: Colors.red),
+ ),
onTap: () {
Navigator.pop(context);
_showDeleteConfirmation(context, message.id);
@@ -712,20 +748,23 @@ class _ChatGroupContentState extends State {
final member = widget.group.members[index];
final initials = member.pseudo.isNotEmpty
? member.pseudo[0].toUpperCase()
- : (member.firstName.isNotEmpty
- ? member.firstName[0].toUpperCase()
- : '?');
-
+ : (member.firstName.isNotEmpty
+ ? member.firstName[0].toUpperCase()
+ : '?');
+
// Construire le nom complet
final fullName = '${member.firstName} ${member.lastName}'.trim();
-
+
return ListTile(
leading: CircleAvatar(
- backgroundImage: (member.profilePictureUrl != null &&
- member.profilePictureUrl!.isNotEmpty)
+ backgroundImage:
+ (member.profilePictureUrl != null &&
+ member.profilePictureUrl!.isNotEmpty)
? NetworkImage(member.profilePictureUrl!)
: null,
- child: (member.profilePictureUrl == null || member.profilePictureUrl!.isEmpty)
+ child:
+ (member.profilePictureUrl == null ||
+ member.profilePictureUrl!.isEmpty)
? Text(initials)
: null,
),
@@ -743,8 +782,11 @@ class _ChatGroupContentState extends State {
),
],
),
- subtitle: member.role == 'admin'
- ? const Text('Administrateur', style: TextStyle(fontSize: 12))
+ subtitle: member.role == 'admin'
+ ? const Text(
+ 'Administrateur',
+ style: TextStyle(fontSize: 12),
+ )
: null,
trailing: IconButton(
icon: const Icon(Icons.edit, size: 18),
@@ -774,7 +816,8 @@ class _ChatGroupContentState extends State {
showDialog(
context: context,
builder: (context) => AlertDialog(
- backgroundColor: theme.dialogBackgroundColor,
+ backgroundColor:
+ theme.dialogTheme.backgroundColor ?? theme.colorScheme.surface,
title: Text(
'Changer le pseudo',
style: theme.textTheme.titleLarge?.copyWith(
@@ -785,9 +828,7 @@ class _ChatGroupContentState extends State {
controller: pseudoController,
decoration: InputDecoration(
hintText: 'Nouveau pseudo',
- border: OutlineInputBorder(
- borderRadius: BorderRadius.circular(8),
- ),
+ border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
@@ -825,11 +866,11 @@ class _ChatGroupContentState extends State {
try {
final updatedMember = member.copyWith(pseudo: newPseudo);
await _groupRepository.addMember(widget.group.id, updatedMember);
-
+
if (mounted) {
// Le stream listener va automatiquement mettre à jour les membres
// Pas besoin de fermer le dialog ou de faire un refresh manuel
-
+
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Pseudo modifié en "$newPseudo"'),
@@ -848,4 +889,4 @@ class _ChatGroupContentState extends State {
}
}
}
-}
\ No newline at end of file
+}
diff --git a/lib/components/group/group_content.dart b/lib/components/group/group_content.dart
index 0f2253c..8d04b7a 100644
--- a/lib/components/group/group_content.dart
+++ b/lib/components/group/group_content.dart
@@ -144,12 +144,9 @@ class _GroupContentState extends State {
final color = colors[group.name.hashCode.abs() % colors.length];
// Membres de manière simple
- String memberInfo = '${group.members.length} membre(s)';
+ String memberInfo = '${group.memberIds.length} membre(s)';
if (group.members.isNotEmpty) {
- final names = group.members
- .take(2)
- .map((m) => m.firstName)
- .join(', ');
+ final names = group.members.take(2).map((m) => m.firstName).join(', ');
memberInfo += '\n$names';
}
diff --git a/lib/components/home/calendar/calendar_page.dart b/lib/components/home/calendar/calendar_page.dart
index 29768d0..a00e1f6 100644
--- a/lib/components/home/calendar/calendar_page.dart
+++ b/lib/components/home/calendar/calendar_page.dart
@@ -290,10 +290,13 @@ class _CalendarPageState extends State {
),
// Zone de drop pour le calendrier
DragTarget(
- onWillAccept: (data) => true,
- onAccept: (activity) {
+ onWillAcceptWithDetails: (details) => true,
+ onAcceptWithDetails: (details) {
if (_selectedDay != null) {
- _selectTimeAndSchedule(activity, _selectedDay!);
+ _selectTimeAndSchedule(
+ details.data,
+ _selectedDay!,
+ );
}
},
builder: (context, candidateData, rejectedData) {
diff --git a/lib/components/home/create_trip_content.dart b/lib/components/home/create_trip_content.dart
index 89b9e48..ba8201d 100644
--- a/lib/components/home/create_trip_content.dart
+++ b/lib/components/home/create_trip_content.dart
@@ -22,9 +22,10 @@ import 'package:http/http.dart' as http;
import 'package:flutter_dotenv/flutter_dotenv.dart';
import '../../services/place_image_service.dart';
import '../../services/trip_geocoding_service.dart';
+import '../../services/logger_service.dart';
/// Create trip content widget for trip creation and editing functionality.
-///
+///
/// This widget provides a comprehensive form interface for creating new trips
/// or editing existing ones. Key features include:
/// - Trip creation with validation
@@ -34,22 +35,19 @@ import '../../services/trip_geocoding_service.dart';
/// - Group creation and member management
/// - Account setup for expense tracking
/// - Integration with mapping services for location selection
-///
+///
/// The widget handles both creation and editing modes based on the
/// provided tripToEdit parameter.
class CreateTripContent extends StatefulWidget {
/// Optional trip to edit. If null, creates a new trip
final Trip? tripToEdit;
-
+
/// Creates a create trip content widget.
- ///
+ ///
/// Args:
/// [tripToEdit]: Optional trip to edit. If provided, the form will
/// be populated with existing trip data for editing
- const CreateTripContent({
- super.key,
- this.tripToEdit,
- });
+ const CreateTripContent({super.key, this.tripToEdit});
@override
State createState() => _CreateTripContentState();
@@ -58,17 +56,17 @@ class CreateTripContent extends StatefulWidget {
class _CreateTripContentState extends State {
/// Service for handling and displaying errors
final _errorService = ErrorService();
-
+
/// Form validation key
final _formKey = GlobalKey();
-
+
/// Text controllers for form fields
final _titleController = TextEditingController();
final _descriptionController = TextEditingController();
final _locationController = TextEditingController();
final _budgetController = TextEditingController();
final _participantController = TextEditingController();
-
+
/// Services for user and group operations
final _userService = UserService();
final _groupRepository = GroupRepository();
@@ -79,7 +77,7 @@ class _CreateTripContentState extends State {
/// Trip date variables
DateTime? _startDate;
DateTime? _endDate;
-
+
/// Loading and state management variables
bool _isLoading = false;
String? _createdTripId;
@@ -127,7 +125,7 @@ class _CreateTripContentState extends State {
void _onLocationChanged() {
final query = _locationController.text.trim();
-
+
if (query.length < 2) {
_hideSuggestions();
return;
@@ -151,14 +149,14 @@ class _CreateTripContentState extends State {
'?input=${Uri.encodeComponent(query)}'
'&types=(cities)'
'&language=fr'
- '&key=$_apiKey'
+ '&key=$_apiKey',
);
final response = await http.get(url);
-
+
if (response.statusCode == 200) {
final data = json.decode(response.body);
-
+
if (data['status'] == 'OK') {
final predictions = data['predictions'] as List;
setState(() {
@@ -170,7 +168,7 @@ class _CreateTripContentState extends State {
}).toList();
_isLoadingSuggestions = false;
});
-
+
if (_placeSuggestions.isNotEmpty) {
_showSuggestions();
} else {
@@ -202,12 +200,14 @@ class _CreateTripContentState extends State {
// Nouvelle méthode pour afficher les suggestions
void _showSuggestions() {
_hideSuggestions(); // Masquer d'abord les suggestions existantes
-
+
if (_placeSuggestions.isEmpty) return;
_suggestionsOverlay = OverlayEntry(
builder: (context) => Positioned(
- width: MediaQuery.of(context).size.width - 32, // Largeur du champ avec padding
+ width:
+ MediaQuery.of(context).size.width -
+ 32, // Largeur du champ avec padding
child: CompositedTransformFollower(
link: _layerLink,
showWhenUnlinked: false,
@@ -258,26 +258,32 @@ class _CreateTripContentState extends State {
setState(() {
_placeSuggestions = [];
});
-
+
// Charger l'image du lieu sélectionné
_loadPlaceImage(suggestion.description);
}
/// Charge l'image du lieu depuis Google Places API
Future _loadPlaceImage(String location) async {
- print('CreateTripContent: Chargement de l\'image pour: $location');
+ LoggerService.info(
+ 'CreateTripContent: Chargement de l\'image pour: $location',
+ );
try {
final imageUrl = await _placeImageService.getPlaceImageUrl(location);
- print('CreateTripContent: Image URL reçue: $imageUrl');
+ LoggerService.info('CreateTripContent: Image URL reçue: $imageUrl');
if (mounted) {
setState(() {
_selectedImageUrl = imageUrl;
});
- print('CreateTripContent: État mis à jour avec imageUrl: $_selectedImageUrl');
+ LoggerService.info(
+ 'CreateTripContent: État mis à jour avec imageUrl: $_selectedImageUrl',
+ );
}
} catch (e) {
- print('CreateTripContent: Erreur lors du chargement de l\'image: $e');
+ LoggerService.error(
+ 'CreateTripContent: Erreur lors du chargement de l\'image: $e',
+ );
if (mounted) {
_errorService.logError(
'create_trip_content.dart',
@@ -337,7 +343,7 @@ class _CreateTripContentState extends State {
}) {
final theme = Theme.of(context);
final isDarkMode = theme.brightness == Brightness.dark;
-
+
return TextFormField(
controller: controller,
validator: validator,
@@ -349,42 +355,36 @@ class _CreateTripContentState extends State {
decoration: InputDecoration(
hintText: label,
hintStyle: theme.textTheme.bodyLarge?.copyWith(
- color: theme.colorScheme.onSurface.withOpacity(0.5),
+ color: theme.colorScheme.onSurface.withValues(alpha: 0.5),
),
prefixIcon: Icon(
icon,
- color: theme.colorScheme.onSurface.withOpacity(0.5),
+ color: theme.colorScheme.onSurface.withValues(alpha: 0.5),
),
suffixIcon: suffixIcon,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
- color: isDarkMode
- ? Colors.white.withOpacity(0.2)
- : Colors.black.withOpacity(0.2),
+ color: isDarkMode
+ ? Colors.white.withValues(alpha: 0.2)
+ : Colors.black.withValues(alpha: 0.2),
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
- color: isDarkMode
- ? Colors.white.withOpacity(0.2)
- : Colors.black.withOpacity(0.2),
+ color: isDarkMode
+ ? Colors.white.withValues(alpha: 0.2)
+ : Colors.black.withValues(alpha: 0.2),
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
- borderSide: BorderSide(
- color: Colors.teal,
- width: 2,
- ),
+ borderSide: BorderSide(color: Colors.teal, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
- borderSide: const BorderSide(
- color: Colors.red,
- width: 2,
- ),
+ borderSide: const BorderSide(color: Colors.red, width: 2),
),
filled: true,
fillColor: theme.cardColor,
@@ -403,7 +403,7 @@ class _CreateTripContentState extends State {
}) {
final theme = Theme.of(context);
final isDarkMode = theme.brightness == Brightness.dark;
-
+
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
@@ -413,27 +413,27 @@ class _CreateTripContentState extends State {
color: theme.cardColor,
borderRadius: BorderRadius.circular(12),
border: Border.all(
- color: isDarkMode
- ? Colors.white.withOpacity(0.2)
- : Colors.black.withOpacity(0.2),
+ color: isDarkMode
+ ? Colors.white.withValues(alpha: 0.2)
+ : Colors.black.withValues(alpha: 0.2),
),
),
child: Row(
children: [
Icon(
Icons.calendar_today,
- color: theme.colorScheme.onSurface.withOpacity(0.5),
+ color: theme.colorScheme.onSurface.withValues(alpha: 0.5),
size: 20,
),
const SizedBox(width: 12),
Text(
- date != null
- ? '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}'
- : 'mm/dd/yyyy',
+ date != null
+ ? '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}'
+ : 'mm/dd/yyyy',
style: theme.textTheme.bodyLarge?.copyWith(
- color: date != null
- ? theme.colorScheme.onSurface
- : theme.colorScheme.onSurface.withOpacity(0.5),
+ color: date != null
+ ? theme.colorScheme.onSurface
+ : theme.colorScheme.onSurface.withValues(alpha: 0.5),
),
),
],
@@ -442,11 +442,11 @@ class _CreateTripContentState extends State {
);
}
- @override
+ @override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDarkMode = theme.brightness == Brightness.dark;
-
+
return BlocListener(
listener: (context, tripState) {
if (tripState is TripCreated) {
@@ -454,7 +454,10 @@ class _CreateTripContentState extends State {
_createGroupAndAccountForTrip(_createdTripId!);
} else if (tripState is TripOperationSuccess) {
if (mounted) {
- _errorService.showSnackbar(message: tripState.message, isError: false);
+ _errorService.showSnackbar(
+ message: tripState.message,
+ isError: false,
+ );
setState(() {
_isLoading = false;
});
@@ -465,7 +468,10 @@ class _CreateTripContentState extends State {
}
} else if (tripState is TripError) {
if (mounted) {
- _errorService.showSnackbar(message: tripState.message, isError: true);
+ _errorService.showSnackbar(
+ message: tripState.message,
+ isError: true,
+ );
setState(() {
_isLoading = false;
});
@@ -478,7 +484,9 @@ class _CreateTripContentState extends State {
return Scaffold(
backgroundColor: theme.scaffoldBackgroundColor,
appBar: AppBar(
- title: Text(isEditing ? 'Modifier le voyage' : 'Créer un voyage'),
+ title: Text(
+ isEditing ? 'Modifier le voyage' : 'Créer un voyage',
+ ),
backgroundColor: theme.appBarTheme.backgroundColor,
foregroundColor: theme.appBarTheme.foregroundColor,
),
@@ -506,7 +514,10 @@ class _CreateTripContentState extends State {
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
- icon: Icon(Icons.arrow_back, color: theme.colorScheme.onSurface),
+ icon: Icon(
+ Icons.arrow_back,
+ color: theme.colorScheme.onSurface,
+ ),
onPressed: () => Navigator.pop(context),
),
),
@@ -519,7 +530,9 @@ class _CreateTripContentState extends State {
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
- color: Colors.black.withOpacity(isDarkMode ? 0.3 : 0.1),
+ color: Colors.black.withValues(
+ alpha: isDarkMode ? 0.3 : 0.1,
+ ),
blurRadius: 10,
offset: const Offset(0, 5),
),
@@ -544,7 +557,9 @@ class _CreateTripContentState extends State {
Text(
'Donne un nom à ton voyage',
style: theme.textTheme.bodyMedium?.copyWith(
- color: theme.colorScheme.onSurface.withOpacity(0.7),
+ color: theme.colorScheme.onSurface.withValues(
+ alpha: 0.7,
+ ),
),
),
const SizedBox(height: 24),
@@ -588,7 +603,9 @@ class _CreateTripContentState extends State {
? const SizedBox(
width: 20,
height: 20,
- child: CircularProgressIndicator(strokeWidth: 2),
+ child: CircularProgressIndicator(
+ strokeWidth: 2,
+ ),
)
: null,
),
@@ -667,7 +684,9 @@ class _CreateTripContentState extends State {
controller: _budgetController,
label: 'Ex : 500',
icon: Icons.euro,
- keyboardType: TextInputType.numberWithOptions(decimal: true),
+ keyboardType: TextInputType.numberWithOptions(
+ decimal: true,
+ ),
),
const SizedBox(height: 20),
@@ -701,7 +720,10 @@ class _CreateTripContentState extends State {
),
child: IconButton(
onPressed: _addParticipant,
- icon: const Icon(Icons.add, color: Colors.white),
+ icon: const Icon(
+ Icons.add,
+ color: Colors.white,
+ ),
),
),
],
@@ -720,7 +742,7 @@ class _CreateTripContentState extends State {
vertical: 8,
),
decoration: BoxDecoration(
- color: Colors.teal.withOpacity(0.1),
+ color: Colors.teal.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(20),
),
child: Row(
@@ -728,10 +750,11 @@ class _CreateTripContentState extends State {
children: [
Text(
email,
- style: theme.textTheme.bodySmall?.copyWith(
- color: Colors.teal,
- fontWeight: FontWeight.w500,
- ),
+ style: theme.textTheme.bodySmall
+ ?.copyWith(
+ color: Colors.teal,
+ fontWeight: FontWeight.w500,
+ ),
),
const SizedBox(width: 8),
GestureDetector(
@@ -758,7 +781,9 @@ class _CreateTripContentState extends State {
width: double.infinity,
height: 56,
child: ElevatedButton(
- onPressed: _isLoading ? null : () => _saveTrip(userState.user),
+ onPressed: _isLoading
+ ? null
+ : () => _saveTrip(userState.user),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.teal,
foregroundColor: Colors.white,
@@ -773,15 +798,20 @@ class _CreateTripContentState extends State {
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
- valueColor: AlwaysStoppedAnimation(Colors.white),
+ valueColor: AlwaysStoppedAnimation(
+ Colors.white,
+ ),
),
)
: Text(
- isEditing ? 'Modifier le voyage' : 'Créer le voyage',
- style: theme.textTheme.titleMedium?.copyWith(
- color: Colors.white,
- fontWeight: FontWeight.w600,
- ),
+ isEditing
+ ? 'Modifier le voyage'
+ : 'Créer le voyage',
+ style: theme.textTheme.titleMedium
+ ?.copyWith(
+ color: Colors.white,
+ fontWeight: FontWeight.w600,
+ ),
),
),
),
@@ -846,15 +876,18 @@ class _CreateTripContentState extends State {
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegex.hasMatch(email)) {
if (mounted) {
- ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Email invalide')));
+ ScaffoldMessenger.of(
+ context,
+ ).showSnackBar(SnackBar(content: Text('Email invalide')));
}
return;
}
if (_participants.contains(email)) {
if (mounted) {
- ScaffoldMessenger.of(context)
- .showSnackBar(SnackBar(content: Text('Ce participant est déjà ajouté')));
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text('Ce participant est déjà ajouté')),
+ );
}
return;
}
@@ -879,7 +912,7 @@ class _CreateTripContentState extends State {
) async {
final groupBloc = context.read();
final accountBloc = context.read();
-
+
try {
final group = await _groupRepository.getGroupByTripId(tripId);
@@ -900,7 +933,9 @@ class _CreateTripContentState extends State {
final currentMemberIds = currentMembers.map((m) => m.userId).toSet();
final newMemberIds = newMembers.map((m) => m.userId).toSet();
- final membersToAdd = newMembers.where((m) => !currentMemberIds.contains(m.userId)).toList();
+ final membersToAdd = newMembers
+ .where((m) => !currentMemberIds.contains(m.userId))
+ .toList();
final membersToRemove = currentMembers
.where((m) => !newMemberIds.contains(m.userId) && m.role != 'admin')
@@ -961,14 +996,16 @@ class _CreateTripContentState extends State {
role: 'admin',
profilePictureUrl: currentUser.profilePictureUrl,
),
- ...participantsData.map((p) => GroupMember(
- userId: p['id'] as String,
- firstName: p['firstName'] as String,
- lastName: p['lastName'] as String? ?? '',
- pseudo: p['firstName'] as String,
- role: 'member',
- profilePictureUrl: p['profilePictureUrl'] as String?,
- )),
+ ...participantsData.map(
+ (p) => GroupMember(
+ userId: p['id'] as String,
+ firstName: p['firstName'] as String,
+ lastName: p['lastName'] as String? ?? '',
+ pseudo: p['firstName'] as String,
+ role: 'member',
+ profilePictureUrl: p['profilePictureUrl'] as String?,
+ ),
+ ),
];
return groupMembers;
}
@@ -976,9 +1013,8 @@ class _CreateTripContentState extends State {
Future _createGroupAndAccountForTrip(String tripId) async {
final groupBloc = context.read();
final accountBloc = context.read();
-
+
try {
-
final userState = context.read().state;
if (userState is! user_state.UserLoaded) {
throw Exception('Utilisateur non connecté');
@@ -998,21 +1034,19 @@ class _CreateTripContentState extends State {
throw Exception('Erreur lors de la création des membres du groupe');
}
- groupBloc.add(CreateGroupWithMembers(
- group: group,
- members: groupMembers,
- ));
+ groupBloc.add(
+ CreateGroupWithMembers(group: group, members: groupMembers),
+ );
final account = Account(
id: '',
tripId: tripId,
name: _titleController.text.trim(),
);
- accountBloc.add(CreateAccountWithMembers(
- account: account,
- members: groupMembers,
- ));
-
+ accountBloc.add(
+ CreateAccountWithMembers(account: account, members: groupMembers),
+ );
+
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
@@ -1025,7 +1059,6 @@ class _CreateTripContentState extends State {
});
Navigator.pop(context);
}
-
} catch (e) {
_errorService.logError(
'create_trip_content.dart',
@@ -1034,10 +1067,7 @@ class _CreateTripContentState extends State {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(
- content: Text('Erreur: $e'),
- backgroundColor: Colors.red,
- ),
+ SnackBar(content: Text('Erreur: $e'), backgroundColor: Colors.red),
);
setState(() {
_isLoading = false;
@@ -1046,8 +1076,6 @@ class _CreateTripContentState extends State {
}
}
-
-
Future _saveTrip(user_state.UserModel currentUser) async {
if (!_formKey.currentState!.validate()) {
return;
@@ -1070,7 +1098,9 @@ class _CreateTripContentState extends State {
try {
final participantsData = await _getParticipantsData(_participants);
- List participantIds = participantsData.map((p) => p['id'] as String).toList();
+ List participantIds = participantsData
+ .map((p) => p['id'] as String)
+ .toList();
if (!participantIds.contains(currentUser.id)) {
participantIds.insert(0, currentUser.id);
@@ -1101,7 +1131,9 @@ class _CreateTripContentState extends State {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
- content: Text('Voyage créé sans géolocalisation (pas d\'impact sur les fonctionnalités)'),
+ content: Text(
+ 'Voyage créé sans géolocalisation (pas d\'impact sur les fonctionnalités)',
+ ),
backgroundColor: Colors.orange,
duration: Duration(seconds: 2),
),
@@ -1114,16 +1146,21 @@ class _CreateTripContentState extends State {
tripBloc.add(TripUpdateRequested(trip: tripWithCoordinates));
// Mettre à jour le groupe ET les comptes avec les nouveaux participants
- if (widget.tripToEdit != null && widget.tripToEdit!.id != null && widget.tripToEdit!.id!.isNotEmpty) {
- print('🔄 [CreateTrip] Mise à jour du groupe et du compte pour le voyage ID: ${widget.tripToEdit!.id}');
- print('👥 Participants: ${participantsData.map((p) => p['id']).toList()}');
+ if (widget.tripToEdit != null &&
+ widget.tripToEdit!.id != null &&
+ widget.tripToEdit!.id!.isNotEmpty) {
+ LoggerService.info(
+ '🔄 [CreateTrip] Mise à jour du groupe et du compte pour le voyage ID: ${widget.tripToEdit!.id}',
+ );
+ LoggerService.info(
+ '👥 Participants: ${participantsData.map((p) => p['id']).toList()}',
+ );
await _updateGroupAndAccountMembers(
widget.tripToEdit!.id!,
currentUser,
participantsData,
);
}
-
} else {
// Mode création - Le groupe sera créé dans le listener TripCreated
tripBloc.add(TripCreateRequested(trip: tripWithCoordinates));
@@ -1131,10 +1168,7 @@ class _CreateTripContentState extends State {
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(
- content: Text('Erreur: $e'),
- backgroundColor: Colors.red,
- ),
+ SnackBar(content: Text('Erreur: $e'), backgroundColor: Colors.red),
);
setState(() {
@@ -1144,7 +1178,9 @@ class _CreateTripContentState extends State {
}
}
- Future>> _getParticipantsData(List emails) async {
+ Future>> _getParticipantsData(
+ List emails,
+ ) async {
List