feat: Add User and UserBalance models with serialization methods
feat: Implement BalanceRepository for group balance calculations feat: Create ExpenseRepository for managing expenses feat: Add services for handling expenses and storage operations fix: Update import paths for models in repositories and services refactor: Rename CountContent to AccountContent in HomePage chore: Add StorageService for image upload and management
This commit is contained in:
54
lib/models/account.dart
Normal file
54
lib/models/account.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
import 'group_member.dart';
|
||||
|
||||
class Account {
|
||||
final String id;
|
||||
final String tripId;
|
||||
final String groupId;
|
||||
final String name;
|
||||
final List<GroupMember> members;
|
||||
|
||||
Account({
|
||||
required this.id,
|
||||
required this.tripId,
|
||||
required this.groupId,
|
||||
required this.name,
|
||||
List<GroupMember>? members,
|
||||
}) : members = members ?? [];
|
||||
|
||||
|
||||
factory Account.fromMap(Map<String, dynamic> map) {
|
||||
return Account(
|
||||
id: map['id'] as String,
|
||||
tripId: map['tripId'] as String,
|
||||
groupId: map['groupId'] as String,
|
||||
name: map['name'] as String,
|
||||
members: [],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'tripId': tripId,
|
||||
'groupId': groupId,
|
||||
'name': name,
|
||||
'members': members.map((member) => member.toMap()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
Account copyWith({
|
||||
String? id,
|
||||
String? tripId,
|
||||
String? groupId,
|
||||
String? name,
|
||||
List<GroupMember>? members,
|
||||
}) {
|
||||
return Account(
|
||||
id: id ?? this.id,
|
||||
tripId: tripId ?? this.tripId,
|
||||
groupId: groupId ?? this.groupId,
|
||||
name: name ?? this.name,
|
||||
members: members ?? this.members,
|
||||
);
|
||||
}
|
||||
}
|
||||
205
lib/models/expense.dart
Normal file
205
lib/models/expense.dart
Normal file
@@ -0,0 +1,205 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'expense_split.dart';
|
||||
|
||||
enum ExpenseCurrency {
|
||||
eur('€', 'EUR'),
|
||||
usd('\$', 'USD'),
|
||||
gbp('£', 'GBP');
|
||||
|
||||
const ExpenseCurrency(this.symbol, this.code);
|
||||
final String symbol;
|
||||
final String code;
|
||||
}
|
||||
|
||||
enum ExpenseCategory {
|
||||
restaurant('Restaurant', Icons.restaurant),
|
||||
transport('Transport', Icons.directions_car),
|
||||
accommodation('Hébergement', Icons.hotel),
|
||||
entertainment('Loisirs', Icons.local_activity),
|
||||
shopping('Shopping', Icons.shopping_bag),
|
||||
other('Autre', Icons.category);
|
||||
|
||||
const ExpenseCategory(this.displayName, this.icon);
|
||||
final String displayName;
|
||||
final IconData icon;
|
||||
}
|
||||
|
||||
class Expense extends Equatable {
|
||||
final String id;
|
||||
final String groupId;
|
||||
final String description;
|
||||
final double amount;
|
||||
final ExpenseCurrency currency;
|
||||
final double amountInEur; // Montant converti en EUR
|
||||
final ExpenseCategory category;
|
||||
final String paidById;
|
||||
final String paidByName;
|
||||
final DateTime date;
|
||||
final DateTime createdAt;
|
||||
final DateTime? editedAt;
|
||||
final bool isEdited;
|
||||
final bool isArchived;
|
||||
final String? receiptUrl;
|
||||
final List<ExpenseSplit> splits;
|
||||
|
||||
const Expense({
|
||||
required 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.date,
|
||||
required this.createdAt,
|
||||
this.editedAt,
|
||||
this.isEdited = false,
|
||||
this.isArchived = false,
|
||||
this.receiptUrl,
|
||||
required this.splits,
|
||||
});
|
||||
|
||||
factory Expense.fromMap(Map<String, dynamic> map, String id) {
|
||||
return Expense(
|
||||
id: id,
|
||||
groupId: map['groupId'] ?? '',
|
||||
description: map['description'] ?? '',
|
||||
amount: (map['amount'] as num?)?.toDouble() ?? 0.0,
|
||||
currency: ExpenseCurrency.values.firstWhere(
|
||||
(c) => c.code == map['currency'],
|
||||
orElse: () => ExpenseCurrency.eur,
|
||||
),
|
||||
amountInEur: (map['amountInEur'] as num?)?.toDouble() ?? 0.0,
|
||||
category: ExpenseCategory.values.firstWhere(
|
||||
(c) => c.name == map['category'],
|
||||
orElse: () => ExpenseCategory.other,
|
||||
),
|
||||
paidById: map['paidById'] ?? '',
|
||||
paidByName: map['paidByName'] ?? '',
|
||||
date: _parseDateTime(map['date']),
|
||||
createdAt: _parseDateTime(map['createdAt']),
|
||||
editedAt: map['editedAt'] != null ? _parseDateTime(map['editedAt']) : null,
|
||||
isEdited: map['isEdited'] ?? false,
|
||||
isArchived: map['isArchived'] ?? false,
|
||||
receiptUrl: map['receiptUrl'],
|
||||
splits: (map['splits'] as List?)
|
||||
?.map((s) => ExpenseSplit.fromMap(s))
|
||||
.toList() ?? [],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'groupId': groupId,
|
||||
'description': description,
|
||||
'amount': amount,
|
||||
'currency': currency.code,
|
||||
'amountInEur': amountInEur,
|
||||
'category': category.name,
|
||||
'paidById': paidById,
|
||||
'paidByName': paidByName,
|
||||
'date': Timestamp.fromDate(date),
|
||||
'createdAt': Timestamp.fromDate(createdAt),
|
||||
'editedAt': editedAt != null ? Timestamp.fromDate(editedAt!) : null,
|
||||
'isEdited': isEdited,
|
||||
'isArchived': isArchived,
|
||||
'receiptUrl': receiptUrl,
|
||||
'splits': splits.map((s) => s.toMap()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
static DateTime _parseDateTime(dynamic value) {
|
||||
if (value is Timestamp) return value.toDate();
|
||||
if (value is String) return DateTime.parse(value);
|
||||
if (value is DateTime) return value;
|
||||
return DateTime.now();
|
||||
}
|
||||
|
||||
Expense copyWith({
|
||||
String? id,
|
||||
String? groupId,
|
||||
String? description,
|
||||
double? amount,
|
||||
ExpenseCurrency? currency,
|
||||
double? amountInEur,
|
||||
ExpenseCategory? category,
|
||||
String? paidById,
|
||||
String? paidByName,
|
||||
DateTime? date,
|
||||
DateTime? createdAt,
|
||||
DateTime? editedAt,
|
||||
bool? isEdited,
|
||||
bool? isArchived,
|
||||
String? receiptUrl,
|
||||
List<ExpenseSplit>? splits,
|
||||
}) {
|
||||
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,
|
||||
date: date ?? this.date,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
editedAt: editedAt ?? this.editedAt,
|
||||
isEdited: isEdited ?? this.isEdited,
|
||||
isArchived: isArchived ?? this.isArchived,
|
||||
receiptUrl: receiptUrl ?? this.receiptUrl,
|
||||
splits: splits ?? this.splits,
|
||||
);
|
||||
}
|
||||
|
||||
Expense copyWithEdit({
|
||||
String? description,
|
||||
double? amount,
|
||||
ExpenseCurrency? currency,
|
||||
double? amountInEur,
|
||||
ExpenseCategory? category,
|
||||
List<ExpenseSplit>? splits,
|
||||
String? receiptUrl,
|
||||
}) {
|
||||
return copyWith(
|
||||
description: description,
|
||||
amount: amount,
|
||||
currency: currency,
|
||||
amountInEur: amountInEur,
|
||||
category: category,
|
||||
splits: splits,
|
||||
receiptUrl: receiptUrl,
|
||||
editedAt: DateTime.now(),
|
||||
isEdited: true,
|
||||
);
|
||||
}
|
||||
|
||||
// Marquer comme archivé
|
||||
Expense copyWithArchived() {
|
||||
return copyWith(
|
||||
isArchived: true,
|
||||
);
|
||||
}
|
||||
|
||||
// Ajouter/mettre à jour l'URL du reçu
|
||||
Expense copyWithReceipt(String receiptUrl) {
|
||||
return copyWith(
|
||||
receiptUrl: receiptUrl,
|
||||
);
|
||||
}
|
||||
|
||||
// Mettre à jour les splits
|
||||
Expense copyWithSplits(List<ExpenseSplit> newSplits) {
|
||||
return copyWith(
|
||||
splits: newSplits,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id];
|
||||
}
|
||||
58
lib/models/expense_split.dart
Normal file
58
lib/models/expense_split.dart
Normal file
@@ -0,0 +1,58 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class ExpenseSplit extends Equatable {
|
||||
final String userId;
|
||||
final String userName;
|
||||
final double amount;
|
||||
final bool isPaid;
|
||||
final DateTime? paidAt;
|
||||
|
||||
const ExpenseSplit({
|
||||
required this.userId,
|
||||
required this.userName,
|
||||
required this.amount,
|
||||
this.isPaid = false,
|
||||
this.paidAt,
|
||||
});
|
||||
|
||||
factory ExpenseSplit.fromMap(Map<String, dynamic> map) {
|
||||
return ExpenseSplit(
|
||||
userId: map['userId'] ?? '',
|
||||
userName: map['userName'] ?? '',
|
||||
amount: (map['amount'] as num?)?.toDouble() ?? 0.0,
|
||||
isPaid: map['isPaid'] ?? false,
|
||||
paidAt: map['paidAt'] != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(map['paidAt'])
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'userId': userId,
|
||||
'userName': userName,
|
||||
'amount': amount,
|
||||
'isPaid': isPaid,
|
||||
'paidAt': paidAt?.millisecondsSinceEpoch,
|
||||
};
|
||||
}
|
||||
|
||||
ExpenseSplit copyWith({
|
||||
String? userId,
|
||||
String? userName,
|
||||
double? amount,
|
||||
bool? isPaid,
|
||||
DateTime? paidAt,
|
||||
}) {
|
||||
return ExpenseSplit(
|
||||
userId: userId ?? this.userId,
|
||||
userName: userName ?? this.userName,
|
||||
amount: amount ?? this.amount,
|
||||
isPaid: isPaid ?? this.isPaid,
|
||||
paidAt: paidAt ?? this.paidAt,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [userId, userName, amount, isPaid];
|
||||
}
|
||||
65
lib/models/group.dart
Normal file
65
lib/models/group.dart
Normal file
@@ -0,0 +1,65 @@
|
||||
import 'group_member.dart';
|
||||
|
||||
class Group {
|
||||
final String id;
|
||||
final String name;
|
||||
final String tripId;
|
||||
final String createdBy;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final List<GroupMember> members;
|
||||
|
||||
Group({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.tripId,
|
||||
required this.createdBy,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
List<GroupMember>? members,
|
||||
}) : createdAt = createdAt ?? DateTime.now(),
|
||||
updatedAt = updatedAt ?? DateTime.now(),
|
||||
members = members ?? [];
|
||||
|
||||
factory Group.fromMap(Map<String, dynamic> map, String id) {
|
||||
return Group(
|
||||
id: id,
|
||||
name: map['name'] ?? '',
|
||||
tripId: map['tripId'] ?? '',
|
||||
createdBy: map['createdBy'] ?? '',
|
||||
createdAt: DateTime.fromMillisecondsSinceEpoch(map['createdAt'] ?? 0),
|
||||
updatedAt: DateTime.fromMillisecondsSinceEpoch(map['updatedAt'] ?? 0),
|
||||
members: [],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'name': name,
|
||||
'tripId': tripId,
|
||||
'createdBy': createdBy,
|
||||
'createdAt': createdAt.millisecondsSinceEpoch,
|
||||
'updatedAt': updatedAt.millisecondsSinceEpoch,
|
||||
};
|
||||
}
|
||||
|
||||
Group copyWith({
|
||||
String? id,
|
||||
String? name,
|
||||
String? tripId,
|
||||
String? createdBy,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
List<GroupMember>? members,
|
||||
}) {
|
||||
return Group(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
tripId: tripId ?? this.tripId,
|
||||
createdBy: createdBy ?? this.createdBy,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
members: members ?? this.members,
|
||||
);
|
||||
}
|
||||
}
|
||||
93
lib/models/group_balance.dart
Normal file
93
lib/models/group_balance.dart
Normal file
@@ -0,0 +1,93 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'user_balance.dart';
|
||||
import 'settlement.dart';
|
||||
|
||||
class GroupBalance extends Equatable {
|
||||
final String groupId;
|
||||
final List<UserBalance> userBalances;
|
||||
final List<Settlement> settlements;
|
||||
final double totalExpenses;
|
||||
final DateTime calculatedAt;
|
||||
|
||||
const GroupBalance({
|
||||
required this.groupId,
|
||||
required this.userBalances,
|
||||
required this.settlements,
|
||||
required this.totalExpenses,
|
||||
required this.calculatedAt,
|
||||
});
|
||||
|
||||
// Constructeur factory pour créer depuis une Map
|
||||
factory GroupBalance.fromMap(Map<String, dynamic> map) {
|
||||
return GroupBalance(
|
||||
groupId: map['groupId'] ?? '',
|
||||
userBalances: (map['userBalances'] as List?)
|
||||
?.map((userBalance) => UserBalance.fromMap(userBalance as Map<String, dynamic>))
|
||||
.toList() ?? [],
|
||||
settlements: (map['settlements'] as List?)
|
||||
?.map((settlement) => Settlement.fromMap(settlement as Map<String, dynamic>))
|
||||
.toList() ?? [],
|
||||
totalExpenses: (map['totalExpenses'] as num?)?.toDouble() ?? 0.0,
|
||||
calculatedAt: _parseDateTime(map['calculatedAt']),
|
||||
);
|
||||
}
|
||||
|
||||
// Convertir en Map pour Firestore
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'groupId': groupId,
|
||||
'userBalances': userBalances.map((userBalance) => userBalance.toMap()).toList(),
|
||||
'settlements': settlements.map((settlement) => settlement.toMap()).toList(),
|
||||
'totalExpenses': totalExpenses,
|
||||
'calculatedAt': Timestamp.fromDate(calculatedAt),
|
||||
};
|
||||
}
|
||||
|
||||
// Méthode copyWith pour créer une copie modifiée
|
||||
GroupBalance copyWith({
|
||||
String? groupId,
|
||||
List<UserBalance>? userBalances,
|
||||
List<Settlement>? settlements,
|
||||
double? totalExpenses,
|
||||
DateTime? calculatedAt,
|
||||
}) {
|
||||
return GroupBalance(
|
||||
groupId: groupId ?? this.groupId,
|
||||
userBalances: userBalances ?? this.userBalances,
|
||||
settlements: settlements ?? this.settlements,
|
||||
totalExpenses: totalExpenses ?? this.totalExpenses,
|
||||
calculatedAt: calculatedAt ?? this.calculatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
// Helper pour parser les dates de différents formats
|
||||
static DateTime _parseDateTime(dynamic value) {
|
||||
if (value is Timestamp) return value.toDate();
|
||||
if (value is String) return DateTime.parse(value);
|
||||
if (value is DateTime) return value;
|
||||
if (value is int) return DateTime.fromMillisecondsSinceEpoch(value);
|
||||
return DateTime.now();
|
||||
}
|
||||
|
||||
// Méthodes utilitaires pour la logique métier
|
||||
bool get hasUnbalancedUsers => userBalances.any((balance) => !balance.isBalanced);
|
||||
|
||||
bool get hasSettlements => settlements.isNotEmpty;
|
||||
|
||||
double get totalSettlementAmount => settlements.fold(0.0, (sum, settlement) => sum + settlement.amount);
|
||||
|
||||
List<UserBalance> get creditors => userBalances.where((b) => b.shouldReceive).toList();
|
||||
|
||||
List<UserBalance> get debtors => userBalances.where((b) => b.shouldPay).toList();
|
||||
|
||||
int get participantCount => userBalances.length;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [groupId, calculatedAt];
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'GroupBalance(groupId: $groupId, totalExpenses: $totalExpenses, participantCount: $participantCount, calculatedAt: $calculatedAt)';
|
||||
}
|
||||
}
|
||||
51
lib/models/group_member.dart
Normal file
51
lib/models/group_member.dart
Normal file
@@ -0,0 +1,51 @@
|
||||
class GroupMember {
|
||||
final String userId;
|
||||
final String firstName;
|
||||
final String pseudo; // Pseudo du membre (par défaut = prénom)
|
||||
final String role; // 'admin' ou 'member'
|
||||
final DateTime joinedAt;
|
||||
|
||||
GroupMember({
|
||||
required this.userId,
|
||||
required this.firstName,
|
||||
String? pseudo,
|
||||
this.role = 'member',
|
||||
DateTime? joinedAt,
|
||||
}) : pseudo = pseudo ?? firstName, // Par défaut, pseudo = prénom
|
||||
joinedAt = joinedAt ?? DateTime.now();
|
||||
|
||||
factory GroupMember.fromMap(Map<String, dynamic> map, String userId) {
|
||||
return GroupMember(
|
||||
userId: userId,
|
||||
firstName: map['firstName'] ?? '',
|
||||
pseudo: map['pseudo'] ?? map['firstName'] ?? '',
|
||||
role: map['role'] ?? 'member',
|
||||
joinedAt: DateTime.fromMillisecondsSinceEpoch(map['joinedAt'] ?? 0),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'firstName': firstName,
|
||||
'pseudo': pseudo,
|
||||
'role': role,
|
||||
'joinedAt': joinedAt.millisecondsSinceEpoch,
|
||||
};
|
||||
}
|
||||
|
||||
GroupMember copyWith({
|
||||
String? userId,
|
||||
String? firstName,
|
||||
String? pseudo,
|
||||
String? role,
|
||||
DateTime? joinedAt,
|
||||
}) {
|
||||
return GroupMember(
|
||||
userId: userId ?? this.userId,
|
||||
firstName: firstName ?? this.firstName,
|
||||
pseudo: pseudo ?? this.pseudo,
|
||||
role: role ?? this.role,
|
||||
joinedAt: joinedAt ?? this.joinedAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
25
lib/models/group_statistics.dart
Normal file
25
lib/models/group_statistics.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
class GroupStatistics {
|
||||
final double totalExpenses;
|
||||
final int expenseCount;
|
||||
final double averageExpense;
|
||||
final String topCategory;
|
||||
final Map<String, double> categoryBreakdown;
|
||||
|
||||
const GroupStatistics({
|
||||
required this.totalExpenses,
|
||||
required this.expenseCount,
|
||||
required this.averageExpense,
|
||||
required this.topCategory,
|
||||
required this.categoryBreakdown,
|
||||
});
|
||||
|
||||
factory GroupStatistics.empty() {
|
||||
return const GroupStatistics(
|
||||
totalExpenses: 0.0,
|
||||
expenseCount: 0,
|
||||
averageExpense: 0.0,
|
||||
topCategory: '',
|
||||
categoryBreakdown: {},
|
||||
);
|
||||
}
|
||||
}
|
||||
81
lib/models/message.dart
Normal file
81
lib/models/message.dart
Normal file
@@ -0,0 +1,81 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
|
||||
class Message {
|
||||
final String id;
|
||||
final String text;
|
||||
final DateTime timestamp;
|
||||
final String senderId;
|
||||
final String senderName;
|
||||
final String groupId;
|
||||
final Map<String, String> reactions; // userId -> emoji
|
||||
final DateTime? editedAt;
|
||||
final bool isEdited;
|
||||
|
||||
Message({
|
||||
this.id = '',
|
||||
required this.text,
|
||||
required this.timestamp,
|
||||
required this.senderId,
|
||||
required this.senderName,
|
||||
required this.groupId,
|
||||
this.reactions = const {},
|
||||
this.editedAt,
|
||||
this.isEdited = false,
|
||||
});
|
||||
|
||||
factory Message.fromFirestore(DocumentSnapshot doc) {
|
||||
final data = doc.data() as Map<String, dynamic>;
|
||||
final timestamp = data['timestamp'] as Timestamp?;
|
||||
final editedAtTimestamp = data['editedAt'] as Timestamp?;
|
||||
final reactionsData = data['reactions'] as Map<String, dynamic>?;
|
||||
|
||||
return Message(
|
||||
id: doc.id,
|
||||
text: data['text'] ?? '',
|
||||
timestamp: timestamp?.toDate() ?? DateTime.now(),
|
||||
senderId: data['senderId'] ?? '',
|
||||
senderName: data['senderName'] ?? 'Anonyme',
|
||||
groupId: data['groupId'] ?? '',
|
||||
reactions: reactionsData?.map((key, value) => MapEntry(key, value.toString())) ?? {},
|
||||
editedAt: editedAtTimestamp?.toDate(),
|
||||
isEdited: data['isEdited'] ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toFirestore() {
|
||||
return {
|
||||
'text': text,
|
||||
'senderId': senderId,
|
||||
'senderName': senderName,
|
||||
'timestamp': Timestamp.fromDate(timestamp),
|
||||
'groupId': groupId,
|
||||
'reactions': reactions,
|
||||
'editedAt': editedAt != null ? Timestamp.fromDate(editedAt!) : null,
|
||||
'isEdited': isEdited,
|
||||
};
|
||||
}
|
||||
|
||||
Message copyWith({
|
||||
String? id,
|
||||
String? text,
|
||||
DateTime? timestamp,
|
||||
String? senderId,
|
||||
String? senderName,
|
||||
String? groupId,
|
||||
Map<String, String>? reactions,
|
||||
DateTime? editedAt,
|
||||
bool? isEdited,
|
||||
}) {
|
||||
return Message(
|
||||
id: id ?? this.id,
|
||||
text: text ?? this.text,
|
||||
timestamp: timestamp ?? this.timestamp,
|
||||
senderId: senderId ?? this.senderId,
|
||||
senderName: senderName ?? this.senderName,
|
||||
groupId: groupId ?? this.groupId,
|
||||
reactions: reactions ?? this.reactions,
|
||||
editedAt: editedAt ?? this.editedAt,
|
||||
isEdited: isEdited ?? this.isEdited,
|
||||
);
|
||||
}
|
||||
}
|
||||
130
lib/models/settlement.dart
Normal file
130
lib/models/settlement.dart
Normal file
@@ -0,0 +1,130 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class Settlement extends Equatable {
|
||||
final String fromUserId;
|
||||
final String fromUserName;
|
||||
final String toUserId;
|
||||
final String toUserName;
|
||||
final double amount;
|
||||
final bool isCompleted;
|
||||
final DateTime? completedAt;
|
||||
|
||||
const Settlement({
|
||||
required this.fromUserId,
|
||||
required this.fromUserName,
|
||||
required this.toUserId,
|
||||
required this.toUserName,
|
||||
required this.amount,
|
||||
this.isCompleted = false,
|
||||
this.completedAt,
|
||||
});
|
||||
|
||||
// Constructeur factory pour créer depuis une Map
|
||||
factory Settlement.fromMap(Map<String, dynamic> map) {
|
||||
return Settlement(
|
||||
fromUserId: map['fromUserId'] ?? '',
|
||||
fromUserName: map['fromUserName'] ?? '',
|
||||
toUserId: map['toUserId'] ?? '',
|
||||
toUserName: map['toUserName'] ?? '',
|
||||
amount: (map['amount'] as num?)?.toDouble() ?? 0.0,
|
||||
isCompleted: map['isCompleted'] ?? false,
|
||||
completedAt: map['completedAt'] != null
|
||||
? _parseDateTime(map['completedAt'])
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
// Convertir en Map pour la sérialisation
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'fromUserId': fromUserId,
|
||||
'fromUserName': fromUserName,
|
||||
'toUserId': toUserId,
|
||||
'toUserName': toUserName,
|
||||
'amount': amount,
|
||||
'isCompleted': isCompleted,
|
||||
'completedAt': completedAt != null
|
||||
? Timestamp.fromDate(completedAt!)
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
// Méthode copyWith étendue pour créer une copie modifiée
|
||||
Settlement copyWith({
|
||||
String? fromUserId,
|
||||
String? fromUserName,
|
||||
String? toUserId,
|
||||
String? toUserName,
|
||||
double? amount,
|
||||
bool? isCompleted,
|
||||
DateTime? completedAt,
|
||||
}) {
|
||||
return Settlement(
|
||||
fromUserId: fromUserId ?? this.fromUserId,
|
||||
fromUserName: fromUserName ?? this.fromUserName,
|
||||
toUserId: toUserId ?? this.toUserId,
|
||||
toUserName: toUserName ?? this.toUserName,
|
||||
amount: amount ?? this.amount,
|
||||
isCompleted: isCompleted ?? this.isCompleted,
|
||||
completedAt: completedAt ?? this.completedAt,
|
||||
);
|
||||
}
|
||||
|
||||
// Constructeur factory pour marquer comme complété
|
||||
factory Settlement.completed(Settlement settlement) {
|
||||
return settlement.copyWith(
|
||||
isCompleted: true,
|
||||
completedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
// Helper pour parser les dates de différents formats
|
||||
static DateTime _parseDateTime(dynamic value) {
|
||||
if (value is Timestamp) return value.toDate();
|
||||
if (value is String) return DateTime.parse(value);
|
||||
if (value is DateTime) return value;
|
||||
if (value is int) return DateTime.fromMillisecondsSinceEpoch(value);
|
||||
return DateTime.now();
|
||||
}
|
||||
|
||||
// Getters utilitaires pour la logique métier
|
||||
bool get isPending => !isCompleted;
|
||||
|
||||
String get formattedAmount => '${amount.toStringAsFixed(2)} €';
|
||||
|
||||
String get description => '$fromUserName doit ${formattedAmount} à $toUserName';
|
||||
|
||||
String get shortDescription => '$fromUserName → $toUserName';
|
||||
|
||||
String get status => isCompleted ? 'Payé' : 'En attente';
|
||||
|
||||
// Durée depuis la completion (si applicable)
|
||||
Duration? get timeSinceCompletion {
|
||||
if (completedAt == null) return null;
|
||||
return DateTime.now().difference(completedAt!);
|
||||
}
|
||||
|
||||
// Formatage de la date de completion
|
||||
String get formattedCompletedAt {
|
||||
if (completedAt == null) return 'Non payé';
|
||||
return 'Payé le ${completedAt!.day}/${completedAt!.month}/${completedAt!.year}';
|
||||
}
|
||||
|
||||
// Méthodes de validation
|
||||
bool get isValid =>
|
||||
fromUserId.isNotEmpty &&
|
||||
toUserId.isNotEmpty &&
|
||||
fromUserId != toUserId &&
|
||||
amount > 0;
|
||||
|
||||
bool get isSelfSettlement => fromUserId == toUserId;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [fromUserId, toUserId, amount, isCompleted];
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Settlement(${shortDescription}: ${formattedAmount}, status: $status)';
|
||||
}
|
||||
}
|
||||
209
lib/models/trip.dart
Normal file
209
lib/models/trip.dart
Normal file
@@ -0,0 +1,209 @@
|
||||
import 'dart:convert';
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
|
||||
class Trip {
|
||||
final String? id;
|
||||
final String title;
|
||||
final String description;
|
||||
final String location;
|
||||
final DateTime startDate;
|
||||
final DateTime endDate;
|
||||
final double? budget;
|
||||
final List<String> participants;
|
||||
final String createdBy;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final String status;
|
||||
|
||||
Trip({
|
||||
this.id,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.location,
|
||||
required this.startDate,
|
||||
required this.endDate,
|
||||
this.budget,
|
||||
required this.participants,
|
||||
required this.createdBy,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.status = 'draft',
|
||||
});
|
||||
|
||||
// NOUVELLE MÉTHODE HELPER pour convertir n'importe quel format de date
|
||||
static DateTime _parseDateTime(dynamic value) {
|
||||
if (value == null) return DateTime.now();
|
||||
|
||||
// Si c'est déjà un Timestamp Firebase
|
||||
if (value is Timestamp) {
|
||||
return value.toDate();
|
||||
}
|
||||
|
||||
// Si c'est un int (millisecondes depuis epoch)
|
||||
if (value is int) {
|
||||
return DateTime.fromMillisecondsSinceEpoch(value);
|
||||
}
|
||||
|
||||
// Si c'est un String (ISO 8601)
|
||||
if (value is String) {
|
||||
return DateTime.parse(value);
|
||||
}
|
||||
|
||||
// Si c'est déjà un DateTime
|
||||
if (value is DateTime) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// Par défaut
|
||||
return DateTime.now();
|
||||
}
|
||||
|
||||
// Constructeur pour créer un Trip depuis un Map (utile pour Firebase)
|
||||
factory Trip.fromMap(Map<String, dynamic> map, String id) {
|
||||
try {
|
||||
return Trip(
|
||||
id: id,
|
||||
title: map['title'] as String? ?? '',
|
||||
description: map['description'] as String? ?? '',
|
||||
location: map['location'] as String? ?? '',
|
||||
startDate: _parseDateTime(map['startDate']),
|
||||
endDate: _parseDateTime(map['endDate']),
|
||||
budget: (map['budget'] as num?)?.toDouble(),
|
||||
createdBy: map['createdBy'] as String? ?? '',
|
||||
participants: List<String>.from(map['participants'] as List? ?? []),
|
||||
createdAt: _parseDateTime(map['createdAt']),
|
||||
updatedAt: _parseDateTime(map['updatedAt']),
|
||||
status: map['status'] as String? ?? 'draft',
|
||||
);
|
||||
} catch (e) {
|
||||
print('❌ Erreur parsing Trip: $e');
|
||||
print('Map reçue: $map');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// MODIFIÉ : Convertir en Map avec Timestamp pour Firestore
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'title': title,
|
||||
'description': description,
|
||||
'location': location,
|
||||
'startDate': Timestamp.fromDate(startDate),
|
||||
'endDate': Timestamp.fromDate(endDate),
|
||||
'budget': budget,
|
||||
'participants': participants,
|
||||
'createdBy': createdBy,
|
||||
'createdAt': Timestamp.fromDate(createdAt),
|
||||
'updatedAt': Timestamp.fromDate(updatedAt),
|
||||
'status': status,
|
||||
};
|
||||
}
|
||||
|
||||
// Méthode pour convertir un Trip en JSON
|
||||
String toJson() {
|
||||
return json.encode(toMap());
|
||||
}
|
||||
|
||||
// Méthode pour créer une copie avec des modifications
|
||||
Trip copyWith({
|
||||
String? id,
|
||||
String? title,
|
||||
String? description,
|
||||
String? location,
|
||||
DateTime? startDate,
|
||||
DateTime? endDate,
|
||||
double? budget,
|
||||
List<String>? participants,
|
||||
String? createdBy,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
String? status,
|
||||
}) {
|
||||
return Trip(
|
||||
id: id ?? this.id,
|
||||
title: title ?? this.title,
|
||||
description: description ?? this.description,
|
||||
location: location ?? this.location,
|
||||
startDate: startDate ?? this.startDate,
|
||||
endDate: endDate ?? this.endDate,
|
||||
budget: budget ?? this.budget,
|
||||
participants: participants ?? this.participants,
|
||||
createdBy: createdBy ?? this.createdBy,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
status: status ?? this.status,
|
||||
);
|
||||
}
|
||||
|
||||
// Méthode pour obtenir la durée du voyage en jours
|
||||
int get durationInDays {
|
||||
return endDate.difference(startDate).inDays + 1;
|
||||
}
|
||||
|
||||
// Méthode pour vérifier si le voyage est en cours
|
||||
bool get isActive {
|
||||
final now = DateTime.now();
|
||||
return status == 'active' &&
|
||||
now.isAfter(startDate) &&
|
||||
now.isBefore(endDate.add(Duration(days: 1)));
|
||||
}
|
||||
|
||||
// Méthode pour vérifier si le voyage est à venir
|
||||
bool get isUpcoming {
|
||||
final now = DateTime.now();
|
||||
return status == 'active' && now.isBefore(startDate);
|
||||
}
|
||||
|
||||
// Méthode pour vérifier si le voyage est terminé
|
||||
bool get isCompleted {
|
||||
final now = DateTime.now();
|
||||
return status == 'completed' ||
|
||||
(status == 'active' && now.isAfter(endDate));
|
||||
}
|
||||
|
||||
// Méthode pour obtenir le budget par participant
|
||||
double? get budgetPerParticipant {
|
||||
if (budget == null || participants.isEmpty) return null;
|
||||
return budget! / (participants.length + 1);
|
||||
}
|
||||
|
||||
// Méthode pour obtenir le nombre total de participants (incluant le créateur)
|
||||
int get totalParticipants {
|
||||
return participants.length + 1;
|
||||
}
|
||||
|
||||
// Méthode pour formater les dates
|
||||
String get formattedDates {
|
||||
return '${startDate.day}/${startDate.month}/${startDate.year} - ${endDate.day}/${endDate.month}/${endDate.year}';
|
||||
}
|
||||
|
||||
// Méthode pour obtenir le statut formaté
|
||||
String get formattedStatus {
|
||||
switch (status) {
|
||||
case 'draft':
|
||||
return 'Brouillon';
|
||||
case 'active':
|
||||
return 'Actif';
|
||||
case 'completed':
|
||||
return 'Terminé';
|
||||
case 'cancelled':
|
||||
return 'Annulé';
|
||||
default:
|
||||
return 'Inconnu';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Trip(id: $id, title: $title, location: $location, status: $status)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
return other is Trip && other.id == id;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode;
|
||||
}
|
||||
79
lib/models/user.dart
Normal file
79
lib/models/user.dart
Normal file
@@ -0,0 +1,79 @@
|
||||
import 'dart:convert';
|
||||
|
||||
class User {
|
||||
final String? id;
|
||||
final String nom;
|
||||
final String prenom;
|
||||
final String email;
|
||||
|
||||
User({
|
||||
this.id,
|
||||
required this.nom,
|
||||
required this.prenom,
|
||||
required this.email,
|
||||
});
|
||||
|
||||
// Constructeur pour créer un User depuis un Map (utile pour Firebase)
|
||||
factory User.fromMap(Map<String, dynamic> map) {
|
||||
return User(
|
||||
id: map['id'],
|
||||
nom: map['nom'] ?? '',
|
||||
prenom: map['prenom'] ?? '',
|
||||
email: map['email'] ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
// Constructeur pour créer un User depuis JSON
|
||||
factory User.fromJson(String jsonStr) {
|
||||
Map<String, dynamic> map = json.decode(jsonStr);
|
||||
return User.fromMap(map);
|
||||
}
|
||||
|
||||
// Méthode pour convertir un User en Map (utile pour Firebase)
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'nom': nom,
|
||||
'prenom': prenom,
|
||||
'email': email,
|
||||
};
|
||||
}
|
||||
|
||||
// Méthode pour convertir un User en JSON
|
||||
String toJson() {
|
||||
return json.encode(toMap());
|
||||
}
|
||||
|
||||
// Méthode pour obtenir le nom complet
|
||||
String get fullName => '$prenom $nom';
|
||||
|
||||
// Méthode pour créer une copie avec des modifications
|
||||
User copyWith({
|
||||
String? id,
|
||||
String? nom,
|
||||
String? prenom,
|
||||
String? email,
|
||||
}) {
|
||||
return User(
|
||||
id: id ?? this.id,
|
||||
nom: nom ?? this.nom,
|
||||
prenom: prenom ?? this.prenom,
|
||||
email: email ?? this.email,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'User(id: $id, nom: $nom, prenom: $prenom, email: $email)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
return other is User && other.email == email;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => email.hashCode;
|
||||
}
|
||||
|
||||
98
lib/models/user_balance.dart
Normal file
98
lib/models/user_balance.dart
Normal file
@@ -0,0 +1,98 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class UserBalance extends Equatable {
|
||||
final String userId;
|
||||
final String userName;
|
||||
final double totalPaid; // Total payé par cet utilisateur
|
||||
final double totalOwed; // Total dû par cet utilisateur
|
||||
final double balance; // Différence (positif = à recevoir, négatif = à payer)
|
||||
|
||||
const UserBalance({
|
||||
required this.userId,
|
||||
required this.userName,
|
||||
required this.totalPaid,
|
||||
required this.totalOwed,
|
||||
required this.balance,
|
||||
});
|
||||
|
||||
// Constructeur factory pour créer depuis une Map
|
||||
factory UserBalance.fromMap(Map<String, dynamic> map) {
|
||||
return UserBalance(
|
||||
userId: map['userId'] ?? '',
|
||||
userName: map['userName'] ?? '',
|
||||
totalPaid: (map['totalPaid'] as num?)?.toDouble() ?? 0.0,
|
||||
totalOwed: (map['totalOwed'] as num?)?.toDouble() ?? 0.0,
|
||||
balance: (map['balance'] as num?)?.toDouble() ?? 0.0,
|
||||
);
|
||||
}
|
||||
|
||||
// Convertir en Map pour la sérialisation
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'userId': userId,
|
||||
'userName': userName,
|
||||
'totalPaid': totalPaid,
|
||||
'totalOwed': totalOwed,
|
||||
'balance': balance,
|
||||
};
|
||||
}
|
||||
|
||||
// Méthode copyWith pour créer une copie modifiée
|
||||
UserBalance copyWith({
|
||||
String? userId,
|
||||
String? userName,
|
||||
double? totalPaid,
|
||||
double? totalOwed,
|
||||
double? balance,
|
||||
}) {
|
||||
return UserBalance(
|
||||
userId: userId ?? this.userId,
|
||||
userName: userName ?? this.userName,
|
||||
totalPaid: totalPaid ?? this.totalPaid,
|
||||
totalOwed: totalOwed ?? this.totalOwed,
|
||||
balance: balance ?? this.balance,
|
||||
);
|
||||
}
|
||||
|
||||
// Constructeur factory pour recalculer automatiquement la balance
|
||||
factory UserBalance.calculated({
|
||||
required String userId,
|
||||
required String userName,
|
||||
required double totalPaid,
|
||||
required double totalOwed,
|
||||
}) {
|
||||
return UserBalance(
|
||||
userId: userId,
|
||||
userName: userName,
|
||||
totalPaid: totalPaid,
|
||||
totalOwed: totalOwed,
|
||||
balance: totalPaid - totalOwed,
|
||||
);
|
||||
}
|
||||
|
||||
// Getters pour la logique métier
|
||||
bool get shouldReceive => balance > 0;
|
||||
bool get shouldPay => balance < 0;
|
||||
bool get isBalanced => balance.abs() < 0.01; // Tolérance pour les arrondis
|
||||
|
||||
// Montants absolus pour l'affichage
|
||||
double get absoluteBalance => balance.abs();
|
||||
String get balanceStatus {
|
||||
if (isBalanced) return 'Équilibré';
|
||||
if (shouldReceive) return 'À recevoir';
|
||||
return 'À payer';
|
||||
}
|
||||
|
||||
// Formatage pour l'affichage
|
||||
String get formattedBalance => '${absoluteBalance.toStringAsFixed(2)} €';
|
||||
String get formattedTotalPaid => '${totalPaid.toStringAsFixed(2)} €';
|
||||
String get formattedTotalOwed => '${totalOwed.toStringAsFixed(2)} €';
|
||||
|
||||
@override
|
||||
List<Object?> get props => [userId, userName, balance];
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'UserBalance(userId: $userId, userName: $userName, balance: ${balance.toStringAsFixed(2)}€)';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user