WIP feature/expenses

This commit is contained in:
elforjani3
2020-06-11 20:41:18 +02:00
parent 55a4319827
commit 45d9199dbb
27 changed files with 2761 additions and 616 deletions

View File

@@ -1,246 +1,246 @@
import React, { useState } from 'react';
import * as Yup from 'yup';
import { useFormik } from 'formik';
import {
FormGroup,
MenuItem,
Intent,
InputGroup,
Position,
Button,
TextArea,
ControlGroup
} from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime';
import { Select } from '@blueprintjs/select';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { momentFormatter } from 'utils';
import moment from 'moment';
import AppToaster from 'components/AppToaster';
// import React, { useState } from 'react';
// import * as Yup from 'yup';
// import { useFormik } from 'formik';
// import {
// FormGroup,
// MenuItem,
// Intent,
// InputGroup,
// Position,
// Button,
// TextArea,
// ControlGroup
// } from '@blueprintjs/core';
// import { DateInput } from '@blueprintjs/datetime';
// import { Select } from '@blueprintjs/select';
// import { FormattedMessage as T, useIntl } from 'react-intl';
// import { momentFormatter } from 'utils';
// import moment from 'moment';
// import AppToaster from 'components/AppToaster';
export default function ExpenseForm({
accounts,
editExpense,
submitExpense,
expenseDetails,
currencies
}) {
const {formatMessage} = useIntl();
// export default function ExpenseForm({
// accounts,
// editExpense,
// submitExpense,
// expenseDetails,
// currencies
// }) {
// const {formatMessage} = useIntl();
const [state, setState] = useState({
selectedExpenseAccount: null,
selectedPaymentAccount: null
});
const validationSchema = Yup.object().shape({
date: Yup.date().required().label(formatMessage({id:'date'})),
description: Yup.string().trim().label(formatMessage({id:'description'})),
expense_account_id: Yup.number().required().label(formatMessage({id:'expense_account_id'})),
payment_account_id: Yup.number().required().label(formatMessage({id:'payment_account_id'})),
amount: Yup.number().required().label(formatMessage({id:'amount'})),
currency_code: Yup.string().required().label(formatMessage({id:'currency_code_'})),
publish: Yup.boolean().label(formatMessage({id:'publish'})),
exchange_rate: Yup.number().label(formatMessage({id:'exchange_rate_'}))
});
// const [state, setState] = useState({
// selectedExpenseAccount: null,
// selectedPaymentAccount: null
// });
// const validationSchema = Yup.object().shape({
// date: Yup.date().required().label(formatMessage({id:'date'})),
// description: Yup.string().trim().label(formatMessage({id:'description'})),
// expense_account_id: Yup.number().required().label(formatMessage({id:'expense_account_id'})),
// payment_account_id: Yup.number().required().label(formatMessage({id:'payment_account_id'})),
// amount: Yup.number().required().label(formatMessage({id:'amount'})),
// currency_code: Yup.string().required().label(formatMessage({id:'currency_code_'})),
// publish: Yup.boolean().label(formatMessage({id:'publish'})),
// exchange_rate: Yup.number().label(formatMessage({id:'exchange_rate_'}))
// });
const formik = useFormik({
enableReinitialize: true,
validationSchema: validationSchema,
initialValues: {
date: null
},
onSubmit: values => {
submitExpense(values)
.then(response => {
AppToaster.show({
message: formatMessage({id:'the_expense_has_been_successfully_created'})
});
})
.catch(error => {});
}
});
// const formik = useFormik({
// enableReinitialize: true,
// validationSchema: validationSchema,
// initialValues: {
// date: null
// },
// onSubmit: values => {
// submitExpense(values)
// .then(response => {
// AppToaster.show({
// message: formatMessage({id:'the_expense_has_been_successfully_created'})
// });
// })
// .catch(error => {});
// }
// });
// Account item of select accounts field.
const accountItem = (item, { handleClick, modifiers, query }) => {
return (
<MenuItem
text={item.name}
label={item.code}
key={item.id}
onClick={handleClick}
/>
);
};
// // Account item of select accounts field.
// const accountItem = (item, { handleClick, modifiers, query }) => {
// return (
// <MenuItem
// text={item.name}
// label={item.code}
// key={item.id}
// onClick={handleClick}
// />
// );
// };
const onChangeAccount = () => {};
// const onChangeAccount = () => {};
const onChangePaymentAccount = () => {};
// const onChangePaymentAccount = () => {};
const handleDateChange = date => {
const formatted = moment(date).format('YYYY/MM/DD');
formik.setFieldValue('date', formatted);
};
// const handleDateChange = date => {
// const formatted = moment(date).format('YYYY/MM/DD');
// formik.setFieldValue('date', formatted);
// };
// Filters accounts items.
const filterAccountsPredicater = (query, account, _index, exactMatch) => {
const normalizedTitle = account.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
// // Filters accounts items.
// const filterAccountsPredicater = (query, account, _index, exactMatch) => {
// const normalizedTitle = account.name.toLowerCase();
// const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return `${account.code} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
}
};
// if (exactMatch) {
// return normalizedTitle === normalizedQuery;
// } else {
// return `${account.code} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
// }
// };
const onExpenseAccountSelect = account => {
setState({ ...state, selectedExpenseAccount: account });
formik.setFieldValue('expense_account_id', account.id);
};
// const onExpenseAccountSelect = account => {
// setState({ ...state, selectedExpenseAccount: account });
// formik.setFieldValue('expense_account_id', account.id);
// };
const onChangePaymentAccountSelect = account => {
setState({ ...state, selectedPaymentAccount: account });
formik.setFieldValue('payment_account_id', account.id);
};
// const onChangePaymentAccountSelect = account => {
// setState({ ...state, selectedPaymentAccount: account });
// formik.setFieldValue('payment_account_id', account.id);
// };
const onAmountCurrencySelect = currency => {
formik.setFieldValue('currency_code', currency.id);
};
// const onAmountCurrencySelect = currency => {
// formik.setFieldValue('currency_code', currency.id);
// };
const paymentAccountLabel = state.selectedPaymentAccount
? state.selectedPaymentAccount.name
: <T id={'select_payment_account'}/>;
// const paymentAccountLabel = state.selectedPaymentAccount
// ? state.selectedPaymentAccount.name
// : <T id={'select_payment_account'}/>;
const expenseAccountLabel = state.selectedExpenseAccount
? state.selectedExpenseAccount.name
: <T id={'select_expense_account'}/>;
// const expenseAccountLabel = state.selectedExpenseAccount
// ? state.selectedExpenseAccount.name
// : <T id={'select_expense_account'}/>;
const handleClose = () => {};
// const handleClose = () => {};
return (
<div class='expense-form'>
<form onSubmit={formik.handleSubmit}>
<FormGroup
label={<T id={'date'}/>}
inline={true}
intent={formik.errors.date && Intent.DANGER}
helperText={formik.errors.date && formik.errors.date}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
defaultValue={new Date()}
onChange={handleDateChange}
popoverProps={{ position: Position.BOTTOM }}
/>
</FormGroup>
// return (
// <div class='expense-form'>
// <form onSubmit={formik.handleSubmit}>
// <FormGroup
// label={<T id={'date'}/>}
// inline={true}
// intent={formik.errors.date && Intent.DANGER}
// helperText={formik.errors.date && formik.errors.date}
// >
// <DateInput
// {...momentFormatter('YYYY/MM/DD')}
// defaultValue={new Date()}
// onChange={handleDateChange}
// popoverProps={{ position: Position.BOTTOM }}
// />
// </FormGroup>
<FormGroup
label={<T id={'expense_account'}/>}
className={'form-group--expense-account'}
inline={true}
intent={formik.errors.expense_account_id && Intent.DANGER}
helperText={
formik.errors.expense_account_id && formik.errors.expense_account_id
}
>
<Select
items={accounts}
itemRenderer={accountItem}
itemPredicate={filterAccountsPredicater}
popoverProps={{ minimal: true }}
onItemSelect={onExpenseAccountSelect}
>
<Button
fill={true}
rightIcon='caret-down'
text={expenseAccountLabel}
/>
</Select>
</FormGroup>
// <FormGroup
// label={<T id={'expense_account'}/>}
// className={'form-group--expense-account'}
// inline={true}
// intent={formik.errors.expense_account_id && Intent.DANGER}
// helperText={
// formik.errors.expense_account_id && formik.errors.expense_account_id
// }
// >
// <Select
// items={accounts}
// itemRenderer={accountItem}
// itemPredicate={filterAccountsPredicater}
// popoverProps={{ minimal: true }}
// onItemSelect={onExpenseAccountSelect}
// >
// <Button
// fill={true}
// rightIcon='caret-down'
// text={expenseAccountLabel}
// />
// </Select>
// </FormGroup>
<FormGroup
label={<T id={'amount'}/>}
className={'form-group--amount'}
intent={formik.errors.amount && Intent.DANGER}
helperText={formik.errors.amount && formik.errors.amount}
inline={true}
>
<ControlGroup>
<Select
items={currencies.map(c => ({
id: c.currency_code,
name: c.currency_code
}))}
itemRenderer={accountItem}
itemPredicate={filterAccountsPredicater}
popoverProps={{ minimal: true }}
onItemSelect={onAmountCurrencySelect}
>
<Button
rightIcon='caret-down'
text={formik.values.currency_code}
/>
</Select>
// <FormGroup
// label={<T id={'amount'}/>}
// className={'form-group--amount'}
// intent={formik.errors.amount && Intent.DANGER}
// helperText={formik.errors.amount && formik.errors.amount}
// inline={true}
// >
// <ControlGroup>
// <Select
// items={currencies.map(c => ({
// id: c.currency_code,
// name: c.currency_code
// }))}
// itemRenderer={accountItem}
// itemPredicate={filterAccountsPredicater}
// popoverProps={{ minimal: true }}
// onItemSelect={onAmountCurrencySelect}
// >
// <Button
// rightIcon='caret-down'
// text={formik.values.currency_code}
// />
// </Select>
<InputGroup
medium={true}
intent={formik.errors.amount && Intent.DANGER}
{...formik.getFieldProps('amount')}
/>
</ControlGroup>
</FormGroup>
// <InputGroup
// medium={true}
// intent={formik.errors.amount && Intent.DANGER}
// {...formik.getFieldProps('amount')}
// />
// </ControlGroup>
// </FormGroup>
<FormGroup
label={<T id={'exchange_rate'}/>}
className={'form-group--exchange-rate'}
inline={true}
>
<InputGroup />
</FormGroup>
// <FormGroup
// label={<T id={'exchange_rate'}/>}
// className={'form-group--exchange-rate'}
// inline={true}
// >
// <InputGroup />
// </FormGroup>
<FormGroup
label={<T id={'payment_account'}/>}
className={'form-group--payment-account'}
inline={true}
intent={formik.errors.payment_account_id && Intent.DANGER}
helperText={
formik.errors.payment_account_id && formik.errors.payment_account_id
}
>
<Select
items={accounts}
itemRenderer={accountItem}
itemPredicate={filterAccountsPredicater}
popoverProps={{ minimal: true }}
onItemSelect={onChangePaymentAccountSelect}
>
<Button
fill={true}
rightIcon='caret-down'
text={paymentAccountLabel}
/>
</Select>
</FormGroup>
// <FormGroup
// label={<T id={'payment_account'}/>}
// className={'form-group--payment-account'}
// inline={true}
// intent={formik.errors.payment_account_id && Intent.DANGER}
// helperText={
// formik.errors.payment_account_id && formik.errors.payment_account_id
// }
// >
// <Select
// items={accounts}
// itemRenderer={accountItem}
// itemPredicate={filterAccountsPredicater}
// popoverProps={{ minimal: true }}
// onItemSelect={onChangePaymentAccountSelect}
// >
// <Button
// fill={true}
// rightIcon='caret-down'
// text={paymentAccountLabel}
// />
// </Select>
// </FormGroup>
<FormGroup
label={<T id={'description'}/>}
className={'form-group--description'}
inline={true}
>
<TextArea
growVertically={true}
large={true}
{...formik.getFieldProps('description')}
/>
</FormGroup>
// <FormGroup
// label={<T id={'description'}/>}
// className={'form-group--description'}
// inline={true}
// >
// <TextArea
// growVertically={true}
// large={true}
// {...formik.getFieldProps('description')}
// />
// </FormGroup>
<div class='form__floating-footer'>
<Button intent={Intent.PRIMARY} type='submit'>
<T id={'save'}/>
</Button>
<Button><T id={'save_as_draft'}/></Button>
<Button onClick={handleClose}><T id={'close'}/></Button>
</div>
</form>
</div>
);
}
// <div class='form__floating-footer'>
// <Button intent={Intent.PRIMARY} type='submit'>
// <T id={'save'}/>
// </Button>
// <Button><T id={'save_as_draft'}/></Button>
// <Button onClick={handleClose}><T id={'close'}/></Button>
// </div>
// </form>
// </div>
// );
// }

