WIP Make journal entries.

This commit is contained in:
Ahmed Bouhuolia
2020-04-07 12:56:12 +02:00
parent 5f6c7ca2d5
commit 490979ded5
19 changed files with 632 additions and 197 deletions

View File

@@ -8,30 +8,9 @@ import { FormattedList } from 'react-intl';
export default function MakeJournalEntriesFooter({
formik,
}) {
const creditSum = useMemo(() => {
return formik.values.entries.reduce((sum, entry) => {
return entry.credit + sum;
}, 0);
}, [formik.values.entries]);
const debitSum = useMemo(() => {
return formik.values.entries.reduce((sum, entry) => {
return entry.debit + sum;
}, 0);
}, [formik.values.entries]);
return (
<div>
<table>
<tbody>
<tr>
<td><strong>Total</strong></td>
<td>{ creditSum }</td>
<td>{ debitSum }</td>
</tr>
</tbody>
</table>
<div class="form__floating-footer">
<Button
intent={Intent.PRIMARY}
@@ -42,9 +21,21 @@ export default function MakeJournalEntriesFooter({
<Button
intent={Intent.PRIMARY}
type="submit"
className={'ml2'}>
className={'ml1'}>
Save & New
</Button>
<Button
type="submit"
className={'button-secondary ml1'}>
Save as Draft
</Button>
<Button
type="submit"
className={'button-secondary ml1'}>
Cancel
</Button>
</div>
</div>
);

View File

@@ -76,13 +76,14 @@ function MakeJournalEntriesForm({
},
});
console.log(formik.errors);
return (
<form onSubmit={formik.handleSubmit}>
<MakeJournalEntriesHeader formik={formik} />
<MakeJournalEntriesTable formik={formik} />
<MakeJournalEntriesFooter formik={formik} />
</form>
<div class="make-journal-entries">
<form onSubmit={formik.handleSubmit}>
<MakeJournalEntriesHeader formik={formik} />
<MakeJournalEntriesTable formik={formik} defaultRow={defaultEntry} />
<MakeJournalEntriesFooter formik={formik} />
</form>
</div>
);
}

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, {useMemo, useCallback} from 'react';
import * as Yup from 'yup';
import {
InputGroup,
@@ -12,6 +12,9 @@ import {useIntl} from 'react-intl';
import {Row, Col} from 'react-grid-system';
import moment from 'moment';
import {momentFormatter} from 'utils';
import Icon from 'components/Icon';
import CurrenciesSelectList from 'components/CurrenciesSelectList';
export default function MakeJournalEntriesHeader({
formik
@@ -22,25 +25,29 @@ export default function MakeJournalEntriesHeader({
const formatted = moment(date).format('YYYY-MM-DD');
formik.setFieldValue('date', formatted);
};
const infoIcon = useMemo(() => (<Icon icon="info-circle" iconSize={12} />), []);
return (
<div class="make-journal-entries__header">
<Row>
<Col sm={4}>
<Col sm={3}>
<FormGroup
label={intl.formatMessage({'id': 'reference'})}
className={'form-group--reference'}
intent={formik.errors.name && Intent.DANGER}
helperText={formik.errors.name && formik.errors.label}
label={'Journal number'}
labelInfo={infoIcon}
className={'form-group--journal-number'}
intent={formik.errors.journal_number && Intent.DANGER}
helperText={formik.errors.journal_number && formik.errors.journal_number}
fill={true}>
<InputGroup
intent={formik.errors.name && Intent.DANGER}
intent={formik.errors.journal_number && Intent.DANGER}
fill={true}
{...formik.getFieldProps('reference')} />
{...formik.getFieldProps('journal_number')} />
</FormGroup>
</Col>
<Col sm={3}>
<Col sm={2}>
<FormGroup
label={intl.formatMessage({'id': 'date'})}
intent={formik.errors.date && Intent.DANGER}
@@ -70,6 +77,28 @@ export default function MakeJournalEntriesHeader({
</FormGroup>
</Col>
</Row>
<Row>
<Col sm={3}>
<FormGroup
label={'Reference'}
labelInfo={infoIcon}
className={'form-group--reference'}
intent={formik.errors.reference && Intent.DANGER}
helperText={formik.errors.reference && formik.errors.reference}
fill={true}>
<InputGroup
intent={formik.errors.reference && Intent.DANGER}
fill={true}
{...formik.getFieldProps('reference')} />
</FormGroup>
</Col>
<Col sm={4}>
<CurrenciesSelectList />
</Col>
</Row>
</div>
);
}

View File

@@ -1,55 +1,88 @@
import React, {useState, useMemo} from 'react';
import React, {useState, useMemo, useCallback} from 'react';
import DataTable from 'components/DataTable';
import {
FormGroup,
InputGroup,
MenuItem,
Button,
Intent,
} from '@blueprintjs/core';
import {Select} from '@blueprintjs/select';
import Icon from 'components/Icon';
import AccountsConnect from 'connectors/Accounts.connector.js';
import {compose} from 'utils';
import {compose, formattedAmount} from 'utils';
import {
AccountsListFieldCell,
MoneyFieldCell,
InputGroupCell,
} from 'comp}onents/DataTableCells';
// Actions cell renderer.
const ActionsCellRenderer = ({
row: { index },
column: { id },
cell: { value: initialValue },
data,
payload,
}) => {
if (data.length <= (index + 2)) {
return '';
}
const onClickRemoveRole = () => {
payload.removeRow(index);
};
return (
<Button
icon={<Icon icon="times-circle" iconSize={14} />}
iconSize={14}
className="ml2"
minimal={true}
intent={Intent.DANGER}
onClick={onClickRemoveRole} />
);
};
// Total text cell renderer.
const TotalAccountCellRenderer = (chainedComponent) => (props) => {
if (props.data.length === (props.row.index + 2)) {
return (<span>{ 'Total USD' }</span>);
}
return chainedComponent(props);
};
// Total credit/debit cell renderer.
const TotalCreditDebitCellRenderer = (chainedComponent, type) => (props) => {
if (props.data.length === (props.row.index + 2)) {
const total = props.data.reduce((total, entry) => {
const amount = parseInt(entry[type], 10);
const computed = amount ? total + amount : total;
return computed;
}, 0);
return (<span>{ formattedAmount(total, 'USD') }</span>);
}
return chainedComponent(props);
};
const NoteCellRenderer = (chainedComponent) => (props) => {
if (props.data.length === (props.row.index + 2)) {
return '';
}
return chainedComponent(props);
};
function MakeJournalEntriesTable({
formik,
accounts,
onClickRemoveRow,
onClickAddNewRow,
defaultRow,
}) {
const [selectAccountsState, setSelectedAccount] = useState(false);
// Account item of select accounts field.
const accountItem = (item, { handleClick, modifiers, query }) => {
return (<MenuItem text={item.name} label={item.code} key={item.id} onClick={handleClick} />)
};
// Filters accounts options by account name or code.
const filterAccountsPredicater = (query, account, _index, exactMatch) => {
const normalizedTitle = account.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return `${account.code} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
}
};
// Handle account field change.
const onChangeAccount = (index) => (account) => {
setSelectedAccount({
...selectAccountsState,
[index + 1]: account,
});
formik.setFieldValue(`entries[${index}].account_id`, account.id);
};
const [data, setData] = useState([
...formik.values.entries,
const [rows, setRow] = useState([
...formik.values.entries.map((e) => ({ ...e, rowType: 'editor'})),
]);
const updateData = (rowIndex, columnId, value) => {
setData((old) =>
// Handles update datatable data.
const handleUpdateData = useCallback((rowIndex, columnId, value) => {
setRow((old) =>
old.map((row, index) => {
if (index === rowIndex) {
return {
@@ -60,51 +93,18 @@ function MakeJournalEntriesTable({
return row
})
)
}
}, []);
const CellInputGroupRenderer = ({
row: { index },
column: { id },
cell: { value: initialValue },
}) => {
const [state, setState] = useState(initialValue);
// Handles click remove datatable row.
const handleRemoveRow = useCallback((rowIndex) => {
const removeIndex = parseInt(rowIndex, 10);
setRow([
...rows.filter((row, index) => index !== removeIndex),
]);
onClickRemoveRow && onClickRemoveRow(removeIndex);
}, [rows, onClickRemoveRow]);
return (
<InputGroup
fill={true}
value={state}
onChange={(event) => {
setState(event.target.value);
}}
onBlur={(event) => {
formik.setFieldValue(`entries[${index}].[${id}]`, event.target.value);
updateData(index, id, state);
}}
/>);
}
const ActionsCellRenderer = ({
row: { index },
column: { id },
cell: { value: initialValue },
}) => {
const onClickRemoveRole = () => {
};
return (
<Button
icon={<Icon icon="mines" />}
iconSize={14}
className="ml2"
minimal={true}
intent={Intent.DANGER}
onClick={onClickRemoveRole()} />
);
}
// Memorized data table columns.
const columns = useMemo(() => [
{
Header: '#',
@@ -112,89 +112,93 @@ function MakeJournalEntriesTable({
Cell: ({ row: {index} }) => (
<span>{ index + 1 }</span>
),
className: "actions",
className: "index",
width: 40,
disableResizing: true,
disableSortBy: true,
},
{
Header: 'Account',
accessor: 'account',
Cell: ({ row: { index } }) => (
<FormGroup
className="{'form-group--account'}"
inline={true}>
<Select
items={accounts}
noResults={<MenuItem disabled={true} text="No results." />}
itemRenderer={accountItem}
itemPredicate={filterAccountsPredicater}
popoverProps={{ minimal: true }}
onItemSelect={onChangeAccount(index)}>
<Button
rightIcon="caret-down"
text={(selectAccountsState[(index + 1)])
? selectAccountsState[(index + 1)].name : "Select Account"}
/>
</Select>
</FormGroup>
),
Cell: TotalAccountCellRenderer(AccountsListFieldCell),
className: "account",
disableSortBy: true,
disableResizing: true,
width: 250,
},
{
Header: 'Credit (USD)',
accessor: 'credit',
Cell: TotalCreditDebitCellRenderer(MoneyFieldCell, 'credit'),
className: "credit",
disableSortBy: true,
disableResizing: true,
width: 150,
},
{
Header: 'Debit (USD)',
accessor: 'debit',
Cell: TotalCreditDebitCellRenderer(MoneyFieldCell, 'debit'),
className: "debit",
disableSortBy: true,
disableResizing: true,
width: 150,
},
{
Header: 'Note',
accessor: 'note',
Cell: CellInputGroupRenderer,
Cell: NoteCellRenderer(InputGroupCell),
disableSortBy: true,
className: "note",
},
{
Header: 'Credit',
accessor: 'credit',
Cell: CellInputGroupRenderer,
className: "credit",
},
{
Header: 'Debit',
accessor: 'debit',
Cell: CellInputGroupRenderer,
className: "debit",
},
{
Header: '',
accessor: 'action',
Cell: ActionsCellRenderer,
className: "actions",
disableSortBy: true,
disableResizing: true,
width: 45,
}
]);
], []);
const onClickNewRow = () => {
setData([
...data,
{
credit: 0,
debit: 0,
account_id: null,
note: '',
}
// Handles click new line.
const onClickNewRow = useCallback(() => {
setRow([
...rows,
{ ...defaultRow, rowType: 'editor' },
]);
}
onClickAddNewRow && onClickAddNewRow();
}, [defaultRow, rows, onClickAddNewRow]);
const rowClassNames = useCallback((row) => ({
'row--total': rows.length === (row.index + 2),
}), [rows]);
return (
<div class="make-journal-entries__table">
<DataTable
columns={columns}
data={data} />
data={rows}
rowClassNames={rowClassNames}
payload={{
accounts,
updateData: handleUpdateData,
removeRow: handleRemoveRow,
}}/>
<div class="mt1">
<Button
minimal={true}
intent={Intent.PRIMARY}
small={true}
className={'button--secondary button--new-line'}
onClick={onClickNewRow}>
+ New Entry
New lines
</Button>
<Button
minimal={true}
intent={Intent.PRIMARY}
small={true}
className={'button--secondary button--clear-lines ml1'}
onClick={onClickNewRow}>
- Clear all entries
Clear all lines
</Button>
</div>
</div>

View File

@@ -46,7 +46,7 @@ export default function BalanceSheetHeader({
formik.setFieldValue('display_columns_by', item.by);
}, []);
// handle submit filter submit button.
// Handle submit filter submit button.
const handleSubmitClick = useCallback(() => {
formik.submitForm();
}, [formik]);