diff --git a/apps/portal/prisma/migrations/20221009160601_/migration.sql b/apps/portal/prisma/migrations/20221009160601_/migration.sql
new file mode 100644
index 00000000..0b63fe6e
--- /dev/null
+++ b/apps/portal/prisma/migrations/20221009160601_/migration.sql
@@ -0,0 +1,204 @@
+-- CreateEnum
+CREATE TYPE "JobType" AS ENUM ('INTERN', 'FULLTIME');
+
+-- CreateTable
+CREATE TABLE "OffersProfile" (
+ "id" TEXT NOT NULL,
+ "profileName" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "editToken" TEXT NOT NULL,
+ "userId" TEXT,
+
+ CONSTRAINT "OffersProfile_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "OffersBackground" (
+ "id" TEXT NOT NULL,
+ "totalYoe" INTEGER,
+ "offersProfileId" TEXT NOT NULL,
+
+ CONSTRAINT "OffersBackground_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "OffersSpecificYoe" (
+ "id" TEXT NOT NULL,
+ "yoe" INTEGER NOT NULL,
+ "domain" TEXT NOT NULL,
+ "backgroundId" TEXT NOT NULL,
+
+ CONSTRAINT "OffersSpecificYoe_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "OffersExperience" (
+ "id" TEXT NOT NULL,
+ "companyId" TEXT,
+ "jobType" "JobType",
+ "title" TEXT,
+ "durationInMonths" INTEGER,
+ "specialization" TEXT,
+ "level" TEXT,
+ "totalCompensationId" TEXT,
+ "monthlySalaryId" TEXT,
+ "backgroundId" TEXT NOT NULL,
+
+ CONSTRAINT "OffersExperience_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "OffersCurrency" (
+ "id" TEXT NOT NULL,
+ "value" INTEGER NOT NULL,
+ "currency" TEXT NOT NULL,
+
+ CONSTRAINT "OffersCurrency_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "OffersEducation" (
+ "id" TEXT NOT NULL,
+ "type" TEXT,
+ "field" TEXT,
+ "isAttending" BOOLEAN,
+ "school" TEXT,
+ "startDate" TIMESTAMP(3),
+ "endDate" TIMESTAMP(3),
+ "backgroundId" TEXT NOT NULL,
+
+ CONSTRAINT "OffersEducation_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "OffersReply" (
+ "id" TEXT NOT NULL,
+ "creator" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "message" TEXT NOT NULL,
+ "replyingToId" TEXT,
+ "profileId" TEXT NOT NULL,
+
+ CONSTRAINT "OffersReply_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "OffersOffer" (
+ "id" TEXT NOT NULL,
+ "profileId" TEXT NOT NULL,
+ "companyId" TEXT NOT NULL,
+ "monthYearReceived" TIMESTAMP(3) NOT NULL,
+ "location" TEXT NOT NULL,
+ "negotiationStrategy" TEXT,
+ "comments" TEXT,
+ "jobType" "JobType" NOT NULL,
+
+ CONSTRAINT "OffersOffer_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "OffersIntern" (
+ "offerId" TEXT NOT NULL,
+ "title" TEXT NOT NULL,
+ "specialization" TEXT NOT NULL,
+ "internshipCycle" TEXT NOT NULL,
+ "startYear" INTEGER NOT NULL,
+ "monthlySalaryId" TEXT NOT NULL,
+
+ CONSTRAINT "OffersIntern_pkey" PRIMARY KEY ("offerId")
+);
+
+-- CreateTable
+CREATE TABLE "OffersFullTime" (
+ "offerId" TEXT NOT NULL,
+ "title" TEXT NOT NULL,
+ "specialization" TEXT NOT NULL,
+ "level" TEXT NOT NULL,
+ "totalCompensationId" TEXT NOT NULL,
+ "baseSalaryId" TEXT NOT NULL,
+ "bonusId" TEXT NOT NULL,
+ "stocksId" TEXT NOT NULL,
+
+ CONSTRAINT "OffersFullTime_pkey" PRIMARY KEY ("offerId")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "OffersBackground_offersProfileId_key" ON "OffersBackground"("offersProfileId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "OffersExperience_totalCompensationId_key" ON "OffersExperience"("totalCompensationId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "OffersExperience_monthlySalaryId_key" ON "OffersExperience"("monthlySalaryId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "OffersIntern_monthlySalaryId_key" ON "OffersIntern"("monthlySalaryId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "OffersFullTime_totalCompensationId_key" ON "OffersFullTime"("totalCompensationId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "OffersFullTime_baseSalaryId_key" ON "OffersFullTime"("baseSalaryId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "OffersFullTime_bonusId_key" ON "OffersFullTime"("bonusId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "OffersFullTime_stocksId_key" ON "OffersFullTime"("stocksId");
+
+-- AddForeignKey
+ALTER TABLE "OffersProfile" ADD CONSTRAINT "OffersProfile_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "OffersBackground" ADD CONSTRAINT "OffersBackground_offersProfileId_fkey" FOREIGN KEY ("offersProfileId") REFERENCES "OffersProfile"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "OffersSpecificYoe" ADD CONSTRAINT "OffersSpecificYoe_backgroundId_fkey" FOREIGN KEY ("backgroundId") REFERENCES "OffersBackground"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "OffersExperience" ADD CONSTRAINT "OffersExperience_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "OffersExperience" ADD CONSTRAINT "OffersExperience_totalCompensationId_fkey" FOREIGN KEY ("totalCompensationId") REFERENCES "OffersCurrency"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "OffersExperience" ADD CONSTRAINT "OffersExperience_monthlySalaryId_fkey" FOREIGN KEY ("monthlySalaryId") REFERENCES "OffersCurrency"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "OffersExperience" ADD CONSTRAINT "OffersExperience_backgroundId_fkey" FOREIGN KEY ("backgroundId") REFERENCES "OffersBackground"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "OffersEducation" ADD CONSTRAINT "OffersEducation_backgroundId_fkey" FOREIGN KEY ("backgroundId") REFERENCES "OffersBackground"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "OffersReply" ADD CONSTRAINT "OffersReply_replyingToId_fkey" FOREIGN KEY ("replyingToId") REFERENCES "OffersReply"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "OffersReply" ADD CONSTRAINT "OffersReply_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "OffersProfile"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "OffersOffer" ADD CONSTRAINT "OffersOffer_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "OffersProfile"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "OffersOffer" ADD CONSTRAINT "OffersOffer_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "OffersIntern" ADD CONSTRAINT "OffersIntern_offerId_fkey" FOREIGN KEY ("offerId") REFERENCES "OffersOffer"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "OffersIntern" ADD CONSTRAINT "OffersIntern_monthlySalaryId_fkey" FOREIGN KEY ("monthlySalaryId") REFERENCES "OffersCurrency"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "OffersFullTime" ADD CONSTRAINT "OffersFullTime_offerId_fkey" FOREIGN KEY ("offerId") REFERENCES "OffersOffer"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "OffersFullTime" ADD CONSTRAINT "OffersFullTime_totalCompensationId_fkey" FOREIGN KEY ("totalCompensationId") REFERENCES "OffersCurrency"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "OffersFullTime" ADD CONSTRAINT "OffersFullTime_baseSalaryId_fkey" FOREIGN KEY ("baseSalaryId") REFERENCES "OffersCurrency"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "OffersFullTime" ADD CONSTRAINT "OffersFullTime_bonusId_fkey" FOREIGN KEY ("bonusId") REFERENCES "OffersCurrency"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "OffersFullTime" ADD CONSTRAINT "OffersFullTime_stocksId_fkey" FOREIGN KEY ("stocksId") REFERENCES "OffersCurrency"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
diff --git a/apps/portal/prisma/schema.prisma b/apps/portal/prisma/schema.prisma
index 59c53a40..70a88985 100644
--- a/apps/portal/prisma/schema.prisma
+++ b/apps/portal/prisma/schema.prisma
@@ -58,6 +58,7 @@ model User {
questionsAnswerVotes QuestionsAnswerVote[]
questionsAnswerComments QuestionsAnswerComment[]
questionsAnswerCommentVotes QuestionsAnswerCommentVote[]
+ OffersProfile OffersProfile[]
}
enum Vote {
@@ -89,13 +90,15 @@ enum TodoStatus {
}
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
+ 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.
@@ -170,6 +173,179 @@ model ResumesCommentVote {
// Add Offers project models here, prefix all models with "Offer",
// use camelCase for field names, and try to name them consistently
// across all models in this file.
+
+model OffersProfile {
+ id String @id @default(cuid())
+ profileName String @unique
+ createdAt DateTime @default(now())
+
+ background OffersBackground?
+
+ editToken String
+
+ discussion OffersReply[]
+
+ offers OffersOffer[]
+
+ user User? @relation(fields: [userId], references: [id])
+ userId String?
+}
+
+model OffersBackground {
+ id String @id @default(cuid())
+
+ totalYoe Int?
+ specificYoes OffersSpecificYoe[]
+
+ experiences OffersExperience[] // For extensibility in the future
+
+ educations OffersEducation[] // For extensibility in the future
+
+ profile OffersProfile @relation(fields: [offersProfileId], references: [id])
+ offersProfileId String @unique
+}
+
+model OffersSpecificYoe {
+ id String @id @default(cuid())
+
+ yoe Int
+ domain String
+
+ background OffersBackground @relation(fields: [backgroundId], references: [id])
+ backgroundId String
+}
+
+model OffersExperience {
+ id String @id @default(cuid())
+
+ company Company? @relation(fields: [companyId], references: [id])
+ companyId String?
+
+ jobType JobType?
+ title String?
+
+ // Add more fields
+ durationInMonths Int?
+ specialization String?
+
+ // 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
+
+ background OffersBackground @relation(fields: [backgroundId], references: [id])
+ backgroundId String
+}
+
+model OffersCurrency {
+ id String @id @default(cuid())
+ value Int
+ currency String
+
+ // 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")
+
+ // Intern
+ OffersMonthlySalary OffersIntern?
+}
+
+enum JobType {
+ INTERN
+ FULLTIME
+}
+
+model OffersEducation {
+ id String @id @default(cuid())
+ type String?
+ field String?
+
+ // Add more fields
+ school String?
+ startDate DateTime?
+ endDate DateTime?
+
+ background OffersBackground @relation(fields: [backgroundId], references: [id])
+ backgroundId String
+}
+
+model OffersReply {
+ id String @id @default(cuid())
+ creator String
+ createdAt DateTime @default(now())
+ message String
+
+ replyingToId String?
+ replyingTo OffersReply? @relation("ReplyThread", fields: [replyingToId], references: [id])
+ replies OffersReply[] @relation("ReplyThread")
+
+ profile OffersProfile @relation(fields: [profileId], references: [id])
+ profileId String
+}
+
+model OffersOffer {
+ id String @id @default(cuid())
+
+ profile OffersProfile @relation(fields: [profileId], references: [id])
+ profileId String
+
+ company Company @relation(fields: [companyId], references: [id])
+ companyId String
+
+ monthYearReceived DateTime
+ location String
+ negotiationStrategy String?
+ comments String?
+
+ jobType JobType
+
+ OffersIntern OffersIntern? @relation(fields: [offersInternId], references: [id])
+ offersInternId String? @unique
+
+ OffersFullTime OffersFullTime? @relation(fields: [offersFullTimeId], references: [id])
+ offersFullTimeId String? @unique
+}
+
+model OffersIntern {
+ id String @id @default(cuid())
+
+ title String
+ specialization String
+ internshipCycle String
+ startYear Int
+ monthlySalary OffersCurrency @relation(fields: [monthlySalaryId], references: [id])
+ monthlySalaryId String @unique
+
+ OffersOffer OffersOffer?
+}
+
+model OffersFullTime {
+ id String @id @default(cuid())
+ title String
+ specialization String
+ level String
+ totalCompensation OffersCurrency @relation("OfferTotalCompensation", fields: [totalCompensationId], references: [id])
+ totalCompensationId String @unique
+ baseSalary OffersCurrency @relation("OfferBaseSalary", fields: [baseSalaryId], references: [id])
+ baseSalaryId String @unique
+ bonus OffersCurrency @relation("OfferBonus", fields: [bonusId], references: [id])
+ bonusId String @unique
+ stocks OffersCurrency @relation("OfferStocks", fields: [stocksId], references: [id])
+ stocksId String @unique
+
+ OffersOffer OffersOffer?
+}
+
// End of Offers project models.
// Start of Questions project models.
diff --git a/apps/portal/prisma/seed.ts b/apps/portal/prisma/seed.ts
index 4af35e05..c31d6705 100644
--- a/apps/portal/prisma/seed.ts
+++ b/apps/portal/prisma/seed.ts
@@ -35,9 +35,37 @@ const COMPANIES = [
},
];
+const OFFER_PROFILES = [
+ {
+ id: 'cl91v97ex000109mt7fka5rto',
+ profileName: 'battery-horse-stable-cow',
+ editToken: 'cl91ulmhg000009l86o45aspt',
+ },
+ {
+ id: 'cl91v9iw2000209mtautgdnxq',
+ profileName: 'house-zebra-fast-giraffe',
+ editToken: 'cl91umigc000109l80f1tcqe8',
+ },
+ {
+ id: 'cl91v9m3y000309mt1ctw55wi',
+ profileName: 'keyboard-mouse-lazy-cat',
+ editToken: 'cl91ummoa000209l87q2b8hl7',
+ },
+ {
+ id: 'cl91v9p09000409mt5rvoasf1',
+ profileName: 'router-hen-bright-pig',
+ editToken: 'cl91umqa3000309l87jyefe9k',
+ },
+ {
+ id: 'cl91v9uda000509mt5i5fez3v',
+ profileName: 'screen-ant-dirty-bird',
+ editToken: 'cl91umuj9000409l87ez85vmg',
+ },
+];
+
async function main() {
console.log('Seeding started...');
- await Promise.all(
+ await Promise.all([
COMPANIES.map(async (company) => {
await prisma.company.upsert({
where: { slug: company.slug },
@@ -45,7 +73,14 @@ async function main() {
create: company,
});
}),
- );
+ OFFER_PROFILES.map(async (offerProfile) => {
+ await prisma.offersProfile.upsert({
+ where: { profileName: offerProfile.profileName },
+ update: offerProfile,
+ create: offerProfile,
+ });
+ }),
+ ]);
console.log('Seeding completed.');
}
diff --git a/apps/portal/src/pages/offers/test.tsx b/apps/portal/src/pages/offers/test.tsx
new file mode 100644
index 00000000..5206935f
--- /dev/null
+++ b/apps/portal/src/pages/offers/test.tsx
@@ -0,0 +1,26 @@
+import React from 'react';
+
+import { trpc } from '~/utils/trpc';
+
+function test() {
+ const data = trpc.useQuery([
+ 'offers.list',
+ {
+ limit: 3,
+ location: 'Singapore, Singapore',
+ offset: 0,
+ sortBy: '-monthYearReceived',
+ yoeCategory: 0,
+ },
+ ]);
+
+ return (
+
+ {data.data?.map((x) => {
+ return - {JSON.stringify(x)}
;
+ })}
+
+ );
+}
+
+export default test;
diff --git a/apps/portal/src/pages/offers/testCreateProfile.tsx b/apps/portal/src/pages/offers/testCreateProfile.tsx
new file mode 100644
index 00000000..f58dbfd4
--- /dev/null
+++ b/apps/portal/src/pages/offers/testCreateProfile.tsx
@@ -0,0 +1,144 @@
+import React, { useState } from 'react';
+
+import { trpc } from '~/utils/trpc';
+
+function Test() {
+// F const data = trpc.useQuery([
+// 'offers.profile.',
+// {
+// limit: 3,
+// location: 'Singapore, Singapore',
+// offset: 0,
+// yoeCategory: 0,
+// },
+// ]);
+
+ const [createdData, setCreatedData] = useState("")
+
+ const createMutation = trpc.useMutation(['offers.profile.create'], {
+ onError(error: any) {
+ alert(error)
+ },
+ onSuccess(data) {
+ setCreatedData(JSON.stringify(data))
+ },
+ });
+
+ const handleClick = () => {
+ createMutation.mutate({
+"background": {
+ "educations": [
+ {
+ "endDate": new Date("2018-09-30T07:58:54.000Z"),
+ "field": "Computer Science",
+ "school": "National University of Singapore",
+ "startDate": new Date("2014-09-30T07:58:54.000Z"),
+ "type": "Bachelors"
+ }
+ ],
+ "experiences": [
+ {
+ "companyId": "cl92ly8xm0000w3mwh5ymyqmx",
+ "durationInMonths": 24,
+ "jobType": "FULLTIME",
+ "level": "Junior",
+ // "monthlySalary": undefined,
+ "specialization": "Front End",
+ "title": "Software Engineer",
+ "totalCompensation": {
+ "currency": "SGD",
+ "value": 104100
+ }
+ }
+ ],
+ "specificYoes": [
+ {
+ "domain": "Front End",
+ "yoe": 2
+ },
+ {
+ "domain": "Full Stack",
+ "yoe": 2
+ }
+ ],
+ "totalYoe": 4
+ },
+ "offers": [
+ {
+ "comments": "",
+ "companyId": "cl92ly8xm0000w3mwh5ymyqmx",
+ "job": {
+ "base": {
+ "currency": "SGD",
+ "value": 84000
+ },
+ "bonus": {
+ "currency": "SGD",
+ "value": 20000
+ },
+ "level": "Junior",
+ "specialization": "Front End",
+ "stocks": {
+ "currency": "SGD",
+ "value": 100
+ },
+ "title": "Software Engineer",
+ "totalCompensation": {
+ "currency": "SGD",
+ "value": 104100
+ }
+ },
+ "jobType": "FULLTIME",
+ "location": "Singapore, Singapore",
+ "monthYearReceived": new Date("2022-09-30T07:58:54.000Z"),
+ "negotiationStrategy": "Leveraged having multiple offers"
+ },
+ {
+ "comments": "",
+ "companyId": "cl92ly8xm0000w3mwh5ymyqmx",
+ "job": {
+ "base": {
+ "currency": "SGD",
+ "value": 84000
+ },
+ "bonus": {
+ "currency": "SGD",
+ "value": 20000
+ },
+ "level": "Junior",
+ "specialization": "Front End",
+ "stocks": {
+ "currency": "SGD",
+ "value": 100
+ },
+ "title": "Software Engineer",
+ "totalCompensation": {
+ "currency": "SGD",
+ "value": 104100
+ }
+ },
+ "jobType": "FULLTIME",
+ "location": "Singapore, Singapore",
+ "monthYearReceived": new Date("2022-09-30T07:58:54.000Z"),
+ "negotiationStrategy": "Leveraged having multiple offers"
+ }
+ ]
+ });
+ };
+
+ return (
+ //
+ // {createdData.map((x) => {
+ // return - {JSON.stringify(x)}
;
+ // })}
+ //
+ <>
+
+ {createdData}
+
+
+ >
+ );
+}
+
+export default Test;
diff --git a/apps/portal/src/server/router/index.ts b/apps/portal/src/server/router/index.ts
index aa8446ca..c018c3f7 100644
--- a/apps/portal/src/server/router/index.ts
+++ b/apps/portal/src/server/router/index.ts
@@ -2,6 +2,8 @@ import superjson from 'superjson';
import { companiesRouter } from './companies-router';
import { createRouter } from './context';
+import { offersRouter } from './offers';
+import { offersProfileRouter } from './offers-profile-router';
import { protectedExampleRouter } from './protected-example-router';
import { questionsAnswerCommentRouter } from './questions-answer-comment-router';
import { questionsAnswerRouter } from './questions-answer-router';
@@ -32,7 +34,9 @@ export const appRouter = createRouter()
.merge('questions.answers.comments.', questionsAnswerCommentRouter)
.merge('questions.answers.', questionsAnswerRouter)
.merge('questions.questions.comments.', questionsQuestionCommentRouter)
- .merge('questions.questions.', questionsQuestionRouter);
+ .merge('questions.questions.', questionsQuestionRouter)
+ .merge('offers.', offersRouter)
+ .merge('offers.profile.', offersProfileRouter);
// Export type definition of API
export type AppRouter = typeof appRouter;
diff --git a/apps/portal/src/server/router/offers-profile-router.ts b/apps/portal/src/server/router/offers-profile-router.ts
new file mode 100644
index 00000000..286b6e5a
--- /dev/null
+++ b/apps/portal/src/server/router/offers-profile-router.ts
@@ -0,0 +1,269 @@
+import crypto, { randomUUID } from "crypto";
+import { z } from "zod";
+import { Prisma } from "@prisma/client";
+
+import { createProtectedRouter } from "./context";
+
+const valuation = z.object({
+ currency: z.string(),
+ value: z.number(),
+})
+
+// TODO: handle both full time and intern
+const offer = z.object({
+ comments: z.string(),
+ companyId: z.string(),
+ job: z.object({
+ base: valuation.optional(), // Full time
+ bonus: valuation.optional(), // Full time
+ internshipCycle: z.string().optional(), // Intern
+ level: z.string().optional(), // Full time
+ monthlySalary: valuation.optional(), // Intern
+ specialization: z.string(),
+ startYear: z.number().optional(), // Intern
+ stocks: valuation.optional(), // Full time
+ title: z.string(),
+ totalCompensation: valuation.optional(), // Full time
+ }),
+ jobType: z.string(),
+ location: z.string(),
+ monthYearReceived: z.date(),
+ negotiationStrategy: z.string(),
+})
+
+const experience = z.object({
+ companyId: z.string().optional(),
+ durationInMonths: z.number().optional(),
+ jobType: z.string().optional(),
+ level: z.string().optional(),
+ monthlySalary: valuation.optional(),
+ specialization: z.string().optional(),
+ title: z.string().optional(),
+ totalCompensation: valuation.optional(),
+})
+
+const education = z.object({
+ endDate: z.date().optional(),
+ field: z.string().optional(),
+ school: z.string().optional(),
+ startDate: z.date().optional(),
+ type: z.string().optional(),
+})
+
+export const offersProfileRouter = createProtectedRouter().mutation(
+ 'create',
+ {
+ input: z.object({
+ background: z.object({
+ educations: z.array(education),
+ experiences: z.array(experience),
+ specificYoes: z.array(z.object({
+ domain: z.string(),
+ yoe: z.number()
+ })),
+ totalYoe: z.number().optional(),
+ }),
+ offers: z.array(offer)
+ }),
+ async resolve({ ctx, input }) {
+
+ // TODO: add more
+ const token = crypto
+ .createHash("sha256")
+ .update(Date.now().toString())
+ .digest("hex");
+
+ const profile = await ctx.prisma.offersProfile.create({
+ data: {
+ background: {
+ create: {
+ educations: {
+ create:
+ input.background.educations.map((x) => ({
+ endDate: x.endDate,
+ field: x.field,
+ school: x.school,
+ startDate: x.startDate,
+ type: x.type
+ }))
+ },
+ experiences: {
+ create:
+ input.background.experiences.map((x) => {
+ if (x.jobType === "FULLTIME" && x.totalCompensation?.currency !== undefined && x.totalCompensation.value !== undefined) {
+ return {
+ company: {
+ connect: {
+ id: x.companyId
+ }
+ },
+ durationInMonths: x.durationInMonths,
+ jobType: x.jobType,
+ level: x.level,
+ specialization: x.specialization,
+ title: x.title,
+ totalCompensation: {
+ create: {
+ currency: x.totalCompensation?.currency,
+ value: x.totalCompensation?.value,
+ }
+ },
+ }
+ }
+ if (x.jobType === "INTERN" && x.monthlySalary?.currency !== undefined && x.monthlySalary.value !== undefined) {
+ return {
+ company: {
+ connect: {
+ id: x.companyId
+ }
+ },
+ durationInMonths: x.durationInMonths,
+ jobType: x.jobType,
+ monthlySalary: {
+ create: {
+ currency: x.monthlySalary?.currency,
+ value: x.monthlySalary?.value
+ }
+ },
+ specialization: x.specialization,
+ title: x.title,
+ }
+ }
+
+ throw Prisma.PrismaClientKnownRequestError
+
+ })
+ },
+ specificYoes: {
+ create:
+ input.background.specificYoes.map((x) => ({
+ domain: x.domain,
+ yoe: x.yoe
+ }))
+ },
+ totalYoe: input.background.totalYoe,
+ }
+ },
+ editToken: token,
+ offers: {
+ create:
+ input.offers.map((x) => {
+ if (x.jobType === "INTERN" && x.job.internshipCycle !== undefined && x.job.monthlySalary?.currency !== undefined && x.job.monthlySalary.value !== undefined && x.job.startYear !== undefined) {
+ return {
+ OffersIntern: {
+ create: {
+ internshipCycle: x.job.internshipCycle,
+ monthlySalary: {
+ create: {
+ currency: x.job.monthlySalary?.currency,
+ value: x.job.monthlySalary?.value
+ }
+ },
+ specialization: x.job.specialization,
+ startYear: x.job.startYear,
+ title: x.job.title,
+ }
+ },
+ comments: x.comments,
+ company: {
+ connect: {
+ id: x.companyId
+ }
+ },
+ jobType: x.jobType,
+ location: x.location,
+ monthYearReceived: x.monthYearReceived,
+ negotiationStrategy: x.negotiationStrategy
+ }
+ }
+ if (x.jobType === "FULLTIME" && x.job.base?.currency !== undefined && x.job.base?.value !== undefined && x.job.bonus?.currency !== undefined && x.job.bonus?.value !== undefined && x.job.stocks?.currency !== undefined && x.job.stocks?.value !== undefined && x.job.totalCompensation?.currency !== undefined && x.job.totalCompensation?.value !== undefined && x.job.level !== undefined) {
+ return {
+ OffersFullTime: {
+ create: {
+ baseSalary: {
+ create: {
+ currency: x.job.base?.currency,
+ value: x.job.base?.value
+ }
+ },
+ bonus: {
+ create: {
+ currency: x.job.bonus?.currency,
+ value: x.job.bonus?.value
+ }
+ },
+ level: x.job.level,
+ specialization: x.job.specialization,
+ stocks: {
+ create: {
+ currency: x.job.stocks?.currency,
+ value: x.job.stocks?.value,
+ }
+ },
+ title: x.job.title,
+ totalCompensation: {
+ create: {
+ currency: x.job.totalCompensation?.currency,
+ value: x.job.totalCompensation?.value,
+ }
+ },
+ }
+ },
+ comments: x.comments,
+ company: {
+ connect: {
+ id: x.companyId
+ }
+ },
+ jobType: x.jobType,
+ location: x.location,
+ monthYearReceived: x.monthYearReceived,
+ negotiationStrategy: x.negotiationStrategy
+ }
+ }
+
+ // Throw error
+ throw Prisma.PrismaClientKnownRequestError
+ })
+ },
+ profileName: randomUUID(),
+ },
+ 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
+ }
+ },
+);
diff --git a/apps/portal/src/server/router/offers.ts b/apps/portal/src/server/router/offers.ts
new file mode 100644
index 00000000..28e6c0eb
--- /dev/null
+++ b/apps/portal/src/server/router/offers.ts
@@ -0,0 +1,297 @@
+import assert from 'assert';
+import { z } from 'zod';
+
+import { createRouter } from './context';
+
+const yoeCategoryMap: Record = {
+ 0: 'Internship',
+ 1: 'Fresh Grad',
+ 2: 'Mid',
+ 3: 'Senior',
+};
+
+const getYoeRange = (yoeCategory: number) => {
+ return yoeCategoryMap[yoeCategory] === 'Fresh Grad'
+ ? { maxYoe: 3, minYoe: 0 }
+ : yoeCategoryMap[yoeCategory] === 'Mid'
+ ? { maxYoe: 7, minYoe: 4 }
+ : yoeCategoryMap[yoeCategory] === 'Senior'
+ ? { maxYoe: null, minYoe: 8 }
+ : null;
+};
+
+const ascOrder = '+';
+const descOrder = '-';
+const sortingKeys = ['monthYearReceived', 'totalCompensation', 'yoe'];
+
+const createSortByValidationRegex = () => {
+ const startsWithPlusOrMinusOnly = '^[+-]{1}';
+ const sortingKeysRegex = sortingKeys.join('|');
+ return new RegExp(startsWithPlusOrMinusOnly + '(' + sortingKeysRegex + ')');
+};
+
+export const offersRouter = createRouter().query('list', {
+ input: z.object({
+ company: z.string().nullish(),
+ dateEnd: z.date().nullish(),
+ dateStart: z.date().nullish(),
+ limit: z.number().nonnegative(),
+ location: z.string(),
+ offset: z.number().nonnegative(),
+ salaryMax: z.number().nullish(),
+ salaryMin: z.number().nonnegative().nullish(),
+ sortBy: z.string().regex(createSortByValidationRegex()).nullish(),
+ title: z.string().nullish(),
+ yoeCategory: z.number().min(0).max(3),
+ }),
+ async resolve({ ctx, input }) {
+ const yoeRange = getYoeRange(input.yoeCategory);
+
+ let data = !yoeRange
+ ? await ctx.prisma.offersOffer.findMany({
+ // Internship
+ include: {
+ OffersFullTime: {
+ include: {
+ baseSalary: true,
+ bonus: true,
+ stocks: true,
+ totalCompensation: true,
+ },
+ },
+ OffersIntern: {
+ include: {
+ monthlySalary: true,
+ },
+ },
+ company: true,
+ },
+ skip: input.limit * input.offset,
+ take: input.limit,
+ where: {
+ AND: [
+ {
+ location: input.location,
+ },
+ {
+ OffersIntern: {
+ isNot: null,
+ },
+ },
+ {
+ OffersFullTime: {
+ is: null,
+ },
+ },
+ ],
+ },
+ })
+ : yoeRange.maxYoe
+ ? await ctx.prisma.offersOffer.findMany({
+ include: {
+ OffersFullTime: {
+ include: {
+ baseSalary: true,
+ bonus: true,
+ stocks: true,
+ totalCompensation: true,
+ },
+ },
+ OffersIntern: {
+ include: {
+ monthlySalary: true,
+ },
+ },
+ company: true,
+ },
+ // Junior, Mid
+ skip: input.limit * input.offset,
+ take: input.limit,
+ 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
+ include: {
+ OffersFullTime: {
+ include: {
+ baseSalary: true,
+ bonus: true,
+ stocks: true,
+ totalCompensation: true,
+ },
+ },
+ OffersIntern: {
+ include: {
+ monthlySalary: true,
+ },
+ },
+ company: true,
+ },
+ skip: input.limit * input.offset,
+ take: input.limit,
+ where: {
+ AND: [
+ {
+ location: input.location,
+ },
+ {
+ OffersIntern: {
+ is: null,
+ },
+ },
+ {
+ OffersFullTime: {
+ isNot: null,
+ },
+ },
+ {
+ profile: {
+ background: {
+ totalYoe: {
+ gte: yoeRange.minYoe,
+ },
+ },
+ },
+ },
+ ],
+ },
+ });
+
+ data = data.filter((offer) => {
+ let validRecord = true;
+
+ if (input.company) {
+ validRecord = validRecord && offer.company.name === input.company;
+ }
+
+ if (input.title) {
+ validRecord =
+ validRecord &&
+ (offer.OffersFullTime?.title === input.title ||
+ offer.OffersIntern?.title === input.title);
+ }
+
+ if (input.dateStart && input.dateEnd) {
+ validRecord =
+ validRecord &&
+ offer.monthYearReceived.getTime() >= input.dateStart.getTime() &&
+ offer.monthYearReceived.getTime() <= input.dateEnd.getTime();
+ }
+
+ if (input.salaryMin && input.salaryMax) {
+ const salary = offer.OffersFullTime?.totalCompensation.value
+ ? offer.OffersFullTime?.totalCompensation.value
+ : offer.OffersIntern?.monthlySalary.value;
+
+ assert(salary);
+
+ validRecord =
+ validRecord && salary >= input.salaryMin && salary <= input.salaryMax;
+ }
+
+ return validRecord;
+ });
+
+ data = data.sort((offer1, offer2) => {
+ const defaultReturn =
+ offer2.monthYearReceived.getTime() - offer1.monthYearReceived.getTime();
+
+ if (!input.sortBy) {
+ return defaultReturn;
+ }
+
+ const order = input.sortBy.charAt(0);
+ const sortingKey = input.sortBy.substring(1);
+
+ if (order === ascOrder) {
+ return (() => {
+ if (sortingKey === 'monthYearReceived') {
+ return (
+ offer1.monthYearReceived.getTime() -
+ offer2.monthYearReceived.getTime()
+ );
+ }
+
+ if (sortingKey === 'totalCompensation') {
+ const salary1 = offer1.OffersFullTime?.totalCompensation.value
+ ? offer1.OffersFullTime?.totalCompensation.value
+ : offer1.OffersIntern?.monthlySalary.value;
+
+ const salary2 = offer2.OffersFullTime?.totalCompensation.value
+ ? offer2.OffersFullTime?.totalCompensation.value
+ : offer2.OffersIntern?.monthlySalary.value;
+
+ if (salary1 && salary2) {
+ return salary1 - salary2;
+ }
+ }
+ return defaultReturn;
+ })();
+ }
+
+ if (order === descOrder) {
+ return (() => {
+ if (sortingKey === 'monthYearReceived') {
+ return (
+ offer2.monthYearReceived.getTime() -
+ offer1.monthYearReceived.getTime()
+ );
+ }
+
+ if (sortingKey === 'totalCompensation') {
+ const salary1 = offer1.OffersFullTime?.totalCompensation.value
+ ? offer1.OffersFullTime?.totalCompensation.value
+ : offer1.OffersIntern?.monthlySalary.value;
+
+ const salary2 = offer2.OffersFullTime?.totalCompensation.value
+ ? offer2.OffersFullTime?.totalCompensation.value
+ : offer2.OffersIntern?.monthlySalary.value;
+
+ if (salary1 && salary2) {
+ return salary2 - salary1;
+ }
+ }
+
+ return defaultReturn;
+ })();
+ }
+ return defaultReturn;
+ });
+
+ return data;
+ },
+});