View File

@@ -1,82 +1,82 @@
import React from 'react';
import {
Button,
AnchorButton,
Classes,
NavbarGroup,
Popover,
MenuItem,
PopoverInteractionKind,
Position,
Menu,
NavbarDivider,
Intent,
} from '@blueprintjs/core';
import { useRouteMatch } from 'react-router-dom'
import { FormattedMessage as T } from 'react-intl';
import classNames from 'classnames';
// import React from 'react';
// import {
// Button,
// AnchorButton,
// Classes,
// NavbarGroup,
// Popover,
// MenuItem,
// PopoverInteractionKind,
// Position,
// Menu,
// NavbarDivider,
// Intent,
// } from '@blueprintjs/core';
// import { useRouteMatch } from 'react-router-dom'
// import { FormattedMessage as T } from 'react-intl';
// import classNames from 'classnames';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import Icon from 'components/Icon';
// import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
// import Icon from 'components/Icon';
export default function ExpensesActionsBar() {
const {path} = useRouteMatch();
const onClickNewAccount = () => {};
const views = [];
// export default function ExpensesActionsBar() {
// const {path} = useRouteMatch();
// const onClickNewAccount = () => {};
// const views = [];
const viewsMenuItems = views.map((view) => {
return (<MenuItem href={`${path}/${view.id}/custom_view`} text={view.name} />);
});
return (
<DashboardActionsBar>
<NavbarGroup>
<Popover
content={<Menu>{viewsMenuItems}</Menu>}
minimal={true}
interactionKind={PopoverInteractionKind.HOVER}
position={Position.BOTTOM_LEFT}
>
<Button
className={classNames(Classes.MINIMAL, 'button--table-views')}
icon={<Icon icon='table' />}
text={<T id={'table_views'}/>}
rightIcon={'caret-down'}
/>
</Popover>
// const viewsMenuItems = views.map((view) => {
// return (<MenuItem href={`${path}/${view.id}/custom_view`} text={view.name} />);
// });
// return (
// <DashboardActionsBar>
// <NavbarGroup>
// <Popover
// content={<Menu>{viewsMenuItems}</Menu>}
// minimal={true}
// interactionKind={PopoverInteractionKind.HOVER}
// position={Position.BOTTOM_LEFT}
// >
// <Button
// className={classNames(Classes.MINIMAL, 'button--table-views')}
// icon={<Icon icon='table' />}
// text={<T id={'table_views'}/>}
// rightIcon={'caret-down'}
// />
// </Popover>
<NavbarDivider />
// <NavbarDivider />
<AnchorButton
className={Classes.MINIMAL}
icon={<Icon icon='plus' />}
href='/expenses/new'
text={<T id={'new_expense'}/>}
onClick={onClickNewAccount}
/>
<Button
className={Classes.MINIMAL}
intent={Intent.DANGER}
icon={<Icon icon='plus' />}
text={<T id={'delete'}/>}
onClick={onClickNewAccount}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon='plus' />}
text={<T id={'bulk_update'}/>}
onClick={onClickNewAccount}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon='file-import' />}
text={<T id={'import'}/>}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon='file-export' />}
text={<T id={'export'}/>}
/>
</NavbarGroup>
</DashboardActionsBar>
);
}
// <AnchorButton
// className={Classes.MINIMAL}
// icon={<Icon icon='plus' />}
// href='/expenses/new'
// text={<T id={'new_expense'}/>}
// onClick={onClickNewAccount}
// />
// <Button
// className={Classes.MINIMAL}
// intent={Intent.DANGER}
// icon={<Icon icon='plus' />}
// text={<T id={'delete'}/>}
// onClick={onClickNewAccount}
// />
// <Button
// className={Classes.MINIMAL}
// icon={<Icon icon='plus' />}
// text={<T id={'bulk_update'}/>}
// onClick={onClickNewAccount}
// />
// <Button
// className={Classes.MINIMAL}
// icon={<Icon icon='file-import' />}
// text={<T id={'import'}/>}
// />
// <Button
// className={Classes.MINIMAL}
// icon={<Icon icon='file-export' />}
// text={<T id={'export'}/>}
// />
// </NavbarGroup>
// </DashboardActionsBar>
// );
// }

View File

@@ -1,92 +1,92 @@
import React from 'react';
import {
GridComponent,
ColumnsDirective,
ColumnDirective,
Inject,
Sort,
// import React from 'react';
// import {
// GridComponent,
// ColumnsDirective,
// ColumnDirective,
// Inject,
// Sort,
} from '@syncfusion/ej2-react-grids';
import {
Checkbox,
Popover,
Button,
Menu,
MenuItem,
MenuDivider,
Position,
} from '@blueprintjs/core';
import Icon from 'components/Icon';
import moment from 'moment';
// } from '@syncfusion/ej2-react-grids';
// import {
// Checkbox,
// Popover,
// Button,
// Menu,
// MenuItem,
// MenuDivider,
// Position,
// } from '@blueprintjs/core';
// import Icon from 'components/Icon';
// import moment from 'moment';
export default function ExpensesTable({
expenses,
onDeleteExpense,
onEditExpense,
}) {
const onDateStateChange = () => {
// export default function ExpensesTable({
// expenses,
// onDeleteExpense,
// onEditExpense,
// }) {
// const onDateStateChange = () => {
}
// }
const actionMenuList = (expense) => (
<Menu>
<MenuItem text="View Details" />
<MenuDivider />
<MenuItem text="Edit Expense" onClick={() => onEditExpense(expense)} />
<MenuItem text="Delete Expense" onClick={() => onDeleteExpense(expense)} />
</Menu>
);
const columns = [
{
headerText: '',
template: () => (<Checkbox />)
},
{
headerText: 'Date',
template: (row) => (<span>{ moment(row.date).format('YYYY/MM/DD') }</span>),
},
{
headerText: 'Expense Account',
template: (row) => (<span>{ row.expenseAccount.name }</span>),
},
{
headerText: 'Paid Through',
template: (row) => (<span>{ row.paymentAccount.name }</span>),
},
{
headerText: 'Amount',
field: 'amount'
},
{
headerText: 'Status',
},
{
headerText: '',
template: (expense) => (
<Popover content={actionMenuList(expense)} position={Position.RIGHT_BOTTOM}>
<Button icon={<Icon icon="ellipsis-h" />} />
</Popover>
)
}
]
return (
<GridComponent
allowSorting={true}
dataSource={{ result: expenses, count: 20 }}
dataStateChange={onDateStateChange}>
// const actionMenuList = (expense) => (
// <Menu>
// <MenuItem text="View Details" />
// <MenuDivider />
// <MenuItem text="Edit Expense" onClick={() => onEditExpense(expense)} />
// <MenuItem text="Delete Expense" onClick={() => onDeleteExpense(expense)} />
// </Menu>
// );
// const columns = [
// {
// headerText: '',
// template: () => (<Checkbox />)
// },
// {
// headerText: 'Date',
// template: (row) => (<span>{ moment(row.date).format('YYYY/MM/DD') }</span>),
// },
// {
// headerText: 'Expense Account',
// template: (row) => (<span>{ row.expenseAccount.name }</span>),
// },
// {
// headerText: 'Paid Through',
// template: (row) => (<span>{ row.paymentAccount.name }</span>),
// },
// {
// headerText: 'Amount',
// field: 'amount'
// },
// {
// headerText: 'Status',
// },
// {
// headerText: '',
// template: (expense) => (
// <Popover content={actionMenuList(expense)} position={Position.RIGHT_BOTTOM}>
// <Button icon={<Icon icon="ellipsis-h" />} />
// </Popover>
// )
// }
// ]
// return (
// <GridComponent
// allowSorting={true}
// dataSource={{ result: expenses, count: 20 }}
// dataStateChange={onDateStateChange}>
<ColumnsDirective>
{columns.map((column) => {
return (<ColumnDirective
field={column.field}
headerText={column.headerText}
template={column.template}
allowSorting={true}
customAttributes={column.customAttributes}
/>);
})}
</ColumnsDirective>
<Inject services={[Sort]} />
</GridComponent>
);
}
// <ColumnsDirective>
// {columns.map((column) => {
// return (<ColumnDirective
// field={column.field}
// headerText={column.headerText}
// template={column.template}
// allowSorting={true}
// customAttributes={column.customAttributes}
// />);
// })}
// </ColumnsDirective>
// <Inject services={[Sort]} />
// </GridComponent>
// );
// }

View File

