[offers][refactor] add types for interfaces (#390)

* [offers][chore] Create types for API responses

* [offers][fix] fix get comments bug

* [offers][fix] make offers api open to unauthenticated users

* [offers][chore] add return types to comment api

* [offers][chore] add types to get comments api

* [offers][chore] Refactor profile and analysis APIs to return defined types

* [offers][chore] Add typed response for get offers API

* [offers][chore] Changed delete offer API response

* [offers][fix] Fix type definitions for OffersCompany in types/offers

* [offers][fix] fix list some offer frontend

Co-authored-by: BryannYeap <e0543723@u.nus.edu>
Co-authored-by: Stuart Long Chay Boon <chayboon@gmail.com>
pull/391/head
Zhang Ziqing 2 years ago committed by GitHub
parent 612bef14ad
commit bc424bee33
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -191,13 +191,13 @@ model OffersProfile {
user User? @relation(fields: [userId], references: [id])
userId String?
OffersAnalysis OffersAnalysis?
analysis OffersAnalysis?
}
model OffersBackground {
id String @id @default(cuid())
totalYoe Int?
totalYoe Int
specificYoes OffersSpecificYoe[]
experiences OffersExperience[] // For extensibility in the future
@ -308,8 +308,8 @@ model OffersOffer {
monthYearReceived DateTime
location String
negotiationStrategy String?
comments String?
negotiationStrategy String
comments String
jobType JobType
@ -320,7 +320,6 @@ model OffersOffer {
offersFullTimeId String? @unique
OffersAnalysis OffersAnalysis? @relation("HighestOverallOffer")
OffersAnalysisTopOverallOffers OffersAnalysis[] @relation("TopOverallOffers")
OffersAnalysisTopCompanyOffers OffersAnalysis[] @relation("TopCompanyOffers")
}

@ -1,11 +1,13 @@
import Link from 'next/link';
import type { OfferTableRowData } from '~/components/offers/table/types';
import type { DashboardOffer } from '../../../types/offers';
import { convertCurrencyToString } from '../../../utils/offers/currency';
import { formatDate } from '../../../utils/offers/time';
export type OfferTableRowProps = Readonly<{ row: OfferTableRowData }>;
export type OfferTableRowProps = Readonly<{ row: DashboardOffer }>;
export default function OfferTableRow({
row: { company, date, id, profileId, salary, title, yoe },
row: { company, id, income, monthYearReceived, profileId, title, totalYoe },
}: OfferTableRowProps) {
return (
<tr
@ -14,12 +16,12 @@ export default function OfferTableRow({
<th
className="whitespace-nowrap py-4 px-6 font-medium text-gray-900 dark:text-white"
scope="row">
{company}
{company.name}
</th>
<td className="py-4 px-6">{title}</td>
<td className="py-4 px-6">{yoe}</td>
<td className="py-4 px-6">{salary}</td>
<td className="py-4 px-6">{date}</td>
<td className="py-4 px-6">{totalYoe}</td>
<td className="py-4 px-6">{convertCurrencyToString(income)}</td>
<td className="py-4 px-6">{formatDate(monthYearReceived)}</td>
<td className="space-x-4 py-4 px-6">
<Link
className="font-medium text-indigo-600 hover:underline dark:text-indigo-500"

@ -2,18 +2,15 @@ import { useEffect, useState } from 'react';
import { HorizontalDivider, Select, Spinner, Tabs } from '@tih/ui';
import OffersTablePagination from '~/components/offers/table/OffersTablePagination';
import type {
OfferTableRowData,
PaginationType,
} from '~/components/offers/table/types';
import { YOE_CATEGORY } from '~/components/offers/table/types';
import CurrencySelector from '~/utils/offers/currency/CurrencySelector';
import { formatDate } from '~/utils/offers/time';
import { trpc } from '~/utils/trpc';
import OffersRow from './OffersRow';
import type { DashboardOffer, Paging } from '~/types/offers';
const NUMBER_OF_OFFERS_IN_PAGE = 10;
export type OffersTableProps = Readonly<{
companyFilter: string;
@ -25,18 +22,18 @@ export default function OffersTable({
}: OffersTableProps) {
const [currency, setCurrency] = useState('SGD'); // TODO: Detect location
const [selectedTab, setSelectedTab] = useState(YOE_CATEGORY.ENTRY);
const [pagination, setPagination] = useState<PaginationType>({
currentPage: 1,
numOfItems: 1,
const [pagination, setPagination] = useState<Paging>({
currentPage: 0,
numOfItems: 0,
numOfPages: 0,
totalItems: 0,
});
const [offers, setOffers] = useState<Array<OfferTableRowData>>([]);
const [offers, setOffers] = useState<Array<DashboardOffer>>([]);
useEffect(() => {
setPagination({
currentPage: 1,
numOfItems: 1,
currentPage: 0,
numOfItems: 0,
numOfPages: 0,
totalItems: 0,
});
@ -48,7 +45,7 @@ export default function OffersTable({
companyId: companyFilter,
limit: NUMBER_OF_OFFERS_IN_PAGE,
location: 'Singapore, Singapore', // TODO: Geolocation
offset: pagination.currentPage - 1,
offset: 0,
sortBy: '-monthYearReceived',
title: jobTitleFilter,
yoeCategory: selectedTab,
@ -56,28 +53,19 @@ export default function OffersTable({
],
{
onSuccess: (response) => {
const filteredData = response.data.map((res) => {
return {
company: res.company.name,
date: formatDate(res.monthYearReceived),
id: res.OffersFullTime
? res.OffersFullTime!.id
: res.OffersIntern!.id,
profileId: res.profileId,
salary: res.OffersFullTime
? res.OffersFullTime?.totalCompensation.value
: res.OffersIntern?.monthlySalary.value,
title: res.OffersFullTime ? res.OffersFullTime?.level : '',
yoe: 100,
};
});
setOffers(filteredData);
setPagination({
currentPage: (response.paging.currPage as number) + 1,
numOfItems: response.paging.numOfItemsInPage,
numOfPages: response.paging.numOfPages,
totalItems: response.paging.totalNumberOfOffers,
});
// Const filteredData = response.data.map((res) => {
// return {
// company: res.company.name,
// date: res.monthYearReceived,
// id: res.id,
// profileId: res.profileId,
// income: res.income,
// title: res.title,
// yoe: res.totalYoe,
// };
// });
setOffers(response.data);
setPagination(response.paging);
},
},
);
@ -90,15 +78,15 @@ export default function OffersTable({
label="Table Navigation"
tabs={[
{
label: 'Fresh Grad (0-3 YOE)',
label: 'Fresh Grad (0-2 YOE)',
value: YOE_CATEGORY.ENTRY,
},
{
label: 'Mid (4-7 YOE)',
label: 'Mid (3-5 YOE)',
value: YOE_CATEGORY.MID,
},
{
label: 'Senior (8+ YOE)',
label: 'Senior (6+ YOE)',
value: YOE_CATEGORY.SENIOR,
},
{
@ -187,14 +175,11 @@ export default function OffersTable({
)}
<OffersTablePagination
endNumber={
(pagination.currentPage - 1) * NUMBER_OF_OFFERS_IN_PAGE +
offers.length
pagination.currentPage * NUMBER_OF_OFFERS_IN_PAGE + offers.length
}
handlePageChange={handlePageChange}
pagination={pagination}
startNumber={
(pagination.currentPage - 1) * NUMBER_OF_OFFERS_IN_PAGE + 1
}
startNumber={pagination.currentPage * NUMBER_OF_OFFERS_IN_PAGE + 1}
/>
</div>
</div>

@ -1,11 +1,11 @@
import { Pagination } from '@tih/ui';
import type { PaginationType } from '~/components/offers/table/types';
import type { Paging } from '~/types/offers';
type OffersTablePaginationProps = Readonly<{
endNumber: number;
handlePageChange: (page: number) => void;
pagination: PaginationType;
pagination: Paging;
startNumber: number;
}>;
@ -30,13 +30,13 @@ export default function OffersTablePagination({
</span>
</span>
<Pagination
current={pagination.currentPage}
current={pagination.currentPage + 1}
end={pagination.numOfPages}
label="Pagination"
pagePadding={1}
start={1}
onSelect={(currPage) => {
handlePageChange(currPage);
handlePageChange(currPage - 1);
}}
/>
</nav>

@ -1,13 +1,3 @@
export type OfferTableRowData = {
company: string;
date: string;
id: string;
profileId: string;
salary: number | undefined;
title: string;
yoe: number;
};
// eslint-disable-next-line no-shadow
export enum YOE_CATEGORY {
INTERN = 0,

@ -0,0 +1,574 @@
import type {
Company,
OffersAnalysis,
OffersBackground,
OffersCurrency,
OffersEducation,
OffersExperience,
OffersFullTime,
OffersIntern,
OffersOffer,
OffersProfile,
OffersReply,
OffersSpecificYoe,
User,
} from '@prisma/client';
import { JobType } from '@prisma/client';
import type {
AddToProfileResponse,
Analysis,
AnalysisHighestOffer,
AnalysisOffer,
Background,
CreateOfferProfileResponse,
DashboardOffer,
Education,
Experience,
GetOffersResponse,
OffersCompany,
Paging,
Profile,
ProfileAnalysis,
ProfileOffer,
SpecificYoe,
Valuation,
} from '~/types/offers';
const analysisOfferDtoMapper = (
offer: OffersOffer & {
OffersFullTime:
| (OffersFullTime & { totalCompensation: OffersCurrency })
| null;
OffersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
company: Company;
profile: OffersProfile & { background: OffersBackground | null };
},
) => {
const { background, profileName } = offer.profile;
const analysisOfferDto: AnalysisOffer = {
company: offersCompanyDtoMapper(offer.company),
id: offer.id,
income: -1,
jobType: offer.jobType,
level: offer.OffersFullTime?.level ?? '',
location: offer.location,
monthYearReceived: offer.monthYearReceived,
negotiationStrategy: offer.negotiationStrategy,
previousCompanies: [],
profileName,
specialization:
offer.jobType === JobType.FULLTIME
? offer.OffersFullTime?.specialization ?? ''
: offer.OffersIntern?.specialization ?? '',
title:
offer.jobType === JobType.FULLTIME
? offer.OffersFullTime?.title ?? ''
: offer.OffersIntern?.title ?? '',
totalYoe: background?.totalYoe ?? -1,
};
if (offer.OffersFullTime?.totalCompensation) {
analysisOfferDto.income = offer.OffersFullTime.totalCompensation.value;
} else if (offer.OffersIntern?.monthlySalary) {
analysisOfferDto.income = offer.OffersIntern.monthlySalary.value;
}
return analysisOfferDto;
};
const analysisDtoMapper = (
noOfOffers: number,
percentile: number,
topPercentileOffers: Array<
OffersOffer & {
OffersFullTime:
| (OffersFullTime & { totalCompensation: OffersCurrency })
| null;
OffersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
company: Company;
profile: OffersProfile & { background: OffersBackground | null };
}
>,
) => {
const analysisDto: Analysis = {
noOfOffers,
percentile,
topPercentileOffers: topPercentileOffers.map((offer) =>
analysisOfferDtoMapper(offer),
),
};
return analysisDto;
};
const analysisHighestOfferDtoMapper = (
offer: OffersOffer & {
OffersFullTime:
| (OffersFullTime & { totalCompensation: OffersCurrency })
| null;
OffersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
company: Company;
profile: OffersProfile & { background: OffersBackground | null };
},
) => {
const analysisHighestOfferDto: AnalysisHighestOffer = {
company: offersCompanyDtoMapper(offer.company),
id: offer.id,
level: offer.OffersFullTime?.level ?? '',
location: offer.location,
specialization:
offer.jobType === JobType.FULLTIME
? offer.OffersFullTime?.specialization ?? ''
: offer.OffersIntern?.specialization ?? '',
totalYoe: offer.profile.background?.totalYoe ?? -1,
};
return analysisHighestOfferDto;
};
export const profileAnalysisDtoMapper = (
analysis:
| (OffersAnalysis & {
overallHighestOffer: OffersOffer & {
OffersFullTime:
| (OffersFullTime & { totalCompensation: OffersCurrency })
| null;
OffersIntern:
| (OffersIntern & { monthlySalary: OffersCurrency })
| null;
company: Company;
profile: OffersProfile & { background: OffersBackground | null };
};
topCompanyOffers: Array<
OffersOffer & {
OffersFullTime:
| (OffersFullTime & { totalCompensation: OffersCurrency })
| null;
OffersIntern:
| (OffersIntern & { monthlySalary: OffersCurrency })
| null;
company: Company;
profile: OffersProfile & {
background:
| (OffersBackground & {
experiences: Array<
OffersExperience & { company: Company | null }
>;
})
| null;
};
}
>;
topOverallOffers: Array<
OffersOffer & {
OffersFullTime:
| (OffersFullTime & { totalCompensation: OffersCurrency })
| null;
OffersIntern:
| (OffersIntern & { monthlySalary: OffersCurrency })
| null;
company: Company;
profile: OffersProfile & {
background:
| (OffersBackground & {
experiences: Array<
OffersExperience & { company: Company | null }
>;
})
| null;
};
}
>;
})
| null,
) => {
if (!analysis) {
return null;
}
const profileAnalysisDto: ProfileAnalysis = {
companyAnalysis: [
analysisDtoMapper(
analysis.noOfSimilarCompanyOffers,
analysis.companyPercentile,
analysis.topCompanyOffers,
),
],
id: analysis.id,
overallAnalysis: analysisDtoMapper(
analysis.noOfSimilarOffers,
analysis.overallPercentile,
analysis.topOverallOffers,
),
overallHighestOffer: analysisHighestOfferDtoMapper(
analysis.overallHighestOffer,
),
profileId: analysis.profileId,
};
return profileAnalysisDto;
};
export const valuationDtoMapper = (currency: {
currency: string;
id?: string;
value: number;
}) => {
const valuationDto: Valuation = {
currency: currency.currency,
value: currency.value,
};
return valuationDto;
};
export const offersCompanyDtoMapper = (company: Company) => {
const companyDto: OffersCompany = {
createdAt: company.createdAt,
description: company?.description ?? '',
id: company.id,
logoUrl: company.logoUrl ?? '',
name: company.name,
slug: company.slug,
updatedAt: company.updatedAt,
};
return companyDto;
};
export const educationDtoMapper = (education: {
backgroundId?: string;
endDate: Date | null;
field: string | null;
id: string;
school: string | null;
startDate: Date | null;
type: string | null;
}) => {
const educationDto: Education = {
endDate: education.endDate,
field: education.field,
id: education.id,
school: education.school,
startDate: education.startDate,
type: education.type,
};
return educationDto;
};
export const experienceDtoMapper = (
experience: OffersExperience & {
company: Company | null;
monthlySalary: OffersCurrency | null;
totalCompensation: OffersCurrency | null;
},
) => {
const experienceDto: Experience = {
company: experience.company
? offersCompanyDtoMapper(experience.company)
: null,
durationInMonths: experience.durationInMonths,
id: experience.id,
jobType: experience.jobType,
level: experience.level,
monthlySalary: experience.monthlySalary
? valuationDtoMapper(experience.monthlySalary)
: experience.monthlySalary,
specialization: experience.specialization,
title: experience.title,
totalCompensation: experience.totalCompensation
? valuationDtoMapper(experience.totalCompensation)
: experience.totalCompensation,
};
return experienceDto;
};
export const specificYoeDtoMapper = (specificYoe: {
backgroundId?: string;
domain: string;
id: string;
yoe: number;
}) => {
const specificYoeDto: SpecificYoe = {
domain: specificYoe.domain,
id: specificYoe.id,
yoe: specificYoe.yoe,
};
return specificYoeDto;
};
export const backgroundDtoMapper = (
background:
| (OffersBackground & {
educations: Array<OffersEducation>;
experiences: Array<
OffersExperience & {
company: Company | null;
monthlySalary: OffersCurrency | null;
totalCompensation: OffersCurrency | null;
}
>;
specificYoes: Array<OffersSpecificYoe>;
})
| null,
) => {
if (!background) {
return null;
}
const educations = background.educations.map((education) =>
educationDtoMapper(education),
);
const experiences = background.experiences.map((experience) =>
experienceDtoMapper(experience),
);
const specificYoes = background.specificYoes.map((specificYoe) =>
specificYoeDtoMapper(specificYoe),
);
const backgroundDto: Background = {
educations,
experiences,
id: background.id,
specificYoes,
totalYoe: background.totalYoe,
};
return backgroundDto;
};
export const profileOfferDtoMapper = (
offer: OffersOffer & {
OffersFullTime:
| (OffersFullTime & {
baseSalary: OffersCurrency;
bonus: OffersCurrency;
stocks: OffersCurrency;
totalCompensation: OffersCurrency;
})
| null;
OffersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
company: Company;
},
) => {
const profileOfferDto: ProfileOffer = {
comments: offer.comments,
company: offersCompanyDtoMapper(offer.company),
id: offer.id,
jobType: offer.jobType,
location: offer.location,
monthYearReceived: offer.monthYearReceived,
negotiationStrategy: offer.negotiationStrategy,
offersFullTime: offer.OffersFullTime,
offersIntern: offer.OffersIntern,
};
if (offer.OffersFullTime) {
profileOfferDto.offersFullTime = {
baseSalary: valuationDtoMapper(offer.OffersFullTime.baseSalary),
bonus: valuationDtoMapper(offer.OffersFullTime.bonus),
id: offer.OffersFullTime.id,
level: offer.OffersFullTime.level,
specialization: offer.OffersFullTime.specialization,
stocks: valuationDtoMapper(offer.OffersFullTime.stocks),
title: offer.OffersFullTime.title,
totalCompensation: valuationDtoMapper(
offer.OffersFullTime.totalCompensation,
),
};
} else if (offer.OffersIntern) {
profileOfferDto.offersIntern = {
id: offer.OffersIntern.id,
internshipCycle: offer.OffersIntern.internshipCycle,
monthlySalary: valuationDtoMapper(offer.OffersIntern.monthlySalary),
specialization: offer.OffersIntern.specialization,
startYear: offer.OffersIntern.startYear,
title: offer.OffersIntern.title,
};
}
return profileOfferDto;
};
export const profileDtoMapper = (
profile: OffersProfile & {
analysis:
| (OffersAnalysis & {
overallHighestOffer: OffersOffer & {
OffersFullTime:
| (OffersFullTime & { totalCompensation: OffersCurrency })
| null;
OffersIntern:
| (OffersIntern & { monthlySalary: OffersCurrency })
| null;
company: Company;
profile: OffersProfile & { background: OffersBackground | null };
};
topCompanyOffers: Array<
OffersOffer & {
OffersFullTime:
| (OffersFullTime & { totalCompensation: OffersCurrency })
| null;
OffersIntern:
| (OffersIntern & { monthlySalary: OffersCurrency })
| null;
company: Company;
profile: OffersProfile & {
background:
| (OffersBackground & {
experiences: Array<
OffersExperience & { company: Company | null }
>;
})
| null;
};
}
>;
topOverallOffers: Array<
OffersOffer & {
OffersFullTime:
| (OffersFullTime & { totalCompensation: OffersCurrency })
| null;
OffersIntern:
| (OffersIntern & { monthlySalary: OffersCurrency })
| null;
company: Company;
profile: OffersProfile & {
background:
| (OffersBackground & {
experiences: Array<
OffersExperience & { company: Company | null }
>;
})
| null;
};
}
>;
})
| null;
background:
| (OffersBackground & {
educations: Array<OffersEducation>;
experiences: Array<
OffersExperience & {
company: Company | null;
monthlySalary: OffersCurrency | null;
totalCompensation: OffersCurrency | null;
}
>;
specificYoes: Array<OffersSpecificYoe>;
})
| null;
discussion: Array<
OffersReply & {
replies: Array<OffersReply>;
replyingTo: OffersReply | null;
user: User | null;
}
>;
offers: Array<
OffersOffer & {
OffersFullTime:
| (OffersFullTime & {
baseSalary: OffersCurrency;
bonus: OffersCurrency;
stocks: OffersCurrency;
totalCompensation: OffersCurrency;
})
| null;
OffersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
company: Company;
}
>;
},
inputToken: string | undefined,
) => {
const profileDto: Profile = {
analysis: profileAnalysisDtoMapper(profile.analysis),
background: backgroundDtoMapper(profile.background),
editToken: null,
id: profile.id,
isEditable: false,
offers: profile.offers.map((offer) => profileOfferDtoMapper(offer)),
profileName: profile.profileName,
};
if (inputToken === profile.editToken) {
profileDto.editToken = profile.editToken;
profileDto.isEditable = true;
}
return profileDto;
};
export const createOfferProfileResponseMapper = (
profile: { id: string },
token: string,
) => {
const res: CreateOfferProfileResponse = {
id: profile.id,
token,
};
return res;
};
export const addToProfileResponseMapper = (updatedProfile: {
id: string;
profileName: string;
userId?: string | null;
}) => {
const addToProfileResponse: AddToProfileResponse = {
id: updatedProfile.id,
profileName: updatedProfile.profileName,
userId: updatedProfile.userId ?? '',
};
return addToProfileResponse;
};
export const dashboardOfferDtoMapper = (
offer: OffersOffer & {
OffersFullTime:
| (OffersFullTime & {
baseSalary: OffersCurrency;
bonus: OffersCurrency;
stocks: OffersCurrency;
totalCompensation: OffersCurrency;
})
| null;
OffersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
company: Company;
profile: OffersProfile & { background: OffersBackground | null };
},
) => {
const dashboardOfferDto: DashboardOffer = {
company: offersCompanyDtoMapper(offer.company),
id: offer.id,
income: valuationDtoMapper({ currency: '', value: -1 }),
monthYearReceived: offer.monthYearReceived,
profileId: offer.profileId,
title: offer.OffersFullTime?.title ?? '',
totalYoe: offer.profile.background?.totalYoe ?? -1,
};
if (offer.OffersFullTime) {
dashboardOfferDto.income = valuationDtoMapper(
offer.OffersFullTime.totalCompensation,
);
} else if (offer.OffersIntern) {
dashboardOfferDto.income = valuationDtoMapper(
offer.OffersIntern.monthlySalary,
);
}
return dashboardOfferDto;
};
export const getOffersResponseMapper = (
data: Array<DashboardOffer>,
paging: Paging,
) => {
const getOffersResponse: GetOffersResponse = {
data,
paging,
};
return getOffersResponse;
};

@ -43,25 +43,23 @@ export default function OfferProfile() {
if (data?.offers) {
const filteredOffers: Array<OfferEntity> = data
? data?.offers.map((res) => {
if (res.OfferFullTime) {
if (res.offersFullTime) {
const filteredOffer: OfferEntity = {
base: convertCurrencyToString(
res.OfferFullTime.baseSalary,
),
bonus: convertCurrencyToString(
res.OfferFullTime.bonus,
res.offersFullTime.baseSalary,
),
bonus: convertCurrencyToString(res.offersFullTime.bonus),
companyName: res.company.name,
id: res.OfferFullTime.id,
jobLevel: res.OfferFullTime.level,
jobTitle: res.OfferFullTime.title,
id: res.offersFullTime.id,
jobLevel: res.offersFullTime.level,
jobTitle: res.offersFullTime.title,
location: res.location,
negotiationStrategy: res.negotiationStrategy || '',
otherComment: res.comments || '',
receivedMonth: formatDate(res.monthYearReceived),
stocks: convertCurrencyToString(res.OfferFullTime.stocks),
stocks: convertCurrencyToString(res.offersFullTime.stocks),
totalCompensation: convertCurrencyToString(
res.OfferFullTime.totalCompensation,
res.offersFullTime.totalCompensation,
),
};
@ -69,11 +67,11 @@ export default function OfferProfile() {
}
const filteredOffer: OfferEntity = {
companyName: res.company.name,
id: res.OfferIntern!.id,
jobTitle: res.OfferIntern!.title,
id: res.offersIntern!.id,
jobTitle: res.offersIntern!.title,
location: res.location,
monthlySalary: convertCurrencyToString(
res.OfferIntern!.monthlySalary,
res.offersIntern!.monthlySalary,
),
negotiationStrategy: res.negotiationStrategy || '',
otherComment: res.comments || '',

@ -7,7 +7,7 @@ function Test() {
const [error, setError] = useState('');
const createMutation = trpc.useMutation(['offers.profile.create'], {
onError(err: any) {
onError(err) {
alert(err);
},
onSuccess(data) {
@ -18,7 +18,7 @@ function Test() {
const addToUserProfileMutation = trpc.useMutation(
['offers.profile.addToUserProfile'],
{
onError(err: any) {
onError(err) {
alert(err);
},
onSuccess(data) {
@ -28,7 +28,7 @@ function Test() {
);
const deleteCommentMutation = trpc.useMutation(['offers.comments.delete'], {
onError(err: any) {
onError(err) {
alert(err);
},
onSuccess(data) {
@ -46,7 +46,7 @@ function Test() {
};
const updateCommentMutation = trpc.useMutation(['offers.comments.update'], {
onError(err: any) {
onError(err) {
alert(err);
},
onSuccess(data) {
@ -64,7 +64,7 @@ function Test() {
};
const createCommentMutation = trpc.useMutation(['offers.comments.create'], {
onError(err: any) {
onError(err) {
alert(err);
},
onSuccess(data) {
@ -74,17 +74,18 @@ function Test() {
const handleCreate = () => {
createCommentMutation.mutate({
message: 'hello',
profileId: 'cl96stky5002ew32gx2kale2x',
// UserId: 'cl97dl51k001e7iygd5v5gt58'
message: 'wassup bro',
profileId: 'cl9efyn9p004ww3u42mjgl1vn',
replyingToId: 'cl9el4xj10001w3w21o3p2iny',
userId: 'cl9ehvpng0000w3ec2mpx0bdd'
});
};
const handleLink = () => {
addToUserProfileMutation.mutate({
profileId: 'cl96stky5002ew32gx2kale2x',
profileId: 'cl9efyn9p004ww3u42mjgl1vn',
token: 'afca11e436d21bde24543718fa957c6c625335439dc504f24ee35eae7b5ef1ba',
userId: 'cl97dl51k001e7iygd5v5gt58',
userId: 'cl9ehvpng0000w3ec2mpx0bdd',
});
};
@ -102,7 +103,7 @@ function Test() {
],
experiences: [
{
companyId: 'cl98yuqk80007txhgjtjp8fk4',
companyId: 'cl9ec1mgg0000w33hg1a3612r',
durationInMonths: 24,
jobType: 'FULLTIME',
level: 'Junior',
@ -150,6 +151,8 @@ function Test() {
value: 104100,
},
},
comments: 'I am a Raffles Institution almumni',
// Comments: '',
companyId: 'cl98yuqk80007txhgjtjp8fk4',
jobType: 'FULLTIME',
@ -179,25 +182,25 @@ function Test() {
value: 104100,
},
},
comments: undefined,
comments: '',
companyId: 'cl98yuqk80007txhgjtjp8fk4',
jobType: 'FULLTIME',
location: 'Singapore, Singapore',
monthYearReceived: new Date('2022-09-30T07:58:54.000Z'),
// NegotiationStrategy: 'Leveraged having multiple offers',
negotiationStrategy: 'Leveraged having multiple offers',
},
],
});
};
const profileId = 'cl99fhrsf00007ijpbrdk8gue'; // Remember to change this filed after testing deleting
const profileId = 'cl9efyn9p004ww3u42mjgl1vn'; // Remember to change this filed after testing deleting
const data = trpc.useQuery(
[
`offers.profile.listOne`,
{
profileId,
token:
'e7effd2a40adba2deb1ddea4fb9f1e6c3c98ab0a85a88ed1567fc2a107fdb445',
'd14666ff76e267c9e99445844b41410e83874936d0c07e664db73ff0ea76919e',
},
],
{
@ -216,6 +219,7 @@ function Test() {
},
);
// Console.log(replies.data?.data)
const deleteMutation = trpc.useMutation(['offers.profile.delete']);
const handleDelete = (id: string) => {
@ -226,7 +230,7 @@ function Test() {
};
const updateMutation = trpc.useMutation(['offers.profile.update'], {
onError(err: any) {
onError(err) {
alert(err);
},
onSuccess(response) {
@ -261,7 +265,7 @@ function Test() {
slug: 'meta',
updatedAt: new Date('2022-10-12T16:19:05.196Z'),
},
companyId: 'cl98yuqk80007txhgjtjp8fk4',
companyId: 'cl9ec1mgg0000w33hg1a3612r',
durationInMonths: 24,
id: 'cl96stky6002iw32gpt6t87s2',
jobType: 'FULLTIME',
@ -368,7 +372,7 @@ function Test() {
slug: 'meta',
updatedAt: new Date('2022-10-12T16:19:05.196Z'),
},
companyId: 'cl98yuqk80007txhgjtjp8fk4',
companyId: 'cl9ec1mgg0000w33hg1a3612r',
id: 'cl976t4de00047iygl0zbce11',
jobType: 'FULLTIME',
location: 'Singapore, Singapore',
@ -410,7 +414,7 @@ function Test() {
totalCompensationId: 'cl96stky90039w32glbpktd0o',
},
OffersIntern: null,
comments: null,
comments: '',
company: {
createdAt: new Date('2022-10-12T16:19:05.196Z'),
description:
@ -421,7 +425,7 @@ function Test() {
slug: 'meta',
updatedAt: new Date('2022-10-12T16:19:05.196Z'),
},
companyId: 'cl98yuqk80007txhgjtjp8fk4',
companyId: 'cl9ec1mgg0000w33hg1a3612r',
id: 'cl96stky80031w32gau9mu1gs',
jobType: 'FULLTIME',
location: 'Singapore, Singapore',
@ -463,7 +467,7 @@ function Test() {
totalCompensationId: 'cl96stky9003jw32gzumcoi7v',
},
OffersIntern: null,
comments: null,
comments: '',
company: {
createdAt: new Date('2022-10-12T16:19:05.196Z'),
description:
@ -474,7 +478,7 @@ function Test() {
slug: 'meta',
updatedAt: new Date('2022-10-12T16:19:05.196Z'),
},
companyId: 'cl98yuqk80007txhgjtjp8fk4',
companyId: 'cl9ec1mgg0000w33hg1a3612r',
id: 'cl96stky9003bw32gc3l955vr',
jobType: 'FULLTIME',
location: 'Singapore, Singapore',
@ -527,7 +531,7 @@ function Test() {
slug: 'meta',
updatedAt: new Date('2022-10-12T16:19:05.196Z'),
},
companyId: 'cl98yuqk80007txhgjtjp8fk4',
companyId: 'cl9ec1mgg0000w33hg1a3612r',
id: 'cl976wf28000t7iyga4noyz7s',
jobType: 'FULLTIME',
location: 'Singapore, Singapore',
@ -580,7 +584,7 @@ function Test() {
slug: 'meta',
updatedAt: new Date('2022-10-12T16:19:05.196Z'),
},
companyId: 'cl98yuqk80007txhgjtjp8fk4',
companyId: 'cl9ec1mgg0000w33hg1a3612r',
id: 'cl96tbb3o0051w32gjrpaiiit',
jobType: 'FULLTIME',
location: 'Singapore, Singapore',
@ -600,7 +604,7 @@ function Test() {
return (
<>
<div>{createdData}</div>
<div>{JSON.stringify(replies.data)}</div>
<div>{JSON.stringify(replies.data?.data)}</div>
<button type="button" onClick={handleClick}>
Click Me!
</button>

@ -8,9 +8,10 @@ import type {
OffersOffer,
OffersProfile,
} from '@prisma/client';
import { JobType } from '@prisma/client';
import { TRPCError } from '@trpc/server';
import { profileAnalysisDtoMapper } from '~/mappers/offers-mappers';
import { createRouter } from '../context';
const searchOfferPercentile = (
@ -27,9 +28,19 @@ const searchOfferPercentile = (
company: Company;
profile: OffersProfile & { background: OffersBackground | null };
},
similarOffers: Array<any> | string,
similarOffers: Array<
OffersOffer & {
OffersFullTime:
| (OffersFullTime & {
totalCompensation: OffersCurrency;
})
| null;
OffersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
company: Company;
profile: OffersProfile & { background: OffersBackground | null };
}
>,
) => {
for (let i = 0; i < similarOffers.length; i++) {
if (similarOffers[i].id === offer.id) {
return i;
@ -39,116 +50,6 @@ const searchOfferPercentile = (
return -1;
};
const topPercentileDtoMapper = (topPercentileOffers: Array<any>) => {
return topPercentileOffers.map((offer) => {
const { background } = offer.profile;
return {
company: { id: offer.company.id, name: offer.company.name },
id: offer.id,
jobType: offer.jobType,
level: offer.OffersFullTime?.level,
monthYearReceived: offer.monthYearReceived,
monthlySalary: offer.OffersIntern?.monthlySalary?.value,
negotiationStrategy: offer.negotiationStrategy,
profile: {
background: {
experiences: background?.experiences.map(
(exp: { company: { id: any; name: any }; id: any }) => {
return {
company: { id: exp.company.id, name: exp.company.name },
id: exp.id,
};
},
),
id: background?.id,
totalYoe: background?.totalYoe,
},
id: offer.profileId,
name: offer.profile.profileName,
},
specialization:
offer.jobType === JobType.FULLTIME
? offer.OffersFullTime?.specialization
: offer.OffersIntern?.specialization,
title:
offer.jobType === JobType.FULLTIME
? offer.OffersFullTime?.title
: offer.OffersIntern?.title,
totalCompensation: offer.OffersFullTime?.totalCompensation?.value,
};
});
};
const specificAnalysisDtoMapper = (
noOfOffers: number,
percentile: number,
topPercentileOffers: Array<any>,
) => {
return {
noOfOffers,
percentile,
topPercentileCompanyOffers: topPercentileDtoMapper(topPercentileOffers),
};
};
const highestOfferDtoMapper = (
offer: OffersOffer & {
OffersFullTime:
| (OffersFullTime & { totalCompensation: OffersCurrency })
| null;
OffersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
company: Company;
profile: OffersProfile & { background: OffersBackground | null };
},
) => {
return {
company: { id: offer.company.id, name: offer.company.name },
id: offer.id,
level: offer.OffersFullTime?.level,
location: offer.location,
specialization:
offer.jobType === JobType.FULLTIME
? offer.OffersFullTime?.specialization
: offer.OffersIntern?.specialization,
totalYoe: offer.profile.background?.totalYoe,
};
};
const profileAnalysisDtoMapper = (
analysisId: string,
profileId: string,
overallHighestOffer: OffersOffer & {
OffersFullTime:
| (OffersFullTime & { totalCompensation: OffersCurrency })
| null;
OffersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
company: Company;
profile: OffersProfile & { background: OffersBackground | null };
},
noOfSimilarOffers: number,
overallPercentile: number,
topPercentileOffers: Array<any>,
noOfSimilarCompanyOffers: number,
companyPercentile: number,
topPercentileCompanyOffers: Array<any>,
) => {
return {
companyAnalysis: specificAnalysisDtoMapper(
noOfSimilarCompanyOffers,
companyPercentile,
topPercentileCompanyOffers,
),
id: analysisId,
overallAnalysis: specificAnalysisDtoMapper(
noOfSimilarOffers,
overallPercentile,
topPercentileOffers,
),
overallHighestOffer: highestOfferDtoMapper(overallHighestOffer),
profileId,
};
};
export const offersAnalysisRouter = createRouter()
.query('generate', {
input: z.object({
@ -213,7 +114,7 @@ export const offersAnalysisRouter = createRouter()
const overallHighestOffer = offers[0];
// TODO: Shift yoe to background to make it mandatory
// TODO: Shift yoe out of background to make it mandatory
if (
!overallHighestOffer.profile.background ||
!overallHighestOffer.profile.background.totalYoe
@ -465,17 +366,7 @@ export const offersAnalysisRouter = createRouter()
},
});
return profileAnalysisDtoMapper(
analysis.id,
analysis.profileId,
overallHighestOffer,
noOfSimilarOffers,
overallPercentile,
topPercentileOffers,
noOfSimilarCompanyOffers,
companyPercentile,
topPercentileCompanyOffers,
);
return profileAnalysisDtoMapper(analysis);
},
})
.query('get', {
@ -574,16 +465,6 @@ export const offersAnalysisRouter = createRouter()
});
}
return profileAnalysisDtoMapper(
analysis.id,
analysis.profileId,
analysis.overallHighestOffer,
analysis.noOfSimilarOffers,
analysis.overallPercentile,
analysis.topOverallOffers,
analysis.noOfSimilarCompanyOffers,
analysis.companyPercentile,
analysis.topCompanyOffers,
);
return profileAnalysisDtoMapper(analysis);
},
});

@ -1,264 +1,329 @@
import { z } from 'zod';
import * as trpc from '@trpc/server';
import { createProtectedRouter } from '../context';
import { createRouter } from '../context';
import type { Reply } from '~/types/offers-profile';
import type { OffersDiscussion, Reply } from '~/types/offers';
export const offersCommentsRouter = createRouter()
.query('getComments', {
input: z.object({
profileId: z.string(),
}),
async resolve({ ctx, input }) {
const profile = await ctx.prisma.offersProfile.findFirst({
where: {
id: input.profileId,
},
});
export const offersCommentsRouter = createProtectedRouter()
.query('getComments', {
input: z.object({
profileId: z.string()
}),
async resolve({ ctx, input }) {
const result = await ctx.prisma.offersProfile.findFirst({
include: {
discussion: {
include: {
replies: {
include: {
user: true,
},
},
replyingTo: true,
user: true,
},
},
},
where: {
id: input.profileId,
},
});
const profile = await ctx.prisma.offersProfile.findFirst({
where: {
id: input.profileId
}
const discussions: OffersDiscussion = {
data: result?.discussion
.filter((x) => {
return x.replyingToId === null
})
.map((x) => {
if (x.user == null) {
x.user = {
email: '',
emailVerified: null,
id: '',
image: '',
name: profile?.profileName ?? '<missing name>',
};
}
const result = await ctx.prisma.offersProfile.findFirst({
include: {
discussion: {
include: {
replies: {
include: {
user: true
}
},
replyingTo: true,
user: true
}
}
},
where: {
id: input.profileId
x.replies?.map((y) => {
if (y.user == null) {
y.user = {
email: '',
emailVerified: null,
id: '',
image: '',
name: profile?.profileName ?? '<missing name>',
};
}
})
});
if (result) {
return result.discussion
.filter((x: Reply) => x.replyingToId === null)
.map((x: Reply) => {
if (x.user == null) {
x.user = {
email: "",
emailVerified: null,
id: "",
image: "",
name: profile?.profileName ?? "<missing name>"
}
}
const replyType: Reply = {
createdAt: x.createdAt,
id: x.id,
message: x.message,
replies: x.replies.map((reply) => {
return {
createdAt: reply.createdAt,
id: reply.id,
message: reply.message,
replies: [],
replyingToId: reply.replyingToId,
user: reply.user
}
}),
replyingToId: x.replyingToId,
user: x.user
}
x.replies?.map((y) => {
if (y.user == null) {
y.user = {
email: "",
emailVerified: null,
id: "",
image: "",
name: profile?.profileName ?? "<missing name>"
}
}
})
return x;
})
}
return replyType
}) ?? []
}
return result
}
})
.mutation("create", {
input: z.object({
message: z.string(),
profileId: z.string(),
replyingToId: z.string().optional(),
userId: z.string().optional()
}),
async resolve({ ctx, input }) {
const createdReply = await ctx.prisma.offersReply.create({
data: {
message: input.message,
profile: {
connect: {
id: input.profileId
}
}
}
})
return discussions
},
})
.mutation('create', {
input: z.object({
message: z.string(),
profileId: z.string(),
replyingToId: z.string().optional(),
token: z.string().optional(),
userId: z.string().optional()
}),
async resolve({ ctx, input }) {
const profile = await ctx.prisma.offersProfile.findFirst({
where: {
id: input.profileId,
},
});
if (input.replyingToId) {
await ctx.prisma.offersReply.update({
data: {
replyingTo: {
connect: {
id: input.replyingToId
}
}
},
where: {
id: createdReply.id
}
})
}
const profileEditToken = profile?.editToken;
if (input.userId) {
await ctx.prisma.offersReply.update({
data: {
user: {
connect: {
id: input.userId
}
}
},
where: {
id: createdReply.id
}
})
}
// Get replies
const result = await ctx.prisma.offersProfile.findFirst({
include: {
discussion: {
include: {
replies: true,
replyingTo: true,
user: true
}
}
if (input.token === profileEditToken || input.userId) {
const createdReply = await ctx.prisma.offersReply.create({
data: {
message: input.message,
profile: {
connect: {
id: input.profileId,
},
},
},
});
if (input.replyingToId) {
await ctx.prisma.offersReply.update({
data: {
replyingTo: {
connect: {
id: input.replyingToId,
},
where: {
id: input.profileId
}
})
},
},
where: {
id: createdReply.id,
},
});
}
if (result) {
return result.discussion.filter((x) => x.replyingToId === null)
}
if (input.userId) {
await ctx.prisma.offersReply.update({
data: {
user: {
connect: {
id: input.userId,
},
},
},
where: {
id: createdReply.id,
},
});
}
const created = await ctx.prisma.offersReply.findFirst({
include: {
user: true
},
where: {
id: createdReply.id,
},
});
return result
const result: Reply = {
createdAt: created!.createdAt,
id: created!.id,
message: created!.message,
replies: [], // New message should have no replies
replyingToId: created!.replyingToId,
user: created!.user ?? {
email: '',
emailVerified: null,
id: '',
image: '',
name: profile?.profileName ?? '<missing name>',
}
}
})
.mutation("update", {
input: z.object({
id: z.string(),
message: z.string(),
profileId: z.string(),
// Have to pass in either userID or token for validation
token: z.string().optional(),
userId: z.string().optional(),
}),
async resolve({ ctx, input }) {
const messageToUpdate = await ctx.prisma.offersReply.findFirst({
where: {
id: input.id
}
})
const profile = await ctx.prisma.offersProfile.findFirst({
where: {
id: input.profileId,
},
});
const profileEditToken = profile?.editToken;
return result
}
// To validate user editing, OP or correct user
// TODO: improve validation process
if (profileEditToken === input.token || messageToUpdate?.userId === input.userId) {
await ctx.prisma.offersReply.update({
data: {
message: input.message
},
where: {
id: input.id
}
})
throw new trpc.TRPCError({
code: 'UNAUTHORIZED',
message: 'Missing userId or wrong token.',
});
},
})
.mutation('update', {
input: z.object({
id: z.string(),
message: z.string(),
profileId: z.string(),
// Have to pass in either userID or token for validation
token: z.string().optional(),
userId: z.string().optional(),
}),
async resolve({ ctx, input }) {
const messageToUpdate = await ctx.prisma.offersReply.findFirst({
where: {
id: input.id,
},
});
const profile = await ctx.prisma.offersProfile.findFirst({
where: {
id: input.profileId,
},
});
const result = await ctx.prisma.offersProfile.findFirst({
include: {
discussion: {
include: {
replies: true,
replyingTo: true,
user: true
}
}
},
where: {
id: input.profileId
}
})
const profileEditToken = profile?.editToken;
if (result) {
return result.discussion.filter((x) => x.replyingToId === null)
}
// To validate user editing, OP or correct user
// TODO: improve validation process
if (
profileEditToken === input.token ||
messageToUpdate?.userId === input.userId
) {
const updated = await ctx.prisma.offersReply.update({
data: {
message: input.message,
},
include: {
replies: {
include: {
user: true
}
},
user: true
},
where: {
id: input.id,
},
});
return result
const result: Reply = {
createdAt: updated!.createdAt,
id: updated!.id,
message: updated!.message,
replies: updated!.replies.map((x) => {
return {
createdAt: x.createdAt,
id: x.id,
message: x.message,
replies: [],
replyingToId: x.replyingToId,
user: x.user ?? {
email: '',
emailVerified: null,
id: '',
image: '',
name: profile?.profileName ?? '<missing name>',
}
}
throw new trpc.TRPCError({
code: 'UNAUTHORIZED',
message: 'Wrong userId or token.'
})
}),
replyingToId: updated!.replyingToId,
user: updated!.user ?? {
email: '',
emailVerified: null,
id: '',
image: '',
name: profile?.profileName ?? '<missing name>',
}
}
})
.mutation("delete", {
input: z.object({
id: z.string(),
profileId: z.string(),
// Have to pass in either userID or token for validation
token: z.string().optional(),
userId: z.string().optional(),
}),
async resolve({ ctx, input }) {
const messageToDelete = await ctx.prisma.offersReply.findFirst({
where: {
id: input.id
}
})
const profile = await ctx.prisma.offersProfile.findFirst({
where: {
id: input.profileId,
},
});
const profileEditToken = profile?.editToken;
return result
}
// To validate user editing, OP or correct user
// TODO: improve validation process
if (profileEditToken === input.token || messageToDelete?.userId === input.userId) {
await ctx.prisma.offersReply.delete({
where: {
id: input.id
}
})
const result = await ctx.prisma.offersProfile.findFirst({
include: {
discussion: {
include: {
replies: true,
replyingTo: true,
user: true
}
}
},
where: {
id: input.profileId
}
})
throw new trpc.TRPCError({
code: 'UNAUTHORIZED',
message: 'Wrong userId or token.',
});
},
})
.mutation('delete', {
input: z.object({
id: z.string(),
profileId: z.string(),
// Have to pass in either userID or token for validation
token: z.string().optional(),
userId: z.string().optional(),
}),
async resolve({ ctx, input }) {
const messageToDelete = await ctx.prisma.offersReply.findFirst({
where: {
id: input.id,
},
});
const profile = await ctx.prisma.offersProfile.findFirst({
where: {
id: input.profileId,
},
});
if (result) {
return result.discussion.filter((x) => x.replyingToId === null)
}
const profileEditToken = profile?.editToken;
return result
}
// To validate user editing, OP or correct user
// TODO: improve validation process
if (
profileEditToken === input.token ||
messageToDelete?.userId === input.userId
) {
await ctx.prisma.offersReply.delete({
where: {
id: input.id,
},
});
await ctx.prisma.offersProfile.findFirst({
include: {
discussion: {
include: {
replies: true,
replyingTo: true,
user: true,
},
},
},
where: {
id: input.profileId,
},
});
throw new trpc.TRPCError({
code: 'UNAUTHORIZED',
message: 'Wrong userId or token.'
})
}
})
// If (result) {
// return result.discussion.filter((x) => x.replyingToId === null);
// }
// return result;
}
throw new trpc.TRPCError({
code: 'UNAUTHORIZED',
message: 'Wrong userId or token.',
});
},
});

@ -2,9 +2,13 @@ import crypto, { randomUUID } from 'crypto';
import { z } from 'zod';
import * as trpc from '@trpc/server';
import { createRouter } from '../context';
import {
addToProfileResponseMapper,
createOfferProfileResponseMapper,
profileDtoMapper,
} from '~/mappers/offers-mappers';
import type { OffersProfile } from '~/types/offers-profile';
import { createRouter } from '../context';
const valuation = z.object({
currency: z.string(),
@ -19,41 +23,45 @@ const company = z.object({
logoUrl: z.string().nullish(),
name: z.string(),
slug: z.string(),
updatedAt: z.date()
})
updatedAt: z.date(),
});
const offer = z.object({
OffersFullTime: z.object({
baseSalary: valuation.nullish(),
baseSalaryId: z.string().nullish(),
bonus: valuation.nullish(),
bonusId: z.string().nullish(),
id: z.string().optional(),
level: z.string().nullish(),
specialization: z.string(),
stocks: valuation.nullish(),
stocksId: z.string().nullish(),
title: z.string(),
totalCompensation: valuation.nullish(),
totalCompensationId: z.string().nullish(),
}).nullish(),
OffersIntern: z.object({
id: z.string().optional(),
internshipCycle: z.string().nullish(),
monthlySalary: valuation.nullish(),
specialization: z.string(),
startYear: z.number().nullish(),
title: z.string(),
totalCompensation: valuation.nullish(), // Full time
}).nullish(),
comments: z.string().nullish(),
OffersFullTime: z
.object({
baseSalary: valuation.nullish(),
baseSalaryId: z.string().nullish(),
bonus: valuation.nullish(),
bonusId: z.string().nullish(),
id: z.string().optional(),
level: z.string().nullish(),
specialization: z.string(),
stocks: valuation.nullish(),
stocksId: z.string().nullish(),
title: z.string(),
totalCompensation: valuation.nullish(),
totalCompensationId: z.string().nullish(),
})
.nullish(),
OffersIntern: z
.object({
id: z.string().optional(),
internshipCycle: z.string().nullish(),
monthlySalary: valuation.nullish(),
specialization: z.string(),
startYear: z.number().nullish(),
title: z.string(),
totalCompensation: valuation.nullish(), // Full time
})
.nullish(),
comments: z.string(),
company: company.nullish(),
companyId: z.string(),
id: z.string().optional(),
jobType: z.string(),
location: z.string(),
monthYearReceived: z.date(),
negotiationStrategy: z.string().nullish(),
negotiationStrategy: z.string(),
offersFullTimeId: z.string().nullish(),
offersInternId: z.string().nullish(),
profileId: z.string().nullish(),
@ -72,7 +80,7 @@ const experience = z.object({
specialization: z.string().nullish(),
title: z.string().nullish(),
totalCompensation: valuation.nullish(),
totalCompensationId: z.string().nullish()
totalCompensationId: z.string().nullish(),
});
const education = z.object({
@ -91,32 +99,8 @@ const reply = z.object({
messages: z.string().nullish(),
profileId: z.string().nullish(),
replyingToId: z.string().nullish(),
userId: z.string().nullish()
})
type WithIsEditable<T> = T & {
isEditable: boolean;
};
function computeIsEditable(
profileInput: OffersProfile,
editToken?: string,
): WithIsEditable<OffersProfile> {
return {
...profileInput,
isEditable: profileInput.editToken === editToken,
};
}
function exclude<Key extends keyof WithIsEditable<OffersProfile>>(
profile: WithIsEditable<OffersProfile>,
...keys: Array<Key>
): Omit<WithIsEditable<OffersProfile>, Key> {
for (const key of keys) {
delete profile[key];
}
return profile;
}
userId: z.string().nullish(),
});
export const offersProfileRouter = createRouter()
.query('listOne', {
@ -127,6 +111,86 @@ export const offersProfileRouter = createRouter()
async resolve({ ctx, input }) {
const result = await ctx.prisma.offersProfile.findFirst({
include: {
analysis: {
include: {
overallHighestOffer: {
include: {
OffersFullTime: {
include: {
totalCompensation: true,
},
},
OffersIntern: {
include: {
monthlySalary: true,
},
},
company: true,
profile: {
include: {
background: true,
},
},
},
},
topCompanyOffers: {
include: {
OffersFullTime: {
include: {
totalCompensation: true,
},
},
OffersIntern: {
include: {
monthlySalary: true,
},
},
company: true,
profile: {
include: {
background: {
include: {
experiences: {
include: {
company: true,
},
},
},
},
},
},
},
},
topOverallOffers: {
include: {
OffersFullTime: {
include: {
totalCompensation: true,
},
},
OffersIntern: {
include: {
monthlySalary: true,
},
},
company: true,
profile: {
include: {
background: {
include: {
experiences: {
include: {
company: true,
},
},
},
},
},
},
},
},
},
},
background: {
include: {
educations: true,
@ -144,7 +208,7 @@ export const offersProfileRouter = createRouter()
include: {
replies: true,
replyingTo: true,
user: true
user: true,
},
},
offers: {
@ -172,7 +236,7 @@ export const offersProfileRouter = createRouter()
});
if (result) {
return exclude(computeIsEditable(result, input.token), 'editToken')
return profileDtoMapper(result, input.token);
}
throw new trpc.TRPCError({
@ -389,7 +453,8 @@ export const offersProfileRouter = createRouter()
title: x.OffersFullTime.title,
totalCompensation: {
create: {
currency: x.OffersFullTime.totalCompensation?.currency,
currency:
x.OffersFullTime.totalCompensation?.currency,
value: x.OffersFullTime.totalCompensation?.value,
},
},
@ -417,41 +482,9 @@ export const offersProfileRouter = createRouter()
},
profileName: randomUUID().substring(0, 10),
},
include: {
background: {
include: {
educations: true,
experiences: {
include: {
company: true,
monthlySalary: true,
totalCompensation: true,
},
},
specificYoes: true,
},
},
offers: {
include: {
OffersFullTime: {
include: {
baseSalary: true,
bonus: true,
stocks: true,
totalCompensation: true,
},
},
OffersIntern: {
include: {
monthlySalary: true,
},
},
},
},
},
});
// TODO: add analysis to profile object then return
return profile;
return createOfferProfileResponseMapper(profile, token);
},
})
.mutation('delete', {
@ -468,11 +501,13 @@ export const offersProfileRouter = createRouter()
const profileEditToken = profileToDelete?.editToken;
if (profileEditToken === input.token) {
return await ctx.prisma.offersProfile.delete({
const deletedProfile = await ctx.prisma.offersProfile.delete({
where: {
id: input.profileId,
},
});
return deletedProfile.id;
}
// TODO: Throw 401
throw new trpc.TRPCError({
@ -493,7 +528,7 @@ export const offersProfileRouter = createRouter()
backgroundId: z.string().optional(),
domain: z.string(),
id: z.string().optional(),
yoe: z.number()
yoe: z.number(),
}),
),
totalYoe: z.number(),
@ -505,7 +540,7 @@ export const offersProfileRouter = createRouter()
offers: z.array(offer),
profileName: z.string(),
token: z.string(),
userId: z.string().nullish()
userId: z.string().nullish(),
}),
async resolve({ ctx, input }) {
const profileToUpdate = await ctx.prisma.offersProfile.findFirst({
@ -522,17 +557,17 @@ export const offersProfileRouter = createRouter()
},
where: {
id: input.id,
}
},
});
await ctx.prisma.offersBackground.update({
data: {
totalYoe: input.background.totalYoe
totalYoe: input.background.totalYoe,
},
where: {
id: input.background.id
}
})
id: input.background.id,
},
});
for (const edu of input.background.educations) {
if (edu.id) {
@ -545,27 +580,26 @@ export const offersProfileRouter = createRouter()
type: edu.type,
},
where: {
id: edu.id
}
})
id: edu.id,
},
});
} else {
await ctx.prisma.offersBackground.update({
data: {
educations: {
create:
{
create: {
endDate: edu.endDate,
field: edu.field,
school: edu.school,
startDate: edu.startDate,
type: edu.type,
}
}
},
},
},
where: {
id: input.background.id
}
})
id: input.background.id,
},
});
}
}
@ -579,9 +613,9 @@ export const offersProfileRouter = createRouter()
specialization: exp.specialization,
},
where: {
id: exp.id
}
})
id: exp.id,
},
});
if (exp.monthlySalary) {
await ctx.prisma.offersCurrency.update({
@ -590,9 +624,9 @@ export const offersProfileRouter = createRouter()
value: exp.monthlySalary.value,
},
where: {
id: exp.monthlySalary.id
}
})
id: exp.monthlySalary.id,
},
});
}
if (exp.totalCompensation) {
@ -602,12 +636,16 @@ export const offersProfileRouter = createRouter()
value: exp.totalCompensation.value,
},
where: {
id: exp.totalCompensation.id
}
})
id: exp.totalCompensation.id,
},
});
}
} else if (!exp.id) {
if (exp.jobType === 'FULLTIME' && exp.totalCompensation?.currency !== undefined && exp.totalCompensation.value !== undefined) {
if (
exp.jobType === 'FULLTIME' &&
exp.totalCompensation?.currency !== undefined &&
exp.totalCompensation.value !== undefined
) {
if (exp.companyId) {
await ctx.prisma.offersBackground.update({
data: {
@ -630,12 +668,12 @@ export const offersProfileRouter = createRouter()
},
},
},
}
},
},
where: {
id: input.background.id
}
})
id: input.background.id,
},
});
} else {
await ctx.prisma.offersBackground.update({
data: {
@ -652,16 +690,15 @@ export const offersProfileRouter = createRouter()
value: exp.totalCompensation?.value,
},
},
}
}
},
},
},
where: {
id: input.background.id
}
})
id: input.background.id,
},
});
}
}
else if (
} else if (
exp.jobType === 'INTERN' &&
exp.monthlySalary?.currency !== undefined &&
exp.monthlySalary.value !== undefined
@ -686,13 +723,13 @@ export const offersProfileRouter = createRouter()
},
specialization: exp.specialization,
title: exp.title,
}
}
},
},
},
where: {
id: input.background.id
}
})
id: input.background.id,
},
});
} else {
await ctx.prisma.offersBackground.update({
data: {
@ -708,44 +745,42 @@ export const offersProfileRouter = createRouter()
},
specialization: exp.specialization,
title: exp.title,
}
}
},
},
},
where: {
id: input.background.id
}
})
id: input.background.id,
},
});
}
}
}
}
for (const yoe of input.background.specificYoes) {
if (yoe.id) {
await ctx.prisma.offersSpecificYoe.update({
data: {
...yoe
...yoe,
},
where: {
id: yoe.id
}
})
id: yoe.id,
},
});
} else {
await ctx.prisma.offersBackground.update({
data: {
specificYoes: {
create:
{
create: {
domain: yoe.domain,
yoe: yoe.yoe,
}
}
},
},
},
where: {
id: input.background.id
}
})
id: input.background.id,
},
});
}
}
@ -760,42 +795,46 @@ export const offersProfileRouter = createRouter()
negotiationStrategy: offerToUpdate.negotiationStrategy,
},
where: {
id: offerToUpdate.id
}
})
id: offerToUpdate.id,
},
});
if (offerToUpdate.jobType === "INTERN" || offerToUpdate.jobType === "FULLTIME") {
if (
offerToUpdate.jobType === 'INTERN' ||
offerToUpdate.jobType === 'FULLTIME'
) {
await ctx.prisma.offersOffer.update({
data: {
jobType: offerToUpdate.jobType
jobType: offerToUpdate.jobType,
},
where: {
id: offerToUpdate.id
}
})
id: offerToUpdate.id,
},
});
}
if (offerToUpdate.OffersIntern?.monthlySalary) {
await ctx.prisma.offersIntern.update({
data: {
internshipCycle: offerToUpdate.OffersIntern.internshipCycle ?? undefined,
internshipCycle:
offerToUpdate.OffersIntern.internshipCycle ?? undefined,
specialization: offerToUpdate.OffersIntern.specialization,
startYear: offerToUpdate.OffersIntern.startYear ?? undefined,
title: offerToUpdate.OffersIntern.title,
},
where: {
id: offerToUpdate.OffersIntern.id,
}
})
},
});
await ctx.prisma.offersCurrency.update({
data: {
currency: offerToUpdate.OffersIntern.monthlySalary.currency,
value: offerToUpdate.OffersIntern.monthlySalary.value
value: offerToUpdate.OffersIntern.monthlySalary.value,
},
where: {
id: offerToUpdate.OffersIntern.monthlySalary.id
}
})
id: offerToUpdate.OffersIntern.monthlySalary.id,
},
});
}
if (offerToUpdate.OffersFullTime?.totalCompensation) {
@ -807,54 +846,55 @@ export const offersProfileRouter = createRouter()
},
where: {
id: offerToUpdate.OffersFullTime.id,
}
})
},
});
if (offerToUpdate.OffersFullTime.baseSalary) {
await ctx.prisma.offersCurrency.update({
data: {
currency: offerToUpdate.OffersFullTime.baseSalary.currency,
value: offerToUpdate.OffersFullTime.baseSalary.value
value: offerToUpdate.OffersFullTime.baseSalary.value,
},
where: {
id: offerToUpdate.OffersFullTime.baseSalary.id
}
})
id: offerToUpdate.OffersFullTime.baseSalary.id,
},
});
}
if (offerToUpdate.OffersFullTime.bonus) {
await ctx.prisma.offersCurrency.update({
data: {
currency: offerToUpdate.OffersFullTime.bonus.currency,
value: offerToUpdate.OffersFullTime.bonus.value
value: offerToUpdate.OffersFullTime.bonus.value,
},
where: {
id: offerToUpdate.OffersFullTime.bonus.id
}
})
id: offerToUpdate.OffersFullTime.bonus.id,
},
});
}
if (offerToUpdate.OffersFullTime.stocks) {
await ctx.prisma.offersCurrency.update({
data: {
currency: offerToUpdate.OffersFullTime.stocks.currency,
value: offerToUpdate.OffersFullTime.stocks.value
value: offerToUpdate.OffersFullTime.stocks.value,
},
where: {
id: offerToUpdate.OffersFullTime.stocks.id
}
})
id: offerToUpdate.OffersFullTime.stocks.id,
},
});
}
await ctx.prisma.offersCurrency.update({
data: {
currency: offerToUpdate.OffersFullTime.totalCompensation.currency,
value: offerToUpdate.OffersFullTime.totalCompensation.value
currency:
offerToUpdate.OffersFullTime.totalCompensation.currency,
value: offerToUpdate.OffersFullTime.totalCompensation.value,
},
where: {
id: offerToUpdate.OffersFullTime.totalCompensation.id
}
})
id: offerToUpdate.OffersFullTime.totalCompensation.id,
},
});
}
} else {
if (
offerToUpdate.jobType === "INTERN" &&
offerToUpdate.jobType === 'INTERN' &&
offerToUpdate.OffersIntern &&
offerToUpdate.OffersIntern.internshipCycle &&
offerToUpdate.OffersIntern.monthlySalary?.currency &&
@ -867,14 +907,19 @@ export const offersProfileRouter = createRouter()
create: {
OffersIntern: {
create: {
internshipCycle: offerToUpdate.OffersIntern.internshipCycle,
internshipCycle:
offerToUpdate.OffersIntern.internshipCycle,
monthlySalary: {
create: {
currency: offerToUpdate.OffersIntern.monthlySalary?.currency,
value: offerToUpdate.OffersIntern.monthlySalary?.value,
currency:
offerToUpdate.OffersIntern.monthlySalary
?.currency,
value:
offerToUpdate.OffersIntern.monthlySalary?.value,
},
},
specialization: offerToUpdate.OffersIntern.specialization,
specialization:
offerToUpdate.OffersIntern.specialization,
startYear: offerToUpdate.OffersIntern.startYear,
title: offerToUpdate.OffersIntern.title,
},
@ -889,13 +934,13 @@ export const offersProfileRouter = createRouter()
location: offerToUpdate.location,
monthYearReceived: offerToUpdate.monthYearReceived,
negotiationStrategy: offerToUpdate.negotiationStrategy,
}
}
},
},
},
where: {
id: input.id,
}
})
},
});
}
if (
offerToUpdate.jobType === 'FULLTIME' &&
@ -918,29 +963,39 @@ export const offersProfileRouter = createRouter()
create: {
baseSalary: {
create: {
currency: offerToUpdate.OffersFullTime.baseSalary?.currency,
value: offerToUpdate.OffersFullTime.baseSalary?.value,
currency:
offerToUpdate.OffersFullTime.baseSalary
?.currency,
value:
offerToUpdate.OffersFullTime.baseSalary?.value,
},
},
bonus: {
create: {
currency: offerToUpdate.OffersFullTime.bonus?.currency,
currency:
offerToUpdate.OffersFullTime.bonus?.currency,
value: offerToUpdate.OffersFullTime.bonus?.value,
},
},
level: offerToUpdate.OffersFullTime.level,
specialization: offerToUpdate.OffersFullTime.specialization,
specialization:
offerToUpdate.OffersFullTime.specialization,
stocks: {
create: {
currency: offerToUpdate.OffersFullTime.stocks?.currency,
currency:
offerToUpdate.OffersFullTime.stocks?.currency,
value: offerToUpdate.OffersFullTime.stocks?.value,
},
},
title: offerToUpdate.OffersFullTime.title,
totalCompensation: {
create: {
currency: offerToUpdate.OffersFullTime.totalCompensation?.currency,
value: offerToUpdate.OffersFullTime.totalCompensation?.value,
currency:
offerToUpdate.OffersFullTime.totalCompensation
?.currency,
value:
offerToUpdate.OffersFullTime.totalCompensation
?.value,
},
},
},
@ -955,17 +1010,17 @@ export const offersProfileRouter = createRouter()
location: offerToUpdate.location,
monthYearReceived: offerToUpdate.monthYearReceived,
negotiationStrategy: offerToUpdate.negotiationStrategy,
}
}
},
},
},
where: {
id: input.id,
}
})
},
});
}
}
}
// TODO: add analysis to profile object then return
const result = await ctx.prisma.offersProfile.findFirst({
include: {
background: {
@ -985,7 +1040,7 @@ export const offersProfileRouter = createRouter()
include: {
replies: true,
replyingTo: true,
user: true
user: true,
},
},
offers: {
@ -1013,7 +1068,7 @@ export const offersProfileRouter = createRouter()
});
if (result) {
return exclude(computeIsEditable(result, input.token), 'editToken')
return createOfferProfileResponseMapper(result, input.token);
}
throw new trpc.TRPCError({
@ -1036,9 +1091,9 @@ export const offersProfileRouter = createRouter()
}),
async resolve({ ctx, input }) {
const profile = await ctx.prisma.offersProfile.findFirst({
where: {
id: input.profileId,
},
where: {
id: input.profileId,
},
});
const profileEditToken = profile?.editToken;
@ -1048,25 +1103,21 @@ export const offersProfileRouter = createRouter()
data: {
user: {
connect: {
id: input.userId
}
}
id: input.userId,
},
},
},
where: {
id: input.profileId
}
})
id: input.profileId,
},
});
return {
id: updated.id,
profileName: updated.profileName,
userId: updated.userId
}
return addToProfileResponseMapper(updated);
}
throw new trpc.TRPCError({
code: 'UNAUTHORIZED',
message: 'Invalid token.',
});
}
},
});

