From 91b848f158d80f98cbe0f7789fb4443c24521eae Mon Sep 17 00:00:00 2001 From: elforjani13 <39470382+elforjani13@users.noreply.github.com> Date: Mon, 1 Nov 2021 20:24:01 +0200 Subject: [PATCH 1/3] feat: Bad Debt. --- src/common/moreVertOptions.js | 12 ++ src/components/DialogsContainer.js | 2 + src/components/MoreVertMenutItems.js | 46 +++++++ src/components/index.js | 4 +- .../Alerts/Invoices/BadDebtAlert.js | 65 ++++++++++ .../BadDebtDialog/BadDebtDialogContent.js | 20 +++ .../Dialogs/BadDebtDialog/BadDebtForm.js | 86 +++++++++++++ .../BadDebtDialog/BadDebtForm.schema.js | 17 +++ .../BadDebtDialog/BadDebtFormContent.js | 17 +++ .../BadDebtDialog/BadDebtFormFields.js | 121 ++++++++++++++++++ .../BadDebtFormFloatingActions.js | 48 +++++++ .../BadDebtDialog/BadDebtFormProvider.js | 46 +++++++ src/containers/Dialogs/BadDebtDialog/index.js | 29 +++++ src/containers/Dialogs/BadDebtDialog/utils.js | 17 +++ .../Drawers/BillDrawer/BillDrawerDetails.js | 5 + .../InvoiceDetailDrawer/BadDebtMenuItem.js | 70 ++++++++++ .../InvoiceDetailDrawer/InvoiceDetail.js | 5 + .../InvoiceDetailActionsBar.js | 24 +++- .../Sales/Invoices/InvoicesAlerts.js | 5 + src/hooks/query/invoices.js | 35 +++++ src/hooks/query/types.js | 5 +- src/lang/ar/index.json | 13 +- src/lang/en/index.json | 12 +- src/static/json/icons.js | 8 +- src/style/pages/BadDebt/BadDebtDialog.scss | 34 +++++ 25 files changed, 734 insertions(+), 12 deletions(-) create mode 100644 src/common/moreVertOptions.js create mode 100644 src/components/MoreVertMenutItems.js create mode 100644 src/containers/Alerts/Invoices/BadDebtAlert.js create mode 100644 src/containers/Dialogs/BadDebtDialog/BadDebtDialogContent.js create mode 100644 src/containers/Dialogs/BadDebtDialog/BadDebtForm.js create mode 100644 src/containers/Dialogs/BadDebtDialog/BadDebtForm.schema.js create mode 100644 src/containers/Dialogs/BadDebtDialog/BadDebtFormContent.js create mode 100644 src/containers/Dialogs/BadDebtDialog/BadDebtFormFields.js create mode 100644 src/containers/Dialogs/BadDebtDialog/BadDebtFormFloatingActions.js create mode 100644 src/containers/Dialogs/BadDebtDialog/BadDebtFormProvider.js create mode 100644 src/containers/Dialogs/BadDebtDialog/index.js create mode 100644 src/containers/Dialogs/BadDebtDialog/utils.js create mode 100644 src/containers/Drawers/InvoiceDetailDrawer/BadDebtMenuItem.js create mode 100644 src/style/pages/BadDebt/BadDebtDialog.scss diff --git a/src/common/moreVertOptions.js b/src/common/moreVertOptions.js new file mode 100644 index 000000000..62a15a755 --- /dev/null +++ b/src/common/moreVertOptions.js @@ -0,0 +1,12 @@ +import intl from 'react-intl-universal'; + +export const moreVertOptions = [ + { + name: intl.get('badDebt.label'), + value: 'bad debt', + }, + { + name: intl.get('badDebt.label_cancel_bad_debt'), + value: 'cancel bad debt', + }, +]; diff --git a/src/components/DialogsContainer.js b/src/components/DialogsContainer.js index 07c00ca6f..f1e803fff 100644 --- a/src/components/DialogsContainer.js +++ b/src/components/DialogsContainer.js @@ -19,6 +19,7 @@ import EstimatePdfPreviewDialog from 'containers/Dialogs/EstimatePdfPreviewDialo import ReceiptPdfPreviewDialog from '../containers/Dialogs/ReceiptPdfPreviewDialog'; import MoneyInDialog from '../containers/Dialogs/MoneyInDialog'; import MoneyOutDialog from '../containers/Dialogs/MoneyOutDialog'; +import BadDebtDialog from '../containers/Dialogs/BadDebtDialog'; /** * Dialogs container. @@ -44,6 +45,7 @@ export default function DialogsContainer() { + ); } diff --git a/src/components/MoreVertMenutItems.js b/src/components/MoreVertMenutItems.js new file mode 100644 index 000000000..9282f33c1 --- /dev/null +++ b/src/components/MoreVertMenutItems.js @@ -0,0 +1,46 @@ +import React from 'react'; +import { + Button, + PopoverInteractionKind, + MenuItem, + Position, +} from '@blueprintjs/core'; + +import { Select } from '@blueprintjs/select'; +import { Icon } from 'components'; + +function MoreVertMenutItems({ text, items, onItemSelect, buttonProps }) { + // Menu items renderer. + const itemsRenderer = (item, { handleClick, modifiers, query }) => ( + + ); + const handleMenuSelect = (type) => { + onItemSelect && onItemSelect(type); + }; + + return ( + + ); +} + +export default MoreVertMenutItems; diff --git a/src/components/index.js b/src/components/index.js index 6d78330e3..36ad2797e 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -61,6 +61,7 @@ import Card from './Card'; import AvaterCell from './AvaterCell'; import { ItemsMultiSelect } from './Items'; +import MoreVertMenutItems from './MoreVertMenutItems'; export * from './Menu'; export * from './AdvancedFilter/AdvancedFilterDropdown'; @@ -82,7 +83,7 @@ export * from './MultiSelectTaggable'; export * from './Utils/FormatNumber'; export * from './Utils/FormatDate'; export * from './BankAccounts'; -export * from './IntersectionObserver' +export * from './IntersectionObserver'; const Hint = FieldHint; @@ -154,4 +155,5 @@ export { ItemsMultiSelect, Card, AvaterCell, + MoreVertMenutItems, }; diff --git a/src/containers/Alerts/Invoices/BadDebtAlert.js b/src/containers/Alerts/Invoices/BadDebtAlert.js new file mode 100644 index 000000000..b3d05c083 --- /dev/null +++ b/src/containers/Alerts/Invoices/BadDebtAlert.js @@ -0,0 +1,65 @@ +import React from 'react'; +import { FormattedMessage as T } from 'components'; +import intl from 'react-intl-universal'; +import { Intent, Alert } from '@blueprintjs/core'; +import { AppToaster } from 'components'; +import { useCancelBadDebt } from 'hooks/query'; + +import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect'; +import withAlertActions from 'containers/Alert/withAlertActions'; + +import { compose } from 'utils'; + +/** + * bad debt alert. + */ +function BadDebtAlert({ + name, + + // #withAlertStoreConnect + isOpen, + payload: { invoiceId }, + + // #withAlertActions + closeAlert, +}) { + // handle cancel alert. + const handleCancel = () => { + closeAlert(name); + }; + + const { mutateAsync: cancelBadDebtMutate, isLoading } = useCancelBadDebt(); + + // handleConfirm alert. + const handleConfirm = () => { + cancelBadDebtMutate(invoiceId) + .then(() => { + AppToaster.show({ + message: intl.get('the_invoice_has_been_deleted_successfully'), + intent: Intent.SUCCESS, + }); + }) + .catch(() => {}) + .finally(() => { + closeAlert(name); + }); + }; + + return ( + } + confirmButtonText={} + intent={Intent.WARNING} + isOpen={isOpen} + onCancel={handleCancel} + onConfirm={handleConfirm} + loading={isLoading} + > +

+ +

+
+ ); +} + +export default compose(withAlertStoreConnect(), withAlertActions)(BadDebtAlert); diff --git a/src/containers/Dialogs/BadDebtDialog/BadDebtDialogContent.js b/src/containers/Dialogs/BadDebtDialog/BadDebtDialogContent.js new file mode 100644 index 000000000..1e4b205b0 --- /dev/null +++ b/src/containers/Dialogs/BadDebtDialog/BadDebtDialogContent.js @@ -0,0 +1,20 @@ +import React from 'react'; + +import 'style/pages/BadDebt/BadDebtDialog.scss'; +import { BadDebtFormProvider } from './BadDebtFormProvider'; +import BadDebtForm from './BadDebtForm'; + +/** + * Bad debt dialog content. + */ +export default function BadDebtDialogContent({ + // #ownProps + dialogName, + invoice, +}) { + return ( + + + + ); +} diff --git a/src/containers/Dialogs/BadDebtDialog/BadDebtForm.js b/src/containers/Dialogs/BadDebtDialog/BadDebtForm.js new file mode 100644 index 000000000..5b579db28 --- /dev/null +++ b/src/containers/Dialogs/BadDebtDialog/BadDebtForm.js @@ -0,0 +1,86 @@ +import React from 'react'; +import intl from 'react-intl-universal'; + +import { Formik } from 'formik'; +import { Intent } from '@blueprintjs/core'; +import { omit } from 'lodash'; + +import { AppToaster } from 'components'; +import { CreateBadDebtFormSchema } from './BadDebtForm.schema'; +import { transformErrors } from './utils'; + +import BadDebtFormContent from './BadDebtFormContent'; + +import withDialogActions from 'containers/Dialog/withDialogActions'; +import withCurrentOrganization from 'containers/Organization/withCurrentOrganization'; + +import { useBadDebtContext } from './BadDebtFormProvider'; + +import { compose } from 'utils'; + +const defaultInitialValues = { + expense_account_id: '', + reason: '', + amount: '', +}; + +function BadDebtForm({ + // #withDialogActions + closeDialog, + + // #withCurrentOrganization + organization: { base_currency }, +}) { + const { invoice, dialogName, createBadDebtMutate, cancelBadDebtMutate } = + useBadDebtContext(); + + // Initial form values + const initialValues = { + ...defaultInitialValues, + currency_code: base_currency, + amount: invoice.due_amount, + }; + + // Handles the form submit. + const handleFormSubmit = (values, { setSubmitting, setErrors }) => { + const form = { + ...omit(values, ['currency_code']), + }; + + // Handle request response success. + const onSuccess = (response) => { + AppToaster.show({ + message: intl.get('badDebt_success_message'), + intent: Intent.SUCCESS, + }); + closeDialog(dialogName); + }; + + // Handle request response errors. + const onError = ({ + response: { + data: { errors }, + }, + }) => { + if (errors) { + transformErrors(errors, { setErrors }); + } + setSubmitting(false); + }; + createBadDebtMutate([invoice.id, form]).then(onSuccess).catch(onError); + }; + + return ( + + ); +} + +export default compose( + withDialogActions, + withCurrentOrganization(), +)(BadDebtForm); diff --git a/src/containers/Dialogs/BadDebtDialog/BadDebtForm.schema.js b/src/containers/Dialogs/BadDebtDialog/BadDebtForm.schema.js new file mode 100644 index 000000000..dc3d00f61 --- /dev/null +++ b/src/containers/Dialogs/BadDebtDialog/BadDebtForm.schema.js @@ -0,0 +1,17 @@ +import * as Yup from 'yup'; +import intl from 'react-intl-universal'; +import { DATATYPES_LENGTH } from 'common/dataTypes'; + +const Schema = Yup.object().shape({ + expense_account_id: Yup.number() + .required() + .label(intl.get('expense_account_id')), + amount: Yup.number().required().label(intl.get('amount')), + reason: Yup.string() + .required() + .min(3) + .max(DATATYPES_LENGTH.TEXT) + .label(intl.get('reason')), +}); + +export const CreateBadDebtFormSchema = Schema; diff --git a/src/containers/Dialogs/BadDebtDialog/BadDebtFormContent.js b/src/containers/Dialogs/BadDebtDialog/BadDebtFormContent.js new file mode 100644 index 000000000..568c04a8c --- /dev/null +++ b/src/containers/Dialogs/BadDebtDialog/BadDebtFormContent.js @@ -0,0 +1,17 @@ +import React from 'react'; +import { Form } from 'formik'; + +import BadDebtFormFields from './BadDebtFormFields'; +import BadDebtFormFloatingActions from './BadDebtFormFloatingActions'; + +/** + * Bad debt form content. + */ +export default function BadDebtFormContent() { + return ( +
+ + + + ); +} diff --git a/src/containers/Dialogs/BadDebtDialog/BadDebtFormFields.js b/src/containers/Dialogs/BadDebtDialog/BadDebtFormFields.js new file mode 100644 index 000000000..2d52b4a3c --- /dev/null +++ b/src/containers/Dialogs/BadDebtDialog/BadDebtFormFields.js @@ -0,0 +1,121 @@ +import React from 'react'; +import { FastField, ErrorMessage } from 'formik'; +import { FormattedMessage as T } from 'components'; + +import { useAutofocus } from 'hooks'; +import { + Classes, + FormGroup, + TextArea, + ControlGroup, + Callout, + Intent, +} from '@blueprintjs/core'; +import classNames from 'classnames'; +import { CLASSES } from 'common/classes'; +import { ACCOUNT_TYPE } from 'common/accountTypes'; +import { inputIntent } from 'utils'; +import { + AccountsSuggestField, + InputPrependText, + MoneyInputGroup, + FieldRequiredHint, +} from 'components'; + +import { useBadDebtContext } from './BadDebtFormProvider'; + +/** + * Bad debt form fields. + */ +function BadDebtFormFields() { + const amountfieldRef = useAutofocus(); + + const { accounts } = useBadDebtContext(); + + return ( +
+ +

+ +

+
+ + {/*------------ Written-off amount -----------*/} + + {({ + form: { values, setFieldValue }, + field: { value }, + meta: { error, touched }, + }) => ( + } + labelInfo={} + className={classNames('form-group--amount', CLASSES.FILL)} + intent={inputIntent({ error, touched })} + helperText={} + > + + + + { + setFieldValue('amount', amount); + }} + intent={inputIntent({ error, touched })} + disabled={amountfieldRef} + /> + + + )} + + {/*------------ Expense account -----------*/} + + {({ form, field: { value }, meta: { error, touched } }) => ( + } + className={classNames( + 'form-group--expense_account_id', + 'form-group--select-list', + CLASSES.FILL, + )} + labelInfo={} + intent={inputIntent({ error, touched })} + helperText={} + > + + form.setFieldValue('expense_account_id', id) + } + filterByTypes={[ACCOUNT_TYPE.EXPENSE]} + /> + + )} + + {/*------------ reason -----------*/} + + {({ field, meta: { error, touched } }) => ( + } + labelInfo={} + className={'form-group--reason'} + intent={inputIntent({ error, touched })} + helperText={} + > +