diff --git a/src/profile-completeness/profile-completeness.controller.ts b/src/profile-completeness/profile-completeness.controller.ts new file mode 100644 index 0000000..4c5184a --- /dev/null +++ b/src/profile-completeness/profile-completeness.controller.ts @@ -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); + } +} \ No newline at end of file diff --git a/src/profile-completeness/profile-completeness.module.ts b/src/profile-completeness/profile-completeness.module.ts new file mode 100644 index 0000000..f20e2e3 --- /dev/null +++ b/src/profile-completeness/profile-completeness.module.ts @@ -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 {} \ No newline at end of file diff --git a/src/profile-completeness/profile-completeness.service.ts b/src/profile-completeness/profile-completeness.service.ts new file mode 100644 index 0000000..8e4868d --- /dev/null +++ b/src/profile-completeness/profile-completeness.service.ts @@ -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, + ) {} + + async getScore(userId: string): Promise { + 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 }; + } +} \ No newline at end of file