[offers][feat] Add multiple company analysis

This commit is contained in:
Bryann Yeap Kok Keong
2022-10-31 11:41:36 +08:00
committed by Bryann Yeap Kok Keong
parent 68f3c72945
commit 91696571fe
17 changed files with 754 additions and 572 deletions

View File

@@ -7,7 +7,7 @@ const navigation: ProductNavigationItems = [
const navigationAuthenticated: ProductNavigationItems = [
{ href: '/offers/submit', name: 'Analyze your offers' },
{ href: '/offers/dashboard', name: 'Your repository' },
{ href: '/offers/dashboard', name: 'Your dashboard' },
{ href: '/offers/features', name: 'Features' },
];

View File

@@ -6,29 +6,20 @@ import OfferPercentileAnalysisText from './OfferPercentileAnalysisText';
import OfferProfileCard from './OfferProfileCard';
import { OVERALL_TAB } from '../constants';
import type {
Analysis,
AnalysisHighestOffer,
ProfileAnalysis,
} from '~/types/offers';
type OfferAnalysisData = {
offer?: AnalysisHighestOffer;
offerAnalysis?: Analysis;
};
import type { AnalysisUnit, ProfileAnalysis } from '~/types/offers';
type OfferAnalysisContentProps = Readonly<{
analysis: OfferAnalysisData;
analysis: AnalysisUnit;
isSubmission: boolean;
tab: string;
}>;
function OfferAnalysisContent({
analysis: { offer, offerAnalysis },
analysis,
tab,
isSubmission,
}: OfferAnalysisContentProps) {
if (!offerAnalysis || !offer || offerAnalysis.noOfOffers === 0) {
if (!analysis || analysis.noOfOffers === 0) {
if (tab === OVERALL_TAB) {
return (
<p className="m-10">
@@ -47,9 +38,8 @@ function OfferAnalysisContent({
return (
<>
<OfferPercentileAnalysisText
companyName={offer.company.name}
analysis={analysis}
isSubmission={isSubmission}
offerAnalysis={offerAnalysis}
tab={tab}
/>
<p className="mt-5">
@@ -57,7 +47,7 @@ function OfferAnalysisContent({
? 'Here are some of the top offers relevant to you:'
: 'Relevant top offers:'}
</p>
{offerAnalysis.topPercentileOffers.map((topPercentileOffer) => (
{analysis.topPercentileOffers.map((topPercentileOffer) => (
<OfferProfileCard
key={topPercentileOffer.id}
offerProfile={topPercentileOffer}
@@ -77,7 +67,7 @@ function OfferAnalysisContent({
}
type OfferAnalysisProps = Readonly<{
allAnalysis?: ProfileAnalysis | null;
allAnalysis: ProfileAnalysis;
isError: boolean;
isLoading: boolean;
isSubmission?: boolean;
@@ -90,61 +80,55 @@ export default function OfferAnalysis({
isSubmission = false,
}: OfferAnalysisProps) {
const [tab, setTab] = useState(OVERALL_TAB);
const [analysis, setAnalysis] = useState<OfferAnalysisData | null>(null);
const [analysis, setAnalysis] = useState<AnalysisUnit>(
allAnalysis.overallAnalysis,
);
useEffect(() => {
if (tab === OVERALL_TAB) {
setAnalysis({
offer: allAnalysis?.overallHighestOffer,
offerAnalysis: allAnalysis?.overallAnalysis,
});
setAnalysis(allAnalysis.overallAnalysis);
} else {
setAnalysis({
offer: allAnalysis?.overallHighestOffer,
offerAnalysis: allAnalysis?.companyAnalysis[0],
});
setAnalysis(allAnalysis.companyAnalysis[parseInt(tab, 10)]);
}
}, [tab, allAnalysis]);
const tabOptions = [
const companyTabs = allAnalysis.companyAnalysis.map((value, index) => ({
label: value.companyName,
value: `${index}`,
}));
let tabOptions = [
{
label: OVERALL_TAB,
value: OVERALL_TAB,
},
{
label: allAnalysis?.overallHighestOffer.company.name || '',
value: allAnalysis?.overallHighestOffer.company.id || '',
},
];
tabOptions = tabOptions.concat(companyTabs);
return (
<>
<div>
{isError && (
<p className="m-10 text-center">
An error occurred while generating profile analysis.
</p>
)}
{isLoading && <Spinner className="m-10" display="block" size="lg" />}
{analysis && (
{!isError && !isLoading && (
<div>
{isError && (
<p className="m-10 text-center">
An error occurred while generating profile analysis.
</p>
)}
{!isError && !isLoading && (
<div>
<Tabs
label="Result Navigation"
tabs={tabOptions}
value={tab}
onChange={setTab}
/>
<HorizontalDivider className="mb-5" />
<OfferAnalysisContent
analysis={analysis}
isSubmission={isSubmission}
tab={tab}
/>
</div>
)}
<Tabs
label="Result Navigation"
tabs={tabOptions}
value={tab}
onChange={setTab}
/>
<HorizontalDivider className="mb-5" />
<OfferAnalysisContent
analysis={analysis}
isSubmission={isSubmission}
tab={tab}
/>
</div>
)}
</>
</div>
);
}

View File

@@ -1,18 +1,16 @@
import { OVERALL_TAB } from '../constants';
import type { Analysis } from '~/types/offers';
import type { AnalysisUnit } from '~/types/offers';
type OfferPercentileAnalysisTextProps = Readonly<{
companyName: string;
analysis: AnalysisUnit;
isSubmission: boolean;
offerAnalysis: Analysis;
tab: string;
}>;
export default function OfferPercentileAnalysisText({
tab,
companyName,
offerAnalysis: { noOfOffers, percentile },
analysis: { noOfOffers, percentile, companyName },
isSubmission,
}: OfferPercentileAnalysisTextProps) {
return tab === OVERALL_TAB ? (

View File

@@ -47,11 +47,13 @@ export default function OfferProfileCard({
</div>
<div className="col-span-10">
<p className="font-bold">{profileName}</p>
<div className="flex flex-row">
<BuildingOffice2Icon className="mr-2 h-5" />
<span className="mr-2 font-bold">Current:</span>
<span>{previousCompanies[0]}</span>
</div>
{previousCompanies.length > 0 && (
<div className="flex flex-row">
<BuildingOffice2Icon className="mr-2 h-5" />
<span className="mr-2 font-bold">Current:</span>
<span>{previousCompanies[0]}</span>
</div>
)}
<div className="flex flex-row">
<CalendarDaysIcon className="mr-2 h-5" />
<span className="mr-2 font-bold">YOE:</span>

View File

@@ -34,7 +34,7 @@ export default function OffersProfileSave({
},
onSuccess: () => {
showToast({
title: `Saved to your repository!`,
title: `Saved to your dashboard!`,
variant: 'success',
});
},
@@ -95,8 +95,8 @@ export default function OffersProfileSave({
</div>
<p className="mb-5 text-slate-900">
If you do not want to keep the edit link, you can opt to save this
profile under your account's respository. It will still only be
editable by you.
profile under your account's dashboard. It will still only be editable
by you.
</p>
<div className="mb-20">
<Button

View File

@@ -17,13 +17,17 @@ export default function OffersSubmissionAnalysis({
<h5 className="mb-8 text-center text-4xl font-bold text-slate-900">
Result
</h5>
<OfferAnalysis
key={3}
allAnalysis={analysis}
isError={isError}
isLoading={isLoading}
isSubmission={true}
/>
{!analysis && (
<p className="mb-8 text-center">Error generating analysis.</p>
)}
{analysis && (
<OfferAnalysis
key={3}
allAnalysis={analysis}
isError={isError}
isLoading={isLoading}
/>
)}
</div>
);
}

View File

@@ -115,7 +115,15 @@ function ProfileAnalysis({
return (
<div className="mx-8 my-4">
<OfferAnalysis allAnalysis={analysis} isError={false} isLoading={false} />
{!analysis ? (
<p>No analysis available.</p>
) : (
<OfferAnalysis
allAnalysis={analysis}
isError={false}
isLoading={false}
/>
)}
{isEditable && (
<div className="flex justify-end">
<Button

View File

@@ -41,7 +41,7 @@ export default function ProfileHeader({
setSelectedTab,
}: ProfileHeaderProps) {
const [isDialogOpen, setIsDialogOpen] = useState(false);
// Const [saved, setSaved] = useState(isSaved);
const [saved, setSaved] = useState(isSaved);
const router = useRouter();
const trpcContext = trpc.useContext();
const { offerProfileId = '', token = '' } = router.query;
@@ -60,7 +60,7 @@ export default function ProfileHeader({
});
},
onSuccess: () => {
// SetSaved(true);
setSaved(true);
showToast({
title: `Saved to dashboard!`,
variant: 'success',
@@ -79,7 +79,7 @@ export default function ProfileHeader({
});
},
onSuccess: () => {
// SetSaved(false);
setSaved(false);
showToast({
title: `Removed from dashboard!`,
variant: 'success',
@@ -90,7 +90,7 @@ export default function ProfileHeader({
);
const toggleSaved = () => {
if (isSaved) {
if (saved) {
unsaveMutation.mutate({ profileId: offerProfileId as string });
} else {
saveMutation.mutate({
@@ -111,10 +111,10 @@ export default function ProfileHeader({
disabled={
isLoading || saveMutation.isLoading || unsaveMutation.isLoading
}
icon={isSaved ? BookmarkIconSolid : BookmarkIconOutline}
icon={saved ? BookmarkIconSolid : BookmarkIconOutline}
isLabelHidden={true}
isLoading={saveMutation.isLoading || unsaveMutation.isLoading}
label={isSaved ? 'Remove from account' : 'Save to your account'}
label={saved ? 'Remove from account' : 'Save to your account'}
size="md"
variant="tertiary"
onClick={toggleSaved}

View File

@@ -1,6 +1,7 @@
import type {
Company,
OffersAnalysis,
OffersAnalysisUnit,
OffersBackground,
OffersCurrency,
OffersEducation,
@@ -18,9 +19,9 @@ import { TRPCError } from '@trpc/server';
import type {
AddToProfileResponse,
Analysis,
AnalysisHighestOffer,
AnalysisOffer,
AnalysisUnit,
Background,
CreateOfferProfileResponse,
DashboardOffer,
@@ -111,32 +112,33 @@ const analysisOfferDtoMapper = (
return analysisOfferDto;
};
const analysisDtoMapper = (
noOfOffers: number,
percentile: number,
topPercentileOffers: Array<
OffersOffer & {
company: Company;
offersFullTime:
| (OffersFullTime & { totalCompensation: OffersCurrency })
| null;
offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
profile: OffersProfile & {
background:
| (OffersBackground & {
experiences: Array<
OffersExperience & { company: Company | null }
>;
})
const analysisUnitDtoMapper = (
analysisUnit: OffersAnalysisUnit & {
topSimilarOffers: Array<
OffersOffer & {
company: Company;
offersFullTime:
| (OffersFullTime & { totalCompensation: OffersCurrency })
| null;
};
}
>,
offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
profile: OffersProfile & {
background:
| (OffersBackground & {
experiences: Array<
OffersExperience & { company: Company | null }
>;
})
| null;
};
}
>;
},
) => {
const analysisDto: Analysis = {
noOfOffers,
percentile,
topPercentileOffers: topPercentileOffers.map((offer) =>
const analysisDto: AnalysisUnit = {
companyName: analysisUnit.companyName,
noOfOffers: analysisUnit.noOfSimilarOffers,
percentile: analysisUnit.percentile,
topPercentileOffers: analysisUnit.topSimilarOffers.map((offer) =>
analysisOfferDtoMapper(offer),
),
};
@@ -166,6 +168,52 @@ const analysisHighestOfferDtoMapper = (
export const profileAnalysisDtoMapper = (
analysis:
| (OffersAnalysis & {
companyAnalysis: Array<
OffersAnalysisUnit & {
topSimilarOffers: Array<
OffersOffer & {
company: Company;
offersFullTime:
| (OffersFullTime & { totalCompensation: OffersCurrency })
| null;
offersIntern:
| (OffersIntern & { monthlySalary: OffersCurrency })
| null;
profile: OffersProfile & {
background:
| (OffersBackground & {
experiences: Array<
OffersExperience & { company: Company | null }
>;
})
| null;
};
}
>;
}
>;
overallAnalysis: OffersAnalysisUnit & {
topSimilarOffers: Array<
OffersOffer & {
company: Company;
offersFullTime:
| (OffersFullTime & { totalCompensation: OffersCurrency })
| null;
offersIntern:
| (OffersIntern & { monthlySalary: OffersCurrency })
| null;
profile: OffersProfile & {
background:
| (OffersBackground & {
experiences: Array<
OffersExperience & { company: Company | null }
>;
})
| null;
};
}
>;
};
overallHighestOffer: OffersOffer & {
company: Company;
offersFullTime:
@@ -176,46 +224,6 @@ export const profileAnalysisDtoMapper = (
| null;
profile: OffersProfile & { background: OffersBackground | null };
};
topCompanyOffers: Array<
OffersOffer & {
company: Company;
offersFullTime:
| (OffersFullTime & { totalCompensation: OffersCurrency })
| null;
offersIntern:
| (OffersIntern & { monthlySalary: OffersCurrency })
| null;
profile: OffersProfile & {
background:
| (OffersBackground & {
experiences: Array<
OffersExperience & { company: Company | null }
>;
})
| null;
};
}
>;
topOverallOffers: Array<
OffersOffer & {
company: Company;
offersFullTime:
| (OffersFullTime & { totalCompensation: OffersCurrency })
| null;
offersIntern:
| (OffersIntern & { monthlySalary: OffersCurrency })
| null;
profile: OffersProfile & {
background:
| (OffersBackground & {
experiences: Array<
OffersExperience & { company: Company | null }
>;
})
| null;
};
}
>;
})
| null,
) => {
@@ -224,23 +232,17 @@ export const profileAnalysisDtoMapper = (
}
const profileAnalysisDto: ProfileAnalysis = {
companyAnalysis: [
analysisDtoMapper(
analysis.noOfSimilarCompanyOffers,
analysis.companyPercentile,
analysis.topCompanyOffers,
),
],
id: analysis.id,
overallAnalysis: analysisDtoMapper(
analysis.noOfSimilarOffers,
analysis.overallPercentile,
analysis.topOverallOffers,
companyAnalysis: analysis.companyAnalysis.map((analysisUnit) =>
analysisUnitDtoMapper(analysisUnit),
),
createdAt: analysis.createdAt,
id: analysis.id,
overallAnalysis: analysisUnitDtoMapper(analysis.overallAnalysis),
overallHighestOffer: analysisHighestOfferDtoMapper(
analysis.overallHighestOffer,
),
profileId: analysis.profileId,
updatedAt: analysis.updatedAt,
};
return profileAnalysisDto;
};
@@ -442,6 +444,52 @@ export const profileDtoMapper = (
profile: OffersProfile & {
analysis:
| (OffersAnalysis & {
companyAnalysis: Array<
OffersAnalysisUnit & {
topSimilarOffers: Array<
OffersOffer & {
company: Company;
offersFullTime:
| (OffersFullTime & { totalCompensation: OffersCurrency })
| null;
offersIntern:
| (OffersIntern & { monthlySalary: OffersCurrency })
| null;
profile: OffersProfile & {
background:
| (OffersBackground & {
experiences: Array<
OffersExperience & { company: Company | null }
>;
})
| null;
};
}
>;
}
>;
overallAnalysis: OffersAnalysisUnit & {
topSimilarOffers: Array<
OffersOffer & {
company: Company;
offersFullTime:
| (OffersFullTime & { totalCompensation: OffersCurrency })
| null;
offersIntern:
| (OffersIntern & { monthlySalary: OffersCurrency })
| null;
profile: OffersProfile & {
background:
| (OffersBackground & {
experiences: Array<
OffersExperience & { company: Company | null }
>;
})
| null;
};
}
>;
};
overallHighestOffer: OffersOffer & {
company: Company;
offersFullTime:
@@ -452,46 +500,6 @@ export const profileDtoMapper = (
| null;
profile: OffersProfile & { background: OffersBackground | null };
};
topCompanyOffers: Array<
OffersOffer & {
company: Company;
offersFullTime:
| (OffersFullTime & { totalCompensation: OffersCurrency })
| null;
offersIntern:
| (OffersIntern & { monthlySalary: OffersCurrency })
| null;
profile: OffersProfile & {
background:
| (OffersBackground & {
experiences: Array<
OffersExperience & { company: Company | null }
>;
})
| null;
};
}
>;
topOverallOffers: Array<
OffersOffer & {
company: Company;
offersFullTime:
| (OffersFullTime & { totalCompensation: OffersCurrency })
| null;
offersIntern:
| (OffersIntern & { monthlySalary: OffersCurrency })
| null;
profile: OffersProfile & {
background:
| (OffersBackground & {
experiences: Array<
OffersExperience & { company: Company | null }
>;
})
| null;
};
}
>;
})
| null;
background:
@@ -528,7 +536,7 @@ export const profileDtoMapper = (
offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
}
>;
user: User | null;
users: Array<User>;
},
inputToken: string | undefined,
inputUserId: string | null | undefined,
@@ -548,18 +556,12 @@ export const profileDtoMapper = (
profileDto.editToken = profile.editToken ?? null;
profileDto.isEditable = true;
const users = profile.user;
const { users } = profile;
// TODO: BRYANN UNCOMMENT THIS ONCE U CHANGE THE SCHEMA
// for (let i = 0; i < users.length; i++) {
// if (users[i].id === inputUserId) {
// profileDto.isSaved = true
// }
// }
// TODO: REMOVE THIS ONCE U CHANGE THE SCHEMA
if (users?.id === inputUserId) {
profileDto.isSaved = true;
for (let i = 0; i < users.length; i++) {
if (users[i].id === inputUserId) {
profileDto.isSaved = true;
}
}
}

View File

@@ -71,11 +71,11 @@ export default function ProfilesDashboard() {
{!userProfilesQuery.isLoading && (
<div className="mt-8 overflow-y-auto">
<h1 className="mx-auto mb-4 w-3/4 text-start text-4xl font-bold text-slate-900">
Your repository
Your dashboard
</h1>
<p className="mx-auto w-3/4 text-start text-xl text-slate-900">
Save your offer profiles to respository to easily access and edit
them later.
Save your offer profiles to dashboard to easily access and edit them
later.
</p>
<div className="justfy-center mt-8 flex w-screen">
<ul className="mx-auto w-3/4 space-y-3" role="list">

View File

@@ -1,5 +1,6 @@
import Error from 'next/error';
import { useRouter } from 'next/router';
import { useSession } from 'next-auth/react';
import { useState } from 'react';
import { Spinner, useToast } from '@tih/ui';
@@ -34,11 +35,16 @@ export default function OfferProfile() {
ProfileDetailTab.OFFERS,
);
const [analysis, setAnalysis] = useState<ProfileAnalysis>();
const { data: session } = useSession();
const getProfileQuery = trpc.useQuery(
[
'offers.profile.listOne',
{ profileId: offerProfileId as string, token: token as string },
{
profileId: offerProfileId as string,
token: token as string,
userId: session?.user?.id,
},
],
{
enabled: typeof offerProfileId === 'string',

View File

@@ -14,20 +14,17 @@ import { profileAnalysisDtoMapper } from '~/mappers/offers-mappers';
import { createRouter } from '../context';
type Offer = OffersOffer & {
company: Company;
offersFullTime:
| (OffersFullTime & { totalCompensation: OffersCurrency })
| null;
offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
profile: OffersProfile & { background: OffersBackground | null };
};
const searchOfferPercentile = (
offer: OffersOffer & {
company: Company;
offersFullTime:
| (OffersFullTime & {
baseSalary: OffersCurrency | null;
bonus: OffersCurrency | null;
stocks: OffersCurrency | null;
totalCompensation: OffersCurrency;
})
| null;
offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
profile: OffersProfile & { background: OffersBackground | null };
},
offer: Offer,
similarOffers: Array<
OffersOffer & {
company: Company;
@@ -58,6 +55,70 @@ export const offersAnalysisRouter = createRouter()
async resolve({ ctx, input }) {
const analysis = await ctx.prisma.offersAnalysis.findFirst({
include: {
companyAnalysis: {
include: {
topSimilarOffers: {
include: {
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: {
include: {
experiences: {
include: {
company: true,
},
},
},
},
},
},
},
},
},
},
overallAnalysis: {
include: {
topSimilarOffers: {
include: {
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: {
include: {
experiences: {
include: {
company: true,
},
},
},
},
},
},
},
},
},
},
overallHighestOffer: {
include: {
company: true,
@@ -78,62 +139,6 @@ export const offersAnalysisRouter = createRouter()
},
},
},
topCompanyOffers: {
include: {
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: {
include: {
experiences: {
include: {
company: true,
},
},
},
},
},
},
},
},
topOverallOffers: {
include: {
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: {
include: {
experiences: {
include: {
company: true,
},
},
},
},
},
},
},
},
},
where: {
profileId: input.profileId,
@@ -310,11 +315,57 @@ export const offersAnalysisRouter = createRouter()
},
});
let similarCompanyOffers = similarOffers.filter(
(offer) => offer.companyId === overallHighestOffer.companyId,
// COMPANY ANALYSIS
const companyMap = new Map<string, Offer>();
offers.forEach((offer) => {
if (companyMap.get(offer.companyId) == null) {
companyMap.set(offer.companyId, offer);
}
});
const companyAnalysis = Array.from(companyMap.values()).map(
(companyOffer) => {
// TODO: Refactor calculating analysis into a function
let similarCompanyOffers = similarOffers.filter(
(offer) => offer.companyId === companyOffer.companyId,
);
const companyIndex = searchOfferPercentile(
companyOffer,
similarCompanyOffers,
);
const companyPercentile =
similarCompanyOffers.length <= 1
? 100
: 100 - (100 * companyIndex) / (similarCompanyOffers.length - 1);
// Get top offers (excluding user's offer)
similarCompanyOffers = similarCompanyOffers.filter(
(offer) => offer.id !== companyOffer.id,
);
const noOfSimilarCompanyOffers = similarCompanyOffers.length;
const similarCompanyOffers90PercentileIndex = Math.ceil(
noOfSimilarCompanyOffers * 0.1,
);
const topPercentileCompanyOffers =
noOfSimilarCompanyOffers > 2
? similarCompanyOffers.slice(
similarCompanyOffers90PercentileIndex,
similarCompanyOffers90PercentileIndex + 2,
)
: similarCompanyOffers;
return {
companyName: companyOffer.company.name,
noOfSimilarOffers: noOfSimilarCompanyOffers,
percentile: companyPercentile,
topSimilarOffers: topPercentileCompanyOffers,
};
},
);
// CALCULATE PERCENTILES
// OVERALL ANALYSIS
const overallIndex = searchOfferPercentile(
overallHighestOffer,
similarOffers,
@@ -324,23 +375,9 @@ export const offersAnalysisRouter = createRouter()
? 100
: 100 - (100 * overallIndex) / (similarOffers.length - 1);
const companyIndex = searchOfferPercentile(
overallHighestOffer,
similarCompanyOffers,
);
const companyPercentile =
similarCompanyOffers.length <= 1
? 100
: 100 - (100 * companyIndex) / (similarCompanyOffers.length - 1);
// FIND TOP >=90 PERCENTILE OFFERS, DOESN'T GIVE 100th PERCENTILE
// e.g. If there only 4 offers, it gives the 2nd and 3rd offer
similarOffers = similarOffers.filter(
(offer) => offer.id !== overallHighestOffer.id,
);
similarCompanyOffers = similarCompanyOffers.filter(
(offer) => offer.id !== overallHighestOffer.id,
);
const noOfSimilarOffers = similarOffers.length;
const similarOffers90PercentileIndex = Math.ceil(noOfSimilarOffers * 0.1);
@@ -352,46 +389,110 @@ export const offersAnalysisRouter = createRouter()
)
: similarOffers;
const noOfSimilarCompanyOffers = similarCompanyOffers.length;
const similarCompanyOffers90PercentileIndex = Math.ceil(
noOfSimilarCompanyOffers * 0.1,
);
const topPercentileCompanyOffers =
noOfSimilarCompanyOffers > 2
? similarCompanyOffers.slice(
similarCompanyOffers90PercentileIndex,
similarCompanyOffers90PercentileIndex + 2,
)
: similarCompanyOffers;
const analysis = await ctx.prisma.offersAnalysis.create({
data: {
companyPercentile,
noOfSimilarCompanyOffers,
noOfSimilarOffers,
companyAnalysis: {
create: companyAnalysis.map((analysisUnit) => {
return {
companyName: analysisUnit.companyName,
noOfSimilarOffers: analysisUnit.noOfSimilarOffers,
percentile: analysisUnit.percentile,
topSimilarOffers: {
connect: analysisUnit.topSimilarOffers.map((offer) => {
return { id: offer.id };
}),
},
};
}),
},
overallAnalysis: {
create: {
companyName: overallHighestOffer.company.name,
noOfSimilarOffers,
percentile: overallPercentile,
topSimilarOffers: {
connect: topPercentileOffers.map((offer) => {
return { id: offer.id };
}),
},
},
},
overallHighestOffer: {
connect: {
id: overallHighestOffer.id,
},
},
overallPercentile,
profile: {
connect: {
id: input.profileId,
},
},
topCompanyOffers: {
connect: topPercentileCompanyOffers.map((offer) => {
return { id: offer.id };
}),
},
topOverallOffers: {
connect: topPercentileOffers.map((offer) => {
return { id: offer.id };
}),
},
},
include: {
companyAnalysis: {
include: {
topSimilarOffers: {
include: {
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: {
include: {
experiences: {
include: {
company: true,
},
},
},
},
},
},
},
},
},
},
overallAnalysis: {
include: {
topSimilarOffers: {
include: {
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: {
include: {
experiences: {
include: {
company: true,
},
},
},
},
},
},
},
},
},
},
overallHighestOffer: {
include: {
company: true,
@@ -412,62 +513,6 @@ export const offersAnalysisRouter = createRouter()
},
},
},
topCompanyOffers: {
include: {
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: {
include: {
experiences: {
include: {
company: true,
},
},
},
},
},
},
},
},
topOverallOffers: {
include: {
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: {
include: {
experiences: {
include: {
company: true,
},
},
},
},
},
},
},
},
},
});

View File

@@ -128,6 +128,70 @@ export const offersProfileRouter = createRouter()
include: {
analysis: {
include: {
companyAnalysis: {
include: {
topSimilarOffers: {
include: {
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: {
include: {
experiences: {
include: {
company: true,
},
},
},
},
},
},
},
},
},
},
overallAnalysis: {
include: {
topSimilarOffers: {
include: {
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: {
include: {
experiences: {
include: {
company: true,
},
},
},
},
},
},
},
},
},
},
overallHighestOffer: {
include: {
company: true,
@@ -148,62 +212,6 @@ export const offersProfileRouter = createRouter()
},
},
},
topCompanyOffers: {
include: {
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: {
include: {
experiences: {
include: {
company: true,
},
},
},
},
},
},
},
},
topOverallOffers: {
include: {
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: {
include: {
experiences: {
include: {
company: true,
},
},
},
},
},
},
},
},
},
},
background: {
@@ -244,7 +252,7 @@ export const offersProfileRouter = createRouter()
},
},
},
user: true,
users: true,
},
where: {
id: input.profileId,
@@ -409,7 +417,7 @@ export const offersProfileRouter = createRouter()
message: 'Missing fields in background experiences.',
});
}),
)
),
},
specificYoes: {
create: input.background.specificYoes.map((x) => {

View File

@@ -3,129 +3,130 @@ import * as trpc from '@trpc/server';
import { TRPCError } from '@trpc/server';
import {
addToProfileResponseMapper, getUserProfileResponseMapper,
addToProfileResponseMapper,
getUserProfileResponseMapper,
} from '~/mappers/offers-mappers';
import { createProtectedRouter } from '../context';
export const offersUserProfileRouter = createProtectedRouter()
.mutation('addToUserProfile', {
input: z.object({
profileId: z.string(),
token: z.string(),
}),
async resolve({ ctx, input }) {
const profile = await ctx.prisma.offersProfile.findFirst({
where: {
id: input.profileId,
},
});
const profileEditToken = profile?.editToken;
if (profileEditToken === input.token) {
const userId = ctx.session.user.id
const updated = await ctx.prisma.offersProfile.update({
data: {
user: {
connect: {
id: userId,
},
},
},
where: {
id: input.profileId,
},
});
return addToProfileResponseMapper(updated);
}
throw new trpc.TRPCError({
code: 'UNAUTHORIZED',
message: 'Invalid token.',
});
.mutation('addToUserProfile', {
input: z.object({
profileId: z.string(),
token: z.string(),
}),
async resolve({ ctx, input }) {
const profile = await ctx.prisma.offersProfile.findFirst({
where: {
id: input.profileId,
},
})
.query('getUserProfiles', {
async resolve({ ctx }) {
const userId = ctx.session.user.id
const result = await ctx.prisma.user.findFirst({
});
const profileEditToken = profile?.editToken;
if (profileEditToken === input.token) {
const userId = ctx.session.user.id;
const updated = await ctx.prisma.offersProfile.update({
data: {
users: {
connect: {
id: userId,
},
},
},
where: {
id: input.profileId,
},
});
return addToProfileResponseMapper(updated);
}
throw new trpc.TRPCError({
code: 'UNAUTHORIZED',
message: 'Invalid token.',
});
},
})
.query('getUserProfiles', {
async resolve({ ctx }) {
const userId = ctx.session.user.id;
const result = await ctx.prisma.user.findFirst({
include: {
OffersProfile: {
include: {
offers: {
include: {
OffersProfile: {
include: {
offers: {
include: {
company: true,
offersFullTime: {
include: {
totalCompensation: true
}
},
offersIntern: {
include: {
monthlySalary: true
}
}
}
}
}
}
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
},
where: {
id: userId
}
})
},
},
},
},
where: {
id: userId,
},
});
return getUserProfileResponseMapper(result)
return getUserProfileResponseMapper(result);
},
})
.mutation('removeFromUserProfile', {
input: z.object({
profileId: z.string(),
}),
async resolve({ ctx, input }) {
const userId = ctx.session.user.id;
const profiles = await ctx.prisma.user.findFirst({
include: {
OffersProfile: true,
},
where: {
id: userId,
},
});
// Validation
let doesProfileExist = false;
if (profiles?.OffersProfile) {
for (let i = 0; i < profiles.OffersProfile.length; i++) {
if (profiles.OffersProfile[i].id === input.profileId) {
doesProfileExist = true;
}
}
})
.mutation('removeFromUserProfile', {
input: z.object({
profileId: z.string(),
}),
async resolve({ ctx, input }) {
const userId = ctx.session.user.id
}
const profiles = await ctx.prisma.user.findFirst({
include: {
OffersProfile: true
},
where: {
id: userId
}
})
if (!doesProfileExist) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'No such profile id saved.',
});
}
// Validation
let doesProfileExist = false;
if (profiles?.OffersProfile) {
for (let i = 0; i < profiles.OffersProfile.length; i++) {
if (profiles.OffersProfile[i].id === input.profileId) {
doesProfileExist = true
}
}
}
if (!doesProfileExist) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'No such profile id saved.'
})
}
await ctx.prisma.user.update({
data: {
OffersProfile: {
disconnect: [{
id: input.profileId
}]
}
},
where: {
id: userId
}
})
}
})
await ctx.prisma.user.update({
data: {
OffersProfile: {
disconnect: [
{
id: input.profileId,
},
],
},
},
where: {
id: userId,
},
});
},
});

View File

@@ -143,14 +143,17 @@ export type OffersDiscussion = {
};
export type ProfileAnalysis = {
companyAnalysis: Array<Analysis>;
companyAnalysis: Array<AnalysisUnit>;
createdAt: Date;
id: string;
overallAnalysis: Analysis;
overallAnalysis: AnalysisUnit;
overallHighestOffer: AnalysisHighestOffer;
profileId: string;
updatedAt: Date;
};
export type Analysis = {
export type AnalysisUnit = {
companyName: string;
noOfOffers: number;
percentile: number;
topPercentileOffers: Array<AnalysisOffer>;