mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 21:30:31 +00:00
Merge remote-tracking branch 'origin/expenses'
This commit is contained in:
@@ -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>
|
||||
// );
|
||||
// }
|
||||
|
||||
@@ -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>
|
||||
// );
|
||||
// }
|
||||
|
||||
@@ -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>
|
||||
// );
|
||||
// }
|
||||
@@ -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);
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -340,7 +340,7 @@ function AccountsChart({
|
||||
onEditAccount={handleEditAccount}
|
||||
onFetchData={handleFetchData}
|
||||
onSelectedRowsChange={handleSelectedRowsChange}
|
||||
loading={tableLoading}
|
||||
|
||||
/>
|
||||
</Route>
|
||||
</Switch>
|
||||
|
||||
@@ -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);
|
||||
@@ -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
|
||||
}
|
||||
|
||||
145
client/src/containers/Expenses/ExpenseActionsBar.js
Normal file
145
client/src/containers/Expenses/ExpenseActionsBar.js
Normal 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);
|
||||
261
client/src/containers/Expenses/ExpenseDataTable.js
Normal file
261
client/src/containers/Expenses/ExpenseDataTable.js
Normal 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);
|
||||
45
client/src/containers/Expenses/ExpenseFooter.js
Normal file
45
client/src/containers/Expenses/ExpenseFooter.js
Normal 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;
|
||||
@@ -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);
|
||||
|
||||
262
client/src/containers/Expenses/ExpenseFormHeader.js
Normal file
262
client/src/containers/Expenses/ExpenseFormHeader.js
Normal 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);
|
||||
239
client/src/containers/Expenses/ExpenseTable.js
Normal file
239
client/src/containers/Expenses/ExpenseTable.js
Normal 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);
|
||||
124
client/src/containers/Expenses/ExpenseViewTabs.js
Normal file
124
client/src/containers/Expenses/ExpenseViewTabs.js
Normal 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);
|
||||
69
client/src/containers/Expenses/Expenses.js
Normal file
69
client/src/containers/Expenses/Expenses.js
Normal 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);
|
||||
@@ -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);
|
||||
|
||||
11
client/src/containers/Expenses/withExpenseDetail.js
Normal file
11
client/src/containers/Expenses/withExpenseDetail.js
Normal 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);
|
||||
18
client/src/containers/Expenses/withExpenses.js
Normal file
18
client/src/containers/Expenses/withExpenses.js
Normal 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);
|
||||
};
|
||||
37
client/src/containers/Expenses/withExpensesActions.js
Normal file
37
client/src/containers/Expenses/withExpensesActions.js
Normal 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);
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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];
|
||||
};
|
||||
|
||||
10
client/src/store/expenses/expenses.selectors.js
Normal file
10
client/src/store/expenses/expenses.selectors.js
Normal 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) || []
|
||||
: [];
|
||||
};
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user