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 stateId String
state State @relation(fields: [stateId], references: [id]) state State @relation(fields: [stateId], references: [id])
questionsQuestionEncounters QuestionsQuestionEncounter[] questionsQuestionEncounters QuestionsQuestionEncounter[]
OffersExperience OffersExperience[]
OffersOffer OffersOffer[]
@@unique([name, stateId]) @@unique([name, stateId])
} }
@ -265,7 +267,8 @@ model OffersExperience {
// Add more fields // Add more fields
durationInMonths Int? durationInMonths Int?
location String? location City? @relation(fields: [cityId], references: [id])
cityId String?
// FULLTIME fields // FULLTIME fields
level String? level String?
@ -348,8 +351,9 @@ model OffersOffer {
company Company @relation(fields: [companyId], references: [id]) company Company @relation(fields: [companyId], references: [id])
companyId String companyId String
location City @relation(fields: [cityId], references: [id])
cityId String
monthYearReceived DateTime monthYearReceived DateTime
location String
negotiationStrategy String negotiationStrategy String
comments String comments String

@ -2,21 +2,6 @@ import { EducationBackgroundType } from './types';
export const emptyOption = '----'; 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 = [ export const internshipCycleOptions = [
{ {
label: 'Summer', label: 'Summer',

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

@ -26,11 +26,11 @@ import { getCurrentMonth, getCurrentYear } from '~/utils/offers/time';
import { trpc } from '~/utils/trpc'; import { trpc } from '~/utils/trpc';
const defaultOfferValues = { const defaultOfferValues = {
cityId: '',
comments: '', comments: '',
companyId: '', companyId: '',
jobTitle: '', jobTitle: '',
jobType: JobType.FULLTIME, jobType: JobType.FULLTIME,
location: '',
monthYearReceived: { monthYearReceived: {
month: getCurrentMonth() as Month, month: getCurrentMonth() as Month,
year: getCurrentYear(), year: getCurrentYear(),
@ -277,7 +277,7 @@ export default function OffersSubmissionForm({
<FormProvider {...formMethods}> <FormProvider {...formMethods}>
<form className="text-sm" onSubmit={handleSubmit(onSubmit)}> <form className="text-sm" onSubmit={handleSubmit(onSubmit)}>
{steps[step]} {steps[step]}
{/* <pre>{JSON.stringify(formMethods.watch(), null, 2)}</pre> */} <pre>{JSON.stringify(formMethods.watch(), null, 2)}</pre>
{step === 0 && ( {step === 0 && (
<div className="flex justify-end"> <div className="flex justify-end">
<Button <Button

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

@ -12,6 +12,7 @@ import { TrashIcon } from '@heroicons/react/24/outline';
import { JobType } from '@prisma/client'; import { JobType } from '@prisma/client';
import { Button, Dialog } from '@tih/ui'; import { Button, Dialog } from '@tih/ui';
import CitiesTypeahead from '~/components/shared/CitiesTypeahead';
import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead'; import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
import type { JobTitleType } from '~/components/shared/JobTitles'; import type { JobTitleType } from '~/components/shared/JobTitles';
import { getLabelForJobTitleType } from '~/components/shared/JobTitles'; import { getLabelForJobTitleType } from '~/components/shared/JobTitles';
@ -25,7 +26,6 @@ import {
emptyOption, emptyOption,
FieldError, FieldError,
internshipCycleOptions, internshipCycleOptions,
locationOptions,
yearOptions, yearOptions,
} from '../../constants'; } from '../../constants';
import FormMonthYearPicker from '../../forms/FormMonthYearPicker'; import FormMonthYearPicker from '../../forms/FormMonthYearPicker';
@ -62,6 +62,12 @@ function FullTimeOfferDetailsForm({
const watchCompanyName = useWatch({ const watchCompanyName = useWatch({
name: `offers.${index}.companyName`, name: `offers.${index}.companyName`,
}); });
const watchCityId = useWatch({
name: `offers.${index}.cityId`,
});
const watchCityName = useWatch({
name: `offers.${index}.cityName`,
});
const watchCurrency = useWatch({ const watchCurrency = useWatch({
name: `offers.${index}.offersFullTime.totalCompensation.currency`, name: `offers.${index}.offersFullTime.totalCompensation.currency`,
}); });
@ -104,32 +110,37 @@ function FullTimeOfferDetailsForm({
/> />
</div> </div>
<div className="mb-5 flex grid grid-cols-2 space-x-3"> <div className="mb-5 flex grid grid-cols-2 space-x-3">
<div> <CompaniesTypeahead
<CompaniesTypeahead required={true}
required={true} value={{
value={{ id: watchCompanyId,
id: watchCompanyId, label: watchCompanyName,
label: watchCompanyName, value: watchCompanyId,
value: watchCompanyId, }}
}} onSelect={(option) => {
onSelect={(option) => { if (option) {
if (option) { setValue(`offers.${index}.companyId`, option.value);
setValue(`offers.${index}.companyId`, option.value); setValue(`offers.${index}.companyName`, option.label);
setValue(`offers.${index}.companyName`, option.label); }
} }}
}} />
/> <CitiesTypeahead
</div>
<FormSelect
display="block"
errorMessage={offerFields?.location?.message}
label="Location" label="Location"
options={locationOptions}
placeholder={emptyOption}
required={true} required={true}
{...register(`offers.${index}.location`, { value={{
required: FieldError.REQUIRED, 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>
<div className="mb-5 flex grid grid-cols-2 items-start space-x-3"> <div className="mb-5 flex grid grid-cols-2 items-start space-x-3">
@ -305,6 +316,12 @@ function InternshipOfferDetailsForm({
const watchCompanyName = useWatch({ const watchCompanyName = useWatch({
name: `offers.${index}.companyName`, name: `offers.${index}.companyName`,
}); });
const watchCityId = useWatch({
name: `offers.${index}.cityId`,
});
const watchCityName = useWatch({
name: `offers.${index}.cityName`,
});
return ( return (
<div className="my-5 rounded-lg border border-slate-200 px-10 py-5"> <div className="my-5 rounded-lg border border-slate-200 px-10 py-5">
@ -342,16 +359,23 @@ function InternshipOfferDetailsForm({
}} }}
/> />
</div> </div>
<FormSelect <CitiesTypeahead
display="block"
errorMessage={offerFields?.location?.message}
label="Location" label="Location"
options={locationOptions}
placeholder={emptyOption}
required={true} required={true}
{...register(`offers.${index}.location`, { value={{
required: FieldError.REQUIRED, 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>
<div className="mb-5 grid grid-cols-2 space-x-3"> <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" /> <BuildingOffice2Icon className="mr-3 h-5" />
</span> </span>
<span className="font-bold"> <span className="font-bold">
{location ? `${companyName}, ${location}` : companyName} {location ? `${companyName}, ${location.cityName}` : companyName}
</span> </span>
</div> </div>
<div className="ml-8 flex flex-row"> <div className="ml-8 flex flex-row">
@ -92,11 +92,11 @@ export default function OfferCard({
</span> </span>
</div> </div>
)} )}
{totalCompensation && ( {(base || stocks || bonus) && totalCompensation && (
<div className="ml-8 flex flex-row font-light"> <div className="ml-8 flex flex-row font-light">
<p> <p>
Base / year: {base} Stocks / year: {stocks} Bonus / year:{' '} Base / year: {base ?? 'N/A'} Stocks / year:{' '}
{bonus} {stocks ?? 'N/A'} Bonus / year: {bonus ?? 'N/A'}
</p> </p>
</div> </div>
)} )}

@ -21,10 +21,12 @@ import type { DashboardOffer, GetOffersResponse, Paging } from '~/types/offers';
const NUMBER_OF_OFFERS_IN_PAGE = 10; const NUMBER_OF_OFFERS_IN_PAGE = 10;
export type OffersTableProps = Readonly<{ export type OffersTableProps = Readonly<{
cityFilter: string;
companyFilter: string; companyFilter: string;
jobTitleFilter: string; jobTitleFilter: string;
}>; }>;
export default function OffersTable({ export default function OffersTable({
cityFilter,
companyFilter, companyFilter,
jobTitleFilter, jobTitleFilter,
}: OffersTableProps) { }: OffersTableProps) {
@ -53,10 +55,11 @@ export default function OffersTable({
[ [
'offers.list', 'offers.list',
{ {
// Location: 'Singapore, Singapore', // TODO: Geolocation
cityId: cityFilter,
companyId: companyFilter, companyId: companyFilter,
currency, currency,
limit: NUMBER_OF_OFFERS_IN_PAGE, limit: NUMBER_OF_OFFERS_IN_PAGE,
location: 'Singapore, Singapore', // TODO: Geolocation
offset: pagination.currentPage, offset: pagination.currentPage,
sortBy: OfferTableSortBy[selectedFilter] ?? '-monthYearReceived', sortBy: OfferTableSortBy[selectedFilter] ?? '-monthYearReceived',
title: jobTitleFilter, title: jobTitleFilter,
@ -102,8 +105,8 @@ export default function OffersTable({
))} ))}
</DropdownMenu> </DropdownMenu>
<div className="divide-x-slate-200 flex items-center space-x-4 divide-x"> <div className="divide-x-slate-200 flex items-center space-x-4 divide-x">
<div className="justify-left flex items-center space-x-2"> <div className="justify-left flex items-center space-x-2 font-medium text-slate-700">
<span>View all offers in</span> <span>Display offers in</span>
<CurrencySelector <CurrencySelector
handleCurrencyChange={(value: string) => setCurrency(value)} handleCurrencyChange={(value: string) => setCurrency(value)}
selectedCurrency={currency} selectedCurrency={currency}

@ -2,6 +2,8 @@ import type { JobType } from '@prisma/client';
import type { MonthYear } from '~/components/shared/MonthYearPicker'; import type { MonthYear } from '~/components/shared/MonthYearPicker';
import type { Location } from '~/types/offers';
export const HOME_URL = '/offers'; export const HOME_URL = '/offers';
/* /*
@ -44,13 +46,14 @@ export type BackgroundPostData = {
}; };
type ExperiencePostData = { type ExperiencePostData = {
cityId?: string | null;
cityName?: string | null;
companyId?: string | null; companyId?: string | null;
companyName?: string | null; companyName?: string | null;
durationInMonths?: number | null; durationInMonths?: number | null;
id?: string; id?: string;
jobType?: string | null; jobType?: string | null;
level?: string | null; level?: string | null;
location?: string | null;
monthlySalary?: Money | null; monthlySalary?: Money | null;
title?: string | null; title?: string | null;
totalCompensation?: Money | null; totalCompensation?: Money | null;
@ -75,12 +78,13 @@ type SpecificYoePostData = {
type SpecificYoe = SpecificYoePostData; type SpecificYoe = SpecificYoePostData;
export type OfferPostData = { export type OfferPostData = {
cityId: string;
cityName?: string;
comments: string; comments: string;
companyId: string; companyId: string;
companyName?: string; companyName?: string;
id?: string; id?: string;
jobType: JobType; jobType: JobType;
location: string;
monthYearReceived: Date; monthYearReceived: Date;
negotiationStrategy: string; negotiationStrategy: string;
offersFullTime?: OfferFullTimePostData | null; offersFullTime?: OfferFullTimePostData | null;
@ -132,7 +136,7 @@ export type OfferDisplayData = {
jobLevel?: string | null; jobLevel?: string | null;
jobTitle?: string | null; jobTitle?: string | null;
jobType?: JobType; jobType?: JobType;
location?: string | null; location?: Location | null;
monthlySalary?: string | null; monthlySalary?: string | null;
negotiationStrategy?: string | null; negotiationStrategy?: string | null;
otherComment?: string | null; otherComment?: string | null;

@ -1,5 +1,7 @@
import type { import type {
City,
Company, Company,
Country,
OffersAnalysis, OffersAnalysis,
OffersAnalysisUnit, OffersAnalysisUnit,
OffersBackground, OffersBackground,
@ -12,6 +14,7 @@ import type {
OffersProfile, OffersProfile,
OffersReply, OffersReply,
OffersSpecificYoe, OffersSpecificYoe,
State,
User, User,
} from '@prisma/client'; } from '@prisma/client';
import { JobType } from '@prisma/client'; import { JobType } from '@prisma/client';
@ -28,6 +31,7 @@ import type {
Education, Education,
Experience, Experience,
GetOffersResponse, GetOffersResponse,
Location,
OffersCompany, OffersCompany,
Paging, Paging,
Profile, Profile,
@ -42,6 +46,7 @@ import type {
const analysisOfferDtoMapper = ( const analysisOfferDtoMapper = (
offer: OffersOffer & { offer: OffersOffer & {
company: Company; company: Company;
location: City & { state: State & { country: Country } };
offersFullTime: offersFullTime:
| (OffersFullTime & { totalCompensation: OffersCurrency }) | (OffersFullTime & { totalCompensation: OffersCurrency })
| null; | null;
@ -49,7 +54,14 @@ const analysisOfferDtoMapper = (
profile: OffersProfile & { profile: OffersProfile & {
background: background:
| (OffersBackground & { | (OffersBackground & {
experiences: Array<OffersExperience & { company: Company | null }>; experiences: Array<
OffersExperience & {
company: Company | null;
location:
| (City & { state: State & { country: Country } })
| null;
}
>;
}) })
| null; | null;
}; };
@ -68,7 +80,7 @@ const analysisOfferDtoMapper = (
}, },
jobType: offer.jobType, jobType: offer.jobType,
level: offer.offersFullTime?.level ?? '', level: offer.offersFullTime?.level ?? '',
location: offer.location, location: locationDtoMapper(offer.location),
monthYearReceived: offer.monthYearReceived, monthYearReceived: offer.monthYearReceived,
negotiationStrategy: offer.negotiationStrategy, negotiationStrategy: offer.negotiationStrategy,
previousCompanies: previousCompanies:
@ -117,6 +129,7 @@ const analysisUnitDtoMapper = (
topSimilarOffers: Array< topSimilarOffers: Array<
OffersOffer & { OffersOffer & {
company: Company; company: Company;
location: City & { state: State & { country: Country } };
offersFullTime: offersFullTime:
| (OffersFullTime & { totalCompensation: OffersCurrency }) | (OffersFullTime & { totalCompensation: OffersCurrency })
| null; | null;
@ -125,7 +138,12 @@ const analysisUnitDtoMapper = (
background: background:
| (OffersBackground & { | (OffersBackground & {
experiences: Array< experiences: Array<
OffersExperience & { company: Company | null } OffersExperience & {
company: Company | null;
location:
| (City & { state: State & { country: Country } })
| null;
}
>; >;
}) })
| null; | null;
@ -148,6 +166,7 @@ const analysisUnitDtoMapper = (
const analysisHighestOfferDtoMapper = ( const analysisHighestOfferDtoMapper = (
offer: OffersOffer & { offer: OffersOffer & {
company: Company; company: Company;
location: City & { state: State & { country: Country } };
offersFullTime: offersFullTime:
| (OffersFullTime & { totalCompensation: OffersCurrency }) | (OffersFullTime & { totalCompensation: OffersCurrency })
| null; | null;
@ -159,7 +178,7 @@ const analysisHighestOfferDtoMapper = (
company: offersCompanyDtoMapper(offer.company), company: offersCompanyDtoMapper(offer.company),
id: offer.id, id: offer.id,
level: offer.offersFullTime?.level ?? '', level: offer.offersFullTime?.level ?? '',
location: offer.location, location: locationDtoMapper(offer.location),
totalYoe: offer.profile.background?.totalYoe ?? -1, totalYoe: offer.profile.background?.totalYoe ?? -1,
}; };
return analysisHighestOfferDto; return analysisHighestOfferDto;
@ -173,6 +192,7 @@ export const profileAnalysisDtoMapper = (
topSimilarOffers: Array< topSimilarOffers: Array<
OffersOffer & { OffersOffer & {
company: Company; company: Company;
location: City & { state: State & { country: Country } };
offersFullTime: offersFullTime:
| (OffersFullTime & { totalCompensation: OffersCurrency }) | (OffersFullTime & { totalCompensation: OffersCurrency })
| null; | null;
@ -183,7 +203,12 @@ export const profileAnalysisDtoMapper = (
background: background:
| (OffersBackground & { | (OffersBackground & {
experiences: Array< experiences: Array<
OffersExperience & { company: Company | null } OffersExperience & {
company: Company | null;
location:
| (City & { state: State & { country: Country } })
| null;
}
>; >;
}) })
| null; | null;
@ -196,6 +221,7 @@ export const profileAnalysisDtoMapper = (
topSimilarOffers: Array< topSimilarOffers: Array<
OffersOffer & { OffersOffer & {
company: Company; company: Company;
location: City & { state: State & { country: Country } };
offersFullTime: offersFullTime:
| (OffersFullTime & { totalCompensation: OffersCurrency }) | (OffersFullTime & { totalCompensation: OffersCurrency })
| null; | null;
@ -206,7 +232,12 @@ export const profileAnalysisDtoMapper = (
background: background:
| (OffersBackground & { | (OffersBackground & {
experiences: Array< experiences: Array<
OffersExperience & { company: Company | null } OffersExperience & {
company: Company | null;
location:
| (City & { state: State & { country: Country } })
| null;
}
>; >;
}) })
| null; | null;
@ -216,6 +247,7 @@ export const profileAnalysisDtoMapper = (
}; };
overallHighestOffer: OffersOffer & { overallHighestOffer: OffersOffer & {
company: Company; company: Company;
location: City & { state: State & { country: Country } };
offersFullTime: offersFullTime:
| (OffersFullTime & { totalCompensation: OffersCurrency }) | (OffersFullTime & { totalCompensation: OffersCurrency })
| null; | null;
@ -247,6 +279,23 @@ export const profileAnalysisDtoMapper = (
return profileAnalysisDto; 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: { export const valuationDtoMapper = (currency: {
baseCurrency: string; baseCurrency: string;
baseValue: number; baseValue: number;
@ -300,6 +349,7 @@ export const educationDtoMapper = (education: {
export const experienceDtoMapper = ( export const experienceDtoMapper = (
experience: OffersExperience & { experience: OffersExperience & {
company: Company | null; company: Company | null;
location: (City & { state: State & { country: Country } }) | null;
monthlySalary: OffersCurrency | null; monthlySalary: OffersCurrency | null;
totalCompensation: OffersCurrency | null; totalCompensation: OffersCurrency | null;
}, },
@ -312,7 +362,10 @@ export const experienceDtoMapper = (
id: experience.id, id: experience.id,
jobType: experience.jobType, jobType: experience.jobType,
level: experience.level, level: experience.level,
location: experience.location, location:
experience.location != null
? locationDtoMapper(experience.location)
: null,
monthlySalary: experience.monthlySalary monthlySalary: experience.monthlySalary
? valuationDtoMapper(experience.monthlySalary) ? valuationDtoMapper(experience.monthlySalary)
: null, : null,
@ -345,6 +398,7 @@ export const backgroundDtoMapper = (
experiences: Array< experiences: Array<
OffersExperience & { OffersExperience & {
company: Company | null; company: Company | null;
location: (City & { state: State & { country: Country } }) | null;
monthlySalary: OffersCurrency | null; monthlySalary: OffersCurrency | null;
totalCompensation: OffersCurrency | null; totalCompensation: OffersCurrency | null;
} }
@ -383,6 +437,7 @@ export const backgroundDtoMapper = (
export const profileOfferDtoMapper = ( export const profileOfferDtoMapper = (
offer: OffersOffer & { offer: OffersOffer & {
company: Company; company: Company;
location: City & { state: State & { country: Country } };
offersFullTime: offersFullTime:
| (OffersFullTime & { | (OffersFullTime & {
baseSalary: OffersCurrency | null; baseSalary: OffersCurrency | null;
@ -399,7 +454,7 @@ export const profileOfferDtoMapper = (
company: offersCompanyDtoMapper(offer.company), company: offersCompanyDtoMapper(offer.company),
id: offer.id, id: offer.id,
jobType: offer.jobType, jobType: offer.jobType,
location: offer.location, location: locationDtoMapper(offer.location),
monthYearReceived: offer.monthYearReceived, monthYearReceived: offer.monthYearReceived,
negotiationStrategy: offer.negotiationStrategy, negotiationStrategy: offer.negotiationStrategy,
offersFullTime: offer.offersFullTime, offersFullTime: offer.offersFullTime,
@ -449,6 +504,7 @@ export const profileDtoMapper = (
topSimilarOffers: Array< topSimilarOffers: Array<
OffersOffer & { OffersOffer & {
company: Company; company: Company;
location: City & { state: State & { country: Country } };
offersFullTime: offersFullTime:
| (OffersFullTime & { totalCompensation: OffersCurrency }) | (OffersFullTime & { totalCompensation: OffersCurrency })
| null; | null;
@ -459,7 +515,14 @@ export const profileDtoMapper = (
background: background:
| (OffersBackground & { | (OffersBackground & {
experiences: Array< experiences: Array<
OffersExperience & { company: Company | null } OffersExperience & {
company: Company | null;
location:
| (City & {
state: State & { country: Country };
})
| null;
}
>; >;
}) })
| null; | null;
@ -472,6 +535,7 @@ export const profileDtoMapper = (
topSimilarOffers: Array< topSimilarOffers: Array<
OffersOffer & { OffersOffer & {
company: Company; company: Company;
location: City & { state: State & { country: Country } };
offersFullTime: offersFullTime:
| (OffersFullTime & { totalCompensation: OffersCurrency }) | (OffersFullTime & { totalCompensation: OffersCurrency })
| null; | null;
@ -482,7 +546,12 @@ export const profileDtoMapper = (
background: background:
| (OffersBackground & { | (OffersBackground & {
experiences: Array< experiences: Array<
OffersExperience & { company: Company | null } OffersExperience & {
company: Company | null;
location:
| (City & { state: State & { country: Country } })
| null;
}
>; >;
}) })
| null; | null;
@ -492,6 +561,7 @@ export const profileDtoMapper = (
}; };
overallHighestOffer: OffersOffer & { overallHighestOffer: OffersOffer & {
company: Company; company: Company;
location: City & { state: State & { country: Country } };
offersFullTime: offersFullTime:
| (OffersFullTime & { totalCompensation: OffersCurrency }) | (OffersFullTime & { totalCompensation: OffersCurrency })
| null; | null;
@ -508,6 +578,7 @@ export const profileDtoMapper = (
experiences: Array< experiences: Array<
OffersExperience & { OffersExperience & {
company: Company | null; company: Company | null;
location: (City & { state: State & { country: Country } }) | null;
monthlySalary: OffersCurrency | null; monthlySalary: OffersCurrency | null;
totalCompensation: OffersCurrency | null; totalCompensation: OffersCurrency | null;
} }
@ -525,6 +596,7 @@ export const profileDtoMapper = (
offers: Array< offers: Array<
OffersOffer & { OffersOffer & {
company: Company; company: Company;
location: City & { state: State & { country: Country } };
offersFullTime: offersFullTime:
| (OffersFullTime & { | (OffersFullTime & {
baseSalary: OffersCurrency | null; baseSalary: OffersCurrency | null;
@ -656,6 +728,7 @@ export const getUserProfileResponseMapper = (
offers: Array< offers: Array<
OffersOffer & { OffersOffer & {
company: Company; company: Company;
location: City & { state: State & { country: Country } };
offersFullTime: offersFullTime:
| (OffersFullTime & { totalCompensation: OffersCurrency }) | (OffersFullTime & { totalCompensation: OffersCurrency })
| null; | null;
@ -691,6 +764,7 @@ export const getUserProfileResponseMapper = (
const userProfileOfferDtoMapper = ( const userProfileOfferDtoMapper = (
offer: OffersOffer & { offer: OffersOffer & {
company: Company; company: Company;
location: City & { state: State & { country: Country } };
offersFullTime: offersFullTime:
| (OffersFullTime & { totalCompensation: OffersCurrency }) | (OffersFullTime & { totalCompensation: OffersCurrency })
| null; | null;
@ -709,7 +783,7 @@ const userProfileOfferDtoMapper = (
}, },
jobType: offer.jobType, jobType: offer.jobType,
level: offer.offersFullTime?.level ?? '', level: offer.offersFullTime?.level ?? '',
location: offer.location, location: locationDtoMapper(offer.location),
monthYearReceived: offer.monthYearReceived, monthYearReceived: offer.monthYearReceived,
title: title:
offer.jobType === JobType.FULLTIME offer.jobType === JobType.FULLTIME

@ -1,15 +1,18 @@
import Link from 'next/link'; import Link from 'next/link';
import { useState } from 'react'; import { useState } from 'react';
import { MapPinIcon } from '@heroicons/react/24/outline';
import { Banner } from '@tih/ui'; import { Banner } from '@tih/ui';
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics'; import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
import OffersTable from '~/components/offers/table/OffersTable'; import OffersTable from '~/components/offers/table/OffersTable';
import CitiesTypeahead from '~/components/shared/CitiesTypeahead';
import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead'; import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
import JobTitlesTypeahead from '~/components/shared/JobTitlesTypahead'; import JobTitlesTypeahead from '~/components/shared/JobTitlesTypahead';
export default function OffersHomePage() { export default function OffersHomePage() {
const [jobTitleFilter, setjobTitleFilter] = useState('software-engineer'); const [jobTitleFilter, setjobTitleFilter] = useState('software-engineer');
const [companyFilter, setCompanyFilter] = useState(''); const [companyFilter, setCompanyFilter] = useState('');
const [cityFilter, setCityFilter] = useState('');
const { event: gaEvent } = useGoogleAnalytics(); const { event: gaEvent } = useGoogleAnalytics();
return ( return (
@ -21,6 +24,25 @@ export default function OffersHomePage() {
</Link> </Link>
. .
</Banner> </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 className="bg-slate-100 py-16 px-4">
<div> <div>
<div> <div>
@ -58,7 +80,7 @@ export default function OffersHomePage() {
if (option) { if (option) {
setCompanyFilter(option.value); setCompanyFilter(option.value);
gaEvent({ gaEvent({
action: 'offers.table_filter_company', action: `offers.table_filter_company_${option.value}`,
category: 'engagement', category: 'engagement',
label: 'Filter by company', label: 'Filter by company',
}); });
@ -70,6 +92,7 @@ export default function OffersHomePage() {
</div> </div>
<div className="flex justify-center bg-white pb-20 pt-10"> <div className="flex justify-center bg-white pb-20 pt-10">
<OffersTable <OffersTable
cityFilter={cityFilter}
companyFilter={companyFilter} companyFilter={companyFilter}
jobTitleFilter={jobTitleFilter} jobTitleFilter={jobTitleFilter}
/> />

@ -36,13 +36,14 @@ export default function OffersEditPage() {
experiences.length === 0 experiences.length === 0
? [{ jobType: JobType.FULLTIME }] ? [{ jobType: JobType.FULLTIME }]
: experiences.map((exp) => ({ : experiences.map((exp) => ({
cityId: exp.location?.cityId,
cityName: exp.location?.cityName,
companyId: exp.company?.id, companyId: exp.company?.id,
companyName: exp.company?.name, companyName: exp.company?.name,
durationInMonths: exp.durationInMonths, durationInMonths: exp.durationInMonths,
id: exp.id, id: exp.id,
jobType: exp.jobType, jobType: exp.jobType,
level: exp.level, level: exp.level,
location: exp.location,
monthlySalary: exp.monthlySalary, monthlySalary: exp.monthlySalary,
title: exp.title, title: exp.title,
totalCompensation: exp.totalCompensation, totalCompensation: exp.totalCompensation,
@ -53,12 +54,13 @@ export default function OffersEditPage() {
}, },
id: data.id, id: data.id,
offers: data.offers.map((offer) => ({ offers: data.offers.map((offer) => ({
cityId: offer.location.cityId,
cityName: offer.location.cityName,
comments: offer.comments, comments: offer.comments,
companyId: offer.company.id, companyId: offer.company.id,
companyName: offer.company.name, companyName: offer.company.name,
id: offer.id, id: offer.id,
jobType: offer.jobType, jobType: offer.jobType,
location: offer.location,
monthYearReceived: convertToMonthYear(offer.monthYearReceived), monthYearReceived: convertToMonthYear(offer.monthYearReceived),
negotiationStrategy: offer.negotiationStrategy, negotiationStrategy: offer.negotiationStrategy,
offersFullTime: offer.offersFullTime, offersFullTime: offer.offersFullTime,

@ -1,4 +1,19 @@
import { z } from 'zod'; 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 { TRPCError } from '@trpc/server';
import { generateAnalysis } from '~/utils/offers/analysisGeneration'; import { generateAnalysis } from '~/utils/offers/analysisGeneration';
@ -19,6 +34,15 @@ export const offersAnalysisRouter = createRouter()
topSimilarOffers: { topSimilarOffers: {
include: { include: {
company: true, company: true,
location: {
include: {
state: {
include: {
country: true,
},
},
},
},
offersFullTime: { offersFullTime: {
include: { include: {
totalCompensation: true, totalCompensation: true,
@ -36,6 +60,15 @@ export const offersAnalysisRouter = createRouter()
experiences: { experiences: {
include: { include: {
company: true, company: true,
location: {
include: {
state: {
include: {
country: true,
},
},
},
},
}, },
}, },
}, },
@ -51,6 +84,15 @@ export const offersAnalysisRouter = createRouter()
topSimilarOffers: { topSimilarOffers: {
include: { include: {
company: true, company: true,
location: {
include: {
state: {
include: {
country: true,
},
},
},
},
offersFullTime: { offersFullTime: {
include: { include: {
totalCompensation: true, totalCompensation: true,
@ -68,6 +110,15 @@ export const offersAnalysisRouter = createRouter()
experiences: { experiences: {
include: { include: {
company: true, company: true,
location: {
include: {
state: {
include: {
country: true,
},
},
},
},
}, },
}, },
}, },
@ -81,6 +132,15 @@ export const offersAnalysisRouter = createRouter()
overallHighestOffer: { overallHighestOffer: {
include: { include: {
company: true, company: true,
location: {
include: {
state: {
include: {
country: true,
},
},
},
},
offersFullTime: { offersFullTime: {
include: { include: {
totalCompensation: true, totalCompensation: true,

@ -35,12 +35,12 @@ const company = z.object({
}); });
const offer = z.object({ const offer = z.object({
cityId: z.string(),
comments: z.string(), comments: z.string(),
company: company.nullish(), company: company.nullish(),
companyId: z.string(), companyId: z.string(),
id: z.string().optional(), id: z.string().optional(),
jobType: z.string().regex(createValidationRegex(Object.keys(JobType), null)), jobType: z.string().regex(createValidationRegex(Object.keys(JobType), null)),
location: z.string(),
monthYearReceived: z.date(), monthYearReceived: z.date(),
negotiationStrategy: z.string(), negotiationStrategy: z.string(),
offersFullTime: z offersFullTime: z
@ -75,6 +75,7 @@ const offer = z.object({
const experience = z.object({ const experience = z.object({
backgroundId: z.string().nullish(), backgroundId: z.string().nullish(),
cityId: z.string().nullish(),
company: company.nullish(), company: company.nullish(),
companyId: z.string().nullish(), companyId: z.string().nullish(),
durationInMonths: z.number().nullish(), durationInMonths: z.number().nullish(),
@ -84,7 +85,6 @@ const experience = z.object({
.regex(createValidationRegex(Object.keys(JobType), null)) .regex(createValidationRegex(Object.keys(JobType), null))
.nullish(), .nullish(),
level: z.string().nullish(), level: z.string().nullish(),
location: z.string().nullish(),
monthlySalary: valuation.nullish(), monthlySalary: valuation.nullish(),
monthlySalaryId: z.string().nullish(), monthlySalaryId: z.string().nullish(),
title: z.string().nullish(), title: z.string().nullish(),
@ -171,6 +171,15 @@ export const offersProfileRouter = createRouter()
topSimilarOffers: { topSimilarOffers: {
include: { include: {
company: true, company: true,
location: {
include: {
state: {
include: {
country: true,
},
},
},
},
offersFullTime: { offersFullTime: {
include: { include: {
totalCompensation: true, totalCompensation: true,
@ -188,6 +197,15 @@ export const offersProfileRouter = createRouter()
experiences: { experiences: {
include: { include: {
company: true, company: true,
location: {
include: {
state: {
include: {
country: true,
},
},
},
},
}, },
}, },
}, },
@ -203,6 +221,15 @@ export const offersProfileRouter = createRouter()
topSimilarOffers: { topSimilarOffers: {
include: { include: {
company: true, company: true,
location: {
include: {
state: {
include: {
country: true,
},
},
},
},
offersFullTime: { offersFullTime: {
include: { include: {
totalCompensation: true, totalCompensation: true,
@ -220,6 +247,15 @@ export const offersProfileRouter = createRouter()
experiences: { experiences: {
include: { include: {
company: true, company: true,
location: {
include: {
state: {
include: {
country: true,
},
},
},
},
}, },
}, },
}, },
@ -233,6 +269,15 @@ export const offersProfileRouter = createRouter()
overallHighestOffer: { overallHighestOffer: {
include: { include: {
company: true, company: true,
location: {
include: {
state: {
include: {
country: true,
},
},
},
},
offersFullTime: { offersFullTime: {
include: { include: {
totalCompensation: true, totalCompensation: true,
@ -258,6 +303,15 @@ export const offersProfileRouter = createRouter()
experiences: { experiences: {
include: { include: {
company: true, company: true,
location: {
include: {
state: {
include: {
country: true,
},
},
},
},
monthlySalary: true, monthlySalary: true,
totalCompensation: true, totalCompensation: true,
}, },
@ -275,6 +329,15 @@ export const offersProfileRouter = createRouter()
offers: { offers: {
include: { include: {
company: true, company: true,
location: {
include: {
state: {
include: {
country: true,
},
},
},
},
offersFullTime: { offersFullTime: {
include: { include: {
baseSalary: true, baseSalary: true,
@ -350,6 +413,39 @@ export const offersProfileRouter = createRouter()
input.background.experiences.map(async (x) => { input.background.experiences.map(async (x) => {
if (x.jobType === JobType.FULLTIME) { if (x.jobType === JobType.FULLTIME) {
if (x.companyId) { 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 { return {
company: { company: {
connect: { connect: {
@ -377,11 +473,40 @@ export const offersProfileRouter = createRouter()
: undefined, : 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 { return {
durationInMonths: x.durationInMonths, durationInMonths: x.durationInMonths,
jobType: x.jobType, jobType: x.jobType,
level: x.level, level: x.level,
location: x.location,
title: x.title, title: x.title,
totalCompensation: totalCompensation:
x.totalCompensation != null x.totalCompensation != null
@ -402,6 +527,40 @@ export const offersProfileRouter = createRouter()
} }
if (x.jobType === JobType.INTERN) { if (x.jobType === JobType.INTERN) {
if (x.companyId) { 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 { return {
company: { company: {
connect: { connect: {
@ -428,6 +587,37 @@ export const offersProfileRouter = createRouter()
title: x.title, 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 { return {
durationInMonths: x.durationInMonths, durationInMonths: x.durationInMonths,
jobType: x.jobType, jobType: x.jobType,
@ -488,7 +678,13 @@ export const offersProfileRouter = createRouter()
}, },
}, },
jobType: x.jobType, jobType: x.jobType,
location: x.location, location: {
connect: {
where: {
id: x.cityId
}
}
},
monthYearReceived: x.monthYearReceived, monthYearReceived: x.monthYearReceived,
negotiationStrategy: x.negotiationStrategy, negotiationStrategy: x.negotiationStrategy,
offersIntern: { offersIntern: {
@ -528,7 +724,13 @@ export const offersProfileRouter = createRouter()
}, },
}, },
jobType: x.jobType, jobType: x.jobType,
location: x.location, location: {
connect: {
where: {
id: x.cityId
}
}
},
monthYearReceived: x.monthYearReceived, monthYearReceived: x.monthYearReceived,
negotiationStrategy: x.negotiationStrategy, negotiationStrategy: x.negotiationStrategy,
offersFullTime: { offersFullTime: {
@ -871,35 +1073,110 @@ export const offersProfileRouter = createRouter()
exp.totalCompensation?.currency != null && exp.totalCompensation?.currency != null &&
exp.totalCompensation?.value != null exp.totalCompensation?.value != null
) { ) {
// FULLTIME
if (exp.companyId) { 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({ await ctx.prisma.offersBackground.update({
data: { data: {
experiences: { experiences: {
create: { create: {
company: {
connect: {
id: exp.companyId,
},
},
durationInMonths: exp.durationInMonths, durationInMonths: exp.durationInMonths,
jobType: exp.jobType, jobType: exp.jobType,
level: exp.level, level: exp.level,
location: exp.location, location: {
connect: {
id: exp.cityId
}
},
title: exp.title, title: exp.title,
totalCompensation: exp.totalCompensation totalCompensation: {
? { create: {
create: { baseCurrency: baseCurrencyString,
baseCurrency: baseCurrencyString, baseValue: await convert(
baseValue: await convert( exp.totalCompensation.value,
exp.totalCompensation.value, exp.totalCompensation.currency,
exp.totalCompensation.currency, baseCurrencyString,
baseCurrencyString, ),
), currency: exp.totalCompensation.currency,
currency: exp.totalCompensation.currency, value: exp.totalCompensation.value,
value: exp.totalCompensation.value, },
}, },
}
: undefined,
}, },
}, },
}, },
@ -915,7 +1192,6 @@ export const offersProfileRouter = createRouter()
durationInMonths: exp.durationInMonths, durationInMonths: exp.durationInMonths,
jobType: exp.jobType, jobType: exp.jobType,
level: exp.level, level: exp.level,
location: exp.location,
title: exp.title, title: exp.title,
totalCompensation: { totalCompensation: {
create: { create: {
@ -938,19 +1214,67 @@ export const offersProfileRouter = createRouter()
}); });
} }
} else if (exp.companyId) { } 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({ await ctx.prisma.offersBackground.update({
data: { data: {
experiences: { experiences: {
create: { create: {
company: {
connect: {
id: exp.companyId,
},
},
durationInMonths: exp.durationInMonths, durationInMonths: exp.durationInMonths,
jobType: exp.jobType, jobType: exp.jobType,
level: exp.level, level: exp.level,
location: exp.location, location: {
connect: {
id: exp.cityId
}
},
title: exp.title, title: exp.title,
}, },
}, },
@ -967,7 +1291,6 @@ export const offersProfileRouter = createRouter()
durationInMonths: exp.durationInMonths, durationInMonths: exp.durationInMonths,
jobType: exp.jobType, jobType: exp.jobType,
level: exp.level, level: exp.level,
location: exp.location,
title: exp.title, title: exp.title,
}, },
}, },
@ -982,19 +1305,90 @@ export const offersProfileRouter = createRouter()
exp.monthlySalary?.currency != null && exp.monthlySalary?.currency != null &&
exp.monthlySalary?.value != null exp.monthlySalary?.value != null
) { ) {
// INTERN
if (exp.companyId) { 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({ await ctx.prisma.offersBackground.update({
data: { data: {
experiences: { experiences: {
create: { create: {
company: {
connect: {
id: exp.companyId,
},
},
durationInMonths: exp.durationInMonths, durationInMonths: exp.durationInMonths,
jobType: exp.jobType, jobType: exp.jobType,
location: exp.location, location: {
connect: {
id: exp.cityId
}
},
monthlySalary: { monthlySalary: {
create: { create: {
baseCurrency: baseCurrencyString, baseCurrency: baseCurrencyString,
@ -1022,7 +1416,6 @@ export const offersProfileRouter = createRouter()
create: { create: {
durationInMonths: exp.durationInMonths, durationInMonths: exp.durationInMonths,
jobType: exp.jobType, jobType: exp.jobType,
location: exp.location,
monthlySalary: { monthlySalary: {
create: { create: {
baseCurrency: baseCurrencyString, baseCurrency: baseCurrencyString,
@ -1045,18 +1438,64 @@ export const offersProfileRouter = createRouter()
}); });
} }
} else if (exp.companyId) { } 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({ await ctx.prisma.offersBackground.update({
data: { data: {
experiences: { experiences: {
create: { create: {
company: {
connect: {
id: exp.companyId,
},
},
durationInMonths: exp.durationInMonths, durationInMonths: exp.durationInMonths,
jobType: exp.jobType, jobType: exp.jobType,
location: exp.location, location: {
connect: {
id: exp.cityId
}
},
title: exp.title, title: exp.title,
}, },
}, },
@ -1072,7 +1511,6 @@ export const offersProfileRouter = createRouter()
create: { create: {
durationInMonths: exp.durationInMonths, durationInMonths: exp.durationInMonths,
jobType: exp.jobType, jobType: exp.jobType,
location: exp.location,
title: exp.title, title: exp.title,
}, },
}, },
@ -1160,12 +1598,20 @@ export const offersProfileRouter = createRouter()
await ctx.prisma.offersOffer.update({ await ctx.prisma.offersOffer.update({
data: { data: {
comments: offerToUpdate.comments, comments: offerToUpdate.comments,
companyId: offerToUpdate.companyId, company: {
connect: {
id: offerToUpdate.companyId
}
},
jobType: jobType:
offerToUpdate.jobType === JobType.FULLTIME offerToUpdate.jobType === JobType.FULLTIME
? JobType.FULLTIME ? JobType.FULLTIME
: JobType.INTERN, : JobType.INTERN,
location: offerToUpdate.location, location: {
connect: {
id: offerToUpdate.cityId
}
},
monthYearReceived: offerToUpdate.monthYearReceived, monthYearReceived: offerToUpdate.monthYearReceived,
negotiationStrategy: offerToUpdate.negotiationStrategy, negotiationStrategy: offerToUpdate.negotiationStrategy,
}, },
@ -1302,7 +1748,11 @@ export const offersProfileRouter = createRouter()
}, },
}, },
jobType: offerToUpdate.jobType, jobType: offerToUpdate.jobType,
location: offerToUpdate.location, location: {
connect: {
id: offerToUpdate.cityId
}
},
monthYearReceived: offerToUpdate.monthYearReceived, monthYearReceived: offerToUpdate.monthYearReceived,
negotiationStrategy: offerToUpdate.negotiationStrategy, negotiationStrategy: offerToUpdate.negotiationStrategy,
offersIntern: { offersIntern: {
@ -1356,7 +1806,11 @@ export const offersProfileRouter = createRouter()
}, },
}, },
jobType: offerToUpdate.jobType, jobType: offerToUpdate.jobType,
location: offerToUpdate.location, location: {
connect: {
id: offerToUpdate.cityId
}
},
monthYearReceived: offerToUpdate.monthYearReceived, monthYearReceived: offerToUpdate.monthYearReceived,
negotiationStrategy: offerToUpdate.negotiationStrategy, negotiationStrategy: offerToUpdate.negotiationStrategy,
offersFullTime: { offersFullTime: {

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

@ -43,12 +43,12 @@ const getYoeRange = (yoeCategory: number) => {
export const offersRouter = createRouter().query('list', { export const offersRouter = createRouter().query('list', {
input: z.object({ input: z.object({
cityId: z.string(),
companyId: z.string().nullish(), companyId: z.string().nullish(),
currency: z.string().nullish(), currency: z.string().nullish(),
dateEnd: z.date().nullish(), dateEnd: z.date().nullish(),
dateStart: z.date().nullish(), dateStart: z.date().nullish(),
limit: z.number().positive(), limit: z.number().positive(),
location: z.string(),
offset: z.number().nonnegative(), offset: z.number().nonnegative(),
salaryMax: z.number().nonnegative().nullish(), salaryMax: z.number().nonnegative().nullish(),
salaryMin: z.number().nonnegative().nullish(), salaryMin: z.number().nonnegative().nullish(),
@ -132,8 +132,7 @@ export const offersRouter = createRouter().query('list', {
where: { where: {
AND: [ AND: [
{ {
location: cityId: input.cityId.length === 0 ? undefined : input.cityId,
input.location.length === 0 ? undefined : input.location,
}, },
{ {
offersIntern: { offersIntern: {
@ -246,8 +245,7 @@ export const offersRouter = createRouter().query('list', {
where: { where: {
AND: [ AND: [
{ {
location: cityId: input.cityId.length === 0 ? undefined : input.cityId,
input.location.length === 0 ? undefined : input.location,
}, },
{ {
offersIntern: { offersIntern: {

@ -25,7 +25,7 @@ export type Experience = {
id: string; id: string;
jobType: JobType?; jobType: JobType?;
level: string?; level: string?;
location: string?; location: Location?;
monthlySalary: Valuation?; monthlySalary: Valuation?;
title: string?; title: string?;
totalCompensation: Valuation?; totalCompensation: Valuation?;
@ -79,7 +79,7 @@ export type ProfileOffer = {
company: OffersCompany; company: OffersCompany;
id: string; id: string;
jobType: JobType; jobType: JobType;
location: string; location: Location;
monthYearReceived: Date; monthYearReceived: Date;
negotiationStrategy: string; negotiationStrategy: string;
offersFullTime: FullTime?; offersFullTime: FullTime?;
@ -163,7 +163,7 @@ export type AnalysisHighestOffer = {
company: OffersCompany; company: OffersCompany;
id: string; id: string;
level: string; level: string;
location: string; location: Location;
totalYoe: number; totalYoe: number;
}; };
@ -173,7 +173,7 @@ export type AnalysisOffer = {
income: Valuation; income: Valuation;
jobType: JobType; jobType: JobType;
level: string; level: string;
location: string; location: Location;
monthYearReceived: Date; monthYearReceived: Date;
negotiationStrategy: string; negotiationStrategy: string;
previousCompanies: Array<string>; previousCompanies: Array<string>;
@ -202,7 +202,17 @@ export type UserProfileOffer = {
income: Valuation; income: Valuation;
jobType: JobType; jobType: JobType;
level: string; level: string;
location: string; location: Location;
monthYearReceived: Date; monthYearReceived: Date;
title: string; 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 & { type Offer = OffersOffer & {
company: Company; company: Company;
location: City & { state: State & { country: Country } };
offersFullTime: offersFullTime:
| (OffersFullTime & { totalCompensation: OffersCurrency }) | (OffersFullTime & {
baseSalary: OffersCurrency | null;
bonus: OffersCurrency | null;
stocks: OffersCurrency | null;
totalCompensation: OffersCurrency;
})
| null; | null;
offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null; offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
profile: OffersProfile & { background: OffersBackground | null }; profile: OffersProfile & { background: OffersBackground | null };
@ -60,298 +66,393 @@ export const generateAnalysis = async (params: {
}) => { }) => {
const { ctx, input } = params; const { ctx, input } = params;
await ctx.prisma.offersAnalysis.deleteMany({ await ctx.prisma.offersAnalysis.deleteMany({
where: { where: {
profileId: input.profileId, 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,
}, },
}, });
profile: {
const offers = await ctx.prisma.offersOffer.findMany({
include: { include: {
background: true, company: true,
}, location: {
}, include: {
}, state: {
orderBy: [ include: {
{ country: true,
offersFullTime: { },
totalCompensation: { },
baseValue: 'desc', },
},
offersFullTime: {
include: {
baseSalary: true,
bonus: true,
stocks: true,
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: true,
},
}, },
}, },
}, orderBy: [
{ {
offersIntern: { offersFullTime: {
monthlySalary: { totalCompensation: {
baseValue: 'desc', baseValue: 'desc',
},
},
},
{
offersIntern: {
monthlySalary: {
baseValue: 'desc',
},
},
}, },
],
where: {
profileId: input.profileId,
}, },
}, });
],
where: {
profileId: input.profileId,
},
});
if (!offers || offers.length === 0) { if (!offers || offers.length === 0) {
throw new TRPCError({ throw new TRPCError({
code: 'NOT_FOUND', code: 'NOT_FOUND',
message: 'No offers found on this profile', message: 'No offers found on this profile',
}); });
} }
const overallHighestOffer = offers[0]; const overallHighestOffer = offers[0];
if ( if (
!overallHighestOffer.profile.background || !overallHighestOffer.profile.background ||
overallHighestOffer.profile.background.totalYoe == null overallHighestOffer.profile.background.totalYoe == null
) { ) {
throw new TRPCError({ throw new TRPCError({
code: 'NOT_FOUND', code: 'NOT_FOUND',
message: 'YOE not found', message: 'YOE not found',
}); });
} }
const yoe = overallHighestOffer.profile.background.totalYoe as number; const yoe = overallHighestOffer.profile.background.totalYoe as number;
const monthYearReceived = new Date(overallHighestOffer.monthYearReceived); const monthYearReceived = new Date(overallHighestOffer.monthYearReceived);
monthYearReceived.setFullYear(monthYearReceived.getFullYear() - 1); monthYearReceived.setFullYear(monthYearReceived.getFullYear() - 1);
const similarOffers = await ctx.prisma.offersOffer.findMany({ let similarOffers = await ctx.prisma.offersOffer.findMany({
include: {
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: { include: {
background: { company: true,
location: {
include: { include: {
experiences: { state: {
include: { include: {
company: true, country: true,
}, },
}, },
}, },
}, },
}, offersFullTime: {
}, include: {
}, totalCompensation: true,
orderBy: [ },
{
offersFullTime: {
totalCompensation: {
baseValue: 'desc',
}, },
}, offersIntern: {
}, include: {
{ monthlySalary: true,
offersIntern: { },
monthlySalary: {
baseValue: 'desc',
}, },
}, profile: {
}, include: {
], background: {
where: { include: {
AND: [ experiences: {
{ include: {
location: overallHighestOffer.location, company: true,
}, location: {
{ include: {
monthYearReceived: { state: {
gte: monthYearReceived, include: {
country: true,
},
},
},
},
},
},
},
},
},
}, },
}, },
{ orderBy: [
OR: [ {
{ offersFullTime: {
offersFullTime: { totalCompensation: {
title: overallHighestOffer.offersFullTime?.title, baseValue: 'desc',
}, },
offersIntern: { },
title: overallHighestOffer.offersIntern?.title, },
{
offersIntern: {
monthlySalary: {
baseValue: 'desc',
}, },
}, },
], },
}, ],
{ where: {
profile: { AND: [
background: { {
AND: [ location: overallHighestOffer.location,
},
{
monthYearReceived: {
gte: monthYearReceived,
},
},
{
OR: [
{ {
totalYoe: { offersFullTime: {
gte: Math.max(yoe - 1, 0), title: overallHighestOffer.offersFullTime?.title,
lte: yoe + 1, },
offersIntern: {
title: overallHighestOffer.offersIntern?.title,
}, },
}, },
], ],
}, },
}, {
profile: {
background: {
AND: [
{
totalYoe: {
gte: Math.max(yoe - 1, 0),
lte: yoe + 1,
},
},
],
},
},
},
],
}, },
], });
},
});
// COMPANY ANALYSIS // COMPANY ANALYSIS
const companyMap = new Map<string, Offer>(); const companyMap = new Map<string, Offer>();
offers.forEach((offer) => { offers.forEach((offer) => {
if (companyMap.get(offer.companyId) == null) { if (companyMap.get(offer.companyId) == null) {
companyMap.set(offer.companyId, offer); companyMap.set(offer.companyId, offer);
} }
}); });
const companyAnalysis = Array.from(companyMap.values()).map( const companyAnalysis = Array.from(companyMap.values()).map(
(companyOffer) => { (companyOffer) => {
// TODO: Refactor calculating analysis into a function // TODO: Refactor calculating analysis into a function
const similarCompanyOffers = similarOffers.filter( let similarCompanyOffers = similarOffers.filter(
(offer) => offer.companyId === companyOffer.companyId, (offer) => offer.companyId === companyOffer.companyId,
); );
const companyIndex = searchOfferPercentile( const companyIndex = searchOfferPercentile(
companyOffer, companyOffer,
similarCompanyOffers, similarCompanyOffers,
); );
const companyPercentile = const companyPercentile =
similarCompanyOffers.length <= 1 similarCompanyOffers.length <= 1
? 100 ? 100
: 100 - (100 * companyIndex) / (similarCompanyOffers.length - 1); : 100 - (100 * companyIndex) / (similarCompanyOffers.length - 1);
// Get top offers (excluding user's offer) // Get top offers (excluding user's offer)
const similarCompanyOffersWithoutUsersOffers = similarCompanyOffers = similarCompanyOffers.filter(
similarCompanyOffers.filter( (offer) => offer.id !== companyOffer.id,
(offer) => offer.profileId !== input.profileId, );
);
const noOfSimilarCompanyOffers = const noOfSimilarCompanyOffers = similarCompanyOffers.length;
similarCompanyOffersWithoutUsersOffers.length; const similarCompanyOffers90PercentileIndex = Math.ceil(
const similarCompanyOffers90PercentileIndex = Math.ceil( noOfSimilarCompanyOffers * 0.1,
noOfSimilarCompanyOffers * 0.1, );
); const topPercentileCompanyOffers =
const topPercentileCompanyOffers = noOfSimilarCompanyOffers > 2
noOfSimilarCompanyOffers > 2 ? similarCompanyOffers.slice(
? similarCompanyOffersWithoutUsersOffers.slice( similarCompanyOffers90PercentileIndex,
similarCompanyOffers90PercentileIndex, similarCompanyOffers90PercentileIndex + 2,
similarCompanyOffers90PercentileIndex + 2, )
) : similarCompanyOffers;
: similarCompanyOffersWithoutUsersOffers;
return { return {
companyName: companyOffer.company.name, companyName: companyOffer.company.name,
noOfSimilarOffers: noOfSimilarCompanyOffers, noOfSimilarOffers: noOfSimilarCompanyOffers,
percentile: companyPercentile, percentile: companyPercentile,
topSimilarOffers: topPercentileCompanyOffers, topSimilarOffers: topPercentileCompanyOffers,
}; };
}, },
); );
// OVERALL ANALYSIS // OVERALL ANALYSIS
const overallIndex = searchOfferPercentile( const overallIndex = searchOfferPercentile(
overallHighestOffer, overallHighestOffer,
similarOffers, similarOffers,
); );
const overallPercentile = const overallPercentile =
similarOffers.length <= 1 similarOffers.length <= 1
? 100 ? 100
: 100 - (100 * overallIndex) / (similarOffers.length - 1); : 100 - (100 * overallIndex) / (similarOffers.length - 1);
const similarOffersWithoutUsersOffers = similarOffers.filter( similarOffers = similarOffers.filter(
(similarOffer) => similarOffer.profileId !== input.profileId, (offer) => offer.id !== overallHighestOffer.id,
); );
const noOfSimilarOffers = similarOffersWithoutUsersOffers.length; const noOfSimilarOffers = similarOffers.length;
const similarOffers90PercentileIndex = Math.ceil(noOfSimilarOffers * 0.1); const similarOffers90PercentileIndex = Math.ceil(noOfSimilarOffers * 0.1);
const topPercentileOffers = const topPercentileOffers =
noOfSimilarOffers > 2 noOfSimilarOffers > 2
? similarOffersWithoutUsersOffers.slice( ? similarOffers.slice(
similarOffers90PercentileIndex, similarOffers90PercentileIndex,
similarOffers90PercentileIndex + 2, similarOffers90PercentileIndex + 2,
) )
: similarOffersWithoutUsersOffers; : similarOffers;
const analysis = await ctx.prisma.offersAnalysis.create({ const analysis = await ctx.prisma.offersAnalysis.create({
data: { data: {
companyAnalysis: { companyAnalysis: {
create: companyAnalysis.map((analysisUnit) => { create: companyAnalysis.map((analysisUnit) => {
return { return {
companyName: analysisUnit.companyName, companyName: analysisUnit.companyName,
noOfSimilarOffers: analysisUnit.noOfSimilarOffers, noOfSimilarOffers: analysisUnit.noOfSimilarOffers,
percentile: analysisUnit.percentile, percentile: analysisUnit.percentile,
topSimilarOffers: { topSimilarOffers: {
connect: analysisUnit.topSimilarOffers.map((offer) => { connect: analysisUnit.topSimilarOffers.map((offer) => {
return { id: offer.id }; return { id: offer.id };
}), }),
}, },
}; };
}),
},
overallAnalysis: {
create: {
companyName: overallHighestOffer.company.name,
noOfSimilarOffers,
percentile: overallPercentile,
topSimilarOffers: {
connect: topPercentileOffers.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: { include: {
topSimilarOffers: { companyAnalysis: {
include: { include: {
company: true, topSimilarOffers: {
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: { 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: { include: {
background: { company: true,
location: {
include: {
state: {
include: {
country: true,
},
},
},
},
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: { include: {
experiences: { background: {
include: { include: {
company: true, experiences: {
include: {
company: true,
location: {
include: {
state: {
include: {
country: true,
},
},
},
},
},
},
}, },
}, },
}, },
@ -360,13 +461,18 @@ export const generateAnalysis = async (params: {
}, },
}, },
}, },
}, overallHighestOffer: {
},
overallAnalysis: {
include: {
topSimilarOffers: {
include: { include: {
company: true, company: true,
location: {
include: {
state: {
include: {
country: true,
},
},
},
},
offersFullTime: { offersFullTime: {
include: { include: {
totalCompensation: true, totalCompensation: true,
@ -379,43 +485,13 @@ export const generateAnalysis = async (params: {
}, },
profile: { profile: {
include: { include: {
background: { background: true,
include: {
experiences: {
include: {
company: true,
},
},
},
},
}, },
}, },
}, },
}, },
}, },
}, });
overallHighestOffer: {
include: {
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: true,
},
},
},
},
},
});
return profileAnalysisDtoMapper(analysis); return profileAnalysisDtoMapper(analysis);
}; };

Loading…
Cancel
Save