[offers][feat] Add multiple company analysis

pull/473/head
Bryann Yeap Kok Keong 2 years ago committed by Bryann Yeap Kok Keong
parent 68f3c72945
commit 91696571fe

@ -0,0 +1,113 @@
/*
Warnings:
- You are about to drop the column `companyPercentile` on the `OffersAnalysis` table. All the data in the column will be lost.
- You are about to drop the column `noOfSimilarCompanyOffers` on the `OffersAnalysis` table. All the data in the column will be lost.
- You are about to drop the column `noOfSimilarOffers` on the `OffersAnalysis` table. All the data in the column will be lost.
- You are about to drop the column `overallPercentile` on the `OffersAnalysis` table. All the data in the column will be lost.
- You are about to drop the column `userId` on the `OffersProfile` table. All the data in the column will be lost.
- You are about to drop the `_TopCompanyOffers` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `_TopOverallOffers` table. If the table is not empty, all the data it contains will be lost.
- Added the required column `overallAnalysisUnitId` to the `OffersAnalysis` table without a default value. This is not possible if the table is not empty.
- Added the required column `updatedAt` to the `OffersAnalysis` table without a default value. This is not possible if the table is not empty.
*/
-- DropForeignKey
ALTER TABLE "OffersProfile" DROP CONSTRAINT "OffersProfile_userId_fkey";
-- DropForeignKey
ALTER TABLE "_TopCompanyOffers" DROP CONSTRAINT "_TopCompanyOffers_A_fkey";
-- DropForeignKey
ALTER TABLE "_TopCompanyOffers" DROP CONSTRAINT "_TopCompanyOffers_B_fkey";
-- DropForeignKey
ALTER TABLE "_TopOverallOffers" DROP CONSTRAINT "_TopOverallOffers_A_fkey";
-- DropForeignKey
ALTER TABLE "_TopOverallOffers" DROP CONSTRAINT "_TopOverallOffers_B_fkey";
-- AlterTable
ALTER TABLE "OffersAnalysis" DROP COLUMN "companyPercentile",
DROP COLUMN "noOfSimilarCompanyOffers",
DROP COLUMN "noOfSimilarOffers",
DROP COLUMN "overallPercentile",
ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "overallAnalysisUnitId" TEXT NOT NULL,
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL;
-- AlterTable
ALTER TABLE "OffersProfile" DROP COLUMN "userId";
-- DropTable
DROP TABLE "_TopCompanyOffers";
-- DropTable
DROP TABLE "_TopOverallOffers";
-- CreateTable
CREATE TABLE "OffersAnalysisUnit" (
"id" TEXT NOT NULL,
"companyName" TEXT NOT NULL,
"percentile" DOUBLE PRECISION NOT NULL,
"noOfSimilarOffers" INTEGER NOT NULL,
CONSTRAINT "OffersAnalysisUnit_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "_OffersProfileToUser" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL
);
-- CreateTable
CREATE TABLE "_CompanyAnalysis" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL
);
-- CreateTable
CREATE TABLE "_OffersAnalysisUnitToOffersOffer" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "_OffersProfileToUser_AB_unique" ON "_OffersProfileToUser"("A", "B");
-- CreateIndex
CREATE INDEX "_OffersProfileToUser_B_index" ON "_OffersProfileToUser"("B");
-- CreateIndex
CREATE UNIQUE INDEX "_CompanyAnalysis_AB_unique" ON "_CompanyAnalysis"("A", "B");
-- CreateIndex
CREATE INDEX "_CompanyAnalysis_B_index" ON "_CompanyAnalysis"("B");
-- CreateIndex
CREATE UNIQUE INDEX "_OffersAnalysisUnitToOffersOffer_AB_unique" ON "_OffersAnalysisUnitToOffersOffer"("A", "B");
-- CreateIndex
CREATE INDEX "_OffersAnalysisUnitToOffersOffer_B_index" ON "_OffersAnalysisUnitToOffersOffer"("B");
-- AddForeignKey
ALTER TABLE "OffersAnalysis" ADD CONSTRAINT "OffersAnalysis_overallAnalysisUnitId_fkey" FOREIGN KEY ("overallAnalysisUnitId") REFERENCES "OffersAnalysisUnit"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_OffersProfileToUser" ADD CONSTRAINT "_OffersProfileToUser_A_fkey" FOREIGN KEY ("A") REFERENCES "OffersProfile"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_OffersProfileToUser" ADD CONSTRAINT "_OffersProfileToUser_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_CompanyAnalysis" ADD CONSTRAINT "_CompanyAnalysis_A_fkey" FOREIGN KEY ("A") REFERENCES "OffersAnalysis"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_CompanyAnalysis" ADD CONSTRAINT "_CompanyAnalysis_B_fkey" FOREIGN KEY ("B") REFERENCES "OffersAnalysisUnit"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_OffersAnalysisUnitToOffersOffer" ADD CONSTRAINT "_OffersAnalysisUnitToOffersOffer_A_fkey" FOREIGN KEY ("A") REFERENCES "OffersAnalysisUnit"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_OffersAnalysisUnitToOffersOffer" ADD CONSTRAINT "_OffersAnalysisUnitToOffersOffer_B_fkey" FOREIGN KEY ("B") REFERENCES "OffersOffer"("id") ON DELETE CASCADE ON UPDATE CASCADE;

