diff --git a/apps/portal/src/mappers/offers-mappers.ts b/apps/portal/src/mappers/offers-mappers.ts index a2a7c80f..e0cea797 100644 --- a/apps/portal/src/mappers/offers-mappers.ts +++ b/apps/portal/src/mappers/offers-mappers.ts @@ -31,6 +31,7 @@ import type { Education, Experience, GetOffersResponse, + Location, OffersCompany, Paging, Profile, @@ -45,6 +46,7 @@ import type { const analysisOfferDtoMapper = ( offer: OffersOffer & { company: Company; + location: City & { state: State & { country: Country } }; offersFullTime: | (OffersFullTime & { totalCompensation: OffersCurrency }) | null; @@ -52,7 +54,14 @@ const analysisOfferDtoMapper = ( profile: OffersProfile & { background: | (OffersBackground & { - experiences: Array; + experiences: Array< + OffersExperience & { + company: Company | null; + location: + | (City & { state: State & { country: Country } }) + | null; + } + >; }) | null; }; @@ -71,7 +80,7 @@ const analysisOfferDtoMapper = ( }, jobType: offer.jobType, level: offer.offersFullTime?.level ?? '', - location: offer.location, + location: locationDtoMapper(offer.location), monthYearReceived: offer.monthYearReceived, negotiationStrategy: offer.negotiationStrategy, previousCompanies: @@ -120,6 +129,7 @@ const analysisUnitDtoMapper = ( topSimilarOffers: Array< OffersOffer & { company: Company; + location: City & { state: State & { country: Country } }; offersFullTime: | (OffersFullTime & { totalCompensation: OffersCurrency }) | null; @@ -128,7 +138,12 @@ const analysisUnitDtoMapper = ( background: | (OffersBackground & { experiences: Array< - OffersExperience & { company: Company | null } + OffersExperience & { + company: Company | null; + location: + | (City & { state: State & { country: Country } }) + | null; + } >; }) | null; @@ -151,6 +166,7 @@ const analysisUnitDtoMapper = ( const analysisHighestOfferDtoMapper = ( offer: OffersOffer & { company: Company; + location: City & { state: State & { country: Country } }; offersFullTime: | (OffersFullTime & { totalCompensation: OffersCurrency }) | null; @@ -162,7 +178,7 @@ const analysisHighestOfferDtoMapper = ( company: offersCompanyDtoMapper(offer.company), id: offer.id, level: offer.offersFullTime?.level ?? '', - location: offer.location, + location: locationDtoMapper(offer.location), totalYoe: offer.profile.background?.totalYoe ?? -1, }; return analysisHighestOfferDto; @@ -176,6 +192,7 @@ export const profileAnalysisDtoMapper = ( topSimilarOffers: Array< OffersOffer & { company: Company; + location: City & { state: State & { country: Country } }; offersFullTime: | (OffersFullTime & { totalCompensation: OffersCurrency }) | null; @@ -186,7 +203,12 @@ export const profileAnalysisDtoMapper = ( background: | (OffersBackground & { experiences: Array< - OffersExperience & { company: Company | null } + OffersExperience & { + company: Company | null; + location: + | (City & { state: State & { country: Country } }) + | null; + } >; }) | null; @@ -199,6 +221,7 @@ export const profileAnalysisDtoMapper = ( topSimilarOffers: Array< OffersOffer & { company: Company; + location: City & { state: State & { country: Country } }; offersFullTime: | (OffersFullTime & { totalCompensation: OffersCurrency }) | null; @@ -209,7 +232,12 @@ export const profileAnalysisDtoMapper = ( background: | (OffersBackground & { experiences: Array< - OffersExperience & { company: Company | null } + OffersExperience & { + company: Company | null; + location: + | (City & { state: State & { country: Country } }) + | null; + } >; }) | null; @@ -219,6 +247,7 @@ export const profileAnalysisDtoMapper = ( }; overallHighestOffer: OffersOffer & { company: Company; + location: City & { state: State & { country: Country } }; offersFullTime: | (OffersFullTime & { totalCompensation: OffersCurrency }) | null; @@ -250,6 +279,23 @@ export const profileAnalysisDtoMapper = ( return profileAnalysisDto; }; +export const locationDtoMapper = ( + city: City & { state: State & { country: Country } }, +) => { + const { state } = city; + const { country } = state; + const locationDto: Location = { + cityId: city.id, + cityName: city.name, + countryCode: country.code, + countryId: country.id, + countryName: country.name, + stateId: state.id, + stateName: state.name, + }; + return locationDto; +}; + export const valuationDtoMapper = (currency: { baseCurrency: string; baseValue: number; @@ -303,6 +349,7 @@ export const educationDtoMapper = (education: { export const experienceDtoMapper = ( experience: OffersExperience & { company: Company | null; + location: (City & { state: State & { country: Country } }) | null; monthlySalary: OffersCurrency | null; totalCompensation: OffersCurrency | null; }, @@ -315,7 +362,10 @@ export const experienceDtoMapper = ( id: experience.id, jobType: experience.jobType, level: experience.level, - location: experience.location, + location: + experience.location != null + ? locationDtoMapper(experience.location) + : null, monthlySalary: experience.monthlySalary ? valuationDtoMapper(experience.monthlySalary) : null, @@ -348,6 +398,7 @@ export const backgroundDtoMapper = ( experiences: Array< OffersExperience & { company: Company | null; + location: (City & { state: State & { country: Country } }) | null; monthlySalary: OffersCurrency | null; totalCompensation: OffersCurrency | null; } @@ -386,6 +437,7 @@ export const backgroundDtoMapper = ( export const profileOfferDtoMapper = ( offer: OffersOffer & { company: Company; + location: City & { state: State & { country: Country } }; offersFullTime: | (OffersFullTime & { baseSalary: OffersCurrency | null; @@ -402,7 +454,7 @@ export const profileOfferDtoMapper = ( company: offersCompanyDtoMapper(offer.company), id: offer.id, jobType: offer.jobType, - location: offer.location, + location: locationDtoMapper(offer.location), monthYearReceived: offer.monthYearReceived, negotiationStrategy: offer.negotiationStrategy, offersFullTime: offer.offersFullTime, @@ -676,6 +728,7 @@ export const getUserProfileResponseMapper = ( offers: Array< OffersOffer & { company: Company; + location: City & { state: State & { country: Country } }; offersFullTime: | (OffersFullTime & { totalCompensation: OffersCurrency }) | null; @@ -711,6 +764,7 @@ export const getUserProfileResponseMapper = ( const userProfileOfferDtoMapper = ( offer: OffersOffer & { company: Company; + location: City & { state: State & { country: Country } }; offersFullTime: | (OffersFullTime & { totalCompensation: OffersCurrency }) | null; @@ -729,7 +783,7 @@ const userProfileOfferDtoMapper = ( }, jobType: offer.jobType, level: offer.offersFullTime?.level ?? '', - location: offer.location, + location: locationDtoMapper(offer.location), monthYearReceived: offer.monthYearReceived, title: offer.jobType === JobType.FULLTIME diff --git a/apps/portal/src/server/router/offers/offers-analysis-router.ts b/apps/portal/src/server/router/offers/offers-analysis-router.ts index 0b22c8a7..06e8db8c 100644 --- a/apps/portal/src/server/router/offers/offers-analysis-router.ts +++ b/apps/portal/src/server/router/offers/offers-analysis-router.ts @@ -19,6 +19,15 @@ export const offersAnalysisRouter = createRouter() topSimilarOffers: { include: { company: true, + location: { + include: { + state: { + include: { + country: true, + }, + }, + }, + }, offersFullTime: { include: { totalCompensation: true, @@ -36,6 +45,15 @@ export const offersAnalysisRouter = createRouter() experiences: { include: { company: true, + location: { + include: { + state: { + include: { + country: true, + }, + }, + }, + }, }, }, }, @@ -51,6 +69,15 @@ export const offersAnalysisRouter = createRouter() topSimilarOffers: { include: { company: true, + location: { + include: { + state: { + include: { + country: true, + }, + }, + }, + }, offersFullTime: { include: { totalCompensation: true, @@ -68,6 +95,15 @@ export const offersAnalysisRouter = createRouter() experiences: { include: { company: true, + location: { + include: { + state: { + include: { + country: true, + }, + }, + }, + }, }, }, }, @@ -81,6 +117,15 @@ export const offersAnalysisRouter = createRouter() overallHighestOffer: { include: { company: true, + location: { + include: { + state: { + include: { + country: true, + }, + }, + }, + }, offersFullTime: { include: { totalCompensation: true, diff --git a/apps/portal/src/server/router/offers/offers-user-profile-router.ts b/apps/portal/src/server/router/offers/offers-user-profile-router.ts index 48994044..ae816261 100644 --- a/apps/portal/src/server/router/offers/offers-user-profile-router.ts +++ b/apps/portal/src/server/router/offers/offers-user-profile-router.ts @@ -57,6 +57,15 @@ export const offersUserProfileRouter = createProtectedRouter() offers: { include: { company: true, + location: { + include: { + state: { + include: { + country: true, + }, + }, + }, + }, offersFullTime: { include: { totalCompensation: true, diff --git a/apps/portal/src/server/router/offers/offers.ts b/apps/portal/src/server/router/offers/offers.ts index cac77069..515cad3f 100644 --- a/apps/portal/src/server/router/offers/offers.ts +++ b/apps/portal/src/server/router/offers/offers.ts @@ -40,12 +40,12 @@ const getYoeRange = (yoeCategory: number) => { export const offersRouter = createRouter().query('list', { input: z.object({ + cityId: z.string(), companyId: z.string().nullish(), currency: z.string().nullish(), dateEnd: z.date().nullish(), dateStart: z.date().nullish(), limit: z.number().positive(), - location: z.string(), offset: z.number().nonnegative(), salaryMax: z.number().nonnegative().nullish(), salaryMin: z.number().nonnegative().nullish(), @@ -129,8 +129,7 @@ export const offersRouter = createRouter().query('list', { where: { AND: [ { - location: - input.location.length === 0 ? undefined : input.location, + cityId: input.cityId.length === 0 ? undefined : input.cityId, }, { offersIntern: { @@ -243,8 +242,7 @@ export const offersRouter = createRouter().query('list', { where: { AND: [ { - location: - input.location.length === 0 ? undefined : input.location, + cityId: input.cityId.length === 0 ? undefined : input.cityId, }, { offersIntern: { diff --git a/apps/portal/src/types/offers.d.ts b/apps/portal/src/types/offers.d.ts index 2ff82a16..3dcba938 100644 --- a/apps/portal/src/types/offers.d.ts +++ b/apps/portal/src/types/offers.d.ts @@ -25,7 +25,7 @@ export type Experience = { id: string; jobType: JobType?; level: string?; - location: string?; + location: Location?; monthlySalary: Valuation?; title: string?; totalCompensation: Valuation?; @@ -79,7 +79,7 @@ export type ProfileOffer = { company: OffersCompany; id: string; jobType: JobType; - location: string; + location: Location; monthYearReceived: Date; negotiationStrategy: string; offersFullTime: FullTime?; @@ -163,7 +163,7 @@ export type AnalysisHighestOffer = { company: OffersCompany; id: string; level: string; - location: string; + location: Location; totalYoe: number; }; @@ -173,7 +173,7 @@ export type AnalysisOffer = { income: Valuation; jobType: JobType; level: string; - location: string; + location: Location; monthYearReceived: Date; negotiationStrategy: string; previousCompanies: Array; @@ -202,7 +202,17 @@ export type UserProfileOffer = { income: Valuation; jobType: JobType; level: string; - location: string; + location: Location; monthYearReceived: Date; title: string; }; + +export type Location = { + cityId: string; + cityName: string; + countryCode: string; + countryId: string; + countryName: string; + stateId: string; + stateName: string; +}; diff --git a/apps/portal/src/utils/offers/analysisGeneration.ts b/apps/portal/src/utils/offers/analysisGeneration.ts index 75ba4f46..a2594079 100644 --- a/apps/portal/src/utils/offers/analysisGeneration.ts +++ b/apps/portal/src/utils/offers/analysisGeneration.ts @@ -1,6 +1,8 @@ import type { Session } from 'next-auth'; import type { + City, Company, + Country, OffersBackground, OffersCurrency, OffersFullTime, @@ -9,6 +11,7 @@ import type { OffersProfile, Prisma, PrismaClient, + State, } from '@prisma/client'; import { TRPCError } from '@trpc/server'; @@ -16,8 +19,14 @@ import { profileAnalysisDtoMapper } from '../../mappers/offers-mappers'; type Offer = OffersOffer & { company: Company; + location: City & { state: State & { country: Country } }; offersFullTime: - | (OffersFullTime & { totalCompensation: OffersCurrency }) + | (OffersFullTime & { + baseSalary: OffersCurrency | null; + bonus: OffersCurrency | null; + stocks: OffersCurrency | null; + totalCompensation: OffersCurrency; + }) | null; offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null; profile: OffersProfile & { background: OffersBackground | null }; @@ -68,6 +77,15 @@ export const generateAnalysis = async (params: { const offers = await ctx.prisma.offersOffer.findMany({ include: { company: true, + location: { + include: { + state: { + include: { + country: true, + }, + }, + }, + }, offersFullTime: { include: { baseSalary: true, @@ -131,9 +149,18 @@ export const generateAnalysis = async (params: { const monthYearReceived = new Date(overallHighestOffer.monthYearReceived); monthYearReceived.setFullYear(monthYearReceived.getFullYear() - 1); - const similarOffers = await ctx.prisma.offersOffer.findMany({ + let similarOffers = await ctx.prisma.offersOffer.findMany({ include: { company: true, + location: { + include: { + state: { + include: { + country: true, + }, + }, + }, + }, offersFullTime: { include: { totalCompensation: true, @@ -151,6 +178,15 @@ export const generateAnalysis = async (params: { experiences: { include: { company: true, + location: { + include: { + state: { + include: { + country: true, + }, + }, + }, + }, }, }, }, @@ -225,7 +261,7 @@ export const generateAnalysis = async (params: { const companyAnalysis = Array.from(companyMap.values()).map( (companyOffer) => { // TODO: Refactor calculating analysis into a function - const similarCompanyOffers = similarOffers.filter( + let similarCompanyOffers = similarOffers.filter( (offer) => offer.companyId === companyOffer.companyId, ); @@ -239,23 +275,21 @@ export const generateAnalysis = async (params: { : 100 - (100 * companyIndex) / (similarCompanyOffers.length - 1); // Get top offers (excluding user's offer) - const similarCompanyOffersWithoutUsersOffers = - similarCompanyOffers.filter( - (offer) => offer.profileId !== input.profileId, - ); + similarCompanyOffers = similarCompanyOffers.filter( + (offer) => offer.id !== companyOffer.id, + ); - const noOfSimilarCompanyOffers = - similarCompanyOffersWithoutUsersOffers.length; + const noOfSimilarCompanyOffers = similarCompanyOffers.length; const similarCompanyOffers90PercentileIndex = Math.ceil( noOfSimilarCompanyOffers * 0.1, ); const topPercentileCompanyOffers = noOfSimilarCompanyOffers > 2 - ? similarCompanyOffersWithoutUsersOffers.slice( + ? similarCompanyOffers.slice( similarCompanyOffers90PercentileIndex, similarCompanyOffers90PercentileIndex + 2, ) - : similarCompanyOffersWithoutUsersOffers; + : similarCompanyOffers; return { companyName: companyOffer.company.name, @@ -276,19 +310,19 @@ export const generateAnalysis = async (params: { ? 100 : 100 - (100 * overallIndex) / (similarOffers.length - 1); - const similarOffersWithoutUsersOffers = similarOffers.filter( - (similarOffer) => similarOffer.profileId !== input.profileId, + similarOffers = similarOffers.filter( + (offer) => offer.id !== overallHighestOffer.id, ); - const noOfSimilarOffers = similarOffersWithoutUsersOffers.length; + const noOfSimilarOffers = similarOffers.length; const similarOffers90PercentileIndex = Math.ceil(noOfSimilarOffers * 0.1); const topPercentileOffers = noOfSimilarOffers > 2 - ? similarOffersWithoutUsersOffers.slice( + ? similarOffers.slice( similarOffers90PercentileIndex, similarOffers90PercentileIndex + 2, ) - : similarOffersWithoutUsersOffers; + : similarOffers; const analysis = await ctx.prisma.offersAnalysis.create({ data: { @@ -335,6 +369,15 @@ export const generateAnalysis = async (params: { topSimilarOffers: { include: { company: true, + location: { + include: { + state: { + include: { + country: true, + }, + }, + }, + }, offersFullTime: { include: { totalCompensation: true, @@ -352,6 +395,15 @@ export const generateAnalysis = async (params: { experiences: { include: { company: true, + location: { + include: { + state: { + include: { + country: true, + }, + }, + }, + }, }, }, }, @@ -367,6 +419,15 @@ export const generateAnalysis = async (params: { topSimilarOffers: { include: { company: true, + location: { + include: { + state: { + include: { + country: true, + }, + }, + }, + }, offersFullTime: { include: { totalCompensation: true, @@ -384,6 +445,15 @@ export const generateAnalysis = async (params: { experiences: { include: { company: true, + location: { + include: { + state: { + include: { + country: true, + }, + }, + }, + }, }, }, }, @@ -397,6 +467,15 @@ export const generateAnalysis = async (params: { overallHighestOffer: { include: { company: true, + location: { + include: { + state: { + include: { + country: true, + }, + }, + }, + }, offersFullTime: { include: { totalCompensation: true,