Skip to content

Commit 92d56f7

Browse files
committed
feat: apply for copilot opportunity
1 parent bd7518e commit 92d56f7

File tree

7 files changed

+153
-8
lines changed

7 files changed

+153
-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,63 @@
1+
import { FC, useCallback, useState } from 'react'
2+
3+
import { BaseModal, Button, InputTextarea } from '~/libs/ui'
4+
5+
import styles from './styles.module.scss'
6+
import { applyCopilotOpportunity } from '../../../services/copilot-opportunities'
7+
8+
interface ApplyOpportunityModalProps {
9+
onClose: () => void
10+
copilotOpportunityId: number
11+
projectName: string
12+
onApplied: () => void
13+
}
14+
15+
const ApplyOpportunityModal: FC<ApplyOpportunityModalProps> = props => {
16+
const [notes, setNotes] = useState('')
17+
const [success, setSuccess] = useState(false);
18+
19+
const onApply = useCallback(async () => {
20+
await applyCopilotOpportunity(props.copilotOpportunityId, {
21+
notes,
22+
})
23+
24+
props.onApplied()
25+
setSuccess(true)
26+
}, [props.copilotOpportunityId, notes])
27+
28+
return (
29+
<BaseModal
30+
onClose={props.onClose}
31+
open
32+
size='lg'
33+
title={success ? `Your Application for ${props.projectName} Has Been Received!` : `Confirm Your Copilot Application for ${props.projectName}`}
34+
buttons={
35+
!success ? (
36+
<>
37+
<Button primary onClick={onApply} label='Apply' />
38+
<Button secondary onClick={props.onClose} label='Cancel' />
39+
</>
40+
) : (
41+
<Button primary onClick={props.onClose} label='Close' />
42+
)
43+
}
44+
>
45+
46+
<div className={styles.applyCopilotModal}>
47+
<div className={styles.info}>
48+
{success ?
49+
"We appreciate the time and effort you've taken to apply for this exciting opportunity. Our team is committed to providing a seamless and efficient process to ensure a great experience for all copilots. We will review your application within short time." : `We're excited to see your interest in joining our team as a copilot for the ${props.projectName} project! Before we proceed, we want to ensure that you have carefully reviewed the project requirements and are committed to meeting them.`}
50+
</div>
51+
{
52+
!success && (
53+
<InputTextarea name="Notes" onChange={(e) => {
54+
setNotes(e.target.value)
55+
}} value={notes} />
56+
)
57+
}
58+
</div>
59+
</BaseModal>
60+
)
61+
}
62+
63+
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: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,34 @@
1-
import { FC, useEffect, useState } from 'react'
1+
import { FC, useContext, useEffect, useMemo, useState } from 'react'
22
import { useNavigate, useParams } from 'react-router-dom'
33
import moment from 'moment'
4+
import { mutate } from 'swr'
45

56
import {
7+
ButtonProps,
68
ContentLayout,
79
IconOutline,
810
LoadingSpinner,
911
PageTitle,
1012
} from '~/libs/ui'
13+
import { profileContext, ProfileContextData, UserRole } from '~/libs/core'
1114

12-
import { CopilotOpportunityResponse, useCopilotOpportunity } from '../../services/copilot-opportunities'
15+
import { copilotBaseUrl, CopilotOpportunityResponse, useCopilotApplications, useCopilotOpportunity } from '../../services/copilot-opportunities'
1316
import { copilotRoutesMap } from '../../copilots.routes'
1417

1518
import styles from './styles.module.scss'
19+
import { ApplyOpportunityModal } from './apply-opportunity-modal'
1620

