Skip to content

Commit 6469522

Browse files
authored
Merge pull request #1466 from topcoder-platform/dev
[PROD RELEASE]
2 parents 9ac441d + c136226 commit 6469522

7 files changed

Lines changed: 233 additions & 53 deletions

File tree

src/apps/onboarding/src/redux/actions/member.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ const createWorksPayloadData: any = (works: WorkInfo[]) => {
212212
const data: any = works.map(work => {
213213
const {
214214
companyName,
215+
company,
215216
position,
216217
industry,
217218
otherIndustry,
@@ -222,10 +223,12 @@ const createWorksPayloadData: any = (works: WorkInfo[]) => {
222223
city,
223224
associatedSkills,
224225
}: any = work
226+
const normalizedCompanyName: string = _.trim(companyName || company || '')
225227
return {
226228
associatedSkills: Array.isArray(associatedSkills) ? associatedSkills : undefined,
227229
cityName: city,
228-
companyName: companyName || '',
230+
company: normalizedCompanyName || undefined,
231+
companyName: normalizedCompanyName,
229232
description: description || undefined,
230233
// eslint-disable-next-line unicorn/no-null
231234
endDate: endDate ? endDate.toISOString() : null,

src/apps/wallet-admin/src/home/tabs/payments/PaymentsTab.tsx

Lines changed: 118 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { toast } from 'react-toastify'
44
import { AxiosError } from 'axios'
55
import React, { FC, useCallback, useEffect } from 'react'
66

7-
import { Collapsible, ConfirmModal, LoadingCircles } from '~/libs/ui'
7+
import { Collapsible, ConfirmModal, InputText, LoadingCircles } from '~/libs/ui'
88
import { UserProfile } from '~/libs/core'
99
import { downloadBlob } from '~/libs/shared'
1010

@@ -13,6 +13,7 @@ import { Winning, WinningDetail } from '../../../lib/models/WinningDetail'
1313
import { FilterBar, formatIOSDateString, PaymentView } from '../../../lib'
1414
import { ConfirmFlowData } from '../../../lib/models/ConfirmFlowData'
1515
import { PaginationInfo } from '../../../lib/models/PaginationInfo'
16+
import { Filter } from '../../../lib/components/filter-bar/FilterBar'
1617
import PaymentEditForm from '../../../lib/components/payment-edit/PaymentEdit'
1718
import PaymentsTable from '../../../lib/components/payments-table/PaymentTable'
1819

@@ -69,6 +70,7 @@ const ListView: FC<ListViewProps> = (props: ListViewProps) => {
6970
const [isConfirmFormValid, setIsConfirmFormValid] = React.useState<boolean>(false)
7071
const [winnings, setWinnings] = React.useState<ReadonlyArray<Winning>>([])
7172
const [selectedPayments, setSelectedPayments] = React.useState<{ [paymentId: string]: Winning }>({})
73+
const selectedPaymentsCount = Object.keys(selectedPayments).length
7274
const [isLoading, setIsLoading] = React.useState<boolean>(false)
7375
const [filters, setFilters] = React.useState<Record<string, string[]>>({})
7476
const [pagination, setPagination] = React.useState<PaginationInfo>({
@@ -290,6 +292,50 @@ const ListView: FC<ListViewProps> = (props: ListViewProps) => {
290292
|| props.profile.roles.includes('Payment Editor')
291293
)
292294

295+
const isEngagementPaymentApprover = props.profile.roles.includes('Engagement Payment Approver')
296+
const [bulkOpen, setBulkOpen] = React.useState(false)
297+
const [bulkAuditNote, setBulkAuditNote] = React.useState('')
298+
299+
const onBulkApprove = useCallback(async (auditNote: string) => {
300+
const ids = Object.keys(selectedPayments)
301+
if (ids.length === 0) return
302+
303+
toast.success('Starting bulk approve', { position: toast.POSITION.BOTTOM_RIGHT })
304+
305+
let successfullyUpdated = 0
306+
for (const id of ids) {
307+
const updates: any = {
308+
auditNote,
309+
paymentStatus: 'OWED',
310+
winningsId: id,
311+
}
312+
313+
try {
314+
// awaiting sequentially to preserve order and server load control
315+
// errors for individual items are caught and reported
316+
// eslint-disable-next-line no-await-in-loop
317+
await editPayment(updates)
318+
successfullyUpdated += 1
319+
} catch (err:any) {
320+
const paymentName = selectedPayments[id]?.handle || id
321+
if (err?.message) {
322+
toast.error(`Failed to update payment ${paymentName} (${id}): ${err.message}`, { position: toast.POSITION.BOTTOM_RIGHT })
323+
} else {
324+
toast.error(`Failed to update payment ${paymentName} (${id})`, { position: toast.POSITION.BOTTOM_RIGHT })
325+
}
326+
}
327+
}
328+
329+
if (successfullyUpdated === ids.length) {
330+
toast.success(`Successfully updated ${successfullyUpdated} winnings`, { position: toast.POSITION.BOTTOM_RIGHT })
331+
}
332+
333+
setBulkAuditNote('')
334+
setBulkOpen(false)
335+
setSelectedPayments({})
336+
await fetchWinnings()
337+
}, [selectedPayments, fetchWinnings])
338+
293339
return (
294340
<>
295341
<div className={styles.container}>
@@ -300,6 +346,8 @@ const ListView: FC<ListViewProps> = (props: ListViewProps) => {
300346
<Collapsible header={<h3>Payment Listing</h3>}>
301347
<FilterBar
302348
showExportButton
349+
selectedCount={selectedPaymentsCount}
350+
onBulkClick={() => setBulkOpen(true)}
303351
onExport={async () => {
304352
toast.success('Downloading payments report. This may take a few moments.', { position: toast.POSITION.BOTTOM_RIGHT })
305353
downloadBlob(
@@ -354,33 +402,35 @@ const ListView: FC<ListViewProps> = (props: ListViewProps) => {
354402
],
355403
type: 'dropdown',
356404
},
357-
{
358-
key: 'type',
359-
label: 'Type',
360-
options: [
361-
{
362-
label: 'Task Payment',
363-
value: 'TASK_PAYMENT',
364-
},
365-
{
366-
label: 'Contest Payment',
367-
value: 'CONTEST_PAYMENT',
368-
},
369-
{
370-
label: 'Copilot Payment',
371-
value: 'COPILOT_PAYMENT',
372-
},
373-
{
374-
label: 'Review Board Payment',
375-
value: 'REVIEW_BOARD_PAYMENT',
376-
},
377-
{
378-
label: 'Engagement Payment',
379-
value: 'ENGAGEMENT_PAYMENT',
380-
},
381-
],
382-
type: 'dropdown',
383-
},
405+
...(isEngagementPaymentApprover ? [] : [
406+
{
407+
key: 'category',
408+
label: 'Type',
409+
options: [
410+
{
411+
label: 'Task Payment',
412+
value: 'TASK_PAYMENT',
413+
},
414+
{
415+
label: 'Contest Payment',
416+
value: 'CONTEST_PAYMENT',
417+
},
418+
{
419+
label: 'Copilot Payment',
420+
value: 'COPILOT_PAYMENT',
421+
},
422+
{
423+
label: 'Review Board Payment',
424+
value: 'REVIEW_BOARD_PAYMENT',
425+
},
426+
{
427+
label: 'Engagement Payment',
428+
value: 'ENGAGEMENT_PAYMENT',
429+
},
430+
],
431+
type: 'dropdown',
432+
},
433+
] as Filter[]),
384434
{
385435
key: 'date',
386436
label: 'Date',
@@ -449,11 +499,13 @@ const ListView: FC<ListViewProps> = (props: ListViewProps) => {
449499
{isLoading && <LoadingCircles className={styles.centered} />}
450500
{!isLoading && winnings.length > 0 && (
451501
<PaymentsTable
502+
enableBulkEdit={isEngagementPaymentApprover}
452503
canEdit={isEditingAllowed()}
453504
currentPage={pagination.currentPage}
454505
numPages={pagination.totalPages}
455506
payments={winnings}
456507
selectedPayments={selectedPayments}
508+
onSelectionChange={selected => setSelectedPayments(selected)}
457509
onNextPageClick={async function onNextPageClicked() {
458510
if (pagination.currentPage === pagination.totalPages) {
459511
return
@@ -513,6 +565,44 @@ const ListView: FC<ListViewProps> = (props: ListViewProps) => {
513565
</Collapsible>
514566
</div>
515567
</div>
568+
{bulkOpen && (
569+
<ConfirmModal
570+
maxWidth='800px'
571+
size='lg'
572+
showButtons
573+
title={`${selectedPaymentsCount > 1 ? 'Bulk ' : ''}Approve Payment${selectedPaymentsCount > 1 ? 's' : ''}`}
574+
action='Approve'
575+
onClose={function onClose() {
576+
setBulkAuditNote('')
577+
setBulkOpen(false)
578+
}}
579+
onConfirm={function onConfirm() {
580+
onBulkApprove(bulkAuditNote)
581+
}}
582+
canSave={bulkAuditNote.trim().length > 0}
583+
open={bulkOpen}
584+
>
585+
<div>
586+
<p>
587+
You are about to approve
588+
{' '}
589+
{selectedPaymentsCount}
590+
{' '}
591+
payment
592+
{selectedPaymentsCount > 1 ? 's' : ''}
593+
.
594+
</p>
595+
<br />
596+
<InputText
597+
type='text'
598+
label='Audit Note'
599+
name='bulkAuditNote'
600+
value={bulkAuditNote}
601+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setBulkAuditNote(e.target.value)}
602+
/>
603+
</div>
604+
</ConfirmModal>
605+
)}
516606
{confirmFlow && (
517607
<ConfirmModal
518608
maxWidth='800px'

src/apps/wallet-admin/src/lib/components/filter-bar/FilterBar.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ type FilterOptions = {
1414
value: string;
1515
};
1616

17-
type Filter = {
17+
export type Filter = {
1818
key: string;
1919
label: string;
2020
type: 'input' | 'dropdown' | 'member_autocomplete';
@@ -27,6 +27,8 @@ interface FilterBarProps {
2727
onFilterChange: (key: string, value: string[]) => void;
2828
onResetFilters?: () => void;
2929
onExport?: () => void;
30+
selectedCount?: number;
31+
onBulkClick?: () => void;
3032
}
3133

3234
const FilterBar: React.FC<FilterBarProps> = (props: FilterBarProps) => {
@@ -120,6 +122,17 @@ const FilterBar: React.FC<FilterBarProps> = (props: FilterBarProps) => {
120122
size='lg'
121123
/>
122124
)}
125+
{!!props.selectedCount && props.selectedCount > 0 && (
126+
<>
127+
<Button
128+
primary
129+
className={styles.bulkButton}
130+
label={`${props.selectedCount > 1 ? 'Bulk ' : ''}Approve (${props.selectedCount})`}
131+
size='lg'
132+
onClick={() => props.onBulkClick?.()}
133+
/>
134+
</>
135+
)}
123136
<Button
124137
primary
125138
className={styles.resetButton}

src/apps/wallet-admin/src/lib/components/payments-table/PaymentTable.tsx

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ import { Winning } from '../../models/WinningDetail'
1010
import styles from './PaymentTable.module.scss'
1111

1212
interface PaymentTableProps {
13+
enableBulkEdit?: boolean
1314
payments: ReadonlyArray<Winning>;
1415
selectedPayments?: { [paymentId: string]: Winning };
16+
onSelectionChange?: (selected: { [paymentId: string]: Winning }) => void;
1517
currentPage: number;
1618
numPages: number;
1719
onPaymentEditClick: (payment: Winning) => void;
@@ -31,12 +33,61 @@ const PaymentsTable: React.FC<PaymentTableProps> = (props: PaymentTableProps) =>
3133
}
3234
}, [props.selectedPayments])
3335

36+
// Only rows with this status are selectable for bulk actions
37+
const selectableStatus = 'On Hold (Admin)'
38+
39+
const onToggleRow = (payment: Winning, checked: boolean) => {
40+
setSelectedPayments(prev => {
41+
const next = { ...prev }
42+
if (checked) {
43+
next[payment.id] = payment
44+
} else {
45+
delete next[payment.id]
46+
}
47+
48+
props.onSelectionChange?.(next)
49+
return next
50+
})
51+
}
52+
53+
const visibleSelectablePayments = props.payments.filter(p => p.status === selectableStatus)
54+
const allVisibleSelected = visibleSelectablePayments.length > 0 && visibleSelectablePayments.every(p => selectedPayments[p.id])
55+
const someVisibleSelected = visibleSelectablePayments.some(p => selectedPayments[p.id]) && !allVisibleSelected
56+
57+
const onToggleSelectAll = (checked: boolean) => {
58+
if (checked) {
59+
const next: { [paymentId: string]: Winning } = {}
60+
visibleSelectablePayments.forEach(p => { next[p.id] = p })
61+
setSelectedPayments(next)
62+
props.onSelectionChange?.(next)
63+
} else {
64+
// deselect all visible selectable rows
65+
setSelectedPayments(prev => {
66+
const next = { ...prev }
67+
visibleSelectablePayments.forEach(p => { delete next[p.id] })
68+
props.onSelectionChange?.(next)
69+
return next
70+
})
71+
}
72+
}
73+
3474
return (
3575
<>
3676
<div className={styles.tableContainer}>
3777
<table>
3878
<thead>
3979
<tr>
80+
{props.enableBulkEdit && (
81+
<th>
82+
<input
83+
type='checkbox'
84+
aria-label='Select All'
85+
checked={allVisibleSelected}
86+
ref={el => { if (el) el.indeterminate = someVisibleSelected }}
87+
onChange={e => onToggleSelectAll(e.target.checked)}
88+
/>
89+
</th>
90+
)}
4091
<th className='body-ultra-small-bold'>HANDLE</th>
4192
<th className={`body-ultra-small-bold ${styles.description}`}>DESCRIPTION</th>
4293
<th className='body-ultra-small-bold'>CREATE DATE</th>
@@ -53,6 +104,17 @@ const PaymentsTable: React.FC<PaymentTableProps> = (props: PaymentTableProps) =>
53104
key={`${payment.id}`}
54105
className={selectedPayments[payment.id] ? 'selected' : ''}
55106
>
107+
{props.enableBulkEdit && (
108+
<td>
109+
<input
110+
type='checkbox'
111+
aria-label={`Select ${payment.handle}`}
112+
checked={!!selectedPayments[payment.id]}
113+
disabled={payment.status !== selectableStatus}
114+
onChange={e => onToggleRow(payment, e.target.checked)}
115+
/>
116+
</td>
117+
)}
56118
<td className='body-small-bold'>{payment.handle}</td>
57119
<td className='body-small'>{payment.description}</td>
58120
<td className='body-small-bold'>{payment.createDate}</td>

src/apps/wallet-admin/src/lib/services/wallet.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export async function exportSearchResults(filters: Record<string, string[]>): Pr
8888
winnerIds?: string[], [key: string]: string | number | string[] | undefined
8989
} = {
9090
...filteredFilters,
91+
type: 'PAYMENT',
9192
}
9293

9394
if (filters.winnerIds && filters.winnerIds.length > 0) {

src/libs/core/lib/profile/user-skill.model.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@ export type UserSkillWithActivity = {
5555
activity: {
5656
certification?: UserSkillActivity
5757
course?: UserSkillActivity
58-
challenge?: UserSkillActivity
58+
challenge?: {
59+
[key: string]: UserSkillActivity,
60+
}
5961
engagement?: UserSkillActivity
6062
}
6163
} & UserSkill

0 commit comments

Comments
 (0)