Skip to content

Commit b57e191

Browse files
authored
Merge pull request #1068 from topcoder-platform/pm-1067
feat(PM-1067): list copilot applications
2 parents 233712b + cf19762 commit b57e191

File tree

13 files changed

+347
-59
lines changed

13 files changed

+347
-59
lines changed

src/apps/copilots/src/models/CopilotApplication.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,6 @@ export interface CopilotApplication {
33
notes?: string,
44
createdAt: Date,
55
opportunityId: string,
6+
handle?: string,
7+
userId: number,
68
}

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

Lines changed: 53 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
/* eslint-disable react/jsx-no-bind */
2+
/* eslint-disable complexity */
23
import {
4+
Dispatch,
35
FC,
6+
SetStateAction,
47
useCallback,
58
useContext,
69
useEffect,
710
useMemo,
811
useState,
912
} from 'react'
10-
import { useNavigate, useParams } from 'react-router-dom'
13+
import { useLocation, useNavigate, useParams } from 'react-router-dom'
1114
import { mutate } from 'swr'
1215
import moment from 'moment'
1316

@@ -17,6 +20,7 @@ import {
1720
IconOutline,
1821
LoadingSpinner,
1922
PageTitle,
23+
TabsNavbar,
2024
} from '~/libs/ui'
2125
import { profileContext, ProfileContextData, UserRole } from '~/libs/core'
2226

@@ -27,29 +31,60 @@ import {
2731
useCopilotApplications,
2832
useCopilotOpportunity,
2933
} from '../../services/copilot-opportunities'
34+
import { FormattedMembers, useMembers } from '../../services/members'
3035
import { copilotRoutesMap } from '../../copilots.routes'
3136

3237
import { ApplyOpportunityModal } from './apply-opportunity-modal'
38+
import {
39+
CopilotDetailsTabViews,
40+
getCopilotDetailsTabsConfig,
41+
getHashFromTabId,
42+
getTabIdFromHash,
43+
} from './tabs/config/copilot-details-tabs-config'
44+
import { OpportunityDetails } from './tabs/opportunity-details'
45+
import { CopilotApplications } from './tabs/copilot-applications'
3346
import styles from './styles.module.scss'
3447

