feat: Add expense management features with tabs for expenses, balances, and settlements
- Implemented ExpensesTab to display a list of expenses with details. - Created GroupExpensesPage to manage group expenses with a tabbed interface. - Added SettlementsTab to show optimized settlements between users. - Developed data models for Expense and Balance, including necessary methods for serialization. - Introduced CountRepository for Firestore interactions related to expenses. - Added CountService to handle business logic for expenses and settlements. - Integrated image picker for receipt uploads. - Updated main.dart to include CountBloc and CountRepository. - Enhanced pubspec.yaml with new dependencies for image picking and Firebase storage. Not Tested yet
This commit is contained in:
34
lib/data/models/balance.dart
Normal file
34
lib/data/models/balance.dart
Normal file
@@ -0,0 +1,34 @@
|
||||
class Balance {
|
||||
final String userId;
|
||||
final String userName;
|
||||
final double totalPaid;
|
||||
final double totalOwed;
|
||||
final double balance;
|
||||
|
||||
Balance({
|
||||
required this.userId,
|
||||
required this.userName,
|
||||
required this.totalPaid,
|
||||
required this.totalOwed,
|
||||
}) : balance = totalPaid - totalOwed;
|
||||
|
||||
bool get shouldReceive => balance > 0;
|
||||
bool get shouldPay => balance < 0;
|
||||
double get absoluteBalance => balance.abs();
|
||||
}
|
||||
|
||||
class Settlement {
|
||||
final String fromUserId;
|
||||
final String fromUserName;
|
||||
final String toUserId;
|
||||
final String toUserName;
|
||||
final double amount;
|
||||
|
||||
Settlement({
|
||||
required this.fromUserId,
|
||||
required this.fromUserName,
|
||||
required this.toUserId,
|
||||
required this.toUserName,
|
||||
required this.amount,
|
||||
});
|
||||
}
|
||||
243
lib/data/models/expense.dart
Normal file
243
lib/data/models/expense.dart
Normal file
@@ -0,0 +1,243 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum ExpenseCategory {
|
||||
restaurant,
|
||||
transport,
|
||||
accommodation,
|
||||
activities,
|
||||
shopping,
|
||||
other,
|
||||
}
|
||||
|
||||
enum ExpenseCurrency {
|
||||
eur,
|
||||
usd,
|
||||
gbp,
|
||||
jpy,
|
||||
chf,
|
||||
cad,
|
||||
aud,
|
||||
}
|
||||
|
||||
class ExpenseSplit {
|
||||
final String userId;
|
||||
final String userName;
|
||||
final double amount;
|
||||
final bool isPaid;
|
||||
|
||||
ExpenseSplit({
|
||||
required this.userId,
|
||||
required this.userName,
|
||||
required this.amount,
|
||||
this.isPaid = false,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'userId': userId,
|
||||
'userName': userName,
|
||||
'amount': amount,
|
||||
'isPaid': isPaid,
|
||||
};
|
||||
}
|
||||
|
||||
factory ExpenseSplit.fromMap(Map<String, dynamic> map) {
|
||||
return ExpenseSplit(
|
||||
userId: map['userId'] ?? '',
|
||||
userName: map['userName'] ?? '',
|
||||
amount: (map['amount'] ?? 0.0).toDouble(),
|
||||
isPaid: map['isPaid'] ?? false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Expense {
|
||||
final String id;
|
||||
final String groupId;
|
||||
final String description;
|
||||
final double amount;
|
||||
final ExpenseCurrency currency;
|
||||
final double amountInEur;
|
||||
final ExpenseCategory category;
|
||||
final String paidById;
|
||||
final String paidByName;
|
||||
final List<ExpenseSplit> splits;
|
||||
final String? receiptUrl;
|
||||
final DateTime date;
|
||||
final DateTime createdAt;
|
||||
final bool isArchived;
|
||||
final bool isEdited;
|
||||
final DateTime? editedAt;
|
||||
|
||||
Expense({
|
||||
this.id = '',
|
||||
required this.groupId,
|
||||
required this.description,
|
||||
required this.amount,
|
||||
required this.currency,
|
||||
required this.amountInEur,
|
||||
required this.category,
|
||||
required this.paidById,
|
||||
required this.paidByName,
|
||||
required this.splits,
|
||||
this.receiptUrl,
|
||||
required this.date,
|
||||
DateTime? createdAt,
|
||||
this.isArchived = false,
|
||||
this.isEdited = false,
|
||||
this.editedAt,
|
||||
}) : createdAt = createdAt ?? DateTime.now();
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'groupId': groupId,
|
||||
'description': description,
|
||||
'amount': amount,
|
||||
'currency': currency.name,
|
||||
'amountInEur': amountInEur,
|
||||
'category': category.name,
|
||||
'paidById': paidById,
|
||||
'paidByName': paidByName,
|
||||
'splits': splits.map((s) => s.toMap()).toList(),
|
||||
'receiptUrl': receiptUrl,
|
||||
'date': Timestamp.fromDate(date),
|
||||
'createdAt': Timestamp.fromDate(createdAt),
|
||||
'isArchived': isArchived,
|
||||
'isEdited': isEdited,
|
||||
'editedAt': editedAt != null ? Timestamp.fromDate(editedAt!) : null,
|
||||
};
|
||||
}
|
||||
|
||||
factory Expense.fromFirestore(DocumentSnapshot doc) {
|
||||
final data = doc.data() as Map<String, dynamic>;
|
||||
final editedAtTimestamp = data['editedAt'] as Timestamp?;
|
||||
|
||||
return Expense(
|
||||
id: doc.id,
|
||||
groupId: data['groupId'] ?? '',
|
||||
description: data['description'] ?? '',
|
||||
amount: (data['amount'] ?? 0.0).toDouble(),
|
||||
currency: ExpenseCurrency.values.firstWhere(
|
||||
(e) => e.name == data['currency'],
|
||||
orElse: () => ExpenseCurrency.eur,
|
||||
),
|
||||
amountInEur: (data['amountInEur'] ?? 0.0).toDouble(),
|
||||
category: ExpenseCategory.values.firstWhere(
|
||||
(e) => e.name == data['category'],
|
||||
orElse: () => ExpenseCategory.other,
|
||||
),
|
||||
paidById: data['paidById'] ?? '',
|
||||
paidByName: data['paidByName'] ?? '',
|
||||
splits: (data['splits'] as List<dynamic>?)
|
||||
?.map((s) => ExpenseSplit.fromMap(s as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
receiptUrl: data['receiptUrl'],
|
||||
date: (data['date'] as Timestamp?)?.toDate() ?? DateTime.now(),
|
||||
createdAt: (data['createdAt'] as Timestamp?)?.toDate() ?? DateTime.now(),
|
||||
isArchived: data['isArchived'] ?? false,
|
||||
isEdited: data['isEdited'] ?? false,
|
||||
editedAt: editedAtTimestamp?.toDate(),
|
||||
);
|
||||
}
|
||||
|
||||
Expense copyWith({
|
||||
String? id,
|
||||
String? groupId,
|
||||
String? description,
|
||||
double? amount,
|
||||
ExpenseCurrency? currency,
|
||||
double? amountInEur,
|
||||
ExpenseCategory? category,
|
||||
String? paidById,
|
||||
String? paidByName,
|
||||
List<ExpenseSplit>? splits,
|
||||
String? receiptUrl,
|
||||
DateTime? date,
|
||||
DateTime? createdAt,
|
||||
bool? isArchived,
|
||||
bool? isEdited,
|
||||
DateTime? editedAt,
|
||||
}) {
|
||||
return Expense(
|
||||
id: id ?? this.id,
|
||||
groupId: groupId ?? this.groupId,
|
||||
description: description ?? this.description,
|
||||
amount: amount ?? this.amount,
|
||||
currency: currency ?? this.currency,
|
||||
amountInEur: amountInEur ?? this.amountInEur,
|
||||
category: category ?? this.category,
|
||||
paidById: paidById ?? this.paidById,
|
||||
paidByName: paidByName ?? this.paidByName,
|
||||
splits: splits ?? this.splits,
|
||||
receiptUrl: receiptUrl ?? this.receiptUrl,
|
||||
date: date ?? this.date,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
isArchived: isArchived ?? this.isArchived,
|
||||
isEdited: isEdited ?? this.isEdited,
|
||||
editedAt: editedAt ?? this.editedAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension ExpenseCategoryExtension on ExpenseCategory {
|
||||
String get displayName {
|
||||
switch (this) {
|
||||
case ExpenseCategory.restaurant:
|
||||
return 'Restaurant';
|
||||
case ExpenseCategory.transport:
|
||||
return 'Transport';
|
||||
case ExpenseCategory.accommodation:
|
||||
return 'Hébergement';
|
||||
case ExpenseCategory.activities:
|
||||
return 'Activités';
|
||||
case ExpenseCategory.shopping:
|
||||
return 'Shopping';
|
||||
case ExpenseCategory.other:
|
||||
return 'Autre';
|
||||
}
|
||||
}
|
||||
|
||||
IconData get icon {
|
||||
switch (this) {
|
||||
case ExpenseCategory.restaurant:
|
||||
return Icons.restaurant;
|
||||
case ExpenseCategory.transport:
|
||||
return Icons.directions_car;
|
||||
case ExpenseCategory.accommodation:
|
||||
return Icons.hotel;
|
||||
case ExpenseCategory.activities:
|
||||
return Icons.attractions;
|
||||
case ExpenseCategory.shopping:
|
||||
return Icons.shopping_bag;
|
||||
case ExpenseCategory.other:
|
||||
return Icons.more_horiz;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ExpenseCurrencyExtension on ExpenseCurrency {
|
||||
String get symbol {
|
||||
switch (this) {
|
||||
case ExpenseCurrency.eur:
|
||||
return '€';
|
||||
case ExpenseCurrency.usd:
|
||||
return '\$';
|
||||
case ExpenseCurrency.gbp:
|
||||
return '£';
|
||||
case ExpenseCurrency.jpy:
|
||||
return '¥';
|
||||
case ExpenseCurrency.chf:
|
||||
return 'CHF';
|
||||
case ExpenseCurrency.cad:
|
||||
return 'CAD';
|
||||
case ExpenseCurrency.aud:
|
||||
return 'AUD';
|
||||
}
|
||||
}
|
||||
|
||||
String get code {
|
||||
return name.toUpperCase();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user