1721
const CopilotOpportunityDetails: FC<{}> = () => {
1822
const { opportunityId }: {opportunityId?: string} = useParams<{ opportunityId?: string }>()
1923
const navigate = useNavigate()
2024
const [showNotFound, setShowNotFound] = useState(false)
25+
const [showApplyOpportunityModal, setShowApplyOpportunityModal] = useState(false)
26+
const { profile }: ProfileContextData = useContext(profileContext)
27+
const isCopilot: boolean = useMemo(
28+
() => !!profile?.roles?.some(role => role === UserRole.copilot),
29+
[profile],
30+
)
31+
const { data: copilotApplications } = useCopilotApplications(opportunityId)
2132

2233
if (!opportunityId) {
2334
navigate(copilotRoutesMap.CopilotOpportunityList)
@@ -44,8 +55,17 @@ const CopilotOpportunityDetails: FC<{}> = () => {
4455
)
4556
}
4657

58+
const applyCopilotOpportunityButton: ButtonProps = {
59+
label: 'Apply as Copilot',
60+
onClick: () => setShowApplyOpportunityModal(true),
61+
}
62+
63+
const onApplied = () => {
64+
mutate(`${copilotBaseUrl}/copilots/opportunity/${opportunityId}/applications`)
65+
}
66+
4767
return (
48-
<ContentLayout title='Copilot Opportunity'>
68+
<ContentLayout title='Copilot Opportunity' buttonConfig={isCopilot && copilotApplications && copilotApplications.length === 0 ? applyCopilotOpportunityButton : undefined}>
4969
<PageTitle>Copilot Opportunity</PageTitle>
5070
{isValidating && !showNotFound && (
5171
<LoadingSpinner />
@@ -132,6 +152,9 @@ const CopilotOpportunityDetails: FC<{}> = () => {
132152
<span className={styles.textCaps}>{opportunity?.requiresCommunication}</span>
133153
</div>
134154
</div>
155+
{
156+
showApplyOpportunityModal && opportunity && <ApplyOpportunityModal copilotOpportunityId={opportunity?.id} onClose={() => setShowApplyOpportunityModal(false)} projectName={opportunity?.projectName} onApplied={onApplied} />
157+
}
135158
</ContentLayout>
136159
)
137160
}

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

Lines changed: 48 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

@@ -26,6 +27,15 @@ function copilotOpportunityFactory(data: any): CopilotOpportunity {
2627
}
2728
}
2829

30+
/**
31+
* Creates a CopilotApplication object by merging the provided data and its nested data
32+
* @param data
33+
* @returns
34+
*/
35+
function opportunityApplicationFactory(data: any): CopilotApplication[] {
36+
return data
37+
}
38+
2939
export interface CopilotOpportunitiesResponse {
3040
isValidating: boolean;
3141
data: CopilotOpportunity[];
@@ -42,7 +52,7 @@ export interface CopilotOpportunitiesResponse {
4252
export const useCopilotOpportunities = (): CopilotOpportunitiesResponse => {
4353
const getKey = (pageIndex: number, previousPageData: CopilotOpportunity[]): string | undefined => {
4454
if (previousPageData && previousPageData.length < PAGE_SIZE) return undefined
45-
return `${baseUrl}/copilots/opportunities?page=${pageIndex + 1}&pageSize=${PAGE_SIZE}&sort=createdAt desc`
55+
return `${copilotBaseUrl}/copilots/opportunities?page=${pageIndex + 1}&pageSize=${PAGE_SIZE}&sort=createdAt desc`
4656
}
4757

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

6979
export type CopilotOpportunityResponse = SWRResponse<CopilotOpportunity, CopilotOpportunity>
7080

81+
export type CopilotApplicationResponse = SWRResponse<CopilotApplication[], CopilotApplication[]>
82+
7183
/**
7284
* Custom hook to fetch copilot opportunity by id.
7385
*
7486
* @param {string} opportunityId - The unique identifier of the copilot request.
7587
* @returns {CopilotOpportunityResponse} - The response containing the copilot request data.
7688
*/
7789
export const useCopilotOpportunity = (opportunityId?: string): CopilotOpportunityResponse => {
78-
const url = opportunityId ? buildUrl(`${baseUrl}/copilot/opportunity/${opportunityId}`) : undefined
90+
const url = opportunityId ? buildUrl(`${copilotBaseUrl}/copilot/opportunity/${opportunityId}`) : undefined
7991

8092
const fetcher = (urlp: string): Promise<CopilotOpportunity> => xhrGetAsync<CopilotOpportunity>(urlp)
8193
.then(copilotOpportunityFactory)
@@ -85,3 +97,35 @@ export const useCopilotOpportunity = (opportunityId?: string): CopilotOpportunit
8597
revalidateOnFocus: false,
8698
})
8799
}
100+
101+
/**
102+
* apply copilot opportunity
103+
* @param opportunityId
104+
* @param request
105+
* @returns
106+
*/
107+
export const applyCopilotOpportunity = async (opportunityId: number, request: {
108+
notes?: string;
109+
}) => {
110+
const url = `${copilotBaseUrl}/copilots/opportunity/${opportunityId}/apply`
111+
112+
return xhrPostAsync(url, request, {})
113+
}
114+
115+
/**
116+
* Custom hook to fetch copilot applications by opportunity id.
117+
*
118+
* @param {string} opportunityId - The unique identifier of the copilot request.
119+
* @returns {CopilotApplicationResponse} - The response containing the copilot application data.
120+
*/
121+
export const useCopilotApplications = (opportunityId?: string): CopilotApplicationResponse => {
122+
const url = opportunityId ? buildUrl(`${copilotBaseUrl}/copilots/opportunity/${opportunityId}/applications`) : undefined
123+
124+
const fetcher = (urlp: string): Promise<CopilotApplication[]> => xhrGetAsync<CopilotApplication[]>(urlp)
125+
.then((data) => data).catch(() => [])
126+
127+
return useSWR(url, fetcher, {
128+
refreshInterval: 0,
129+
revalidateOnFocus: false,
130+
})
131+
}

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)