[offers][fix] Refactor and fix offer analysis (#413)

pull/414/head
Ai Ling 2 years ago committed by GitHub
parent 6bf1a60bbd
commit 77d0714e33
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -84,7 +84,7 @@ export default function OfferProfileSave({
onClick={saveProfile} onClick={saveProfile}
/> />
</div> </div>
<div className="mb-10"> <div>
<Button <Button
icon={EyeIcon} icon={EyeIcon}
label="View your profile" label="View your profile"

@ -2,6 +2,7 @@ import { useRef, useState } from 'react';
import type { SubmitHandler } from 'react-hook-form'; import type { SubmitHandler } from 'react-hook-form';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/20/solid'; import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/20/solid';
import { JobType } from '@prisma/client';
import { Button } from '@tih/ui'; import { Button } from '@tih/ui';
import { Breadcrumbs } from '~/components/offers/Breadcrumb'; import { Breadcrumbs } from '~/components/offers/Breadcrumb';
@ -13,7 +14,6 @@ import type {
OfferFormData, OfferFormData,
OffersProfileFormData, OffersProfileFormData,
} from '~/components/offers/types'; } from '~/components/offers/types';
import { JobType } from '~/components/offers/types';
import type { Month } from '~/components/shared/MonthYearPicker'; import type { Month } from '~/components/shared/MonthYearPicker';
import { cleanObject, removeInvalidMoneyData } from '~/utils/offers/form'; import { cleanObject, removeInvalidMoneyData } from '~/utils/offers/form';
@ -25,7 +25,7 @@ import type { CreateOfferProfileResponse } from '~/types/offers';
const defaultOfferValues = { const defaultOfferValues = {
comments: '', comments: '',
companyId: '', companyId: '',
jobType: JobType.FullTime, jobType: JobType.FULLTIME,
location: '', location: '',
monthYearReceived: { monthYearReceived: {
month: getCurrentMonth() as Month, month: getCurrentMonth() as Month,
@ -36,18 +36,18 @@ const defaultOfferValues = {
export const defaultFullTimeOfferValues = { export const defaultFullTimeOfferValues = {
...defaultOfferValues, ...defaultOfferValues,
jobType: JobType.FullTime, jobType: JobType.FULLTIME,
}; };
export const defaultInternshipOfferValues = { export const defaultInternshipOfferValues = {
...defaultOfferValues, ...defaultOfferValues,
jobType: JobType.Intern, jobType: JobType.INTERN,
}; };
const defaultOfferProfileValues = { const defaultOfferProfileValues = {
background: { background: {
educations: [], educations: [],
experiences: [{ jobType: JobType.FullTime }], experiences: [{ jobType: JobType.FULLTIME }],
specificYoes: [], specificYoes: [],
totalYoe: 0, totalYoe: 0,
}, },
@ -90,7 +90,12 @@ export default function OffersSubmissionForm({
const formSteps: Array<FormStep> = [ const formSteps: Array<FormStep> = [
{ {
component: <OfferDetailsForm key={0} />, component: (
<OfferDetailsForm
key={0}
defaultJobType={initialOfferProfileValues.offers[0].jobType}
/>
),
hasNext: true, hasNext: true,
hasPrevious: false, hasPrevious: false,
label: 'Offer details', label: 'Offer details',

@ -4,7 +4,7 @@ import { HorizontalDivider, Spinner, Tabs } from '@tih/ui';
import { trpc } from '~/utils/trpc'; import { trpc } from '~/utils/trpc';
import OfferPercentileAnalysis from './OfferPercentileAnalysis'; import OfferPercentileAnalysisText from './OfferPercentileAnalysisText';
import OfferProfileCard from './OfferProfileCard'; import OfferProfileCard from './OfferProfileCard';
import { OVERALL_TAB } from '../../constants'; import { OVERALL_TAB } from '../../constants';
@ -38,11 +38,12 @@ function OfferAnalysisContent({
} }
return ( return (
<> <>
<OfferPercentileAnalysis <OfferPercentileAnalysisText
companyName={offer.company.name} companyName={offer.company.name}
offerAnalysis={offerAnalysis} offerAnalysis={offerAnalysis}
tab={tab} tab={tab}
/> />
<p className="mt-5">Here are some of the top offers relevant to you:</p>
{offerAnalysis.topPercentileOffers.map((topPercentileOffer) => ( {offerAnalysis.topPercentileOffers.map((topPercentileOffer) => (
<OfferProfileCard <OfferProfileCard
key={topPercentileOffer.id} key={topPercentileOffer.id}

@ -1,27 +0,0 @@
import type { Analysis } from '~/types/offers';
type OfferPercentileAnalysisProps = Readonly<{
companyName: string;
offerAnalysis: Analysis;
tab: string;
}>;
export default function OfferPercentileAnalysis({
tab,
companyName,
offerAnalysis: { noOfOffers, percentile },
}: OfferPercentileAnalysisProps) {
return tab === 'Overall' ? (
<p>
Your highest offer is from {companyName}, which is {percentile} percentile
out of {noOfOffers} offers received for the same job type, same level, and
same YOE(+/-1) in the last year.
</p>
) : (
<p>
Your offer from {companyName} is {percentile} percentile out of{' '}
{noOfOffers} offers received in {companyName} for the same job type, same
level, and same YOE(+/-1) in the last year.
</p>
);
}

@ -0,0 +1,27 @@
import type { Analysis } from '~/types/offers';
type OfferPercentileAnalysisTextProps = Readonly<{
companyName: string;
offerAnalysis: Analysis;
tab: string;
}>;
export default function OfferPercentileAnalysisText({
tab,
companyName,
offerAnalysis: { noOfOffers, percentile },
}: OfferPercentileAnalysisTextProps) {
return tab === 'Overall' ? (
<p>
Your highest offer is from <b>{companyName}</b>, which is{' '}
<b>{percentile}</b> percentile out of <b>{noOfOffers}</b> offers received
for the same job title and YOE(+/-1) in the last year.
</p>
) : (
<p>
Your offer from <b>{companyName}</b> is <b>{percentile}</b> percentile out
of <b>{noOfOffers}</b> offers received in {companyName} for the same job
title and YOE(+/-1) in the last year.
</p>
);
}

@ -1,9 +1,10 @@
import { UserCircleIcon } from '@heroicons/react/24/outline'; import { JobType } from '@prisma/client';
import { HorizontalDivider } from '~/../../../packages/ui/dist'; import { HorizontalDivider } from '~/../../../packages/ui/dist';
import { convertMoneyToString } from '~/utils/offers/currency';
import { formatDate } from '~/utils/offers/time'; import { formatDate } from '~/utils/offers/time';
import { JobType } from '../../types'; import ProfilePhotoHolder from '../../profile/ProfilePhotoHolder';
import type { AnalysisOffer } from '~/types/offers'; import type { AnalysisOffer } from '~/types/offers';
@ -29,7 +30,7 @@ export default function OfferProfileCard({
<div className="my-5 block rounded-lg border p-4"> <div className="my-5 block rounded-lg border p-4">
<div className="grid grid-flow-col grid-cols-12 gap-x-10"> <div className="grid grid-flow-col grid-cols-12 gap-x-10">
<div className="col-span-1"> <div className="col-span-1">
<UserCircleIcon width={50} /> <ProfilePhotoHolder size="sm" />
</div> </div>
<div className="col-span-10"> <div className="col-span-10">
<p className="text-sm font-semibold">{profileName}</p> <p className="text-sm font-semibold">{profileName}</p>
@ -50,9 +51,9 @@ export default function OfferProfileCard({
<div className="col-span-1 row-span-3"> <div className="col-span-1 row-span-3">
<p className="text-end text-sm">{formatDate(monthYearReceived)}</p> <p className="text-end text-sm">{formatDate(monthYearReceived)}</p>
<p className="text-end text-xl"> <p className="text-end text-xl">
{jobType === JobType.FullTime {jobType === JobType.FULLTIME
? `$${income} / year` ? `${convertMoneyToString(income)} / year`
: `$${income} / month`} : `${convertMoneyToString(income)} / month`}
</p> </p>
</div> </div>
</div> </div>

@ -1,4 +1,5 @@
import { useFormContext, useWatch } from 'react-hook-form'; import { useFormContext, useWatch } from 'react-hook-form';
import { JobType } from '@prisma/client';
import { Collapsible, RadioList } from '@tih/ui'; import { Collapsible, RadioList } from '@tih/ui';
import { import {
@ -10,7 +11,6 @@ import {
titleOptions, titleOptions,
} from '~/components/offers/constants'; } from '~/components/offers/constants';
import type { BackgroundPostData } from '~/components/offers/types'; import type { BackgroundPostData } from '~/components/offers/types';
import { JobType } from '~/components/offers/types';
import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead'; import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
import { CURRENCY_OPTIONS } from '~/utils/offers/currency/CurrencyEnum'; import { CURRENCY_OPTIONS } from '~/utils/offers/currency/CurrencyEnum';
@ -239,7 +239,7 @@ function InternshipJobFields() {
function CurrentJobSection() { function CurrentJobSection() {
const { register } = useFormContext(); const { register } = useFormContext();
const watchJobType = useWatch({ const watchJobType = useWatch({
defaultValue: JobType.FullTime, defaultValue: JobType.FULLTIME,
name: 'background.experiences.0.jobType', name: 'background.experiences.0.jobType',
}); });
@ -251,7 +251,7 @@ function CurrentJobSection() {
<div className="mb-5 rounded-lg border border-gray-200 px-10 py-5"> <div className="mb-5 rounded-lg border border-gray-200 px-10 py-5">
<div className="mb-5"> <div className="mb-5">
<FormRadioList <FormRadioList
defaultValue={JobType.FullTime} defaultValue={JobType.FULLTIME}
isLabelHidden={true} isLabelHidden={true}
label="Job Type" label="Job Type"
orientation="horizontal" orientation="horizontal"
@ -259,16 +259,16 @@ function CurrentJobSection() {
<RadioList.Item <RadioList.Item
key="Full-time" key="Full-time"
label="Full-time" label="Full-time"
value={JobType.FullTime} value={JobType.FULLTIME}
/> />
<RadioList.Item <RadioList.Item
key="Internship" key="Internship"
label="Internship" label="Internship"
value={JobType.Intern} value={JobType.INTERN}
/> />
</FormRadioList> </FormRadioList>
</div> </div>
{watchJobType === JobType.FullTime ? ( {watchJobType === JobType.FULLTIME ? (
<FullTimeJobFields /> <FullTimeJobFields />
) : ( ) : (
<InternshipJobFields /> <InternshipJobFields />

@ -9,6 +9,7 @@ import { useFormContext } from 'react-hook-form';
import { useFieldArray } from 'react-hook-form'; import { useFieldArray } from 'react-hook-form';
import { PlusIcon } from '@heroicons/react/20/solid'; import { PlusIcon } from '@heroicons/react/20/solid';
import { TrashIcon } from '@heroicons/react/24/outline'; import { TrashIcon } from '@heroicons/react/24/outline';
import { JobType } from '@prisma/client';
import { Button, Dialog } from '@tih/ui'; import { Button, Dialog } from '@tih/ui';
import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead'; import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
@ -31,7 +32,6 @@ import FormTextArea from '../../forms/FormTextArea';
import FormTextInput from '../../forms/FormTextInput'; import FormTextInput from '../../forms/FormTextInput';
import type { OfferFormData } from '../../types'; import type { OfferFormData } from '../../types';
import { JobTypeLabel } from '../../types'; import { JobTypeLabel } from '../../types';
import { JobType } from '../../types';
import { CURRENCY_OPTIONS } from '../../../../utils/offers/currency/CurrencyEnum'; import { CURRENCY_OPTIONS } from '../../../../utils/offers/currency/CurrencyEnum';
type FullTimeOfferDetailsFormProps = Readonly<{ type FullTimeOfferDetailsFormProps = Readonly<{
@ -448,7 +448,7 @@ function OfferDetailsFormArray({
{fields.map((item, index) => { {fields.map((item, index) => {
return ( return (
<div key={item.id}> <div key={item.id}>
{jobType === JobType.FullTime ? ( {jobType === JobType.FULLTIME ? (
<FullTimeOfferDetailsForm index={index} remove={remove} /> <FullTimeOfferDetailsForm index={index} remove={remove} />
) : ( ) : (
<InternshipOfferDetailsForm index={index} remove={remove} /> <InternshipOfferDetailsForm index={index} remove={remove} />
@ -464,7 +464,7 @@ function OfferDetailsFormArray({
variant="tertiary" variant="tertiary"
onClick={() => onClick={() =>
append( append(
jobType === JobType.FullTime jobType === JobType.FULLTIME
? defaultFullTimeOfferValues ? defaultFullTimeOfferValues
: defaultInternshipOfferValues, : defaultInternshipOfferValues,
) )
@ -474,8 +474,14 @@ function OfferDetailsFormArray({
); );
} }
export default function OfferDetailsForm() { type OfferDetailsFormProps = Readonly<{
const [jobType, setJobType] = useState(JobType.FullTime); defaultJobType?: JobType;
}>;
export default function OfferDetailsForm({
defaultJobType = JobType.FULLTIME,
}: OfferDetailsFormProps) {
const [jobType, setJobType] = useState(defaultJobType);
const [isDialogOpen, setDialogOpen] = useState(false); const [isDialogOpen, setDialogOpen] = useState(false);
const { control } = useFormContext(); const { control } = useFormContext();
const fieldArrayValues = useFieldArray({ control, name: 'offers' }); const fieldArrayValues = useFieldArray({ control, name: 'offers' });
@ -483,17 +489,17 @@ export default function OfferDetailsForm() {
const toggleJobType = () => { const toggleJobType = () => {
remove(); remove();
if (jobType === JobType.FullTime) { if (jobType === JobType.FULLTIME) {
setJobType(JobType.Intern); setJobType(JobType.INTERN);
append(defaultInternshipOfferValues); append(defaultInternshipOfferValues);
} else { } else {
setJobType(JobType.FullTime); setJobType(JobType.FULLTIME);
append(defaultFullTimeOfferValues); append(defaultFullTimeOfferValues);
} }
}; };
const switchJobTypeLabel = () => const switchJobTypeLabel = () =>
jobType === JobType.FullTime ? JobTypeLabel.INTERN : JobTypeLabel.FULLTIME; jobType === JobType.FULLTIME ? JobTypeLabel.INTERN : JobTypeLabel.FULLTIME;
return ( return (
<div className="mb-5"> <div className="mb-5">
@ -506,9 +512,9 @@ export default function OfferDetailsForm() {
display="block" display="block"
label={JobTypeLabel.FULLTIME} label={JobTypeLabel.FULLTIME}
size="md" size="md"
variant={jobType === JobType.FullTime ? 'secondary' : 'tertiary'} variant={jobType === JobType.FULLTIME ? 'secondary' : 'tertiary'}
onClick={() => { onClick={() => {
if (jobType === JobType.FullTime) { if (jobType === JobType.FULLTIME) {
return; return;
} }
setDialogOpen(true); setDialogOpen(true);
@ -520,9 +526,9 @@ export default function OfferDetailsForm() {
display="block" display="block"
label={JobTypeLabel.INTERN} label={JobTypeLabel.INTERN}
size="md" size="md"
variant={jobType === JobType.Intern ? 'secondary' : 'tertiary'} variant={jobType === JobType.INTERN ? 'secondary' : 'tertiary'}
onClick={() => { onClick={() => {
if (jobType === JobType.Intern) { if (jobType === JobType.INTERN) {
return; return;
} }
setDialogOpen(true); setDialogOpen(true);

@ -3,18 +3,10 @@ import {
LightBulbIcon, LightBulbIcon,
} from '@heroicons/react/24/outline'; } from '@heroicons/react/24/outline';
import type { EducationBackgroundType } from '~/components/offers/types'; import type { EducationDisplayData } from '~/components/offers/types';
type EducationEntity = {
endDate?: string;
field?: string;
school?: string;
startDate?: string;
type?: EducationBackgroundType;
};
type Props = Readonly<{ type Props = Readonly<{
education: EducationEntity; education: EducationDisplayData;
}>; }>;
export default function EducationCard({ export default function EducationCard({
@ -39,9 +31,7 @@ export default function EducationCard({
</div> </div>
{(startDate || endDate) && ( {(startDate || endDate) && (
<div className="font-light text-gray-400"> <div className="font-light text-gray-400">
<p>{`${startDate ? startDate : 'N/A'} - ${ <p>{`${startDate || 'N/A'} - ${endDate || 'N/A'}`}</p>
endDate ? endDate : 'N/A'
}`}</p>
</div> </div>
)} )}
</div> </div>

@ -6,10 +6,10 @@ import {
} from '@heroicons/react/24/outline'; } from '@heroicons/react/24/outline';
import { HorizontalDivider } from '@tih/ui'; import { HorizontalDivider } from '@tih/ui';
import type { OfferEntity } from '~/components/offers/types'; import type { OfferDisplayData } from '~/components/offers/types';
type Props = Readonly<{ type Props = Readonly<{
offer: OfferEntity; offer: OfferDisplayData;
}>; }>;
export default function OfferCard({ export default function OfferCard({

@ -3,13 +3,18 @@ import { Spinner } from '@tih/ui';
import EducationCard from '~/components/offers/profile/EducationCard'; import EducationCard from '~/components/offers/profile/EducationCard';
import OfferCard from '~/components/offers/profile/OfferCard'; import OfferCard from '~/components/offers/profile/OfferCard';
import type { BackgroundCard, OfferEntity } from '~/components/offers/types'; import type {
import { EducationBackgroundType } from '~/components/offers/types'; BackgroundDisplayData,
OfferDisplayData,
} from '~/components/offers/types';
import type { ProfileAnalysis } from '~/types/offers';
type ProfileHeaderProps = Readonly<{ type ProfileHeaderProps = Readonly<{
background?: BackgroundCard; analysis?: ProfileAnalysis;
background?: BackgroundDisplayData;
isLoading: boolean; isLoading: boolean;
offers: Array<OfferEntity>; offers: Array<OfferDisplayData>;
selectedTab: string; selectedTab: string;
}>; }>;
@ -52,7 +57,7 @@ export default function ProfileDetails({
<BriefcaseIcon className="mr-1 h-5" /> <BriefcaseIcon className="mr-1 h-5" />
<span className="font-bold">Work Experience</span> <span className="font-bold">Work Experience</span>
</div> </div>
<OfferCard offer={background?.experiences[0]} /> <OfferCard offer={background.experiences[0]} />
</> </>
)} )}
{background?.educations && background?.educations.length > 0 && ( {background?.educations && background?.educations.length > 0 && (
@ -61,15 +66,7 @@ export default function ProfileDetails({
<AcademicCapIcon className="mr-1 h-5" /> <AcademicCapIcon className="mr-1 h-5" />
<span className="font-bold">Education</span> <span className="font-bold">Education</span>
</div> </div>
<EducationCard <EducationCard education={background.educations[0]} />
education={{
endDate: background.educations[0].endDate,
field: background.educations[0].field,
school: background.educations[0].school,
startDate: background.educations[0].startDate,
type: EducationBackgroundType.Bachelor,
}}
/>
</> </>
)} )}
</> </>

@ -1,7 +1,6 @@
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useState } from 'react'; import { useState } from 'react';
import { import {
BookmarkSquareIcon,
BuildingOffice2Icon, BuildingOffice2Icon,
CalendarDaysIcon, CalendarDaysIcon,
PencilSquareIcon, PencilSquareIcon,
@ -10,12 +9,12 @@ import {
import { Button, Dialog, Spinner, Tabs } from '@tih/ui'; import { Button, Dialog, Spinner, Tabs } from '@tih/ui';
import ProfilePhotoHolder from '~/components/offers/profile/ProfilePhotoHolder'; import ProfilePhotoHolder from '~/components/offers/profile/ProfilePhotoHolder';
import type { BackgroundCard } from '~/components/offers/types'; import type { BackgroundDisplayData } from '~/components/offers/types';
import { getProfileEditPath } from '~/utils/offers/link'; import { getProfileEditPath } from '~/utils/offers/link';
type ProfileHeaderProps = Readonly<{ type ProfileHeaderProps = Readonly<{
background?: BackgroundCard; background?: BackgroundDisplayData;
handleDelete: () => void; handleDelete: () => void;
isEditable: boolean; isEditable: boolean;
isLoading: boolean; isLoading: boolean;
@ -42,14 +41,14 @@ export default function ProfileHeader({
function renderActionList() { function renderActionList() {
return ( return (
<div className="space-x-2"> <div className="space-x-2">
<Button {/* <Button
disabled={isLoading} disabled={isLoading}
icon={BookmarkSquareIcon} icon={BookmarkSquareIcon}
isLabelHidden={true} isLabelHidden={true}
label="Save to user account" label="Save to user account"
size="md" size="md"
variant="tertiary" variant="tertiary"
/> /> */}
<Button <Button
disabled={isLoading} disabled={isLoading}
icon={PencilSquareIcon} icon={PencilSquareIcon}
@ -109,6 +108,13 @@ export default function ProfileHeader({
</div> </div>
); );
} }
if (!background) {
return null;
}
const { experiences, totalYoe, specificYoes, profileName } = background;
return ( return (
<div className="h-40 bg-white p-4"> <div className="h-40 bg-white p-4">
<div className="justify-left flex h-1/2"> <div className="justify-left flex h-1/2">
@ -118,7 +124,7 @@ export default function ProfileHeader({
<div className="w-full"> <div className="w-full">
<div className="justify-left flex flex-1"> <div className="justify-left flex flex-1">
<h2 className="flex w-4/5 text-2xl font-bold"> <h2 className="flex w-4/5 text-2xl font-bold">
{background?.profileName ?? 'anonymous'} {profileName ?? 'anonymous'}
</h2> </h2>
{isEditable && ( {isEditable && (
<div className="flex h-8 w-1/5 justify-end"> <div className="flex h-8 w-1/5 justify-end">
@ -126,22 +132,26 @@ export default function ProfileHeader({
</div> </div>
)} )}
</div> </div>
<div className="flex flex-row"> {(experiences[0]?.companyName ||
<BuildingOffice2Icon className="mr-2.5 h-5" /> experiences[0]?.jobLevel ||
<span className="mr-2 font-bold">Current:</span> experiences[0]?.jobTitle) && (
<span> <div className="flex flex-row">
{`${background?.experiences[0]?.companyName ?? '-'} ${ <BuildingOffice2Icon className="mr-2.5 h-5" />
background?.experiences[0]?.jobLevel || '' <span className="mr-2 font-bold">Current:</span>
} ${background?.experiences[0]?.jobTitle || ''}`} <span>
</span> {`${experiences[0]?.companyName || ''} ${
</div> experiences[0]?.jobLevel || ''
} ${experiences[0]?.jobTitle || ''}`}
</span>
</div>
)}
<div className="flex flex-row"> <div className="flex flex-row">
<CalendarDaysIcon className="mr-2.5 h-5" /> <CalendarDaysIcon className="mr-2.5 h-5" />
<span className="mr-2 font-bold">YOE:</span> <span className="mr-2 font-bold">YOE:</span>
<span className="mr-4">{background?.totalYoe}</span> <span className="mr-4">{totalYoe}</span>
{background?.specificYoes && {specificYoes &&
background?.specificYoes.length > 0 && specificYoes.length > 0 &&
background?.specificYoes.map(({ domain, yoe }) => { specificYoes.map(({ domain, yoe }) => {
return ( return (
<span <span
key={domain} key={domain}

@ -1,6 +1,14 @@
export default function ProfilePhotoHolder() { type ProfilePhotoHolderProps = {
size?: 'lg' | 'sm';
};
export default function ProfilePhotoHolder({
size = 'lg',
}: ProfilePhotoHolderProps) {
const sizeMap = { lg: '16', sm: '12' };
return ( return (
<span className="inline-block h-16 w-16 overflow-hidden rounded-full bg-gray-100"> <span
className={`inline-block h-${sizeMap[size]} w-${sizeMap[size]} overflow-hidden rounded-full bg-gray-100`}>
<svg <svg
className="h-full w-full text-gray-300" className="h-full w-full text-gray-300"
fill="currentColor" fill="currentColor"

@ -1,14 +1,11 @@
import type { JobType } from '@prisma/client';
import type { MonthYear } from '~/components/shared/MonthYearPicker'; import type { MonthYear } from '~/components/shared/MonthYearPicker';
/* /*
* Offer Profile * Offer Profile
*/ */
export enum JobType {
FullTime = 'FULLTIME',
Intern = 'INTERN',
}
export const JobTypeLabel = { export const JobTypeLabel = {
FULLTIME: 'Full-time', FULLTIME: 'Full-time',
INTERN: 'Internship', INTERN: 'Internship',
@ -26,17 +23,20 @@ export enum EducationBackgroundType {
export type OffersProfilePostData = { export type OffersProfilePostData = {
background: BackgroundPostData; background: BackgroundPostData;
id?: string;
offers: Array<OfferPostData>; offers: Array<OfferPostData>;
}; };
export type OffersProfileFormData = { export type OffersProfileFormData = {
background: BackgroundPostData; background: BackgroundPostData;
id?: string;
offers: Array<OfferFormData>; offers: Array<OfferFormData>;
}; };
export type BackgroundPostData = { export type BackgroundPostData = {
educations: Array<EducationPostData>; educations: Array<EducationPostData>;
experiences: Array<ExperiencePostData>; experiences: Array<ExperiencePostData>;
id?: string;
specificYoes: Array<SpecificYoePostData>; specificYoes: Array<SpecificYoePostData>;
totalYoe: number; totalYoe: number;
}; };
@ -44,6 +44,7 @@ export type BackgroundPostData = {
type ExperiencePostData = { type ExperiencePostData = {
companyId?: string | null; companyId?: string | null;
durationInMonths?: number | null; durationInMonths?: number | null;
id?: string;
jobType?: string | null; jobType?: string | null;
level?: string | null; level?: string | null;
location?: string | null; location?: string | null;
@ -57,6 +58,7 @@ type ExperiencePostData = {
type EducationPostData = { type EducationPostData = {
endDate?: Date | null; endDate?: Date | null;
field?: string | null; field?: string | null;
id?: string;
school?: string | null; school?: string | null;
startDate?: Date | null; startDate?: Date | null;
type?: string | null; type?: string | null;
@ -64,6 +66,7 @@ type EducationPostData = {
type SpecificYoePostData = { type SpecificYoePostData = {
domain: string; domain: string;
id?: string;
yoe: number; yoe: number;
}; };
@ -72,7 +75,8 @@ type SpecificYoe = SpecificYoePostData;
export type OfferPostData = { export type OfferPostData = {
comments: string; comments: string;
companyId: string; companyId: string;
jobType: string; id?: string;
jobType: JobType;
location: string; location: string;
monthYearReceived: Date; monthYearReceived: Date;
negotiationStrategy: string; negotiationStrategy: string;
@ -87,6 +91,7 @@ export type OfferFormData = Omit<OfferPostData, 'monthYearReceived'> & {
export type OfferFullTimePostData = { export type OfferFullTimePostData = {
baseSalary: Money; baseSalary: Money;
bonus: Money; bonus: Money;
id?: string;
level: string; level: string;
specialization: string; specialization: string;
stocks: Money; stocks: Money;
@ -95,6 +100,7 @@ export type OfferFullTimePostData = {
}; };
export type OfferInternPostData = { export type OfferInternPostData = {
id?: string;
internshipCycle: string; internshipCycle: string;
monthlySalary: Money; monthlySalary: Money;
specialization: string; specialization: string;
@ -104,40 +110,41 @@ export type OfferInternPostData = {
export type Money = { export type Money = {
currency: string; currency: string;
id?: string;
value: number; value: number;
}; };
type EducationDisplay = { export type EducationDisplayData = {
endDate?: string; endDate?: string | null;
field: string; field?: string | null;
school: string; school?: string | null;
startDate?: string; startDate?: string | null;
type: string; type?: string | null;
}; };
export type OfferEntity = { export type OfferDisplayData = {
base?: string; base?: string | null;
bonus?: string; bonus?: string | null;
companyName?: string; companyName?: string | null;
duration?: string; duration?: number | null;
id?: string; id?: string;
jobLevel?: string; jobLevel?: string | null;
jobTitle?: string; jobTitle?: string | null;
location?: string; location?: string | null;
monthlySalary?: string; monthlySalary?: string | null;
negotiationStrategy?: string; negotiationStrategy?: string | null;
otherComment?: string; otherComment?: string | null;
receivedMonth?: string; receivedMonth?: string | null;
stocks?: string; stocks?: string | null;
totalCompensation?: string; totalCompensation?: string | null;
}; };
export type BackgroundCard = { export type BackgroundDisplayData = {
educations: Array<EducationDisplay>; educations: Array<EducationDisplayData>;
experiences: Array<OfferEntity>; experiences: Array<OfferDisplayData>;
profileName: string; profileName: string;
specificYoes: Array<SpecificYoe>; specificYoes: Array<SpecificYoe>;
totalYoe: string; totalYoe: number;
}; };
export type CommentEntity = { export type CommentEntity = {

@ -56,7 +56,13 @@ const analysisOfferDtoMapper = (
const analysisOfferDto: AnalysisOffer = { const analysisOfferDto: AnalysisOffer = {
company: offersCompanyDtoMapper(offer.company), company: offersCompanyDtoMapper(offer.company),
id: offer.id, id: offer.id,
income: { baseCurrency: '', baseValue: -1, currency: '', value: -1 }, income: {
baseCurrency: '',
baseValue: -1,
currency: '',
id: '',
value: -1,
},
jobType: offer.jobType, jobType: offer.jobType,
level: offer.offersFullTime?.level ?? '', level: offer.offersFullTime?.level ?? '',
location: offer.location, location: offer.location,
@ -83,6 +89,7 @@ const analysisOfferDtoMapper = (
offer.offersFullTime.totalCompensation.value; offer.offersFullTime.totalCompensation.value;
analysisOfferDto.income.currency = analysisOfferDto.income.currency =
offer.offersFullTime.totalCompensation.currency; offer.offersFullTime.totalCompensation.currency;
analysisOfferDto.income.id = offer.offersFullTime.totalCompensation.id;
analysisOfferDto.income.baseValue = analysisOfferDto.income.baseValue =
offer.offersFullTime.totalCompensation.baseValue; offer.offersFullTime.totalCompensation.baseValue;
analysisOfferDto.income.baseCurrency = analysisOfferDto.income.baseCurrency =
@ -91,6 +98,7 @@ const analysisOfferDtoMapper = (
analysisOfferDto.income.value = offer.offersIntern.monthlySalary.value; analysisOfferDto.income.value = offer.offersIntern.monthlySalary.value;
analysisOfferDto.income.currency = analysisOfferDto.income.currency =
offer.offersIntern.monthlySalary.currency; offer.offersIntern.monthlySalary.currency;
analysisOfferDto.income.id = offer.offersIntern.monthlySalary.id;
analysisOfferDto.income.baseValue = analysisOfferDto.income.baseValue =
offer.offersIntern.monthlySalary.baseValue; offer.offersIntern.monthlySalary.baseValue;
analysisOfferDto.income.baseCurrency = analysisOfferDto.income.baseCurrency =
@ -255,13 +263,14 @@ export const valuationDtoMapper = (currency: {
baseCurrency: string; baseCurrency: string;
baseValue: number; baseValue: number;
currency: string; currency: string;
id?: string; id: string;
value: number; value: number;
}) => { }) => {
const valuationDto: Valuation = { const valuationDto: Valuation = {
baseCurrency: currency.baseCurrency, baseCurrency: currency.baseCurrency,
baseValue: currency.baseValue, baseValue: currency.baseValue,
currency: currency.currency, currency: currency.currency,
id: currency.id,
value: currency.value, value: currency.value,
}; };
return valuationDto; return valuationDto;
@ -595,11 +604,12 @@ export const dashboardOfferDtoMapper = (
baseCurrency: '', baseCurrency: '',
baseValue: -1, baseValue: -1,
currency: '', currency: '',
id: '',
value: -1, value: -1,
}), }),
monthYearReceived: offer.monthYearReceived, monthYearReceived: offer.monthYearReceived,
profileId: offer.profileId, profileId: offer.profileId,
title: offer.offersFullTime?.title ?? '', title: offer.offersFullTime?.title || offer.offersIntern?.title || '',
totalYoe: offer.profile.background?.totalYoe ?? -1, totalYoe: offer.profile.background?.totalYoe ?? -1,
}; };

@ -5,14 +5,17 @@ import { useState } from 'react';
import ProfileComments from '~/components/offers/profile/ProfileComments'; import ProfileComments from '~/components/offers/profile/ProfileComments';
import ProfileDetails from '~/components/offers/profile/ProfileDetails'; import ProfileDetails from '~/components/offers/profile/ProfileDetails';
import ProfileHeader from '~/components/offers/profile/ProfileHeader'; import ProfileHeader from '~/components/offers/profile/ProfileHeader';
import type { BackgroundCard, OfferEntity } from '~/components/offers/types'; import type {
BackgroundDisplayData,
OfferDisplayData,
} from '~/components/offers/types';
import { convertMoneyToString } from '~/utils/offers/currency'; import { convertMoneyToString } from '~/utils/offers/currency';
import { getProfilePath } from '~/utils/offers/link'; import { getProfilePath } from '~/utils/offers/link';
import { formatDate } from '~/utils/offers/time'; import { formatDate } from '~/utils/offers/time';
import { trpc } from '~/utils/trpc'; import { trpc } from '~/utils/trpc';
import type { Profile, ProfileOffer } from '~/types/offers'; import type { Profile, ProfileAnalysis, ProfileOffer } from '~/types/offers';
export default function OfferProfile() { export default function OfferProfile() {
const ErrorPage = ( const ErrorPage = (
@ -21,10 +24,11 @@ export default function OfferProfile() {
const router = useRouter(); const router = useRouter();
const { offerProfileId, token = '' } = router.query; const { offerProfileId, token = '' } = router.query;
const [isEditable, setIsEditable] = useState(false); const [isEditable, setIsEditable] = useState(false);
const [background, setBackground] = useState<BackgroundCard>(); const [background, setBackground] = useState<BackgroundDisplayData>();
const [offers, setOffers] = useState<Array<OfferEntity>>([]); const [offers, setOffers] = useState<Array<OfferDisplayData>>([]);
const [selectedTab, setSelectedTab] = useState('offers'); const [selectedTab, setSelectedTab] = useState('offers');
const [analysis, setAnalysis] = useState<ProfileAnalysis>();
const getProfileQuery = trpc.useQuery( const getProfileQuery = trpc.useQuery(
[ [
@ -44,75 +48,79 @@ export default function OfferProfile() {
setIsEditable(data?.isEditable ?? false); setIsEditable(data?.isEditable ?? false);
if (data?.offers) { const filteredOffers: Array<OfferDisplayData> = data
const filteredOffers: Array<OfferEntity> = data ? data?.offers.map((res: ProfileOffer) => {
? data?.offers.map((res: ProfileOffer) => { if (res.offersFullTime) {
if (res.offersFullTime) { const filteredOffer: OfferDisplayData = {
const filteredOffer: OfferEntity = { base: convertMoneyToString(res.offersFullTime.baseSalary),
base: convertMoneyToString(res.offersFullTime.baseSalary), bonus: convertMoneyToString(res.offersFullTime.bonus),
bonus: convertMoneyToString(res.offersFullTime.bonus),
companyName: res.company.name,
id: res.offersFullTime.id,
jobLevel: res.offersFullTime.level,
jobTitle: res.offersFullTime.title,
location: res.location,
negotiationStrategy: res.negotiationStrategy || '',
otherComment: res.comments || '',
receivedMonth: formatDate(res.monthYearReceived),
stocks: convertMoneyToString(res.offersFullTime.stocks),
totalCompensation: convertMoneyToString(
res.offersFullTime.totalCompensation,
),
};
return filteredOffer;
}
const filteredOffer: OfferEntity = {
companyName: res.company.name, companyName: res.company.name,
id: res.offersIntern!.id, id: res.offersFullTime.id,
jobTitle: res.offersIntern!.title, jobLevel: res.offersFullTime.level,
jobTitle: res.offersFullTime.title,
location: res.location, location: res.location,
monthlySalary: convertMoneyToString( negotiationStrategy: res.negotiationStrategy,
res.offersIntern!.monthlySalary, otherComment: res.comments,
),
negotiationStrategy: res.negotiationStrategy || '',
otherComment: res.comments || '',
receivedMonth: formatDate(res.monthYearReceived), receivedMonth: formatDate(res.monthYearReceived),
stocks: convertMoneyToString(res.offersFullTime.stocks),
totalCompensation: convertMoneyToString(
res.offersFullTime.totalCompensation,
),
}; };
return filteredOffer; return filteredOffer;
}) }
: []; const filteredOffer: OfferDisplayData = {
setOffers(filteredOffers); companyName: res.company.name,
} id: res.offersIntern!.id,
jobTitle: res.offersIntern!.title,
location: res.location,
monthlySalary: convertMoneyToString(
res.offersIntern!.monthlySalary,
),
negotiationStrategy: res.negotiationStrategy,
otherComment: res.comments,
receivedMonth: formatDate(res.monthYearReceived),
};
return filteredOffer;
})
: [];
setOffers(filteredOffers);
if (data?.background) { if (data?.background) {
const transformedBackground = { const transformedBackground = {
educations: data.background.educations.map((education) => ({ educations: data.background.educations.map((education) => ({
endDate: education.endDate ? formatDate(education.endDate) : '-', endDate: education.endDate ? formatDate(education.endDate) : null,
field: education.field || '-', field: education.field,
school: education.school || '-', school: education.school,
startDate: education.startDate startDate: education.startDate
? formatDate(education.startDate) ? formatDate(education.startDate)
: '-', : null,
type: education.type || '-', type: education.type,
})),
experiences: data.background.experiences.map((experience) => ({
companyName: experience.company?.name ?? '-',
duration: String(experience.durationInMonths) ?? '-',
jobLevel: experience.level ?? '',
jobTitle: experience.title ?? '-',
monthlySalary: experience.monthlySalary
? convertMoneyToString(experience.monthlySalary)
: '-',
totalCompensation: experience.totalCompensation
? convertMoneyToString(experience.totalCompensation)
: '-',
})), })),
experiences: data.background.experiences.map(
(experience): OfferDisplayData => ({
companyName: experience.company?.name,
duration: experience.durationInMonths,
jobLevel: experience.level,
jobTitle: experience.title,
monthlySalary: experience.monthlySalary
? convertMoneyToString(experience.monthlySalary)
: null,
totalCompensation: experience.totalCompensation
? convertMoneyToString(experience.totalCompensation)
: null,
}),
),
profileName: data.profileName, profileName: data.profileName,
specificYoes: data.background.specificYoes, specificYoes: data.background.specificYoes,
totalYoe: String(data.background.totalYoe) || '-', totalYoe: data.background.totalYoe,
}; };
setBackground(transformedBackground); setBackground(transformedBackground);
} }
if (data.analysis) {
setAnalysis(data.analysis);
}
}, },
}, },
); );
@ -153,6 +161,7 @@ export default function OfferProfile() {
/> />
<div className="h-4/5 w-full overflow-y-scroll pb-32"> <div className="h-4/5 w-full overflow-y-scroll pb-32">
<ProfileDetails <ProfileDetails
analysis={analysis}
background={background} background={background}
isLoading={getProfileQuery.isLoading} isLoading={getProfileQuery.isLoading}
offers={offers} offers={offers}

@ -1,9 +1,9 @@
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useState } from 'react'; import { useState } from 'react';
import { JobType } from '@prisma/client';
import OffersSubmissionForm from '~/components/offers/offersSubmission/OffersSubmissionForm'; import OffersSubmissionForm from '~/components/offers/offersSubmission/OffersSubmissionForm';
import type { OffersProfileFormData } from '~/components/offers/types'; import type { OffersProfileFormData } from '~/components/offers/types';
import { JobType } from '~/components/offers/types';
import { Spinner } from '~/../../../packages/ui/dist'; import { Spinner } from '~/../../../packages/ui/dist';
import { getProfilePath } from '~/utils/offers/link'; import { getProfilePath } from '~/utils/offers/link';
@ -25,7 +25,7 @@ export default function OffersEditPage() {
console.error(error.message); console.error(error.message);
}, },
onSuccess(data) { onSuccess(data) {
const { educations, experiences, specificYoes, totalYoe } = const { educations, experiences, specificYoes, totalYoe, id } =
data.background!; data.background!;
setInitialData({ setInitialData({
@ -33,11 +33,13 @@ export default function OffersEditPage() {
educations, educations,
experiences: experiences:
experiences.length === 0 experiences.length === 0
? [{ jobType: JobType.FullTime }] ? [{ jobType: JobType.FULLTIME }]
: experiences, : experiences,
id,
specificYoes, specificYoes,
totalYoe, totalYoe,
}, },
id: data.id,
offers: data.offers.map((offer) => ({ offers: data.offers.map((offer) => ({
comments: offer.comments, comments: offer.comments,
companyId: offer.company.id, companyId: offer.company.id,
@ -67,7 +69,7 @@ export default function OffersEditPage() {
<Spinner className="m-10" display="block" size="lg" /> <Spinner className="m-10" display="block" size="lg" />
</div> </div>
)} )}
{!getProfileResult.isLoading && ( {!getProfileResult.isLoading && initialData && (
<OffersSubmissionForm <OffersSubmissionForm
initialOfferProfileValues={initialData} initialOfferProfileValues={initialData}
profileId={profile?.id} profileId={profile?.id}

@ -285,7 +285,6 @@ export const offersAnalysisRouter = createRouter()
OR: [ OR: [
{ {
offersFullTime: { offersFullTime: {
level: overallHighestOffer.offersFullTime?.level,
title: overallHighestOffer.offersFullTime?.title, title: overallHighestOffer.offersFullTime?.title,
}, },
offersIntern: { offersIntern: {

@ -385,7 +385,7 @@ export const offersProfileRouter = createRouter()
throw new trpc.TRPCError({ throw new trpc.TRPCError({
code: 'BAD_REQUEST', code: 'BAD_REQUEST',
message: 'Missing fields.', message: 'Missing fields in background experiences.',
}); });
}), }),
}, },
@ -533,7 +533,7 @@ export const offersProfileRouter = createRouter()
// Throw error // Throw error
throw new trpc.TRPCError({ throw new trpc.TRPCError({
code: 'BAD_REQUEST', code: 'BAD_REQUEST',
message: 'Missing fields.', message: 'Missing fields in offers.',
}); });
}), }),
), ),

@ -45,6 +45,7 @@ export type Valuation = {
baseCurrency: string; baseCurrency: string;
baseValue: number; baseValue: number;
currency: string; currency: string;
id: string;
value: number; value: number;
}; };

Loading…
Cancel
Save