From 985ac3f235d0f0f14bde780224f3f67711d56d54 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Thu, 2 Jul 2020 02:50:57 +0200 Subject: [PATCH] feat: Control selected account from selectedAccountId prop. feat: Allow to reset form of manual journal and expense. --- client/src/components/AccountsSelectList.js | 28 +- .../DataTableCells/AccountsListFieldCell.js | 10 +- client/src/components/index.js | 2 + .../Accounting/MakeJournalEntriesForm.js | 70 +++-- .../Accounting/MakeJournalEntriesHeader.js | 5 +- .../Accounting/MakeJournalEntriesTable.js | 136 ++++----- .../src/containers/Accounts/AccountsChart.js | 1 + .../containers/Dialogs/AccountFormDialog.js | 12 +- .../containers/Expenses/ExpenseDataTable.js | 14 +- client/src/containers/Expenses/ExpenseForm.js | 23 +- .../containers/Expenses/ExpenseFormHeader.js | 30 +- .../src/containers/Expenses/ExpenseTable.js | 276 +++++++++--------- client/src/lang/en/index.js | 3 +- client/src/store/accounts/accounts.actions.js | 1 - client/src/utils.js | 32 +- server/src/http/controllers/Accounts.js | 2 +- 16 files changed, 366 insertions(+), 279 deletions(-) diff --git a/client/src/components/AccountsSelectList.js b/client/src/components/AccountsSelectList.js index e1bc9de96..57a78c856 100644 --- a/client/src/components/AccountsSelectList.js +++ b/client/src/components/AccountsSelectList.js @@ -1,17 +1,35 @@ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useState, useEffect, useMemo } from 'react'; import { MenuItem, Button } from '@blueprintjs/core'; import { Select } from '@blueprintjs/select'; +import { FormattedMessage as T } from 'react-intl'; export default function AccountsSelectList({ accounts, onAccountSelected, error = [], - initialAccount, - defautlSelectText = 'Select account', + initialAccountId, + selectedAccountId, + defaultSelectText = 'Select account', }) { + // Find initial account object to set it as default account in initial render. + const initialAccount = useMemo( + () => accounts.find((a) => a.id === initialAccountId), + [initialAccountId], + ); + const [selectedAccount, setSelectedAccount] = useState( initialAccount || null, ); + + useEffect(() => { + if (typeof selectedAccountId !== 'undefined') { + const account = selectedAccountId + ? accounts.find((a) => a.id === selectedAccountId) + : null; + setSelectedAccount(account); + } + }, [selectedAccountId, accounts, setSelectedAccount]); + // Account item of select accounts field. const accountItem = useCallback((item, { handleClick, modifiers, query }) => { return ( @@ -52,7 +70,7 @@ export default function AccountsSelectList({ return ( ); diff --git a/client/src/components/DataTableCells/AccountsListFieldCell.js b/client/src/components/DataTableCells/AccountsListFieldCell.js index 6cee2c770..c4c5e33a5 100644 --- a/client/src/components/DataTableCells/AccountsListFieldCell.js +++ b/client/src/components/DataTableCells/AccountsListFieldCell.js @@ -9,7 +9,7 @@ import { // Account cell renderer. const AccountCellRenderer = ({ - column: { id, value }, + column: { id }, row: { index, original }, cell: { value: initialValue }, payload: { accounts, updateData, errors }, @@ -20,9 +20,9 @@ const AccountCellRenderer = ({ const { account_id = false } = (errors[index] || {}); - const initialAccount = useMemo(() => - accounts.find(a => a.id === initialValue), - [accounts, initialValue]); + // const initialAccount = useMemo(() => + // accounts.find(a => a.id === initialValue), + // [accounts, initialValue]); return ( + selectedAccountId={initialValue} /> ); }; diff --git a/client/src/components/index.js b/client/src/components/index.js index fbdd3b084..7db273f77 100644 --- a/client/src/components/index.js +++ b/client/src/components/index.js @@ -18,6 +18,7 @@ import FieldRequiredHint from './FieldRequiredHint'; import Dialog from './Dialog'; import AppToaster from './AppToaster'; import DataTable from './DataTable'; +import AccountsSelectList from './AccountsSelectList'; const Hint = FieldHint; @@ -43,4 +44,5 @@ export { Dialog, AppToaster, DataTable, + AccountsSelectList, }; \ No newline at end of file diff --git a/client/src/containers/Accounting/MakeJournalEntriesForm.js b/client/src/containers/Accounting/MakeJournalEntriesForm.js index 2d1a9ff2c..3afe8950e 100644 --- a/client/src/containers/Accounting/MakeJournalEntriesForm.js +++ b/client/src/containers/Accounting/MakeJournalEntriesForm.js @@ -26,7 +26,15 @@ import Dragzone from 'components/Dragzone'; import withMediaActions from 'containers/Media/withMediaActions'; import useMedia from 'hooks/useMedia'; -import { compose } from 'utils'; +import { compose, repeatValue } from 'utils'; + +const ERROR = { + JOURNAL_NUMBER_ALREADY_EXISTS: 'JOURNAL.NUMBER.ALREADY.EXISTS', + CUSTOMERS_NOT_WITH_RECEVIABLE_ACC: 'CUSTOMERS.NOT.WITH.RECEIVABLE.ACCOUNT', + VENDORS_NOT_WITH_PAYABLE_ACCOUNT: 'VENDORS.NOT.WITH.PAYABLE.ACCOUNT', + PAYABLE_ENTRIES_HAS_NO_VENDORS: 'PAYABLE.ENTRIES.HAS.NO.VENDORS', + RECEIVABLE_ENTRIES_HAS_NO_CUSTOMERS: 'RECEIVABLE.ENTRIES.HAS.NO.CUSTOMERS', +}; /** * Journal entries form. @@ -139,7 +147,7 @@ function MakeJournalEntriesForm({ date: moment(new Date()).format('YYYY-MM-DD'), description: '', reference: '', - entries: [defaultEntry, defaultEntry, defaultEntry, defaultEntry], + entries: [...repeatValue(defaultEntry, 4)], }), [defaultEntry], ); @@ -171,44 +179,50 @@ function MakeJournalEntriesForm({ : []; }, [manualJournal]); + // Transform API errors in toasts messages. const transformErrors = (errors, { setErrors }) => { const hasError = (errorType) => errors.some((e) => e.type === errorType); - if (hasError('CUSTOMERS.NOT.WITH.RECEIVABLE.ACCOUNT')) { + if (hasError(ERROR.CUSTOMERS_NOT_WITH_RECEVIABLE_ACC)) { AppToaster.show({ - message: - 'customers_should_assign_with_receivable_account_only', + message: formatMessage({ + id: 'customers_should_assign_with_receivable_account_only', + }), intent: Intent.DANGER, }); } - if (hasError('VENDORS.NOT.WITH.PAYABLE.ACCOUNT')) { + if (hasError(ERROR.PAYABLE_ENTRIES_HAS_NO_VENDORS)) { AppToaster.show({ - message: 'vendors_should_assign_with_payable_account_only', + message: formatMessage({ + id: 'vendors_should_assign_with_payable_account_only', + }), intent: Intent.DANGER, }); } - if (hasError('RECEIVABLE.ENTRIES.HAS.NO.CUSTOMERS')) { + if (hasError(ERROR.RECEIVABLE_ENTRIES_HAS_NO_CUSTOMERS)) { AppToaster.show({ - message: - 'entries_with_receivable_account_no_assigned_with_customers', - intent: Intent.DANGER, - }); - } - if (hasError('PAYABLE.ENTRIES.HAS.NO.VENDORS')) { - AppToaster.show({ - message: - 'entries_with_payable_account_no_assigned_with_vendors', + message: formatMessage({ + id: 'entries_with_receivable_account_no_assigned_with_customers', + }), intent: Intent.DANGER, }); } - if (hasError('JOURNAL.NUMBER.ALREADY.EXISTS')) { + if (hasError(ERROR.PAYABLE_ENTRIES_HAS_NO_VENDORS)) { + AppToaster.show({ + message: formatMessage({ + id: 'entries_with_payable_account_no_assigned_with_vendors', + }), + intent: Intent.DANGER, + }); + } + if (hasError(ERROR.JOURNAL_NUMBER_ALREADY_EXISTS)) { setErrors({ journal_number: formatMessage({ id: 'journal_number_is_already_used', }), }); } - } + }; const formik = useFormik({ enableReinitialize: true, @@ -313,6 +327,7 @@ function MakeJournalEntriesForm({ const handleSubmitClick = useCallback( (payload) => { setPayload(payload); + // formik.resetForm(); formik.handleSubmit(); }, [setPayload, formik], @@ -336,15 +351,30 @@ function MakeJournalEntriesForm({ [setDeletedFiles, deletedFiles], ); + // Handle click on add a new line/row. + const handleClickAddNewRow = () => { + formik.setFieldValue('entries', [...formik.values.entries, defaultEntry]); + }; + + // Handle click `Clear all lines` button. + const handleClickClearLines = () => { + formik.setFieldValue( + 'entries', + reorderingEntriesIndex([...repeatValue(defaultEntry, 4)]), + ); + }; + return (
+ + } + position={Position.LEFT_BOTTOM} + /> + + ); +} + // Actions cell renderer. const ActionsCellRenderer = ({ row: { index }, @@ -87,64 +100,22 @@ function MakeJournalEntriesTable({ // #ownPorps onClickRemoveRow, onClickAddNewRow, + onClickClearAllLines, defaultRow, - initialValues, - formik: { errors, values, setFieldValue }, + values, + formik: { errors, setFieldValue }, }) { - const [rows, setRow] = useState([]); + const [rows, setRows] = useState([]); const { formatMessage } = useIntl(); + useEffect(() => { - setRow([ - ...initialValues.entries.map((e) => ({ ...e, rowType: 'editor' })), - defaultRow, - defaultRow, - ]); - }, [initialValues, defaultRow]); + setRows([...values.entries.map((e) => ({ ...e, rowType: 'editor' }))]); + }, [values, setRows]); - // Handles update datatable data. - const handleUpdateData = useCallback( - (rowIndex, columnIdOrBulk, value) => { - const columnId = typeof columnIdOrBulk !== 'object' - ? columnIdOrBulk : null; - - const updateTable = typeof columnIdOrBulk === 'object' - ? columnIdOrBulk : null; - - const newData = updateTable ? updateTable : { [columnId]: value }; - - const newRows = rows.map((row, index) => { - if (index === rowIndex) { - return { ...rows[rowIndex], ...newData }; - } - return { ...row }; - }); - setRow(newRows); - setFieldValue( - 'entries', - newRows.map((row) => ({ - ...omit(row, ['rowType']), - })), - ); - }, - [rows, setFieldValue], - ); - - // Handles click remove datatable row. - const handleRemoveRow = useCallback( - (rowIndex) => { - const removeIndex = parseInt(rowIndex, 10); - const newRows = rows.filter((row, index) => index !== removeIndex); - - setRow([...newRows]); - setFieldValue( - 'entries', - newRows - .filter((row) => row.rowType === 'editor') - .map((row) => ({ ...omit(row, ['rowType']) })), - ); - onClickRemoveRow && onClickRemoveRow(removeIndex); - }, - [rows, setFieldValue, onClickRemoveRow], + // Final table rows editor rows and total and final blank row. + const tableRows = useMemo( + () => [...rows, { rowType: 'total' }, { rowType: 'final_space' }], + [rows], ); // Memorized data table columns. @@ -189,15 +160,7 @@ function MakeJournalEntriesTable({ width: 150, }, { - Header: ( - <> - - } - position={Position.LEFT_BOTTOM} - /> - - ), + Header: ContactHeaderCell, id: 'contact_id', accessor: 'contact_id', Cell: NoteCellRenderer(ContactsListFieldCell), @@ -228,10 +191,47 @@ function MakeJournalEntriesTable({ // Handles click new line. const onClickNewRow = useCallback(() => { - setRow([...rows, { ...defaultRow, rowType: 'editor' }]); onClickAddNewRow && onClickAddNewRow(); }, [defaultRow, rows, onClickAddNewRow]); + // Handles update datatable data. + const handleUpdateData = useCallback( + (rowIndex, columnIdOrObj, value) => { + const newRows = transformUpdatedRows( + rows, + rowIndex, + columnIdOrObj, + value, + ); + setFieldValue( + 'entries', + newRows + .filter((row) => row.rowType === 'editor') + .map((row) => ({ + ...omit(row, ['rowType']), + })), + ); + }, + [rows, setFieldValue], + ); + + const handleRemoveRow = useCallback( + (rowIndex) => { + const removeIndex = parseInt(rowIndex, 10); + const newRows = rows.filter((row, index) => index !== removeIndex); + + setFieldValue( + 'entries', + newRows + .filter((row) => row.rowType === 'editor') + .map((row) => ({ ...omit(row, ['rowType']) })), + ); + onClickRemoveRow && onClickRemoveRow(removeIndex); + }, + [rows, setFieldValue, onClickRemoveRow], + ); + + // Rows class names callback. const rowClassNames = useCallback( (row) => ({ 'row--total': rows.length === row.index + 2, @@ -239,11 +239,15 @@ function MakeJournalEntriesTable({ [rows], ); + const handleClickClearAllLines = () => { + onClickClearAllLines && onClickClearAllLines(); + }; + return (
diff --git a/client/src/containers/Accounts/AccountsChart.js b/client/src/containers/Accounts/AccountsChart.js index bb99b7825..fd813a124 100644 --- a/client/src/containers/Accounts/AccountsChart.js +++ b/client/src/containers/Accounts/AccountsChart.js @@ -106,6 +106,7 @@ function AccountsChart({ message: formatMessage({ id: 'cannot_delete_account_has_associated_transactions', }), + intent: Intent.DANGER, }); } }; diff --git a/client/src/containers/Dialogs/AccountFormDialog.js b/client/src/containers/Dialogs/AccountFormDialog.js index f9a5d0bd0..670349962 100644 --- a/client/src/containers/Dialogs/AccountFormDialog.js +++ b/client/src/containers/Dialogs/AccountFormDialog.js @@ -63,7 +63,7 @@ function AccountFormDialog({ .required() .label(formatMessage({ id: 'account_type_id' })), description: Yup.string().nullable().trim(), - // parent_account_id: Yup.string().nullable(), + parent_account_id: Yup.string().nullable(), }); const initialValues = useMemo( () => ({ @@ -97,11 +97,8 @@ function AccountFormDialog({ } = useFormik({ enableReinitialize: true, initialValues: { - // ...initialValues, - // ...(payload.action === 'edit' && account ? account : initialValues), - - ...(payload.action === 'edit' && - pick(account, Object.keys(initialValues))), + ...initialValues, + ...(payload.action === 'edit' && pick(account, Object.keys(initialValues))), }, validationSchema: accountFormValidationSchema, onSubmit: (values, { setSubmitting, setErrors }) => { @@ -114,7 +111,7 @@ function AccountFormDialog({ requestEditAccount(payload.id, values) .then((response) => { closeDialog(dialogName); - queryCache.invalidateQueries('accounts-table', { force: true }); + queryCache.invalidateQueries('accounts-table'); AppToaster.show({ message: formatMessage( @@ -154,7 +151,6 @@ function AccountFormDialog({ }); }) .catch((errors) => { - debugger; const errorsTransformed = transformApiErrors(errors); setErrors({ ...errorsTransformed }); setSubmitting(false); diff --git a/client/src/containers/Expenses/ExpenseDataTable.js b/client/src/containers/Expenses/ExpenseDataTable.js index 721ec1922..3f1ffb325 100644 --- a/client/src/containers/Expenses/ExpenseDataTable.js +++ b/client/src/containers/Expenses/ExpenseDataTable.js @@ -155,21 +155,13 @@ function ExpensesDataTable({ id: 'payment_date', Header: formatMessage({ id: 'payment_date' }), accessor: () => moment().format('YYYY MMM DD'), - width: 150, + width: 140, className: 'payment_date', }, - { - id: 'beneficiary', - Header: formatMessage({ id: 'beneficiary' }), - // accessor: 'beneficiary', - width: 150, - className: 'beneficiary', - }, { id: 'total_amount', Header: formatMessage({ id: 'full_amount' }), accessor: (r) => , - disableResizing: true, className: 'total_amount', width: 150, }, @@ -184,7 +176,7 @@ function ExpensesDataTable({ id: 'expense_account_id', Header: formatMessage({ id: 'expense_account' }), accessor: expenseAccountAccessor, - width: 150, + width: 160, className: 'expense_account', }, { @@ -201,7 +193,6 @@ function ExpensesDataTable({ ); }, - disableResizing: true, width: 100, className: 'publish', }, @@ -237,6 +228,7 @@ function ExpensesDataTable({ ), className: 'actions', width: 50, + disableResizing: true, }, ], [actionMenuList, formatMessage], diff --git a/client/src/containers/Expenses/ExpenseForm.js b/client/src/containers/Expenses/ExpenseForm.js index 6bcb28760..a0351a219 100644 --- a/client/src/containers/Expenses/ExpenseForm.js +++ b/client/src/containers/Expenses/ExpenseForm.js @@ -267,7 +267,7 @@ function ExpenseForm({ const handleSubmitClick = useCallback( (payload) => { setPayload(payload); - formik.handleSubmit(); + formik.resetForm(); }, [setPayload, formik], ); @@ -290,13 +290,30 @@ function ExpenseForm({ [setDeletedFiles, deletedFiles], ); + // Handle click on add a new line/row. + const handleClickAddNewRow = () => { + formik.setFieldValue( + 'categories', + orderingCategoriesIndex([...formik.values.categories, defaultCategory]), + ); + }; + + const handleClearAllLines = () => { + formik.setFieldValue( + 'categories', + orderingCategoriesIndex([defaultCategory, defaultCategory, defaultCategory, defaultCategory]), + ); + } + return ( -
+
diff --git a/client/src/containers/Expenses/ExpenseFormHeader.js b/client/src/containers/Expenses/ExpenseFormHeader.js index 1afdea739..9e015855a 100644 --- a/client/src/containers/Expenses/ExpenseFormHeader.js +++ b/client/src/containers/Expenses/ExpenseFormHeader.js @@ -11,13 +11,12 @@ import { DateInput } from '@blueprintjs/datetime'; import { FormattedMessage as T } from 'react-intl'; import { Row, Col } from 'react-grid-system'; import moment from 'moment'; -import { momentFormatter, compose } from 'utils'; - +import { momentFormatter, compose, tansformDateValue } from 'utils'; import classNames from 'classnames'; import { ListSelect, ErrorMessage, - Icon, + AccountsSelectList, FieldRequiredHint, Hint, } from 'components'; @@ -118,12 +117,15 @@ function ExpenseFormHeader({ } + label={} className={classNames('form-group--select-list', Classes.FILL)} labelInfo={} intent={errors.beneficiary && touched.beneficiary && Intent.DANGER} helperText={ - + } > } > - } - itemRenderer={accountItem} - itemPredicate={filterAccountsPredicater} - popoverProps={{ minimal: true }} - onItemSelect={onChangeAccount} - selectedItem={values.payment_account_id} - selectedItemProp={'id'} - defaultText={} - labelProp={'name'} + } + selectedAccountId={values.payment_account_id} /> @@ -193,7 +189,7 @@ function ExpenseFormHeader({ > diff --git a/client/src/containers/Expenses/ExpenseTable.js b/client/src/containers/Expenses/ExpenseTable.js index 162dfb9f3..51dee5071 100644 --- a/client/src/containers/Expenses/ExpenseTable.js +++ b/client/src/containers/Expenses/ExpenseTable.js @@ -1,19 +1,90 @@ import React, { useState, useMemo, useEffect, useCallback } from 'react'; import { Button, Intent, Position, Tooltip } from '@blueprintjs/core'; import { FormattedMessage as T, useIntl } from 'react-intl'; +import { omit } from 'lodash'; import DataTable from 'components/DataTable'; import Icon from 'components/Icon'; import { Hint } from 'components'; -import { compose, formattedAmount } from 'utils'; +import { compose, formattedAmount, transformUpdatedRows } from 'utils'; import { AccountsListFieldCell, MoneyFieldCell, InputGroupCell, } from 'components/DataTableCells'; -import { omit } from 'lodash'; import withAccounts from 'containers/Accounts/withAccounts'; + +const ExpenseCategoryHeaderCell = () => { + return ( + <> + + + + ); +} + + // Actions cell renderer. + const ActionsCellRenderer = ({ + row: { index }, + column: { id }, + cell: { value: initialValue }, + data, + payload, +}) => { + if (data.length <= index + 1) { + return ''; + } + const onClickRemoveRole = () => { + payload.removeRow(index); + }; + return ( + } position={Position.LEFT}> + diff --git a/client/src/lang/en/index.js b/client/src/lang/en/index.js index 06e996664..ed97347c1 100644 --- a/client/src/lang/en/index.js +++ b/client/src/lang/en/index.js @@ -181,7 +181,7 @@ export default { you_could_not_delete_predefined_accounts: "You could't delete predefined accounts.", cannot_delete_account_has_associated_transactions: - "you could't not delete account that has associated transactions.", + "You could't not delete account that has associated transactions.", the_account_has_been_successfully_inactivated: 'The account has been successfully inactivated.', the_account_has_been_successfully_activated: @@ -527,4 +527,5 @@ export default { account_code_hint: 'A unique code/number for this account (limited to 10 characters)', logic_expression: 'logic expression', + assign_to_customer: 'Assign to Customer', }; diff --git a/client/src/store/accounts/accounts.actions.js b/client/src/store/accounts/accounts.actions.js index 65ff7f07d..2c8778cf8 100644 --- a/client/src/store/accounts/accounts.actions.js +++ b/client/src/store/accounts/accounts.actions.js @@ -168,7 +168,6 @@ export const editAccount = (id, form) => { .catch((error) => { const { response } = error; const { data } = response; - // const { errors } = data; dispatch({ type: t.CLEAR_ACCOUNT_FORM_ERRORS }); if (error) { diff --git a/client/src/utils.js b/client/src/utils.js index daf69213c..2135f7d15 100644 --- a/client/src/utils.js +++ b/client/src/utils.js @@ -188,4 +188,34 @@ export const uniqueMultiProps = (items, props) => { return _.uniqBy(items, (item) => { return JSON.stringify(_.pick(item, props)); }); -} \ No newline at end of file +} + + +export const transformUpdatedRows = (rows, rowIndex, columnIdOrObj, value) => { + const columnId = + typeof columnIdOrObj !== 'object' ? columnIdOrObj : null; + + const updateTable = + typeof columnIdOrObj === 'object' ? columnIdOrObj : null; + + const newData = updateTable ? updateTable : { [columnId]: value }; + + return rows.map((row, index) => { + if (index === rowIndex) { + return { ...rows[rowIndex], ...newData }; + } + return { ...row }; + }); +} + +export const tansformDateValue = (date) => { + return moment(date).toDate() || new Date(); +}; + +export const repeatValue = (value, len) => { + var arr = []; + for (var i = 0; i < len; i++) { + arr.push(value); + } + return arr; +}; \ No newline at end of file diff --git a/server/src/http/controllers/Accounts.js b/server/src/http/controllers/Accounts.js index 5f127381c..c19539657 100644 --- a/server/src/http/controllers/Accounts.js +++ b/server/src/http/controllers/Accounts.js @@ -175,7 +175,7 @@ export default { } if (errorReasons.length > 0) { - return res.status(400).send({ error: errorReasons }); + return res.status(400).send({ errors: errorReasons }); } // Update the account on the storage. const updatedAccount = await Account.query().patchAndFetchById(account.id, { ...form });