diff --git a/client/src/components/AccountsSelectList.js b/client/src/components/AccountsSelectList.js index b326184f2..f826d33c7 100644 --- a/client/src/components/AccountsSelectList.js +++ b/client/src/components/AccountsSelectList.js @@ -1,29 +1,23 @@ -import React, {useMemo, useCallback, useState} from 'react'; -import {omit} from 'lodash'; +import React, {useCallback, useState} from 'react'; import { MenuItem, - FormGroup, Button, - Intent, } from '@blueprintjs/core'; import {Select} from '@blueprintjs/select'; -// import MultiSelect from 'components/MultiSelect'; export default function AccountsMultiSelect({ accounts, onAccountSelected, error, + initialAccount, }) { - const [selectedAccount, setSelectedAccount] = useState(null); - + const [selectedAccount, setSelectedAccount] = useState( + initialAccount || null + ); // Account item of select accounts field. const accountItem = useCallback((item, { handleClick, modifiers, query }) => { return ( - + ); }, []); @@ -33,20 +27,17 @@ export default function AccountsMultiSelect({ }, [setSelectedAccount, onAccountSelected]); return ( - - ); } \ No newline at end of file diff --git a/client/src/components/DataTableCells/AccountsListFieldCell.js b/client/src/components/DataTableCells/AccountsListFieldCell.js index 70b2d1d2e..6cee2c770 100644 --- a/client/src/components/DataTableCells/AccountsListFieldCell.js +++ b/client/src/components/DataTableCells/AccountsListFieldCell.js @@ -1,4 +1,4 @@ -import React, {useCallback} from 'react'; +import React, {useCallback, useMemo} from 'react'; import AccountsSelectList from 'components/AccountsSelectList'; import classNames from 'classnames'; import { @@ -20,6 +20,10 @@ const AccountCellRenderer = ({ const { account_id = false } = (errors[index] || {}); + const initialAccount = useMemo(() => + accounts.find(a => a.id === initialValue), + [accounts, initialValue]); + return ( + error={account_id} + initialAccount={initialAccount} /> ); }; diff --git a/client/src/components/MoneyInputGroup.js b/client/src/components/MoneyInputGroup.js index adefb2913..97812135c 100644 --- a/client/src/components/MoneyInputGroup.js +++ b/client/src/components/MoneyInputGroup.js @@ -64,7 +64,9 @@ export default function MoneyFieldGroup({ const options = useMemo(() => ({ prefix, suffix, thousands, decimal, precision, - }), []); + }), [ + prefix, suffix, thousands, decimal, precision, + ]); const handleChange = useCallback((event) => { const formatted = formatter(event.target.value, options); @@ -72,12 +74,12 @@ export default function MoneyFieldGroup({ setState(formatted); onChange && onChange(event, value); - }, []); + }, [onChange, options]); useEffect(() => { const formatted = formatter(value, options); setState(formatted) - }, []); + }, [value, options, setState]); return ( ({ - + getManualJournal: (id) => getManualJournal(state, id), }); export const mapDispatchToProps = (dispatch) => ({ - makeJournalEntries: (form) => dispatch(makeJournalEntries({ form })), + requestMakeJournalEntries: (form) => dispatch(makeJournalEntries({ form })), + fetchManualJournal: (id) => dispatch(fetchManualJournal({ id })), + requestEditManualJournal: (id, form) => dispatch(editManualJournal({ id, form })) }); export default connect(mapStateToProps, mapDispatchToProps); \ No newline at end of file diff --git a/client/src/containers/Dashboard/Accounting/MakeJournalEntriesForm.js b/client/src/containers/Dashboard/Accounting/MakeJournalEntriesForm.js index e56efab7f..c32079c79 100644 --- a/client/src/containers/Dashboard/Accounting/MakeJournalEntriesForm.js +++ b/client/src/containers/Dashboard/Accounting/MakeJournalEntriesForm.js @@ -13,21 +13,23 @@ import {compose} from 'utils'; import useAsync from 'hooks/async'; import moment from 'moment'; import AppToaster from 'components/AppToaster'; +import {pick, omit} from 'lodash'; function MakeJournalEntriesForm({ - makeJournalEntries, - fetchAccounts, + requestMakeJournalEntries, + requestEditManualJournal, changePageTitle, + changePageSubtitle, + editJournal, }) { useEffect(() => { - changePageTitle('New Journal'); - }, []); - - const fetchHook = useAsync(async () => { - await Promise.all([ - fetchAccounts(), - ]); - }); + if (editJournal && editJournal.id) { + changePageTitle('Edit Journal'); + changePageSubtitle(`No. ${editJournal.journal_number}`); + } else { + changePageTitle('New Journal'); + } + }, [changePageTitle, changePageSubtitle, editJournal]); const validationSchema = Yup.object().shape({ journal_number: Yup.string().required(), @@ -54,22 +56,31 @@ function MakeJournalEntriesForm({ note: '', }), []); + const initialValues = useMemo(() => ({ + journal_number: '', + date: moment(new Date()).format('YYYY-MM-DD'), + description: '', + reference: '', + entries: [ + defaultEntry, + defaultEntry, + defaultEntry, + defaultEntry, + ], + }), [defaultEntry]); + const formik = useFormik({ enableReinitialize: true, validationSchema, initialValues: { - journal_number: '', - date: moment(new Date()).format('YYYY-MM-DD'), - description: '', - reference: '', - entries: [ - defaultEntry, - defaultEntry, - defaultEntry, - defaultEntry, - defaultEntry, - defaultEntry, - ], + ...(editJournal) ? { + ...pick(editJournal, Object.keys(initialValues)), + entries: editJournal.entries.map((entry) => ({ + ...pick(entry, Object.keys(defaultEntry)), + })) + } : { + ...initialValues, + } }, onSubmit: (values, actions) => { const form = values.entries.filter((entry) => ( @@ -87,18 +98,31 @@ function MakeJournalEntriesForm({ AppToaster.show({ message: 'credit_and_debit_not_equal', }); + actions.setSubmitting(false); return; } - - makeJournalEntries({ ...values, entries: form }) - .then((response) => { - AppToaster.show({ - message: 'manual_journal_has_been_submit', - }); - actions.setSubmitting(false); - }).catch((error) => { - actions.setSubmitting(false); - }); + + if (editJournal && editJournal.id) { + requestEditManualJournal(editJournal.id, { ...values, entries: form }) + .then((response) => { + AppToaster.show({ + message: 'manual_journal_has_been_edited', + }); + actions.setSubmitting(false); + }).catch((error) => { + actions.setSubmitting(false); + }); + } else { + requestMakeJournalEntries({ ...values, entries: form }) + .then((response) => { + AppToaster.show({ + message: 'manual_journal_has_been_submit', + }); + actions.setSubmitting(false); + }).catch((error) => { + actions.setSubmitting(false); + }); + } }, }); diff --git a/client/src/containers/Dashboard/Accounting/MakeJournalEntriesHeader.js b/client/src/containers/Dashboard/Accounting/MakeJournalEntriesHeader.js index bf8ba92d9..761c8f696 100644 --- a/client/src/containers/Dashboard/Accounting/MakeJournalEntriesHeader.js +++ b/client/src/containers/Dashboard/Accounting/MakeJournalEntriesHeader.js @@ -21,12 +21,13 @@ export default function MakeJournalEntriesHeader({ }) { const intl = useIntl(); - const handleDateChange = (date) => { + const handleDateChange = useCallback((date) => { const formatted = moment(date).format('YYYY-MM-DD'); formik.setFieldValue('date', formatted); - }; + }, [formik]); - const infoIcon = useMemo(() => (), []); + const infoIcon = useMemo(() => + (), []); return (
diff --git a/client/src/containers/Dashboard/Accounting/MakeJournalEntriesPage.js b/client/src/containers/Dashboard/Accounting/MakeJournalEntriesPage.js index 0359d7028..5d05a85ce 100644 --- a/client/src/containers/Dashboard/Accounting/MakeJournalEntriesPage.js +++ b/client/src/containers/Dashboard/Accounting/MakeJournalEntriesPage.js @@ -1,14 +1,39 @@ -import React from 'react'; +import React, {useMemo} from 'react'; +import { useParams } from 'react-router-dom'; +import { useAsync } from 'react-use'; import MakeJournalEntriesForm from './MakeJournalEntriesForm'; +import LoadingIndicator from 'components/LoadingIndicator'; import DashboardConnect from 'connectors/Dashboard.connector'; import {compose} from 'utils'; +import MakeJournalEntriesConnect from 'connectors/MakeJournalEntries.connect'; +import AccountsConnect from 'connectors/Accounts.connector'; + +function MakeJournalEntriesPage({ + fetchManualJournal, + getManualJournal, + fetchAccounts, +}) { + const { id } = useParams(); + + const fetchJournal = useAsync(() => { + return Promise.all([ + fetchAccounts(), + (id) && fetchManualJournal(id), + ]); + }); + const editJournal = useMemo(() => + getManualJournal(id) || null, + [getManualJournal, id]); -function MakeJournalEntriesPage() { return ( - + + + ); } export default compose( DashboardConnect, + AccountsConnect, + MakeJournalEntriesConnect, )(MakeJournalEntriesPage); \ No newline at end of file diff --git a/client/src/containers/Dashboard/Accounting/MakeJournalEntriesTable.js b/client/src/containers/Dashboard/Accounting/MakeJournalEntriesTable.js index e64a3aaf8..9cfdc041e 100644 --- a/client/src/containers/Dashboard/Accounting/MakeJournalEntriesTable.js +++ b/client/src/containers/Dashboard/Accounting/MakeJournalEntriesTable.js @@ -70,6 +70,9 @@ const NoteCellRenderer = (chainedComponent) => (props) => { return chainedComponent(props); }; +/** + * Make journal entries table component. + */ function MakeJournalEntriesTable({ formik, accounts, @@ -79,6 +82,8 @@ function MakeJournalEntriesTable({ }) { const [rows, setRow] = useState([ ...formik.values.entries.map((e) => ({ ...e, rowType: 'editor'})), + defaultRow, + defaultRow, ]); // Handles update datatable data. @@ -101,11 +106,15 @@ function MakeJournalEntriesTable({ // Handles click remove datatable row. const handleRemoveRow = useCallback((rowIndex) => { const removeIndex = parseInt(rowIndex, 10); - setRow([ - ...rows.filter((row, index) => index !== removeIndex), - ]); + const newRows = rows.filter((row, index) => index !== removeIndex); + + setRow([ ...newRows ]); + formik.setFieldValue('entries', newRows + .filter(row => row.rowType === 'editor') + .map(row => ({ ...omit(row, ['rowType']) }) + )); onClickRemoveRow && onClickRemoveRow(removeIndex); - }, [rows, onClickRemoveRow]); + }, [rows, formik, onClickRemoveRow]); // Memorized data table columns. const columns = useMemo(() => [ @@ -123,7 +132,7 @@ function MakeJournalEntriesTable({ { Header: 'Account', id: 'account_id', - accessor: 'account', + accessor: 'account_id', Cell: TotalAccountCellRenderer(AccountsListFieldCell), className: "account", disableSortBy: true, diff --git a/client/src/routes/dashboard.js b/client/src/routes/dashboard.js index 3ab430c4b..e97152b4f 100644 --- a/client/src/routes/dashboard.js +++ b/client/src/routes/dashboard.js @@ -66,6 +66,15 @@ export default [ text: 'Make Journal Entry' }, + { + path: `${BASE_URL}/accounting/manual-journal/:id`, + name: 'dashboard.manual.journal.edit', + component: LazyLoader({ + loader: () => + import('containers/Dashboard/Accounting/MakeJournalEntriesPage') + }), + }, + // Items { path: `${BASE_URL}/items/list`, diff --git a/client/src/store/accounting/accounting.actions.js b/client/src/store/accounting/accounting.actions.js index c985dbb8f..a2be95e35 100644 --- a/client/src/store/accounting/accounting.actions.js +++ b/client/src/store/accounting/accounting.actions.js @@ -7,4 +7,27 @@ export const makeJournalEntries = ({ form }) => { resolve(response); }).catch((error) => { reject(error); }); }); +}; + +export const fetchManualJournal = ({ id }) => { + return (dispatch) => new Promise((resolve, reject) => { + ApiService.get(`accounting/manual-journals/${id}`).then((response) => { + dispatch({ + type: t.MANUAL_JOURNAL_SET, + payload: { + id, + manualJournal: response.data.manual_journal, + } + }); + resolve(response); + }).catch((error) => { reject(error); }); + }); +}; + +export const editManualJournal = ({ form, id }) => { + return (dispatch) => new Promise((resolve, reject) => { + ApiService.post(`accounting/manual-journals/${id}`, form).then((response) => { + resolve(response); + }).catch((error) => { reject(error); }); + }); } \ No newline at end of file diff --git a/client/src/store/accounting/accounting.reducers.js b/client/src/store/accounting/accounting.reducers.js index e69de29bb..8d47a87cb 100644 --- a/client/src/store/accounting/accounting.reducers.js +++ b/client/src/store/accounting/accounting.reducers.js @@ -0,0 +1,19 @@ +import t from 'store/types'; +import { createReducer } from '@reduxjs/toolkit'; + +const initialState = { + manualJournals: {}, +}; + +export default createReducer(initialState, { + + [t.MANUAL_JOURNAL_SET]: (state, action) => { + const { id, manualJournal } = action.payload; + state.manualJournals[id] = manualJournal; + }, +}); + + +export const getManualJournal = (state, id) => { + return state.accounting.manualJournals[id]; +} \ No newline at end of file diff --git a/client/src/store/accounting/accountsing.types.js b/client/src/store/accounting/accounting.types.js similarity index 60% rename from client/src/store/accounting/accountsing.types.js rename to client/src/store/accounting/accounting.types.js index f10249128..84eae0701 100644 --- a/client/src/store/accounting/accountsing.types.js +++ b/client/src/store/accounting/accounting.types.js @@ -2,4 +2,5 @@ export default { MAKE_JOURNAL_ENTRIES: 'MAKE_JOURNAL_ENTRIES', + MANUAL_JOURNAL_SET: 'MANUAL_JOURNAL_SET', } \ No newline at end of file diff --git a/client/src/store/reducers.js b/client/src/store/reducers.js index d3efde8b0..e050e2d9f 100644 --- a/client/src/store/reducers.js +++ b/client/src/store/reducers.js @@ -13,12 +13,14 @@ import resources from './resources/resources.reducer'; import financialStatements from './financialStatement/financialStatements.reducer'; import itemCategories from './itemCategories/itemsCateory.reducer'; import settings from './settings/settings.reducer'; +import accounting from './accounting/accounting.reducers'; export default combineReducers({ authentication, dashboard, users, accounts, + accounting, fields, views, expenses, diff --git a/client/src/store/types.js b/client/src/store/types.js index 307f75f93..29adce107 100644 --- a/client/src/store/types.js +++ b/client/src/store/types.js @@ -1,5 +1,6 @@ import authentication from './authentication/authentication.types'; import accounts from './accounts/accounts.types'; +import accounting from './accounting/accounting.types' import currencies from './currencies/currencies.types'; import customFields from './customFields/customFields.types'; import customViews from './customViews/customViews.types'; @@ -27,5 +28,6 @@ export default { ...users, ...financialStatements, ...itemCategories, - ...settings + ...settings, + ...accounting, }; diff --git a/server/src/http/controllers/Accounting.js b/server/src/http/controllers/Accounting.js index 08adda0e0..8925028ef 100644 --- a/server/src/http/controllers/Accounting.js +++ b/server/src/http/controllers/Accounting.js @@ -25,6 +25,10 @@ export default { const router = express.Router(); router.use(JWTAuth); + router.get('/manual-journals/:id', + this.getManualJournal.validation, + asyncMiddleware(this.getManualJournal.handler)); + router.get('/manual-journals', this.manualJournals.validation, asyncMiddleware(this.manualJournals.handler)); @@ -33,7 +37,7 @@ export default { this.makeJournalEntries.validation, asyncMiddleware(this.makeJournalEntries.handler)); - router.post('/manual-journal/:id', + router.post('/manual-journals/:id', this.editManualJournal.validation, asyncMiddleware(this.editManualJournal.handler)); @@ -375,6 +379,26 @@ export default { journal.loadEntries(transactions); journal.removeEntries(); + entries.forEach((entry) => { + const account = accounts.find((a) => a.id === entry.account_id); + + const jouranlEntry = new JournalEntry({ + debit: entry.debit, + credit: entry.credit, + account: account.id, + referenceType: 'Journal', + referenceId: manualJournal.id, + accountNormal: account.type.normal, + note: entry.note, + date: formattedDate, + userId: user.id, + }); + if (entry.debit) { + journal.debit(jouranlEntry); + } else { + journal.credit(jouranlEntry); + } + }); await Promise.all([ journal.deleteEntries(), journal.saveEntries(), @@ -405,8 +429,19 @@ export default { return res.status(404).send({ errors: [{ type: 'MANUAL.JOURNAL.NOT.FOUND', code: 100 }], }); - } - + } + const transactions = await AccountTransaction.query() + .whereIn('reference_type', ['Journal', 'ManualJournal']) + .where('reference_id', manualJournal.id); + + return res.status(200).send({ + manual_journal: { + ...manualJournal.toJSON(), + entries: [ + ...transactions, + ], + }, + }); }, }, diff --git a/server/tests/routes/accounts.test.js b/server/tests/routes/accounts.test.js index d5b17c864..bb427d036 100644 --- a/server/tests/routes/accounts.test.js +++ b/server/tests/routes/accounts.test.js @@ -16,7 +16,6 @@ describe('routes: /accounts/', () => { .post('/api/accounts') .set('x-access-token', loginRes.body.token) .send(); - expect(res.status).equals(422); expect(res.body.code).equals('validation_error'); }); @@ -191,7 +190,6 @@ describe('routes: /accounts/', () => { }); describe('GET: `/accounts`', () => { - it('Should retrieve accounts resource not found.', async () => { const res = await request() .get('/api/accounts')