diff --git a/apps/portal/prisma/schema.prisma b/apps/portal/prisma/schema.prisma index aa29f612..4085e88b 100644 --- a/apps/portal/prisma/schema.prisma +++ b/apps/portal/prisma/schema.prisma @@ -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") } diff --git a/apps/portal/src/components/offers/table/OffersRow.tsx b/apps/portal/src/components/offers/table/OffersRow.tsx index 8ab39bdd..d5039014 100644 --- a/apps/portal/src/components/offers/table/OffersRow.tsx +++ b/apps/portal/src/components/offers/table/OffersRow.tsx @@ -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 ( - {company} + {company.name} {title} - {yoe} - {salary} - {date} + {totalYoe} + {convertCurrencyToString(income)} + {formatDate(monthYearReceived)} ({ - currentPage: 1, - numOfItems: 1, + const [pagination, setPagination] = useState({ + currentPage: 0, + numOfItems: 0, numOfPages: 0, totalItems: 0, }); - const [offers, setOffers] = useState>([]); + const [offers, setOffers] = useState>([]); 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({ )} diff --git a/apps/portal/src/components/offers/table/OffersTablePagination.tsx b/apps/portal/src/components/offers/table/OffersTablePagination.tsx index e7346c44..0800a529 100644 --- a/apps/portal/src/components/offers/table/OffersTablePagination.tsx +++ b/apps/portal/src/components/offers/table/OffersTablePagination.tsx @@ -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({ { - handlePageChange(currPage); + handlePageChange(currPage - 1); }} /> diff --git a/apps/portal/src/components/offers/table/types.ts b/apps/portal/src/components/offers/table/types.ts index 9522a9bb..c7d92680 100644 --- a/apps/portal/src/components/offers/table/types.ts +++ b/apps/portal/src/components/offers/table/types.ts @@ -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, diff --git a/apps/portal/src/mappers/offers-mappers.ts b/apps/portal/src/mappers/offers-mappers.ts new file mode 100644 index 00000000..1a6e415c --- /dev/null +++ b/apps/portal/src/mappers/offers-mappers.ts @@ -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; + experiences: Array< + OffersExperience & { + company: Company | null; + monthlySalary: OffersCurrency | null; + totalCompensation: OffersCurrency | null; + } + >; + specificYoes: Array; + }) + | 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; + experiences: Array< + OffersExperience & { + company: Company | null; + monthlySalary: OffersCurrency | null; + totalCompensation: OffersCurrency | null; + } + >; + specificYoes: Array; + }) + | null; + discussion: Array< + OffersReply & { + replies: Array; + 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, + paging: Paging, +) => { + const getOffersResponse: GetOffersResponse = { + data, + paging, + }; + return getOffersResponse; +}; diff --git a/apps/portal/src/pages/offers/profile/[offerProfileId].tsx b/apps/portal/src/pages/offers/profile/[offerProfileId].tsx index ba629b87..db1965ca 100644 --- a/apps/portal/src/pages/offers/profile/[offerProfileId].tsx +++ b/apps/portal/src/pages/offers/profile/[offerProfileId].tsx @@ -43,25 +43,23 @@ export default function OfferProfile() { if (data?.offers) { const filteredOffers: Array = 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 || '', diff --git a/apps/portal/src/pages/offers/test/createProfile.tsx b/apps/portal/src/pages/offers/test/createProfile.tsx index e47980b3..6aad538b 100644 --- a/apps/portal/src/pages/offers/test/createProfile.tsx +++ b/apps/portal/src/pages/offers/test/createProfile.tsx @@ -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 ( <>
{createdData}
-
{JSON.stringify(replies.data)}
+
{JSON.stringify(replies.data?.data)}
diff --git a/apps/portal/src/server/router/offers/offers-analysis-router.ts b/apps/portal/src/server/router/offers/offers-analysis-router.ts index efa684af..de92b546 100644 --- a/apps/portal/src/server/router/offers/offers-analysis-router.ts +++ b/apps/portal/src/server/router/offers/offers-analysis-router.ts @@ -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 | 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) => { - 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, -) => { - 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, - noOfSimilarCompanyOffers: number, - companyPercentile: number, - topPercentileCompanyOffers: Array, -) => { - 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); }, }); diff --git a/apps/portal/src/server/router/offers/offers-comments-router.ts b/apps/portal/src/server/router/offers/offers-comments-router.ts index 6e4cf6cc..20c79248 100644 --- a/apps/portal/src/server/router/offers/offers-comments-router.ts +++ b/apps/portal/src/server/router/offers/offers-comments-router.ts @@ -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 ?? '', + }; + } - 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 ?? '', + }; } - }) + }); - 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 ?? "" - } - } + 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 ?? "" - } - } - }) - 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 ?? '', + } } - }) - .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 ?? '', + } } - - 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 ?? '', + } } - }) - .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.' - }) - } - }) \ No newline at end of file + // If (result) { + // return result.discussion.filter((x) => x.replyingToId === null); + // } + + // return result; + } + + throw new trpc.TRPCError({ + code: 'UNAUTHORIZED', + message: 'Wrong userId or token.', + }); + }, + }); diff --git a/apps/portal/src/server/router/offers/offers-profile-router.ts b/apps/portal/src/server/router/offers/offers-profile-router.ts index 09e073da..11f74c3e 100644 --- a/apps/portal/src/server/router/offers/offers-profile-router.ts +++ b/apps/portal/src/server/router/offers/offers-profile-router.ts @@ -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 & { - isEditable: boolean; -}; - -function computeIsEditable( - profileInput: OffersProfile, - editToken?: string, -): WithIsEditable { - return { - ...profileInput, - isEditable: profileInput.editToken === editToken, - }; -} - -function exclude>( - profile: WithIsEditable, - ...keys: Array -): Omit, 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.', }); - } + }, }); diff --git a/apps/portal/src/server/router/offers/offers.ts b/apps/portal/src/server/router/offers/offers.ts index ceb1367c..a35e2d2e 100644 --- a/apps/portal/src/server/router/offers/offers.ts +++ b/apps/portal/src/server/router/offers/offers.ts @@ -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 = { @@ -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, }, - }; + ); }, }); diff --git a/apps/portal/src/types/offers-profile.d.ts b/apps/portal/src/types/offers-profile.d.ts deleted file mode 100644 index cc431184..00000000 --- a/apps/portal/src/types/offers-profile.d.ts +++ /dev/null @@ -1,130 +0,0 @@ -export type OffersProfile = { - background?: Background | null; - createdAt: Date; -// Discussions: Array; - editToken: string; - id: string; - offers: Array; - profileName: string; - userId?: string | null; -}; - -export type Background = { - educations: Array; - experiences: Array; - id: string; - offersProfileId: string; - specificYoes: Array; - 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?; - replyingTo: Discussion?; - replyingToId: string | null; - user: User?; - userId: string | null; -} - -export type User = { - email: string?; - emailVerified: Date?; - id: string; - image: string?; - name: string?; -} \ No newline at end of file diff --git a/apps/portal/src/types/offers.d.ts b/apps/portal/src/types/offers.d.ts new file mode 100644 index 00000000..35539b45 --- /dev/null +++ b/apps/portal/src/types/offers.d.ts @@ -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; + profileName: string; +}; + +export type Background = { + educations: Array; + experiences: Array; + id: string; + specificYoes: Array; + 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?; + replyingToId: string?; + user: User?; +}; + +export type User = { + email: string?; + emailVerified: Date?; + id: string; + image: string?; + name: string?; +}; + +export type GetOffersResponse = { + data: Array; + 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; +}; + +export type ProfileAnalysis = { + companyAnalysis: Array; + id: string; + overallAnalysis: Analysis; + overallHighestOffer: AnalysisHighestOffer; + profileId: string; +}; + +export type Analysis = { + noOfOffers: number; + percentile: number; + topPercentileOffers: Array; +}; + +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; + profileName: string; + specialization: string; + title: string; + totalYoe: number; +}; + +export type AddToProfileResponse = { + id: string; + profileName: string; + userId: string; +};