@@ -1,51 +1,51 @@
import React from 'react';
import {useHistory} from 'react-router';
import {connect} from 'react-redux';
import {
Alignment,
Navbar,
NavbarGroup,
Tabs,
Tab,
Button
} from "@blueprintjs/core";
import Icon from 'components/Icon';
import {useRouteMatch, Link} from 'react-router-dom';
// import React from 'react';
// import {useHistory} from 'react-router';
// import {connect} from 'react-redux';
// import {
// Alignment,
// Navbar,
// NavbarGroup,
// Tabs,
// Tab,
// Button
// } from "@blueprintjs/core";
// import Icon from 'components/Icon';
// import {useRouteMatch, Link} from 'react-router-dom';
function AccountsViewsTabs({ views }) {
const history = useHistory();
const {path} = useRouteMatch();
// function AccountsViewsTabs({ views }) {
// const history = useHistory();
// const {path} = useRouteMatch();
const handleClickNewView = () => {
history.push('/custom_views/new');
};
// const handleClickNewView = () => {
// history.push('/custom_views/new');
// };
const tabs = views.map((view) => {
const link = (<Link to={`${path}/${view.id}/custom_view`}>{ view.name }</Link>);
return (<Tab id={`custom_view_${view.id}`} title={link} />);
});
return (
<Navbar className="navbar--dashboard-views">
<NavbarGroup
align={Alignment.LEFT}>
<Tabs
id="navbar"
large={true}
className="tabs--dashboard-views"
>
{ tabs }
<Button
className="button--new-view"
icon={<Icon icon="plus" />}
onClick={handleClickNewView} />
</Tabs>
</NavbarGroup>
</Navbar>
);
}
// const tabs = views.map((view) => {
// const link = (<Link to={`${path}/${view.id}/custom_view`}>{ view.name }</Link>);
// return (<Tab id={`custom_view_${view.id}`} title={link} />);
// });
// return (
// <Navbar className="navbar--dashboard-views">
// <NavbarGroup
// align={Alignment.LEFT}>
// <Tabs
// id="navbar"
// large={true}
// className="tabs--dashboard-views"
// >
// { tabs }
// <Button
// className="button--new-view"
// icon={<Icon icon="plus" />}
// onClick={handleClickNewView} />
// </Tabs>
// </NavbarGroup>
// </Navbar>
// );
// }
const mapStateToProps = (state) => ({
views: state.views.resourceViews['expenses'],
});
// const mapStateToProps = (state) => ({
// views: state.views.resourceViews['expenses'],
// });
export default connect(mapStateToProps)(AccountsViewsTabs);
// export default connect(mapStateToProps)(AccountsViewsTabs);

View File

@@ -119,11 +119,11 @@ export default [
children: [
{
text: <T id={'expenses'}/>,
href: '/expenses',
href: '/expenses/new',
},
{
text: <T id={'new_expenses'}/>,
href: '/expenses/new',
text: <T id={'expenses_list'}/>,
href: '/expenses-list',
},
],
},

View File

