This commit is contained in:
elforjani3
2020-12-31 12:26:53 +02:00
63 changed files with 2496 additions and 1932 deletions

View File

@@ -49,7 +49,6 @@ const CLASSES = {
SELECT_LIST_FILL_POPOVER: 'select-list--fill-popover',
PREFERENCES_PAGE: 'preferences-page',
PREFERENCES_PAGE_SIDEBAR: 'preferences-page__sidebar',
PREFERENCES_PAGE_TOPBAR: 'preferences-page__topbar',

View File

@@ -104,6 +104,7 @@ export default function DataTable({
initialState: {
pageIndex: initialPageIndex,
pageSize: initialPageSize,
expanded
},
manualPagination,
pageCount: controlledPageCount,

View File

@@ -41,6 +41,7 @@ import EmptyStatus from './EmptyStatus';
import DashboardCard from './Dashboard/DashboardCard';
import InputPrependText from './Forms/InputPrependText';
import PageFormBigNumber from './PageFormBigNumber';
import AccountsMultiSelect from './AccountsMultiSelect';
const Hint = FieldHint;
@@ -87,5 +88,6 @@ export {
EmptyStatus,
DashboardCard,
InputPrependText,
PageFormBigNumber
PageFormBigNumber,
AccountsMultiSelect,
};

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));
};

View File

