diff --git a/apps/portal/prisma/migrations/20221021231817_/migration.sql b/apps/portal/prisma/migrations/20221021231817_/migration.sql new file mode 100644 index 00000000..3820338d --- /dev/null +++ b/apps/portal/prisma/migrations/20221021231817_/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - Added the required column `baseValue` to the `OffersCurrency` table without a default value. This is not possible if the table is not empty. + - Added the required column `updatedAt` to the `OffersCurrency` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "OffersCurrency" ADD COLUMN "baseCurrency" TEXT NOT NULL DEFAULT 'USD', +ADD COLUMN "baseValue" INTEGER NOT NULL, +ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL; diff --git a/apps/portal/prisma/migrations/20221021233952_change_currency_values_to_float/migration.sql b/apps/portal/prisma/migrations/20221021233952_change_currency_values_to_float/migration.sql new file mode 100644 index 00000000..089e963d --- /dev/null +++ b/apps/portal/prisma/migrations/20221021233952_change_currency_values_to_float/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "OffersCurrency" ALTER COLUMN "value" SET DATA TYPE DOUBLE PRECISION, +ALTER COLUMN "baseValue" SET DATA TYPE DOUBLE PRECISION; diff --git a/apps/portal/prisma/schema.prisma b/apps/portal/prisma/schema.prisma index e06fa885..f32471f1 100644 --- a/apps/portal/prisma/schema.prisma +++ b/apps/portal/prisma/schema.prisma @@ -206,9 +206,9 @@ model OffersBackground { totalYoe Int specificYoes OffersSpecificYoe[] - experiences OffersExperience[] // For extensibility in the future + experiences OffersExperience[] - educations OffersEducation[] // For extensibility in the future + educations OffersEducation[] profile OffersProfile @relation(fields: [offersProfileId], references: [id], onDelete: Cascade) offersProfileId String @unique @@ -252,10 +252,16 @@ model OffersExperience { } model OffersCurrency { - id String @id @default(cuid()) - value Int + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + value Float currency String + baseValue Float + baseCurrency String @default("USD") + // Experience OffersExperienceTotalCompensation OffersExperience? @relation("ExperienceTotalCompensation") OffersExperienceMonthlySalary OffersExperience? @relation("ExperienceMonthlySalary") diff --git a/apps/portal/src/components/offers/table/OffersTable.tsx b/apps/portal/src/components/offers/table/OffersTable.tsx index 9f46d4d4..d636add7 100644 --- a/apps/portal/src/components/offers/table/OffersTable.tsx +++ b/apps/portal/src/components/offers/table/OffersTable.tsx @@ -9,6 +9,7 @@ import { YOE_CATEGORY, } from '~/components/offers/table/types'; +import { Currency } from '~/utils/offers/currency/CurrencyEnum'; import CurrencySelector from '~/utils/offers/currency/CurrencySelector'; import { trpc } from '~/utils/trpc'; @@ -25,7 +26,7 @@ export default function OffersTable({ companyFilter, jobTitleFilter, }: OffersTableProps) { - const [currency, setCurrency] = useState('SGD'); // TODO: Detect location + const [currency, setCurrency] = useState(Currency.SGD.toString()); // TODO: Detect location const [selectedTab, setSelectedTab] = useState(YOE_CATEGORY.ENTRY); const [pagination, setPagination] = useState({ currentPage: 0, @@ -44,12 +45,13 @@ export default function OffersTable({ numOfPages: 0, totalItems: 0, }); - }, [selectedTab]); + }, [selectedTab, currency]); const offersQuery = trpc.useQuery( [ 'offers.list', { companyId: companyFilter, + currency, limit: NUMBER_OF_OFFERS_IN_PAGE, location: 'Singapore, Singapore', // TODO: Geolocation offset: pagination.currentPage, diff --git a/apps/portal/src/components/resumes/browse/ResumeListItem.tsx b/apps/portal/src/components/resumes/browse/ResumeListItem.tsx index b0ef8b4d..5726badd 100644 --- a/apps/portal/src/components/resumes/browse/ResumeListItem.tsx +++ b/apps/portal/src/components/resumes/browse/ResumeListItem.tsx @@ -42,7 +42,7 @@ export default function ResumeListItem({ href, resumeInfo }: Props) {
{`${resumeInfo.numComments} comment${ - resumeInfo.numComments > 0 ? 's' : '' + resumeInfo.numComments === 1 ? '' : 's' }`}
@@ -51,7 +51,9 @@ export default function ResumeListItem({ href, resumeInfo }: Props) { ) : ( )} - {resumeInfo.numStars} stars + {`${resumeInfo.numStars} star${ + resumeInfo.numStars === 1 ? '' : 's' + }`}
diff --git a/apps/portal/src/components/resumes/comments/ResumeCommentListItem.tsx b/apps/portal/src/components/resumes/comments/ResumeCommentListItem.tsx index 5561a49e..07d951bb 100644 --- a/apps/portal/src/components/resumes/comments/ResumeCommentListItem.tsx +++ b/apps/portal/src/components/resumes/comments/ResumeCommentListItem.tsx @@ -96,15 +96,17 @@ export default function ResumeCommentListItem({
- {/* Action buttons; only present when not editing/replying */} - {isCommentOwner && !isEditingComment && !isReplyingComment && ( + {/* Action buttons; only present for authenticated user when not editing/replying */} + {userId && !isEditingComment && !isReplyingComment && ( <> - + {isCommentOwner && ( + + )} {!comment.parentId && ( -
+ + {/* Mobile Filters */} +
+ + + +
+ + +
+ + +
+

+ Shortcuts +

+
-
-
-
-
-
-
-
-

Shortcuts

+
    {SHORTCUTS.map((shortcut) => (
  • @@ -333,18 +299,16 @@ export default function ResumeHomePage() {
  • ))}
-

- Explore these filters: -

+ {filters.map((filter) => ( + className="border-t border-gray-200 px-4 py-6"> {({ open }) => ( <> -

- +

+ {filter.label} @@ -363,12 +327,8 @@ export default function ResumeHomePage() {

- - + +
{filter.options.map((option) => (
))} - +
)} ))} + + +

+ + +
+ +
+
+ +
+ +
+
+

+ Shortcuts: +

+
+
+
    + {SHORTCUTS.map((shortcut) => ( +
  • + onShortcutChange(shortcut)} + /> +
  • + ))} +
+

+ Explore these filters: +

+ {filters.map((filter) => ( + + {({ open }) => ( + <> +

+ + + {filter.label} + + + {open ? ( + + +

+ + + {filter.options.map((option) => ( +
+ + onFilterCheckboxChange( + isChecked, + filter.id, + option.value, + ) + } + /> +
+ ))} +
+
+ + )} +
+ ))} +
+
+
+
+
+
+
+ +
+ +
+ +
+
+
+
+
+ + +
+
+ + {Object.entries(SORT_OPTIONS).map(([key, value]) => ( + setSortOrder(key)}> + ))} + +
+ + +
+
-
- {sessionData === null && +
+
+ {allResumesQuery.isLoading || + starredResumesQuery.isLoading || + myResumesQuery.isLoading ? ( +
+ {' '} + {' '} +
+ ) : sessionData === null && tabsValue !== BROWSE_TABS_VALUES.ALL ? ( - + ) : getTabResumes().length === 0 ? ( +
+ - ) : getTabResumes().length === 0 ? ( -
- + ) : ( + <> + +
+ setCurrentPage(page)} /> - {getEmptyDataText(tabsValue, searchValue, userFilters)}
- ) : ( - <> - -
- setCurrentPage(page)} - /> -
- - )} -
+ + )}
diff --git a/apps/portal/src/pages/resumes/index.tsx b/apps/portal/src/pages/resumes/index.tsx index 4f84c9d1..ae063c0b 100644 --- a/apps/portal/src/pages/resumes/index.tsx +++ b/apps/portal/src/pages/resumes/index.tsx @@ -3,7 +3,6 @@ import Head from 'next/head'; import { CallToAction } from '~/components/resumes/landing/CallToAction'; import { Hero } from '~/components/resumes/landing/Hero'; import { PrimaryFeatures } from '~/components/resumes/landing/PrimaryFeatures'; -import { Testimonials } from '~/components/resumes/landing/Testimonials'; export default function Home() { return ( @@ -16,7 +15,6 @@ export default function Home() { -
); diff --git a/apps/portal/src/pages/resumes/submit.tsx b/apps/portal/src/pages/resumes/submit.tsx index 92a1c392..28b7a3b6 100644 --- a/apps/portal/src/pages/resumes/submit.tsx +++ b/apps/portal/src/pages/resumes/submit.tsx @@ -80,6 +80,7 @@ export default function SubmitResumeForm({ const { data: session, status } = useSession(); const router = useRouter(); + const trpcContext = trpc.useContext(); const resumeUpsertMutation = trpc.useMutation('resumes.resume.user.upsert'); const isNewForm = initFormDetails == null; @@ -170,6 +171,7 @@ export default function SubmitResumeForm({ }, onSuccess() { if (isNewForm) { + trpcContext.invalidateQueries('resumes.resume.findAll'); router.push('/resumes/browse'); } else { onClose(); @@ -228,7 +230,7 @@ export default function SubmitResumeForm({ Upload a Resume -
+
diff --git a/apps/portal/src/pages/test__.tsx b/apps/portal/src/pages/test__.tsx new file mode 100644 index 00000000..26859c56 --- /dev/null +++ b/apps/portal/src/pages/test__.tsx @@ -0,0 +1,51 @@ +import { useState } from 'react'; +import type { TypeaheadOption } from '@tih/ui'; +import { Button } from '@tih/ui'; +import { useToast } from '@tih/ui'; +import { HorizontalDivider } from '@tih/ui'; + +import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead'; +import type { Month, MonthYear } from '~/components/shared/MonthYearPicker'; +import MonthYearPicker from '~/components/shared/MonthYearPicker'; + +export default function HomePage() { + const [selectedCompany, setSelectedCompany] = + useState(null); + const [monthYear, setMonthYear] = useState({ + month: (new Date().getMonth() + 1) as Month, + year: new Date().getFullYear(), + }); + + const { showToast } = useToast(); + + return ( +
+
+
+

+ Test Page +

+ setSelectedCompany(option)} + /> +
{JSON.stringify(selectedCompany, null, 2)}
+ + + +
+
+
+ ); +} 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 37b0d83b..973f3136 100644 --- a/apps/portal/src/server/router/offers/offers-analysis-router.ts +++ b/apps/portal/src/server/router/offers/offers-analysis-router.ts @@ -187,14 +187,14 @@ export const offersAnalysisRouter = createRouter() { offersFullTime: { totalCompensation: { - value: 'desc', + baseValue: 'desc', }, }, }, { offersIntern: { monthlySalary: { - value: 'desc', + baseValue: 'desc', }, }, }, @@ -216,15 +216,17 @@ export const offersAnalysisRouter = createRouter() // TODO: Shift yoe out of background to make it mandatory if ( !overallHighestOffer.profile.background || - overallHighestOffer.profile.background.totalYoe === undefined + overallHighestOffer.profile.background.totalYoe == null ) { throw new TRPCError({ - code: 'BAD_REQUEST', - message: 'Cannot analyse without YOE', + code: 'NOT_FOUND', + message: 'YOE not found', }); } const yoe = overallHighestOffer.profile.background.totalYoe as number; + const monthYearReceived = new Date(overallHighestOffer.monthYearReceived); + monthYearReceived.setFullYear(monthYearReceived.getFullYear() - 1); let similarOffers = await ctx.prisma.offersOffer.findMany({ include: { @@ -257,14 +259,14 @@ export const offersAnalysisRouter = createRouter() { offersFullTime: { totalCompensation: { - value: 'desc', + baseValue: 'desc', }, }, }, { offersIntern: { monthlySalary: { - value: 'desc', + baseValue: 'desc', }, }, }, @@ -274,17 +276,20 @@ export const offersAnalysisRouter = createRouter() { location: overallHighestOffer.location, }, + { + monthYearReceived: { + gte: monthYearReceived, + }, + }, { OR: [ { offersFullTime: { level: overallHighestOffer.offersFullTime?.level, - specialization: - overallHighestOffer.offersFullTime?.specialization, + title: overallHighestOffer.offersFullTime?.title, }, offersIntern: { - specialization: - overallHighestOffer.offersIntern?.specialization, + title: overallHighestOffer.offersIntern?.title, }, }, ], @@ -317,7 +322,9 @@ export const offersAnalysisRouter = createRouter() similarOffers, ); const overallPercentile = - similarOffers.length === 0 ? 0 : overallIndex / similarOffers.length; + similarOffers.length === 0 + ? 100 + : (100 * overallIndex) / similarOffers.length; const companyIndex = searchOfferPercentile( overallHighestOffer, @@ -325,10 +332,11 @@ export const offersAnalysisRouter = createRouter() ); const companyPercentile = similarCompanyOffers.length === 0 - ? 0 - : companyIndex / similarCompanyOffers.length; + ? 100 + : (100 * companyIndex) / similarCompanyOffers.length; - // FIND TOP >=90 PERCENTILE OFFERS + // FIND TOP >=90 PERCENTILE OFFERS, DOESN'T GIVE 100th PERCENTILE + // e.g. If there only 4 offers, it gives the 2nd and 3rd offer similarOffers = similarOffers.filter( (offer) => offer.id !== overallHighestOffer.id, ); @@ -337,10 +345,9 @@ export const offersAnalysisRouter = createRouter() ); const noOfSimilarOffers = similarOffers.length; - const similarOffers90PercentileIndex = - Math.floor(noOfSimilarOffers * 0.9) - 1; + const similarOffers90PercentileIndex = Math.ceil(noOfSimilarOffers * 0.1); const topPercentileOffers = - noOfSimilarOffers > 1 + noOfSimilarOffers > 2 ? similarOffers.slice( similarOffers90PercentileIndex, similarOffers90PercentileIndex + 2, @@ -348,10 +355,11 @@ export const offersAnalysisRouter = createRouter() : similarOffers; const noOfSimilarCompanyOffers = similarCompanyOffers.length; - const similarCompanyOffers90PercentileIndex = - Math.floor(noOfSimilarCompanyOffers * 0.9) - 1; + const similarCompanyOffers90PercentileIndex = Math.ceil( + noOfSimilarCompanyOffers * 0.1, + ); const topPercentileCompanyOffers = - noOfSimilarCompanyOffers > 1 + noOfSimilarCompanyOffers > 2 ? similarCompanyOffers.slice( similarCompanyOffers90PercentileIndex, similarCompanyOffers90PercentileIndex + 2, 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 2e6b9e38..f2160243 100644 --- a/apps/portal/src/server/router/offers/offers-comments-router.ts +++ b/apps/portal/src/server/router/offers/offers-comments-router.ts @@ -26,26 +26,27 @@ export const offersCommentsRouter = createRouter() user: true, }, orderBy: { - createdAt: 'desc' - } + createdAt: 'desc', + }, }, replyingTo: true, user: true, }, orderBy: { - createdAt: 'desc' - } + createdAt: 'desc', + }, }, }, where: { id: input.profileId, - } + }, }); const discussions: OffersDiscussion = { - data: result?.discussion + data: + result?.discussion .filter((x) => { - return x.replyingToId === null + return x.replyingToId === null; }) .map((x) => { if (x.user == null) { @@ -81,18 +82,18 @@ export const offersCommentsRouter = createRouter() message: reply.message, replies: [], replyingToId: reply.replyingToId, - user: reply.user - } + user: reply.user, + }; }), replyingToId: x.replyingToId, - user: x.user - } + user: x.user, + }; - return replyType - }) ?? [] - } + return replyType; + }) ?? [], + }; - return discussions + return discussions; }, }) .mutation('create', { @@ -101,7 +102,7 @@ export const offersCommentsRouter = createRouter() profileId: z.string(), replyingToId: z.string().optional(), token: z.string().optional(), - userId: z.string().optional() + userId: z.string().optional(), }), async resolve({ ctx, input }) { const profile = await ctx.prisma.offersProfile.findFirst({ @@ -156,7 +157,7 @@ export const offersCommentsRouter = createRouter() const created = await ctx.prisma.offersReply.findFirst({ include: { - user: true + user: true, }, where: { id: createdReply.id, @@ -175,10 +176,10 @@ export const offersCommentsRouter = createRouter() id: '', image: '', name: profile?.profileName ?? '', - } - } + }, + }; - return result + return result; } throw new trpc.TRPCError({ @@ -223,10 +224,10 @@ export const offersCommentsRouter = createRouter() include: { replies: { include: { - user: true - } + user: true, + }, }, - user: true + user: true, }, where: { id: input.id, @@ -250,8 +251,8 @@ export const offersCommentsRouter = createRouter() id: '', image: '', name: profile?.profileName ?? '', - } - } + }, + }; }), replyingToId: updated!.replyingToId, user: updated!.user ?? { @@ -260,10 +261,10 @@ export const offersCommentsRouter = createRouter() id: '', image: '', name: profile?.profileName ?? '', - } - } + }, + }; - return result + return result; } throw new trpc.TRPCError({ 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 b8552018..9aade630 100644 --- a/apps/portal/src/server/router/offers/offers-profile-router.ts +++ b/apps/portal/src/server/router/offers/offers-profile-router.ts @@ -1,5 +1,6 @@ import crypto, { randomUUID } from 'crypto'; import { z } from 'zod'; +import { JobType } from '@prisma/client'; import * as trpc from '@trpc/server'; import { @@ -7,6 +8,9 @@ import { createOfferProfileResponseMapper, profileDtoMapper, } from '~/mappers/offers-mappers'; +import { baseCurrencyString } from '~/utils/offers/currency'; +import { convert } from '~/utils/offers/currency/currencyExchange'; +import { createValidationRegex } from '~/utils/offers/zodRegex'; import { createRouter } from '../context'; @@ -31,7 +35,7 @@ const offer = z.object({ company: company.nullish(), companyId: z.string(), id: z.string().optional(), - jobType: z.string(), + jobType: z.string().regex(createValidationRegex(Object.keys(JobType), null)), location: z.string(), monthYearReceived: z.date(), negotiationStrategy: z.string(), @@ -73,7 +77,10 @@ const experience = z.object({ companyId: z.string().nullish(), durationInMonths: z.number().nullish(), id: z.string().optional(), - jobType: z.string().nullish(), + jobType: z + .string() + .regex(createValidationRegex(Object.keys(JobType), null)) + .nullish(), level: z.string().nullish(), location: z.string().nullish(), monthlySalary: valuation.nullish(), @@ -94,15 +101,6 @@ const education = z.object({ type: z.string().nullish(), }); -// Const reply = z.object({ -// createdAt: z.date().nullish(), -// id: z.string().optional(), -// messages: z.string().nullish(), -// profileId: z.string().nullish(), -// replyingToId: z.string().nullish(), -// userId: z.string().nullish(), -// }); - export const offersProfileRouter = createRouter() .query('listOne', { input: z.object({ @@ -282,11 +280,11 @@ export const offersProfileRouter = createRouter() })), }, experiences: { - create: input.background.experiences.map((x) => { + create: input.background.experiences.map(async (x) => { if ( - x.jobType === 'FULLTIME' && - x.totalCompensation?.currency !== undefined && - x.totalCompensation.value !== undefined + x.jobType === JobType.FULLTIME && + x.totalCompensation?.currency != null && + x.totalCompensation?.value != null ) { if (x.companyId) { return { @@ -302,8 +300,14 @@ export const offersProfileRouter = createRouter() title: x.title, totalCompensation: { create: { - currency: x.totalCompensation?.currency, - value: x.totalCompensation?.value, + baseCurrency: baseCurrencyString, + baseValue: await convert( + x.totalCompensation.value, + x.totalCompensation.currency, + baseCurrencyString, + ), + currency: x.totalCompensation.currency, + value: x.totalCompensation.value, }, }, }; @@ -312,20 +316,27 @@ export const offersProfileRouter = createRouter() durationInMonths: x.durationInMonths, jobType: x.jobType, level: x.level, + location: x.location, specialization: x.specialization, title: x.title, totalCompensation: { create: { - currency: x.totalCompensation?.currency, - value: x.totalCompensation?.value, + baseCurrency: baseCurrencyString, + baseValue: await convert( + x.totalCompensation.value, + x.totalCompensation.currency, + baseCurrencyString, + ), + currency: x.totalCompensation.currency, + value: x.totalCompensation.value, }, }, }; } if ( - x.jobType === 'INTERN' && - x.monthlySalary?.currency !== undefined && - x.monthlySalary.value !== undefined + x.jobType === JobType.INTERN && + x.monthlySalary?.currency != null && + x.monthlySalary?.value != null ) { if (x.companyId) { return { @@ -338,8 +349,14 @@ export const offersProfileRouter = createRouter() jobType: x.jobType, monthlySalary: { create: { - currency: x.monthlySalary?.currency, - value: x.monthlySalary?.value, + baseCurrency: baseCurrencyString, + baseValue: await convert( + x.monthlySalary.value, + x.monthlySalary.currency, + baseCurrencyString, + ), + currency: x.monthlySalary.currency, + value: x.monthlySalary.value, }, }, specialization: x.specialization, @@ -351,8 +368,14 @@ export const offersProfileRouter = createRouter() jobType: x.jobType, monthlySalary: { create: { - currency: x.monthlySalary?.currency, - value: x.monthlySalary?.value, + baseCurrency: baseCurrencyString, + baseValue: await convert( + x.monthlySalary.value, + x.monthlySalary.currency, + baseCurrencyString, + ), + currency: x.monthlySalary.currency, + value: x.monthlySalary.value, }, }, specialization: x.specialization, @@ -379,107 +402,141 @@ export const offersProfileRouter = createRouter() }, editToken: token, offers: { - create: input.offers.map((x) => { - if ( - x.jobType === 'INTERN' && - x.offersIntern && - x.offersIntern.internshipCycle && - x.offersIntern.monthlySalary?.currency && - x.offersIntern.monthlySalary.value && - x.offersIntern.startYear - ) { - return { - comments: x.comments, - company: { - connect: { - id: x.companyId, + create: await Promise.all( + input.offers.map(async (x) => { + if ( + x.jobType === JobType.INTERN && + x.offersIntern && + x.offersIntern.internshipCycle != null && + x.offersIntern.monthlySalary?.currency != null && + x.offersIntern.monthlySalary?.value != null && + x.offersIntern.startYear != null + ) { + return { + comments: x.comments, + company: { + connect: { + id: x.companyId, + }, }, - }, - jobType: x.jobType, - location: x.location, - monthYearReceived: x.monthYearReceived, - negotiationStrategy: x.negotiationStrategy, - offersIntern: { - create: { - internshipCycle: x.offersIntern.internshipCycle, - monthlySalary: { - create: { - currency: x.offersIntern.monthlySalary?.currency, - value: x.offersIntern.monthlySalary?.value, + jobType: x.jobType, + location: x.location, + monthYearReceived: x.monthYearReceived, + negotiationStrategy: x.negotiationStrategy, + offersIntern: { + create: { + internshipCycle: x.offersIntern.internshipCycle, + monthlySalary: { + create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + x.offersIntern.monthlySalary.value, + x.offersIntern.monthlySalary.currency, + baseCurrencyString, + ), + currency: x.offersIntern.monthlySalary.currency, + value: x.offersIntern.monthlySalary.value, + }, }, + specialization: x.offersIntern.specialization, + startYear: x.offersIntern.startYear, + title: x.offersIntern.title, }, - specialization: x.offersIntern.specialization, - startYear: x.offersIntern.startYear, - title: x.offersIntern.title, }, - }, - }; - } - if ( - x.jobType === 'FULLTIME' && - x.offersFullTime && - x.offersFullTime.baseSalary?.currency && - x.offersFullTime.baseSalary?.value && - x.offersFullTime.bonus?.currency && - x.offersFullTime.bonus?.value && - x.offersFullTime.stocks?.currency && - x.offersFullTime.stocks?.value && - x.offersFullTime.totalCompensation?.currency && - x.offersFullTime.totalCompensation?.value && - x.offersFullTime.level - ) { - return { - comments: x.comments, - company: { - connect: { - id: x.companyId, + }; + } + if ( + x.jobType === JobType.FULLTIME && + x.offersFullTime && + x.offersFullTime.baseSalary?.currency != null && + x.offersFullTime.baseSalary?.value != null && + x.offersFullTime.bonus?.currency != null && + x.offersFullTime.bonus?.value != null && + x.offersFullTime.stocks?.currency != null && + x.offersFullTime.stocks?.value != null && + x.offersFullTime.totalCompensation?.currency != null && + x.offersFullTime.totalCompensation?.value != null && + x.offersFullTime.level != null && + x.offersFullTime.title != null && + x.offersFullTime.specialization != null + ) { + return { + comments: x.comments, + company: { + connect: { + id: x.companyId, + }, }, - }, - jobType: x.jobType, - location: x.location, - monthYearReceived: x.monthYearReceived, - negotiationStrategy: x.negotiationStrategy, - offersFullTime: { - create: { - baseSalary: { - create: { - currency: x.offersFullTime.baseSalary?.currency, - value: x.offersFullTime.baseSalary?.value, + jobType: x.jobType, + location: x.location, + monthYearReceived: x.monthYearReceived, + negotiationStrategy: x.negotiationStrategy, + offersFullTime: { + create: { + baseSalary: { + create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + x.offersFullTime.baseSalary.value, + x.offersFullTime.baseSalary.currency, + baseCurrencyString, + ), + currency: x.offersFullTime.baseSalary.currency, + value: x.offersFullTime.baseSalary.value, + }, }, - }, - bonus: { - create: { - currency: x.offersFullTime.bonus?.currency, - value: x.offersFullTime.bonus?.value, + bonus: { + create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + x.offersFullTime.bonus.value, + x.offersFullTime.bonus.currency, + baseCurrencyString, + ), + currency: x.offersFullTime.bonus.currency, + value: x.offersFullTime.bonus.value, + }, }, - }, - level: x.offersFullTime.level, - specialization: x.offersFullTime.specialization, - stocks: { - create: { - currency: x.offersFullTime.stocks?.currency, - value: x.offersFullTime.stocks?.value, + level: x.offersFullTime.level, + specialization: x.offersFullTime.specialization, + stocks: { + create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + x.offersFullTime.stocks.value, + x.offersFullTime.stocks.currency, + baseCurrencyString, + ), + currency: x.offersFullTime.stocks.currency, + value: x.offersFullTime.stocks.value, + }, }, - }, - title: x.offersFullTime.title, - totalCompensation: { - create: { - currency: - x.offersFullTime.totalCompensation?.currency, - value: x.offersFullTime.totalCompensation?.value, + title: x.offersFullTime.title, + totalCompensation: { + create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + x.offersFullTime.totalCompensation.value, + x.offersFullTime.totalCompensation.currency, + baseCurrencyString, + ), + currency: + x.offersFullTime.totalCompensation.currency, + value: x.offersFullTime.totalCompensation.value, + }, }, }, }, - }, - }; - } + }; + } - // Throw error - throw new trpc.TRPCError({ - code: 'BAD_REQUEST', - message: 'Missing fields.', - }); - }), + // Throw error + throw new trpc.TRPCError({ + code: 'BAD_REQUEST', + message: 'Missing fields.', + }); + }), + ), }, profileName: randomUUID().substring(0, 10), }, @@ -510,7 +567,7 @@ export const offersProfileRouter = createRouter() return deletedProfile.id; } - // TODO: Throw 401 + throw new trpc.TRPCError({ code: 'UNAUTHORIZED', message: 'Invalid token.', @@ -535,7 +592,6 @@ export const offersProfileRouter = createRouter() totalYoe: z.number(), }), createdAt: z.string().optional(), - // Discussion: z.array(reply), id: z.string(), isEditable: z.boolean().nullish(), offers: z.array(offer), @@ -573,19 +629,21 @@ export const offersProfileRouter = createRouter() }); // Delete educations - const educationsId = (await ctx.prisma.offersEducation.findMany({ - where: { - backgroundId: input.background.id - } - })).map((x) => x.id) + const educationsId = ( + await ctx.prisma.offersEducation.findMany({ + where: { + backgroundId: input.background.id, + }, + }) + ).map((x) => x.id); for (const id of educationsId) { if (!input.background.educations.map((x) => x.id).includes(id)) { await ctx.prisma.offersEducation.delete({ where: { - id - } - }) + id, + }, + }); } } @@ -626,19 +684,21 @@ export const offersProfileRouter = createRouter() } // Delete experiences - const experiencesId = (await ctx.prisma.offersExperience.findMany({ - where: { - backgroundId: input.background.id - } - })).map((x) => x.id) + const experiencesId = ( + await ctx.prisma.offersExperience.findMany({ + where: { + backgroundId: input.background.id, + }, + }) + ).map((x) => x.id); for (const id of experiencesId) { if (!input.background.experiences.map((x) => x.id).includes(id)) { await ctx.prisma.offersExperience.delete({ where: { - id - } - }) + id, + }, + }); } } @@ -660,6 +720,12 @@ export const offersProfileRouter = createRouter() if (exp.monthlySalary) { await ctx.prisma.offersCurrency.update({ data: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + exp.monthlySalary.value, + exp.monthlySalary.currency, + baseCurrencyString, + ), currency: exp.monthlySalary.currency, value: exp.monthlySalary.value, }, @@ -672,6 +738,12 @@ export const offersProfileRouter = createRouter() if (exp.totalCompensation) { await ctx.prisma.offersCurrency.update({ data: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + exp.totalCompensation.value, + exp.totalCompensation.currency, + baseCurrencyString, + ), currency: exp.totalCompensation.currency, value: exp.totalCompensation.value, }, @@ -682,12 +754,76 @@ export const offersProfileRouter = createRouter() } } else if (!exp.id) { // Create new experience - if ( - exp.jobType === 'FULLTIME' && - exp.totalCompensation?.currency !== undefined && - exp.totalCompensation.value !== undefined - ) { - if (exp.companyId) { + if (exp.jobType === JobType.FULLTIME) { + if (exp.totalCompensation?.currency != null && + exp.totalCompensation?.value != null) { + if (exp.companyId) { + await ctx.prisma.offersBackground.update({ + data: { + experiences: { + create: { + company: { + connect: { + id: exp.companyId, + }, + }, + durationInMonths: exp.durationInMonths, + jobType: exp.jobType, + level: exp.level, + location: exp.location, + specialization: exp.specialization, + title: exp.title, + totalCompensation: { + create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + exp.totalCompensation.value, + exp.totalCompensation.currency, + baseCurrencyString, + ), + currency: exp.totalCompensation.currency, + value: exp.totalCompensation.value, + }, + }, + }, + }, + }, + where: { + id: input.background.id, + }, + }); + } else { + await ctx.prisma.offersBackground.update({ + data: { + experiences: { + create: { + durationInMonths: exp.durationInMonths, + jobType: exp.jobType, + level: exp.level, + location: exp.location, + specialization: exp.specialization, + title: exp.title, + totalCompensation: { + create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + exp.totalCompensation.value, + exp.totalCompensation.currency, + baseCurrencyString, + ), + currency: exp.totalCompensation.currency, + value: exp.totalCompensation.value, + }, + }, + }, + }, + }, + where: { + id: input.background.id, + }, + }); + } + } else if (exp.companyId) { await ctx.prisma.offersBackground.update({ data: { experiences: { @@ -700,14 +836,9 @@ export const offersProfileRouter = createRouter() durationInMonths: exp.durationInMonths, jobType: exp.jobType, level: exp.level, + location: exp.location, specialization: exp.specialization, title: exp.title, - totalCompensation: { - create: { - currency: exp.totalCompensation?.currency, - value: exp.totalCompensation?.value, - }, - }, }, }, }, @@ -723,14 +854,9 @@ export const offersProfileRouter = createRouter() durationInMonths: exp.durationInMonths, jobType: exp.jobType, level: exp.level, + location: exp.location, specialization: exp.specialization, title: exp.title, - totalCompensation: { - create: { - currency: exp.totalCompensation?.currency, - value: exp.totalCompensation?.value, - }, - }, }, }, }, @@ -739,12 +865,74 @@ export const offersProfileRouter = createRouter() }, }); } - } else if ( - exp.jobType === 'INTERN' && - exp.monthlySalary?.currency !== undefined && - exp.monthlySalary.value !== undefined - ) { - if (exp.companyId) { + } else if (exp.jobType === JobType.INTERN) { + if (exp.monthlySalary?.currency != null && + exp.monthlySalary?.value != null) { + if (exp.companyId) { + await ctx.prisma.offersBackground.update({ + data: { + experiences: { + create: { + company: { + connect: { + id: exp.companyId, + }, + }, + durationInMonths: exp.durationInMonths, + jobType: exp.jobType, + location: exp.location, + monthlySalary: { + create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + exp.monthlySalary.value, + exp.monthlySalary.currency, + baseCurrencyString, + ), + currency: exp.monthlySalary.currency, + value: exp.monthlySalary.value, + }, + }, + specialization: exp.specialization, + title: exp.title, + }, + }, + }, + where: { + id: input.background.id, + }, + }); + } else { + await ctx.prisma.offersBackground.update({ + data: { + experiences: { + create: { + durationInMonths: exp.durationInMonths, + jobType: exp.jobType, + location: exp.location, + monthlySalary: { + create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + exp.monthlySalary.value, + exp.monthlySalary.currency, + baseCurrencyString, + ), + currency: exp.monthlySalary.currency, + value: exp.monthlySalary.value, + }, + }, + specialization: exp.specialization, + title: exp.title, + }, + }, + }, + where: { + id: input.background.id, + }, + }); + } + } else if (exp.companyId) { await ctx.prisma.offersBackground.update({ data: { experiences: { @@ -756,12 +944,7 @@ export const offersProfileRouter = createRouter() }, durationInMonths: exp.durationInMonths, jobType: exp.jobType, - monthlySalary: { - create: { - currency: exp.monthlySalary?.currency, - value: exp.monthlySalary?.value, - }, - }, + location: exp.location, specialization: exp.specialization, title: exp.title, }, @@ -778,12 +961,7 @@ export const offersProfileRouter = createRouter() create: { durationInMonths: exp.durationInMonths, jobType: exp.jobType, - monthlySalary: { - create: { - currency: exp.monthlySalary?.currency, - value: exp.monthlySalary?.value, - }, - }, + location: exp.location, specialization: exp.specialization, title: exp.title, }, @@ -799,19 +977,21 @@ export const offersProfileRouter = createRouter() } // Delete specific yoes - const yoesId = (await ctx.prisma.offersSpecificYoe.findMany({ - where: { - backgroundId: input.background.id - } - })).map((x) => x.id) + const yoesId = ( + await ctx.prisma.offersSpecificYoe.findMany({ + where: { + backgroundId: input.background.id, + }, + }) + ).map((x) => x.id); for (const id of yoesId) { if (!input.background.specificYoes.map((x) => x.id).includes(id)) { await ctx.prisma.offersSpecificYoe.delete({ where: { - id - } - }) + id, + }, + }); } } @@ -845,19 +1025,21 @@ export const offersProfileRouter = createRouter() } // Delete specific offers - const offers = (await ctx.prisma.offersOffer.findMany({ - where: { - profileId: input.id - } - })).map((x) => x.id) + const offers = ( + await ctx.prisma.offersOffer.findMany({ + where: { + profileId: input.id, + }, + }) + ).map((x) => x.id); for (const id of offers) { if (!input.offers.map((x) => x.id).includes(id)) { await ctx.prisma.offersOffer.delete({ where: { - id - } - }) + id, + }, + }); } } @@ -869,6 +1051,10 @@ export const offersProfileRouter = createRouter() data: { comments: offerToUpdate.comments, companyId: offerToUpdate.companyId, + jobType: + offerToUpdate.jobType === JobType.FULLTIME + ? JobType.FULLTIME + : JobType.INTERN, location: offerToUpdate.location, monthYearReceived: offerToUpdate.monthYearReceived, negotiationStrategy: offerToUpdate.negotiationStrategy, @@ -878,21 +1064,7 @@ export const offersProfileRouter = createRouter() }, }); - if ( - offerToUpdate.jobType === 'INTERN' || - offerToUpdate.jobType === 'FULLTIME' - ) { - await ctx.prisma.offersOffer.update({ - data: { - jobType: offerToUpdate.jobType, - }, - where: { - id: offerToUpdate.id, - }, - }); - } - - if (offerToUpdate.offersIntern?.monthlySalary) { + if (offerToUpdate.offersIntern?.monthlySalary != null) { await ctx.prisma.offersIntern.update({ data: { internshipCycle: @@ -907,6 +1079,12 @@ export const offersProfileRouter = createRouter() }); await ctx.prisma.offersCurrency.update({ data: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + offerToUpdate.offersIntern.monthlySalary.value, + offerToUpdate.offersIntern.monthlySalary.currency, + baseCurrencyString, + ), currency: offerToUpdate.offersIntern.monthlySalary.currency, value: offerToUpdate.offersIntern.monthlySalary.value, }, @@ -916,7 +1094,7 @@ export const offersProfileRouter = createRouter() }); } - if (offerToUpdate.offersFullTime?.totalCompensation) { + if (offerToUpdate.offersFullTime?.totalCompensation != null) { await ctx.prisma.offersFullTime.update({ data: { level: offerToUpdate.offersFullTime.level ?? undefined, @@ -927,9 +1105,15 @@ export const offersProfileRouter = createRouter() id: offerToUpdate.offersFullTime.id, }, }); - if (offerToUpdate.offersFullTime.baseSalary) { + if (offerToUpdate.offersFullTime.baseSalary != null) { await ctx.prisma.offersCurrency.update({ data: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + offerToUpdate.offersFullTime.baseSalary.value, + offerToUpdate.offersFullTime.baseSalary.currency, + baseCurrencyString, + ), currency: offerToUpdate.offersFullTime.baseSalary.currency, value: offerToUpdate.offersFullTime.baseSalary.value, }, @@ -941,6 +1125,12 @@ export const offersProfileRouter = createRouter() if (offerToUpdate.offersFullTime.bonus) { await ctx.prisma.offersCurrency.update({ data: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + offerToUpdate.offersFullTime.bonus.value, + offerToUpdate.offersFullTime.bonus.currency, + baseCurrencyString, + ), currency: offerToUpdate.offersFullTime.bonus.currency, value: offerToUpdate.offersFullTime.bonus.value, }, @@ -952,6 +1142,12 @@ export const offersProfileRouter = createRouter() if (offerToUpdate.offersFullTime.stocks) { await ctx.prisma.offersCurrency.update({ data: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + offerToUpdate.offersFullTime.stocks.value, + offerToUpdate.offersFullTime.stocks.currency, + baseCurrencyString, + ), currency: offerToUpdate.offersFullTime.stocks.currency, value: offerToUpdate.offersFullTime.stocks.value, }, @@ -962,6 +1158,12 @@ export const offersProfileRouter = createRouter() } await ctx.prisma.offersCurrency.update({ data: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + offerToUpdate.offersFullTime.totalCompensation.value, + offerToUpdate.offersFullTime.totalCompensation.currency, + baseCurrencyString, + ), currency: offerToUpdate.offersFullTime.totalCompensation.currency, value: offerToUpdate.offersFullTime.totalCompensation.value, @@ -974,12 +1176,12 @@ export const offersProfileRouter = createRouter() } else { // Create new offer if ( - offerToUpdate.jobType === 'INTERN' && + offerToUpdate.jobType === JobType.INTERN && offerToUpdate.offersIntern && - offerToUpdate.offersIntern.internshipCycle && - offerToUpdate.offersIntern.monthlySalary?.currency && - offerToUpdate.offersIntern.monthlySalary.value && - offerToUpdate.offersIntern.startYear + offerToUpdate.offersIntern.internshipCycle != null && + offerToUpdate.offersIntern.monthlySalary?.currency != null && + offerToUpdate.offersIntern.monthlySalary?.value != null && + offerToUpdate.offersIntern.startYear != null ) { await ctx.prisma.offersProfile.update({ data: { @@ -1001,11 +1203,18 @@ export const offersProfileRouter = createRouter() offerToUpdate.offersIntern.internshipCycle, monthlySalary: { create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + offerToUpdate.offersIntern.monthlySalary.value, + offerToUpdate.offersIntern.monthlySalary + .currency, + baseCurrencyString, + ), currency: offerToUpdate.offersIntern.monthlySalary - ?.currency, + .currency, value: - offerToUpdate.offersIntern.monthlySalary?.value, + offerToUpdate.offersIntern.monthlySalary.value, }, }, specialization: @@ -1023,17 +1232,18 @@ export const offersProfileRouter = createRouter() }); } if ( - offerToUpdate.jobType === 'FULLTIME' && + offerToUpdate.jobType === JobType.FULLTIME && offerToUpdate.offersFullTime && - offerToUpdate.offersFullTime.baseSalary?.currency && - offerToUpdate.offersFullTime.baseSalary?.value && - offerToUpdate.offersFullTime.bonus?.currency && - offerToUpdate.offersFullTime.bonus?.value && - offerToUpdate.offersFullTime.stocks?.currency && - offerToUpdate.offersFullTime.stocks?.value && - offerToUpdate.offersFullTime.totalCompensation?.currency && - offerToUpdate.offersFullTime.totalCompensation?.value && - offerToUpdate.offersFullTime.level + offerToUpdate.offersFullTime.baseSalary?.currency != null && + offerToUpdate.offersFullTime.baseSalary?.value != null && + offerToUpdate.offersFullTime.bonus?.currency != null && + offerToUpdate.offersFullTime.bonus?.value != null && + offerToUpdate.offersFullTime.stocks?.currency != null && + offerToUpdate.offersFullTime.stocks?.value != null && + offerToUpdate.offersFullTime.totalCompensation?.currency != + null && + offerToUpdate.offersFullTime.totalCompensation?.value != null && + offerToUpdate.offersFullTime.level != null ) { await ctx.prisma.offersProfile.update({ data: { @@ -1053,18 +1263,31 @@ export const offersProfileRouter = createRouter() create: { baseSalary: { create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + offerToUpdate.offersFullTime.baseSalary.value, + offerToUpdate.offersFullTime.baseSalary + .currency, + baseCurrencyString, + ), currency: offerToUpdate.offersFullTime.baseSalary - ?.currency, + .currency, value: - offerToUpdate.offersFullTime.baseSalary?.value, + offerToUpdate.offersFullTime.baseSalary.value, }, }, bonus: { create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + offerToUpdate.offersFullTime.bonus.value, + offerToUpdate.offersFullTime.bonus.currency, + baseCurrencyString, + ), currency: - offerToUpdate.offersFullTime.bonus?.currency, - value: offerToUpdate.offersFullTime.bonus?.value, + offerToUpdate.offersFullTime.bonus.currency, + value: offerToUpdate.offersFullTime.bonus.value, }, }, level: offerToUpdate.offersFullTime.level, @@ -1072,20 +1295,34 @@ export const offersProfileRouter = createRouter() offerToUpdate.offersFullTime.specialization, stocks: { create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + offerToUpdate.offersFullTime.stocks.value, + offerToUpdate.offersFullTime.stocks.currency, + baseCurrencyString, + ), currency: - offerToUpdate.offersFullTime.stocks?.currency, - value: offerToUpdate.offersFullTime.stocks?.value, + offerToUpdate.offersFullTime.stocks.currency, + value: offerToUpdate.offersFullTime.stocks.value, }, }, title: offerToUpdate.offersFullTime.title, totalCompensation: { create: { + baseCurrency: baseCurrencyString, + baseValue: await convert( + offerToUpdate.offersFullTime.totalCompensation + .value, + offerToUpdate.offersFullTime.totalCompensation + .currency, + baseCurrencyString, + ), currency: offerToUpdate.offersFullTime.totalCompensation - ?.currency, + .currency, value: offerToUpdate.offersFullTime.totalCompensation - ?.value, + .value, }, }, }, @@ -1102,46 +1339,6 @@ export const offersProfileRouter = createRouter() } const result = await ctx.prisma.offersProfile.findFirst({ - include: { - background: { - include: { - educations: true, - experiences: { - include: { - company: true, - monthlySalary: true, - totalCompensation: true, - }, - }, - specificYoes: true, - }, - }, - discussion: { - include: { - replies: true, - replyingTo: true, - user: true, - }, - }, - offers: { - include: { - company: true, - offersFullTime: { - include: { - baseSalary: true, - bonus: true, - stocks: true, - totalCompensation: true, - }, - }, - offersIntern: { - include: { - monthlySalary: true, - }, - }, - }, - }, - }, where: { id: input.id, }, diff --git a/apps/portal/src/server/router/offers/offers.ts b/apps/portal/src/server/router/offers/offers.ts index 953e133a..8b2321e6 100644 --- a/apps/portal/src/server/router/offers/offers.ts +++ b/apps/portal/src/server/router/offers/offers.ts @@ -5,9 +5,25 @@ import { dashboardOfferDtoMapper, getOffersResponseMapper, } from '~/mappers/offers-mappers'; +import { convertWithDate } from '~/utils/offers/currency/currencyExchange'; +import { Currency } from '~/utils/offers/currency/CurrencyEnum'; +import { createValidationRegex } from '~/utils/offers/zodRegex'; import { createRouter } from '../context'; +const getOrder = (prefix: string) => { + if (prefix === '+') { + return 'asc'; + } + return 'desc'; +}; + +const sortingKeysMap = { + monthYearReceived: 'monthYearReceived', + totalCompensation: 'totalCompensation', + totalYoe: 'totalYoe', +}; + const yoeCategoryMap: Record = { 0: 'Internship', 1: 'Fresh Grad', @@ -25,19 +41,10 @@ const getYoeRange = (yoeCategory: number) => { : null; // Internship }; -const ascOrder = '+'; -const descOrder = '-'; -const sortingKeys = ['monthYearReceived', 'totalCompensation', 'totalYoe']; - -const createSortByValidationRegex = () => { - const startsWithPlusOrMinusOnly = '^[+-]{1}'; - const sortingKeysRegex = sortingKeys.join('|'); - return new RegExp(startsWithPlusOrMinusOnly + '(' + sortingKeysRegex + ')'); -}; - export const offersRouter = createRouter().query('list', { input: z.object({ companyId: z.string().nullish(), + currency: z.string().nullish(), dateEnd: z.date().nullish(), dateStart: z.date().nullish(), limit: z.number().positive(), @@ -45,7 +52,10 @@ export const offersRouter = createRouter().query('list', { offset: z.number().nonnegative(), salaryMax: z.number().nonnegative().nullish(), salaryMin: z.number().nonnegative().nullish(), - sortBy: z.string().regex(createSortByValidationRegex()).nullish(), + sortBy: z + .string() + .regex(createValidationRegex(Object.keys(sortingKeysMap), '[+-]{1}')) + .nullish(), title: z.string().nullish(), yoeCategory: z.number().min(0).max(3), yoeMax: z.number().max(100).nullish(), @@ -56,6 +66,13 @@ export const offersRouter = createRouter().query('list', { const yoeMin = input.yoeMin ? input.yoeMin : yoeRange?.minYoe; const yoeMax = input.yoeMax ? input.yoeMax : yoeRange?.maxYoe; + if (!input.sortBy) { + input.sortBy = '-' + sortingKeysMap.monthYearReceived; + } + + const order = getOrder(input.sortBy.charAt(0)); + const sortingKey = input.sortBy.substring(1); + let data = !yoeRange ? await ctx.prisma.offersOffer.findMany({ // Internship @@ -80,21 +97,84 @@ export const offersRouter = createRouter().query('list', { }, }, }, + orderBy: + sortingKey === sortingKeysMap.monthYearReceived + ? { + monthYearReceived: order, + } + : sortingKey === sortingKeysMap.totalCompensation + ? { + offersIntern: { + monthlySalary: { + baseValue: order, + }, + }, + } + : sortingKey === sortingKeysMap.totalYoe + ? { + profile: { + background: { + totalYoe: order, + }, + }, + } + : undefined, where: { AND: [ { - location: input.location, + location: + input.location.length === 0 ? undefined : input.location, }, { offersIntern: { isNot: null, }, }, + { + offersIntern: { + title: + input.title && input.title.length !== 0 + ? input.title + : undefined, + }, + }, + { + offersIntern: { + monthlySalary: { + baseValue: { + gte: input.salaryMin ?? undefined, + lte: input.salaryMax ?? undefined, + }, + }, + }, + }, { offersFullTime: { is: null, }, }, + { + companyId: + input.companyId && input.companyId.length !== 0 + ? input.companyId + : undefined, + }, + { + profile: { + background: { + totalYoe: { + gte: yoeMin, + lte: yoeMax, + }, + }, + }, + }, + { + monthYearReceived: { + gte: input.dateStart ?? undefined, + lte: input.dateEnd ?? undefined, + }, + }, ], }, }) @@ -121,10 +201,33 @@ export const offersRouter = createRouter().query('list', { }, }, }, + orderBy: + sortingKey === sortingKeysMap.monthYearReceived + ? { + monthYearReceived: order, + } + : sortingKey === sortingKeysMap.totalCompensation + ? { + offersFullTime: { + totalCompensation: { + baseValue: order, + }, + }, + } + : sortingKey === sortingKeysMap.totalYoe + ? { + profile: { + background: { + totalYoe: order, + }, + }, + } + : undefined, where: { AND: [ { - location: input.location, + location: + input.location.length === 0 ? undefined : input.location, }, { offersIntern: { @@ -136,6 +239,30 @@ export const offersRouter = createRouter().query('list', { isNot: null, }, }, + { + offersFullTime: { + title: + input.title && input.title.length !== 0 + ? input.title + : undefined, + }, + }, + { + offersFullTime: { + totalCompensation: { + baseValue: { + gte: input.salaryMin ?? undefined, + lte: input.salaryMax ?? undefined, + }, + }, + }, + }, + { + companyId: + input.companyId && input.companyId.length !== 0 + ? input.companyId + : undefined, + }, { profile: { background: { @@ -146,165 +273,70 @@ export const offersRouter = createRouter().query('list', { }, }, }, + { + monthYearReceived: { + gte: input.dateStart ?? undefined, + lte: input.dateEnd ?? undefined, + }, + }, ], }, }); - // FILTERING - data = data.filter((offer) => { - let validRecord = true; - - if (input.companyId && input.companyId.length !== 0) { - validRecord = validRecord && offer.company.id === input.companyId; - } - - if (input.title && input.title.length !== 0) { - validRecord = - validRecord && - (offer.offersFullTime?.title === input.title || - offer.offersIntern?.title === input.title); - } - - if ( - input.dateStart && - input.dateEnd && - input.dateStart.getTime() <= input.dateEnd.getTime() - ) { - validRecord = - validRecord && - offer.monthYearReceived.getTime() >= input.dateStart.getTime() && - offer.monthYearReceived.getTime() <= input.dateEnd.getTime(); - } - - if (input.salaryMin != null || input.salaryMax != null) { - const salary = offer.offersFullTime?.totalCompensation.value - ? offer.offersFullTime?.totalCompensation.value - : offer.offersIntern?.monthlySalary.value; - - if (salary == null) { - throw new TRPCError({ - code: 'NOT_FOUND', - message: 'Total Compensation or Salary not found', - }); - } - - if (input.salaryMin != null) { - validRecord = validRecord && salary >= input.salaryMin; - } - - if (input.salaryMax != null) { - validRecord = validRecord && salary <= input.salaryMax; - } - } - - return validRecord; - }); - - // SORTING - 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() + // CONVERTING + const currency = input.currency?.toUpperCase(); + if (currency != null && currency in Currency) { + data = await Promise.all( + data.map(async (offer) => { + if (offer.offersFullTime?.totalCompensation != null) { + offer.offersFullTime.totalCompensation.value = + await convertWithDate( + offer.offersFullTime.totalCompensation.value, + offer.offersFullTime.totalCompensation.currency, + currency, + offer.offersFullTime.totalCompensation.updatedAt, + ); + offer.offersFullTime.totalCompensation.currency = currency; + offer.offersFullTime.baseSalary.value = await convertWithDate( + offer.offersFullTime.baseSalary.value, + offer.offersFullTime.baseSalary.currency, + currency, + offer.offersFullTime.baseSalary.updatedAt, ); - } - - 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 == null || salary2 == null) { - throw new TRPCError({ - code: 'NOT_FOUND', - message: 'Total Compensation or Salary not found', - }); - } - - return salary1 - salary2; - } - - if (sortingKey === 'totalYoe') { - const yoe1 = offer1.profile.background?.totalYoe; - const yoe2 = offer2.profile.background?.totalYoe; - - if (yoe1 == null || yoe2 == null) { - throw new TRPCError({ - code: 'NOT_FOUND', - message: 'Total years of experience not found', - }); - } - - return yoe1 - yoe2; - } - - return defaultReturn; - })(); - } - - if (order === descOrder) { - return (() => { - if (sortingKey === 'monthYearReceived') { - return ( - offer2.monthYearReceived.getTime() - - offer1.monthYearReceived.getTime() + offer.offersFullTime.baseSalary.currency = currency; + offer.offersFullTime.stocks.value = await convertWithDate( + offer.offersFullTime.stocks.value, + offer.offersFullTime.stocks.currency, + currency, + offer.offersFullTime.stocks.updatedAt, ); + offer.offersFullTime.stocks.currency = currency; + offer.offersFullTime.bonus.value = await convertWithDate( + offer.offersFullTime.bonus.value, + offer.offersFullTime.bonus.currency, + currency, + offer.offersFullTime.bonus.updatedAt, + ); + offer.offersFullTime.bonus.currency = currency; + } else if (offer.offersIntern?.monthlySalary != null) { + offer.offersIntern.monthlySalary.value = await convertWithDate( + offer.offersIntern.monthlySalary.value, + offer.offersIntern.monthlySalary.currency, + currency, + offer.offersIntern.monthlySalary.updatedAt, + ); + offer.offersIntern.monthlySalary.currency = currency; + } else { + throw new TRPCError({ + code: 'NOT_FOUND', + message: 'Total Compensation or Salary not found', + }); } - 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 == null || salary2 == null) { - throw new TRPCError({ - code: 'NOT_FOUND', - message: 'Total Compensation or Salary not found', - }); - } - - return salary2 - salary1; - } - - if (sortingKey === 'totalYoe') { - const yoe1 = offer1.profile.background?.totalYoe; - const yoe2 = offer2.profile.background?.totalYoe; - - if (yoe1 == null || yoe2 == null) { - throw new TRPCError({ - code: 'NOT_FOUND', - message: 'Total years of experience not found', - }); - } - - return yoe2 - yoe1; - } - - return defaultReturn; - })(); - } - return defaultReturn; - }); + return offer; + }), + ); + } const startRecordIndex: number = input.limit * input.offset; const endRecordIndex: number = diff --git a/apps/portal/src/types/offers.d.ts b/apps/portal/src/types/offers.d.ts index 2605a30f..e2ca2d2f 100644 --- a/apps/portal/src/types/offers.d.ts +++ b/apps/portal/src/types/offers.d.ts @@ -42,6 +42,8 @@ export type OffersCompany = { }; export type Valuation = { + baseCurrency: string; + baseValue: number; currency: string; value: number; }; diff --git a/apps/portal/src/utils/offers/currency/CurrencyEnum.tsx b/apps/portal/src/utils/offers/currency/CurrencyEnum.tsx index 7cfbc0cb..88efa1c1 100644 --- a/apps/portal/src/utils/offers/currency/CurrencyEnum.tsx +++ b/apps/portal/src/utils/offers/currency/CurrencyEnum.tsx @@ -1,167 +1,170 @@ // eslint-disable-next-line no-shadow -export enum Currency { - AED = 'AED', // United Arab Emirates Dirham - AFN = 'AFN', // Afghanistan Afghani - ALL = 'ALL', // Albania Lek - AMD = 'AMD', // Armenia Dram - ANG = 'ANG', // Netherlands Antilles Guilder - AOA = 'AOA', // Angola Kwanza - ARS = 'ARS', // Argentina Peso - AUD = 'AUD', // Australia Dollar - AWG = 'AWG', // Aruba Guilder - AZN = 'AZN', // Azerbaijan New Manat - BAM = 'BAM', // Bosnia and Herzegovina Convertible Marka - BBD = 'BBD', // Barbados Dollar - BDT = 'BDT', // Bangladesh Taka - BGN = 'BGN', // Bulgaria Lev - BHD = 'BHD', // Bahrain Dinar - BIF = 'BIF', // Burundi Franc - BMD = 'BMD', // Bermuda Dollar - BND = 'BND', // Brunei Darussalam Dollar - BOB = 'BOB', // Bolivia Bolíviano - BRL = 'BRL', // Brazil Real - BSD = 'BSD', // Bahamas Dollar - BTN = 'BTN', // Bhutan Ngultrum - BWP = 'BWP', // Botswana Pula - BYR = 'BYR', // Belarus Ruble - BZD = 'BZD', // Belize Dollar - CAD = 'CAD', // Canada Dollar - CDF = 'CDF', // Congo/Kinshasa Franc - CHF = 'CHF', // Switzerland Franc - CLP = 'CLP', // Chile Peso - CNY = 'CNY', // China Yuan Renminbi - COP = 'COP', // Colombia Peso - CRC = 'CRC', // Costa Rica Colon - CUC = 'CUC', // Cuba Convertible Peso - CUP = 'CUP', // Cuba Peso - CVE = 'CVE', // Cape Verde Escudo - CZK = 'CZK', // Czech Republic Koruna - DJF = 'DJF', // Djibouti Franc - DKK = 'DKK', // Denmark Krone - DOP = 'DOP', // Dominican Republic Peso - DZD = 'DZD', // Algeria Dinar - EGP = 'EGP', // Egypt Pound - ERN = 'ERN', // Eritrea Nakfa - ETB = 'ETB', // Ethiopia Birr - EUR = 'EUR', // Euro Member Countries - FJD = 'FJD', // Fiji Dollar - FKP = 'FKP', // Falkland Islands (Malvinas) Pound - GBP = 'GBP', // United Kingdom Pound - GEL = 'GEL', // Georgia Lari - GGP = 'GGP', // Guernsey Pound - GHS = 'GHS', // Ghana Cedi - GIP = 'GIP', // Gibraltar Pound - GMD = 'GMD', // Gambia Dalasi - GNF = 'GNF', // Guinea Franc - GTQ = 'GTQ', // Guatemala Quetzal - GYD = 'GYD', // Guyana Dollar - HKD = 'HKD', // Hong Kong Dollar - HNL = 'HNL', // Honduras Lempira - HRK = 'HRK', // Croatia Kuna - HTG = 'HTG', // Haiti Gourde - HUF = 'HUF', // Hungary Forint - IDR = 'IDR', // Indonesia Rupiah - ILS = 'ILS', // Israel Shekel - IMP = 'IMP', // Isle of Man Pound - INR = 'INR', // India Rupee - IQD = 'IQD', // Iraq Dinar - IRR = 'IRR', // Iran Rial - ISK = 'ISK', // Iceland Krona - JEP = 'JEP', // Jersey Pound - JMD = 'JMD', // Jamaica Dollar - JOD = 'JOD', // Jordan Dinar - JPY = 'JPY', // Japan Yen - KES = 'KES', // Kenya Shilling - KGS = 'KGS', // Kyrgyzstan Som - KHR = 'KHR', // Cambodia Riel - KMF = 'KMF', // Comoros Franc - KPW = 'KPW', // Korea (North) Won - KRW = 'KRW', // Korea (South) Won - KWD = 'KWD', // Kuwait Dinar - KYD = 'KYD', // Cayman Islands Dollar - KZT = 'KZT', // Kazakhstan Tenge - LAK = 'LAK', // Laos Kip - LBP = 'LBP', // Lebanon Pound - LKR = 'LKR', // Sri Lanka Rupee - LRD = 'LRD', // Liberia Dollar - LSL = 'LSL', // Lesotho Loti - LYD = 'LYD', // Libya Dinar - MAD = 'MAD', // Morocco Dirham - MDL = 'MDL', // Moldova Leu - MGA = 'MGA', // Madagascar Ariary - MKD = 'MKD', // Macedonia Denar - MMK = 'MMK', // Myanmar (Burma) Kyat - MNT = 'MNT', // Mongolia Tughrik - MOP = 'MOP', // Macau Pataca - MRO = 'MRO', // Mauritania Ouguiya - MUR = 'MUR', // Mauritius Rupee - MVR = 'MVR', // Maldives (Maldive Islands) Rufiyaa - MWK = 'MWK', // Malawi Kwacha - MXN = 'MXN', // Mexico Peso - MYR = 'MYR', // Malaysia Ringgit - MZN = 'MZN', // Mozambique Metical - NAD = 'NAD', // Namibia Dollar - NGN = 'NGN', // Nigeria Naira - NIO = 'NIO', // Nicaragua Cordoba - NOK = 'NOK', // Norway Krone - NPR = 'NPR', // Nepal Rupee - NZD = 'NZD', // New Zealand Dollar - OMR = 'OMR', // Oman Rial - PAB = 'PAB', // Panama Balboa - PEN = 'PEN', // Peru Sol - PGK = 'PGK', // Papua New Guinea Kina - PHP = 'PHP', // Philippines Peso - PKR = 'PKR', // Pakistan Rupee - PLN = 'PLN', // Poland Zloty - PYG = 'PYG', // Paraguay Guarani - QAR = 'QAR', // Qatar Riyal - RON = 'RON', // Romania New Leu - RSD = 'RSD', // Serbia Dinar - RUB = 'RUB', // Russia Ruble - RWF = 'RWF', // Rwanda Franc - SAR = 'SAR', // Saudi Arabia Riyal - SBD = 'SBD', // Solomon Islands Dollar - SCR = 'SCR', // Seychelles Rupee - SDG = 'SDG', // Sudan Pound - SEK = 'SEK', // Sweden Krona - SGD = 'SGD', // Singapore Dollar - SHP = 'SHP', // Saint Helena Pound - SLL = 'SLL', // Sierra Leone Leone - SOS = 'SOS', // Somalia Shilling - SPL = 'SPL', // Seborga Luigino - SRD = 'SRD', // Suriname Dollar - STD = 'STD', // São Tomé and Príncipe Dobra - SVC = 'SVC', // El Salvador Colon - SYP = 'SYP', // Syria Pound - SZL = 'SZL', // Swaziland Lilangeni - THB = 'THB', // Thailand Baht - TJS = 'TJS', // Tajikistan Somoni - TMT = 'TMT', // Turkmenistan Manat - TND = 'TND', // Tunisia Dinar - TOP = 'TOP', // Tonga Pa'anga - TRY = 'TRY', // Turkey Lira - TTD = 'TTD', // Trinidad and Tobago Dollar - TVD = 'TVD', // Tuvalu Dollar - TWD = 'TWD', // Taiwan New Dollar - TZS = 'TZS', // Tanzania Shilling - UAH = 'UAH', // Ukraine Hryvnia - UGX = 'UGX', // Uganda Shilling - USD = 'USD', // United States Dollar - UYU = 'UYU', // Uruguay Peso - UZS = 'UZS', // Uzbekistan Som - VEF = 'VEF', // Venezuela Bolivar - VND = 'VND', // Viet Nam Dong - VUV = 'VUV', // Vanuatu Vatu - WST = 'WST', // Samoa Tala - XAF = 'XAF', // Communauté Financière Africaine (BEAC) CFA Franc BEAC - XCD = 'XCD', // East Caribbean Dollar - XDR = 'XDR', // International Monetary Fund (IMF) Special Drawing Rights - XOF = 'XOF', // Communauté Financière Africaine (BCEAO) Franc - XPF = 'XPF', // Comptoirs Français du Pacifique (CFP) Franc - YER = 'YER', // Yemen Rial - ZAR = 'ZAR', // South Africa Rand - ZMW = 'ZMW', // Zambia Kwacha - ZWD = 'ZWD', // Zimbabwe Dollar +export enum Currency { + AED = "AED", // 'UNITED ARAB EMIRATES DIRHAM' + AFN = "AFN", // 'AFGHAN AFGHANI' + ALL = "ALL", // 'ALBANIAN LEK' + AMD = "AMD", // 'ARMENIAN DRAM' + ANG = "ANG", // 'NETHERLANDS ANTILLEAN GUILDER' + AOA = "AOA", // 'ANGOLAN KWANZA' + ARS = "ARS", // 'ARGENTINE PESO' + AUD = "AUD", // 'AUSTRALIAN DOLLAR' + AWG = "AWG", // 'ARUBAN FLORIN' + AZN = "AZN", // 'AZERBAIJANI MANAT' + BAM = "BAM", // 'BOSNIA-HERZEGOVINA CONVERTIBLE MARK' + BBD = "BBD", // 'BAJAN DOLLAR' + BDT = "BDT", // 'BANGLADESHI TAKA' + BGN = "BGN", // 'BULGARIAN LEV' + BHD = "BHD", // 'BAHRAINI DINAR' + BIF = "BIF", // 'BURUNDIAN FRANC' + BMD = "BMD", // 'BERMUDAN DOLLAR' + BND = "BND", // 'BRUNEI DOLLAR' + BOB = "BOB", // 'BOLIVIAN BOLIVIANO' + BRL = "BRL", // 'BRAZILIAN REAL' + BSD = "BSD", // 'BAHAMIAN DOLLAR' + BTN = "BTN", // 'BHUTAN CURRENCY' + BWP = "BWP", // 'BOTSWANAN PULA' + BYN = "BYN", // 'NEW BELARUSIAN RUBLE' + BYR = "BYR", // 'BELARUSIAN RUBLE' + BZD = "BZD", // 'BELIZE DOLLAR' + CAD = "CAD", // 'CANADIAN DOLLAR' + CDF = "CDF", // 'CONGOLESE FRANC' + CHF = "CHF", // 'SWISS FRANC' + CLF = "CLF", // 'CHILEAN UNIT OF ACCOUNT (UF)' + CLP = "CLP", // 'CHILEAN PESO' + CNY = "CNY", // 'CHINESE YUAN' + COP = "COP", // 'COLOMBIAN PESO' + CRC = "CRC", // 'COSTA RICAN COLÓN' + CUC = "CUC", // 'CUBAN CONVERTIBLE PESO' + CUP = "CUP", // 'CUBAN PESO' + CVE = "CVE", // 'CAPE VERDEAN ESCUDO' + CVX = "CVX", // 'CONVEX FINANCE' + CZK = "CZK", // 'CZECH KORUNA' + DJF = "DJF", // 'DJIBOUTIAN FRANC' + DKK = "DKK", // 'DANISH KRONE' + DOP = "DOP", // 'DOMINICAN PESO' + DZD = "DZD", // 'ALGERIAN DINAR' + EGP = "EGP", // 'EGYPTIAN POUND' + ERN = "ERN", // 'ERITREAN NAKFA' + ETB = "ETB", // 'ETHIOPIAN BIRR' + ETC = "ETC", // 'ETHEREUM CLASSIC' + EUR = "EUR", // 'EURO' + FEI = "FEI", // 'FEI USD' + FJD = "FJD", // 'FIJIAN DOLLAR' + FKP = "FKP", // 'FALKLAND ISLANDS POUND' + GBP = "GBP", // 'POUND STERLING' + GEL = "GEL", // 'GEORGIAN LARI' + GHS = "GHS", // 'GHANAIAN CEDI' + GIP = "GIP", // 'GIBRALTAR POUND' + GMD = "GMD", // 'GAMBIAN DALASI' + GNF = "GNF", // 'GUINEAN FRANC' + GTQ = "GTQ", // 'GUATEMALAN QUETZAL' + GYD = "GYD", // 'GUYANAESE DOLLAR' + HKD = "HKD", // 'HONG KONG DOLLAR' + HNL = "HNL", // 'HONDURAN LEMPIRA' + HRK = "HRK", // 'CROATIAN KUNA' + HTG = "HTG", // 'HAITIAN GOURDE' + HUF = "HUF", // 'HUNGARIAN FORINT' + ICP = "ICP", // 'INTERNET COMPUTER' + IDR = "IDR", // 'INDONESIAN RUPIAH' + ILS = "ILS", // 'ISRAELI NEW SHEKEL' + INR = "INR", // 'INDIAN RUPEE' + IQD = "IQD", // 'IRAQI DINAR' + IRR = "IRR", // 'IRANIAN RIAL' + ISK = "ISK", // 'ICELANDIC KRÓNA' + JEP = "JEP", // 'JERSEY POUND' + JMD = "JMD", // 'JAMAICAN DOLLAR' + JOD = "JOD", // 'JORDANIAN DINAR' + JPY = "JPY", // 'JAPANESE YEN' + KES = "KES", // 'KENYAN SHILLING' + KGS = "KGS", // 'KYRGYSTANI SOM' + KHR = "KHR", // 'CAMBODIAN RIEL' + KMF = "KMF", // 'COMORIAN FRANC' + KPW = "KPW", // 'NORTH KOREAN WON' + KRW = "KRW", // 'SOUTH KOREAN WON' + KWD = "KWD", // 'KUWAITI DINAR' + KYD = "KYD", // 'CAYMAN ISLANDS DOLLAR' + KZT = "KZT", // 'KAZAKHSTANI TENGE' + LAK = "LAK", // 'LAOTIAN KIP' + LBP = "LPB", // 'LEBANESE POUND' + LKR = "LKR", // 'SRI LANKAN RUPEE' + LRD = "LRD", // 'LIBERIAN DOLLAR' + LSL = "LSL", // 'LESOTHO LOTI' + LTL = "LTL", // 'LITHUANIAN LITAS' + LVL = "LVL", // 'LATVIAN LATS' + LYD = "LYD", // 'LIBYAN DINAR' + MAD = "MAD", // 'MOROCCAN DIRHAM' + MDL = "MDL", // 'MOLDOVAN LEU' + MGA = "MGA", // 'MALAGASY ARIARY' + MKD = "MKD", // 'MACEDONIAN DENAR' + MMK = "MMK", // 'MYANMAR KYAT' + MNT = "MNT", // 'MONGOLIAN TUGRIK' + MOP = "MOP", // 'MACANESE PATACA' + MRO = "MRO", // 'MAURITANIAN OUGUIYA' + MUR = "MUR", // 'MAURITIAN RUPEE' + MVR = "MVR", // 'MALDIVIAN RUFIYAA' + MWK = "MWK", // 'MALAWIAN KWACHA' + MXN = "MXN", // 'MEXICAN PESO' + MYR = "MYR", // 'MALAYSIAN RINGGIT' + MZN = "MZN", // 'MOZAMBICAN METICAL' + NAD = "NAD", // 'NAMIBIAN DOLLAR' + NGN = "NGN", // 'NIGERIAN NAIRA' + NIO = "NIO", // 'NICARAGUAN CÓRDOBA' + NOK = "NOK", // 'NORWEGIAN KRONE' + NPR = "NPR", // 'NEPALESE RUPEE' + NZD = "NZD", // 'NEW ZEALAND DOLLAR' + OMR = "OMR", // 'OMANI RIAL' + ONE = "ONE", // 'MENLO ONE' + PAB = "PAB", // 'PANAMANIAN BALBOA' + PGK = "PGK", // 'PAPUA NEW GUINEAN KINA' + PHP = "PHP", // 'PHILIPPINE PESO' + PKR = "PKR", // 'PAKISTANI RUPEE' + PLN = "PLN", // 'POLAND ZŁOTY' + PYG = "PYG", // 'PARAGUAYAN GUARANI' + QAR = "QAR", // 'QATARI RIAL' + RON = "RON", // 'ROMANIAN LEU' + RSD = "RSD", // 'SERBIAN DINAR' + RUB = "RUB", // 'RUSSIAN RUBLE' + RWF = "RWF", // 'RWANDAN FRANC' + SAR = "SAR", // 'SAUDI RIYAL' + SBD = "SBD", // 'SOLOMON ISLANDS DOLLAR' + SCR = "SCR", // 'SEYCHELLOIS RUPEE' + SDG = "SDG", // 'SUDANESE POUND' + SEK = "SEK", // 'SWEDISH KRONA' + SGD = "SGD", // 'SINGAPORE DOLLAR' + SHIB = "SHIB", // 'SHIBA INU' + SHP = "SHP", // 'SAINT HELENA POUND' + SLL = "SLL", // 'SIERRA LEONEAN LEONE' + SOS = "SOS", // 'SOMALI SHILLING' + SRD = "SRD", // 'SURINAMESE DOLLAR' + STD = "STD", // 'SÃO TOMÉ AND PRÍNCIPE DOBRA (PRE-2018)' + SVC = "SVC", // 'SALVADORAN COLÓN' + SYP = "SYP", // 'SYRIAN POUND' + SZL = "SZL", // 'SWAZI LILANGENI' + THB = "THB", // 'THAI BAHT' + TJS = "TJS", // 'TAJIKISTANI SOMONI' + TMT = "TMT", // 'TURKMENISTANI MANAT' + TND = "TND", // 'TUNISIAN DINAR' + TOP = "TOP", // "TONGAN PA'ANGA" + TRY = "TRY", // 'TURKISH LIRA' + TTD = "TTD", // 'TRINIDAD & TOBAGO DOLLAR' + TWD = "TWD", // 'NEW TAIWAN DOLLAR' + TZS = "TZS", // 'TANZANIAN SHILLING' + UAH = "UAH", // 'UKRAINIAN HRYVNIA' + UGX = "UGX", // 'UGANDAN SHILLING' + USD = "USD", // 'UNITED STATES DOLLAR' + UYU = "UYU", // 'URUGUAYAN PESO' + UZS = "UZS", // 'UZBEKISTANI SOM' + VND = "VND", // 'VIETNAMESE DONG' + VUV = "VUV", // 'VANUATU VATU' + WST = "WST", // 'SAMOAN TALA' + XAF = "XAF", // 'CENTRAL AFRICAN CFA FRANC' + XCD = "XCD", // 'EAST CARIBBEAN DOLLAR' + XOF = "XOF", // 'WEST AFRICAN CFA FRANC' + XPF = "XPF", // 'CFP FRANC' + YER = "YER", // 'YEMENI RIAL' + ZAR = "ZAR", // 'SOUTH AFRICAN RAND' + ZMW = "ZMW", // 'ZAMBIAN KWACHA' + ZWL = "ZWL", // 'ZIMBABWEAN DOLLAR' } export const CURRENCY_OPTIONS = Object.entries(Currency).map( diff --git a/apps/portal/src/utils/offers/currency/currencyExchange.ts b/apps/portal/src/utils/offers/currency/currencyExchange.ts new file mode 100644 index 00000000..0f642100 --- /dev/null +++ b/apps/portal/src/utils/offers/currency/currencyExchange.ts @@ -0,0 +1,49 @@ +// API from https://github.com/fawazahmed0/currency-api#readme + +export const convert = async ( + value: number, + fromCurrency: string, + toCurrency: string, +) => { + fromCurrency = fromCurrency.trim().toLowerCase(); + toCurrency = toCurrency.trim().toLowerCase(); + const url = [ + 'https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api@1/latest/currencies', + fromCurrency, + toCurrency, + ].join('/'); + + return await fetch(url + '.json') + .then((res) => res.json()) + .then((data) => value * data[toCurrency]); +}; +// https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api@{apiVersion}/{date}/{endpoint} + +export const convertWithDate = async ( + value: number, + fromCurrency: string, + toCurrency: string, + date: Date, +) => { + if (new Date().toDateString === date.toDateString) { + return await convert(value, fromCurrency, toCurrency); + } + + fromCurrency = fromCurrency.trim().toLowerCase(); + toCurrency = toCurrency.trim().toLowerCase(); + + // Format date to YYYY-MM-DD + const formattedDate = date.toJSON().substring(0, 10); + + const url = [ + 'https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api@1', + formattedDate, + 'currencies', + fromCurrency, + toCurrency, + ].join('/'); + + return await fetch(url + '.json') + .then((res) => res.json()) + .then((data) => value * data[toCurrency]); +}; diff --git a/apps/portal/src/utils/offers/currency/index.tsx b/apps/portal/src/utils/offers/currency/index.tsx index 373d2984..1e219c45 100644 --- a/apps/portal/src/utils/offers/currency/index.tsx +++ b/apps/portal/src/utils/offers/currency/index.tsx @@ -1,5 +1,9 @@ import type { Money } from '~/components/offers/types'; +import { Currency } from './CurrencyEnum'; + +export const baseCurrencyString = Currency.USD.toString(); + export function convertMoneyToString({ currency, value }: Money) { if (!value) { return '-'; diff --git a/apps/portal/src/utils/offers/zodRegex.ts b/apps/portal/src/utils/offers/zodRegex.ts new file mode 100644 index 00000000..614b76d4 --- /dev/null +++ b/apps/portal/src/utils/offers/zodRegex.ts @@ -0,0 +1,8 @@ +export const createValidationRegex = ( + keywordArray: Array, + prepend: string | null | undefined, +) => { + const sortingKeysRegex = keywordArray.join('|'); + prepend = prepend != null ? prepend : ''; + return new RegExp('^' + prepend + '(' + sortingKeysRegex + ')$'); +}; diff --git a/packages/ui/src/Toast/Toast.tsx b/packages/ui/src/Toast/Toast.tsx new file mode 100644 index 00000000..4d6efe51 --- /dev/null +++ b/packages/ui/src/Toast/Toast.tsx @@ -0,0 +1,108 @@ +import { Fragment, useEffect, useRef } from 'react'; +import { Transition } from '@headlessui/react'; +import { CheckIcon } from '@heroicons/react/24/outline'; +import { XMarkIcon } from '@heroicons/react/24/solid'; + +type ToastVariant = 'failure' | 'success'; + +export type ToastMessage = { + duration?: number; + subtitle?: string; + title: string; + variant: ToastVariant; +}; + +type Props = Readonly<{ + duration?: number; + onClose: () => void; + subtitle?: string; + title: string; + variant: ToastVariant; +}>; + +const DEFAULT_DURATION = 5000; + +function ToastIcon({ variant }: Readonly<{ variant: ToastVariant }>) { + switch (variant) { + case 'success': + return ( +