+
}
endAddOnType="element"
- errorMessage={offerFields?.job?.totalCompensation?.value?.message}
+ errorMessage={
+ offerFields?.offersFullTime?.totalCompensation?.value?.message
+ }
label="Total Compensation (Annual)"
placeholder="0"
required={true}
startAddOn="$"
startAddOnType="label"
type="number"
- {...register(`offers.${index}.job.totalCompensation.value`, {
- min: { message: FieldError.NonNegativeNumber, value: 0 },
- required: FieldError.Required,
- valueAsNumber: true,
- })}
+ {...register(
+ `offers.${index}.offersFullTime.totalCompensation.value`,
+ {
+ min: { message: FieldError.NonNegativeNumber, value: 0 },
+ required: FieldError.Required,
+ valueAsNumber: true,
+ },
+ )}
/>
@@ -160,20 +169,23 @@ function FullTimeOfferDetailsForm({
isLabelHidden={true}
label="Currency"
options={CURRENCY_OPTIONS}
- {...register(`offers.${index}.job.base.currency`, {
- required: FieldError.Required,
- })}
+ {...register(
+ `offers.${index}.offersFullTime.baseSalary.currency`,
+ {
+ required: FieldError.Required,
+ },
+ )}
/>
}
endAddOnType="element"
- errorMessage={offerFields?.job?.base?.value?.message}
+ errorMessage={offerFields?.offersFullTime?.baseSalary?.value?.message}
label="Base Salary (Annual)"
placeholder="0"
required={true}
startAddOn="$"
startAddOnType="label"
type="number"
- {...register(`offers.${index}.job.base.value`, {
+ {...register(`offers.${index}.offersFullTime.baseSalary.value`, {
min: { message: FieldError.NonNegativeNumber, value: 0 },
required: FieldError.Required,
valueAsNumber: true,
@@ -186,20 +198,20 @@ function FullTimeOfferDetailsForm({
isLabelHidden={true}
label="Currency"
options={CURRENCY_OPTIONS}
- {...register(`offers.${index}.job.bonus.currency`, {
+ {...register(`offers.${index}.offersFullTime.bonus.currency`, {
required: FieldError.Required,
})}
/>
}
endAddOnType="element"
- errorMessage={offerFields?.job?.bonus?.value?.message}
+ errorMessage={offerFields?.offersFullTime?.bonus?.value?.message}
label="Bonus (Annual)"
placeholder="0"
required={true}
startAddOn="$"
startAddOnType="label"
type="number"
- {...register(`offers.${index}.job.bonus.value`, {
+ {...register(`offers.${index}.offersFullTime.bonus.value`, {
min: { message: FieldError.NonNegativeNumber, value: 0 },
required: FieldError.Required,
valueAsNumber: true,
@@ -214,20 +226,20 @@ function FullTimeOfferDetailsForm({
isLabelHidden={true}
label="Currency"
options={CURRENCY_OPTIONS}
- {...register(`offers.${index}.job.stocks.currency`, {
+ {...register(`offers.${index}.offersFullTime.stocks.currency`, {
required: FieldError.Required,
})}
/>
}
endAddOnType="element"
- errorMessage={offerFields?.job?.stocks?.value?.message}
+ errorMessage={offerFields?.offersFullTime?.stocks?.value?.message}
label="Stocks (Annual)"
placeholder="0"
required={true}
startAddOn="$"
startAddOnType="label"
type="number"
- {...register(`offers.${index}.job.stocks.value`, {
+ {...register(`offers.${index}.offersFullTime.stocks.value`, {
min: { message: FieldError.NonNegativeNumber, value: 0 },
required: FieldError.Required,
valueAsNumber: true,
@@ -254,7 +266,7 @@ function FullTimeOfferDetailsForm({
icon={TrashIcon}
label="Delete"
variant="secondary"
- onClick={() => setDialogOpen(true)}
+ onClick={() => remove(index)}
/>
)}
@@ -264,15 +276,15 @@ function FullTimeOfferDetailsForm({
type InternshipOfferDetailsFormProps = Readonly<{
index: number;
- setDialogOpen: (isOpen: boolean) => void;
+ remove: UseFieldArrayRemove;
}>;
function InternshipOfferDetailsForm({
index,
- setDialogOpen,
+ remove,
}: InternshipOfferDetailsFormProps) {
- const { register, formState } = useFormContext<{
- offers: Array
;
+ const { register, formState, setValue } = useFormContext<{
+ offers: Array;
}>();
const offerFields = formState.errors.offers?.[index];
@@ -282,39 +294,35 @@ function InternshipOfferDetailsForm({
-
+
+
+ setValue(`offers.${index}.companyId`, value)
+ }
+ />
+
@@ -369,20 +378,25 @@ function InternshipOfferDetailsForm({
isLabelHidden={true}
label="Currency"
options={CURRENCY_OPTIONS}
- {...register(`offers.${index}.job.monthlySalary.currency`, {
- required: FieldError.Required,
- })}
+ {...register(
+ `offers.${index}.offersIntern.monthlySalary.currency`,
+ {
+ required: FieldError.Required,
+ },
+ )}
/>
}
endAddOnType="element"
- errorMessage={offerFields?.job?.monthlySalary?.value?.message}
+ errorMessage={
+ offerFields?.offersIntern?.monthlySalary?.value?.message
+ }
label="Salary (Monthly)"
placeholder="0"
required={true}
startAddOn="$"
startAddOnType="label"
type="number"
- {...register(`offers.${index}.job.monthlySalary.value`, {
+ {...register(`offers.${index}.offersIntern.monthlySalary.value`, {
min: { message: FieldError.NonNegativeNumber, value: 0 },
required: FieldError.Required,
valueAsNumber: true,
@@ -410,7 +424,7 @@ function InternshipOfferDetailsForm({
label="Delete"
variant="secondary"
onClick={() => {
- setDialogOpen(true);
+ remove(index);
}}
/>
)}
@@ -429,7 +443,6 @@ function OfferDetailsFormArray({
jobType,
}: OfferDetailsFormArrayProps) {
const { append, remove, fields } = fieldArrayValues;
- const [isDialogOpen, setDialogOpen] = useState(false);
return (
@@ -437,44 +450,10 @@ function OfferDetailsFormArray({
return (
{jobType === JobType.FullTime ? (
-
+
) : (
-
+
)}
-
{
- remove(index);
- setDialogOpen(false);
- }}
- />
- }
- secondaryButton={
- setDialogOpen(false)}
- />
- }
- title="Remove this offer"
- onClose={() => setDialogOpen(false)}>
-
- Are you sure you want to remove this offer? This action cannot
- be reversed.
-
-
);
})}
@@ -501,22 +480,21 @@ export default function OfferDetailsForm() {
const [isDialogOpen, setDialogOpen] = useState(false);
const { control } = useFormContext();
const fieldArrayValues = useFieldArray({ control, name: 'offers' });
+ const { append, remove } = fieldArrayValues;
const toggleJobType = () => {
- fieldArrayValues.remove();
+ remove();
if (jobType === JobType.FullTime) {
- setJobType(JobType.Internship);
- fieldArrayValues.append(defaultInternshipOfferValues);
+ setJobType(JobType.Intern);
+ append(defaultInternshipOfferValues);
} else {
setJobType(JobType.FullTime);
- fieldArrayValues.append(defaultFullTimeOfferValues);
+ append(defaultFullTimeOfferValues);
}
};
const switchJobTypeLabel = () =>
- jobType === JobType.FullTime
- ? JobTypeLabel.INTERNSHIP
- : JobTypeLabel.FULLTIME;
+ jobType === JobType.FullTime ? JobTypeLabel.INTERN : JobTypeLabel.FULLTIME;
return (
@@ -541,11 +519,11 @@ export default function OfferDetailsForm() {
{
- if (jobType === JobType.Internship) {
+ if (jobType === JobType.Intern) {
return;
}
setDialogOpen(true);
diff --git a/apps/portal/src/components/offers/forms/OfferProfileSave.tsx b/apps/portal/src/components/offers/offers-submission/OfferProfileSave.tsx
similarity index 96%
rename from apps/portal/src/components/offers/forms/OfferProfileSave.tsx
rename to apps/portal/src/components/offers/offers-submission/OfferProfileSave.tsx
index 866fa8e7..44c61ed7 100644
--- a/apps/portal/src/components/offers/forms/OfferProfileSave.tsx
+++ b/apps/portal/src/components/offers/offers-submission/OfferProfileSave.tsx
@@ -52,7 +52,8 @@ export default function OfferProfileSave() {
If you do not want to keep the edit link, you can opt to save this
- profile under your user accont. It will still only be editable by you.
+ profile under your user account. It will still only be editable by
+ you.
;
};
-type OfferDetailsGeneralData = {
- comments: string;
- companyId: string;
- jobType: string;
- location: string;
- monthYearReceived: MonthYear;
- negotiationStrategy: string;
+export type OffersProfileFormData = {
+ background: BackgroundPostData;
+ offers: Array;
};
-export type FullTimeOfferDetailsFormData = OfferDetailsGeneralData & {
- job: FullTimeJobData;
+export type BackgroundPostData = {
+ educations: Array;
+ experiences: Array;
+ specificYoes: Array;
+ totalYoe: number;
};
-export type InternshipOfferDetailsFormData = OfferDetailsGeneralData & {
- job: InternshipJobData;
+type ExperiencePostData = {
+ companyId?: string | null;
+ durationInMonths?: number | null;
+ jobType?: string | null;
+ level?: string | null;
+ location?: string | null;
+ monthlySalary?: Money | null;
+ specialization?: string | null;
+ title?: string | null;
+ totalCompensation?: Money | null;
+ totalCompensationId?: string | null;
};
-export type OfferDetailsFormData =
- | FullTimeOfferDetailsFormData
- | InternshipOfferDetailsFormData;
-
-export type OfferDetailsPostData = Omit<
- OfferDetailsFormData,
- 'monthYearReceived'
-> & {
- monthYearReceived: Date;
+type EducationPostData = {
+ endDate?: Date | null;
+ field?: string | null;
+ school?: string | null;
+ startDate?: Date | null;
+ type?: string | null;
};
-type SpecificYoe = {
+type SpecificYoePostData = {
domain: string;
yoe: number;
};
-type FullTimeExperience = {
- level?: string;
- totalCompensation?: Money;
-};
-
-type InternshipExperience = {
- monthlySalary?: Money;
-};
+type SpecificYoe = SpecificYoePostData;
-type GeneralExperience = {
- companyId?: string;
- durationInMonths?: number;
- jobType?: string;
- specialization?: string;
- title?: string;
+export type OfferPostData = {
+ comments: string;
+ companyId: string;
+ jobType: string;
+ location: string;
+ monthYearReceived: Date;
+ negotiationStrategy: string;
+ offersFullTime?: OfferFullTimePostData | null;
+ offersIntern?: OfferInternPostData | null;
};
-export type Experience =
- | (FullTimeExperience & GeneralExperience)
- | (GeneralExperience & InternshipExperience);
-
-type Education = {
- endDate?: Date;
- field?: string;
- school?: string;
- startDate?: Date;
- type?: string;
+export type OfferFormData = Omit & {
+ monthYearReceived: MonthYear;
};
-type BackgroundFormData = {
- educations: Array;
- experiences: Array;
- specificYoes: Array;
- totalYoe?: number;
+export type OfferFullTimePostData = {
+ baseSalary: Money;
+ bonus: Money;
+ level: string;
+ specialization: string;
+ stocks: Money;
+ title: string;
+ totalCompensation: Money;
};
-export type OfferProfileFormData = {
- background: BackgroundFormData;
- offers: Array;
+export type OfferInternPostData = {
+ internshipCycle: string;
+ monthlySalary: Money;
+ specialization: string;
+ startYear: number;
+ title: string;
};
-export type OfferProfilePostData = {
- background: BackgroundFormData;
- offers: Array;
+export type Money = {
+ currency: string;
+ value: number;
};
type EducationDisplay = {
diff --git a/apps/portal/src/pages/offers/submit.tsx b/apps/portal/src/pages/offers/submit.tsx
index 8e461859..6847bbf7 100644
--- a/apps/portal/src/pages/offers/submit.tsx
+++ b/apps/portal/src/pages/offers/submit.tsx
@@ -5,13 +5,13 @@ import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/20/solid';
import { Button } from '@tih/ui';
import { Breadcrumbs } from '~/components/offers/Breadcrumb';
-import BackgroundForm from '~/components/offers/forms/BackgroundForm';
-import OfferAnalysis from '~/components/offers/forms/OfferAnalysis';
-import OfferDetailsForm from '~/components/offers/forms/OfferDetailsForm';
-import OfferProfileSave from '~/components/offers/forms/OfferProfileSave';
+import BackgroundForm from '~/components/offers/offers-submission/BackgroundForm';
+import OfferAnalysis from '~/components/offers/offers-submission/OfferAnalysis';
+import OfferDetailsForm from '~/components/offers/offers-submission/OfferDetailsForm';
+import OfferProfileSave from '~/components/offers/offers-submission/OfferProfileSave';
import type {
- OfferDetailsFormData,
- OfferProfileFormData,
+ OfferFormData,
+ OffersProfileFormData,
} from '~/components/offers/types';
import { JobType } from '~/components/offers/types';
import type { Month } from '~/components/shared/MonthYearPicker';
@@ -20,10 +20,11 @@ import { cleanObject, removeInvalidMoneyData } from '~/utils/offers/form';
import { getCurrentMonth, getCurrentYear } from '~/utils/offers/time';
import { trpc } from '~/utils/trpc';
+import type { CreateOfferProfileResponse } from '~/types/offers';
+
const defaultOfferValues = {
comments: '',
companyId: '',
- job: {},
jobType: JobType.FullTime,
location: '',
monthYearReceived: {
@@ -40,7 +41,7 @@ export const defaultFullTimeOfferValues = {
export const defaultInternshipOfferValues = {
...defaultOfferValues,
- jobType: JobType.Internship,
+ jobType: JobType.Intern,
};
const defaultOfferProfileValues = {
@@ -61,10 +62,13 @@ type FormStep = {
export default function OffersSubmissionPage() {
const [formStep, setFormStep] = useState(0);
+ const [createProfileResponse, setCreateProfileResponse] =
+ useState();
+
const pageRef = useRef(null);
const scrollToTop = () =>
pageRef.current?.scrollTo({ behavior: 'smooth', top: 0 });
- const formMethods = useForm({
+ const formMethods = useForm({
defaultValues: defaultOfferProfileValues,
mode: 'all',
});
@@ -84,7 +88,9 @@ export default function OffersSubmissionPage() {
label: 'Background',
},
{
- component: ,
+ component: (
+
+ ),
hasNext: true,
hasPrevious: false,
label: 'Analysis',
@@ -115,18 +121,30 @@ export default function OffersSubmissionPage() {
scrollToTop();
};
+ const generateAnalysisMutation = trpc.useMutation(
+ ['offers.analysis.generate'],
+ {
+ onError(error) {
+ console.error(error.message);
+ },
+ },
+ );
+
const createMutation = trpc.useMutation(['offers.profile.create'], {
onError(error) {
console.error(error.message);
},
- onSuccess() {
- alert('offer profile submit success!');
+ onSuccess(data) {
+ generateAnalysisMutation.mutate({
+ profileId: data?.id || '',
+ });
+ setCreateProfileResponse(data);
setFormStep(formStep + 1);
scrollToTop();
},
});
- const onSubmit: SubmitHandler = async (data) => {
+ const onSubmit: SubmitHandler = async (data) => {
const result = await trigger();
if (!result) {
return;
@@ -142,7 +160,7 @@ export default function OffersSubmissionPage() {
background.experiences = [];
}
- const offers = data.offers.map((offer: OfferDetailsFormData) => ({
+ const offers = data.offers.map((offer: OfferFormData) => ({
...offer,
monthYearReceived: new Date(
offer.monthYearReceived.year,
diff --git a/apps/portal/src/pages/offers/test/generateAnalysis.tsx b/apps/portal/src/pages/offers/test/generateAnalysis.tsx
index 07c66e77..5172175b 100644
--- a/apps/portal/src/pages/offers/test/generateAnalysis.tsx
+++ b/apps/portal/src/pages/offers/test/generateAnalysis.tsx
@@ -3,12 +3,15 @@ import React from 'react';
import { trpc } from '~/utils/trpc';
function GenerateAnalysis() {
- const analysis = trpc.useQuery([
- 'offers.analysis.generate',
- { profileId: 'cl98ywtbv0000tx1s4p18eol1' },
- ]);
+ const analysisMutation = trpc.useMutation(['offers.analysis.generate']);
- return {JSON.stringify(analysis.data)}
;
+ return (
+
+ {JSON.stringify(
+ analysisMutation.mutate({ profileId: 'cl98ywtbv0000tx1s4p18eol1' }),
+ )}
+
+ );
}
export default GenerateAnalysis;
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 a7d68ac5..25611507 100644
--- a/apps/portal/src/server/router/offers/offers-analysis-router.ts
+++ b/apps/portal/src/server/router/offers/offers-analysis-router.ts
@@ -51,7 +51,106 @@ const searchOfferPercentile = (
};
export const offersAnalysisRouter = createRouter()
- .query('generate', {
+ .query('get', {
+ input: z.object({
+ profileId: z.string(),
+ }),
+ async resolve({ ctx, input }) {
+ const analysis = await ctx.prisma.offersAnalysis.findFirst({
+ include: {
+ overallHighestOffer: {
+ include: {
+ company: true,
+ offersFullTime: {
+ include: {
+ totalCompensation: true,
+ },
+ },
+ offersIntern: {
+ include: {
+ monthlySalary: true,
+ },
+ },
+ profile: {
+ include: {
+ background: true,
+ },
+ },
+ },
+ },
+ topCompanyOffers: {
+ include: {
+ company: true,
+ offersFullTime: {
+ include: {
+ totalCompensation: true,
+ },
+ },
+ offersIntern: {
+ include: {
+ monthlySalary: true,
+ },
+ },
+ profile: {
+ include: {
+ background: {
+ include: {
+ experiences: {
+ include: {
+ company: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ topOverallOffers: {
+ include: {
+ company: true,
+ offersFullTime: {
+ include: {
+ totalCompensation: true,
+ },
+ },
+ offersIntern: {
+ include: {
+ monthlySalary: true,
+ },
+ },
+ profile: {
+ include: {
+ background: {
+ include: {
+ experiences: {
+ include: {
+ company: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ where: {
+ profileId: input.profileId,
+ },
+ });
+
+ if (!analysis) {
+ throw new TRPCError({
+ code: 'NOT_FOUND',
+ message: 'No analysis found on this profile',
+ });
+ }
+
+ return profileAnalysisDtoMapper(analysis);
+ },
+ })
+ .mutation('generate', {
input: z.object({
profileId: z.string(),
}),
@@ -366,105 +465,6 @@ export const offersAnalysisRouter = createRouter()
},
});
- return profileAnalysisDtoMapper(analysis);
- },
- })
- .query('get', {
- input: z.object({
- profileId: z.string(),
- }),
- async resolve({ ctx, input }) {
- const analysis = await ctx.prisma.offersAnalysis.findFirst({
- include: {
- overallHighestOffer: {
- include: {
- company: true,
- offersFullTime: {
- include: {
- totalCompensation: true,
- },
- },
- offersIntern: {
- include: {
- monthlySalary: true,
- },
- },
- profile: {
- include: {
- background: true,
- },
- },
- },
- },
- topCompanyOffers: {
- include: {
- company: true,
- offersFullTime: {
- include: {
- totalCompensation: true,
- },
- },
- offersIntern: {
- include: {
- monthlySalary: true,
- },
- },
- profile: {
- include: {
- background: {
- include: {
- experiences: {
- include: {
- company: true,
- },
- },
- },
- },
- },
- },
- },
- },
- topOverallOffers: {
- include: {
- company: true,
- offersFullTime: {
- include: {
- totalCompensation: true,
- },
- },
- offersIntern: {
- include: {
- monthlySalary: true,
- },
- },
- profile: {
- include: {
- background: {
- include: {
- experiences: {
- include: {
- company: true,
- },
- },
- },
- },
- },
- },
- },
- },
- },
- where: {
- profileId: input.profileId,
- },
- });
-
- if (!analysis) {
- throw new TRPCError({
- code: 'NOT_FOUND',
- message: 'No analysis found on this profile',
- });
- }
-
return profileAnalysisDtoMapper(analysis);
},
});