diff --git a/client/package.json b/client/package.json index c0b3f4f2c..909ba049c 100644 --- a/client/package.json +++ b/client/package.json @@ -71,6 +71,7 @@ "react-sortablejs": "^2.0.11", "react-table": "^7.0.0", "react-use": "^13.26.1", + "react-window": "^1.8.5", "redux": "^4.0.5", "redux-thunk": "^2.3.0", "resolve": "1.15.0", diff --git a/client/src/components/DataTable.js b/client/src/components/DataTable.js index 3cd9a97ae..25bb97c7d 100644 --- a/client/src/components/DataTable.js +++ b/client/src/components/DataTable.js @@ -1,4 +1,4 @@ -import React, {useEffect} from 'react'; +import React, {useEffect, useMemo, useCallback} from 'react'; import { useTable, useExpanded, @@ -11,6 +11,7 @@ import { } from 'react-table' import {Checkbox} from '@blueprintjs/core'; import classnames from 'classnames'; +import { FixedSizeList } from 'react-window' import Icon from 'components/Icon'; const IndeterminateCheckbox = React.forwardRef( @@ -34,8 +35,15 @@ export default function DataTable({ onSelectedRowsChange, manualSortBy = 'false', selectionColumn = false, + expandSubRows = true, 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 { getTableProps, @@ -43,6 +51,7 @@ export default function DataTable({ headerGroups, prepareRow, page, + rows, canPreviousPage, canNextPage, pageOptions, @@ -52,21 +61,23 @@ export default function DataTable({ previousPage, setPageSize, selectedFlatRows, + totalColumnsWidth, // Get the state from the instance state: { pageIndex, pageSize, sortBy, selectedRowIds }, } = useTable( { columns, - data, - initialState: { pageIndex: 0 }, // Pass our hoisted table state + data: data, + initialState: { pageIndex: 0, expanded }, // Pass our hoisted table state manualPagination: true, // Tell the usePagination // hook that we'll handle our own data fetching // This means we'll also have to provide our own // pageCount. // pageCount: controlledPageCount, getSubRows: row => row.children, - manualSortBy + manualSortBy, + expandSubRows, }, useSortBy, useExpanded, @@ -102,14 +113,50 @@ export default function DataTable({ ]) } ); - + // When these table states change, fetch new data! useEffect(() => { onFetchData && onFetchData({ pageIndex, pageSize, sortBy }) }, [pageIndex, pageSize, sortBy]); + // Renders table row. + const RenderRow = useCallback(({ style = {}, row }) => { + prepareRow(row); + return ( +
.#{$ns}-disabled to .#{$ns}-control 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;
+ }
+ }
}
\ No newline at end of file
diff --git a/client/src/style/pages/financial-statements.scss b/client/src/style/pages/financial-statements.scss
index 8963cfef4..8690ef6a1 100644
--- a/client/src/style/pages/financial-statements.scss
+++ b/client/src/style/pages/financial-statements.scss
@@ -7,26 +7,46 @@
padding: 25px 26px 25px;
background: #FDFDFD;
- .bp3-form-group .bp3-label{
- font-weight: 500;
- font-size: 13px;
- color: #444;
+ .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{
padding-left: 20px;
padding-right: 20px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
}
}
.financial-sheet{
border: 1px solid #E2E2E2;
min-width: 640px;
- width: 0;
- padding: 20px;
- padding-top: 30px;
+ width: auto;
+ padding: 30px 20px;
+ max-width: 100%;
margin: 35px auto;
+ min-height: 400px;
+ display: flex;
+ flex-direction: column;
&__title{
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{
min-width: 720px;
}
-
&--general-ledger,
&--journal{
width: auto;
@@ -73,11 +103,8 @@
margin-top: 10px;
border-color: #EEEDED;
}
-
&--journal{
-
.financial-sheet__table{
-
.tbody{
.tr .td{
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;
+ }
+ }
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/client/src/utils.js b/client/src/utils.js
index 8f6f0936b..77ade104c 100644
--- a/client/src/utils.js
+++ b/client/src/utils.js
@@ -111,4 +111,24 @@ export const parseDateRangeQuery = (keyword) => {
from_date: moment().startOf(query.range).toDate(),
to_date: moment().endOf(query.range).toDate(),
};
-};
\ No newline at end of file
+};
+
+
+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;
+}
\ No newline at end of file
diff --git a/server/src/http/controllers/FinancialStatements.js b/server/src/http/controllers/FinancialStatements.js
index be387033c..dd3c2e522 100644
--- a/server/src/http/controllers/FinancialStatements.js
+++ b/server/src/http/controllers/FinancialStatements.js
@@ -274,7 +274,9 @@ export default {
query('accounting_method').optional().isIn(['cash', 'accural']),
query('from_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.divide_1000').optional().isBoolean().toBoolean(),
query('none_zero').optional().isBoolean().toBoolean(),
@@ -288,7 +290,8 @@ export default {
});
}
const filter = {
- display_columns_by: 'total',
+ display_columns_type: 'total',
+ display_columns_by: '',
from_date: moment().startOf('year').format('YYYY-MM-DD'),
to_date: moment().endOf('year').format('YYYY-MM-DD'),
number_format: {
@@ -299,7 +302,6 @@ export default {
basis: 'cash',
...req.query,
};
-
const balanceSheetTypes = await AccountType.query().where('balance_sheet', true);
// Fetch all balance sheet accounts.
@@ -317,91 +319,76 @@ export default {
// Account balance formmatter based on the given query.
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;
- // Gets the date range set from start to end date.
- const dateRangeSet = dateRangeCollection(
- filter.from_date,
- filter.to_date,
- filterDateType,
- );
- // Retrieve the asset balance sheet.
- const assets = accounts
- .filter((account) => (
- account.type.normal === 'debit'
- && (account.transactions.length > 0 || !filter.none_zero)
- ))
- .map((account) => {
+ const dateRangeSet = (filter.display_columns_type === 'date_periods')
+ ? dateRangeCollection(
+ filter.from_date, filter.to_date, comparatorDateType,
+ ) : [];
+
+ const totalPeriods = (account) => {
+ // Gets the date range set from start to end date.
+ return {
+ total_periods: dateRangeSet.map((date) => {
+ const balance = journalEntries.getClosingBalance(account.id, date, comparatorDateType);
+ return {
+ date,
+ formatted_amount: balanceFormatter(balance),
+ amount: balance,
+ };
+ }),
+ };
+ };
+
+ const accountsMapper = (balanceSheetAccounts) => {
+ return balanceSheetAccounts.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: {
+ // Date periods when display columns.
+ ...(filter.display_columns_type === 'date_periods') && totalPeriods(account),
+
+ total: {
formatted_amount: balanceFormatter(closingBalance),
amount: closingBalance,
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.
- const liabilitiesEquity = accounts
- .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,
- },
- };
- });
+ const liabilitiesEquity = accountsMapper(liabilitiesAccounts);
return res.status(200).send({
query: { ...filter },
columns: { ...dateRangeSet },
- balance_sheet: {
- assets: {
- title: 'Assets',
- accounts: [...assets],
+ accounts: [
+ {
+ name: 'Assets',
+ children: [...assets],
},
- liabilities_equity: {
- title: 'Liabilities & Equity',
- accounts: [...liabilitiesEquity],
+ {
+ name: 'Liabilities & Equity',
+ children: [...liabilitiesEquity],
},
- },
+ ],
});
},
},
@@ -492,9 +479,8 @@ export default {
query('display_columns_type').optional().isIn([
'total', 'date_periods',
]),
- query('display_columns_by').optional().isIn([
- 'year', 'month', 'week', 'day', 'quarter',
- ]),
+ query('display_columns_by').optional({ nullable: true, checkFalsy: true })
+ .isIn(['year', 'month', 'week', 'day', 'quarter']),
],
async handler(req, res) {
const validationErrors = validationResult(req);
diff --git a/server/tests/routes/financial_statements.test.js b/server/tests/routes/financial_statements.test.js
index b86b593e3..7928b8d49 100644
--- a/server/tests/routes/financial_statements.test.js
+++ b/server/tests/routes/financial_statements.test.js
@@ -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 () => {
const res = await request()
.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');
});
- 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()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.query({
- display_columns_by: 'total',
+ display_columns_type: 'total',
from_date: '2012-01-01',
to_date: '2032-02-02',
})
.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',
});
- 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',
});
});
diff --git a/server/tests/routes/itemsCategories.test.js b/server/tests/routes/itemsCategories.test.js
index 7f20624d6..2f0ae0ad1 100644
--- a/server/tests/routes/itemsCategories.test.js
+++ b/server/tests/routes/itemsCategories.test.js
@@ -8,7 +8,7 @@ import knex from '@/database/knex';
let loginRes;
-describe.only('routes: /item_categories/', () => {
+describe('routes: /item_categories/', () => {
beforeEach(async () => {
loginRes = await login();
});