mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-21 07:10:33 +00:00
WIP feature/expenses
This commit is contained in:
@@ -1,246 +1,246 @@
|
|||||||
import React, { useState } from 'react';
|
// import React, { useState } from 'react';
|
||||||
import * as Yup from 'yup';
|
// import * as Yup from 'yup';
|
||||||
import { useFormik } from 'formik';
|
// import { useFormik } from 'formik';
|
||||||
import {
|
// import {
|
||||||
FormGroup,
|
// FormGroup,
|
||||||
MenuItem,
|
// MenuItem,
|
||||||
Intent,
|
// Intent,
|
||||||
InputGroup,
|
// InputGroup,
|
||||||
Position,
|
// Position,
|
||||||
Button,
|
// Button,
|
||||||
TextArea,
|
// TextArea,
|
||||||
ControlGroup
|
// ControlGroup
|
||||||
} from '@blueprintjs/core';
|
// } from '@blueprintjs/core';
|
||||||
import { DateInput } from '@blueprintjs/datetime';
|
// import { DateInput } from '@blueprintjs/datetime';
|
||||||
import { Select } from '@blueprintjs/select';
|
// import { Select } from '@blueprintjs/select';
|
||||||
import { FormattedMessage as T, useIntl } from 'react-intl';
|
// import { FormattedMessage as T, useIntl } from 'react-intl';
|
||||||
import { momentFormatter } from 'utils';
|
// import { momentFormatter } from 'utils';
|
||||||
import moment from 'moment';
|
// import moment from 'moment';
|
||||||
import AppToaster from 'components/AppToaster';
|
// import AppToaster from 'components/AppToaster';
|
||||||
|
|
||||||
export default function ExpenseForm({
|
// export default function ExpenseForm({
|
||||||
accounts,
|
// accounts,
|
||||||
editExpense,
|
// editExpense,
|
||||||
submitExpense,
|
// submitExpense,
|
||||||
expenseDetails,
|
// expenseDetails,
|
||||||
currencies
|
// currencies
|
||||||
}) {
|
// }) {
|
||||||
const {formatMessage} = useIntl();
|
// const {formatMessage} = useIntl();
|
||||||
|
|
||||||
const [state, setState] = useState({
|
// const [state, setState] = useState({
|
||||||
selectedExpenseAccount: null,
|
// selectedExpenseAccount: null,
|
||||||
selectedPaymentAccount: null
|
// selectedPaymentAccount: null
|
||||||
});
|
// });
|
||||||
const validationSchema = Yup.object().shape({
|
// const validationSchema = Yup.object().shape({
|
||||||
date: Yup.date().required().label(formatMessage({id:'date'})),
|
// date: Yup.date().required().label(formatMessage({id:'date'})),
|
||||||
description: Yup.string().trim().label(formatMessage({id:'description'})),
|
// description: Yup.string().trim().label(formatMessage({id:'description'})),
|
||||||
expense_account_id: Yup.number().required().label(formatMessage({id:'expense_account_id'})),
|
// expense_account_id: Yup.number().required().label(formatMessage({id:'expense_account_id'})),
|
||||||
payment_account_id: Yup.number().required().label(formatMessage({id:'payment_account_id'})),
|
// payment_account_id: Yup.number().required().label(formatMessage({id:'payment_account_id'})),
|
||||||
amount: Yup.number().required().label(formatMessage({id:'amount'})),
|
// amount: Yup.number().required().label(formatMessage({id:'amount'})),
|
||||||
currency_code: Yup.string().required().label(formatMessage({id:'currency_code_'})),
|
// currency_code: Yup.string().required().label(formatMessage({id:'currency_code_'})),
|
||||||
publish: Yup.boolean().label(formatMessage({id:'publish'})),
|
// publish: Yup.boolean().label(formatMessage({id:'publish'})),
|
||||||
exchange_rate: Yup.number().label(formatMessage({id:'exchange_rate_'}))
|
// exchange_rate: Yup.number().label(formatMessage({id:'exchange_rate_'}))
|
||||||
});
|
// });
|
||||||
|
|
||||||
const formik = useFormik({
|
// const formik = useFormik({
|
||||||
enableReinitialize: true,
|
// enableReinitialize: true,
|
||||||
validationSchema: validationSchema,
|
// validationSchema: validationSchema,
|
||||||
initialValues: {
|
// initialValues: {
|
||||||
date: null
|
// date: null
|
||||||
},
|
// },
|
||||||
onSubmit: values => {
|
// onSubmit: values => {
|
||||||
submitExpense(values)
|
// submitExpense(values)
|
||||||
.then(response => {
|
// .then(response => {
|
||||||
AppToaster.show({
|
// AppToaster.show({
|
||||||
message: formatMessage({id:'the_expense_has_been_successfully_created'})
|
// message: formatMessage({id:'the_expense_has_been_successfully_created'})
|
||||||
});
|
// });
|
||||||
})
|
// })
|
||||||
.catch(error => {});
|
// .catch(error => {});
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
|
|
||||||
// Account item of select accounts field.
|
// // Account item of select accounts field.
|
||||||
const accountItem = (item, { handleClick, modifiers, query }) => {
|
// const accountItem = (item, { handleClick, modifiers, query }) => {
|
||||||
return (
|
// return (
|
||||||
<MenuItem
|
// <MenuItem
|
||||||
text={item.name}
|
// text={item.name}
|
||||||
label={item.code}
|
// label={item.code}
|
||||||
key={item.id}
|
// key={item.id}
|
||||||
onClick={handleClick}
|
// onClick={handleClick}
|
||||||
/>
|
// />
|
||||||
);
|
// );
|
||||||
};
|
// };
|
||||||
|
|
||||||
const onChangeAccount = () => {};
|
// const onChangeAccount = () => {};
|
||||||
|
|
||||||
const onChangePaymentAccount = () => {};
|
// const onChangePaymentAccount = () => {};
|
||||||
|
|
||||||
const handleDateChange = date => {
|
// const handleDateChange = date => {
|
||||||
const formatted = moment(date).format('YYYY/MM/DD');
|
// const formatted = moment(date).format('YYYY/MM/DD');
|
||||||
formik.setFieldValue('date', formatted);
|
// formik.setFieldValue('date', formatted);
|
||||||
};
|
// };
|
||||||
|
|
||||||
// Filters accounts items.
|
// // Filters accounts items.
|
||||||
const filterAccountsPredicater = (query, account, _index, exactMatch) => {
|
// const filterAccountsPredicater = (query, account, _index, exactMatch) => {
|
||||||
const normalizedTitle = account.name.toLowerCase();
|
// const normalizedTitle = account.name.toLowerCase();
|
||||||
const normalizedQuery = query.toLowerCase();
|
// const normalizedQuery = query.toLowerCase();
|
||||||
|
|
||||||
if (exactMatch) {
|
// if (exactMatch) {
|
||||||
return normalizedTitle === normalizedQuery;
|
// return normalizedTitle === normalizedQuery;
|
||||||
} else {
|
// } else {
|
||||||
return `${account.code} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
|
// return `${account.code} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
const onExpenseAccountSelect = account => {
|
// const onExpenseAccountSelect = account => {
|
||||||
setState({ ...state, selectedExpenseAccount: account });
|
// setState({ ...state, selectedExpenseAccount: account });
|
||||||
formik.setFieldValue('expense_account_id', account.id);
|
// formik.setFieldValue('expense_account_id', account.id);
|
||||||
};
|
// };
|
||||||
|
|
||||||
const onChangePaymentAccountSelect = account => {
|
// const onChangePaymentAccountSelect = account => {
|
||||||
setState({ ...state, selectedPaymentAccount: account });
|
// setState({ ...state, selectedPaymentAccount: account });
|
||||||
formik.setFieldValue('payment_account_id', account.id);
|
// formik.setFieldValue('payment_account_id', account.id);
|
||||||
};
|
// };
|
||||||
|
|
||||||
const onAmountCurrencySelect = currency => {
|
// const onAmountCurrencySelect = currency => {
|
||||||
formik.setFieldValue('currency_code', currency.id);
|
// formik.setFieldValue('currency_code', currency.id);
|
||||||
};
|
// };
|
||||||
|
|
||||||
const paymentAccountLabel = state.selectedPaymentAccount
|
// const paymentAccountLabel = state.selectedPaymentAccount
|
||||||
? state.selectedPaymentAccount.name
|
// ? state.selectedPaymentAccount.name
|
||||||
: <T id={'select_payment_account'}/>;
|
// : <T id={'select_payment_account'}/>;
|
||||||
|
|
||||||
const expenseAccountLabel = state.selectedExpenseAccount
|
// const expenseAccountLabel = state.selectedExpenseAccount
|
||||||
? state.selectedExpenseAccount.name
|
// ? state.selectedExpenseAccount.name
|
||||||
: <T id={'select_expense_account'}/>;
|
// : <T id={'select_expense_account'}/>;
|
||||||
|
|
||||||
const handleClose = () => {};
|
// const handleClose = () => {};
|
||||||
|
|
||||||
return (
|
// return (
|
||||||
<div class='expense-form'>
|
// <div class='expense-form'>
|
||||||
<form onSubmit={formik.handleSubmit}>
|
// <form onSubmit={formik.handleSubmit}>
|
||||||
<FormGroup
|
// <FormGroup
|
||||||
label={<T id={'date'}/>}
|
// label={<T id={'date'}/>}
|
||||||
inline={true}
|
// inline={true}
|
||||||
intent={formik.errors.date && Intent.DANGER}
|
// intent={formik.errors.date && Intent.DANGER}
|
||||||
helperText={formik.errors.date && formik.errors.date}
|
// helperText={formik.errors.date && formik.errors.date}
|
||||||
>
|
// >
|
||||||
<DateInput
|
// <DateInput
|
||||||
{...momentFormatter('YYYY/MM/DD')}
|
// {...momentFormatter('YYYY/MM/DD')}
|
||||||
defaultValue={new Date()}
|
// defaultValue={new Date()}
|
||||||
onChange={handleDateChange}
|
// onChange={handleDateChange}
|
||||||
popoverProps={{ position: Position.BOTTOM }}
|
// popoverProps={{ position: Position.BOTTOM }}
|
||||||
/>
|
// />
|
||||||
</FormGroup>
|
// </FormGroup>
|
||||||
|
|
||||||
<FormGroup
|
// <FormGroup
|
||||||
label={<T id={'expense_account'}/>}
|
// label={<T id={'expense_account'}/>}
|
||||||
className={'form-group--expense-account'}
|
// className={'form-group--expense-account'}
|
||||||
inline={true}
|
// inline={true}
|
||||||
intent={formik.errors.expense_account_id && Intent.DANGER}
|
// intent={formik.errors.expense_account_id && Intent.DANGER}
|
||||||
helperText={
|
// helperText={
|
||||||
formik.errors.expense_account_id && formik.errors.expense_account_id
|
// formik.errors.expense_account_id && formik.errors.expense_account_id
|
||||||
}
|
// }
|
||||||
>
|
// >
|
||||||
<Select
|
// <Select
|
||||||
items={accounts}
|
// items={accounts}
|
||||||
itemRenderer={accountItem}
|
// itemRenderer={accountItem}
|
||||||
itemPredicate={filterAccountsPredicater}
|
// itemPredicate={filterAccountsPredicater}
|
||||||
popoverProps={{ minimal: true }}
|
// popoverProps={{ minimal: true }}
|
||||||
onItemSelect={onExpenseAccountSelect}
|
// onItemSelect={onExpenseAccountSelect}
|
||||||
>
|
// >
|
||||||
<Button
|
// <Button
|
||||||
fill={true}
|
// fill={true}
|
||||||
rightIcon='caret-down'
|
// rightIcon='caret-down'
|
||||||
text={expenseAccountLabel}
|
// text={expenseAccountLabel}
|
||||||
/>
|
// />
|
||||||
</Select>
|
// </Select>
|
||||||
</FormGroup>
|
// </FormGroup>
|
||||||
|
|
||||||
<FormGroup
|
// <FormGroup
|
||||||
label={<T id={'amount'}/>}
|
// label={<T id={'amount'}/>}
|
||||||
className={'form-group--amount'}
|
// className={'form-group--amount'}
|
||||||
intent={formik.errors.amount && Intent.DANGER}
|
// intent={formik.errors.amount && Intent.DANGER}
|
||||||
helperText={formik.errors.amount && formik.errors.amount}
|
// helperText={formik.errors.amount && formik.errors.amount}
|
||||||
inline={true}
|
// inline={true}
|
||||||
>
|
// >
|
||||||
<ControlGroup>
|
// <ControlGroup>
|
||||||
<Select
|
// <Select
|
||||||
items={currencies.map(c => ({
|
// items={currencies.map(c => ({
|
||||||
id: c.currency_code,
|
// id: c.currency_code,
|
||||||
name: c.currency_code
|
// name: c.currency_code
|
||||||
}))}
|
// }))}
|
||||||
itemRenderer={accountItem}
|
// itemRenderer={accountItem}
|
||||||
itemPredicate={filterAccountsPredicater}
|
// itemPredicate={filterAccountsPredicater}
|
||||||
popoverProps={{ minimal: true }}
|
// popoverProps={{ minimal: true }}
|
||||||
onItemSelect={onAmountCurrencySelect}
|
// onItemSelect={onAmountCurrencySelect}
|
||||||
>
|
// >
|
||||||
<Button
|
// <Button
|
||||||
rightIcon='caret-down'
|
// rightIcon='caret-down'
|
||||||
text={formik.values.currency_code}
|
// text={formik.values.currency_code}
|
||||||
/>
|
// />
|
||||||
</Select>
|
// </Select>
|
||||||
|
|
||||||
<InputGroup
|
// <InputGroup
|
||||||
medium={true}
|
// medium={true}
|
||||||
intent={formik.errors.amount && Intent.DANGER}
|
// intent={formik.errors.amount && Intent.DANGER}
|
||||||
{...formik.getFieldProps('amount')}
|
// {...formik.getFieldProps('amount')}
|
||||||
/>
|
// />
|
||||||
</ControlGroup>
|
// </ControlGroup>
|
||||||
</FormGroup>
|
// </FormGroup>
|
||||||
|
|
||||||
<FormGroup
|
// <FormGroup
|
||||||
label={<T id={'exchange_rate'}/>}
|
// label={<T id={'exchange_rate'}/>}
|
||||||
className={'form-group--exchange-rate'}
|
// className={'form-group--exchange-rate'}
|
||||||
inline={true}
|
// inline={true}
|
||||||
>
|
// >
|
||||||
<InputGroup />
|
// <InputGroup />
|
||||||
</FormGroup>
|
// </FormGroup>
|
||||||
|
|
||||||
<FormGroup
|
// <FormGroup
|
||||||
label={<T id={'payment_account'}/>}
|
// label={<T id={'payment_account'}/>}
|
||||||
className={'form-group--payment-account'}
|
// className={'form-group--payment-account'}
|
||||||
inline={true}
|
// inline={true}
|
||||||
intent={formik.errors.payment_account_id && Intent.DANGER}
|
// intent={formik.errors.payment_account_id && Intent.DANGER}
|
||||||
helperText={
|
// helperText={
|
||||||
formik.errors.payment_account_id && formik.errors.payment_account_id
|
// formik.errors.payment_account_id && formik.errors.payment_account_id
|
||||||
}
|
// }
|
||||||
>
|
// >
|
||||||
<Select
|
// <Select
|
||||||
items={accounts}
|
// items={accounts}
|
||||||
itemRenderer={accountItem}
|
// itemRenderer={accountItem}
|
||||||
itemPredicate={filterAccountsPredicater}
|
// itemPredicate={filterAccountsPredicater}
|
||||||
popoverProps={{ minimal: true }}
|
// popoverProps={{ minimal: true }}
|
||||||
onItemSelect={onChangePaymentAccountSelect}
|
// onItemSelect={onChangePaymentAccountSelect}
|
||||||
>
|
// >
|
||||||
<Button
|
// <Button
|
||||||
fill={true}
|
// fill={true}
|
||||||
rightIcon='caret-down'
|
// rightIcon='caret-down'
|
||||||
text={paymentAccountLabel}
|
// text={paymentAccountLabel}
|
||||||
/>
|
// />
|
||||||
</Select>
|
// </Select>
|
||||||
</FormGroup>
|
// </FormGroup>
|
||||||
|
|
||||||
<FormGroup
|
// <FormGroup
|
||||||
label={<T id={'description'}/>}
|
// label={<T id={'description'}/>}
|
||||||
className={'form-group--description'}
|
// className={'form-group--description'}
|
||||||
inline={true}
|
// inline={true}
|
||||||
>
|
// >
|
||||||
<TextArea
|
// <TextArea
|
||||||
growVertically={true}
|
// growVertically={true}
|
||||||
large={true}
|
// large={true}
|
||||||
{...formik.getFieldProps('description')}
|
// {...formik.getFieldProps('description')}
|
||||||
/>
|
// />
|
||||||
</FormGroup>
|
// </FormGroup>
|
||||||
|
|
||||||
<div class='form__floating-footer'>
|
// <div class='form__floating-footer'>
|
||||||
<Button intent={Intent.PRIMARY} type='submit'>
|
// <Button intent={Intent.PRIMARY} type='submit'>
|
||||||
<T id={'save'}/>
|
// <T id={'save'}/>
|
||||||
</Button>
|
// </Button>
|
||||||
<Button><T id={'save_as_draft'}/></Button>
|
// <Button><T id={'save_as_draft'}/></Button>
|
||||||
<Button onClick={handleClose}><T id={'close'}/></Button>
|
// <Button onClick={handleClose}><T id={'close'}/></Button>
|
||||||
</div>
|
// </div>
|
||||||
</form>
|
// </form>
|
||||||
</div>
|
// </div>
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|||||||
@@ -1,82 +1,82 @@
|
|||||||
import React from 'react';
|
// import React from 'react';
|
||||||
import {
|
// import {
|
||||||
Button,
|
// Button,
|
||||||
AnchorButton,
|
// AnchorButton,
|
||||||
Classes,
|
// Classes,
|
||||||
NavbarGroup,
|
// NavbarGroup,
|
||||||
Popover,
|
// Popover,
|
||||||
MenuItem,
|
// MenuItem,
|
||||||
PopoverInteractionKind,
|
// PopoverInteractionKind,
|
||||||
Position,
|
// Position,
|
||||||
Menu,
|
// Menu,
|
||||||
NavbarDivider,
|
// NavbarDivider,
|
||||||
Intent,
|
// Intent,
|
||||||
} from '@blueprintjs/core';
|
// } from '@blueprintjs/core';
|
||||||
import { useRouteMatch } from 'react-router-dom'
|
// import { useRouteMatch } from 'react-router-dom'
|
||||||
import { FormattedMessage as T } from 'react-intl';
|
// import { FormattedMessage as T } from 'react-intl';
|
||||||
import classNames from 'classnames';
|
// import classNames from 'classnames';
|
||||||
|
|
||||||
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
|
// import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
|
||||||
import Icon from 'components/Icon';
|
// import Icon from 'components/Icon';
|
||||||
|
|
||||||
export default function ExpensesActionsBar() {
|
// export default function ExpensesActionsBar() {
|
||||||
const {path} = useRouteMatch();
|
// const {path} = useRouteMatch();
|
||||||
const onClickNewAccount = () => {};
|
// const onClickNewAccount = () => {};
|
||||||
const views = [];
|
// const views = [];
|
||||||
|
|
||||||
const viewsMenuItems = views.map((view) => {
|
// const viewsMenuItems = views.map((view) => {
|
||||||
return (<MenuItem href={`${path}/${view.id}/custom_view`} text={view.name} />);
|
// return (<MenuItem href={`${path}/${view.id}/custom_view`} text={view.name} />);
|
||||||
});
|
// });
|
||||||
return (
|
// return (
|
||||||
<DashboardActionsBar>
|
// <DashboardActionsBar>
|
||||||
<NavbarGroup>
|
// <NavbarGroup>
|
||||||
<Popover
|
// <Popover
|
||||||
content={<Menu>{viewsMenuItems}</Menu>}
|
// content={<Menu>{viewsMenuItems}</Menu>}
|
||||||
minimal={true}
|
// minimal={true}
|
||||||
interactionKind={PopoverInteractionKind.HOVER}
|
// interactionKind={PopoverInteractionKind.HOVER}
|
||||||
position={Position.BOTTOM_LEFT}
|
// position={Position.BOTTOM_LEFT}
|
||||||
>
|
// >
|
||||||
<Button
|
// <Button
|
||||||
className={classNames(Classes.MINIMAL, 'button--table-views')}
|
// className={classNames(Classes.MINIMAL, 'button--table-views')}
|
||||||
icon={<Icon icon='table' />}
|
// icon={<Icon icon='table' />}
|
||||||
text={<T id={'table_views'}/>}
|
// text={<T id={'table_views'}/>}
|
||||||
rightIcon={'caret-down'}
|
// rightIcon={'caret-down'}
|
||||||
/>
|
// />
|
||||||
</Popover>
|
// </Popover>
|
||||||
|
|
||||||
<NavbarDivider />
|
// <NavbarDivider />
|
||||||
|
|
||||||
<AnchorButton
|
// <AnchorButton
|
||||||
className={Classes.MINIMAL}
|
// className={Classes.MINIMAL}
|
||||||
icon={<Icon icon='plus' />}
|
// icon={<Icon icon='plus' />}
|
||||||
href='/expenses/new'
|
// href='/expenses/new'
|
||||||
text={<T id={'new_expense'}/>}
|
// text={<T id={'new_expense'}/>}
|
||||||
onClick={onClickNewAccount}
|
// onClick={onClickNewAccount}
|
||||||
/>
|
// />
|
||||||
<Button
|
// <Button
|
||||||
className={Classes.MINIMAL}
|
// className={Classes.MINIMAL}
|
||||||
intent={Intent.DANGER}
|
// intent={Intent.DANGER}
|
||||||
icon={<Icon icon='plus' />}
|
// icon={<Icon icon='plus' />}
|
||||||
text={<T id={'delete'}/>}
|
// text={<T id={'delete'}/>}
|
||||||
onClick={onClickNewAccount}
|
// onClick={onClickNewAccount}
|
||||||
/>
|
// />
|
||||||
<Button
|
// <Button
|
||||||
className={Classes.MINIMAL}
|
// className={Classes.MINIMAL}
|
||||||
icon={<Icon icon='plus' />}
|
// icon={<Icon icon='plus' />}
|
||||||
text={<T id={'bulk_update'}/>}
|
// text={<T id={'bulk_update'}/>}
|
||||||
onClick={onClickNewAccount}
|
// onClick={onClickNewAccount}
|
||||||
/>
|
// />
|
||||||
<Button
|
// <Button
|
||||||
className={Classes.MINIMAL}
|
// className={Classes.MINIMAL}
|
||||||
icon={<Icon icon='file-import' />}
|
// icon={<Icon icon='file-import' />}
|
||||||
text={<T id={'import'}/>}
|
// text={<T id={'import'}/>}
|
||||||
/>
|
// />
|
||||||
<Button
|
// <Button
|
||||||
className={Classes.MINIMAL}
|
// className={Classes.MINIMAL}
|
||||||
icon={<Icon icon='file-export' />}
|
// icon={<Icon icon='file-export' />}
|
||||||
text={<T id={'export'}/>}
|
// text={<T id={'export'}/>}
|
||||||
/>
|
// />
|
||||||
</NavbarGroup>
|
// </NavbarGroup>
|
||||||
</DashboardActionsBar>
|
// </DashboardActionsBar>
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|||||||
@@ -1,92 +1,92 @@
|
|||||||
import React from 'react';
|
// import React from 'react';
|
||||||
import {
|
// import {
|
||||||
GridComponent,
|
// GridComponent,
|
||||||
ColumnsDirective,
|
// ColumnsDirective,
|
||||||
ColumnDirective,
|
// ColumnDirective,
|
||||||
Inject,
|
// Inject,
|
||||||
Sort,
|
// Sort,
|
||||||
|
|
||||||
} from '@syncfusion/ej2-react-grids';
|
// } from '@syncfusion/ej2-react-grids';
|
||||||
import {
|
// import {
|
||||||
Checkbox,
|
// Checkbox,
|
||||||
Popover,
|
// Popover,
|
||||||
Button,
|
// Button,
|
||||||
Menu,
|
// Menu,
|
||||||
MenuItem,
|
// MenuItem,
|
||||||
MenuDivider,
|
// MenuDivider,
|
||||||
Position,
|
// Position,
|
||||||
} from '@blueprintjs/core';
|
// } from '@blueprintjs/core';
|
||||||
import Icon from 'components/Icon';
|
// import Icon from 'components/Icon';
|
||||||
import moment from 'moment';
|
// import moment from 'moment';
|
||||||
|
|
||||||
export default function ExpensesTable({
|
// export default function ExpensesTable({
|
||||||
expenses,
|
// expenses,
|
||||||
onDeleteExpense,
|
// onDeleteExpense,
|
||||||
onEditExpense,
|
// onEditExpense,
|
||||||
}) {
|
// }) {
|
||||||
const onDateStateChange = () => {
|
// const onDateStateChange = () => {
|
||||||
|
|
||||||
}
|
// }
|
||||||
|
|
||||||
const actionMenuList = (expense) => (
|
// const actionMenuList = (expense) => (
|
||||||
<Menu>
|
// <Menu>
|
||||||
<MenuItem text="View Details" />
|
// <MenuItem text="View Details" />
|
||||||
<MenuDivider />
|
// <MenuDivider />
|
||||||
<MenuItem text="Edit Expense" onClick={() => onEditExpense(expense)} />
|
// <MenuItem text="Edit Expense" onClick={() => onEditExpense(expense)} />
|
||||||
<MenuItem text="Delete Expense" onClick={() => onDeleteExpense(expense)} />
|
// <MenuItem text="Delete Expense" onClick={() => onDeleteExpense(expense)} />
|
||||||
</Menu>
|
// </Menu>
|
||||||
);
|
// );
|
||||||
const columns = [
|
// const columns = [
|
||||||
{
|
// {
|
||||||
headerText: '',
|
// headerText: '',
|
||||||
template: () => (<Checkbox />)
|
// template: () => (<Checkbox />)
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
headerText: 'Date',
|
// headerText: 'Date',
|
||||||
template: (row) => (<span>{ moment(row.date).format('YYYY/MM/DD') }</span>),
|
// template: (row) => (<span>{ moment(row.date).format('YYYY/MM/DD') }</span>),
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
headerText: 'Expense Account',
|
// headerText: 'Expense Account',
|
||||||
template: (row) => (<span>{ row.expenseAccount.name }</span>),
|
// template: (row) => (<span>{ row.expenseAccount.name }</span>),
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
headerText: 'Paid Through',
|
// headerText: 'Paid Through',
|
||||||
template: (row) => (<span>{ row.paymentAccount.name }</span>),
|
// template: (row) => (<span>{ row.paymentAccount.name }</span>),
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
headerText: 'Amount',
|
// headerText: 'Amount',
|
||||||
field: 'amount'
|
// field: 'amount'
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
headerText: 'Status',
|
// headerText: 'Status',
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
headerText: '',
|
// headerText: '',
|
||||||
template: (expense) => (
|
// template: (expense) => (
|
||||||
<Popover content={actionMenuList(expense)} position={Position.RIGHT_BOTTOM}>
|
// <Popover content={actionMenuList(expense)} position={Position.RIGHT_BOTTOM}>
|
||||||
<Button icon={<Icon icon="ellipsis-h" />} />
|
// <Button icon={<Icon icon="ellipsis-h" />} />
|
||||||
</Popover>
|
// </Popover>
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
]
|
// ]
|
||||||
return (
|
// return (
|
||||||
<GridComponent
|
// <GridComponent
|
||||||
allowSorting={true}
|
// allowSorting={true}
|
||||||
dataSource={{ result: expenses, count: 20 }}
|
// dataSource={{ result: expenses, count: 20 }}
|
||||||
dataStateChange={onDateStateChange}>
|
// dataStateChange={onDateStateChange}>
|
||||||
|
|
||||||
<ColumnsDirective>
|
// <ColumnsDirective>
|
||||||
{columns.map((column) => {
|
// {columns.map((column) => {
|
||||||
return (<ColumnDirective
|
// return (<ColumnDirective
|
||||||
field={column.field}
|
// field={column.field}
|
||||||
headerText={column.headerText}
|
// headerText={column.headerText}
|
||||||
template={column.template}
|
// template={column.template}
|
||||||
allowSorting={true}
|
// allowSorting={true}
|
||||||
customAttributes={column.customAttributes}
|
// customAttributes={column.customAttributes}
|
||||||
/>);
|
// />);
|
||||||
})}
|
// })}
|
||||||
</ColumnsDirective>
|
// </ColumnsDirective>
|
||||||
<Inject services={[Sort]} />
|
// <Inject services={[Sort]} />
|
||||||
</GridComponent>
|
// </GridComponent>
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
@@ -1,51 +1,51 @@
|
|||||||
import React from 'react';
|
// import React from 'react';
|
||||||
import {useHistory} from 'react-router';
|
// import {useHistory} from 'react-router';
|
||||||
import {connect} from 'react-redux';
|
// import {connect} from 'react-redux';
|
||||||
import {
|
// import {
|
||||||
Alignment,
|
// Alignment,
|
||||||
Navbar,
|
// Navbar,
|
||||||
NavbarGroup,
|
// NavbarGroup,
|
||||||
Tabs,
|
// Tabs,
|
||||||
Tab,
|
// Tab,
|
||||||
Button
|
// Button
|
||||||
} from "@blueprintjs/core";
|
// } from "@blueprintjs/core";
|
||||||
import Icon from 'components/Icon';
|
// import Icon from 'components/Icon';
|
||||||
import {useRouteMatch, Link} from 'react-router-dom';
|
// import {useRouteMatch, Link} from 'react-router-dom';
|
||||||
|
|
||||||
function AccountsViewsTabs({ views }) {
|
// function AccountsViewsTabs({ views }) {
|
||||||
const history = useHistory();
|
// const history = useHistory();
|
||||||
const {path} = useRouteMatch();
|
// const {path} = useRouteMatch();
|
||||||
|
|
||||||
const handleClickNewView = () => {
|
// const handleClickNewView = () => {
|
||||||
history.push('/custom_views/new');
|
// history.push('/custom_views/new');
|
||||||
};
|
// };
|
||||||
|
|
||||||
const tabs = views.map((view) => {
|
// const tabs = views.map((view) => {
|
||||||
const link = (<Link to={`${path}/${view.id}/custom_view`}>{ view.name }</Link>);
|
// const link = (<Link to={`${path}/${view.id}/custom_view`}>{ view.name }</Link>);
|
||||||
return (<Tab id={`custom_view_${view.id}`} title={link} />);
|
// return (<Tab id={`custom_view_${view.id}`} title={link} />);
|
||||||
});
|
// });
|
||||||
return (
|
// return (
|
||||||
<Navbar className="navbar--dashboard-views">
|
// <Navbar className="navbar--dashboard-views">
|
||||||
<NavbarGroup
|
// <NavbarGroup
|
||||||
align={Alignment.LEFT}>
|
// align={Alignment.LEFT}>
|
||||||
<Tabs
|
// <Tabs
|
||||||
id="navbar"
|
// id="navbar"
|
||||||
large={true}
|
// large={true}
|
||||||
className="tabs--dashboard-views"
|
// className="tabs--dashboard-views"
|
||||||
>
|
// >
|
||||||
{ tabs }
|
// { tabs }
|
||||||
<Button
|
// <Button
|
||||||
className="button--new-view"
|
// className="button--new-view"
|
||||||
icon={<Icon icon="plus" />}
|
// icon={<Icon icon="plus" />}
|
||||||
onClick={handleClickNewView} />
|
// onClick={handleClickNewView} />
|
||||||
</Tabs>
|
// </Tabs>
|
||||||
</NavbarGroup>
|
// </NavbarGroup>
|
||||||
</Navbar>
|
// </Navbar>
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
const mapStateToProps = (state) => ({
|
// const mapStateToProps = (state) => ({
|
||||||
views: state.views.resourceViews['expenses'],
|
// views: state.views.resourceViews['expenses'],
|
||||||
});
|
// });
|
||||||
|
|
||||||
export default connect(mapStateToProps)(AccountsViewsTabs);
|
// export default connect(mapStateToProps)(AccountsViewsTabs);
|
||||||
@@ -119,11 +119,11 @@ export default [
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
text: <T id={'expenses'}/>,
|
text: <T id={'expenses'}/>,
|
||||||
href: '/expenses',
|
href: '/expenses/new',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: <T id={'new_expenses'}/>,
|
text: <T id={'expenses_list'}/>,
|
||||||
href: '/expenses/new',
|
href: '/expenses-list',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -327,7 +327,7 @@ const handleConfirmBulkActivate = useCallback(() => {
|
|||||||
onEditAccount={handleEditAccount}
|
onEditAccount={handleEditAccount}
|
||||||
onFetchData={handleFetchData}
|
onFetchData={handleFetchData}
|
||||||
onSelectedRowsChange={handleSelectedRowsChange}
|
onSelectedRowsChange={handleSelectedRowsChange}
|
||||||
loading={tableLoading}
|
|
||||||
/>
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import {
|
|||||||
fetchAccount,
|
fetchAccount,
|
||||||
deleteBulkAccounts,
|
deleteBulkAccounts,
|
||||||
bulkActivateAccounts,
|
bulkActivateAccounts,
|
||||||
bulkInactiveAccounts
|
bulkInactiveAccounts,
|
||||||
|
editAccount
|
||||||
} from 'store/accounts/accounts.actions';
|
} from 'store/accounts/accounts.actions';
|
||||||
|
|
||||||
const mapActionsToProps = (dispatch) => ({
|
const mapActionsToProps = (dispatch) => ({
|
||||||
@@ -23,6 +24,7 @@ const mapActionsToProps = (dispatch) => ({
|
|||||||
requestDeleteBulkAccounts: (ids) => dispatch(deleteBulkAccounts({ ids })),
|
requestDeleteBulkAccounts: (ids) => dispatch(deleteBulkAccounts({ ids })),
|
||||||
requestBulkActivateAccounts:(ids)=>dispatch(bulkActivateAccounts({ids})),
|
requestBulkActivateAccounts:(ids)=>dispatch(bulkActivateAccounts({ids})),
|
||||||
requestBulkInactiveAccounts:(ids)=>dispatch(bulkInactiveAccounts({ids})),
|
requestBulkInactiveAccounts:(ids)=>dispatch(bulkInactiveAccounts({ids})),
|
||||||
|
requestEditAccount:({id,form}) => dispatch(editAccount({id,form}))
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(null, mapActionsToProps);
|
export default connect(null, mapActionsToProps);
|
||||||
@@ -14,7 +14,7 @@ import * as Yup from 'yup';
|
|||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import { FormattedMessage as T, useIntl } from 'react-intl';
|
import { FormattedMessage as T, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { omit } from 'lodash';
|
import { omit, pick } from 'lodash';
|
||||||
import { useQuery, queryCache } from 'react-query';
|
import { useQuery, queryCache } from 'react-query';
|
||||||
|
|
||||||
import Dialog from 'components/Dialog';
|
import Dialog from 'components/Dialog';
|
||||||
@@ -27,7 +27,6 @@ import Icon from 'components/Icon';
|
|||||||
import ErrorMessage from 'components/ErrorMessage';
|
import ErrorMessage from 'components/ErrorMessage';
|
||||||
import { ListSelect } from 'components';
|
import { ListSelect } from 'components';
|
||||||
|
|
||||||
|
|
||||||
function AccountFormDialog({
|
function AccountFormDialog({
|
||||||
name,
|
name,
|
||||||
payload,
|
payload,
|
||||||
@@ -105,29 +104,39 @@ function AccountFormDialog({
|
|||||||
if (payload.action === 'edit') {
|
if (payload.action === 'edit') {
|
||||||
requestEditAccount({
|
requestEditAccount({
|
||||||
payload: payload.id,
|
payload: payload.id,
|
||||||
form: { ...omit(values, [...exclude, 'account_type_id']) },
|
// form: { ...omit(values, [...exclude, 'account_type_id']) },
|
||||||
}).then((response) => {
|
form: {
|
||||||
closeDialog(name);
|
...pick(values, [
|
||||||
queryCache.refetchQueries('accounts-table', { force: true });
|
...exclude,
|
||||||
|
'account_type_id',
|
||||||
|
'name',
|
||||||
|
'description',
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
closeDialog(name);
|
||||||
|
queryCache.refetchQueries('accounts-table', { force: true });
|
||||||
|
|
||||||
AppToaster.show({
|
AppToaster.show({
|
||||||
message: formatMessage(
|
message: formatMessage(
|
||||||
{ id: 'service_has_been_successful_edited', },
|
{ id: 'service_has_been_successful_edited' },
|
||||||
{
|
{
|
||||||
name: toastAccountName,
|
name: toastAccountName,
|
||||||
service: formatMessage({ id: 'account' }),
|
service: formatMessage({ id: 'account' }),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
intent: Intent.SUCCESS,
|
intent: Intent.SUCCESS,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((errors) => {
|
||||||
|
const errorsTransformed = transformApiErrors(errors);
|
||||||
|
setErrors({ ...errorsTransformed });
|
||||||
|
setSubmitting(false);
|
||||||
});
|
});
|
||||||
}).catch((errors) => {
|
|
||||||
const errorsTransformed = transformApiErrors(errors);
|
|
||||||
setErrors({ ...errorsTransformed });
|
|
||||||
setSubmitting(false);
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
requestSubmitAccount({ form: { ...omit(values, exclude) } }).then(
|
requestSubmitAccount({ form: { ...omit(values, exclude) } })
|
||||||
(response) => {
|
.then((response) => {
|
||||||
closeDialog(name);
|
closeDialog(name);
|
||||||
queryCache.refetchQueries('accounts-table', { force: true });
|
queryCache.refetchQueries('accounts-table', { force: true });
|
||||||
|
|
||||||
@@ -142,20 +151,24 @@ function AccountFormDialog({
|
|||||||
intent: Intent.SUCCESS,
|
intent: Intent.SUCCESS,
|
||||||
position: Position.BOTTOM,
|
position: Position.BOTTOM,
|
||||||
});
|
});
|
||||||
},
|
})
|
||||||
).catch((errors) => {
|
.catch((errors) => {
|
||||||
const errorsTransformed = transformApiErrors(errors);
|
const errorsTransformed = transformApiErrors(errors);
|
||||||
setErrors({ ...errorsTransformed });
|
setErrors({ ...errorsTransformed });
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Filtered accounts based on the given account type.
|
// Filtered accounts based on the given account type.
|
||||||
const filteredAccounts = useMemo(() => accounts.filter((account) =>
|
const filteredAccounts = useMemo(
|
||||||
account.account_type_id === values.account_type_id
|
() =>
|
||||||
), [accounts, values.account_type_id]);
|
accounts.filter(
|
||||||
|
(account) => account.account_type_id === values.account_type_id,
|
||||||
|
),
|
||||||
|
[accounts, values.account_type_id],
|
||||||
|
);
|
||||||
|
|
||||||
// Filters accounts types items.
|
// Filters accounts types items.
|
||||||
const filterAccountTypeItems = (query, accountType, _index, exactMatch) => {
|
const filterAccountTypeItems = (query, accountType, _index, exactMatch) => {
|
||||||
@@ -307,7 +320,9 @@ function AccountFormDialog({
|
|||||||
Classes.FILL,
|
Classes.FILL,
|
||||||
)}
|
)}
|
||||||
inline={true}
|
inline={true}
|
||||||
helperText={<ErrorMessage name="account_type_id" {...{ errors, touched }} />}
|
helperText={
|
||||||
|
<ErrorMessage name="account_type_id" {...{ errors, touched }} />
|
||||||
|
}
|
||||||
intent={
|
intent={
|
||||||
errors.account_type_id && touched.account_type_id && Intent.DANGER
|
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 React, {
|
||||||
import { useAsync } from 'react-use';
|
useMemo,
|
||||||
import {useParams} from 'react-router-dom';
|
useState,
|
||||||
import Connector from 'connectors/ExpenseForm.connector';
|
useEffect,
|
||||||
import DashboardInsider from 'components/Dashboard/DashboardInsider';
|
useRef,
|
||||||
import ExpenseForm from 'components/Expenses/ExpenseForm';
|
useCallback,
|
||||||
import { useIntl } from 'react-intl';
|
} 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({
|
import ExpenseFormHeader from './ExpenseFormHeader';
|
||||||
fetchAccounts,
|
import ExpenseTable from './ExpenseTable';
|
||||||
fetchCurrencies,
|
import ExpenseFooter from './ExpenseFooter';
|
||||||
accounts,
|
|
||||||
|
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,
|
changePageTitle,
|
||||||
submitExpense,
|
changePageSubtitle,
|
||||||
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]);
|
|
||||||
|
|
||||||
const fetchHook = useAsync(async () => {
|
//#withExpenseDetail
|
||||||
await Promise.all([
|
expenseDetail,
|
||||||
fetchAccounts(),
|
|
||||||
fetchCurrencies(),
|
// #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 (
|
return (
|
||||||
<DashboardInsider isLoading={fetchHook.loading} name={'expense-form'}>
|
<div className={'dashboard__insider--expense-form'}>
|
||||||
<ExpenseForm {...{submitExpense, editExpense, accounts, currencies} } />
|
<form onSubmit={formik.handleSubmit}>
|
||||||
</DashboardInsider>
|
<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 React, { useEffect, useState, useMemo, useCallback } from 'react';
|
||||||
import { useAsync } from 'react-use';
|
import { Route, Switch, useHistory, useParams } from 'react-router-dom';
|
||||||
|
import { useQuery } from 'react-query';
|
||||||
import { Alert, Intent } from '@blueprintjs/core';
|
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 AppToaster from 'components/AppToaster';
|
||||||
import { FormattedMessage as T, useIntl } from 'react-intl';
|
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({
|
function ExpensesList({
|
||||||
fetchExpenses,
|
// #withDashboardActions
|
||||||
deleteExpense,
|
changePageTitle,
|
||||||
// fetchViews,
|
|
||||||
expenses,
|
// #withViewsActions
|
||||||
getResourceViews,
|
requestFetchResourceViews,
|
||||||
changePageTitle
|
|
||||||
|
//#withExpensesActions
|
||||||
|
requestFetchExpensesTable,
|
||||||
|
requestDeleteExpense,
|
||||||
|
requestPublishExpense,
|
||||||
|
requestDeleteBulkExpenses,
|
||||||
|
addExpensesTableQueries,
|
||||||
|
requestFetchExpense,
|
||||||
}) {
|
}) {
|
||||||
const {formatMessage} =useIntl()
|
const history = useHistory();
|
||||||
useEffect(() => {
|
const { id } = useParams();
|
||||||
changePageTitle(formatMessage({id:'expenses_list'}));
|
const { formatMessage } = useIntl();
|
||||||
}, [changePageTitle,formatMessage]);
|
|
||||||
|
|
||||||
const [deleteExpenseState, setDeleteExpense] = useState();
|
const [deleteExpense, setDeleteExpense] = useState(false);
|
||||||
|
const [selectedRows, setSelectedRows] = useState([]);
|
||||||
|
const [bulkDelete, setBulkDelete] = useState(false);
|
||||||
|
|
||||||
const handleDeleteExpense = expense => {
|
const fetchViews = useQuery('expenses-resource-views', () => {
|
||||||
setDeleteExpense(expense);
|
return requestFetchResourceViews('expenses');
|
||||||
};
|
|
||||||
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 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 (
|
return (
|
||||||
<DashboardInsider loading={false}>
|
<DashboardInsider
|
||||||
<ExpensesActionsBar />
|
loading={fetchViews.isFetching || fetchExpenses.isFetching}
|
||||||
<ExpensesViewsTabs />
|
name={'expenses'}
|
||||||
|
>
|
||||||
|
<ExpenseActionsBar
|
||||||
|
onBulkDelete={handleBulkDelete}
|
||||||
|
selectedRows={selectedRows}
|
||||||
|
onFilterChanged={handleFilterChanged}
|
||||||
|
/>
|
||||||
|
|
||||||
<DashboardPageContent>
|
<DashboardPageContent>
|
||||||
<ExpensesTable
|
<Switch>
|
||||||
expenses={expenses}
|
<Route
|
||||||
onDeleteExpense={handleDeleteExpense}
|
// exact={true}
|
||||||
/>
|
// path={[
|
||||||
</DashboardPageContent>
|
// '/expenses/:custom_view_id/custom_view',
|
||||||
|
// '/expenses/new',
|
||||||
|
// ]}
|
||||||
|
>
|
||||||
|
<ExpenseViewTabs />
|
||||||
|
|
||||||
<Alert
|
<ExpenseDataTable
|
||||||
cancelButtonText={<T id={'cancel'}/>}
|
onDeleteExpense={handleDeleteExpense}
|
||||||
confirmButtonText={<T id={'move_to_trash'}/>}
|
onFetchData={handleFetchData}
|
||||||
icon='trash'
|
onEditExpense={handleEidtExpense}
|
||||||
intent={Intent.DANGER}
|
onPublishExpense={handlePublishExpense}
|
||||||
isOpen={deleteExpenseState}
|
onSelectedRowsChange={handleSelectedRowsChange}
|
||||||
onCancel={handleCancelAccountDelete}
|
/>
|
||||||
onConfirm={handleConfirmAccountDelete}
|
</Route>
|
||||||
>
|
</Switch>
|
||||||
<p>
|
|
||||||
Are you sure you want to move <b>filename</b> to Trash? You will be
|
<Alert
|
||||||
able to restore it later, but it will become private to you.
|
cancelButtonText={<T id={'cancel'} />}
|
||||||
</p>
|
confirmButtonText={<T id={'delete'} />}
|
||||||
</Alert>
|
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>
|
</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 journal has been successfully deleted',
|
||||||
the_manual_journal_id_has_been_published:
|
the_manual_journal_id_has_been_published:
|
||||||
'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',
|
credit: 'Credit',
|
||||||
debit: 'Debit',
|
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.`,
|
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',
|
table: 'Table',
|
||||||
nucleus: 'Nucleus',
|
nucleus: 'Nucleus',
|
||||||
logout: 'Logout',
|
logout: 'Logout',
|
||||||
the_expense_has_been_successfully_created:
|
|
||||||
'The expense has been successfully created.',
|
|
||||||
select_payment_account: 'Select Payment Account',
|
select_payment_account: 'Select Payment Account',
|
||||||
select_expense_account: 'Select Expense Account',
|
select_expense_account: 'Select Expense Account',
|
||||||
and: 'And',
|
and: 'And',
|
||||||
@@ -357,6 +356,10 @@ export default {
|
|||||||
'There is exchange rate in this date with the same currency.',
|
'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:
|
||||||
'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',
|
january: 'January',
|
||||||
february: 'February',
|
february: 'February',
|
||||||
march: 'March',
|
march: 'March',
|
||||||
@@ -417,4 +420,31 @@ export default {
|
|||||||
quick_new: 'Quick new',
|
quick_new: 'Quick new',
|
||||||
help: 'Help',
|
help: 'Help',
|
||||||
organization_id: 'Orgnization ID',
|
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',
|
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';
|
import t from 'store/types';
|
||||||
|
|
||||||
export const fetchExpensesList = ({ query }) => {
|
export const fetchExpensesTable = ({ query } = {}) => {
|
||||||
return (dispatch) => new Promise((resolve, reject) => {
|
return (dispatch, getState) =>
|
||||||
ApiService.get('expenses').then((response) => {
|
new Promise((resolve, reject) => {
|
||||||
|
const pageQuery = getState().expenses.tableQuery;
|
||||||
dispatch({
|
dispatch({
|
||||||
type: t.EXPENSES_LIST_SET,
|
type: t.SET_DASHBOARD_REQUEST_LOADING,
|
||||||
expenses: response.data.expenses,
|
|
||||||
});
|
});
|
||||||
}).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 }) => {
|
export const fetchExpense = ({ id }) => {
|
||||||
return (dispatch) => new Promise((resolve, reject) => {
|
return (dispatch) =>
|
||||||
ApiService.get(`expenses/${id}`).then((response) => {
|
new Promise((resolve, reject) => {
|
||||||
dispatch({
|
ApiService.get(`expenses/${id}`)
|
||||||
type: t.EXPENSE_SET,
|
.then((response) => {
|
||||||
expense: response.data.expense,
|
dispatch({
|
||||||
});
|
type: t.EXPENSE_SET,
|
||||||
}).catch(error => { reject(error); });
|
payload: {
|
||||||
});
|
id,
|
||||||
};
|
expense: response.data.expense,
|
||||||
|
},
|
||||||
export const submitExpense = ({ form }) => {
|
});
|
||||||
return (dispatch) => ApiService.post('expenses', { ...form });
|
resolve(response);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const editExpense = ({ form, id }) => {
|
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 }) => {
|
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 }) => {
|
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 { createReducer } from '@reduxjs/toolkit';
|
||||||
|
import { createTableQueryReducers } from 'store/queryReducers';
|
||||||
|
|
||||||
import t from 'store/types';
|
import t from 'store/types';
|
||||||
|
import { omit } from 'lodash';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
list: [],
|
items: {},
|
||||||
detailsById: {},
|
views: {},
|
||||||
|
loading: false,
|
||||||
|
currentViewId: -1,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default createReducer(initialState, {
|
const defaultExpense = {
|
||||||
[t.EXPENSES_LIST_SET]: (state, action) => {
|
categories: [],
|
||||||
state.list = action.expenses;
|
};
|
||||||
|
|
||||||
|
const reducer = createReducer(initialState, {
|
||||||
|
[t.EXPENSE_SET]: (state, action) => {
|
||||||
|
const { id, expense } = action.payload;
|
||||||
|
state.items[id] = { ...defaultExpense, ...expense };
|
||||||
},
|
},
|
||||||
|
|
||||||
[t.EXPENSE_SET]: (state, action) => {
|
[t.EXPENSE_PUBLISH]: (state, action) => {
|
||||||
state.detailsById[action.expense.id] = action.expense;
|
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) => {
|
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 {
|
export default {
|
||||||
EXPENSES_LIST_SET: 'EXPENSES_LIST_SET',
|
EXPENSES_LIST_SET: 'EXPENSES_LIST_SET',
|
||||||
EXPENSE_SET: 'EXPENSE_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;
|
||||||
|
|
||||||
|
&__header {
|
||||||
.dashboard__insider--expense-form{
|
padding: 25px 27px 20px;
|
||||||
padding: 40px 20px;
|
background: #fbfbfb;
|
||||||
|
width: 100%;
|
||||||
.#{$ns}-form-group{
|
.bp3-form-group {
|
||||||
margin-bottom: 22px;
|
.bp3-label {
|
||||||
|
margin-bottom: 15px;
|
||||||
.#{$ns}-label{
|
margin-right: 15px;
|
||||||
min-width: 130px;
|
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{
|
.form-group--payment_account {
|
||||||
width: 300px;
|
.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