@@ -4,7 +4,7 @@ import { toast } from 'react-toastify'
44import { AxiosError } from 'axios'
55import React , { FC , useCallback , useEffect } from 'react'
66
7- import { Collapsible , ConfirmModal , LoadingCircles } from '~/libs/ui'
7+ import { Collapsible , ConfirmModal , InputText , LoadingCircles } from '~/libs/ui'
88import { UserProfile } from '~/libs/core'
99import { downloadBlob } from '~/libs/shared'
1010
@@ -13,6 +13,7 @@ import { Winning, WinningDetail } from '../../../lib/models/WinningDetail'
1313import { FilterBar , formatIOSDateString , PaymentView } from '../../../lib'
1414import { ConfirmFlowData } from '../../../lib/models/ConfirmFlowData'
1515import { PaginationInfo } from '../../../lib/models/PaginationInfo'
16+ import { Filter } from '../../../lib/components/filter-bar/FilterBar'
1617import PaymentEditForm from '../../../lib/components/payment-edit/PaymentEdit'
1718import 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'
0 commit comments