mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2025-07-15 02:33:50 +08:00
[offers][feat] Add multiple company analysis
This commit is contained in:

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
|
||||
noOfSimilarOffers Int
|
||||
topOverallOffers OffersOffer[] @relation("TopOverallOffers")
|
||||
overallAnalysis OffersAnalysisUnit @relation("OverallAnalysis", fields: [overallAnalysisUnitId], references: [id])
|
||||
overallAnalysisUnitId String
|
||||
|
||||
// Company
|
||||
companyPercentile Float
|
||||
noOfSimilarCompanyOffers Int
|
||||
topCompanyOffers OffersOffer[] @relation("TopCompanyOffers")
|
||||
companyAnalysis OffersAnalysisUnit[] @relation("CompanyAnalysis")
|
||||
}
|
||||
|
||||
model OffersAnalysisUnit {
|
||||
id String @id @default(cuid())
|
||||
|
||||
companyName String
|
||||
percentile Float
|
||||
noOfSimilarOffers Int
|
||||
topSimilarOffers OffersOffer[]
|
||||
|
||||
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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -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) => {
|
||||
|
@ -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,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
9
apps/portal/src/types/offers.d.ts
vendored
9
apps/portal/src/types/offers.d.ts
vendored
@ -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>;
|
||||
|
Reference in New Issue
Block a user