[offers][feat] Add admin dashboard offers api

pull/544/head
Bryann Yeap Kok Keong 2 years ago
parent 55c23f6c0c
commit 93d5c4cace

@ -22,6 +22,7 @@ import { TRPCError } from '@trpc/server';
import type {
AddToProfileResponse,
AdminDashboardOffer,
AnalysisHighestOffer,
AnalysisOffer,
AnalysisUnit,
@ -30,6 +31,7 @@ import type {
DashboardOffer,
Education,
Experience,
GetAdminOffersResponse,
GetOffersResponse,
Location,
OffersCompany,
@ -942,3 +944,86 @@ const userProfileOfferDtoMapper = (
return mappedOffer;
};
export const adminDashboardOfferDtoMapper = (
offer: OffersOffer & {
company: Company;
location: City & { state: State & { country: Country } };
offersFullTime:
| (OffersFullTime & {
baseSalary: OffersCurrency | null;
bonus: OffersCurrency | null;
stocks: OffersCurrency | null;
totalCompensation: OffersCurrency;
})
| null;
offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
profile: OffersProfile & {
background: OffersBackground | null;
offers: Array<OffersOffer>;
};
},
) => {
const adminDashboardOfferDto: AdminDashboardOffer = {
company: offersCompanyDtoMapper(offer.company),
id: offer.id,
income: valuationDtoMapper({
baseCurrency: '',
baseValue: -1,
currency: '',
id: '',
value: -1,
}),
location: locationDtoMapper(offer.location),
monthYearReceived: offer.monthYearReceived,
numberOfOtherOffers:
offer.profile.offers.length < 2 ? 0 : offer.profile.offers.length - 1,
profileId: offer.profileId,
title: offer.offersFullTime?.title || offer.offersIntern?.title || '',
token: offer.profile.editToken,
totalYoe: offer.profile.background?.totalYoe ?? -1,
};
if (offer.offersFullTime && offer.jobType === JobType.FULLTIME) {
adminDashboardOfferDto.income = valuationDtoMapper(
offer.offersFullTime.totalCompensation,
);
if (offer.offersFullTime.baseSalary) {
adminDashboardOfferDto.baseSalary = valuationDtoMapper(
offer.offersFullTime.baseSalary,
);
}
if (offer.offersFullTime.bonus) {
adminDashboardOfferDto.bonus = valuationDtoMapper(
offer.offersFullTime.bonus,
);
}
if (offer.offersFullTime.stocks) {
adminDashboardOfferDto.stocks = valuationDtoMapper(
offer.offersFullTime.stocks,
);
}
} else if (offer.offersIntern && offer.jobType === JobType.INTERN) {
adminDashboardOfferDto.income = valuationDtoMapper(
offer.offersIntern.monthlySalary,
);
}
return adminDashboardOfferDto;
};
export const getAdminOffersResponseMapper = (
data: Array<AdminDashboardOffer>,
paging: Paging,
jobType: JobType,
) => {
const getAdminOffersResponse: GetAdminOffersResponse = {
data,
jobType,
paging,
};
return getAdminOffersResponse;
};

@ -4,6 +4,7 @@ import { companiesRouter } from './companies-router';
import { createRouter } from './context';
import { locationsRouter } from './locations-router';
import { offersRouter } from './offers/offers';
import { offerAdminRouter } from './offers/offers-admin-router';
import { offersAnalysisRouter } from './offers/offers-analysis-router';
import { offersCommentsRouter } from './offers/offers-comments-router';
import { offersProfileRouter } from './offers/offers-profile-router';
@ -70,7 +71,8 @@ export const appRouter = createRouter()
.merge('offers.profile.', offersProfileRouter)
.merge('offers.analysis.', offersAnalysisRouter)
.merge('offers.comments.', offersCommentsRouter)
.merge('offers.user.profile.', offersUserProfileRouter);
.merge('offers.user.profile.', offersUserProfileRouter)
.merge('offers.admin.', offerAdminRouter);
// Export type definition of API
export type AppRouter = typeof appRouter;

@ -0,0 +1,466 @@
import { z } from 'zod';
import { JobType } from '@prisma/client';
import { TRPCError } from '@trpc/server';
import {
adminDashboardOfferDtoMapper,
getAdminOffersResponseMapper,
} from '~/mappers/offers-mappers';
import { Currency } from '~/utils/offers/currency/CurrencyEnum';
import { convertWithDate } from '~/utils/offers/currency/currencyExchange';
import { createValidationRegex } from '~/utils/offers/zodRegex';
import { createRouter } from '../context';
const getOrder = (prefix: string) => {
return prefix === '+' ? 'asc' : 'desc';
};
const sortingKeysMap = {
companyName: 'companyName',
jobTitle: 'jobTitle',
monthYearReceived: 'monthYearReceived',
totalCompensation: 'totalCompensation',
totalYoe: 'totalYoe',
};
const yoeCategoryMap: Record<number, string> = {
0: 'Internship',
1: 'Fresh Grad',
2: 'Mid',
3: 'Senior',
};
const getYoeRange = (yoeCategory: number | null | undefined) => {
return yoeCategory == null
? { maxYoe: 100, minYoe: 0 }
: yoeCategoryMap[yoeCategory] === 'Fresh Grad'
? { maxYoe: 2, minYoe: 0 }
: yoeCategoryMap[yoeCategory] === 'Mid'
? { maxYoe: 5, minYoe: 3 }
: yoeCategoryMap[yoeCategory] === 'Senior'
? { maxYoe: 100, minYoe: 6 }
: null; // Internship
};
export const offerAdminRouter = createRouter().query('list', {
input: z.object({
companyId: z.string().nullish(),
countryId: z.string().nullish(),
currency: z.string().nullish(),
dateEnd: z.date().nullish(),
dateStart: z.date().nullish(),
limit: z.number().positive(),
offset: z.number().nonnegative(),
salaryMax: z.number().nonnegative().nullish(),
salaryMin: z.number().nonnegative().nullish(),
sortBy: z
.string()
.regex(createValidationRegex(Object.keys(sortingKeysMap), '[+-]{1}'))
.nullish(),
title: z.string().nullish(),
yoeCategory: z.number().min(0).max(3).nullish(),
yoeMax: z.number().max(100).nullish(),
yoeMin: z.number().min(0).nullish(),
}),
async resolve({ ctx, input }) {
const yoeRange = getYoeRange(input.yoeCategory);
const yoeMin = input.yoeMin != null ? input.yoeMin : yoeRange?.minYoe;
const yoeMax = input.yoeMax != null ? input.yoeMax : yoeRange?.maxYoe;
if (!input.sortBy) {
input.sortBy = '-' + sortingKeysMap.monthYearReceived;
}
const order = getOrder(input.sortBy.charAt(0));
const sortingKey = input.sortBy.substring(1);
const data = !yoeRange
? await ctx.prisma.offersOffer.findMany({
// Internship
include: {
company: true,
location: {
include: {
state: {
include: {
country: true,
},
},
},
},
offersFullTime: {
include: {
baseSalary: true,
bonus: true,
stocks: true,
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: true,
offers: true,
},
},
},
orderBy:
sortingKey === sortingKeysMap.monthYearReceived
? {
monthYearReceived: order,
}
: sortingKey === sortingKeysMap.totalCompensation
? [
{
offersIntern: {
monthlySalary: {
baseValue: order,
},
},
},
{
monthYearReceived: 'desc',
},
]
: sortingKey === sortingKeysMap.totalYoe
? [
{
profile: {
background: {
totalYoe: order,
},
},
},
{
monthYearReceived: 'desc',
},
]
: sortingKey === sortingKeysMap.companyName
? [
{
company: {
name: order,
},
},
{
monthYearReceived: 'desc',
},
]
: sortingKey === sortingKeysMap.jobTitle
? [
{
offersIntern: {
title: order,
},
},
{
monthYearReceived: 'desc',
},
]
: { monthYearReceived: 'desc' },
where: {
AND: [
{
location: {
state: {
countryId:
input.countryId != null && input.countryId.length !== 0
? input.countryId
: undefined,
},
},
},
{
offersIntern: {
isNot: null,
},
},
{
offersIntern: {
title:
input.title != null && input.title.length !== 0
? input.title
: undefined,
},
},
{
offersIntern: {
monthlySalary: {
baseValue: {
gte: input.salaryMin ?? undefined,
lte: input.salaryMax ?? undefined,
},
},
},
},
{
offersFullTime: {
is: null,
},
},
{
companyId:
input.companyId && input.companyId.length !== 0
? input.companyId
: undefined,
},
{
profile: {
background: {
totalYoe: {
gte: yoeMin,
lte: yoeMax,
},
},
},
},
{
monthYearReceived: {
gte: input.dateStart ?? undefined,
lte: input.dateEnd ?? undefined,
},
},
],
},
})
: await ctx.prisma.offersOffer.findMany({
// Junior, Mid, Senior
include: {
company: true,
location: {
include: {
state: {
include: {
country: true,
},
},
},
},
offersFullTime: {
include: {
baseSalary: true,
bonus: true,
stocks: true,
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
profile: {
include: {
background: true,
offers: true,
},
},
},
orderBy:
sortingKey === sortingKeysMap.monthYearReceived
? {
monthYearReceived: order,
}
: sortingKey === sortingKeysMap.totalCompensation
? [
{
offersFullTime: {
totalCompensation: {
baseValue: order,
},
},
},
{
monthYearReceived: 'desc',
},
]
: sortingKey === sortingKeysMap.totalYoe
? [
{
profile: {
background: {
totalYoe: order,
},
},
},
{
monthYearReceived: 'desc',
},
]
: sortingKey === sortingKeysMap.companyName
? [
{
company: {
name: order,
},
},
{
monthYearReceived: 'desc',
},
]
: sortingKey === sortingKeysMap.jobTitle
? [
{
offersFullTime: {
title: order,
},
},
{
monthYearReceived: 'desc',
},
]
: { monthYearReceived: 'desc' },
where: {
AND: [
{
location: {
state: {
countryId:
input.countryId != null && input.countryId.length !== 0
? input.countryId
: undefined,
},
},
},
{
offersIntern: {
is: null,
},
},
{
offersFullTime: {
isNot: null,
},
},
{
offersFullTime: {
title:
input.title != null && input.title.length !== 0
? input.title
: undefined,
},
},
{
offersFullTime: {
totalCompensation: {
baseValue: {
gte: input.salaryMin ?? undefined,
lte: input.salaryMax ?? undefined,
},
},
},
},
{
companyId:
input.companyId && input.companyId.length !== 0
? input.companyId
: undefined,
},
{
profile: {
background: {
totalYoe: {
gte: yoeMin,
lte: yoeMax,
},
},
},
},
{
monthYearReceived: {
gte: input.dateStart ?? undefined,
lte: input.dateEnd ?? undefined,
},
},
],
},
});
const startRecordIndex: number = input.limit * input.offset;
const endRecordIndex: number =
startRecordIndex + input.limit <= data.length
? startRecordIndex + input.limit
: data.length;
let paginatedData = data.slice(startRecordIndex, endRecordIndex);
// CONVERTING
const currency = input.currency?.toUpperCase();
if (currency != null && currency in Currency) {
paginatedData = await Promise.all(
paginatedData.map(async (offer) => {
if (offer.offersFullTime?.totalCompensation != null) {
offer.offersFullTime.totalCompensation.value =
await convertWithDate(
offer.offersFullTime.totalCompensation.value,
offer.offersFullTime.totalCompensation.currency,
currency,
offer.offersFullTime.totalCompensation.updatedAt,
);
offer.offersFullTime.totalCompensation.currency = currency;
if (offer.offersFullTime?.baseSalary != null) {
offer.offersFullTime.baseSalary.value = await convertWithDate(
offer.offersFullTime.baseSalary.value,
offer.offersFullTime.baseSalary.currency,
currency,
offer.offersFullTime.baseSalary.updatedAt,
);
offer.offersFullTime.baseSalary.currency = currency;
}
if (offer.offersFullTime?.stocks != null) {
offer.offersFullTime.stocks.value = await convertWithDate(
offer.offersFullTime.stocks.value,
offer.offersFullTime.stocks.currency,
currency,
offer.offersFullTime.stocks.updatedAt,
);
offer.offersFullTime.stocks.currency = currency;
}
if (offer.offersFullTime?.bonus != null) {
offer.offersFullTime.bonus.value = await convertWithDate(
offer.offersFullTime.bonus.value,
offer.offersFullTime.bonus.currency,
currency,
offer.offersFullTime.bonus.updatedAt,
);
offer.offersFullTime.bonus.currency = currency;
}
} else if (offer.offersIntern?.monthlySalary != null) {
offer.offersIntern.monthlySalary.value = await convertWithDate(
offer.offersIntern.monthlySalary.value,
offer.offersIntern.monthlySalary.currency,
currency,
offer.offersIntern.monthlySalary.updatedAt,
);
offer.offersIntern.monthlySalary.currency = currency;
} else {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Total Compensation or Salary not found',
});
}
return offer;
}),
);
}
return getAdminOffersResponseMapper(
paginatedData.map((offer) => adminDashboardOfferDtoMapper(offer)),
{
currentPage: input.offset,
numOfItems: paginatedData.length,
numOfPages: Math.ceil(data.length / input.limit),
totalItems: data.length,
},
!yoeRange ? JobType.INTERN : JobType.FULLTIME,
);
},
});

@ -228,3 +228,25 @@ export type Location = {
stateId: string;
stateName: string;
};
export type GetAdminOffersResponse = {
data: Array<AdminDashboardOffer>;
jobType: JobType;
paging: Paging;
};
export type AdminDashboardOffer = {
baseSalary?: Valuation;
bonus?: Valuation;
company: OffersCompany;
id: string;
income: Valuation;
location: Location;
monthYearReceived: Date;
numberOfOtherOffers: number;
profileId: string;
stocks?: Valuation;
title: string;
token: string;
totalYoe: number;
};

Loading…
Cancel
Save