From 011542e2a3cd46f5483fb8c11847f3556907fa53 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sun, 29 Nov 2020 00:06:59 +0200 Subject: [PATCH] feat: expand sidebar once open form editor page. feat: rounding money amount. feat: optimize page form structure. feat: refactoring make journal and expense form with FastField component. --- client/jsconfig.json | 8 - client/package.json | 7 +- client/src/common/classes.js | 4 + client/src/components/AccountsSelectList.js | 4 +- client/src/components/CurrencySelectList.js | 5 +- client/src/components/Dashboard/Dashboard.js | 50 +- .../Dashboard/DashboardSplitePane.js | 2 +- .../components/Dashboard/DashboardTopbar.js | 16 +- .../DataTableCells/MoneyFieldCell.js | 20 +- .../DataTableCells/PercentFieldCell.js | 23 +- client/src/components/DataTableCells/index.js | 1 + client/src/components/Forms/Checkbox.tsx | 2 +- .../MoneyInputGroup/CurrencyInputProps.ts | 120 ++++ .../Forms/MoneyInputGroup/index.tsx | 190 +++++++ .../utils/__tests__/addSeparators.spec.ts | 11 + .../utils/__tests__/cleanValue.spec.ts | 220 ++++++++ .../utils/__tests__/fixedDecimalValue.spec.ts | 29 + .../utils/__tests__/formatValue.spec.ts | 172 ++++++ .../utils/__tests__/isNumber.spec.ts | 43 ++ .../utils/__tests__/padTrimValue.spec.ts | 38 ++ .../utils/__tests__/parseAbbrValue.spec.ts | 77 +++ .../__tests__/removeInvalidChars.spec.ts | 23 + .../utils/__tests__/removeSeparators.spec.ts | 11 + .../MoneyInputGroup/utils/addSeparators.ts | 6 + .../Forms/MoneyInputGroup/utils/cleanValue.ts | 66 +++ .../MoneyInputGroup/utils/escapeRegExp.ts | 8 + .../utils/fixedDecimalValue.ts | 27 + .../MoneyInputGroup/utils/formatValue.ts | 78 +++ .../Forms/MoneyInputGroup/utils/index.tsx | 5 + .../Forms/MoneyInputGroup/utils/isNumber.ts | 1 + .../MoneyInputGroup/utils/padTrimValue.ts | 22 + .../MoneyInputGroup/utils/parseAbbrValue.ts | 42 ++ .../utils/removeInvalidChars.ts | 10 + .../MoneyInputGroup/utils/removeSeparators.ts | 9 + client/src/components/Money.js | 2 +- client/src/components/MoneyInputGroup.js | 109 ---- client/src/components/PageFormBigNumber.js | 20 + client/src/components/Sidebar/SidebarMenu.js | 4 +- client/src/components/index.js | 4 +- client/src/config/sidebarMenu.js | 4 +- .../Accounting/MakeJournalEntries.schema.js | 39 ++ .../Accounting/MakeJournalEntriesField.js | 37 ++ .../Accounting/MakeJournalEntriesFooter.js | 58 -- .../Accounting/MakeJournalEntriesForm.js | 523 +++++------------- .../Accounting/MakeJournalEntriesHeader.js | 240 +------- .../MakeJournalEntriesHeaderFields.js | 188 +++++++ .../Accounting/MakeJournalEntriesPage.js | 18 +- .../Accounting/MakeJournalEntriesTable.js | 156 ++---- .../MakeJournalFormFloatingActions.js | 59 ++ .../Accounting/MakeJournalFormFooter.js | 42 ++ .../Accounting/MakeJournalNumberWatcher.js | 48 ++ .../src/containers/Accounting/components.js | 85 ++- client/src/containers/Accounting/utils.js | 81 +++ .../Dashboard/withDashboardActions.js | 21 +- .../containers/Entries/ItemsEntriesTable.js | 3 +- .../Expenses/ExpenseFloatingActions.js | 1 + client/src/containers/Expenses/ExpenseForm.js | 401 ++++---------- .../containers/Expenses/ExpenseForm.schema.js | 5 +- .../containers/Expenses/ExpenseFormBody.js | 12 + ...{ExpenseTable.js => ExpenseFormEntries.js} | 59 +- .../Expenses/ExpenseFormEntriesField.js | 21 + .../containers/Expenses/ExpenseFormFooter.js | 39 ++ .../containers/Expenses/ExpenseFormHeader.js | 234 +------- .../Expenses/ExpenseFormHeaderFields.js | 164 ++++++ client/src/containers/Expenses/Expenses.js | 18 +- client/src/containers/Expenses/utils.js | 21 + .../containers/Items/ItemCategoriesTable.js | 2 +- .../src/containers/Purchases/Bill/BillForm.js | 16 +- .../Purchases/Bill/BillForm.schema.js | 3 +- .../containers/Purchases/Bill/BillFormBody.js | 15 + .../Purchases/Bill/BillFormHeader.js | 156 +----- .../Purchases/Bill/BillFormHeaderFields.js | 161 ++++++ client/src/containers/Purchases/Bill/Bills.js | 18 +- .../Purchases/PaymentMades/PaymentMade.js | 12 + .../Purchases/PaymentMades/PaymentMadeForm.js | 35 +- .../PaymentMades/PaymentMadeFormHeader.js | 65 ++- .../containers/Sales/Estimate/EstimateForm.js | 16 +- .../Sales/Estimate/EstimateForm.schema.js | 3 +- .../Sales/Estimate/EstimateFormBody.js | 15 + .../Sales/Estimate/EstimateFormHeader.js | 175 +----- .../Estimate/EstimateFormHeaderFields.js | 182 ++++++ .../containers/Sales/Estimate/Estimates.js | 18 +- .../containers/Sales/Invoice/InvoiceForm.js | 22 +- .../Sales/Invoice/InvoiceForm.schema.js | 3 +- .../Sales/Invoice/InvoiceFormHeader.js | 192 +------ .../Sales/Invoice/InvoiceFormHeaderFields.js | 176 ++++++ .../src/containers/Sales/Invoice/Invoices.js | 22 +- .../PaymentReceive/PaymentReceiveForm.js | 23 +- .../PaymentReceiveFormHeader.js | 3 + .../PaymentReceive/PaymentReceiveFormPage.js | 16 +- .../containers/Sales/Receipt/ReceiptForm.js | 10 +- .../Sales/Receipt/ReceiptForm.schema.js | 3 +- .../Sales/Receipt/ReceiptFormBody.js | 13 + .../Sales/Receipt/ReceiptFormHeader.js | 195 +------ .../Sales/Receipt/ReceiptFormHeaderFields.js | 195 +++++++ .../src/containers/Sales/Receipt/Receipts.js | 18 +- client/src/lang/en/index.js | 3 + client/src/routes/dashboard.js | 2 +- client/src/static/json/icons.js | 52 ++ .../src/store/dashboard/dashboard.reducer.js | 19 +- client/src/store/dashboard/dashboard.types.js | 4 + client/src/style/App.scss | 152 +++-- client/src/style/components/data-table.scss | 4 + client/src/style/objects/buttons.scss | 22 +- client/src/style/pages/accounts-chart.scss | 6 +- client/src/style/pages/bills.scss | 18 +- client/src/style/pages/dashboard.scss | 2 +- client/src/style/pages/estimate.scss | 2 + client/src/style/pages/estimates.scss | 29 +- client/src/style/pages/expense-form.scss | 257 +-------- client/src/style/pages/invoice-form.scss | 22 +- .../src/style/pages/make-journal-entries.scss | 55 +- client/src/style/pages/payment-made.scss | 1 - client/src/style/pages/payment-receive.scss | 1 - client/src/style/pages/receipt-form.scss | 11 +- client/src/style/views/Sidebar.scss | 6 +- client/src/utils.js | 9 +- client/tsconfig.json | 17 + 118 files changed, 3883 insertions(+), 2660 deletions(-) delete mode 100644 client/jsconfig.json create mode 100644 client/src/components/Forms/MoneyInputGroup/CurrencyInputProps.ts create mode 100644 client/src/components/Forms/MoneyInputGroup/index.tsx create mode 100644 client/src/components/Forms/MoneyInputGroup/utils/__tests__/addSeparators.spec.ts create mode 100644 client/src/components/Forms/MoneyInputGroup/utils/__tests__/cleanValue.spec.ts create mode 100644 client/src/components/Forms/MoneyInputGroup/utils/__tests__/fixedDecimalValue.spec.ts create mode 100644 client/src/components/Forms/MoneyInputGroup/utils/__tests__/formatValue.spec.ts create mode 100644 client/src/components/Forms/MoneyInputGroup/utils/__tests__/isNumber.spec.ts create mode 100644 client/src/components/Forms/MoneyInputGroup/utils/__tests__/padTrimValue.spec.ts create mode 100644 client/src/components/Forms/MoneyInputGroup/utils/__tests__/parseAbbrValue.spec.ts create mode 100644 client/src/components/Forms/MoneyInputGroup/utils/__tests__/removeInvalidChars.spec.ts create mode 100644 client/src/components/Forms/MoneyInputGroup/utils/__tests__/removeSeparators.spec.ts create mode 100644 client/src/components/Forms/MoneyInputGroup/utils/addSeparators.ts create mode 100644 client/src/components/Forms/MoneyInputGroup/utils/cleanValue.ts create mode 100644 client/src/components/Forms/MoneyInputGroup/utils/escapeRegExp.ts create mode 100644 client/src/components/Forms/MoneyInputGroup/utils/fixedDecimalValue.ts create mode 100644 client/src/components/Forms/MoneyInputGroup/utils/formatValue.ts create mode 100644 client/src/components/Forms/MoneyInputGroup/utils/index.tsx create mode 100644 client/src/components/Forms/MoneyInputGroup/utils/isNumber.ts create mode 100644 client/src/components/Forms/MoneyInputGroup/utils/padTrimValue.ts create mode 100644 client/src/components/Forms/MoneyInputGroup/utils/parseAbbrValue.ts create mode 100644 client/src/components/Forms/MoneyInputGroup/utils/removeInvalidChars.ts create mode 100644 client/src/components/Forms/MoneyInputGroup/utils/removeSeparators.ts delete mode 100644 client/src/components/MoneyInputGroup.js create mode 100644 client/src/components/PageFormBigNumber.js create mode 100644 client/src/containers/Accounting/MakeJournalEntries.schema.js create mode 100644 client/src/containers/Accounting/MakeJournalEntriesField.js delete mode 100644 client/src/containers/Accounting/MakeJournalEntriesFooter.js create mode 100644 client/src/containers/Accounting/MakeJournalEntriesHeaderFields.js create mode 100644 client/src/containers/Accounting/MakeJournalFormFloatingActions.js create mode 100644 client/src/containers/Accounting/MakeJournalFormFooter.js create mode 100644 client/src/containers/Accounting/MakeJournalNumberWatcher.js create mode 100644 client/src/containers/Accounting/utils.js create mode 100644 client/src/containers/Expenses/ExpenseFormBody.js rename client/src/containers/Expenses/{ExpenseTable.js => ExpenseFormEntries.js} (87%) create mode 100644 client/src/containers/Expenses/ExpenseFormEntriesField.js create mode 100644 client/src/containers/Expenses/ExpenseFormFooter.js create mode 100644 client/src/containers/Expenses/ExpenseFormHeaderFields.js create mode 100644 client/src/containers/Expenses/utils.js create mode 100644 client/src/containers/Purchases/Bill/BillFormBody.js create mode 100644 client/src/containers/Purchases/Bill/BillFormHeaderFields.js create mode 100644 client/src/containers/Sales/Estimate/EstimateFormBody.js create mode 100644 client/src/containers/Sales/Estimate/EstimateFormHeaderFields.js create mode 100644 client/src/containers/Sales/Invoice/InvoiceFormHeaderFields.js create mode 100644 client/src/containers/Sales/Receipt/ReceiptFormBody.js create mode 100644 client/src/containers/Sales/Receipt/ReceiptFormHeaderFields.js create mode 100644 client/tsconfig.json diff --git a/client/jsconfig.json b/client/jsconfig.json deleted file mode 100644 index ee1b584ef..000000000 --- a/client/jsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ - -{ - "compilerOptions": { - "jsx": "react", - "baseUrl": "src" - }, - "include": ["src"] -} \ No newline at end of file diff --git a/client/package.json b/client/package.json index 56d99a20e..976cf75fa 100644 --- a/client/package.json +++ b/client/package.json @@ -131,10 +131,15 @@ }, "devDependencies": { "@babel/preset-flow": "^7.9.0", + "@types/jest": "^26.0.15", + "@types/node": "^14.14.9", + "@types/react": "^17.0.0", + "@types/react-dom": "^17.0.0", "@welldone-software/why-did-you-render": "^6.0.0-rc.1", "http-proxy-middleware": "^1.0.0", "react-query-devtools": "^2.1.1", - "redux-devtools": "^3.5.0" + "redux-devtools": "^3.5.0", + "typescript": "^4.1.2" }, "jest": { "roots": [ diff --git a/client/src/common/classes.js b/client/src/common/classes.js index fed98ed41..e1102405b 100644 --- a/client/src/common/classes.js +++ b/client/src/common/classes.js @@ -8,6 +8,7 @@ const CLASSES = { DATATABLE_EDITOR: 'datatable-editor', DATATABLE_EDITOR_ACTIONS: 'datatable-editor__actions', DATATABLE_EDITOR_ITEMS_ENTRIES: 'items-entries-table', + DATATABLE_EDITOR_HAS_TOTAL_ROW: 'has-total-row', PAGE_FORM: 'page-form', PAGE_FORM_HEADER: 'page-form__header', @@ -16,6 +17,7 @@ const CLASSES = { PAGE_FORM_HEADER_BIG_NUMBERS: 'page-form__big-numbers', PAGE_FORM_TABS: 'page-form__tabs', PAGE_FORM_BODY: 'page-form__body', + PAGE_FORM_STRIP_STYLE: 'page-form--strip', PAGE_FORM_FOOTER: 'page-form__footer', PAGE_FORM_FLOATING_ACTIONS: 'page-form__floating-actions', @@ -29,6 +31,8 @@ const CLASSES = { PAGE_FORM_CUSTOMER: 'page-form--customer', PAGE_FORM_VENDOR: 'page-form--customer', PAGE_FORM_ITEM: 'page-form--item', + PAGE_FORM_MAKE_JOURNAL: 'page-form--make-journal-entries', + PAGE_FORM_EXPENSE: 'page-form--expense', FORM_GROUP_LIST_SELECT: 'form-group--select-list', diff --git a/client/src/components/AccountsSelectList.js b/client/src/components/AccountsSelectList.js index 5eae83ea0..585d96b01 100644 --- a/client/src/components/AccountsSelectList.js +++ b/client/src/components/AccountsSelectList.js @@ -16,7 +16,8 @@ export default function AccountsSelectList({ popoverFill = false, filterByRootTypes = [], filterByTypes = [], - filterByNormal + filterByNormal, + buttonProps = {} }) { // Filters accounts based on filter props. const filteredAccounts = useMemo(() => { @@ -113,6 +114,7 @@ export default function AccountsSelectList({ - - - - - - - - - ); -} diff --git a/client/src/containers/Accounting/MakeJournalEntriesForm.js b/client/src/containers/Accounting/MakeJournalEntriesForm.js index 0eded5f18..d05ad1a4c 100644 --- a/client/src/containers/Accounting/MakeJournalEntriesForm.js +++ b/client/src/containers/Accounting/MakeJournalEntriesForm.js @@ -1,20 +1,21 @@ -import React, { - useMemo, - useState, - useEffect, - useRef, - useCallback, -} from 'react'; -import * as Yup from 'yup'; -import { useFormik } from 'formik'; +import React, { useMemo, useEffect, useCallback } from 'react'; +import { Formik, Form } from 'formik'; import moment from 'moment'; import { Intent } from '@blueprintjs/core'; import { useIntl } from 'react-intl'; -import { pick, setWith } from 'lodash'; +import { pick } from 'lodash'; +import classNames from 'classnames'; +import { CLASSES } from 'common/classes'; +import { + CreateJournalSchema, + EditJournalSchema, +} from './MakeJournalEntries.schema'; import MakeJournalEntriesHeader from './MakeJournalEntriesHeader'; -import MakeJournalEntriesFooter from './MakeJournalEntriesFooter'; -import MakeJournalEntriesTable from './MakeJournalEntriesTable'; +import MakeJournalFormFloatingActions from './MakeJournalFormFloatingActions'; +import MakeJournalEntriesField from './MakeJournalEntriesField'; +import MakeJournalNumberWatcher from './MakeJournalNumberWatcher'; +import MakeJournalFormFooter from './MakeJournalFormFooter'; import withJournalsActions from 'containers/Accounting/withJournalsActions'; import withManualJournalDetail from 'containers/Accounting/withManualJournalDetail'; @@ -25,25 +26,31 @@ import withSettings from 'containers/Settings/withSettings'; import AppToaster from 'components/AppToaster'; import Dragzone from 'components/Dragzone'; import withMediaActions from 'containers/Media/withMediaActions'; - -import useMedia from 'hooks/useMedia'; import { compose, repeatValue, orderingLinesIndexes, defaultToTransform, } from 'utils'; +import { transformErrors } from './utils'; import withManualJournalsActions from './withManualJournalsActions'; -import withManualJournals from './withManualJournals'; -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', - CREDIT_DEBIT_SUMATION_SHOULD_NOT_EQUAL_ZERO: - 'CREDIT.DEBIT.SUMATION.SHOULD.NOT.EQUAL.ZERO', +const defaultEntry = { + index: 0, + account_id: '', + credit: '', + debit: '', + contact_id: '', + note: '', +}; +const defaultInitialValues = { + journal_number: '', + journal_type: 'Journal', + date: moment(new Date()).format('YYYY-MM-DD'), + description: '', + reference: '', + currency_code: '', + entries: [...repeatValue(defaultEntry, 4)], }; /** @@ -77,27 +84,9 @@ function MakeJournalEntriesForm({ onCancelForm, }) { const { formatMessage } = useIntl(); - const { - setFiles, - saveMedia, - deletedFiles, - setDeletedFiles, - deleteMedia, - } = useMedia({ - saveCallback: requestSubmitMedia, - deleteCallback: requestDeleteMedia, - }); + const isNewMode = manualJournalId; - const handleDropFiles = useCallback((_files) => { - setFiles(_files.filter((file) => file.uploaded === false)); - }, []); - - const savedMediaIds = useRef([]); - const clearSavedMediaIds = () => { - savedMediaIds.current = []; - }; - - const journalNumber = journalNumberPrefix + const journalNumber = isNewMode ? `${journalNumberPrefix}-${journalNextNumber}` : journalNextNumber; @@ -122,73 +111,6 @@ function MakeJournalEntriesForm({ formatMessage, ]); - const validationSchema = Yup.object().shape({ - journal_number: Yup.string() - .required() - .min(1) - .max(255) - .label(formatMessage({ id: 'journal_number_' })), - journal_type: Yup.string() - .required() - .min(1) - .max(255) - .label(formatMessage({ id: 'journal_type' })), - date: Yup.date() - .required() - .label(formatMessage({ id: 'date' })), - currency_code: Yup.string(), - reference: Yup.string().min(1).max(255), - description: Yup.string().min(1).max(1024), - entries: Yup.array().of( - Yup.object().shape({ - credit: Yup.number().decimalScale(13).nullable(), - debit: Yup.number().decimalScale(13).nullable(), - account_id: Yup.number() - .nullable() - .when(['credit', 'debit'], { - is: (credit, debit) => credit || debit, - then: Yup.number().required(), - }), - contact_id: Yup.number().nullable(), - contact_type: Yup.string().nullable(), - note: Yup.string().max(255).nullable(), - }), - ), - }); - - const saveInvokeSubmit = useCallback( - (payload) => { - onFormSubmit && onFormSubmit(payload); - }, - [onFormSubmit], - ); - - const [payload, setPayload] = useState({}); - - const defaultEntry = useMemo( - () => ({ - index: 0, - account_id: null, - credit: 0, - debit: 0, - contact_id: null, - note: '', - }), - [], - ); - const defaultInitialValues = useMemo( - () => ({ - journal_number: journalNumber, - journal_type: 'Journal', - date: moment(new Date()).format('YYYY-MM-DD'), - description: '', - reference: '', - currency_code: '', - entries: [...repeatValue(defaultEntry, 4)], - }), - [defaultEntry, journalNumber], - ); - const initialValues = useMemo( () => ({ ...(manualJournal @@ -198,300 +120,124 @@ function MakeJournalEntriesForm({ ...pick(entry, Object.keys(defaultEntry)), })), } - : { + : { ...defaultInitialValues, + journal_number: journalNumber, entries: orderingLinesIndexes(defaultInitialValues.entries), }), }), - [manualJournal, defaultInitialValues, defaultEntry], + [manualJournal, journalNumber], ); - const initialAttachmentFiles = useMemo(() => { - return manualJournal && manualJournal.media - ? manualJournal.media.map((attach) => ({ - preview: attach.attachment_file, - uploaded: true, - metadata: { ...attach }, - })) - : []; - }, [manualJournal]); - - // Transform API errors in toasts messages. - const transformErrors = (resErrors, { setErrors, errors }) => { - const getError = (errorType) => resErrors.find((e) => e.type === errorType); - const toastMessages = []; - let error; - let newErrors = { ...errors, entries: [] }; - - const setEntriesErrors = (indexes, prop, message) => - indexes.forEach((i) => { - const index = Math.max(i - 1, 0); - newErrors = setWith(newErrors, `entries.[${index}].${prop}`, message); - }); - - if ((error = getError(ERROR.PAYABLE_ENTRIES_HAS_NO_VENDORS))) { - toastMessages.push( - formatMessage({ - id: 'vendors_should_selected_with_payable_account_only', - }), - ); - setEntriesErrors(error.indexes, 'contact_id', 'error'); - } - if ((error = getError(ERROR.RECEIVABLE_ENTRIES_HAS_NO_CUSTOMERS))) { - toastMessages.push( - formatMessage({ - id: 'should_select_customers_with_entries_have_receivable_account', - }), - ); - setEntriesErrors(error.indexes, 'contact_id', 'error'); - } - if ((error = getError(ERROR.CUSTOMERS_NOT_WITH_RECEVIABLE_ACC))) { - toastMessages.push( - formatMessage({ - id: 'customers_should_selected_with_receivable_account_only', - }), - ); - setEntriesErrors(error.indexes, 'account_id', 'error'); - } - if ((error = getError(ERROR.VENDORS_NOT_WITH_PAYABLE_ACCOUNT))) { - toastMessages.push( - formatMessage({ - id: 'vendors_should_selected_with_payable_account_only', - }), - ); - setEntriesErrors(error.indexes, 'account_id', 'error'); - } - if ((error = getError(ERROR.JOURNAL_NUMBER_ALREADY_EXISTS))) { - newErrors = setWith( - newErrors, - 'journal_number', - formatMessage({ - id: 'journal_number_is_already_used', - }), - ); - } - setErrors({ ...newErrors }); - - if (toastMessages.length > 0) { - AppToaster.show({ - message: toastMessages.map((message) => { - return
- {message}
; - }), - intent: Intent.DANGER, - }); - } - }; - - const { - values, - errors, - setFieldError, - setFieldValue, - handleSubmit, - getFieldProps, - touched, - isSubmitting, - } = useFormik({ - validationSchema, - initialValues, - onSubmit: (values, { setErrors, setSubmitting, resetForm }) => { - setSubmitting(true); - const entries = values.entries.filter( - (entry) => entry.debit || entry.credit, - ); - const getTotal = (type = 'credit') => { - return entries.reduce((total, item) => { - return item[type] ? item[type] + total : total; - }, 0); - }; - const totalCredit = getTotal('credit'); - const totalDebit = getTotal('debit'); - - // Validate the total credit should be eqials total debit. - if (totalCredit !== totalDebit) { - AppToaster.show({ - message: formatMessage({ - id: 'should_total_of_credit_and_debit_be_equal', - }), - intent: Intent.DANGER, - }); - setSubmitting(false); - return; - } else if (totalCredit === 0 || totalDebit === 0) { - AppToaster.show({ - message: formatMessage({ - id: 'amount_cannot_be_zero_or_empty', - }), - intent: Intent.DANGER, - }); - setSubmitting(false); - return; - } - const form = { ...values, status: payload.publish, entries }; - - const saveJournal = (mediaIds) => - new Promise((resolve, reject) => { - const requestForm = { ...form, media_ids: mediaIds }; - - if (manualJournal && manualJournal.id) { - requestEditManualJournal(manualJournal.id, requestForm) - .then((response) => { - AppToaster.show({ - message: formatMessage( - { id: 'the_journal_has_been_successfully_edited' }, - { number: values.journal_number }, - ), - intent: Intent.SUCCESS, - }); - setSubmitting(false); - saveInvokeSubmit({ action: 'update', ...payload }); - clearSavedMediaIds([]); - resetForm(); - resolve(response); - }) - .catch((errors) => { - transformErrors(errors, { setErrors }); - setSubmitting(false); - }); - } else { - requestMakeJournalEntries(requestForm) - .then((response) => { - AppToaster.show({ - message: formatMessage( - { id: 'the_journal_has_been_successfully_created' }, - { number: values.journal_number }, - ), - intent: Intent.SUCCESS, - }); - setSubmitting(false); - saveInvokeSubmit({ action: 'new', ...payload }); - clearSavedMediaIds(); - resetForm(); - resolve(response); - }) - .catch((errors) => { - transformErrors(errors, { setErrors }); - setSubmitting(false); - }); - } - }); - - Promise.all([saveMedia(), deleteMedia()]) - .then(([savedMediaResponses]) => { - const mediaIds = savedMediaResponses.map((res) => res.data.media.id); - savedMediaIds.current = mediaIds; - - return savedMediaResponses; - }) - .then(() => { - return saveJournal(savedMediaIds.current); - }); - }, - }); - - // Observes journal number settings changes. - useEffect(() => { - if (journalNumberChanged) { - setFieldValue('journal_number', journalNumber); - changePageSubtitle( - defaultToTransform(journalNumber, `No. ${journalNumber}`, ''), - ); - setJournalNumberChanged(false); - } - }, [ - journalNumber, - journalNumberChanged, - setJournalNumberChanged, - setFieldValue, - changePageSubtitle, - ]); - - const handleSubmitClick = useCallback( - (payload) => { - setPayload(payload); - // formik.resetForm(); - handleSubmit(); - }, - [setPayload, handleSubmit], - ); - - const handleCancelClick = useCallback( - (payload) => { - onCancelForm && onCancelForm(payload); - }, - [onCancelForm], - ); - - const handleDeleteFile = useCallback( - (_deletedFiles) => { - _deletedFiles.forEach((deletedFile) => { - if (deletedFile.uploaded && deletedFile.metadata.id) { - setDeletedFiles([...deletedFiles, deletedFile.metadata.id]); - } - }); - }, - [setDeletedFiles, deletedFiles], - ); - - // Handle click on add a new line/row. - const handleClickAddNewRow = useCallback(() => { - setFieldValue( - 'entries', - orderingLinesIndexes([...values.entries, defaultEntry]), - ); - }, [values.entries, defaultEntry, setFieldValue]); - - // Handle click `Clear all lines` button. - const handleClickClearLines = useCallback(() => { - setFieldValue( - 'entries', - orderingLinesIndexes([...repeatValue(defaultEntry, 4)]), - ); - }, [defaultEntry, setFieldValue]); - // Handle journal number field change. const handleJournalNumberChanged = useCallback( (journalNumber) => { changePageSubtitle( - defaultToTransform(journalNumber, `No. ${journalNumber}`, '') + defaultToTransform(journalNumber, `No. ${journalNumber}`, ''), ); }, [changePageSubtitle], ); - return ( -
-
- - - - - { + setSubmitting(true); + const entries = values.entries.filter( + (entry) => entry.debit || entry.credit, + ); + const getTotal = (type = 'credit') => { + return entries.reduce((total, item) => { + return item[type] ? item[type] + total : total; + }, 0); + }; + const totalCredit = getTotal('credit'); + const totalDebit = getTotal('debit'); + + // Validate the total credit should be eqials total debit. + if (totalCredit !== totalDebit) { + AppToaster.show({ + message: formatMessage({ + id: 'should_total_of_credit_and_debit_be_equal', + }), + intent: Intent.DANGER, + }); + setSubmitting(false); + return; + } else if (totalCredit === 0 || totalDebit === 0) { + AppToaster.show({ + message: formatMessage({ + id: 'amount_cannot_be_zero_or_empty', + }), + intent: Intent.DANGER, + }); + setSubmitting(false); + return; + } + const form = { ...values, entries }; + + const handleError = (error) => { + transformErrors(error, { setErrors }); + setSubmitting(false); + }; + + const handleSuccess = (errors) => { + AppToaster.show({ + message: formatMessage( + { + id: isNewMode + ? 'the_journal_has_been_successfully_created' + : 'the_journal_has_been_successfully_edited', + }, + { number: values.journal_number }, + ), + intent: Intent.SUCCESS, + }); + setSubmitting(false); + resetForm(); + }; + + if (isNewMode) { + requestEditManualJournal(manualJournal.id, form) + .then(handleSuccess) + .catch(handleError); + } else { + requestMakeJournalEntries(form).then(handleSuccess).catch(handleError); + } + }; + return ( +
+ + {({ isSubmitting, values }) => ( +
+ + + + + + + )} +
+ {/* + /> */}
); } @@ -503,11 +249,8 @@ export default compose( withDashboardActions, withMediaActions, withSettings(({ manualJournalsSettings }) => ({ - journalNextNumber: manualJournalsSettings.nextNumber, - journalNumberPrefix: manualJournalsSettings.numberPrefix, + journalNextNumber: parseInt(manualJournalsSettings?.nextNumber, 10), + journalNumberPrefix: manualJournalsSettings?.numberPrefix, })), withManualJournalsActions, - withManualJournals(({ journalNumberChanged }) => ({ - journalNumberChanged, - })), )(MakeJournalEntriesForm); diff --git a/client/src/containers/Accounting/MakeJournalEntriesHeader.js b/client/src/containers/Accounting/MakeJournalEntriesHeader.js index a2bec082e..cd2c60ef8 100644 --- a/client/src/containers/Accounting/MakeJournalEntriesHeader.js +++ b/client/src/containers/Accounting/MakeJournalEntriesHeader.js @@ -1,238 +1,12 @@ -import React, { useMemo, useState, useCallback } from 'react'; -import { - InputGroup, - FormGroup, - Intent, - Position, - ControlGroup, -} from '@blueprintjs/core'; -import { DateInput } from '@blueprintjs/datetime'; -import { FormattedMessage as T } from 'react-intl'; -import { Row, Col } from 'react-grid-system'; -import moment from 'moment'; +import React from 'react'; import classNames from 'classnames'; - import { CLASSES } from 'common/classes'; -import { momentFormatter, tansformDateValue, saveInvoke } from 'utils'; -import { - ErrorMessage, - Hint, - FieldHint, - FieldRequiredHint, - Icon, - InputPrependButton, - CurrencySelectList, -} from 'components'; - -import withDialogActions from 'containers/Dialog/withDialogActions'; -import withCurrencies from 'containers/Currencies/withCurrencies'; - -import { compose } from 'utils'; - -function MakeJournalEntriesHeader({ - errors, - touched, - values, - setFieldValue, - getFieldProps, - - // #ownProps - manualJournal, - onJournalNumberChanged, - - // #withCurrencies - currenciesList, - - // #withDialog - openDialog, -}) { - const [selectedItems, setSelectedItems] = useState({}); - - const handleDateChange = useCallback( - (date) => { - const formatted = moment(date).format('YYYY-MM-DD'); - setFieldValue('date', formatted); - }, - [setFieldValue], - ); - const handleJournalNumberChange = useCallback(() => { - openDialog('journal-number-form', {}); - }, [openDialog]); - - // Handle journal number field blur event. - const handleJournalNumberChanged = (event) => { - saveInvoke(onJournalNumberChanged, event.currentTarget.value); - }; - - const onItemsSelect = useCallback( - (filedName) => { - return (filed) => { - setSelectedItems({ - ...selectedItems, - [filedName]: filed, - }); - setFieldValue(filedName, filed.currency_code); - }; - }, - [setFieldValue, selectedItems], - ); +import MakeJournalEntriesHeaderFields from "./MakeJournalEntriesHeaderFields"; +export default function MakeJournalEntriesHeader() { return ( -
- - - } - labelInfo={ - <> - - - - } - className={'form-group--journal-number'} - intent={ - errors.journal_number && touched.journal_number && Intent.DANGER - } - helperText={ - - } - fill={true} - > - - - , - }} - tooltip={true} - tooltipProps={{ - content: 'Setting your auto-generated journal number', - position: Position.BOTTOM_LEFT, - }} - /> - - - - - - } - labelInfo={} - intent={errors.date && touched.date && Intent.DANGER} - helperText={} - minimal={true} - className={classNames(CLASSES.FILL)} - > - - - - - - } - className={'form-group--description'} - intent={errors.name && touched.name && Intent.DANGER} - helperText={ - - } - fill={true} - > - - - - - - - - } - labelInfo={ - } - position={Position.RIGHT} - /> - } - className={'form-group--reference'} - intent={errors.reference && touched.reference && Intent.DANGER} - helperText={ - - } - fill={true} - > - - - - - - } - className={classNames( - 'form-group--account-type', - 'form-group--select-list', - CLASSES.FILL, - )} - > - - - - - - } - className={classNames( - 'form-group--select-list', - 'form-group--currency', - CLASSES.FILL, - )} - > - - - - +
+
- ); -} - -export default compose( - withDialogActions, - withCurrencies(({ currenciesList }) => ({ - currenciesList, - })), -)(MakeJournalEntriesHeader); + ) +} \ No newline at end of file diff --git a/client/src/containers/Accounting/MakeJournalEntriesHeaderFields.js b/client/src/containers/Accounting/MakeJournalEntriesHeaderFields.js new file mode 100644 index 000000000..f58927935 --- /dev/null +++ b/client/src/containers/Accounting/MakeJournalEntriesHeaderFields.js @@ -0,0 +1,188 @@ +import React, { useCallback } from 'react'; +import { + InputGroup, + FormGroup, + Position, + ControlGroup, +} from '@blueprintjs/core'; +import { FastField, ErrorMessage } from 'formik'; +import { DateInput } from '@blueprintjs/datetime'; +import { FormattedMessage as T } from 'react-intl'; +import classNames from 'classnames'; + +import { CLASSES } from 'common/classes'; +import { momentFormatter, tansformDateValue, saveInvoke } from 'utils'; +import { + Hint, + FieldHint, + FieldRequiredHint, + Icon, + InputPrependButton, + CurrencySelectList, +} from 'components'; + +import withDialogActions from 'containers/Dialog/withDialogActions'; +import withCurrencies from 'containers/Currencies/withCurrencies'; + +import { compose, inputIntent, handleDateChange } from 'utils'; + +function MakeJournalEntriesHeader({ + // #ownProps + manualJournal, + onJournalNumberChanged, + + // #withCurrencies + currenciesList, + + // #withDialog + openDialog, +}) { + const handleJournalNumberChange = useCallback(() => { + openDialog('journal-number-form', {}); + }, [openDialog]); + + // Handle journal number field blur event. + const handleJournalNumberChanged = (event) => { + saveInvoke(onJournalNumberChanged, event.currentTarget.value); + }; + + return ( +
+ + {({ form, field: { value }, meta: { error, touched } }) => ( + } + labelInfo={} + intent={inputIntent({ error, touched })} + helperText={} + minimal={true} + inline={true} + className={classNames(CLASSES.FILL)} + > + { + form.setFieldValue('date', formattedDate); + })} + value={tansformDateValue(value)} + popoverProps={{ + position: Position.BOTTOM, + minimal: true, + }} + inputProps={{ + leftIcon: , + }} + /> + + )} + + + + {({ form, field, meta: { error, touched } }) => ( + } + labelInfo={ + <> + + + + } + className={'form-group--journal-number'} + intent={inputIntent({ error, touched })} + helperText={} + fill={true} + inline={true} + > + + + , + }} + tooltip={true} + tooltipProps={{ + content: 'Setting your auto-generated journal number', + position: Position.BOTTOM_LEFT, + }} + /> + + + )} + + + + {({ form, field, meta: { error, touched } }) => ( + } + labelInfo={ + } + position={Position.RIGHT} + /> + } + className={'form-group--reference'} + intent={inputIntent({ error, touched })} + helperText={} + fill={true} + inline={true} + > + + + )} + + + + {({ form, field, meta: { error, touched } }) => ( + } + className={classNames( + 'form-group--account-type', + CLASSES.FILL, + )} + inline={true} + > + + + )} + + + + {({ form, field: { value }, meta: { error, touched } }) => ( + } + className={classNames( + 'form-group--currency', + CLASSES.FILL, + )} + inline={true} + > + { + form.setFieldValue('currency_code', currencyItem.currency_code); + }} + defaultSelectText={value} + /> + + )} + +
+ ); +} + +export default compose( + withDialogActions, + withCurrencies(({ currenciesList }) => ({ + currenciesList, + })), +)(MakeJournalEntriesHeader); diff --git a/client/src/containers/Accounting/MakeJournalEntriesPage.js b/client/src/containers/Accounting/MakeJournalEntriesPage.js index 387d0d301..035881408 100644 --- a/client/src/containers/Accounting/MakeJournalEntriesPage.js +++ b/client/src/containers/Accounting/MakeJournalEntriesPage.js @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { useParams, useHistory } from 'react-router-dom'; import { useQuery } from 'react-query'; @@ -10,6 +10,7 @@ import withAccountsActions from 'containers/Accounts/withAccountsActions'; import withManualJournalsActions from 'containers/Accounting/withManualJournalsActions'; import withCurrenciesActions from 'containers/Currencies/withCurrenciesActions'; import withSettingsActions from 'containers/Settings/withSettingsActions'; +import withDashboardActions from 'containers/Dashboard/withDashboardActions'; import { compose } from 'utils'; @@ -28,10 +29,24 @@ function MakeJournalEntriesPage({ // #withSettingsActions requestFetchOptions, + + // #withDashboardActions + setSidebarShrink, + resetSidebarPreviousExpand }) { const history = useHistory(); const { id } = useParams(); + useEffect(() => { + // Shrink the sidebar by foce. + setSidebarShrink(); + + return () => { + // Reset the sidebar to the previous status. + resetSidebarPreviousExpand(); + }; + }, [resetSidebarPreviousExpand, setSidebarShrink]); + const fetchAccounts = useQuery('accounts-list', (key) => requestFetchAccounts(), ); @@ -88,4 +103,5 @@ export default compose( withManualJournalsActions, withCurrenciesActions, withSettingsActions, + withDashboardActions, )(MakeJournalEntriesPage); diff --git a/client/src/containers/Accounting/MakeJournalEntriesTable.js b/client/src/containers/Accounting/MakeJournalEntriesTable.js index 14b6dcae1..d57286dc8 100644 --- a/client/src/containers/Accounting/MakeJournalEntriesTable.js +++ b/client/src/containers/Accounting/MakeJournalEntriesTable.js @@ -2,91 +2,32 @@ import React, { useState, useMemo, useEffect, useCallback } from 'react'; import { Button, Tooltip, Position, Intent } from '@blueprintjs/core'; import { FormattedMessage as T, useIntl } from 'react-intl'; import { omit } from 'lodash'; +import classNames from 'classnames'; +import { CLASSES } from 'common/classes'; import DataTable from 'components/DataTable'; -import Icon from 'components/Icon'; -import { Hint } from 'components'; -import { compose, formattedAmount, transformUpdatedRows } from 'utils'; +import { + compose, + formattedAmount, + transformUpdatedRows, + saveInvoke, +} from 'utils'; import { AccountsListFieldCell, MoneyFieldCell, InputGroupCell, ContactsListFieldCell, } from 'components/DataTableCells'; +import { + ContactHeaderCell, + ActionsCellRenderer, + TotalAccountCellRenderer, + TotalCreditDebitCellRenderer, + NoteCellRenderer, +} from './components'; import withAccounts from 'containers/Accounts/withAccounts'; import withCustomers from 'containers/Customers/withCustomers'; -// Contact header cell. -function ContactHeaderCell() { - return ( - <> - - } - position={Position.LEFT_BOTTOM} - /> - - ); -} - -// 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 ( - } position={Position.LEFT}> - + + + + + + +
+ ); +} diff --git a/client/src/containers/Accounting/MakeJournalFormFooter.js b/client/src/containers/Accounting/MakeJournalFormFooter.js new file mode 100644 index 000000000..b91c8b51c --- /dev/null +++ b/client/src/containers/Accounting/MakeJournalFormFooter.js @@ -0,0 +1,42 @@ +import React from 'react'; +import { FastField } from 'formik'; +import classNames from 'classnames'; +import { CLASSES } from 'common/classes'; +import { FormGroup, TextArea } from '@blueprintjs/core'; +import { FormattedMessage as T } from 'react-intl'; +import { ErrorMessage, Row, Col } from 'components'; +import Dragzone from 'components/Dragzone'; +import { inputIntent } from 'utils'; + +export default function MakeJournalFormFooter() { + return ( +
+ + + + {({ field, meta: { error, touched } }) => ( + } + className={'form-group--description'} + intent={inputIntent({ error, touched })} + helperText={} + fill={true} + > +