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:
Dayron
2025-10-21 16:02:58 +02:00
parent 62eb434548
commit 4edbd1cf34
60 changed files with 1973 additions and 342 deletions

54
lib/models/account.dart Normal file
View 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
View 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];
}

View 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
View 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,
);
}
}

View 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)';
}
}

View 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,
);
}
}

View 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
View 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
View 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
View 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
View 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;
}

View 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)}€)';
}
}