Skip to content
Merged
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
18 changes: 18 additions & 0 deletions src/profile-completeness/profile-completeness.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Controller, Get, Param, UseGuards } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { ProfileCompletenessService } from './profile-completeness.service';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';

@ApiTags('profile-completeness')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Controller('users/:userId/profile-completeness')
export class ProfileCompletenessController {
constructor(private readonly profileCompletenessService: ProfileCompletenessService) {}

@Get()
@ApiOperation({ summary: 'Get profile completeness score and progress for a user' })
getScore(@Param('userId') userId: string) {
return this.profileCompletenessService.getScore(userId);
}
}
13 changes: 13 additions & 0 deletions src/profile-completeness/profile-completeness.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ProfileCompletenessController } from './profile-completeness.controller';
import { ProfileCompletenessService } from './profile-completeness.service';
import { User } from '../users/entities/user.entity';

@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [ProfileCompletenessController],
providers: [ProfileCompletenessService],
exports: [ProfileCompletenessService],
})
export class ProfileCompletenessModule {}
70 changes: 70 additions & 0 deletions src/profile-completeness/profile-completeness.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from '../users/entities/user.entity';

export interface ProfileScoreResult {
score: number;
maxScore: number;
percentage: number;
completedFields: string[];
missingFields: string[];
level: 'starter' | 'intermediate' | 'complete';
nextIncentive: string | null;
}

const SCORED_FIELDS: Array<{ field: keyof User; label: string; points: number }> = [
{ field: 'firstName', label: 'First name', points: 10 },
{ field: 'lastName', label: 'Last name', points: 10 },
{ field: 'username', label: 'Username', points: 10 },
{ field: 'profilePicture', label: 'Profile picture', points: 20 },
{ field: 'isEmailVerified', label: 'Email verified', points: 20 },
{ field: 'role', label: 'Role set', points: 10 },
{ field: 'lastLoginAt', label: 'Logged in at least once', points: 10 },
{ field: 'tenantId', label: 'Organisation linked', points: 10 },
];

const MAX_SCORE = SCORED_FIELDS.reduce((sum, f) => sum + f.points, 0);

@Injectable()
export class ProfileCompletenessService {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {}

async getScore(userId: string): Promise<ProfileScoreResult> {
const user = await this.userRepository.findOneOrFail({ where: { id: userId } });
return this.calculateScore(user);
}

calculateScore(user: User): ProfileScoreResult {
let score = 0;
const completedFields: string[] = [];
const missingFields: string[] = [];

for (const { field, label, points } of SCORED_FIELDS) {
const value = user[field];
const filled = value !== null && value !== undefined && value !== '' && value !== false;
if (filled) {
score += points;
completedFields.push(label);
} else {
missingFields.push(label);
}
}

const percentage = Math.round((score / MAX_SCORE) * 100);
const level =
percentage >= 80 ? 'complete' : percentage >= 40 ? 'intermediate' : 'starter';

const nextIncentive =
level === 'starter'
? 'Reach 40% to unlock Intermediate badge'
: level === 'intermediate'
? 'Reach 80% to unlock Complete Profile badge'
: null;

return { score, maxScore: MAX_SCORE, percentage, completedFields, missingFields, level, nextIncentive };
}
}
Loading