feat: balance sheet report.

feat: trial balance sheet.
feat: general ledger report.
feat: journal report.
feat: profit/loss report.
This commit is contained in:
a.bouhuolia
2020-12-30 20:39:17 +02:00
parent de9f6d9521
commit 7ae73ed6cd
62 changed files with 2403 additions and 1850 deletions

View File

@@ -4,6 +4,7 @@ import { compose } from 'utils';
import { useQuery } from 'react-query';
import moment from 'moment';
import { useIntl } from 'react-intl';
import { queryCache } from 'react-query';
import BalanceSheetHeader from './BalanceSheetHeader';
import BalanceSheetTable from './BalanceSheetTable';
@@ -18,66 +19,74 @@ import withSettings from 'containers/Settings/withSettings';
import withBalanceSheetActions from './withBalanceSheetActions';
import withBalanceSheetDetail from './withBalanceSheetDetail';
import { transformFilterFormToQuery } from 'containers/FinancialStatements/common';
function BalanceSheet({
// #withDashboardActions
changePageTitle,
setDashboardBackLink,
// #withBalanceSheetActions
fetchBalanceSheet,
refreshBalanceSheet,
// #withBalanceSheetDetail
balanceSheetFilter,
balanceSheetRefresh,
// #withPreferences
organizationSettings,
organizationName,
}) {
const { formatMessage } = useIntl();
const [filter, setFilter] = useState({
from_date: moment().startOf('year').format('YYYY-MM-DD'),
to_date: moment().endOf('year').format('YYYY-MM-DD'),
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
toDate: moment().endOf('year').format('YYYY-MM-DD'),
basis: 'cash',
display_columns_type: 'total',
display_columns_by: '',
none_zero: false,
displayColumnsType: 'total',
accountsFilter: 'all-accounts',
});
const [refresh, setRefresh] = useState(true);
const fetchHook = useQuery(
['balance-sheet', filter],
(key, query) => fetchBalanceSheet({ ...query }),
{ manual: true },
// Fetches the balance sheet.
const fetchHook = useQuery(['balance-sheet', filter], (key, query) =>
fetchBalanceSheet({ ...transformFilterFormToQuery(query) }),
);
// Handle fetch the data of balance sheet.
const handleFetchData = useCallback(() => {
setRefresh(true);
}, []);
useEffect(() => {
changePageTitle(formatMessage({ id: 'balance_sheet' }));
}, [changePageTitle, formatMessage]);
// Observes the balance sheet refresh to invalid the query to refresh it.
useEffect(() => {
if (balanceSheetRefresh) {
queryCache.invalidateQueries('balance-sheet');
refreshBalanceSheet(false);
}
}, [balanceSheetRefresh, refreshBalanceSheet]);
useEffect(() => {
// Show the back link on dashboard topbar.
setDashboardBackLink(true);
return () => {
// Hide the back link on dashboard topbar.
setDashboardBackLink(false);
};
});
// Handle re-fetch balance sheet after filter change.
const handleFilterSubmit = useCallback(
(filter) => {
const _filter = {
...filter,
from_date: moment(filter.from_date).format('YYYY-MM-DD'),
to_date: moment(filter.to_date).format('YYYY-MM-DD'),
fromDate: moment(filter.fromDate).format('YYYY-MM-DD'),
toDate: moment(filter.toDate).format('YYYY-MM-DD'),
};
setFilter({ ..._filter });
setRefresh(true);
refreshBalanceSheet(true);
},
[setFilter],
[setFilter, refreshBalanceSheet],
);
useEffect(() => {
if (refresh) {
fetchHook.refetch({ force: true });
setRefresh(false);
}
}, [refresh]);
return (
<DashboardInsider>
<BalanceSheetActionsBar />
@@ -87,15 +96,9 @@ function BalanceSheet({
<BalanceSheetHeader
pageFilter={filter}
onSubmitFilter={handleFilterSubmit}
show={balanceSheetFilter}
/>
<div class="financial-statement__body">
<BalanceSheetTable
companyName={organizationSettings.name}
balanceSheetQuery={filter}
onFetchData={handleFetchData}
/>
<BalanceSheetTable companyName={organizationName} />
</div>
</FinancialStatement>
</DashboardPageContent>
@@ -106,8 +109,10 @@ function BalanceSheet({
export default compose(
withDashboardActions,
withBalanceSheetActions,
withBalanceSheetDetail(({ balanceSheetFilter }) => ({
balanceSheetFilter,
withBalanceSheetDetail(({ balanceSheetRefresh }) => ({
balanceSheetRefresh,
})),
withSettings(({ organizationSettings }) => ({
organizationName: organizationSettings.name,
})),
withSettings,
)(BalanceSheet);

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React from 'react';
import {
NavbarGroup,
Button,
@@ -13,26 +13,24 @@ import classNames from 'classnames';
import Icon from 'components/Icon';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import FilterDropdown from 'components/FilterDropdown';
import { If } from 'components';
import { compose } from 'utils';
import withBalanceSheetDetail from './withBalanceSheetDetail';
import withBalanceSheetActions from './withBalanceSheetActions';
function BalanceSheetActionsBar({
// #withBalanceSheetDetail
balanceSheetFilter,
// #withBalanceSheetActions
toggleBalanceSheetFilter,
refreshBalanceSheet
refreshBalanceSheet,
}) {
const handleFilterToggleClick = () => {
toggleBalanceSheetFilter();
};
// Handle recalculate the report button.
const handleRecalcReport = () => {
refreshBalanceSheet(true);
};
@@ -41,39 +39,21 @@ function BalanceSheetActionsBar({
<DashboardActionsBar>
<NavbarGroup>
<Button
className={classNames(Classes.MINIMAL, 'button--table-views')}
icon={<Icon icon="cog-16" iconSize={16} />}
text={<T id={'customize_report'} />}
/>
<NavbarDivider />
<Button
className={classNames(
Classes.MINIMAL,
'button--gray-highlight',
)}
className={classNames(Classes.MINIMAL, 'button--gray-highlight')}
text={<T id={'recalc_report'} />}
onClick={handleRecalcReport}
icon={<Icon icon="refresh-16" iconSize={16} />}
/>
<NavbarDivider />
<If condition={balanceSheetFilter}>
<Button
className={Classes.MINIMAL}
text={<T id={'hide_filter'} />}
onClick={handleFilterToggleClick}
icon={<Icon icon="arrow-to-top" />}
/>
</If>
<If condition={!balanceSheetFilter}>
<Button
className={Classes.MINIMAL}
text={<T id={'show_filter'} />}
onClick={handleFilterToggleClick}
icon={<Icon icon="arrow-to-bottom" />}
/>
</If>
<Button
className={classNames(Classes.MINIMAL, 'button--table-views')}
icon={<Icon icon="cog-16" iconSize={16} />}
text={!balanceSheetFilter ? <T id={'customize_report'} /> : <T id={'hide_customizer'} />}
onClick={handleFilterToggleClick}
active={balanceSheetFilter}
/>
<NavbarDivider />
<Popover
// content={}
@@ -91,7 +71,7 @@ function BalanceSheetActionsBar({
<Button
className={Classes.MINIMAL}
icon={<Icon icon='print-16' iconSize={16} />}
icon={<Icon icon="print-16" iconSize={16} />}
text={<T id={'print'} />}
/>
<Button
@@ -107,4 +87,4 @@ function BalanceSheetActionsBar({
export default compose(
withBalanceSheetDetail(({ balanceSheetFilter }) => ({ balanceSheetFilter })),
withBalanceSheetActions,
)(BalanceSheetActionsBar);
)(BalanceSheetActionsBar);

View File

@@ -1,126 +1,105 @@
import React, { useCallback, useEffect } from 'react';
import FinancialStatementHeader from 'containers/FinancialStatements/FinancialStatementHeader';
import { Row, Col, Visible } from 'react-grid-system';
import { FormGroup } from '@blueprintjs/core';
import React, { useEffect } from 'react';
import { Tabs, Tab, Button, Intent } from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl';
import moment from 'moment';
import * as Yup from 'yup';
import { useFormik } from 'formik';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { Formik, Form } from 'formik';
import FinancialStatementDateRange from 'containers/FinancialStatements/FinancialStatementDateRange';
import SelectDisplayColumnsBy from '../SelectDisplayColumnsBy';
import RadiosAccountingBasis from '../RadiosAccountingBasis';
import FinancialAccountsFilter from '../FinancialAccountsFilter';
import FinancialStatementHeader from 'containers/FinancialStatements/FinancialStatementHeader';
import withBalanceSheet from './withBalanceSheetDetail';
import withBalanceSheetActions from './withBalanceSheetActions';
import { compose } from 'utils';
import BalanceSheetHeaderGeneralPanal from './BalanceSheetHeaderGeneralPanal';
function BalanceSheetHeader({
// #ownProps
onSubmitFilter,
pageFilter,
show,
refresh,
// #withBalanceSheet
balanceSheetFilter,
// #withBalanceSheetActions
refreshBalanceSheet,
toggleBalanceSheetFilter,
}) {
const { formatMessage } = useIntl();
const formik = useFormik({
enableReinitialize: true,
initialValues: {
...pageFilter,
basis: 'cash',
from_date: moment(pageFilter.from_date).toDate(),
to_date: moment(pageFilter.to_date).toDate(),
none_zero: false,
},
validationSchema: Yup.object().shape({
from_date: Yup.date()
.required()
.label(formatMessage({ id: 'from_data' })),
to_date: Yup.date()
.min(Yup.ref('from_date'))
.required()
.label(formatMessage({ id: 'to_date' })),
none_zero: Yup.boolean(),
}),
onSubmit: (values, actions) => {
onSubmitFilter(values);
actions.setSubmitting(false);
},
// Filter form initial values.
const initialValues = {
basis: 'cash',
...pageFilter,
fromDate: moment(pageFilter.fromDate).toDate(),
toDate: moment(pageFilter.toDate).toDate(),
};
// Validation schema.
const validationSchema = Yup.object().shape({
dateRange: Yup.string().optional(),
fromDate: Yup.date()
.required()
.label(formatMessage({ id: 'fromDate' })),
toDate: Yup.date()
.min(Yup.ref('fromDate'))
.required()
.label(formatMessage({ id: 'toDate' })),
accountsFilter: Yup.string(),
displayColumnsType: Yup.string(),
});
// Handle item select of `display columns by` field.
const onItemSelectDisplayColumns = useCallback(
(item) => {
formik.setFieldValue('display_columns_type', item.type);
formik.setFieldValue('display_columns_by', item.by);
},
[formik],
);
// Handle form submit.
const handleSubmit = (values, actions) => {
onSubmitFilter(values);
toggleBalanceSheetFilter();
actions.setSubmitting(false);
};
const handleAccountingBasisChange = useCallback(
(value) => {
formik.setFieldValue('basis', value);
},
[formik],
);
useEffect(() => {
if (refresh) {
formik.submitForm();
refreshBalanceSheet(false);
}
}, [refresh]);
const handleAccountsFilterSelect = (filterType) => {
const noneZero = filterType.key === 'without-zero-balance' ? true : false;
formik.setFieldValue('none_zero', noneZero);
// Handle cancel button click.
const handleCancelClick = () => {
toggleBalanceSheetFilter();
};
// Handle drawer close action.
const handleDrawerClose = () => {
toggleBalanceSheetFilter();
};
return (
<FinancialStatementHeader show={show}>
<Row>
<FinancialStatementDateRange formik={formik} />
<Visible xl>
<Col width={'100%'} />
</Visible>
<Col width={260} offset={10}>
<SelectDisplayColumnsBy onItemSelect={onItemSelectDisplayColumns} />
</Col>
<Col width={260}>
<FormGroup
label={<T id={'filter_accounts'} />}
className="form-group--select-list bp3-fill"
inline={false}
>
<FinancialAccountsFilter
initialSelectedItem={'all-accounts'}
onItemSelect={handleAccountsFilterSelect}
<FinancialStatementHeader
isOpen={balanceSheetFilter}
drawerProps={{ onClose: handleDrawerClose }}
>
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={handleSubmit}
>
<Form>
<Tabs animate={true} vertical={true} renderActiveTabPanelOnly={true}>
<Tab
id="general"
title={<T id={'general'} />}
panel={<BalanceSheetHeaderGeneralPanal />}
/>
</FormGroup>
</Col>
</Tabs>
<Col width={260}>
<RadiosAccountingBasis
selectedValue={formik.values.basis}
onChange={handleAccountingBasisChange}
/>
</Col>
</Row>
<div class="financial-header-drawer__footer">
<Button className={'mr1'} intent={Intent.PRIMARY} type={'submit'}>
<T id={'calculate_report'} />
</Button>
<Button onClick={handleCancelClick} minimal={true}>
<T id={'cancel'} />
</Button>
</div>
</Form>
</Formik>
</FinancialStatementHeader>
);
}
export default compose(
withBalanceSheet(({ balanceSheetRefresh }) => ({
refresh: balanceSheetRefresh,
withBalanceSheet(({ balanceSheetFilter }) => ({
balanceSheetFilter,
})),
withBalanceSheetActions,
)(BalanceSheetHeader);

View File

@@ -0,0 +1,21 @@
import React from 'react';
import FinancialStatementDateRange from 'containers/FinancialStatements/FinancialStatementDateRange';
import SelectDisplayColumnsBy from '../SelectDisplayColumnsBy';
import RadiosAccountingBasis from '../RadiosAccountingBasis';
import FinancialAccountsFilter from '../FinancialAccountsFilter';
/**
* Balance sheet header - General panal.
*/
export default function BalanceSheetHeaderGeneralTab({}) {
return (
<div>
<FinancialStatementDateRange />
<SelectDisplayColumnsBy />
<FinancialAccountsFilter
initialSelectedItem={'all-accounts'}
/>
<RadiosAccountingBasis key={'basis'} />
</div>
);
}

View File

@@ -1,31 +1,56 @@
import React, { useMemo, useCallback } from 'react';
import { connect } from 'react-redux';
import { useIntl } from 'react-intl';
import classNames from 'classnames';
import Money from 'components/Money';
import FinancialSheet from 'components/FinancialSheet';
import DataTable from 'components/DataTable';
import withSettings from 'containers/Settings/withSettings';
import withBalanceSheetDetail from './withBalanceSheetDetail';
import { getFinancialSheetIndexByQuery } from 'store/financialStatement/financialStatements.selectors';
import { compose, defaultExpanderReducer } from 'utils';
import { compose, defaultExpanderReducer, getColumnWidth } from 'utils';
// Total cell.
function TotalCell({ cell }) {
const row = cell.row.original;
if (row.total) {
return (
<Money
amount={row.total.formatted_amount}
currency={row.total.currency_code}
/>
);
}
return '';
}
// Total period cell.
const TotalPeriodCell = (index) => ({ cell }) => {
const { original } = cell.row;
if (original.total_periods && original.total_periods[index]) {
const amount = original.total_periods[index].formatted_amount;
const currencyCode = original.total_periods[index].currency_code;
return <Money amount={amount} currency={currencyCode} />;
}
return '';
};
/**
* Balance sheet table.
*/
function BalanceSheetTable({
// #withPreferences
organizationSettings,
// #withBalanceSheetDetail
balanceSheetAccounts,
balanceSheetTableRows,
balanceSheetColumns,
balanceSheetQuery,
balanceSheetLoading,
// #ownProps
onFetchData,
companyName,
}) {
const { formatMessage } = useIntl();
@@ -33,35 +58,18 @@ function BalanceSheetTable({
() => [
{
Header: formatMessage({ id: 'account_name' }),
accessor: 'name',
accessor: (row) => (row.code ? `${row.name} - ${row.code}` : row.name),
className: 'account_name',
width: 120,
},
{
Header: formatMessage({ id: 'code' }),
accessor: 'code',
className: 'code',
width: 60,
width: 240,
},
...(balanceSheetQuery.display_columns_type === 'total'
? [
{
Header: formatMessage({ id: 'total' }),
accessor: 'balance.formatted_amount',
Cell: ({ cell }) => {
const row = cell.row.original;
if (row.total) {
return (
<Money
amount={row.total.formatted_amount}
currency={'USD'}
/>
);
}
return '';
},
Cell: TotalCell,
className: 'total',
width: 80,
width: 140,
},
]
: []),
@@ -70,44 +78,43 @@ function BalanceSheetTable({
id: `date_period_${index}`,
Header: column,
accessor: `total_periods[${index}]`,
Cell: ({ cell }) => {
const { original } = cell.row;
if (original.total_periods && original.total_periods[index]) {
const amount = original.total_periods[index].formatted_amount;
return <Money amount={amount} currency={'USD'} />;
}
return '';
},
Cell: TotalPeriodCell(index),
className: classNames('total-period', `total-periods-${index}`),
width: 80,
width: getColumnWidth(
balanceSheetTableRows,
`total_periods.${index}.formatted_amount`,
{ minWidth: 100 },
),
}))
: []),
],
[balanceSheetQuery, balanceSheetColumns, formatMessage],
[balanceSheetQuery, balanceSheetColumns, balanceSheetTableRows, formatMessage],
);
const handleFetchData = useCallback(() => {
onFetchData && onFetchData();
}, [onFetchData]);
// Calculates the default expanded rows of balance sheet table.
const expandedRows = useMemo(
() => defaultExpanderReducer(balanceSheetTableRows, 3),
() => defaultExpanderReducer(balanceSheetTableRows, 4),
[balanceSheetTableRows],
);
const rowClassNames = (row) => {
const rowClassNames = useCallback((row) => {
const { original } = row;
console.log(row);
const rowTypes = Array.isArray(original.row_types)
? original.row_types
: [];
return {
[`row_type--${original.row_type}`]: original.row_type,
...rowTypes.reduce((acc, rowType) => {
acc[`row_type--${rowType}`] = rowType;
return acc;
}, {}),
};
};
}, []);
return (
<FinancialSheet
name="balance-sheet"
companyName={organizationSettings.name}
companyName={companyName}
sheetType={formatMessage({ id: 'balance_sheet' })}
fromDate={balanceSheetQuery.from_date}
toDate={balanceSheetQuery.to_date}
@@ -119,46 +126,29 @@ function BalanceSheetTable({
columns={columns}
data={balanceSheetTableRows}
rowClassNames={rowClassNames}
onFetchData={handleFetchData}
noInitialFetch={true}
expanded={expandedRows}
expandable={true}
expanded={expandedRows}
expandToggleColumn={1}
sticky={true}
expandColumnSpace={0.8}
sticky={true}
/>
</FinancialSheet>
);
}
const mapStateToProps = (state, props) => {
const { balanceSheetQuery } = props;
return {
balanceSheetIndex: getFinancialSheetIndexByQuery(
state.financialStatements.balanceSheet.sheets,
balanceSheetQuery,
),
};
};
const withBalanceSheetTable = connect(mapStateToProps);
export default compose(
withBalanceSheetTable,
withBalanceSheetDetail(
({
balanceSheetAccounts,
balanceSheetTableRows,
balanceSheetColumns,
balanceSheetQuery,
balanceSheetLoading,
}) => ({
balanceSheetAccounts,
balanceSheetTableRows,
balanceSheetColumns,
balanceSheetQuery,
balanceSheetLoading,
}),
),
withSettings,
)(BalanceSheetTable);

View File

@@ -1,28 +1,36 @@
import { connect } from 'react-redux';
import {
getFinancialSheet,
getFinancialSheetAccounts,
getFinancialSheetColumns,
getFinancialSheetQuery,
getFinancialSheetTableRows,
getFinancialSheetFactory,
getFinancialSheetAccountsFactory,
getFinancialSheetColumnsFactory,
getFinancialSheetQueryFactory,
getFinancialSheetTableRowsFactory,
} from 'store/financialStatement/financialStatements.selectors';
export default (mapState) => {
const mapStateToProps = (state, props) => {
const { balanceSheetIndex } = props;
const getBalanceSheet = getFinancialSheetFactory('balanceSheet');
const getBalanceSheetAccounts = getFinancialSheetAccountsFactory(
'balanceSheet',
);
const getBalanceSheetTableRows = getFinancialSheetTableRowsFactory(
'balanceSheet',
);
const getBalanceSheetColumns = getFinancialSheetColumnsFactory('balanceSheet');
const getBalanceSheetQuery = getFinancialSheetQueryFactory('balanceSheet');
const mapped = {
balanceSheet: getFinancialSheet(state.financialStatements.balanceSheet.sheets, balanceSheetIndex),
balanceSheetAccounts: getFinancialSheetAccounts(state.financialStatements.balanceSheet.sheets, balanceSheetIndex),
balanceSheetTableRows: getFinancialSheetTableRows(state.financialStatements.balanceSheet.sheets, balanceSheetIndex),
balanceSheetColumns: getFinancialSheetColumns(state.financialStatements.balanceSheet.sheets, balanceSheetIndex),
balanceSheetQuery: getFinancialSheetQuery(state.financialStatements.balanceSheet.sheets, balanceSheetIndex),
balanceSheet: getBalanceSheet(state, props),
balanceSheetAccounts: getBalanceSheetAccounts(state, props),
balanceSheetTableRows: getBalanceSheetTableRows(state, props),
balanceSheetColumns: getBalanceSheetColumns(state, props),
balanceSheetQuery: getBalanceSheetQuery(state, props),
balanceSheetLoading: state.financialStatements.balanceSheet.loading,
balanceSheetFilter: state.financialStatements.balanceSheet.filter,
balanceSheetRefresh: state.financialStatements.balanceSheet.refresh,
};
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
}
};

View File

@@ -1,73 +1,70 @@
import React, { useMemo, useCallback } from 'react';
import React from 'react';
import {
PopoverInteractionKind,
Tooltip,
MenuItem,
Position,
FormGroup,
} from '@blueprintjs/core';
import { useIntl } from 'react-intl';
import { ListSelect, MODIFIER } from 'components';
import { FormattedMessage as T } from 'react-intl';
import classNames from 'classnames';
import { FastField } from 'formik';
export default function FinancialAccountsFilter({
...restProps
}) {
const { formatMessage } = useIntl();
const filterAccountsOptions = useMemo(
() => [
{
key: 'all-accounts',
name: formatMessage({ id: 'all_accounts' }),
hint: formatMessage({ id: 'all_accounts_including_with_zero_balance' }),
},
{
key: 'without-zero-balance',
name: formatMessage({ id: 'accounts_without_zero_balance' }),
hint: formatMessage({ id: 'include_accounts_and_exclude_zero_balance' }),
},
{
key: 'with-transactions',
name: formatMessage({ id: 'accounts_with_transactions' }),
hint: formatMessage({ id: 'include_accounts_once_has_transactions_on_given_date_period' }),
},
],
[formatMessage],
);
import { CLASSES } from 'common/classes';
import { Col, Row, ListSelect, MODIFIER } from 'components';
import { filterAccountsOptions } from './common';
export default function FinancialAccountsFilter({ ...restProps }) {
const SUBMENU_POPOVER_MODIFIERS = {
flip: { boundariesElement: 'viewport', padding: 20 },
offset: { offset: '0, 10' },
preventOverflow: { boundariesElement: 'viewport', padding: 40 },
};
const filterAccountRenderer = useCallback(
(item, { handleClick, modifiers, query }) => {
return (
<Tooltip
interactionKind={PopoverInteractionKind.HOVER}
position={Position.RIGHT_TOP}
content={item.hint}
modifiers={SUBMENU_POPOVER_MODIFIERS}
inline={true}
minimal={true}
className={MODIFIER.SELECT_LIST_TOOLTIP_ITEMS}
>
<MenuItem text={item.name} key={item.key} onClick={handleClick} />
</Tooltip>
);
},
[],
);
const filterAccountRenderer = (item, { handleClick, modifiers, query }) => {
return (
<Tooltip
interactionKind={PopoverInteractionKind.HOVER}
position={Position.RIGHT_TOP}
content={item.hint}
modifiers={SUBMENU_POPOVER_MODIFIERS}
inline={true}
minimal={true}
className={MODIFIER.SELECT_LIST_TOOLTIP_ITEMS}
>
<MenuItem text={item.name} key={item.key} onClick={handleClick} />
</Tooltip>
);
};
return (
<ListSelect
items={filterAccountsOptions}
itemRenderer={filterAccountRenderer}
popoverProps={{ minimal: true, }}
filterable={false}
selectedItemProp={'key'}
labelProp={'name'}
// className={}
{...restProps}
/>
<Row>
<Col xs={4}>
<FastField name={'accountsFilter'}>
{({ form: { setFieldValue }, field: { value } }) => (
<FormGroup
label={<T id={'filter_accounts'} />}
className="form-group--select-list bp3-fill"
inline={false}
>
<ListSelect
items={filterAccountsOptions}
itemRenderer={filterAccountRenderer}
popoverProps={{ minimal: true }}
filterable={false}
selectedItem={value}
selectedItemProp={'key'}
labelProp={'name'}
onItemSelect={(item) => {
setFieldValue('accountsFilter', item.key);
}}
className={classNames(CLASSES.SELECT_LIST_FILL_POPOVER)}
{...restProps}
/>
</FormGroup>
)}
</FastField>
</Col>
</Row>
);
}
}

View File

@@ -1,106 +1,120 @@
import React, { useState, useCallback, useMemo } from 'react';
import { Row, Col } from 'react-grid-system';
import { momentFormatter } from 'utils';
import React from 'react';
import { FastField, ErrorMessage } from 'formik';
import { HTMLSelect, FormGroup, Intent, Position } from '@blueprintjs/core';
import moment from 'moment';
import { Row, Col, Hint } from 'components';
import { momentFormatter, parseDateRangeQuery } from 'utils';
import { DateInput } from '@blueprintjs/datetime';
import { useIntl } from 'react-intl';
import { HTMLSelect, FormGroup, Intent, Position } from '@blueprintjs/core';
import { Hint } from 'components';
import { parseDateRangeQuery } from 'utils';
import { dateRangeOptions } from 'containers/FinancialStatements/common';
export default function FinancialStatementDateRange({ formik }) {
const intl = useIntl();
const [reportDateRange, setReportDateRange] = useState('this_year');
const dateRangeOptions = useMemo(
() => [
{ value: 'today', label: 'Today' },
{ value: 'this_week', label: 'This Week' },
{ value: 'this_month', label: 'This Month' },
{ value: 'this_quarter', label: 'This Quarter' },
{ value: 'this_year', label: 'This Year' },
{ value: 'custom', label: 'Custom Range' },
],
[],
);
const handleDateChange = useCallback(
(name) => (date) => {
setReportDateRange('custom');
formik.setFieldValue(name, date);
},
[setReportDateRange, formik],
);
// Handles date range field change.
const handleDateRangeChange = useCallback(
(e) => {
const value = e.target.value;
if (value !== 'custom') {
const dateRange = parseDateRangeQuery(value);
if (dateRange) {
formik.setFieldValue('from_date', dateRange.from_date);
formik.setFieldValue('to_date', dateRange.to_date);
}
}
setReportDateRange(value);
},
[formik],
);
/**
* Financial statement - Date range select.
*/
export default function FinancialStatementDateRange() {
const { formatMessage } = useIntl();
return (
<>
<Col width={260}>
<FormGroup
label={intl.formatMessage({ id: 'report_date_range' })}
labelInfo={<Hint />}
minimal={true}
fill={true}
>
<HTMLSelect
fill={true}
options={dateRangeOptions}
value={reportDateRange}
onChange={handleDateRangeChange}
/>
</FormGroup>
</Col>
<Row>
<Col xs={4}>
<FastField name={'date_range'}>
{({
form: { setFieldValue },
field: { value },
}) => (
<FormGroup
label={formatMessage({ id: 'report_date_range' })}
labelInfo={<Hint />}
minimal={true}
fill={true}
>
<HTMLSelect
fill={true}
options={dateRangeOptions}
value={value}
onChange={(e) => {
const newValue = e.target.value;
<Col width={260}>
<FormGroup
label={intl.formatMessage({ id: 'from_date' })}
labelInfo={<Hint />}
fill={true}
intent={formik.errors.from_date && Intent.DANGER}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={formik.values.from_date}
onChange={handleDateChange('from_date')}
popoverProps={{ position: Position.BOTTOM }}
minimal={true}
fill={true}
/>
</FormGroup>
</Col>
if (newValue !== 'custom') {
const dateRange = parseDateRangeQuery(newValue);
<Col width={260}>
<FormGroup
label={intl.formatMessage({ id: 'to_date' })}
labelInfo={<Hint />}
fill={true}
intent={formik.errors.to_date && Intent.DANGER}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={formik.values.to_date}
onChange={handleDateChange('to_date')}
popoverProps={{ position: Position.BOTTOM }}
fill={true}
minimal={true}
intent={formik.errors.to_date && Intent.DANGER}
/>
</FormGroup>
</Col>
if (dateRange) {
setFieldValue('fromDate', moment(dateRange.fromDate).toDate());
setFieldValue('toDate', moment(dateRange.toDate).toDate());
}
}
setFieldValue('dateRange', newValue);
}}
/>
</FormGroup>
)}
</FastField>
</Col>
</Row>
<Row>
<Col xs={4}>
<FastField name={'fromDate'}>
{({
form: { setFieldValue },
field: { value },
meta: { error, touched },
}) => (
<FormGroup
label={formatMessage({ id: 'from_date' })}
labelInfo={<Hint />}
fill={true}
intent={error && Intent.DANGER}
helperText={<ErrorMessage name={'fromDate'} />}
>
<DateInput
{...momentFormatter('YYYY-MM-DD')}
value={value}
onChange={(selectedDate) => {
setFieldValue('fromDate', selectedDate);
}}
popoverProps={{ minimal: true, position: Position.BOTTOM }}
canClearSelection={false}
minimal={true}
fill={true}
/>
</FormGroup>
)}
</FastField>
</Col>
<Col xs={4}>
<FastField name={'toDate'}>
{({
form: { setFieldValue },
field: { value },
meta: { error },
}) => (
<FormGroup
label={formatMessage({ id: 'to_date' })}
labelInfo={<Hint />}
fill={true}
intent={error && Intent.DANGER}
helperText={<ErrorMessage name={'toDate'} />}
>
<DateInput
{...momentFormatter('YYYY-MM-DD')}
value={value}
onChange={(selectedDate) => {
setFieldValue('toDate', selectedDate);
}}
popoverProps={{ minimal: true, position: Position.BOTTOM }}
canClearSelection={false}
fill={true}
minimal={true}
intent={error && Intent.DANGER}
/>
</FormGroup>
)}
</FastField>
</Col>
</Row>
</>
);
}

View File

@@ -1,14 +1,59 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import classNames from 'classnames';
import { Position, Drawer } from '@blueprintjs/core';
export default function FinancialStatementHeader({
children,
isOpen,
drawerProps,
}) {
const timeoutRef = React.useRef();
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
// Hides the content scrollbar and scroll to the top of the page once the drawer open.
useEffect(() => {
const contentPanel = document.querySelector('body');
contentPanel.classList.toggle('hide-scrollbar', isOpen);
if (isOpen) {
document.querySelector('.Pane2').scrollTo(0, 0);
}
return () => {
contentPanel.classList.remove('hide-scrollbar');
};
}, [isOpen]);
useEffect(() => {
clearTimeout(timeoutRef.current);
if (isOpen) {
setIsDrawerOpen(isOpen);
} else {
timeoutRef.current = setTimeout(() => setIsDrawerOpen(isOpen), 300);
}
}, [isOpen]);
export default function FinancialStatementHeader({ show, children }) {
return (
<div
className={classNames('financial-statement__header', {
'is-hidden': !show,
})}
className={classNames(
'financial-statement__header',
'financial-header-drawer',
{
'is-hidden': !isDrawerOpen,
},
)}
>
{children}
<Drawer
isOpen={isOpen}
usePortal={false}
hasBackdrop={true}
position={Position.TOP}
canOutsideClickClose={true}
canEscapeKeyClose={true}
{...drawerProps}
>
{children}
</Drawer>
</div>
);
}

View File

@@ -1,14 +1,13 @@
import React, { useEffect, useCallback, useState} from 'react';
import React, { useEffect, useCallback, useState } from 'react';
import moment from 'moment';
import GeneralLedgerTable from 'containers/FinancialStatements/GeneralLedger/GeneralLedgerTable';
import { useQuery } from 'react-query';
import { useIntl } from 'react-intl';
import { queryCache } from 'react-query';
import GeneralLedgerTable from 'containers/FinancialStatements/GeneralLedger/GeneralLedgerTable';
import GeneralLedgerHeader from './GeneralLedgerHeader';
import { compose } from 'utils';
import DashboardInsider from 'components/Dashboard/DashboardInsider'
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import GeneralLedgerActionsBar from './GeneralLedgerActionsBar';
@@ -17,73 +16,101 @@ import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withAccountsActions from 'containers/Accounts/withAccountsActions';
import withSettings from 'containers/Settings/withSettings';
import { compose } from 'utils';
import { transformFilterFormToQuery } from 'containers/FinancialStatements/common';
import withGeneralLedger from './withGeneralLedger';
/**
* General Ledger (GL) sheet.
*/
function GeneralLedger({
// #withDashboardActions
changePageTitle,
setDashboardBackLink,
// #withGeneralLedgerActions
fetchGeneralLedger,
refreshGeneralLedgerSheet,
// #withAccountsActions
requestFetchAccounts,
// #withGeneralLedger
generalLedgerSheetRefresh,
// #withSettings
organizationSettings,
organizationName,
}) {
const { formatMessage } = useIntl()
const { formatMessage } = useIntl();
const [filter, setFilter] = useState({
from_date: moment().startOf('year').format('YYYY-MM-DD'),
to_date: moment().endOf('year').format('YYYY-MM-DD'),
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
toDate: moment().endOf('year').format('YYYY-MM-DD'),
basis: 'accural',
none_zero: true,
});
// Change page title of the dashboard.
useEffect(() => {
changePageTitle(formatMessage({id:'general_ledger'}));
}, [changePageTitle,formatMessage]);
changePageTitle(formatMessage({ id: 'general_ledger' }));
}, [changePageTitle, formatMessage]);
const fetchAccounts = useQuery(['accounts-list'],
() => requestFetchAccounts());
useEffect(() => {
// Show the back link on dashboard topbar.
setDashboardBackLink(true);
const fetchSheet = useQuery(['general-ledger', filter],
(key, query) => fetchGeneralLedger(query),
{ manual: true });
// Handle fetch data of trial balance table.
const handleFetchData = useCallback(() => {
fetchSheet.refetch({ force: true });
}, []);
// Handle financial statement filter change.
const handleFilterSubmit = useCallback((filter) => {
const parsedFilter = {
...filter,
from_date: moment(filter.from_date).format('YYYY-MM-DD'),
to_date: moment(filter.to_date).format('YYYY-MM-DD'),
return () => {
// Hide the back link on dashboard topbar.
setDashboardBackLink(false);
};
setFilter(parsedFilter);
}, [setFilter]);
});
const handleFilterChanged = () => { };
// Observes the GL sheet refresh to invalid the query to refresh it.
useEffect(() => {
if (generalLedgerSheetRefresh) {
queryCache.invalidateQueries('general-ledger');
refreshGeneralLedgerSheet(false);
}
}, [generalLedgerSheetRefresh, refreshGeneralLedgerSheet]);
// Fetches accounts list.
const fetchAccounts = useQuery(['accounts-list'], () =>
requestFetchAccounts(),
);
// Fetches the general ledger sheet.
const fetchSheet = useQuery(['general-ledger', filter], (key, q) =>
fetchGeneralLedger({ ...transformFilterFormToQuery(q) }),
);
// Handle financial statement filter change.
const handleFilterSubmit = useCallback(
(filter) => {
const parsedFilter = {
...filter,
fromDate: moment(filter.fromDate).format('YYYY-MM-DD'),
toDate: moment(filter.toDate).format('YYYY-MM-DD'),
};
setFilter(parsedFilter);
refreshGeneralLedgerSheet(true);
},
[setFilter, refreshGeneralLedgerSheet],
);
return (
<DashboardInsider>
<GeneralLedgerActionsBar
onFilterChanged={handleFilterChanged} />
<GeneralLedgerActionsBar />
<DashboardPageContent>
<div class="financial-statement financial-statement--general-ledger">
<GeneralLedgerHeader
pageFilter={filter}
onSubmitFilter={handleFilterSubmit} />
onSubmitFilter={handleFilterSubmit}
/>
<div class="financial-statement__body">
<GeneralLedgerTable
companyName={organizationSettings.name}
companyName={organizationName}
generalLedgerQuery={filter}
onFetchData={handleFetchData} />
/>
</div>
</div>
</DashboardPageContent>
@@ -95,5 +122,10 @@ export default compose(
withGeneralLedgerActions,
withDashboardActions,
withAccountsActions,
withSettings,
)(GeneralLedger);
withGeneralLedger(({ generalLedgerSheetRefresh }) => ({
generalLedgerSheetRefresh,
})),
withSettings(({ organizationSettings }) => ({
organizationName: organizationSettings.name,
})),
)(GeneralLedger);

View File

@@ -9,11 +9,10 @@ import {
Position,
} from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import Icon from 'components/Icon';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar'
import { If } from 'components';
import classNames from 'classnames';
import FilterDropdown from 'components/FilterDropdown';
import Icon from 'components/Icon';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import withGeneralLedger from './withGeneralLedger';
import withGeneralLedgerActions from './withGeneralLedgerActions';
@@ -21,7 +20,7 @@ import withGeneralLedgerActions from './withGeneralLedgerActions';
import { compose } from 'utils';
/**
* General ledger actions bar.
* General ledger - Actions bar.
*/
function GeneralLedgerActionsBar({
// #withGeneralLedger
@@ -29,12 +28,13 @@ function GeneralLedgerActionsBar({
// #withGeneralLedgerActions
toggleGeneralLedgerSheetFilter,
refreshGeneralLedgerSheet
refreshGeneralLedgerSheet,
}) {
const handleFilterClick = () => {
// Handle customize button click.
const handleCustomizeClick = () => {
toggleGeneralLedgerSheetFilter();
};
// Handle re-calculate button click.
const handleRecalcReport = () => {
refreshGeneralLedgerSheet(true);
};
@@ -43,62 +43,50 @@ function GeneralLedgerActionsBar({
<DashboardActionsBar>
<NavbarGroup>
<Button
className={classNames(Classes.MINIMAL, 'button--table-views')}
icon={<Icon icon='cog-16' iconSize={16} />}
text={<T id={'customize_report'}/>}
/>
<NavbarDivider />
<Button
className={classNames(
Classes.MINIMAL,
'button--gray-highlight',
)}
text={'Re-calc Report'}
className={classNames(Classes.MINIMAL, 'button--gray-highlight')}
text={<T id={'recalc_report'} />}
onClick={handleRecalcReport}
icon={<Icon icon="refresh-16" iconSize={16} />}
/>
<NavbarDivider />
<If condition={generalLedgerSheetFilter}>
<Button
className={Classes.MINIMAL}
text={<T id={'hide_filter'} />}
icon={<Icon icon="arrow-to-top" />}
onClick={handleFilterClick}
/>
</If>
<If condition={!generalLedgerSheetFilter}>
<Button
className={Classes.MINIMAL}
text={<T id={'show_filter'} />}
icon={<Icon icon="arrow-to-bottom" />}
onClick={handleFilterClick}
/>
</If>
<Button
className={classNames(Classes.MINIMAL, 'button--table-views')}
icon={<Icon icon="cog-16" iconSize={16} />}
text={
generalLedgerSheetFilter ? (
<T id={'hide_customizer'} />
) : (
<T id={'customize_report'} />
)
}
onClick={handleCustomizeClick}
active={generalLedgerSheetFilter}
/>
<NavbarDivider />
<Popover
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}>
position={Position.BOTTOM_LEFT}
>
<Button
className={classNames(Classes.MINIMAL, 'button--filter')}
text={<T id={'filter'}/>}
icon={<Icon icon="filter-16" iconSize={16} /> } />
text={<T id={'filter'} />}
icon={<Icon icon="filter-16" iconSize={16} />}
/>
</Popover>
<NavbarDivider />
<Button
className={Classes.MINIMAL}
icon={<Icon icon='print-16' iconSize={16} />}
text={<T id={'print'}/>}
icon={<Icon icon="print-16" iconSize={16} />}
text={<T id={'print'} />}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon='file-export-16' iconSize={16} />}
text={<T id={'export'}/>}
icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'} />}
/>
</NavbarGroup>
</DashboardActionsBar>
@@ -106,6 +94,8 @@ function GeneralLedgerActionsBar({
}
export default compose(
withGeneralLedger(({ generalLedgerSheetFilter }) => ({ generalLedgerSheetFilter })),
withGeneralLedger(({ generalLedgerSheetFilter }) => ({
generalLedgerSheetFilter,
})),
withGeneralLedgerActions,
)(GeneralLedgerActionsBar);
)(GeneralLedgerActionsBar);

View File

@@ -1,113 +1,100 @@
import React, { useEffect, useCallback } from 'react';
import { Button, FormGroup, Classes } from '@blueprintjs/core';
import { Row, Col, Visible } from 'react-grid-system';
import React from 'react';
import moment from 'moment';
import * as Yup from 'yup';
import { useFormik } from 'formik';
import { Formik, Form } from 'formik';
import { Tabs, Tab, Button, Intent } from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import AccountsMultiSelect from 'components/AccountsMultiSelect';
import FinancialStatementHeader from 'containers/FinancialStatements/FinancialStatementHeader';
import withAccounts from 'containers/Accounts/withAccounts';
import classNames from 'classnames';
import FinancialStatementDateRange from 'containers/FinancialStatements/FinancialStatementDateRange';
import RadiosAccountingBasis from '../RadiosAccountingBasis';
import GeneralLedgerHeaderGeneralPane from './GeneralLedgerHeaderGeneralPane';
import withGeneralLedger from './withGeneralLedger';
import withGeneralLedgerActions from './withGeneralLedgerActions';
import { compose } from 'utils';
/**
* Geenral Ledger (GL) - Header.
*/
function GeneralLedgerHeader({
// #ownProps
onSubmitFilter,
pageFilter,
// #withAccounts
accountsList,
// #withGeneralLedgerActions
refreshGeneralLedgerSheet,
toggleGeneralLedgerSheetFilter,
// #withGeneralLedger
generalLedgerSheetFilter,
generalLedgerSheetRefresh
}) {
const formik = useFormik({
enableReinitialize: true,
initialValues: {
...pageFilter,
from_date: moment(pageFilter.from_date).toDate(),
to_date: moment(pageFilter.to_date).toDate(),
},
validationSchema: Yup.object().shape({
from_date: Yup.date().required(),
to_date: Yup.date().min(Yup.ref('from_date')).required(),
}),
onSubmit(values, actions) {
onSubmitFilter(values);
actions.setSubmitting(false);
},
// Initial values.
const initialValues = {
...pageFilter,
fromDate: moment(pageFilter.fromDate).toDate(),
toDate: moment(pageFilter.toDate).toDate(),
};
// Validation schema.
const validationSchema = Yup.object().shape({
dateRange: Yup.string().optional(),
fromDate: Yup.date().required(),
toDate: Yup.date().min(Yup.ref('fromDate')).required(),
});
const onAccountSelected = useCallback((selectedAccounts) => {
formik.setFieldValue('accounts_ids', Object.keys(selectedAccounts));
}, [formik.setFieldValue]);
// Handle form submit.
const handleSubmit = (values, { setSubmitting }) => {
onSubmitFilter(values);
toggleGeneralLedgerSheetFilter();
setSubmitting(false);
};
const handleAccountingBasisChange = useCallback(
(value) => {
formik.setFieldValue('basis', value);
},
[formik],
);
// handle submit filter submit button.
useEffect(() => {
if (generalLedgerSheetRefresh) {
formik.submitForm();
refreshGeneralLedgerSheet(false);
}
}, [formik, generalLedgerSheetRefresh])
// Handle cancel button click.
const handleCancelClick = () => {
toggleGeneralLedgerSheetFilter(false);
};
// Handle drawer close action.
const handleDrawerClose = () => {
toggleGeneralLedgerSheetFilter(false);
};
return (
<FinancialStatementHeader show={generalLedgerSheetFilter}>
<Row>
<FinancialStatementDateRange formik={formik} />
<Visible xl><Col width={'100%'} /></Visible>
<Col width={260}>
<FormGroup
label={<T id={'specific_accounts'} />}
className={classNames('form-group--select-list', Classes.FILL)}
>
<AccountsMultiSelect
accounts={accountsList}
onAccountSelected={onAccountSelected}
<FinancialStatementHeader
isOpen={generalLedgerSheetFilter}
drawerProps={{ onClose: handleDrawerClose }}
>
<Formik
validationSchema={validationSchema}
initialValues={initialValues}
onSubmit={handleSubmit}
>
<Form>
<Tabs animate={true} vertical={true} renderActiveTabPanelOnly={true}>
<Tab
id="general"
title={<T id={'general'} />}
panel={<GeneralLedgerHeaderGeneralPane />}
/>
</FormGroup>
</Col>
</Tabs>
<Col width={260}>
<RadiosAccountingBasis
onChange={handleAccountingBasisChange}
selectedValue={formik.values.basis}
/>
</Col>
<div class="financial-header-drawer__footer">
<Button className={'mr1'} intent={Intent.PRIMARY} type={'submit'}>
<T id={'calculate_report'} />
</Button>
</Row>
<Button onClick={handleCancelClick} minimal={true}>
<T id={'cancel'} />
</Button>
</div>
</Form>
</Formik>
</FinancialStatementHeader>
);
}
export default compose(
withAccounts(({ accountsList }) => ({
accountsList,
})),
withGeneralLedger(({ generalLedgerSheetFilter, generalLedgerSheetRefresh }) => ({
withGeneralLedger(({ generalLedgerSheetFilter }) => ({
generalLedgerSheetFilter,
generalLedgerSheetRefresh,
})),
withGeneralLedgerActions,
)(GeneralLedgerHeader);

View File

@@ -0,0 +1,44 @@
import React from 'react';
import { FormGroup, Classes } from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import classNames from 'classnames';
import { AccountsMultiSelect, Row, Col } from 'components';
import FinancialStatementDateRange from 'containers/FinancialStatements/FinancialStatementDateRange';
import RadiosAccountingBasis from '../RadiosAccountingBasis';
import withAccounts from 'containers/Accounts/withAccounts';
import { compose } from 'redux';
/**
* General ledger (GL) - Header - General panel.
*/
function GeneralLedgerHeaderGeneralPane({
// #withAccounts
accountsList,
}) {
return (
<div>
<FinancialStatementDateRange />
<Row>
<Col xs={4}>
<FormGroup
label={<T id={'specific_accounts'} />}
className={classNames('form-group--select-list', Classes.FILL)}
>
<AccountsMultiSelect accounts={accountsList} />
</FormGroup>
</Col>
</Row>
<RadiosAccountingBasis key={'basis'} />
</div>
);
}
export default compose(withAccounts(({ accountsList }) => ({ accountsList })))(
GeneralLedgerHeaderGeneralPane,
);

View File

@@ -1,6 +1,5 @@
import React, { useCallback, useMemo } from 'react';
import moment from 'moment';
import { connect } from 'react-redux';
import { defaultExpanderReducer, compose } from 'utils';
import { useIntl } from 'react-intl';
@@ -8,7 +7,6 @@ import FinancialSheet from 'components/FinancialSheet';
import DataTable from 'components/DataTable';
import Money from 'components/Money';
import { getFinancialSheetIndexByQuery } from 'store/financialStatement/financialStatements.selectors';
import withGeneralLedger from './withGeneralLedger';
const ROW_TYPE = {
@@ -20,7 +18,6 @@ const ROW_TYPE = {
function GeneralLedgerTable({
companyName,
onFetchData,
generalLedgerSheetLoading,
generalLedgerTableRows,
@@ -29,35 +26,29 @@ function GeneralLedgerTable({
const { formatMessage } = useIntl();
// Account name column accessor.
const accountNameAccessor = useCallback(
(row) => {
switch (row.rowType) {
case ROW_TYPE.OPENING_BALANCE:
return 'Opening Balance';
case ROW_TYPE.CLOSING_BALANCE:
return 'Closing Balance';
default:
return row.name;
}
},
[ROW_TYPE],
);
const accountNameAccessor = (row) => {
switch (row.rowType) {
case ROW_TYPE.OPENING_BALANCE:
return 'Opening Balance';
case ROW_TYPE.CLOSING_BALANCE:
return 'Closing Balance';
default:
return row.name;
}
};
// Date accessor.
const dateAccessor = useCallback(
(row) => {
const TYPES = [
ROW_TYPE.OPENING_BALANCE,
ROW_TYPE.CLOSING_BALANCE,
ROW_TYPE.TRANSACTION,
];
const dateAccessor = (row) => {
const TYPES = [
ROW_TYPE.OPENING_BALANCE,
ROW_TYPE.CLOSING_BALANCE,
ROW_TYPE.TRANSACTION,
];
return TYPES.indexOf(row.rowType) !== -1
? moment(row.date).format('DD MMM YYYY')
: '';
},
[moment, ROW_TYPE],
);
return TYPES.indexOf(row.rowType) !== -1
? moment(row.date).format('DD MMM YYYY')
: '';
};
// Amount cell
const amountCell = useCallback(({ cell }) => {
@@ -73,10 +64,6 @@ function GeneralLedgerTable({
return <Money amount={transaction.amount} currency={'USD'} />;
}, []);
const referenceLink = useCallback((row) => {
return <a href="">{row.referenceId}</a>;
});
const columns = useMemo(
() => [
{
@@ -99,7 +86,7 @@ function GeneralLedgerTable({
},
{
Header: formatMessage({ id: 'trans_num' }),
accessor: referenceLink,
accessor: 'reference_id',
className: 'transaction_number',
width: 110,
},
@@ -125,10 +112,6 @@ function GeneralLedgerTable({
[],
);
const handleFetchData = useCallback(() => {
onFetchData && onFetchData();
}, [onFetchData]);
// Default expanded rows of general ledger table.
const expandedRows = useMemo(
() => defaultExpanderReducer(generalLedgerTableRows, 1),
@@ -140,12 +123,11 @@ function GeneralLedgerTable({
return (
<FinancialSheet
companyName={companyName}
// sheetType={formatMessage({ id: 'general_ledger_sheet' })}
sheetType={formatMessage({ id: 'general_ledger_sheet' })}
fromDate={generalLedgerQuery.from_date}
toDate={generalLedgerQuery.to_date}
name="general-ledger"
loading={generalLedgerSheetLoading}
minimal={true}
fullWidth={true}
>
<DataTable
@@ -155,7 +137,6 @@ function GeneralLedgerTable({
})}
columns={columns}
data={generalLedgerTableRows}
onFetchData={handleFetchData}
rowClassNames={rowClassNames}
expanded={expandedRows}
virtualizedRows={true}
@@ -169,21 +150,7 @@ function GeneralLedgerTable({
);
}
const mapStateToProps = (state, props) => {
const { generalLedgerQuery } = props;
return {
generalLedgerIndex: getFinancialSheetIndexByQuery(
state.financialStatements.generalLedger.sheets,
generalLedgerQuery,
),
};
};
const withGeneralLedgerTable = connect(mapStateToProps);
export default compose(
withGeneralLedgerTable,
withGeneralLedger(
({
generalLedgerTableRows,

View File

@@ -1,27 +1,20 @@
import { connect } from 'react-redux';
import {
getFinancialSheet,
getFinancialSheetQuery,
getFinancialSheetTableRows,
getFinancialSheetFactory,
getFinancialSheetQueryFactory,
getFinancialSheetTableRowsFactory,
} from 'store/financialStatement/financialStatements.selectors';
export default (mapState) => {
const mapStateToProps = (state, props) => {
const { generalLedgerIndex } = props;
const getGeneralLedgerSheet = getFinancialSheetFactory('generalLedger');
const getSheetTableRows = getFinancialSheetTableRowsFactory('generalLedger');
const getSheetQuery = getFinancialSheetQueryFactory('generalLedger');
const mapped = {
generalLedgerSheet: getFinancialSheet(
state.financialStatements.generalLedger.sheets,
generalLedgerIndex,
),
generalLedgerTableRows: getFinancialSheetTableRows(
state.financialStatements.generalLedger.sheets,
generalLedgerIndex,
),
generalLedgerQuery: getFinancialSheetQuery(
state.financialStatements.generalLedger.sheets,
generalLedgerIndex,
),
generalLedgerSheet: getGeneralLedgerSheet(state, props),
generalLedgerTableRows: getSheetTableRows(state, props),
generalLedgerQuery: getSheetQuery(state, props),
generalLedgerSheetLoading:
state.financialStatements.generalLedger.loading,
generalLedgerSheetFilter: state.financialStatements.generalLedger.filter,

View File

@@ -2,6 +2,7 @@ import React, { useState, useCallback, useEffect } from 'react';
import { useQuery } from 'react-query';
import moment from 'moment';
import { useIntl } from 'react-intl';
import { queryCache } from 'react-query';
import { compose } from 'utils';
import JournalTable from './JournalTable';
@@ -12,79 +13,89 @@ import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import withSettings from 'containers/Settings/withSettings';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withJournalActions from './withJournalActions';
import withJournal from './withJournal';
import { transformFilterFormToQuery } from 'containers/FinancialStatements/common';
function Journal({
// #withJournalActions
requestFetchJournalSheet,
refreshJournalSheet,
// #withJournal
journalSheetRefresh,
// #withDashboardActions
changePageTitle,
setDashboardBackLink,
// #withPreferences
organizationSettings,
organizationName,
}) {
const [filter, setFilter] = useState({
from_date: moment().startOf('year').format('YYYY-MM-DD'),
to_date: moment().endOf('year').format('YYYY-MM-DD'),
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
toDate: moment().endOf('year').format('YYYY-MM-DD'),
basis: 'accural',
});
const { formatMessage } = useIntl();
const fetchJournalSheet = useQuery(['journal-sheet', filter], (key, query) =>
requestFetchJournalSheet({
...transformFilterFormToQuery(filter),
}),
);
useEffect(() => {
changePageTitle(formatMessage({ id: 'journal_sheet' }));
}, [changePageTitle, formatMessage]);
const fetchHook = useQuery(
['journal', filter],
(key, query) => requestFetchJournalSheet(query),
{ manual: true },
);
useEffect(() => {
// Show the back link on dashboard topbar.
setDashboardBackLink(true);
return () => {
// Hide the back link on dashboard topbar.
setDashboardBackLink(false);
};
});
useEffect(() => {
if (journalSheetRefresh) {
queryCache.invalidateQueries('journal-sheet');
refreshJournalSheet(false);
}
}, [journalSheetRefresh, refreshJournalSheet]);
// Handle financial statement filter change.
const handleFilterSubmit = useCallback(
(filter) => {
const _filter = {
...filter,
from_date: moment(filter.from_date).format('YYYY-MM-DD'),
to_date: moment(filter.to_date).format('YYYY-MM-DD'),
fromDate: moment(filter.fromDate).format('YYYY-MM-DD'),
toDate: moment(filter.toDate).format('YYYY-MM-DD'),
};
setFilter(_filter);
fetchHook.refetch({ force: true });
queryCache.invalidateQueries('journal-sheet');
},
[fetchHook],
[setFilter],
);
const handlePrintClick = useCallback(() => {}, []);
const handleExportClick = useCallback(() => {}, []);
const handleFetchData = useCallback(({ sortBy, pageIndex, pageSize }) => {
fetchHook.refetch({ force: true });
}, []);
return (
<DashboardInsider>
<JournalActionsBar
onSubmitFilter={handleFilterSubmit}
onPrintClick={handlePrintClick}
onExportClick={handleExportClick}
/>
<JournalActionsBar />
<DashboardPageContent>
<div class="financial-statement financial-statement--journal">
<JournalHeader
pageFilter={filter}
onSubmitFilter={handleFilterSubmit}
pageFilter={filter}
/>
<div class="financial-statement__body">
<JournalTable
companyName={organizationSettings.name}
companyName={organizationName}
journalQuery={filter}
onFetchData={handleFetchData}
/>
</div>
</div>
@@ -96,5 +107,10 @@ function Journal({
export default compose(
withDashboardActions,
withJournalActions,
withSettings,
withSettings(({ organizationSettings }) => ({
organizationName: organizationSettings.name,
})),
withJournal(({ journalSheetRefresh }) => ({
journalSheetRefresh,
})),
)(Journal);

View File

@@ -11,16 +11,16 @@ import {
import { FormattedMessage as T } from 'react-intl';
import Icon from 'components/Icon';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import FilterDropdown from 'components/FilterDropdown';
import classNames from 'classnames';
import { If } from 'components';
import withJournalActions from './withJournalActions';
import withJournal from './withJournal';
import { compose } from 'utils';
/**
* Journal sheeet - Actions bar.
*/
function JournalActionsBar({
// #withJournal
journalSheetFilter,
@@ -40,36 +40,28 @@ function JournalActionsBar({
return (
<DashboardActionsBar>
<NavbarGroup>
<Button
className={classNames(Classes.MINIMAL, 'button--table-views')}
icon={<Icon icon="cog-16" iconSize={16} />}
text={<T id={'customize_report'} />}
/>
<NavbarDivider />
<Button
className={classNames(Classes.MINIMAL, 'button--gray-highlight')}
text={<T id={'recalc_report'} />}
onClick={handleRecalcReport}
icon={<Icon icon="refresh-16" iconSize={16} />}
/>
<If condition={journalSheetFilter}>
<Button
className={Classes.MINIMAL}
text={<T id={'hide_filter'} />}
icon={<Icon icon="arrow-to-top" />}
onClick={handleFilterToggleClick}
/>
</If>
<NavbarDivider />
<If condition={!journalSheetFilter}>
<Button
className={Classes.MINIMAL}
text={<T id={'show_filter'} />}
icon={<Icon icon="arrow-to-bottom" />}
onClick={handleFilterToggleClick}
/>
</If>
<Button
className={classNames(Classes.MINIMAL, 'button--table-views')}
icon={<Icon icon="cog-16" iconSize={16} />}
text={
(journalSheetFilter) ? (
<T id={'hide_customizer'} />
) : (
<T id={'customize_report'} />
)
}
active={journalSheetFilter}
onClick={handleFilterToggleClick}
/>
<NavbarDivider />
<Popover
interactionKind={PopoverInteractionKind.CLICK}

View File

@@ -1,12 +1,12 @@
import React, { useCallback, useEffect } from 'react';
import { Row, Col } from 'react-grid-system';
import { Button } from '@blueprintjs/core';
import React from 'react';
import moment from 'moment';
import { useFormik } from 'formik';
import { FormattedMessage as T } from 'react-intl';
import { Formik, Form } from 'formik';
import { Tab, Tabs, Button, Intent } from '@blueprintjs/core';
import * as Yup from 'yup';
import { FormattedMessage as T } from 'react-intl';
import JournalSheetHeaderGeneralPanel from './JournalSheetHeaderGeneralPanel';
import FinancialStatementDateRange from 'containers/FinancialStatements/FinancialStatementDateRange';
import FinancialStatementHeader from 'containers/FinancialStatements/FinancialStatementHeader';
import withJournal from './withJournal';
@@ -23,49 +23,75 @@ function JournalHeader({
// #withJournalActions
refreshJournalSheet,
toggleJournalSheetFilter,
// #withJournal
journalSheetFilter,
journalSheetRefresh,
}) {
const formik = useFormik({
enableReinitialize: true,
initialValues: {
...pageFilter,
from_date: moment(pageFilter.from_date).toDate(),
to_date: moment(pageFilter.to_date).toDate(),
},
validationSchema: Yup.object().shape({
from_date: Yup.date().required(),
to_date: Yup.date().min(Yup.ref('from_date')).required(),
}),
onSubmit: (values, { setSubmitting }) => {
onSubmitFilter(values);
setSubmitting(false);
},
const initialValues = {
...pageFilter,
fromDate: moment(pageFilter.fromDate).toDate(),
toDate: moment(pageFilter.toDate).toDate(),
};
// Validation schema.
const validationSchema = Yup.object().shape({
fromDate: Yup.date().required(),
toDate: Yup.date().min(Yup.ref('fromDate')).required(),
});
useEffect(() => {
if (journalSheetRefresh) {
formik.submitForm();
refreshJournalSheet(false);
}
}, [formik, journalSheetRefresh]);
// Handle form submit.
const handleSubmit = (values, { setSubmitting }) => {
onSubmitFilter(values);
setSubmitting(false);
toggleJournalSheetFilter();
};
// Handle cancel journal drawer header.
const handleCancelClick = () => {
toggleJournalSheetFilter();
};
const handleDrawerClose = () => {
toggleJournalSheetFilter();
};
return (
<FinancialStatementHeader show={journalSheetFilter}>
<Row>
<FinancialStatementDateRange formik={formik} />
</Row>
<FinancialStatementHeader
isOpen={journalSheetFilter}
drawerProps={{ onClose: handleDrawerClose }}
>
<Formik
initialValues={initialValues}
onSubmit={handleSubmit}
validationSchema={validationSchema}
>
<Form>
<Tabs animate={true} vertical={true} renderActiveTabPanelOnly={true}>
<Tab
id="general"
title={'General'}
panel={<JournalSheetHeaderGeneralPanel />}
/>
</Tabs>
<div class="financial-header-drawer__footer">
<Button className={'mr1'} intent={Intent.PRIMARY} type={'submit'}>
<T id={'calculate_report'} />
</Button>
<Button onClick={handleCancelClick} minimal={true}>
<T id={'cancel'} />
</Button>
</div>
</Form>
</Formik>
</FinancialStatementHeader>
);
}
export default compose(
withJournal(({
journalSheetFilter,
journalSheetRefresh
}) => ({
withJournal(({ journalSheetFilter, journalSheetRefresh }) => ({
journalSheetFilter,
journalSheetRefresh,
})),

View File

@@ -0,0 +1,10 @@
import React from 'react';
import FinancialStatementDateRange from 'containers/FinancialStatements/FinancialStatementDateRange';
export default function JournalSheetHeaderGeneralPanel({}) {
return (
<div>
<FinancialStatementDateRange />
</div>
);
}

View File

@@ -1,17 +1,15 @@
import React, { useCallback, useMemo } from 'react';
import { connect } from 'react-redux';
import moment from 'moment';
import { useIntl } from 'react-intl';
import FinancialSheet from 'components/FinancialSheet';
import DataTable from 'components/DataTable';
import { compose, defaultExpanderReducer } from 'utils';
import Money from 'components/Money';
import { getFinancialSheetIndexByQuery } from 'store/financialStatement/financialStatements.selectors';
import withJournal from './withJournal';
import { compose, defaultExpanderReducer } from 'utils';
function JournalSheetTable({
// #withJournal
journalSheetTableRows,
@@ -106,12 +104,12 @@ function JournalSheetTable({
return (
<FinancialSheet
companyName={companyName}
// sheetType={formatMessage({ id: 'journal_sheet' })}
sheetType={formatMessage({ id: 'journal_sheet' })}
fromDate={journalSheetQuery.from_date}
toDate={journalSheetQuery.to_date}
name="journal"
loading={journalSheetLoading}
minimal={true}
// minimal={true}
fullWidth={true}
>
<DataTable
@@ -129,20 +127,7 @@ function JournalSheetTable({
);
}
const mapStateToProps = (state, props) => {
const { journalQuery } = props;
return {
journalIndex: getFinancialSheetIndexByQuery(
state.financialStatements.journal.sheets,
journalQuery,
),
};
};
const withJournalTable = connect(mapStateToProps);
export default compose(
withJournalTable,
withJournal(
({ journalSheetTableRows, journalSheetLoading, journalSheetQuery }) => ({
journalSheetTableRows,

View File

@@ -1,34 +1,27 @@
import {connect} from 'react-redux';
import { connect } from 'react-redux';
import {
getFinancialSheetIndexByQuery,
getFinancialSheet,
getFinancialSheetTableRows,
getFinancialSheetQuery,
getFinancialSheetFactory,
getFinancialSheetTableRowsFactory,
getFinancialSheetQueryFactory,
} from 'store/financialStatement/financialStatements.selectors';
export default (mapState) => {
const mapStateToProps = (state, props) => {
const { journalIndex } = props;
const getJournalSheet = getFinancialSheetFactory('journal');
const getJournalSheetTableRows = getFinancialSheetTableRowsFactory(
'journal',
);
const getJournalSheetQuery = getFinancialSheetQueryFactory('journal');
const mapped = {
journalSheet: getFinancialSheet(
state.financialStatements.journal.sheets,
journalIndex
),
journalSheetTableRows: getFinancialSheetTableRows(
state.financialStatements.journal.sheets,
journalIndex
),
journalSheetQuery: getFinancialSheetQuery(
state.financialStatements.journal.sheets,
journalIndex,
),
journalSheet: getJournalSheet(state, props),
journalSheetTableRows: getJournalSheetTableRows(state, props),
journalSheetQuery: getJournalSheetQuery(state, props),
journalSheetLoading: state.financialStatements.journal.loading,
journalSheetFilter: state.financialStatements.journal.filter,
journalSheetRefresh: state.financialStatements.journal.refresh,
};
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};

View File

@@ -1,18 +1,24 @@
import React from 'react';
import { NavbarGroup, Button, Classes, NavbarDivider } from '@blueprintjs/core';
import {
NavbarGroup,
Button,
Classes,
NavbarDivider,
Popover,
Position,
PopoverInteractionKind,
} from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import classNames from 'classnames';
import Icon from 'components/Icon';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import { If } from 'components';
import withProfitLossActions from './withProfitLossActions';
import withProfitLoss from './withProfitLoss';
import { compose } from 'utils';
function ProfitLossActionsBar({
// #withProfitLoss
profitLossSheetFilter,
@@ -33,45 +39,43 @@ function ProfitLossActionsBar({
<DashboardActionsBar>
<NavbarGroup>
<Button
className={classNames(Classes.MINIMAL, 'button--table-views')}
icon={<Icon icon="cog-16" iconSize={16} />}
text={<T id={'customize_report'} />}
/>
<NavbarDivider />
<Button
className={classNames(
Classes.MINIMAL,
'button--gray-highlight',
)}
text={'Re-calc Report'}
className={classNames(Classes.MINIMAL, 'button--gray-highlight')}
text={<T id={'recalc_report'} />}
onClick={handleRecalcReport}
icon={<Icon icon="refresh-16" iconSize={16} />}
/>
<If condition={profitLossSheetFilter}>
<Button
className={Classes.MINIMAL}
text={<T id={'hide_filter'} />}
icon={<Icon icon="arrow-to-top" />}
onClick={handleFilterClick}
/>
</If>
<If condition={!profitLossSheetFilter}>
<Button
className={Classes.MINIMAL}
text={<T id={'show_filter'} />}
icon={<Icon icon="arrow-to-bottom" />}
onClick={handleFilterClick}
/>
</If>
<NavbarDivider />
<Button
className={classNames(Classes.MINIMAL, 'button--table-views')}
icon={<Icon icon="cog-16" iconSize={16} />}
text={
profitLossSheetFilter ? (
<T id={'hide_customizer'} />
) : (
<T id={'customize_report'} />
)
}
onClick={handleFilterClick}
active={profitLossSheetFilter}
/>
<NavbarDivider />
<Popover
// content={}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
className={classNames(Classes.MINIMAL, 'button--filter')}
text={<T id={'filter'} />}
icon={<Icon icon="filter-16" iconSize={16} />}
/>
</Popover>
<Button
className={Classes.MINIMAL}
icon={<Icon icon='print-16' iconSize={16} />}
icon={<Icon icon="print-16" iconSize={16} />}
text={<T id={'print'} />}
/>
<Button

View File

@@ -2,6 +2,8 @@ import React, {useState, useCallback, useEffect} from 'react';
import moment from 'moment';
import {compose} from 'utils';
import { useQuery } from 'react-query';
import { useIntl } from 'react-intl';
import { queryCache } from 'react-query';
import ProfitLossSheetHeader from './ProfitLossSheetHeader';
import ProfitLossSheetTable from './ProfitLossSheetTable';
@@ -13,59 +15,71 @@ import DashboardPageContent from 'components/Dashboard/DashboardPageContent'
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withProfitLossActions from './withProfitLossActions';
import withProfitLoss from './withProfitLoss';
// import SettingsConnect from 'connectors/Settings.connect';
import withSettings from 'containers/Settings/withSettings';
import { transformFilterFormToQuery } from 'containers/FinancialStatements/common';
function ProfitLossSheet({
// #withDashboardActions
changePageTitle,
setDashboardBackLink,
// #withProfitLoss
profitLossSheetRefresh,
// #withProfitLossActions
fetchProfitLossSheet,
refreshProfitLossSheet,
// #withPreferences
organizationSettings,
organizationName,
}) {
const [filter, setFilter] = useState({
basis: 'cash',
from_date: moment().startOf('year').format('YYYY-MM-DD'),
to_date: moment().endOf('year').format('YYYY-MM-DD'),
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
toDate: moment().endOf('year').format('YYYY-MM-DD'),
displayColumnsType: 'total',
accountsFilter: 'all-accounts',
});
const [refresh, setRefresh] = useState(true);
const { formatMessage } = useIntl();
// Change page title of the dashboard.
useEffect(() => {
changePageTitle('Profit/Loss Sheet');
}, [changePageTitle]);
changePageTitle(formatMessage({ id: 'profit_loss_sheet' }));
}, [changePageTitle, formatMessage]);
// Observes the P&L sheet refresh to invalid the query to refresh it.
useEffect(() => {
if (profitLossSheetRefresh) {
refreshProfitLossSheet(false);
queryCache.invalidateQueries('profit-loss-sheet');
}
}, [profitLossSheetRefresh, refreshProfitLossSheet]);
useEffect(() => {
// Show the back link on dashboard topbar.
setDashboardBackLink(true);
return () => {
// Hide the back link on dashboard topbar.
setDashboardBackLink(false);
};
});
// Fetches profit/loss sheet.
const fetchHook = useQuery(['profit-loss', filter],
(key, query) => fetchProfitLossSheet(query),
const fetchSheetHook = useQuery(['profit-loss-sheet', filter],
(key, query) => fetchProfitLossSheet({ ...transformFilterFormToQuery(query) }),
{ manual: true });
// Handle submit filter.
const handleSubmitFilter = useCallback((filter) => {
const _filter = {
...filter,
from_date: moment(filter.from_date).format('YYYY-MM-DD'),
to_date: moment(filter.to_date).format('YYYY-MM-DD'),
fromDate: moment(filter.fromDate).format('YYYY-MM-DD'),
toDate: moment(filter.toDate).format('YYYY-MM-DD'),
};
setFilter(_filter);
setRefresh(true);
}, []);
// Handle fetch data of profit/loss sheet table.
const handleFetchData = useCallback(() => {
setRefresh(true);
}, []);
useEffect(() => {
if (refresh) {
fetchHook.refetch({ force: true });
setRefresh(false);
}
}, [refresh, fetchHook]);
}, [setFilter]);
return (
<DashboardInsider>
@@ -79,10 +93,9 @@ function ProfitLossSheet({
<div class="financial-statement__body">
<ProfitLossSheetTable
companyName={organizationSettings.name}
profitLossQuery={filter}
onFetchData={handleFetchData} />
</div>
companyName={organizationName}
profitLossQuery={filter} />
</div>
</div>
</DashboardPageContent>
</DashboardInsider>
@@ -92,5 +105,8 @@ function ProfitLossSheet({
export default compose(
withDashboardActions,
withProfitLossActions,
withSettings,
withProfitLoss(({ profitLossSheetRefresh }) => ({ profitLossSheetRefresh })),
withSettings(({ organizationSettings }) => ({
organizationName: organizationSettings.name,
})),
)(ProfitLossSheet);

View File

@@ -1,126 +1,102 @@
import React, { useCallback, useEffect } from 'react';
import { Row, Col, Visible } from 'react-grid-system';
import React, { useEffect } from 'react';
import moment from 'moment';
import { useFormik } from 'formik';
import { Formik, Form } from 'formik';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { FormGroup } from '@blueprintjs/core';
import * as Yup from 'yup';
import { Tabs, Tab, Button, Intent } from '@blueprintjs/core';
import FinancialStatementDateRange from 'containers/FinancialStatements/FinancialStatementDateRange';
import FinancialStatementHeader from 'containers/FinancialStatements/FinancialStatementHeader';
import SelectsListColumnsBy from '../SelectDisplayColumnsBy';
import RadiosAccountingBasis from '../RadiosAccountingBasis';
import FinancialAccountsFilter from '../FinancialAccountsFilter';
import ProfitLossSheetHeaderGeneralPane from './ProfitLossSheetHeaderGeneralPane';
import withProfitLoss from './withProfitLoss';
import withProfitLossActions from './withProfitLossActions';
import { compose } from 'utils';
function ProfitLossHeader({
// #ownProps
pageFilter,
onSubmitFilter,
// #withProfitLoss
profitLossSheetFilter,
profitLossSheetRefresh,
// #withProfitLossActions
refreshProfitLossSheet,
toggleProfitLossSheetFilter,
}) {
const { formatMessage } = useIntl();
const formik = useFormik({
enableReinitialize: true,
initialValues: {
...pageFilter,
from_date: moment(pageFilter.from_date).toDate(),
to_date: moment(pageFilter.to_date).toDate(),
},
validationSchema: Yup.object().shape({
from_date: Yup.date()
.required()
.label(formatMessage({ id: 'from_date' })),
to_date: Yup.date()
.min(Yup.ref('from_date'))
.required()
.label(formatMessage({ id: 'to_date' })),
}),
onSubmit: (values, actions) => {
onSubmitFilter(values);
actions.setSubmitting(false);
},
// Validation schema.
const validationSchema = Yup.object().shape({
fromDate: Yup.date()
.required()
.label(formatMessage({ id: 'from_date' })),
toDate: Yup.date()
.min(Yup.ref('fromDate'))
.required()
.label(formatMessage({ id: 'to_date' })),
accountsFilter: Yup.string(),
displayColumnsType: Yup.string(),
});
// Handle item select of `display columns by` field.
const handleItemSelectDisplayColumns = useCallback(
(item) => {
formik.setFieldValue('display_columns_type', item.type);
formik.setFieldValue('display_columns_by', item.by);
},
[formik],
);
// Initial values.
const initialValues = {
...pageFilter,
fromDate: moment(pageFilter.fromDate).toDate(),
toDate: moment(pageFilter.toDate).toDate(),
};
const handleAccountingBasisChange = useCallback(
(value) => {
formik.setFieldValue('basis', value);
},
[formik],
);
// Handle form submit.
const handleSubmit = (values, actions) => {
onSubmitFilter(values);
toggleProfitLossSheetFilter();
};
useEffect(() => {
if (profitLossSheetRefresh) {
formik.submitForm();
refreshProfitLossSheet(false);
}
}, [profitLossSheetRefresh]);
const handleAccountsFilterSelect = (filterType) => {
const noneZero = filterType.key === 'without-zero-balance' ? true : false;
formik.setFieldValue('none_zero', noneZero);
// Handles the cancel button click.
const handleCancelClick = () => {
toggleProfitLossSheetFilter();
};
// Handles the drawer close action.
const handleDrawerClose = () => {
toggleProfitLossSheetFilter();
};
return (
<FinancialStatementHeader show={profitLossSheetFilter}>
<Row>
<FinancialStatementDateRange formik={formik} />
<Visible xl><Col width={'100%'} /></Visible>
<Col width={260}>
<SelectsListColumnsBy onItemSelect={handleItemSelectDisplayColumns} />
</Col>
<Col width={260}>
<FormGroup
label={<T id={'filter_accounts'} />}
className="form-group--select-list bp3-fill"
inline={false}
>
<FinancialAccountsFilter
initialSelectedItem={'all-accounts'}
onItemSelect={handleAccountsFilterSelect}
<FinancialStatementHeader
isOpen={profitLossSheetFilter}
drawerProps={{ onClose: handleDrawerClose }}
>
<Formik
validationSchema={validationSchema}
initialValues={initialValues}
onSubmit={handleSubmit}
>
<Form>
<Tabs animate={true} vertical={true} renderActiveTabPanelOnly={true}>
<Tab
id="general"
title={<T id={'general'} />}
panel={<ProfitLossSheetHeaderGeneralPane />}
/>
</FormGroup>
</Col>
</Tabs>
<Col width={260}>
<RadiosAccountingBasis
selectedValue={formik.values.basis}
onChange={handleAccountingBasisChange}
/>
</Col>
</Row>
<div class="financial-header-drawer__footer">
<Button className={'mr1'} intent={Intent.PRIMARY} type={'submit'}>
<T id={'calculate_report'} />
</Button>
<Button onClick={handleCancelClick} minimal={true}>
<T id={'cancel'} />
</Button>
</div>
</Form>
</Formik>
</FinancialStatementHeader>
);
}
export default compose(
withProfitLoss(({
withProfitLoss(({ profitLossSheetFilter }) => ({
profitLossSheetFilter,
profitLossSheetRefresh,
}) => ({
profitLossSheetFilter,
profitLossSheetRefresh,
})),
withProfitLossActions,
)(ProfitLossHeader);

View File

@@ -0,0 +1,20 @@
import React from 'react';
import FinancialStatementDateRange from 'containers/FinancialStatements/FinancialStatementDateRange';
import SelectDisplayColumnsBy from '../SelectDisplayColumnsBy';
import RadiosAccountingBasis from '../RadiosAccountingBasis';
import FinancialAccountsFilter from '../FinancialAccountsFilter';
/**
* Profit/Loss sheet - Drawer header - General panel.
*/
export default function ProfitLossSheetHeaderGeneralPane({}) {
return (
<div>
<FinancialStatementDateRange />
<SelectDisplayColumnsBy />
<FinancialAccountsFilter initialSelectedItem={'all-accounts'} />
<RadiosAccountingBasis key={'basis'} />
</div>
);
}

View File

@@ -1,13 +1,11 @@
import React, { useMemo, useCallback } from 'react';
import { connect } from 'react-redux';
import { FormattedMessage as T, useIntl } from 'react-intl';
import FinancialSheet from 'components/FinancialSheet';
import DataTable from 'components/DataTable';
import Money from 'components/Money';
import { compose, defaultExpanderReducer } from 'utils';
import { getFinancialSheetIndexByQuery } from 'store/financialStatement/financialStatements.selectors';
import { compose, defaultExpanderReducer, getColumnWidth } from 'utils';
import withProfitLossDetail from './withProfitLoss';
function ProfitLossSheetTable({
@@ -18,7 +16,6 @@ function ProfitLossSheetTable({
profitLossSheetLoading,
// #ownProps
onFetchData,
companyName,
}) {
const { formatMessage } = useIntl();
@@ -26,14 +23,10 @@ function ProfitLossSheetTable({
const columns = useMemo(
() => [
{
Header: formatMessage({ id: 'account_name' }),
accessor: 'name',
Header: formatMessage({ id: 'account' }),
accessor: (row) => (row.code ? `${row.name} - ${row.code}` : row.name),
className: 'name',
},
{
Header: formatMessage({ id: 'account_code' }),
accessor: 'code',
className: 'account_code',
width: 240,
},
...(profitLossQuery.display_columns_type === 'total'
? [
@@ -45,13 +38,14 @@ function ProfitLossSheetTable({
return (
<Money
amount={row.total.formatted_amount}
currency={'USD'}
currency={row.total.currency_code}
/>
);
}
return '';
},
className: 'total',
width: 140,
},
]
: []),
@@ -60,40 +54,44 @@ function ProfitLossSheetTable({
id: `date_period_${index}`,
Header: column,
accessor: (row) => {
if (row.periods && row.periods[index]) {
const amount = row.periods[index].formatted_amount;
if (row.total_periods && row.total_periods[index]) {
const amount = row.total_periods[index].formatted_amount;
return <Money amount={amount} currency={'USD'} />;
}
return '';
},
width: 100,
width: getColumnWidth(
profitLossTableRows,
`total_periods.${index}.formatted_amount`,
{ minWidth: 100 },
),
className: 'total-period',
}))
: []),
],
[profitLossQuery.display_columns_type, profitLossColumns, formatMessage],
);
// Handle data table fetch data.
const handleFetchData = useCallback(
(...args) => {
onFetchData && onFetchData(...args);
},
[onFetchData],
[profitLossQuery.display_columns_type, profitLossTableRows, profitLossColumns, formatMessage],
);
// Retrieve default expanded rows of balance sheet.
const expandedRows = useMemo(
() => defaultExpanderReducer(profitLossTableRows, 1),
() => defaultExpanderReducer(profitLossTableRows, 3),
[profitLossTableRows],
);
// Retrieve conditional datatable row classnames.
const rowClassNames = useCallback(
(row) => ({
[`row--${row.rowType}`]: row.rowType,
}),
[],
);
const rowClassNames = useCallback((row) => {
const { original } = row;
const rowTypes = Array.isArray(original.rowTypes)
? original.rowTypes
: [];
return {
...rowTypes.reduce((acc, rowType) => {
acc[`row_type--${rowType}`] = rowType;
return acc;
}, {}),
};
}, []);
return (
<FinancialSheet
@@ -109,7 +107,6 @@ function ProfitLossSheetTable({
className="bigcapital-datatable--financial-report"
columns={columns}
data={profitLossTableRows}
onFetchData={handleFetchData}
noInitialFetch={true}
expanded={expandedRows}
rowClassNames={rowClassNames}
@@ -121,19 +118,14 @@ function ProfitLossSheetTable({
);
}
const mapStateToProps = (state, props) => ({
profitLossIndex: getFinancialSheetIndexByQuery(
state.financialStatements.profitLoss.sheets,
props.profitLossQuery,
),
});
const withProfitLossTable = connect(mapStateToProps);
export default compose(
withProfitLossTable,
withProfitLossDetail(
({ profitLossQuery, profitLossColumns, profitLossTableRows, profitLossSheetLoading }) => ({
({
profitLossQuery,
profitLossColumns,
profitLossTableRows,
profitLossSheetLoading,
}) => ({
profitLossColumns,
profitLossQuery,
profitLossTableRows,

View File

@@ -1,22 +1,23 @@
import {connect} from 'react-redux';
import {
getFinancialSheetIndexByQuery,
getFinancialSheet,
getFinancialSheetColumns,
getFinancialSheetQuery,
getFinancialSheetTableRows,
getFinancialSheetFactory,
getFinancialSheetColumnsFactory,
getFinancialSheetQueryFactory,
getFinancialSheetTableRowsFactory,
} from 'store/financialStatement/financialStatements.selectors';
export default (mapState) => {
const mapStateToProps = (state, props) => {
const { profitLossIndex } = props;
const getProfitLossSheet = getFinancialSheetFactory('profitLoss');
const getProfitLossColumns = getFinancialSheetColumnsFactory('profitLoss');
const getProfitLossQuery = getFinancialSheetQueryFactory('profitLoss');
const getProfitLossTableRows = getFinancialSheetTableRowsFactory('profitLoss');
const mapped = {
profitLossSheet: getFinancialSheet(state.financialStatements.profitLoss.sheets, profitLossIndex),
profitLossColumns: getFinancialSheetColumns(state.financialStatements.profitLoss.sheets, profitLossIndex),
profitLossQuery: getFinancialSheetQuery(state.financialStatements.profitLoss.sheets, profitLossIndex),
profitLossTableRows: getFinancialSheetTableRows(state.financialStatements.profitLoss.sheets, profitLossIndex),
profitLossSheet: getProfitLossSheet(state, props),
profitLossColumns: getProfitLossColumns(state, props),
profitLossQuery: getProfitLossQuery(state, props),
profitLossTableRows: getProfitLossTableRows(state, props),
profitLossSheetLoading: state.financialStatements.profitLoss.loading,
profitLossSheetFilter: state.financialStatements.profitLoss.filter,

View File

@@ -1,28 +1,34 @@
import React from 'react';
import {handleStringChange} from 'utils';
import {useIntl} from 'react-intl';
import {
RadioGroup,
Radio,
} from "@blueprintjs/core";
import { FastField } from 'formik';
import { handleStringChange } from 'utils';
import { useIntl } from 'react-intl';
import { RadioGroup, Radio } from '@blueprintjs/core';
export default function RadiosAccountingBasis(props) {
const { onChange, ...rest } = props;
const {formatMessage} = useIntl();
const { key = 'basis', ...rest } = props;
const { formatMessage } = useIntl();
return (
<RadioGroup
inline={true}
label={formatMessage({'id': 'accounting_basis'})}
name="basis"
onChange={handleStringChange((value) => {
onChange && onChange(value);
})}
className={'radio-group---accounting-basis'}
{...rest}>
<Radio label={formatMessage({id:'cash'})} value="cash" />
<Radio label={formatMessage({id:'accrual'})} value="accural" />
</RadioGroup>
<FastField name={'basis'}>
{({
form: { setFieldValue },
field: { value },
}) => (
<RadioGroup
inline={true}
label={formatMessage({ id: 'accounting_basis' })}
name="basis"
onChange={handleStringChange((value) => {
setFieldValue(key, value);
})}
className={'radio-group---accounting-basis'}
selectedValue={value}
{...rest}
>
<Radio label={formatMessage({ id: 'cash' })} value="cash" />
<Radio label={formatMessage({ id: 'accrual' })} value="accural" />
</RadioGroup>
)}
</FastField>
);
}
}

View File

@@ -1,58 +1,43 @@
import React, { useMemo, useState, useCallback } from 'react';
import SelectList from 'components/SelectList';
import {
FormGroup,
MenuItem,
} from '@blueprintjs/core';
import React from 'react';
import { FormGroup } from '@blueprintjs/core';
import { FastField } from 'formik';
import { FormattedMessage as T } from 'react-intl';
import classNames from 'classnames';
import { MODIFIER } from 'components';
import { Row, Col, ListSelect } from 'components';
import { displayColumnsByOptions } from 'containers/FinancialStatements/common';
/**
* Financial statement - Display columns by and type select.
*/
export default function SelectsListColumnsBy(props) {
const { onItemSelect, formGroupProps, selectListProps } = props;
const [itemSelected, setItemSelected] = useState(null);
const displayColumnsByOptions = useMemo(() => [
{key: 'total', name: 'Total', type: 'total', by: '', },
{key: 'year', name: 'Date/Year', type: 'date_periods', by: 'year'},
{key: 'month', name: 'Date/Month', type: 'date_periods', by: 'month'},
{key: 'week', name: 'Date/Week', type: 'date_periods', by: 'month'},
{key: 'day', name: 'Date/Day', type: 'date_periods', by: 'day'},
{key: 'quarter', name: 'Date/Quarter', type: 'date_periods', by: 'quarter'},
],[]);
const itemRenderer = useCallback((item, { handleClick, modifiers, query }) => {
return (<MenuItem text={item.name} key={item.id} onClick={handleClick} />);
}, []);
const handleItemSelect = useCallback((item) => {
setItemSelected(item);
onItemSelect && onItemSelect(item);
}, [setItemSelected, onItemSelect]);
const buttonLabel = useMemo(() =>
itemSelected ? itemSelected.name : <T id={'select_display_columns_by'}/>,
[itemSelected]);
const { formGroupProps, selectListProps } = props;
return (
<FormGroup
label={<T id={'display_report_columns'}/>}
className="form-group-display-columns-by form-group--select-list bp3-fill"
inline={false}
{...formGroupProps}>
<SelectList
items={displayColumnsByOptions}
noResults={<MenuItem disabled={true} text="No results." />}
filterable={false}
itemRenderer={itemRenderer}
popoverProps={{ minimal: true, usePortal: false, inline: true }}
buttonLabel={buttonLabel}
onItemSelect={handleItemSelect}
className={classNames(MODIFIER.SELECT_LIST_FILL_POPOVER)}
{...selectListProps} />
</FormGroup>
<Row>
<Col xs={4}>
<FastField name={'displayColumnsType'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'display_report_columns'} />}
className="form-group-display-columns-by form-group--select-list bp3-fill"
inline={false}
{...formGroupProps}
>
<ListSelect
items={displayColumnsByOptions}
filterable={false}
selectedItem={value}
selectedItemProp={'key'}
labelProp={'name'}
onItemSelect={(item) => {
form.setFieldValue('displayColumnsType', item.key);
}}
popoverProps={{ minimal: true }}
{...selectListProps}
/>
</FormGroup>
)}
</FastField>
</Col>
</Row>
);
}
}

View File

@@ -1,20 +1,24 @@
import React from 'react';
import { NavbarGroup, Button, Classes, NavbarDivider } from '@blueprintjs/core';
import Icon from 'components/Icon';
import { FormattedMessage as T } from 'react-intl';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import {
NavbarGroup,
Button,
Classes,
NavbarDivider,
Popover,
PopoverInteractionKind,
Position,
} from '@blueprintjs/core';
import classNames from 'classnames';
// import FilterDropdown from 'components/FilterDropdown';
import { FormattedMessage as T } from 'react-intl';
import { If } from 'components';
import Icon from 'components/Icon';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import withTrialBalance from './withTrialBalance';
import withTrialBalanceActions from './withTrialBalanceActions';
import { compose } from 'utils';
function TrialBalanceActionsBar({
// #withTrialBalance
trialBalanceSheetFilter,
@@ -22,7 +26,6 @@ function TrialBalanceActionsBar({
toggleTrialBalanceFilter,
refreshTrialBalance,
}) {
const handleFilterToggleClick = () => {
toggleTrialBalanceFilter();
};
@@ -35,45 +38,43 @@ function TrialBalanceActionsBar({
<DashboardActionsBar>
<NavbarGroup>
<Button
className={classNames(Classes.MINIMAL, 'button--table-views')}
icon={<Icon icon="cog-16" iconSize={16} />}
text={<T id={'customize_report'} />}
/>
<NavbarDivider />
<Button
className={classNames(
Classes.MINIMAL,
'button--gray-highlight',
)}
className={classNames(Classes.MINIMAL, 'button--gray-highlight')}
text={'Re-calc Report'}
onClick={handleRecalcReport}
icon={<Icon icon="refresh-16" iconSize={16} />}
/>
<If condition={trialBalanceSheetFilter}>
<Button
className={Classes.MINIMAL}
text={<T id={'hide_filter'} />}
icon={<Icon icon="arrow-to-top" />}
onClick={handleFilterToggleClick}
/>
</If>
<If condition={!trialBalanceSheetFilter}>
<Button
className={Classes.MINIMAL}
text={<T id={'show_filter'} />}
icon={<Icon icon="arrow-to-bottom" />}
onClick={handleFilterToggleClick}
/>
</If>
<NavbarDivider />
<Button
className={classNames(Classes.MINIMAL, 'button--table-views')}
icon={<Icon icon="cog-16" iconSize={16} />}
text={
trialBalanceSheetFilter ? (
<T id={'hide_customizer'} />
) : (
<T id={'customize_report'} />
)
}
active={trialBalanceSheetFilter}
onClick={handleFilterToggleClick}
/>
<NavbarDivider />
<Popover
// content={}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
className={classNames(Classes.MINIMAL, 'button--filter')}
text={<T id={'filter'} />}
icon={<Icon icon="filter-16" iconSize={16} />}
/>
</Popover>
<Button
className={Classes.MINIMAL}
icon={<Icon icon='print-16' iconSize={16} />}
icon={<Icon icon="print-16" iconSize={16} />}
text={<T id={'print'} />}
/>
<Button
@@ -87,6 +88,8 @@ function TrialBalanceActionsBar({
}
export default compose(
withTrialBalance(({ trialBalanceSheetFilter }) => ({ trialBalanceSheetFilter })),
withTrialBalanceActions
)(TrialBalanceActionsBar);
withTrialBalance(({ trialBalanceSheetFilter }) => ({
trialBalanceSheetFilter,
})),
withTrialBalanceActions,
)(TrialBalanceActionsBar);

View File

@@ -2,73 +2,93 @@ import React, { useEffect, useCallback, useState } from 'react';
import { useQuery } from 'react-query';
import moment from 'moment';
import { useIntl } from 'react-intl';
import { queryCache } from 'react-query';
import TrialBalanceActionsBar from './TrialBalanceActionsBar';
import TrialBalanceSheetHeader from './TrialBalanceSheetHeader';
import TrialBalanceSheetTable from './TrialBalanceSheetTable';
import TrialBalanceActionsBar from './TrialBalanceActionsBar';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import { compose } from 'utils';
import { transformFilterFormToQuery } from 'containers/FinancialStatements/common';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withTrialBalanceActions from './withTrialBalanceActions';
import withSettings from 'containers/Settings/withSettings';
import withTrialBalance from './withTrialBalance';
/**
* Trial balance sheet.
*/
function TrialBalanceSheet({
// #withDashboardActions
changePageTitle,
setDashboardBackLink,
// #withTrialBalance
trialBalanceSheetRefresh,
// #withTrialBalanceActions
fetchTrialBalanceSheet,
refreshTrialBalance,
// #withPreferences
organizationSettings,
organizationName,
}) {
const [filter, setFilter] = useState({
from_date: moment().startOf('year').format('YYYY-MM-DD'),
to_date: moment().endOf('year').format('YYYY-MM-DD'),
basis: 'accural',
none_zero: false,
});
const [refresh, setRefresh] = useState(true);
const { formatMessage } = useIntl();
const fetchHook = useQuery(
['trial-balance', filter],
(key, query) => fetchTrialBalanceSheet(query),
const [filter, setFilter] = useState({
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
toDate: moment().endOf('year').format('YYYY-MM-DD'),
basis: 'accural',
accountsFilter: 'all-accounts',
});
// Fetches trial balance sheet.
const fetchSheet = useQuery(
['trial-balance-sheet', filter],
(key, query) =>
fetchTrialBalanceSheet({
...transformFilterFormToQuery(query),
}),
{ manual: true },
);
// handle fetch data of trial balance table.
const handleFetchData = useCallback(() => {
setRefresh(true);
}, []);
// Change page title of the dashboard.
useEffect(() => {
changePageTitle(formatMessage({ id: 'trial_balance_sheet' }));
}, [changePageTitle, formatMessage]);
useEffect(() => {
// Show the back link on dashboard topbar.
setDashboardBackLink(true);
return () => {
// Hide the back link on dashboard topbar.
setDashboardBackLink(false);
};
});
const handleFilterSubmit = useCallback(
(filter) => {
const parsedFilter = {
...filter,
from_date: moment(filter.from_date).format('YYYY-MM-DD'),
to_date: moment(filter.to_date).format('YYYY-MM-DD'),
fromDate: moment(filter.fromDate).format('YYYY-MM-DD'),
toDate: moment(filter.toDate).format('YYYY-MM-DD'),
};
setFilter(parsedFilter);
setRefresh(true);
refreshTrialBalance(true);
},
[fetchHook],
[setFilter, refreshTrialBalance],
);
// Observes the trial balance sheet refresh to invaoid the query.
useEffect(() => {
if (refresh) {
fetchHook.refetch({ force: true });
setRefresh(false);
if (trialBalanceSheetRefresh) {
queryCache.invalidateQueries('trial-balance-sheet');
refreshTrialBalance(false);
}
}, [refresh, fetchHook.refetch]);
}, [trialBalanceSheetRefresh, refreshTrialBalance]);
return (
<DashboardInsider>
@@ -82,11 +102,7 @@ function TrialBalanceSheet({
/>
<div class="financial-statement__body">
<TrialBalanceSheetTable
companyName={organizationSettings.name}
trialBalanceQuery={filter}
onFetchData={handleFetchData}
/>
<TrialBalanceSheetTable companyName={organizationName} />
</div>
</div>
</DashboardPageContent>
@@ -97,5 +113,10 @@ function TrialBalanceSheet({
export default compose(
withDashboardActions,
withTrialBalanceActions,
withSettings,
withTrialBalance(({ trialBalanceSheetRefresh }) => ({
trialBalanceSheetRefresh,
})),
withSettings(({ organizationSettings }) => ({
organizationName: organizationSettings.name,
})),
)(TrialBalanceSheet);

View File

@@ -1,15 +1,12 @@
import React, { useEffect, useCallback } from 'react';
import React from 'react';
import * as Yup from 'yup';
import moment from 'moment';
import { Row, Col, Visible } from 'react-grid-system';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { FormGroup } from '@blueprintjs/core';
import { useFormik } from 'formik';
import { Formik, Form } from 'formik';
import { Tabs, Tab, Button, Intent } from '@blueprintjs/core';
import FinancialStatementHeader from 'containers/FinancialStatements/FinancialStatementHeader';
import FinancialStatementDateRange from 'containers/FinancialStatements/FinancialStatementDateRange';
import RadiosAccountingBasis from '../RadiosAccountingBasis';
import FinancialAccountsFilter from '../FinancialAccountsFilter';
import TrialBalanceSheetHeaderGeneralPanel from './TrialBalanceSheetHeaderGeneralPanel';
import withTrialBalance from './withTrialBalance';
import withTrialBalanceActions from './withTrialBalanceActions';
@@ -17,6 +14,7 @@ import withTrialBalanceActions from './withTrialBalanceActions';
import { compose } from 'utils';
function TrialBalanceSheetHeader({
// #ownProps
pageFilter,
onSubmitFilter,
@@ -26,78 +24,78 @@ function TrialBalanceSheetHeader({
// #withTrialBalanceActions
refreshTrialBalance,
toggleTrialBalanceFilter
}) {
const { formatMessage } = useIntl();
const formik = useFormik({
enableReinitialize: true,
initialValues: {
...pageFilter,
from_date: moment(pageFilter.from_date).toDate(),
to_date: moment(pageFilter.to_date).toDate(),
},
validationSchema: Yup.object().shape({
from_date: Yup.date()
.required()
.label(formatMessage({ id: 'from_date' })),
to_date: Yup.date()
.min(Yup.ref('from_date'))
.required()
.label(formatMessage({ id: 'to_date' })),
}),
onSubmit: (values, { setSubmitting }) => {
onSubmitFilter(values);
setSubmitting(false);
},
// Form validation schema.
const validationSchema = Yup.object().shape({
fromDate: Yup.date()
.required()
.label(formatMessage({ id: 'from_date' })),
toDate: Yup.date()
.min(Yup.ref('fromDate'))
.required()
.label(formatMessage({ id: 'to_date' })),
});
useEffect(() => {
if (trialBalanceSheetRefresh) {
formik.submitForm();
refreshTrialBalance(false);
}
}, [formik, trialBalanceSheetRefresh]);
// Initial values.
const initialValues = {
...pageFilter,
fromDate: moment(pageFilter.fromDate).toDate(),
toDate: moment(pageFilter.toDate).toDate(),
};
const handleAccountingBasisChange = useCallback(
(value) => {
formik.setFieldValue('basis', value);
},
[formik],
);
// Handle form submit.
const handleSubmit = (values, { setSubmitting }) => {
onSubmitFilter(values);
setSubmitting(false);
toggleTrialBalanceFilter(false);
};
const handleAccountsFilterSelect = (filterType) => {
const noneZero = filterType.key === 'without-zero-balance' ? true : false;
formik.setFieldValue('none_zero', noneZero);
// Handle drawer close action.
const handleDrawerClose = () => {
toggleTrialBalanceFilter(false);
};
// Handle cancel button click.
const handleCancelClick = () => {
toggleTrialBalanceFilter(false);
};
return (
<FinancialStatementHeader show={trialBalanceSheetFilter}>
<Row>
<FinancialStatementDateRange formik={formik} />
<Visible xl>
<Col width={'100%'} />
</Visible>
<Col width={260}>
<FormGroup
label={<T id={'filter_accounts'} />}
className="form-group--select-list bp3-fill"
inline={false}
>
<FinancialAccountsFilter
initialSelectedItem={'all-accounts'}
onItemSelect={handleAccountsFilterSelect}
<FinancialStatementHeader
isOpen={trialBalanceSheetFilter}
drawerProps={{ onClose: handleDrawerClose }}
>
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={handleSubmit}
>
<Form>
<Tabs animate={true} vertical={true} renderActiveTabPanelOnly={true}>
<Tab
id="general"
title={<T id={'general'} />}
panel={<TrialBalanceSheetHeaderGeneralPanel />}
/>
</FormGroup>
</Col>
</Tabs>
<Col width={260}>
<RadiosAccountingBasis
selectedValue={formik.values.basis}
onChange={handleAccountingBasisChange}
/>
</Col>
</Row>
<div class="financial-header-drawer__footer">
<Button
className={'mr1'}
intent={Intent.PRIMARY}
type={'submit'}
>
<T id={'calculate_report'} />
</Button>
<Button onClick={handleCancelClick} minimal={true}>
<T id={'cancel'} />
</Button>
</div>
</Form>
</Formik>
</FinancialStatementHeader>
);
}

View File

@@ -0,0 +1,19 @@
import React from 'react';
import FinancialStatementDateRange from 'containers/FinancialStatements/FinancialStatementDateRange';
import RadiosAccountingBasis from '../RadiosAccountingBasis';
import FinancialAccountsFilter from '../FinancialAccountsFilter';
/**
* Trial balance sheet - Drawer header - General panel.
*/
export default function TrialBalanceSheetHeaderGeneralPanel({
}) {
return (
<div>
<FinancialStatementDateRange />
<FinancialAccountsFilter initialSelectedItem={'all-accounts'} />
<RadiosAccountingBasis />
</div>
);
}

View File

@@ -1,11 +1,9 @@
import React, { useCallback, useMemo } from 'react';
import { connect } from 'react-redux';
import React, { useMemo } from 'react';
import { useIntl } from 'react-intl';
import FinancialSheet from 'components/FinancialSheet';
import DataTable from 'components/DataTable';
import Money from 'components/Money';
import { getFinancialSheetIndexByQuery } from 'store/financialStatement/financialStatements.selectors';
import withTrialBalance from './withTrialBalance';
@@ -13,14 +11,12 @@ import { compose } from 'utils';
function TrialBalanceSheetTable({
// #withTrialBalanceDetail
trialBalanceAccounts,
trialBalance,
trialBalanceSheetLoading,
// #withTrialBalanceTable
trialBalanceIndex,
trialBalanceQuery,
onFetchData,
companyName,
}) {
const { formatMessage } = useIntl();
@@ -29,55 +25,46 @@ function TrialBalanceSheetTable({
() => [
{
Header: formatMessage({ id: 'account_name' }),
accessor: 'name',
accessor: (row) => (row.code ? `${row.name} - ${row.code}` : row.name),
className: 'name',
minWidth: 150,
maxWidth: 150,
width: 150,
},
{
Header: formatMessage({ id: 'code' }),
accessor: 'code',
className: 'code',
minWidth: 80,
maxWidth: 80,
width: 80,
},
{
Header: formatMessage({ id: 'credit' }),
accessor: 'credit',
Cell: ({ cell }) => <Money amount={cell.row.original.credit} currency="USD" />,
Cell: ({ cell }) => {
const { currency_code, credit } = cell.row.original;
return (<Money amount={credit} currency={currency_code} />);
},
className: 'credit',
minWidth: 95,
maxWidth: 95,
width: 95,
},
{
Header: formatMessage({ id: 'debit' }),
accessor: 'debit',
Cell: ({ cell }) => <Money amount={cell.row.original.debit} currency="USD" />,
Cell: ({ cell }) => {
const { currency_code, debit } = cell.row.original;
return (<Money amount={debit} currency={currency_code} />);
},
className: 'debit',
minWidth: 95,
maxWidth: 95,
width: 95,
},
{
Header: formatMessage({ id: 'balance' }),
accessor: 'balance',
Cell: ({ cell }) => <Money amount={cell.row.original.balance} currency="USD" />,
Cell: ({ cell }) => {
const { currency_code, balance } = cell.row.original;
return (<Money amount={balance} currency={currency_code} />);
},
className: 'balance',
minWidth: 95,
maxWidth: 95,
width: 95,
},
],
[formatMessage],
);
const handleFetchData = useCallback(() => {
onFetchData && onFetchData();
}, [onFetchData]);
return (
<FinancialSheet
companyName={companyName}
@@ -86,12 +73,12 @@ function TrialBalanceSheetTable({
toDate={trialBalanceQuery.to_date}
name="trial-balance"
loading={trialBalanceSheetLoading}
basis={'cash'}
>
<DataTable
className="bigcapital-datatable--financial-report"
columns={columns}
data={trialBalanceAccounts}
onFetchData={handleFetchData}
data={trialBalance.data}
expandable={true}
expandToggleColumn={1}
expandColumnSpace={1}
@@ -101,25 +88,14 @@ function TrialBalanceSheetTable({
);
}
const mapStateToProps = (state, props) => {
const { trialBalanceQuery } = props;
return {
trialBalanceIndex: getFinancialSheetIndexByQuery(
state.financialStatements.trialBalance.sheets,
trialBalanceQuery,
),
};
};
const withTrialBalanceTable = connect(mapStateToProps);
export default compose(
withTrialBalanceTable,
withTrialBalance(({
trialBalanceAccounts,
trialBalance,
trialBalanceSheetLoading,
trialBalanceQuery
}) => ({
trialBalanceAccounts,
trialBalanceSheetLoading
trialBalance,
trialBalanceSheetLoading,
trialBalanceQuery
})),
)(TrialBalanceSheetTable);

View File

@@ -1,28 +1,22 @@
import {connect} from 'react-redux';
import {
getFinancialSheetAccounts,
getFinancialSheetQuery,
getFinancialSheetFactory,
getFinancialSheetQueryFactory,
} from 'store/financialStatement/financialStatements.selectors';
export default (mapState) => {
const mapStateToProps = (state, props) => {
const { trialBalanceIndex } = props;
const getTrialBalance = getFinancialSheetFactory('trialBalance');
const getBalanceSheetQuery = getFinancialSheetQueryFactory('trialBalance');
const mapped = {
trialBalanceAccounts: getFinancialSheetAccounts(
state.financialStatements.trialBalance.sheets,
trialBalanceIndex
),
trialBalanceQuery: getFinancialSheetQuery(
state.financialStatements.trialBalance.sheets,
trialBalanceIndex
),
trialBalance: getTrialBalance(state, props),
trialBalanceQuery: getBalanceSheetQuery(state, props),
trialBalanceSheetLoading: state.financialStatements.trialBalance.loading,
trialBalanceSheetFilter: state.financialStatements.trialBalance.filter,
trialBalanceSheetRefresh: state.financialStatements.trialBalance.refresh,
};
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};

View File

@@ -0,0 +1,61 @@
import { mapKeys, omit, snakeCase } from 'lodash';
import { formatMessage } from 'services/intl';
export const displayColumnsByOptions = [
{ key: 'total', name: 'Total', type: 'total', by: '' },
{ key: 'year', name: 'Date/Year', type: 'date_periods', by: 'year' },
{ key: 'month', name: 'Date/Month', type: 'date_periods', by: 'month' },
{ key: 'week', name: 'Date/Week', type: 'date_periods', by: 'month' },
{ key: 'day', name: 'Date/Day', type: 'date_periods', by: 'day' },
{ key: 'quarter', name: 'Date/Quarter', type: 'date_periods', by: 'quarter' },
];
export const dateRangeOptions = [
{ value: 'today', label: 'Today' },
{ value: 'this_week', label: 'This Week' },
{ value: 'this_month', label: 'This Month' },
{ value: 'this_quarter', label: 'This Quarter' },
{ value: 'this_year', label: 'This Year' },
{ value: 'custom', label: 'Custom Range' },
];
export const filterAccountsOptions = [
{
key: 'all-accounts',
name: formatMessage({ id: 'all_accounts' }),
hint: formatMessage({ id: 'all_accounts_including_with_zero_balance' }),
},
{
key: 'without-zero-balance',
name: formatMessage({ id: 'accounts_without_zero_balance' }),
hint: formatMessage({
id: 'include_accounts_and_exclude_zero_balance',
}),
},
{
key: 'with-transactions',
name: formatMessage({ id: 'accounts_with_transactions' }),
hint: formatMessage({
id: 'include_accounts_once_has_transactions_on_given_date_period',
}),
},
];
export const transformDisplayColumnsType = (form) => {
const columnType = displayColumnsByOptions.find(
(o) => o.key === form.displayColumnsType,
);
return {
displayColumnsBy: columnType ? columnType.by : '',
displayColumnsType: columnType ? columnType.type : 'total',
};
};
export const transformFilterFormToQuery = (form) => {
return mapKeys({
...omit(form, ['accountsFilter']),
...transformDisplayColumnsType(form),
noneZero: form.accountsFilter === 'without-zero-balance',
noneTransactions: form.accountsFilter === 'with-transactions',
}, (v, k) => snakeCase(k));
};