Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions BackendAcademy/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,6 @@ import { SocialModule } from './social/social.module';
import { OnboardingModule } from './onboarding/onboarding.module';
import { LessonModule } from './lessons/lesson.module';
import { TaskModule } from './tasks/task.module';
import { JobsModule } from './jobs/jobs.module';
import { LoggingModule } from './logging/logging.module';
import { ProgressModule } from './courses/progress/progress.module';
import { AppConfigModule } from './config/config.module';
import { ContractsModule } from './contracts/contracts.module';
import { AssetsModule } from './assets/assets.module';
import { PathfindingModule } from './pathfinding/pathfinding.module';
import { MonitoringModule } from './monitoring/monitoring.module';
import { SearchModule } from './search/search.module';
import { PaymentsModule } from './payments/payments.module';
import { SessionsModule } from './sessions/sessions.module';

@Module({
Expand Down
43 changes: 43 additions & 0 deletions BackendAcademy/src/sessions/attendance.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
import { AttendanceService } from './attendance.service';
import { JoinSessionAttendanceDto } from './dto/join-session-attendance.dto';
import { LeaveSessionAttendanceDto } from './dto/leave-session-attendance.dto';

@Controller('sessions/attendance')
export class AttendanceController {
constructor(private readonly attendanceService: AttendanceService) {}

@Post('join')
async join(@Body() dto: JoinSessionAttendanceDto) {
const record = await this.attendanceService.join(dto.sessionKey, dto.userId);
return {
id: record.id,
sessionKey: record.sessionKey,
userId: record.userId,
joinedAt: record.joinedAt,
leftAt: record.leftAt,
durationSeconds: record.durationSeconds,
isActive: record.isActive,
};
}

@Post('leave')
async leave(@Body() dto: LeaveSessionAttendanceDto) {
const record = await this.attendanceService.leave(dto.sessionKey, dto.userId);
return {
id: record.id,
sessionKey: record.sessionKey,
userId: record.userId,
joinedAt: record.joinedAt,
leftAt: record.leftAt,
durationSeconds: record.durationSeconds,
isActive: record.isActive,
};
}

@Get(':sessionKey/stats')
async stats(@Param('sessionKey') sessionKey: string) {
return this.attendanceService.getSessionStats(sessionKey);
}
}

27 changes: 27 additions & 0 deletions BackendAcademy/src/sessions/attendance.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Column, CreateDateColumn, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';

@Entity('session_attendance')
@Index(['sessionKey', 'userId'], { unique: false })
export class AttendanceEntity {
@PrimaryGeneratedColumn('uuid')
id!: string;

@Column({ type: 'varchar', length: 128 })
sessionKey!: string;

@Column({ type: 'varchar', length: 128 })
userId!: string;

@CreateDateColumn({ type: 'timestamptz' })
joinedAt!: Date;

@Column({ type: 'timestamptz', nullable: true })
leftAt: Date | null = null;

@Column({ type: 'int', nullable: true })
durationSeconds: number | null = null;

@Column({ type: 'boolean', default: true })
isActive!: boolean;
}

90 changes: 90 additions & 0 deletions BackendAcademy/src/sessions/attendance.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { AttendanceEntity } from './attendance.entity';

@Injectable()
export class AttendanceService {
constructor(
@InjectRepository(AttendanceEntity)
private readonly attendanceRepo: Repository<AttendanceEntity>,
) {}

async join(sessionKey: string, userId: string): Promise<AttendanceEntity> {
const now = new Date();

const existing = await this.attendanceRepo.findOne({
where: {
sessionKey,
userId,
isActive: true,
},
order: { joinedAt: 'DESC' },
});

if (existing) {
return existing;
}

const record = this.attendanceRepo.create({
sessionKey,
userId,
joinedAt: now,
isActive: true,
leftAt: null,
durationSeconds: null,
});

return this.attendanceRepo.save(record);
}

async leave(sessionKey: string, userId: string): Promise<AttendanceEntity> {
const existing = await this.attendanceRepo.findOne({
where: {
sessionKey,
userId,
isActive: true,
},
order: { joinedAt: 'DESC' },
});

if (!existing) {
throw new BadRequestException('No active attendance found for this user/session');
}

const now = new Date();
const durationMs = now.getTime() - existing.joinedAt.getTime();
const durationSeconds = Math.max(0, Math.floor(durationMs / 1000));

existing.isActive = false;
existing.leftAt = now;
existing.durationSeconds = durationSeconds;

return this.attendanceRepo.save(existing);
}

async getSessionStats(sessionKey: string): Promise<{
presentCount: number;
totalJoins: number;
totalDurationSeconds: number;
}> {
const active = await this.attendanceRepo.count({
where: {
sessionKey,
isActive: true,
},
});

const all = await this.attendanceRepo.find({ where: { sessionKey } });

const totalJoins = all.length;
const totalDurationSeconds = all.reduce((acc, r) => acc + (r.durationSeconds ?? 0), 0);

return {
presentCount: active,
totalJoins,
totalDurationSeconds,
};
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { IsInt, IsNotEmpty, IsObject, IsOptional, IsString, MaxLength, ValidateNested } from 'class-validator';

export class SessionAttendanceStatsDto {
@IsString()
@IsNotEmpty()
@MaxLength(128)
sessionKey!: string;

@IsInt()
presentCount!: number;

@IsInt()
totalJoins!: number;

@IsInt()
totalDurationSeconds!: number;
}

14 changes: 14 additions & 0 deletions BackendAcademy/src/sessions/dto/join-session-attendance.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { IsNotEmpty, IsString, MaxLength } from 'class-validator';

export class JoinSessionAttendanceDto {
@IsString()
@IsNotEmpty()
@MaxLength(128)
sessionKey!: string;

@IsString()
@IsNotEmpty()
@MaxLength(128)
userId!: string;
}

14 changes: 14 additions & 0 deletions BackendAcademy/src/sessions/dto/leave-session-attendance.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { IsNotEmpty, IsString, MaxLength } from 'class-validator';

export class LeaveSessionAttendanceDto {
@IsString()
@IsNotEmpty()
@MaxLength(128)
sessionKey!: string;

@IsString()
@IsNotEmpty()
@MaxLength(128)
userId!: string;
}

14 changes: 9 additions & 5 deletions BackendAcademy/src/sessions/sessions.module.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { Module } from '@nestjs/common';
import { OfficeHoursController } from './office-hours.controller';
import { OfficeHoursService } from './office-hours.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AttendanceEntity } from './attendance.entity';
import { AttendanceController } from './attendance.controller';
import { AttendanceService } from './attendance.service';

@Module({
controllers: [OfficeHoursController],
providers: [OfficeHoursService],
exports: [OfficeHoursService],
imports: [TypeOrmModule.forFeature([AttendanceEntity])],
controllers: [AttendanceController],
providers: [AttendanceService],
exports: [AttendanceService],
})
export class SessionsModule {}

33 changes: 33 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# TODO - Live session attendance tracking (BackendAcademy)

## Step 1: Add persistence model
- Create `BackendAcademy/src/sessions/attendance.entity.ts` using TypeORM.
- Decide primary key and fields: `sessionKey`, `userId`, `joinedAt`, `leftAt`, `durationSeconds`.

## Step 2: Implement attendance service
- Create `BackendAcademy/src/sessions/attendance.service.ts`.
- Add methods: `join`, `leave`, `getSessionStats`.
- Join is idempotent per `(sessionKey,userId)` while active.

## Step 3: Implement controller + DTOs
- Create `BackendAcademy/src/sessions/dto/join-session-attendance.dto.ts`.
- Create `BackendAcademy/src/sessions/dto/leave-session-attendance.dto.ts`.
- Create `BackendAcademy/src/sessions/dto/get-session-attendance-stats.dto.ts` (if needed).
- Create `BackendAcademy/src/sessions/attendance.controller.ts` with endpoints:
- `POST /api/v1/sessions/attendance/join`
- `POST /api/v1/sessions/attendance/leave`
- `GET /api/v1/sessions/attendance/:sessionKey/stats`

## Step 4: Add NestJS module wiring
- Create `BackendAcademy/src/sessions/sessions.module.ts` and register TypeORM entity + providers.

## Step 5: Wire into app
- Update `BackendAcademy/src/app.module.ts` to import `SessionsModule`.

## Step 6: Verify build
- Run `npm run build` (or `npm test`) inside `BackendAcademy`.

## Progress
- Step 1-5 implemented (sessions module + attendance tracking).