feat: Implement activity management feature with Firestore integration

- Added AddActivityBottomSheet for adding custom activities to trips.
- Created Activity model to represent tourist activities.
- Developed ActivityRepository for managing activities in Firestore.
- Integrated ActivityPlacesService for searching activities via Google Places API.
- Updated ShowTripDetailsContent to navigate to activities page.
- Enhanced main.dart to include ActivityBloc and necessary repositories.
This commit is contained in:
Dayron
2025-11-03 16:40:33 +01:00
parent 64fcc88984
commit 8ff9e12fd4
11 changed files with 3185 additions and 1 deletions

231
lib/models/activity.dart Normal file
View File

@@ -0,0 +1,231 @@
import 'package:cloud_firestore/cloud_firestore.dart';
/// Modèle représentant une activité touristique liée à un voyage
class Activity {
final String id;
final String tripId;
final String name;
final String description;
final String category;
final String? imageUrl;
final double? rating;
final String? priceLevel;
final String? address;
final double? latitude;
final double? longitude;
final String? placeId; // Google Places ID
final String? website;
final String? phoneNumber;
final List<String> openingHours;
final Map<String, int> votes; // userId -> vote (1 pour pour, -1 pour contre)
final DateTime createdAt;
final DateTime updatedAt;
Activity({
required this.id,
required this.tripId,
required this.name,
required this.description,
required this.category,
this.imageUrl,
this.rating,
this.priceLevel,
this.address,
this.latitude,
this.longitude,
this.placeId,
this.website,
this.phoneNumber,
this.openingHours = const [],
this.votes = const {},
required this.createdAt,
required this.updatedAt,
});
/// Calcule le score total des votes
int get totalVotes {
return votes.values.fold(0, (sum, vote) => sum + vote);
}
/// Calcule le nombre de votes positifs
int get positiveVotes {
return votes.values.where((vote) => vote > 0).length;
}
/// Calcule le nombre de votes négatifs
int get negativeVotes {
return votes.values.where((vote) => vote < 0).length;
}
/// Vérifie si l'utilisateur a voté
bool hasUserVoted(String userId) {
return votes.containsKey(userId);
}
/// Récupère le vote d'un utilisateur (-1, 0, 1)
int getUserVote(String userId) {
return votes[userId] ?? 0;
}
/// Crée une copie avec des modifications
Activity copyWith({
String? id,
String? tripId,
String? name,
String? description,
String? category,
String? imageUrl,
double? rating,
String? priceLevel,
String? address,
double? latitude,
double? longitude,
String? placeId,
String? website,
String? phoneNumber,
List<String>? openingHours,
Map<String, int>? votes,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return Activity(
id: id ?? this.id,
tripId: tripId ?? this.tripId,
name: name ?? this.name,
description: description ?? this.description,
category: category ?? this.category,
imageUrl: imageUrl ?? this.imageUrl,
rating: rating ?? this.rating,
priceLevel: priceLevel ?? this.priceLevel,
address: address ?? this.address,
latitude: latitude ?? this.latitude,
longitude: longitude ?? this.longitude,
placeId: placeId ?? this.placeId,
website: website ?? this.website,
phoneNumber: phoneNumber ?? this.phoneNumber,
openingHours: openingHours ?? this.openingHours,
votes: votes ?? this.votes,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
/// Conversion vers Map pour Firestore
Map<String, dynamic> toMap() {
return {
'id': id,
'tripId': tripId,
'name': name,
'description': description,
'category': category,
'imageUrl': imageUrl,
'rating': rating,
'priceLevel': priceLevel,
'address': address,
'latitude': latitude,
'longitude': longitude,
'placeId': placeId,
'website': website,
'phoneNumber': phoneNumber,
'openingHours': openingHours,
'votes': votes,
'createdAt': Timestamp.fromDate(createdAt),
'updatedAt': Timestamp.fromDate(updatedAt),
};
}
/// Création depuis Map Firestore
factory Activity.fromMap(Map<String, dynamic> map) {
return Activity(
id: map['id'] ?? '',
tripId: map['tripId'] ?? '',
name: map['name'] ?? '',
description: map['description'] ?? '',
category: map['category'] ?? '',
imageUrl: map['imageUrl'],
rating: map['rating']?.toDouble(),
priceLevel: map['priceLevel'],
address: map['address'],
latitude: map['latitude']?.toDouble(),
longitude: map['longitude']?.toDouble(),
placeId: map['placeId'],
website: map['website'],
phoneNumber: map['phoneNumber'],
openingHours: List<String>.from(map['openingHours'] ?? []),
votes: Map<String, int>.from(map['votes'] ?? {}),
createdAt: (map['createdAt'] as Timestamp).toDate(),
updatedAt: (map['updatedAt'] as Timestamp).toDate(),
);
}
/// Création depuis snapshot Firestore
factory Activity.fromSnapshot(DocumentSnapshot snapshot) {
final data = snapshot.data() as Map<String, dynamic>;
return Activity.fromMap({...data, 'id': snapshot.id});
}
@override
String toString() {
return 'Activity(id: $id, name: $name, category: $category, votes: ${totalVotes})';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is Activity && other.id == id;
}
@override
int get hashCode => id.hashCode;
}
/// Énumération des catégories d'activités
enum ActivityCategory {
museum('Musée', 'museum'),
restaurant('Restaurant', 'restaurant'),
attraction('Attraction', 'tourist_attraction'),
entertainment('Divertissement', 'amusement_park'),
shopping('Shopping', 'shopping_mall'),
nature('Nature', 'park'),
culture('Culture', 'establishment'),
nightlife('Vie nocturne', 'night_club'),
sports('Sports', 'gym'),
relaxation('Détente', 'spa');
const ActivityCategory(this.displayName, this.googlePlaceType);
final String displayName;
final String googlePlaceType;
static ActivityCategory? fromGoogleType(String type) {
for (final category in ActivityCategory.values) {
if (category.googlePlaceType == type) {
return category;
}
}
return null;
}
}
/// Énumération des niveaux de prix
enum PriceLevel {
free('Gratuit', 0),
inexpensive('Bon marché', 1),
moderate('Modéré', 2),
expensive('Cher', 3),
veryExpensive('Très cher', 4);
const PriceLevel(this.displayName, this.level);
final String displayName;
final int level;
static PriceLevel? fromLevel(int level) {
for (final price in PriceLevel.values) {
if (price.level == level) {
return price;
}
}
return null;
}
}