3548
const CopilotOpportunityDetails: FC<{}> = () => {
3649
const { opportunityId }: {opportunityId?: string} = useParams<{ opportunityId?: string }>()
3750
const navigate = useNavigate()
3851
const [showNotFound, setShowNotFound] = useState(false)
3952
const [showApplyOpportunityModal, setShowApplyOpportunityModal] = useState(false)
40-
const { profile }: ProfileContextData = useContext(profileContext)
53+
const { profile, initialized }: ProfileContextData = useContext(profileContext)
4154
const isCopilot: boolean = useMemo(
4255
() => !!profile?.roles?.some(role => role === UserRole.copilot),
4356
[profile],
4457
)
58+
const isAdminOrPM: boolean = useMemo(
59+
() => !!profile?.roles?.some(role => role === UserRole.administrator || role === UserRole.projectManager),
60+
[profile],
61+
)
4562
const { data: copilotApplications }: { data?: CopilotApplication[] } = useCopilotApplications(opportunityId)
63+
const { data: members }: { data?: FormattedMembers[]} = useMembers(
64+
copilotApplications ? copilotApplications?.map(item => item.userId) : [],
65+
)
4666

4767
if (!opportunityId) {
4868
navigate(copilotRoutesMap.CopilotOpportunityList)
4969
}
5070

5171
const { data: opportunity, isValidating }: CopilotOpportunityResponse = useCopilotOpportunity(opportunityId)
5272

73+
const { hash }: { hash: string } = useLocation()
74+
75+
const activeTabHash: string = useMemo<string>(() => getTabIdFromHash(hash), [hash])
76+
77+
const [activeTab, setActiveTab]: [string, Dispatch<SetStateAction<string>>] = useState<string>(activeTabHash)
78+
79+
useEffect(() => {
80+
setActiveTab(activeTabHash)
81+
}, [activeTabHash])
82+
83+
const handleTabChange = useCallback((tabId: string): void => {
84+
setActiveTab(tabId)
85+
window.location.hash = getHashFromTabId(tabId)
86+
}, [getHashFromTabId, setActiveTab])
87+
5388
useEffect(() => {
5489
const timer = setTimeout(() => {
5590
if (!opportunity) {
@@ -154,29 +189,23 @@ const CopilotOpportunityDetails: FC<{}> = () => {
154189
</div>
155190
</div>
156191
</div>
157-
<div className={styles.content}>
158-
<div>
159-
<h2 className={styles.subHeading}> Required skills </h2>
160-
<div className={styles.skillsContainer}>
161-
{opportunity?.skills.map((skill: any) => (
162-
<div key={skill.id} className={styles.skillPill}>
163-
{skill.name}
164-
</div>
165-
))}
166-
</div>
167-
<h2 className={styles.subHeading}> Description </h2>
168-
<p>
169-
{opportunity?.overview}
170-
</p>
171-
</div>
172-
<div>
173-
<h2 className={styles.subHeading}> Complexity </h2>
174-
<span className={styles.textCaps}>{opportunity?.complexity}</span>
192+
{
193+
initialized && (
194+
<TabsNavbar
195+
defaultActive={activeTab}
196+
onChange={handleTabChange}
197+
tabs={getCopilotDetailsTabsConfig(isAdminOrPM)}
198+
/>
199+
)
200+
}
201+
{activeTab === CopilotDetailsTabViews.details && <OpportunityDetails opportunity={opportunity} />}
202+
{activeTab === CopilotDetailsTabViews.applications && isAdminOrPM && (
203+
<CopilotApplications
204+
copilotApplications={copilotApplications}
205+
members={members}
206+
/>
207+
)}
175208

176-
<h2 className={styles.subHeading}> Requires Communication </h2>
177-
<span className={styles.textCaps}>{opportunity?.requiresCommunication}</span>
178-
</div>
179-
</div>
180209
{
181210
showApplyOpportunityModal
182211
&& opportunity && (

src/apps/copilots/src/pages/copilot-opportunity-details/styles.module.scss

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -13,40 +13,6 @@
1313
color: $teal-100;
1414
}
1515

16-
.subHeading {
17-
margin-top: $sp-8;
18-
}
19-
20-
.content {
21-
margin-top: $sp-6;
22-
display: flex;
23-
flex-direction: row;
24-
gap: 100px;
25-
}
26-
27-
.content > div:first-child {
28-
flex: 3;
29-
}
30-
31-
.content > div:last-child {
32-
flex: 1;
33-
}
34-
35-
.skillsContainer {
36-
display: flex;
37-
flex-wrap: wrap;
38-
gap: 8px;
39-
margin-top: $sp-2;
40-
}
41-
42-
.skillPill {
43-
background-color: $teal-100;
44-
color: white;
45-
padding: 4px 8px;
46-
border-radius: 10px;
47-
white-space: nowrap;
48-
font-size: 14px;
49-
}
5016

5117
.infoRow {
5218
display: flex;
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { TabsNavItem } from '~/libs/ui'
2+
3+
export enum CopilotDetailsTabViews {
4+
details = '0',
5+
applications = '1',
6+
}
7+
8+
export const getCopilotDetailsTabsConfig = (isAdminOrPM: boolean): TabsNavItem[] => (isAdminOrPM ? [
9+
{
10+
id: CopilotDetailsTabViews.details,
11+
title: 'Details',
12+
},
13+
{
14+
id: CopilotDetailsTabViews.applications,
15+
title: 'Applications',
16+
},
17+
] : [
18+
{
19+
id: CopilotDetailsTabViews.details,
20+
title: 'Details',
21+
},
22+
])
23+
24+
export const CopilotDetailsTabsConfig: TabsNavItem[] = [
25+
{
26+
id: CopilotDetailsTabViews.details,
27+
title: 'Details',
28+
},
29+
{
30+
id: CopilotDetailsTabViews.applications,
31+
title: 'Applications',
32+
},
33+
]
34+
35+
export function getHashFromTabId(tabId: string): string {
36+
switch (tabId) {
37+
case CopilotDetailsTabViews.details:
38+
return '#details'
39+
case CopilotDetailsTabViews.applications:
40+
return '#applications'
41+
default:
42+
return '#details'
43+
}
44+
}
45+
46+
export function getTabIdFromHash(hash: string): string {
47+
switch (hash) {
48+
case '#details':
49+
return CopilotDetailsTabViews.details
50+
case '#applications':
51+
return CopilotDetailsTabViews.applications
52+
default:
53+
return CopilotDetailsTabViews.details
54+
}
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { FC, useMemo } from 'react'
2+
3+
import { Table, TableColumn } from '~/libs/ui'
4+
import { USER_PROFILE_URL } from '~/config/environments/default.env'
5+
6+
import { CopilotApplication } from '../../../../models/CopilotApplication'
7+
import { FormattedMembers } from '../../../../services/members'
8+
9+
import styles from './styles.module.scss'
10+
11+
const tableColumns: TableColumn<CopilotApplication>[] = [
12+
{
13+
label: 'Topcoder Handle',
14+
propertyName: 'handle',
15+
renderer: (copilotApplication: CopilotApplication) => (
16+
<a
17+
href={`${USER_PROFILE_URL}/${copilotApplication.handle}`}
18+
target='_blank'
19+
rel='noreferrer'
20+
>
21+
{copilotApplication.handle}
22+
</a>
23+
),
24+
type: 'element',
25+
},
26+
{
27+
label: 'Fulfillment Rating',
28+
propertyName: 'fulfilment',
29+
type: 'text',
30+
},
31+
{
32+
label: 'Active Projects',
33+
propertyName: 'activeProjects',
34+
type: 'text',
35+
},
36+
{
37+
label: 'Applied Date',
38+
propertyName: 'createdAt',
39+
type: 'date',
40+
},
41+
{
42+
label: 'Notes',
43+
propertyName: 'notes',
44+
renderer: (copilotApplication: CopilotApplication) => (
45+
<div className={styles.title}>
46+
{copilotApplication.notes}
47+
</div>
48+
),
49+
type: 'element',
50+
},
51+
]
52+
53+
const CopilotApplications: FC<{
54+
copilotApplications?: CopilotApplication[]
55+
members?: FormattedMembers[]
56+
}> = props => {
57+
const getData = (): CopilotApplication[] => (props.copilotApplications ? props.copilotApplications.map(item => {
58+
const member = props.members && props.members.find(each => each.userId === item.userId)
59+
return {
60+
...item,
61+
activeProjects: member?.activeProjects || 0,
62+
fulfilment: member?.copilotFulfillment || 0,
63+
handle: member?.handle,
64+
}
65+
})
66+
.sort((a, b) => (b.fulfilment || 0) - (a.fulfilment || 0)) : [])
67+
68+
const tableData = useMemo(getData, [props.copilotApplications, props.members])
69+
70+
return (
71+
<div>
72+
{
73+
tableData.length > 0 && (
74+
<Table
75+
columns={tableColumns}
76+
data={tableData}
77+
disableSorting
78+
removeDefaultSort
79+
preventDefault
80+
/>
81+
)
82+
}
83+
</div>
84+
)
85+
}
86+
87+
export default CopilotApplications
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as CopilotApplications } from './CopilotApplications'

src/apps/copilots/src/pages/copilot-opportunity-details/tabs/copilot-applications/styles.module.scss

Whitespace-only changes.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { FC } from 'react'
2+
3+
import { CopilotOpportunity } from '../../../../models/CopilotOpportunity'
4+
5+
import styles from './styles.module.scss'
6+
7+
const OpportunityDetails: FC<{
8+
opportunity?: CopilotOpportunity
9+
}> = props => (
10+
<div className={styles.content}>
11+
<div>
12+
<h2 className={styles.subHeading}> Required skills </h2>
13+
<div className={styles.skillsContainer}>
14+
{props.opportunity?.skills.map((skill: any) => (
15+
<div key={skill.id} className={styles.skillPill}>
16+
{skill.name}
17+
</div>
18+
))}
19+
</div>
20+
<h2 className={styles.subHeading}> Description </h2>
21+
<p>
22+
{props.opportunity?.overview}
23+
</p>
24+
</div>
25+
<div>
26+
<h2 className={styles.subHeading}> Complexity </h2>
27+
<span className={styles.textCaps}>{props.opportunity?.complexity}</span>
28+
29+
<h2 className={styles.subHeading}> Requires Communication </h2>
30+
<span className={styles.textCaps}>{props.opportunity?.requiresCommunication}</span>
31+
</div>
32+
</div>
33+
)
34+
35+
export default OpportunityDetails
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as OpportunityDetails } from './OpportunityDetails'

0 commit comments

Comments
 (0)