parent
0f349dfc9d
commit
d5b36038cd
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "OffersBackground" ALTER COLUMN "totalYoe" SET DEFAULT 0;
|
@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { trpc } from '~/utils/trpc';
|
||||||
|
|
||||||
|
function GenerateAnalysis() {
|
||||||
|
const analysisMutation = trpc.useMutation(['offers.analysis.generate']);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{JSON.stringify(
|
||||||
|
analysisMutation.mutate({ profileId: 'cl9luzsqh0005utr2d7jpjabt' }),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GenerateAnalysis;
|
@ -0,0 +1,53 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { trpc } from '~/utils/trpc';
|
||||||
|
|
||||||
|
function Test() {
|
||||||
|
const data = trpc.useQuery([
|
||||||
|
'offers.list',
|
||||||
|
{
|
||||||
|
currency: 'SGD',
|
||||||
|
limit: 100,
|
||||||
|
location: 'Singapore, Singapore',
|
||||||
|
offset: 0,
|
||||||
|
sortBy: '-totalCompensation',
|
||||||
|
yoeCategory: 1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const deleteMutation = trpc.useMutation(['offers.profile.delete']);
|
||||||
|
|
||||||
|
const handleDelete = (id: string) => {
|
||||||
|
deleteMutation.mutate({ profileId: id, token: ' dadaadad' });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<b>{JSON.stringify(data.data?.paging)}</b>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<ul>
|
||||||
|
{data.data?.data.map((offer) => {
|
||||||
|
return (
|
||||||
|
<li key={offer.id}>
|
||||||
|
<button
|
||||||
|
className="text-danger-600"
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
handleDelete(offer.profileId);
|
||||||
|
}}>
|
||||||
|
DELETE THIS PROFILE AND ALL ITS OFFERS
|
||||||
|
</button>
|
||||||
|
<div>{JSON.stringify(offer)}</div>
|
||||||
|
<br />
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Test;
|
@ -0,0 +1,383 @@
|
|||||||
|
import type { Session } from 'next-auth';
|
||||||
|
import type {
|
||||||
|
Company,
|
||||||
|
OffersBackground,
|
||||||
|
OffersCurrency,
|
||||||
|
OffersFullTime,
|
||||||
|
OffersIntern,
|
||||||
|
OffersOffer,
|
||||||
|
OffersProfile,
|
||||||
|
Prisma,
|
||||||
|
PrismaClient,
|
||||||
|
} from '@prisma/client';
|
||||||
|
import { TRPCError } from '@trpc/server';
|
||||||
|
|
||||||
|
import { profileAnalysisDtoMapper } from '../../mappers/offers-mappers';
|
||||||
|
|
||||||
|
const searchOfferPercentile = (
|
||||||
|
offer: OffersOffer & {
|
||||||
|
company: Company;
|
||||||
|
offersFullTime:
|
||||||
|
| (OffersFullTime & {
|
||||||
|
baseSalary: OffersCurrency;
|
||||||
|
bonus: OffersCurrency;
|
||||||
|
stocks: OffersCurrency;
|
||||||
|
totalCompensation: OffersCurrency;
|
||||||
|
})
|
||||||
|
| null;
|
||||||
|
offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
|
||||||
|
profile: OffersProfile & { background: OffersBackground | null };
|
||||||
|
},
|
||||||
|
similarOffers: Array<
|
||||||
|
OffersOffer & {
|
||||||
|
company: Company;
|
||||||
|
offersFullTime:
|
||||||
|
| (OffersFullTime & {
|
||||||
|
totalCompensation: OffersCurrency;
|
||||||
|
})
|
||||||
|
| null;
|
||||||
|
offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
|
||||||
|
profile: OffersProfile & { background: OffersBackground | null };
|
||||||
|
}
|
||||||
|
>,
|
||||||
|
) => {
|
||||||
|
for (let i = 0; i < similarOffers.length; i++) {
|
||||||
|
if (similarOffers[i].id === offer.id) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateAnalysis = async (params: {
|
||||||
|
ctx: {
|
||||||
|
prisma: PrismaClient<
|
||||||
|
Prisma.PrismaClientOptions,
|
||||||
|
never,
|
||||||
|
Prisma.RejectOnNotFound | Prisma.RejectPerOperation | undefined
|
||||||
|
>;
|
||||||
|
session: Session | null;
|
||||||
|
};
|
||||||
|
input: { profileId: string };
|
||||||
|
}) => {
|
||||||
|
const { ctx, input } = params;
|
||||||
|
await ctx.prisma.offersAnalysis.deleteMany({
|
||||||
|
where: {
|
||||||
|
profileId: input.profileId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const offers = await ctx.prisma.offersOffer.findMany({
|
||||||
|
include: {
|
||||||
|
company: true,
|
||||||
|
offersFullTime: {
|
||||||
|
include: {
|
||||||
|
baseSalary: true,
|
||||||
|
bonus: true,
|
||||||
|
stocks: true,
|
||||||
|
totalCompensation: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
offersIntern: {
|
||||||
|
include: {
|
||||||
|
monthlySalary: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
profile: {
|
||||||
|
include: {
|
||||||
|
background: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: [
|
||||||
|
{
|
||||||
|
offersFullTime: {
|
||||||
|
totalCompensation: {
|
||||||
|
baseValue: 'desc',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
offersIntern: {
|
||||||
|
monthlySalary: {
|
||||||
|
baseValue: 'desc',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
where: {
|
||||||
|
profileId: input.profileId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!offers || offers.length === 0) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'NOT_FOUND',
|
||||||
|
message: 'No offers found on this profile',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const overallHighestOffer = offers[0];
|
||||||
|
|
||||||
|
// TODO: Shift yoe out of background to make it mandatory
|
||||||
|
if (
|
||||||
|
!overallHighestOffer.profile.background ||
|
||||||
|
overallHighestOffer.profile.background.totalYoe == null
|
||||||
|
) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'NOT_FOUND',
|
||||||
|
message: 'YOE not found',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const yoe = overallHighestOffer.profile.background.totalYoe as number;
|
||||||
|
const monthYearReceived = new Date(overallHighestOffer.monthYearReceived);
|
||||||
|
monthYearReceived.setFullYear(monthYearReceived.getFullYear() - 1);
|
||||||
|
|
||||||
|
let similarOffers = await ctx.prisma.offersOffer.findMany({
|
||||||
|
include: {
|
||||||
|
company: true,
|
||||||
|
offersFullTime: {
|
||||||
|
include: {
|
||||||
|
totalCompensation: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
offersIntern: {
|
||||||
|
include: {
|
||||||
|
monthlySalary: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
profile: {
|
||||||
|
include: {
|
||||||
|
background: {
|
||||||
|
include: {
|
||||||
|
experiences: {
|
||||||
|
include: {
|
||||||
|
company: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: [
|
||||||
|
{
|
||||||
|
offersFullTime: {
|
||||||
|
totalCompensation: {
|
||||||
|
baseValue: 'desc',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
offersIntern: {
|
||||||
|
monthlySalary: {
|
||||||
|
baseValue: 'desc',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
where: {
|
||||||
|
AND: [
|
||||||
|
{
|
||||||
|
location: overallHighestOffer.location,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
monthYearReceived: {
|
||||||
|
gte: monthYearReceived,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
offersFullTime: {
|
||||||
|
title: overallHighestOffer.offersFullTime?.title,
|
||||||
|
},
|
||||||
|
offersIntern: {
|
||||||
|
title: overallHighestOffer.offersIntern?.title,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
profile: {
|
||||||
|
background: {
|
||||||
|
AND: [
|
||||||
|
{
|
||||||
|
totalYoe: {
|
||||||
|
gte: Math.max(yoe - 1, 0),
|
||||||
|
lte: yoe + 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let similarCompanyOffers = similarOffers.filter(
|
||||||
|
(offer) => offer.companyId === overallHighestOffer.companyId,
|
||||||
|
);
|
||||||
|
|
||||||
|
// CALCULATE PERCENTILES
|
||||||
|
const overallIndex = searchOfferPercentile(
|
||||||
|
overallHighestOffer,
|
||||||
|
similarOffers,
|
||||||
|
);
|
||||||
|
const overallPercentile =
|
||||||
|
similarOffers.length === 0
|
||||||
|
? 100
|
||||||
|
: (100 * overallIndex) / similarOffers.length;
|
||||||
|
|
||||||
|
const companyIndex = searchOfferPercentile(
|
||||||
|
overallHighestOffer,
|
||||||
|
similarCompanyOffers,
|
||||||
|
);
|
||||||
|
const companyPercentile =
|
||||||
|
similarCompanyOffers.length === 0
|
||||||
|
? 100
|
||||||
|
: (100 * companyIndex) / similarCompanyOffers.length;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
const topPercentileOffers =
|
||||||
|
noOfSimilarOffers > 2
|
||||||
|
? similarOffers.slice(
|
||||||
|
similarOffers90PercentileIndex,
|
||||||
|
similarOffers90PercentileIndex + 2,
|
||||||
|
)
|
||||||
|
: 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,
|
||||||
|
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: {
|
||||||
|
include: {
|
||||||
|
company: true,
|
||||||
|
offersFullTime: {
|
||||||
|
include: {
|
||||||
|
totalCompensation: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
offersIntern: {
|
||||||
|
include: {
|
||||||
|
monthlySalary: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
profile: {
|
||||||
|
include: {
|
||||||
|
background: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return profileAnalysisDtoMapper(analysis);
|
||||||
|
};
|
Loading…
Reference in new issue