Skip to content
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

Bulk submit expenditures for approval #1150

Open
wants to merge 4 commits into
base: chore/gh-1132
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion api/controller/expenditures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import {
ExpenditureType,
ExpenditureSubType,
PayeeType,
PurposeType
PurposeType,
Expenditure
} from '../models/entity/Expenditure';
import { PaymentMethod } from '../models/entity/Expenditure';
import { bugsnagClient } from '../services/bugsnagService';
Expand Down Expand Up @@ -284,6 +285,36 @@ export async function updateExpenditure(request: IRequest, response: Response, n
}
}

export async function bulkUpdateExpenditures(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 updatedExpenditure = request.body;
delete updatedExpenditure.ids;
const updateExpenditureDto = Object.assign(new UpdateExpenditureDto(), {
...updatedExpenditure,
id
});
await checkDto(updateExpenditureDto);
return await updateExpenditureAsync(updateExpenditureDto);
});
const expenditures = await Promise.all(promises);
const completedExpenditures = expenditures.filter(contribution => (contribution as Expenditure).status === request.body.status);
return response.status(200).json({
message: `${completedExpenditures.length} of ${expenditures.length} successfully updated.`
});
} else {
return response.status(422).json({message: 'No expenditures submitted' });
}
} 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 ExpenditureCommentDto {

currentUserId: number;
Expand Down
64 changes: 55 additions & 9 deletions api/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -535,16 +535,16 @@ export const AppRoutes = [
* content:
* application/json:
* schema:
* type: array
* items:
* type: object
* properties:
* currentUserId:
* type: integer
* id:
* type: integer
* status:
* type: object
* properties:
* currentUserId:
* type: integer
* ids:
* type: array
* items:
* type: string
* status:
* type: string
* responses:
* 200:
* description: Success response (X of X updated, X invalid)
Expand Down Expand Up @@ -914,6 +914,52 @@ export const AppRoutes = [
method: 'put',
action: expenditures.updateExpenditure
},

/**
* @swagger
* /bulk-update-expenditures:
* put:
* summary: Bulk update expenditures
* tags:
* - Expenditures
* security:
* - cookieAuth: []
* produces:
* - application/json
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* currentUserId:
* type: integer
* ids:
* type: array
* items:
* type: string
* 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-expenditures',
method: 'put',
action: expenditures.bulkUpdateExpenditures
},
/**
* @swagger
* /expenditures/{id}:
Expand Down
87 changes: 75 additions & 12 deletions app/src/Pages/Portal/Expenses/ExpensesTable/ExpensesTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import PageHoc from '../../../../components/PageHoc/PageHoc';
import Table from '../../../../components/Table';
import Button from '../../../../components/Button/Button';
import {
bulkUpdateExpenditures,
getExpenditures,
getExpendituresList,
getExpendituresTotal,
Expand Down Expand Up @@ -37,7 +38,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 => [
{
Expand Down Expand Up @@ -109,11 +110,28 @@ const columns = isGovAdmin => [
class ExpensesTable extends React.Component {
constructor(props) {
super(props);
this.state = {
itemsToSubmit: null,
bulkSubmitted: false,
};
props.getExpenditures({
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() {
Expand All @@ -130,14 +148,20 @@ class ExpensesTable extends React.Component {
userId,
campaignId,
isGovAdmin,
bulkSubmitExpenditures,
} = this.props;

const isLoading = isListLoading && !Array.isArray(expendituresList);

const actions = [
actionInfo('View', 'submit', (event, rowData) => {
history.push(`/expenses/${rowData.id}`);
}),
actionInfo(
'View',
'submit',
(event, rowData) => {
history.push(`/expenses/${rowData.id}`);
},
false
),
];

const components = {
Expand Down Expand Up @@ -242,6 +266,14 @@ class ExpensesTable 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}
Expand All @@ -264,20 +296,49 @@ class ExpensesTable extends React.Component {
perPage={filterOptions.perPage}
pageNumber={filterOptions.page}
totalRows={total}
// eslint-disable-next-line no-use-before-define
onChangePage={handleOnChangePage}
// eslint-disable-next-line no-use-before-define
onChangeRowsPerPage={handleOnRowsPerPageChange}
toolbarAction={
!isGovAdmin ? (
<Button
buttonType="green"
onClick={() => history.push({ pathname: '/expenses/new' })}
>
Add New Expense
</Button>
<>
<Button
buttonType="green"
onClick={() => history.push({ pathname: '/expenses/new' })}
>
Add New Expense
</Button>
{this.state.itemsToSubmit && (
<Button
buttonType="green"
onClick={() => {
bulkSubmitExpenditures(this.state.itemsToSubmit);
this.setState({
itemsToSubmit: null,
bulkSubmitted: true,
});
}}
>
Bulk submit
andrewbiang888 marked this conversation as resolved.
Show resolved Hide resolved
</Button>
)}
{this.state.bulkSubmitted && (
<Button
buttonType="green"
onClick={() => {
fetchList();
this.setState({
itemsToSubmit: null,
bulkSubmitted: false,
});
}}
>
Refresh
</Button>
)}
</>
) : null
}
onSelectionChange={items => this.updateItemsToSubmit(items)}
/>
</PageHoc>
);
Expand Down Expand Up @@ -308,6 +369,7 @@ export default connect(
showModal: payload => {
dispatch(showModal(payload));
},
bulkSubmitExpenditures: data => dispatch(bulkUpdateExpenditures(data)),
};
}
)(ExpensesTable);
Expand All @@ -324,4 +386,5 @@ ExpensesTable.propTypes = {
userId: PropTypes.number,
campaignId: PropTypes.number,
isGovAdmin: PropTypes.bool,
bulkSubmitExpenditures: PropTypes.func,
};
6 changes: 6 additions & 0 deletions app/src/api/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,12 @@ export function updateExpenditure(expenditureAttrs) {
);
}

// path: '/bulk-update-expenditures'
// method: 'put',
export function bulkUpdateExpenditures(expenditureAttrsArray) {
return put(`${baseUrl()}/bulk-update-expenditures`, expenditureAttrsArray);
}

// path: '/summary'
// method: 'post',
// summaryArttrs = {governmentId: integer OR campaignId: integer}
Expand Down
32 changes: 32 additions & 0 deletions app/src/api/api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,38 @@ describe('API', () => {
expect(response.status).toEqual(204);
});

it('bulkUpdateExpenditure', async () => {
process.env.TOKEN = campaignStaffToken;

let response = await api.createExpenditure({
address1: '123 ABC ST',
amount: 250,
campaignId,
city: 'Portland',
currentUserId: campaignStaffId,
date: 1562436237700,
governmentId,
type: api.ExpenditureTypeEnum.EXPENDITURE,
subType: api.ExpenditureSubTypeEnum.CASH_EXPENDITURE,
state: 'OR',
status: api.ExpenditureStatusEnum.DRAFT,
zip: '97214',
payeeType: api.PayeeTypeEnum.INDIVIDUAL,
name: 'Test Expenditure',
description: 'This is an update test',
});
const expenditure = await response.json();

response = await api.bulkUpdateExpenditures([
{
ids: [expenditure.id],
status: 'submitted',
currentUserId: campaignStaffId,
},
]);
expect(response.status).toEqual(200);
});

it('getExpenditureActivities', async () => {
process.env.TOKEN = campaignStaffToken;

Expand Down
39 changes: 39 additions & 0 deletions app/src/state/ducks/expenditures.js
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,45 @@ export function updateExpenditure(expenditureAttrs) {
};
}

export function bulkUpdateExpenditures(expenditureAttrsArray) {
return async (dispatch, getState, { api, schema }) => {
dispatch(actionCreators.updateExpenditure.request());
try {
const state = getState();
const currentUserId = state.auth.me.id;
const ids = expenditureAttrsArray.map(expenditure => expenditure.id);
const bulkSubmitInfo = {
currentUserId,
ids,
status: 'submitted',
};
const response = await api.bulkUpdateExpenditures(bulkSubmitInfo);
const res = await response.json();
if (response.status === 200) {
dispatch(actionCreators.updateExpenditure.success());
if (res.message) {
dispatch(
flashMessage(`${res.message}`, {
props: { variant: 'success' },
})
);
}
} else {
dispatch(actionCreators.updateExpenditure.failure());
dispatch(
flashMessage(`Error - ${res}`, { props: { variant: 'error' } })
);
}
} catch (error) {
dispatch(actionCreators.updateExpenditure.failure(error));
dispatch(
flashMessage(`Error - ${error}`, { props: { variant: 'error' } })
);
return error;
}
};
}

export function getExpenditures(expenditureSearchAttrs, applyFilter = false) {
return async (dispatch, getState, { api, schema }) => {
dispatch(actionCreators.getExpenditures.request());
Expand Down
Loading