@@ -9,6 +9,14 @@ import * as serviceWorker from 'serviceWorker';
import { store, persistor } from 'store/createStore';
import AppProgress from 'components/NProgress/AppProgress';
if (process.env.NODE_ENV === 'development') {
const whyDidYouRender = require('@welldone-software/why-did-you-render');
whyDidYouRender(React, {
trackAllPureComponents: false,
});
}
ReactDOM.render(
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>

View File

@@ -256,7 +256,7 @@ export default {
accrual: 'Accrual',
from: 'From',
to: 'To',
accounting_basis: 'Accounting Basis:',
accounting_basis: 'Accounting basis:',
general: 'General',
users: 'Users',
currencies: 'Currencies',
@@ -282,7 +282,7 @@ export default {
journal: 'Journal',
general_ledger: 'General Ledger',
general_ledger_sheet: 'General Ledger Sheet',
profit_loss_sheet: 'Profit Loss Sheet',
profit_loss_sheet: 'Profit/Loss Sheet',
expenses: 'Expenses',
expenses_list: 'Expenses List',
new_expenses: 'New Expenses',
@@ -335,8 +335,8 @@ export default {
export: 'Export',
accounts_with_zero_balance: 'Accounts with Zero Balance',
all_transactions: 'All Transactions',
filter_accounts: 'Filter Accounts',
calculate_report: 'Calculate Report',
filter_accounts: 'Filter accounts',
calculate_report: 'Calculate report',
total: 'Total',
specific_accounts: 'Specific Accounts',
trans_num: 'Trans. NUM',
@@ -929,7 +929,8 @@ export default {
'Are you sure you want to activate this item? You will be able to inactivate it later',
inactivate_item: 'Inactivate Item',
activate_item: 'Activate Item',
all_payments: 'All Payments',
all_payments:'All Payments',
hide_customizer: 'Hide Customizer',
opening_quantity_: 'Opening quantity',
opening_average_cost: 'Opening average cost',
opening_cost_: 'Opening cost ',

View File

@@ -112,7 +112,7 @@ export default [
component: LazyLoader({
loader: () =>
import(
'containers/FinancialStatements/TrialBalanceSheet/TrialBalanceSheet'
'containers/FinancialStatements/TrialBalanceSheet/TrialBalanceSheet'
),
}),
breadcrumb: 'Trial Balance Sheet',
@@ -127,16 +127,16 @@ export default [
}),
breadcrumb: 'Profit Loss Sheet',
},
{
path: '/financial-reports/receivable-aging-summary',
component: LazyLoader({
loader: () =>
import(
'containers/FinancialStatements/ReceivableAgingSummary/ReceivableAgingSummary'
),
}),
breadcrumb: 'Receivable Aging Summary',
},
// {
// path: '/financial-reports/receivable-aging-summary',
// component: LazyLoader({
// loader: () =>
// import(
// 'containers/FinancialStatements/ReceivableAgingSummary/ReceivableAgingSummary'
// ),
// }),
// breadcrumb: 'Receivable Aging Summary',
// },
{
path: `/financial-reports/journal-sheet`,
component: LazyLoader({

View File

@@ -95,7 +95,7 @@ export const fetchProfitLossSheet = ({ query }) => {
ApiService.get('/financial_statements/profit_loss_sheet', { params: query }).then((response) => {
dispatch({
type: t.PROFIT_LOSS_SHEET_SET,
profitLoss: response.data.profitLoss,
profitLoss: response.data.data,
columns: response.data.columns,
query: response.data.query,
});

View File

@@ -0,0 +1,159 @@
import { omit } from 'lodash';
export const mapBalanceSheetToTableRows = (accounts) => {
return accounts.map((account) => {
const PRIMARY_SECTIONS = ['assets', 'liability', 'equity'];
const rowTypes = [
'total_row',
...(PRIMARY_SECTIONS.indexOf(account.section_type) !== -1
? ['total_assets']
: []),
];
return {
...account,
children: mapBalanceSheetToTableRows([
...(account.children ? account.children : []),
...(account.total && account.children && account.children.length > 0
? [
{
name: `Total ${account.name}`,
row_types: rowTypes,
total: { ...account.total },
...(account.total_periods && {
total_periods: account.total_periods,
}),
},
]
: []),
]),
};
});
};
export const journalToTableRowsMapper = (journal) => {
return journal.reduce((rows, journal) => {
journal.entries.forEach((entry, index) => {
rows.push({
...entry,
rowType: index === 0 ? 'first_entry' : 'entry',
});
});
rows.push({
credit: journal.credit,
debit: journal.debit,
rowType: 'entries_total',
});
rows.push({
rowType: 'space_entry',
});
return rows;
}, []);
};
export const generalLedgerToTableRows = (accounts) => {
return accounts.reduce((tableRows, account) => {
const children = [];
children.push({
...account.opening,
rowType: 'opening_balance',
});
account.transactions.map((transaction) => {
children.push({
...transaction,
...omit(account, ['transactions']),
rowType: 'transaction',
});
});
children.push({
...account.closing,
rowType: 'closing_balance',
});
tableRows.push({
...omit(account, ['transactions']),
children,
rowType: 'account_name',
});
return tableRows;
}, []);
};
export const profitLossToTableRowsMapper = (profitLoss) => {
return [
{
name: 'Income',
total: profitLoss.income.total,
children: [
...profitLoss.income.accounts,
{
name: 'Total Income',
total: profitLoss.income.total,
total_periods: profitLoss.income.total_periods,
rowTypes: ['income_total', 'section_total', 'total'],
},
],
total_periods: profitLoss.income.total_periods,
},
{
name: 'Cost of sales',
total: profitLoss.cost_of_sales.total,
children: [
...profitLoss.cost_of_sales.accounts,
{
name: 'Total cost of sales',
total: profitLoss.cost_of_sales.total,
total_periods: profitLoss.cost_of_sales.total_periods,
rowTypes: ['cogs_total', 'section_total', 'total'],
},
],
total_periods: profitLoss.cost_of_sales.total_periods
},
{
name: 'Gross profit',
total: profitLoss.gross_profit.total,
total_periods: profitLoss.gross_profit.total_periods,
rowTypes: ['gross_total', 'section_total', 'total'],
},
{
name: 'Expenses',
total: profitLoss.expenses.total,
children: [
...profitLoss.expenses.accounts,
{
name: 'Total Expenses',
total: profitLoss.expenses.total,
total_periods: profitLoss.expenses.total_periods,
rowTypes: ['expenses_total', 'section_total', 'total'],
},
],
total_periods: profitLoss.expenses.total_periods,
},
{
name: 'Net Operating income',
total: profitLoss.operating_profit.total,
total_periods: profitLoss.income.total_periods,
rowTypes: ['net_operating_total', 'section_total', 'total'],
},
{
name: 'Other expenses',
total: profitLoss.other_expenses.total,
total_periods: profitLoss.other_expenses.total_periods,
children: [
...profitLoss.other_expenses.accounts,
{
name: 'Total other expenses',
total: profitLoss.other_expenses.total,
total_periods: profitLoss.other_expenses.total_periods,
rowTypes: ['expenses_total', 'section_total', 'total'],
},
],
},
{
name: 'Net Income',
total: profitLoss.net_income.total,
total_periods: profitLoss.net_income.total_periods,
rowTypes: ['net_income_total', 'section_total', 'total'],
},
];
};

View File

@@ -1,36 +1,41 @@
import { createReducer } from '@reduxjs/toolkit';
import t from 'store/types';
import { getFinancialSheetIndexByQuery } from './financialStatements.selectors';
import { omit } from 'lodash';
import {
mapBalanceSheetToTableRows,
journalToTableRowsMapper,
generalLedgerToTableRows,
profitLossToTableRowsMapper
} from './financialStatements.mappers';
const initialState = {
balanceSheet: {
sheets: [],
sheet: {},
loading: true,
filter: true,
refresh: false,
},
trialBalance: {
sheets: [],
sheet: {},
loading: true,
filter: true,
refresh: false,
},
generalLedger: {
sheets: [],
sheet: {},
loading: false,
filter: true,
refresh: false,
},
journal: {
sheets: [],
sheet: {},
loading: false,
tableRows: [],
filter: true,
refresh: true,
},
profitLoss: {
sheets: [],
sheet: {},
loading: true,
tableRows: [],
filter: true,
@@ -44,52 +49,8 @@ const initialState = {
},
};
const mapGeneralLedgerAccountsToRows = (accounts) => {
return accounts.reduce((tableRows, account) => {
const children = [];
children.push({
...account.opening,
rowType: 'opening_balance',
});
account.transactions.map((transaction) => {
children.push({
...transaction,
...omit(account, ['transactions']),
rowType: 'transaction',
});
});
children.push({
...account.closing,
rowType: 'closing_balance',
});
tableRows.push({
...omit(account, ['transactions']),
children,
rowType: 'account_name',
});
return tableRows;
}, []);
};
const mapJournalTableRows = (journal) => {
return journal.reduce((rows, journal) => {
journal.entries.forEach((entry, index) => {
rows.push({
...entry,
rowType: index === 0 ? 'first_entry' : 'entry',
});
});
rows.push({
credit: journal.credit,
debit: journal.debit,
rowType: 'entries_total',
});
rows.push({
rowType: 'space_entry',
});
return rows;
}, []);
};
const mapContactAgingSummary = (sheet) => {
const rows = [];
@@ -120,70 +81,6 @@ const mapContactAgingSummary = (sheet) => {
return rows;
};
const mapProfitLossToTableRows = (profitLoss) => {
return [
{
name: 'Income',
total: profitLoss.income.total,
children: [
...profitLoss.income.accounts,
{
name: 'Total Income',
total: profitLoss.income.total,
rowType: 'income_total',
},
],
},
{
name: 'Expenses',
total: profitLoss.expenses.total,
children: [
...profitLoss.expenses.accounts,
{
name: 'Total Expenses',
total: profitLoss.expenses.total,
rowType: 'expense_total',
},
],
},
{
name: 'Net Income',
total: profitLoss.net_income.total,
rowType: 'net_income',
},
];
};
const mapTotalToChildrenRows = (accounts) => {
return accounts.map((account) => {
return {
...account,
children: mapTotalToChildrenRows([
...(account.children ? account.children : []),
...(account.total &&
account.children &&
account.children.length > 0 &&
account.row_type !== 'total_row'
? [
{
name: `Total ${account.name}`,
row_type: 'total_row',
total: { ...account.total },
...(account.total_periods && {
total_periods: account.total_periods,
}),
},
]
: []),
]),
};
});
};
const mapBalanceSheetRows = (balanceSheet) => {
return balanceSheet.map((section) => {});
};
const financialStatementFilterToggle = (financialName, statePath) => {
return {
[`${financialName}_FILTER_TOGGLE`]: (state, action) => {
@@ -194,22 +91,13 @@ const financialStatementFilterToggle = (financialName, statePath) => {
export default createReducer(initialState, {
[t.BALANCE_SHEET_STATEMENT_SET]: (state, action) => {
const index = getFinancialSheetIndexByQuery(
state.balanceSheet.sheets,
action.query,
);
const balanceSheet = {
sheet: action.data.balanceSheet,
columns: Object.values(action.data.columns),
sheet: action.data.data,
columns: action.data.columns,
query: action.data.query,
tableRows: mapTotalToChildrenRows(action.data.balance_sheet),
tableRows: mapBalanceSheetToTableRows(action.data.data),
};
if (index !== -1) {
state.balanceSheet.sheets[index] = balanceSheet;
} else {
state.balanceSheet.sheets.push(balanceSheet);
}
state.balanceSheet.sheet = balanceSheet;
},
[t.BALANCE_SHEET_LOADING]: (state, action) => {
@@ -224,19 +112,11 @@ export default createReducer(initialState, {
...financialStatementFilterToggle('BALANCE_SHEET', 'balanceSheet'),
[t.TRAIL_BALANCE_STATEMENT_SET]: (state, action) => {
const index = getFinancialSheetIndexByQuery(
state.trialBalance.sheets,
action.query,
);
const trailBalanceSheet = {
accounts: action.data.accounts,
data: action.data.data,
query: action.data.query,
};
if (index !== -1) {
state.trialBalance.sheets[index] = trailBalanceSheet;
} else {
state.trialBalance.sheets.push(trailBalanceSheet);
}
state.trialBalance.sheet = trailBalanceSheet;
},
[t.TRIAL_BALANCE_SHEET_LOADING]: (state, action) => {
@@ -251,21 +131,12 @@ export default createReducer(initialState, {
...financialStatementFilterToggle('TRIAL_BALANCE', 'trialBalance'),
[t.JOURNAL_SHEET_SET]: (state, action) => {
const index = getFinancialSheetIndexByQuery(
state.journal.sheets,
action.query,
);
const journal = {
query: action.data.query,
journal: action.data.journal,
tableRows: mapJournalTableRows(action.data.journal),
data: action.data.data,
tableRows: journalToTableRowsMapper(action.data.data),
};
if (index !== -1) {
state.journal.sheets[index] = journal;
} else {
state.journal.sheets.push(journal);
}
state.journal.sheet = journal;
},
[t.JOURNAL_SHEET_LOADING]: (state, action) => {
@@ -278,21 +149,12 @@ export default createReducer(initialState, {
...financialStatementFilterToggle('JOURNAL', 'journal'),
[t.GENERAL_LEDGER_STATEMENT_SET]: (state, action) => {
const index = getFinancialSheetIndexByQuery(
state.generalLedger.sheets,
action.query,
);
const generalLedger = {
query: action.data.query,
accounts: action.data.accounts,
tableRows: mapGeneralLedgerAccountsToRows(action.data.accounts),
accounts: action.data.data,
tableRows: generalLedgerToTableRows(action.data.data),
};
if (index !== -1) {
state.generalLedger.sheets[index] = generalLedger;
} else {
state.generalLedger.sheets.push(generalLedger);
}
state.generalLedger.sheet = generalLedger;
},
[t.GENERAL_LEDGER_SHEET_LOADING]: (state, action) => {
@@ -305,22 +167,13 @@ export default createReducer(initialState, {
...financialStatementFilterToggle('GENERAL_LEDGER', 'generalLedger'),
[t.PROFIT_LOSS_SHEET_SET]: (state, action) => {
const index = getFinancialSheetIndexByQuery(
state.profitLoss.sheets,
action.query,
);
const profitLossSheet = {
query: action.query,
profitLoss: action.profitLoss,
columns: action.columns,
tableRows: mapProfitLossToTableRows(action.profitLoss),
tableRows: profitLossToTableRowsMapper(action.profitLoss),
};
if (index !== -1) {
state.profitLoss.sheets[index] = profitLossSheet;
} else {
state.profitLoss.sheets.push(profitLossSheet);
}
state.profitLoss.sheet = profitLossSheet;
},
[t.PROFIT_LOSS_SHEET_LOADING]: (state, action) => {
@@ -334,34 +187,34 @@ export default createReducer(initialState, {
...financialStatementFilterToggle('PROFIT_LOSS', 'profitLoss'),
[t.RECEIVABLE_AGING_SUMMARY_LOADING]: (state, action) => {
const { loading } = action.payload;
state.receivableAgingSummary.loading = loading;
},
// [t.RECEIVABLE_AGING_SUMMARY_LOADING]: (state, action) => {
// const { loading } = action.payload;
// state.receivableAgingSummary.loading = loading;
// },
[t.RECEIVABLE_AGING_SUMMARY_SET]: (state, action) => {
const { aging, columns, query } = action.payload;
const index = getFinancialSheetIndexByQuery(
state.receivableAgingSummary.sheets,
query,
);
// [t.RECEIVABLE_AGING_SUMMARY_SET]: (state, action) => {
// const { aging, columns, query } = action.payload;
// const index = getFinancialSheetIndexByQuery(
// state.receivableAgingSummary.sheets,
// query,
// );
const receivableSheet = {
query,
columns,
aging,
tableRows: mapContactAgingSummary(aging),
};
if (index !== -1) {
state.receivableAgingSummary[index] = receivableSheet;
} else {
state.receivableAgingSummary.sheets.push(receivableSheet);
}
},
[t.RECEIVABLE_AGING_SUMMARY_REFRESH]: (state, action) => {
const { refresh } = action.payload;
state.receivableAgingSummary.refresh = !!refresh;
},
// const receivableSheet = {
// query,
// columns,
// aging,
// tableRows: mapContactAgingSummary(aging),
// };
// if (index !== -1) {
// state.receivableAgingSummary[index] = receivableSheet;
// } else {
// state.receivableAgingSummary.sheets.push(receivableSheet);
// }
// },
// [t.RECEIVABLE_AGING_SUMMARY_REFRESH]: (state, action) => {
// const { refresh } = action.payload;
// state.receivableAgingSummary.refresh = !!refresh;
// },
...financialStatementFilterToggle(
'RECEIVABLE_AGING_SUMMARY',
'receivableAgingSummary',

View File

@@ -1,17 +1,14 @@
import {getObjectDiff} from 'utils';
import { createSelector } from 'reselect';
import { camelCase } from 'lodash';
const transformSheetType = (sheetType) => {
return camelCase(sheetType);
};
// Financial Statements selectors.
/**
* Retrieve financial statement sheet by the given query.
* @param {array} sheets
* @param {object} query
*/
export const getFinancialSheetIndexByQuery = (sheets, query) => {
return sheets.findIndex(balanceSheet => (
getObjectDiff(query, balanceSheet.query).length === 0
));
export const sheetByTypeSelector = (sheetType) => (state, props) => {
const sheetName = transformSheetType(sheetType);
return state.financialStatements[sheetName].sheet;
};
/**
@@ -19,38 +16,56 @@ export const getFinancialSheetIndexByQuery = (sheets, query) => {
* @param {array} sheets
* @param {number} index
*/
export const getFinancialSheet = (sheets, index) => {
return (typeof sheets[index] !== 'undefined') ? sheets[index] : null;
};
export const getFinancialSheetFactory = (sheetType) =>
createSelector(
sheetByTypeSelector(sheetType),
(sheet) => {
return sheet;
},
);
/**
* Retrieve financial statement columns by the given sheet index.
* @param {array} sheets
* @param {number} index
*/
export const getFinancialSheetColumns = (sheets, index) => {
const sheet = getFinancialSheet(sheets, index);
return (sheet && sheet.columns) ? sheet.columns : [];
};
export const getFinancialSheetColumnsFactory = (sheetType) =>
createSelector(
sheetByTypeSelector(sheetType),
(sheet) => {
return (sheet && sheet.columns) ? sheet.columns : [];
},
);
/**
* Retrieve financial statement query by the given sheet index.
* @param {array} sheets
* @param {number} index
*/
export const getFinancialSheetQuery = (sheets, index) => {
const sheet = getFinancialSheet(sheets, index);
return (sheet && sheet.query) ? sheet.query : {};
};
export const getFinancialSheetQueryFactory = (sheetType) =>
createSelector(
sheetByTypeSelector(sheetType),
(sheet) => {
return (sheet && sheet.query) ? sheet.query : {};
},
);
/**
* Retrieve financial statement accounts by the given sheet index.
*/
export const getFinancialSheetAccountsFactory = (sheetType) =>
createSelector(
sheetByTypeSelector(sheetType),
(sheet) => {
return (sheet && sheet.accounts) ? sheet.accounts : [];
}
);
export const getFinancialSheetAccounts = (sheets, index) => {
const sheet = getFinancialSheet(sheets, index);
return (sheet && sheet.accounts) ? sheet.accounts : [];
};
export const getFinancialSheetTableRows = (sheets, index) => {
const sheet = getFinancialSheet(sheets, index);
return (sheet && sheet.tableRows) ? sheet.tableRows : [];
};
/**
* Retrieve financial statement table rows by the given sheet index.
*/
export const getFinancialSheetTableRowsFactory = (sheetType) =>
createSelector(
sheetByTypeSelector(sheetType),
(sheet) => {
return (sheet && sheet.tableRows) ? sheet.tableRows : [];
}
);

View File

@@ -117,6 +117,9 @@ $button-background-color-hover: #CFDCEE !default;
body.authentication {
background-color: #fcfdff;
}
body.hide-scrollbar .Pane2{
overflow: hidden;
}
.bp3-toast {
box-shadow: none;

View File

@@ -296,6 +296,12 @@
.tbody{
.tr .td{
border-bottom: 0;
}
.tr:not(:first-child) .td{
border-top: 1px dotted #CCC;
}
.tr:last-child .td{
border-bottom: 1px dotted #CCC;
}
}

View File

@@ -6,32 +6,7 @@
.financial-statement{
&__header{
padding: 25px 26px 25px;
background: #FDFDFD;
&.is-hidden{
display: none;
}
.bp3-form-group,
.radio-group---accounting-basis{
.bp3-label{
font-weight: 500;
font-size: 13px;
color: #444;
}
}
.bp3-button.button--submit-filter{
min-height: 34px;
padding-left: 16px;
padding-right: 16px;
}
.radio-group---accounting-basis{
.bp3-label{
margin-bottom: 12px;
}
}
}
&__body{
@@ -41,25 +16,146 @@
justify-content: center;
align-items: center;
}
}
&__header.is-hidden + .financial-statement__body{
.financial-sheet{
margin-top: 40px;
.financial-header-drawer{
padding: 25px 26px 25px;
position: absolute;
top: 101px;
bottom: 0;
left: 0;
right: 0;
overflow: hidden;
&.is-hidden{
visibility: hidden;
}
.row{
.col{
max-width: 400px;
min-width: 250px;
}
}
.bp3-drawer{
box-shadow: 0 0 0 transparent;
max-height: 550px;
height: 100%;
padding-bottom: 49px;
> form{
display: flex;
flex-direction: column;
flex: 1 0 0;
height: 100%;
}
.bp3-drawer-backdrop{
background-color: rgba(2, 9, 19, 0.65);
}
}
.bp3-form-group{
margin-bottom: 22px;
label.bp3-label{
margin-bottom: 6px;
}
}
.bp3-button.button--submit-filter{
min-height: 34px;
padding-left: 16px;
padding-right: 16px;
}
.radio-group---accounting-basis{
.bp3-label{
margin-bottom: 12px;
}
}
.bp3-tabs{
height: 100%;
&.bp3-vertical > .bp3-tab-panel{
flex: 1 0 0;
border-top: 24px solid transparent;
padding-left: 20px;
padding-right: 20px;
padding-bottom: 24px;
overflow: auto;
}
}
.bp3-tabs.bp3-vertical{
flex: 1 0 0;
.bp3-tab-list{
width: 220px;
border-right: 1px solid #c3cdd5;
padding-top: 10px;
> *:not(:last-child){
margin-right: 0;
}
.bp3-tab-indicator-wrapper{
width: 100%;
.bp3-tab-indicator{
border-left: 3px solid #0350f8;
background-color: #edf5ff;
border-radius: 0;
}
}
.bp3-tab{
color: #333;
line-height: 45px;
border-radius: 0;
padding-left: 14px;
padding-right: 14px;
font-weight: 500;
}
}
}
.bp3-tab-panel{
}
&__footer{
background-color: #ecf0f3;
border-top: 1px solid #c3cdd5;
padding: 8px;
padding-left: 230px;
position: absolute;
bottom: 0;
width: 100%;
}
.row{
margin-left: -0.85rem;
margin-right: -0.85rem;
.col{
padding-left: 0.85rem;
padding-right: 0.85rem;
}
}
}
.financial-sheet{
border: 2px solid #F1F1F1;
border: 2px solid #EBEBEB;
border-radius: 10px;
min-width: 640px;
width: auto;
padding: 30px 18px;
max-width: 100%;
margin: 15px auto 35px;
margin: 35px auto;
min-height: 400px;
display: flex;
flex-direction: column;
background: #fff;
&__title{
margin: 0;
@@ -103,7 +199,6 @@
}
}
}
&__inner{
&.is-loading{
display: none;
@@ -113,8 +208,8 @@
color: #888;
text-align: center;
margin-top: auto;
padding-top: 16px;
font-size: 12px;
padding-top: 18px;
font-size: 13px;
}
.dashboard__loading-indicator{
margin: auto;
@@ -137,13 +232,19 @@
}
&--general-ledger{
.financial-sheet__table{
.tbody{
.tbody{
.tr.row-type{
&--opening_balance,
&--closing_balance{
.td{
background-color: #fbfbfb;
border-top: 1px solid #333;
}
.name,
.amount,
.balance{
font-weight: 500;
}
}
@@ -185,18 +286,36 @@
&--profit-loss-sheet{
.financial-sheet__table{
.thead,
.tbody{
.total.td {
border-bottom-color: #000;
.tr .td:not(:first-child),
.tr .th:not(:first-child) {
justify-content: flex-end;
}
}
.tbody{
.tr .td:not(:first-child) {
border-top-color: #000;
}
.tr.row_type--total{
font-weight: 500;
}
.tr.row_type--section_total .td{
border-top: 1px solid #BBB
}
.tr.row_type--section_total + .tr .td{
border-top: 1px solid #666;
}
.tr.row_type--section_total:last-child .td{
border-bottom: 1px solid #666;
}
.row--income_total,
.row--expense_total,
.row--net_income{
font-weight: 600;
.total.td{
border-bottom-color: #555;
.tr.is-expanded{
.td.total,
.td.total-period{
> span{
display: none;
}
}
}
}
@@ -205,13 +324,28 @@
&--balance-sheet{
.financial-sheet__table{
.thead,
.tbody{
.total.td{
border-bottom-color: #000;
.tr .td.account_name ~ .td,
.tr .th.account_name ~ .th{
justify-content: flex-end;
}
}
.tbody{
.tr .total.td{
border-top-color: #000;
}
.tr.row_type--total_row .td{
border-top: 1px solid #BBB;
}
.tr.row_type--total_assets + .tr .td{
border-top: 1px solid #666;
}
.tr.row_type--total_row{
.total.td,
.account_name.td{
.account_name.td,
.total-period.td{
font-weight: 600;
color: #333;
}
@@ -267,6 +401,7 @@
&.is-full-width{
width: 100%;
margin-top: 25px;
}
}
@@ -309,4 +444,14 @@
margin-bottom: 0;
}
}
}
.financial-statement--journal{
.financial-header-drawer{
.bp3-drawer{
max-height: 350px;
}
}
}

View File

@@ -122,24 +122,22 @@ export const parseDateRangeQuery = (keyword) => {
const query = queries[keyword];
return {
from_date: moment().startOf(query.range).toDate(),
to_date: moment().endOf(query.range).toDate(),
fromDate: moment().startOf(query.range).toDate(),
toDate: moment().endOf(query.range).toDate(),
};
};
export const defaultExpanderReducer = (tableRows, level) => {
let currentLevel = 1;
const expended = [];
const walker = (rows, parentIndex = null) => {
const walker = (rows, parentIndex = null, currentLevel = 1) => {
return rows.forEach((row, index) => {
const _index = parentIndex ? `${parentIndex}.${index}` : `${index}`;
expended[_index] = true;
if (row.children && currentLevel < level) {
walker(row.children, _index);
walker(row.children, _index, currentLevel + 1);
}
currentLevel++;
}, {});
};
walker(tableRows);
@@ -371,4 +369,22 @@ export function defaultToTransform(
export function isBlank(value) {
return _.isEmpty(value) && !_.isNumber(value) || _.isNaN(value);
}
}
export const getColumnWidth = (
rows,
accessor,
{ maxWidth, minWidth, magicSpacing = 14 },
) => {
const cellLength = Math.max(
...rows.map((row) => (`${_.get(row, accessor)}` || '').length),
);
let result = cellLength * magicSpacing;
result = minWidth ? Math.max(minWidth, result) : result;
result = maxWidth ? Math.min(maxWidth, result) : result;
return result;
};