From fbdbb20ba81a20936568e67e1a54410322102894 Mon Sep 17 00:00:00 2001 From: navneethkrish Date: Wed, 20 Nov 2024 14:30:22 +0530 Subject: [PATCH 01/41] feat(team filter): added API for fetching filters and tags for teams --- apps/web-api/src/teams/teams.controller.ts | 22 +++ apps/web-api/src/teams/teams.service.ts | 148 +++++++++++++++------ libs/contracts/src/lib/contract-team.ts | 9 ++ 3 files changed, 137 insertions(+), 42 deletions(-) diff --git a/apps/web-api/src/teams/teams.controller.ts b/apps/web-api/src/teams/teams.controller.ts index a026e2138..b7fad7488 100644 --- a/apps/web-api/src/teams/teams.controller.ts +++ b/apps/web-api/src/teams/teams.controller.ts @@ -18,6 +18,7 @@ import { TeamsService } from './teams.service'; import { NoCache } from '../decorators/no-cache.decorator'; import { UserTokenValidation } from '../guards/user-token-validation.guard'; import { ParticipantsReqValidationPipe } from '../pipes/participant-request-validation.pipe'; +import { request } from 'http'; const server = initNestServer(apiTeam); type RouteShape = typeof server.routeShapes; @@ -25,6 +26,26 @@ type RouteShape = typeof server.routeShapes; export class TeamsController { constructor(private readonly teamsService: TeamsService) {} + @Api(server.route.teamFilters) + @ApiQueryFromZod(TeamQueryParams) + @NoCache() + async getTeamFilters(@Req() request: Request) { + const queryableFields = prismaQueryableFieldsFromZod( + ResponseTeamWithRelationsSchema + ); + const builder = new PrismaQueryBuilder(queryableFields); + const builtQuery = builder.build(request.query); + const { focusAreas } : any = request.query; + builtQuery.where = { + AND: [ + builtQuery.where ? builtQuery.where : {}, + this.teamsService.buildFocusAreaFilters(focusAreas), + this.teamsService.buildRecentTeamsFilter(request.query) + ] + } + return await this.teamsService.getTeamFilters(builtQuery); + } + @Api(server.route.getTeams) @ApiQueryFromZod(TeamQueryParams) @ApiOkResponseFromZod(ResponseTeamWithRelationsSchema.array()) @@ -74,4 +95,5 @@ export class TeamsController { await this.teamsService.validateRequestor(req.userEmail, teamUid); return await this.teamsService.updateTeamFromParticipantsRequest(teamUid, body, req.userEmail); } + } diff --git a/apps/web-api/src/teams/teams.service.ts b/apps/web-api/src/teams/teams.service.ts index dc05703e2..9fd1f4a6b 100644 --- a/apps/web-api/src/teams/teams.service.ts +++ b/apps/web-api/src/teams/teams.service.ts @@ -19,7 +19,7 @@ import { ParticipantsRequestService } from '../participants-request/participants import { hashFileName } from '../utils/hashing'; import { ForestAdminService } from '../utils/forest-admin/forest-admin.service'; import { MembersService } from '../members/members.service'; -import { LogService } from '../shared/log.service'; +import { LogService } from '../shared/log.service'; import { Cache } from 'cache-manager'; import { copyObj, buildMultiRelationMapping, buildRelationMapping } from '../utils/helper/helper'; @@ -36,7 +36,7 @@ export class TeamsService { private forestadminService: ForestAdminService, private notificationService: NotificationService, @Inject(CACHE_MANAGER) private cacheService: Cache - ) {} + ) { } /** * Find all teams based on provided query options. @@ -48,8 +48,8 @@ export class TeamsService { */ async findAll(queryOptions: Prisma.TeamFindManyArgs): Promise { try { - return this.prisma.team.findMany({ ...queryOptions }); - } catch(err) { + return this.prisma.team.findMany({ ...queryOptions }); + } catch (err) { return this.handleErrors(err); } } @@ -104,7 +104,7 @@ export class TeamsService { }); team.teamFocusAreas = this.removeDuplicateFocusAreas(team.teamFocusAreas); return team; - } catch(err) { + } catch (err) { return this.handleErrors(err, uid); } } @@ -120,7 +120,7 @@ export class TeamsService { return this.prisma.team.findUniqueOrThrow({ where: { name }, }); - } catch(err) { + } catch (err) { return this.handleErrors(err); } }; @@ -133,14 +133,14 @@ export class TeamsService { * @returns The created team record */ async createTeam( - team: Prisma.TeamUncheckedCreateInput, + team: Prisma.TeamUncheckedCreateInput, tx: Prisma.TransactionClient ): Promise { try { return await tx.team.create({ data: team, }); - } catch(err) { + } catch (err) { return this.handleErrors(err); } } @@ -163,7 +163,7 @@ export class TeamsService { where: { uid }, data: team, }); - } catch(err) { + } catch (err) { return this.handleErrors(err, `${uid}`); } } @@ -211,7 +211,7 @@ export class TeamsService { const formattedTeam = await this.formatTeam(null, newTeam, tx); const createdTeam = await this.createTeam(formattedTeam, tx); return createdTeam; - } + } /** * Format team data for creation or update @@ -243,7 +243,7 @@ export class TeamsService { team['membershipSources'] = buildMultiRelationMapping('membershipSources', teamData, type); if (type === 'create') { team['teamFocusAreas'] = await this.createTeamWithFocusAreas(teamData, tx); - } + } if (teamUid) { team['teamFocusAreas'] = await this.updateTeamWithFocusAreas(teamUid, teamData, tx); } @@ -277,7 +277,7 @@ export class TeamsService { * @param focusAreas - An array of focus areas associated with the team * @returns A deduplicated array of focus areas */ - private removeDuplicateFocusAreas(focusAreas):any { + private removeDuplicateFocusAreas(focusAreas): any { const uniqueFocusAreas = {}; focusAreas.forEach(item => { const { uid, title } = item.focusArea; @@ -308,7 +308,7 @@ export class TeamsService { participantType: 'TEAM', newData: { ...newTeamData }, }, - tx + tx ); } @@ -330,7 +330,7 @@ export class TeamsService { */ async createTeamWithFocusAreas(team, transaction: Prisma.TransactionClient) { if (team.focusAreas && team.focusAreas.length > 0) { - let teamFocusAreas:any = []; + let teamFocusAreas: any = []; const focusAreaHierarchies = await transaction.focusAreaHierarchy.findMany({ where: { subFocusAreaUid: { @@ -358,7 +358,7 @@ export class TeamsService { } return {}; } - + /** * Updates focus areas for an existing team. * @@ -376,7 +376,7 @@ export class TeamsService { } return await this.createTeamWithFocusAreas(team, transaction); } - + /** * Builds filter for focus areas by splitting the input and matching ancestor titles. * @param focusAreas - Comma-separated focus area titles @@ -387,7 +387,7 @@ export class TeamsService { return { teamFocusAreas: { some: { - ancestorArea:{ + ancestorArea: { title: { in: focusAreas?.split(',') } @@ -398,23 +398,23 @@ export class TeamsService { } return {}; } - + /** * Constructs the team filter based on multiple query parameters. * @param queryParams - Query parameters from the request * @returns - Prisma AND filter combining all conditions */ - buildTeamFilter(queryParams){ - const { + buildTeamFilter(queryParams) { + const { name, - plnFriend, - industryTags, + plnFriend, + industryTags, technologies, membershipSources, fundingStage, - officeHours + officeHours } = queryParams; - const filter:any = []; + const filter: any = []; this.buildNameAndPLNFriendFilter(name, plnFriend, filter); this.buildIndustryTagsFilter(industryTags, filter); this.buildTechnologiesFilter(technologies, filter); @@ -422,7 +422,7 @@ export class TeamsService { this.buildFundingStageFilter(fundingStage, filter); this.buildOfficeHoursFilter(officeHours, filter); this.buildRecentTeamsFilter(queryParams, filter); - return { + return { AND: filter }; }; @@ -435,17 +435,17 @@ export class TeamsService { */ buildNameAndPLNFriendFilter(name, plnFriend, filter) { if (name) { - filter.push({ + filter.push({ name: { contains: name, mode: 'insensitive' } }); - } + } if (!(plnFriend === "true")) { - filter.push({ + filter.push({ plnFriend: false - }); + }); } } @@ -455,14 +455,14 @@ export class TeamsService { * @param filter - Filter array to be appended to */ buildIndustryTagsFilter(industryTags, filter) { - const tags = industryTags?.split(',').map(tag=> tag.trim()); + const tags = industryTags?.split(',').map(tag => tag.trim()); if (tags?.length > 0) { - tags.map((tag)=> { + tags.map((tag) => { filter.push({ - industryTags:{ + industryTags: { some: { - title: { - in: tag + title: { + in: tag } } } @@ -479,12 +479,12 @@ export class TeamsService { buildTechnologiesFilter(technologies, filter) { const tags = technologies?.split(',').map(tech => tech.trim()); if (tags?.length > 0) { - tags.map((tag)=> { + tags.map((tag) => { filter.push({ technologies: { some: { - title: { - in: tag + title: { + in: tag } } } @@ -501,12 +501,12 @@ export class TeamsService { buildMembershipSourcesFilter(membershipSources, filter) { const sources = membershipSources?.split(',').map(source => source.trim()); if (sources?.length > 0) { - sources.map((source)=> { + sources.map((source) => { filter.push({ membershipSources: { some: { - title: { - in: source + title: { + in: source } } } @@ -555,7 +555,7 @@ export class TeamsService { * @returns The constructed query with a 'createdAt' filter if 'is_recent' is 'true', * or an empty object if 'is_recent' is not provided or set to 'false'. */ - buildRecentTeamsFilter(queryParams, filter?) { + buildRecentTeamsFilter(queryParams, filter?) { const { isRecent } = queryParams; const recentFilter = { createdAt: { @@ -564,7 +564,7 @@ export class TeamsService { }; if (isRecent === 'true' && !filter) { return recentFilter; - } + } if (isRecent === 'true' && filter) { filter.push(recentFilter); } @@ -720,4 +720,68 @@ export class TeamsService { }); } } + + /** + * Fetches filter tags for teams for felicitating ease searching. + * + * @returns Set of industry tags, membership sources, funding stages + * and technologies that contains atleast one team. + */ + async getTeamFilters(queryParams) { + const [industryTags, membershipSources, fundingStages, technologies] = await Promise.all([ + this.prisma.industryTag.findMany({ + where: { + teams: { + some: {...queryParams.where}, + }, + }, + select: { + uid: true, + title: true, + }, + }), + + this.prisma.membershipSource.findMany({ + where: { + teams: { + some: {...queryParams.where}, + }, + }, + select: { + uid: true, + title: true, + }, + }), + + this.prisma.fundingStage.findMany({ + where: { + teams: { + some: {...queryParams.where}, + }, + }, + select: { + uid: true, + title: true, + }, + }), + + this.prisma.technology.findMany({ + where: { + teams: { + some: {...queryParams.where}, + }, + }, + select: { + uid: true, + title: true, + }, + }), + ]); + return { + industryTags: industryTags.map((tag) => ({ uid: tag.uid, title: tag.title })), + membershipSources: membershipSources.map((source) => ({ uid: source.uid, title: source.title })), + fundingStages: fundingStages.map((stage) => ({ uid: stage.uid, title: stage.title })), + technologies: technologies.map((tech) =>({ uid: tech.uid, title: tech.title })), + }; + } } diff --git a/libs/contracts/src/lib/contract-team.ts b/libs/contracts/src/lib/contract-team.ts index 4578d06f6..632ecb04d 100644 --- a/libs/contracts/src/lib/contract-team.ts +++ b/libs/contracts/src/lib/contract-team.ts @@ -9,6 +9,15 @@ import { getAPIVersionAsPath } from '../utils/versioned-path'; const contract = initContract(); export const apiTeam = contract.router({ + teamFilters: { + method: 'GET', + path: `${getAPIVersionAsPath('1')}/teams/filter`, + query: TeamQueryParams, + responses: { + 200: contract.response(), + }, + summary: 'filter teams', + }, getTeams: { method: 'GET', path: `${getAPIVersionAsPath('1')}/teams`, From add3b6ab62d909aa123e641c9eaa74be0a4b69e5 Mon Sep 17 00:00:00 2001 From: navneethkrish Date: Thu, 21 Nov 2024 12:35:33 +0530 Subject: [PATCH 02/41] feat(project filters): added project filters api --- .../src/projects/projects.controller.ts | 13 +- apps/web-api/src/projects/projects.service.ts | 163 ++++++++++-------- apps/web-api/src/teams/teams.controller.ts | 8 +- apps/web-api/src/teams/teams.service.ts | 20 +-- libs/contracts/src/lib/contract-project.ts | 10 +- libs/contracts/src/lib/contract-team.ts | 2 +- 6 files changed, 125 insertions(+), 91 deletions(-) diff --git a/apps/web-api/src/projects/projects.controller.ts b/apps/web-api/src/projects/projects.controller.ts index 3bb5881c0..ab0b9f482 100644 --- a/apps/web-api/src/projects/projects.controller.ts +++ b/apps/web-api/src/projects/projects.controller.ts @@ -19,7 +19,13 @@ type RouteShape = typeof server.routeShapes; @Controller() export class ProjectsController { - constructor(private readonly projectsService: ProjectsService) {} + constructor(private readonly projectsService: ProjectsService) { } + + @Api(server.route.getProjectFilters) + @NoCache() + async getProjectFilters() { + return await this.projectsService.getProjectFilters(); + } @Api(server.route.createProject) @UsePipes(ZodValidationPipe) @@ -40,7 +46,7 @@ export class ProjectsController { ) { return this.projectsService.updateProjectByUid(uid, body as any, request.userEmail); } - + @Api(server.route.getProjects) @ApiOkResponseFromZod(ResponseProjectWithRelationsSchema.array()) async findAll(@Req() req) { @@ -49,7 +55,7 @@ export class ProjectsController { ); const builder = new PrismaQueryBuilder(queryableFields); const builtQuery = builder.build(req.query); - const { focusAreas } : any = req.query; + const { focusAreas }: any = req.query; builtQuery.where = { AND: [ builtQuery.where ? builtQuery.where : {}, @@ -82,4 +88,5 @@ export class ProjectsController { ) { return this.projectsService.removeProjectByUid(uid, request.userEmail); } + } diff --git a/apps/web-api/src/projects/projects.service.ts b/apps/web-api/src/projects/projects.service.ts index ea189f53d..4651a646c 100644 --- a/apps/web-api/src/projects/projects.service.ts +++ b/apps/web-api/src/projects/projects.service.ts @@ -12,20 +12,20 @@ export class ProjectsService { private memberService: MembersService, private logger: LogService, @Inject(CACHE_MANAGER) private cacheService: Cache - ) {} + ) { } async createProject(project: Prisma.ProjectUncheckedCreateInput, userEmail: string) { try { - const member:any = await this.getMemberInfo(userEmail); - const { contributingTeams, contributions, focusAreas} : any = project; + const member: any = await this.getMemberInfo(userEmail); + const { contributingTeams, contributions, focusAreas }: any = project; project.createdBy = member.uid; - project['projectFocusAreas'] = {...await this.createProjectWithFocusAreas(focusAreas, this.prisma)}; + project['projectFocusAreas'] = { ...await this.createProjectWithFocusAreas(focusAreas, this.prisma) }; delete project['focusAreas']; const result = await this.prisma.project.create({ data: { ...project, contributingTeams: { - connect: contributingTeams?.map(team => { return { uid: team.uid }}) + connect: contributingTeams?.map(team => { return { uid: team.uid } }) }, contributions: { create: contributions?.map((contribution) => { @@ -36,7 +36,7 @@ export class ProjectsService { }); await this.cacheService.reset(); return result; - } catch(err) { + } catch (err) { this.handleErrors(err); } } @@ -47,13 +47,13 @@ export class ProjectsService { userEmail: string ) { try { - const member:any = await this.getMemberInfo(userEmail); - const existingData:any = await this.getProjectByUid(uid); + const member: any = await this.getMemberInfo(userEmail); + const existingData: any = await this.getProjectByUid(uid); const contributingTeamsUid = existingData?.contributingTeams?.map(team => team.uid) || []; await this.isMemberAllowedToEdit(member, [existingData?.maintainingTeamUid, ...contributingTeamsUid], existingData); - const { contributingTeams, contributions, focusAreas } : any = project; - const contributionsToCreate:any = []; - const contributionUidsToDelete:any = []; + const { contributingTeams, contributions, focusAreas }: any = project; + const contributionsToCreate: any = []; + const contributionUidsToDelete: any = []; contributions?.map((contribution) => { if (!contribution.uid) { contributionsToCreate.push(contribution); @@ -62,8 +62,8 @@ export class ProjectsService { contributionUidsToDelete.push({ uid: contribution.uid }); } }); - return await this.prisma.$transaction(async(tx) => { - project['projectFocusAreas'] = {...await this.updateProjectWithFocusAreas(uid, focusAreas, tx)}; + return await this.prisma.$transaction(async (tx) => { + project['projectFocusAreas'] = { ...await this.updateProjectWithFocusAreas(uid, focusAreas, tx) }; delete project['focusAreas']; const result = await tx.project.update({ where: { @@ -72,8 +72,8 @@ export class ProjectsService { data: { ...project, contributingTeams: { - disconnect: contributingTeamsUid?.map(uid => { return { uid }}), - connect: contributingTeams?.map(team => { return { uid: team.uid }}) || [] + disconnect: contributingTeamsUid?.map(uid => { return { uid } }), + connect: contributingTeams?.map(team => { return { uid: team.uid } }) || [] }, contributions: { create: contributionsToCreate, @@ -84,7 +84,7 @@ export class ProjectsService { await this.cacheService.reset(); return result; }); - } catch(err) { + } catch (err) { this.handleErrors(err, `${uid}`); } } @@ -96,24 +96,24 @@ export class ProjectsService { isDeleted: false }; queryOptions.include = { - contributions: { - select: { + contributions: { + select: { uid: true, - member: { - select: { - uid: true, - name: true, + member: { + select: { + uid: true, + name: true, image: true } } } - }, - maintainingTeam: { select: { uid: true, name: true, logo: true }}, - creator: { select: { uid: true, name: true, image: true }}, + }, + maintainingTeam: { select: { uid: true, name: true, logo: true } }, + creator: { select: { uid: true, name: true, image: true } }, logo: true }; return await this.prisma.project.findMany(queryOptions); - } catch(err) { + } catch (err) { this.handleErrors(err); } } @@ -125,23 +125,23 @@ export class ProjectsService { const project = await this.prisma.project.findUniqueOrThrow({ where: { uid }, include: { - maintainingTeam: { select: { uid: true, name: true, logo: true }}, - contributingTeams: { select: { uid: true, name: true, logo: true }}, - contributions: { - select: { + maintainingTeam: { select: { uid: true, name: true, logo: true } }, + contributingTeams: { select: { uid: true, name: true, logo: true } }, + contributions: { + select: { uid: true, - member: { - select: { - uid: true, - name: true, - image: true , - teamMemberRoles:{ - select:{ - mainTeam:true, - teamLead:true, - role:true, - team:{ - select:{ + member: { + select: { + uid: true, + name: true, + image: true, + teamMemberRoles: { + select: { + mainTeam: true, + teamLead: true, + role: true, + team: { + select: { uid: true, name: true } @@ -149,18 +149,18 @@ export class ProjectsService { } } } - }, + }, projectUid: true } }, - creator: { select: { uid: true, name: true, image: true }}, + creator: { select: { uid: true, name: true, image: true } }, logo: true, projectFocusAreas: { select: { focusArea: { select: { uid: true, - title: true + title: true } } } @@ -169,7 +169,7 @@ export class ProjectsService { }); project['projectFocusAreas'] = this.removeDuplicateFocusAreas(project?.projectFocusAreas); return project; - } catch(err) { + } catch (err) { this.handleErrors(err, `${uid}`); } } @@ -178,7 +178,7 @@ export class ProjectsService { uid: string, userEmail: string ) { - const member:any = await this.getMemberInfo(userEmail); + const member: any = await this.getMemberInfo(userEmail); const existingData = await this.getProjectByUid(uid); await this.isMemberAllowedToDelete(member, existingData); try { @@ -188,7 +188,7 @@ export class ProjectsService { }); await this.cacheService.reset(); return result; - } catch(err) { + } catch (err) { this.handleErrors(err, `${uid}`); } } @@ -216,7 +216,7 @@ export class ProjectsService { return await this.memberService.findMemberByEmail(memberEmail) }; - async isMemberAllowedToEdit(member, teams, project ) { + async isMemberAllowedToEdit(member, teams, project) { const res = await this.memberService.isMemberPartOfTeams(member, teams); if (res || this.memberService.checkIfAdminUser(member) || member.uid === project.createdBy) { return true; @@ -236,7 +236,7 @@ export class ProjectsService { async createProjectWithFocusAreas(focusAreas, transaction) { if (focusAreas && focusAreas.length) { - const projectFocusAreas:any = []; + const projectFocusAreas: any = []; const focusAreaHierarchies = await transaction.focusAreaHierarchy.findMany({ where: { subFocusAreaUid: { @@ -261,7 +261,7 @@ export class ProjectsService { data: projectFocusAreas } } - } + } } async isFocusAreaModified(projectId, focusAreas, transaction) { @@ -275,10 +275,10 @@ export class ProjectsService { if (newFocusAreaUIds.length !== focusAreasUIds.length) { return true; - } + } if (projectFocusAreas.length === 0 && focusAreas.length === 0) { - return false + return false } return !focusAreasUIds.every(area => newFocusAreaUIds.includes(area)); } @@ -309,7 +309,7 @@ export class ProjectsService { return { projectFocusAreas: { some: { - ancestorArea:{ + ancestorArea: { title: { in: focusAreas?.split(',') } @@ -324,56 +324,56 @@ export class ProjectsService { removeDuplicateFocusAreas(focusAreas): any { const uniqueFocusAreas = {}; focusAreas.forEach(item => { - const uid = item.focusArea.uid; - const title = item.focusArea.title; - uniqueFocusAreas[uid] = { uid, title }; + const uid = item.focusArea.uid; + const title = item.focusArea.title; + uniqueFocusAreas[uid] = { uid, title }; }); return Object.values(uniqueFocusAreas); } - buildProjectFilter(query){ - const { + buildProjectFilter(query) { + const { name, lookingForFunding, team } = query; - const filter:any = [{ + const filter: any = [{ isDeleted: false }]; this.buildNameFilter(name, filter); this.buildFundingFilter(lookingForFunding, filter); this.buildMaintainingTeamFilter(team, filter); this.buildRecentProjectsFilter(query, filter); - return { + return { AND: filter }; } buildNameFilter(name, filter) { if (name) { - filter.push({ + filter.push({ name: { contains: name, mode: 'insensitive' } }); - } + } } buildFundingFilter(funding, filter) { if (funding === "true") { - filter.push({ + filter.push({ lookingForFunding: true }); - } + } } buildMaintainingTeamFilter(team, filter) { if (team) { - filter.push({ + filter.push({ maintainingTeamUid: team }); - } + } } /** @@ -388,7 +388,7 @@ export class ProjectsService { * @returns The constructed query with a 'createdAt' filter if 'is_recent' is 'true', * or an empty object if 'is_recent' is not provided or set to 'false'. */ - buildRecentProjectsFilter(queryParams, filter?) { + buildRecentProjectsFilter(queryParams, filter?) { const { isRecent } = queryParams; const recentFilter = { createdAt: { @@ -397,10 +397,35 @@ export class ProjectsService { }; if (isRecent === 'true' && !filter) { return recentFilter; - } + } if (isRecent === 'true' && filter) { filter.push(recentFilter); } return {}; } + + /** + * Fetches team names that maintain atleast a single project. + * + * @returns Set of team names. + */ + async getProjectFilters() { + const maintainingTeams = await this.prisma.team.findMany({ + where: { + maintainingProjects: { + some: {}, + } + }, + select: { + uid: true, + name: true, + logo: { + select: { + url: true + } + } + } + }) + return { maintainedBy: maintainingTeams.map((team) => ({ uid: team.uid, name: team.name, logo: team.logo?.url })) }; + } } diff --git a/apps/web-api/src/teams/teams.controller.ts b/apps/web-api/src/teams/teams.controller.ts index b7fad7488..b614f1e1d 100644 --- a/apps/web-api/src/teams/teams.controller.ts +++ b/apps/web-api/src/teams/teams.controller.ts @@ -18,24 +18,22 @@ import { TeamsService } from './teams.service'; import { NoCache } from '../decorators/no-cache.decorator'; import { UserTokenValidation } from '../guards/user-token-validation.guard'; import { ParticipantsReqValidationPipe } from '../pipes/participant-request-validation.pipe'; -import { request } from 'http'; const server = initNestServer(apiTeam); type RouteShape = typeof server.routeShapes; @Controller() export class TeamsController { - constructor(private readonly teamsService: TeamsService) {} + constructor(private readonly teamsService: TeamsService) { } @Api(server.route.teamFilters) @ApiQueryFromZod(TeamQueryParams) - @NoCache() async getTeamFilters(@Req() request: Request) { const queryableFields = prismaQueryableFieldsFromZod( ResponseTeamWithRelationsSchema ); const builder = new PrismaQueryBuilder(queryableFields); const builtQuery = builder.build(request.query); - const { focusAreas } : any = request.query; + const { focusAreas }: any = request.query; builtQuery.where = { AND: [ builtQuery.where ? builtQuery.where : {}, @@ -56,7 +54,7 @@ export class TeamsController { ); const builder = new PrismaQueryBuilder(queryableFields); const builtQuery = builder.build(request.query); - const { focusAreas } : any = request.query; + const { focusAreas }: any = request.query; builtQuery.where = { AND: [ builtQuery.where ? builtQuery.where : {}, diff --git a/apps/web-api/src/teams/teams.service.ts b/apps/web-api/src/teams/teams.service.ts index 9fd1f4a6b..00469cf33 100644 --- a/apps/web-api/src/teams/teams.service.ts +++ b/apps/web-api/src/teams/teams.service.ts @@ -732,11 +732,10 @@ export class TeamsService { this.prisma.industryTag.findMany({ where: { teams: { - some: {...queryParams.where}, + some: { ...queryParams.where }, }, }, select: { - uid: true, title: true, }, }), @@ -744,11 +743,10 @@ export class TeamsService { this.prisma.membershipSource.findMany({ where: { teams: { - some: {...queryParams.where}, + some: { ...queryParams.where }, }, }, select: { - uid: true, title: true, }, }), @@ -756,11 +754,10 @@ export class TeamsService { this.prisma.fundingStage.findMany({ where: { teams: { - some: {...queryParams.where}, + some: { ...queryParams.where }, }, }, select: { - uid: true, title: true, }, }), @@ -768,20 +765,19 @@ export class TeamsService { this.prisma.technology.findMany({ where: { teams: { - some: {...queryParams.where}, + some: { ...queryParams.where }, }, }, select: { - uid: true, title: true, }, }), ]); return { - industryTags: industryTags.map((tag) => ({ uid: tag.uid, title: tag.title })), - membershipSources: membershipSources.map((source) => ({ uid: source.uid, title: source.title })), - fundingStages: fundingStages.map((stage) => ({ uid: stage.uid, title: stage.title })), - technologies: technologies.map((tech) =>({ uid: tech.uid, title: tech.title })), + industryTags: industryTags.map((tag) => tag.title), + membershipSources: membershipSources.map((source) => source.title), + fundingStages: fundingStages.map((stage) => stage.title), + technologies: technologies.map((tech) => tech.title), }; } } diff --git a/libs/contracts/src/lib/contract-project.ts b/libs/contracts/src/lib/contract-project.ts index b5fabc1a7..85d6f9adc 100644 --- a/libs/contracts/src/lib/contract-project.ts +++ b/libs/contracts/src/lib/contract-project.ts @@ -1,12 +1,20 @@ import { initContract } from '@ts-rest/core'; import { - ResponseProjectWithRelationsSchema + ResponseProjectWithRelationsSchema } from '../schema'; import { getAPIVersionAsPath } from '../utils/versioned-path'; const contract = initContract(); export const apiProjects = contract.router({ + getProjectFilters: { + method: 'GET', + path: `${getAPIVersionAsPath('1')}/projects/filters`, + responses: { + 200: contract.response(), + }, + summary: 'Get project filters', + }, getProjects: { method: 'GET', path: `${getAPIVersionAsPath('1')}/projects`, diff --git a/libs/contracts/src/lib/contract-team.ts b/libs/contracts/src/lib/contract-team.ts index 632ecb04d..199bc93d7 100644 --- a/libs/contracts/src/lib/contract-team.ts +++ b/libs/contracts/src/lib/contract-team.ts @@ -11,7 +11,7 @@ const contract = initContract(); export const apiTeam = contract.router({ teamFilters: { method: 'GET', - path: `${getAPIVersionAsPath('1')}/teams/filter`, + path: `${getAPIVersionAsPath('1')}/teams/filters`, query: TeamQueryParams, responses: { 200: contract.response(), From bcd32f70c98d957ccb58c6366fe4217de8ed924d Mon Sep 17 00:00:00 2001 From: Navaneethakrishnan Date: Thu, 21 Nov 2024 12:50:38 +0530 Subject: [PATCH 03/41] feat: added members filter API --- .../web-api/src/members/members.controller.ts | 29 ++++++++++- apps/web-api/src/members/members.service.ts | 49 +++++++++++++++++++ apps/web-api/src/projects/projects.service.ts | 21 -------- libs/contracts/src/lib/contract-member.ts | 9 ++++ libs/contracts/src/schema/project.ts | 15 ++++-- 5 files changed, 97 insertions(+), 26 deletions(-) diff --git a/apps/web-api/src/members/members.controller.ts b/apps/web-api/src/members/members.controller.ts index 5102acdfd..6d1c4ce02 100644 --- a/apps/web-api/src/members/members.controller.ts +++ b/apps/web-api/src/members/members.controller.ts @@ -71,7 +71,7 @@ export class MemberController { * @returns Array of roles with member counts */ @Api(server.route.getMemberRoles) - async getMemberFilters(@Req() request: Request) { + async getMemberRoleFilters(@Req() request: Request) { const queryableFields = prismaQueryableFieldsFromZod(ResponseMemberWithRelationsSchema); const queryParams = request.query; const builder = new PrismaQueryBuilder(queryableFields); @@ -89,6 +89,33 @@ export class MemberController { }; return await this.membersService.getRolesWithCount(builtQuery, queryParams); } + + /** + * Retrieves member filters. + * + * @param request - HTTP request object containing query parameters + * @returns return list of member filters. + */ + @Api(server.route.getMemberFilters) + async getMembersFilter(@Req() request: Request) { + const queryableFields = prismaQueryableFieldsFromZod(ResponseMemberWithRelationsSchema); + const queryParams = request.query; + const builder = new PrismaQueryBuilder(queryableFields); + const builtQuery = builder.build(queryParams); + const { name__icontains } = queryParams; + if (name__icontains) { + delete builtQuery.where?.name; + } + builtQuery.where = { + AND: [ + builtQuery.where, + this.membersService.buildNameFilters(queryParams), + this.membersService.buildRoleFilters(queryParams), + this.membersService.buildRecentMembersFilter(queryParams) + ], + }; + return await this.membersService.getMemberFilters(builtQuery); + } /** * Retrieves details of a specific member by UID. diff --git a/apps/web-api/src/members/members.service.ts b/apps/web-api/src/members/members.service.ts index 12cd54869..56df891b1 100644 --- a/apps/web-api/src/members/members.service.ts +++ b/apps/web-api/src/members/members.service.ts @@ -1292,6 +1292,55 @@ export class MembersService { return { }; } + /** + * Fetches filter tags for members for facilitating easy searching. + * @param queryParams HTTP request query params object + * @returns Set of skills, locations that contain at least one member. + */ + async getMemberFilters(queryParams) { + // Fetch unique skills + const skills = await this.prisma.skill.findMany({ + where: { + members: { + some: { ...queryParams.where }, + }, + }, + select: { + title: true, + }, + }); + + // Fetch unique locations + const locations = await this.prisma.location.findMany({ + where: { + members: { + some: { ...queryParams.where }, + }, + }, + select: { + city: true, + continent: true, + country: true, + region: true, + }, + }); + + // Deduplicate cities, countries, and regions using Set + const uniqueCities = [...new Set(locations.map((location) => location.city).filter(Boolean))]; + const uniqueCountries = [...new Set(locations.map((location) => location.country).filter(Boolean))]; + const uniqueRegions = [...new Set(locations.map((location) => location.region).filter(Boolean))]; + + // Return deduplicated skills and locations + return { + skills: skills.map((skill) => skill.title), + cities: uniqueCities, + countries: uniqueCountries, + regions: uniqueRegions, + }; + } + + + /** * Updates the member's field if the value has changed. * diff --git a/apps/web-api/src/projects/projects.service.ts b/apps/web-api/src/projects/projects.service.ts index 4651a646c..00cf3f2f4 100644 --- a/apps/web-api/src/projects/projects.service.ts +++ b/apps/web-api/src/projects/projects.service.ts @@ -91,27 +91,6 @@ export class ProjectsService { async getProjects(queryOptions: Prisma.ProjectFindManyArgs) { try { - queryOptions.where = { - ...queryOptions.where, - isDeleted: false - }; - queryOptions.include = { - contributions: { - select: { - uid: true, - member: { - select: { - uid: true, - name: true, - image: true - } - } - } - }, - maintainingTeam: { select: { uid: true, name: true, logo: true } }, - creator: { select: { uid: true, name: true, image: true } }, - logo: true - }; return await this.prisma.project.findMany(queryOptions); } catch (err) { this.handleErrors(err); diff --git a/libs/contracts/src/lib/contract-member.ts b/libs/contracts/src/lib/contract-member.ts index 7e2b2352e..66eba60ac 100644 --- a/libs/contracts/src/lib/contract-member.ts +++ b/libs/contracts/src/lib/contract-member.ts @@ -28,6 +28,15 @@ export const apiMembers = contract.router({ }, summary: 'Get member roles', }, + getMemberFilters: { + method: 'GET', + path: `${getAPIVersionAsPath('1')}/members/filters`, + query: MemberQueryParams, + responses: { + 200: contract.response(), + }, + summary: 'Get member filter values', + }, getMember: { method: 'GET', path: `${getAPIVersionAsPath('1')}/members/:uid`, diff --git a/libs/contracts/src/schema/project.ts b/libs/contracts/src/schema/project.ts index c31399961..c26161c67 100644 --- a/libs/contracts/src/schema/project.ts +++ b/libs/contracts/src/schema/project.ts @@ -1,5 +1,7 @@ -import { createZodDto } from 'nestjs-zod'; -import { z } from 'nestjs-zod/z'; +import { createZodDto } from '@abitia/zod-dto'; +import { z } from 'zod'; +import { ResponseTeamWithRelationsSchema } from './team'; +import { ResponseMemberWithRelationsSchema } from './member'; const TypeEnum = z.enum(['MAINTENER', 'COLLABORATOR']); @@ -40,11 +42,16 @@ const ProjectSchema = z.object({ uid: z.string(), title: z.string() }).array().optional(), - contributions: ContributionSchema.array().optional() + contributions: ContributionSchema.array().optional(), + isDeleted: z.boolean().default(false) }); export const ResponseProjectSchema = ProjectSchema.omit({ id: true }).strict(); -export const ResponseProjectWithRelationsSchema = ProjectSchema.extend({}); +export const ResponseProjectWithRelationsSchema = ResponseProjectSchema.extend({ + maintainingTeam: ResponseTeamWithRelationsSchema.optional(), + contributingTeams: ResponseTeamWithRelationsSchema.array().optional(), + creator: ResponseMemberWithRelationsSchema.optional() +}); export const ResponseProjectSuccessSchema = z.object({ success: z.boolean()}); // omit score and id to avoid update from request export class UpdateProjectDto extends createZodDto(ProjectSchema.partial().omit({ id:true, score: true })) {} From baa627696105fd5a6f0d84cd5aa140161f69c75e Mon Sep 17 00:00:00 2001 From: navneethkrish Date: Fri, 22 Nov 2024 15:14:46 +0530 Subject: [PATCH 04/41] feat(added count): modified get api of teams, members, projects to fetch count --- apps/web-api/src/members/members.service.ts | 8 ++++++-- apps/web-api/src/projects/projects.service.ts | 6 +++++- apps/web-api/src/teams/teams.controller.ts | 1 - apps/web-api/src/teams/teams.service.ts | 8 ++++++-- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/apps/web-api/src/members/members.service.ts b/apps/web-api/src/members/members.service.ts index 56df891b1..e88fac792 100644 --- a/apps/web-api/src/members/members.service.ts +++ b/apps/web-api/src/members/members.service.ts @@ -75,9 +75,13 @@ export class MembersService { * the members. These options are based on Prisma's `MemberFindManyArgs`. * @returns A promise that resolves to an array of member records matching the query criteria. */ - async findAll(queryOptions: Prisma.MemberFindManyArgs): Promise { + async findAll(queryOptions: Prisma.MemberFindManyArgs): Promise<{count:Number, members:Member[]}> { try { - return await this.prisma.member.findMany(queryOptions); + const [members, membersCount] = await Promise.all([ + this.prisma.member.findMany(queryOptions), + this.prisma.member.count({ where: queryOptions.where }), + ]); + return { count: membersCount, members: members } } catch(error) { return this.handleErrors(error); } diff --git a/apps/web-api/src/projects/projects.service.ts b/apps/web-api/src/projects/projects.service.ts index 00cf3f2f4..4ea954232 100644 --- a/apps/web-api/src/projects/projects.service.ts +++ b/apps/web-api/src/projects/projects.service.ts @@ -91,7 +91,11 @@ export class ProjectsService { async getProjects(queryOptions: Prisma.ProjectFindManyArgs) { try { - return await this.prisma.project.findMany(queryOptions); + const [projects, projectsCount] = await Promise.all([ + this.prisma.project.findMany(queryOptions), + this.prisma.project.count({ where: queryOptions.where }), + ]); + return { count: projectsCount, projects: projects } } catch (err) { this.handleErrors(err); } diff --git a/apps/web-api/src/teams/teams.controller.ts b/apps/web-api/src/teams/teams.controller.ts index b614f1e1d..68a813b12 100644 --- a/apps/web-api/src/teams/teams.controller.ts +++ b/apps/web-api/src/teams/teams.controller.ts @@ -47,7 +47,6 @@ export class TeamsController { @Api(server.route.getTeams) @ApiQueryFromZod(TeamQueryParams) @ApiOkResponseFromZod(ResponseTeamWithRelationsSchema.array()) - @NoCache() findAll(@Req() request: Request) { const queryableFields = prismaQueryableFieldsFromZod( ResponseTeamWithRelationsSchema diff --git a/apps/web-api/src/teams/teams.service.ts b/apps/web-api/src/teams/teams.service.ts index 00469cf33..49510534f 100644 --- a/apps/web-api/src/teams/teams.service.ts +++ b/apps/web-api/src/teams/teams.service.ts @@ -46,9 +46,13 @@ export class TeamsService { * (filter, pagination, sorting, etc.) * @returns A list of teams that match the query options */ - async findAll(queryOptions: Prisma.TeamFindManyArgs): Promise { + async findAll(queryOptions: Prisma.TeamFindManyArgs): Promise<{ count: Number, teams: Team[] }> { try { - return this.prisma.team.findMany({ ...queryOptions }); + const [teams, teamsCount] = await Promise.all([ + this.prisma.team.findMany(queryOptions), + this.prisma.team.count({ where: queryOptions.where }), + ]); + return { count: teamsCount, teams: teams }; } catch (err) { return this.handleErrors(err); } From 03272b240b8952fad3eeeac663913fe6bb1c961c Mon Sep 17 00:00:00 2001 From: Navaneethakrishnan Date: Mon, 25 Nov 2024 16:18:33 +0530 Subject: [PATCH 05/41] fix: fixed issue in role with count api --- apps/web-api/src/members/members.service.ts | 11 +++++++---- libs/contracts/src/schema/project.ts | 3 +++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/apps/web-api/src/members/members.service.ts b/apps/web-api/src/members/members.service.ts index e88fac792..ed2ead7a4 100644 --- a/apps/web-api/src/members/members.service.ts +++ b/apps/web-api/src/members/members.service.ts @@ -178,6 +178,9 @@ export class MembersService { formattedDefaultRoles.push({ ...defaultRole, count: 0 }); } }); + if (!searchTerm) { + return formattedDefaultRoles; + } const result: any = await this.getRoleCountForExcludedAndNonSelectedRoles(selectedRoles, members, searchTerm); return [...formattedDefaultRoles, ...result]; } catch (error) { @@ -1302,7 +1305,6 @@ export class MembersService { * @returns Set of skills, locations that contain at least one member. */ async getMemberFilters(queryParams) { - // Fetch unique skills const skills = await this.prisma.skill.findMany({ where: { members: { @@ -1313,8 +1315,6 @@ export class MembersService { title: true, }, }); - - // Fetch unique locations const locations = await this.prisma.location.findMany({ where: { members: { @@ -1326,13 +1326,15 @@ export class MembersService { continent: true, country: true, region: true, + metroArea: true }, }); // Deduplicate cities, countries, and regions using Set const uniqueCities = [...new Set(locations.map((location) => location.city).filter(Boolean))]; const uniqueCountries = [...new Set(locations.map((location) => location.country).filter(Boolean))]; - const uniqueRegions = [...new Set(locations.map((location) => location.region).filter(Boolean))]; + const uniqueRegions = [...new Set(locations.map((location) => location.continent).filter(Boolean))]; + const uniqueMetroAreas = [...new Set(locations.map((location) => location.metroArea).filter(Boolean))]; // Return deduplicated skills and locations return { @@ -1340,6 +1342,7 @@ export class MembersService { cities: uniqueCities, countries: uniqueCountries, regions: uniqueRegions, + metroAreas: uniqueMetroAreas }; } diff --git a/libs/contracts/src/schema/project.ts b/libs/contracts/src/schema/project.ts index c26161c67..312ee9115 100644 --- a/libs/contracts/src/schema/project.ts +++ b/libs/contracts/src/schema/project.ts @@ -2,6 +2,7 @@ import { createZodDto } from '@abitia/zod-dto'; import { z } from 'zod'; import { ResponseTeamWithRelationsSchema } from './team'; import { ResponseMemberWithRelationsSchema } from './member'; +import { ResponseImageWithRelationsSchema } from './image'; const TypeEnum = z.enum(['MAINTENER', 'COLLABORATOR']); @@ -14,6 +15,7 @@ const ContributionSchema = z.object({ const ProjectSchema = z.object({ id: z.number().int(), + uid: z.string(), logoUid: z.string().optional().nullable(), name: z.string(), tagline: z.string(), @@ -48,6 +50,7 @@ const ProjectSchema = z.object({ export const ResponseProjectSchema = ProjectSchema.omit({ id: true }).strict(); export const ResponseProjectWithRelationsSchema = ResponseProjectSchema.extend({ + logo: ResponseImageWithRelationsSchema.optional(), maintainingTeam: ResponseTeamWithRelationsSchema.optional(), contributingTeams: ResponseTeamWithRelationsSchema.array().optional(), creator: ResponseMemberWithRelationsSchema.optional() From 989b6262c424a5b9d1f918fdfb0d37c30e16d1b2 Mon Sep 17 00:00:00 2001 From: navneethkrish Date: Wed, 27 Nov 2024 13:20:05 +0530 Subject: [PATCH 06/41] feat(update member verification): added api for verifying member --- .../web-api/src/members/members.controller.ts | 28 +++++++++++++++++-- libs/contracts/src/lib/contract-member.ts | 10 +++++++ libs/contracts/src/schema/member.ts | 3 +- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/apps/web-api/src/members/members.controller.ts b/apps/web-api/src/members/members.controller.ts index 6d1c4ce02..b7943fdbd 100644 --- a/apps/web-api/src/members/members.controller.ts +++ b/apps/web-api/src/members/members.controller.ts @@ -31,8 +31,8 @@ type RouteShape = typeof server.routeShapes; @Controller() @NoCache() export class MemberController { - constructor(private readonly membersService: MembersService, private logger: LogService) {} - + constructor(private readonly membersService: MembersService, private logger: LogService) { } + /** * Retrieves a list of members based on query parameters. * Builds a Prisma query from the queryable fields and adds filters for names, roles, and recent members. @@ -182,6 +182,9 @@ export class MemberController { @Api(server.route.updateMember) @UseGuards(UserTokenValidation) async updateMemberByUid(@Param('uid') uid, @Body() body) { + if (body.isVerified) { + delete body.isVerified; + } return await this.membersService.updateMemberByUid(uid, body); } @@ -236,7 +239,7 @@ export class MemberController { @UsePipes(ZodValidationPipe) async updateMemberEmail(@Body() changeEmailRequest: ChangeEmailRequestDto, @Req() req) { const memberInfo = await this.membersService.findMemberByEmail(req.userEmail); - if(!memberInfo || !memberInfo.externalId) { + if (!memberInfo || !memberInfo.externalId) { throw new ForbiddenException("Please login again and try") } return await this.membersService.updateMemberEmail(changeEmailRequest.newEmail, req.userEmail, memberInfo); @@ -252,4 +255,23 @@ export class MemberController { async getGitProjects(@Param('uid') uid) { return await this.membersService.getGitProjects(uid); } + + /** + * Updates a member as verified member. + * + * @param uid - uid of the member to be updated + * @param req - HTTP request object containing user details + * @returns Updated member data with new details. + */ + @Api(server.route.updateMemberVerificationStatus) + @UseGuards(UserTokenValidation) + @UsePipes(ZodValidationPipe) + async updateMemberVerificationStatus(@Param('uid') uid, @Body() body, @Req() req) { + this.logger.info(`Member verification request - Initated by -> ${req.userEmail}`); + const member = await this.membersService.findMemberByEmail(req.userEmail); + if (!this.membersService.checkIfAdminUser(member)) { + throw new ForbiddenException(`Member with email ${req.userEmail} isn't admin to verify a member`); + } + return await this.membersService.updateMemberByUid(uid, { ...body, isVerified: true }); + } } diff --git a/libs/contracts/src/lib/contract-member.ts b/libs/contracts/src/lib/contract-member.ts index 66eba60ac..2a389f4be 100644 --- a/libs/contracts/src/lib/contract-member.ts +++ b/libs/contracts/src/lib/contract-member.ts @@ -10,6 +10,16 @@ import { getAPIVersionAsPath } from '../utils/versioned-path'; const contract = initContract(); export const apiMembers = contract.router({ + updateMemberVerificationStatus: { + method:'PUT', + path: `${getAPIVersionAsPath('1')}/members/:uid/verify`, + body: contract.body(), + responses: { + 200: contract.response(), + }, + summary: 'Verify a member', + + }, getMembers: { method: 'GET', path: `${getAPIVersionAsPath('1')}/members`, diff --git a/libs/contracts/src/schema/member.ts b/libs/contracts/src/schema/member.ts index 7a026bbdd..658d9f2d4 100644 --- a/libs/contracts/src/schema/member.ts +++ b/libs/contracts/src/schema/member.ts @@ -48,7 +48,8 @@ export const MemberSchema = z.object({ linkedinHandler: z.string().nullish(), repositories: GitHubRepositorySchema.array().optional(), preferences: PreferenceSchema.optional(), - projectContributions: z.array(ProjectContributionSchema).optional() + projectContributions: z.array(ProjectContributionSchema).optional(), + isVerified:z.boolean().default(false) }); From 38fadde835fa975e9de5e9e47747d4d0c1b05c8c Mon Sep 17 00:00:00 2001 From: Navaneethakrishnan Date: Wed, 27 Nov 2024 14:43:37 +0530 Subject: [PATCH 07/41] feat: added isVerified and signup source field in member --- apps/web-api/prisma/fixtures/members.ts | 2 ++ .../migration.sql | 13 +++++++++++++ apps/web-api/prisma/schema.prisma | 4 +++- .../verified-member.interceptor.ts | 15 +++++++++++++++ apps/web-api/src/members/members.controller.ts | 7 ++++++- libs/contracts/src/schema/member.ts | 18 ++++++++++++------ .../src/schema/participants-request.ts | 12 +++++++++--- 7 files changed, 60 insertions(+), 11 deletions(-) create mode 100644 apps/web-api/prisma/migrations/20241127093907_member_isverified/migration.sql create mode 100644 apps/web-api/src/interceptors/verified-member.interceptor.ts diff --git a/apps/web-api/prisma/fixtures/members.ts b/apps/web-api/prisma/fixtures/members.ts index a53ca402e..eaf012a4e 100644 --- a/apps/web-api/prisma/fixtures/members.ts +++ b/apps/web-api/prisma/fixtures/members.ts @@ -53,6 +53,8 @@ const membersFactory = Factory.define>( plnStartDate: faker.date.past(), updatedAt: faker.date.recent(), locationUid: '', + signUpSource: faker.company.name(), + isVerified: faker.datatype.boolean(), openToWork: faker.datatype.boolean(), preferences: {showEmail:true,showGithubHandle:true,showTelegram:true,showLinkedin:true,showDiscord:false,showGithubProjects:false,showTwitter:true} }; diff --git a/apps/web-api/prisma/migrations/20241127093907_member_isverified/migration.sql b/apps/web-api/prisma/migrations/20241127093907_member_isverified/migration.sql new file mode 100644 index 000000000..056b75254 --- /dev/null +++ b/apps/web-api/prisma/migrations/20241127093907_member_isverified/migration.sql @@ -0,0 +1,13 @@ +/* + Warnings: + + - A unique constraint covering the columns `[memberUid,teamUid,eventUid]` on the table `PLEventGuest` will be added. If there are existing duplicate values, this will fail. + +*/ +-- AlterTable +ALTER TABLE "Member" ADD COLUMN "isVerified" BOOLEAN DEFAULT false, +ADD COLUMN "signUpSource" TEXT, +ALTER COLUMN "plnFriend" DROP NOT NULL; + +-- CreateIndex +CREATE UNIQUE INDEX "PLEventGuest_memberUid_teamUid_eventUid_key" ON "PLEventGuest"("memberUid", "teamUid", "eventUid"); diff --git a/apps/web-api/prisma/schema.prisma b/apps/web-api/prisma/schema.prisma index b08be33b8..ae8c05615 100644 --- a/apps/web-api/prisma/schema.prisma +++ b/apps/web-api/prisma/schema.prisma @@ -66,12 +66,14 @@ model Member { officeHours String? moreDetails String? bio String? - plnFriend Boolean @default(false) + plnFriend Boolean? @default(false) plnStartDate DateTime? airtableRecId String? @unique externalId String? @unique openToWork Boolean? @default(false) isFeatured Boolean? @default(false) + isVerified Boolean? @default(false) + signUpSource String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt approvedAt DateTime @default(now()) diff --git a/apps/web-api/src/interceptors/verified-member.interceptor.ts b/apps/web-api/src/interceptors/verified-member.interceptor.ts new file mode 100644 index 000000000..fd719b4f0 --- /dev/null +++ b/apps/web-api/src/interceptors/verified-member.interceptor.ts @@ -0,0 +1,15 @@ +import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; +import { Observable } from 'rxjs'; + +@Injectable() +export class IsVerifiedMemberInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + const request = context.switchToHttp().getRequest(); + if (request.query.isVerified === 'all') { + delete request.query.isVerified; + } else if (request.query.isVerified !== 'false') { + request.query.isVerified = 'true'; + } + return next.handle(); + } +} diff --git a/apps/web-api/src/members/members.controller.ts b/apps/web-api/src/members/members.controller.ts index b7943fdbd..c0fe3217e 100644 --- a/apps/web-api/src/members/members.controller.ts +++ b/apps/web-api/src/members/members.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Param, Req, UseGuards, UsePipes, BadRequestException, ForbiddenException } from '@nestjs/common'; +import { Body, Controller, Param, Req, UseGuards, UsePipes, UseInterceptors, BadRequestException, ForbiddenException } from '@nestjs/common'; import { ApiNotFoundResponse, ApiParam } from '@nestjs/swagger'; import { Api, ApiDecorator, initNestServer } from '@ts-rest/nest'; import { Request } from 'express'; @@ -24,6 +24,7 @@ import { AuthGuard } from '../guards/auth.guard'; import { UserAccessTokenValidateGuard } from '../guards/user-access-token-validate.guard'; import { LogService } from '../shared/log.service'; import { ParticipantsReqValidationPipe } from '../pipes/participant-request-validation.pipe'; +import { IsVerifiedMemberInterceptor } from '../interceptors/verified-member.interceptor'; const server = initNestServer(apiMembers); type RouteShape = typeof server.routeShapes; @@ -43,6 +44,7 @@ export class MemberController { @Api(server.route.getMembers) @ApiQueryFromZod(MemberQueryParams) @ApiOkResponseFromZod(ResponseMemberWithRelationsSchema.array()) + @UseInterceptors(IsVerifiedMemberInterceptor) async findAll(@Req() request: Request) { const queryableFields = prismaQueryableFieldsFromZod(ResponseMemberWithRelationsSchema); const queryParams = request.query; @@ -71,6 +73,7 @@ export class MemberController { * @returns Array of roles with member counts */ @Api(server.route.getMemberRoles) + @UseInterceptors(IsVerifiedMemberInterceptor) async getMemberRoleFilters(@Req() request: Request) { const queryableFields = prismaQueryableFieldsFromZod(ResponseMemberWithRelationsSchema); const queryParams = request.query; @@ -97,6 +100,7 @@ export class MemberController { * @returns return list of member filters. */ @Api(server.route.getMemberFilters) + @UseInterceptors(IsVerifiedMemberInterceptor) async getMembersFilter(@Req() request: Request) { const queryableFields = prismaQueryableFieldsFromZod(ResponseMemberWithRelationsSchema); const queryParams = request.query; @@ -130,6 +134,7 @@ export class MemberController { @ApiNotFoundResponse(NOT_FOUND_GLOBAL_RESPONSE_SCHEMA) @ApiOkResponseFromZod(ResponseMemberWithRelationsSchema) @ApiQueryFromZod(MemberDetailQueryParams) + @UseInterceptors(IsVerifiedMemberInterceptor) @NoCache() async findOne(@Req() request: Request, @ApiDecorator() { params: { uid } }: RouteShape['getMember']) { const queryableFields = prismaQueryableFieldsFromZod(ResponseMemberWithRelationsSchema); diff --git a/libs/contracts/src/schema/member.ts b/libs/contracts/src/schema/member.ts index 658d9f2d4..2162ca626 100644 --- a/libs/contracts/src/schema/member.ts +++ b/libs/contracts/src/schema/member.ts @@ -30,7 +30,7 @@ export const MemberSchema = z.object({ uid: z.string(), name: z.string(), email: z.string(), - externalId: z.string(), + externalId: z.string().nullish(), imageUid: z.string().nullish(), githubHandler: z.string().nullish(), discordHandler: z.string().nullish(), @@ -38,18 +38,19 @@ export const MemberSchema = z.object({ telegramHandler: z.string().nullish(), officeHours: z.string().nullish(), airtableRecId: z.string().nullish(), - plnFriend: z.boolean(), + plnFriend: z.boolean().nullable(), bio: z.string().nullish(), + signUpSource: z.string().nullish(), isFeatured: z.boolean().nullish(), createdAt: z.string(), updatedAt: z.string(), - locationUid: z.string(), - openToWork: z.boolean(), + locationUid: z.string().nullable(), + openToWork: z.boolean().nullable(), linkedinHandler: z.string().nullish(), repositories: GitHubRepositorySchema.array().optional(), preferences: PreferenceSchema.optional(), projectContributions: z.array(ProjectContributionSchema).optional(), - isVerified:z.boolean().default(false) + isVerified:z.boolean().nullish() }); @@ -75,7 +76,12 @@ export const CreateMemberSchema = MemberSchema.pick({ officeHours: true, plnFriend: true, locationUid: true, - bio: true + bio: true, + signUpSource: true, + isFeatured: true, + openToWork: true, + linkedinHandler: true, + telegramHandler: true }); export const MemberRelationalFields = ResponseMemberWithRelationsSchema.pick({ diff --git a/libs/contracts/src/schema/participants-request.ts b/libs/contracts/src/schema/participants-request.ts index 4d998cdb9..e330780fe 100644 --- a/libs/contracts/src/schema/participants-request.ts +++ b/libs/contracts/src/schema/participants-request.ts @@ -35,8 +35,8 @@ const newDataMemberSchema = z.object({ name: z.string(), email: z.string(), plnStartDate: z.string().optional().nullable(), - teamAndRoles: z.array(teamMappingSchema).nonempty(), - skills: z.array(skillsMappingSchema).nonempty(), + teamAndRoles: z.array(teamMappingSchema).optional(), + skills: z.array(skillsMappingSchema).optional(), city: z.string().optional().nullable(), country: z.string().optional().nullable(), region: z.string().optional().nullable(), @@ -48,7 +48,13 @@ const newDataMemberSchema = z.object({ officeHours: z.string().optional().nullable(), imageUid: z.string().optional().nullable(), moreDetails: z.string().optional().nullable(), - projectContributions: z.array(ProjectContributionSchema as any).optional() + projectContributions: z.array(ProjectContributionSchema as any).optional(), + bio: z.string().nullish(), + signUpSource: z.string().nullish(), + isFeatured: z.boolean().nullish(), + locationUid: z.string().nullable(), + openToWork: z.boolean().nullable(), + isVerified: z.boolean().nullish() }); const newDataTeamSchema = z.object({ From 917afc977b150ac2f0d6a4e1fea650b3b66737d4 Mon Sep 17 00:00:00 2001 From: Navaneethakrishnan Date: Wed, 27 Nov 2024 14:43:37 +0530 Subject: [PATCH 08/41] feat: added isVerified and signup source field in member --- libs/contracts/src/schema/participants-request.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/contracts/src/schema/participants-request.ts b/libs/contracts/src/schema/participants-request.ts index e330780fe..659cf0925 100644 --- a/libs/contracts/src/schema/participants-request.ts +++ b/libs/contracts/src/schema/participants-request.ts @@ -6,7 +6,7 @@ export const statusEnum = z.enum(['PENDING', 'APPROVED', 'REJECTED']); export const participantTypeEnum = z.enum(['MEMBER', 'TEAM']); const oldDataPostSchema = z.object({}); const teamMappingSchema = z.object({ - role: z.string(), + role: z.string().optional(), teamUid: z.string(), teamTitle: z.string(), }); From 328be7e169702f7e63a477dfe3a2632337c83bfe Mon Sep 17 00:00:00 2001 From: navneethkrish Date: Thu, 28 Nov 2024 17:40:06 +0530 Subject: [PATCH 09/41] feat(Added isVerified update): added api for updated isverified for member --- .../web-api/src/members/members.controller.ts | 25 ++++--------------- apps/web-api/src/members/members.service.ts | 4 +-- libs/contracts/src/lib/contract-member.ts | 10 -------- 3 files changed, 7 insertions(+), 32 deletions(-) diff --git a/apps/web-api/src/members/members.controller.ts b/apps/web-api/src/members/members.controller.ts index c0fe3217e..0528feace 100644 --- a/apps/web-api/src/members/members.controller.ts +++ b/apps/web-api/src/members/members.controller.ts @@ -25,6 +25,7 @@ import { UserAccessTokenValidateGuard } from '../guards/user-access-token-valida import { LogService } from '../shared/log.service'; import { ParticipantsReqValidationPipe } from '../pipes/participant-request-validation.pipe'; import { IsVerifiedMemberInterceptor } from '../interceptors/verified-member.interceptor'; +import { isEmpty } from 'lodash'; const server = initNestServer(apiMembers); type RouteShape = typeof server.routeShapes; @@ -166,6 +167,9 @@ export class MemberController { ) { throw new ForbiddenException(`Member isn't authorized to update the member`); } + if(!isEmpty(participantsRequest.newData.isVerified) && !this.membersService.checkIfAdminUser(requestor)) { + throw new ForbiddenException(`Member isn't authorized to verify a member`); + } return await this.membersService.updateMemberFromParticipantsRequest(uid, participantsRequest, requestor.email); } @@ -187,7 +191,7 @@ export class MemberController { @Api(server.route.updateMember) @UseGuards(UserTokenValidation) async updateMemberByUid(@Param('uid') uid, @Body() body) { - if (body.isVerified) { + if (!isEmpty(body.isVerified)) { delete body.isVerified; } return await this.membersService.updateMemberByUid(uid, body); @@ -260,23 +264,4 @@ export class MemberController { async getGitProjects(@Param('uid') uid) { return await this.membersService.getGitProjects(uid); } - - /** - * Updates a member as verified member. - * - * @param uid - uid of the member to be updated - * @param req - HTTP request object containing user details - * @returns Updated member data with new details. - */ - @Api(server.route.updateMemberVerificationStatus) - @UseGuards(UserTokenValidation) - @UsePipes(ZodValidationPipe) - async updateMemberVerificationStatus(@Param('uid') uid, @Body() body, @Req() req) { - this.logger.info(`Member verification request - Initated by -> ${req.userEmail}`); - const member = await this.membersService.findMemberByEmail(req.userEmail); - if (!this.membersService.checkIfAdminUser(member)) { - throw new ForbiddenException(`Member with email ${req.userEmail} isn't admin to verify a member`); - } - return await this.membersService.updateMemberByUid(uid, { ...body, isVerified: true }); - } } diff --git a/apps/web-api/src/members/members.service.ts b/apps/web-api/src/members/members.service.ts index ed2ead7a4..0864a71a0 100644 --- a/apps/web-api/src/members/members.service.ts +++ b/apps/web-api/src/members/members.service.ts @@ -650,7 +650,7 @@ export class MembersService { memberUid, { ...member, - ...(isEmailChanged && isExternalIdAvailable && { externalId: null }) + ...(isEmailChanged && isExternalIdAvailable && { externalId: null }), }, tx ); @@ -708,7 +708,7 @@ export class MembersService { const directFields = [ 'name', 'email', 'githubHandler', 'discordHandler', 'bio', 'twitterHandler', 'linkedinHandler', 'telegramHandler', - 'officeHours', 'moreDetails', 'plnStartDate', 'openToWork' + 'officeHours', 'moreDetails', 'plnStartDate', 'openToWork', "isVerified" ]; copyObj(memberData, member, directFields); member.email = member.email.toLowerCase().trim(); diff --git a/libs/contracts/src/lib/contract-member.ts b/libs/contracts/src/lib/contract-member.ts index 2a389f4be..66eba60ac 100644 --- a/libs/contracts/src/lib/contract-member.ts +++ b/libs/contracts/src/lib/contract-member.ts @@ -10,16 +10,6 @@ import { getAPIVersionAsPath } from '../utils/versioned-path'; const contract = initContract(); export const apiMembers = contract.router({ - updateMemberVerificationStatus: { - method:'PUT', - path: `${getAPIVersionAsPath('1')}/members/:uid/verify`, - body: contract.body(), - responses: { - 200: contract.response(), - }, - summary: 'Verify a member', - - }, getMembers: { method: 'GET', path: `${getAPIVersionAsPath('1')}/members`, From e971ea4adf2fb653d1710b749fecf675d6f8eecb Mon Sep 17 00:00:00 2001 From: Navaneethakrishnan Date: Fri, 29 Nov 2024 14:51:27 +0530 Subject: [PATCH 10/41] feat: added isUserConsent, isSubscriberToNews field in member --- apps/web-api/prisma/fixtures/members.ts | 3 +++ .../migration.sql | 10 ++++++++-- apps/web-api/prisma/schema.prisma | 3 +++ apps/web-api/src/members/members.service.ts | 4 +++- libs/contracts/src/schema/member.ts | 5 ++++- libs/contracts/src/schema/participants-request.ts | 5 ++++- libs/contracts/src/schema/project-contribution.ts | 2 +- 7 files changed, 26 insertions(+), 6 deletions(-) rename apps/web-api/prisma/migrations/{20241127093907_member_isverified => 20241127093907_member_signup}/migration.sql (60%) diff --git a/apps/web-api/prisma/fixtures/members.ts b/apps/web-api/prisma/fixtures/members.ts index eaf012a4e..e17927105 100644 --- a/apps/web-api/prisma/fixtures/members.ts +++ b/apps/web-api/prisma/fixtures/members.ts @@ -55,6 +55,9 @@ const membersFactory = Factory.define>( locationUid: '', signUpSource: faker.company.name(), isVerified: faker.datatype.boolean(), + isUserConsent: faker.datatype.boolean(), + isSubscribedToNewsletter: faker.datatype.boolean(), + teamOrProjectURL: faker.internet.url(), openToWork: faker.datatype.boolean(), preferences: {showEmail:true,showGithubHandle:true,showTelegram:true,showLinkedin:true,showDiscord:false,showGithubProjects:false,showTwitter:true} }; diff --git a/apps/web-api/prisma/migrations/20241127093907_member_isverified/migration.sql b/apps/web-api/prisma/migrations/20241127093907_member_signup/migration.sql similarity index 60% rename from apps/web-api/prisma/migrations/20241127093907_member_isverified/migration.sql rename to apps/web-api/prisma/migrations/20241127093907_member_signup/migration.sql index 056b75254..b6ee98657 100644 --- a/apps/web-api/prisma/migrations/20241127093907_member_isverified/migration.sql +++ b/apps/web-api/prisma/migrations/20241127093907_member_signup/migration.sql @@ -5,9 +5,15 @@ */ -- AlterTable -ALTER TABLE "Member" ADD COLUMN "isVerified" BOOLEAN DEFAULT false, -ADD COLUMN "signUpSource" TEXT, +ALTER TABLE "Member" +ADD COLUMN "isVerified" BOOLEAN DEFAULT false, +ADD COLUMN "signUpSource" TEXT, +ADD COLUMN "isSubscribedToNewsletter" BOOLEAN DEFAULT false, +ADD COLUMN "isUserConsent" BOOLEAN DEFAULT false, +ADD COLUMN "teamOrProjectURL" TEXT; ALTER COLUMN "plnFriend" DROP NOT NULL; -- CreateIndex CREATE UNIQUE INDEX "PLEventGuest_memberUid_teamUid_eventUid_key" ON "PLEventGuest"("memberUid", "teamUid", "eventUid"); + + diff --git a/apps/web-api/prisma/schema.prisma b/apps/web-api/prisma/schema.prisma index ae8c05615..c1222b177 100644 --- a/apps/web-api/prisma/schema.prisma +++ b/apps/web-api/prisma/schema.prisma @@ -74,6 +74,9 @@ model Member { isFeatured Boolean? @default(false) isVerified Boolean? @default(false) signUpSource String? + isUserConsent Boolean? @default(false) + isSubscribedToNewsletter Boolean? @default(false) + teamOrProjectURL String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt approvedAt DateTime @default(now()) diff --git a/apps/web-api/src/members/members.service.ts b/apps/web-api/src/members/members.service.ts index 0864a71a0..5474a35e3 100644 --- a/apps/web-api/src/members/members.service.ts +++ b/apps/web-api/src/members/members.service.ts @@ -708,7 +708,9 @@ export class MembersService { const directFields = [ 'name', 'email', 'githubHandler', 'discordHandler', 'bio', 'twitterHandler', 'linkedinHandler', 'telegramHandler', - 'officeHours', 'moreDetails', 'plnStartDate', 'openToWork', "isVerified" + 'officeHours', 'moreDetails', 'plnStartDate', 'openToWork', + 'isVerified', 'signUpSource', 'isUserConsent', 'isSubscribedToNewsletter', + 'teamOrProjectURL' ]; copyObj(memberData, member, directFields); member.email = member.email.toLowerCase().trim(); diff --git a/libs/contracts/src/schema/member.ts b/libs/contracts/src/schema/member.ts index 2162ca626..35f78fab4 100644 --- a/libs/contracts/src/schema/member.ts +++ b/libs/contracts/src/schema/member.ts @@ -50,7 +50,10 @@ export const MemberSchema = z.object({ repositories: GitHubRepositorySchema.array().optional(), preferences: PreferenceSchema.optional(), projectContributions: z.array(ProjectContributionSchema).optional(), - isVerified:z.boolean().nullish() + isVerified:z.boolean().nullish(), + isUserConsent: z.boolean().nullish(), + isSubscribedToNewsletter: z.boolean().nullish(), + teamOrProjectURL: z.string().nullish() }); diff --git a/libs/contracts/src/schema/participants-request.ts b/libs/contracts/src/schema/participants-request.ts index 659cf0925..625f95cd7 100644 --- a/libs/contracts/src/schema/participants-request.ts +++ b/libs/contracts/src/schema/participants-request.ts @@ -54,7 +54,10 @@ const newDataMemberSchema = z.object({ isFeatured: z.boolean().nullish(), locationUid: z.string().nullable(), openToWork: z.boolean().nullable(), - isVerified: z.boolean().nullish() + isVerified: z.boolean().nullish(), + isUserConsent: z.boolean().nullish(), + isSubscribedToNewsletter: z.boolean().nullish(), + teamOrProjectURL: z.string().nullish() }); const newDataTeamSchema = z.object({ diff --git a/libs/contracts/src/schema/project-contribution.ts b/libs/contracts/src/schema/project-contribution.ts index 0b1335565..eb74c1c29 100644 --- a/libs/contracts/src/schema/project-contribution.ts +++ b/libs/contracts/src/schema/project-contribution.ts @@ -3,7 +3,7 @@ import { compareDateWithoutTime, compareMonthYear } from '../../src/utils/date-u const ProjectContribution = z.object({ role: z.string(), - currentProject: z.boolean(), + currentProject: z.boolean().optional(), startDate: z.string().nullish(), endDate: z.string().optional(), description: z.string().optional().nullish(), From 0088c08a86df75ae57035013d2d9580ff3a5cde7 Mon Sep 17 00:00:00 2001 From: navneethkrish Date: Fri, 29 Nov 2024 17:06:07 +0530 Subject: [PATCH 11/41] feat(entities search): added api for searching the teams and projects --- apps/web-api/src/home/home.controller.ts | 38 +++++++++---- apps/web-api/src/home/home.service.ts | 72 +++++++++++++++++++++++- libs/contracts/src/lib/contract-home.ts | 9 +++ 3 files changed, 105 insertions(+), 14 deletions(-) diff --git a/apps/web-api/src/home/home.controller.ts b/apps/web-api/src/home/home.controller.ts index 30c352def..ecd0872a0 100644 --- a/apps/web-api/src/home/home.controller.ts +++ b/apps/web-api/src/home/home.controller.ts @@ -6,15 +6,17 @@ import { ApiQueryFromZod } from '../decorators/api-query-from-zod'; import { ApiOkResponseFromZod } from '../decorators/api-response-from-zod'; import { apiHome } from 'libs/contracts/src/lib/contract-home'; import { HomeService } from './home.service'; -import { +import { DiscoveryQuestionQueryParams, ResponseDiscoveryQuestionSchemaWithRelations, ResponseDiscoveryQuestionSchema, CreateDiscoveryQuestionSchemaDto, - UpdateDiscoveryQuestionSchemaDto + UpdateDiscoveryQuestionSchemaDto, + TeamQueryParams, + MemberQueryParams } from 'libs/contracts/src/schema'; import { UserTokenValidation } from '../guards/user-token-validation.guard'; -import { MembersService } from '../members/members.service'; +import { MembersService } from '../members/members.service'; import { NoCache } from '../decorators/no-cache.decorator'; import { PrismaQueryBuilder } from '../utils/prisma-query-builder'; import { prismaQueryableFieldsFromZod } from '../utils/prisma-queryable-fields-from-zod'; @@ -28,15 +30,15 @@ export class HomeController { constructor( private homeService: HomeService, private memberService: MembersService, - private huskyService: HuskyService - ) {} - + private huskyService: HuskyService + ) { } + @Api(server.route.getAllFeaturedData) async getAllFeaturedData() { return await this.homeService.fetchAllFeaturedData(); } - @Api(server.route.getAllDiscoveryQuestions) + @Api(server.route.getAllDiscoveryQuestions) @ApiQueryFromZod(DiscoveryQuestionQueryParams) @ApiOkResponseFromZod(ResponseDiscoveryQuestionSchemaWithRelations.array()) @NoCache() @@ -50,12 +52,11 @@ export class HomeController { } - @Api(server.route.getDiscoveryQuestion) + @Api(server.route.getDiscoveryQuestion) @ApiQueryFromZod(DiscoveryQuestionQueryParams) @ApiOkResponseFromZod(ResponseDiscoveryQuestionSchemaWithRelations) @NoCache() - async getDiscoveryQuestion(@Param('slug') slug: string) - { + async getDiscoveryQuestion(@Param('slug') slug: string) { return await this.huskyService.fetchDiscoverQuestionBySlug(slug); } @@ -99,7 +100,7 @@ export class HomeController { ) { const attribute = body.attribute; switch (attribute) { - case "shareCount": + case "shareCount": return this.huskyService.updateDiscoveryQuestionShareCount(slug); case "viewCount": return this.huskyService.updateDiscoveryQuestionViewCount(slug); @@ -107,4 +108,19 @@ export class HomeController { throw new BadRequestException(`Invalid attribute: ${attribute}`); } } + + /** + * Retrieves a list of teams and projects based on search query. + * + * @param request - HTTP request object containing query parameters + * @returns Array of projects and teams. + */ + @Api(server.route.getTeamsAndProjects) + @ApiQueryFromZod(TeamQueryParams) + @ApiQueryFromZod(MemberQueryParams) + @NoCache() + async getTeamsAndProjects(@Req() request: Request) { + const queryParams = request.query; + return this.homeService.fetchTeamsAndProjects(queryParams); + } } diff --git a/apps/web-api/src/home/home.service.ts b/apps/web-api/src/home/home.service.ts index 8954b2ba8..c2f9bd1ae 100644 --- a/apps/web-api/src/home/home.service.ts +++ b/apps/web-api/src/home/home.service.ts @@ -1,4 +1,4 @@ -import { +import { Injectable, InternalServerErrorException } from '@nestjs/common'; @@ -6,7 +6,6 @@ import { MembersService } from '../members/members.service'; import { TeamsService } from '../teams/teams.service'; import { PLEventsService } from '../pl-events/pl-events.service'; import { ProjectsService } from '../projects/projects.service'; - @Injectable() export class HomeService { constructor( @@ -14,7 +13,7 @@ export class HomeService { private teamsService: TeamsService, private plEventsService: PLEventsService, private projectsService: ProjectsService - ) {} + ) { } async fetchAllFeaturedData() { try { @@ -45,4 +44,71 @@ export class HomeService { throw new InternalServerErrorException(`Error occured while retrieving featured data: ${error.message}`); } } + + /** + * Retrieves a list of teams and projects based on search term. + * Builds a Prisma query from the queryable fields and adds filters for team and project name. + * + * @param request - HTTP request object containing query parameters + * @returns Array of projects and teams. + */ + async fetchTeamsAndProjects(queryParams) { + let result: any[] = [] + const entities: string[] = queryParams.include?.split(","); + if (entities.includes('teams')) { + const resultantTeams = await this.fetchTeams(queryParams.name_icontains); + resultantTeams.teams.forEach((team) => result.push({ ...team, category: "TEAM" })); + } + if (entities.includes('projects')) { + const resultantProjects = await this.fetchProjects(queryParams.name_icontains); + resultantProjects?.projects.forEach((project) => result.push({ ...project, category: "PROJECT" })); + } + return [...result].sort((team, project) => team.name.localeCompare(project.name)) + } + + private fetchTeams(name_icontains) { + return this.teamsService.findAll({ + where: { + name: { + startsWith: name_icontains, + mode: 'insensitive' + } + }, + select: { + uid: true, + name: true, + logo: { + select: { + url: true, + } + } + }, + orderBy: { + name: 'asc' + } + }) + } + + private fetchProjects(name_icontains) { + return this.projectsService.getProjects({ + where: { + name: { + startsWith: name_icontains, + mode: 'insensitive' + } + }, + select: { + uid: true, + name: true, + logo: { + select: { + url: true, + } + } + }, + orderBy: { + name: 'asc' + } + }) + } } diff --git a/libs/contracts/src/lib/contract-home.ts b/libs/contracts/src/lib/contract-home.ts index 8b3b25f4e..09a322c46 100644 --- a/libs/contracts/src/lib/contract-home.ts +++ b/libs/contracts/src/lib/contract-home.ts @@ -7,6 +7,15 @@ import { const contract = initContract(); export const apiHome = contract.router({ + getTeamsAndProjects: { + method: 'GET', + path: `${getAPIVersionAsPath('1')}/home/entities`, + query: contract.query, + responses: { + 200: contract.response() + }, + summary: 'Get all featured members, projects, teams and events' + }, getAllFeaturedData: { method: 'GET', path: `${getAPIVersionAsPath('1')}/home/featured`, From 7f8e125b9ae24a3662d0d54ff8b65474a4da9b29 Mon Sep 17 00:00:00 2001 From: navneethkrish Date: Fri, 29 Nov 2024 17:30:52 +0530 Subject: [PATCH 12/41] fix(entities search): modifies method names --- apps/web-api/src/home/home.service.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/web-api/src/home/home.service.ts b/apps/web-api/src/home/home.service.ts index c2f9bd1ae..7600f4851 100644 --- a/apps/web-api/src/home/home.service.ts +++ b/apps/web-api/src/home/home.service.ts @@ -56,21 +56,21 @@ export class HomeService { let result: any[] = [] const entities: string[] = queryParams.include?.split(","); if (entities.includes('teams')) { - const resultantTeams = await this.fetchTeams(queryParams.name_icontains); + const resultantTeams = await this.fetchTeamsBySearchTerm(queryParams.name); resultantTeams.teams.forEach((team) => result.push({ ...team, category: "TEAM" })); } if (entities.includes('projects')) { - const resultantProjects = await this.fetchProjects(queryParams.name_icontains); + const resultantProjects = await this.fetchProjectsBySearchTerm(queryParams.name); resultantProjects?.projects.forEach((project) => result.push({ ...project, category: "PROJECT" })); } return [...result].sort((team, project) => team.name.localeCompare(project.name)) } - private fetchTeams(name_icontains) { + private fetchTeamsBySearchTerm(name) { return this.teamsService.findAll({ where: { name: { - startsWith: name_icontains, + startsWith: name, mode: 'insensitive' } }, @@ -89,11 +89,11 @@ export class HomeService { }) } - private fetchProjects(name_icontains) { + private fetchProjectsBySearchTerm(name) { return this.projectsService.getProjects({ where: { name: { - startsWith: name_icontains, + startsWith: name, mode: 'insensitive' } }, From 4ad3e31231455a68322debaf36cbd82fdbc4f022 Mon Sep 17 00:00:00 2001 From: navneethkrish Date: Sat, 30 Nov 2024 18:38:49 +0530 Subject: [PATCH 13/41] feat(bulk approval): added api for processing multiple participant request --- .../admin/participants-request.controller.ts | 44 +++++++++++++++++-- .../src/schema/participants-request.ts | 14 ++++-- 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/apps/web-api/src/admin/participants-request.controller.ts b/apps/web-api/src/admin/participants-request.controller.ts index 1a0a1d027..7af686da1 100644 --- a/apps/web-api/src/admin/participants-request.controller.ts +++ b/apps/web-api/src/admin/participants-request.controller.ts @@ -15,7 +15,7 @@ import { NoCache } from '../decorators/no-cache.decorator'; import { ParticipantsRequestService } from '../participants-request/participants-request.service'; import { AdminAuthGuard } from '../guards/admin-auth.guard'; import { ParticipantsReqValidationPipe } from '../pipes/participant-request-validation.pipe'; -import { ProcessParticipantReqDto } from 'libs/contracts/src/schema'; +import { ProcessBulkParticipantRequest, ProcessParticipantReqDto } from 'libs/contracts/src/schema'; import { ApprovalStatus, ParticipantsRequest, ParticipantType } from '@prisma/client'; @Controller('v1/admin/participants-request') @@ -23,7 +23,45 @@ import { ApprovalStatus, ParticipantsRequest, ParticipantType } from '@prisma/cl export class AdminParticipantsRequestController { constructor( private readonly participantsRequestService: ParticipantsRequestService - ) {} + ) { } + + /** + * Process (approve/reject) multiple pending participants requests. + * @param body - The request body containing array of uids and status of participants to be processed; + * @returns The result of processing the participants request + */ + @Patch('/') + async processBulkRequest( + @Body() body: ProcessBulkParticipantRequest[] + ): Promise { + let result: any[] = []; + for (const request of body) { + const participantRequest: ParticipantsRequest | any = await this.participantsRequestService.findOneByUid(request.uid); + if (!participantRequest) { + result.push({ + uid: request.uid, + error: 'Request not found' + }); + continue; + } + if (participantRequest?.status !== ApprovalStatus.PENDING) { + result.push({ + uid: request.uid, + error: '`Request cannot be processed. It has already been ${participantRequest?.status.toLowerCase())`' + }); + continue; + } + if (participantRequest?.participantType === ParticipantType.TEAM && !participantRequest.requesterEmailId) { + result.push({ + uid: request.uid, + error: 'Requester email is required for team participation requests. Please provide a valid email address.' + }); + continue; + } + const requestStatus = await this.participantsRequestService.processRequestByUid(request.uid, participantRequest, request.status); + } + return result; + } /** * Retrieve all participants requests based on query parameters. @@ -89,5 +127,5 @@ export class AdminParticipantsRequestController { } return await this.participantsRequestService.processRequestByUid(uid, participantRequest, body.status); } + } - \ No newline at end of file diff --git a/libs/contracts/src/schema/participants-request.ts b/libs/contracts/src/schema/participants-request.ts index 625f95cd7..be538bd36 100644 --- a/libs/contracts/src/schema/participants-request.ts +++ b/libs/contracts/src/schema/participants-request.ts @@ -48,14 +48,14 @@ const newDataMemberSchema = z.object({ officeHours: z.string().optional().nullable(), imageUid: z.string().optional().nullable(), moreDetails: z.string().optional().nullable(), - projectContributions: z.array(ProjectContributionSchema as any).optional(), + projectContributions: z.array(ProjectContributionSchema as any).optional(), bio: z.string().nullish(), signUpSource: z.string().nullish(), isFeatured: z.boolean().nullish(), locationUid: z.string().nullable(), openToWork: z.boolean().nullable(), isVerified: z.boolean().nullish(), - isUserConsent: z.boolean().nullish(), + isUserConsent: z.boolean().nullish(), isSubscribedToNewsletter: z.boolean().nullish(), teamOrProjectURL: z.string().nullish() }); @@ -112,7 +112,13 @@ export const FindUniqueIdentiferSchema = z.object({ }) const ProcessParticipantRequest = z.object({ - status: statusEnum, + status: statusEnum, +}) +const ProcessBulkRequest = z.object({ + uid: z.string(), + status: statusEnum, + participantType: participantTypeEnum }) -export class ProcessParticipantReqDto extends createZodDto(ProcessParticipantRequest) {} +export class ProcessBulkParticipantRequest extends createZodDto(ProcessBulkRequest) { } +export class ProcessParticipantReqDto extends createZodDto(ProcessParticipantRequest) { } export class FindUniqueIdentiferDto extends createZodDto(FindUniqueIdentiferSchema) { } From a41663d24d0560bd2608a92b665c629cbc68a4f6 Mon Sep 17 00:00:00 2001 From: navneethkrish Date: Sat, 30 Nov 2024 18:47:05 +0530 Subject: [PATCH 14/41] fix(changed http method): changed patch to post method --- apps/web-api/src/admin/participants-request.controller.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/web-api/src/admin/participants-request.controller.ts b/apps/web-api/src/admin/participants-request.controller.ts index 7af686da1..622383efd 100644 --- a/apps/web-api/src/admin/participants-request.controller.ts +++ b/apps/web-api/src/admin/participants-request.controller.ts @@ -9,7 +9,8 @@ import { UseGuards, UsePipes, BadRequestException, - NotFoundException + NotFoundException, + Post } from '@nestjs/common'; import { NoCache } from '../decorators/no-cache.decorator'; import { ParticipantsRequestService } from '../participants-request/participants-request.service'; @@ -30,7 +31,7 @@ export class AdminParticipantsRequestController { * @param body - The request body containing array of uids and status of participants to be processed; * @returns The result of processing the participants request */ - @Patch('/') + @Post('/') async processBulkRequest( @Body() body: ProcessBulkParticipantRequest[] ): Promise { From c0fe8cfceba57ff2619c67c6b65e5c3bb2156ea1 Mon Sep 17 00:00:00 2001 From: navneethkrish Date: Sun, 1 Dec 2024 17:23:12 +0530 Subject: [PATCH 15/41] fix(removed isverified condition): removed isverifed interceptor fofr findone api --- apps/web-api/src/members/members.controller.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/web-api/src/members/members.controller.ts b/apps/web-api/src/members/members.controller.ts index 0528feace..ceece26e9 100644 --- a/apps/web-api/src/members/members.controller.ts +++ b/apps/web-api/src/members/members.controller.ts @@ -135,7 +135,6 @@ export class MemberController { @ApiNotFoundResponse(NOT_FOUND_GLOBAL_RESPONSE_SCHEMA) @ApiOkResponseFromZod(ResponseMemberWithRelationsSchema) @ApiQueryFromZod(MemberDetailQueryParams) - @UseInterceptors(IsVerifiedMemberInterceptor) @NoCache() async findOne(@Req() request: Request, @ApiDecorator() { params: { uid } }: RouteShape['getMember']) { const queryableFields = prismaQueryableFieldsFromZod(ResponseMemberWithRelationsSchema); From ef6d651a0d912943b5291ac37d5b5609872a1345 Mon Sep 17 00:00:00 2001 From: navneethkrish Date: Mon, 2 Dec 2024 11:37:08 +0530 Subject: [PATCH 16/41] fix(bulk approval): modified method to be asynchronous --- .../admin/participants-request.controller.ts | 75 ++++++++++++------- .../src/schema/participants-request.ts | 4 +- 2 files changed, 51 insertions(+), 28 deletions(-) diff --git a/apps/web-api/src/admin/participants-request.controller.ts b/apps/web-api/src/admin/participants-request.controller.ts index 622383efd..f235f0579 100644 --- a/apps/web-api/src/admin/participants-request.controller.ts +++ b/apps/web-api/src/admin/participants-request.controller.ts @@ -35,33 +35,54 @@ export class AdminParticipantsRequestController { async processBulkRequest( @Body() body: ProcessBulkParticipantRequest[] ): Promise { - let result: any[] = []; - for (const request of body) { - const participantRequest: ParticipantsRequest | any = await this.participantsRequestService.findOneByUid(request.uid); - if (!participantRequest) { - result.push({ - uid: request.uid, - error: 'Request not found' - }); - continue; - } - if (participantRequest?.status !== ApprovalStatus.PENDING) { - result.push({ - uid: request.uid, - error: '`Request cannot be processed. It has already been ${participantRequest?.status.toLowerCase())`' - }); - continue; - } - if (participantRequest?.participantType === ParticipantType.TEAM && !participantRequest.requesterEmailId) { - result.push({ - uid: request.uid, - error: 'Requester email is required for team participation requests. Please provide a valid email address.' - }); - continue; - } - const requestStatus = await this.participantsRequestService.processRequestByUid(request.uid, participantRequest, request.status); - } - return result; + let successCount = 0; + const results = await Promise.all( + body.map(async (request) => { + try { + const participantRequest: ParticipantsRequest | null = + await this.participantsRequestService.findOneByUid(request.uid); + + if (!participantRequest) { + return { + uid: request.uid, + message: 'Request not found', + }; + } + + if (participantRequest.status !== ApprovalStatus.PENDING) { + return { + uid: request.uid, + message: `Request cannot be processed. It has already been ${participantRequest.status.toLowerCase()}.`, + }; + } + + if ( + participantRequest.participantType === ParticipantType.TEAM && + !participantRequest.requesterEmailId + ) { + return { + uid: request.uid, + message: 'Requester email is required for team participation requests. Please provide a valid email address.', + }; + } + + await this.participantsRequestService.processRequestByUid( + request.uid, + participantRequest, + request.status + ); + successCount++; + return { uid: request.uid, message: 'Processed successfully' }; + } catch (error) { + return { + uid: request.uid, + message: 'An error occurred while processing the request', + }; + } + }) + ); + + return { count: successCount, results }; } /** diff --git a/libs/contracts/src/schema/participants-request.ts b/libs/contracts/src/schema/participants-request.ts index be538bd36..a4fc7ce2d 100644 --- a/libs/contracts/src/schema/participants-request.ts +++ b/libs/contracts/src/schema/participants-request.ts @@ -113,11 +113,13 @@ export const FindUniqueIdentiferSchema = z.object({ const ProcessParticipantRequest = z.object({ status: statusEnum, + isVerified: z.boolean() }) const ProcessBulkRequest = z.object({ uid: z.string(), status: statusEnum, - participantType: participantTypeEnum + participantType: participantTypeEnum, + isVerified: z.boolean() }) export class ProcessBulkParticipantRequest extends createZodDto(ProcessBulkRequest) { } export class ProcessParticipantReqDto extends createZodDto(ProcessParticipantRequest) { } From 009438c286a54d01e34dc70704b7dcc37e6daaf9 Mon Sep 17 00:00:00 2001 From: Navaneethakrishnan Date: Mon, 2 Dec 2024 11:40:06 +0530 Subject: [PATCH 17/41] fix: fixed issue in member preference --- apps/web-api/prisma/fixtures/members.ts | 11 ++++- .../web-api/src/members/members.controller.ts | 26 +++++++++-- apps/web-api/src/members/members.service.ts | 43 +++++++++++++++++++ libs/contracts/src/lib/contract-member.ts | 9 ++++ libs/contracts/src/schema/member.ts | 15 +++++-- .../src/schema/participants-request.ts | 8 ++-- .../src/schema/project-contribution.ts | 18 +++----- 7 files changed, 105 insertions(+), 25 deletions(-) diff --git a/apps/web-api/prisma/fixtures/members.ts b/apps/web-api/prisma/fixtures/members.ts index e17927105..0f5b1855d 100644 --- a/apps/web-api/prisma/fixtures/members.ts +++ b/apps/web-api/prisma/fixtures/members.ts @@ -59,7 +59,16 @@ const membersFactory = Factory.define>( isSubscribedToNewsletter: faker.datatype.boolean(), teamOrProjectURL: faker.internet.url(), openToWork: faker.datatype.boolean(), - preferences: {showEmail:true,showGithubHandle:true,showTelegram:true,showLinkedin:true,showDiscord:false,showGithubProjects:false,showTwitter:true} + preferences: { + showEmail:true, + showGithubHandle:true, + showTelegram:true, + showLinkedin:true, + showDiscord:false, + showGithubProjects:false, + showTwitter:true, + showSubscription:true + } }; } ); diff --git a/apps/web-api/src/members/members.controller.ts b/apps/web-api/src/members/members.controller.ts index ceece26e9..35ac98113 100644 --- a/apps/web-api/src/members/members.controller.ts +++ b/apps/web-api/src/members/members.controller.ts @@ -172,6 +172,18 @@ export class MemberController { return await this.membersService.updateMemberFromParticipantsRequest(uid, participantsRequest, requestor.email); } + @Api(server.route.verifyMembers) + @UseGuards(UserAccessTokenValidateGuard) + async verifyMembers(@Body() body, @Req() request) { + const requestor = await this.membersService.findMemberByEmail(request.userEmail); + if(!this.membersService.checkIfAdminUser(requestor)) { + throw new ForbiddenException(`Member isn't authorized to verify members`); + } + const { memberIds } = body; + return await this.membersService.verifyMembers(memberIds, requestor.email); + + } + /** * Updates a member's preference settings. * @@ -189,9 +201,17 @@ export class MemberController { @Api(server.route.updateMember) @UseGuards(UserTokenValidation) - async updateMemberByUid(@Param('uid') uid, @Body() body) { - if (!isEmpty(body.isVerified)) { - delete body.isVerified; + async updateMemberByUid(@Param('uid') uid, @Body() body, @Req() req) { + this.logger.info(`Member update request - Initated by -> ${req.userEmail}`); + const requestor = await this.membersService.findMemberByEmail(req.userEmail); + if ( + !requestor.isDirectoryAdmin && + uid !== requestor.uid + ) { + throw new ForbiddenException(`Member isn't authorized to update the member`); + } + if(!isEmpty(body.isVerified) && !this.membersService.checkIfAdminUser(requestor)) { + throw new ForbiddenException(`Member isn't authorized to verify a member`); } return await this.membersService.updateMemberByUid(uid, body); } diff --git a/apps/web-api/src/members/members.service.ts b/apps/web-api/src/members/members.service.ts index 5474a35e3..b7e70fcee 100644 --- a/apps/web-api/src/members/members.service.ts +++ b/apps/web-api/src/members/members.service.ts @@ -1114,6 +1114,47 @@ export class MembersService { ); } + /** + * Verify the list of members and log into participant request. + * @param memberIds array of member IDs + * @param userEmail logged in member email + * @returns result + */ + async verifyMembers(memberIds: string[], userEmail:string): Promise { + return await this.prisma.$transaction(async (tx) => { + const result = await tx.member.updateMany({ + where: { uid: { in: memberIds } }, + data: { + isVerified: true + } + }); + if (result.count !== memberIds.length) { + throw new NotFoundException('One or more member IDs are invalid.'); + } + const members = await tx.member.findMany({ + where: { uid: { in: memberIds } } + }) + await Promise.all(members.map(async(member) => { + await this.participantsRequestService.add({ + status: 'AUTOAPPROVED', + requesterEmailId: userEmail, + referenceUid: member.uid, + uniqueIdentifier: member?.email || '', + participantType: 'MEMBER', + oldData: { + isVerified: false + }, + newData: { + isVerified: true + }, + }, + tx + ); + })); + return result; + }); + } + /** * Updates the member's preferences and resets the cache. * @@ -1153,6 +1194,7 @@ export class MembersService { linkedinHandler: true, twitterHandler: true, preferences: true, + isSubscribedToNewsletter: true }, }); return this.buildPreferenceResponse(member); @@ -1177,6 +1219,7 @@ export class MembersService { preferences.discord = !!member.discordHandler; preferences.linkedin = !!member.linkedinHandler; preferences.twitter = !!member.twitterHandler; + preferences.subscription = !!member.isSubscribedToNewsletter; return preferences; } diff --git a/libs/contracts/src/lib/contract-member.ts b/libs/contracts/src/lib/contract-member.ts index 66eba60ac..a656ad0b7 100644 --- a/libs/contracts/src/lib/contract-member.ts +++ b/libs/contracts/src/lib/contract-member.ts @@ -81,6 +81,15 @@ export const apiMembers = contract.router({ }, summary: 'Modify a member', }, + verifyMembers: { + method: 'POST', + path: `${getAPIVersionAsPath('1')}/members`, + body: contract.body(), + responses: { + 200: contract.response(), + }, + summary: 'Verify members', + }, sendOtpForEmailChange: { method: 'POST', path: `${getAPIVersionAsPath('1')}/members/:uid/email/otp`, diff --git a/libs/contracts/src/schema/member.ts b/libs/contracts/src/schema/member.ts index 35f78fab4..1ccef0acb 100644 --- a/libs/contracts/src/schema/member.ts +++ b/libs/contracts/src/schema/member.ts @@ -22,7 +22,8 @@ export const PreferenceSchema = z.object({ showLinkedin:z.boolean(), showDiscord:z.boolean(), showGithubProjects:z.boolean(), - showTwitter:z.boolean() + showTwitter:z.boolean(), + showSubscription:z.boolean() }); export const MemberSchema = z.object({ @@ -38,14 +39,14 @@ export const MemberSchema = z.object({ telegramHandler: z.string().nullish(), officeHours: z.string().nullish(), airtableRecId: z.string().nullish(), - plnFriend: z.boolean().nullable(), + plnFriend: z.boolean().nullish(), bio: z.string().nullish(), signUpSource: z.string().nullish(), isFeatured: z.boolean().nullish(), createdAt: z.string(), updatedAt: z.string(), locationUid: z.string().nullable(), - openToWork: z.boolean().nullable(), + openToWork: z.boolean().nullish(), linkedinHandler: z.string().nullish(), repositories: GitHubRepositorySchema.array().optional(), preferences: PreferenceSchema.optional(), @@ -84,7 +85,13 @@ export const CreateMemberSchema = MemberSchema.pick({ isFeatured: true, openToWork: true, linkedinHandler: true, - telegramHandler: true + telegramHandler: true, + isVerified: true, + isUserConsent: true, + isSubscribedToNewsletter: true, + teamOrProjectURL: true, + preferences: true, + projectContributions: true }); export const MemberRelationalFields = ResponseMemberWithRelationsSchema.pick({ diff --git a/libs/contracts/src/schema/participants-request.ts b/libs/contracts/src/schema/participants-request.ts index a4fc7ce2d..31f34095d 100644 --- a/libs/contracts/src/schema/participants-request.ts +++ b/libs/contracts/src/schema/participants-request.ts @@ -46,14 +46,14 @@ const newDataMemberSchema = z.object({ linkedinHandler: z.string().optional().nullable(), telegramHandler: z.string().optional().nullable(), officeHours: z.string().optional().nullable(), - imageUid: z.string().optional().nullable(), - moreDetails: z.string().optional().nullable(), + imageUid: z.string().optional().nullish(), + moreDetails: z.string().optional().nullish(), projectContributions: z.array(ProjectContributionSchema as any).optional(), bio: z.string().nullish(), signUpSource: z.string().nullish(), isFeatured: z.boolean().nullish(), - locationUid: z.string().nullable(), - openToWork: z.boolean().nullable(), + locationUid: z.string().nullish(), + openToWork: z.boolean().nullish(), isVerified: z.boolean().nullish(), isUserConsent: z.boolean().nullish(), isSubscribedToNewsletter: z.boolean().nullish(), diff --git a/libs/contracts/src/schema/project-contribution.ts b/libs/contracts/src/schema/project-contribution.ts index eb74c1c29..5be0fd3a5 100644 --- a/libs/contracts/src/schema/project-contribution.ts +++ b/libs/contracts/src/schema/project-contribution.ts @@ -2,24 +2,16 @@ import { z } from 'zod'; import { compareDateWithoutTime, compareMonthYear } from '../../src/utils/date-utils'; const ProjectContribution = z.object({ - role: z.string(), - currentProject: z.boolean().optional(), + role: z.string().nullish(), + currentProject: z.boolean().nullish(), startDate: z.string().nullish(), - endDate: z.string().optional(), - description: z.string().optional().nullish(), + endDate: z.string().nullish(), + description: z.string().nullish(), projectUid: z.string(), - uid: z.string().optional() + uid: z.string().nullish() }); export const ProjectContributionSchema = ProjectContribution.superRefine((data, ctx) => { - if (!data.currentProject && !data.endDate) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: 'End date should not be null for past contribution', - fatal: true, - }); - } - if (data.startDate && data.endDate && compareDateWithoutTime(data.startDate, data.endDate) >= 0) { ctx.addIssue({ code: z.ZodIssueCode.custom, From f5fc1deb43929a5c876a1c21f564f98428d005f5 Mon Sep 17 00:00:00 2001 From: Vellaiyan-Marimuthu Date: Mon, 2 Dec 2024 20:01:59 +0530 Subject: [PATCH 18/41] Feat: Backoffice revamp --- apps/back-office/components/common/loader.tsx | 2 +- .../components/member-request-list.tsx | 52 +++ .../components/member-table/member-table.tsx | 397 ++++++++++++++++++ apps/back-office/components/request-list.tsx | 83 +--- apps/back-office/components/tab/tab.tsx | 15 + .../components/team-request-list.tsx | 71 ++++ apps/back-office/layout/approval-layout.tsx | 4 +- apps/back-office/pages/pending-list.tsx | 58 ++- .../public/assets/images/delete-disabled.svg | 3 + .../public/assets/images/delete.svg | 3 + .../public/assets/images/sort-unselected.svg | 5 + .../assets/images/unverified-disabled.svg | 3 + .../public/assets/images/unverified.svg | 3 + .../assets/images/verified-disabled.svg | 3 + .../public/assets/images/verified.svg | 3 + apps/back-office/utils/constants.ts | 4 + 16 files changed, 609 insertions(+), 100 deletions(-) create mode 100644 apps/back-office/components/member-request-list.tsx create mode 100644 apps/back-office/components/member-table/member-table.tsx create mode 100644 apps/back-office/components/tab/tab.tsx create mode 100644 apps/back-office/components/team-request-list.tsx create mode 100644 apps/back-office/public/assets/images/delete-disabled.svg create mode 100644 apps/back-office/public/assets/images/delete.svg create mode 100644 apps/back-office/public/assets/images/sort-unselected.svg create mode 100644 apps/back-office/public/assets/images/unverified-disabled.svg create mode 100644 apps/back-office/public/assets/images/unverified.svg create mode 100644 apps/back-office/public/assets/images/verified-disabled.svg create mode 100644 apps/back-office/public/assets/images/verified.svg diff --git a/apps/back-office/components/common/loader.tsx b/apps/back-office/components/common/loader.tsx index 2762e28df..8b14bc147 100644 --- a/apps/back-office/components/common/loader.tsx +++ b/apps/back-office/components/common/loader.tsx @@ -4,7 +4,7 @@ import APP_CONSTANTS from '../../utils/constants'; export default function Loader() { return ( //
-
+
{/* */}
diff --git a/apps/back-office/components/member-request-list.tsx b/apps/back-office/components/member-request-list.tsx new file mode 100644 index 000000000..e2f868d25 --- /dev/null +++ b/apps/back-office/components/member-request-list.tsx @@ -0,0 +1,52 @@ +import { Fragment, useState } from 'react'; +import APP_CONSTANTS from '../utils/constants'; +import MemberTable from './member-table/member-table'; +import Tab from './tab/tab'; + +const MemberRequestList = (props: any) => { + const dataList = props?.members; + const [allMembers, setAllMembers]: any = useState(() => getFilterMembes(dataList ?? [])); + const [currentTab, setCurrentTab] = useState(APP_CONSTANTS.PENDING_FLAG); + + function getFilterMembes(data: any) { + const pending = data.filter((item: any) => item.status === APP_CONSTANTS.PENDING_LABEL); + const unverified = data.filter((item: any) => item.isVerified === false ); + return { + pending, + unverified, + }; + } + + const availableTabs = [ + { label: APP_CONSTANTS.PENDING_LABEL, name: APP_CONSTANTS.PENDING_FLAG }, + { + label: APP_CONSTANTS.UNVERIFIED_LABEL, + name: APP_CONSTANTS.UNVERIFIED_FLAG, + }, + ]; + + const onTabSelected = (name: string) => { + setCurrentTab(name); + }; + + const onTabClickHandler = (name: string) => { + onTabSelected(name); + } + + return ( +
+
+ {availableTabs.map((tab: any, index: number) => ( + + + + ))} +
+
+ +
+
+ ); +}; + +export default MemberRequestList; diff --git a/apps/back-office/components/member-table/member-table.tsx b/apps/back-office/components/member-table/member-table.tsx new file mode 100644 index 000000000..b991c1bef --- /dev/null +++ b/apps/back-office/components/member-table/member-table.tsx @@ -0,0 +1,397 @@ +import api from 'apps/back-office/utils/api'; +import APP_CONSTANTS, { API_ROUTE, ENROLLMENT_TYPE, ROUTE_CONSTANTS } from 'apps/back-office/utils/constants'; +import router from 'next/router'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; +import Loader from '../common/loader'; +import { useNavbarContext } from 'apps/back-office/context/navbar-context'; + +const MemberTable = (props: any) => { + const members = props?.members ?? []; + const setAllMembers = props?.setAllMembers; + const selectedTab = props?.selectedTab ?? ''; + + const [isAllSelected, setIsAllSelected] = useState(false); + const [selectedMembers, setSelectedMembes] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [isSort, setIsSort] = useState(false); + const { setMemberList } = useNavbarContext(); + + + // const onSortClickHandler = () => { + // setIsSort(!isSort); + // const sortedMembers = members.sort((a: any, b: any) => { + // if (isSort) { + // return a.name.localeCompare(b.name); + // } else { + // return b.name.localeCompare(a.name); + // } + // }); + // setAllMembers(sortedMembers); + // }; + + const onSelectAllClickHandler = () => { + setIsAllSelected(!isAllSelected); + if (isAllSelected) { + setSelectedMembes([]); + } else { + setSelectedMembes(members.map((member: any) => member.id)); + } + }; + + const onMemberSelectHandler = (id: any) => { + setIsAllSelected(false); + if (selectedMembers.includes(id)) { + const filteredMembes = selectedMembers.filter((uid) => uid !== id); + setSelectedMembes(filteredMembes); + if (filteredMembes.length === members.length) { + setIsAllSelected(true); + } + } else { + const addedMembes = [...selectedMembers, id]; + setSelectedMembes(addedMembes); + if (addedMembes.length === members.length) { + setIsAllSelected(true); + } + } + }; + + function redirectToDetail(request) { + setIsLoading(true); + const route = ROUTE_CONSTANTS.MEMBER_VIEW; + router.push({ + pathname: route, + query: { + id: request.id, + }, + }); + } + + const onSuccessHandler = async () => { + setIsLoading(true); + const config = { + headers: { + authorization: `Bearer ${props.plnadmin}`, + }, + }; + const listData = await api.get(`${API_ROUTE.PARTICIPANTS_REQUEST}?status=PENDING`, config); + const unVerifiedMembes = await api.get(`${API_ROUTE.MEMBERS}?isVerified=false&pagination=false`, config); + const pendingMembers = listData.data.filter((item) => item.participantType === ENROLLMENT_TYPE.MEMBER); + + const formattedPendingMembes = pendingMembers?.map((data) => { + return { + id: data.uid, + name: data.newData.name, + status: data.status, + }; + }); + const filteredUnVerifiedMembers = unVerifiedMembes.data.members.map((data) => { + return { + id: data.uid, + name: data.name, + isVerified: data?.isVerified || false, + }; + }); + + setMemberList([...formattedPendingMembes, ...filteredUnVerifiedMembers]); + + setAllMembers({ + pending: formattedPendingMembes, + unverified: filteredUnVerifiedMembers, + }); + }; + + async function approvelClickHandler(id: any, status: any, isVerified: any) { + const data = { + status: status, + participantType: ENROLLMENT_TYPE.MEMBER, + isVerified, + }; + const configuration = { + headers: { + authorization: `Bearer ${props.plnadmin}`, + }, + }; + setIsLoading(true); + try { + let message=""; + setIsLoading(true); + if (selectedTab === APP_CONSTANTS.PENDING_FLAG) { + await api.patch(`${API_ROUTE.PARTICIPANTS_REQUEST}/${id}`, data, configuration); + message = `Successfully ${APP_CONSTANTS.UNVERIFIED_FLAG}`; + + } else { + await api.post(`${API_ROUTE.MEMBERS}/${id}`, [id], configuration); + message = `Successfully ${APP_CONSTANTS.VERIFIED_FLAG}`; + + } + + onSuccessHandler(); + toast(message); + } catch (error: any) { + if (error.response?.status === 500) { + router.push({ + pathname: ROUTE_CONSTANTS.INTERNAL_SERVER_ERROR, + }); + } else if (error.response?.status === 400) { + toast(error.response?.data?.message || 'Bad request'); + } else { + toast(error.message || 'An unexpected error occurred'); + } + } finally { + setIsLoading(false); + } + } + + async function bulkApprovedClickHandler(isVerified: any) { + const data = selectedMembers.map((memberId: any) => { + return { + uid: memberId, + status: APP_CONSTANTS.APPROVED_FLAG, + partcipantType: ENROLLMENT_TYPE.MEMBER, + isVerified, + }; + }); + const configuration = { + headers: { + authorization: `Bearer ${props.plnadmin}`, + }, + }; + try { + setIsAllSelected(false); + setSelectedMembes([]); + setIsLoading(true); + if (selectedTab === APP_CONSTANTS.PENDING_FLAG) { + await api.post(`${API_ROUTE.PARTICIPANTS_REQUEST}`, data, configuration); + } else { + const data = selectedMembers?.map((memberId: any) => memberId); + await api.post(`${API_ROUTE.MEMBERS}`, { memberIds: data }, configuration); + } + onSuccessHandler(); + const message = `Successfully ${APP_CONSTANTS.APPROVED_LABEL}`; + toast(message); + } catch (error: any) { + if (error.response?.status === 500) { + router.push({ + pathname: ROUTE_CONSTANTS.INTERNAL_SERVER_ERROR, + }); + } else if (error.response?.status === 400) { + toast(error.response?.data?.message || 'Bad request'); + } else { + toast(error.message || 'An unexpected error occurred'); + } + } finally { + setIsLoading(false); + } + } + + const onRemoveClickHandler = async (members: any) => { + const data = members.map((memberId: any) => { + return { + uid: memberId, + status: APP_CONSTANTS.REJECTED_FLAG, + partcipantType: ENROLLMENT_TYPE.MEMBER, + }; + }); + const configuration = { + headers: { + authorization: `Bearer ${props.plnadmin}`, + }, + }; + try { + setIsLoading(true); + await api.post(`${API_ROUTE.PARTICIPANTS_REQUEST}`, data, configuration); + onSuccessHandler(); + setSelectedMembes([]); + setIsAllSelected(false); + const message = `Successfully ${APP_CONSTANTS.REJECTED_LABEL}`; + toast(message); + } catch (error: any) { + if (error.response?.status === 500) { + router.push({ + pathname: ROUTE_CONSTANTS.INTERNAL_SERVER_ERROR, + }); + } else if (error.response?.status === 400) { + toast(error.response?.data?.message || 'Bad request'); + } else { + toast(error.message || 'An unexpected error occurred'); + } + } finally { + setIsLoading(false); + } + }; + + return ( + <> + {isLoading && } + {members?.length > 0 && ( +
+ {/* Header */} +
+
+ + Applicant Name + {/* */} +
+
Actions
+
+ {/* Members */} +
+ {members?.map((member: any, index: number) => { + const isSelected = selectedMembers.includes(member.id) || isAllSelected; + const isDisableOptions = selectedMembers.length > 0; + return ( +
+
+ + + {member.name} + +
+ + {/* Options */} +
+ + + + + {selectedTab === APP_CONSTANTS.PENDING_FLAG && ( + + )} + + {selectedTab === APP_CONSTANTS.PENDING_FLAG && ( + + )} +
+
+ ); + })} +
+
+ )} + {members?.length === 0 && ( +
+
+ {APP_CONSTANTS.NO_DATA_AVAILABLE_LABEL} +
+
+ )} + + {selectedMembers?.length > 0 && ( +
+
+
+ {`${selectedMembers.length} Applicant${selectedMembers.length > 1 ? 's' : ''} selected`} +
+ +
+ + + {selectedTab === APP_CONSTANTS.PENDING_FLAG && ( + + )} + + {selectedTab === APP_CONSTANTS.PENDING_FLAG && ( + + )} +
+
+
+ )} + + ); +}; + +export default MemberTable; diff --git a/apps/back-office/components/request-list.tsx b/apps/back-office/components/request-list.tsx index f2bf76d3c..6945d4e15 100644 --- a/apps/back-office/components/request-list.tsx +++ b/apps/back-office/components/request-list.tsx @@ -1,35 +1,20 @@ -import React from 'react'; -import APP_CONSTANTS, { ROUTE_CONSTANTS } from '../utils/constants'; import { InputField } from '@protocol-labs-network/ui'; -import { useNavbarContext } from '../context/navbar-context'; -import router from 'next/router'; -import { useState } from 'react'; -import { useEffect } from 'react'; -import Loader from '../components/common/loader'; +import { useEffect, useState } from 'react'; import { ReactComponent as SearchIcon } from '../public/assets/icons/searchicon.svg'; +import APP_CONSTANTS from '../utils/constants'; +import TeamRequestList from './team-request-list'; +import { useNavbarContext } from '../context/navbar-context'; +import MemberRequestList from './member-request-list'; -export default function RequestList({ list, type }) { - const { isTeamActive } = useNavbarContext(); +export default function RequestList({ list, type, plnadmin}) { const [dataList, setDataList] = useState([]); - const [isLoading, setIsLoading] = useState(false); + const { isTeamActive } = useNavbarContext(); useEffect(() => { setDataList(list); - }, [list]); + }, [list, isTeamActive]); const backupList = list; - function redirectToDetail(request) { - setIsLoading(true); - const route = isTeamActive - ? ROUTE_CONSTANTS.TEAM_VIEW - : ROUTE_CONSTANTS.MEMBER_VIEW; - router.push({ - pathname: route, - query: { - id: request.id, - }, - }); - } function searchList(input = '') { if (input === '') { @@ -43,11 +28,11 @@ export default function RequestList({ list, type }) { } } + return ( <> - {isLoading && }
-
+
{type !== APP_CONSTANTS.PENDING_LABEL && (
)} - {dataList && - dataList.map((request, index) => { - const borderClass = - dataList.length == 1 - ? 'rounded-xl' - : index == 0 - ? 'rounded-tl-xl rounded-tr-xl' - : index == dataList.length - 1 - ? 'rounded-bl-xl rounded-br-xl' - : ''; - return ( -
redirectToDetail(request)} - > -
- - {request?.name} - - {request.status !== APP_CONSTANTS.PENDING_LABEL && ( - - {request.status === 'REJECTED' - ? APP_CONSTANTS.REJECTED_LABEL - : APP_CONSTANTS.APPROVED_LABEL} - - )} -
-
- ); - })} + {dataList.length > 0 && ( + <> + {isTeamActive && } + {!isTeamActive && } + + )} + {dataList.length === 0 && (
onTabClickHandler(name)} className={`px-[24px] py-[14px] border-b-[#CBD5E1] border-b-[2px] text-[#1D4ED8] ${isSelected ? "border-b-[#1D4ED8] font-[600]" : ""}`}>{name} + ) + +} + +export default Tab; \ No newline at end of file diff --git a/apps/back-office/components/team-request-list.tsx b/apps/back-office/components/team-request-list.tsx new file mode 100644 index 000000000..21ec423f8 --- /dev/null +++ b/apps/back-office/components/team-request-list.tsx @@ -0,0 +1,71 @@ +import { useState } from "react"; +import APP_CONSTANTS, { ROUTE_CONSTANTS } from "../utils/constants"; +import Loader from "./common/loader"; +import { useNavbarContext } from "../context/navbar-context"; +import router from 'next/router'; + + + +const TeamRequestList = (props: any) => { + const dataList = props?.teams; + const [isLoading, setIsLoading] = useState(false); + + + function redirectToDetail(request) { + setIsLoading(true); + const route = ROUTE_CONSTANTS.TEAM_VIEW + router.push({ + pathname: route, + query: { + id: request.id, + }, + }); + } + + return <> + {isLoading && } + {dataList && + dataList.map((request, index) => { + const borderClass = + dataList.length == 1 + ? 'rounded-xl' + : index == 0 + ? 'rounded-tl-xl rounded-tr-xl' + : index == dataList.length - 1 + ? 'rounded-bl-xl rounded-br-xl' + : ''; + return ( +
redirectToDetail(request)} + > +
+ + {request?.name} + + {request.status !== APP_CONSTANTS.PENDING_LABEL && ( + + {request.status === 'REJECTED' + ? APP_CONSTANTS.REJECTED_LABEL + : APP_CONSTANTS.APPROVED_LABEL} + + )} +
+
+ ); + })} + +} + +export default TeamRequestList; \ No newline at end of file diff --git a/apps/back-office/layout/approval-layout.tsx b/apps/back-office/layout/approval-layout.tsx index bd5a52537..9dc66795f 100644 --- a/apps/back-office/layout/approval-layout.tsx +++ b/apps/back-office/layout/approval-layout.tsx @@ -2,9 +2,9 @@ import { Navbar } from '../components/navbar/navbar'; export function ApprovalLayout({ children }) { return ( -
+
-
+
{children}
diff --git a/apps/back-office/pages/pending-list.tsx b/apps/back-office/pages/pending-list.tsx index b91ddbc1a..c7ac02c8c 100644 --- a/apps/back-office/pages/pending-list.tsx +++ b/apps/back-office/pages/pending-list.tsx @@ -9,41 +9,29 @@ import { ApprovalLayout } from '../layout/approval-layout'; import { parseCookies } from 'nookies'; export default function PendingList(props) { - const { - setIsOpenRequest, - setMemberList, - setTeamList, - isTeamActive, - setShowMenu, - } = useNavbarContext(); + const { setIsOpenRequest, setMemberList, setTeamList, isTeamActive, setShowMenu } = useNavbarContext(); setShowMenu(true); + console.log('pln admin', props.plnadmin); + useEffect(() => { - setMemberList(props.memberList); + setMemberList([...props.memberList, ...props.unverifiedMembers]); setTeamList(props.teamList); setIsOpenRequest(true); - }, [ - isTeamActive, - setMemberList, - props.memberList, - props.teamList, - setTeamList, - setIsOpenRequest, - ]); + }, [isTeamActive, setMemberList, props.memberList, props.teamList, setTeamList, setIsOpenRequest]); return ( ); } -export const getServerSideProps: GetServerSideProps = async ( - context -) => { +export const getServerSideProps: GetServerSideProps = async (context) => { const { plnadmin } = parseCookies(context); if (!plnadmin) { @@ -61,21 +49,18 @@ export const getServerSideProps: GetServerSideProps = async ( authorization: `Bearer ${plnadmin}`, }, }; - const listData = await api.get( - `${API_ROUTE.PARTICIPANTS_REQUEST}?status=PENDING`, - config - ); + const listData = await api.get(`${API_ROUTE.PARTICIPANTS_REQUEST}?status=PENDING`, config); + const unVerifiedMembes = await api.get(`${API_ROUTE.MEMBERS}?isVerified=false&pagination=false`, config); + let memberResponse = []; let teamResponse = []; let team = []; let member = []; + let membersCount = 0; + let unverifiedMembers = []; if (listData.data) { - teamResponse = listData.data.filter( - (item) => item.participantType === ENROLLMENT_TYPE.TEAM - ); - memberResponse = listData.data.filter( - (item) => item.participantType === ENROLLMENT_TYPE.MEMBER - ); + teamResponse = listData.data.filter((item) => item.participantType === ENROLLMENT_TYPE.TEAM); + memberResponse = listData.data.filter((item) => item.participantType === ENROLLMENT_TYPE.MEMBER); member = memberResponse?.map((data) => { return { id: data.uid, @@ -83,6 +68,14 @@ export const getServerSideProps: GetServerSideProps = async ( status: data.status, }; }); + unverifiedMembers = unVerifiedMembes.data.members.map((data) => { + return { + id: data.uid, + name: data.name, + isVerified: data?.isVerified || false, + }; + }); + team = teamResponse?.map((data) => { return { id: data.uid, @@ -91,12 +84,15 @@ export const getServerSideProps: GetServerSideProps = async ( }; }); } + membersCount = member?.length + unverifiedMembers?.length; return { props: { memberList: member, + unverifiedMembers: unverifiedMembers, teamList: team, teamCount: team?.length ?? 0, - memberCount: member?.length ?? 0, + memberCount: membersCount ?? 0, + plnadmin, }, }; }; diff --git a/apps/back-office/public/assets/images/delete-disabled.svg b/apps/back-office/public/assets/images/delete-disabled.svg new file mode 100644 index 000000000..31668aeae --- /dev/null +++ b/apps/back-office/public/assets/images/delete-disabled.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/back-office/public/assets/images/delete.svg b/apps/back-office/public/assets/images/delete.svg new file mode 100644 index 000000000..353162086 --- /dev/null +++ b/apps/back-office/public/assets/images/delete.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/back-office/public/assets/images/sort-unselected.svg b/apps/back-office/public/assets/images/sort-unselected.svg new file mode 100644 index 000000000..3526a2a6e --- /dev/null +++ b/apps/back-office/public/assets/images/sort-unselected.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/back-office/public/assets/images/unverified-disabled.svg b/apps/back-office/public/assets/images/unverified-disabled.svg new file mode 100644 index 000000000..500058f0a --- /dev/null +++ b/apps/back-office/public/assets/images/unverified-disabled.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/back-office/public/assets/images/unverified.svg b/apps/back-office/public/assets/images/unverified.svg new file mode 100644 index 000000000..1af9691f9 --- /dev/null +++ b/apps/back-office/public/assets/images/unverified.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/back-office/public/assets/images/verified-disabled.svg b/apps/back-office/public/assets/images/verified-disabled.svg new file mode 100644 index 000000000..500058f0a --- /dev/null +++ b/apps/back-office/public/assets/images/verified-disabled.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/back-office/public/assets/images/verified.svg b/apps/back-office/public/assets/images/verified.svg new file mode 100644 index 000000000..fda0c6a23 --- /dev/null +++ b/apps/back-office/public/assets/images/verified.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/back-office/utils/constants.ts b/apps/back-office/utils/constants.ts index e010c32c2..a7bf20125 100644 --- a/apps/back-office/utils/constants.ts +++ b/apps/back-office/utils/constants.ts @@ -2,6 +2,10 @@ const APP_CONSTANTS = { AUTO_APPROVED_LABEL:'AUTOAPPROVED', APPROVED_LABEL: 'Approved', PENDING_LABEL: 'PENDING', + PENDING_FLAG: "Pending", + UNVERIFIED_LABEL: 'UNVERIFIED', + UNVERIFIED_FLAG: 'Unverified', + VERIFIED_FLAG: 'Verified', REJECTED_LABEL: 'Rejected', APPROVED_FLAG: 'APPROVED', REJECTED_FLAG: 'REJECTED', From b248939297dbbec4174cc11ef3e3b297a24470b3 Mon Sep 17 00:00:00 2001 From: Navaneethakrishnan Date: Tue, 3 Dec 2024 16:06:07 +0530 Subject: [PATCH 19/41] fix: fixed issue in member-signup migration --- .../migrations/20241127093907_member_signup/migration.sql | 5 +++-- apps/web-api/prisma/schema.prisma | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web-api/prisma/migrations/20241127093907_member_signup/migration.sql b/apps/web-api/prisma/migrations/20241127093907_member_signup/migration.sql index b6ee98657..6c866c0b9 100644 --- a/apps/web-api/prisma/migrations/20241127093907_member_signup/migration.sql +++ b/apps/web-api/prisma/migrations/20241127093907_member_signup/migration.sql @@ -11,9 +11,10 @@ ADD COLUMN "signUpSource" TEXT, ADD COLUMN "isSubscribedToNewsletter" BOOLEAN DEFAULT false, ADD COLUMN "isUserConsent" BOOLEAN DEFAULT false, ADD COLUMN "teamOrProjectURL" TEXT; + +-- Modify the "plnFriend" column to drop NOT NULL constraint +ALTER TABLE "Member" ALTER COLUMN "plnFriend" DROP NOT NULL; --- CreateIndex -CREATE UNIQUE INDEX "PLEventGuest_memberUid_teamUid_eventUid_key" ON "PLEventGuest"("memberUid", "teamUid", "eventUid"); diff --git a/apps/web-api/prisma/schema.prisma b/apps/web-api/prisma/schema.prisma index c1222b177..efb2ac638 100644 --- a/apps/web-api/prisma/schema.prisma +++ b/apps/web-api/prisma/schema.prisma @@ -413,7 +413,6 @@ model PLEventGuest { isHost Boolean @default(false) isSpeaker Boolean @default(false) isFeatured Boolean @default(false) - @@unique([memberUid, teamUid, eventUid]) } model FocusArea { From 84b402e14ae2d065367e29afaa94fcbcfa9fe0bf Mon Sep 17 00:00:00 2001 From: prasanth Date: Sun, 1 Dec 2024 22:55:46 +0530 Subject: [PATCH 20/41] feat: removed validation in form for members --- .../components/members/memberskillform.tsx | 19 +++- .../components/members/teamandrole.tsx | 6 +- apps/back-office/pages/member-view.tsx | 90 ++++++++++--------- apps/back-office/utils/members.types.ts | 1 + 4 files changed, 65 insertions(+), 51 deletions(-) diff --git a/apps/back-office/components/members/memberskillform.tsx b/apps/back-office/components/members/memberskillform.tsx index dd00b8e04..3fe7e7fc9 100644 --- a/apps/back-office/components/members/memberskillform.tsx +++ b/apps/back-office/components/members/memberskillform.tsx @@ -1,4 +1,4 @@ -import { MultiSelect, Switch } from '@protocol-labs-network/ui'; +import { InputField, MultiSelect, Switch } from '@protocol-labs-network/ui'; import { TeamAndRoleGrid } from './teamandrole'; import { ReactComponent as InformationCircleIcon } from '../../public/assets/icons/info_icon.svg'; @@ -20,8 +20,8 @@ export default function AddMemberSkillForm(props) {
{teamAndRoles?.length > 0 && (
- Team* - Role* + Team + Role
)} {teamAndRoles?.map((item, index) => ( @@ -56,11 +56,22 @@ export default function AddMemberSkillForm(props) {
+
+ +
1 && props.isEditEnabled + teamRowId > 0 && props.isEditEnabled ? `cursor-pointer"` : `invisible` } diff --git a/apps/back-office/pages/member-view.tsx b/apps/back-office/pages/member-view.tsx index 612fa2cea..c1924afff 100644 --- a/apps/back-office/pages/member-view.tsx +++ b/apps/back-office/pages/member-view.tsx @@ -29,32 +29,32 @@ function validateBasicForm(formValues, imageUrl) { if (!formValues.email.trim() || !formValues.email?.trim().match(emailRE)) { errors.push('Please add valid Email'); } - if ( - !formValues.requestorEmail?.trim() || - !formValues.requestorEmail?.trim().match(emailRE) - ) { - errors.push('Please add a valid Requestor Email'); - } + // if ( + // !formValues.requestorEmail?.trim() || + // !formValues.requestorEmail?.trim().match(emailRE) + // ) { + // errors.push('Please add a valid Requestor Email'); + // } return errors; } -function validateSkillForm(formValues) { - const errors = []; - if (!formValues.teamAndRoles.length) { - errors.push('Please add your Team and Role details'); - } else { - const missingValues = formValues.teamAndRoles.filter( - (item) => item.teamUid == '' || item.role == '' - ); - if (missingValues.length) { - errors.push('Please add missing Team(s)/Role(s)'); - } - } - if (!formValues.skills.length) { - errors.push('Please add your skill details'); - } - return errors; -} +// function validateSkillForm(formValues) { +// const errors = []; +// if (!formValues.teamAndRoles.length) { +// errors.push('Please add your Team and Role details'); +// } else { +// const missingValues = formValues.teamAndRoles.filter( +// (item) => item.teamUid == '' || item.role == '' +// ); +// if (missingValues.length) { +// errors.push('Please add missing Team(s)/Role(s)'); +// } +// } +// if (!formValues.skills.length) { +// errors.push('Please add your skill details'); +// } +// return errors; +// } function validateForm(formValues, imageUrl) { let errors = []; @@ -62,10 +62,10 @@ function validateForm(formValues, imageUrl) { if (basicFormErrors.length) { errors = [...errors, ...basicFormErrors]; } - const skillFormErrors = validateSkillForm(formValues); - if (skillFormErrors.length) { - errors = [...errors, ...skillFormErrors]; - } + // const skillFormErrors = validateSkillForm(formValues); + // if (skillFormErrors.length) { + // errors = [...errors, ...skillFormErrors]; + // } return errors; } @@ -130,6 +130,7 @@ export default function MemberView(props) { telegramHandler: formValues.telegramHandler?.trim(), officeHours: formValues.officeHours?.trim() === ''? null : formValues.officeHours?.trim(), comments: formValues.comments?.trim(), + teamOrProjectURL: formValues.teamOrProjectURL, plnStartDate: formValues.plnStartDate ? new Date(formValues.plnStartDate)?.toISOString() : null, @@ -203,7 +204,7 @@ export default function MemberView(props) { const data = { participantType: ENROLLMENT_TYPE.MEMBER, // referenceUid: props.id, - requesterEmailId: requestorEmail, + requesterEmailId: requestorEmail ? requestorEmail : null, uniqueIdentifier: values.email, newData: { ...values, @@ -329,7 +330,6 @@ export default function MemberView(props) {
)} { oldName = requestData?.oldName ?? requestData?.name; status = requestDetailResponse?.data?.status; const teamAndRoles = - requestData.teamAndRoles?.length && - requestData.teamAndRoles.map((team) => { + requestData?.teamAndRoles?.length && + requestData?.teamAndRoles?.map((team) => { return { role: team.role, teamUid: team.teamUid, @@ -466,27 +466,28 @@ export const getServerSideProps = async (context) => { formValues = { name: requestData?.name, - email: requestData.email, - imageUid: requestData.imageUid ?? '', + email: requestData?.email, + imageUid: requestData?.imageUid ?? '', imageFile: null, - plnStartDate: requestData.plnStartDate - ? new Date(requestData.plnStartDate).toISOString().split('T')[0] + plnStartDate: requestData?.plnStartDate + ? new Date(requestData?.plnStartDate).toISOString().split('T')[0] : null, city: requestData?.city ?? '', region: requestData?.region ?? '', country: requestData?.country ?? '', - linkedinHandler: requestData.linkedinHandler ?? '', - discordHandler: requestData.discordHandler ?? '', - twitterHandler: requestData.twitterHandler ?? '', - githubHandler: requestData.githubHandler ?? '', - telegramHandler: requestData.telegramHandler ?? '', - officeHours: requestData.officeHours ?? '', + linkedinHandler: requestData?.linkedinHandler ?? '', + discordHandler: requestData?.discordHandler ?? '', + twitterHandler: requestData?.twitterHandler ?? '', + githubHandler: requestData?.githubHandler ?? '', + telegramHandler: requestData?.telegramHandler ?? '', + officeHours: requestData?.officeHours ?? '', requestorEmail: requestDetailResponse?.data?.requesterEmailId ?? '', comments: requestData?.comments ?? '', teamAndRoles: teamAndRoles || [ { teamUid: '', teamTitle: '', role: '', rowId: 1 }, ], - skills: requestData.skills?.map((item) => { + teamOrProjectURL: requestData?.teamOrProjectURL ?? '', + skills: requestData?.skills?.map((item) => { return { value: item.uid, label: item.title }; }), openToWork: requestData?.openToWork ?? '', @@ -510,9 +511,10 @@ export const getServerSideProps = async (context) => { .filter((item) => item.status !== APP_CONSTANTS.PENDING_LABEL); } - teams = memberTeamsResponse?.data?.map((item) => { + teams = Array.isArray(memberTeamsResponse?.data) ? + memberTeamsResponse?.data?.map((item) => { return { value: item.uid, label: item.name }; - }); + }) : []; skills = skillsResponse?.data?.map((item) => { return { value: item.uid, label: item.title }; }); diff --git a/apps/back-office/utils/members.types.ts b/apps/back-office/utils/members.types.ts index 0544c383a..1ad4560ff 100644 --- a/apps/back-office/utils/members.types.ts +++ b/apps/back-office/utils/members.types.ts @@ -66,4 +66,5 @@ export interface IFormValues { skills: Skill[]; openToWork: boolean; projectContributions: IProjectContribution[]; + teamOrProjectURL: string; } From 2759b31f84263da1c2752406d8c25aaea754f3bb Mon Sep 17 00:00:00 2001 From: yosuva Rajendran Date: Mon, 2 Dec 2024 20:41:39 +0530 Subject: [PATCH 21/41] feat: get all teams api change --- apps/back-office/components/members/memberskillform.tsx | 2 +- apps/back-office/components/members/teamandrole.tsx | 2 +- apps/back-office/pages/member-view.tsx | 2 +- apps/back-office/utils/services/team.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/back-office/components/members/memberskillform.tsx b/apps/back-office/components/members/memberskillform.tsx index 3fe7e7fc9..614a88480 100644 --- a/apps/back-office/components/members/memberskillform.tsx +++ b/apps/back-office/components/members/memberskillform.tsx @@ -59,7 +59,7 @@ export default function AddMemberSkillForm(props) {
{ requestorEmail: requestDetailResponse?.data?.requesterEmailId ?? '', comments: requestData?.comments ?? '', teamAndRoles: teamAndRoles || [ - { teamUid: '', teamTitle: '', role: '', rowId: 1 }, + // { teamUid: '', teamTitle: '', role: '', rowId: 1 }, ], teamOrProjectURL: requestData?.teamOrProjectURL ?? '', skills: requestData?.skills?.map((item) => { diff --git a/apps/back-office/utils/services/team.ts b/apps/back-office/utils/services/team.ts index ac1c2121f..4ca1390d9 100644 --- a/apps/back-office/utils/services/team.ts +++ b/apps/back-office/utils/services/team.ts @@ -18,7 +18,7 @@ export const fetchTeamsForAutocomplete = async (searchTerm) => { try { const response = await api.get(`/v1/teams?name__istartswith=${searchTerm}`); if (response.data) { - return response.data.map((item) => { + return response.data?.teams?.map((item) => { return { value: item.uid, label: item.name }; }); } From d6f1a08f8844d243633eb86910d72c9707a153de Mon Sep 17 00:00:00 2001 From: Vellaiyan-Marimuthu Date: Tue, 3 Dec 2024 16:53:13 +0530 Subject: [PATCH 22/41] Fix: Bug fixing in closed requests --- .../components/member-request-list.tsx | 76 ++++++++++++++++++- apps/back-office/components/request-list.tsx | 21 ++--- apps/back-office/pages/closed-list.tsx | 29 ++----- apps/back-office/pages/member-view.tsx | 2 +- apps/back-office/pages/pending-list.tsx | 2 - 5 files changed, 89 insertions(+), 41 deletions(-) diff --git a/apps/back-office/components/member-request-list.tsx b/apps/back-office/components/member-request-list.tsx index e2f868d25..422e35745 100644 --- a/apps/back-office/components/member-request-list.tsx +++ b/apps/back-office/components/member-request-list.tsx @@ -1,12 +1,17 @@ import { Fragment, useState } from 'react'; -import APP_CONSTANTS from '../utils/constants'; +import APP_CONSTANTS, { ROUTE_CONSTANTS } from '../utils/constants'; import MemberTable from './member-table/member-table'; import Tab from './tab/tab'; +import Loader from './common/loader'; +import router from 'next/router'; + const MemberRequestList = (props: any) => { const dataList = props?.members; + const type = props?.type; const [allMembers, setAllMembers]: any = useState(() => getFilterMembes(dataList ?? [])); const [currentTab, setCurrentTab] = useState(APP_CONSTANTS.PENDING_FLAG); + const [isLoading, setIsLoading] = useState(false); function getFilterMembes(data: any) { const pending = data.filter((item: any) => item.status === APP_CONSTANTS.PENDING_LABEL); @@ -33,8 +38,23 @@ const MemberRequestList = (props: any) => { onTabSelected(name); } - return ( + function redirectToDetail(request) { + setIsLoading(true); + const route = ROUTE_CONSTANTS.MEMBER_VIEW + router.push({ + pathname: route, + query: { + id: request.id, + }, + }); + } + + return <>
+ {isLoading && } + + {type !== APP_CONSTANTS.CLOSED_FLAG && ( + <>
{availableTabs.map((tab: any, index: number) => ( @@ -45,8 +65,58 @@ const MemberRequestList = (props: any) => {
+ + )} + + {type === APP_CONSTANTS.CLOSED_FLAG && ( + <> + {dataList && + dataList.map((request, index) => { + const borderClass = + dataList.length == 1 + ? 'rounded-xl' + : index == 0 + ? 'rounded-tl-xl rounded-tr-xl' + : index == dataList.length - 1 + ? 'rounded-bl-xl rounded-br-xl' + : ''; + return ( +
redirectToDetail(request)} + > +
+ + {request?.name} + + {request.status !== APP_CONSTANTS.PENDING_LABEL && ( + + {request.status === 'REJECTED' + ? APP_CONSTANTS.REJECTED_LABEL + : APP_CONSTANTS.APPROVED_LABEL} + + )} +
+
+ ); + })} + + + + )}
- ); + }; export default MemberRequestList; diff --git a/apps/back-office/components/request-list.tsx b/apps/back-office/components/request-list.tsx index 6945d4e15..d1ac83ab8 100644 --- a/apps/back-office/components/request-list.tsx +++ b/apps/back-office/components/request-list.tsx @@ -6,7 +6,7 @@ import TeamRequestList from './team-request-list'; import { useNavbarContext } from '../context/navbar-context'; import MemberRequestList from './member-request-list'; -export default function RequestList({ list, type, plnadmin}) { +export default function RequestList({ list, type, plnadmin }) { const [dataList, setDataList] = useState([]); const { isTeamActive } = useNavbarContext(); @@ -20,15 +20,10 @@ export default function RequestList({ list, type, plnadmin}) { if (input === '') { setDataList(backupList); } else { - setDataList( - backupList.filter((req) => - req.name.toLowerCase().includes(input.toLowerCase()) - ) - ); + setDataList(backupList.filter((req) => req.name.toLowerCase().includes(input.toLowerCase()))); } } - return ( <>
@@ -53,10 +48,10 @@ export default function RequestList({ list, type, plnadmin}) { )} {dataList.length > 0 && ( <> - {isTeamActive && } - {!isTeamActive && } - - )} + {isTeamActive && } + {!isTeamActive && } + + )} {dataList.length === 0 && (
- - {APP_CONSTANTS.NO_DATA_AVAILABLE_LABEL} - + {APP_CONSTANTS.NO_DATA_AVAILABLE_LABEL}
)} diff --git a/apps/back-office/pages/closed-list.tsx b/apps/back-office/pages/closed-list.tsx index f1ab96773..eb0f3a7e9 100644 --- a/apps/back-office/pages/closed-list.tsx +++ b/apps/back-office/pages/closed-list.tsx @@ -13,31 +13,19 @@ type RequestList = { }; export default function ClosedList(props) { - const { - setIsOpenRequest, - setMemberList, - setTeamList, - isTeamActive, - setShowMenu, - } = useNavbarContext(); + const { setIsOpenRequest, setMemberList, setTeamList, isTeamActive, setShowMenu } = useNavbarContext(); setShowMenu(true); useEffect(() => { setMemberList(props.memberList); setTeamList(props.teamList); setIsOpenRequest(false); - }, [ - isTeamActive, - setMemberList, - props.memberList, - props.teamList, - setTeamList, - setIsOpenRequest, - ]); + }, [isTeamActive, setMemberList, props.memberList, props.teamList, setTeamList, setIsOpenRequest]); return ( @@ -45,9 +33,7 @@ export default function ClosedList(props) { ); } -export const getServerSideProps: GetServerSideProps = async ( - context -) => { +export const getServerSideProps: GetServerSideProps = async (context) => { const { plnadmin } = parseCookies(context); if (!plnadmin) { @@ -76,7 +62,7 @@ export const getServerSideProps: GetServerSideProps = async ( teamResponse = listData.data.filter( (item) => item.participantType === ENROLLMENT_TYPE.TEAM && - item.status !== APP_CONSTANTS.PENDING_LABEL && + item.status !== APP_CONSTANTS.PENDING_LABEL && item.status !== APP_CONSTANTS.AUTO_APPROVED_LABEL ); memberResponse = listData.data.filter( @@ -88,14 +74,14 @@ export const getServerSideProps: GetServerSideProps = async ( member = memberResponse?.map((data) => { return { id: data.uid, - name: data.newData.name, + name: data.newData.name ?? '', status: data.status, }; }); team = teamResponse?.map((data) => { return { id: data.uid, - name: data.newData.name, + name: data.newData.name ?? '', status: data.status, }; }); @@ -107,6 +93,7 @@ export const getServerSideProps: GetServerSideProps = async ( teamList: team, teamCount: team?.length ?? 0, memberCount: member?.length ?? 0, + plnadmin, }, }; }; diff --git a/apps/back-office/pages/member-view.tsx b/apps/back-office/pages/member-view.tsx index e5bcc6249..41758e572 100644 --- a/apps/back-office/pages/member-view.tsx +++ b/apps/back-office/pages/member-view.tsx @@ -457,7 +457,7 @@ export const getServerSideProps = async (context) => { requestData?.teamAndRoles?.length && requestData?.teamAndRoles?.map((team) => { return { - role: team.role, + role: team.role ?? "", teamUid: team.teamUid, teamTitle: team.teamTitle, rowId: counter++, diff --git a/apps/back-office/pages/pending-list.tsx b/apps/back-office/pages/pending-list.tsx index c7ac02c8c..2745d265c 100644 --- a/apps/back-office/pages/pending-list.tsx +++ b/apps/back-office/pages/pending-list.tsx @@ -12,8 +12,6 @@ export default function PendingList(props) { const { setIsOpenRequest, setMemberList, setTeamList, isTeamActive, setShowMenu } = useNavbarContext(); setShowMenu(true); - console.log('pln admin', props.plnadmin); - useEffect(() => { setMemberList([...props.memberList, ...props.unverifiedMembers]); setTeamList(props.teamList); From 3ebd54f9027edcaf24c940da9c0e605819249cf1 Mon Sep 17 00:00:00 2001 From: navneethkrish Date: Tue, 3 Dec 2024 18:02:59 +0530 Subject: [PATCH 23/41] fix(participant approval): added isverified field for bulk approval in participant request --- .../admin/participants-request.controller.ts | 52 +------ apps/web-api/src/home/home.service.ts | 14 ++ .../web-api/src/members/members.controller.ts | 6 + .../participants-request.service.ts | 129 ++++++++++++------ .../src/schema/participants-request.ts | 2 +- 5 files changed, 114 insertions(+), 89 deletions(-) diff --git a/apps/web-api/src/admin/participants-request.controller.ts b/apps/web-api/src/admin/participants-request.controller.ts index f235f0579..b633bcbcd 100644 --- a/apps/web-api/src/admin/participants-request.controller.ts +++ b/apps/web-api/src/admin/participants-request.controller.ts @@ -35,54 +35,8 @@ export class AdminParticipantsRequestController { async processBulkRequest( @Body() body: ProcessBulkParticipantRequest[] ): Promise { - let successCount = 0; - const results = await Promise.all( - body.map(async (request) => { - try { - const participantRequest: ParticipantsRequest | null = - await this.participantsRequestService.findOneByUid(request.uid); - - if (!participantRequest) { - return { - uid: request.uid, - message: 'Request not found', - }; - } - - if (participantRequest.status !== ApprovalStatus.PENDING) { - return { - uid: request.uid, - message: `Request cannot be processed. It has already been ${participantRequest.status.toLowerCase()}.`, - }; - } - - if ( - participantRequest.participantType === ParticipantType.TEAM && - !participantRequest.requesterEmailId - ) { - return { - uid: request.uid, - message: 'Requester email is required for team participation requests. Please provide a valid email address.', - }; - } - - await this.participantsRequestService.processRequestByUid( - request.uid, - participantRequest, - request.status - ); - successCount++; - return { uid: request.uid, message: 'Processed successfully' }; - } catch (error) { - return { - uid: request.uid, - message: 'An error occurred while processing the request', - }; - } - }) - ); - - return { count: successCount, results }; + const participationRequests = body; + return await this.participantsRequestService.processBulkRequest(participationRequests); } /** @@ -147,7 +101,7 @@ export class AdminParticipantsRequestController { 'Requester email is required for team participation requests. Please provide a valid email address.' ); } - return await this.participantsRequestService.processRequestByUid(uid, participantRequest, body.status); + return await this.participantsRequestService.processRequestByUid(uid, participantRequest, body.status, body.isVerified); } } diff --git a/apps/web-api/src/home/home.service.ts b/apps/web-api/src/home/home.service.ts index 7600f4851..0a0e329d1 100644 --- a/apps/web-api/src/home/home.service.ts +++ b/apps/web-api/src/home/home.service.ts @@ -66,6 +66,13 @@ export class HomeService { return [...result].sort((team, project) => team.name.localeCompare(project.name)) } + /** + * Retrieves a list of teams based on search term. + * Builds a Prisma query from the queryable fields and adds filters for team name. + * + * @param name - name of the team to be searched for. + * @returns Array of resultant teams. + */ private fetchTeamsBySearchTerm(name) { return this.teamsService.findAll({ where: { @@ -89,6 +96,13 @@ export class HomeService { }) } + /** + * Retrieves a list of projects based on search term. + * Builds a Prisma query from the queryable fields and adds filters for project name. + * + * @param name - name of the project to be searched for. + * @returns Array of resultant projects. + */ private fetchProjectsBySearchTerm(name) { return this.projectsService.getProjects({ where: { diff --git a/apps/web-api/src/members/members.controller.ts b/apps/web-api/src/members/members.controller.ts index 35ac98113..53f6409d5 100644 --- a/apps/web-api/src/members/members.controller.ts +++ b/apps/web-api/src/members/members.controller.ts @@ -172,6 +172,12 @@ export class MemberController { return await this.membersService.updateMemberFromParticipantsRequest(uid, participantsRequest, requestor.email); } + /** + * Updates a member to be verfied user. + * + * @param body - array of memberIds to be updated. + * @returns Array of updation status of the provided memberIds. + */ @Api(server.route.verifyMembers) @UseGuards(UserAccessTokenValidateGuard) async verifyMembers(@Body() body, @Req() request) { diff --git a/apps/web-api/src/participants-request/participants-request.service.ts b/apps/web-api/src/participants-request/participants-request.service.ts index 7c022a551..9595e3199 100644 --- a/apps/web-api/src/participants-request/participants-request.service.ts +++ b/apps/web-api/src/participants-request/participants-request.service.ts @@ -1,12 +1,12 @@ /* eslint-disable prettier/prettier */ -import { - BadRequestException, - ConflictException, - NotFoundException, - Inject, - Injectable, - CACHE_MANAGER, - forwardRef +import { + BadRequestException, + ConflictException, + NotFoundException, + Inject, + Injectable, + CACHE_MANAGER, + forwardRef } from '@nestjs/common'; import { ApprovalStatus, ParticipantType } from '@prisma/client'; import { Cache } from 'cache-manager'; @@ -28,13 +28,13 @@ export class ParticipantsRequestService { private locationTransferService: LocationTransferService, private forestAdminService: ForestAdminService, private notificationService: NotificationService, - @Inject(CACHE_MANAGER) + @Inject(CACHE_MANAGER) private cacheService: Cache, @Inject(forwardRef(() => MembersService)) private membersService: MembersService, - @Inject(forwardRef(() => TeamsService)) + @Inject(forwardRef(() => TeamsService)) private teamsService: TeamsService, - ) {} + ) { } /** * Find all participant requests based on the query. @@ -63,7 +63,7 @@ export class ParticipantsRequestService { where: filters, orderBy: { createdAt: 'desc' }, }); - } catch(err) { + } catch (err) { return this.handleErrors(err) } } @@ -77,13 +77,13 @@ export class ParticipantsRequestService { */ async add( newEntry: Prisma.ParticipantsRequestUncheckedCreateInput, - tx?: Prisma.TransactionClient, + tx?: Prisma.TransactionClient, ): Promise { try { return await (tx || this.prisma).participantsRequest.create({ data: { ...newEntry }, }); - } catch(err) { + } catch (err) { return this.handleErrors(err) } } @@ -99,7 +99,7 @@ export class ParticipantsRequestService { return await this.prisma.participantsRequest.findUnique({ where: { uid }, }); - } catch(err) { + } catch (err) { return this.handleErrors(err, uid) } } @@ -115,9 +115,9 @@ export class ParticipantsRequestService { async checkIfIdentifierAlreadyExist( type: ParticipantType, identifier: string - ): Promise<{ - isRequestPending: boolean; - isUniqueIdentifierExist: boolean + ): Promise<{ + isRequestPending: boolean; + isUniqueIdentifierExist: boolean }> { try { const existingRequest = await this.prisma.participantsRequest.findFirst({ @@ -130,16 +130,16 @@ export class ParticipantsRequestService { if (existingRequest) { return { isRequestPending: true, isUniqueIdentifierExist: false }; } - const existingEntry = - type === ParticipantType.TEAM - ? await this.teamsService.findTeamByName(identifier) + const existingEntry = + type === ParticipantType.TEAM + ? await this.teamsService.findTeamByName(identifier) : await this.membersService.findMemberByEmail(identifier); if (existingEntry) { return { isRequestPending: false, isUniqueIdentifierExist: true }; } return { isRequestPending: false, isUniqueIdentifierExist: false }; - } - catch(err) { + } + catch (err) { return this.handleErrors(err) } } @@ -155,20 +155,20 @@ export class ParticipantsRequestService { async updateByUid( uid: string, participantRequest: Prisma.ParticipantsRequestUncheckedUpdateInput, - ):Promise { + ): Promise { try { const formattedData = { ...participantRequest }; delete formattedData.id; delete formattedData.uid; delete formattedData.status; delete formattedData.participantType; - const result:ParticipantsRequest = await this.prisma.participantsRequest.update({ + const result: ParticipantsRequest = await this.prisma.participantsRequest.update({ where: { uid }, data: formattedData, }); await this.cacheService.reset(); return result; - } catch(err) { + } catch (err) { return this.handleErrors(err) } } @@ -183,13 +183,13 @@ export class ParticipantsRequestService { */ async rejectRequestByUid(uidToReject: string): Promise { try { - const result:ParticipantsRequest = await this.prisma.participantsRequest.update({ + const result: ParticipantsRequest = await this.prisma.participantsRequest.update({ where: { uid: uidToReject }, data: { status: ApprovalStatus.REJECTED } }); await this.cacheService.reset(); return result; - } catch(err) { + } catch (err) { return this.handleErrors(err) } } @@ -209,12 +209,14 @@ export class ParticipantsRequestService { * @returns The updated participant request with the status set to `APPROVED`. */ private async approveRequestByUid( - uidToApprove: string, - participantsRequest: ParticipantsRequest + uidToApprove: string, + participantsRequest: ParticipantsRequest, + isVerified: boolean ): Promise { let result; let createdItem; const dataToProcess: any = participantsRequest; + dataToProcess.newData.isVerified = isVerified; const participantType = participantsRequest.participantType; // Add new member or team and update status to approved await this.prisma.$transaction(async (tx) => { @@ -259,11 +261,11 @@ export class ParticipantsRequestService { * @param uid * @returns */ - async processRequestByUid(uid:string, participantsRequest:ParticipantsRequest, statusToProcess) { + async processRequestByUid(uid: string, participantsRequest: ParticipantsRequest, statusToProcess, isVerified: boolean) { if (statusToProcess === ApprovalStatus.REJECTED) { return await this.rejectRequestByUid(uid); } else { - return await this.approveRequestByUid(uid, participantsRequest); + return await this.approveRequestByUid(uid, participantsRequest, isVerified); } } @@ -288,7 +290,7 @@ export class ParticipantsRequestService { // Add the new request const result: ParticipantsRequest = await this.add({ ...postData - }, + }, tx ); if (!disableNotification) { @@ -313,7 +315,7 @@ export class ParticipantsRequestService { } } } - + /** * Extract unique identifier based on participant type. * @param requestData @@ -324,7 +326,7 @@ export class ParticipantsRequestService { ? requestData.newData.name : requestData.newData.email?.toLowerCase().trim(); } - + /** * Validate if the unique identifier already exists. * @param participantType @@ -332,7 +334,7 @@ export class ParticipantsRequestService { * @throws BadRequestException if identifier already exists */ async validateUniqueIdentifier( - participantType: ParticipantType, + participantType: ParticipantType, uniqueIdentifier: string ): Promise { const { isRequestPending, isUniqueIdentifierExist } = await this.checkIfIdentifierAlreadyExist( @@ -344,7 +346,7 @@ export class ParticipantsRequestService { throw new BadRequestException(`${typeLabel} already exists`); } } - + /** * Validate location for members or email for teams. * @param requestData @@ -360,7 +362,7 @@ export class ParticipantsRequestService { ); } } - + /** * Send notification based on the participant type. * @param result @@ -409,8 +411,57 @@ export class ParticipantsRequestService { return error; } - + generateMemberProfileURL(value) { return generateProfileURL(value); } + + /** + * Process (approve/reject) multiple pending participants requests. + * @param participantRequests - The request body containing array of uids and status of participants to be processed; + * @returns The result of processing the participants request along with the success count. + */ + async processBulkRequest(participantRequests) { + let successCount = 0; + const results = await Promise.all( + participantRequests.map(async (request) => { + try { + const participantRequest: ParticipantsRequest | null = + await this.findOneByUid(request.uid); + if (!participantRequest) { + return { + uid: request.uid, + message: 'Request not found', + }; + } + if (participantRequest.status !== ApprovalStatus.PENDING) { + return { + uid: request.uid, + message: `Request cannot be processed. It has already been ${participantRequest.status.toLowerCase()}.`, + }; + } + if (participantRequest.participantType === ParticipantType.TEAM && !participantRequest.requesterEmailId) { + return { + uid: request.uid, + message: 'Requester email is required for team participation requests. Please provide a valid email address.', + }; + } + await this.processRequestByUid( + request.uid, + participantRequest, + request.status, + request.isVerified + ); + successCount++; + return { uid: request.uid, message: 'Processed successfully' }; + } catch (error) { + return { + uid: request.uid, + message: 'An error occurred while processing the request', + }; + } + }) + ); + return { count: successCount, results }; + } } diff --git a/libs/contracts/src/schema/participants-request.ts b/libs/contracts/src/schema/participants-request.ts index 31f34095d..c6f24e6fc 100644 --- a/libs/contracts/src/schema/participants-request.ts +++ b/libs/contracts/src/schema/participants-request.ts @@ -6,7 +6,7 @@ export const statusEnum = z.enum(['PENDING', 'APPROVED', 'REJECTED']); export const participantTypeEnum = z.enum(['MEMBER', 'TEAM']); const oldDataPostSchema = z.object({}); const teamMappingSchema = z.object({ - role: z.string().optional(), + role: z.string().nullish().optional(), teamUid: z.string(), teamTitle: z.string(), }); From 9bad2ed370fd5f679a898b50efbabbe9189f9880 Mon Sep 17 00:00:00 2001 From: navneethkrish Date: Tue, 3 Dec 2024 21:55:38 +0530 Subject: [PATCH 24/41] feat(member verification): added member controller for admin to verify members --- .../components/member-table/member-table.tsx | 4 +- apps/web-api/src/admin/admin.module.ts | 8 +- apps/web-api/src/admin/member.controller.ts | 22 +++ .../web-api/src/members/members.controller.ts | 26 +-- apps/web-api/src/members/members.service.ts | 165 ++++++++++-------- libs/contracts/src/lib/contract-member.ts | 9 - 6 files changed, 125 insertions(+), 109 deletions(-) create mode 100644 apps/web-api/src/admin/member.controller.ts diff --git a/apps/back-office/components/member-table/member-table.tsx b/apps/back-office/components/member-table/member-table.tsx index b991c1bef..e9f14fd0c 100644 --- a/apps/back-office/components/member-table/member-table.tsx +++ b/apps/back-office/components/member-table/member-table.tsx @@ -40,10 +40,10 @@ const MemberTable = (props: any) => { }; const onMemberSelectHandler = (id: any) => { - setIsAllSelected(false); if (selectedMembers.includes(id)) { + setIsAllSelected(false); const filteredMembes = selectedMembers.filter((uid) => uid !== id); - setSelectedMembes(filteredMembes); + setSelectedMembes([...filteredMembes]); if (filteredMembes.length === members.length) { setIsAllSelected(true); } diff --git a/apps/web-api/src/admin/admin.module.ts b/apps/web-api/src/admin/admin.module.ts index 35195ad6c..076a07066 100644 --- a/apps/web-api/src/admin/admin.module.ts +++ b/apps/web-api/src/admin/admin.module.ts @@ -5,12 +5,14 @@ import { ParticipantsRequestModule } from '../participants-request/participants- import { SharedModule } from '../shared/shared.module'; import { AdminParticipantsRequestController } from './participants-request.controller'; import { AdminAuthController } from './auth.controller'; +import { MemberController } from './member.controller'; +import { MembersModule } from '../members/members.module'; @Module({ - imports: [CacheModule.register(), ParticipantsRequestModule, SharedModule], - controllers: [AdminParticipantsRequestController, AdminAuthController], + imports: [CacheModule.register(), ParticipantsRequestModule, SharedModule, MembersModule], + controllers: [AdminParticipantsRequestController, AdminAuthController, MemberController], providers: [ AdminService, JwtService ], }) -export class AdminModule {} +export class AdminModule { } diff --git a/apps/web-api/src/admin/member.controller.ts b/apps/web-api/src/admin/member.controller.ts new file mode 100644 index 000000000..aca610b21 --- /dev/null +++ b/apps/web-api/src/admin/member.controller.ts @@ -0,0 +1,22 @@ +import { Body, Controller, Post, UseGuards } from '@nestjs/common'; +import { AdminAuthGuard } from '../guards/admin-auth.guard'; +import { MembersService } from '../members/members.service'; + +@Controller('v1/admin/members') +export class MemberController { + constructor(private readonly membersService: MembersService) { } + + /** + * Updates a member to a verfied user. + * + * @param body - array of memberIds to be updated. + * @returns Array of updation status of the provided memberIds. + */ + @Post("/") + @UseGuards(AdminAuthGuard) + async verifyMembers(@Body() body) { + const requestor = await this.membersService.findMemberByRole(); + const { memberIds } = body; + return await this.membersService.verifyMembers(memberIds, requestor?.email); + } +} diff --git a/apps/web-api/src/members/members.controller.ts b/apps/web-api/src/members/members.controller.ts index 53f6409d5..366478e3b 100644 --- a/apps/web-api/src/members/members.controller.ts +++ b/apps/web-api/src/members/members.controller.ts @@ -93,7 +93,7 @@ export class MemberController { }; return await this.membersService.getRolesWithCount(builtQuery, queryParams); } - + /** * Retrieves member filters. * @@ -166,30 +166,12 @@ export class MemberController { ) { throw new ForbiddenException(`Member isn't authorized to update the member`); } - if(!isEmpty(participantsRequest.newData.isVerified) && !this.membersService.checkIfAdminUser(requestor)) { + if (!isEmpty(participantsRequest.newData.isVerified) && !this.membersService.checkIfAdminUser(requestor)) { throw new ForbiddenException(`Member isn't authorized to verify a member`); } return await this.membersService.updateMemberFromParticipantsRequest(uid, participantsRequest, requestor.email); } - /** - * Updates a member to be verfied user. - * - * @param body - array of memberIds to be updated. - * @returns Array of updation status of the provided memberIds. - */ - @Api(server.route.verifyMembers) - @UseGuards(UserAccessTokenValidateGuard) - async verifyMembers(@Body() body, @Req() request) { - const requestor = await this.membersService.findMemberByEmail(request.userEmail); - if(!this.membersService.checkIfAdminUser(requestor)) { - throw new ForbiddenException(`Member isn't authorized to verify members`); - } - const { memberIds } = body; - return await this.membersService.verifyMembers(memberIds, requestor.email); - - } - /** * Updates a member's preference settings. * @@ -207,7 +189,7 @@ export class MemberController { @Api(server.route.updateMember) @UseGuards(UserTokenValidation) - async updateMemberByUid(@Param('uid') uid, @Body() body, @Req() req) { + async updateMemberByUid(@Param('uid') uid, @Body() body, @Req() req) { this.logger.info(`Member update request - Initated by -> ${req.userEmail}`); const requestor = await this.membersService.findMemberByEmail(req.userEmail); if ( @@ -216,7 +198,7 @@ export class MemberController { ) { throw new ForbiddenException(`Member isn't authorized to update the member`); } - if(!isEmpty(body.isVerified) && !this.membersService.checkIfAdminUser(requestor)) { + if (!isEmpty(body.isVerified) && !this.membersService.checkIfAdminUser(requestor)) { throw new ForbiddenException(`Member isn't authorized to verify a member`); } return await this.membersService.updateMemberByUid(uid, body); diff --git a/apps/web-api/src/members/members.service.ts b/apps/web-api/src/members/members.service.ts index b7e70fcee..5a6000e07 100644 --- a/apps/web-api/src/members/members.service.ts +++ b/apps/web-api/src/members/members.service.ts @@ -43,7 +43,7 @@ export class MembersService { private notificationService: NotificationService, @Inject(CACHE_MANAGER) private cacheService: Cache - ) {} + ) { } /** * Creates a new member in the database within a transaction. @@ -60,9 +60,9 @@ export class MembersService { return await tx.member.create({ data: member, }); - } catch(error) { + } catch (error) { return this.handleErrors(error); - } + } } /** @@ -75,14 +75,14 @@ export class MembersService { * the members. These options are based on Prisma's `MemberFindManyArgs`. * @returns A promise that resolves to an array of member records matching the query criteria. */ - async findAll(queryOptions: Prisma.MemberFindManyArgs): Promise<{count:Number, members:Member[]}> { + async findAll(queryOptions: Prisma.MemberFindManyArgs): Promise<{ count: Number, members: Member[] }> { try { const [members, membersCount] = await Promise.all([ this.prisma.member.findMany(queryOptions), this.prisma.member.count({ where: queryOptions.where }), ]); return { count: membersCount, members: members } - } catch(error) { + } catch (error) { return this.handleErrors(error); } } @@ -210,7 +210,7 @@ export class MembersService { where: { uid }, data: member, }); - } catch(error) { + } catch (error) { return this.handleErrors(error); } } @@ -259,7 +259,7 @@ export class MembersService { }, }, }); - } catch(error) { + } catch (error) { return this.handleErrors(error); } } @@ -281,8 +281,8 @@ export class MembersService { teamMemberRoles: true, projectContributions: true, } - }); - } catch(error) { + }); + } catch (error) { return this.handleErrors(error); } } @@ -292,20 +292,20 @@ export class MembersService { * @param tx - Prisma transaction client or Prisma client. * @param uid - Member UID to fetch. */ - async findMemberByUid(uid: string, tx: Prisma.TransactionClient = this.prisma){ + async findMemberByUid(uid: string, tx: Prisma.TransactionClient = this.prisma) { try { return tx.member.findUniqueOrThrow({ where: { uid }, - include: { - image: true, - location: true, - skills: true, - teamMemberRoles: true, - memberRoles: true, + include: { + image: true, + location: true, + skills: true, + teamMemberRoles: true, + memberRoles: true, projectContributions: true }, - }); - } catch(error) { + }); + } catch (error) { return this.handleErrors(error); } } @@ -320,11 +320,11 @@ export class MembersService { try { return await this.prisma.member.findUniqueOrThrow({ where: { email: email.toLowerCase().trim() }, - include: { - memberRoles: true + include: { + memberRoles: true }, }); - } catch(error) { + } catch (error) { return this.handleErrors(error); } } @@ -364,7 +364,7 @@ export class MembersService { .filter((role) => role.teamLead) .map((role) => role.teamUid), }; - } catch(error) { + } catch (error) { return this.handleErrors(error); } } @@ -397,7 +397,7 @@ export class MembersService { * * @throws If any operation within the transaction fails, the entire transaction is rolled back. */ - async updateMemberEmail(newEmail:string, oldEmail:string, memberInfo) { + async updateMemberEmail(newEmail: string, oldEmail: string, memberInfo) { try { let newTokens; let newMemberInfo; @@ -408,22 +408,23 @@ export class MembersService { referenceUid: memberInfo.uid, uniqueIdentifier: oldEmail, participantType: 'MEMBER', - newData: { - oldEmail: oldEmail, - email: newEmail - }}, + newData: { + oldEmail: oldEmail, + email: newEmail + } + }, false, tx ); newMemberInfo = await tx.member.update({ - where: { email: oldEmail.toLowerCase().trim()}, - data: { email: newEmail.toLowerCase().trim()}, - include: { - memberRoles: true, - image: true, - teamMemberRoles: true, - } - }) + where: { email: oldEmail.toLowerCase().trim() }, + data: { email: newEmail.toLowerCase().trim() }, + include: { + memberRoles: true, + image: true, + teamMemberRoles: true, + } + }) newTokens = await this.authService.updateEmailInAuth(newEmail, oldEmail, memberInfo.externalId) }); this.logger.info(`Email has been successfully updated from ${oldEmail} to ${newEmail}`) @@ -434,7 +435,7 @@ export class MembersService { accessToken: newTokens.access_token, userInfo: this.memberToUserInfo(newMemberInfo) }; - } catch(error) { + } catch (error) { return this.handleErrors(error); } } @@ -488,7 +489,7 @@ export class MembersService { where: { email: emailId.toLowerCase().trim() }, data: { externalId }, }); - } catch(error){ + } catch (error) { return this.handleErrors(error); } } @@ -506,7 +507,7 @@ export class MembersService { select: { githubHandler: true }, }); return member?.githubHandler || null; - } catch(error) { + } catch (error) { return this.handleErrors(error); } } @@ -631,7 +632,7 @@ export class MembersService { await this.mapLocationToMember(memberData, null, member, tx); return await this.createMember(member, tx); } - + async updateMemberFromParticipantsRequest( memberUid: string, memberParticipantsRequest: ParticipantsRequest, @@ -647,11 +648,11 @@ export class MembersService { const member = await this.prepareMemberFromParticipantRequest(memberUid, memberData, existingMember, tx, 'Update'); await this.mapLocationToMember(memberData, existingMember, member, tx); result = await this.updateMemberByUid( - memberUid, + memberUid, { ...member, ...(isEmailChanged && isExternalIdAvailable && { externalId: null }), - }, + }, tx ); await this.updateMemberEmailChange(memberUid, isEmailChanged, isExternalIdAvailable, memberData, existingMember); @@ -662,7 +663,7 @@ export class MembersService { await this.postUpdateActions(); return result; } - + /** * Checks if the email has changed during update and verifies if the new email is already in use. * @@ -707,15 +708,15 @@ export class MembersService { const member: any = {}; const directFields = [ 'name', 'email', 'githubHandler', 'discordHandler', 'bio', - 'twitterHandler', 'linkedinHandler', 'telegramHandler', + 'twitterHandler', 'linkedinHandler', 'telegramHandler', 'officeHours', 'moreDetails', 'plnStartDate', 'openToWork', - 'isVerified', 'signUpSource', 'isUserConsent', 'isSubscribedToNewsletter', - 'teamOrProjectURL' + 'isVerified', 'signUpSource', 'isUserConsent', 'isSubscribedToNewsletter', + 'teamOrProjectURL' ]; copyObj(memberData, member, directFields); member.email = member.email.toLowerCase().trim(); - member['image'] = memberData.imageUid ? { connect: { uid: memberData.imageUid } } - : type === 'Update' ? { disconnect: true } : undefined ; + member['image'] = memberData.imageUid ? { connect: { uid: memberData.imageUid } } + : type === 'Update' ? { disconnect: true } : undefined; member['skills'] = buildMultiRelationMapping('skills', memberData, type); if (type === 'Create') { member['teamMemberRoles'] = this.buildTeamMemberRoles(memberData); @@ -726,7 +727,7 @@ export class MembersService { } } else { await this.updateProjectContributions(memberData, existingMember, memberUid, tx); - await this.updateTeamMemberRoles(memberData, existingMember, memberUid, tx); + await this.updateTeamMemberRoles(memberData, existingMember, memberUid, tx); } return member; } @@ -768,7 +769,7 @@ export class MembersService { throw new BadRequestException('Invalid Location info'); } } else { - if (existingMember) { + if (existingMember) { member['location'] = { disconnect: true }; } } @@ -928,7 +929,7 @@ export class MembersService { }, }; } - + /** * function to handle creation, updating, and deletion of project contributions * with fewer database calls by using batch operations. @@ -1110,7 +1111,7 @@ export class MembersService { participantType: 'MEMBER', newData: { ...newMemberData }, }, - tx + tx ); } @@ -1120,11 +1121,11 @@ export class MembersService { * @param userEmail logged in member email * @returns result */ - async verifyMembers(memberIds: string[], userEmail:string): Promise { - return await this.prisma.$transaction(async (tx) => { + async verifyMembers(memberIds: string[], userEmail: any): Promise { + return await this.prisma.$transaction(async (tx) => { const result = await tx.member.updateMany({ where: { uid: { in: memberIds } }, - data: { + data: { isVerified: true } }); @@ -1134,7 +1135,7 @@ export class MembersService { const members = await tx.member.findMany({ where: { uid: { in: memberIds } } }) - await Promise.all(members.map(async(member) => { + await Promise.all(members.map(async (member) => { await this.participantsRequestService.add({ status: 'AUTOAPPROVED', requesterEmailId: userEmail, @@ -1148,7 +1149,7 @@ export class MembersService { isVerified: true }, }, - tx + tx ); })); return result; @@ -1265,7 +1266,7 @@ export class MembersService { * @returns Constructed query with a 'createdAt' filter if 'recent' is set to 'true', * or an empty object if 'recent' is not provided or set to 'false'. */ - buildRecentMembersFilter(queryParams) { + buildRecentMembersFilter(queryParams) { const { isRecent } = queryParams; if (isRecent === 'true') { return { @@ -1284,7 +1285,7 @@ export class MembersService { * @returns Constructed query based on given member role input */ buildRoleFilters(queryParams) { - const { memberRoles } : any = queryParams; + const { memberRoles }: any = queryParams; const roles = memberRoles?.split(','); if (roles?.length > 0) { return { @@ -1309,39 +1310,41 @@ export class MembersService { if (name__icontains) { return { OR: [ - { name: { + { + name: { contains: name__icontains, mode: 'insensitive' } }, - { teamMemberRoles : { + { + teamMemberRoles: { some: { - team: { + team: { name: { contains: name__icontains, mode: 'insensitive' - } - } - } + } + } + } } }, - { - projectContributions : { + { + projectContributions: { some: { - project: { + project: { name: { contains: name__icontains, mode: 'insensitive' }, - isDeleted: false - } - } + isDeleted: false + } + } } } ] } } - return { }; + return {}; } /** @@ -1391,7 +1394,7 @@ export class MembersService { }; } - + /** * Updates the member's field if the value has changed. @@ -1481,7 +1484,7 @@ export class MembersService { } return error; } - + async insertManyWithLocationsFromAirtable( airtableMembers: z.infer[] @@ -1570,4 +1573,20 @@ export class MembersService { }); } } + + async findMemberByRole() { + const member = await this.prisma.member.findFirst({ + where: { + memberRoles: { + some: { + name: 'DIRECTORYADMIN', // Adjust this based on the actual field name in your schema + }, + }, + }, + select: { + email: true + } + }); + return member; + } } diff --git a/libs/contracts/src/lib/contract-member.ts b/libs/contracts/src/lib/contract-member.ts index a656ad0b7..66eba60ac 100644 --- a/libs/contracts/src/lib/contract-member.ts +++ b/libs/contracts/src/lib/contract-member.ts @@ -81,15 +81,6 @@ export const apiMembers = contract.router({ }, summary: 'Modify a member', }, - verifyMembers: { - method: 'POST', - path: `${getAPIVersionAsPath('1')}/members`, - body: contract.body(), - responses: { - 200: contract.response(), - }, - summary: 'Verify members', - }, sendOtpForEmailChange: { method: 'POST', path: `${getAPIVersionAsPath('1')}/members/:uid/email/otp`, From a5f8fe2506d7ccd24108b9fdbc7d0d8b067104f8 Mon Sep 17 00:00:00 2001 From: prasanth Date: Wed, 4 Dec 2024 01:57:06 +0530 Subject: [PATCH 25/41] fix: footer changes for edit details component --- .../footer-buttons/footer-buttons.tsx | 143 ++++++++---------- .../public/assets/icons/TrashIcon.svg | 3 + .../public/assets/icons/upgrade-rounded.svg | 3 + 3 files changed, 70 insertions(+), 79 deletions(-) create mode 100644 apps/back-office/public/assets/icons/TrashIcon.svg create mode 100644 apps/back-office/public/assets/icons/upgrade-rounded.svg diff --git a/apps/back-office/components/footer-buttons/footer-buttons.tsx b/apps/back-office/components/footer-buttons/footer-buttons.tsx index 84e2d4ae8..0afac22c6 100644 --- a/apps/back-office/components/footer-buttons/footer-buttons.tsx +++ b/apps/back-office/components/footer-buttons/footer-buttons.tsx @@ -3,6 +3,7 @@ import api from '../../utils/api'; import router from 'next/router'; import APP_CONSTANTS, { API_ROUTE, + ENROLLMENT_TYPE, ROUTE_CONSTANTS, } from '../../utils/constants'; import { toast } from 'react-toastify'; @@ -11,19 +12,12 @@ export function FooterButtons(props) { const saveButtonClassName = props.disableSave ? 'shadow-special-button-default inline-flex w-full justify-center rounded-full bg-slate-400 px-6 py-2 text-base font-semibold leading-6 text-white outline-none' : 'on-focus leading-3.5 text-md mb-2 mr-2 flex items-center rounded-full border border-blue-600 bg-blue-600 px-4 py-3 text-left font-medium text-white last:mr-0 focus-within:rounded-full hover:border-slate-400 focus:rounded-full focus-visible:rounded-full'; - async function handleAprroveOrReject( - id, - type, - referenceUid, - isApproved, - setLoader - ) { + async function approvelClickHandler(id: any, status: any, isVerified: any, setLoader) { const data = { - status: isApproved - ? APP_CONSTANTS.APPROVED_FLAG - : APP_CONSTANTS.REJECTED_FLAG, - participantType: type, - ...(referenceUid && { referenceUid: referenceUid }), + status: status, + participantType: ENROLLMENT_TYPE.MEMBER, + isVerified, + uid: id, }; const configuration = { headers: { @@ -31,35 +25,31 @@ export function FooterButtons(props) { }, }; setLoader(true); - await api - .patch(`${API_ROUTE.PARTICIPANTS_REQUEST}/${id}`, data, configuration) - .then((res) => { - if (res?.data?.code == 1) { - const message = `${ - isApproved - ? APP_CONSTANTS.APPROVED_LABEL - : APP_CONSTANTS.REJECTED_LABEL - } successfully`; - toast(message); - } else { - toast(res?.data?.message); - } + try { + let message = ""; + setLoader(true); + await api.patch(`${API_ROUTE.PARTICIPANTS_REQUEST}/${id}`, data, configuration) + message = status === "REJECTED" + ? `Successfully ${APP_CONSTANTS.REJECTED_LABEL}` + : `Successfully ${isVerified ? APP_CONSTANTS.VERIFIED_FLAG : APP_CONSTANTS.UNVERIFIED_FLAG}`; + + toast(message); + router.push({ + pathname: ROUTE_CONSTANTS.PENDING_LIST, + }); + } catch (error: any) { + if (error.response?.status === 500) { router.push({ - pathname: ROUTE_CONSTANTS.PENDING_LIST, + pathname: ROUTE_CONSTANTS.INTERNAL_SERVER_ERROR, }); - }) - .catch((e) => { - if (e.response.status === 500) { - router.push({ - pathname: ROUTE_CONSTANTS.INTERNAL_SERVER_ERROR, - }); - } else if (e.response.status === 400) { - toast(e?.response?.data?.message); - } else { - toast(e?.message); - } - }) - .finally(() => setLoader(false)); + } else if (error.response?.status === 400) { + toast(error.response?.data?.message || 'Bad request'); + } else { + toast(error.message || 'An unexpected error occurred'); + } + } finally { + setLoader(false); + } } return ( @@ -85,46 +75,41 @@ export function FooterButtons(props) {
-
- -
-
- -
+ + + + +
diff --git a/apps/back-office/public/assets/icons/TrashIcon.svg b/apps/back-office/public/assets/icons/TrashIcon.svg new file mode 100644 index 000000000..de038b51c --- /dev/null +++ b/apps/back-office/public/assets/icons/TrashIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/back-office/public/assets/icons/upgrade-rounded.svg b/apps/back-office/public/assets/icons/upgrade-rounded.svg new file mode 100644 index 000000000..c9cec6c84 --- /dev/null +++ b/apps/back-office/public/assets/icons/upgrade-rounded.svg @@ -0,0 +1,3 @@ + + + From c464add84ec074d823bbad64822c1f1d11980ece Mon Sep 17 00:00:00 2001 From: Vellaiyan-Marimuthu Date: Wed, 4 Dec 2024 02:37:55 +0530 Subject: [PATCH 26/41] Fix: Approval api issue fixed --- .../components/member-request-list.tsx | 203 +++++++++++------- .../components/member-table/member-table.tsx | 78 ++----- apps/back-office/components/menu/menu.tsx | 2 + apps/back-office/components/request-list.tsx | 15 +- apps/back-office/components/tab/tab.tsx | 3 +- apps/back-office/pages/pending-list.tsx | 4 +- apps/back-office/utils/constants.ts | 1 + 7 files changed, 156 insertions(+), 150 deletions(-) diff --git a/apps/back-office/components/member-request-list.tsx b/apps/back-office/components/member-request-list.tsx index 422e35745..204341870 100644 --- a/apps/back-office/components/member-request-list.tsx +++ b/apps/back-office/components/member-request-list.tsx @@ -1,32 +1,71 @@ -import { Fragment, useState } from 'react'; -import APP_CONSTANTS, { ROUTE_CONSTANTS } from '../utils/constants'; +import { Fragment, useEffect, useState } from 'react'; +import APP_CONSTANTS, { API_ROUTE, ENROLLMENT_TYPE, ROUTE_CONSTANTS } from '../utils/constants'; import MemberTable from './member-table/member-table'; import Tab from './tab/tab'; import Loader from './common/loader'; import router from 'next/router'; - +import api from '../utils/api'; +import { useNavbarContext } from '../context/navbar-context'; const MemberRequestList = (props: any) => { - const dataList = props?.members; + const dataList = [...props.members]; const type = props?.type; - const [allMembers, setAllMembers]: any = useState(() => getFilterMembes(dataList ?? [])); const [currentTab, setCurrentTab] = useState(APP_CONSTANTS.PENDING_FLAG); const [isLoading, setIsLoading] = useState(false); - function getFilterMembes(data: any) { - const pending = data.filter((item: any) => item.status === APP_CONSTANTS.PENDING_LABEL); - const unverified = data.filter((item: any) => item.isVerified === false ); - return { - pending, - unverified, + const [allMembers, setAllMembers] = useState({ pending: [], unverified: [] }); + const { setMemberList } = useNavbarContext(); + + useEffect(() => { + if (type !== APP_CONSTANTS.CLOSED_FLAG) { + updateMembers(); + } + }, []); + + const updateMembers = async () => { + setIsLoading(true); + const config = { + headers: { + authorization: `Bearer ${props.plnadmin}`, + }, }; - } + const [listData, unVerifiedMembers] = await Promise.all([ + api.get(`${API_ROUTE.PARTICIPANTS_REQUEST}?status=PENDING`, config), + api.get(`${API_ROUTE.MEMBERS}?isVerified=false&pagination=false`, config), + ]); + + const pendingMembers = listData.data.filter((item) => item.participantType === ENROLLMENT_TYPE.MEMBER); + + const formattedPendingMembes = pendingMembers?.map((data) => { + return { + id: data.uid, + name: data.newData.name, + status: data.status, + }; + }); + + const filteredUnVerifiedMembers = unVerifiedMembers.data.members.map((data) => { + return { + id: data.uid, + name: data.name, + isVerified: data?.isVerified || false, + }; + }); + + setAllMembers({ + pending: [...formattedPendingMembes], + unverified: [...filteredUnVerifiedMembers], + }); + setIsLoading(false); + setMemberList([...formattedPendingMembes, ...filteredUnVerifiedMembers]); + }; const availableTabs = [ - { label: APP_CONSTANTS.PENDING_LABEL, name: APP_CONSTANTS.PENDING_FLAG }, + { label: APP_CONSTANTS.PENDING_LABEL, name: APP_CONSTANTS.PENDING_FLAG, count: allMembers.pending.length }, { label: APP_CONSTANTS.UNVERIFIED_LABEL, name: APP_CONSTANTS.UNVERIFIED_FLAG, + count: allMembers.unverified.length, }, ]; @@ -36,11 +75,11 @@ const MemberRequestList = (props: any) => { const onTabClickHandler = (name: string) => { onTabSelected(name); - } + }; function redirectToDetail(request) { setIsLoading(true); - const route = ROUTE_CONSTANTS.MEMBER_VIEW + const route = ROUTE_CONSTANTS.MEMBER_VIEW; router.push({ pathname: route, query: { @@ -49,74 +88,78 @@ const MemberRequestList = (props: any) => { }); } - return <> -
- {isLoading && } - - {type !== APP_CONSTANTS.CLOSED_FLAG && ( - <> -
- {availableTabs.map((tab: any, index: number) => ( - - - - ))} -
-
- -
- - )} - - {type === APP_CONSTANTS.CLOSED_FLAG && ( - <> - {dataList && - dataList.map((request, index) => { - const borderClass = - dataList.length == 1 - ? 'rounded-xl' - : index == 0 - ? 'rounded-tl-xl rounded-tr-xl' - : index == dataList.length - 1 - ? 'rounded-bl-xl rounded-br-xl' - : ''; - return ( -
redirectToDetail(request)} - > + return ( + <> +
+ {isLoading && } + + {type !== APP_CONSTANTS.CLOSED_FLAG && ( + <> +
+ {availableTabs.map((tab: any, index: number) => ( + + + + ))} +
+
+ +
+ + )} + + {type === APP_CONSTANTS.CLOSED_FLAG && ( + <> + {dataList && + dataList.map((request, index) => { + const borderClass = + dataList.length == 1 + ? 'rounded-xl' + : index == 0 + ? 'rounded-tl-xl rounded-tr-xl' + : index == dataList.length - 1 + ? 'rounded-bl-xl rounded-br-xl' + : ''; + return (
redirectToDetail(request)} > - - {request?.name} - - {request.status !== APP_CONSTANTS.PENDING_LABEL && ( - - {request.status === 'REJECTED' - ? APP_CONSTANTS.REJECTED_LABEL - : APP_CONSTANTS.APPROVED_LABEL} - - )} +
+ {request?.name} + {request.status !== APP_CONSTANTS.PENDING_LABEL && ( + + {request.status === 'REJECTED' ? APP_CONSTANTS.REJECTED_LABEL : APP_CONSTANTS.APPROVED_LABEL} + + )} +
-
- ); - })} - - - - )} -
- + ); + })} + + )} +
+ + ); }; export default MemberRequestList; diff --git a/apps/back-office/components/member-table/member-table.tsx b/apps/back-office/components/member-table/member-table.tsx index e9f14fd0c..3f64e875d 100644 --- a/apps/back-office/components/member-table/member-table.tsx +++ b/apps/back-office/components/member-table/member-table.tsx @@ -1,15 +1,15 @@ import api from 'apps/back-office/utils/api'; import APP_CONSTANTS, { API_ROUTE, ENROLLMENT_TYPE, ROUTE_CONSTANTS } from 'apps/back-office/utils/constants'; import router from 'next/router'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import Loader from '../common/loader'; import { useNavbarContext } from 'apps/back-office/context/navbar-context'; const MemberTable = (props: any) => { - const members = props?.members ?? []; - const setAllMembers = props?.setAllMembers; const selectedTab = props?.selectedTab ?? ''; + const allMembers = props?.allMembers ?? []; + const updateMembers = props?.updateMembers; const [isAllSelected, setIsAllSelected] = useState(false); const [selectedMembers, setSelectedMembes] = useState([]); @@ -17,7 +17,6 @@ const MemberTable = (props: any) => { const [isSort, setIsSort] = useState(false); const { setMemberList } = useNavbarContext(); - // const onSortClickHandler = () => { // setIsSort(!isSort); // const sortedMembers = members.sort((a: any, b: any) => { @@ -35,7 +34,7 @@ const MemberTable = (props: any) => { if (isAllSelected) { setSelectedMembes([]); } else { - setSelectedMembes(members.map((member: any) => member.id)); + setSelectedMembes(allMembers.map((member: any) => member.id)); } }; @@ -43,14 +42,14 @@ const MemberTable = (props: any) => { if (selectedMembers.includes(id)) { setIsAllSelected(false); const filteredMembes = selectedMembers.filter((uid) => uid !== id); - setSelectedMembes([...filteredMembes]); - if (filteredMembes.length === members.length) { + setSelectedMembes(filteredMembes); + if (filteredMembes.length === allMembers.length) { setIsAllSelected(true); } } else { const addedMembes = [...selectedMembers, id]; setSelectedMembes(addedMembes); - if (addedMembes.length === members.length) { + if (addedMembes.length === allMembers.length) { setIsAllSelected(true); } } @@ -67,45 +66,12 @@ const MemberTable = (props: any) => { }); } - const onSuccessHandler = async () => { - setIsLoading(true); - const config = { - headers: { - authorization: `Bearer ${props.plnadmin}`, - }, - }; - const listData = await api.get(`${API_ROUTE.PARTICIPANTS_REQUEST}?status=PENDING`, config); - const unVerifiedMembes = await api.get(`${API_ROUTE.MEMBERS}?isVerified=false&pagination=false`, config); - const pendingMembers = listData.data.filter((item) => item.participantType === ENROLLMENT_TYPE.MEMBER); - - const formattedPendingMembes = pendingMembers?.map((data) => { - return { - id: data.uid, - name: data.newData.name, - status: data.status, - }; - }); - const filteredUnVerifiedMembers = unVerifiedMembes.data.members.map((data) => { - return { - id: data.uid, - name: data.name, - isVerified: data?.isVerified || false, - }; - }); - - setMemberList([...formattedPendingMembes, ...filteredUnVerifiedMembers]); - - setAllMembers({ - pending: formattedPendingMembes, - unverified: filteredUnVerifiedMembers, - }); - }; - async function approvelClickHandler(id: any, status: any, isVerified: any) { const data = { status: status, participantType: ENROLLMENT_TYPE.MEMBER, isVerified, + uid: id, }; const configuration = { headers: { @@ -114,19 +80,17 @@ const MemberTable = (props: any) => { }; setIsLoading(true); try { - let message=""; + let message = ''; setIsLoading(true); if (selectedTab === APP_CONSTANTS.PENDING_FLAG) { - await api.patch(`${API_ROUTE.PARTICIPANTS_REQUEST}/${id}`, data, configuration); - message = `Successfully ${APP_CONSTANTS.UNVERIFIED_FLAG}`; - + await api.post(`${API_ROUTE.PARTICIPANTS_REQUEST}`, [data], configuration); + message = `Successfully ${(status === APP_CONSTANTS.REJECTED_FLAG ? APP_CONSTANTS.REJECTED_LABEL : APP_CONSTANTS.VERIFIED_FLAG)}`; } else { - await api.post(`${API_ROUTE.MEMBERS}/${id}`, [id], configuration); - message = `Successfully ${APP_CONSTANTS.VERIFIED_FLAG}`; - + await api.post(`${API_ROUTE.ADMIN_APPROVAL}`, { memberIds: [id] }, configuration); + message = `Successfully ${APP_CONSTANTS.VERIFIED_FLAG}`; } - onSuccessHandler(); + updateMembers(); toast(message); } catch (error: any) { if (error.response?.status === 500) { @@ -158,16 +122,16 @@ const MemberTable = (props: any) => { }, }; try { - setIsAllSelected(false); setSelectedMembes([]); setIsLoading(true); if (selectedTab === APP_CONSTANTS.PENDING_FLAG) { await api.post(`${API_ROUTE.PARTICIPANTS_REQUEST}`, data, configuration); } else { const data = selectedMembers?.map((memberId: any) => memberId); - await api.post(`${API_ROUTE.MEMBERS}`, { memberIds: data }, configuration); + await api.post(`${API_ROUTE.ADMIN_APPROVAL}`, { memberIds: data }, configuration); } - onSuccessHandler(); + updateMembers(); + setIsAllSelected(false); const message = `Successfully ${APP_CONSTANTS.APPROVED_LABEL}`; toast(message); } catch (error: any) { @@ -201,7 +165,7 @@ const MemberTable = (props: any) => { try { setIsLoading(true); await api.post(`${API_ROUTE.PARTICIPANTS_REQUEST}`, data, configuration); - onSuccessHandler(); + updateMembers(); setSelectedMembes([]); setIsAllSelected(false); const message = `Successfully ${APP_CONSTANTS.REJECTED_LABEL}`; @@ -224,7 +188,7 @@ const MemberTable = (props: any) => { return ( <> {isLoading && } - {members?.length > 0 && ( + {allMembers?.length > 0 && (
{/* Header */}
@@ -246,7 +210,7 @@ const MemberTable = (props: any) => {
{/* Members */}
- {members?.map((member: any, index: number) => { + {allMembers?.map((member: any, index: number) => { const isSelected = selectedMembers.includes(member.id) || isAllSelected; const isDisableOptions = selectedMembers.length > 0; return ( @@ -337,7 +301,7 @@ const MemberTable = (props: any) => {
)} - {members?.length === 0 && ( + {allMembers?.length === 0 && (
) => JSX.Element; @@ -18,6 +19,7 @@ export function Menu() { setIsTeamActive, isTeamActive, isOpenRequest, + setMemberList, } = useNavbarContext(); // const [isTeamActive, setIsTeamActive] = useState(true); const MENU_ITEMS: IMenuItem[] = [ diff --git a/apps/back-office/components/request-list.tsx b/apps/back-office/components/request-list.tsx index d1ac83ab8..0033aea30 100644 --- a/apps/back-office/components/request-list.tsx +++ b/apps/back-office/components/request-list.tsx @@ -7,8 +7,8 @@ import { useNavbarContext } from '../context/navbar-context'; import MemberRequestList from './member-request-list'; export default function RequestList({ list, type, plnadmin }) { - const [dataList, setDataList] = useState([]); - const { isTeamActive } = useNavbarContext(); + const [dataList, setDataList] = useState(list); + const { isTeamActive, setMemberList } = useNavbarContext(); useEffect(() => { setDataList(list); @@ -46,14 +46,9 @@ export default function RequestList({ list, type, plnadmin }) { />
)} - {dataList.length > 0 && ( - <> - {isTeamActive && } - {!isTeamActive && } - - )} - - {dataList.length === 0 && ( + {dataList.length > 0 && <>{isTeamActive && }} + {!isTeamActive && } + {dataList.length === 0 && isTeamActive && (
{ const name=props?.name ?? ""; const isSelected = props?.isSelected ?? false; const onTabClickHandler = props?.onClick; + const count = props?.count ?? 0; return ( - + ) } diff --git a/apps/back-office/pages/pending-list.tsx b/apps/back-office/pages/pending-list.tsx index 2745d265c..aaaafa1a6 100644 --- a/apps/back-office/pages/pending-list.tsx +++ b/apps/back-office/pages/pending-list.tsx @@ -9,7 +9,7 @@ import { ApprovalLayout } from '../layout/approval-layout'; import { parseCookies } from 'nookies'; export default function PendingList(props) { - const { setIsOpenRequest, setMemberList, setTeamList, isTeamActive, setShowMenu } = useNavbarContext(); + const { setIsOpenRequest, setMemberList, setTeamList, isTeamActive, setShowMenu, memberList, teamList } = useNavbarContext(); setShowMenu(true); useEffect(() => { @@ -22,7 +22,7 @@ export default function PendingList(props) { diff --git a/apps/back-office/utils/constants.ts b/apps/back-office/utils/constants.ts index a7bf20125..91ed19d14 100644 --- a/apps/back-office/utils/constants.ts +++ b/apps/back-office/utils/constants.ts @@ -44,6 +44,7 @@ export const API_ROUTE = { INDUSTRIES: APP_CONSTANTS.V1 + 'industry-tags', TECHNOLOGIES: APP_CONSTANTS.V1 + 'technologies', MEMBERS: APP_CONSTANTS.V1 + 'members', + ADMIN_APPROVAL: APP_CONSTANTS.V1 + 'admin/members', }; export const TOKEN = 'plnetwork@1'; From 823867b44c70c34096de6f7cf068fa6974d6c475 Mon Sep 17 00:00:00 2001 From: Navaneethakrishnan Date: Wed, 4 Dec 2024 09:54:40 +0530 Subject: [PATCH 27/41] feat: disable cache in members for testing --- apps/web-api/src/members/members.controller.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/web-api/src/members/members.controller.ts b/apps/web-api/src/members/members.controller.ts index 366478e3b..c09670ef7 100644 --- a/apps/web-api/src/members/members.controller.ts +++ b/apps/web-api/src/members/members.controller.ts @@ -46,6 +46,7 @@ export class MemberController { @ApiQueryFromZod(MemberQueryParams) @ApiOkResponseFromZod(ResponseMemberWithRelationsSchema.array()) @UseInterceptors(IsVerifiedMemberInterceptor) + @NoCache() async findAll(@Req() request: Request) { const queryableFields = prismaQueryableFieldsFromZod(ResponseMemberWithRelationsSchema); const queryParams = request.query; @@ -75,6 +76,7 @@ export class MemberController { */ @Api(server.route.getMemberRoles) @UseInterceptors(IsVerifiedMemberInterceptor) + @NoCache() async getMemberRoleFilters(@Req() request: Request) { const queryableFields = prismaQueryableFieldsFromZod(ResponseMemberWithRelationsSchema); const queryParams = request.query; @@ -102,6 +104,7 @@ export class MemberController { */ @Api(server.route.getMemberFilters) @UseInterceptors(IsVerifiedMemberInterceptor) + @NoCache() async getMembersFilter(@Req() request: Request) { const queryableFields = prismaQueryableFieldsFromZod(ResponseMemberWithRelationsSchema); const queryParams = request.query; From 5dd781808bc7cbd215c543a6f42585305c49dfc6 Mon Sep 17 00:00:00 2001 From: navneethkrish Date: Wed, 4 Dec 2024 10:27:36 +0530 Subject: [PATCH 28/41] fix(field validation): added conditional check for member roles --- apps/web-api/src/members/members.service.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/web-api/src/members/members.service.ts b/apps/web-api/src/members/members.service.ts index 5a6000e07..bcdae87c3 100644 --- a/apps/web-api/src/members/members.service.ts +++ b/apps/web-api/src/members/members.service.ts @@ -44,6 +44,7 @@ export class MembersService { @Inject(CACHE_MANAGER) private cacheService: Cache ) { } + ) { } /** * Creates a new member in the database within a transaction. @@ -719,7 +720,9 @@ export class MembersService { : type === 'Update' ? { disconnect: true } : undefined; member['skills'] = buildMultiRelationMapping('skills', memberData, type); if (type === 'Create') { - member['teamMemberRoles'] = this.buildTeamMemberRoles(memberData); + if (Array.isArray(memberData.teamMemberRoles)) { + member['teamMemberRoles'] = this.buildTeamMemberRoles(memberData); + } if (Array.isArray(memberData.projectContributions)) { member['projectContributions'] = { createMany: { data: memberData.projectContributions }, @@ -1121,8 +1124,8 @@ export class MembersService { * @param userEmail logged in member email * @returns result */ - async verifyMembers(memberIds: string[], userEmail: any): Promise { - return await this.prisma.$transaction(async (tx) => { + async verifyMembers(memberIds: string[], userEmail:string): Promise { + return await this.prisma.$transaction(async (tx) => { const result = await tx.member.updateMany({ where: { uid: { in: memberIds } }, data: { From 67ffaf3bbb47597227200c203a8f5ebdb7886b9d Mon Sep 17 00:00:00 2001 From: Navaneethakrishnan Date: Wed, 4 Dec 2024 11:00:17 +0530 Subject: [PATCH 29/41] fix: fixed issue member type --- apps/web-api/src/members/members.service.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/web-api/src/members/members.service.ts b/apps/web-api/src/members/members.service.ts index bcdae87c3..815f1134c 100644 --- a/apps/web-api/src/members/members.service.ts +++ b/apps/web-api/src/members/members.service.ts @@ -42,8 +42,6 @@ export class MembersService { @Inject(forwardRef(() => NotificationService)) private notificationService: NotificationService, @Inject(CACHE_MANAGER) private cacheService: Cache - - ) { } ) { } /** @@ -1124,7 +1122,7 @@ export class MembersService { * @param userEmail logged in member email * @returns result */ - async verifyMembers(memberIds: string[], userEmail:string): Promise { + async verifyMembers(memberIds: string[], userEmail): Promise { return await this.prisma.$transaction(async (tx) => { const result = await tx.member.updateMany({ where: { uid: { in: memberIds } }, From 5557a6d7a756e6a1aa3407ea8cc28cd887f00e09 Mon Sep 17 00:00:00 2001 From: Navaneethakrishnan Date: Tue, 3 Dec 2024 23:40:59 +0530 Subject: [PATCH 30/41] feat: added cache revalidation API integration --- .env.example | 4 +- apps/web-api/src/auth/auth.module.ts | 3 +- apps/web-api/src/members/members.service.ts | 13 ++--- .../participants-request.service.ts | 24 ++++---- .../src/pl-events/pl-event-guests.service.ts | 10 ++-- apps/web-api/src/projects/projects.service.ts | 12 ++-- apps/web-api/src/shared/shared.module.ts | 3 + apps/web-api/src/teams/teams.service.ts | 9 ++- apps/web-api/src/utils/cache/cache.service.ts | 58 +++++++++++++++++++ apps/web-api/src/utils/redis/redis.service.ts | 24 -------- 10 files changed, 97 insertions(+), 63 deletions(-) create mode 100644 apps/web-api/src/utils/cache/cache.service.ts delete mode 100644 apps/web-api/src/utils/redis/redis.service.ts diff --git a/.env.example b/.env.example index 8ebeba876..c1467cdfe 100644 --- a/.env.example +++ b/.env.example @@ -116,7 +116,9 @@ ADMIN_LOGIN_USERNAME= # For local development, use: admin ADMIN_LOGIN_PASSWORD= - +# Revalidate cache API token +# For local development, use: random string +REVALIDATE_TOKEN= # Optional for local development diff --git a/apps/web-api/src/auth/auth.module.ts b/apps/web-api/src/auth/auth.module.ts index c3482180c..03ab0ff58 100644 --- a/apps/web-api/src/auth/auth.module.ts +++ b/apps/web-api/src/auth/auth.module.ts @@ -4,12 +4,11 @@ import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; import { HttpModule } from '@nestjs/axios'; import { PrismaService } from '../shared/prisma.service'; -import { RedisService } from '../utils/redis/redis.service'; import { OtpModule } from '../otp/otp.module'; @Module({ imports: [HttpModule, OtpModule], controllers: [AuthController], - providers: [AuthService, PrismaService, RedisService], + providers: [AuthService, PrismaService], exports: [AuthService] }) export class AuthModule {} diff --git a/apps/web-api/src/members/members.service.ts b/apps/web-api/src/members/members.service.ts index 815f1134c..dde769aac 100644 --- a/apps/web-api/src/members/members.service.ts +++ b/apps/web-api/src/members/members.service.ts @@ -1,7 +1,6 @@ /* eslint-disable prettier/prettier */ import { BadRequestException, - CACHE_MANAGER, ConflictException, NotFoundException, Inject, @@ -11,7 +10,6 @@ import { import { z } from 'zod'; import axios from 'axios'; import * as path from 'path'; -import { Cache } from 'cache-manager'; import { Prisma, Member, ParticipantsRequest } from '@prisma/client'; import { PrismaService } from '../shared/prisma.service'; import { ParticipantsRequestService } from '../participants-request/participants-request.service'; @@ -26,6 +24,7 @@ import { LogService } from '../shared/log.service'; import { DEFAULT_MEMBER_ROLES } from '../utils/constants'; import { hashFileName } from '../utils/hashing'; import { copyObj, buildMultiRelationMapping } from '../utils/helper/helper'; +import { CacheService } from '../utils/cache/cache.service'; @Injectable() export class MembersService { @@ -41,8 +40,8 @@ export class MembersService { private participantsRequestService: ParticipantsRequestService, @Inject(forwardRef(() => NotificationService)) private notificationService: NotificationService, - @Inject(CACHE_MANAGER) private cacheService: Cache - ) { } + private cacheService: CacheService + ) {} /** * Creates a new member in the database within a transaction. @@ -427,7 +426,7 @@ export class MembersService { newTokens = await this.authService.updateEmailInAuth(newEmail, oldEmail, memberInfo.externalId) }); this.logger.info(`Email has been successfully updated from ${oldEmail} to ${newEmail}`) - await this.cacheService.reset(); + await this.cacheService.reset({ service: 'members'}); return { refreshToken: newTokens.refresh_token, idToken: newTokens.id_token, @@ -1166,7 +1165,7 @@ export class MembersService { */ async updatePreference(id: string, preferences: any): Promise { const updatedMember = await this.updateMemberByUid(id, { preferences }); - await this.cacheService.reset(); + await this.cacheService.reset({ service: 'members'}); return updatedMember; } @@ -1175,7 +1174,7 @@ export class MembersService { * This ensures that the system is up-to-date with the latest changes. */ private async postUpdateActions(): Promise { - await this.cacheService.reset(); + await this.cacheService.reset({ service: 'members'}); await this.forestadminService.triggerAirtableSync(); } diff --git a/apps/web-api/src/participants-request/participants-request.service.ts b/apps/web-api/src/participants-request/participants-request.service.ts index 9595e3199..e549454f4 100644 --- a/apps/web-api/src/participants-request/participants-request.service.ts +++ b/apps/web-api/src/participants-request/participants-request.service.ts @@ -1,15 +1,13 @@ /* eslint-disable prettier/prettier */ -import { - BadRequestException, - ConflictException, +import { + BadRequestException, + ConflictException, NotFoundException, - Inject, Injectable, - CACHE_MANAGER, - forwardRef + forwardRef, + Inject } from '@nestjs/common'; import { ApprovalStatus, ParticipantType } from '@prisma/client'; -import { Cache } from 'cache-manager'; import { Prisma, ParticipantsRequest, PrismaClient } from '@prisma/client'; import { generateProfileURL } from '../utils/helper/helper'; import { LogService } from '../shared/log.service'; @@ -19,6 +17,7 @@ import { TeamsService } from '../teams/teams.service'; import { NotificationService } from '../utils/notification/notification.service'; import { LocationTransferService } from '../utils/location-transfer/location-transfer.service'; import { ForestAdminService } from '../utils/forest-admin/forest-admin.service'; +import { CacheService } from '../utils/cache/cache.service'; @Injectable() export class ParticipantsRequestService { @@ -28,8 +27,7 @@ export class ParticipantsRequestService { private locationTransferService: LocationTransferService, private forestAdminService: ForestAdminService, private notificationService: NotificationService, - @Inject(CACHE_MANAGER) - private cacheService: Cache, + private cacheService: CacheService, @Inject(forwardRef(() => MembersService)) private membersService: MembersService, @Inject(forwardRef(() => TeamsService)) @@ -166,7 +164,7 @@ export class ParticipantsRequestService { where: { uid }, data: formattedData, }); - await this.cacheService.reset(); + await this.cacheService.reset({ service: "participants-requests" }); return result; } catch (err) { return this.handleErrors(err) @@ -187,7 +185,7 @@ export class ParticipantsRequestService { where: { uid: uidToReject }, data: { status: ApprovalStatus.REJECTED } }); - await this.cacheService.reset(); + await this.cacheService.reset({ service: "participants-requests" }); return result; } catch (err) { return this.handleErrors(err) @@ -250,7 +248,7 @@ export class ParticipantsRequestService { participantsRequest.requesterEmailId ); } - await this.cacheService.reset(); + await this.cacheService.reset({ service: "participants-requests" }); await this.forestAdminService.triggerAirtableSync(); return result; } @@ -296,7 +294,7 @@ export class ParticipantsRequestService { if (!disableNotification) { this.notifyForCreate(result); } - await this.cacheService.reset(); + await this.cacheService.reset({ service: "participants-requests" }); return result; } diff --git a/apps/web-api/src/pl-events/pl-event-guests.service.ts b/apps/web-api/src/pl-events/pl-event-guests.service.ts index 8ea507220..e16df6f5e 100644 --- a/apps/web-api/src/pl-events/pl-event-guests.service.ts +++ b/apps/web-api/src/pl-events/pl-event-guests.service.ts @@ -1,9 +1,8 @@ -import { Injectable, NotFoundException, ConflictException, BadRequestException, Inject, CACHE_MANAGER } from '@nestjs/common'; +import { Injectable, NotFoundException, ConflictException, BadRequestException } from '@nestjs/common'; import { LogService } from '../shared/log.service'; import { PrismaService } from '../shared/prisma.service'; import { Prisma, Member } from '@prisma/client'; import { MembersService } from '../members/members.service'; -import { Cache } from 'cache-manager'; import { PLEventLocationsService } from './pl-event-locations.service'; import { CreatePLEventGuestSchemaDto, @@ -13,6 +12,7 @@ import { FormattedLocationWithEvents, PLEvent } from './pl-event-locations.types'; +import { CacheService } from '../utils/cache/cache.service'; @Injectable() export class PLEventGuestsService { @@ -21,7 +21,7 @@ export class PLEventGuestsService { private logger: LogService, private memberService: MembersService, private eventLocationsService: PLEventLocationsService, - @Inject(CACHE_MANAGER) private cacheService: Cache + private cacheService: CacheService ) {} /** @@ -43,7 +43,7 @@ export class PLEventGuestsService { data.memberUid = isAdmin ? data.memberUid : member.uid; const guests = this.formatInputToEventGuests(data); const result = await (tx || this.prisma).pLEventGuest.createMany({ data: guests }); - this.cacheService.reset(); + this.cacheService.reset({ service: 'PLEventGuest' }); return result; } catch(err) { this.handleErrors(err); @@ -100,7 +100,7 @@ export class PLEventGuestsService { OR: deleteConditions } }); - await this.cacheService.reset(); + await this.cacheService.reset({ service: 'PLEventGuest' }); return result; } catch (err) { this.handleErrors(err); diff --git a/apps/web-api/src/projects/projects.service.ts b/apps/web-api/src/projects/projects.service.ts index 4ea954232..253c08924 100644 --- a/apps/web-api/src/projects/projects.service.ts +++ b/apps/web-api/src/projects/projects.service.ts @@ -1,9 +1,9 @@ -import { Inject, CACHE_MANAGER, BadRequestException, ConflictException, ForbiddenException, Injectable, NotFoundException, HttpException } from '@nestjs/common'; +import { BadRequestException, ConflictException, ForbiddenException, Injectable, NotFoundException } from '@nestjs/common'; import { LogService } from '../shared/log.service'; import { PrismaService } from '../shared/prisma.service'; import { Prisma } from '@prisma/client'; import { MembersService } from '../members/members.service'; -import { Cache } from 'cache-manager'; +import { CacheService } from '../utils/cache/cache.service'; @Injectable() export class ProjectsService { @@ -11,7 +11,7 @@ export class ProjectsService { private prisma: PrismaService, private memberService: MembersService, private logger: LogService, - @Inject(CACHE_MANAGER) private cacheService: Cache + private cacheService: CacheService ) { } async createProject(project: Prisma.ProjectUncheckedCreateInput, userEmail: string) { @@ -34,7 +34,7 @@ export class ProjectsService { } } }); - await this.cacheService.reset(); + await this.cacheService.reset({ service: 'projects'}); return result; } catch (err) { this.handleErrors(err); @@ -81,7 +81,7 @@ export class ProjectsService { } } }); - await this.cacheService.reset(); + await this.cacheService.reset({ service: 'projects'}); return result; }); } catch (err) { @@ -169,7 +169,7 @@ export class ProjectsService { where: { uid }, data: { isDeleted: true } }); - await this.cacheService.reset(); + await this.cacheService.reset({ service: 'projects'}); return result; } catch (err) { this.handleErrors(err, `${uid}`); diff --git a/apps/web-api/src/shared/shared.module.ts b/apps/web-api/src/shared/shared.module.ts index 26a424c30..a29c68c0a 100644 --- a/apps/web-api/src/shared/shared.module.ts +++ b/apps/web-api/src/shared/shared.module.ts @@ -10,6 +10,7 @@ import { ImagesController } from '../images/images.controller'; import { ImagesService } from '../images/images.service'; import { FileUploadService } from '../utils/file-upload/file-upload.service'; import { FileEncryptionService } from '../utils/file-encryption/file-encryption.service'; +import { CacheService } from '../utils/cache/cache.service'; @Global() @Module({ @@ -26,6 +27,7 @@ import { FileEncryptionService } from '../utils/file-encryption/file-encryption. ImagesService, FileUploadService, FileEncryptionService, + CacheService ], exports: [ PrismaService, @@ -39,6 +41,7 @@ import { FileEncryptionService } from '../utils/file-encryption/file-encryption. ImagesService, FileUploadService, FileEncryptionService, + CacheService ], }) export class SharedModule {} \ No newline at end of file diff --git a/apps/web-api/src/teams/teams.service.ts b/apps/web-api/src/teams/teams.service.ts index 49510534f..8c9456ee3 100644 --- a/apps/web-api/src/teams/teams.service.ts +++ b/apps/web-api/src/teams/teams.service.ts @@ -4,9 +4,8 @@ import { ForbiddenException, BadRequestException, NotFoundException, - Inject, forwardRef, - CACHE_MANAGER + Inject } from '@nestjs/common'; import * as path from 'path'; import { z } from 'zod'; @@ -20,8 +19,8 @@ import { hashFileName } from '../utils/hashing'; import { ForestAdminService } from '../utils/forest-admin/forest-admin.service'; import { MembersService } from '../members/members.service'; import { LogService } from '../shared/log.service'; -import { Cache } from 'cache-manager'; import { copyObj, buildMultiRelationMapping, buildRelationMapping } from '../utils/helper/helper'; +import { CacheService } from '../utils/cache/cache.service'; @Injectable() export class TeamsService { @@ -35,7 +34,7 @@ export class TeamsService { private logger: LogService, private forestadminService: ForestAdminService, private notificationService: NotificationService, - @Inject(CACHE_MANAGER) private cacheService: Cache + private cacheService: CacheService ) { } /** @@ -321,7 +320,7 @@ export class TeamsService { * This ensures that the system is up-to-date with the latest changes. */ private async postUpdateActions(): Promise { - await this.cacheService.reset(); + await this.cacheService.reset({ service: "teams" }); await this.forestadminService.triggerAirtableSync(); } diff --git a/apps/web-api/src/utils/cache/cache.service.ts b/apps/web-api/src/utils/cache/cache.service.ts new file mode 100644 index 000000000..b078cae12 --- /dev/null +++ b/apps/web-api/src/utils/cache/cache.service.ts @@ -0,0 +1,58 @@ +import { Injectable, Inject, CACHE_MANAGER } from '@nestjs/common'; +import { Cache } from 'cache-manager'; +import axios from 'axios'; +import { LogService } from '../../shared/log.service'; + +@Injectable() +export class CacheService { + constructor( + @Inject(CACHE_MANAGER) private cache: Cache, + private logService: LogService + ) {} + + // Mapping service names to tags + private serviceTagsMap = { + members: ['member-filters', 'member-list'], + projects: ['project-list', 'focus-areas'], + teams: ['team-filters', 'team-list', 'focus-areas'], + }; + + // Reset cache and call API based on service + async reset(data) { + const { service } = data; + await this.cache.reset(); // Reset the cache + const tags = this.serviceTagsMap[service]; + if (tags) { + await this.revalidateCache(tags); + } + } + + // Function to call the revalidate API + private async revalidateCache(tags: string[]) { + const baseUrl = process.env.WEB_UI_BASE_URL; + const token = process.env.REVALIDATE_API_TOKEN; // Assuming token is stored in env variable + if (!baseUrl) { + this.logService.error('WEB_UI_BASE_URL is not defined in the environment variables.'); + return; + } + if (!token) { + this.logService.error('REVALIDATE_API_TOKEN is not defined in the environment variables.'); + return; + } + const url = `${baseUrl}/api/revalidate`; + try { + await axios.post( + url, + { tags }, + { + headers: { + Authorization: `Bearer ${token}`, // Adding Bearer token to headers + }, + }, + ); + this.logService.info(`Revalidation API called successfully with tags: ${tags.join(', ')}`); + } catch (error) { + this.logService.error('Error calling revalidate API:', error.message); + } + } +} diff --git a/apps/web-api/src/utils/redis/redis.service.ts b/apps/web-api/src/utils/redis/redis.service.ts deleted file mode 100644 index a44562f50..000000000 --- a/apps/web-api/src/utils/redis/redis.service.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* eslint-disable prettier/prettier */ -import { Injectable } from '@nestjs/common'; -import cacheManager from 'cache-manager'; -import redisStore from 'cache-manager-redis-store'; - -@Injectable() -export class RedisService { - async resetAllCache() { - const redisCache = cacheManager.caching({ - store: redisStore, - host: process.env.REDIS_HOST, - url: process.env.REDIS_TLS_URL, - port: Number(process.env.REDIS_PORT), - password: process.env.REDIS_PASSWORD, - tls: process.env.REDIS_WITH_TLS - ? { - rejectUnauthorized: false, - requestCert: true, - } - : null, - }); - await redisCache.reset(); - } -} From ca82945253f79a1ca4d5127631468e0659591257 Mon Sep 17 00:00:00 2001 From: navneethkrish Date: Wed, 4 Dec 2024 11:49:20 +0530 Subject: [PATCH 31/41] fix(cache service): added participant request tags for cache reset --- apps/web-api/src/utils/cache/cache.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/web-api/src/utils/cache/cache.service.ts b/apps/web-api/src/utils/cache/cache.service.ts index b078cae12..0bfbd5eb5 100644 --- a/apps/web-api/src/utils/cache/cache.service.ts +++ b/apps/web-api/src/utils/cache/cache.service.ts @@ -15,6 +15,7 @@ export class CacheService { members: ['member-filters', 'member-list'], projects: ['project-list', 'focus-areas'], teams: ['team-filters', 'team-list', 'focus-areas'], + 'participants-requests': ['member-filters', 'member-list','team-filters', 'team-list', 'focus-areas'] }; // Reset cache and call API based on service @@ -41,7 +42,7 @@ export class CacheService { } const url = `${baseUrl}/api/revalidate`; try { - await axios.post( + const response = await axios.post( url, { tags }, { From d5059483ddf5ab8133e17ac1af73a5915b4bf659 Mon Sep 17 00:00:00 2001 From: navneethkrish Date: Wed, 4 Dec 2024 12:14:47 +0530 Subject: [PATCH 32/41] fix(sample env): modified token key name --- .env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.example b/.env.example index c1467cdfe..af921c8c4 100644 --- a/.env.example +++ b/.env.example @@ -118,7 +118,7 @@ ADMIN_LOGIN_PASSWORD= # Revalidate cache API token # For local development, use: random string -REVALIDATE_TOKEN= +REVALIDATE_API_TOKEN= # Optional for local development From 7bd75c814ae86652c79c52a11bf97eccc6c8d04d Mon Sep 17 00:00:00 2001 From: Vellaiyan-Marimuthu Date: Wed, 4 Dec 2024 13:39:18 +0530 Subject: [PATCH 33/41] Fix: Toast message change --- apps/back-office/components/member-table/member-table.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/back-office/components/member-table/member-table.tsx b/apps/back-office/components/member-table/member-table.tsx index 3f64e875d..bd5ec1fb7 100644 --- a/apps/back-office/components/member-table/member-table.tsx +++ b/apps/back-office/components/member-table/member-table.tsx @@ -58,10 +58,12 @@ const MemberTable = (props: any) => { function redirectToDetail(request) { setIsLoading(true); const route = ROUTE_CONSTANTS.MEMBER_VIEW; + const from = selectedTab === APP_CONSTANTS.PENDING_FLAG ? "pending": "approved"; router.push({ pathname: route, query: { id: request.id, + from, }, }); } @@ -84,7 +86,7 @@ const MemberTable = (props: any) => { setIsLoading(true); if (selectedTab === APP_CONSTANTS.PENDING_FLAG) { await api.post(`${API_ROUTE.PARTICIPANTS_REQUEST}`, [data], configuration); - message = `Successfully ${(status === APP_CONSTANTS.REJECTED_FLAG ? APP_CONSTANTS.REJECTED_LABEL : APP_CONSTANTS.VERIFIED_FLAG)}`; + message = `Successfully ${(status === APP_CONSTANTS.REJECTED_FLAG ? APP_CONSTANTS.REJECTED_LABEL : (isVerified ? APP_CONSTANTS.VERIFIED_FLAG : APP_CONSTANTS.UNVERIFIED_FLAG))}`; } else { await api.post(`${API_ROUTE.ADMIN_APPROVAL}`, { memberIds: [id] }, configuration); message = `Successfully ${APP_CONSTANTS.VERIFIED_FLAG}`; From 6d3e96964f359553b6c5395a25372050f976f665 Mon Sep 17 00:00:00 2001 From: Navaneethakrishnan Date: Wed, 4 Dec 2024 13:16:58 +0530 Subject: [PATCH 34/41] feat: introduced new field signUpMedium, signUpCampaign in member --- apps/web-api/prisma/fixtures/members.ts | 2 ++ .../migrations/20241127093907_member_signup/migration.sql | 4 +++- apps/web-api/prisma/schema.prisma | 2 ++ apps/web-api/src/members/members.service.ts | 4 ++-- libs/contracts/src/schema/member.ts | 2 ++ libs/contracts/src/schema/participants-request.ts | 2 ++ 6 files changed, 13 insertions(+), 3 deletions(-) diff --git a/apps/web-api/prisma/fixtures/members.ts b/apps/web-api/prisma/fixtures/members.ts index 0f5b1855d..8413b96c1 100644 --- a/apps/web-api/prisma/fixtures/members.ts +++ b/apps/web-api/prisma/fixtures/members.ts @@ -54,6 +54,8 @@ const membersFactory = Factory.define>( updatedAt: faker.date.recent(), locationUid: '', signUpSource: faker.company.name(), + signUpCampaign: faker.company.name(), + signUpMedium: faker.company.name(), isVerified: faker.datatype.boolean(), isUserConsent: faker.datatype.boolean(), isSubscribedToNewsletter: faker.datatype.boolean(), diff --git a/apps/web-api/prisma/migrations/20241127093907_member_signup/migration.sql b/apps/web-api/prisma/migrations/20241127093907_member_signup/migration.sql index 6c866c0b9..ea6e2f518 100644 --- a/apps/web-api/prisma/migrations/20241127093907_member_signup/migration.sql +++ b/apps/web-api/prisma/migrations/20241127093907_member_signup/migration.sql @@ -10,7 +10,9 @@ ADD COLUMN "isVerified" BOOLEAN DEFAULT false, ADD COLUMN "signUpSource" TEXT, ADD COLUMN "isSubscribedToNewsletter" BOOLEAN DEFAULT false, ADD COLUMN "isUserConsent" BOOLEAN DEFAULT false, -ADD COLUMN "teamOrProjectURL" TEXT; +ADD COLUMN "teamOrProjectURL" TEXT, +ADD COLUMN "signUpCampaign" TEXT, +ADD COLUMN "signUpMedium" TEXT; -- Modify the "plnFriend" column to drop NOT NULL constraint ALTER TABLE "Member" diff --git a/apps/web-api/prisma/schema.prisma b/apps/web-api/prisma/schema.prisma index efb2ac638..6b83e72af 100644 --- a/apps/web-api/prisma/schema.prisma +++ b/apps/web-api/prisma/schema.prisma @@ -74,6 +74,8 @@ model Member { isFeatured Boolean? @default(false) isVerified Boolean? @default(false) signUpSource String? + signUpMedium String? + signUpCampaign String? isUserConsent Boolean? @default(false) isSubscribedToNewsletter Boolean? @default(false) teamOrProjectURL String? diff --git a/apps/web-api/src/members/members.service.ts b/apps/web-api/src/members/members.service.ts index dde769aac..5d7d1fe07 100644 --- a/apps/web-api/src/members/members.service.ts +++ b/apps/web-api/src/members/members.service.ts @@ -708,8 +708,8 @@ export class MembersService { 'name', 'email', 'githubHandler', 'discordHandler', 'bio', 'twitterHandler', 'linkedinHandler', 'telegramHandler', 'officeHours', 'moreDetails', 'plnStartDate', 'openToWork', - 'isVerified', 'signUpSource', 'isUserConsent', 'isSubscribedToNewsletter', - 'teamOrProjectURL' + 'isVerified', 'signUpSource', 'signUpMedium', 'signUpCampaign', + 'isUserConsent', 'isSubscribedToNewsletter', 'teamOrProjectURL', ]; copyObj(memberData, member, directFields); member.email = member.email.toLowerCase().trim(); diff --git a/libs/contracts/src/schema/member.ts b/libs/contracts/src/schema/member.ts index 1ccef0acb..4cbcf8084 100644 --- a/libs/contracts/src/schema/member.ts +++ b/libs/contracts/src/schema/member.ts @@ -42,6 +42,8 @@ export const MemberSchema = z.object({ plnFriend: z.boolean().nullish(), bio: z.string().nullish(), signUpSource: z.string().nullish(), + signUpMedium: z.string().nullish(), + signUpCampaign: z.string().nullish(), isFeatured: z.boolean().nullish(), createdAt: z.string(), updatedAt: z.string(), diff --git a/libs/contracts/src/schema/participants-request.ts b/libs/contracts/src/schema/participants-request.ts index c6f24e6fc..6d83db9c4 100644 --- a/libs/contracts/src/schema/participants-request.ts +++ b/libs/contracts/src/schema/participants-request.ts @@ -51,6 +51,8 @@ const newDataMemberSchema = z.object({ projectContributions: z.array(ProjectContributionSchema as any).optional(), bio: z.string().nullish(), signUpSource: z.string().nullish(), + signUpMedium: z.string().nullish(), + signUpCampaign: z.string().nullish(), isFeatured: z.boolean().nullish(), locationUid: z.string().nullish(), openToWork: z.boolean().nullish(), From 3c36252abf96f910438b88f84dd24d9335796010 Mon Sep 17 00:00:00 2001 From: prasanth Date: Wed, 4 Dec 2024 22:58:25 +0530 Subject: [PATCH 35/41] fix: bugfixes in backoffice --- .../footer-buttons/footer-buttons.tsx | 121 ++++++-- .../components/member-table/member-table.tsx | 151 ++++++--- apps/back-office/pages/member-view.tsx | 292 +++++++++++------- 3 files changed, 372 insertions(+), 192 deletions(-) diff --git a/apps/back-office/components/footer-buttons/footer-buttons.tsx b/apps/back-office/components/footer-buttons/footer-buttons.tsx index 0afac22c6..4073da62d 100644 --- a/apps/back-office/components/footer-buttons/footer-buttons.tsx +++ b/apps/back-office/components/footer-buttons/footer-buttons.tsx @@ -1,14 +1,17 @@ import { CheckIcon, XIcon } from '@heroicons/react/outline'; import api from '../../utils/api'; import router from 'next/router'; +import Modal from '../modal/modal'; import APP_CONSTANTS, { API_ROUTE, ENROLLMENT_TYPE, ROUTE_CONSTANTS, } from '../../utils/constants'; import { toast } from 'react-toastify'; +import { useState } from 'react'; export function FooterButtons(props) { + const [openModal, setOpenModal] = useState(false); const saveButtonClassName = props.disableSave ? 'shadow-special-button-default inline-flex w-full justify-center rounded-full bg-slate-400 px-6 py-2 text-base font-semibold leading-6 text-white outline-none' : 'on-focus leading-3.5 text-md mb-2 mr-2 flex items-center rounded-full border border-blue-600 bg-blue-600 px-4 py-3 text-left font-medium text-white last:mr-0 focus-within:rounded-full hover:border-slate-400 focus:rounded-full focus-visible:rounded-full'; @@ -28,11 +31,16 @@ export function FooterButtons(props) { try { let message = ""; setLoader(true); - await api.patch(`${API_ROUTE.PARTICIPANTS_REQUEST}/${id}`, data, configuration) - message = status === "REJECTED" - ? `Successfully ${APP_CONSTANTS.REJECTED_LABEL}` - : `Successfully ${isVerified ? APP_CONSTANTS.VERIFIED_FLAG : APP_CONSTANTS.UNVERIFIED_FLAG}`; - + if (props.from === "approved") { + await api.post(`${API_ROUTE.ADMIN_APPROVAL}`, { memberIds: [props.id] }, configuration); + message = `Successfully ${APP_CONSTANTS.VERIFIED_FLAG}`; + } else { + await api.patch(`${API_ROUTE.PARTICIPANTS_REQUEST}/${id}`, data, configuration) + message = status === "REJECTED" + ? `Successfully ${APP_CONSTANTS.REJECTED_LABEL}` + : `Successfully ${isVerified ? APP_CONSTANTS.VERIFIED_FLAG : APP_CONSTANTS.UNVERIFIED_FLAG}`; + } + setOpenModal(false) toast(message); router.push({ pathname: ROUTE_CONSTANTS.PENDING_LIST, @@ -48,12 +56,64 @@ export function FooterButtons(props) { toast(error.message || 'An unexpected error occurred'); } } finally { + setOpenModal(false) setLoader(false); } } + const handleOpen = () => { + setOpenModal(true); + } + + const onClose = () => { + setOpenModal(false); + } + return (
+ {openModal && + +
+
+ +
+
+
+ Are you sure you want to reject? +
+
+ Clicking remove will remove the member from the list. +
+ +
+ + + +
+
+
+
+ } diff --git a/apps/back-office/components/member-table/member-table.tsx b/apps/back-office/components/member-table/member-table.tsx index bd5ec1fb7..cb571520e 100644 --- a/apps/back-office/components/member-table/member-table.tsx +++ b/apps/back-office/components/member-table/member-table.tsx @@ -5,6 +5,7 @@ import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import Loader from '../common/loader'; import { useNavbarContext } from 'apps/back-office/context/navbar-context'; +import Modal from '../modal/modal'; const MemberTable = (props: any) => { const selectedTab = props?.selectedTab ?? ''; @@ -16,6 +17,9 @@ const MemberTable = (props: any) => { const [isLoading, setIsLoading] = useState(false); const [isSort, setIsSort] = useState(false); const { setMemberList } = useNavbarContext(); + const [openModal, setOpenModal] = useState(false); + const [rejectId, setRejectId] = useState([]); + // const onSortClickHandler = () => { // setIsSort(!isSort); @@ -58,7 +62,7 @@ const MemberTable = (props: any) => { function redirectToDetail(request) { setIsLoading(true); const route = ROUTE_CONSTANTS.MEMBER_VIEW; - const from = selectedTab === APP_CONSTANTS.PENDING_FLAG ? "pending": "approved"; + const from = selectedTab === APP_CONSTANTS.PENDING_FLAG ? "pending" : "approved"; router.push({ pathname: route, query: { @@ -86,7 +90,7 @@ const MemberTable = (props: any) => { setIsLoading(true); if (selectedTab === APP_CONSTANTS.PENDING_FLAG) { await api.post(`${API_ROUTE.PARTICIPANTS_REQUEST}`, [data], configuration); - message = `Successfully ${(status === APP_CONSTANTS.REJECTED_FLAG ? APP_CONSTANTS.REJECTED_LABEL : (isVerified ? APP_CONSTANTS.VERIFIED_FLAG : APP_CONSTANTS.UNVERIFIED_FLAG))}`; + message = `Successfully ${(status === APP_CONSTANTS.REJECTED_FLAG ? APP_CONSTANTS.REJECTED_LABEL : (isVerified ? APP_CONSTANTS.VERIFIED_FLAG : APP_CONSTANTS.UNVERIFIED_FLAG))}`; } else { await api.post(`${API_ROUTE.ADMIN_APPROVAL}`, { memberIds: [id] }, configuration); message = `Successfully ${APP_CONSTANTS.VERIFIED_FLAG}`; @@ -171,6 +175,7 @@ const MemberTable = (props: any) => { setSelectedMembes([]); setIsAllSelected(false); const message = `Successfully ${APP_CONSTANTS.REJECTED_LABEL}`; + setOpenModal(false) toast(message); } catch (error: any) { if (error.response?.status === 500) { @@ -183,22 +188,35 @@ const MemberTable = (props: any) => { toast(error.message || 'An unexpected error occurred'); } } finally { + setOpenModal(false); setIsLoading(false); } }; + const handleOpen = (id: any) => { + setOpenModal(true); + if (Array.isArray(id)) { + setRejectId(id); + } else { + setRejectId(id); + } + } + + const onClose = () => { + setOpenModal(false); + } + return ( <> {isLoading && } {allMembers?.length > 0 && ( -
+
{/* Header */}
- - {selectedTab === APP_CONSTANTS.PENDING_FLAG && ( )} + + {selectedTab === APP_CONSTANTS.PENDING_FLAG && ( )}
@@ -326,36 +340,81 @@ const MemberTable = (props: any) => {
- - {selectedTab === APP_CONSTANTS.PENDING_FLAG && ( )} + + {selectedTab === APP_CONSTANTS.PENDING_FLAG && ( )}
)} + + {openModal && + +
+
+ +
+
+
+ Are you sure you want to reject? +
+
+ Clicking remove will remove the member from the list. +
+ +
+ + + +
+
+
+
+ } ); }; diff --git a/apps/back-office/pages/member-view.tsx b/apps/back-office/pages/member-view.tsx index 41758e572..7f9df3ef7 100644 --- a/apps/back-office/pages/member-view.tsx +++ b/apps/back-office/pages/member-view.tsx @@ -85,7 +85,7 @@ export default function MemberView(props) { const [disableSave, setDisableSave] = useState(false); const [formValues, setFormValues] = useState(props?.formValues); const [isLoading, setIsLoading] = useState(false); - const [resetImg, setResetImg] = useState(false); + const [resetImg, setResetImg] = useState(false); const { setIsOpenRequest, setMemberList, @@ -102,7 +102,7 @@ export default function MemberView(props) { useEffect(() => { setDropDownValues({ skillValues: props?.skills, teamNames: props?.teams }); }, [props]); - + const handleResetImg = () => { setResetImg(false); } @@ -128,7 +128,7 @@ export default function MemberView(props) { twitterHandler: formValues.twitterHandler?.trim(), githubHandler: formValues.githubHandler?.trim(), telegramHandler: formValues.telegramHandler?.trim(), - officeHours: formValues.officeHours?.trim() === ''? null : formValues.officeHours?.trim(), + officeHours: formValues.officeHours?.trim() === '' ? null : formValues.officeHours?.trim(), comments: formValues.comments?.trim(), teamOrProjectURL: formValues.teamOrProjectURL, plnStartDate: formValues.plnStartDate @@ -137,7 +137,7 @@ export default function MemberView(props) { skills: skills, teamAndRoles: formattedTeamAndRoles, openToWork: formValues.openToWork, - projectContributions:formValues.projectContributions, + projectContributions: formValues.projectContributions, oldName: name, }; delete formattedData.requestorEmail; @@ -212,13 +212,25 @@ export default function MemberView(props) { imageUrl: image?.url ?? imageUrl, }, }; - const configuration = { + const configuration = { headers: { authorization: `Bearer ${props.plnadmin}`, }, }; - await api + if(props?.from === "approved") { + await api.patch( + `${API_ROUTE.ADMIN_APPROVAL}/${props.id}`, + data, + configuration + ) + .then((response) => { + setSaveCompleted(true); + setIsEditEnabled(false); + setResetImg(true); + }); + } else { + await api .put( `${API_ROUTE.PARTICIPANTS_REQUEST}/${props.id}`, data, @@ -229,6 +241,7 @@ export default function MemberView(props) { setIsEditEnabled(false); setResetImg(true); }); + } } catch (err) { toast(err?.message); console.log('error', err); @@ -386,6 +399,7 @@ export default function MemberView(props) { referenceUid={props.referenceUid} setLoader={setIsLoading} token={props.plnadmin} + from={props.from} /> )} @@ -393,9 +407,10 @@ export default function MemberView(props) { } export const getServerSideProps = async (context) => { - const { id, backLink = ROUTE_CONSTANTS.PENDING_LIST } = context.query as { + const { id, from, backLink = ROUTE_CONSTANTS.PENDING_LIST } = context.query as { id: string; backLink: string; + from: string; }; const { plnadmin } = parseCookies(context); @@ -421,119 +436,162 @@ export const getServerSideProps = async (context) => { let teamList = []; let oldName = ''; - // Check if provided ID is an Airtable ID, and if so, get the corresponding backend UID - - const [ - requestDetailResponse, - allRequestResponse, - memberTeamsResponse, - skillsResponse, - ] = await Promise.all([ - api.get(`${API_ROUTE.PARTICIPANTS_REQUEST}/${id}`, config), - api.get(API_ROUTE.PARTICIPANTS_REQUEST, config), - api.get(API_ROUTE.TEAMS), - api.get(API_ROUTE.SKILLS), - ]); - - if ( - requestDetailResponse.status === 200 && - allRequestResponse.status === 200 && - memberTeamsResponse.status === 200 && - skillsResponse.status === 200 - ) { - teamList = allRequestResponse?.data?.filter( - (item) => item.participantType === ENROLLMENT_TYPE.TEAM - ); - memberList = allRequestResponse?.data?.filter( - (item) => item.participantType === ENROLLMENT_TYPE.MEMBER - ); + + if (from !== "approved") { + const [ + requestDetailResponse, + allRequestResponse, + memberTeamsResponse, + skillsResponse, + ] = await Promise.all([ + api.get(`${API_ROUTE.PARTICIPANTS_REQUEST}/${id}`, config), + api.get(API_ROUTE.PARTICIPANTS_REQUEST, config), + api.get(API_ROUTE.TEAMS), + api.get(API_ROUTE.SKILLS), + ]); - let counter = 1; - referenceUid = requestDetailResponse?.data?.referenceUid ?? ''; - const requestData = requestDetailResponse?.data?.newData; - oldName = requestData?.oldName ?? requestData?.name; - status = requestDetailResponse?.data?.status; - const teamAndRoles = - requestData?.teamAndRoles?.length && - requestData?.teamAndRoles?.map((team) => { - return { - role: team.role ?? "", - teamUid: team.teamUid, - teamTitle: team.teamTitle, - rowId: counter++, - }; - }); - - formValues = { - name: requestData?.name, - email: requestData?.email, - imageUid: requestData?.imageUid ?? '', - imageFile: null, - plnStartDate: requestData?.plnStartDate - ? new Date(requestData?.plnStartDate).toISOString().split('T')[0] - : null, - city: requestData?.city ?? '', - region: requestData?.region ?? '', - country: requestData?.country ?? '', - linkedinHandler: requestData?.linkedinHandler ?? '', - discordHandler: requestData?.discordHandler ?? '', - twitterHandler: requestData?.twitterHandler ?? '', - githubHandler: requestData?.githubHandler ?? '', - telegramHandler: requestData?.telegramHandler ?? '', - officeHours: requestData?.officeHours ?? '', - requestorEmail: requestDetailResponse?.data?.requesterEmailId ?? '', - comments: requestData?.comments ?? '', - teamAndRoles: teamAndRoles || [ - // { teamUid: '', teamTitle: '', role: '', rowId: 1 }, - ], - teamOrProjectURL: requestData?.teamOrProjectURL ?? '', - skills: requestData?.skills?.map((item) => { + if ( + requestDetailResponse.status === 200 && + allRequestResponse.status === 200 && + memberTeamsResponse.status === 200 && + skillsResponse.status === 200 + ) { + teamList = allRequestResponse?.data?.filter( + (item) => item.participantType === ENROLLMENT_TYPE.TEAM + ); + memberList = allRequestResponse?.data?.filter( + (item) => item.participantType === ENROLLMENT_TYPE.MEMBER + ); + + let counter = 1; + referenceUid = requestDetailResponse?.data?.referenceUid ?? ''; + const requestData = requestDetailResponse?.data?.newData; + oldName = requestData?.oldName ?? requestData?.name; + status = requestDetailResponse?.data?.status; + const teamAndRoles = + requestData?.teamAndRoles?.length && + requestData?.teamAndRoles?.map((team) => { + return { + role: team.role ?? "", + teamUid: team.teamUid, + teamTitle: team.teamTitle, + rowId: counter++, + }; + }); + + formValues = { + name: requestData?.name, + email: requestData?.email, + imageUid: requestData?.imageUid ?? '', + imageFile: null, + plnStartDate: requestData?.plnStartDate + ? new Date(requestData?.plnStartDate).toISOString().split('T')[0] + : null, + city: requestData?.city ?? '', + region: requestData?.region ?? '', + country: requestData?.country ?? '', + linkedinHandler: requestData?.linkedinHandler ?? '', + discordHandler: requestData?.discordHandler ?? '', + twitterHandler: requestData?.twitterHandler ?? '', + githubHandler: requestData?.githubHandler ?? '', + telegramHandler: requestData?.telegramHandler ?? '', + officeHours: requestData?.officeHours ?? '', + requestorEmail: requestDetailResponse?.data?.requesterEmailId ?? '', + comments: requestData?.comments ?? '', + teamAndRoles: teamAndRoles || [ + // { teamUid: '', teamTitle: '', role: '', rowId: 1 }, + ], + teamOrProjectURL: requestData?.teamOrProjectURL ?? '', + skills: requestData?.skills?.map((item) => { + return { value: item.uid, label: item.title }; + }) || [], + openToWork: requestData?.openToWork ?? '', + projectContributions: requestData?.projectContributions ?? [] + }; + imageUrl = requestData?.imageUrl ?? ''; + + if (status == APP_CONSTANTS.PENDING_LABEL) { + teamList = allRequestResponse?.data + ?.filter((item) => item.participantType === ENROLLMENT_TYPE.TEAM) + ?.filter((item) => item.status === APP_CONSTANTS.PENDING_LABEL); + memberList = allRequestResponse?.data + ?.filter((item) => item.participantType === ENROLLMENT_TYPE.MEMBER) + .filter((item) => item.status === APP_CONSTANTS.PENDING_LABEL); + } else { + teamList = allRequestResponse?.data + ?.filter((item) => item.participantType === ENROLLMENT_TYPE.TEAM) + ?.filter((item) => item.status !== APP_CONSTANTS.PENDING_LABEL); + memberList = allRequestResponse?.data + ?.filter((item) => item.participantType === ENROLLMENT_TYPE.MEMBER) + .filter((item) => item.status !== APP_CONSTANTS.PENDING_LABEL); + } + + teams = Array.isArray(memberTeamsResponse?.data) ? + memberTeamsResponse?.data?.map((item) => { + return { value: item.uid, label: item.name }; + }) : []; + skills = skillsResponse?.data?.map((item) => { return { value: item.uid, label: item.title }; - }), - openToWork: requestData?.openToWork ?? '', - projectContributions:requestData?.projectContributions ?? [] - }; - imageUrl = requestData?.imageUrl ?? ''; - - if (status == APP_CONSTANTS.PENDING_LABEL) { - teamList = allRequestResponse?.data - ?.filter((item) => item.participantType === ENROLLMENT_TYPE.TEAM) - ?.filter((item) => item.status === APP_CONSTANTS.PENDING_LABEL); - memberList = allRequestResponse?.data - ?.filter((item) => item.participantType === ENROLLMENT_TYPE.MEMBER) - .filter((item) => item.status === APP_CONSTANTS.PENDING_LABEL); - } else { - teamList = allRequestResponse?.data - ?.filter((item) => item.participantType === ENROLLMENT_TYPE.TEAM) - ?.filter((item) => item.status !== APP_CONSTANTS.PENDING_LABEL); - memberList = allRequestResponse?.data - ?.filter((item) => item.participantType === ENROLLMENT_TYPE.MEMBER) - .filter((item) => item.status !== APP_CONSTANTS.PENDING_LABEL); + }); } + } else { + const approvedApiResponse = await api.get(`${API_ROUTE.MEMBERS}/${id}`, config); - teams = Array.isArray(memberTeamsResponse?.data) ? - memberTeamsResponse?.data?.map((item) => { - return { value: item.uid, label: item.name }; - }) : []; - skills = skillsResponse?.data?.map((item) => { - return { value: item.uid, label: item.title }; - }); - } + if (approvedApiResponse.status === 200) { + const requestData = approvedApiResponse?.data; + formValues = { + name: requestData?.name, + email: requestData?.email, + imageUid: requestData?.imageUid ?? '', + imageFile: null, + plnStartDate: requestData?.plnStartDate + ? new Date(requestData?.plnStartDate).toISOString().split('T')[0] + : null, + city: requestData?.city ?? '', + region: requestData?.region ?? '', + country: requestData?.country ?? '', + linkedinHandler: requestData?.linkedinHandler ?? '', + discordHandler: requestData?.discordHandler ?? '', + twitterHandler: requestData?.twitterHandler ?? '', + githubHandler: requestData?.githubHandler ?? '', + telegramHandler: requestData?.telegramHandler ?? '', + officeHours: requestData?.officeHours ?? '', + comments: requestData?.comments ?? '', + teamAndRoles: // teamAndRoles || + [ + // { teamUid: '', teamTitle: '', role: '', rowId: 1 }, + ], + teamOrProjectURL: requestData?.teamOrProjectURL ?? '', + skills: requestData?.skills?.map((item) => { + return { value: item.uid, label: item.title }; + }), + openToWork: requestData?.openToWork ?? '', + projectContributions: requestData?.projectContributions ?? [] + }; + imageUrl = requestData?.imageUrl ?? ''; + teamList = approvedApiResponse?.data?.teamList ?? []; + memberList = approvedApiResponse?.data?.memberList ?? []; + teams = approvedApiResponse?.data?.teams ?? []; + skills = approvedApiResponse?.data?.skills ?? []; + status= APP_CONSTANTS.PENDING_LABEL; + } + } - return { - props: { - formValues, - teams, - skills, - id, - referenceUid, - imageUrl, - status, - backLink, - teamList, - memberList, - plnadmin, - oldName, - }, + return { + props: { + formValues, + teams, + skills, + id, + // referenceUid, + imageUrl, + status, + backLink, + teamList, + memberList, + plnadmin, + oldName, + from, + }, + }; }; -}; From 0c225985f3e38d99315ef2a4433b9db9a5c76092 Mon Sep 17 00:00:00 2001 From: navneethkrish Date: Wed, 4 Dec 2024 21:03:49 +0530 Subject: [PATCH 36/41] feat(member update): added member details updation api for admin --- apps/web-api/src/admin/member.controller.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/apps/web-api/src/admin/member.controller.ts b/apps/web-api/src/admin/member.controller.ts index aca610b21..fa0bc841a 100644 --- a/apps/web-api/src/admin/member.controller.ts +++ b/apps/web-api/src/admin/member.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Post, UseGuards } from '@nestjs/common'; +import { Body, Controller, Param, Patch, Post, UseGuards } from '@nestjs/common'; import { AdminAuthGuard } from '../guards/admin-auth.guard'; import { MembersService } from '../members/members.service'; @@ -19,4 +19,19 @@ export class MemberController { const { memberIds } = body; return await this.membersService.verifyMembers(memberIds, requestor?.email); } + + /** + * Updates a member to a verfied user. + * + * @param body - participation request data with updated member details + * @returns updated member object + */ + @Patch("/:uid") + @UseGuards(AdminAuthGuard) + async updateMemberAndVerify(@Param('uid') uid, @Body() participantsRequest) { + const requestor = await this.membersService.findMemberByRole(); + const requestorEmail = requestor?.email ?? ''; + return await this.membersService.updateMemberFromParticipantsRequest(uid, participantsRequest, requestorEmail); + } + } From 075e34d750e3d040d8bd552a7985d1586f6c09f5 Mon Sep 17 00:00:00 2001 From: Navaneethakrishnan Date: Thu, 5 Dec 2024 13:26:53 +0530 Subject: [PATCH 37/41] fix: fixed issue in sending email for member email update --- apps/web-api/src/admin/member.controller.ts | 2 +- apps/web-api/src/members/members.controller.ts | 2 +- apps/web-api/src/members/members.service.ts | 7 +++++-- .../web-api/src/utils/notification/notification.service.ts | 4 ++-- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/apps/web-api/src/admin/member.controller.ts b/apps/web-api/src/admin/member.controller.ts index fa0bc841a..d5c13e50b 100644 --- a/apps/web-api/src/admin/member.controller.ts +++ b/apps/web-api/src/admin/member.controller.ts @@ -31,7 +31,7 @@ export class MemberController { async updateMemberAndVerify(@Param('uid') uid, @Body() participantsRequest) { const requestor = await this.membersService.findMemberByRole(); const requestorEmail = requestor?.email ?? ''; - return await this.membersService.updateMemberFromParticipantsRequest(uid, participantsRequest, requestorEmail); + return await this.membersService.updateMemberFromParticipantsRequest(uid, participantsRequest, requestorEmail, true); } } diff --git a/apps/web-api/src/members/members.controller.ts b/apps/web-api/src/members/members.controller.ts index c09670ef7..fd04d3616 100644 --- a/apps/web-api/src/members/members.controller.ts +++ b/apps/web-api/src/members/members.controller.ts @@ -172,7 +172,7 @@ export class MemberController { if (!isEmpty(participantsRequest.newData.isVerified) && !this.membersService.checkIfAdminUser(requestor)) { throw new ForbiddenException(`Member isn't authorized to verify a member`); } - return await this.membersService.updateMemberFromParticipantsRequest(uid, participantsRequest, requestor.email); + return await this.membersService.updateMemberFromParticipantsRequest(uid, participantsRequest, requestor.email, requestor.isDirectoryAdmin); } /** diff --git a/apps/web-api/src/members/members.service.ts b/apps/web-api/src/members/members.service.ts index 5d7d1fe07..05ade0347 100644 --- a/apps/web-api/src/members/members.service.ts +++ b/apps/web-api/src/members/members.service.ts @@ -634,7 +634,8 @@ export class MembersService { async updateMemberFromParticipantsRequest( memberUid: string, memberParticipantsRequest: ParticipantsRequest, - requestorEmail: string + requestorEmail: string, + isDirectoryAdmin=false ): Promise { let result; await this.prisma.$transaction(async (tx) => { @@ -655,7 +656,9 @@ export class MembersService { ); await this.updateMemberEmailChange(memberUid, isEmailChanged, isExternalIdAvailable, memberData, existingMember); await this.logParticipantRequest(requestorEmail, memberData, existingMember.uid, tx); - this.notificationService.notifyForMemberEditApproval(memberData.name, memberUid, requestorEmail); + if (isEmailChanged && isDirectoryAdmin) { + this.notificationService.notifyForMemberEditApproval(memberData.name, memberUid, [existingMember.email, memberData.email]); + } this.logger.info(`Member update request - completed, requestId -> ${result.uid}, requestor -> ${requestorEmail}`) }); await this.postUpdateActions(); diff --git a/apps/web-api/src/utils/notification/notification.service.ts b/apps/web-api/src/utils/notification/notification.service.ts index a6895db7c..3b6af9d44 100644 --- a/apps/web-api/src/utils/notification/notification.service.ts +++ b/apps/web-api/src/utils/notification/notification.service.ts @@ -105,7 +105,7 @@ export class NotificationService { * @param memberEmailId The email address of the member * @returns Sends an email notifying approval and posts a notification to Slack. */ - async notifyForMemberEditApproval(memberName: string, uid: string, memberEmailId: string) { + async notifyForMemberEditApproval(memberName: string, uid: string, memberEmailIds: string[]) { const memberUrl = `${process.env.WEB_UI_BASE_URL}/members/${uid}?utm_source=notification&utm_medium=email&utm_code=${getRandomId()}`; const slackConfig = { requestLabel: 'Edit Labber Request Completed', url: memberUrl, name: memberName }; await this.awsService.sendEmail( @@ -113,7 +113,7 @@ export class NotificationService { { memberName, memberUid: uid, adminSiteUrl: memberUrl } ); await this.awsService.sendEmail( - 'EditMemberSuccess', false, [memberEmailId], + 'EditMemberSuccess', false, memberEmailIds, { memberName, memberProfileLink: memberUrl } ); await this.slackService.notifyToChannel(slackConfig); From 0402b7daca3c858783c6e1b0f524fdffd916dc49 Mon Sep 17 00:00:00 2001 From: navneethkrish Date: Thu, 5 Dec 2024 16:09:37 +0530 Subject: [PATCH 38/41] fix: backoffice bug fixes --- .../footer-buttons/footer-buttons.tsx | 2 +- .../components/member-table/member-table.tsx | 2 +- apps/back-office/pages/member-view.tsx | 22 +++++++++++++++---- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/apps/back-office/components/footer-buttons/footer-buttons.tsx b/apps/back-office/components/footer-buttons/footer-buttons.tsx index 4073da62d..3d6c097da 100644 --- a/apps/back-office/components/footer-buttons/footer-buttons.tsx +++ b/apps/back-office/components/footer-buttons/footer-buttons.tsx @@ -92,7 +92,7 @@ export function FooterButtons(props) { Are you sure you want to reject?
- Clicking remove will remove the member from the list. + Clicking reject will remove the member from the list.
diff --git a/apps/back-office/components/member-table/member-table.tsx b/apps/back-office/components/member-table/member-table.tsx index cb571520e..82f93e578 100644 --- a/apps/back-office/components/member-table/member-table.tsx +++ b/apps/back-office/components/member-table/member-table.tsx @@ -393,7 +393,7 @@ const MemberTable = (props: any) => { Are you sure you want to reject?
- Clicking remove will remove the member from the list. + Clicking reject will remove the member from the list.
diff --git a/apps/back-office/pages/member-view.tsx b/apps/back-office/pages/member-view.tsx index 7f9df3ef7..a73b9b15c 100644 --- a/apps/back-office/pages/member-view.tsx +++ b/apps/back-office/pages/member-view.tsx @@ -535,14 +535,26 @@ export const getServerSideProps = async (context) => { }); } } else { - const approvedApiResponse = await api.get(`${API_ROUTE.MEMBERS}/${id}`, config); + const approvedApiResponse = await api.get(`${API_ROUTE.MEMBERS}/${id}?with=image`, config); + const skillsResponse = await api.get(API_ROUTE.SKILLS); + let counter = 1; if (approvedApiResponse.status === 200) { const requestData = approvedApiResponse?.data; + const teamAndRoles = + requestData?.teamMemberRoles?.length && + requestData?.teamMemberRoles?.map((team) => { + return { + role: team.role ?? "", + teamUid: team.teamUid, + teamTitle: team.team.name, + rowId: counter++, + }; + }); formValues = { name: requestData?.name, email: requestData?.email, - imageUid: requestData?.imageUid ?? '', + imageUid: requestData?.image?.url ?? '', imageFile: null, plnStartDate: requestData?.plnStartDate ? new Date(requestData?.plnStartDate).toISOString().split('T')[0] @@ -557,7 +569,7 @@ export const getServerSideProps = async (context) => { telegramHandler: requestData?.telegramHandler ?? '', officeHours: requestData?.officeHours ?? '', comments: requestData?.comments ?? '', - teamAndRoles: // teamAndRoles || + teamAndRoles: teamAndRoles || [ // { teamUid: '', teamTitle: '', role: '', rowId: 1 }, ], @@ -572,7 +584,9 @@ export const getServerSideProps = async (context) => { teamList = approvedApiResponse?.data?.teamList ?? []; memberList = approvedApiResponse?.data?.memberList ?? []; teams = approvedApiResponse?.data?.teams ?? []; - skills = approvedApiResponse?.data?.skills ?? []; + skills = skillsResponse?.data?.map((item) => { + return { value: item.uid, label: item.title }; + }); status= APP_CONSTANTS.PENDING_LABEL; } } From a86d72bdf9952604f0c636a7fb335f5568e7e470 Mon Sep 17 00:00:00 2001 From: madan v Date: Thu, 5 Dec 2024 17:55:50 +0530 Subject: [PATCH 39/41] fix: email template mismatch when admin edits member email --- apps/web-api/src/members/members.service.ts | 2 +- apps/web-api/src/utils/notification/notification.service.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web-api/src/members/members.service.ts b/apps/web-api/src/members/members.service.ts index 05ade0347..d514595f0 100644 --- a/apps/web-api/src/members/members.service.ts +++ b/apps/web-api/src/members/members.service.ts @@ -657,7 +657,7 @@ export class MembersService { await this.updateMemberEmailChange(memberUid, isEmailChanged, isExternalIdAvailable, memberData, existingMember); await this.logParticipantRequest(requestorEmail, memberData, existingMember.uid, tx); if (isEmailChanged && isDirectoryAdmin) { - this.notificationService.notifyForMemberEditApproval(memberData.name, memberUid, [existingMember.email, memberData.email]); + this.notificationService.notifyForMemberChangesByAdmin(memberData.name, memberUid, existingMember.email, memberData.email); } this.logger.info(`Member update request - completed, requestId -> ${result.uid}, requestor -> ${requestorEmail}`) }); diff --git a/apps/web-api/src/utils/notification/notification.service.ts b/apps/web-api/src/utils/notification/notification.service.ts index 3b6af9d44..a6895db7c 100644 --- a/apps/web-api/src/utils/notification/notification.service.ts +++ b/apps/web-api/src/utils/notification/notification.service.ts @@ -105,7 +105,7 @@ export class NotificationService { * @param memberEmailId The email address of the member * @returns Sends an email notifying approval and posts a notification to Slack. */ - async notifyForMemberEditApproval(memberName: string, uid: string, memberEmailIds: string[]) { + async notifyForMemberEditApproval(memberName: string, uid: string, memberEmailId: string) { const memberUrl = `${process.env.WEB_UI_BASE_URL}/members/${uid}?utm_source=notification&utm_medium=email&utm_code=${getRandomId()}`; const slackConfig = { requestLabel: 'Edit Labber Request Completed', url: memberUrl, name: memberName }; await this.awsService.sendEmail( @@ -113,7 +113,7 @@ export class NotificationService { { memberName, memberUid: uid, adminSiteUrl: memberUrl } ); await this.awsService.sendEmail( - 'EditMemberSuccess', false, memberEmailIds, + 'EditMemberSuccess', false, [memberEmailId], { memberName, memberProfileLink: memberUrl } ); await this.slackService.notifyToChannel(slackConfig); From a0791283c4cca614a4df14a0bbd310cb849241dc Mon Sep 17 00:00:00 2001 From: navneethkrish Date: Thu, 5 Dec 2024 20:04:05 +0530 Subject: [PATCH 40/41] fix: image url fix --- apps/back-office/pages/member-view.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/back-office/pages/member-view.tsx b/apps/back-office/pages/member-view.tsx index a73b9b15c..e26f91c95 100644 --- a/apps/back-office/pages/member-view.tsx +++ b/apps/back-office/pages/member-view.tsx @@ -211,7 +211,7 @@ export default function MemberView(props) { imageUid: image?.uid ?? values.imageUid, imageUrl: image?.url ?? imageUrl, }, - }; + }; const configuration = { headers: { authorization: `Bearer ${props.plnadmin}`, @@ -554,7 +554,7 @@ export const getServerSideProps = async (context) => { formValues = { name: requestData?.name, email: requestData?.email, - imageUid: requestData?.image?.url ?? '', + imageUid: requestData?.imageUid ?? '', imageFile: null, plnStartDate: requestData?.plnStartDate ? new Date(requestData?.plnStartDate).toISOString().split('T')[0] @@ -580,7 +580,7 @@ export const getServerSideProps = async (context) => { openToWork: requestData?.openToWork ?? '', projectContributions: requestData?.projectContributions ?? [] }; - imageUrl = requestData?.imageUrl ?? ''; + imageUrl = requestData?.image?.url ?? '', teamList = approvedApiResponse?.data?.teamList ?? []; memberList = approvedApiResponse?.data?.memberList ?? []; teams = approvedApiResponse?.data?.teams ?? []; From cf25c5d0e37e773998559da7e81a5fa29d269b46 Mon Sep 17 00:00:00 2001 From: navneethkrish Date: Thu, 5 Dec 2024 20:50:18 +0530 Subject: [PATCH 41/41] fix(back office): modified team and role mapping --- apps/web-api/src/members/members.service.ts | 31 ++++++++++++++------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/apps/web-api/src/members/members.service.ts b/apps/web-api/src/members/members.service.ts index d514595f0..8c6852a9d 100644 --- a/apps/web-api/src/members/members.service.ts +++ b/apps/web-api/src/members/members.service.ts @@ -41,7 +41,7 @@ export class MembersService { @Inject(forwardRef(() => NotificationService)) private notificationService: NotificationService, private cacheService: CacheService - ) {} + ) { } /** * Creates a new member in the database within a transaction. @@ -426,7 +426,7 @@ export class MembersService { newTokens = await this.authService.updateEmailInAuth(newEmail, oldEmail, memberInfo.externalId) }); this.logger.info(`Email has been successfully updated from ${oldEmail} to ${newEmail}`) - await this.cacheService.reset({ service: 'members'}); + await this.cacheService.reset({ service: 'members' }); return { refreshToken: newTokens.refresh_token, idToken: newTokens.id_token, @@ -628,14 +628,16 @@ export class MembersService { const memberData: any = memberParticipantRequest.newData; const member = await this.prepareMemberFromParticipantRequest(null, memberData, null, tx); await this.mapLocationToMember(memberData, null, member, tx); - return await this.createMember(member, tx); + const createdMember = await this.createMember(member, tx); + await this.postCreateActions(); + return createdMember; } async updateMemberFromParticipantsRequest( memberUid: string, memberParticipantsRequest: ParticipantsRequest, requestorEmail: string, - isDirectoryAdmin=false + isDirectoryAdmin = false ): Promise { let result; await this.prisma.$transaction(async (tx) => { @@ -711,8 +713,8 @@ export class MembersService { 'name', 'email', 'githubHandler', 'discordHandler', 'bio', 'twitterHandler', 'linkedinHandler', 'telegramHandler', 'officeHours', 'moreDetails', 'plnStartDate', 'openToWork', - 'isVerified', 'signUpSource', 'signUpMedium', 'signUpCampaign', - 'isUserConsent', 'isSubscribedToNewsletter', 'teamOrProjectURL', + 'isVerified', 'signUpSource', 'signUpMedium', 'signUpCampaign', + 'isUserConsent', 'isSubscribedToNewsletter', 'teamOrProjectURL', ]; copyObj(memberData, member, directFields); member.email = member.email.toLowerCase().trim(); @@ -720,7 +722,7 @@ export class MembersService { : type === 'Update' ? { disconnect: true } : undefined; member['skills'] = buildMultiRelationMapping('skills', memberData, type); if (type === 'Create') { - if (Array.isArray(memberData.teamMemberRoles)) { + if (Array.isArray(memberData.teamAndRoles)) { member['teamMemberRoles'] = this.buildTeamMemberRoles(memberData); } if (Array.isArray(memberData.projectContributions)) { @@ -1125,7 +1127,7 @@ export class MembersService { * @returns result */ async verifyMembers(memberIds: string[], userEmail): Promise { - return await this.prisma.$transaction(async (tx) => { + return await this.prisma.$transaction(async (tx) => { const result = await tx.member.updateMany({ where: { uid: { in: memberIds } }, data: { @@ -1168,16 +1170,25 @@ export class MembersService { */ async updatePreference(id: string, preferences: any): Promise { const updatedMember = await this.updateMemberByUid(id, { preferences }); - await this.cacheService.reset({ service: 'members'}); + await this.cacheService.reset({ service: 'members' }); return updatedMember; } + /** + * Executes post-create actions such as resetting the cache and triggering Airtable sync. + * This ensures that the system is up-to-date with the latest changes. + */ + private async postCreateActions(): Promise { + await this.cacheService.reset({ service: 'members'}); + await this.forestadminService.triggerAirtableSync(); + } + /** * Executes post-update actions such as resetting the cache and triggering Airtable sync. * This ensures that the system is up-to-date with the latest changes. */ private async postUpdateActions(): Promise { - await this.cacheService.reset({ service: 'members'}); + await this.cacheService.reset({ service: 'members' }); await this.forestadminService.triggerAirtableSync(); }