293 lines
8.3 KiB
Dart
293 lines
8.3 KiB
Dart
//import 'package:supabase_flutter/supabase_flutter.dart';
|
|
import '../main.dart';
|
|
|
|
/// Data model for a collection.
|
|
class Collection {
|
|
final String id;
|
|
final String name;
|
|
final String? description;
|
|
final String ownerId;
|
|
final DateTime createdAt;
|
|
final String role; // 'owner' or 'member'
|
|
final int itemCount;
|
|
final int memberCount;
|
|
|
|
Collection({
|
|
required this.id,
|
|
required this.name,
|
|
this.description,
|
|
required this.ownerId,
|
|
required this.createdAt,
|
|
required this.role,
|
|
this.itemCount = 0,
|
|
this.memberCount = 1,
|
|
});
|
|
|
|
bool get isOwner => role == 'owner';
|
|
}
|
|
|
|
/// Member of a collection.
|
|
class CollectionMember {
|
|
final String id;
|
|
final String userId;
|
|
final String email;
|
|
final String role;
|
|
final DateTime joinedAt;
|
|
|
|
CollectionMember({
|
|
required this.id,
|
|
required this.userId,
|
|
required this.email,
|
|
required this.role,
|
|
required this.joinedAt,
|
|
});
|
|
|
|
bool get isOwner => role == 'owner';
|
|
}
|
|
|
|
/// Service for managing collections and membership.
|
|
class CollectionService {
|
|
CollectionService._();
|
|
|
|
/// Fetch all collections the current user is a member of,
|
|
/// including item count and member count.
|
|
static Future<List<Collection>> getMyCollections() async {
|
|
final userId = supabase.auth.currentUser!.id;
|
|
|
|
// Get memberships with collection data.
|
|
final memberships = await supabase
|
|
.from('collection_members')
|
|
.select('role, collections(id, name, description, owner_id, created_at)')
|
|
.eq('user_id', userId);
|
|
|
|
// If the user has no memberships, return early.
|
|
if (memberships.isEmpty) {
|
|
return <Collection>[];
|
|
}
|
|
|
|
// Collect all distinct collection IDs from memberships.
|
|
final collectionIds = <String>{};
|
|
for (final m in memberships) {
|
|
final c = m['collections'] as Map<String, dynamic>;
|
|
final collectionId = c['id'] as String;
|
|
collectionIds.add(collectionId);
|
|
}
|
|
|
|
final collectionIdList = collectionIds.toList();
|
|
|
|
// Fetch all items for these collections in a single query and count them in memory.
|
|
final items = await supabase
|
|
.from('hotwheels')
|
|
.select('id, collection_id')
|
|
.inFilter('collection_id', collectionIdList);
|
|
|
|
final itemCounts = <String, int>{};
|
|
for (final item in items) {
|
|
final collectionId = item['collection_id'] as String;
|
|
itemCounts[collectionId] = (itemCounts[collectionId] ?? 0) + 1;
|
|
}
|
|
|
|
// Fetch all members for these collections in a single query and count them in memory.
|
|
final members = await supabase
|
|
.from('collection_members')
|
|
.select('id, collection_id')
|
|
.inFilter('collection_id', collectionIdList);
|
|
|
|
final memberCounts = <String, int>{};
|
|
for (final member in members) {
|
|
final collectionId = member['collection_id'] as String;
|
|
memberCounts[collectionId] = (memberCounts[collectionId] ?? 0) + 1;
|
|
}
|
|
|
|
final collections = <Collection>[];
|
|
|
|
for (final m in memberships) {
|
|
final c = m['collections'] as Map<String, dynamic>;
|
|
final collectionId = c['id'] as String;
|
|
|
|
collections.add(Collection(
|
|
id: collectionId,
|
|
name: c['name'] as String,
|
|
description: c['description'] as String?,
|
|
ownerId: c['owner_id'] as String,
|
|
createdAt: DateTime.parse(c['created_at'] as String),
|
|
role: m['role'] as String,
|
|
itemCount: itemCounts[collectionId] ?? 0,
|
|
memberCount: memberCounts[collectionId] ?? 1,
|
|
));
|
|
}
|
|
|
|
// Sort: owned first, then by name.
|
|
collections.sort((a, b) {
|
|
if (a.isOwner && !b.isOwner) return -1;
|
|
if (!a.isOwner && b.isOwner) return 1;
|
|
return a.name.compareTo(b.name);
|
|
});
|
|
|
|
return collections;
|
|
}
|
|
|
|
/// Create a new collection. The caller is automatically added as owner.
|
|
static Future<Collection> create({
|
|
required String name,
|
|
String? description,
|
|
}) async {
|
|
final userId = supabase.auth.currentUser!.id;
|
|
|
|
final row = await supabase
|
|
.from('collections')
|
|
.insert({
|
|
'name': name,
|
|
'owner_id': userId,
|
|
if (description != null && description.isNotEmpty)
|
|
'description': description,
|
|
})
|
|
.select()
|
|
.single();
|
|
|
|
// Add owner as a member.
|
|
await supabase.from('collection_members').insert({
|
|
'collection_id': row['id'],
|
|
'user_id': userId,
|
|
'role': 'owner',
|
|
});
|
|
|
|
return Collection(
|
|
id: row['id'] as String,
|
|
name: row['name'] as String,
|
|
description: row['description'] as String?,
|
|
ownerId: userId,
|
|
createdAt: DateTime.parse(row['created_at'] as String),
|
|
role: 'owner',
|
|
itemCount: 0,
|
|
memberCount: 1,
|
|
);
|
|
}
|
|
|
|
/// Update a collection's name/description. Owner only.
|
|
static Future<void> update({
|
|
required String collectionId,
|
|
required String name,
|
|
String? description,
|
|
}) async {
|
|
final userId = supabase.auth.currentUser!.id;
|
|
|
|
final collection = await supabase
|
|
.from('collections')
|
|
.select('owner_id')
|
|
.eq('id', collectionId)
|
|
.maybeSingle();
|
|
|
|
if (collection == null) {
|
|
throw Exception('Collection not found.');
|
|
}
|
|
if (collection['owner_id'] != userId) {
|
|
throw Exception('Only the collection owner can perform this action.');
|
|
}
|
|
|
|
await supabase.from('collections').update({
|
|
'name': name,
|
|
if (description != null && description.isNotEmpty)
|
|
'description': description,
|
|
}).eq('id', collectionId);
|
|
}
|
|
|
|
/// Delete a collection. Owner only. Cascade deletes members & items.
|
|
static Future<void> delete(String collectionId) async {
|
|
final userId = supabase.auth.currentUser!.id;
|
|
|
|
final collection = await supabase
|
|
.from('collections')
|
|
.select('owner_id')
|
|
.eq('id', collectionId)
|
|
.maybeSingle();
|
|
|
|
if (collection == null) {
|
|
throw Exception('Collection not found.');
|
|
}
|
|
if (collection['owner_id'] != userId) {
|
|
throw Exception('Only the collection owner can perform this action.');
|
|
}
|
|
|
|
await supabase.from('collections').delete().eq('id', collectionId);
|
|
}
|
|
|
|
/// Get members of a collection (with emails via RPC).
|
|
static Future<List<CollectionMember>> getMembers(
|
|
String collectionId) async {
|
|
final rows = await supabase.rpc('get_collection_members', params: {
|
|
'p_collection_id': collectionId,
|
|
});
|
|
|
|
final members = <CollectionMember>[];
|
|
for (final r in rows) {
|
|
members.add(CollectionMember(
|
|
id: r['id'] as String,
|
|
userId: r['user_id'] as String,
|
|
email: r['email'] as String? ?? 'unknown',
|
|
role: r['role'] as String,
|
|
joinedAt: DateTime.parse(r['created_at'] as String),
|
|
));
|
|
}
|
|
|
|
return members;
|
|
}
|
|
|
|
/// Invite a user by email. Looks up auth.users via an RPC function,
|
|
/// then inserts a collection_members row.
|
|
static Future<void> inviteByEmail({
|
|
required String collectionId,
|
|
required String email,
|
|
}) async {
|
|
// Call an RPC to look up the user ID by email.
|
|
final result = await supabase.rpc('get_user_id_by_email', params: {
|
|
'lookup_email': email.trim().toLowerCase(),
|
|
});
|
|
|
|
if (result == null || (result is List && result.isEmpty)) {
|
|
throw Exception(
|
|
'No user found with that email. They must create an account first.');
|
|
}
|
|
|
|
final userId = result is List ? result.first['id'] as String : result as String;
|
|
|
|
// Check if already a member.
|
|
final existing = await supabase
|
|
.from('collection_members')
|
|
.select('id')
|
|
.eq('collection_id', collectionId)
|
|
.eq('user_id', userId)
|
|
.maybeSingle();
|
|
|
|
if (existing != null) {
|
|
throw Exception('This user is already a member of this collection.');
|
|
}
|
|
|
|
await supabase.from('collection_members').insert({
|
|
'collection_id': collectionId,
|
|
'user_id': userId,
|
|
'role': 'member',
|
|
});
|
|
}
|
|
|
|
/// Remove a member from a collection.
|
|
static Future<void> removeMember({
|
|
required String collectionId,
|
|
required String membershipId,
|
|
}) async {
|
|
await supabase
|
|
.from('collection_members')
|
|
.delete()
|
|
.eq('id', membershipId);
|
|
}
|
|
|
|
/// Leave a collection (for non-owners).
|
|
static Future<void> leave(String collectionId) async {
|
|
final userId = supabase.auth.currentUser!.id;
|
|
await supabase
|
|
.from('collection_members')
|
|
.delete()
|
|
.eq('collection_id', collectionId)
|
|
.eq('user_id', userId);
|
|
}
|
|
}
|