feat: Add Groups system - Flutter models and API methods
This commit is contained in:
parent
abfbeb2119
commit
c1c7ebd678
304
sojorn_app/lib/models/group.dart
Normal file
304
sojorn_app/lib/models/group.dart
Normal file
|
|
@ -0,0 +1,304 @@
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
enum GroupCategory {
|
||||||
|
general('General', 'general'),
|
||||||
|
hobby('Hobby', 'hobby'),
|
||||||
|
sports('Sports', 'sports'),
|
||||||
|
professional('Professional', 'professional'),
|
||||||
|
localBusiness('Local Business', 'local_business'),
|
||||||
|
support('Support', 'support'),
|
||||||
|
education('Education', 'education');
|
||||||
|
|
||||||
|
const GroupCategory(this.displayName, this.value);
|
||||||
|
final String displayName;
|
||||||
|
final String value;
|
||||||
|
|
||||||
|
static GroupCategory fromString(String value) {
|
||||||
|
return GroupCategory.values.firstWhere(
|
||||||
|
(cat) => cat.value == value,
|
||||||
|
orElse: () => GroupCategory.general,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum GroupRole {
|
||||||
|
owner('Owner'),
|
||||||
|
admin('Admin'),
|
||||||
|
moderator('Moderator'),
|
||||||
|
member('Member');
|
||||||
|
|
||||||
|
const GroupRole(this.displayName);
|
||||||
|
final String displayName;
|
||||||
|
|
||||||
|
static GroupRole fromString(String value) {
|
||||||
|
return GroupRole.values.firstWhere(
|
||||||
|
(role) => role.name.toLowerCase() == value.toLowerCase(),
|
||||||
|
orElse: () => GroupRole.member,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum JoinRequestStatus {
|
||||||
|
pending('Pending'),
|
||||||
|
approved('Approved'),
|
||||||
|
rejected('Rejected');
|
||||||
|
|
||||||
|
const JoinRequestStatus(this.displayName);
|
||||||
|
final String displayName;
|
||||||
|
|
||||||
|
static JoinRequestStatus fromString(String value) {
|
||||||
|
return JoinRequestStatus.values.firstWhere(
|
||||||
|
(status) => status.name.toLowerCase() == value.toLowerCase(),
|
||||||
|
orElse: () => JoinRequestStatus.pending,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Group extends Equatable {
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
final String description;
|
||||||
|
final GroupCategory category;
|
||||||
|
final String? avatarUrl;
|
||||||
|
final String? bannerUrl;
|
||||||
|
final bool isPrivate;
|
||||||
|
final String createdBy;
|
||||||
|
final int memberCount;
|
||||||
|
final int postCount;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final DateTime updatedAt;
|
||||||
|
final GroupRole? userRole;
|
||||||
|
final bool isMember;
|
||||||
|
final bool hasPendingRequest;
|
||||||
|
|
||||||
|
const Group({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.description,
|
||||||
|
required this.category,
|
||||||
|
this.avatarUrl,
|
||||||
|
this.bannerUrl,
|
||||||
|
required this.isPrivate,
|
||||||
|
required this.createdBy,
|
||||||
|
required this.memberCount,
|
||||||
|
required this.postCount,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.updatedAt,
|
||||||
|
this.userRole,
|
||||||
|
this.isMember = false,
|
||||||
|
this.hasPendingRequest = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory Group.fromJson(Map<String, dynamic> json) {
|
||||||
|
return Group(
|
||||||
|
id: json['id'] as String,
|
||||||
|
name: json['name'] as String,
|
||||||
|
description: json['description'] as String? ?? '',
|
||||||
|
category: GroupCategory.fromString(json['category'] as String),
|
||||||
|
avatarUrl: json['avatar_url'] as String?,
|
||||||
|
bannerUrl: json['banner_url'] as String?,
|
||||||
|
isPrivate: json['is_private'] as bool? ?? false,
|
||||||
|
createdBy: json['created_by'] as String,
|
||||||
|
memberCount: json['member_count'] as int? ?? 0,
|
||||||
|
postCount: json['post_count'] as int? ?? 0,
|
||||||
|
createdAt: DateTime.parse(json['created_at'] as String),
|
||||||
|
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||||
|
userRole: json['user_role'] != null
|
||||||
|
? GroupRole.fromString(json['user_role'] as String)
|
||||||
|
: null,
|
||||||
|
isMember: json['is_member'] as bool? ?? false,
|
||||||
|
hasPendingRequest: json['has_pending_request'] as bool? ?? false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Group copyWith({
|
||||||
|
String? id,
|
||||||
|
String? name,
|
||||||
|
String? description,
|
||||||
|
GroupCategory? category,
|
||||||
|
String? avatarUrl,
|
||||||
|
String? bannerUrl,
|
||||||
|
bool? isPrivate,
|
||||||
|
String? createdBy,
|
||||||
|
int? memberCount,
|
||||||
|
int? postCount,
|
||||||
|
DateTime? createdAt,
|
||||||
|
DateTime? updatedAt,
|
||||||
|
GroupRole? userRole,
|
||||||
|
bool? isMember,
|
||||||
|
bool? hasPendingRequest,
|
||||||
|
}) {
|
||||||
|
return Group(
|
||||||
|
id: id ?? this.id,
|
||||||
|
name: name ?? this.name,
|
||||||
|
description: description ?? this.description,
|
||||||
|
category: category ?? this.category,
|
||||||
|
avatarUrl: avatarUrl ?? this.avatarUrl,
|
||||||
|
bannerUrl: bannerUrl ?? this.bannerUrl,
|
||||||
|
isPrivate: isPrivate ?? this.isPrivate,
|
||||||
|
createdBy: createdBy ?? this.createdBy,
|
||||||
|
memberCount: memberCount ?? this.memberCount,
|
||||||
|
postCount: postCount ?? this.postCount,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
userRole: userRole ?? this.userRole,
|
||||||
|
isMember: isMember ?? this.isMember,
|
||||||
|
hasPendingRequest: hasPendingRequest ?? this.hasPendingRequest,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
category,
|
||||||
|
avatarUrl,
|
||||||
|
bannerUrl,
|
||||||
|
isPrivate,
|
||||||
|
createdBy,
|
||||||
|
memberCount,
|
||||||
|
postCount,
|
||||||
|
createdAt,
|
||||||
|
updatedAt,
|
||||||
|
userRole,
|
||||||
|
isMember,
|
||||||
|
hasPendingRequest,
|
||||||
|
];
|
||||||
|
|
||||||
|
String get memberCountText {
|
||||||
|
if (memberCount >= 1000000) {
|
||||||
|
return '${(memberCount / 1000000).toStringAsFixed(1)}M members';
|
||||||
|
} else if (memberCount >= 1000) {
|
||||||
|
return '${(memberCount / 1000).toStringAsFixed(1)}K members';
|
||||||
|
}
|
||||||
|
return '$memberCount members';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get postCountText {
|
||||||
|
if (postCount >= 1000) {
|
||||||
|
return '${(postCount / 1000).toStringAsFixed(1)}K posts';
|
||||||
|
}
|
||||||
|
return '$postCount posts';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GroupMember extends Equatable {
|
||||||
|
final String id;
|
||||||
|
final String groupId;
|
||||||
|
final String userId;
|
||||||
|
final GroupRole role;
|
||||||
|
final DateTime joinedAt;
|
||||||
|
final String? username;
|
||||||
|
final String? avatarUrl;
|
||||||
|
|
||||||
|
const GroupMember({
|
||||||
|
required this.id,
|
||||||
|
required this.groupId,
|
||||||
|
required this.userId,
|
||||||
|
required this.role,
|
||||||
|
required this.joinedAt,
|
||||||
|
this.username,
|
||||||
|
this.avatarUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory GroupMember.fromJson(Map<String, dynamic> json) {
|
||||||
|
return GroupMember(
|
||||||
|
id: json['id'] as String,
|
||||||
|
groupId: json['group_id'] as String,
|
||||||
|
userId: json['user_id'] as String,
|
||||||
|
role: GroupRole.fromString(json['role'] as String),
|
||||||
|
joinedAt: DateTime.parse(json['joined_at'] as String),
|
||||||
|
username: json['username'] as String?,
|
||||||
|
avatarUrl: json['avatar_url'] as String?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
id,
|
||||||
|
groupId,
|
||||||
|
userId,
|
||||||
|
role,
|
||||||
|
joinedAt,
|
||||||
|
username,
|
||||||
|
avatarUrl,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
class JoinRequest extends Equatable {
|
||||||
|
final String id;
|
||||||
|
final String groupId;
|
||||||
|
final String userId;
|
||||||
|
final JoinRequestStatus status;
|
||||||
|
final String? message;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final DateTime? reviewedAt;
|
||||||
|
final String? reviewedBy;
|
||||||
|
final String? username;
|
||||||
|
final String? avatarUrl;
|
||||||
|
|
||||||
|
const JoinRequest({
|
||||||
|
required this.id,
|
||||||
|
required this.groupId,
|
||||||
|
required this.userId,
|
||||||
|
required this.status,
|
||||||
|
this.message,
|
||||||
|
required this.createdAt,
|
||||||
|
this.reviewedAt,
|
||||||
|
this.reviewedBy,
|
||||||
|
this.username,
|
||||||
|
this.avatarUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory JoinRequest.fromJson(Map<String, dynamic> json) {
|
||||||
|
return JoinRequest(
|
||||||
|
id: json['id'] as String,
|
||||||
|
groupId: json['group_id'] as String,
|
||||||
|
userId: json['user_id'] as String,
|
||||||
|
status: JoinRequestStatus.fromString(json['status'] as String),
|
||||||
|
message: json['message'] as String?,
|
||||||
|
createdAt: DateTime.parse(json['created_at'] as String),
|
||||||
|
reviewedAt: json['reviewed_at'] != null
|
||||||
|
? DateTime.parse(json['reviewed_at'] as String)
|
||||||
|
: null,
|
||||||
|
reviewedBy: json['reviewed_by'] as String?,
|
||||||
|
username: json['username'] as String?,
|
||||||
|
avatarUrl: json['avatar_url'] as String?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
id,
|
||||||
|
groupId,
|
||||||
|
userId,
|
||||||
|
status,
|
||||||
|
message,
|
||||||
|
createdAt,
|
||||||
|
reviewedAt,
|
||||||
|
reviewedBy,
|
||||||
|
username,
|
||||||
|
avatarUrl,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
class SuggestedGroup extends Equatable {
|
||||||
|
final Group group;
|
||||||
|
final String reason;
|
||||||
|
|
||||||
|
const SuggestedGroup({
|
||||||
|
required this.group,
|
||||||
|
required this.reason,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory SuggestedGroup.fromJson(Map<String, dynamic> json) {
|
||||||
|
return SuggestedGroup(
|
||||||
|
group: Group.fromJson(json),
|
||||||
|
reason: json['reason'] as String? ?? 'Suggested for you',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [group, reason];
|
||||||
|
}
|
||||||
|
|
@ -10,6 +10,7 @@ import '../models/user_settings.dart';
|
||||||
import '../models/comment.dart';
|
import '../models/comment.dart';
|
||||||
import '../models/notification.dart';
|
import '../models/notification.dart';
|
||||||
import '../models/beacon.dart';
|
import '../models/beacon.dart';
|
||||||
|
import '../models/group.dart';
|
||||||
import '../config/api_config.dart';
|
import '../config/api_config.dart';
|
||||||
import '../services/auth_service.dart';
|
import '../services/auth_service.dart';
|
||||||
import '../models/search_results.dart';
|
import '../models/search_results.dart';
|
||||||
|
|
@ -1590,4 +1591,104 @@ class ApiService {
|
||||||
final data = await _callGoApi('/users/$userId/following', method: 'GET');
|
final data = await _callGoApi('/users/$userId/following', method: 'GET');
|
||||||
return (data['following'] as List?)?.cast<Map<String, dynamic>>() ?? [];
|
return (data['following'] as List?)?.cast<Map<String, dynamic>>() ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Groups System
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/// List all groups with optional category filter
|
||||||
|
Future<List<Group>> listGroups({String? category, int page = 0, int limit = 20}) async {
|
||||||
|
final queryParams = <String, String>{
|
||||||
|
'page': page.toString(),
|
||||||
|
'limit': limit.toString(),
|
||||||
|
};
|
||||||
|
if (category != null) {
|
||||||
|
queryParams['category'] = category;
|
||||||
|
}
|
||||||
|
|
||||||
|
final data = await _callGoApi('/groups', method: 'GET', queryParams: queryParams);
|
||||||
|
final groups = (data['groups'] as List?) ?? [];
|
||||||
|
return groups.map((g) => Group.fromJson(g)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get groups the user is a member of
|
||||||
|
Future<List<Group>> getMyGroups() async {
|
||||||
|
final data = await _callGoApi('/groups/mine', method: 'GET');
|
||||||
|
final groups = (data['groups'] as List?) ?? [];
|
||||||
|
return groups.map((g) => Group.fromJson(g)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get suggested groups for the user
|
||||||
|
Future<List<SuggestedGroup>> getSuggestedGroups({int limit = 10}) async {
|
||||||
|
final data = await _callGoApi('/groups/suggested', method: 'GET',
|
||||||
|
queryParams: {'limit': limit.toString()});
|
||||||
|
final suggestions = (data['suggestions'] as List?) ?? [];
|
||||||
|
return suggestions.map((s) => SuggestedGroup.fromJson(s)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get group details by ID
|
||||||
|
Future<Group> getGroup(String groupId) async {
|
||||||
|
final data = await _callGoApi('/groups/$groupId', method: 'GET');
|
||||||
|
return Group.fromJson(data['group']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new group
|
||||||
|
Future<Map<String, dynamic>> createGroup({
|
||||||
|
required String name,
|
||||||
|
String? description,
|
||||||
|
required GroupCategory category,
|
||||||
|
bool isPrivate = false,
|
||||||
|
String? avatarUrl,
|
||||||
|
String? bannerUrl,
|
||||||
|
}) async {
|
||||||
|
final body = {
|
||||||
|
'name': name,
|
||||||
|
'description': description ?? '',
|
||||||
|
'category': category.value,
|
||||||
|
'is_private': isPrivate,
|
||||||
|
if (avatarUrl != null) 'avatar_url': avatarUrl,
|
||||||
|
if (bannerUrl != null) 'banner_url': bannerUrl,
|
||||||
|
};
|
||||||
|
|
||||||
|
return await _callGoApi('/groups', method: 'POST', body: body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Join a group or request to join (for private groups)
|
||||||
|
Future<Map<String, dynamic>> joinGroup(String groupId, {String? message}) async {
|
||||||
|
final body = <String, dynamic>{};
|
||||||
|
if (message != null) {
|
||||||
|
body['message'] = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await _callGoApi('/groups/$groupId/join', method: 'POST', body: body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Leave a group
|
||||||
|
Future<void> leaveGroup(String groupId) async {
|
||||||
|
await _callGoApi('/groups/$groupId/leave', method: 'POST');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get group members
|
||||||
|
Future<List<GroupMember>> getGroupMembers(String groupId, {int page = 0, int limit = 50}) async {
|
||||||
|
final data = await _callGoApi('/groups/$groupId/members', method: 'GET',
|
||||||
|
queryParams: {'page': page.toString(), 'limit': limit.toString()});
|
||||||
|
final members = (data['members'] as List?) ?? [];
|
||||||
|
return members.map((m) => GroupMember.fromJson(m)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get pending join requests (admin only)
|
||||||
|
Future<List<JoinRequest>> getPendingRequests(String groupId) async {
|
||||||
|
final data = await _callGoApi('/groups/$groupId/requests', method: 'GET');
|
||||||
|
final requests = (data['requests'] as List?) ?? [];
|
||||||
|
return requests.map((r) => JoinRequest.fromJson(r)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Approve a join request (admin only)
|
||||||
|
Future<void> approveJoinRequest(String groupId, String requestId) async {
|
||||||
|
await _callGoApi('/groups/$groupId/requests/$requestId/approve', method: 'POST');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reject a join request (admin only)
|
||||||
|
Future<void> rejectJoinRequest(String groupId, String requestId) async {
|
||||||
|
await _callGoApi('/groups/$groupId/requests/$requestId/reject', method: 'POST');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue