Skip to content

Commit 233712b

Browse files
authored
Merge pull request #1067 from topcoder-platform/pm-578
feat(PM-578): Apply for copilot opportunity
2 parents b5892cd + c767c8b commit 233712b

File tree

7 files changed

+204
-8
lines changed

7 files changed

+204
-8
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export interface CopilotApplication {
2+
id: number,
3+
notes?: string,
4+
createdAt: Date,
5+
opportunityId: string,
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/* eslint-disable react/jsx-no-bind */
2+
import { FC, useCallback, useState } from 'react'
3+
4+
import { BaseModal, Button, InputTextarea } from '~/libs/ui'
5+
6+
import { applyCopilotOpportunity } from '../../../services/copilot-opportunities'
7+
8+
import styles from './styles.module.scss'
9+
10+
interface ApplyOpportunityModalProps {
11+
onClose: () => void
12+
copilotOpportunityId: number
13+
projectName: string
14+
onApplied: () => void
15+
}
16+
17+
const ApplyOpportunityModal: FC<ApplyOpportunityModalProps> = props => {
18+
const [notes, setNotes] = useState('')
19+
const [success, setSuccess] = useState(false)
20+
21+
const onApply = useCallback(async () => {
22+
try {
23+
await applyCopilotOpportunity(props.copilotOpportunityId, {
24+
notes,
25+
})
26+
27+
props.onApplied()
28+
setSuccess(true)
29+
} catch (e) {
30+
setSuccess(true)
31+
}
32+
}, [props.copilotOpportunityId, notes])
33+
34+
const onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void = useCallback(e => {
35+
setNotes(e.target.value)
36+
}, [setNotes])
37+
38+
return (
39+
<BaseModal
40+
onClose={props.onClose}
41+
open
42+
size='lg'
43+
title={
44+
success
45+
? `Your Application for ${props.projectName} Has Been Received!`
46+
: `Confirm Your Copilot Application for ${props.projectName}`
47+
}
48+
buttons={
49+
!success ? (
50+
<>
51+
<Button primary onClick={onApply} label='Apply' />
52+
<Button secondary onClick={props.onClose} label='Cancel' />
53+
</>
54+
) : (
55+
<Button primary onClick={props.onClose} label='Close' />
56+
)
57+
}
58+
>
59+
<div className={styles.applyCopilotModal}>
60+
<div className={styles.info}>
61+
{
62+
success
63+
? `We appreciate the time and effort you've taken to apply
64+
for this exciting opportunity. Our team is committed
65+
to providing a seamless and efficient process to ensure a
66+
great experience for all copilots. We will review your application
67+
within short time.`
68+
: `We're excited to see your interest in joining our team as a copilot
69+
for the ${props.projectName} project! Before we proceed, we want to
70+
ensure that you have carefully reviewed the project requirements and
71+
are committed to meeting them.`
72+
}
73+
</div>
74+
{
75+
!success && (
76+
<InputTextarea
77+
name='Notes'
78+
onChange={onChange}
79+
value={notes}
80+
/>
81+
)
82+
}
83+
</div>
84+
</BaseModal>
85+
)
86+
}
87+
88+
export default ApplyOpportunityModal
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as ApplyOpportunityModal } from './ApplyOpportunityModal'
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
@import '@libs/ui/styles/includes';
2+
3+
.applyCopilotModal {
4+
.info {
5+
margin-bottom: 12px;
6+
}
7+
}

src/apps/copilots/src/pages/copilot-opportunity-details/index.tsx

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,48 @@
1-
import { FC, useEffect, useState } from 'react'
1+
/* eslint-disable react/jsx-no-bind */
2+
import {
3+
FC,
4+
useCallback,
5+
useContext,
6+
useEffect,
7+
useMemo,
8+
useState,
9+
} from 'react'
210
import { useNavigate, useParams } from 'react-router-dom'
11+
import { mutate } from 'swr'
312
import moment from 'moment'
413

514
import {
15+
ButtonProps,
616
ContentLayout,
717
IconOutline,
818
LoadingSpinner,
919
PageTitle,
1020
} from '~/libs/ui'
21+
import { profileContext, ProfileContextData, UserRole } from '~/libs/core'
1122

12-
import { CopilotOpportunityResponse, useCopilotOpportunity } from '../../services/copilot-opportunities'
23+
import { CopilotApplication } from '../../models/CopilotApplication'
24+
import {
25+
copilotBaseUrl,
26+
CopilotOpportunityResponse,
27+
useCopilotApplications,
28+
useCopilotOpportunity,
29+
} from '../../services/copilot-opportunities'
1330
import { copilotRoutesMap } from '../../copilots.routes'
1431

32+
import { ApplyOpportunityModal } from './apply-opportunity-modal'
1533
import styles from './styles.module.scss'
1634

1735
const CopilotOpportunityDetails: FC<{}> = () => {
1836
const { opportunityId }: {opportunityId?: string} = useParams<{ opportunityId?: string }>()
1937
const navigate = useNavigate()
2038
const [showNotFound, setShowNotFound] = useState(false)
39+
const [showApplyOpportunityModal, setShowApplyOpportunityModal] = useState(false)
40+
const { profile }: ProfileContextData = useContext(profileContext)
41+
const isCopilot: boolean = useMemo(
42+
() => !!profile?.roles?.some(role => role === UserRole.copilot),
43+
[profile],
44+
)
45+
const { data: copilotApplications }: { data?: CopilotApplication[] } = useCopilotApplications(opportunityId)
2146

2247
if (!opportunityId) {
2348
navigate(copilotRoutesMap.CopilotOpportunityList)
@@ -35,6 +60,14 @@ const CopilotOpportunityDetails: FC<{}> = () => {
3560
return () => clearTimeout(timer) // Cleanup on unmount
3661
}, [opportunity])
3762

63+
const onApplied: () => void = useCallback(() => {
64+
mutate(`${copilotBaseUrl}/copilots/opportunity/${opportunityId}/applications`)
65+
}, [])
66+
67+
const onCloseApplyModal: () => void = useCallback(() => {
68+
setShowApplyOpportunityModal(false)
69+
}, [setShowApplyOpportunityModal])
70+
3871
if (!opportunity && showNotFound) {
3972
return (
4073
<ContentLayout title='Copilot Opportunity Details'>
@@ -44,8 +77,20 @@ const CopilotOpportunityDetails: FC<{}> = () => {
4477
)
4578
}
4679

80+
const applyCopilotOpportunityButton: ButtonProps = {
81+
label: 'Apply as Copilot',
82+
onClick: () => setShowApplyOpportunityModal(true),
83+
}
84+
4785
return (
48-
<ContentLayout title='Copilot Opportunity'>
86+
<ContentLayout
87+
title='Copilot Opportunity'
88+
buttonConfig={
89+
isCopilot
90+
&& copilotApplications
91+
&& copilotApplications.length === 0 ? applyCopilotOpportunityButton : undefined
92+
}
93+
>
4994
<PageTitle>Copilot Opportunity</PageTitle>
5095
{isValidating && !showNotFound && (
5196
<LoadingSpinner />
@@ -132,6 +177,17 @@ const CopilotOpportunityDetails: FC<{}> = () => {
132177
<span className={styles.textCaps}>{opportunity?.requiresCommunication}</span>
133178
</div>
134179
</div>
180+
{
181+
showApplyOpportunityModal
182+
&& opportunity && (
183+
<ApplyOpportunityModal
184+
copilotOpportunityId={opportunity?.id}
185+
onClose={onCloseApplyModal}
186+
projectName={opportunity?.projectName}
187+
onApplied={onApplied}
188+
/>
189+
)
190+
}
135191
</ContentLayout>
136192
)
137193
}

src/apps/copilots/src/services/copilot-opportunities.ts

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ import useSWR, { SWRResponse } from 'swr'
22
import useSWRInfinite, { SWRInfiniteResponse } from 'swr/infinite'
33

44
import { EnvironmentConfig } from '~/config'
5-
import { xhrGetAsync } from '~/libs/core'
5+
import { xhrGetAsync, xhrPostAsync } from '~/libs/core'
66
import { buildUrl } from '~/libs/shared/lib/utils/url'
77

88
import { CopilotOpportunity } from '../models/CopilotOpportunity'
9+
import { CopilotApplication } from '../models/CopilotApplication'
910

10-
const baseUrl = `${EnvironmentConfig.API.V5}/projects`
11+
export const copilotBaseUrl = `${EnvironmentConfig.API.V5}/projects`
1112

1213
const PAGE_SIZE = 20
1314

@@ -42,7 +43,9 @@ export interface CopilotOpportunitiesResponse {
4243
export const useCopilotOpportunities = (): CopilotOpportunitiesResponse => {
4344
const getKey = (pageIndex: number, previousPageData: CopilotOpportunity[]): string | undefined => {
4445
if (previousPageData && previousPageData.length < PAGE_SIZE) return undefined
45-
return `${baseUrl}/copilots/opportunities?page=${pageIndex + 1}&pageSize=${PAGE_SIZE}&sort=createdAt desc`
46+
return `
47+
${copilotBaseUrl}/copilots/opportunities?page=${pageIndex + 1}&pageSize=${PAGE_SIZE}&sort=createdAt desc
48+
`
4649
}
4750

4851
const fetcher = (url: string): Promise<CopilotOpportunity[]> => xhrGetAsync<CopilotOpportunity[]>(url)
@@ -68,14 +71,16 @@ export const useCopilotOpportunities = (): CopilotOpportunitiesResponse => {
6871

6972
export type CopilotOpportunityResponse = SWRResponse<CopilotOpportunity, CopilotOpportunity>
7073

74+
export type CopilotApplicationResponse = SWRResponse<CopilotApplication[], CopilotApplication[]>
75+
7176
/**
7277
* Custom hook to fetch copilot opportunity by id.
7378
*
7479
* @param {string} opportunityId - The unique identifier of the copilot request.
7580
* @returns {CopilotOpportunityResponse} - The response containing the copilot request data.
7681
*/
7782
export const useCopilotOpportunity = (opportunityId?: string): CopilotOpportunityResponse => {
78-
const url = opportunityId ? buildUrl(`${baseUrl}/copilot/opportunity/${opportunityId}`) : undefined
83+
const url = opportunityId ? buildUrl(`${copilotBaseUrl}/copilot/opportunity/${opportunityId}`) : undefined
7984

8085
const fetcher = (urlp: string): Promise<CopilotOpportunity> => xhrGetAsync<CopilotOpportunity>(urlp)
8186
.then(copilotOpportunityFactory)
@@ -85,3 +90,35 @@ export const useCopilotOpportunity = (opportunityId?: string): CopilotOpportunit
8590
revalidateOnFocus: false,
8691
})
8792
}
93+
94+
/**
95+
* apply copilot opportunity
96+
* @param opportunityId
97+
* @param request
98+
* @returns
99+
*/
100+
export const applyCopilotOpportunity = async (opportunityId: number, request: {
101+
notes?: string;
102+
}): Promise<CopilotApplication> => {
103+
const url = `${copilotBaseUrl}/copilots/opportunity/${opportunityId}/apply`
104+
105+
return xhrPostAsync(url, request, {})
106+
}
107+
108+
/**
109+
* Custom hook to fetch copilot applications by opportunity id.
110+
*
111+
* @param {string} opportunityId - The unique identifier of the copilot request.
112+
* @returns {CopilotApplicationResponse} - The response containing the copilot application data.
113+
*/
114+
export const useCopilotApplications = (opportunityId?: string): CopilotApplicationResponse => {
115+
const url = opportunityId
116+
? buildUrl(`${copilotBaseUrl}/copilots/opportunity/${opportunityId}/applications`)
117+
: undefined
118+
119+
const fetcher = (urlp: string): Promise<CopilotApplication[]> => xhrGetAsync<CopilotApplication[]>(urlp)
120+
.then(data => data)
121+
.catch(() => [])
122+
123+
return useSWR(url, fetcher)
124+
}

src/libs/core/lib/profile/profile-functions/profile-factory/user-role.enum.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@ export enum UserRole {
1111
paymentProviderViewer = 'PaymentProvider Viewer',
1212
projectManager = 'Project Manager',
1313
taxFormAdmin = 'TaxForm Admin',
14-
taxFormViewer = 'TaxForm Viewer'
14+
taxFormViewer = 'TaxForm Viewer',
15+
copilot = 'copilot'
1516
}

0 commit comments

Comments
 (0)