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