@ -225,8 +225,7 @@ model OffersProfile {
offers OffersOffer[]
user User? @relation(fields: [userId], references: [id])
userId String?
users User[]
analysis OffersAnalysis?
}
@ -362,9 +361,8 @@ model OffersOffer {
offersFullTime OffersFullTime? @relation(fields: [offersFullTimeId], references: [id], onDelete: Cascade)
offersFullTimeId String? @unique
OffersAnalysis OffersAnalysis? @relation("HighestOverallOffer")
OffersAnalysisTopOverallOffers OffersAnalysis[] @relation("TopOverallOffers")
OffersAnalysisTopCompanyOffers OffersAnalysis[] @relation("TopCompanyOffers")
offersAnalysis OffersAnalysis? @relation("HighestOverallOffer")
offersAnalysisUnit OffersAnalysisUnit[]
}
model OffersIntern {
@ -396,7 +394,9 @@ model OffersFullTime {
}
model OffersAnalysis {
id String @id @default(cuid())
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
profile OffersProfile @relation(fields: [profileId], references: [id], onDelete: Cascade)
profileId String @unique
@ -405,14 +405,22 @@ model OffersAnalysis {
offerId String @unique
// OVERALL
overallPercentile Float
overallAnalysis OffersAnalysisUnit @relation("OverallAnalysis", fields: [overallAnalysisUnitId], references: [id])
overallAnalysisUnitId String
companyAnalysis OffersAnalysisUnit[] @relation("CompanyAnalysis")
}
model OffersAnalysisUnit {
id String @id @default(cuid())
companyName String
percentile Float
noOfSimilarOffers Int
topOverallOffers OffersOffer[] @relation("TopOverallOffers")
topSimilarOffers OffersOffer[]
// Company
companyPercentile Float
noOfSimilarCompanyOffers Int
topCompanyOffers OffersOffer[] @relation("TopCompanyOffers")
offersAnalysisOverall OffersAnalysis[] @relation("OverallAnalysis")
offersAnalysisCompany OffersAnalysis[] @relation("CompanyAnalysis")
}
// End of Offers project models.

@ -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' },
];

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

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

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

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

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

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

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

@ -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;
// 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
// }
// }
const { users } = profile;
// 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;
}
}
}

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

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

