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

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

@ -1,11 +1,11 @@
import { Pagination } from '@tih/ui'; import { Pagination } from '@tih/ui';
import type { PaginationType } from '~/components/offers/table/types'; import type { Paging } from '~/types/offers';
type OffersTablePaginationProps = Readonly<{ type OffersTablePaginationProps = Readonly<{
endNumber: number; endNumber: number;
handlePageChange: (page: number) => void; handlePageChange: (page: number) => void;
pagination: PaginationType; pagination: Paging;
startNumber: number; startNumber: number;
}>; }>;
@ -30,13 +30,13 @@ export default function OffersTablePagination({
</span> </span>
</span> </span>
<Pagination <Pagination
current={pagination.currentPage} current={pagination.currentPage + 1}
end={pagination.numOfPages} end={pagination.numOfPages}
label="Pagination" label="Pagination"
pagePadding={1} pagePadding={1}
start={1} start={1}
onSelect={(currPage) => { onSelect={(currPage) => {
handlePageChange(currPage); handlePageChange(currPage - 1);
}} }}
/> />
</nav> </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 // eslint-disable-next-line no-shadow
export enum YOE_CATEGORY { export enum YOE_CATEGORY {
INTERN = 0, 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) { if (data?.offers) {
const filteredOffers: Array<OfferEntity> = data const filteredOffers: Array<OfferEntity> = data
? data?.offers.map((res) => { ? data?.offers.map((res) => {
if (res.OfferFullTime) { if (res.offersFullTime) {
const filteredOffer: OfferEntity = { const filteredOffer: OfferEntity = {
base: convertCurrencyToString( base: convertCurrencyToString(
res.OfferFullTime.baseSalary, res.offersFullTime.baseSalary,
),
bonus: convertCurrencyToString(
res.OfferFullTime.bonus,
), ),
bonus: convertCurrencyToString(res.offersFullTime.bonus),
companyName: res.company.name, companyName: res.company.name,
id: res.OfferFullTime.id, id: res.offersFullTime.id,
jobLevel: res.OfferFullTime.level, jobLevel: res.offersFullTime.level,
jobTitle: res.OfferFullTime.title, jobTitle: res.offersFullTime.title,
location: res.location, location: res.location,
negotiationStrategy: res.negotiationStrategy || '', negotiationStrategy: res.negotiationStrategy || '',
otherComment: res.comments || '', otherComment: res.comments || '',
receivedMonth: formatDate(res.monthYearReceived), receivedMonth: formatDate(res.monthYearReceived),
stocks: convertCurrencyToString(res.OfferFullTime.stocks), stocks: convertCurrencyToString(res.offersFullTime.stocks),
totalCompensation: convertCurrencyToString( totalCompensation: convertCurrencyToString(
res.OfferFullTime.totalCompensation, res.offersFullTime.totalCompensation,
), ),
}; };
@ -69,11 +67,11 @@ export default function OfferProfile() {
} }
const filteredOffer: OfferEntity = { const filteredOffer: OfferEntity = {
companyName: res.company.name, companyName: res.company.name,
id: res.OfferIntern!.id, id: res.offersIntern!.id,
jobTitle: res.OfferIntern!.title, jobTitle: res.offersIntern!.title,
location: res.location, location: res.location,
monthlySalary: convertCurrencyToString( monthlySalary: convertCurrencyToString(
res.OfferIntern!.monthlySalary, res.offersIntern!.monthlySalary,
), ),
negotiationStrategy: res.negotiationStrategy || '', negotiationStrategy: res.negotiationStrategy || '',
otherComment: res.comments || '', otherComment: res.comments || '',

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

@ -8,9 +8,10 @@ import type {
OffersOffer, OffersOffer,
OffersProfile, OffersProfile,
} from '@prisma/client'; } from '@prisma/client';
import { JobType } from '@prisma/client';
import { TRPCError } from '@trpc/server'; import { TRPCError } from '@trpc/server';
import { profileAnalysisDtoMapper } from '~/mappers/offers-mappers';
import { createRouter } from '../context'; import { createRouter } from '../context';
const searchOfferPercentile = ( const searchOfferPercentile = (
@ -27,9 +28,19 @@ const searchOfferPercentile = (
company: Company; company: Company;
profile: OffersProfile & { background: OffersBackground | null }; 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++) { for (let i = 0; i < similarOffers.length; i++) {
if (similarOffers[i].id === offer.id) { if (similarOffers[i].id === offer.id) {
return i; return i;
@ -39,116 +50,6 @@ const searchOfferPercentile = (
return -1; 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() export const offersAnalysisRouter = createRouter()
.query('generate', { .query('generate', {
input: z.object({ input: z.object({
@ -213,7 +114,7 @@ export const offersAnalysisRouter = createRouter()
const overallHighestOffer = offers[0]; const overallHighestOffer = offers[0];
// TODO: Shift yoe to background to make it mandatory // TODO: Shift yoe out of background to make it mandatory
if ( if (
!overallHighestOffer.profile.background || !overallHighestOffer.profile.background ||
!overallHighestOffer.profile.background.totalYoe !overallHighestOffer.profile.background.totalYoe
@ -465,17 +366,7 @@ export const offersAnalysisRouter = createRouter()
}, },
}); });
return profileAnalysisDtoMapper( return profileAnalysisDtoMapper(analysis);
analysis.id,
analysis.profileId,
overallHighestOffer,
noOfSimilarOffers,
overallPercentile,
topPercentileOffers,
noOfSimilarCompanyOffers,
companyPercentile,
topPercentileCompanyOffers,
);
}, },
}) })
.query('get', { .query('get', {
@ -574,16 +465,6 @@ export const offersAnalysisRouter = createRouter()
}); });
} }
return profileAnalysisDtoMapper( return profileAnalysisDtoMapper(analysis);
analysis.id,
analysis.profileId,
analysis.overallHighestOffer,
analysis.noOfSimilarOffers,
analysis.overallPercentile,
analysis.topOverallOffers,
analysis.noOfSimilarCompanyOffers,
analysis.companyPercentile,
analysis.topCompanyOffers,
);
}, },
}); });

@ -1,264 +1,329 @@
import { z } from 'zod'; import { z } from 'zod';
import * as trpc from '@trpc/server'; 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() const result = await ctx.prisma.offersProfile.findFirst({
.query('getComments', { include: {
input: z.object({ discussion: {
profileId: z.string() include: {
}), replies: {
async resolve({ ctx, input }) { include: {
user: true,
},
},
replyingTo: true,
user: true,
},
},
},
where: {
id: input.profileId,
},
});
const profile = await ctx.prisma.offersProfile.findFirst({ const discussions: OffersDiscussion = {
where: { data: result?.discussion
id: input.profileId .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({ x.replies?.map((y) => {
include: { if (y.user == null) {
discussion: { y.user = {
include: { email: '',
replies: { emailVerified: null,
include: { id: '',
user: true image: '',
} name: profile?.profileName ?? '<missing name>',
}, };
replyingTo: true,
user: true
}
}
},
where: {
id: input.profileId
} }
}) });
if (result) { const replyType: Reply = {
return result.discussion createdAt: x.createdAt,
.filter((x: Reply) => x.replyingToId === null) id: x.id,
.map((x: Reply) => { message: x.message,
if (x.user == null) { replies: x.replies.map((reply) => {
x.user = { return {
email: "", createdAt: reply.createdAt,
emailVerified: null, id: reply.id,
id: "", message: reply.message,
image: "", replies: [],
name: profile?.profileName ?? "<missing name>" replyingToId: reply.replyingToId,
} user: reply.user
} }
}),
replyingToId: x.replyingToId,
user: x.user
}
x.replies?.map((y) => { return replyType
if (y.user == null) { }) ?? []
y.user = { }
email: "",
emailVerified: null,
id: "",
image: "",
name: profile?.profileName ?? "<missing name>"
}
}
})
return x;
})
}
return result return discussions
} },
}) })
.mutation("create", { .mutation('create', {
input: z.object({ input: z.object({
message: z.string(), message: z.string(),
profileId: z.string(), profileId: z.string(),
replyingToId: z.string().optional(), replyingToId: z.string().optional(),
userId: z.string().optional() token: z.string().optional(),
}), userId: z.string().optional()
async resolve({ ctx, input }) { }),
const createdReply = await ctx.prisma.offersReply.create({ async resolve({ ctx, input }) {
data: { const profile = await ctx.prisma.offersProfile.findFirst({
message: input.message, where: {
profile: { id: input.profileId,
connect: { },
id: input.profileId });
}
}
}
})
if (input.replyingToId) { const profileEditToken = profile?.editToken;
await ctx.prisma.offersReply.update({
data: {
replyingTo: {
connect: {
id: input.replyingToId
}
}
},
where: {
id: createdReply.id
}
})
}
if (input.userId) { if (input.token === profileEditToken || input.userId) {
await ctx.prisma.offersReply.update({ const createdReply = await ctx.prisma.offersReply.create({
data: { data: {
user: { message: input.message,
connect: { profile: {
id: input.userId connect: {
} id: input.profileId,
} },
}, },
where: { },
id: createdReply.id });
}
}) if (input.replyingToId) {
} await ctx.prisma.offersReply.update({
// Get replies data: {
const result = await ctx.prisma.offersProfile.findFirst({ replyingTo: {
include: { connect: {
discussion: { id: input.replyingToId,
include: {
replies: true,
replyingTo: true,
user: true
}
}
}, },
where: { },
id: input.profileId },
} where: {
}) id: createdReply.id,
},
});
}
if (result) { if (input.userId) {
return result.discussion.filter((x) => x.replyingToId === null) 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 throw new trpc.TRPCError({
// TODO: improve validation process code: 'UNAUTHORIZED',
if (profileEditToken === input.token || messageToUpdate?.userId === input.userId) { message: 'Missing userId or wrong token.',
await ctx.prisma.offersReply.update({ });
data: { },
message: input.message })
}, .mutation('update', {
where: { input: z.object({
id: input.id 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({ const profileEditToken = profile?.editToken;
include: {
discussion: {
include: {
replies: true,
replyingTo: true,
user: true
}
}
},
where: {
id: input.profileId
}
})
if (result) { // To validate user editing, OP or correct user
return result.discussion.filter((x) => x.replyingToId === null) // 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({ replyingToId: updated!.replyingToId,
code: 'UNAUTHORIZED', user: updated!.user ?? {
message: 'Wrong userId or token.' 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 throw new trpc.TRPCError({
// TODO: improve validation process code: 'UNAUTHORIZED',
if (profileEditToken === input.token || messageToDelete?.userId === input.userId) { message: 'Wrong userId or token.',
await ctx.prisma.offersReply.delete({ });
where: { },
id: input.id })
} .mutation('delete', {
}) input: z.object({
const result = await ctx.prisma.offersProfile.findFirst({ id: z.string(),
include: { profileId: z.string(),
discussion: { // Have to pass in either userID or token for validation
include: { token: z.string().optional(),
replies: true, userId: z.string().optional(),
replyingTo: true, }),
user: true async resolve({ ctx, input }) {
} const messageToDelete = await ctx.prisma.offersReply.findFirst({
} where: {
}, id: input.id,
where: { },
id: input.profileId });
} const profile = await ctx.prisma.offersProfile.findFirst({
}) where: {
id: input.profileId,
},
});
if (result) { const profileEditToken = profile?.editToken;
return result.discussion.filter((x) => x.replyingToId === null)
}
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({ // If (result) {
code: 'UNAUTHORIZED', // return result.discussion.filter((x) => x.replyingToId === null);
message: 'Wrong userId or token.' // }
})
} // 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 { z } from 'zod';
import * as trpc from '@trpc/server'; 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({ const valuation = z.object({
currency: z.string(), currency: z.string(),
@ -19,41 +23,45 @@ const company = z.object({
logoUrl: z.string().nullish(), logoUrl: z.string().nullish(),
name: z.string(), name: z.string(),
slug: z.string(), slug: z.string(),
updatedAt: z.date() updatedAt: z.date(),
}) });
const offer = z.object({ const offer = z.object({
OffersFullTime: z.object({ OffersFullTime: z
baseSalary: valuation.nullish(), .object({
baseSalaryId: z.string().nullish(), baseSalary: valuation.nullish(),
bonus: valuation.nullish(), baseSalaryId: z.string().nullish(),
bonusId: z.string().nullish(), bonus: valuation.nullish(),
id: z.string().optional(), bonusId: z.string().nullish(),
level: z.string().nullish(), id: z.string().optional(),
specialization: z.string(), level: z.string().nullish(),
stocks: valuation.nullish(), specialization: z.string(),
stocksId: z.string().nullish(), stocks: valuation.nullish(),
title: z.string(), stocksId: z.string().nullish(),
totalCompensation: valuation.nullish(), title: z.string(),
totalCompensationId: z.string().nullish(), totalCompensation: valuation.nullish(),
}).nullish(), totalCompensationId: z.string().nullish(),
OffersIntern: z.object({ })
id: z.string().optional(), .nullish(),
internshipCycle: z.string().nullish(), OffersIntern: z
monthlySalary: valuation.nullish(), .object({
specialization: z.string(), id: z.string().optional(),
startYear: z.number().nullish(), internshipCycle: z.string().nullish(),
title: z.string(), monthlySalary: valuation.nullish(),
totalCompensation: valuation.nullish(), // Full time specialization: z.string(),
}).nullish(), startYear: z.number().nullish(),
comments: z.string().nullish(), title: z.string(),
totalCompensation: valuation.nullish(), // Full time
})
.nullish(),
comments: z.string(),
company: company.nullish(), company: company.nullish(),
companyId: z.string(), companyId: z.string(),
id: z.string().optional(), id: z.string().optional(),
jobType: z.string(), jobType: z.string(),
location: z.string(), location: z.string(),
monthYearReceived: z.date(), monthYearReceived: z.date(),
negotiationStrategy: z.string().nullish(), negotiationStrategy: z.string(),
offersFullTimeId: z.string().nullish(), offersFullTimeId: z.string().nullish(),
offersInternId: z.string().nullish(), offersInternId: z.string().nullish(),
profileId: z.string().nullish(), profileId: z.string().nullish(),
@ -72,7 +80,7 @@ const experience = z.object({
specialization: z.string().nullish(), specialization: z.string().nullish(),
title: z.string().nullish(), title: z.string().nullish(),
totalCompensation: valuation.nullish(), totalCompensation: valuation.nullish(),
totalCompensationId: z.string().nullish() totalCompensationId: z.string().nullish(),
}); });
const education = z.object({ const education = z.object({
@ -91,32 +99,8 @@ const reply = z.object({
messages: z.string().nullish(), messages: z.string().nullish(),
profileId: z.string().nullish(), profileId: z.string().nullish(),
replyingToId: z.string().nullish(), replyingToId: z.string().nullish(),
userId: 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;
}
export const offersProfileRouter = createRouter() export const offersProfileRouter = createRouter()
.query('listOne', { .query('listOne', {
@ -127,6 +111,86 @@ export const offersProfileRouter = createRouter()
async resolve({ ctx, input }) { async resolve({ ctx, input }) {
const result = await ctx.prisma.offersProfile.findFirst({ const result = await ctx.prisma.offersProfile.findFirst({
include: { 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: { background: {
include: { include: {
educations: true, educations: true,
@ -144,7 +208,7 @@ export const offersProfileRouter = createRouter()
include: { include: {
replies: true, replies: true,
replyingTo: true, replyingTo: true,
user: true user: true,
}, },
}, },
offers: { offers: {
@ -172,7 +236,7 @@ export const offersProfileRouter = createRouter()
}); });
if (result) { if (result) {
return exclude(computeIsEditable(result, input.token), 'editToken') return profileDtoMapper(result, input.token);
} }
throw new trpc.TRPCError({ throw new trpc.TRPCError({
@ -389,7 +453,8 @@ export const offersProfileRouter = createRouter()
title: x.OffersFullTime.title, title: x.OffersFullTime.title,
totalCompensation: { totalCompensation: {
create: { create: {
currency: x.OffersFullTime.totalCompensation?.currency, currency:
x.OffersFullTime.totalCompensation?.currency,
value: x.OffersFullTime.totalCompensation?.value, value: x.OffersFullTime.totalCompensation?.value,
}, },
}, },
@ -417,41 +482,9 @@ export const offersProfileRouter = createRouter()
}, },
profileName: randomUUID().substring(0, 10), 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', { .mutation('delete', {
@ -468,11 +501,13 @@ export const offersProfileRouter = createRouter()
const profileEditToken = profileToDelete?.editToken; const profileEditToken = profileToDelete?.editToken;
if (profileEditToken === input.token) { if (profileEditToken === input.token) {
return await ctx.prisma.offersProfile.delete({ const deletedProfile = await ctx.prisma.offersProfile.delete({
where: { where: {
id: input.profileId, id: input.profileId,
}, },
}); });
return deletedProfile.id;
} }
// TODO: Throw 401 // TODO: Throw 401
throw new trpc.TRPCError({ throw new trpc.TRPCError({
@ -493,7 +528,7 @@ export const offersProfileRouter = createRouter()
backgroundId: z.string().optional(), backgroundId: z.string().optional(),
domain: z.string(), domain: z.string(),
id: z.string().optional(), id: z.string().optional(),
yoe: z.number() yoe: z.number(),
}), }),
), ),
totalYoe: z.number(), totalYoe: z.number(),
@ -505,7 +540,7 @@ export const offersProfileRouter = createRouter()
offers: z.array(offer), offers: z.array(offer),
profileName: z.string(), profileName: z.string(),
token: z.string(), token: z.string(),
userId: z.string().nullish() userId: z.string().nullish(),
}), }),
async resolve({ ctx, input }) { async resolve({ ctx, input }) {
const profileToUpdate = await ctx.prisma.offersProfile.findFirst({ const profileToUpdate = await ctx.prisma.offersProfile.findFirst({
@ -522,17 +557,17 @@ export const offersProfileRouter = createRouter()
}, },
where: { where: {
id: input.id, id: input.id,
} },
}); });
await ctx.prisma.offersBackground.update({ await ctx.prisma.offersBackground.update({
data: { data: {
totalYoe: input.background.totalYoe totalYoe: input.background.totalYoe,
}, },
where: { where: {
id: input.background.id id: input.background.id,
} },
}) });
for (const edu of input.background.educations) { for (const edu of input.background.educations) {
if (edu.id) { if (edu.id) {
@ -545,27 +580,26 @@ export const offersProfileRouter = createRouter()
type: edu.type, type: edu.type,
}, },
where: { where: {
id: edu.id id: edu.id,
} },
}) });
} else { } else {
await ctx.prisma.offersBackground.update({ await ctx.prisma.offersBackground.update({
data: { data: {
educations: { educations: {
create: create: {
{
endDate: edu.endDate, endDate: edu.endDate,
field: edu.field, field: edu.field,
school: edu.school, school: edu.school,
startDate: edu.startDate, startDate: edu.startDate,
type: edu.type, type: edu.type,
} },
} },
}, },
where: { where: {
id: input.background.id id: input.background.id,
} },
}) });
} }
} }
@ -579,9 +613,9 @@ export const offersProfileRouter = createRouter()
specialization: exp.specialization, specialization: exp.specialization,
}, },
where: { where: {
id: exp.id id: exp.id,
} },
}) });
if (exp.monthlySalary) { if (exp.monthlySalary) {
await ctx.prisma.offersCurrency.update({ await ctx.prisma.offersCurrency.update({
@ -590,9 +624,9 @@ export const offersProfileRouter = createRouter()
value: exp.monthlySalary.value, value: exp.monthlySalary.value,
}, },
where: { where: {
id: exp.monthlySalary.id id: exp.monthlySalary.id,
} },
}) });
} }
if (exp.totalCompensation) { if (exp.totalCompensation) {
@ -602,12 +636,16 @@ export const offersProfileRouter = createRouter()
value: exp.totalCompensation.value, value: exp.totalCompensation.value,
}, },
where: { where: {
id: exp.totalCompensation.id id: exp.totalCompensation.id,
} },
}) });
} }
} else if (!exp.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) { if (exp.companyId) {
await ctx.prisma.offersBackground.update({ await ctx.prisma.offersBackground.update({
data: { data: {
@ -630,12 +668,12 @@ export const offersProfileRouter = createRouter()
}, },
}, },
}, },
} },
}, },
where: { where: {
id: input.background.id id: input.background.id,
} },
}) });
} else { } else {
await ctx.prisma.offersBackground.update({ await ctx.prisma.offersBackground.update({
data: { data: {
@ -652,16 +690,15 @@ export const offersProfileRouter = createRouter()
value: exp.totalCompensation?.value, value: exp.totalCompensation?.value,
}, },
}, },
} },
} },
}, },
where: { where: {
id: input.background.id id: input.background.id,
} },
}) });
} }
} } else if (
else if (
exp.jobType === 'INTERN' && exp.jobType === 'INTERN' &&
exp.monthlySalary?.currency !== undefined && exp.monthlySalary?.currency !== undefined &&
exp.monthlySalary.value !== undefined exp.monthlySalary.value !== undefined
@ -686,13 +723,13 @@ export const offersProfileRouter = createRouter()
}, },
specialization: exp.specialization, specialization: exp.specialization,
title: exp.title, title: exp.title,
} },
} },
}, },
where: { where: {
id: input.background.id id: input.background.id,
} },
}) });
} else { } else {
await ctx.prisma.offersBackground.update({ await ctx.prisma.offersBackground.update({
data: { data: {
@ -708,44 +745,42 @@ export const offersProfileRouter = createRouter()
}, },
specialization: exp.specialization, specialization: exp.specialization,
title: exp.title, title: exp.title,
} },
} },
}, },
where: { where: {
id: input.background.id id: input.background.id,
} },
}) });
} }
} }
} }
} }
for (const yoe of input.background.specificYoes) { for (const yoe of input.background.specificYoes) {
if (yoe.id) { if (yoe.id) {
await ctx.prisma.offersSpecificYoe.update({ await ctx.prisma.offersSpecificYoe.update({
data: { data: {
...yoe ...yoe,
}, },
where: { where: {
id: yoe.id id: yoe.id,
} },
}) });
} else { } else {
await ctx.prisma.offersBackground.update({ await ctx.prisma.offersBackground.update({
data: { data: {
specificYoes: { specificYoes: {
create: create: {
{
domain: yoe.domain, domain: yoe.domain,
yoe: yoe.yoe, yoe: yoe.yoe,
} },
} },
}, },
where: { where: {
id: input.background.id id: input.background.id,
} },
}) });
} }
} }
@ -760,42 +795,46 @@ export const offersProfileRouter = createRouter()
negotiationStrategy: offerToUpdate.negotiationStrategy, negotiationStrategy: offerToUpdate.negotiationStrategy,
}, },
where: { 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({ await ctx.prisma.offersOffer.update({
data: { data: {
jobType: offerToUpdate.jobType jobType: offerToUpdate.jobType,
}, },
where: { where: {
id: offerToUpdate.id id: offerToUpdate.id,
} },
}) });
} }
if (offerToUpdate.OffersIntern?.monthlySalary) { if (offerToUpdate.OffersIntern?.monthlySalary) {
await ctx.prisma.offersIntern.update({ await ctx.prisma.offersIntern.update({
data: { data: {
internshipCycle: offerToUpdate.OffersIntern.internshipCycle ?? undefined, internshipCycle:
offerToUpdate.OffersIntern.internshipCycle ?? undefined,
specialization: offerToUpdate.OffersIntern.specialization, specialization: offerToUpdate.OffersIntern.specialization,
startYear: offerToUpdate.OffersIntern.startYear ?? undefined, startYear: offerToUpdate.OffersIntern.startYear ?? undefined,
title: offerToUpdate.OffersIntern.title, title: offerToUpdate.OffersIntern.title,
}, },
where: { where: {
id: offerToUpdate.OffersIntern.id, id: offerToUpdate.OffersIntern.id,
} },
}) });
await ctx.prisma.offersCurrency.update({ await ctx.prisma.offersCurrency.update({
data: { data: {
currency: offerToUpdate.OffersIntern.monthlySalary.currency, currency: offerToUpdate.OffersIntern.monthlySalary.currency,
value: offerToUpdate.OffersIntern.monthlySalary.value value: offerToUpdate.OffersIntern.monthlySalary.value,
}, },
where: { where: {
id: offerToUpdate.OffersIntern.monthlySalary.id id: offerToUpdate.OffersIntern.monthlySalary.id,
} },
}) });
} }
if (offerToUpdate.OffersFullTime?.totalCompensation) { if (offerToUpdate.OffersFullTime?.totalCompensation) {
@ -807,54 +846,55 @@ export const offersProfileRouter = createRouter()
}, },
where: { where: {
id: offerToUpdate.OffersFullTime.id, id: offerToUpdate.OffersFullTime.id,
} },
}) });
if (offerToUpdate.OffersFullTime.baseSalary) { if (offerToUpdate.OffersFullTime.baseSalary) {
await ctx.prisma.offersCurrency.update({ await ctx.prisma.offersCurrency.update({
data: { data: {
currency: offerToUpdate.OffersFullTime.baseSalary.currency, currency: offerToUpdate.OffersFullTime.baseSalary.currency,
value: offerToUpdate.OffersFullTime.baseSalary.value value: offerToUpdate.OffersFullTime.baseSalary.value,
}, },
where: { where: {
id: offerToUpdate.OffersFullTime.baseSalary.id id: offerToUpdate.OffersFullTime.baseSalary.id,
} },
}) });
} }
if (offerToUpdate.OffersFullTime.bonus) { if (offerToUpdate.OffersFullTime.bonus) {
await ctx.prisma.offersCurrency.update({ await ctx.prisma.offersCurrency.update({
data: { data: {
currency: offerToUpdate.OffersFullTime.bonus.currency, currency: offerToUpdate.OffersFullTime.bonus.currency,
value: offerToUpdate.OffersFullTime.bonus.value value: offerToUpdate.OffersFullTime.bonus.value,
}, },
where: { where: {
id: offerToUpdate.OffersFullTime.bonus.id id: offerToUpdate.OffersFullTime.bonus.id,
} },
}) });
} }
if (offerToUpdate.OffersFullTime.stocks) { if (offerToUpdate.OffersFullTime.stocks) {
await ctx.prisma.offersCurrency.update({ await ctx.prisma.offersCurrency.update({
data: { data: {
currency: offerToUpdate.OffersFullTime.stocks.currency, currency: offerToUpdate.OffersFullTime.stocks.currency,
value: offerToUpdate.OffersFullTime.stocks.value value: offerToUpdate.OffersFullTime.stocks.value,
}, },
where: { where: {
id: offerToUpdate.OffersFullTime.stocks.id id: offerToUpdate.OffersFullTime.stocks.id,
} },
}) });
} }
await ctx.prisma.offersCurrency.update({ await ctx.prisma.offersCurrency.update({
data: { data: {
currency: offerToUpdate.OffersFullTime.totalCompensation.currency, currency:
value: offerToUpdate.OffersFullTime.totalCompensation.value offerToUpdate.OffersFullTime.totalCompensation.currency,
value: offerToUpdate.OffersFullTime.totalCompensation.value,
}, },
where: { where: {
id: offerToUpdate.OffersFullTime.totalCompensation.id id: offerToUpdate.OffersFullTime.totalCompensation.id,
} },
}) });
} }
} else { } else {
if ( if (
offerToUpdate.jobType === "INTERN" && offerToUpdate.jobType === 'INTERN' &&
offerToUpdate.OffersIntern && offerToUpdate.OffersIntern &&
offerToUpdate.OffersIntern.internshipCycle && offerToUpdate.OffersIntern.internshipCycle &&
offerToUpdate.OffersIntern.monthlySalary?.currency && offerToUpdate.OffersIntern.monthlySalary?.currency &&
@ -867,14 +907,19 @@ export const offersProfileRouter = createRouter()
create: { create: {
OffersIntern: { OffersIntern: {
create: { create: {
internshipCycle: offerToUpdate.OffersIntern.internshipCycle, internshipCycle:
offerToUpdate.OffersIntern.internshipCycle,
monthlySalary: { monthlySalary: {
create: { create: {
currency: offerToUpdate.OffersIntern.monthlySalary?.currency, currency:
value: offerToUpdate.OffersIntern.monthlySalary?.value, offerToUpdate.OffersIntern.monthlySalary
?.currency,
value:
offerToUpdate.OffersIntern.monthlySalary?.value,
}, },
}, },
specialization: offerToUpdate.OffersIntern.specialization, specialization:
offerToUpdate.OffersIntern.specialization,
startYear: offerToUpdate.OffersIntern.startYear, startYear: offerToUpdate.OffersIntern.startYear,
title: offerToUpdate.OffersIntern.title, title: offerToUpdate.OffersIntern.title,
}, },
@ -889,13 +934,13 @@ export const offersProfileRouter = createRouter()
location: offerToUpdate.location, location: offerToUpdate.location,
monthYearReceived: offerToUpdate.monthYearReceived, monthYearReceived: offerToUpdate.monthYearReceived,
negotiationStrategy: offerToUpdate.negotiationStrategy, negotiationStrategy: offerToUpdate.negotiationStrategy,
} },
} },
}, },
where: { where: {
id: input.id, id: input.id,
} },
}) });
} }
if ( if (
offerToUpdate.jobType === 'FULLTIME' && offerToUpdate.jobType === 'FULLTIME' &&
@ -918,29 +963,39 @@ export const offersProfileRouter = createRouter()
create: { create: {
baseSalary: { baseSalary: {
create: { create: {
currency: offerToUpdate.OffersFullTime.baseSalary?.currency, currency:
value: offerToUpdate.OffersFullTime.baseSalary?.value, offerToUpdate.OffersFullTime.baseSalary
?.currency,
value:
offerToUpdate.OffersFullTime.baseSalary?.value,
}, },
}, },
bonus: { bonus: {
create: { create: {
currency: offerToUpdate.OffersFullTime.bonus?.currency, currency:
offerToUpdate.OffersFullTime.bonus?.currency,
value: offerToUpdate.OffersFullTime.bonus?.value, value: offerToUpdate.OffersFullTime.bonus?.value,
}, },
}, },
level: offerToUpdate.OffersFullTime.level, level: offerToUpdate.OffersFullTime.level,
specialization: offerToUpdate.OffersFullTime.specialization, specialization:
offerToUpdate.OffersFullTime.specialization,
stocks: { stocks: {
create: { create: {
currency: offerToUpdate.OffersFullTime.stocks?.currency, currency:
offerToUpdate.OffersFullTime.stocks?.currency,
value: offerToUpdate.OffersFullTime.stocks?.value, value: offerToUpdate.OffersFullTime.stocks?.value,
}, },
}, },
title: offerToUpdate.OffersFullTime.title, title: offerToUpdate.OffersFullTime.title,
totalCompensation: { totalCompensation: {
create: { create: {
currency: offerToUpdate.OffersFullTime.totalCompensation?.currency, currency:
value: offerToUpdate.OffersFullTime.totalCompensation?.value, offerToUpdate.OffersFullTime.totalCompensation
?.currency,
value:
offerToUpdate.OffersFullTime.totalCompensation
?.value,
}, },
}, },
}, },
@ -955,17 +1010,17 @@ export const offersProfileRouter = createRouter()
location: offerToUpdate.location, location: offerToUpdate.location,
monthYearReceived: offerToUpdate.monthYearReceived, monthYearReceived: offerToUpdate.monthYearReceived,
negotiationStrategy: offerToUpdate.negotiationStrategy, negotiationStrategy: offerToUpdate.negotiationStrategy,
} },
} },
}, },
where: { where: {
id: input.id, id: input.id,
} },
}) });
} }
} }
} }
// TODO: add analysis to profile object then return
const result = await ctx.prisma.offersProfile.findFirst({ const result = await ctx.prisma.offersProfile.findFirst({
include: { include: {
background: { background: {
@ -985,7 +1040,7 @@ export const offersProfileRouter = createRouter()
include: { include: {
replies: true, replies: true,
replyingTo: true, replyingTo: true,
user: true user: true,
}, },
}, },
offers: { offers: {
@ -1013,7 +1068,7 @@ export const offersProfileRouter = createRouter()
}); });
if (result) { if (result) {
return exclude(computeIsEditable(result, input.token), 'editToken') return createOfferProfileResponseMapper(result, input.token);
} }
throw new trpc.TRPCError({ throw new trpc.TRPCError({
@ -1036,9 +1091,9 @@ export const offersProfileRouter = createRouter()
}), }),
async resolve({ ctx, input }) { async resolve({ ctx, input }) {
const profile = await ctx.prisma.offersProfile.findFirst({ const profile = await ctx.prisma.offersProfile.findFirst({
where: { where: {
id: input.profileId, id: input.profileId,
}, },
}); });
const profileEditToken = profile?.editToken; const profileEditToken = profile?.editToken;
@ -1048,25 +1103,21 @@ export const offersProfileRouter = createRouter()
data: { data: {
user: { user: {
connect: { connect: {
id: input.userId id: input.userId,
} },
} },
}, },
where: { where: {
id: input.profileId id: input.profileId,
} },
}) });
return { return addToProfileResponseMapper(updated);
id: updated.id,
profileName: updated.profileName,
userId: updated.userId
}
} }
throw new trpc.TRPCError({ throw new trpc.TRPCError({
code: 'UNAUTHORIZED', code: 'UNAUTHORIZED',
message: 'Invalid token.', message: 'Invalid token.',
}); });
} },
}); });

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