File size: 6,718 Bytes
92c8a69
 
 
 
 
 
 
 
 
 
 
 
 
26add47
 
92c8a69
 
 
 
 
 
 
 
 
 
26add47
92c8a69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26add47
 
 
 
 
 
 
 
 
 
 
 
92c8a69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26add47
 
 
 
 
 
 
 
 
 
 
 
92c8a69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
import {
  Injectable,
  Logger,
  NotFoundException,
  BadRequestException,
  ForbiddenException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { StudyGroup } from '../entities/study-group.entity';
import { StudyGroupMember } from '../entities/study-group-member.entity';
import { CreateStudyGroupDto } from '../dto/create-study-group.dto';
import { UpdateStudyGroupDto } from '../dto/update-study-group.dto';
import { NotificationsService } from '../../notifications/services/notifications.service';
import { NotificationType } from '../../notifications/enums';

@Injectable()
export class StudyGroupsService {
  private readonly logger = new Logger(StudyGroupsService.name);

  constructor(
    @InjectRepository(StudyGroup)
    private groupRepo: Repository<StudyGroup>,
    @InjectRepository(StudyGroupMember)
    private memberRepo: Repository<StudyGroupMember>,
    private notificationsService: NotificationsService,
  ) {}

  async findAll(courseId?: number) {
    const qb = this.groupRepo.createQueryBuilder('sg')
      .leftJoinAndSelect('sg.creator', 'creator')
      .orderBy('sg.createdAt', 'DESC');

    if (courseId) {
      qb.where('sg.courseId = :courseId', { courseId });
    }

    const data = await qb.getMany();
    return { data };
  }

  async findById(id: number) {
    const group = await this.groupRepo.findOne({
      where: { groupId: id },
      relations: ['creator'],
    });
    if (!group) {
      throw new NotFoundException(`Study group #${id} not found`);
    }
    return { data: group };
  }

  async findMyGroups(userId: number) {
    const memberships = await this.memberRepo.find({
      where: { userId },
      relations: ['group', 'group.creator'],
    });
    const data = memberships.map((m) => ({
      ...m.group,
      memberRole: m.role,
      joinedAt: m.joinedAt,
    }));
    return { data };
  }

  async createGroup(dto: CreateStudyGroupDto, userId: number) {
    const group = this.groupRepo.create({
      ...dto,
      createdBy: userId,
      currentMembers: 1,
    });
    const saved = await this.groupRepo.save(group);

    // Add creator as first member
    const member = this.memberRepo.create({
      groupId: saved.groupId,
      userId,
      role: 'creator',
    });
    await this.memberRepo.save(member);

    return { data: saved, message: 'Study group created successfully' };
  }

  async updateGroup(id: number, dto: UpdateStudyGroupDto, userId: number) {
    const group = await this.groupRepo.findOne({ where: { groupId: id } });
    if (!group) {
      throw new NotFoundException(`Study group #${id} not found`);
    }
    if (group.createdBy != userId) {
      throw new ForbiddenException('Only the group creator can update this group');
    }
    Object.assign(group, dto);
    const saved = await this.groupRepo.save(group);
    return { data: saved, message: `Study group #${id} updated successfully` };
  }

  async deleteGroup(id: number, userId: number, userRoles: string[]) {
    const group = await this.groupRepo.findOne({ where: { groupId: id } });
    if (!group) {
      throw new NotFoundException(`Study group #${id} not found`);
    }
    const isAdmin = userRoles.some((r) => r === 'admin' || r === 'it_admin');
    if (group.createdBy != userId && !isAdmin) {
      throw new ForbiddenException('Only the group creator or admins can delete this group');
    }
    // Remove all members first
    await this.memberRepo.delete({ groupId: id });
    await this.groupRepo.remove(group);
    return { message: `Study group #${id} deleted successfully` };
  }

  async joinGroup(id: number, userId: number) {
    const group = await this.groupRepo.findOne({ where: { groupId: id } });
    if (!group) {
      throw new NotFoundException(`Study group #${id} not found`);
    }
    if (group.status !== 'active') {
      throw new BadRequestException('This group is not active');
    }
    if (group.currentMembers >= group.maxMembers) {
      throw new BadRequestException('This group is full');
    }

    // Check if already a member
    const existing = await this.memberRepo.findOne({
      where: { groupId: id, userId },
    });
    if (existing) {
      throw new BadRequestException('You are already a member of this group');
    }

    const member = this.memberRepo.create({
      groupId: id,
      userId,
      role: 'member',
    });
    await this.memberRepo.save(member);

    group.currentMembers += 1;
    await this.groupRepo.save(group);

    // Notify the group creator
    if (group.createdBy !== userId) {
      this.notificationsService.createNotification({
        userId: group.createdBy,
        notificationType: NotificationType.SYSTEM,
        title: 'New Study Group Member',
        body: `A new member has joined your study group: ${group.groupName}.`,
        relatedEntityType: 'study_group',
        relatedEntityId: group.groupId,
      }).catch((err) => this.logger.error('Failed to send notification', err));
    }

    return { message: 'Joined study group successfully' };
  }

  async leaveGroup(id: number, userId: number) {
    const group = await this.groupRepo.findOne({ where: { groupId: id } });
    if (!group) {
      throw new NotFoundException(`Study group #${id} not found`);
    }

    const member = await this.memberRepo.findOne({
      where: { groupId: id, userId },
    });
    if (!member) {
      throw new BadRequestException('You are not a member of this group');
    }
    if (member.role === 'creator') {
      throw new BadRequestException('The group creator cannot leave. Delete the group instead.');
    }

    await this.memberRepo.remove(member);
    group.currentMembers = Math.max(0, group.currentMembers - 1);
    await this.groupRepo.save(group);

    // Notify the group creator
    if (group.createdBy !== userId) {
      this.notificationsService.createNotification({
        userId: group.createdBy,
        notificationType: NotificationType.SYSTEM,
        title: 'Study Group Member Left',
        body: `A member has left your study group: ${group.groupName}.`,
        relatedEntityType: 'study_group',
        relatedEntityId: group.groupId,
      }).catch((err) => this.logger.error('Failed to send notification', err));
    }

    return { message: 'Left study group successfully' };
  }

  async getMembers(id: number) {
    const group = await this.groupRepo.findOne({ where: { groupId: id } });
    if (!group) {
      throw new NotFoundException(`Study group #${id} not found`);
    }
    const data = await this.memberRepo.find({
      where: { groupId: id },
      relations: ['user'],
      order: { joinedAt: 'ASC' },
    });
    return { data };
  }
}