@@ -327,7 +327,7 @@ const handleConfirmBulkActivate = useCallback(() => {
onEditAccount={handleEditAccount}
onFetchData={handleFetchData}
onSelectedRowsChange={handleSelectedRowsChange}
loading={tableLoading}
/>
</Route>
</Switch>

View File

@@ -9,7 +9,8 @@ import {
fetchAccount,
deleteBulkAccounts,
bulkActivateAccounts,
bulkInactiveAccounts
bulkInactiveAccounts,
editAccount
} from 'store/accounts/accounts.actions';
const mapActionsToProps = (dispatch) => ({
@@ -23,6 +24,7 @@ const mapActionsToProps = (dispatch) => ({
requestDeleteBulkAccounts: (ids) => dispatch(deleteBulkAccounts({ ids })),
requestBulkActivateAccounts:(ids)=>dispatch(bulkActivateAccounts({ids})),
requestBulkInactiveAccounts:(ids)=>dispatch(bulkInactiveAccounts({ids})),
requestEditAccount:({id,form}) => dispatch(editAccount({id,form}))
});
export default connect(null, mapActionsToProps);

View File

@@ -14,7 +14,7 @@ import * as Yup from 'yup';
import { useFormik } from 'formik';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { omit } from 'lodash';
import { omit, pick } from 'lodash';
import { useQuery, queryCache } from 'react-query';
import Dialog from 'components/Dialog';
@@ -27,7 +27,6 @@ import Icon from 'components/Icon';
import ErrorMessage from 'components/ErrorMessage';
import { ListSelect } from 'components';
function AccountFormDialog({
name,
payload,
@@ -105,29 +104,39 @@ function AccountFormDialog({
if (payload.action === 'edit') {
requestEditAccount({
payload: payload.id,
form: { ...omit(values, [...exclude, 'account_type_id']) },
}).then((response) => {
closeDialog(name);
queryCache.refetchQueries('accounts-table', { force: true });
// form: { ...omit(values, [...exclude, 'account_type_id']) },
form: {
...pick(values, [
...exclude,
'account_type_id',
'name',
'description',
]),
},
})
.then((response) => {
closeDialog(name);
queryCache.refetchQueries('accounts-table', { force: true });
AppToaster.show({
message: formatMessage(
{ id: 'service_has_been_successful_edited', },
{
name: toastAccountName,
service: formatMessage({ id: 'account' }),
},
),
intent: Intent.SUCCESS,
AppToaster.show({
message: formatMessage(
{ id: 'service_has_been_successful_edited' },
{
name: toastAccountName,
service: formatMessage({ id: 'account' }),
},
),
intent: Intent.SUCCESS,
});
})
.catch((errors) => {
const errorsTransformed = transformApiErrors(errors);
setErrors({ ...errorsTransformed });
setSubmitting(false);
});
}).catch((errors) => {
const errorsTransformed = transformApiErrors(errors);
setErrors({ ...errorsTransformed });
setSubmitting(false);
});
} else {
requestSubmitAccount({ form: { ...omit(values, exclude) } }).then(
(response) => {
requestSubmitAccount({ form: { ...omit(values, exclude) } })
.then((response) => {
closeDialog(name);
queryCache.refetchQueries('accounts-table', { force: true });
@@ -142,20 +151,24 @@ function AccountFormDialog({
intent: Intent.SUCCESS,
position: Position.BOTTOM,
});
},
).catch((errors) => {
const errorsTransformed = transformApiErrors(errors);
setErrors({ ...errorsTransformed });
setSubmitting(false);
});
})
.catch((errors) => {
const errorsTransformed = transformApiErrors(errors);
setErrors({ ...errorsTransformed });
setSubmitting(false);
});
}
},
});
// Filtered accounts based on the given account type.
const filteredAccounts = useMemo(() => accounts.filter((account) =>
account.account_type_id === values.account_type_id
), [accounts, values.account_type_id]);
const filteredAccounts = useMemo(
() =>
accounts.filter(
(account) => account.account_type_id === values.account_type_id,
),
[accounts, values.account_type_id],
);
// Filters accounts types items.
const filterAccountTypeItems = (query, accountType, _index, exactMatch) => {
@@ -307,7 +320,9 @@ function AccountFormDialog({
Classes.FILL,
)}
inline={true}
helperText={<ErrorMessage name="account_type_id" {...{ errors, touched }} />}
helperText={
<ErrorMessage name="account_type_id" {...{ errors, touched }} />
}
intent={
errors.account_type_id && touched.account_type_id && Intent.DANGER
}

View File

@@ -0,0 +1,145 @@
import React, { useMemo, useCallback } from 'react';
import Icon from 'components/Icon';
import {
Button,
NavbarGroup,
Classes,
NavbarDivider,
MenuItem,
Menu,
Popover,
PopoverInteractionKind,
Position,
Intent,
} from '@blueprintjs/core';
import classNames from 'classnames';
import { useRouteMatch, useHistory } from 'react-router-dom';
import { FormattedMessage as T } from 'react-intl';
import FilterDropdown from 'components/FilterDropdown';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { If } from 'components';
import withResourceDetail from 'containers/Resources/withResourceDetails';
import withExpenses from 'containers/Expenses/withExpenses';
import withExpensesActions from 'containers/Expenses/withExpensesActions';
import { compose } from 'utils';
function ExpenseActionsBar({
// #withResourceDetail
resourceFields,
//#withExpenses
expensesViews,
//#withExpensesActions
addExpensesTableQueries,
onFilterChanged,
selectedRows,
onBulkDelete,
}) {
const { path } = useRouteMatch();
const history = useHistory();
const viewsMenuItems = expensesViews.map((view) => {
return (
<MenuItem href={`${path}/${view.id}/custom_view`} text={view.name} />
);
});
const onClickNewExpense = useCallback(() => {
history.push('/expenses/new');
}, [history]);
const filterDropdown = FilterDropdown({
fields: resourceFields,
onFilterChange: (filterConditions) => {
addExpensesTableQueries({
filter_roles: filterConditions || '',
});
onFilterChanged && onFilterChanged(filterConditions);
},
});
const hasSelectedRows = useMemo(() => selectedRows.length > 0, [
selectedRows,
]);
// Handle delete button click.
const handleBulkDelete = useCallback(() => {
onBulkDelete && onBulkDelete(selectedRows.map((r) => r.id));
}, [onBulkDelete, selectedRows]);
return (
<DashboardActionsBar>
<NavbarGroup>
<Popover
content={<Menu>{viewsMenuItems}</Menu>}
minimal={true}
interactionKind={PopoverInteractionKind.HOVER}
position={Position.BOTTOM_LEFT}
>
<Button
className={classNames(Classes.MINIMAL, 'button--table-views')}
icon={<Icon icon="table-16" iconSize={16} />}
text={<T id={'table_views'} />}
rightIcon={'caret-down'}
/>
</Popover>
<NavbarDivider />
<Button
className={Classes.MINIMAL}
icon={<Icon icon="plus" />}
text={<T id={'new_expense'} />}
onClick={onClickNewExpense}
/>
<Popover
content={filterDropdown}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
className={classNames(Classes.MINIMAL, 'button--filter')}
text="Filter"
icon={<Icon icon="filter-16" iconSize={16} />}
/>
</Popover>
<If condition={hasSelectedRows}>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="trash-16" iconSize={16} />}
text={<T id={'delete'} />}
intent={Intent.DANGER}
onClick={handleBulkDelete}
/>
</If>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-import-16" iconSize={16} />}
text={<T id={'import'} />}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'} />}
/>
</NavbarGroup>
</DashboardActionsBar>
);
}
export default compose(
withDialogActions,
withResourceDetail(({ resourceFields }) => ({
resourceFields,
})),
withExpenses(({ expensesViews }) => ({
expensesViews,
})),
withExpensesActions,
)(ExpenseActionsBar);

View File

@@ -0,0 +1,261 @@
import React, { useEffect, useCallback, useState, useMemo } from 'react';
import {
Intent,
Button,
Classes,
Popover,
Tooltip,
Menu,
MenuItem,
MenuDivider,
Position,
Tag,
} from '@blueprintjs/core';
import { useParams } from 'react-router-dom';
import { FormattedMessage as T, useIntl } from 'react-intl';
import moment from 'moment';
import Icon from 'components/Icon';
import { compose } from 'utils';
import { useUpdateEffect } from 'hooks';
import LoadingIndicator from 'components/LoadingIndicator';
import { If, Money } from 'components';
import DataTable from 'components/DataTable';
import withDialogActions from 'containers/Dialog/withDialogActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withViewDetails from 'containers/Views/withViewDetails';
import withExpenses from 'containers/Expenses/withExpenses';
import withExpensesActions from 'containers/Expenses/withExpensesActions';
function ExpenseDataTable({
loading,
//#withExpenes
expenses,
expensesLoading,
// #withDashboardActions
changeCurrentView,
changePageSubtitle,
setTopbarEditView,
viewMeta,
onFetchData,
onEditExpense,
onDeleteExpense,
onPublishExpense,
onSelectedRowsChange,
}) {
const { custom_view_id: customViewId } = useParams();
const [initialMount, setInitialMount] = useState(false);
const { formatMessage } = useIntl();
useUpdateEffect(() => {
if (!expensesLoading) {
setInitialMount(true);
}
}, [expensesLoading, setInitialMount]);
useEffect(() => {
if (customViewId) {
changeCurrentView(customViewId);
setTopbarEditView(customViewId);
}
changePageSubtitle(customViewId && viewMeta ? viewMeta.name : '');
}, [
customViewId,
changeCurrentView,
changePageSubtitle,
setTopbarEditView,
viewMeta,
]);
const handlePublishExpense = useCallback(
(expense) => () => {
onPublishExpense && onPublishExpense(expense);
},
[onPublishExpense],
);
const handleEditExpense = useCallback(
(expense) => () => {
onEditExpense && onEditExpense(expense);
},
[onEditExpense],
);
const handleDeleteExpense = useCallback(
(expense) => () => {
onDeleteExpense && onDeleteExpense(expense);
},
[onDeleteExpense],
);
const actionMenuList = useCallback(
(expense) => (
<Menu>
<MenuItem text={<T id={'view_details'} />} />
<MenuDivider />
<If condition={expenses.published}>
<MenuItem
text={<T id={'publish_expense'} />}
onClick={handlePublishExpense(expense)}
/>
</If>
<MenuItem
text={<T id={'edit_expense'} />}
onClick={handleEditExpense(expense)}
/>
<MenuItem
text={<T id={'delete_expense'} />}
intent={Intent.DANGER}
onClick={handleDeleteExpense(expense)}
/>
</Menu>
),
[handleEditExpense, handleDeleteExpense, handlePublishExpense],
);
console.log(Object.values(expenses), 'ER');
const columns = useMemo(
() => [
{
id: 'payment_date',
Header: formatMessage({ id: 'payment_date' }),
accessor: () => moment().format('YYYY-MM-DD'),
width: 150,
className: 'payment_date',
},
{
id: 'beneficiary',
Header: formatMessage({ id: 'beneficiary' }),
// accessor: 'beneficiary',
width: 150,
className: 'beneficiary',
},
{
id: 'total_amount',
Header: formatMessage({ id: 'full_amount' }),
accessor: (r) => <Money amount={r.total_amount} currency={'USD'} />,
disableResizing: true,
className: 'total_amount',
width: 150,
},
{
id: 'payment_account_id',
Header: formatMessage({ id: 'payment_account' }),
accessor: 'payment_account.name',
className: 'payment_account',
width: 150,
},
{
id: 'expense_account_id',
Header: formatMessage({ id: 'expense_account' }),
accessor:'expense_account_id',
width: 150,
className: 'expense_account',
},
{
id: 'publish',
Header: formatMessage({ id: 'publish' }),
accessor: (r) => {
return !r.published ? (
<Tag minimal={true}>
<T id={'published'} />
</Tag>
) : (
<Tag minimal={true} intent={Intent.WARNING}>
<T id={'draft'} />
</Tag>
);
},
disableResizing: true,
width: 100,
className: 'publish',
},
{
id: 'description',
Header: formatMessage({ id: 'description' }),
accessor: (row) => (
<If condition={row.description}>
<Tooltip
className={Classes.TOOLTIP_INDICATOR}
content={row.description}
position={Position.TOP}
hoverOpenDelay={250}
>
<Icon icon={'file-alt'} iconSize={16} />
</Tooltip>
</If>
),
disableSorting: true,
width: 150,
className: 'description',
},
{
id: 'actions',
Header: '',
Cell: ({ cell }) => (
<Popover
content={actionMenuList(cell.row.original)}
position={Position.RIGHT_BOTTOM}
>
<Button icon={<Icon icon="more-h-16" iconSize={16} />} />
</Popover>
),
className: 'actions',
width: 50,
},
],
[actionMenuList, formatMessage],
);
const handleDataTableFetchData = useCallback(
(...args) => {
onFetchData && onFetchData(...args);
},
[onFetchData],
);
const handleSelectedRowsChange = useCallback(
(selectedRows) => {
onSelectedRowsChange &&
onSelectedRowsChange(selectedRows.map((s) => s.original));
},
[onSelectedRowsChange],
);
return (
<div>
<LoadingIndicator loading={loading} mount={false}>
<DataTable
columns={columns}
data={expenses}
onFetchData={handleDataTableFetchData}
manualSortBy={true}
selectionColumn={true}
noInitialFetch={true}
sticky={true}
loading={expensesLoading && !initialMount}
onSelectedRowsChange={handleSelectedRowsChange}
/>
</LoadingIndicator>
</div>
);
}
export default compose(
withDialogActions,
withDashboardActions,
withExpensesActions,
withExpenses(({ expenses, expensesLoading }) => ({
expenses,
expensesLoading,
})),
withViewDetails,
)(ExpenseDataTable);

View File

@@ -0,0 +1,45 @@
import React from 'react';
import { Intent, Button } from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
function ExpenseFooter({
formik: { isSubmitting },
onSubmitClick,
onCancelClick,
}) {
return (
<div className={'form__floating-footer'}>
<Button
disabled={isSubmitting}
intent={Intent.PRIMARY}
className={'ml1'}
name={'save'}
onClick={() => {
onSubmitClick({ publish: true, redirect: true });
}}
>
<T id={'save'} />
</Button>
<Button
disabled={isSubmitting}
className={'button-secondary ml1'}
onClick={() => {
onSubmitClick({ publish: true, redirect: false });
}}
>
<T id={'save_as_draft'} />
</Button>
<Button
className={'button-secondary ml1'}
onClick={() => {
onCancelClick && onCancelClick({ publish: false, redirect: false });
}}
>
<T id={'cancel'} />
</Button>
</div>
);
}
export default ExpenseFooter;

View File

@@ -1,41 +1,329 @@
import React, {useEffect} from 'react';
import { useAsync } from 'react-use';
import {useParams} from 'react-router-dom';
import Connector from 'connectors/ExpenseForm.connector';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import ExpenseForm from 'components/Expenses/ExpenseForm';
import { useIntl } from 'react-intl';
import React, {
useMemo,
useState,
useEffect,
useRef,
useCallback,
} from 'react';
import * as Yup from 'yup';
import { useFormik } from 'formik';
import moment from 'moment';
import { Intent, FormGroup, TextArea } from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { pick } from 'lodash';
import { useQuery } from 'react-query';
function ExpenseFormContainer({
fetchAccounts,
fetchCurrencies,
accounts,
import ExpenseFormHeader from './ExpenseFormHeader';
import ExpenseTable from './ExpenseTable';
import ExpenseFooter from './ExpenseFooter';
import withExpensesActions from 'containers/Expenses/withExpensesActions';
import withExpneseDetail from 'containers/Expenses/withExpenseDetail';
import withAccountsActions from 'containers/Accounts/withAccountsActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withMediaActions from 'containers/Media/withMediaActions';
import AppToaster from 'components/AppToaster';
import Dragzone from 'components/Dragzone';
import useMedia from 'hooks/useMedia';
import { compose } from 'utils';
function ExpenseForm({
// #withMedia
requestSubmitMedia,
requestDeleteMedia,
//#withExpensesActions
requestSubmitExpense,
requestEditExpense,
requestFetchExpensesTable,
// #withDashboard
changePageTitle,
submitExpense,
editExpense,
currencies,
}) {
const { id } = useParams();
const { formatMessage } = useIntl();
useEffect(() => {
if (id) {
changePageTitle(formatMessage({id:'edit_expense_details'}));
} else {
changePageTitle(formatMessage({id:'new_expense'}));
}
}, [id,changePageTitle,formatMessage]);
changePageSubtitle,
const fetchHook = useAsync(async () => {
await Promise.all([
fetchAccounts(),
fetchCurrencies(),
]);
//#withExpenseDetail
expenseDetail,
// #own Props
expenseId,
onFormSubmit,
onCancelForm,
}) {
const { formatMessage } = useIntl();
const [payload, setPayload] = useState({});
const {
setFiles,
saveMedia,
deletedFiles,
setDeletedFiles,
deleteMedia,
} = useMedia({
saveCallback: requestSubmitMedia,
deleteCallback: requestDeleteMedia,
});
const handleDropFiles = useCallback((_files) => {
setFiles(_files.filter((file) => file.uploaded === false));
}, []);
const savedMediaIds = useRef([]);
const clearSavedMediaIds = () => {
savedMediaIds.current = [];
};
useEffect(() => {
if (expenseDetail && expenseDetail.id) {
changePageTitle(formatMessage({ id: 'edit_expense' }));
changePageSubtitle(`No. ${expenseDetail.payment_account_id}`);
} else {
changePageTitle(formatMessage({ id: 'new_expense' }));
}
}, [changePageTitle, changePageSubtitle, expenseDetail, formatMessage]);
const validationSchema = Yup.object().shape({
beneficiary: Yup.string()
// .required()
.label(formatMessage({ id: 'beneficiary' })),
payment_account_id: Yup.string()
.required()
.label(formatMessage({ id: 'payment_account_' })),
payment_date: Yup.date()
.required()
.label(formatMessage({ id: 'payment_date_' })),
reference_no: Yup.string(),
currency_code: Yup.string().label(formatMessage({ id: 'currency_code' })),
description: Yup.string()
.trim()
.label(formatMessage({ id: 'description' })),
publish: Yup.boolean().label(formatMessage({ id: 'publish' })),
categories: Yup.array().of(
Yup.object().shape({
index: Yup.number().nullable(),
amount: Yup.number().nullable(),
expense_account_id: Yup.number().nullable(),
description: Yup.string().nullable(),
}),
),
});
const saveInvokeSubmit = useCallback(
(payload) => {
onFormSubmit && onFormSubmit(payload);
},
[onFormSubmit],
);
const defaultCategory = useMemo(
() => ({
index: 0,
amount: 0,
expense_account_id: null,
description: '',
}),
[],
);
const defaultInitialValues = useMemo(
() => ({
payment_account_id: '',
beneficiary: '',
payment_date: moment(new Date()).format('YYYY-MM-DD'),
description: '',
reference_no: '',
currency_code: '',
categories: [
defaultCategory,
defaultCategory,
defaultCategory,
defaultCategory,
],
}),
[defaultCategory],
);
const initialValues = useMemo(
() => ({
...(expenseDetail
? {
...pick(expenseDetail, Object.keys(defaultInitialValues)),
categories: expenseDetail.categories.map((category) => ({
...pick(category, Object.keys(defaultCategory)),
})),
}
: {
...defaultInitialValues,
}),
}),
[expenseDetail, defaultInitialValues, defaultCategory],
);
const initialAttachmentFiles = useMemo(() => {
return expenseDetail && expenseDetail.media
? expenseDetail.media.map((attach) => ({
preview: attach.attachment_file,
uploaded: true,
metadata: { ...attach },
}))
: [];
}, [expenseDetail]);
const formik = useFormik({
enableReinitialize: true,
validationSchema,
initialValues: {
...initialValues,
},
onSubmit: async (values, { setSubmitting, setErrors, resetForm }) => {
const categories = values.categories.filter(
(category) => category.amount || category.index,
);
const form = {
...values,
published: payload.publish,
categories,
};
const saveExpense = (mdeiaIds) =>
new Promise((resolve, reject) => {
const requestForm = { ...form, media_ids: mdeiaIds };
if (expenseDetail && expenseDetail.id) {
requestEditExpense(expenseDetail.id, requestForm)
.then((response) => {
AppToaster.show({
message: formatMessage(
{ id: 'the_expense_has_been_successfully_edited' },
{ number: values.payment_account_id },
),
intent: Intent.SUCCESS,
});
setSubmitting(false);
saveInvokeSubmit({ action: 'update', ...payload });
clearSavedMediaIds([]);
resetForm();
resolve(response);
})
.catch((errors) => {
if (errors.find((e) => e.type === 'TOTAL.AMOUNT.EQUALS.ZERO')) {
}
setErrors(
AppToaster.show({
message: formatMessage({
id: 'total_amount_equals_zero',
}),
intent: Intent.DANGER,
}),
);
setSubmitting(false);
});
} else {
requestSubmitExpense(requestForm)
.then((response) => {
AppToaster.show({
message: formatMessage(
{ id: 'the_expense_has_been_successfully_created' },
{ number: values.payment_account_id },
),
intent: Intent.SUCCESS,
});
setSubmitting(false);
saveInvokeSubmit({ action: 'new', ...payload });
clearSavedMediaIds();
resetForm();
resolve(response);
})
.catch((errors) => {
setSubmitting(false);
});
}
});
Promise.all([saveMedia(), deleteMedia()])
.then(([savedMediaResponses]) => {
const mediaIds = savedMediaResponses.map((res) => res.data.media.id);
savedMediaIds.current = mediaIds;
return savedMediaResponses;
})
.then(() => {
return saveExpense(savedMediaIds.current);
});
},
});
const handleSubmitClick = useCallback(
(payload) => {
setPayload(payload);
formik.handleSubmit();
},
[setPayload, formik],
);
const handleCancelClick = useCallback(
(payload) => {
onCancelForm && onCancelForm(payload);
},
[onCancelForm],
);
const handleDeleteFile = useCallback(
(_deletedFiles) => {
_deletedFiles.forEach((deletedFile) => {
if (deletedFile.uploaded && deletedFile.metadata.id) {
setDeletedFiles([...deletedFiles, deletedFile.metadata.id]);
}
});
},
[setDeletedFiles, deletedFiles],
);
const fetchHook = useQuery('expense-form', () => requestFetchExpensesTable());
return (
<DashboardInsider isLoading={fetchHook.loading} name={'expense-form'}>
<ExpenseForm {...{submitExpense, editExpense, accounts, currencies} } />
</DashboardInsider>
<div className={'dashboard__insider--expense-form'}>
<form onSubmit={formik.handleSubmit}>
<ExpenseFormHeader formik={formik} />
<ExpenseTable
initialValues={initialValues}
formik={formik}
defaultRow={defaultCategory}
/>
<FormGroup
label={<T id={'description'} />}
className={'form-group--description'}
>
<TextArea
growVertically={true}
large={true}
{...formik.getFieldProps('description')}
/>
</FormGroup>
<ExpenseFooter
formik={formik}
onSubmitClick={handleSubmitClick}
onCancelClick={handleCancelClick}
/>
</form>
<Dragzone
initialFiles={initialAttachmentFiles}
onDrop={handleDropFiles}
onDeleteFile={handleDeleteFile}
hint={'Attachments: Maxiumum size: 20MB'}
/>
</div>
);
}
export default Connector(ExpenseFormContainer);
export default compose(
withExpensesActions,
withAccountsActions,
withDashboardActions,
withMediaActions,
withExpneseDetail,
)(ExpenseForm);

View File

@@ -0,0 +1,262 @@
import React, { useMemo, useCallback, useState } from 'react';
import {
InputGroup,
FormGroup,
Intent,
Position,
MenuItem,
Classes,
} from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime';
import { FormattedMessage as T } from 'react-intl';
import { Row, Col } from 'react-grid-system';
import moment from 'moment';
import { momentFormatter, compose } from 'utils';
import classNames from 'classnames';
import Icon from 'components/Icon';
import ErrorMessage from 'components/ErrorMessage';
import { ListSelect } from 'components';
import withCurrencies from 'containers/Currencies/withCurrencies';
import withAccounts from 'containers/Accounts/withAccounts';
function ExpenseFormHeader({
formik: { errors, touched, setFieldValue, getFieldProps, values },
currenciesList,
accounts,
}) {
const [selectedItems, setSelectedItems] = useState({});
const handleDateChange = useCallback(
(date) => {
const formatted = moment(date).format('YYYY-MM-DD');
setFieldValue('payment_date', formatted);
},
[setFieldValue],
);
const infoIcon = useMemo(() => <Icon icon="info-circle" iconSize={12} />, []);
const requiredSpan = useMemo(() => <span className="required">*</span>, []);
const currencyCodeRenderer = useCallback((item, { handleClick }) => {
return (
<MenuItem key={item.id} text={item.currency_code} onClick={handleClick} />
);
}, []);
// Filters Currency code.
const filterCurrencyCode = (query, currency, _index, exactMatch) => {
const normalizedTitle = currency.currency_code.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return (
`${currency.currency_code} ${normalizedTitle}`.indexOf(
normalizedQuery,
) >= 0
);
}
};
// Account item of select accounts field.
const accountItem = (item, { handleClick }) => {
return (
<MenuItem
key={item.id}
text={item.name}
label={item.code}
onClick={handleClick}
/>
);
};
// Filters accounts items.
const filterAccountsPredicater = useCallback(
(query, account, _index, exactMatch) => {
const normalizedTitle = account.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return (
`${account.code} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0
);
}
},
[],
);
// Handles change account.
const onChangeAccount = useCallback(
(account) => {
setFieldValue('payment_account_id', account.id);
},
[setFieldValue],
);
const onItemsSelect = useCallback(
(filedName) => {
return (filed) => {
setSelectedItems({
...selectedItems,
[filedName]: filed,
});
setFieldValue(filedName, filed.currency_code);
};
},
[setFieldValue, selectedItems],
);
return (
<div className={'dashboard__insider--expense-form__header'}>
<Row>
<Col sm={3.5}>
<FormGroup
label={<T id={'beneficiary'} />}
className={classNames('form-group--select-list', Classes.FILL)}
labelInfo={infoIcon}
intent={errors.beneficiary && touched.beneficiary && Intent.DANGER}
helperText={
<ErrorMessage name={'beneficiary'} {...{ errors, touched }} />
}
>
<ListSelect
items={[]}
noResults={<MenuItem disabled={true} text="No results." />}
// itemRenderer={}
// itemPredicate={}
popoverProps={{ minimal: true }}
// onItemSelect={}
selectedItem={values.beneficiary}
// selectedItemProp={'id'}
defaultText={<T id={'select_beneficiary_account'} />}
labelProp={'beneficiary'}
/>
</FormGroup>
</Col>
<Col sm={3}>
<FormGroup
label={<T id={'payment_account'} />}
className={classNames(
'form-group--payment_account',
'form-group--select-list',
Classes.FILL,
)}
labelInfo={requiredSpan}
intent={
errors.payment_account_id &&
touched.payment_account_id &&
Intent.DANGER
}
helperText={
<ErrorMessage
name={'payment_account_id'}
{...{ errors, touched }}
/>
}
>
<ListSelect
items={accounts}
noResults={<MenuItem disabled={true} text="No results." />}
itemRenderer={accountItem}
itemPredicate={filterAccountsPredicater}
popoverProps={{ minimal: true }}
onItemSelect={onChangeAccount}
selectedItem={values.payment_account_id}
selectedItemProp={'id'}
defaultText={<T id={'select_payment_account'} />}
labelProp={'name'}
/>
</FormGroup>
</Col>
</Row>
<Row>
<Col sm={3.5}>
<FormGroup
label={<T id={'payment_date'} />}
labelInfo={infoIcon}
className={classNames('form-group--select-list', Classes.FILL)}
intent={
errors.payment_date && touched.payment_date && Intent.DANGER
}
helperText={
<ErrorMessage name="payment_date" {...{ errors, touched }} />
}
minimal={true}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
defaultValue={new Date()}
onChange={handleDateChange}
popoverProps={{ position: Position.BOTTOM }}
/>
</FormGroup>
</Col>
<Col sm={2}>
<FormGroup
label={<T id={'currency'} />}
className={classNames(
'form-group--select-list',
'form-group--currency',
Classes.FILL,
)}
intent={
errors.currency_code && touched.currency_code && Intent.DANGER
}
helperText={
<ErrorMessage name="currency_code" {...{ errors, touched }} />
}
>
<ListSelect
items={currenciesList}
noResults={<MenuItem disabled={true} text="No results." />}
itemRenderer={currencyCodeRenderer}
itemPredicate={filterCurrencyCode}
popoverProps={{ minimal: true }}
onItemSelect={onItemsSelect('currency_code')}
selectedItem={values.currency_code}
selectedItemProp={'currency_code'}
defaultText={<T id={'select_currency_code'} />}
labelProp={'currency_code'}
/>
</FormGroup>
</Col>
<Col sm={3}>
<FormGroup
label={<T id={'ref_no'} />}
className={'form-group--ref_no'}
intent={
errors.reference_no && touched.reference_no && Intent.DANGER
}
helperText={
<ErrorMessage name="reference_no" {...{ errors, touched }} />
}
minimal={true}
>
<InputGroup
intent={
errors.reference_no && touched.reference_no && Intent.DANGER
}
minimal={true}
{...getFieldProps('reference_no')}
/>
</FormGroup>
</Col>
</Row>
</div>
);
}
export default compose(
withAccounts(({ accounts }) => ({
accounts,
})),
withCurrencies(({ currenciesList }) => ({
currenciesList,
})),
)(ExpenseFormHeader);

View File

@@ -0,0 +1,239 @@
import React, { useState, useMemo, useEffect, useCallback } from 'react';
import { Button, Intent } from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl';
import DataTable from 'components/DataTable';
import Icon from 'components/Icon';
import { compose, formattedAmount } from 'utils';
import {
AccountsListFieldCell,
MoneyFieldCell,
InputGroupCell,
} from 'components/DataTableCells';
import { omit } from 'lodash';
import withAccounts from 'containers/Accounts/withAccounts';
function ExpenseTable({
// #withAccounts
accounts,
// #ownPorps
onClickRemoveRow,
onClickAddNewRow,
defaultRow,
initialValues,
formik: { errors, values, setFieldValue },
}) {
const [rows, setRow] = useState([]);
const { formatMessage } = useIntl();
useEffect(() => {
setRow([
...initialValues.categories.map((e) => ({ ...e, rowType: 'editor' })),
defaultRow,
defaultRow,
]);
}, [initialValues, defaultRow]);
// Handles update datatable data.
const handleUpdateData = useCallback(
(rowIndex, columnId, value) => {
const newRows = rows.map((row, index) => {
if (index === rowIndex) {
return { ...rows[rowIndex], [columnId]: value };
}
return { ...row };
});
setRow(newRows);
setFieldValue(
'categories',
newRows.map((row) => ({
...omit(row, ['rowType']),
})),
);
},
[rows, setFieldValue],
);
// Handles click remove datatable row.
const handleRemoveRow = useCallback(
(rowIndex) => {
const removeIndex = parseInt(rowIndex, 10);
const newRows = rows.filter((row, index) => index !== removeIndex);
setRow([...newRows]);
setFieldValue(
'categories',
newRows
.filter((row) => row.rowType === 'editor')
.map((row) => ({ ...omit(row, ['rowType']) })),
);
onClickRemoveRow && onClickRemoveRow(removeIndex);
},
[rows, setFieldValue, onClickRemoveRow],
);
// Actions cell renderer.
const ActionsCellRenderer = ({
row: { index },
column: { id },
cell: { value: initialValue },
data,
payload,
}) => {
if (data.length <= index + 2) {
return '';
}
const onClickRemoveRole = () => {
payload.removeRow(index);
};
return (
<Button
icon={<Icon icon="times-circle" iconSize={14} />}
iconSize={14}
className="ml2"
minimal={true}
intent={Intent.DANGER}
onClick={onClickRemoveRole}
/>
);
};
// Total text cell renderer.
const TotalExpenseCellRenderer = (chainedComponent) => (props) => {
if (props.data.length === props.row.index + 2) {
return (
<span>
{formatMessage({ id: 'total_currency' }, { currency: 'USD' })}
</span>
);
}
return chainedComponent(props);
};
const NoteCellRenderer = (chainedComponent) => (props) => {
if (props.data.length === props.row.index + 2) {
return '';
}
return chainedComponent(props);
};
const TotalAmountCellRenderer = (chainedComponent, type) => (props) => {
if (props.data.length === props.row.index + 2) {
const total = props.data.reduce((total, entry) => {
const amount = parseInt(entry[type], 10);
const computed = amount ? total + amount : total;
return computed;
}, 0);
return <span>{formattedAmount(total, 'USD')}</span>;
}
return chainedComponent(props);
};
const columns = useMemo(
() => [
{
Header: '#',
accessor: 'index',
Cell: ({ row: { index } }) => <span>{index + 1}</span>,
className: 'index',
width: 40,
disableResizing: true,
disableSortBy: true,
},
{
Header: formatMessage({ id: 'expense_category' }),
id: 'expense_account_id',
accessor: 'expense_account_id',
Cell: TotalExpenseCellRenderer(AccountsListFieldCell),
className: 'expense_account_id',
disableSortBy: true,
disableResizing: true,
width: 250,
},
{
Header: formatMessage({ id: 'amount_currency' }, { currency: 'USD' }),
accessor: 'amount',
Cell: TotalAmountCellRenderer(MoneyFieldCell, 'amount'),
disableSortBy: true,
disableResizing: true,
width: 150,
},
{
Header: formatMessage({ id: 'description' }),
accessor: 'description',
Cell: NoteCellRenderer(InputGroupCell),
disableSortBy: true,
className: 'description',
},
{
Header: '',
accessor: 'action',
Cell: ActionsCellRenderer,
className: 'actions',
disableSortBy: true,
disableResizing: true,
width: 45,
},
],
[formatMessage],
);
// Handles click new line.
const onClickNewRow = useCallback(() => {
setRow([...rows, { ...defaultRow, rowType: 'editor' }]);
onClickAddNewRow && onClickAddNewRow();
}, [defaultRow, rows, onClickAddNewRow]);
const rowClassNames = useCallback(
(row) => ({
'row--total': rows.length === row.index + 2,
}),
[rows],
);
return (
<div className={'dashboard__insider--expense-form__table'}>
<DataTable
columns={columns}
data={rows}
rowClassNames={rowClassNames}
sticky={true}
payload={{
accounts,
errors: errors.categories || [],
updateData: handleUpdateData,
removeRow: handleRemoveRow,
}}
/>
<div className={'mt1'}>
<Button
small={true}
className={'button--secondary button--new-line'}
onClick={onClickNewRow}
>
<T id={'new_lines'} />
</Button>
<Button
small={true}
className={'button--secondary button--clear-lines ml1'}
onClick={onClickNewRow}
>
<T id={'clear_all_lines'} />
</Button>
</div>
</div>
);
}
export default compose(
withAccounts(({ accounts }) => ({
accounts,
})),
)(ExpenseTable);

View File

@@ -0,0 +1,124 @@
import React, { useEffect } from 'react';
import { useHistory } from 'react-router';
import {
Alignment,
Navbar,
NavbarGroup,
Tabs,
Tab,
Button,
} from '@blueprintjs/core';
import { useParams, withRouter } from 'react-router-dom';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import { FormattedMessage as T } from 'react-intl';
import { useUpdateEffect } from 'hooks';
import Icon from 'components/Icon';
import withExpenses from './withExpenses';
import withExpensesActions from './withExpensesActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose } from 'utils';
function ExpenseViewTabs({
//#withExpenses
expensesViews,
//#withExpensesActions
addExpensesTableQueries,
// #withDashboardActions
setTopbarEditView,
// #ownProps
customViewChanged,
onViewChanged,
}) {
const history = useHistory();
const { custom_view_id: customViewId } = useParams();
const handleClickNewView = () => {
setTopbarEditView(null);
history.push('/custom_views/expenses/new');
};
const handleViewLinkClick = () => {
setTopbarEditView(customViewId);
};
useUpdateEffect(() => {
customViewChanged && customViewChanged(customViewId);
addExpensesTableQueries({
custom_view_id: customViewId || null,
});
onViewChanged && onViewChanged(customViewId);
}, [customViewId]);
useEffect(() => {
addExpensesTableQueries({
custom_view_id: customViewId,
});
}, [customViewId, addExpensesTableQueries]);
const tabs = expensesViews.map((view) => {
const baseUrl = '/expenses/new';
const link = (
<Link
to={`${baseUrl}/${view.id}/custom_view`}
onClick={handleViewLinkClick}
>
{view.name}
</Link>
);
return <Tab id={`custom_view_${view.id}`} title={link} />;
});
return (
<Navbar className={'navbar--dashboard-views'}>
<NavbarGroup align={Alignment.LEFT}>
<Tabs
id="navbar"
large={true}
selectedTabId={`custom_view_${customViewId}`}
className="tabs--dashboard-views"
>
<Tab
id="all"
title={
<Link to={``}>
<T id={'all'} />
</Link>
}
/>
{tabs}
<Button
className="button--new-view"
icon={<Icon icon="plus" />}
onClick={handleClickNewView}
minimal={true}
/>
</Tabs>
</NavbarGroup>
</Navbar>
);
}
const mapStateToProps = (state, ownProps) => ({
// Mapping view id from matched route params.
viewId: ownProps.match.params.custom_view_id,
});
const withExpensesViewTabs = connect(mapStateToProps);
export default compose(
withRouter,
withExpensesViewTabs,
withExpenses(({ expensesViews }) => ({
expensesViews,
})),
withExpensesActions,
withDashboardActions,
)(ExpenseViewTabs);

View File

@@ -0,0 +1,69 @@
import React, { useCallback } from 'react';
import { useParams, useHistory } from 'react-router-dom';
import { useQuery } from 'react-query';
import ExpenseForm from './ExpenseForm';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import withAccountsActions from 'containers/Accounts/withAccountsActions';
import withExpensesActions from 'containers/Expenses/withExpensesActions';
import withCurrenciesActions from 'containers/Currencies/withCurrenciesActions';
import { compose } from 'utils';
function Expenses({
//#withwithAccountsActions
requestFetchAccounts,
//#withExpensesActions
requestFetchExpense,
// #wihtCurrenciesActions
requestFetchCurrencies,
}) {
const history = useHistory();
const { id } = useParams();
const fetchAccounts = useQuery('accounts-expense-list', (key) =>
requestFetchAccounts(),
);
const fetchExpense = useQuery(id && ['expense', id], (key, expense_Id) =>
requestFetchExpense(expense_Id),
);
const fetchCurrencies = useQuery('currencies-expense-list', () =>
requestFetchCurrencies(),
);
const handleFormSubmit = useCallback(
(payload) => {
payload.redirect && history.push('/expenses-list');
},
[history],
);
const handleCancel = useCallback(() => {
history.push('/expenses-list');
}, [history]);
return (
<DashboardInsider
loading={
fetchExpense.isFetching ||
fetchAccounts.isFetching ||
fetchCurrencies.isFetching
}
>
<ExpenseForm
onFormSubmit={handleFormSubmit}
expenseId={id}
onCancelForm={handleCancel}
/>
</DashboardInsider>
);
}
export default compose(
withAccountsActions,
withCurrenciesActions,
withExpensesActions,
)(Expenses);

View File

@@ -1,81 +1,243 @@
import React, { useEffect, useState } from 'react';
import { useAsync } from 'react-use';
import React, { useEffect, useState, useMemo, useCallback } from 'react';
import { Route, Switch, useHistory, useParams } from 'react-router-dom';
import { useQuery } from 'react-query';
import { Alert, Intent } from '@blueprintjs/core';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import ExpensesActionsBar from 'components/Expenses/ExpensesActionsBar';
import ExpensesViewsTabs from 'components/Expenses/ExpensesViewsTabs';
import ExpensesTable from 'components/Expenses/ExpensesTable';
import connector from 'connectors/ExpensesList.connector';
import AppToaster from 'components/AppToaster';
import { FormattedMessage as T, useIntl } from 'react-intl';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import ExpenseViewTabs from 'containers/Expenses/ExpenseViewTabs';
import ExpenseDataTable from 'containers/Expenses/ExpenseDataTable';
import ExpenseActionsBar from 'containers/Expenses/ExpenseActionsBar';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withExpensesActions from 'containers/Expenses/withExpensesActions';
import withViewsActions from 'containers/Views/withViewsActions';
import { compose } from 'utils';
function ExpensesList({
fetchExpenses,
deleteExpense,
// fetchViews,
expenses,
getResourceViews,
changePageTitle
// #withDashboardActions
changePageTitle,
// #withViewsActions
requestFetchResourceViews,
//#withExpensesActions
requestFetchExpensesTable,
requestDeleteExpense,
requestPublishExpense,
requestDeleteBulkExpenses,
addExpensesTableQueries,
requestFetchExpense,
}) {
const {formatMessage} =useIntl()
useEffect(() => {
changePageTitle(formatMessage({id:'expenses_list'}));
}, [changePageTitle,formatMessage]);
const history = useHistory();
const { id } = useParams();
const { formatMessage } = useIntl();
const [deleteExpenseState, setDeleteExpense] = useState();
const [deleteExpense, setDeleteExpense] = useState(false);
const [selectedRows, setSelectedRows] = useState([]);
const [bulkDelete, setBulkDelete] = useState(false);
const handleDeleteExpense = expense => {
setDeleteExpense(expense);
};
const handleCancelAccountDelete = () => {
setDeleteExpense(false);
};
const handleConfirmAccountDelete = () => {
deleteExpense(deleteExpenseState.id).then(() => {
setDeleteExpense(false);
AppToaster.show({
message: formatMessage({id:'the_expense_has_been_successfully_deleted'})
});
});
};
const fetchHook = useAsync(async () => {
await Promise.all([
fetchExpenses()
// getResourceViews('expenses'),
]);
const fetchViews = useQuery('expenses-resource-views', () => {
return requestFetchResourceViews('expenses');
});
const fetchExpenses = useQuery('expenses-table', () =>
requestFetchExpensesTable(),
);
useEffect(() => {
changePageTitle(formatMessage({ id: 'expenses_list' }));
}, [changePageTitle, formatMessage]);
// Handle delete expense click.
const handleDeleteExpense = useCallback(
(expnese) => {
setDeleteExpense(expnese);
},
[setDeleteExpense],
);
// Handle cancel expense journal.
const handleCancelExpenseDelete = useCallback(() => {
setDeleteExpense(false);
}, [setDeleteExpense]);
// Handle confirm delete expense.
const handleConfirmExpenseDelete = useCallback(() => {
requestDeleteExpense(deleteExpense.id).then(() => {
AppToaster.show({
message: formatMessage(
{
id: 'the_expense_has_been_successfully_deleted',
},
{
number: deleteExpense.payment_account_id,
},
),
intent: Intent.SUCCESS,
});
setDeleteExpense(false);
});
}, [deleteExpense, requestDeleteExpense, formatMessage]);
// Calculates the selected rows count.
const selectedRowsCount = useMemo(() => Object.values(selectedRows).length, [
selectedRows,
]);
const handleBulkDelete = useCallback(
(accountsIds) => {
setBulkDelete(accountsIds);
},
[setBulkDelete],
);
// Handle confirm journals bulk delete.
const handleConfirmBulkDelete = useCallback(() => {
requestDeleteBulkExpenses(bulkDelete)
.then(() => {
AppToaster.show({
message: formatMessage(
{
id: 'the_expenses_has_been_successfully_deleted',
},
{
count: selectedRowsCount,
},
),
intent: Intent.SUCCESS,
});
setBulkDelete(false);
})
.catch((error) => {
setBulkDelete(false);
});
}, [requestDeleteBulkExpenses, bulkDelete, formatMessage, selectedRowsCount]);
// Handle cancel bulk delete alert.
const handleCancelBulkDelete = useCallback(() => {
setBulkDelete(false);
}, []);
const handleEidtExpense = useCallback(
(expense) => {
history.push(`/expenses/${expense.id}/edit`);
},
[history],
);
// Handle filter change to re-fetch data-table.
const handleFilterChanged = useCallback(() => {}, []);
// Handle fetch data of manual jouranls datatable.
const handleFetchData = useCallback(
({ pageIndex, pageSize, sortBy }) => {
addExpensesTableQueries({
...(sortBy.length > 0
? {
column_sort_by: sortBy[0].id,
sort_order: sortBy[0].desc ? 'desc' : 'asc',
}
: {}),
});
},
[addExpensesTableQueries],
);
const handlePublishExpense = useCallback(
(expense) => {
requestPublishExpense(expense.id).then(() => {
AppToaster.show({
message: formatMessage({ id: 'the_expense_id_has_been_published' }),
});
});
},
[requestPublishExpense, formatMessage],
);
// Handle selected rows change.
const handleSelectedRowsChange = useCallback(
(accounts) => {
setSelectedRows(accounts);
},
[setSelectedRows],
);
return (
<DashboardInsider loading={false}>
<ExpensesActionsBar />
<ExpensesViewsTabs />
<DashboardInsider
loading={fetchViews.isFetching || fetchExpenses.isFetching}
name={'expenses'}
>
<ExpenseActionsBar
onBulkDelete={handleBulkDelete}
selectedRows={selectedRows}
onFilterChanged={handleFilterChanged}
/>
<DashboardPageContent>
<ExpensesTable
expenses={expenses}
onDeleteExpense={handleDeleteExpense}
/>
</DashboardPageContent>
<Switch>
<Route
// exact={true}
// path={[
// '/expenses/:custom_view_id/custom_view',
// '/expenses/new',
// ]}
>
<ExpenseViewTabs />
<Alert
cancelButtonText={<T id={'cancel'}/>}
confirmButtonText={<T id={'move_to_trash'}/>}
icon='trash'
intent={Intent.DANGER}
isOpen={deleteExpenseState}
onCancel={handleCancelAccountDelete}
onConfirm={handleConfirmAccountDelete}
>
<p>
Are you sure you want to move <b>filename</b> to Trash? You will be
able to restore it later, but it will become private to you.
</p>
</Alert>
<ExpenseDataTable
onDeleteExpense={handleDeleteExpense}
onFetchData={handleFetchData}
onEditExpense={handleEidtExpense}
onPublishExpense={handlePublishExpense}
onSelectedRowsChange={handleSelectedRowsChange}
/>
</Route>
</Switch>
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={<T id={'delete'} />}
icon="trash"
intent={Intent.DANGER}
isOpen={deleteExpense}
onCancel={handleCancelExpenseDelete}
onConfirm={handleConfirmExpenseDelete}
>
<p>
<T id={'once_delete_this_expense_you_will_able_to_restore_it'} />
</p>
</Alert>
{/* <Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={
<T id={'delete_count'} values={{ count: selectedRowsCount }} />
}
icon="trash"
intent={Intent.DANGER}
isOpen={bulkDelete}
onCancel={handleCancelBulkDelete}
onConfirm={handleConfirmBulkDelete}
>
<p>
<T
id={'once_delete_these_journalss_you_will_not_able_restore_them'}
/>
</p>
</Alert> */}
</DashboardPageContent>
</DashboardInsider>
);
}
export default connector(ExpensesList);
export default compose(
withDashboardActions,
withExpensesActions,
withViewsActions,
)(ExpensesList);

View File

@@ -0,0 +1,11 @@
import { connect } from 'react-redux';
import { getExpenseById } from 'store/expenses/expenses.reducer';
const mapStateToProps = (state, props) => {
return {
expenseDetail: getExpenseById(state, props.expenseId),
};
};
export default connect(mapStateToProps);

View File

@@ -0,0 +1,18 @@
import { connect } from 'react-redux';
import { getResourceViews } from 'store/customViews/customViews.selectors';
import { getExpensesItems } from 'store/expenses/expenses.selectors';
export default (mapState) => {
const mapStateToProps = (state, props) => {
const mapped = {
expenses: getExpensesItems(state, state.expenses.currentViewId),
expensesViews: getResourceViews(state, 'expenses'),
expensesItems: state.expenses.items,
expensesTableQuery: state.expenses.tableQuery,
expensesLoading: state.expenses.loading,
};
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};

View File

@@ -0,0 +1,37 @@
import { connect } from 'react-redux';
import {
submitExpense,
fetchExpense,
editExpense,
deleteExpense,
deleteBulkExpenses,
publishExpense,
fetchExpensesTable,
} from 'store/expenses/expenses.actions';
import t from 'store/types';
export const mapDispatchToProps = (dispatch) => ({
requestSubmitExpense: (form) => dispatch(submitExpense({ form })),
requestFetchExpense: (id) => dispatch(fetchExpense({ id })),
requestEditExpense: (id, form) => dispatch(editExpense({ id, form })),
requestDeleteExpense: (id) => dispatch(deleteExpense({ id })),
requestFetchExpensesTable: (query = {}) =>
dispatch(fetchExpensesTable({ query: { ...query } })),
requestPublishExpense: (id) => dispatch(publishExpense({ id })),
requestDeleteBulkExpenses: (ids) => dispatch(deleteBulkExpenses({ ids })),
changeCurrentView: (id) =>
dispatch({
type: t.EXPENSES_SET_CURRENT_VIEW,
currentViewId: parseInt(id, 10),
}),
addExpensesTableQueries: (queries) =>
dispatch({
type: t.EXPENSES_TABLE_QUERIES_ADD,
queries,
}),
});
export default connect(null, mapDispatchToProps);

View File

@@ -203,7 +203,8 @@ export default {
'The journal has been successfully deleted',
the_manual_journal_id_has_been_published:
'The manual journal id has been published',
the_journals_has_been_successfully_deleted:
'The journals has been successfully deleted ',
credit: 'Credit',
debit: 'Debit',
once_delete_this_item_you_will_able_to_restore_it: `Once you delete this item, you won\'t be able to restore the item later. Are you sure you want to delete ?<br /><br />If you're not sure, you can inactivate it instead.`,
@@ -226,8 +227,6 @@ export default {
table: 'Table',
nucleus: 'Nucleus',
logout: 'Logout',
the_expense_has_been_successfully_created:
'The expense has been successfully created.',
select_payment_account: 'Select Payment Account',
select_expense_account: 'Select Expense Account',
and: 'And',
@@ -357,6 +356,10 @@ export default {
'There is exchange rate in this date with the same currency.',
the_exchange_rates_has_been_successfully_deleted:
'The exchange rates has been successfully deleted',
once_delete_this_expense_you_will_able_to_restore_it: `Once you delete this expense, you won\'t be able to restore it later. Are you sure you want to delete this expense?`,
january: 'January',
february: 'February',
march: 'March',
@@ -417,4 +420,31 @@ export default {
quick_new: 'Quick new',
help: 'Help',
organization_id: 'Orgnization ID',
beneficiary: 'Beneficiary',
payment_account: 'Payment Account',
payment_date: 'Payment Date',
ref_no: 'Ref No.',
payment_account_: 'Payment account',
expense_category: 'Expense Category',
total_currency: 'Total ({currency})',
amount_currency: 'Amount({currency})',
publish_expense: 'Publish Expense',
edit_expense: 'Edit Expense',
delete_expense: 'Delete Expense',
new_expense: 'New Expense',
full_amount: 'Full Amount',
payment_date_: 'Payment date',
the_expense_has_been_successfully_created:
'The expense #{number} has been successfully created.',
the_expense_has_been_successfully_edited:
'The expense #{number} has been successfully edited.',
the_expense_has_been_successfully_deleted:
'The expense has been successfully deleted',
the_expenses_has_been_successfully_deleted:
'The expenses has been successfully deleted',
the_expense_id_has_been_published: 'The expense id has been published',
select_beneficiary_account: 'Select Beneficiary Account',
total_amount_equals_zero: 'Total amount equals zero',
};

View File

@@ -141,4 +141,28 @@ export default [
}),
breadcrumb: 'Exchange Rates',
},
// Expenses
{
path: `/expenses/new`, // expenses/
component: LazyLoader({
loader: () => import('containers/Expenses/Expenses'),
}),
breadcrumb: 'Expenses',
},
{
path: `/expenses/:id/edit`,
component: LazyLoader({
loader: () => import('containers/Expenses/Expenses'),
}),
breadcrumb: 'Edit',
},
{
path: `/expenses-list`,
component: LazyLoader({
loader: () => import('containers/Expenses/ExpensesList'),
}),
breadcrumb: 'Expenses List',
},
];

View File

@@ -1,41 +1,144 @@
import ApiService from "services/ApiService";
import ApiService from 'services/ApiService';
import t from 'store/types';
export const fetchExpensesList = ({ query }) => {
return (dispatch) => new Promise((resolve, reject) => {
ApiService.get('expenses').then((response) => {
export const fetchExpensesTable = ({ query } = {}) => {
return (dispatch, getState) =>
new Promise((resolve, reject) => {
const pageQuery = getState().expenses.tableQuery;
dispatch({
type: t.EXPENSES_LIST_SET,
expenses: response.data.expenses,
type: t.SET_DASHBOARD_REQUEST_LOADING,
});
}).catch(error => { reject(error); });
});
dispatch({
type: t.EXPENSES_TABLE_LOADING,
loading: true,
});
ApiService.get('expenses', {
params: { ...pageQuery, ...query },
})
.then((response) => {
dispatch({
type: t.EXPENSES_PAGE_SET,
expenses: response.data.expenses.results,
customViewId: response.data.customViewId || -1,
});
dispatch({
type: t.EXPENSES_ITEMS_SET,
expenses: response.data.expenses.results,
});
dispatch({
type: t.EXPENSES_TABLE_LOADING,
loading: false,
});
dispatch({
type: t.SET_DASHBOARD_REQUEST_COMPLETED,
});
resolve(response);
})
.catch((error) => {
reject(error);
});
});
};
export const fetchExpense = ({ id }) => {
return (dispatch) => new Promise((resolve, reject) => {
ApiService.get(`expenses/${id}`).then((response) => {
dispatch({
type: t.EXPENSE_SET,
expense: response.data.expense,
});
}).catch(error => { reject(error); });
});
};
export const submitExpense = ({ form }) => {
return (dispatch) => ApiService.post('expenses', { ...form });
return (dispatch) =>
new Promise((resolve, reject) => {
ApiService.get(`expenses/${id}`)
.then((response) => {
dispatch({
type: t.EXPENSE_SET,
payload: {
id,
expense: response.data.expense,
},
});
resolve(response);
})
.catch((error) => {
reject(error);
});
});
};
export const editExpense = ({ form, id }) => {
return (dispatch) => ApiService.post(`expensed/${id}`, form);
return (dispatch) =>
new Promise((resolve, reject) => {
ApiService.post(`expenses/${id}`, form)
.then((response) => {
resolve(response);
})
.catch((error) => {
const { response } = error;
const { data } = response;
reject(data?.errors);
});
});
};
export const submitExpense = ({ form }) => {
return (dispatch) =>
new Promise((resolve, reject) => {
ApiService.post('expenses', form)
.then((response) => {
resolve(response);
})
.catch((error) => {
const { response } = error;
const { data } = response;
reject(data?.errors);
});
});
};
export const deleteExpense = ({ id }) => {
return (dispatch) => ApiService.delete(`expenses/${id}`);
return (dispatch) =>
new Promise((resolve, reject) => {
ApiService.delete(`expenses/${id}`)
.then((response) => {
dispatch({
type: t.EXPENSE_DELETE,
payload: { id },
});
resolve(response);
})
.catch((error) => {
reject(error.response.data.errors || []);
});
});
};
export const deleteBulkExpenses = ({ ids }) => {
return (dispatch) =>
new Promise((resolve, reject) => {
ApiService.delete('expenses/bulk', { params: { ids } })
.then((response) => {
dispatch({
type: t.EXPENSES_BULK_DELETE,
payload: { ids },
});
resolve(response);
})
.catch((error) => {
reject(error.response.data.errors || []);
});
});
};
export const publishExpense = ({ id }) => {
return (dispatch) => ApiService.post(`expenses/${id}/publish`);
return (dispatch) =>
new Promise((resolve, reject) => {
ApiService.post(`expenses/${id}/publish`)
.then((response) => {
dispatch({
type: t.EXPENSE_PUBLISH,
payload: { id },
});
resolve(response);
})
.catch((error) => {
reject(error);
});
});
};

View File

@@ -1,21 +1,92 @@
import { createReducer } from '@reduxjs/toolkit';
import { createTableQueryReducers } from 'store/queryReducers';
import t from 'store/types';
import { omit } from 'lodash';
const initialState = {
list: [],
detailsById: {},
items: {},
views: {},
loading: false,
currentViewId: -1,
};
export default createReducer(initialState, {
[t.EXPENSES_LIST_SET]: (state, action) => {
state.list = action.expenses;
const defaultExpense = {
categories: [],
};
const reducer = createReducer(initialState, {
[t.EXPENSE_SET]: (state, action) => {
const { id, expense } = action.payload;
state.items[id] = { ...defaultExpense, ...expense };
},
[t.EXPENSE_SET]: (state, action) => {
state.detailsById[action.expense.id] = action.expense;
[t.EXPENSE_PUBLISH]: (state, action) => {
const { id } = action.payload;
const item = state.items[id] || {};
state.items[id] = { ...item, status: 1 };
},
[t.EXPENSES_ITEMS_SET]: (state, action) => {
const _expenses = {};
action.expenses.forEach((expense) => {
_expenses[expense.id] = {
...defaultExpense,
...expense,
};
});
state.items = {
...state.items,
..._expenses,
};
},
[t.EXPENSES_PAGE_SET]: (state, action) => {
const viewId = action.customViewId || -1;
const view = state.views[viewId] || {};
state.views[viewId] = {
...view,
ids: action.expenses.map((i) => i.id),
};
},
[t.EXPENSES_TABLE_LOADING]: (state, action) => {
state.loading = action.loading;
},
[t.EXPENSES_SET_CURRENT_VIEW]: (state, action) => {
state.currentViewId = action.currentViewId;
},
[t.EXPENSE_DELETE]: (state, action) => {
const { id } = action.payload;
// state.items = omit(state.items, [id]);
if (typeof state.items[id] !== 'undefined') {
delete state.items[id];
}
},
[t.EXPENSES_BULK_DELETE]: (state, action) => {
const { ids } = action.payload;
const items = { ...state.items };
ids.forEach((id) => {
if (typeof items[id] !== 'undefined') {
delete items[id];
}
});
state.items = items;
},
});
export default createTableQueryReducers('expenses', reducer);
export const getExpenseById = (state, id) => {
return state.expenses.detailsById[id];
};
// debugger;
// state.items = omit(state.items, [id]);
return state.expenses.items[id];
};

View File

@@ -0,0 +1,10 @@
import { pickItemsFromIds } from 'store/selectors';
export const getExpensesItems = (state, viewId) => {
const accountsView = state.expenses.views[viewId || -1];
const accountsItems = state.expenses.items;
return typeof accountsView === 'object'
? pickItemsFromIds(accountsItems, accountsView.ids) || []
: [];
};

View File

@@ -1,5 +1,12 @@
export default {
EXPENSES_LIST_SET: 'EXPENSES_LIST_SET',
EXPENSE_SET: 'EXPENSE_SET',
};
EXPENSE_DELETE: 'EXPENSE_DELETE',
EXPENSES_BULK_DELETE: 'EXPENSES_BULK_DELETE',
EXPENSES_SET_CURRENT_VIEW: 'EXPENSES_SET_CURRENT_VIEW',
EXPENSES_TABLE_QUERIES_ADD:'EXPENSES_TABLE_QUERIES_ADD',
EXPENSE_PUBLISH: 'EXPENSE_PUBLISH',
EXPENSES_TABLE_LOADING: 'EXPENSES_TABLE_LOADING',
EXPENSES_PAGE_SET: 'EXPENSES_PAGE_SET',
EXPENSES_ITEMS_SET: 'EXPENSES_ITEMS_SET',
};

View File

@@ -1,17 +1,239 @@
.dashboard__insider--expense-form {
padding-bottom: 80px;
display: flex;
flex-direction: column;
.dashboard__insider--expense-form{
padding: 40px 20px;
.#{$ns}-form-group{
margin-bottom: 22px;
.#{$ns}-label{
min-width: 130px;
&__header {
padding: 25px 27px 20px;
background: #fbfbfb;
width: 100%;
.bp3-form-group {
.bp3-label {
margin-bottom: 15px;
margin-right: 15px;
font-weight: 500;
font-size: 13px;
color: #444;
}
.bp3-form-content {
.bp3-button:not([class*='bp3-intent-']):not(.bp3-minimal) {
min-width: 300px;
min-height: 32px;
background: #fff;
box-shadow: 0 0 0 transparent;
border: 1px solid #ced4da;
}
}
.bp3-input-group {
display: block;
position: relative;
width: 300px;
}
&.form-group--ref_no {
.bp3-input-group .bp3-input {
position: relative;
width: 180px;
}
}
}
.#{$ns}-form-content{
width: 300px;
.form-group--payment_account {
.bp3-form-group {
display: flex;
flex-direction: column;
margin: 0 0 15px;
}
.bp3-form-content {
.bp3-button:not([class*='bp3-intent-']):not(.bp3-minimal) {
min-width: 380px;
min-height: 32px;
background: #fff;
box-shadow: 0 0 0 transparent;
border: 1px solid #ced4da;
}
}
}
.form-group--currency {
.bp3-form-group {
display: flex;
flex-direction: column;
margin: 0 0 15px;
}
.bp3-form-content {
.bp3-button:not([class*='bp3-intent-']):not(.bp3-minimal) {
min-width: 180px;
min-height: 32px;
background: #fff;
box-shadow: 0 0 0 transparent;
border: 1px solid #ced4da;
}
}
}
}
&__table {
padding: 15px 25px 0;
.bp3-form-group {
margin-bottom: 0;
}
.table {
border: 1px dotted rgb(195, 195, 195);
border-bottom: transparent;
border-left: transparent;
.th,
.td {
border-left: 1px dotted rgb(195, 195, 195);
&.index {
span {
width: 100%;
font-weight: 500;
}
}
}
.thead {
.tr .th {
padding: 10px 10px;
background-color: #f2f5fa;
font-size: 14px;
font-weight: 500;
color: #333;
}
}
.tbody {
.tr .td {
padding: 7px;
border-bottom: 1px dotted rgb(195, 195, 195);
min-height: 46px;
&.index {
background-color: #f2f5fa;
text-align: center;
> span {
margin-top: auto;
margin-bottom: auto;
}
}
}
.tr {
.bp3-input,
.form-group--select-list .bp3-button {
border-color: #e5e5e5;
border-radius: 3px;
padding-left: 8px;
padding-right: 8px;
}
.form-group--select-list {
&.bp3-intent-danger {
.bp3-button:not(.bp3-minimal) {
border-color: #efa8a8;
}
}
}
&:last-of-type {
.td {
border-bottom: transparent;
.bp3-button,
.bp3-input-group {
display: none;
}
}
}
.td.actions {
.bp3-button {
background-color: transparent;
color: #e68f8e;
&:hover {
color: #c23030;
}
}
}
&.row--total {
.account.td,
.debit.td,
.credit.td {
> span {
padding-top: 6px;
}
}
.debit.td,
.credit.td {
> span {
font-weight: 600;
color: #444;
}
}
}
}
}
.th {
color: #444;
font-weight: 600;
border-bottom: 1px dotted #666;
}
.td {
border-bottom: 1px dotted #999;
}
.actions.td {
.bp3-button {
background: transparent;
margin: 0;
}
}
}
}
.bp3-button {
&.button--clear-lines {
background-color: #fcefef;
}
}
.button--clear-lines,
.button--new-line {
padding-left: 14px;
padding-right: 14px;
}
.dropzone-container {
margin-top: 0;
align-self: flex-end;
}
.dropzone {
width: 300px;
height: 75px;
margin-right: 20px;
}
.form-group--description {
padding: 25px 27px 20px;
width: 100%;
.bp3-label {
margin-bottom: 15px;
margin-right: 15px;
font-weight: 500;
font-size: 13px;
color: #444;
}
.bp3-form-content {
// width: 280px;
textarea {
width: 300px;
height: 75px;
margin-right: 20px;
}
}
}
}