From b7e0d8ff90e07c3070b8175575ccf95b424f5fb5 Mon Sep 17 00:00:00 2001 From: BryannYeap Date: Fri, 14 Oct 2022 22:15:04 +0800 Subject: [PATCH] [offers][feat] Create Offer Analysis API --- apps/portal/prisma/schema.prisma | 624 +++++++++--------- .../src/pages/offers/test/createProfile.tsx | 561 ++++++++-------- .../src/pages/offers/test/listOffers.tsx | 7 +- .../src/pages/offers/test/profileAnalysis.tsx | 14 + apps/portal/src/server/router/index.ts | 2 + .../router/offers/offers-analysis-router.ts | 264 ++++++++ .../portal/src/server/router/offers/offers.ts | 75 +-- 7 files changed, 893 insertions(+), 654 deletions(-) create mode 100644 apps/portal/src/pages/offers/test/profileAnalysis.tsx create mode 100644 apps/portal/src/server/router/offers/offers-analysis-router.ts diff --git a/apps/portal/prisma/schema.prisma b/apps/portal/prisma/schema.prisma index 9ce74736..9d84a2d5 100644 --- a/apps/portal/prisma/schema.prisma +++ b/apps/portal/prisma/schema.prisma @@ -1,105 +1,105 @@ // Refer to the Prisma schema docs: https://pris.ly/d/prisma-schema generator client { - provider = "prisma-client-js" + provider = "prisma-client-js" } datasource db { - provider = "postgresql" - url = env("DATABASE_URL") + provider = "postgresql" + url = env("DATABASE_URL") } // Necessary for NextAuth. model Account { - id String @id @default(cuid()) - userId String - type String - provider String - providerAccountId String - refresh_token String? @db.Text - access_token String? @db.Text - expires_at Int? - token_type String? - scope String? - id_token String? @db.Text - session_state String? - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@unique([provider, providerAccountId]) + id String @id @default(cuid()) + userId String + type String + provider String + providerAccountId String + refresh_token String? @db.Text + access_token String? @db.Text + expires_at Int? + token_type String? + scope String? + id_token String? @db.Text + session_state String? + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([provider, providerAccountId]) } model Session { - id String @id @default(cuid()) - sessionToken String @unique - userId String - expires DateTime - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + id String @id @default(cuid()) + sessionToken String @unique + userId String + expires DateTime + user User @relation(fields: [userId], references: [id], onDelete: Cascade) } model User { - id String @id @default(cuid()) - name String? - email String? @unique - emailVerified DateTime? - image String? - accounts Account[] - sessions Session[] - todos Todo[] - resumesResumes ResumesResume[] - resumesStars ResumesStar[] - resumesComments ResumesComment[] - resumesCommentVotes ResumesCommentVote[] - questionsQuestions QuestionsQuestion[] - questionsQuestionEncounters QuestionsQuestionEncounter[] - questionsQuestionVotes QuestionsQuestionVote[] - questionsQuestionComments QuestionsQuestionComment[] - questionsQuestionCommentVotes QuestionsQuestionCommentVote[] - questionsAnswers QuestionsAnswer[] - questionsAnswerVotes QuestionsAnswerVote[] - questionsAnswerComments QuestionsAnswerComment[] - questionsAnswerCommentVotes QuestionsAnswerCommentVote[] - OffersProfile OffersProfile[] - offersDiscussion OffersReply[] + id String @id @default(cuid()) + name String? + email String? @unique + emailVerified DateTime? + image String? + accounts Account[] + sessions Session[] + todos Todo[] + resumesResumes ResumesResume[] + resumesStars ResumesStar[] + resumesComments ResumesComment[] + resumesCommentVotes ResumesCommentVote[] + questionsQuestions QuestionsQuestion[] + questionsQuestionEncounters QuestionsQuestionEncounter[] + questionsQuestionVotes QuestionsQuestionVote[] + questionsQuestionComments QuestionsQuestionComment[] + questionsQuestionCommentVotes QuestionsQuestionCommentVote[] + questionsAnswers QuestionsAnswer[] + questionsAnswerVotes QuestionsAnswerVote[] + questionsAnswerComments QuestionsAnswerComment[] + questionsAnswerCommentVotes QuestionsAnswerCommentVote[] + OffersProfile OffersProfile[] + offersDiscussion OffersReply[] } enum Vote { - UPVOTE - DOWNVOTE + UPVOTE + DOWNVOTE } model VerificationToken { - identifier String - token String @unique - expires DateTime + identifier String + token String @unique + expires DateTime - @@unique([identifier, token]) + @@unique([identifier, token]) } model Todo { - id String @id @default(cuid()) - userId String - text String @db.Text - status TodoStatus @default(INCOMPLETE) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + id String @id @default(cuid()) + userId String + text String @db.Text + status TodoStatus @default(INCOMPLETE) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + user User @relation(fields: [userId], references: [id], onDelete: Cascade) } enum TodoStatus { - INCOMPLETE - COMPLETE + INCOMPLETE + COMPLETE } model Company { - id String @id @default(cuid()) - name String @db.Text - slug String @unique - description String? @db.Text - logoUrl String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - OffersExperience OffersExperience[] - OffersOffer OffersOffer[] + id String @id @default(cuid()) + name String @db.Text + slug String @unique + description String? @db.Text + logoUrl String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + OffersExperience OffersExperience[] + OffersOffer OffersOffer[] } // Start of Resumes project models. @@ -107,65 +107,65 @@ model Company { // use camelCase for field names, and try to name them consistently // across all models in this file. model ResumesResume { - id String @id @default(cuid()) - userId String - title String @db.Text - // TODO: Update role, experience, location to use Enums - role String @db.Text - experience String @db.Text - location String @db.Text - url String - additionalInfo String? @db.Text - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - stars ResumesStar[] - comments ResumesComment[] + id String @id @default(cuid()) + userId String + title String @db.Text + // TODO: Update role, experience, location to use Enums + role String @db.Text + experience String @db.Text + location String @db.Text + url String + additionalInfo String? @db.Text + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + stars ResumesStar[] + comments ResumesComment[] } model ResumesStar { - id String @id @default(cuid()) - userId String - resumeId String - createdAt DateTime @default(now()) - resume ResumesResume @relation(fields: [resumeId], references: [id], onDelete: Cascade) - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + id String @id @default(cuid()) + userId String + resumeId String + createdAt DateTime @default(now()) + resume ResumesResume @relation(fields: [resumeId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) - @@unique([userId, resumeId]) + @@unique([userId, resumeId]) } model ResumesComment { - id String @id @default(cuid()) - userId String - resumeId String - description String @db.Text - section ResumesSection - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - resume ResumesResume @relation(fields: [resumeId], references: [id], onDelete: Cascade) - votes ResumesCommentVote[] - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + id String @id @default(cuid()) + userId String + resumeId String + description String @db.Text + section ResumesSection + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + resume ResumesResume @relation(fields: [resumeId], references: [id], onDelete: Cascade) + votes ResumesCommentVote[] + user User @relation(fields: [userId], references: [id], onDelete: Cascade) } enum ResumesSection { - GENERAL - EDUCATION - EXPERIENCE - PROJECTS - SKILLS + GENERAL + EDUCATION + EXPERIENCE + PROJECTS + SKILLS } model ResumesCommentVote { - id String @id @default(cuid()) - userId String - commentId String - value Vote - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - comment ResumesComment @relation(fields: [commentId], references: [id], onDelete: Cascade) - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + id String @id @default(cuid()) + userId String + commentId String + value Vote + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + comment ResumesComment @relation(fields: [commentId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) - @@unique([userId, commentId]) + @@unique([userId, commentId]) } // End of Resumes project models. @@ -176,176 +176,176 @@ model ResumesCommentVote { // across all models in this file. model OffersProfile { - id String @id @default(cuid()) - profileName String @unique - createdAt DateTime @default(now()) + id String @id @default(cuid()) + profileName String @unique + createdAt DateTime @default(now()) - background OffersBackground? + background OffersBackground? - editToken String + editToken String - discussion OffersReply[] + discussion OffersReply[] - offers OffersOffer[] + offers OffersOffer[] - user User? @relation(fields: [userId], references: [id]) - userId String? + user User? @relation(fields: [userId], references: [id]) + userId String? } model OffersBackground { - id String @id @default(cuid()) + id String @id @default(cuid()) - totalYoe Int? - specificYoes OffersSpecificYoe[] + totalYoe Int? + specificYoes OffersSpecificYoe[] - experiences OffersExperience[] // For extensibility in the future + experiences OffersExperience[] // For extensibility in the future - educations OffersEducation[] // For extensibility in the future + educations OffersEducation[] // For extensibility in the future - profile OffersProfile @relation(fields: [offersProfileId], references: [id], onDelete: Cascade) - offersProfileId String @unique + profile OffersProfile @relation(fields: [offersProfileId], references: [id], onDelete: Cascade) + offersProfileId String @unique } model OffersSpecificYoe { - id String @id @default(cuid()) + id String @id @default(cuid()) - yoe Int - domain String + yoe Int + domain String - background OffersBackground @relation(fields: [backgroundId], references: [id], onDelete: Cascade) - backgroundId String + background OffersBackground @relation(fields: [backgroundId], references: [id], onDelete: Cascade) + backgroundId String } model OffersExperience { - id String @id @default(cuid()) + id String @id @default(cuid()) - company Company? @relation(fields: [companyId], references: [id]) - companyId String? + company Company? @relation(fields: [companyId], references: [id]) + companyId String? - jobType JobType? - title String? + jobType JobType? + title String? - // Add more fields - durationInMonths Int? - specialization String? + // Add more fields + durationInMonths Int? + specialization String? - // FULLTIME fields - level String? - totalCompensation OffersCurrency? @relation("ExperienceTotalCompensation", fields: [totalCompensationId], references: [id]) - totalCompensationId String? @unique + // FULLTIME fields + level String? + totalCompensation OffersCurrency? @relation("ExperienceTotalCompensation", fields: [totalCompensationId], references: [id]) + totalCompensationId String? @unique - // INTERN fields - monthlySalary OffersCurrency? @relation("ExperienceMonthlySalary", fields: [monthlySalaryId], references: [id]) - monthlySalaryId String? @unique + // INTERN fields + monthlySalary OffersCurrency? @relation("ExperienceMonthlySalary", fields: [monthlySalaryId], references: [id]) + monthlySalaryId String? @unique - background OffersBackground @relation(fields: [backgroundId], references: [id], onDelete: Cascade) - backgroundId String + background OffersBackground @relation(fields: [backgroundId], references: [id], onDelete: Cascade) + backgroundId String } model OffersCurrency { - id String @id @default(cuid()) - value Int - currency String + id String @id @default(cuid()) + value Int + currency String - // Experience - OffersExperienceTotalCompensation OffersExperience? @relation("ExperienceTotalCompensation") - OffersExperienceMonthlySalary OffersExperience? @relation("ExperienceMonthlySalary") + // Experience + OffersExperienceTotalCompensation OffersExperience? @relation("ExperienceTotalCompensation") + OffersExperienceMonthlySalary OffersExperience? @relation("ExperienceMonthlySalary") - // Full Time - OffersTotalCompensation OffersFullTime? @relation("OfferTotalCompensation") - OffersBaseSalary OffersFullTime? @relation("OfferBaseSalary") - OffersBonus OffersFullTime? @relation("OfferBonus") - OffersStocks OffersFullTime? @relation("OfferStocks") + // Full Time + OffersTotalCompensation OffersFullTime? @relation("OfferTotalCompensation") + OffersBaseSalary OffersFullTime? @relation("OfferBaseSalary") + OffersBonus OffersFullTime? @relation("OfferBonus") + OffersStocks OffersFullTime? @relation("OfferStocks") - // Intern - OffersMonthlySalary OffersIntern? + // Intern + OffersMonthlySalary OffersIntern? } enum JobType { - INTERN - FULLTIME + INTERN + FULLTIME } model OffersEducation { - id String @id @default(cuid()) - type String? - field String? + id String @id @default(cuid()) + type String? + field String? - school String? - startDate DateTime? - endDate DateTime? + school String? + startDate DateTime? + endDate DateTime? - background OffersBackground @relation(fields: [backgroundId], references: [id], onDelete: Cascade) - backgroundId String + background OffersBackground @relation(fields: [backgroundId], references: [id], onDelete: Cascade) + backgroundId String } model OffersReply { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - message String + id String @id @default(cuid()) + createdAt DateTime @default(now()) + message String - replyingToId String? - replyingTo OffersReply? @relation("ReplyThread", fields: [replyingToId], references: [id]) - replies OffersReply[] @relation("ReplyThread") + replyingToId String? + replyingTo OffersReply? @relation("ReplyThread", fields: [replyingToId], references: [id]) + replies OffersReply[] @relation("ReplyThread") - profile OffersProfile @relation(fields: [profileId], references: [id], onDelete: Cascade) - profileId String + profile OffersProfile @relation(fields: [profileId], references: [id], onDelete: Cascade) + profileId String - user User? @relation(fields: [userId], references: [id]) - userId String? + user User? @relation(fields: [userId], references: [id]) + userId String? } model OffersOffer { - id String @id @default(cuid()) + id String @id @default(cuid()) - profile OffersProfile @relation(fields: [profileId], references: [id], onDelete: Cascade) - profileId String + profile OffersProfile @relation(fields: [profileId], references: [id], onDelete: Cascade) + profileId String - company Company @relation(fields: [companyId], references: [id]) - companyId String + company Company @relation(fields: [companyId], references: [id]) + companyId String - monthYearReceived DateTime - location String - negotiationStrategy String? - comments String? + monthYearReceived DateTime + location String + negotiationStrategy String? + comments String? - jobType JobType + jobType JobType - OffersIntern OffersIntern? @relation(fields: [offersInternId], references: [id], onDelete: Cascade) - offersInternId String? @unique + OffersIntern OffersIntern? @relation(fields: [offersInternId], references: [id], onDelete: Cascade) + offersInternId String? @unique - OffersFullTime OffersFullTime? @relation(fields: [offersFullTimeId], references: [id], onDelete: Cascade) - offersFullTimeId String? @unique + OffersFullTime OffersFullTime? @relation(fields: [offersFullTimeId], references: [id], onDelete: Cascade) + offersFullTimeId String? @unique } model OffersIntern { - id String @id @default(cuid()) + id String @id @default(cuid()) - title String - specialization String - internshipCycle String - startYear Int - monthlySalary OffersCurrency @relation(fields: [monthlySalaryId], references: [id], onDelete: Cascade) - monthlySalaryId String @unique + title String + specialization String + internshipCycle String + startYear Int + monthlySalary OffersCurrency @relation(fields: [monthlySalaryId], references: [id], onDelete: Cascade) + monthlySalaryId String @unique - OffersOffer OffersOffer? + OffersOffer OffersOffer? } model OffersFullTime { - id String @id @default(cuid()) - title String - specialization String - level String - totalCompensation OffersCurrency @relation("OfferTotalCompensation", fields: [totalCompensationId], references: [id], onDelete: Cascade) - totalCompensationId String @unique - baseSalary OffersCurrency @relation("OfferBaseSalary", fields: [baseSalaryId], references: [id], onDelete: Cascade) - baseSalaryId String @unique - bonus OffersCurrency @relation("OfferBonus", fields: [bonusId], references: [id], onDelete: Cascade) - bonusId String @unique - stocks OffersCurrency @relation("OfferStocks", fields: [stocksId], references: [id], onDelete: Cascade) - stocksId String @unique - - OffersOffer OffersOffer? + id String @id @default(cuid()) + title String + specialization String + level String + totalCompensation OffersCurrency @relation("OfferTotalCompensation", fields: [totalCompensationId], references: [id], onDelete: Cascade) + totalCompensationId String @unique + baseSalary OffersCurrency @relation("OfferBaseSalary", fields: [baseSalaryId], references: [id], onDelete: Cascade) + baseSalaryId String @unique + bonus OffersCurrency @relation("OfferBonus", fields: [bonusId], references: [id], onDelete: Cascade) + bonusId String @unique + stocks OffersCurrency @relation("OfferStocks", fields: [stocksId], references: [id], onDelete: Cascade) + stocksId String @unique + + OffersOffer OffersOffer? } // End of Offers project models. @@ -356,136 +356,136 @@ model OffersFullTime { // across all models in this file. enum QuestionsQuestionType { - CODING - SYSTEM_DESIGN - BEHAVIORAL + CODING + SYSTEM_DESIGN + BEHAVIORAL } model QuestionsQuestion { - id String @id @default(cuid()) - userId String? - content String @db.Text - questionType QuestionsQuestionType - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(cuid()) + userId String? + content String @db.Text + questionType QuestionsQuestionType + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt - user User? @relation(fields: [userId], references: [id], onDelete: SetNull) - encounters QuestionsQuestionEncounter[] - votes QuestionsQuestionVote[] - comments QuestionsQuestionComment[] - answers QuestionsAnswer[] + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + encounters QuestionsQuestionEncounter[] + votes QuestionsQuestionVote[] + comments QuestionsQuestionComment[] + answers QuestionsAnswer[] } model QuestionsQuestionEncounter { - id String @id @default(cuid()) - questionId String - userId String? - // TODO: sync with models - company String @db.Text - location String @db.Text - role String @db.Text - seenAt DateTime - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(cuid()) + questionId String + userId String? + // TODO: sync with models + company String @db.Text + location String @db.Text + role String @db.Text + seenAt DateTime + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt - user User? @relation(fields: [userId], references: [id], onDelete: SetNull) - question QuestionsQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade) + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + question QuestionsQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade) } model QuestionsQuestionVote { - id String @id @default(cuid()) - questionId String - userId String? - vote Vote - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(cuid()) + questionId String + userId String? + vote Vote + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt - user User? @relation(fields: [userId], references: [id], onDelete: SetNull) - question QuestionsQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade) + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + question QuestionsQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade) - @@unique([questionId, userId]) + @@unique([questionId, userId]) } model QuestionsQuestionComment { - id String @id @default(cuid()) - questionId String - userId String? - content String @db.Text - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(cuid()) + questionId String + userId String? + content String @db.Text + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt - user User? @relation(fields: [userId], references: [id], onDelete: SetNull) - question QuestionsQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade) - votes QuestionsQuestionCommentVote[] + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + question QuestionsQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade) + votes QuestionsQuestionCommentVote[] } model QuestionsQuestionCommentVote { - id String @id @default(cuid()) - questionCommentId String - userId String? - vote Vote - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(cuid()) + questionCommentId String + userId String? + vote Vote + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt - user User? @relation(fields: [userId], references: [id], onDelete: SetNull) - comment QuestionsQuestionComment @relation(fields: [questionCommentId], references: [id], onDelete: Cascade) + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + comment QuestionsQuestionComment @relation(fields: [questionCommentId], references: [id], onDelete: Cascade) - @@unique([questionCommentId, userId]) + @@unique([questionCommentId, userId]) } model QuestionsAnswer { - id String @id @default(cuid()) - questionId String - userId String? - content String @db.Text - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(cuid()) + questionId String + userId String? + content String @db.Text + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt - user User? @relation(fields: [userId], references: [id], onDelete: SetNull) - question QuestionsQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade) - votes QuestionsAnswerVote[] - comments QuestionsAnswerComment[] + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + question QuestionsQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade) + votes QuestionsAnswerVote[] + comments QuestionsAnswerComment[] } model QuestionsAnswerVote { - id String @id @default(cuid()) - answerId String - userId String? - vote Vote - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(cuid()) + answerId String + userId String? + vote Vote + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt - user User? @relation(fields: [userId], references: [id], onDelete: SetNull) - answer QuestionsAnswer @relation(fields: [answerId], references: [id], onDelete: Cascade) + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + answer QuestionsAnswer @relation(fields: [answerId], references: [id], onDelete: Cascade) - @@unique([answerId, userId]) + @@unique([answerId, userId]) } model QuestionsAnswerComment { - id String @id @default(cuid()) - answerId String - userId String? - content String @db.Text - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(cuid()) + answerId String + userId String? + content String @db.Text + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt - user User? @relation(fields: [userId], references: [id], onDelete: SetNull) - answer QuestionsAnswer @relation(fields: [answerId], references: [id], onDelete: Cascade) - votes QuestionsAnswerCommentVote[] + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + answer QuestionsAnswer @relation(fields: [answerId], references: [id], onDelete: Cascade) + votes QuestionsAnswerCommentVote[] } model QuestionsAnswerCommentVote { - id String @id @default(cuid()) - answerCommentId String - userId String? - vote Vote - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(cuid()) + answerCommentId String + userId String? + vote Vote + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt - user User? @relation(fields: [userId], references: [id], onDelete: SetNull) - comment QuestionsAnswerComment @relation(fields: [answerCommentId], references: [id], onDelete: Cascade) + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + comment QuestionsAnswerComment @relation(fields: [answerCommentId], references: [id], onDelete: Cascade) - @@unique([answerCommentId, userId]) + @@unique([answerCommentId, userId]) } // End of Questions project models. diff --git a/apps/portal/src/pages/offers/test/createProfile.tsx b/apps/portal/src/pages/offers/test/createProfile.tsx index b174d8a7..44740940 100644 --- a/apps/portal/src/pages/offers/test/createProfile.tsx +++ b/apps/portal/src/pages/offers/test/createProfile.tsx @@ -4,7 +4,7 @@ import { trpc } from '~/utils/trpc'; function Test() { const [createdData, setCreatedData] = useState(''); - const [error, setError] = useState(""); + const [error, setError] = useState(''); const createMutation = trpc.useMutation(['offers.profile.create'], { onError(err: any) { @@ -15,14 +15,17 @@ function Test() { }, }); - const addToUserProfileMutation = trpc.useMutation(['offers.profile.addToUserProfile'], { - onError(err: any) { - alert(err); - }, - onSuccess(data) { - setCreatedData(JSON.stringify(data)); + const addToUserProfileMutation = trpc.useMutation( + ['offers.profile.addToUserProfile'], + { + onError(err: any) { + alert(err); + }, + onSuccess(data) { + setCreatedData(JSON.stringify(data)); + }, }, - }) + ); const deleteCommentMutation = trpc.useMutation(['offers.comments.delete'], { onError(err: any) { @@ -38,9 +41,9 @@ function Test() { id: 'cl97fprun001j7iyg6ev9x983', profileId: 'cl96stky5002ew32gx2kale2x', token: 'afca11e436d21bde24543718fa957c6c625335439dc504f24ee35eae7b5ef1', - userId: 'cl97dl51k001e7iygd5v5gt58' - }) - } + userId: 'cl97dl51k001e7iygd5v5gt58', + }); + }; const updateCommentMutation = trpc.useMutation(['offers.comments.update'], { onError(err: any) { @@ -56,9 +59,9 @@ function Test() { id: 'cl97fxb0y001l7iyg14sdobt2', message: 'hello hello', profileId: 'cl96stky5002ew32gx2kale2x', - token: 'afca11e436d21bde24543718fa957c6c625335439dc504f24ee35eae7b5ef1ba' - }) - } + token: 'afca11e436d21bde24543718fa957c6c625335439dc504f24ee35eae7b5ef1ba', + }); + }; const createCommentMutation = trpc.useMutation(['offers.comments.create'], { onError(err: any) { @@ -74,16 +77,16 @@ function Test() { message: 'hello', profileId: 'cl96stky5002ew32gx2kale2x', // UserId: 'cl97dl51k001e7iygd5v5gt58' - }) - } + }); + }; const handleLink = () => { addToUserProfileMutation.mutate({ profileId: 'cl96stky5002ew32gx2kale2x', token: 'afca11e436d21bde24543718fa957c6c625335439dc504f24ee35eae7b5ef1ba', - userId: 'cl97dl51k001e7iygd5v5gt58' - }) - } + userId: 'cl97dl51k001e7iygd5v5gt58', + }); + }; const handleClick = () => { createMutation.mutate({ @@ -126,7 +129,6 @@ function Test() { }, offers: [ { - OffersFullTime: { baseSalary: { currency: 'SGD', @@ -189,23 +191,30 @@ function Test() { }; const profileId = 'cl96stky5002ew32gx2kale2x'; // Remember to change this filed after testing deleting - const data = trpc.useQuery([ - `offers.profile.listOne`, + const data = trpc.useQuery( + [ + `offers.profile.listOne`, + { + profileId, + token: + 'afca11e436d21bde24543718fa957c6c625335439dc504f24ee35eae7b5ef1ba', + }, + ], { - profileId, - token: 'afca11e436d21bde24543718fa957c6c625335439dc504f24ee35eae7b5ef1ba', + onError(err) { + setError(err.shape?.message || ''); + }, }, - ], { - onError(err) { - setError(err.shape?.message || "") - } - }); + ); - const replies = trpc.useQuery(['offers.comments.getComments', {profileId: 'cl96stky5002ew32gx2kale2x'}], { - onError(err) { - setError(err.shape?.message || "") + const replies = trpc.useQuery( + ['offers.comments.getComments', { profileId: 'cl96stky5002ew32gx2kale2x' }], + { + onError(err) { + setError(err.shape?.message || ''); + }, }, - }); + ); const deleteMutation = trpc.useMutation(['offers.profile.delete']); @@ -230,357 +239,363 @@ function Test() { background: { educations: [ { - backgroundId: "cl96stky6002fw32g6vj4meyr", - endDate: new Date("2018-09-30T07:58:54.000Z"), - field: "Computer Science", - id: "cl96stky6002gw32gey2ffawd", - school: "National University of Singapore", - startDate: new Date("2014-09-30T07:58:54.000Z"), - type: "Bachelors" - } + backgroundId: 'cl96stky6002fw32g6vj4meyr', + endDate: new Date('2018-09-30T07:58:54.000Z'), + field: 'Computer Science', + id: 'cl96stky6002gw32gey2ffawd', + school: 'National University of Singapore', + startDate: new Date('2014-09-30T07:58:54.000Z'), + type: 'Bachelors', + }, ], experiences: [ { - backgroundId: "cl96stky6002fw32g6vj4meyr", + backgroundId: 'cl96stky6002fw32g6vj4meyr', company: { - createdAt: new Date("2022-10-12T16:19:05.196Z"), - description: "Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.", - id: "cl95u79f000007im531ysjg79", - logoUrl: "https://logo.clearbit.com/meta.com", - name: "Meta", - slug: "meta", - updatedAt: new Date("2022-10-12T16:19:05.196Z") + createdAt: new Date('2022-10-12T16:19:05.196Z'), + description: + 'Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.', + id: 'cl95u79f000007im531ysjg79', + logoUrl: 'https://logo.clearbit.com/meta.com', + name: 'Meta', + slug: 'meta', + updatedAt: new Date('2022-10-12T16:19:05.196Z'), }, - companyId: "cl95u79f000007im531ysjg79", + companyId: 'cl95u79f000007im531ysjg79', durationInMonths: 24, - id: "cl96stky6002iw32gpt6t87s2", - jobType: "FULLTIME", - level: "Junior", + id: 'cl96stky6002iw32gpt6t87s2', + jobType: 'FULLTIME', + level: 'Junior', monthlySalary: null, monthlySalaryId: null, - specialization: "Front End", - title: "Software Engineer", + specialization: 'Front End', + title: 'Software Engineer', totalCompensation: { - currency: "SGD", - id: "cl96stky6002jw32g73svfacr", - value: 104100 + currency: 'SGD', + id: 'cl96stky6002jw32g73svfacr', + value: 104100, }, - totalCompensationId: "cl96stky6002jw32g73svfacr" - } + totalCompensationId: 'cl96stky6002jw32g73svfacr', + }, ], - id: "cl96stky6002fw32g6vj4meyr", - offersProfileId: "cl96stky5002ew32gx2kale2x", + id: 'cl96stky6002fw32g6vj4meyr', + offersProfileId: 'cl96stky5002ew32gx2kale2x', specificYoes: [ { - backgroundId: "cl96stky6002fw32g6vj4meyr", - domain: "Backend", - id: "cl96t7890004tw32g5in3px5j", - yoe: 2 + backgroundId: 'cl96stky6002fw32g6vj4meyr', + domain: 'Backend', + id: 'cl96t7890004tw32g5in3px5j', + yoe: 2, }, { - backgroundId: "cl96stky6002fw32g6vj4meyr", - domain: "Backend", - id: "cl96tb87x004xw32gnu17jbzv", - yoe: 2 + backgroundId: 'cl96stky6002fw32g6vj4meyr', + domain: 'Backend', + id: 'cl96tb87x004xw32gnu17jbzv', + yoe: 2, }, { - backgroundId: "cl96stky6002fw32g6vj4meyr", - domain: "Backend", - id: "cl976t39z00007iygt3np3cgo", - yoe: 2 + backgroundId: 'cl96stky6002fw32g6vj4meyr', + domain: 'Backend', + id: 'cl976t39z00007iygt3np3cgo', + yoe: 2, }, { - backgroundId: "cl96stky6002fw32g6vj4meyr", - domain: "Front End", - id: "cl96stky7002mw32gn4jc7uml", - yoe: 2 + backgroundId: 'cl96stky6002fw32g6vj4meyr', + domain: 'Front End', + id: 'cl96stky7002mw32gn4jc7uml', + yoe: 2, }, { - backgroundId: "cl96stky6002fw32g6vj4meyr", - domain: "Full Stack", - id: "cl96stky7002nw32gpprghtxr", - yoe: 2 + backgroundId: 'cl96stky6002fw32g6vj4meyr', + domain: 'Full Stack', + id: 'cl96stky7002nw32gpprghtxr', + yoe: 2, }, { - backgroundId: "cl96stky6002fw32g6vj4meyr", - domain: "Backend", - id: "cl976we5h000p7iygiomdo9fh", - yoe: 2 - } + backgroundId: 'cl96stky6002fw32g6vj4meyr', + domain: 'Backend', + id: 'cl976we5h000p7iygiomdo9fh', + yoe: 2, + }, ], - totalYoe: 6 + totalYoe: 6, }, - createdAt: "2022-10-13T08:28:13.518Z", + createdAt: '2022-10-13T08:28:13.518Z', discussion: [], - id: "cl96stky5002ew32gx2kale2x", + id: 'cl96stky5002ew32gx2kale2x', isEditable: true, offers: [ { OffersFullTime: { baseSalary: { - currency: "SGD", - id: "cl976t4de00067iyg3pjir7k9", - value: 1999999999 + currency: 'SGD', + id: 'cl976t4de00067iyg3pjir7k9', + value: 1999999999, }, - baseSalaryId: "cl976t4de00067iyg3pjir7k9", + baseSalaryId: 'cl976t4de00067iyg3pjir7k9', bonus: { - currency: "SGD", - id: "cl976t4de00087iygcnlmh8aw", - value: 1410065407 + currency: 'SGD', + id: 'cl976t4de00087iygcnlmh8aw', + value: 1410065407, }, - bonusId: "cl976t4de00087iygcnlmh8aw", - id: "cl976t4de00057iygq3ktce3v", - level: "EXPERT", - specialization: "FRONTEND", + bonusId: 'cl976t4de00087iygcnlmh8aw', + id: 'cl976t4de00057iygq3ktce3v', + level: 'EXPERT', + specialization: 'FRONTEND', stocks: { - currency: "SGD", - id: "cl976t4df000a7iygkrsgr1xh", - value: -558038585 + currency: 'SGD', + id: 'cl976t4df000a7iygkrsgr1xh', + value: -558038585, }, - stocksId: "cl976t4df000a7iygkrsgr1xh", - title: "Software Engineer", + stocksId: 'cl976t4df000a7iygkrsgr1xh', + title: 'Software Engineer', totalCompensation: { - currency: "SGD", - id: "cl976t4df000c7iyg73ryf5uw", - value: 55555555 + currency: 'SGD', + id: 'cl976t4df000c7iyg73ryf5uw', + value: 55555555, }, - totalCompensationId: "cl976t4df000c7iyg73ryf5uw" + totalCompensationId: 'cl976t4df000c7iyg73ryf5uw', }, OffersIntern: null, - comments: "this IS SO IEUHDAEUIGDI", + comments: 'this IS SO IEUHDAEUIGDI', company: { - createdAt: new Date("2022-10-12T16:19:05.196Z"), - description: "Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.", - id: "cl95u79f000007im531ysjg79", - logoUrl: "https://logo.clearbit.com/meta.com", - name: "Meta", - slug: "meta", - updatedAt: new Date("2022-10-12T16:19:05.196Z") + createdAt: new Date('2022-10-12T16:19:05.196Z'), + description: + 'Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.', + id: 'cl95u79f000007im531ysjg79', + logoUrl: 'https://logo.clearbit.com/meta.com', + name: 'Meta', + slug: 'meta', + updatedAt: new Date('2022-10-12T16:19:05.196Z'), }, - companyId: "cl95u79f000007im531ysjg79", - id: "cl976t4de00047iygl0zbce11", - jobType: "FULLTIME", - location: "Singapore, Singapore", - monthYearReceived: new Date("2022-09-30T07:58:54.000Z"), - negotiationStrategy: "Charmed the guy with my face", - offersFullTimeId: "cl976t4de00057iygq3ktce3v", + companyId: 'cl95u79f000007im531ysjg79', + id: 'cl976t4de00047iygl0zbce11', + jobType: 'FULLTIME', + location: 'Singapore, Singapore', + monthYearReceived: new Date('2022-09-30T07:58:54.000Z'), + negotiationStrategy: 'Charmed the guy with my face', + offersFullTimeId: 'cl976t4de00057iygq3ktce3v', offersInternId: null, - profileId: "cl96stky5002ew32gx2kale2x" + profileId: 'cl96stky5002ew32gx2kale2x', }, { OffersFullTime: { baseSalary: { - currency: "SGD", - id: "cl96stky80033w32gxw5goc4z", - value: 84000 + currency: 'SGD', + id: 'cl96stky80033w32gxw5goc4z', + value: 84000, }, - baseSalaryId: "cl96stky80033w32gxw5goc4z", + baseSalaryId: 'cl96stky80033w32gxw5goc4z', bonus: { - currency: "SGD", - id: "cl96stky80035w32gajjwdo1p", - value: 123456789 + currency: 'SGD', + id: 'cl96stky80035w32gajjwdo1p', + value: 123456789, }, - bonusId: "cl96stky80035w32gajjwdo1p", - id: "cl96stky80032w32gep9ovgj3", - level: "Junior", - specialization: "Front End", + bonusId: 'cl96stky80035w32gajjwdo1p', + id: 'cl96stky80032w32gep9ovgj3', + level: 'Junior', + specialization: 'Front End', stocks: { - currency: "SGD", - id: "cl96stky90037w32gu04t6ybh", - value: 100 + currency: 'SGD', + id: 'cl96stky90037w32gu04t6ybh', + value: 100, }, - stocksId: "cl96stky90037w32gu04t6ybh", - title: "Software Engineer", + stocksId: 'cl96stky90037w32gu04t6ybh', + title: 'Software Engineer', totalCompensation: { - currency: "SGD", - id: "cl96stky90039w32glbpktd0o", - value: 104100 + currency: 'SGD', + id: 'cl96stky90039w32glbpktd0o', + value: 104100, }, - totalCompensationId: "cl96stky90039w32glbpktd0o" + totalCompensationId: 'cl96stky90039w32glbpktd0o', }, OffersIntern: null, comments: null, company: { - createdAt: new Date("2022-10-12T16:19:05.196Z"), - description: "Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.", - id: "cl95u79f000007im531ysjg79", - logoUrl: "https://logo.clearbit.com/meta.com", - name: "Meta", - slug: "meta", - updatedAt: new Date("2022-10-12T16:19:05.196Z") + createdAt: new Date('2022-10-12T16:19:05.196Z'), + description: + 'Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.', + id: 'cl95u79f000007im531ysjg79', + logoUrl: 'https://logo.clearbit.com/meta.com', + name: 'Meta', + slug: 'meta', + updatedAt: new Date('2022-10-12T16:19:05.196Z'), }, - companyId: "cl95u79f000007im531ysjg79", - id: "cl96stky80031w32gau9mu1gs", - jobType: "FULLTIME", - location: "Singapore, Singapore", - monthYearReceived: new Date("2022-09-30T07:58:54.000Z"), - negotiationStrategy: "Leveraged having million offers", - offersFullTimeId: "cl96stky80032w32gep9ovgj3", + companyId: 'cl95u79f000007im531ysjg79', + id: 'cl96stky80031w32gau9mu1gs', + jobType: 'FULLTIME', + location: 'Singapore, Singapore', + monthYearReceived: new Date('2022-09-30T07:58:54.000Z'), + negotiationStrategy: 'Leveraged having million offers', + offersFullTimeId: 'cl96stky80032w32gep9ovgj3', offersInternId: null, - profileId: "cl96stky5002ew32gx2kale2x" + profileId: 'cl96stky5002ew32gx2kale2x', }, { OffersFullTime: { baseSalary: { - currency: "SGD", - id: "cl96stky9003dw32gcvqbijlo", - value: 1 + currency: 'SGD', + id: 'cl96stky9003dw32gcvqbijlo', + value: 1, }, - baseSalaryId: "cl96stky9003dw32gcvqbijlo", + baseSalaryId: 'cl96stky9003dw32gcvqbijlo', bonus: { - currency: "SGD", - id: "cl96stky9003fw32goc3zqxwr", - value: 0 + currency: 'SGD', + id: 'cl96stky9003fw32goc3zqxwr', + value: 0, }, - bonusId: "cl96stky9003fw32goc3zqxwr", - id: "cl96stky9003cw32g5v10izfu", - level: "Senior", - specialization: "Front End", + bonusId: 'cl96stky9003fw32goc3zqxwr', + id: 'cl96stky9003cw32g5v10izfu', + level: 'Senior', + specialization: 'Front End', stocks: { - currency: "SGD", - id: "cl96stky9003hw32g1lbbkqqr", - value: 999999 + currency: 'SGD', + id: 'cl96stky9003hw32g1lbbkqqr', + value: 999999, }, - stocksId: "cl96stky9003hw32g1lbbkqqr", - title: "Software Engineer DOG", + stocksId: 'cl96stky9003hw32g1lbbkqqr', + title: 'Software Engineer DOG', totalCompensation: { - currency: "SGD", - id: "cl96stky9003jw32gzumcoi7v", - value: 999999 + currency: 'SGD', + id: 'cl96stky9003jw32gzumcoi7v', + value: 999999, }, - totalCompensationId: "cl96stky9003jw32gzumcoi7v" + totalCompensationId: 'cl96stky9003jw32gzumcoi7v', }, OffersIntern: null, comments: null, company: { - createdAt: new Date("2022-10-12T16:19:05.196Z"), - description: "Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.", - id: "cl95u79f000007im531ysjg79", - logoUrl: "https://logo.clearbit.com/meta.com", - name: "Meta", - slug: "meta", - updatedAt: new Date("2022-10-12T16:19:05.196Z") + createdAt: new Date('2022-10-12T16:19:05.196Z'), + description: + 'Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.', + id: 'cl95u79f000007im531ysjg79', + logoUrl: 'https://logo.clearbit.com/meta.com', + name: 'Meta', + slug: 'meta', + updatedAt: new Date('2022-10-12T16:19:05.196Z'), }, - companyId: "cl95u79f000007im531ysjg79", - id: "cl96stky9003bw32gc3l955vr", - jobType: "FULLTIME", - location: "Singapore, Singapore", - monthYearReceived: new Date("2022-09-30T07:58:54.000Z"), - negotiationStrategy: "LOst out having multiple offers", - offersFullTimeId: "cl96stky9003cw32g5v10izfu", + companyId: 'cl95u79f000007im531ysjg79', + id: 'cl96stky9003bw32gc3l955vr', + jobType: 'FULLTIME', + location: 'Singapore, Singapore', + monthYearReceived: new Date('2022-09-30T07:58:54.000Z'), + negotiationStrategy: 'LOst out having multiple offers', + offersFullTimeId: 'cl96stky9003cw32g5v10izfu', offersInternId: null, - profileId: "cl96stky5002ew32gx2kale2x" + profileId: 'cl96stky5002ew32gx2kale2x', }, { OffersFullTime: { baseSalary: { - currency: "SGD", - id: "cl976wf28000v7iygmk1b7qaq", - value: 1999999999 + currency: 'SGD', + id: 'cl976wf28000v7iygmk1b7qaq', + value: 1999999999, }, - baseSalaryId: "cl976wf28000v7iygmk1b7qaq", + baseSalaryId: 'cl976wf28000v7iygmk1b7qaq', bonus: { - currency: "SGD", - id: "cl976wf28000x7iyg63w7kcli", - value: 1410065407 + currency: 'SGD', + id: 'cl976wf28000x7iyg63w7kcli', + value: 1410065407, }, - bonusId: "cl976wf28000x7iyg63w7kcli", - id: "cl976wf28000u7iyg6euei8e9", - level: "EXPERT", - specialization: "FRONTEND", + bonusId: 'cl976wf28000x7iyg63w7kcli', + id: 'cl976wf28000u7iyg6euei8e9', + level: 'EXPERT', + specialization: 'FRONTEND', stocks: { - currency: "SGD", - id: "cl976wf28000z7iyg9ivun6ap", - value: 111222333 + currency: 'SGD', + id: 'cl976wf28000z7iyg9ivun6ap', + value: 111222333, }, - stocksId: "cl976wf28000z7iyg9ivun6ap", - title: "Software Engineer", + stocksId: 'cl976wf28000z7iyg9ivun6ap', + title: 'Software Engineer', totalCompensation: { - currency: "SGD", - id: "cl976wf2800117iygmzsc0xit", - value: 55555555 + currency: 'SGD', + id: 'cl976wf2800117iygmzsc0xit', + value: 55555555, }, - totalCompensationId: "cl976wf2800117iygmzsc0xit" + totalCompensationId: 'cl976wf2800117iygmzsc0xit', }, OffersIntern: null, - comments: "this IS SO COOL", + comments: 'this IS SO COOL', company: { - createdAt: new Date("2022-10-12T16:19:05.196Z"), - description: "Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.", - id: "cl95u79f000007im531ysjg79", - logoUrl: "https://logo.clearbit.com/meta.com", - name: "Meta", - slug: "meta", - updatedAt: new Date("2022-10-12T16:19:05.196Z") + createdAt: new Date('2022-10-12T16:19:05.196Z'), + description: + 'Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.', + id: 'cl95u79f000007im531ysjg79', + logoUrl: 'https://logo.clearbit.com/meta.com', + name: 'Meta', + slug: 'meta', + updatedAt: new Date('2022-10-12T16:19:05.196Z'), }, - companyId: "cl95u79f000007im531ysjg79", - id: "cl976wf28000t7iyga4noyz7s", - jobType: "FULLTIME", - location: "Singapore, Singapore", - monthYearReceived: new Date("2022-09-30T07:58:54.000Z"), - negotiationStrategy: "Charmed the guy with my face", - offersFullTimeId: "cl976wf28000u7iyg6euei8e9", + companyId: 'cl95u79f000007im531ysjg79', + id: 'cl976wf28000t7iyga4noyz7s', + jobType: 'FULLTIME', + location: 'Singapore, Singapore', + monthYearReceived: new Date('2022-09-30T07:58:54.000Z'), + negotiationStrategy: 'Charmed the guy with my face', + offersFullTimeId: 'cl976wf28000u7iyg6euei8e9', offersInternId: null, - profileId: "cl96stky5002ew32gx2kale2x" + profileId: 'cl96stky5002ew32gx2kale2x', }, { OffersFullTime: { baseSalary: { - currency: "SGD", - id: "cl96tbb3o0053w32gz11paaxu", - value: 1999999999 + currency: 'SGD', + id: 'cl96tbb3o0053w32gz11paaxu', + value: 1999999999, }, - baseSalaryId: "cl96tbb3o0053w32gz11paaxu", + baseSalaryId: 'cl96tbb3o0053w32gz11paaxu', bonus: { - currency: "SGD", - id: "cl96tbb3o0055w32gpyqgz5hx", - value: 1410065407 + currency: 'SGD', + id: 'cl96tbb3o0055w32gpyqgz5hx', + value: 1410065407, }, - bonusId: "cl96tbb3o0055w32gpyqgz5hx", - id: "cl96tbb3o0052w32guguajzin", - level: "EXPERT", - specialization: "FRONTEND", + bonusId: 'cl96tbb3o0055w32gpyqgz5hx', + id: 'cl96tbb3o0052w32guguajzin', + level: 'EXPERT', + specialization: 'FRONTEND', stocks: { - currency: "SGD", - id: "cl96tbb3o0057w32gu4nyxguf", - value: 500 + currency: 'SGD', + id: 'cl96tbb3o0057w32gu4nyxguf', + value: 500, }, - stocksId: "cl96tbb3o0057w32gu4nyxguf", - title: "Software Engineer", + stocksId: 'cl96tbb3o0057w32gu4nyxguf', + title: 'Software Engineer', totalCompensation: { - currency: "SGD", - id: "cl96tbb3o0059w32gm3iy1zk4", - value: 55555555 + currency: 'SGD', + id: 'cl96tbb3o0059w32gm3iy1zk4', + value: 55555555, }, - totalCompensationId: "cl96tbb3o0059w32gm3iy1zk4" + totalCompensationId: 'cl96tbb3o0059w32gm3iy1zk4', }, OffersIntern: null, - comments: "this rocks", + comments: 'this rocks', company: { - createdAt: new Date("2022-10-12T16:19:05.196Z"), - description: "Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.", - id: "cl95u79f000007im531ysjg79", - logoUrl: "https://logo.clearbit.com/meta.com", - name: "Meta", - slug: "meta", - updatedAt: new Date("2022-10-12T16:19:05.196Z") + createdAt: new Date('2022-10-12T16:19:05.196Z'), + description: + 'Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.', + id: 'cl95u79f000007im531ysjg79', + logoUrl: 'https://logo.clearbit.com/meta.com', + name: 'Meta', + slug: 'meta', + updatedAt: new Date('2022-10-12T16:19:05.196Z'), }, - companyId: "cl95u79f000007im531ysjg79", - id: "cl96tbb3o0051w32gjrpaiiit", - jobType: "FULLTIME", - location: "Singapore, Singapore", - monthYearReceived: new Date("2022-09-30T07:58:54.000Z"), - negotiationStrategy: "Charmed the guy with my face", - offersFullTimeId: "cl96tbb3o0052w32guguajzin", + companyId: 'cl95u79f000007im531ysjg79', + id: 'cl96tbb3o0051w32gjrpaiiit', + jobType: 'FULLTIME', + location: 'Singapore, Singapore', + monthYearReceived: new Date('2022-09-30T07:58:54.000Z'), + negotiationStrategy: 'Charmed the guy with my face', + offersFullTimeId: 'cl96tbb3o0052w32guguajzin', offersInternId: null, - profileId: "cl96stky5002ew32gx2kale2x" - } + profileId: 'cl96stky5002ew32gx2kale2x', + }, ], - profileName: "ailing bryann stuart ziqing", - token: "afca11e436d21bde24543718fa957c6c625335439dc504f24ee35eae7b5ef1ba", - userId: null + profileName: 'ailing bryann stuart ziqing', + token: 'afca11e436d21bde24543718fa957c6c625335439dc504f24ee35eae7b5ef1ba', + userId: null, }); - } + }; return ( <> diff --git a/apps/portal/src/pages/offers/test/listOffers.tsx b/apps/portal/src/pages/offers/test/listOffers.tsx index 7baaac90..db295d0d 100644 --- a/apps/portal/src/pages/offers/test/listOffers.tsx +++ b/apps/portal/src/pages/offers/test/listOffers.tsx @@ -6,12 +6,11 @@ function Test() { const data = trpc.useQuery([ 'offers.list', { - companyId: 'cl95u79f000007im531ysjg79', - limit: 20, + limit: 100, location: 'Singapore, Singapore', offset: 0, - sortBy: '-monthYearReceived', - yoeCategory: 1, + sortBy: '-totalYoe', + yoeCategory: 2, }, ]); diff --git a/apps/portal/src/pages/offers/test/profileAnalysis.tsx b/apps/portal/src/pages/offers/test/profileAnalysis.tsx new file mode 100644 index 00000000..34f55d26 --- /dev/null +++ b/apps/portal/src/pages/offers/test/profileAnalysis.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +import { trpc } from '~/utils/trpc'; + +function profileAnalysis() { + const analysis = trpc.useQuery([ + 'offers.analysis.generate', + { profileId: 'cl96stky5002ew32gx2kale2x' }, + ]); + + return
{JSON.stringify(analysis.data)}
; +} + +export default profileAnalysis; diff --git a/apps/portal/src/server/router/index.ts b/apps/portal/src/server/router/index.ts index 43fcbd8d..8592986c 100644 --- a/apps/portal/src/server/router/index.ts +++ b/apps/portal/src/server/router/index.ts @@ -3,6 +3,7 @@ import superjson from 'superjson'; import { companiesRouter } from './companies-router'; import { createRouter } from './context'; import { offersRouter } from './offers/offers'; +import { offersAnalysisRouter } from './offers/offers-analysis-router'; import { offersCommentsRouter } from './offers/offers-comments-router'; import { offersProfileRouter } from './offers/offers-profile-router'; import { protectedExampleRouter } from './protected-example-router'; @@ -38,6 +39,7 @@ export const appRouter = createRouter() .merge('questions.questions.', questionsQuestionRouter) .merge('offers.', offersRouter) .merge('offers.profile.', offersProfileRouter) + .merge('offers.analysis.', offersAnalysisRouter) .merge('offers.comments.', offersCommentsRouter); // Export type definition of API diff --git a/apps/portal/src/server/router/offers/offers-analysis-router.ts b/apps/portal/src/server/router/offers/offers-analysis-router.ts new file mode 100644 index 00000000..3552d9ea --- /dev/null +++ b/apps/portal/src/server/router/offers/offers-analysis-router.ts @@ -0,0 +1,264 @@ +import { z } from 'zod'; +import type { + Company, + OffersBackground, + OffersCurrency, + OffersFullTime, + OffersIntern, + OffersOffer, + OffersProfile, +} from '@prisma/client'; +import { TRPCError } from '@trpc/server'; + +import { createRouter } from '../context'; + +const binarySearchOfferPercentile = ( + 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 }; + }, + similarOffers: Array | string, +) => { + let start = 0; + let end = similarOffers.length - 1; + + while (start <= end) { + const mid = Math.floor((start + end) / 2); + + if (similarOffers[mid].id === offer.id) { + return mid; + } + + if (offer.id < similarOffers[mid].id) { + end = mid - 1; + } else { + start = mid + 1; + } + } + return -1; +}; + +export const offersAnalysisRouter = createRouter().query('generate', { + input: z.object({ + profileId: z.string(), + }), + async resolve({ ctx, input }) { + const offers = await ctx.prisma.offersOffer.findMany({ + include: { + OffersFullTime: { + include: { + baseSalary: true, + bonus: true, + stocks: true, + totalCompensation: true, + }, + }, + OffersIntern: { + include: { + monthlySalary: true, + }, + }, + company: true, + profile: { + include: { + background: true, + }, + }, + }, + orderBy: [ + { + OffersFullTime: { + totalCompensation: { + value: 'desc', + }, + }, + }, + { + OffersIntern: { + monthlySalary: { + value: 'desc', + }, + }, + }, + ], + where: { + profileId: input.profileId, + }, + }); + + if (!offers || offers.length === 0) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: 'No offers found on this profile', + }); + } + + const overallHighestOffer = offers[0]; + + // TODO: Shift yoe to background to make it mandatory + if ( + !overallHighestOffer.profile.background || + !overallHighestOffer.profile.background.totalYoe + ) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Cannot analyse without YOE', + }); + } + + const yoe = overallHighestOffer.profile.background.totalYoe as number; + + let similarOffers = await ctx.prisma.offersOffer.findMany({ + include: { + OffersFullTime: { + include: { + totalCompensation: true, + }, + }, + OffersIntern: { + include: { + monthlySalary: true, + }, + }, + company: true, + profile: { + include: { + background: { + include: { + experiences: { + include: { + company: true, + }, + }, + }, + }, + }, + }, + }, + orderBy: [ + { + OffersFullTime: { + totalCompensation: { + value: 'desc', + }, + }, + }, + { + OffersIntern: { + monthlySalary: { + value: 'desc', + }, + }, + }, + ], + where: { + AND: [ + { + location: overallHighestOffer.location, + }, + { + OR: [ + { + OffersFullTime: { + level: overallHighestOffer.OffersFullTime?.level, + specialization: + overallHighestOffer.OffersFullTime?.specialization, + }, + OffersIntern: { + specialization: + overallHighestOffer.OffersIntern?.specialization, + }, + }, + ], + }, + { + profile: { + background: { + AND: [ + { + totalYoe: { + gte: Math.max(yoe - 1, 0), + lte: yoe + 1, + }, + }, + ], + }, + }, + }, + ], + }, + }); + + let similarCompanyOffers = similarOffers.filter( + (offer) => offer.companyId === overallHighestOffer.companyId, + ); + + // CALCULATE PERCENTILES + const highestOfferAgainstOverallIndex = binarySearchOfferPercentile( + overallHighestOffer, + similarOffers, + ); + const highestOfferAgainstOverallPercentile = + highestOfferAgainstOverallIndex / similarOffers.length; + + const highestOfferAgainstCompanyIndex = binarySearchOfferPercentile( + overallHighestOffer, + similarCompanyOffers, + ); + const highestOfferAgainstCompanyPercentile = + highestOfferAgainstCompanyIndex / similarCompanyOffers.length; + + // FIND TOP >=90 PERCENTILE OFFERS + similarOffers = similarOffers.filter( + (offer) => offer.id !== overallHighestOffer.id, + ); + similarCompanyOffers = similarCompanyOffers.filter( + (offer) => offer.id !== overallHighestOffer.id, + ); + + const similarOffersCount = similarOffers.length; + const similarOffers90PercentileIndex = + Math.floor(similarOffersCount * 0.9) - 1; + const topPercentileOffers = + similarOffersCount > 1 + ? similarOffers.slice( + similarOffers90PercentileIndex, + similarOffers90PercentileIndex + 2, + ) + : similarOffers; + + const similarCompanyOffersCount = similarCompanyOffers.length; + const similarCompanyOffers90PercentileIndex = + Math.floor(similarCompanyOffersCount * 0.9) - 1; + const topPercentileCompanyOffers = + similarCompanyOffersCount > 1 + ? similarCompanyOffers.slice( + similarCompanyOffers90PercentileIndex, + similarCompanyOffers90PercentileIndex + 2, + ) + : similarCompanyOffers; + + return { + company: { + highestOfferAgainstCompanyPercentile, + similarCompanyOffersCount, + topPercentileCompanyOffers, + }, + overall: { + highestOfferAgainstOverallPercentile, + similarOffersCount, + topPercentileOffers, + }, + overallHighestOffer, + }; + }, +}); diff --git a/apps/portal/src/server/router/offers/offers.ts b/apps/portal/src/server/router/offers/offers.ts index 9aaeb487..ceb1367c 100644 --- a/apps/portal/src/server/router/offers/offers.ts +++ b/apps/portal/src/server/router/offers/offers.ts @@ -16,8 +16,8 @@ const getYoeRange = (yoeCategory: number) => { : yoeCategoryMap[yoeCategory] === 'Mid' ? { maxYoe: 7, minYoe: 4 } : yoeCategoryMap[yoeCategory] === 'Senior' - ? { maxYoe: null, minYoe: 8 } - : null; + ? { maxYoe: 100, minYoe: 8 } + : null; // Internship }; const ascOrder = '+'; @@ -35,7 +35,7 @@ export const offersRouter = createRouter().query('list', { companyId: z.string().nullish(), dateEnd: z.date().nullish(), dateStart: z.date().nullish(), - limit: z.number().nonnegative(), + limit: z.number().positive(), location: z.string(), offset: z.number().nonnegative(), salaryMax: z.number().nullish(), @@ -43,9 +43,13 @@ export const offersRouter = createRouter().query('list', { sortBy: z.string().regex(createSortByValidationRegex()).nullish(), title: z.string().nullish(), yoeCategory: z.number().min(0).max(3), + yoeMax: z.number().max(100).nullish(), + yoeMin: z.number().min(0).nullish(), }), async resolve({ ctx, input }) { const yoeRange = getYoeRange(input.yoeCategory); + const yoeMin = input.yoeMin ? input.yoeMin : yoeRange?.minYoe; + const yoeMax = input.yoeMax ? input.yoeMax : yoeRange?.maxYoe; let data = !yoeRange ? await ctx.prisma.offersOffer.findMany({ @@ -89,68 +93,8 @@ export const offersRouter = createRouter().query('list', { ], }, }) - : yoeRange.maxYoe - ? await ctx.prisma.offersOffer.findMany({ - // Junior, Mid - include: { - OffersFullTime: { - include: { - baseSalary: true, - bonus: true, - stocks: true, - totalCompensation: true, - }, - }, - OffersIntern: { - include: { - monthlySalary: true, - }, - }, - company: true, - profile: { - include: { - background: true, - }, - }, - }, - where: { - AND: [ - { - location: input.location, - }, - { - OffersIntern: { - is: null, - }, - }, - { - OffersFullTime: { - isNot: null, - }, - }, - { - profile: { - background: { - totalYoe: { - gte: yoeRange.minYoe, - }, - }, - }, - }, - { - profile: { - background: { - totalYoe: { - gte: yoeRange.maxYoe, - }, - }, - }, - }, - ], - }, - }) : await ctx.prisma.offersOffer.findMany({ - // Senior + // Junior, Mid, Senior include: { OffersFullTime: { include: { @@ -191,7 +135,8 @@ export const offersRouter = createRouter().query('list', { profile: { background: { totalYoe: { - gte: yoeRange.minYoe, + gte: yoeMin, + lte: yoeMax, }, }, },