Merge branch 'BryannYeap/location' into stuart/seed-db

# Conflicts:
#	apps/portal/src/server/router/offers/offers-analysis-router.ts
#	apps/portal/src/server/router/offers/offers-profile-router.ts
pull/501/head^2
Stuart Long Chay Boon 3 years ago
commit 927e4d3864

@ -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

@ -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',

@ -36,7 +36,7 @@ export default function DashboardProfileCard({
</p>
<p>
{location
? `Company: ${company.name}, ${location}`
? `Company: ${company.name}, ${location.cityName}`
: `Company: ${company.name}`}
</p>
{level && <p>Level: {level}</p>}

@ -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({
<FormProvider {...formMethods}>
<form className="text-sm" onSubmit={handleSubmit(onSubmit)}>
{steps[step]}
{/* <pre>{JSON.stringify(formMethods.watch(), null, 2)}</pre> */}
<pre>{JSON.stringify(formMethods.watch(), null, 2)}</pre>
{step === 0 && (
<div className="flex justify-end">
<Button

@ -7,9 +7,9 @@ import {
educationLevelOptions,
emptyOption,
FieldError,
locationOptions,
} from '~/components/offers/constants';
import type { BackgroundPostData } from '~/components/offers/types';
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';
@ -104,6 +104,12 @@ function FullTimeJobFields() {
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 (
<>
@ -172,11 +178,22 @@ function FullTimeJobFields() {
placeholder="e.g. L4, Junior"
{...register(`background.experiences.0.level`)}
/>
<FormSelect
display="block"
<CitiesTypeahead
label="Location"
options={locationOptions}
{...register(`background.experiences.0.location`)}
value={{
id: watchCityId,
label: watchCityName,
value: watchCityId,
}}
onSelect={(option) => {
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', '');
}
}}
/>
</div>
<div className="mb-5 grid grid-cols-2 space-x-3">
@ -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() {
</div>
<Collapsible label="Add more details">
<div className="mb-5 grid grid-cols-2 space-x-3">
<FormSelect
display="block"
<CitiesTypeahead
label="Location"
options={locationOptions}
placeholder={emptyOption}
{...register(`background.experiences.0.location`)}
value={{
id: watchCityId,
label: watchCityName,
value: watchCityId,
}}
onSelect={(option) => {
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', '');
}
}}
/>
</div>
</Collapsible>

@ -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,7 +110,6 @@ function FullTimeOfferDetailsForm({
/>
</div>
<div className="mb-5 flex grid grid-cols-2 space-x-3">
<div>
<CompaniesTypeahead
required={true}
value={{
@ -119,17 +124,23 @@ function FullTimeOfferDetailsForm({
}
}}
/>
</div>
<FormSelect
display="block"
errorMessage={offerFields?.location?.message}
<CitiesTypeahead
label="Location"
options={locationOptions}
placeholder={emptyOption}
required={true}
{...register(`offers.${index}.location`, {
required: FieldError.REQUIRED,
})}
value={{
id: watchCityId,
label: watchCityName,
value: watchCityId,
}}
onSelect={(option) => {
if (option) {
setValue(`offers.${index}.cityId`, option.value);
setValue(`offers.${index}.cityName`, option.label);
} else {
setValue(`offers.${index}.cityId`, '');
setValue(`offers.${index}.cityName`, '');
}
}}
/>
</div>
<div className="mb-5 flex grid grid-cols-2 items-start space-x-3">
@ -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 (
<div className="my-5 rounded-lg border border-slate-200 px-10 py-5">
@ -342,16 +359,23 @@ function InternshipOfferDetailsForm({
}}
/>
</div>
<FormSelect
display="block"
errorMessage={offerFields?.location?.message}
<CitiesTypeahead
label="Location"
options={locationOptions}
placeholder={emptyOption}
required={true}
{...register(`offers.${index}.location`, {
required: FieldError.REQUIRED,
})}
value={{
id: watchCityId,
label: watchCityName,
value: watchCityId,
}}
onSelect={(option) => {
if (option) {
setValue(`offers.${index}.cityId`, option.value);
setValue(`offers.${index}.cityName`, option.label);
} else {
setValue(`offers.${index}.cityId`, '');
setValue(`offers.${index}.cityName`, '');
}
}}
/>
</div>
<div className="mb-5 grid grid-cols-2 space-x-3">

@ -40,7 +40,7 @@ export default function OfferCard({
<BuildingOffice2Icon className="mr-3 h-5" />
</span>
<span className="font-bold">
{location ? `${companyName}, ${location}` : companyName}
{location ? `${companyName}, ${location.cityName}` : companyName}
</span>
</div>
<div className="ml-8 flex flex-row">
@ -92,11 +92,11 @@ export default function OfferCard({
</span>
</div>
)}
{totalCompensation && (
{(base || stocks || bonus) && totalCompensation && (
<div className="ml-8 flex flex-row font-light">
<p>
Base / year: {base} Stocks / year: {stocks} Bonus / year:{' '}
{bonus}
Base / year: {base ?? 'N/A'} Stocks / year:{' '}
{stocks ?? 'N/A'} Bonus / year: {bonus ?? 'N/A'}
</p>
</div>
)}

@ -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({
))}
</DropdownMenu>
<div className="divide-x-slate-200 flex items-center space-x-4 divide-x">
<div className="justify-left flex items-center space-x-2">
<span>View all offers in</span>
<div className="justify-left flex items-center space-x-2 font-medium text-slate-700">
<span>Display offers in</span>
<CurrencySelector
handleCurrencyChange={(value: string) => setCurrency(value)}
selectedCurrency={currency}

@ -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;

@ -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<OffersExperience & { company: Company | null }>;
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

@ -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() {
</Link>
.
</Banner>
<div className="text-primary-600 flex items-center justify-end space-x-1 bg-slate-100 px-4 pt-4">
<span>
<MapPinIcon className="flex h-7 w-7" />
</span>
<CitiesTypeahead
isLabelHidden={true}
placeholder="All Cities"
onSelect={(option) => {
if (option) {
setCityFilter(option.value);
gaEvent({
action: `offers.table_filter_city_${option.value}`,
category: 'engagement',
label: 'Filter by city',
});
}
}}
/>
</div>
<div className="bg-slate-100 py-16 px-4">
<div>
<div>
@ -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() {
</div>
<div className="flex justify-center bg-white pb-20 pt-10">
<OffersTable
cityFilter={cityFilter}
companyFilter={companyFilter}
jobTitleFilter={jobTitleFilter}
/>

@ -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,

@ -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,

@ -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,7 +1073,9 @@ 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: {
@ -884,7 +1088,11 @@ export const offersProfileRouter = createRouter()
durationInMonths: exp.durationInMonths,
jobType: exp.jobType,
level: exp.level,
location: exp.location,
location: {
connect: {
id: exp.cityId
}
},
title: exp.title,
totalCompensation: exp.totalCompensation
? {
@ -907,6 +1115,75 @@ export const offersProfileRouter = createRouter()
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: {
durationInMonths: exp.durationInMonths,
jobType: exp.jobType,
level: exp.level,
location: {
connect: {
id: exp.cityId
}
},
title: exp.title,
totalCompensation: {
create: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
exp.totalCompensation.value,
exp.totalCompensation.currency,
baseCurrencyString,
),
currency: exp.totalCompensation.currency,
value: exp.totalCompensation.value,
},
},
},
},
},
where: {
id: input.background.id,
},
});
} else {
await ctx.prisma.offersBackground.update({
data: {
@ -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,6 +1214,33 @@ 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: {
@ -950,7 +1253,28 @@ export const offersProfileRouter = createRouter()
durationInMonths: exp.durationInMonths,
jobType: exp.jobType,
level: exp.level,
location: exp.location,
title: exp.title,
},
},
},
where: {
id: input.background.id,
},
});
}
} else if (exp.cityId) {
await ctx.prisma.offersBackground.update({
data: {
experiences: {
create: {
durationInMonths: exp.durationInMonths,
jobType: exp.jobType,
level: exp.level,
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,7 +1305,9 @@ 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: {
@ -994,7 +1319,76 @@ export const offersProfileRouter = createRouter()
},
durationInMonths: exp.durationInMonths,
jobType: exp.jobType,
location: exp.location,
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: {
durationInMonths: exp.durationInMonths,
jobType: exp.jobType,
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,6 +1438,7 @@ export const offersProfileRouter = createRouter()
});
}
} else if (exp.companyId) {
if (exp.cityId) {
await ctx.prisma.offersBackground.update({
data: {
experiences: {
@ -1056,7 +1450,52 @@ export const offersProfileRouter = createRouter()
},
durationInMonths: exp.durationInMonths,
jobType: exp.jobType,
location: exp.location,
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: {
durationInMonths: exp.durationInMonths,
jobType: exp.jobType,
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: {

@ -57,6 +57,15 @@ export const offersUserProfileRouter = createProtectedRouter()
offers: {
include: {
company: true,
location: {
include: {
state: {
include: {
country: true,
},
},
},
},
offersFullTime: {
include: {
totalCompensation: true,

@ -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: {

@ -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<string>;
@ -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;
};

@ -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 };
@ -68,6 +74,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 +146,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 +175,15 @@ export const generateAnalysis = async (params: {
experiences: {
include: {
company: true,
location: {
include: {
state: {
include: {
country: true,
},
},
},
},
},
},
},
@ -225,7 +258,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 +272,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 +307,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 +366,15 @@ export const generateAnalysis = async (params: {
topSimilarOffers: {
include: {
company: true,
location: {
include: {
state: {
include: {
country: true,
},
},
},
},
offersFullTime: {
include: {
totalCompensation: true,
@ -352,6 +392,15 @@ export const generateAnalysis = async (params: {
experiences: {
include: {
company: true,
location: {
include: {
state: {
include: {
country: true,
},
},
},
},
},
},
},
@ -367,6 +416,15 @@ export const generateAnalysis = async (params: {
topSimilarOffers: {
include: {
company: true,
location: {
include: {
state: {
include: {
country: true,
},
},
},
},
offersFullTime: {
include: {
totalCompensation: true,
@ -384,6 +442,15 @@ export const generateAnalysis = async (params: {
experiences: {
include: {
company: true,
location: {
include: {
state: {
include: {
country: true,
},
},
},
},
},
},
},
@ -397,6 +464,15 @@ export const generateAnalysis = async (params: {
overallHighestOffer: {
include: {
company: true,
location: {
include: {
state: {
include: {
country: true,
},
},
},
},
offersFullTime: {
include: {
totalCompensation: true,

Loading…
Cancel
Save