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/notification.dart';
|
||||
import '../models/beacon.dart';
|
||||
import '../models/group.dart';
|
||||
import '../config/api_config.dart';
|
||||
import '../services/auth_service.dart';
|
||||
import '../models/search_results.dart';
|
||||
|
|
@ -1590,4 +1591,104 @@ class ApiService {
|
|||
final data = await _callGoApi('/users/$userId/following', method: 'GET');
|
||||
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