diff --git a/apps/portal/prisma/schema.prisma b/apps/portal/prisma/schema.prisma index 744c15ae..8b0fcbd9 100644 --- a/apps/portal/prisma/schema.prisma +++ b/apps/portal/prisma/schema.prisma @@ -131,6 +131,8 @@ model City { stateId String state State @relation(fields: [stateId], references: [id]) questionsQuestionEncounters QuestionsQuestionEncounter[] + OffersExperience OffersExperience[] + OffersOffer OffersOffer[] @@unique([name, stateId]) } @@ -265,7 +267,8 @@ model OffersExperience { // Add more fields durationInMonths Int? - location String? + location City? @relation(fields: [cityId], references: [id]) + cityId String? // FULLTIME fields level String? @@ -348,8 +351,9 @@ model OffersOffer { company Company @relation(fields: [companyId], references: [id]) companyId String + location City @relation(fields: [cityId], references: [id]) + cityId String monthYearReceived DateTime - location String negotiationStrategy String comments String diff --git a/apps/portal/src/components/offers/constants.ts b/apps/portal/src/components/offers/constants.ts index e2b14d96..d49dca1a 100644 --- a/apps/portal/src/components/offers/constants.ts +++ b/apps/portal/src/components/offers/constants.ts @@ -2,21 +2,6 @@ import { EducationBackgroundType } from './types'; export const emptyOption = '----'; -export const locationOptions = [ - { - label: 'Singapore, Singapore', - value: 'Singapore, Singapore', - }, - { - label: 'New York, US', - value: 'New York, US', - }, - { - label: 'San Francisco, US', - value: 'San Francisco, US', - }, -]; - export const internshipCycleOptions = [ { label: 'Summer', diff --git a/apps/portal/src/components/offers/dashboard/DashboardOfferCard.tsx b/apps/portal/src/components/offers/dashboard/DashboardOfferCard.tsx index df49ada9..748a4142 100644 --- a/apps/portal/src/components/offers/dashboard/DashboardOfferCard.tsx +++ b/apps/portal/src/components/offers/dashboard/DashboardOfferCard.tsx @@ -36,7 +36,7 @@ export default function DashboardProfileCard({

{location - ? `Company: ${company.name}, ${location}` + ? `Company: ${company.name}, ${location.cityName}` : `Company: ${company.name}`}

{level &&

Level: {level}

} diff --git a/apps/portal/src/components/offers/offersSubmission/OffersSubmissionForm.tsx b/apps/portal/src/components/offers/offersSubmission/OffersSubmissionForm.tsx index 325c9afc..98e9146a 100644 --- a/apps/portal/src/components/offers/offersSubmission/OffersSubmissionForm.tsx +++ b/apps/portal/src/components/offers/offersSubmission/OffersSubmissionForm.tsx @@ -26,11 +26,11 @@ import { getCurrentMonth, getCurrentYear } from '~/utils/offers/time'; import { trpc } from '~/utils/trpc'; const defaultOfferValues = { + cityId: '', comments: '', companyId: '', jobTitle: '', jobType: JobType.FULLTIME, - location: '', monthYearReceived: { month: getCurrentMonth() as Month, year: getCurrentYear(), @@ -277,7 +277,7 @@ export default function OffersSubmissionForm({
{steps[step]} - {/*
{JSON.stringify(formMethods.watch(), null, 2)}
*/} +
{JSON.stringify(formMethods.watch(), null, 2)}
{step === 0 && (
@@ -210,6 +227,12 @@ function InternshipJobFields() { const watchCompanyName = useWatch({ name: 'background.experiences.0.companyName', }); + const watchCityId = useWatch({ + name: 'background.experiences.0.cityId', + }); + const watchCityName = useWatch({ + name: 'background.experiences.0.cityName', + }); return ( <> @@ -271,12 +294,22 @@ function InternshipJobFields() {
- { + if (option) { + setValue('background.experiences.0.cityId', option.value); + setValue('background.experiences.0.cityName', option.label); + } else { + setValue('background.experiences.0.cityId', ''); + setValue('background.experiences.0.cityName', ''); + } + }} />
diff --git a/apps/portal/src/components/offers/offersSubmission/submissionForm/OfferDetailsForm.tsx b/apps/portal/src/components/offers/offersSubmission/submissionForm/OfferDetailsForm.tsx index 63abe7d1..6eed3983 100644 --- a/apps/portal/src/components/offers/offersSubmission/submissionForm/OfferDetailsForm.tsx +++ b/apps/portal/src/components/offers/offersSubmission/submissionForm/OfferDetailsForm.tsx @@ -12,6 +12,7 @@ import { TrashIcon } from '@heroicons/react/24/outline'; import { JobType } from '@prisma/client'; import { Button, Dialog } from '@tih/ui'; +import CitiesTypeahead from '~/components/shared/CitiesTypeahead'; import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead'; import type { JobTitleType } from '~/components/shared/JobTitles'; import { getLabelForJobTitleType } from '~/components/shared/JobTitles'; @@ -25,7 +26,6 @@ import { emptyOption, FieldError, internshipCycleOptions, - locationOptions, yearOptions, } from '../../constants'; import FormMonthYearPicker from '../../forms/FormMonthYearPicker'; @@ -62,6 +62,12 @@ function FullTimeOfferDetailsForm({ const watchCompanyName = useWatch({ name: `offers.${index}.companyName`, }); + const watchCityId = useWatch({ + name: `offers.${index}.cityId`, + }); + const watchCityName = useWatch({ + name: `offers.${index}.cityName`, + }); const watchCurrency = useWatch({ name: `offers.${index}.offersFullTime.totalCompensation.currency`, }); @@ -104,32 +110,37 @@ function FullTimeOfferDetailsForm({ />
-
- { - if (option) { - setValue(`offers.${index}.companyId`, option.value); - setValue(`offers.${index}.companyName`, option.label); - } - }} - /> -
- { + if (option) { + setValue(`offers.${index}.companyId`, option.value); + setValue(`offers.${index}.companyName`, option.label); + } + }} + /> + { + if (option) { + setValue(`offers.${index}.cityId`, option.value); + setValue(`offers.${index}.cityName`, option.label); + } else { + setValue(`offers.${index}.cityId`, ''); + setValue(`offers.${index}.cityName`, ''); + } + }} />
@@ -305,6 +316,12 @@ function InternshipOfferDetailsForm({ const watchCompanyName = useWatch({ name: `offers.${index}.companyName`, }); + const watchCityId = useWatch({ + name: `offers.${index}.cityId`, + }); + const watchCityName = useWatch({ + name: `offers.${index}.cityName`, + }); return (
@@ -342,16 +359,23 @@ function InternshipOfferDetailsForm({ }} />
- { + if (option) { + setValue(`offers.${index}.cityId`, option.value); + setValue(`offers.${index}.cityName`, option.label); + } else { + setValue(`offers.${index}.cityId`, ''); + setValue(`offers.${index}.cityName`, ''); + } + }} />
diff --git a/apps/portal/src/components/offers/profile/OfferCard.tsx b/apps/portal/src/components/offers/profile/OfferCard.tsx index 6d02f134..0fcc9823 100644 --- a/apps/portal/src/components/offers/profile/OfferCard.tsx +++ b/apps/portal/src/components/offers/profile/OfferCard.tsx @@ -40,7 +40,7 @@ export default function OfferCard({ - {location ? `${companyName}, ${location}` : companyName} + {location ? `${companyName}, ${location.cityName}` : companyName}
@@ -92,11 +92,11 @@ export default function OfferCard({
)} - {totalCompensation && ( + {(base || stocks || bonus) && totalCompensation && (

- Base / year: {base} ⋅ Stocks / year: {stocks} ⋅ Bonus / year:{' '} - {bonus} + Base / year: {base ?? 'N/A'} ⋅ Stocks / year:{' '} + {stocks ?? 'N/A'} ⋅ Bonus / year: {bonus ?? 'N/A'}

)} diff --git a/apps/portal/src/components/offers/table/OffersTable.tsx b/apps/portal/src/components/offers/table/OffersTable.tsx index 627cb330..9a400b76 100644 --- a/apps/portal/src/components/offers/table/OffersTable.tsx +++ b/apps/portal/src/components/offers/table/OffersTable.tsx @@ -21,10 +21,12 @@ import type { DashboardOffer, GetOffersResponse, Paging } from '~/types/offers'; const NUMBER_OF_OFFERS_IN_PAGE = 10; export type OffersTableProps = Readonly<{ + cityFilter: string; companyFilter: string; jobTitleFilter: string; }>; export default function OffersTable({ + cityFilter, companyFilter, jobTitleFilter, }: OffersTableProps) { @@ -53,10 +55,11 @@ export default function OffersTable({ [ 'offers.list', { + // Location: 'Singapore, Singapore', // TODO: Geolocation + cityId: cityFilter, companyId: companyFilter, currency, limit: NUMBER_OF_OFFERS_IN_PAGE, - location: 'Singapore, Singapore', // TODO: Geolocation offset: pagination.currentPage, sortBy: OfferTableSortBy[selectedFilter] ?? '-monthYearReceived', title: jobTitleFilter, @@ -102,8 +105,8 @@ export default function OffersTable({ ))}
-
- View all offers in +
+ Display offers in setCurrency(value)} selectedCurrency={currency} diff --git a/apps/portal/src/components/offers/types.ts b/apps/portal/src/components/offers/types.ts index 366ca25b..0e963a17 100644 --- a/apps/portal/src/components/offers/types.ts +++ b/apps/portal/src/components/offers/types.ts @@ -2,6 +2,8 @@ import type { JobType } from '@prisma/client'; import type { MonthYear } from '~/components/shared/MonthYearPicker'; +import type { Location } from '~/types/offers'; + export const HOME_URL = '/offers'; /* @@ -44,13 +46,14 @@ export type BackgroundPostData = { }; type ExperiencePostData = { + cityId?: string | null; + cityName?: string | null; companyId?: string | null; companyName?: string | null; durationInMonths?: number | null; id?: string; jobType?: string | null; level?: string | null; - location?: string | null; monthlySalary?: Money | null; title?: string | null; totalCompensation?: Money | null; @@ -75,12 +78,13 @@ type SpecificYoePostData = { type SpecificYoe = SpecificYoePostData; export type OfferPostData = { + cityId: string; + cityName?: string; comments: string; companyId: string; companyName?: string; id?: string; jobType: JobType; - location: string; monthYearReceived: Date; negotiationStrategy: string; offersFullTime?: OfferFullTimePostData | null; @@ -132,7 +136,7 @@ export type OfferDisplayData = { jobLevel?: string | null; jobTitle?: string | null; jobType?: JobType; - location?: string | null; + location?: Location | null; monthlySalary?: string | null; negotiationStrategy?: string | null; otherComment?: string | null; diff --git a/apps/portal/src/mappers/offers-mappers.ts b/apps/portal/src/mappers/offers-mappers.ts index 7ce52626..e0cea797 100644 --- a/apps/portal/src/mappers/offers-mappers.ts +++ b/apps/portal/src/mappers/offers-mappers.ts @@ -1,5 +1,7 @@ import type { + City, Company, + Country, OffersAnalysis, OffersAnalysisUnit, OffersBackground, @@ -12,6 +14,7 @@ import type { OffersProfile, OffersReply, OffersSpecificYoe, + State, User, } from '@prisma/client'; import { JobType } from '@prisma/client'; @@ -28,6 +31,7 @@ import type { Education, Experience, GetOffersResponse, + Location, OffersCompany, Paging, Profile, @@ -42,6 +46,7 @@ import type { const analysisOfferDtoMapper = ( offer: OffersOffer & { company: Company; + location: City & { state: State & { country: Country } }; offersFullTime: | (OffersFullTime & { totalCompensation: OffersCurrency }) | null; @@ -49,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; }; @@ -68,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: @@ -117,6 +129,7 @@ const analysisUnitDtoMapper = ( topSimilarOffers: Array< OffersOffer & { company: Company; + location: City & { state: State & { country: Country } }; offersFullTime: | (OffersFullTime & { totalCompensation: OffersCurrency }) | null; @@ -125,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; @@ -148,6 +166,7 @@ const analysisUnitDtoMapper = ( const analysisHighestOfferDtoMapper = ( offer: OffersOffer & { company: Company; + location: City & { state: State & { country: Country } }; offersFullTime: | (OffersFullTime & { totalCompensation: OffersCurrency }) | null; @@ -159,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; @@ -173,6 +192,7 @@ export const profileAnalysisDtoMapper = ( topSimilarOffers: Array< OffersOffer & { company: Company; + location: City & { state: State & { country: Country } }; offersFullTime: | (OffersFullTime & { totalCompensation: OffersCurrency }) | null; @@ -183,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; @@ -196,6 +221,7 @@ export const profileAnalysisDtoMapper = ( topSimilarOffers: Array< OffersOffer & { company: Company; + location: City & { state: State & { country: Country } }; offersFullTime: | (OffersFullTime & { totalCompensation: OffersCurrency }) | null; @@ -206,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; @@ -216,6 +247,7 @@ export const profileAnalysisDtoMapper = ( }; overallHighestOffer: OffersOffer & { company: Company; + location: City & { state: State & { country: Country } }; offersFullTime: | (OffersFullTime & { totalCompensation: OffersCurrency }) | null; @@ -247,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; @@ -300,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; }, @@ -312,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, @@ -345,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; } @@ -383,6 +437,7 @@ export const backgroundDtoMapper = ( export const profileOfferDtoMapper = ( offer: OffersOffer & { company: Company; + location: City & { state: State & { country: Country } }; offersFullTime: | (OffersFullTime & { baseSalary: OffersCurrency | null; @@ -399,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, @@ -449,6 +504,7 @@ export const profileDtoMapper = ( topSimilarOffers: Array< OffersOffer & { company: Company; + location: City & { state: State & { country: Country } }; offersFullTime: | (OffersFullTime & { totalCompensation: OffersCurrency }) | null; @@ -459,7 +515,14 @@ export const profileDtoMapper = ( background: | (OffersBackground & { experiences: Array< - OffersExperience & { company: Company | null } + OffersExperience & { + company: Company | null; + location: + | (City & { + state: State & { country: Country }; + }) + | null; + } >; }) | null; @@ -472,6 +535,7 @@ export const profileDtoMapper = ( topSimilarOffers: Array< OffersOffer & { company: Company; + location: City & { state: State & { country: Country } }; offersFullTime: | (OffersFullTime & { totalCompensation: OffersCurrency }) | null; @@ -482,7 +546,12 @@ export const profileDtoMapper = ( background: | (OffersBackground & { experiences: Array< - OffersExperience & { company: Company | null } + OffersExperience & { + company: Company | null; + location: + | (City & { state: State & { country: Country } }) + | null; + } >; }) | null; @@ -492,6 +561,7 @@ export const profileDtoMapper = ( }; overallHighestOffer: OffersOffer & { company: Company; + location: City & { state: State & { country: Country } }; offersFullTime: | (OffersFullTime & { totalCompensation: OffersCurrency }) | null; @@ -508,6 +578,7 @@ export const profileDtoMapper = ( experiences: Array< OffersExperience & { company: Company | null; + location: (City & { state: State & { country: Country } }) | null; monthlySalary: OffersCurrency | null; totalCompensation: OffersCurrency | null; } @@ -525,6 +596,7 @@ export const profileDtoMapper = ( offers: Array< OffersOffer & { company: Company; + location: City & { state: State & { country: Country } }; offersFullTime: | (OffersFullTime & { baseSalary: OffersCurrency | null; @@ -656,6 +728,7 @@ export const getUserProfileResponseMapper = ( offers: Array< OffersOffer & { company: Company; + location: City & { state: State & { country: Country } }; offersFullTime: | (OffersFullTime & { totalCompensation: OffersCurrency }) | null; @@ -691,6 +764,7 @@ export const getUserProfileResponseMapper = ( const userProfileOfferDtoMapper = ( offer: OffersOffer & { company: Company; + location: City & { state: State & { country: Country } }; offersFullTime: | (OffersFullTime & { totalCompensation: OffersCurrency }) | null; @@ -709,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/pages/offers/index.tsx b/apps/portal/src/pages/offers/index.tsx index e696ca10..ad0ff41c 100644 --- a/apps/portal/src/pages/offers/index.tsx +++ b/apps/portal/src/pages/offers/index.tsx @@ -1,15 +1,18 @@ import Link from 'next/link'; import { useState } from 'react'; +import { MapPinIcon } from '@heroicons/react/24/outline'; import { Banner } from '@tih/ui'; import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics'; import OffersTable from '~/components/offers/table/OffersTable'; +import CitiesTypeahead from '~/components/shared/CitiesTypeahead'; import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead'; import JobTitlesTypeahead from '~/components/shared/JobTitlesTypahead'; export default function OffersHomePage() { const [jobTitleFilter, setjobTitleFilter] = useState('software-engineer'); const [companyFilter, setCompanyFilter] = useState(''); + const [cityFilter, setCityFilter] = useState(''); const { event: gaEvent } = useGoogleAnalytics(); return ( @@ -21,6 +24,25 @@ export default function OffersHomePage() { . ⭐ +
+ + + + { + if (option) { + setCityFilter(option.value); + gaEvent({ + action: `offers.table_filter_city_${option.value}`, + category: 'engagement', + label: 'Filter by city', + }); + } + }} + /> +
@@ -58,7 +80,7 @@ export default function OffersHomePage() { if (option) { setCompanyFilter(option.value); gaEvent({ - action: 'offers.table_filter_company', + action: `offers.table_filter_company_${option.value}`, category: 'engagement', label: 'Filter by company', }); @@ -70,6 +92,7 @@ export default function OffersHomePage() {
diff --git a/apps/portal/src/pages/offers/profile/edit/[offerProfileId].tsx b/apps/portal/src/pages/offers/profile/edit/[offerProfileId].tsx index 05e47b7c..829acb28 100644 --- a/apps/portal/src/pages/offers/profile/edit/[offerProfileId].tsx +++ b/apps/portal/src/pages/offers/profile/edit/[offerProfileId].tsx @@ -36,13 +36,14 @@ export default function OffersEditPage() { experiences.length === 0 ? [{ jobType: JobType.FULLTIME }] : experiences.map((exp) => ({ + cityId: exp.location?.cityId, + cityName: exp.location?.cityName, companyId: exp.company?.id, companyName: exp.company?.name, durationInMonths: exp.durationInMonths, id: exp.id, jobType: exp.jobType, level: exp.level, - location: exp.location, monthlySalary: exp.monthlySalary, title: exp.title, totalCompensation: exp.totalCompensation, @@ -53,12 +54,13 @@ export default function OffersEditPage() { }, id: data.id, offers: data.offers.map((offer) => ({ + cityId: offer.location.cityId, + cityName: offer.location.cityName, comments: offer.comments, companyId: offer.company.id, companyName: offer.company.name, id: offer.id, jobType: offer.jobType, - location: offer.location, monthYearReceived: convertToMonthYear(offer.monthYearReceived), negotiationStrategy: offer.negotiationStrategy, offersFullTime: offer.offersFullTime, 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 55689f54..0c084763 100644 --- a/apps/portal/src/server/router/offers/offers-analysis-router.ts +++ b/apps/portal/src/server/router/offers/offers-analysis-router.ts @@ -1,4 +1,19 @@ import { z } from 'zod'; +<<<<<<< HEAD +======= +import type { + City, + Company, + Country, + OffersBackground, + OffersCurrency, + OffersFullTime, + OffersIntern, + OffersOffer, + OffersProfile, + State, +} from '@prisma/client'; +>>>>>>> BryannYeap/location import { TRPCError } from '@trpc/server'; import { generateAnalysis } from '~/utils/offers/analysisGeneration'; @@ -19,6 +34,15 @@ export const offersAnalysisRouter = createRouter() topSimilarOffers: { include: { company: true, + location: { + include: { + state: { + include: { + country: true, + }, + }, + }, + }, offersFullTime: { include: { totalCompensation: true, @@ -36,6 +60,15 @@ export const offersAnalysisRouter = createRouter() experiences: { include: { company: true, + location: { + include: { + state: { + include: { + country: true, + }, + }, + }, + }, }, }, }, @@ -51,6 +84,15 @@ export const offersAnalysisRouter = createRouter() topSimilarOffers: { include: { company: true, + location: { + include: { + state: { + include: { + country: true, + }, + }, + }, + }, offersFullTime: { include: { totalCompensation: true, @@ -68,6 +110,15 @@ export const offersAnalysisRouter = createRouter() experiences: { include: { company: true, + location: { + include: { + state: { + include: { + country: true, + }, + }, + }, + }, }, }, }, @@ -81,6 +132,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-profile-router.ts b/apps/portal/src/server/router/offers/offers-profile-router.ts index 42c15eb3..8617da46 100644 --- a/apps/portal/src/server/router/offers/offers-profile-router.ts +++ b/apps/portal/src/server/router/offers/offers-profile-router.ts @@ -35,12 +35,12 @@ const company = z.object({ }); const offer = z.object({ + cityId: z.string(), comments: z.string(), company: company.nullish(), companyId: z.string(), id: z.string().optional(), jobType: z.string().regex(createValidationRegex(Object.keys(JobType), null)), - location: z.string(), monthYearReceived: z.date(), negotiationStrategy: z.string(), offersFullTime: z @@ -75,6 +75,7 @@ const offer = z.object({ const experience = z.object({ backgroundId: z.string().nullish(), + cityId: z.string().nullish(), company: company.nullish(), companyId: z.string().nullish(), durationInMonths: z.number().nullish(), @@ -84,7 +85,6 @@ const experience = z.object({ .regex(createValidationRegex(Object.keys(JobType), null)) .nullish(), level: z.string().nullish(), - location: z.string().nullish(), monthlySalary: valuation.nullish(), monthlySalaryId: z.string().nullish(), title: z.string().nullish(), @@ -171,6 +171,15 @@ export const offersProfileRouter = createRouter() topSimilarOffers: { include: { company: true, + location: { + include: { + state: { + include: { + country: true, + }, + }, + }, + }, offersFullTime: { include: { totalCompensation: true, @@ -188,6 +197,15 @@ export const offersProfileRouter = createRouter() experiences: { include: { company: true, + location: { + include: { + state: { + include: { + country: true, + }, + }, + }, + }, }, }, }, @@ -203,6 +221,15 @@ export const offersProfileRouter = createRouter() topSimilarOffers: { include: { company: true, + location: { + include: { + state: { + include: { + country: true, + }, + }, + }, + }, offersFullTime: { include: { totalCompensation: true, @@ -220,6 +247,15 @@ export const offersProfileRouter = createRouter() experiences: { include: { company: true, + location: { + include: { + state: { + include: { + country: true, + }, + }, + }, + }, }, }, }, @@ -233,6 +269,15 @@ export const offersProfileRouter = createRouter() overallHighestOffer: { include: { company: true, + location: { + include: { + state: { + include: { + country: true, + }, + }, + }, + }, offersFullTime: { include: { totalCompensation: true, @@ -258,6 +303,15 @@ export const offersProfileRouter = createRouter() experiences: { include: { company: true, + location: { + include: { + state: { + include: { + country: true, + }, + }, + }, + }, monthlySalary: true, totalCompensation: true, }, @@ -275,6 +329,15 @@ export const offersProfileRouter = createRouter() offers: { include: { company: true, + location: { + include: { + state: { + include: { + country: true, + }, + }, + }, + }, offersFullTime: { include: { baseSalary: true, @@ -350,6 +413,39 @@ export const offersProfileRouter = createRouter() input.background.experiences.map(async (x) => { if (x.jobType === JobType.FULLTIME) { if (x.companyId) { + if (x.cityId) { + return { + company: { + connect: { + id: x.companyId, + }, + }, + durationInMonths: x.durationInMonths, + jobType: x.jobType, + level: x.level, + location: { + connect: { + id: x.cityId + } + }, + title: x.title, + totalCompensation: + x.totalCompensation != null + ? { + create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + x.totalCompensation.value, + x.totalCompensation.currency, + baseCurrencyString, + ), + currency: x.totalCompensation.currency, + value: x.totalCompensation.value, + }, + } + : undefined, + }; + } return { company: { connect: { @@ -377,11 +473,40 @@ export const offersProfileRouter = createRouter() : undefined, }; } + if (x.cityId) { + return { + durationInMonths: x.durationInMonths, + jobType: x.jobType, + level: x.level, + location: { + connect: { + where: { + id: x.cityId + } + } + }, + title: x.title, + totalCompensation: + x.totalCompensation != null + ? { + create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + x.totalCompensation.value, + x.totalCompensation.currency, + baseCurrencyString, + ), + currency: x.totalCompensation.currency, + value: x.totalCompensation.value, + }, + } + : undefined, + }; + } return { durationInMonths: x.durationInMonths, jobType: x.jobType, level: x.level, - location: x.location, title: x.title, totalCompensation: x.totalCompensation != null @@ -402,6 +527,40 @@ export const offersProfileRouter = createRouter() } if (x.jobType === JobType.INTERN) { if (x.companyId) { + if (x.cityId) { + return { + company: { + connect: { + id: x.companyId, + }, + }, + durationInMonths: x.durationInMonths, + jobType: x.jobType, + location: { + connect: { + where: { + id: x.cityId + } + } + }, + monthlySalary: + x.monthlySalary != null + ? { + create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + x.monthlySalary.value, + x.monthlySalary.currency, + baseCurrencyString, + ), + currency: x.monthlySalary.currency, + value: x.monthlySalary.value, + }, + } + : undefined, + title: x.title, + }; + } return { company: { connect: { @@ -428,6 +587,37 @@ export const offersProfileRouter = createRouter() title: x.title, }; } + + if (x.cityId) { + return { + durationInMonths: x.durationInMonths, + jobType: x.jobType, + location: { + connect: { + where: { + id: x.cityId + } + } + }, + monthlySalary: + x.monthlySalary != null + ? { + create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + x.monthlySalary.value, + x.monthlySalary.currency, + baseCurrencyString, + ), + currency: x.monthlySalary.currency, + value: x.monthlySalary.value, + }, + } + : undefined, + title: x.title, + }; + } + return { durationInMonths: x.durationInMonths, jobType: x.jobType, @@ -488,7 +678,13 @@ export const offersProfileRouter = createRouter() }, }, jobType: x.jobType, - location: x.location, + location: { + connect: { + where: { + id: x.cityId + } + } + }, monthYearReceived: x.monthYearReceived, negotiationStrategy: x.negotiationStrategy, offersIntern: { @@ -528,7 +724,13 @@ export const offersProfileRouter = createRouter() }, }, jobType: x.jobType, - location: x.location, + location: { + connect: { + where: { + id: x.cityId + } + } + }, monthYearReceived: x.monthYearReceived, negotiationStrategy: x.negotiationStrategy, offersFullTime: { @@ -871,35 +1073,110 @@ export const offersProfileRouter = createRouter() exp.totalCompensation?.currency != null && exp.totalCompensation?.value != null ) { + // FULLTIME if (exp.companyId) { + if (exp.cityId) { + await ctx.prisma.offersBackground.update({ + data: { + experiences: { + create: { + company: { + connect: { + id: exp.companyId, + }, + }, + durationInMonths: exp.durationInMonths, + jobType: exp.jobType, + level: exp.level, + location: { + connect: { + id: exp.cityId + } + }, + title: exp.title, + totalCompensation: exp.totalCompensation + ? { + create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + exp.totalCompensation.value, + exp.totalCompensation.currency, + baseCurrencyString, + ), + currency: exp.totalCompensation.currency, + value: exp.totalCompensation.value, + }, + } + : undefined, + }, + }, + }, + where: { + id: input.background.id, + }, + }); + } else { + await ctx.prisma.offersBackground.update({ + data: { + experiences: { + create: { + company: { + connect: { + id: exp.companyId, + }, + }, + durationInMonths: exp.durationInMonths, + jobType: exp.jobType, + level: exp.level, + title: exp.title, + totalCompensation: exp.totalCompensation + ? { + create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + exp.totalCompensation.value, + exp.totalCompensation.currency, + baseCurrencyString, + ), + currency: exp.totalCompensation.currency, + value: exp.totalCompensation.value, + }, + } + : undefined, + }, + }, + }, + where: { + id: input.background.id, + }, + }); + } + } else if (exp.cityId) { await ctx.prisma.offersBackground.update({ data: { experiences: { create: { - company: { - connect: { - id: exp.companyId, - }, - }, durationInMonths: exp.durationInMonths, jobType: exp.jobType, level: exp.level, - location: exp.location, + location: { + connect: { + id: exp.cityId + } + }, title: exp.title, - totalCompensation: exp.totalCompensation - ? { - create: { - baseCurrency: baseCurrencyString, - baseValue: await convert( - exp.totalCompensation.value, - exp.totalCompensation.currency, - baseCurrencyString, - ), - currency: exp.totalCompensation.currency, - value: exp.totalCompensation.value, - }, - } - : undefined, + totalCompensation: { + create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + exp.totalCompensation.value, + exp.totalCompensation.currency, + baseCurrencyString, + ), + currency: exp.totalCompensation.currency, + value: exp.totalCompensation.value, + }, + }, }, }, }, @@ -915,7 +1192,6 @@ export const offersProfileRouter = createRouter() durationInMonths: exp.durationInMonths, jobType: exp.jobType, level: exp.level, - location: exp.location, title: exp.title, totalCompensation: { create: { @@ -938,19 +1214,67 @@ export const offersProfileRouter = createRouter() }); } } else if (exp.companyId) { + if (exp.cityId) { + await ctx.prisma.offersBackground.update({ + data: { + experiences: { + create: { + company: { + connect: { + id: exp.companyId, + }, + }, + durationInMonths: exp.durationInMonths, + jobType: exp.jobType, + level: exp.level, + location: { + connect: { + id: exp.cityId + } + }, + title: exp.title, + }, + }, + }, + where: { + id: input.background.id, + }, + }); + } else { + await ctx.prisma.offersBackground.update({ + data: { + experiences: { + create: { + company: { + connect: { + id: exp.companyId, + }, + }, + durationInMonths: exp.durationInMonths, + jobType: exp.jobType, + level: exp.level, + title: exp.title, + }, + }, + }, + where: { + id: input.background.id, + }, + }); + } + } else if (exp.cityId) { await ctx.prisma.offersBackground.update({ data: { experiences: { create: { - company: { - connect: { - id: exp.companyId, - }, - }, durationInMonths: exp.durationInMonths, jobType: exp.jobType, level: exp.level, - location: exp.location, + location: { + connect: { + id: exp.cityId + } + }, title: exp.title, }, }, @@ -967,7 +1291,6 @@ export const offersProfileRouter = createRouter() durationInMonths: exp.durationInMonths, jobType: exp.jobType, level: exp.level, - location: exp.location, title: exp.title, }, }, @@ -982,19 +1305,90 @@ export const offersProfileRouter = createRouter() exp.monthlySalary?.currency != null && exp.monthlySalary?.value != null ) { + // INTERN if (exp.companyId) { + if (exp.cityId) { + await ctx.prisma.offersBackground.update({ + data: { + experiences: { + create: { + company: { + connect: { + id: exp.companyId, + }, + }, + durationInMonths: exp.durationInMonths, + jobType: exp.jobType, + location: { + connect: { + id: exp.cityId + } + }, + monthlySalary: { + create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + exp.monthlySalary.value, + exp.monthlySalary.currency, + baseCurrencyString, + ), + currency: exp.monthlySalary.currency, + value: exp.monthlySalary.value, + }, + }, + title: exp.title, + }, + }, + }, + where: { + id: input.background.id, + }, + }); + } else { + await ctx.prisma.offersBackground.update({ + data: { + experiences: { + create: { + company: { + connect: { + id: exp.companyId, + }, + }, + durationInMonths: exp.durationInMonths, + jobType: exp.jobType, + monthlySalary: { + create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + exp.monthlySalary.value, + exp.monthlySalary.currency, + baseCurrencyString, + ), + currency: exp.monthlySalary.currency, + value: exp.monthlySalary.value, + }, + }, + title: exp.title, + }, + }, + }, + where: { + id: input.background.id, + }, + }); + } + } else if (exp.cityId) { await ctx.prisma.offersBackground.update({ data: { experiences: { create: { - company: { - connect: { - id: exp.companyId, - }, - }, durationInMonths: exp.durationInMonths, jobType: exp.jobType, - location: exp.location, + location: { + connect: { + id: exp.cityId + } + }, monthlySalary: { create: { baseCurrency: baseCurrencyString, @@ -1022,7 +1416,6 @@ export const offersProfileRouter = createRouter() create: { durationInMonths: exp.durationInMonths, jobType: exp.jobType, - location: exp.location, monthlySalary: { create: { baseCurrency: baseCurrencyString, @@ -1045,18 +1438,64 @@ export const offersProfileRouter = createRouter() }); } } else if (exp.companyId) { + if (exp.cityId) { + await ctx.prisma.offersBackground.update({ + data: { + experiences: { + create: { + company: { + connect: { + id: exp.companyId, + }, + }, + durationInMonths: exp.durationInMonths, + jobType: exp.jobType, + location: { + connect: { + id: exp.cityId, + } + }, + title: exp.title, + }, + }, + }, + where: { + id: input.background.id, + }, + }); + } else { + await ctx.prisma.offersBackground.update({ + data: { + experiences: { + create: { + company: { + connect: { + id: exp.companyId, + }, + }, + durationInMonths: exp.durationInMonths, + jobType: exp.jobType, + title: exp.title, + }, + }, + }, + where: { + id: input.background.id, + }, + }); + } + } else if (exp.cityId) { await ctx.prisma.offersBackground.update({ data: { experiences: { create: { - company: { - connect: { - id: exp.companyId, - }, - }, durationInMonths: exp.durationInMonths, jobType: exp.jobType, - location: exp.location, + location: { + connect: { + id: exp.cityId + } + }, title: exp.title, }, }, @@ -1072,7 +1511,6 @@ export const offersProfileRouter = createRouter() create: { durationInMonths: exp.durationInMonths, jobType: exp.jobType, - location: exp.location, title: exp.title, }, }, @@ -1160,12 +1598,20 @@ export const offersProfileRouter = createRouter() await ctx.prisma.offersOffer.update({ data: { comments: offerToUpdate.comments, - companyId: offerToUpdate.companyId, + company: { + connect: { + id: offerToUpdate.companyId + } + }, jobType: offerToUpdate.jobType === JobType.FULLTIME ? JobType.FULLTIME : JobType.INTERN, - location: offerToUpdate.location, + location: { + connect: { + id: offerToUpdate.cityId + } + }, monthYearReceived: offerToUpdate.monthYearReceived, negotiationStrategy: offerToUpdate.negotiationStrategy, }, @@ -1302,7 +1748,11 @@ export const offersProfileRouter = createRouter() }, }, jobType: offerToUpdate.jobType, - location: offerToUpdate.location, + location: { + connect: { + id: offerToUpdate.cityId + } + }, monthYearReceived: offerToUpdate.monthYearReceived, negotiationStrategy: offerToUpdate.negotiationStrategy, offersIntern: { @@ -1356,7 +1806,11 @@ export const offersProfileRouter = createRouter() }, }, jobType: offerToUpdate.jobType, - location: offerToUpdate.location, + location: { + connect: { + id: offerToUpdate.cityId + } + }, monthYearReceived: offerToUpdate.monthYearReceived, negotiationStrategy: offerToUpdate.negotiationStrategy, offersFullTime: { 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 86cd29ef..916dcb57 100644 --- a/apps/portal/src/server/router/offers/offers.ts +++ b/apps/portal/src/server/router/offers/offers.ts @@ -43,12 +43,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(), @@ -132,8 +132,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: { @@ -246,8 +245,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..4d80cc8d 100644 --- a/apps/portal/src/utils/offers/analysisGeneration.ts +++ b/apps/portal/src/utils/offers/analysisGeneration.ts @@ -16,8 +16,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 }; @@ -60,298 +66,393 @@ export const generateAnalysis = async (params: { }) => { const { ctx, input } = params; await ctx.prisma.offersAnalysis.deleteMany({ - where: { - profileId: input.profileId, - }, - }); - - const offers = await ctx.prisma.offersOffer.findMany({ - include: { - company: true, - offersFullTime: { - include: { - baseSalary: true, - bonus: true, - stocks: true, - totalCompensation: true, - }, - }, - offersIntern: { - include: { - monthlySalary: true, + where: { + profileId: input.profileId, }, - }, - profile: { + }); + + const offers = await ctx.prisma.offersOffer.findMany({ include: { - background: true, - }, - }, - }, - orderBy: [ - { - offersFullTime: { - totalCompensation: { - baseValue: 'desc', + company: true, + location: { + include: { + state: { + include: { + country: true, + }, + }, + }, + }, + offersFullTime: { + include: { + baseSalary: true, + bonus: true, + stocks: true, + totalCompensation: true, + }, + }, + offersIntern: { + include: { + monthlySalary: true, + }, + }, + profile: { + include: { + background: true, + }, }, }, - }, - { - offersIntern: { - monthlySalary: { - baseValue: 'desc', + orderBy: [ + { + offersFullTime: { + totalCompensation: { + baseValue: 'desc', + }, + }, + }, + { + offersIntern: { + monthlySalary: { + baseValue: 'desc', + }, + }, }, + ], + where: { + profileId: input.profileId, }, - }, - ], - where: { - profileId: input.profileId, - }, - }); + }); - if (!offers || offers.length === 0) { - throw new TRPCError({ - code: 'NOT_FOUND', - message: 'No offers found on this profile', - }); - } + if (!offers || offers.length === 0) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: 'No offers found on this profile', + }); + } - const overallHighestOffer = offers[0]; + const overallHighestOffer = offers[0]; - if ( - !overallHighestOffer.profile.background || - overallHighestOffer.profile.background.totalYoe == null - ) { - throw new TRPCError({ - code: 'NOT_FOUND', - message: 'YOE not found', - }); - } + if ( + !overallHighestOffer.profile.background || + overallHighestOffer.profile.background.totalYoe == null + ) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: 'YOE not found', + }); + } - const yoe = overallHighestOffer.profile.background.totalYoe as number; - const monthYearReceived = new Date(overallHighestOffer.monthYearReceived); - monthYearReceived.setFullYear(monthYearReceived.getFullYear() - 1); + const yoe = overallHighestOffer.profile.background.totalYoe as number; + const monthYearReceived = new Date(overallHighestOffer.monthYearReceived); + monthYearReceived.setFullYear(monthYearReceived.getFullYear() - 1); - const similarOffers = await ctx.prisma.offersOffer.findMany({ - include: { - company: true, - offersFullTime: { - include: { - totalCompensation: true, - }, - }, - offersIntern: { - include: { - monthlySalary: true, - }, - }, - profile: { + let similarOffers = await ctx.prisma.offersOffer.findMany({ include: { - background: { + company: true, + location: { include: { - experiences: { + state: { include: { - company: true, + country: true, }, }, }, }, - }, - }, - }, - orderBy: [ - { - offersFullTime: { - totalCompensation: { - baseValue: 'desc', + offersFullTime: { + include: { + totalCompensation: true, + }, }, - }, - }, - { - offersIntern: { - monthlySalary: { - baseValue: 'desc', + offersIntern: { + include: { + monthlySalary: true, + }, }, - }, - }, - ], - where: { - AND: [ - { - location: overallHighestOffer.location, - }, - { - monthYearReceived: { - gte: monthYearReceived, + profile: { + include: { + background: { + include: { + experiences: { + include: { + company: true, + location: { + include: { + state: { + include: { + country: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, }, }, - { - OR: [ - { - offersFullTime: { - title: overallHighestOffer.offersFullTime?.title, + orderBy: [ + { + offersFullTime: { + totalCompensation: { + baseValue: 'desc', }, - offersIntern: { - title: overallHighestOffer.offersIntern?.title, + }, + }, + { + offersIntern: { + monthlySalary: { + baseValue: 'desc', }, }, - ], - }, - { - profile: { - background: { - AND: [ + }, + ], + where: { + AND: [ + { + location: overallHighestOffer.location, + }, + { + monthYearReceived: { + gte: monthYearReceived, + }, + }, + { + OR: [ { - totalYoe: { - gte: Math.max(yoe - 1, 0), - lte: yoe + 1, + offersFullTime: { + title: overallHighestOffer.offersFullTime?.title, + }, + offersIntern: { + title: overallHighestOffer.offersIntern?.title, }, }, ], }, - }, + { + profile: { + background: { + AND: [ + { + totalYoe: { + gte: Math.max(yoe - 1, 0), + lte: yoe + 1, + }, + }, + ], + }, + }, + }, + ], }, - ], - }, - }); + }); - // COMPANY ANALYSIS - const companyMap = new Map(); - offers.forEach((offer) => { - if (companyMap.get(offer.companyId) == null) { - companyMap.set(offer.companyId, offer); - } - }); + // COMPANY ANALYSIS + const companyMap = new Map(); + offers.forEach((offer) => { + if (companyMap.get(offer.companyId) == null) { + companyMap.set(offer.companyId, offer); + } + }); - const companyAnalysis = Array.from(companyMap.values()).map( - (companyOffer) => { - // TODO: Refactor calculating analysis into a function - const similarCompanyOffers = similarOffers.filter( - (offer) => offer.companyId === companyOffer.companyId, - ); + const companyAnalysis = Array.from(companyMap.values()).map( + (companyOffer) => { + // TODO: Refactor calculating analysis into a function + let similarCompanyOffers = similarOffers.filter( + (offer) => offer.companyId === companyOffer.companyId, + ); - const companyIndex = searchOfferPercentile( - companyOffer, - similarCompanyOffers, - ); - const companyPercentile = - similarCompanyOffers.length <= 1 - ? 100 - : 100 - (100 * companyIndex) / (similarCompanyOffers.length - 1); + const companyIndex = searchOfferPercentile( + companyOffer, + similarCompanyOffers, + ); + const companyPercentile = + similarCompanyOffers.length <= 1 + ? 100 + : 100 - (100 * companyIndex) / (similarCompanyOffers.length - 1); - // Get top offers (excluding user's offer) - const similarCompanyOffersWithoutUsersOffers = - similarCompanyOffers.filter( - (offer) => offer.profileId !== input.profileId, - ); + // Get top offers (excluding user's offer) + similarCompanyOffers = similarCompanyOffers.filter( + (offer) => offer.id !== companyOffer.id, + ); - const noOfSimilarCompanyOffers = - similarCompanyOffersWithoutUsersOffers.length; - const similarCompanyOffers90PercentileIndex = Math.ceil( - noOfSimilarCompanyOffers * 0.1, - ); - const topPercentileCompanyOffers = - noOfSimilarCompanyOffers > 2 - ? similarCompanyOffersWithoutUsersOffers.slice( - similarCompanyOffers90PercentileIndex, - similarCompanyOffers90PercentileIndex + 2, - ) - : similarCompanyOffersWithoutUsersOffers; + const noOfSimilarCompanyOffers = similarCompanyOffers.length; + const similarCompanyOffers90PercentileIndex = Math.ceil( + noOfSimilarCompanyOffers * 0.1, + ); + const topPercentileCompanyOffers = + noOfSimilarCompanyOffers > 2 + ? similarCompanyOffers.slice( + similarCompanyOffers90PercentileIndex, + similarCompanyOffers90PercentileIndex + 2, + ) + : similarCompanyOffers; - return { - companyName: companyOffer.company.name, - noOfSimilarOffers: noOfSimilarCompanyOffers, - percentile: companyPercentile, - topSimilarOffers: topPercentileCompanyOffers, - }; - }, - ); + return { + companyName: companyOffer.company.name, + noOfSimilarOffers: noOfSimilarCompanyOffers, + percentile: companyPercentile, + topSimilarOffers: topPercentileCompanyOffers, + }; + }, + ); - // OVERALL ANALYSIS - const overallIndex = searchOfferPercentile( - overallHighestOffer, - similarOffers, - ); - const overallPercentile = - similarOffers.length <= 1 - ? 100 - : 100 - (100 * overallIndex) / (similarOffers.length - 1); + // OVERALL ANALYSIS + const overallIndex = searchOfferPercentile( + overallHighestOffer, + similarOffers, + ); + const overallPercentile = + similarOffers.length <= 1 + ? 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 similarOffers90PercentileIndex = Math.ceil(noOfSimilarOffers * 0.1); - const topPercentileOffers = - noOfSimilarOffers > 2 - ? similarOffersWithoutUsersOffers.slice( - similarOffers90PercentileIndex, - similarOffers90PercentileIndex + 2, - ) - : similarOffersWithoutUsersOffers; + const noOfSimilarOffers = similarOffers.length; + const similarOffers90PercentileIndex = Math.ceil(noOfSimilarOffers * 0.1); + const topPercentileOffers = + noOfSimilarOffers > 2 + ? similarOffers.slice( + similarOffers90PercentileIndex, + similarOffers90PercentileIndex + 2, + ) + : similarOffers; - const analysis = await ctx.prisma.offersAnalysis.create({ - data: { - companyAnalysis: { - create: companyAnalysis.map((analysisUnit) => { - return { - companyName: analysisUnit.companyName, - noOfSimilarOffers: analysisUnit.noOfSimilarOffers, - percentile: analysisUnit.percentile, - topSimilarOffers: { - connect: analysisUnit.topSimilarOffers.map((offer) => { - return { id: offer.id }; - }), - }, - }; - }), - }, - overallAnalysis: { - create: { - companyName: overallHighestOffer.company.name, - noOfSimilarOffers, - percentile: overallPercentile, - topSimilarOffers: { - connect: topPercentileOffers.map((offer) => { - return { id: offer.id }; + const analysis = await ctx.prisma.offersAnalysis.create({ + data: { + companyAnalysis: { + create: companyAnalysis.map((analysisUnit) => { + return { + companyName: analysisUnit.companyName, + noOfSimilarOffers: analysisUnit.noOfSimilarOffers, + percentile: analysisUnit.percentile, + topSimilarOffers: { + connect: analysisUnit.topSimilarOffers.map((offer) => { + return { id: offer.id }; + }), + }, + }; }), }, + overallAnalysis: { + create: { + companyName: overallHighestOffer.company.name, + noOfSimilarOffers, + percentile: overallPercentile, + topSimilarOffers: { + connect: topPercentileOffers.map((offer) => { + return { id: offer.id }; + }), + }, + }, + }, + overallHighestOffer: { + connect: { + id: overallHighestOffer.id, + }, + }, + profile: { + connect: { + id: input.profileId, + }, + }, }, - }, - overallHighestOffer: { - connect: { - id: overallHighestOffer.id, - }, - }, - profile: { - connect: { - id: input.profileId, - }, - }, - }, - include: { - companyAnalysis: { include: { - topSimilarOffers: { + companyAnalysis: { include: { - company: true, - offersFullTime: { - include: { - totalCompensation: true, - }, - }, - offersIntern: { + topSimilarOffers: { include: { - monthlySalary: true, + company: true, + location: { + include: { + state: { + include: { + country: true, + }, + }, + }, + }, + offersFullTime: { + include: { + totalCompensation: true, + }, + }, + offersIntern: { + include: { + monthlySalary: true, + }, + }, + profile: { + include: { + background: { + include: { + experiences: { + include: { + company: true, + location: { + include: { + state: { + include: { + country: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, }, }, - profile: { + }, + }, + overallAnalysis: { + include: { + topSimilarOffers: { include: { - background: { + company: true, + location: { + include: { + state: { + include: { + country: true, + }, + }, + }, + }, + offersFullTime: { + include: { + totalCompensation: true, + }, + }, + offersIntern: { + include: { + monthlySalary: true, + }, + }, + profile: { include: { - experiences: { + background: { include: { - company: true, + experiences: { + include: { + company: true, + location: { + include: { + state: { + include: { + country: true, + }, + }, + }, + }, + }, + }, }, }, }, @@ -360,13 +461,18 @@ export const generateAnalysis = async (params: { }, }, }, - }, - }, - overallAnalysis: { - include: { - topSimilarOffers: { + overallHighestOffer: { include: { company: true, + location: { + include: { + state: { + include: { + country: true, + }, + }, + }, + }, offersFullTime: { include: { totalCompensation: true, @@ -379,43 +485,13 @@ export const generateAnalysis = async (params: { }, profile: { include: { - background: { - include: { - experiences: { - include: { - company: true, - }, - }, - }, - }, + background: true, }, }, }, }, }, - }, - overallHighestOffer: { - include: { - company: true, - offersFullTime: { - include: { - totalCompensation: true, - }, - }, - offersIntern: { - include: { - monthlySalary: true, - }, - }, - profile: { - include: { - background: true, - }, - }, - }, - }, - }, - }); + }); - return profileAnalysisDtoMapper(analysis); + return profileAnalysisDtoMapper(analysis); };