From c2507629628c86044b6a44a8b58bbd3c87fa733f Mon Sep 17 00:00:00 2001 From: "a.bouhuolia" Date: Mon, 8 Mar 2021 14:52:59 +0200 Subject: [PATCH] fix(auth): hide/show password revealer in auth pages. fix(expense): auto-adding new lines. fix(journal): auto-adding new lines. --- .../MakeJournal/MakeJournalEntriesTable.js | 52 +++++----- .../Accounting/MakeJournal/utils.js | 26 ++--- .../Authentication/InviteAcceptFormContent.js | 15 ++- .../containers/Authentication/LoginForm.js | 31 ++++-- .../containers/Authentication/RegisterForm.js | 52 ++++++---- .../containers/Authentication/components.js | 12 ++- .../containers/Entries/ItemsEntriesTable.js | 95 ++++++------------- client/src/containers/Entries/utils.js | 11 ++- .../Expenses/ExpenseForm/ExpenseFormBody.js | 8 +- .../ExpenseForm/ExpenseFormEntriesField.js | 6 +- .../ExpenseForm/ExpenseFormEntriesTable.js | 53 +++++------ .../InvoiceItemsEntriesEditorField.js | 2 +- client/src/utils.js | 48 +++++++--- server/src/services/Sales/SalesReceipts.ts | 22 +---- 14 files changed, 220 insertions(+), 213 deletions(-) diff --git a/client/src/containers/Accounting/MakeJournal/MakeJournalEntriesTable.js b/client/src/containers/Accounting/MakeJournal/MakeJournalEntriesTable.js index 8f0c0144a..0fb099ba3 100644 --- a/client/src/containers/Accounting/MakeJournal/MakeJournalEntriesTable.js +++ b/client/src/containers/Accounting/MakeJournal/MakeJournalEntriesTable.js @@ -1,23 +1,21 @@ import React from 'react'; -import { Button } from '@blueprintjs/core'; -import { FormattedMessage as T } from 'react-intl'; -import { saveInvoke, removeRowsByIndex } from 'utils'; import { DataTableEditable } from 'components'; -import withAlertActions from 'containers/Alert/withAlertActions'; - -import { updateDataReducer } from './utils'; +import { + compose, + saveInvoke, + updateMinEntriesLines, + updateRemoveLineByIndex, + updateAutoAddNewLine, + updateTableRow, +} from 'utils'; import { useMakeJournalFormContext } from './MakeJournalProvider'; import { useJournalTableEntriesColumns } from './components'; - -import { compose } from 'redux'; +import { updateAdjustEntries } from './utils'; /** * Make journal entries table component. */ -function MakeJournalEntriesTable({ - // #withAlertsActions - openAlert, - +export default function MakeJournalEntriesTable({ // #ownPorps onChange, entries, @@ -30,22 +28,34 @@ function MakeJournalEntriesTable({ // Memorized data table columns. const columns = useJournalTableEntriesColumns(); - + // Handles update datatable data. const handleUpdateData = (rowIndex, columnId, value) => { - const newRows = updateDataReducer(entries, rowIndex, columnId, value); + const newRows = compose( + // Auto-adding new lines. + updateAutoAddNewLine(defaultEntry, ['account_id', 'credit', 'debit']), + // Update items entries total. + updateAdjustEntries(rowIndex, columnId, value), + // Update entry of the given row index and column id. + updateTableRow(rowIndex, columnId, value), + )(entries); + saveInvoke(onChange, newRows); }; // Handle remove datatable row. const handleRemoveRow = (rowIndex) => { - const newRows = removeRowsByIndex(entries, rowIndex); + const newRows = compose( + // Ensure minimum lines count. + updateMinEntriesLines(minLinesNumber, defaultEntry), + // Remove the line by the given index. + updateRemoveLineByIndex(rowIndex), + )(entries); + saveInvoke(onChange, newRows); }; - - - return ( + return ( + /> ); -} - -export default compose(withAlertActions)(MakeJournalEntriesTable); +} \ No newline at end of file diff --git a/client/src/containers/Accounting/MakeJournal/utils.js b/client/src/containers/Accounting/MakeJournal/utils.js index 6f63c1592..df4b4aea4 100644 --- a/client/src/containers/Accounting/MakeJournal/utils.js +++ b/client/src/containers/Accounting/MakeJournal/utils.js @@ -3,7 +3,7 @@ import { Intent } from '@blueprintjs/core'; import { sumBy, setWith, toSafeInteger, get } from 'lodash'; import moment from 'moment'; -import { transformUpdatedRows, repeatValue, transformToForm } from 'utils'; +import { updateTableRow, repeatValue, transformToForm } from 'utils'; import { AppToaster } from 'components'; import { formatMessage } from 'services/intl'; @@ -70,10 +70,14 @@ function adjustmentEntries(entries) { } /** - * + * Adjustment credit/debit entries. + * @param {number} rowIndex + * @param {number} columnId + * @param {string} value + * @return {array} */ -export const updateDataReducer = (rows, rowIndex, columnId, value) => { - let newRows = transformUpdatedRows(rows, rowIndex, columnId, value); +export const updateAdjustEntries = (rowIndex, columnId, value) => (rows) => { + let newRows = [...rows]; const oldCredit = get(rows, `[${rowIndex}].credit`); const oldDebit = get(rows, `[${rowIndex}].debit`); @@ -82,20 +86,10 @@ export const updateDataReducer = (rows, rowIndex, columnId, value) => { const adjustment = adjustmentEntries(rows); if (adjustment.credit) { - newRows = transformUpdatedRows( - newRows, - rowIndex, - 'credit', - adjustment.credit, - ); + newRows = updateTableRow(rowIndex, 'credit', adjustment.credit)(newRows); } if (adjustment.debit) { - newRows = transformUpdatedRows( - newRows, - rowIndex, - 'debit', - adjustment.debit, - ); + newRows = updateTableRow(rowIndex, 'debit', adjustment.debit)(newRows); } } return newRows; diff --git a/client/src/containers/Authentication/InviteAcceptFormContent.js b/client/src/containers/Authentication/InviteAcceptFormContent.js index 993d028e9..b13e15994 100644 --- a/client/src/containers/Authentication/InviteAcceptFormContent.js +++ b/client/src/containers/Authentication/InviteAcceptFormContent.js @@ -17,6 +17,17 @@ export default function InviteUserFormContent() { // Formik context. const { isSubmitting } = useFormikContext(); + const [passwordType, setPasswordType] = React.useState('password'); + + // Handle password revealer changing. + const handlePasswordRevealerChange = React.useCallback( + (shown) => { + const type = shown ? 'text' : 'password'; + setPasswordType(type); + }, + [setPasswordType], + ); + return (
@@ -74,14 +85,14 @@ export default function InviteUserFormContent() { {({ form, field, meta: { error, touched } }) => ( } - labelInfo={} + labelInfo={} className={'form-group--password has-password-revealer'} intent={inputIntent({ error, touched })} helperText={} > diff --git a/client/src/containers/Authentication/LoginForm.js b/client/src/containers/Authentication/LoginForm.js index 45fb79f44..97856d420 100644 --- a/client/src/containers/Authentication/LoginForm.js +++ b/client/src/containers/Authentication/LoginForm.js @@ -6,7 +6,7 @@ import { FormGroup, Checkbox, } from '@blueprintjs/core'; -import { Form, ErrorMessage, FastField } from 'formik'; +import { Form, ErrorMessage, Field } from 'formik'; import { FormattedMessage as T } from 'react-intl'; import { inputIntent } from 'utils'; import { PasswordRevealer } from './components'; @@ -14,12 +14,21 @@ import { PasswordRevealer } from './components'; /** * Login form. */ -export default function LoginForm({ - isSubmitting -}) { +export default function LoginForm({ isSubmitting }) { + const [passwordType, setPasswordType] = React.useState('password'); + + // Handle password revealer changing. + const handlePasswordRevealerChange = React.useCallback( + (shown) => { + const type = shown ? 'text' : 'password'; + setPasswordType(type); + }, + [setPasswordType], + ); + return ( - + {({ form, field, meta: { error, touched } }) => ( } @@ -34,13 +43,15 @@ export default function LoginForm({ /> )} - + - + {({ form, field, meta: { error, touched } }) => ( } - labelInfo={} + labelInfo={ + + } intent={inputIntent({ error, touched })} helperText={} className={'form-group--password has-password-revealer'} @@ -48,12 +59,12 @@ export default function LoginForm({ )} - +
diff --git a/client/src/containers/Authentication/RegisterForm.js b/client/src/containers/Authentication/RegisterForm.js index 194652b53..22bf5c3bd 100644 --- a/client/src/containers/Authentication/RegisterForm.js +++ b/client/src/containers/Authentication/RegisterForm.js @@ -4,26 +4,35 @@ import { InputGroup, Intent, FormGroup, - Spinner + Spinner, } from '@blueprintjs/core'; -import { ErrorMessage, FastField, Form } from 'formik'; +import { ErrorMessage, Field, Form } from 'formik'; import { FormattedMessage as T } from 'react-intl'; import { Link } from 'react-router-dom'; import { Row, Col, If } from 'components'; - +import { PasswordRevealer } from './components'; import { inputIntent } from 'utils'; /** * Register form. */ -export default function RegisterForm({ - isSubmitting, -}) { +export default function RegisterForm({ isSubmitting }) { + const [passwordType, setPasswordType] = React.useState('password'); + + // Handle password revealer changing. + const handlePasswordRevealerChange = React.useCallback( + (shown) => { + const type = shown ? 'text' : 'password'; + setPasswordType(type); + }, + [setPasswordType], + ); + return ( - + {({ form, field, meta: { error, touched } }) => ( } @@ -37,11 +46,11 @@ export default function RegisterForm({ /> )} - + - + {({ form, field, meta: { error, touched } }) => ( } @@ -55,11 +64,11 @@ export default function RegisterForm({ /> )} - + - + {({ form, field, meta: { error, touched } }) => ( } @@ -70,8 +79,9 @@ export default function RegisterForm({ )} - - + + + {({ form, field, meta: { error, touched } }) => ( } @@ -82,28 +92,28 @@ export default function RegisterForm({ )} - + - + {({ form, field, meta: { error, touched } }) => ( } - // labelInfo={passwordRevealerTmp} - intent={inputIntent({ error, touched })} - helperText={ - + labelInfo={ + } + intent={inputIntent({ error, touched })} + helperText={} className={'form-group--password has-password-revealer'} > )} - +

diff --git a/client/src/containers/Authentication/components.js b/client/src/containers/Authentication/components.js index afa369ea4..2c3740f8e 100644 --- a/client/src/containers/Authentication/components.js +++ b/client/src/containers/Authentication/components.js @@ -2,10 +2,18 @@ import React from 'react'; import { FormattedMessage as T } from 'react-intl'; import ContentLoader from 'react-content-loader'; import { If, Icon } from 'components'; +import { saveInvoke } from 'utils'; + +export function PasswordRevealer({ defaultShown = false, onChange }) { + const [shown, setShown] = React.useState(defaultShown); + + const handleClick = () => { + setShown(!shown); + saveInvoke(onChange, !shown); + }; -export function PasswordRevealer({ shown, onClick }) { return ( - + {' '} diff --git a/client/src/containers/Entries/ItemsEntriesTable.js b/client/src/containers/Entries/ItemsEntriesTable.js index 24319c773..3167afbdb 100644 --- a/client/src/containers/Entries/ItemsEntriesTable.js +++ b/client/src/containers/Entries/ItemsEntriesTable.js @@ -1,40 +1,25 @@ import React, { useEffect, useCallback } from 'react'; -import { Button } from '@blueprintjs/core'; -import { FormattedMessage as T } from 'react-intl'; import classNames from 'classnames'; import { useItem } from 'hooks/query'; -import ItemsEntriesDeleteAlert from 'containers/Alerts/ItemsEntries/ItemsEntriesDeleteAlert'; -import withAlertActions from 'containers/Alert/withAlertActions'; - import { CLASSES } from 'common/classes'; import { DataTableEditable } from 'components'; import { useEditableItemsEntriesColumns } from './components'; import { saveInvoke, - updateTableRow, - repeatValue, - removeRowsByIndex, compose, + updateTableRow, + updateMinEntriesLines, + updateAutoAddNewLine, + updateRemoveLineByIndex, } from 'utils'; import { updateItemsEntriesTotal, ITEM_TYPE } from './utils'; -import { last } from 'lodash'; - -const updateAutoAddNewLine = (defaultEntry) => (entries) => { - const newEntries = [...entries]; - const lastEntry = last(newEntries); - - return (lastEntry.item_id) ? [...entries, defaultEntry] : [...entries]; -}; /** * Items entries table. */ function ItemsEntriesTable({ - // #withAlertActions - openAlert, - // #ownProps items, entries, @@ -116,7 +101,7 @@ function ItemsEntriesTable({ setRowItem({ rowIndex, columnId, itemId: value }); } const newRows = compose( - updateAutoAddNewLine(defaultEntry), + updateAutoAddNewLine(defaultEntry, ['item_id']), updateItemsEntriesTotal, updateTableRow(rowIndex, columnId, value), )(rows); @@ -129,55 +114,35 @@ function ItemsEntriesTable({ // Handle table rows removing by index. const handleRemoveRow = (rowIndex) => { - const newRows = removeRowsByIndex(rows, rowIndex); - setRows(newRows); - saveInvoke(onUpdateData, newRows); - }; + const newRows = compose( + // Ensure minimum lines count. + updateMinEntriesLines(4, defaultEntry), + // Remove the line by the given index. + updateRemoveLineByIndex(rowIndex), + )(rows); - // Handle table rows adding a new row. - const onClickNewRow = (event) => { - const newRows = [...rows, defaultEntry]; - setRows(newRows); - saveInvoke(onUpdateData, newRows); - }; - - // Handle table clearing all rows. - const handleClickClearAllLines = (event) => { - openAlert('items-entries-clear-lines'); - }; - - // Handle alert confirm of clear all lines. - const handleClearLinesAlertConfirm = () => { - const newRows = repeatValue(defaultEntry, linesNumber); setRows(newRows); saveInvoke(onUpdateData, newRows); }; return ( - <> - - - + ); } @@ -186,7 +151,7 @@ ItemsEntriesTable.defaultProps = { index: 0, item_id: '', description: '', - quantity: 1, + quantity: '', rate: '', discount: '', }, @@ -194,4 +159,4 @@ ItemsEntriesTable.defaultProps = { linesNumber: 4, }; -export default compose(withAlertActions)(ItemsEntriesTable); +export default ItemsEntriesTable; diff --git a/client/src/containers/Entries/utils.js b/client/src/containers/Entries/utils.js index 082038efc..e51b59c5f 100644 --- a/client/src/containers/Entries/utils.js +++ b/client/src/containers/Entries/utils.js @@ -1,10 +1,11 @@ +import { repeat } from 'lodash'; import { toSafeNumber } from 'utils'; /** * Retrieve item entry total from the given rate, quantity and discount. - * @param {number} rate - * @param {number} quantity - * @param {number} discount + * @param {number} rate + * @param {number} quantity + * @param {number} discount * @return {number} */ export const calcItemEntryTotal = (discount, quantity, rate) => { @@ -21,9 +22,9 @@ export const calcItemEntryTotal = (discount, quantity, rate) => { export function updateItemsEntriesTotal(rows) { return rows.map((row) => ({ ...row, - total: calcItemEntryTotal(row.discount, row.quantity, row.rate) + total: calcItemEntryTotal(row.discount, row.quantity, row.rate), })); -}; +} export const ITEM_TYPE = { SELLABLE: 'SELLABLE', diff --git a/client/src/containers/Expenses/ExpenseForm/ExpenseFormBody.js b/client/src/containers/Expenses/ExpenseForm/ExpenseFormBody.js index 2e6f1dd6b..f2cf39ace 100644 --- a/client/src/containers/Expenses/ExpenseForm/ExpenseFormBody.js +++ b/client/src/containers/Expenses/ExpenseForm/ExpenseFormBody.js @@ -3,12 +3,10 @@ import classNames from 'classnames'; import { CLASSES } from 'common/classes'; import ExpenseFormEntriesField from './ExpenseFormEntriesField'; -export default function ExpenseFormBody({ - -}) { +export default function ExpenseFormBody() { return (

- ) -} \ No newline at end of file + ); +} diff --git a/client/src/containers/Expenses/ExpenseForm/ExpenseFormEntriesField.js b/client/src/containers/Expenses/ExpenseForm/ExpenseFormEntriesField.js index 36c8794b9..4d579c5df 100644 --- a/client/src/containers/Expenses/ExpenseForm/ExpenseFormEntriesField.js +++ b/client/src/containers/Expenses/ExpenseForm/ExpenseFormEntriesField.js @@ -1,7 +1,7 @@ import { FastField } from 'formik'; import React from 'react'; import ExpenseFormEntriesTable from './ExpenseFormEntriesTable'; -import { useExpenseFormContext } from './ExpenseFormPageProvider'; +import { defaultExpenseEntry } from './utils'; /** * Expense form entries field. @@ -9,8 +9,6 @@ import { useExpenseFormContext } from './ExpenseFormPageProvider'; export default function ExpenseFormEntriesField({ linesNumber = 4, }) { - const { defaultCategoryEntry } = useExpenseFormContext(); - return ( {({ form, field: { value }, meta: { error, touched } }) => ( @@ -20,7 +18,7 @@ export default function ExpenseFormEntriesField({ onChange={(entries) => { form.setFieldValue('categories', entries); }} - defaultEntry={defaultCategoryEntry} + defaultEntry={defaultExpenseEntry} linesNumber={linesNumber} /> )} diff --git a/client/src/containers/Expenses/ExpenseForm/ExpenseFormEntriesTable.js b/client/src/containers/Expenses/ExpenseForm/ExpenseFormEntriesTable.js index d795e305d..2cce9d4c3 100644 --- a/client/src/containers/Expenses/ExpenseForm/ExpenseFormEntriesTable.js +++ b/client/src/containers/Expenses/ExpenseForm/ExpenseFormEntriesTable.js @@ -1,23 +1,21 @@ import React, { useCallback } from 'react'; -import { Button } from '@blueprintjs/core'; -import { FormattedMessage as T } from 'react-intl'; import { DataTableEditable } from 'components'; -import ExpenseDeleteEntriesAlert from 'containers/Alerts/Expenses/ExpenseDeleteEntriesAlert'; import { useExpenseFormContext } from './ExpenseFormPageProvider'; import { useExpenseFormTableColumns } from './components'; - -import withAlertActions from 'containers/Alert/withAlertActions'; - -import { transformUpdatedRows, compose, saveInvoke, repeatValue } from 'utils'; +import { + saveInvoke, + compose, + updateTableRow, + updateMinEntriesLines, + updateAutoAddNewLine, + updateRemoveLineByIndex, +} from 'utils'; /** * Expenses form entries. */ -function ExpenseFormEntriesTable({ - // #withAlertActions - openAlert, - +export default function ExpenseFormEntriesTable({ // #ownPorps entries, defaultEntry, @@ -32,29 +30,30 @@ function ExpenseFormEntriesTable({ // Handles update datatable data. const handleUpdateData = useCallback( - (rowIndex, columnIdOrObj, value) => { - const newRows = transformUpdatedRows( - entries, - rowIndex, - columnIdOrObj, - value, - ); + (rowIndex, columnId, value) => { + const newRows = compose( + updateAutoAddNewLine(defaultEntry, ['expense_account_id']), + updateTableRow(rowIndex, columnId, value), + )(entries); + saveInvoke(onChange, newRows); }, - [entries, onChange], + [entries, defaultEntry, onChange], ); // Handles click remove datatable row. const handleRemoveRow = useCallback( (rowIndex) => { - // Can't continue if there is just one row line or less. - if (entries.length <= 1) { - return; - } - const newRows = entries.filter((row, index) => index !== rowIndex); + const newRows = compose( + // Ensure minimum lines count. + updateMinEntriesLines(4, defaultEntry), + // Remove the line by the given index. + updateRemoveLineByIndex(rowIndex), + )(entries); + saveInvoke(onChange, newRows); }, - [entries, onChange], + [entries, defaultEntry, onChange], ); return ( @@ -72,6 +71,4 @@ function ExpenseFormEntriesTable({ footer={true} /> ); -} - -export default compose(withAlertActions)(ExpenseFormEntriesTable); +} \ No newline at end of file diff --git a/client/src/containers/Sales/Invoices/InvoiceForm/InvoiceItemsEntriesEditorField.js b/client/src/containers/Sales/Invoices/InvoiceForm/InvoiceItemsEntriesEditorField.js index b09755137..6502f736a 100644 --- a/client/src/containers/Sales/Invoices/InvoiceForm/InvoiceItemsEntriesEditorField.js +++ b/client/src/containers/Sales/Invoices/InvoiceForm/InvoiceItemsEntriesEditorField.js @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react'; +import React from 'react'; import { FastField } from 'formik'; import classNames from 'classnames'; import { CLASSES } from 'common/classes'; diff --git a/client/src/utils.js b/client/src/utils.js index 197a515d1..0d57c3e59 100644 --- a/client/src/utils.js +++ b/client/src/utils.js @@ -93,17 +93,6 @@ export const compose = (...funcs) => ); -export const updateTableRow = (rowIndex, columnId, value) => (old) => { - return old.map((row, index) => { - if (index === rowIndex) { - return { - ...old[rowIndex], - [columnId]: value, - } - } - return row - }) -} export const getObjectDiff = (a, b) => { return _.reduce( a, @@ -600,4 +589,41 @@ export const amountPaymentEntries = (amount, entries) => { payment_amount: diff, }; }); +}; + +export const updateAutoAddNewLine = (defaultEntry, props) => (entries) => { + const newEntries = [...entries]; + const lastEntry = _.last(newEntries); + const newLine = props.filter((entryKey) => !isBlank(lastEntry[entryKey])); + + return newLine.length > 0 ? [...entries, defaultEntry] : [...entries]; +}; + +/** + * Ensure min entries lines. + * @param {number} min + * @param {any} defaultEntry + */ +export const updateMinEntriesLines = (min, defaultEntry) => (lines) => { + if (lines.length < min) { + const diffLines = Math.max(min - lines.length, 0); + return [...lines, ...repeatValue(defaultEntry, diffLines)]; + } +}; + +export const updateRemoveLineByIndex = (rowIndex) => (entries) => { + const removeIndex = parseInt(rowIndex, 10); + return entries.filter((row, index) => index !== removeIndex); +}; + +export const updateTableRow = (rowIndex, columnId, value) => (old) => { + return old.map((row, index) => { + if (index === rowIndex) { + return { + ...old[rowIndex], + [columnId]: value, + } + } + return row + }) }; \ No newline at end of file diff --git a/server/src/services/Sales/SalesReceipts.ts b/server/src/services/Sales/SalesReceipts.ts index bf5210239..25360d6ca 100644 --- a/server/src/services/Sales/SalesReceipts.ts +++ b/server/src/services/Sales/SalesReceipts.ts @@ -172,26 +172,6 @@ export default class SalesReceiptService { ); } - /** - * Retrieve estimate number to object model. - * @param {number} tenantId - * @param {ISaleReceiptDTO} saleReceiptDTO - Sale receipt DTO. - * @param {ISaleReceipt} oldSaleReceipt - Old receipt model object. - */ - transformReceiptNumberToModel( - tenantId: number, - saleReceiptDTO: ISaleReceiptDTO, - oldSaleReceipt?: ISaleReceipt - ): string { - // Retreive the next invoice number. - const autoNextNumber = this.getNextReceiptNumber(tenantId); - - if (saleReceiptDTO.receiptNumber) { - return saleReceiptDTO.receiptNumber; - } - return oldSaleReceipt ? oldSaleReceipt.receiptNumber : autoNextNumber; - } - /** * Transform create DTO object to model object. * @param {ISaleReceiptDTO} saleReceiptDTO - @@ -233,7 +213,7 @@ export default class SalesReceiptService { receiptNumber, // Avoid rewrite the deliver date in edit mode when already published. ...(saleReceiptDTO.closed && - !oldSaleReceipt.closedAt && { + !oldSaleReceipt?.closedAt && { closedAt: moment().toMySqlDateTime(), }), entries: saleReceiptDTO.entries.map((entry) => ({