-
Notifications
You must be signed in to change notification settings - Fork 20
[PROD RELEASE] #1466
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[PROD RELEASE] #1466
Changes from all commits
39ed430
d14ba85
cabcb5b
1444613
6428dba
f8234a2
84f2d72
cb782f0
8a61315
986aaf3
e4e8602
e2b6979
638aaf8
ae6d2c9
b36fe8c
c136226
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -212,6 +212,7 @@ const createWorksPayloadData: any = (works: WorkInfo[]) => { | |
| const data: any = works.map(work => { | ||
| const { | ||
| companyName, | ||
| company, | ||
| position, | ||
| industry, | ||
| otherIndustry, | ||
|
|
@@ -222,10 +223,12 @@ const createWorksPayloadData: any = (works: WorkInfo[]) => { | |
| city, | ||
| associatedSkills, | ||
| }: any = work | ||
| const normalizedCompanyName: string = _.trim(companyName || company || '') | ||
| return { | ||
| associatedSkills: Array.isArray(associatedSkills) ? associatedSkills : undefined, | ||
| cityName: city, | ||
| companyName: companyName || '', | ||
| company: normalizedCompanyName || undefined, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| companyName: normalizedCompanyName, | ||
| description: description || undefined, | ||
| // eslint-disable-next-line unicorn/no-null | ||
| endDate: endDate ? endDate.toISOString() : null, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,7 +4,7 @@ import { toast } from 'react-toastify' | |
| import { AxiosError } from 'axios' | ||
| import React, { FC, useCallback, useEffect } from 'react' | ||
|
|
||
| import { Collapsible, ConfirmModal, LoadingCircles } from '~/libs/ui' | ||
| import { Collapsible, ConfirmModal, InputText, LoadingCircles } from '~/libs/ui' | ||
| import { UserProfile } from '~/libs/core' | ||
| import { downloadBlob } from '~/libs/shared' | ||
|
|
||
|
|
@@ -13,6 +13,7 @@ import { Winning, WinningDetail } from '../../../lib/models/WinningDetail' | |
| import { FilterBar, formatIOSDateString, PaymentView } from '../../../lib' | ||
| import { ConfirmFlowData } from '../../../lib/models/ConfirmFlowData' | ||
| import { PaginationInfo } from '../../../lib/models/PaginationInfo' | ||
| import { Filter } from '../../../lib/components/filter-bar/FilterBar' | ||
| import PaymentEditForm from '../../../lib/components/payment-edit/PaymentEdit' | ||
| import PaymentsTable from '../../../lib/components/payments-table/PaymentTable' | ||
|
|
||
|
|
@@ -69,6 +70,7 @@ const ListView: FC<ListViewProps> = (props: ListViewProps) => { | |
| const [isConfirmFormValid, setIsConfirmFormValid] = React.useState<boolean>(false) | ||
| const [winnings, setWinnings] = React.useState<ReadonlyArray<Winning>>([]) | ||
| const [selectedPayments, setSelectedPayments] = React.useState<{ [paymentId: string]: Winning }>({}) | ||
| const selectedPaymentsCount = Object.keys(selectedPayments).length | ||
| const [isLoading, setIsLoading] = React.useState<boolean>(false) | ||
| const [filters, setFilters] = React.useState<Record<string, string[]>>({}) | ||
| const [pagination, setPagination] = React.useState<PaginationInfo>({ | ||
|
|
@@ -290,6 +292,50 @@ const ListView: FC<ListViewProps> = (props: ListViewProps) => { | |
| || props.profile.roles.includes('Payment Editor') | ||
| ) | ||
|
|
||
| const isEngagementPaymentApprover = props.profile.roles.includes('Engagement Payment Approver') | ||
| const [bulkOpen, setBulkOpen] = React.useState(false) | ||
| const [bulkAuditNote, setBulkAuditNote] = React.useState('') | ||
|
|
||
| const onBulkApprove = useCallback(async (auditNote: string) => { | ||
| const ids = Object.keys(selectedPayments) | ||
| if (ids.length === 0) return | ||
|
|
||
| toast.success('Starting bulk approve', { position: toast.POSITION.BOTTOM_RIGHT }) | ||
|
|
||
| let successfullyUpdated = 0 | ||
| for (const id of ids) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| const updates: any = { | ||
| auditNote, | ||
| paymentStatus: 'OWED', | ||
| winningsId: id, | ||
| } | ||
|
|
||
| try { | ||
| // awaiting sequentially to preserve order and server load control | ||
| // errors for individual items are caught and reported | ||
| // eslint-disable-next-line no-await-in-loop | ||
| await editPayment(updates) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| successfullyUpdated += 1 | ||
| } catch (err:any) { | ||
| const paymentName = selectedPayments[id]?.handle || id | ||
| if (err?.message) { | ||
| toast.error(`Failed to update payment ${paymentName} (${id}): ${err.message}`, { position: toast.POSITION.BOTTOM_RIGHT }) | ||
| } else { | ||
| toast.error(`Failed to update payment ${paymentName} (${id})`, { position: toast.POSITION.BOTTOM_RIGHT }) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (successfullyUpdated === ids.length) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [💡 |
||
| toast.success(`Successfully updated ${successfullyUpdated} winnings`, { position: toast.POSITION.BOTTOM_RIGHT }) | ||
| } | ||
|
|
||
| setBulkAuditNote('') | ||
| setBulkOpen(false) | ||
| setSelectedPayments({}) | ||
| await fetchWinnings() | ||
| }, [selectedPayments, fetchWinnings]) | ||
|
|
||
| return ( | ||
| <> | ||
| <div className={styles.container}> | ||
|
|
@@ -300,6 +346,8 @@ const ListView: FC<ListViewProps> = (props: ListViewProps) => { | |
| <Collapsible header={<h3>Payment Listing</h3>}> | ||
| <FilterBar | ||
| showExportButton | ||
| selectedCount={selectedPaymentsCount} | ||
| onBulkClick={() => setBulkOpen(true)} | ||
| onExport={async () => { | ||
| toast.success('Downloading payments report. This may take a few moments.', { position: toast.POSITION.BOTTOM_RIGHT }) | ||
| downloadBlob( | ||
|
|
@@ -354,33 +402,35 @@ const ListView: FC<ListViewProps> = (props: ListViewProps) => { | |
| ], | ||
| type: 'dropdown', | ||
| }, | ||
| { | ||
| key: 'type', | ||
| label: 'Type', | ||
| options: [ | ||
| { | ||
| label: 'Task Payment', | ||
| value: 'TASK_PAYMENT', | ||
| }, | ||
| { | ||
| label: 'Contest Payment', | ||
| value: 'CONTEST_PAYMENT', | ||
| }, | ||
| { | ||
| label: 'Copilot Payment', | ||
| value: 'COPILOT_PAYMENT', | ||
| }, | ||
| { | ||
| label: 'Review Board Payment', | ||
| value: 'REVIEW_BOARD_PAYMENT', | ||
| }, | ||
| { | ||
| label: 'Engagement Payment', | ||
| value: 'ENGAGEMENT_PAYMENT', | ||
| }, | ||
| ], | ||
| type: 'dropdown', | ||
| }, | ||
| ...(isEngagementPaymentApprover ? [] : [ | ||
| { | ||
| key: 'category', | ||
| label: 'Type', | ||
| options: [ | ||
| { | ||
| label: 'Task Payment', | ||
| value: 'TASK_PAYMENT', | ||
| }, | ||
| { | ||
| label: 'Contest Payment', | ||
| value: 'CONTEST_PAYMENT', | ||
| }, | ||
| { | ||
| label: 'Copilot Payment', | ||
| value: 'COPILOT_PAYMENT', | ||
| }, | ||
| { | ||
| label: 'Review Board Payment', | ||
| value: 'REVIEW_BOARD_PAYMENT', | ||
| }, | ||
| { | ||
| label: 'Engagement Payment', | ||
| value: 'ENGAGEMENT_PAYMENT', | ||
| }, | ||
| ], | ||
| type: 'dropdown', | ||
| }, | ||
| ] as Filter[]), | ||
| { | ||
| key: 'date', | ||
| label: 'Date', | ||
|
|
@@ -449,11 +499,13 @@ const ListView: FC<ListViewProps> = (props: ListViewProps) => { | |
| {isLoading && <LoadingCircles className={styles.centered} />} | ||
| {!isLoading && winnings.length > 0 && ( | ||
| <PaymentsTable | ||
| enableBulkEdit={isEngagementPaymentApprover} | ||
| canEdit={isEditingAllowed()} | ||
| currentPage={pagination.currentPage} | ||
| numPages={pagination.totalPages} | ||
| payments={winnings} | ||
| selectedPayments={selectedPayments} | ||
| onSelectionChange={selected => setSelectedPayments(selected)} | ||
| onNextPageClick={async function onNextPageClicked() { | ||
| if (pagination.currentPage === pagination.totalPages) { | ||
| return | ||
|
|
@@ -513,6 +565,44 @@ const ListView: FC<ListViewProps> = (props: ListViewProps) => { | |
| </Collapsible> | ||
| </div> | ||
| </div> | ||
| {bulkOpen && ( | ||
| <ConfirmModal | ||
| maxWidth='800px' | ||
| size='lg' | ||
| showButtons | ||
| title={`${selectedPaymentsCount > 1 ? 'Bulk ' : ''}Approve Payment${selectedPaymentsCount > 1 ? 's' : ''}`} | ||
| action='Approve' | ||
| onClose={function onClose() { | ||
| setBulkAuditNote('') | ||
| setBulkOpen(false) | ||
| }} | ||
| onConfirm={function onConfirm() { | ||
| onBulkApprove(bulkAuditNote) | ||
| }} | ||
| canSave={bulkAuditNote.trim().length > 0} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [💡 |
||
| open={bulkOpen} | ||
| > | ||
| <div> | ||
| <p> | ||
| You are about to approve | ||
| {' '} | ||
| {selectedPaymentsCount} | ||
| {' '} | ||
| payment | ||
| {selectedPaymentsCount > 1 ? 's' : ''} | ||
| . | ||
| </p> | ||
| <br /> | ||
| <InputText | ||
| type='text' | ||
| label='Audit Note' | ||
| name='bulkAuditNote' | ||
| value={bulkAuditNote} | ||
| onChange={(e: React.ChangeEvent<HTMLInputElement>) => setBulkAuditNote(e.target.value)} | ||
| /> | ||
| </div> | ||
| </ConfirmModal> | ||
| )} | ||
| {confirmFlow && ( | ||
| <ConfirmModal | ||
| maxWidth='800px' | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,7 +14,7 @@ type FilterOptions = { | |
| value: string; | ||
| }; | ||
|
|
||
| type Filter = { | ||
| export type Filter = { | ||
| key: string; | ||
| label: string; | ||
| type: 'input' | 'dropdown' | 'member_autocomplete'; | ||
|
|
@@ -27,6 +27,8 @@ interface FilterBarProps { | |
| onFilterChange: (key: string, value: string[]) => void; | ||
| onResetFilters?: () => void; | ||
| onExport?: () => void; | ||
| selectedCount?: number; | ||
| onBulkClick?: () => void; | ||
| } | ||
|
|
||
| const FilterBar: React.FC<FilterBarProps> = (props: FilterBarProps) => { | ||
|
|
@@ -120,6 +122,17 @@ const FilterBar: React.FC<FilterBarProps> = (props: FilterBarProps) => { | |
| size='lg' | ||
| /> | ||
| )} | ||
| {!!props.selectedCount && props.selectedCount > 0 && ( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [💡 |
||
| <> | ||
| <Button | ||
| primary | ||
| className={styles.bulkButton} | ||
| label={`${props.selectedCount > 1 ? 'Bulk ' : ''}Approve (${props.selectedCount})`} | ||
| size='lg' | ||
| onClick={() => props.onBulkClick?.()} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| /> | ||
| </> | ||
| )} | ||
| <Button | ||
| primary | ||
| className={styles.resetButton} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,8 +10,10 @@ import { Winning } from '../../models/WinningDetail' | |
| import styles from './PaymentTable.module.scss' | ||
|
|
||
| interface PaymentTableProps { | ||
| enableBulkEdit?: boolean | ||
| payments: ReadonlyArray<Winning>; | ||
| selectedPayments?: { [paymentId: string]: Winning }; | ||
| onSelectionChange?: (selected: { [paymentId: string]: Winning }) => void; | ||
| currentPage: number; | ||
| numPages: number; | ||
| onPaymentEditClick: (payment: Winning) => void; | ||
|
|
@@ -31,12 +33,61 @@ const PaymentsTable: React.FC<PaymentTableProps> = (props: PaymentTableProps) => | |
| } | ||
| }, [props.selectedPayments]) | ||
|
|
||
| // Only rows with this status are selectable for bulk actions | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [💡 |
||
| const selectableStatus = 'On Hold (Admin)' | ||
|
|
||
| const onToggleRow = (payment: Winning, checked: boolean) => { | ||
| setSelectedPayments(prev => { | ||
| const next = { ...prev } | ||
| if (checked) { | ||
| next[payment.id] = payment | ||
| } else { | ||
| delete next[payment.id] | ||
| } | ||
|
|
||
| props.onSelectionChange?.(next) | ||
| return next | ||
| }) | ||
| } | ||
|
|
||
| const visibleSelectablePayments = props.payments.filter(p => p.status === selectableStatus) | ||
| const allVisibleSelected = visibleSelectablePayments.length > 0 && visibleSelectablePayments.every(p => selectedPayments[p.id]) | ||
| const someVisibleSelected = visibleSelectablePayments.some(p => selectedPayments[p.id]) && !allVisibleSelected | ||
|
|
||
| const onToggleSelectAll = (checked: boolean) => { | ||
| if (checked) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| const next: { [paymentId: string]: Winning } = {} | ||
| visibleSelectablePayments.forEach(p => { next[p.id] = p }) | ||
| setSelectedPayments(next) | ||
| props.onSelectionChange?.(next) | ||
| } else { | ||
| // deselect all visible selectable rows | ||
| setSelectedPayments(prev => { | ||
| const next = { ...prev } | ||
| visibleSelectablePayments.forEach(p => { delete next[p.id] }) | ||
| props.onSelectionChange?.(next) | ||
| return next | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| return ( | ||
| <> | ||
| <div className={styles.tableContainer}> | ||
| <table> | ||
| <thead> | ||
| <tr> | ||
| {props.enableBulkEdit && ( | ||
| <th> | ||
| <input | ||
| type='checkbox' | ||
| aria-label='Select All' | ||
| checked={allVisibleSelected} | ||
| ref={el => { if (el) el.indeterminate = someVisibleSelected }} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| onChange={e => onToggleSelectAll(e.target.checked)} | ||
| /> | ||
| </th> | ||
| )} | ||
| <th className='body-ultra-small-bold'>HANDLE</th> | ||
| <th className={`body-ultra-small-bold ${styles.description}`}>DESCRIPTION</th> | ||
| <th className='body-ultra-small-bold'>CREATE DATE</th> | ||
|
|
@@ -53,6 +104,17 @@ const PaymentsTable: React.FC<PaymentTableProps> = (props: PaymentTableProps) => | |
| key={`${payment.id}`} | ||
| className={selectedPayments[payment.id] ? 'selected' : ''} | ||
| > | ||
| {props.enableBulkEdit && ( | ||
| <td> | ||
| <input | ||
| type='checkbox' | ||
| aria-label={`Select ${payment.handle}`} | ||
| checked={!!selectedPayments[payment.id]} | ||
| disabled={payment.status !== selectableStatus} | ||
| onChange={e => onToggleRow(payment, e.target.checked)} | ||
| /> | ||
| </td> | ||
| )} | ||
| <td className='body-small-bold'>{payment.handle}</td> | ||
| <td className='body-small'>{payment.description}</td> | ||
| <td className='body-small-bold'>{payment.createDate}</td> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -55,7 +55,9 @@ export type UserSkillWithActivity = { | |
| activity: { | ||
| certification?: UserSkillActivity | ||
| course?: UserSkillActivity | ||
| challenge?: UserSkillActivity | ||
| challenge?: { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| [key: string]: UserSkillActivity, | ||
| } | ||
| engagement?: UserSkillActivity | ||
| } | ||
| } & UserSkill | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[⚠️
correctness]Using
_.trimoncompanyName || company || ''is safe, but consider handling cases where bothcompanyNameandcompanyare non-empty but different. This could lead to unexpected results if the data is inconsistent.