@ -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,46 +55,62 @@ export const offersAnalysisRouter = createRouter()
async resolve({ ctx, input }) {
const analysis = await ctx.prisma.offersAnalysis.findFirst({
include: {
overallHighestOffer: {
companyAnalysis: {
include: {
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
topSimilarOffers: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: true,
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: {
include: {
experiences: {
include: {
company: true,
},
},
},
},
},
},
},
},
},
},
topCompanyOffers: {
overallAnalysis: {
include: {
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
topSimilarOffers: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: {
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: {
experiences: {
monthlySalary: true,
},
},
profile: {
include: {
background: {
include: {
company: true,
experiences: {
include: {
company: true,
},
},
},
},
},
@ -106,7 +119,7 @@ export const offersAnalysisRouter = createRouter()
},
},
},
topOverallOffers: {
overallHighestOffer: {
include: {
company: true,
offersFullTime: {
@ -121,15 +134,7 @@ export const offersAnalysisRouter = createRouter()
},
profile: {
include: {
background: {
include: {
experiences: {
include: {
company: true,
},
},
},
},
background: true,
},
},
},
@ -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,86 +389,102 @@ 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: {
overallHighestOffer: {
companyAnalysis: {
include: {
company: true,
offersFullTime: {
topSimilarOffers: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: true,
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: {
include: {
experiences: {
include: {
company: true,
},
},
},
},
},
},
},
},
},
},
topCompanyOffers: {
overallAnalysis: {
include: {
company: true,
offersFullTime: {
topSimilarOffers: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: {
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
experiences: {
background: {
include: {
company: true,
experiences: {
include: {
company: true,
},
},
},
},
},
@ -440,7 +493,7 @@ export const offersAnalysisRouter = createRouter()
},
},
},
topOverallOffers: {
overallHighestOffer: {
include: {
company: true,
offersFullTime: {
@ -455,15 +508,7 @@ export const offersAnalysisRouter = createRouter()
},
profile: {
include: {
background: {
include: {
experiences: {
include: {
company: true,
},
},
},
},
background: true,
},
},
},

@ -128,46 +128,62 @@ export const offersProfileRouter = createRouter()
include: {
analysis: {
include: {
overallHighestOffer: {
companyAnalysis: {
include: {
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
topSimilarOffers: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: true,
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: {
include: {
experiences: {
include: {
company: true,
},
},
},
},
},
},
},
},
},
},
topCompanyOffers: {
overallAnalysis: {
include: {
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
topSimilarOffers: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: {
company: true,
offersFullTime: {
include: {
experiences: {
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: {
include: {
company: true,
experiences: {
include: {
company: true,
},
},
},
},
},
@ -176,7 +192,7 @@ export const offersProfileRouter = createRouter()
},
},
},
topOverallOffers: {
overallHighestOffer: {
include: {
company: true,
offersFullTime: {
@ -191,15 +207,7 @@ export const offersProfileRouter = createRouter()
},
profile: {
include: {
background: {
include: {
experiences: {
include: {
company: true,
},
},
},
},
background: true,
},
},
},
@ -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) => {

@ -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,
},
});
.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 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,
},
});
const userId = ctx.session.user.id
const updated = await ctx.prisma.offersProfile.update({
data: {
user: {
connect: {
id: userId,
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: {
company: true,
offersFullTime: {
include: {
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
},
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: {
company: true,
offersFullTime: {
include: {
totalCompensation: true
}
},
offersIntern: {
include: {
monthlySalary: true
}
}
}
}
}
}
},
where: {
id: userId
}
})
return getUserProfileResponseMapper(result)
}
})
.mutation('removeFromUserProfile', {
input: z.object({
profileId: z.string(),
}),
async resolve({ ctx, input }) {
const userId = ctx.session.user.id
where: {
id: userId,
},
});
const profiles = await ctx.prisma.user.findFirst({
include: {
OffersProfile: true
},
where: {
id: userId
}
})
return getUserProfileResponseMapper(result);
},
})
.mutation('removeFromUserProfile', {
input: z.object({
profileId: z.string(),
}),
async resolve({ ctx, input }) {
const userId = ctx.session.user.id;
// Validation
let doesProfileExist = false;
const profiles = await ctx.prisma.user.findFirst({
include: {
OffersProfile: true,
},
where: {
id: userId,
},
});
if (profiles?.OffersProfile) {
for (let i = 0; i < profiles.OffersProfile.length; i++) {
if (profiles.OffersProfile[i].id === input.profileId) {
doesProfileExist = true
}
}
}
// Validation
let doesProfileExist = false;
if (!doesProfileExist) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'No such profile id saved.'
})
}
if (profiles?.OffersProfile) {
for (let i = 0; i < profiles.OffersProfile.length; i++) {
if (profiles.OffersProfile[i].id === input.profileId) {
doesProfileExist = true;
}
}
}
await ctx.prisma.user.update({
data: {
OffersProfile: {
disconnect: [{
id: input.profileId
}]
}
},
where: {
id: userId
}
})
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,
},
});
},
});

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

Loading…
Cancel
Save