WIP Financial statements.

This commit is contained in:
Ahmed Bouhuolia
2020-04-05 14:27:06 +02:00
parent 4227f2f9a8
commit b09fc58042
33 changed files with 725 additions and 343 deletions

View File

@@ -71,6 +71,7 @@
"react-sortablejs": "^2.0.11", "react-sortablejs": "^2.0.11",
"react-table": "^7.0.0", "react-table": "^7.0.0",
"react-use": "^13.26.1", "react-use": "^13.26.1",
"react-window": "^1.8.5",
"redux": "^4.0.5", "redux": "^4.0.5",
"redux-thunk": "^2.3.0", "redux-thunk": "^2.3.0",
"resolve": "1.15.0", "resolve": "1.15.0",

View File

@@ -1,4 +1,4 @@
import React, {useEffect} from 'react'; import React, {useEffect, useMemo, useCallback} from 'react';
import { import {
useTable, useTable,
useExpanded, useExpanded,
@@ -11,6 +11,7 @@ import {
} from 'react-table' } from 'react-table'
import {Checkbox} from '@blueprintjs/core'; import {Checkbox} from '@blueprintjs/core';
import classnames from 'classnames'; import classnames from 'classnames';
import { FixedSizeList } from 'react-window'
import Icon from 'components/Icon'; import Icon from 'components/Icon';
const IndeterminateCheckbox = React.forwardRef( const IndeterminateCheckbox = React.forwardRef(
@@ -34,8 +35,15 @@ export default function DataTable({
onSelectedRowsChange, onSelectedRowsChange,
manualSortBy = 'false', manualSortBy = 'false',
selectionColumn = false, selectionColumn = false,
expandSubRows = true,
className, className,
noResults = 'This report does not contain any data.' noResults = 'This report does not contain any data.',
expanded = {},
rowClassNames,
stickyHeader = true,
virtualizedRows = false,
fixedSizeHeight = 100,
fixedItemSize = 30,
}) { }) {
const { const {
getTableProps, getTableProps,
@@ -43,6 +51,7 @@ export default function DataTable({
headerGroups, headerGroups,
prepareRow, prepareRow,
page, page,
rows,
canPreviousPage, canPreviousPage,
canNextPage, canNextPage,
pageOptions, pageOptions,
@@ -52,21 +61,23 @@ export default function DataTable({
previousPage, previousPage,
setPageSize, setPageSize,
selectedFlatRows, selectedFlatRows,
totalColumnsWidth,
// Get the state from the instance // Get the state from the instance
state: { pageIndex, pageSize, sortBy, selectedRowIds }, state: { pageIndex, pageSize, sortBy, selectedRowIds },
} = useTable( } = useTable(
{ {
columns, columns,
data, data: data,
initialState: { pageIndex: 0 }, // Pass our hoisted table state initialState: { pageIndex: 0, expanded }, // Pass our hoisted table state
manualPagination: true, // Tell the usePagination manualPagination: true, // Tell the usePagination
// hook that we'll handle our own data fetching // hook that we'll handle our own data fetching
// This means we'll also have to provide our own // This means we'll also have to provide our own
// pageCount. // pageCount.
// pageCount: controlledPageCount, // pageCount: controlledPageCount,
getSubRows: row => row.children, getSubRows: row => row.children,
manualSortBy manualSortBy,
expandSubRows,
}, },
useSortBy, useSortBy,
useExpanded, useExpanded,
@@ -108,8 +119,44 @@ export default function DataTable({
onFetchData && onFetchData({ pageIndex, pageSize, sortBy }) onFetchData && onFetchData({ pageIndex, pageSize, sortBy })
}, [pageIndex, pageSize, sortBy]); }, [pageIndex, pageSize, sortBy]);
// Renders table row.
const RenderRow = useCallback(({ style = {}, row }) => {
prepareRow(row);
return (
<div {...row.getRowProps({ style })} className="tr">
{row.cells.map((cell) => {
return <div {...cell.getCellProps({
className: classnames(cell.column.className || '', 'td'),
})}>{ cell.render('Cell') }</div>
})}
</div>);
}, [prepareRow]);
// Renders virtualize circle table rows.
const RenderVirtualizedRows = useCallback(({ index, style }) => {
const row = rows[index];
return RenderRow({ row, style });
}, [RenderRow, rows]);
const RenderPage = useCallback(({ style, index } = {}) => {
return page.map((row, index) => RenderRow({ row }));
}, [RenderRow, page]);
const RenderTBody = useCallback(() => {
return (virtualizedRows) ? (
<FixedSizeList
height={fixedSizeHeight}
itemCount={rows.length}
itemSize={fixedItemSize}
>
{RenderVirtualizedRows}
</FixedSizeList>
) : RenderPage();
}, [fixedSizeHeight, rows, fixedItemSize, virtualizedRows,
RenderVirtualizedRows, RenderPage])
return ( return (
<div className={classnames('bigcapital-datatable', className)}> <div className={classnames('bigcapital-datatable', className, {'has-sticky-header': stickyHeader})}>
<div {...getTableProps()} className="table"> <div {...getTableProps()} className="table">
<div className="thead"> <div className="thead">
{headerGroups.map(headerGroup => ( {headerGroups.map(headerGroup => (
@@ -144,18 +191,7 @@ export default function DataTable({
))} ))}
</div> </div>
<div {...getTableBodyProps()} className="tbody"> <div {...getTableBodyProps()} className="tbody">
{page.map((row, i) => { { RenderTBody() }
prepareRow(row);
return (
<div {...row.getRowProps()} className="tr">
{row.cells.map((cell) => {
return <div {...cell.getCellProps({
className: classnames(cell.column.className || '', 'td'),
})}>{ cell.render('Cell') }</div>
})}
</div>)
})}
{ (page.length === 0) && ( { (page.length === 0) && (
<div className={'tr no-results'}> <div className={'tr no-results'}>

View File

@@ -11,12 +11,13 @@ export default function FinancialSheet({
accountingBasis, accountingBasis,
name, name,
loading, loading,
className,
}) { }) {
const formattedDate = moment(date).format('DD MMMM YYYY') const formattedDate = moment(date).format('DD MMMM YYYY')
const nameModifer = name ? `financial-sheet--${name}` : ''; const nameModifer = name ? `financial-sheet--${name}` : '';
return ( return (
<div className={classnames('financial-sheet', nameModifer)}> <div className={classnames('financial-sheet', nameModifer, className)}>
<LoadingIndicator loading={loading}> <LoadingIndicator loading={loading}>
<h1 class="financial-sheet__title">{ companyTitle }</h1> <h1 class="financial-sheet__title">{ companyTitle }</h1>
<h6 class="financial-sheet__sheet-type">{ sheetType }</h6> <h6 class="financial-sheet__sheet-type">{ sheetType }</h6>
@@ -29,6 +30,10 @@ export default function FinancialSheet({
<div class="financial-sheet__accounting-basis"> <div class="financial-sheet__accounting-basis">
{ accountingBasis } { accountingBasis }
</div> </div>
<div class="financial-sheet__basis">
Accounting Basis: Accural
</div>
</LoadingIndicator> </LoadingIndicator>
</div> </div>
); );

View File

@@ -0,0 +1,27 @@
import {connect} from 'react-redux';
import {
fetchBalanceSheet,
} from 'store/financialStatement/financialStatements.actions';
import {
getFinancialSheetIndexByQuery,
getFinancialSheet,
getFinancialSheetAccounts,
getFinancialSheetColumns,
getFinancialSheetQuery,
} from 'store/financialStatement/financialStatements.selectors';
export const mapStateToProps = (state, props) => ({
getBalanceSheetIndex: (query) => getFinancialSheetIndexByQuery(state.financialStatements.balanceSheet.sheets, query),
getBalanceSheet: (index) => getFinancialSheet(state.financialStatements.balanceSheet.sheets, index),
getBalanceSheetAccounts: (index) => getFinancialSheetAccounts(state.financialStatements.balanceSheet.sheets, index),
getBalanceSheetColumns:(index) => getFinancialSheetColumns(state.financialStatements.balanceSheet.sheets, index),
getBalanceSheetQuery: (index) => getFinancialSheetQuery(state.financialStatements.balanceSheet.sheets, index),
balanceSheetLoading: state.financialStatements.balanceSheet.loading,
});
export const mapDispatchToProps = (dispatch) => ({
fetchBalanceSheet: (query = {}) => dispatch(fetchBalanceSheet({ query })),
});
export default connect(mapStateToProps, mapDispatchToProps);

View File

@@ -0,0 +1,27 @@
import {connect} from 'react-redux';
import {
fetchBalanceSheet,
} from 'store/financialStatement/financialStatements.actions';
import {
getFinancialSheetIndexByQuery,
getFinancialSheet,
getFinancialSheetsAccounts,
getFinancialSheetsColumns,
} from 'store/financialStatement/financialStatements.selectors';
export const mapStateToProps = (state, props) => {
const sheetIndex = props.balanceSheetIndex;
return {
balanceSheetAccounts: props.getBalanceSheetAccounts(sheetIndex),
balanceSheetQuery: props.getBalanceSheetQuery(sheetIndex),
balanceSheetColumns: props.getBalanceSheetColumns(sheetIndex),
};
};
export const mapDispatchToProps = (dispatch) => ({
});
export default connect(mapStateToProps, mapDispatchToProps);

View File

@@ -5,11 +5,19 @@ import {
import { import {
getFinancialSheetIndexByQuery, getFinancialSheetIndexByQuery,
getFinancialSheet, getFinancialSheet,
getFinancialSheetColumns,
getFinancialSheetQuery,
getFinancialSheetTableRows,
} from 'store/financialStatement/financialStatements.selectors'; } from 'store/financialStatement/financialStatements.selectors';
export const mapStateToProps = (state, props) => ({ export const mapStateToProps = (state, props) => ({
getProfitLossSheetIndex: (query) => getFinancialSheetIndexByQuery(state.financialStatements.profitLoss.sheets, query), getProfitLossSheetIndex: (query) => getFinancialSheetIndexByQuery(state.financialStatements.profitLoss.sheets, query),
getProfitLossSheet: (index) => getFinancialSheet(state.financialStatements.profitLoss.sheets, index), getProfitLossSheet: (index) => getFinancialSheet(state.financialStatements.profitLoss.sheets, index),
getProfitLossColumns: (index) => getFinancialSheetColumns(state.financialStatements.profitLoss.sheets, index),
getProfitLossQuery: (index) => getFinancialSheetQuery(state.financialStatements.profitLoss.sheets, index),
getProfitLossTableRows: (index) => getFinancialSheetTableRows(state.financialStatements.profitLoss.sheets, index),
profitLossSheetLoading: state.financialStatements.profitLoss.loading,
}); });
export const mapDispatchToProps = (dispatch) => ({ export const mapDispatchToProps = (dispatch) => ({

View File

@@ -0,0 +1,17 @@
import {connect} from 'react-redux';
export const mapStateToProps = (state, props) => {
const sheetIndex = props.profitLossSheetIndex;
return {
profitLossTableRows: props.getProfitLossTableRows(sheetIndex),
profitLossColumns: props.getProfitLossColumns(sheetIndex),
profitLossQuery: props.getProfitLossQuery(sheetIndex),
};
};
export const mapDispatchToProps = (dispatch) => ({
});
export default connect(mapStateToProps, mapDispatchToProps);

View File

@@ -3,16 +3,15 @@ import {
fetchTrialBalanceSheet fetchTrialBalanceSheet
} from 'store/financialStatement/financialStatements.actions'; } from 'store/financialStatement/financialStatements.actions';
import { import {
getTrialBalanceSheetIndex,
getTrialBalanceAccounts,
getTrialBalanceQuery,
getFinancialSheetIndexByQuery, getFinancialSheetIndexByQuery,
getFinancialSheetAccounts,
getFinancialSheetQuery,
} from 'store/financialStatement/financialStatements.selectors'; } from 'store/financialStatement/financialStatements.selectors';
export const mapStateToProps = (state, props) => ({ export const mapStateToProps = (state, props) => ({
getTrialBalanceSheetIndex: (query) => getFinancialSheetIndexByQuery(state.financialStatements.trialBalance.sheets, query), getTrialBalanceSheetIndex: (query) => getFinancialSheetIndexByQuery(state.financialStatements.trialBalance.sheets, query),
getTrialBalanceAccounts: (sheetIndex) => getTrialBalanceAccounts(state.financialStatements.trialBalance.sheets, sheetIndex), getTrialBalanceAccounts: (sheetIndex) => getFinancialSheetAccounts(state.financialStatements.trialBalance.sheets, sheetIndex),
getTrialBalanceQuery: (sheetIndex) => getTrialBalanceQuery(state.financialStatements.trialBalance.sheets, sheetIndex), getTrialBalanceQuery: (sheetIndex) => getFinancialSheetQuery(state.financialStatements.trialBalance.sheets, sheetIndex),
trialBalanceSheetLoading: state.financialStatements.trialBalance.loading, trialBalanceSheetLoading: state.financialStatements.trialBalance.loading,
}); });

View File

@@ -2,7 +2,7 @@ import React, {useEffect, useMemo, useCallback, useState} from 'react';
import DashboardConnect from 'connectors/Dashboard.connector'; import DashboardConnect from 'connectors/Dashboard.connector';
import {compose} from 'utils'; import {compose} from 'utils';
import useAsync from 'hooks/async'; import useAsync from 'hooks/async';
import FinancialStatementConnect from 'connectors/FinancialStatements.connector'; import BalanceSheetConnect from 'connectors/BalanceSheet.connect';
import {useIntl} from 'react-intl'; import {useIntl} from 'react-intl';
import BalanceSheetHeader from './BalanceSheetHeader'; import BalanceSheetHeader from './BalanceSheetHeader';
import LoadingIndicator from 'components/LoadingIndicator'; import LoadingIndicator from 'components/LoadingIndicator';
@@ -15,10 +15,9 @@ import BalanceSheetActionsBar from './BalanceSheetActionsBar';
function BalanceSheet({ function BalanceSheet({
fetchBalanceSheet, fetchBalanceSheet,
changePageTitle, changePageTitle,
getBalanceSheetByQuery, balanceSheetLoading,
getBalanceSheetIndexByQuery, getBalanceSheetIndex,
getBalanceSheetByIndex, getBalanceSheet,
balanceSheets
}) { }) {
const intl = useIntl(); const intl = useIntl();
const [filter, setFilter] = useState({ const [filter, setFilter] = useState({
@@ -45,13 +44,8 @@ function BalanceSheet({
// Retrieve balance sheet index by the given filter query. // Retrieve balance sheet index by the given filter query.
const balanceSheetIndex = useMemo(() => const balanceSheetIndex = useMemo(() =>
getBalanceSheetIndexByQuery(filter), getBalanceSheetIndex(filter),
[filter, getBalanceSheetIndexByQuery]); [filter, getBalanceSheetIndex]);
// Retreive balance sheet by the given sheet index.
const balanceSheet = useMemo(() =>
getBalanceSheetByIndex(balanceSheetIndex),
[balanceSheetIndex, getBalanceSheetByIndex]);
// Handle re-fetch balance sheet after filter change. // Handle re-fetch balance sheet after filter change.
const handleFilterSubmit = useCallback((filter) => { const handleFilterSubmit = useCallback((filter) => {
@@ -75,13 +69,10 @@ function BalanceSheet({
onSubmitFilter={handleFilterSubmit} /> onSubmitFilter={handleFilterSubmit} />
<div class="financial-statement__body"> <div class="financial-statement__body">
<LoadingIndicator loading={fetchHook.pending}> <BalanceSheetTable
<BalanceSheetTable loading={balanceSheetLoading}
balanceSheet={balanceSheet} balanceSheetIndex={balanceSheetIndex}
balanceSheetIndex={balanceSheetIndex} onFetchData={handleFetchData} />
onFetchData={handleFetchData}
asDate={new Date()} />
</LoadingIndicator>
</div> </div>
</div> </div>
</DashboardPageContent> </DashboardPageContent>
@@ -91,5 +82,5 @@ function BalanceSheet({
export default compose( export default compose(
DashboardConnect, DashboardConnect,
FinancialStatementConnect, BalanceSheetConnect,
)(BalanceSheet); )(BalanceSheet);

View File

@@ -73,7 +73,8 @@ export default function BalanceSheetHeader({
<Row> <Row>
<Col sm={3}> <Col sm={3}>
<SelectDisplayColumnsBy onItemSelect={onItemSelectDisplayColumns} /> <SelectDisplayColumnsBy
onItemSelect={onItemSelectDisplayColumns} />
</Col> </Col>
<Col sm={3}> <Col sm={3}>
@@ -102,7 +103,7 @@ export default function BalanceSheetHeader({
type="submit" type="submit"
onClick={handleSubmitClick} onClick={handleSubmitClick}
disabled={formik.isSubmitting} disabled={formik.isSubmitting}
className={'button--submit-filter'}> className={'button--submit-filter mt2'}>
{ 'Calculate Report' } { 'Calculate Report' }
</Button> </Button>
</Col> </Col>

View File

@@ -1,32 +1,23 @@
import React, {useMemo, useState, useCallback, useEffect} from 'react'; import React, {useMemo, useState, useCallback, useEffect} from 'react';
import FinancialSheet from 'components/FinancialSheet';
import DataTable from 'components/DataTable';
import FinancialStatementConnect from 'connectors/FinancialStatements.connector';
import {compose} from 'utils';
import moment from 'moment'; import moment from 'moment';
import Money from 'components/Money'; import Money from 'components/Money';
import FinancialSheet from 'components/FinancialSheet';
import DataTable from 'components/DataTable';
import BalanceSheetConnect from 'connectors/BalanceSheet.connect';
import BalanceSheetTableConnect from 'connectors/BalanceSheetTable.connect';
import {
compose,
defaultExpanderReducer,
} from 'utils';
function BalanceSheetTable({ function BalanceSheetTable({
balanceSheet, balanceSheetAccounts,
balanceSheetIndex, balanceSheetColumns,
getBalanceSheetColumns, balanceSheetQuery,
getBalanceSheetAssetsAccounts,
getBalanceSheetLiabilitiesAccounts,
getBalanceSheetQuery,
onFetchData, onFetchData,
asDate, asDate,
loading,
}) { }) {
const balanceSheetColumns = useMemo(() =>
getBalanceSheetColumns(balanceSheetIndex),
[getBalanceSheetColumns, balanceSheetIndex]);
const balanceSheetQuery = useMemo(() =>
getBalanceSheetQuery(balanceSheetIndex),
[getBalanceSheetQuery, balanceSheetIndex])
const columns = useMemo(() => [ const columns = useMemo(() => [
{ {
// Build our expander column // Build our expander column
@@ -77,63 +68,65 @@ function BalanceSheetTable({
accessor: 'code', accessor: 'code',
className: "code", className: "code",
}, },
...(balanceSheetQuery && ...(balanceSheetQuery.display_columns_type === 'total') ? [
balanceSheetQuery.display_columns_by === 'total') ? [
{ {
Header: 'Total', Header: 'Total',
accessor: 'balance.formatted_amount', accessor: 'balance.formatted_amount',
Cell: ({ cell }) => { Cell: ({ cell }) => {
const row = cell.row.original; const row = cell.row.original;
if (!row.balance) { return ''; } if (row.total) {
return (<Money amount={row.balance.formatted_amount} currency={'USD'} />); return (<Money amount={row.total.formatted_amount} currency={'USD'} />);
}
return '';
}, },
className: "credit", className: "credit",
} }
] : (balanceSheetColumns.map((column, index) => ({ ] : [],
Header: column, ...(balanceSheetQuery.display_columns_type === 'date_periods') ?
accessor: (row) => { (balanceSheetColumns.map((column, index) => ({
if (row.periods_balance && row.periods_balance[index]) { id: `date_period_${index}`,
return row.periods_balance[index].formatted_amount; Header: column,
} accessor: (row) => {
}, if (row.total_periods && row.total_periods[index]) {
}))), const amount = row.total_periods[index].formatted_amount;
], [balanceSheetColumns, balanceSheetQuery]); return (<Money amount={amount} currency={'USD'} />);
}
const [data, setData] = useState([]); return '';
},
useEffect(() => { width: 100,
if (!balanceSheet) { return; } })))
setData([ : [],
{ ], [balanceSheetQuery, balanceSheetColumns]);
name: 'Assets',
children: getBalanceSheetAssetsAccounts(balanceSheetIndex),
},
{
name: 'Liabilies & Equity',
children: getBalanceSheetLiabilitiesAccounts(balanceSheetIndex),
}
])
}, []);
const handleFetchData = useCallback(() => { const handleFetchData = useCallback(() => {
onFetchData && onFetchData(); onFetchData && onFetchData();
}, [onFetchData]); }, [onFetchData]);
// Calculates the default expanded rows of balance sheet table.
const expandedRows = useMemo(() =>
defaultExpanderReducer(balanceSheetAccounts, 1),
[balanceSheetAccounts]);
return ( return (
<FinancialSheet <FinancialSheet
companyTitle={'Facebook, Incopration'} companyTitle={'Facebook, Incopration'}
sheetType={'Balance Sheet'} sheetType={'Balance Sheet'}
date={asDate}> date={asDate}
loading={loading}>
<DataTable <DataTable
className="bigcapital-datatable--financial-report" className="bigcapital-datatable--financial-report"
columns={columns} columns={columns}
data={data} data={balanceSheetAccounts}
onFetchData={handleFetchData} /> onFetchData={handleFetchData}
expandSubRows={true}
expanded={expandedRows} />
</FinancialSheet> </FinancialSheet>
); );
} }
export default compose( export default compose(
FinancialStatementConnect, BalanceSheetConnect,
BalanceSheetTableConnect,
)(BalanceSheetTable); )(BalanceSheetTable);

View File

@@ -83,7 +83,7 @@ function GeneralLedgerHeader({
type="submit" type="submit"
onClick={handleSubmitClick} onClick={handleSubmitClick}
disabled={formik.isSubmitting} disabled={formik.isSubmitting}
className={'button--submit-filter'}> className={'button--submit-filter mt2'}>
{ 'Calculate Report' } { 'Calculate Report' }
</Button> </Button>
</Col> </Col>

View File

@@ -3,6 +3,10 @@ import FinancialSheet from 'components/FinancialSheet';
import DataTable from 'components/DataTable'; import DataTable from 'components/DataTable';
import Money from 'components/Money'; import Money from 'components/Money';
import moment from 'moment'; import moment from 'moment';
import {
defaultExpanderReducer,
} from 'utils';
const ROW_TYPE = { const ROW_TYPE = {
CLOSING_BALANCE: 'closing_balance', CLOSING_BALANCE: 'closing_balance',
@@ -135,6 +139,9 @@ export default function GeneralLedgerTable({
onFetchData && onFetchData(); onFetchData && onFetchData();
}, [onFetchData]); }, [onFetchData]);
// Default expanded rows of general ledger table.
const expandedRows = useMemo(() => defaultExpanderReducer(data, 1), [data]);
return ( return (
<FinancialSheet <FinancialSheet
companyTitle={'Facebook, Incopration'} companyTitle={'Facebook, Incopration'}
@@ -147,7 +154,11 @@ export default function GeneralLedgerTable({
className="bigcapital-datatable--financial-report" className="bigcapital-datatable--financial-report"
columns={columns} columns={columns}
data={data} data={data}
onFetchData={handleFetchData} /> onFetchData={handleFetchData}
expanded={expandedRows}
virtualizedRows={true}
fixedItemSize={37}
fixedSizeHeight={1000} />
</FinancialSheet> </FinancialSheet>
); );
} }

View File

@@ -47,7 +47,7 @@ export default function JournalHeader({
<Button <Button
type="submit" type="submit"
onClick={handleSubmitClick} onClick={handleSubmitClick}
class={'button--submit-filter'}> className={'button--submit-filter'}>
{ 'Run Report' } { 'Run Report' }
</Button> </Button>
</Col> </Col>

View File

@@ -1,7 +1,7 @@
import React, {useState, useEffect, useCallback, useMemo} from 'react'; import React, {useState, useEffect, useCallback, useMemo} from 'react';
import FinancialSheet from 'components/FinancialSheet'; import FinancialSheet from 'components/FinancialSheet';
import DataTable from 'components/DataTable'; import DataTable from 'components/DataTable';
import {compose} from 'utils'; import {compose, defaultExpanderReducer} from 'utils';
import moment from 'moment'; import moment from 'moment';
import JournalConnect from 'connectors/Journal.connect'; import JournalConnect from 'connectors/Journal.connect';
import { import {
@@ -71,6 +71,9 @@ function JournalSheetTable({
onFetchData && onFetchData(...args) onFetchData && onFetchData(...args)
}, [onFetchData]); }, [onFetchData]);
// Default expanded rows of general journal table.
const expandedRows = useMemo(() => defaultExpanderReducer(data, 1), [data]);
return ( return (
<FinancialSheet <FinancialSheet
companyTitle={'Facebook, Incopration'} companyTitle={'Facebook, Incopration'}
@@ -84,7 +87,8 @@ function JournalSheetTable({
columns={columns} columns={columns}
data={data} data={data}
onFetchData={handleFetchData} onFetchData={handleFetchData}
noResults={"This report does not contain any data."} /> noResults={"This report does not contain any data."}
expanded={expandedRows} />
</FinancialSheet> </FinancialSheet>
); );
} }

View File

@@ -14,11 +14,11 @@ import moment from 'moment';
function ProfitLossSheet({ function ProfitLossSheet({
changePageTitle, changePageTitle,
fetchProfitLossSheet, fetchProfitLossSheet,
getProfitLossSheetIndex, getProfitLossSheetIndex,
getProfitLossSheet, profitLossSheetLoading,
}) { }) {
const [filter, setFilter] = useState({ const [filter, setFilter] = useState({
basis: 'cash',
from_date: moment().startOf('year').format('YYYY-MM-DD'), from_date: moment().startOf('year').format('YYYY-MM-DD'),
to_date: moment().endOf('year').format('YYYY-MM-DD'), to_date: moment().endOf('year').format('YYYY-MM-DD'),
}); });
@@ -26,7 +26,7 @@ function ProfitLossSheet({
// Change page title of the dashboard. // Change page title of the dashboard.
useEffect(() => { useEffect(() => {
changePageTitle('Profit/Loss Sheet'); changePageTitle('Profit/Loss Sheet');
}, []); }, [changePageTitle]);
// Fetches profit/loss sheet. // Fetches profit/loss sheet.
const fetchHook = useAsync((query = filter) => { const fetchHook = useAsync((query = filter) => {
@@ -35,14 +35,12 @@ function ProfitLossSheet({
]); ]);
}, false); }, false);
// Retrieve profit/loss sheet index based on the given filter query.
const profitLossSheetIndex = useMemo(() => const profitLossSheetIndex = useMemo(() =>
getProfitLossSheetIndex(filter), getProfitLossSheetIndex(filter),
[getProfitLossSheetIndex, filter]) [getProfitLossSheetIndex, filter]);
const profitLossSheet = useMemo(() =>
getProfitLossSheet(profitLossSheetIndex),
[getProfitLossSheet, profitLossSheetIndex]);
// Handle submit filter.
const handleSubmitFilter = useCallback((filter) => { const handleSubmitFilter = useCallback((filter) => {
const _filter = { const _filter = {
...filter, ...filter,
@@ -51,30 +49,29 @@ function ProfitLossSheet({
}; };
setFilter(_filter); setFilter(_filter);
fetchHook.execute(_filter); fetchHook.execute(_filter);
}, []); }, [fetchHook]);
// Handle fetch data of profit/loss sheet table. // Handle fetch data of profit/loss sheet table.
const handleFetchData = useCallback(() => { const handleFetchData = useCallback(() => { fetchHook.execute(); }, [fetchHook]);
fetchHook.execute();
}, [fetchHook]);
return ( return (
<DashboardInsider> <DashboardInsider>
<ProfitLossActionsBar /> <ProfitLossActionsBar />
<div class="financial-statement"> <DashboardPageContent>
<ProfitLossSheetHeader <div class="financial-statement">
pageFilter={filter} <ProfitLossSheetHeader
onSubmitFilter={handleSubmitFilter} /> pageFilter={filter}
onSubmitFilter={handleSubmitFilter} />
<div class="financial-statement__body"> <div class="financial-statement__body">
<LoadingIndicator loading={false}>
<ProfitLossSheetTable <ProfitLossSheetTable
data={[]} profitLossSheetIndex={profitLossSheetIndex}
onFetchData={handleFetchData} /> onFetchData={handleFetchData}
</LoadingIndicator> loading={profitLossSheetLoading} />
</div>
</div> </div>
</div> </DashboardPageContent>
</DashboardInsider> </DashboardInsider>
); );
} }

View File

@@ -39,7 +39,7 @@ export default function JournalHeader({
const handleItemSelectDisplayColumns = useCallback((item) => { const handleItemSelectDisplayColumns = useCallback((item) => {
formik.setFieldValue('display_columns_type', item.type); formik.setFieldValue('display_columns_type', item.type);
formik.setFieldValue('display_columns_by', item.by); formik.setFieldValue('display_columns_by', item.by);
}, []); }, [formik]);
const handleSubmitClick = useCallback(() => { const handleSubmitClick = useCallback(() => {
formik.submitForm(); formik.submitForm();
@@ -68,7 +68,7 @@ export default function JournalHeader({
<Button <Button
type="submit" type="submit"
onClick={handleSubmitClick} onClick={handleSubmitClick}
className={'button--submit-filter'}> className={'button--submit-filter mt2'}>
{ 'Run Report' } { 'Run Report' }
</Button> </Button>
</Col> </Col>

View File

@@ -2,14 +2,59 @@ import React, {useState, useMemo, useCallback} from 'react';
import FinancialSheet from 'components/FinancialSheet'; import FinancialSheet from 'components/FinancialSheet';
import DataTable from 'components/DataTable'; import DataTable from 'components/DataTable';
import Money from 'components/Money'; import Money from 'components/Money';
import ProfitLossSheetConnect from 'connectors/ProfitLossSheet.connect';
import ProfitLossSheetTableConnect from 'connectors/ProfitLossTable.connect';
import { compose, defaultExpanderReducer } from 'utils';
export default function ProfitLossSheetTable({ function ProfitLossSheetTable({
loading, loading,
data, data,
onFetchData, onFetchData,
profitLossTableRows,
profitLossQuery,
profitLossColumns
}) { }) {
const columns = useMemo(() => [ const columns = useMemo(() => [
{
// Build our expander column
id: 'expander', // Make sure it has an ID
className: 'expander',
Header: ({
getToggleAllRowsExpandedProps,
isAllRowsExpanded
}) => (
<span {...getToggleAllRowsExpandedProps()} className="toggle">
{isAllRowsExpanded ?
(<span class="arrow-down" />) :
(<span class="arrow-right" />)
}
</span>
),
Cell: ({ row }) =>
// Use the row.canExpand and row.getToggleRowExpandedProps prop getter
// to build the toggle for expanding a row
row.canExpand ? (
<span
{...row.getToggleRowExpandedProps({
style: {
// We can even use the row.depth property
// and paddingLeft to indicate the depth
// of the row
paddingLeft: `${row.depth * 2}rem`,
},
className: 'toggle',
})}
>
{row.isExpanded ?
(<span class="arrow-down" />) :
(<span class="arrow-right" />)
}
</span>
) : null,
width: 20,
disableResizing: true,
},
{ {
Header: 'Account Name', Header: 'Account Name',
accessor: 'name', accessor: 'name',
@@ -20,12 +65,52 @@ export default function ProfitLossSheetTable({
accessor: 'code', accessor: 'code',
className: "account_code", className: "account_code",
}, },
]) ...(profitLossQuery.display_columns_type === 'total') ? [
{
Header: 'Total',
Cell: ({ cell }) => {
const row = cell.row.original;
if (row.total) {
return (<Money amount={row.total.formatted_amount} currency={'USD'} />);
}
return '';
},
className: "total",
}
] : [],
...(profitLossQuery.display_columns_type === 'date_periods') ?
(profitLossColumns.map((column, index) => ({
id: `date_period_${index}`,
Header: column,
accessor: (row) => {
if (row.periods && row.periods[index]) {
const amount = row.periods[index].formatted_amount;
return (<Money amount={amount} currency={'USD'} />);
}
return '';
},
width: 100,
})))
: [],
], [profitLossQuery.display_columns_type, profitLossColumns]);
// Handle data table fetch data.
const handleFetchData = useCallback((...args) => { const handleFetchData = useCallback((...args) => {
onFetchData && onFetchData(...args); onFetchData && onFetchData(...args);
}, [onFetchData]); }, [onFetchData]);
// Retrieve default expanded rows of balance sheet.
const expandedRows = useMemo(() =>
defaultExpanderReducer(profitLossTableRows, 1),
[profitLossTableRows]);
// Retrieve conditional datatable row classnames.
const rowClassNames = useCallback((row) => {
return {
[`row--${row.rowType}`]: row.rowType,
};
}, []);
return ( return (
<FinancialSheet <FinancialSheet
companyTitle={'Facebook, Incopration'} companyTitle={'Facebook, Incopration'}
@@ -37,8 +122,15 @@ export default function ProfitLossSheetTable({
<DataTable <DataTable
className="bigcapital-datatable--financial-report" className="bigcapital-datatable--financial-report"
columns={columns} columns={columns}
data={data} data={profitLossTableRows}
onFetchData={handleFetchData} /> onFetchData={handleFetchData}
expanded={expandedRows}
rowClassNames={rowClassNames} />
</FinancialSheet> </FinancialSheet>
) );
} }
export default compose(
ProfitLossSheetConnect,
ProfitLossSheetTableConnect,
)(ProfitLossSheetTable);

View File

@@ -19,6 +19,7 @@ export default function RadiosAccountingBasis(props) {
onChange={handleStringChange((value) => { onChange={handleStringChange((value) => {
onChange && onChange(value); onChange && onChange(value);
})} })}
className={'radio-group---accounting-basis'}
{...rest}> {...rest}>
<Radio label="Cash" value="cash" /> <Radio label="Cash" value="cash" />
<Radio label="Accural" value="accural" /> <Radio label="Accural" value="accural" />

View File

@@ -1,6 +1,6 @@
import React, {useMemo, useCallback} from 'react'; import React, { useMemo, useState, useCallback } from 'react';
import SelectList from 'components/SelectList'; import SelectList from 'components/SelectList';
import { import {
FormGroup, FormGroup,
@@ -8,21 +8,31 @@ import {
} from '@blueprintjs/core'; } from '@blueprintjs/core';
export default function SelectsListColumnsBy(props) { export default function SelectsListColumnsBy(props) {
const { formGroupProps, selectListProps } = props; const { onItemSelect, formGroupProps, selectListProps } = props;
const [itemSelected, setItemSelected] = useState(null);
const displayColumnsByOptions = useMemo(() => [ const displayColumnsByOptions = useMemo(() => [
{key: 'total', name: 'Total', type: 'total', by: '', }, {key: 'total', name: 'Total', type: 'total', by: '', },
{key: 'year', name: 'Year', type: 'date', by: 'year'}, {key: 'year', name: 'Date/Year', type: 'date_periods', by: 'year'},
{key: 'month', name: 'Month', type: 'date', by: 'month'}, {key: 'month', name: 'Date/Month', type: 'date_periods', by: 'month'},
{key: 'week', name: 'Week', type: 'date', by: 'month'}, {key: 'week', name: 'Date/Week', type: 'date_periods', by: 'month'},
{key: 'day', name: 'Day', type: 'date', by: 'day'}, {key: 'day', name: 'Date/Day', type: 'date_periods', by: 'day'},
{key: 'quarter', name: 'Quarter', type: 'date', by: 'quarter'}, {key: 'quarter', name: 'Date/Quarter', type: 'date_periods', by: 'quarter'},
]); ]);
const itemRenderer = useCallback((item, { handleClick, modifiers, query }) => { const itemRenderer = useCallback((item, { handleClick, modifiers, query }) => {
return (<MenuItem text={item.name} key={item.id} onClick={handleClick} />); 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 : 'Select display columns by...',
[itemSelected]);
return ( return (
<FormGroup <FormGroup
label={'Display report columns'} label={'Display report columns'}
@@ -36,7 +46,8 @@ export default function SelectsListColumnsBy(props) {
filterable={false} filterable={false}
itemRenderer={itemRenderer} itemRenderer={itemRenderer}
popoverProps={{ minimal: true }} popoverProps={{ minimal: true }}
buttonLabel={'Select...'} buttonLabel={buttonLabel}
onItemSelect={handleItemSelect}
{...selectListProps} /> {...selectListProps} />
</FormGroup> </FormGroup>
); );

View File

@@ -55,7 +55,8 @@ export default function TrialBalanceSheetHeader({
<Button <Button
type="submit" type="submit"
onClick={handleSubmitClick} onClick={handleSubmitClick}
disabled={formik.isSubmitting}> disabled={formik.isSubmitting}
className={'button--submit-filter'}>
{ 'Run Report' } { 'Run Report' }
</Button> </Button>
</Col> </Col>

View File

@@ -3,11 +3,19 @@ import t from 'store/types';
export const fetchGeneralLedger = ({ query }) => { export const fetchGeneralLedger = ({ query }) => {
return (dispatch) => new Promise((resolve, reject) => { return (dispatch) => new Promise((resolve, reject) => {
dispatch({
type: t.GENERAL_LEDGER_SHEET_LOADING,
loading: true,
})
ApiService.get('/financial_statements/general_ledger', { params: query }).then((response) => { ApiService.get('/financial_statements/general_ledger', { params: query }).then((response) => {
dispatch({ dispatch({
type: t.GENERAL_LEDGER_STATEMENT_SET, type: t.GENERAL_LEDGER_STATEMENT_SET,
data: response.data, data: response.data,
}); });
dispatch({
type: t.GENERAL_LEDGER_SHEET_LOADING,
loading: false,
});
resolve(response); resolve(response);
}).catch((error) => { reject(error); }); }).catch((error) => { reject(error); });
}); });
@@ -15,12 +23,20 @@ export const fetchGeneralLedger = ({ query }) => {
export const fetchBalanceSheet = ({ query }) => { export const fetchBalanceSheet = ({ query }) => {
return (dispatch) => new Promise((resolve, reject) => { return (dispatch) => new Promise((resolve, reject) => {
dispatch({
type: t.BALANCE_SHEET_LOADING,
loading: true,
});
ApiService.get('/financial_statements/balance_sheet', { params: query }).then((response) => { ApiService.get('/financial_statements/balance_sheet', { params: query }).then((response) => {
dispatch({ dispatch({
type: t.BALANCE_SHEET_STATEMENT_SET, type: t.BALANCE_SHEET_STATEMENT_SET,
data: response.data, data: response.data,
query: query, query: query,
}); });
dispatch({
type: t.BALANCE_SHEET_LOADING,
loading: false,
});
resolve(response); resolve(response);
}).catch((error) => { reject(error); }); }).catch((error) => { reject(error); });
}); });
@@ -50,9 +66,9 @@ export const fetchProfitLossSheet = ({ query }) => {
return (dispatch) => new Promise((resolve, reject) => { return (dispatch) => new Promise((resolve, reject) => {
dispatch({ dispatch({
type: t.PROFIT_LOSS_SHEET_LOADING, type: t.PROFIT_LOSS_SHEET_LOADING,
loading: false, loading: true,
}); });
ApiService.get('/financial_statements/profit_loss_sheet').then((response) => { ApiService.get('/financial_statements/profit_loss_sheet', { params: query }).then((response) => {
dispatch({ dispatch({
type: t.PROFIT_LOSS_SHEET_SET, type: t.PROFIT_LOSS_SHEET_SET,
profitLoss: response.data.profitLoss, profitLoss: response.data.profitLoss,

View File

@@ -1,14 +1,17 @@
import { createReducer } from '@reduxjs/toolkit'; import { createReducer } from '@reduxjs/toolkit';
import t from 'store/types'; import t from 'store/types';
import { import {
getBalanceSheetIndexByQuery, // getBalanceSheetIndexByQuery,
getFinancialSheetIndexByQuery, getFinancialSheetIndexByQuery,
// getFinancialSheetIndexByQuery, // getFinancialSheetIndexByQuery,
} from './financialStatements.selectors'; } from './financialStatements.selectors';
import {omit} from 'lodash'; import {omit} from 'lodash';
const initialState = { const initialState = {
balanceSheets: [], balanceSheet: {
sheets: [],
loading: false,
},
trialBalance: { trialBalance: {
sheets: [], sheets: [],
loading: false, loading: false,
@@ -72,22 +75,61 @@ const mapJournalTableRows = (journal) => {
}, []); }, []);
} }
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',
}
]
};
export default createReducer(initialState, { export default createReducer(initialState, {
[t.BALANCE_SHEET_STATEMENT_SET]: (state, action) => { [t.BALANCE_SHEET_STATEMENT_SET]: (state, action) => {
const index = getBalanceSheetIndexByQuery(state.balanceSheets, action.query); const index = getFinancialSheetIndexByQuery(state.balanceSheet.sheets, action.query);
const balanceSheet = { const balanceSheet = {
balances: action.data.balance_sheet, accounts: action.data.accounts,
columns: Object.values(action.data.columns), columns: Object.values(action.data.columns),
query: action.data.query, query: action.data.query,
}; };
if (index !== -1) { if (index !== -1) {
state.balanceSheets[index] = balanceSheet; state.balanceSheet.sheets[index] = balanceSheet;
} else { } else {
state.balanceSheets.push(balanceSheet); state.balanceSheet.sheets.push(balanceSheet);
} }
}, },
[t.BALANCE_SHEET_LOADING]: (state, action) => {
state.balanceSheet.loading = !!action.loading;
},
[t.TRAIL_BALANCE_STATEMENT_SET]: (state, action) => { [t.TRAIL_BALANCE_STATEMENT_SET]: (state, action) => {
const index = getFinancialSheetIndexByQuery(state.trialBalance.sheets, action.query); const index = getFinancialSheetIndexByQuery(state.trialBalance.sheets, action.query);
const trailBalanceSheet = { const trailBalanceSheet = {
@@ -139,6 +181,10 @@ export default createReducer(initialState, {
} }
}, },
[t.GENERAL_LEDGER_SHEET_LOADING]: (state, action) => {
state.generalLedger.loading = !!action.loading;
},
[t.PROFIT_LOSS_SHEET_SET]: (state, action) => { [t.PROFIT_LOSS_SHEET_SET]: (state, action) => {
const index = getFinancialSheetIndexByQuery(state.profitLoss.sheets, action.query); const index = getFinancialSheetIndexByQuery(state.profitLoss.sheets, action.query);
@@ -146,6 +192,7 @@ export default createReducer(initialState, {
query: action.query, query: action.query,
profitLoss: action.profitLoss, profitLoss: action.profitLoss,
columns: action.columns, columns: action.columns,
tableRows: mapProfitLossToTableRows(action.profitLoss),
}; };
if (index !== -1) { if (index !== -1) {
state.profitLoss.sheets[index] = profitLossSheet; state.profitLoss.sheets[index] = profitLossSheet;

View File

@@ -38,97 +38,19 @@ export const getFinancialSheetColumns = (sheets, index) => {
* @param {array} sheets * @param {array} sheets
* @param {number} index * @param {number} index
*/ */
export const getFinancialSheetsQuery = (sheets, index) => { export const getFinancialSheetQuery = (sheets, index) => {
const sheet = getFinancialSheet(sheets, index); const sheet = getFinancialSheet(sheets, index);
return (sheet && sheet.query) ? sheet.columns : {}; return (sheet && sheet.query) ? sheet.query : {};
}; };
// Balance Sheet. export const getFinancialSheetAccounts = (sheets, index) => {
export const getBalanceSheetByQuery = (balanceSheets, query) => { const sheet = getFinancialSheet(sheets, index);
return balanceSheets.find(balanceSheet => { return (sheet && sheet.accounts) ? sheet.accounts : [];
return getObjectDiff(query, balanceSheet.query).length === 0;
});
};
export const getBalanceSheetIndexByQuery = (balanceSheets, query) => {
return balanceSheets.findIndex((balanceSheet) => {
return getObjectDiff(query, balanceSheet.query).length === 0;
});
};
export const getBalanceSheetByIndex = (balanceSheets, sheetIndex) => {
return balanceSheets[sheetIndex];
};
export const getBalanceSheetQuery = (balanceSheets, sheetIndex) => {
if (typeof balanceSheets[sheetIndex] === 'object') {
return balanceSheets[sheetIndex].query || {};
}
return {};
};
export const getBalanceSheetAssetsAccounts = (balanceSheets, sheetIndex) => {
if (typeof balanceSheets[sheetIndex] === 'object') {
return balanceSheets[sheetIndex].balances.assets.accounts || [];
}
return [];
};
export const getBalanceSheetLiabilitiesAccounts = (balanceSheets, sheetIndex) => {
if (typeof balanceSheets[sheetIndex] === 'object') {
return balanceSheets[sheetIndex].balances.liabilities_equity.accounts || [];
}
return [];
};
export const getBalanceSheetColumns = (balanceSheets, sheetIndex) => {
if (typeof balanceSheets[sheetIndex] === 'object') {
return balanceSheets[sheetIndex].columns;
}
return [];
}; };
// Trial Balance Sheet. export const getFinancialSheetTableRows = (sheets, index) => {
export const getTrialBalanceSheetIndex = (trialBalanceSheets, query) => { const sheet = getFinancialSheet(sheets, index);
return trialBalanceSheets.find((trialBalanceSheet) => { return (sheet && sheet.tableRows) ? sheet.tableRows : [];
return getObjectDiff(query, trialBalanceSheet.query).length === 0;
});
};
export const getTrialBalanceAccounts = (trialBalanceSheets, sheetIndex) => {
if (typeof trialBalanceSheets[sheetIndex] === 'object') {
return trialBalanceSheets[sheetIndex].accounts;
}
return [];
};
export const getTrialBalanceQuery = (trialBalanceSheets, sheetIndex) => {
if (typeof trialBalanceSheets[sheetIndex] === 'object') {
return trialBalanceSheets[sheetIndex].query;
}
return [];
};
// Profit/Loss Sheet selectors.
export const getProfitLossSheetIndex = (profitLossSheets, query) => {
return profitLossSheets.find((profitLossSheet) => {
return getObjectDiff(query, profitLossSheet.query).length === 0;
});
}
export const getProfitLossSheet = (profitLossSheets, index) => {
return (typeof profitLossSheets[index] !== 'undefined') ?
profitLossSheets[index] : null;
};
export const getProfitLossSheetColumns = (profitLossSheets, index) => {
const sheet = getProfitLossSheet(profitLossSheets, index);
return (sheet) ? sheet.columns : [];
};
export const getProfitLossSheetAccounts = (profitLossSheets, index) => {
const sheet = getProfitLossSheet(profitLossSheets, index);
return (sheet) ? sheet.accounts : [];
}; };

View File

@@ -2,9 +2,14 @@
export default { export default {
GENERAL_LEDGER_STATEMENT_SET: 'GENERAL_LEDGER_STATEMENT_SET', GENERAL_LEDGER_STATEMENT_SET: 'GENERAL_LEDGER_STATEMENT_SET',
GENERAL_LEDGER_SHEET_LOADING: 'GENERAL_LEDGER_SHEET_LOADING',
BALANCE_SHEET_STATEMENT_SET: 'BALANCE_SHEET_STATEMENT_SET', BALANCE_SHEET_STATEMENT_SET: 'BALANCE_SHEET_STATEMENT_SET',
BALANCE_SHEET_LOADING: 'BALANCE_SHEET_LOADING',
TRAIL_BALANCE_STATEMENT_SET: 'TRAIL_BALANCE_STATEMENT_SET', TRAIL_BALANCE_STATEMENT_SET: 'TRAIL_BALANCE_STATEMENT_SET',
TRIAL_BALANCE_SHEET_LOADING: 'TRIAL_BALANCE_SHEET_LOADING', TRIAL_BALANCE_SHEET_LOADING: 'TRIAL_BALANCE_SHEET_LOADING',
JOURNAL_SHEET_SET: 'JOURNAL_SHEET_SET', JOURNAL_SHEET_SET: 'JOURNAL_SHEET_SET',
JOURNAL_SHEET_LOADING: 'JOURNAL_SHEET_LOADING', JOURNAL_SHEET_LOADING: 'JOURNAL_SHEET_LOADING',

View File

@@ -139,7 +139,6 @@
} }
.no-results{ .no-results{
color: #666; color: #666;
.td{ .td{
@@ -149,6 +148,15 @@
} }
} }
&.has-sticky-header{
.thead{
.tr .th{
position: sticky;
}
}
}
&--financial-report{ &--financial-report{
.thead{ .thead{

View File

@@ -30,7 +30,7 @@
min-height: 32px; min-height: 32px;
background-color: #E6EFFB; background-color: #E6EFFB;
color: #555555; color: #555555;
box-shadow: 0 0 0; box-shadow: 0 0 0 transparent;
.form-group--select-list &{ .form-group--select-list &{
border-radius: 0; border-radius: 0;
@@ -38,7 +38,7 @@
&, &,
&:hover{ &:hover{
background: #fff; background: #fff;
box-shadow: 0 0 0; box-shadow: 0 0 0 transparent;
border: 1px solid #ced4da; border: 1px solid #ced4da;
} }
} }
@@ -53,7 +53,7 @@
&, &,
&:hover{ &:hover{
box-shadow: 0 0 0; box-shadow: 0 0 0 transparent;
} }
} }
} }

View File

@@ -95,3 +95,109 @@ label{
color: #8D8D8D; color: #8D8D8D;
} }
} }
///@extend
.#{$ns}-control {
input:checked ~ .#{$ns}-control-indicator {
box-shadow: 0 0 0 transparent;
background-color: transparent;
background-image: none;
}
&:hover input:checked ~ .#{$ns}-control-indicator {
box-shadow: 0 0 0 transparent;
background-color: transparent;
}
input:not(:disabled):active:checked ~ .#{$ns}-control-indicator {
box-shadow: 0 0 0 transparent;
background: transparent;
}
input:disabled:checked ~ .#{$ns}-control-indicator {
box-shadow: none;
background: transparent;
}
&.#{$ns}-disabled {
cursor: not-allowed;
color: $pt-text-color-disabled;
}
&.#{$ns}-inline {
display: inline-block;
margin-right: $pt-grid-size * 2;
}
.#{$ns}-control-indicator {
box-shadow: 0 0 0 transparent;
background-clip: padding-box;
background-color: transparent;
background-image: none;
width: 18px;
height: 18px;
&::before {
width: 18px;
height: 18px;
}
}
&:hover .#{$ns}-control-indicator {
background-color: transparent;
}
input:not(:disabled):active ~ .#{$ns}-control-indicator {
box-shadow: 0 0 0 transparent;
background: transparent;
}
/*
Radio
Markup:
<label class="#{$ns}-control #{$ns}-radio {{.modifier}}">
<input type="radio" name="docs-radio-regular" {{:modifier}} />
<span class="#{$ns}-control-indicator"></span>
Radio
</label>
:checked - Selected
:disabled - Disabled. Also add <code>.#{$ns}-disabled</code> to <code>.#{$ns}-control</code> to change text color (not shown below).
.#{$ns}-align-right - Right-aligned indicator
.#{$ns}-large - Large
Styleguide radio
*/
&.#{$ns}-radio {
.#{$ns}-control-indicator{
border: 2px solid #cecece;
&::before{
height: 14px;
width: 14px;
}
}
input:checked ~ .#{$ns}-control-indicator{
border-color: #137cbd;
&::before {
background-image: radial-gradient(#137cbd 40%, transparent 40%);
}
}
input:checked:disabled ~ .#{$ns}-control-indicator::before {
opacity: 0.5;
}
input:focus ~ .#{$ns}-control-indicator {
-moz-outline-radius: $control-indicator-size;
}
}
}

View File

@@ -7,26 +7,46 @@
padding: 25px 26px 25px; padding: 25px 26px 25px;
background: #FDFDFD; background: #FDFDFD;
.bp3-form-group .bp3-label{ .bp3-form-group,
font-weight: 500; .radio-group---accounting-basis{
font-size: 13px;
color: #444; .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{ &__body{
padding-left: 20px; padding-left: 20px;
padding-right: 20px; padding-right: 20px;
display: flex;
justify-content: center;
align-items: center;
} }
} }
.financial-sheet{ .financial-sheet{
border: 1px solid #E2E2E2; border: 1px solid #E2E2E2;
min-width: 640px; min-width: 640px;
width: 0; width: auto;
padding: 20px; padding: 30px 20px;
padding-top: 30px; max-width: 100%;
margin: 35px auto; margin: 35px auto;
min-height: 400px;
display: flex;
flex-direction: column;
&__title{ &__title{
margin: 0; margin: 0;
@@ -57,14 +77,24 @@
} }
} }
} }
&__accounting-basis{ &__basis{
color: #888;
text-align: center;
margin-top: auto;
padding-top: 16px;
font-size: 12px;
}
.dashboard__loading-indicator{
margin-left: auto;
margin-right: auto;
} }
&--expended{
width: auto;
}
&--trial-balance{ &--trial-balance{
min-width: 720px; min-width: 720px;
} }
&--general-ledger, &--general-ledger,
&--journal{ &--journal{
width: auto; width: auto;
@@ -73,11 +103,8 @@
margin-top: 10px; margin-top: 10px;
border-color: #EEEDED; border-color: #EEEDED;
} }
&--journal{ &--journal{
.financial-sheet__table{ .financial-sheet__table{
.tbody{ .tbody{
.tr .td{ .tr .td{
padding: 0.4rem; padding: 0.4rem;
@@ -96,4 +123,25 @@
} }
} }
} }
&--profit-loss-sheet{
.financial-sheet__table{
.tbody{
.account_code.td{
color: #666;
}
.row--income_total,
.row--expense_total,
.row--net_income{
font-weight: 600;
.total.td{
border-bottom-color: #555;
}
}
}
}
}
} }

View File

@@ -112,3 +112,23 @@ export const parseDateRangeQuery = (keyword) => {
to_date: moment().endOf(query.range).toDate(), to_date: moment().endOf(query.range).toDate(),
}; };
}; };
export const defaultExpanderReducer = (tableRows, level) => {
let currentLevel = 1;
const expended = [];
const walker = (rows, parentIndex = null) => {
return rows.forEach((row, index) => {
const _index = parentIndex ? `${parentIndex}.${index}` : `${index}`;
expended[_index] = true;
if (row.children && currentLevel < level) {
walker(row.children, _index);
}
currentLevel++;
}, {});
};
walker(tableRows);
return expended;
}

View File

@@ -274,7 +274,9 @@ export default {
query('accounting_method').optional().isIn(['cash', 'accural']), query('accounting_method').optional().isIn(['cash', 'accural']),
query('from_date').optional(), query('from_date').optional(),
query('to_date').optional(), query('to_date').optional(),
query('display_columns_by').optional().isIn(['total', 'year', 'month', 'week', 'day', 'quarter']), query('display_columns_type').optional().isIn(['date_periods', 'total']),
query('display_columns_by').optional({ nullable: true, checkFalsy: true })
.isIn(['year', 'month', 'week', 'day', 'quarter']),
query('number_format.no_cents').optional().isBoolean().toBoolean(), query('number_format.no_cents').optional().isBoolean().toBoolean(),
query('number_format.divide_1000').optional().isBoolean().toBoolean(), query('number_format.divide_1000').optional().isBoolean().toBoolean(),
query('none_zero').optional().isBoolean().toBoolean(), query('none_zero').optional().isBoolean().toBoolean(),
@@ -288,7 +290,8 @@ export default {
}); });
} }
const filter = { const filter = {
display_columns_by: 'total', display_columns_type: 'total',
display_columns_by: '',
from_date: moment().startOf('year').format('YYYY-MM-DD'), from_date: moment().startOf('year').format('YYYY-MM-DD'),
to_date: moment().endOf('year').format('YYYY-MM-DD'), to_date: moment().endOf('year').format('YYYY-MM-DD'),
number_format: { number_format: {
@@ -299,7 +302,6 @@ export default {
basis: 'cash', basis: 'cash',
...req.query, ...req.query,
}; };
const balanceSheetTypes = await AccountType.query().where('balance_sheet', true); const balanceSheetTypes = await AccountType.query().where('balance_sheet', true);
// Fetch all balance sheet accounts. // Fetch all balance sheet accounts.
@@ -317,91 +319,76 @@ export default {
// Account balance formmatter based on the given query. // Account balance formmatter based on the given query.
const balanceFormatter = formatNumberClosure(filter.number_format); const balanceFormatter = formatNumberClosure(filter.number_format);
const filterDateType = filter.display_columns_by === 'total' const comparatorDateType = filter.display_columns_type === 'total'
? 'day' : filter.display_columns_by; ? 'day' : filter.display_columns_by;
// Gets the date range set from start to end date. const dateRangeSet = (filter.display_columns_type === 'date_periods')
const dateRangeSet = dateRangeCollection( ? dateRangeCollection(
filter.from_date, filter.from_date, filter.to_date, comparatorDateType,
filter.to_date, ) : [];
filterDateType,
); const totalPeriods = (account) => {
// Retrieve the asset balance sheet. // Gets the date range set from start to end date.
const assets = accounts return {
.filter((account) => ( total_periods: dateRangeSet.map((date) => {
account.type.normal === 'debit' const balance = journalEntries.getClosingBalance(account.id, date, comparatorDateType);
&& (account.transactions.length > 0 || !filter.none_zero) return {
)) date,
.map((account) => { formatted_amount: balanceFormatter(balance),
amount: balance,
};
}),
};
};
const accountsMapper = (balanceSheetAccounts) => {
return balanceSheetAccounts.map((account) => {
// Calculates the closing balance to the given date. // Calculates the closing balance to the given date.
const closingBalance = journalEntries.getClosingBalance(account.id, filter.to_date); const closingBalance = journalEntries.getClosingBalance(account.id, filter.to_date);
const type = filter.display_columns_by;
return { return {
...pick(account, ['id', 'index', 'name', 'code']), ...pick(account, ['id', 'index', 'name', 'code']),
...(type !== 'total') ? {
periods_balance: dateRangeSet.map((date) => {
const balance = journalEntries.getClosingBalance(account.id, date, filterDateType);
return { // Date periods when display columns.
date, ...(filter.display_columns_type === 'date_periods') && totalPeriods(account),
formatted_amount: balanceFormatter(balance),
amount: balance, total: {
};
}),
} : {},
balance: {
formatted_amount: balanceFormatter(closingBalance), formatted_amount: balanceFormatter(closingBalance),
amount: closingBalance, amount: closingBalance,
date: filter.to_date, date: filter.to_date,
}, },
}; };
}); });
};
// Retrieve all assets accounts.
const assetsAccounts = accounts.filter((account) => (
account.type.normal === 'debit'
&& (account.transactions.length > 0 || !filter.none_zero)));
// Retrieve all liability accounts.
const liabilitiesAccounts = accounts.filter((account) => (
account.type.normal === 'credit'
&& (account.transactions.length > 0 || !filter.none_zero)));
// Retrieve the asset balance sheet.
const assets = accountsMapper(assetsAccounts);
// Retrieve liabilities and equity balance sheet. // Retrieve liabilities and equity balance sheet.
const liabilitiesEquity = accounts const liabilitiesEquity = accountsMapper(liabilitiesAccounts);
.filter((account) => (
account.type.normal === 'credit'
&& (account.transactions.length > 0 || !filter.none_zero)
))
.map((account) => {
// Calculates the closing balance to the given date.
const closingBalance = journalEntries.getClosingBalance(account.id, filter.to_date);
const type = filter.display_columns_by;
return {
...pick(account, ['id', 'index', 'name', 'code']),
...(type !== 'total') ? {
periods_balance: dateRangeSet.map((date) => {
const balance = journalEntries.getClosingBalance(account.id, date, filterDateType);
return {
date,
formatted_amount: balanceFormatter(balance),
amount: balance,
};
}),
} : {},
balance: {
formatted_amount: balanceFormatter(closingBalance),
amount: closingBalance,
date: filter.to_date,
},
};
});
return res.status(200).send({ return res.status(200).send({
query: { ...filter }, query: { ...filter },
columns: { ...dateRangeSet }, columns: { ...dateRangeSet },
balance_sheet: { accounts: [
assets: { {
title: 'Assets', name: 'Assets',
accounts: [...assets], children: [...assets],
}, },
liabilities_equity: { {
title: 'Liabilities & Equity', name: 'Liabilities & Equity',
accounts: [...liabilitiesEquity], children: [...liabilitiesEquity],
}, },
}, ],
}); });
}, },
}, },
@@ -492,9 +479,8 @@ export default {
query('display_columns_type').optional().isIn([ query('display_columns_type').optional().isIn([
'total', 'date_periods', 'total', 'date_periods',
]), ]),
query('display_columns_by').optional().isIn([ query('display_columns_by').optional({ nullable: true, checkFalsy: true })
'year', 'month', 'week', 'day', 'quarter', .isIn(['year', 'month', 'week', 'day', 'quarter']),
]),
], ],
async handler(req, res) { async handler(req, res) {
const validationErrors = validationResult(req); const validationErrors = validationResult(req);

View File

@@ -363,7 +363,7 @@ describe('routes: `/financial_statements`', () => {
}); });
}); });
describe('routes: `financial_statements/balance_sheet`', () => { describe.only('routes: `financial_statements/balance_sheet`', () => {
it('Should response unauthorzied in case the user was not authorized.', async () => { it('Should response unauthorzied in case the user was not authorized.', async () => {
const res = await request() const res = await request()
.get('/api/financial_statements/balance_sheet') .get('/api/financial_statements/balance_sheet')
@@ -406,21 +406,23 @@ describe('routes: `/financial_statements`', () => {
expect(res.body.balance_sheet.liabilities_equity.accounts).to.be.a('array'); expect(res.body.balance_sheet.liabilities_equity.accounts).to.be.a('array');
}); });
it('Should retrieve assets/liabilities total balance between the given date range.', async () => { it.only('Should retrieve assets/liabilities total balance between the given date range.', async () => {
const res = await request() const res = await request()
.get('/api/financial_statements/balance_sheet') .get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token) .set('x-access-token', loginRes.body.token)
.query({ .query({
display_columns_by: 'total', display_columns_type: 'total',
from_date: '2012-01-01', from_date: '2012-01-01',
to_date: '2032-02-02', to_date: '2032-02-02',
}) })
.send(); .send();
expect(res.body.balance_sheet.assets.accounts[0].balance).deep.equals({ console.log(res.body.balance_sheet.assets.accounts);
expect(res.body.balance_sheet.assets.accounts[0].total).deep.equals({
amount: 4000, formatted_amount: 4000, date: '2032-02-02', amount: 4000, formatted_amount: 4000, date: '2032-02-02',
}); });
expect(res.body.balance_sheet.liabilities_equity.accounts[0].balance).deep.equals({ expect(res.body.balance_sheet.liabilities_equity.accounts[0].total).deep.equals({
amount: 2000, formatted_amount: 2000, date: '2032-02-02', amount: 2000, formatted_amount: 2000, date: '2032-02-02',
}); });
}); });

View File

@@ -8,7 +8,7 @@ import knex from '@/database/knex';
let loginRes; let loginRes;
describe.only('routes: /item_categories/', () => { describe('routes: /item_categories/', () => {
beforeEach(async () => { beforeEach(async () => {
loginRes = await login(); loginRes = await login();
}); });