diff --git a/api/controller/contributions.ts b/api/controller/contributions.ts index 1d4aeb75f..6379dcf20 100644 --- a/api/controller/contributions.ts +++ b/api/controller/contributions.ts @@ -17,6 +17,7 @@ import { checkCurrentUser, IRequest } from '../routes/helpers'; import { Response } from 'express'; import { checkDto } from './helpers'; import { + Contribution, ContributionStatus, ContributionSubType, ContributionType, @@ -191,6 +192,35 @@ export async function updateContribution(request: IRequest, response: Response, } } +export async function bulkUpdateContributions(request: IRequest, response: Response, next: Function) { + try { + checkCurrentUser(request); + if (request.body.ids && request.body.ids.length > 0) { + const promises = request.body.ids.map(async (id: string) => { + const updatedContribution = request.body; + delete updatedContribution.ids; + const updateContributionDto = Object.assign(new UpdateContributionDto(), { + ...updatedContribution, + id + }); + await checkDto(updateContributionDto); + return await updateContributionAsync(updateContributionDto); + }); + const contributions = await Promise.all(promises); + const completedContributions = contributions.filter(contribution => (contribution as Contribution).status === request.body.status); + console.log(completedContributions, contributions); + return response.status(200).json({ + message: `${completedContributions.length} of ${contributions.length} successfully updated.` + }); + } + } catch (err) { + if (process.env.NODE_ENV === 'production' && err.message !== 'No token set') { + bugsnagClient.notify(err); + } + return response.status(422).json({ message: err.message }); + } +} + export class GetContributionsDto implements IGetContributionAttrs { @IsNumber() governmentId: number; diff --git a/api/routes/index.ts b/api/routes/index.ts index 2567fb93c..145b3a4ed 100644 --- a/api/routes/index.ts +++ b/api/routes/index.ts @@ -519,6 +519,52 @@ export const AppRoutes = [ action: contributions.updateContribution }, + /** + * @swagger + * /bulk-update-contributions: + * put: + * summary: Bulk update contributions + * tags: + * - Contributions + * security: + * - cookieAuth: [] + * produces: + * - application/json + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: array + * items: + * type: object + * properties: + * currentUserId: + * type: integer + * id: + * type: integer + * status: + * type: string + * responses: + * 200: + * description: Success response (X of X updated, X invalid) + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * 422: + * $ref: '#/components/responses/UnprocessableEntity' + * + */ + { + path: '/bulk-update-contributions', + method: 'put', + action: contributions.bulkUpdateContributions + }, + /** * @swagger * /contributions/{id}: diff --git a/app/src/Pages/Portal/Contributions/ContributionsTable/ContributionsTable.js b/app/src/Pages/Portal/Contributions/ContributionsTable/ContributionsTable.js index 7de7912da..4eca695a3 100644 --- a/app/src/Pages/Portal/Contributions/ContributionsTable/ContributionsTable.js +++ b/app/src/Pages/Portal/Contributions/ContributionsTable/ContributionsTable.js @@ -10,6 +10,7 @@ import FilterContribution from '../../../../components/Forms/FilterContributions import Table from '../../../../components/Table'; import Button from '../../../../components/Button/Button'; import { + bulkUpdateContributions, getContributions, getContributionsList, getFilterOptions, @@ -41,7 +42,7 @@ const buttonWrapper = css` const actionInfo = (name, buttonType, onClick, isFreeAction = undefined) => isFreeAction ? { icon: 'none', name, buttonType, onClick, isFreeAction } - : { icon: 'none', name, buttonType, onClick }; + : { icon: 'none', name, buttonType, onClick, position: 'row' }; const columns = isGovAdmin => { const cols = [ @@ -118,11 +119,28 @@ const columns = isGovAdmin => { class ContributionsTable extends React.Component { constructor(props) { super(props); + this.state = { + itemsToSubmit: null, + bulkSubmitted: false, + }; props.getContributions({ governmentId: props.govId, currentUserId: props.userId, campaignId: props.campaignId, }); + this.updateItemsToSubmit = this.updateItemsToSubmit.bind(this); + } + + updateItemsToSubmit(items) { + if (items.length > 0) { + this.setState({ + itemsToSubmit: items, + }); + } else { + this.setState({ + itemsToSubmit: null, + }); + } } render() { @@ -139,14 +157,20 @@ class ContributionsTable extends React.Component { userId, campaignId, isGovAdmin, + bulkSubmitContributions, } = this.props; const isLoading = isListLoading && !Array.isArray(contributionList); const actions = [ - actionInfo('View', 'submit', (event, rowData) => { - history.push(`/contributions/${rowData.id}`); - }), + actionInfo( + 'View', + 'submit', + (event, rowData) => { + history.push(`/contributions/${rowData.id}`); + }, + false + ), ]; const components = { @@ -253,6 +277,14 @@ class ContributionsTable extends React.Component { options={{ pageSize: filterOptions.perPage || 50, showTitle: false, + actionsColumnIndex: -1, + selection: true, + selectionProps: rowData => { + return { + disabled: rowData.status === 'Submitted', + color: 'primary', + }; + }, }} actions={actions} components={components} @@ -275,18 +307,50 @@ class ContributionsTable extends React.Component { pageNumber={filterOptions.page || 0} totalRows={total} onChangePage={handleOnChangePage} - // eslint-disable-next-line no-use-before-define onChangeRowsPerPage={handleOnRowsPerPageChange} toolbarAction={ !isGovAdmin ? ( - + <> + + {this.state.itemsToSubmit && ( + + )} + {this.state.bulkSubmitted && ( + + )} + ) : null } + onSelectionChange={items => this.updateItemsToSubmit(items)} /> ); @@ -316,6 +380,7 @@ export default connect( showModal: payload => { dispatch(showModal(payload)); }, + bulkSubmitContributions: data => dispatch(bulkUpdateContributions(data)), }; } )(ContributionsTable); diff --git a/app/src/Pages/Portal/Contributions/Utils/ContributionsFields.js b/app/src/Pages/Portal/Contributions/Utils/ContributionsFields.js index 99bf31db2..09e76db6d 100644 --- a/app/src/Pages/Portal/Contributions/Utils/ContributionsFields.js +++ b/app/src/Pages/Portal/Contributions/Utils/ContributionsFields.js @@ -530,9 +530,6 @@ export const validate = values => { checkNumber, occupation, occupationLetterDate, - employerName, - employerCity, - employerState, subTypeOfContribution, inKindType, lastName, diff --git a/app/src/api/api.js b/app/src/api/api.js index 5415c5ad4..0588f42b1 100644 --- a/app/src/api/api.js +++ b/app/src/api/api.js @@ -603,6 +603,12 @@ export function updateContribution(contributionAttrs) { ); } +// path: '/bulk-update-contributions' +// method: 'put', +export function bulkUpdateContributions(contributionAttrs) { + return put(`${baseUrl()}/bulk-update-contributions`, contributionAttrs); +} + // path: '/contributions/new', // method: 'post', export function createContribution(contributionAttrs) { diff --git a/app/src/api/api.test.js b/app/src/api/api.test.js index c53b32ee5..7bef7df03 100644 --- a/app/src/api/api.test.js +++ b/app/src/api/api.test.js @@ -559,4 +559,36 @@ describe('API', () => { ); expect(response.status).toEqual(204); }); + it('bulkUpdateContributions', async () => { + process.env.TOKEN = campaignAdminToken; + + let response = await api.createContribution({ + address1: '123 ABC ST', + amount: 250, + campaignId, + city: 'Portland', + currentUserId: campaignAdminId, + date: 1562436237619, + firstName: 'John', + middleInitial: '', + lastName: 'Doe', + governmentId, + type: api.ContributionTypeEnum.CONTRIBUTION, + subType: api.ContributionSubTypeEnum.CASH, + paymentMethod: api.PaymentMethodEnum.CASH, + state: 'OR', + status: api.ContributionStatusEnum.DRAFT, + zip: '97214', + contributorType: api.ContributorTypeEnum.INDIVIDUAL, + }); + const contribution = await response.json(); + console.log('added it', contribution); + response = await api.bulkUpdateContributions({ + ids: [contribution.id], + status: 'Submitted', + currentUserId: campaignStaffId, + }); + console.log('bulk response', response); + expect(response.status).toEqual(200); + }); }); diff --git a/app/src/state/ducks/contributions.js b/app/src/state/ducks/contributions.js index 872bed9ba..ee6c05df3 100644 --- a/app/src/state/ducks/contributions.js +++ b/app/src/state/ducks/contributions.js @@ -302,6 +302,44 @@ export function updateContribution(contributionAttrs) { }; } +export function bulkUpdateContributions(contributionAttrsArray) { + return async (dispatch, getState, { api, schema }) => { + dispatch(actionCreators.updateContribution.request()); + try { + const state = getState(); + const currentUserId = state.auth.me.id; + const ids = contributionAttrsArray.map(contribution => contribution.id); + const bulkSubmitInfo = { + currentUserId, + ids, + status: 'Submitted', + }; + const response = await api.bulkUpdateContributions(bulkSubmitInfo); + const res = await response.json(); + if (response.status === 200) { + dispatch(actionCreators.updateContribution.success()); + if (res.message) { + dispatch( + flashMessage(`${res.message}`, { + props: { variant: 'success' }, + }) + ); + } + } else { + dispatch(actionCreators.updateContribution.failure()); + dispatch( + flashMessage(`Error - ${res}`, { props: { variant: 'error' } }) + ); + } + } catch (error) { + dispatch(actionCreators.updateContribution.failure(error)); + dispatch( + flashMessage(`Error - ${error}`, { props: { variant: 'error' } }) + ); + } + }; +} + export function getContributions(contributionSearchAttrs, applyFilter = false) { return async (dispatch, getState, { api, schema }) => { dispatch(actionCreators.getContributions.request()); diff --git a/app/src/state/ducks/contributions.test.js b/app/src/state/ducks/contributions.test.js index 3bf9d165a..08e5a2a2e 100644 --- a/app/src/state/ducks/contributions.test.js +++ b/app/src/state/ducks/contributions.test.js @@ -360,4 +360,50 @@ describe('Side Effects', () => { expect(actions[2].type).toEqual(expectedActions[2].type); }); }); + + it('bulk Update Contributions', async () => { + const expectedActions = [ + { type: actionTypes.UPDATE_CONTRIBUTION.REQUEST }, + { type: actionTypes.UPDATE_CONTRIBUTION.SUCCESS }, + ]; + const store = mockStore({ + auth: { + me: { + id: 2, + }, + }, + }); + + process.env.TOKEN = campaignAdminToken; + + const contribution = await api.createContribution({ + address1: '123 ABC ST', + amount: 250, + campaignId, + city: 'Portland', + currentUserId: campaignAdminId, + date: 1562436237619, + firstName: 'John', + middleInitial: '', + lastName: 'Doe', + governmentId, + type: api.ContributionTypeEnum.CONTRIBUTION, + subType: api.ContributionSubTypeEnum.CASH, + paymentMethod: api.PaymentMethodEnum.CASH, + state: 'OR', + status: api.ContributionStatusEnum.DRAFT, + zip: '97214', + contributorType: api.ContributorTypeEnum.INDIVIDUAL, + }); + const { id } = await contribution.json(); + + return store + .dispatch(contributions.bulkUpdateContributions([{ id }])) + .then(() => { + const actions = store.getActions(); + console.log({ actions }); + expect(actions[0].type).toEqual(expectedActions[0].type); + expect(actions[1].type).toEqual(expectedActions[1].type); + }); + }); }); diff --git a/app/test/recordings/http%3A__localhost%3A3000_bulk-update-contributions_PUT_1179124204_body.raw b/app/test/recordings/http%3A__localhost%3A3000_bulk-update-contributions_PUT_1179124204_body.raw new file mode 100644 index 000000000..bf6b4af77 --- /dev/null +++ b/app/test/recordings/http%3A__localhost%3A3000_bulk-update-contributions_PUT_1179124204_body.raw @@ -0,0 +1 @@ +{"message":"1 of 1 successfully updated."} \ No newline at end of file diff --git a/app/test/recordings/http%3A__localhost%3A3000_bulk-update-contributions_PUT_1179124204_options.json b/app/test/recordings/http%3A__localhost%3A3000_bulk-update-contributions_PUT_1179124204_options.json new file mode 100644 index 000000000..339e1f2d7 --- /dev/null +++ b/app/test/recordings/http%3A__localhost%3A3000_bulk-update-contributions_PUT_1179124204_options.json @@ -0,0 +1 @@ +{"url":"http://localhost:3000/bulk-update-contributions","status":200,"statusText":"OK","ok":true,"headers":{"x-powered-by":["Express"],"vary":["Origin"],"access-control-allow-credentials":["true"],"content-type":["application/json; charset=utf-8"],"content-length":["42"],"etag":["W/\"2a-LIcWy6ni0+o5ijinQ/GzC9gLQko\""],"date":["Mon, 18 Jan 2021 05:30:20 GMT"],"connection":["close"]}} \ No newline at end of file diff --git a/app/test/recordings/http%3A__localhost%3A3000_bulk-update-contributions_PUT_217525293_body.raw b/app/test/recordings/http%3A__localhost%3A3000_bulk-update-contributions_PUT_217525293_body.raw new file mode 100644 index 000000000..bf6b4af77 --- /dev/null +++ b/app/test/recordings/http%3A__localhost%3A3000_bulk-update-contributions_PUT_217525293_body.raw @@ -0,0 +1 @@ +{"message":"1 of 1 successfully updated."} \ No newline at end of file diff --git a/app/test/recordings/http%3A__localhost%3A3000_bulk-update-contributions_PUT_217525293_options.json b/app/test/recordings/http%3A__localhost%3A3000_bulk-update-contributions_PUT_217525293_options.json new file mode 100644 index 000000000..c09c56094 --- /dev/null +++ b/app/test/recordings/http%3A__localhost%3A3000_bulk-update-contributions_PUT_217525293_options.json @@ -0,0 +1 @@ +{"url":"http://localhost:3000/bulk-update-contributions","status":200,"statusText":"OK","ok":true,"headers":{"x-powered-by":["Express"],"vary":["Origin"],"access-control-allow-credentials":["true"],"content-type":["application/json; charset=utf-8"],"content-length":["42"],"etag":["W/\"2a-LIcWy6ni0+o5ijinQ/GzC9gLQko\""],"date":["Mon, 18 Jan 2021 05:30:19 GMT"],"connection":["close"]}} \ No newline at end of file