@ -1,6 +1,11 @@
import { z } from 'zod';
import { TRPCError } from '@trpc/server';
import {
dashboardOfferDtoMapper,
getOffersResponseMapper,
} from '~/mappers/offers-mappers';
import { createRouter } from '../context';
const yoeCategoryMap: Record<number, string> = {
@ -299,14 +304,14 @@ export const offersRouter = createRouter().query('list', {
: data.length;
const paginatedData = data.slice(startRecordIndex, endRecordIndex);
return {
data: paginatedData,
paging: {
currPage: input.offset,
numOfItemsInPage: paginatedData.length,
return getOffersResponseMapper(
paginatedData.map((offer) => dashboardOfferDtoMapper(offer)),
{
currentPage: input.offset,
numOfItems: paginatedData.length,
numOfPages: Math.ceil(data.length / input.limit),
totalNumberOfOffers: data.length,
totalItems: data.length,
},
};
);
},
});

@ -1,130 +0,0 @@
export type OffersProfile = {
background?: Background | null;
createdAt: Date;
// Discussions: Array<discussion>;
editToken: string;
id: string;
offers: Array<Offer>;
profileName: string;
userId?: string | null;
};
export type Background = {
educations: Array<Education>;
experiences: Array<Experience>;
id: string;
offersProfileId: string;
specificYoes: Array<SpecificYoe>;
totalYoe?: number | null;
}
export type Experience = {
backgroundId: string;
company?: Company | null;
companyId?: string | null;
durationInMonths?: number | null;
id: string;
jobType?: string | null;
level?: string | null;
monthlySalary?: Valuation | null;
monthlySalaryId?: string | null;
specialization?: string | null;
title?: string | null;
totalCompensation?: Valuation | null;
totalCompensationId?: string | null;
}
export type Company = {
createdAt: Date;
description: string | null;
id: string;
logoUrl: string | null;
name: string;
slug: string;
updatedAt: Date
}
export type Valuation = {
currency: string;
id: string;
value: number;
}
export type Education = {
backgroundId: string;
endDate?: Date | null;
field?: string | null;
id: string;
school?: string | null;
startDate?: Date | null;
type?: string | null;
}
export type SpecificYoe = {
backgroundId: string;
domain: string;
id: string;
yoe: number;
}
export type Offer = {
OfferFullTime?: OfferFullTime | null;
OfferIntern?: OfferIntern | null;
comments?: string | null;
company: Company;
companyId: string;
id: string;
jobType: string;
location: string;
monthYearReceived: Date;
negotiationStrategy?: string | null;
offersFullTimeId?: string | null;
offersInternId?: string | null;
profileId: string;
}
export type OfferFullTime = {
baseSalary: Valuation;
baseSalaryId: string;
bonus: Valuation;
bonusId: string;
id: string;
level: string;
specialization: string;
stocks: Valuation;
stocksId: string;
title?: string;
totalCompensation: Valuation;
totalCompensationId: string;
}
export type OfferIntern = {
id: string;
internshipCycle: string;
monthlySalary: Valuation;
monthlySalaryId: string;
specialization: string;
startYear: number;
title?: string;
}
export type Reply = {
createdAt: Date;
id: string;
message: string;
// Profile: OffersProfile | null;
profileId: string;
replies: Array<Discussion>?;
replyingTo: Discussion?;
replyingToId: string | null;
user: User?;
userId: string | null;
}
export type User = {
email: string?;
emailVerified: Date?;
id: string;
image: string?;
name: string?;
}

@ -0,0 +1,186 @@
import type { JobType } from '@prisma/client';
export type Profile = {
analysis: ProfileAnalysis?;
background: Background?;
editToken: string?;
id: string;
isEditable: boolean;
offers: Array<ProfileOffer>;
profileName: string;
};
export type Background = {
educations: Array<Education>;
experiences: Array<Experience>;
id: string;
specificYoes: Array<SpecificYoe>;
totalYoe: number;
};
export type Experience = {
company: OffersCompany?;
durationInMonths: number?;
id: string;
jobType: JobType?;
level: string?;
monthlySalary: Valuation?;
specialization: string?;
title: string?;
totalCompensation: Valuation?;
};
export type OffersCompany = {
createdAt: Date;
description: string;
id: string;
logoUrl: string;
name: string;
slug: string;
updatedAt: Date;
};
export type Valuation = {
currency: string;
value: number;
};
export type Education = {
endDate: Date?;
field: string?;
id: string;
school: string?;
startDate: Date?;
type: string?;
};
export type SpecificYoe = {
domain: string;
id: string;
yoe: number;
};
export type DashboardOffer = {
company: OffersCompany;
id: string;
income: Valuation;
monthYearReceived: Date;
profileId: string;
title: string;
totalYoe: number;
};
export type ProfileOffer = {
comments: string;
company: OffersCompany;
id: string;
jobType: JobType;
location: string;
monthYearReceived: Date;
negotiationStrategy: string;
offersFullTime: FullTime?;
offersIntern: Intern?;
};
export type FullTime = {
baseSalary: Valuation;
bonus: Valuation;
id: string;
level: string;
specialization: string;
stocks: Valuation;
title: string;
totalCompensation: Valuation;
};
export type Intern = {
id: string;
internshipCycle: string;
monthlySalary: Valuation;
specialization: string;
startYear: number;
title: string;
};
export type Reply = {
createdAt: Date;
id: string;
message: string;
replies: Array<Reply>?;
replyingToId: string?;
user: User?;
};
export type User = {
email: string?;
emailVerified: Date?;
id: string;
image: string?;
name: string?;
};
export type GetOffersResponse = {
data: Array<DashboardOffer>;
paging: Paging;
};
export type Paging = {
currentPage: number;
numOfItems: number;
numOfPages: number;
totalItems: number;
};
export type CreateOfferProfileResponse = {
id: string;
token: string;
};
export type OffersDiscussion = {
data: Array<Reply>;
};
export type ProfileAnalysis = {
companyAnalysis: Array<Analysis>;
id: string;
overallAnalysis: Analysis;
overallHighestOffer: AnalysisHighestOffer;
profileId: string;
};
export type Analysis = {
noOfOffers: number;
percentile: number;
topPercentileOffers: Array<AnalysisOffer>;
};
export type AnalysisHighestOffer = {
company: OffersCompany;
id: string;
level: string;
location: string;
specialization: string;
totalYoe: number;
};
export type AnalysisOffer = {
company: OffersCompany;
id: string;
income: number;
jobType: JobType;
level: string;
location: string;
monthYearReceived: Date;
negotiationStrategy: string;
previousCompanies: Array<string>;
profileName: string;
specialization: string;
title: string;
totalYoe: number;
};
export type AddToProfileResponse = {
id: string;
profileName: string;
userId: string;
};
Loading…
Cancel
Save