feat: Bad Debt.

This commit is contained in:
elforjani13
2021-11-01 20:24:01 +02:00
parent 613454a862
commit 91b848f158
25 changed files with 734 additions and 12 deletions

View File

@@ -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 (
<BadDebtFormProvider invoiceId={invoice} dialogName={dialogName}>
<BadDebtForm />
</BadDebtFormProvider>
);
}

View File

@@ -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 (
<Formik
validationSchema={CreateBadDebtFormSchema}
initialValues={initialValues}
onSubmit={handleFormSubmit}
component={BadDebtFormContent}
/>
);
}
export default compose(
withDialogActions,
withCurrentOrganization(),
)(BadDebtForm);

View File

@@ -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;

View File

@@ -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 (
<Form>
<BadDebtFormFields />
<BadDebtFormFloatingActions />
</Form>
);
}

View File

@@ -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 (
<div className={Classes.DIALOG_BODY}>
<Callout intent={Intent.PRIMARY}>
<p>
<T id={'badDebt_the_seller_can_charge_the_amount_of_an_invoice'} />
</p>
</Callout>
{/*------------ Written-off amount -----------*/}
<FastField name={'amount'}>
{({
form: { values, setFieldValue },
field: { value },
meta: { error, touched },
}) => (
<FormGroup
label={<T id={'badDebt.label_written_off_amount'} />}
labelInfo={<FieldRequiredHint />}
className={classNames('form-group--amount', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="amount" />}
>
<ControlGroup>
<InputPrependText text={values.currency_code} />
<MoneyInputGroup
value={value}
minimal={true}
onChange={(amount) => {
setFieldValue('amount', amount);
}}
intent={inputIntent({ error, touched })}
disabled={amountfieldRef}
/>
</ControlGroup>
</FormGroup>
)}
</FastField>
{/*------------ Expense account -----------*/}
<FastField name={'expense_account_id'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'expense_account_id'} />}
className={classNames(
'form-group--expense_account_id',
'form-group--select-list',
CLASSES.FILL,
)}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'expense_account_id'} />}
>
<AccountsSuggestField
selectedAccountId={value}
accounts={accounts}
onAccountSelected={({ id }) =>
form.setFieldValue('expense_account_id', id)
}
filterByTypes={[ACCOUNT_TYPE.EXPENSE]}
/>
</FormGroup>
)}
</FastField>
{/*------------ reason -----------*/}
<FastField name={'reason'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'reason'} />}
labelInfo={<FieldRequiredHint />}
className={'form-group--reason'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'reason'} />}
>
<TextArea
growVertically={true}
large={true}
intent={inputIntent({ error, touched })}
{...field}
/>
</FormGroup>
)}
</FastField>
</div>
);
}
export default BadDebtFormFields;

View File

@@ -0,0 +1,48 @@
import React from 'react';
import { Intent, Button, Classes } from '@blueprintjs/core';
import { useFormikContext } from 'formik';
import { FormattedMessage as T } from 'components';
import { useBadDebtContext } from './BadDebtFormProvider';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
/**
* Bad bebt form floating actions.
*/
function BadDebtFormFloatingActions({
// #withDialogActions
closeDialog,
}) {
// bad debt invoice dialog context.
const { dialogName } = useBadDebtContext();
// Formik context.
const { isSubmitting } = useFormikContext();
// Handle close button click.
const handleCancelBtnClick = () => {
closeDialog(dialogName);
};
return (
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button onClick={handleCancelBtnClick} style={{ minWidth: '75px' }}>
<T id={'cancel'} />
</Button>
<Button
intent={Intent.PRIMARY}
loading={isSubmitting}
style={{ minWidth: '75px' }}
type="submit"
>
{<T id={'save'} />}
</Button>
</div>
</div>
);
}
export default compose(withDialogActions)(BadDebtFormFloatingActions);

View File

@@ -0,0 +1,46 @@
import React from 'react';
import { DialogContent } from 'components';
import {
useAccounts,
useInvoice,
useCreateBadDebt,
useCancelBadDebt,
} from 'hooks/query';
const BadDebtContext = React.createContext();
/**
* Bad debt provider.
*/
function BadDebtFormProvider({ invoiceId, dialogName, ...props }) {
// Handle fetch accounts data.
const { data: accounts, isLoading: isAccountsLoading } = useAccounts();
// Handle fetch invoice data.
const { data: invoice, isLoading: isInvoiceLoading } = useInvoice(invoiceId, {
enabled: !!invoiceId,
});
// Create and cancel bad debt mutations.
const { mutateAsync: createBadDebtMutate } = useCreateBadDebt();
// State provider.
const provider = {
accounts,
invoice,
invoiceId,
dialogName,
createBadDebtMutate,
};
return (
<DialogContent isLoading={isAccountsLoading || isInvoiceLoading}>
<BadDebtContext.Provider value={provider} {...props} />
</DialogContent>
);
}
const useBadDebtContext = () => React.useContext(BadDebtContext);
export { BadDebtFormProvider, useBadDebtContext };

View File

@@ -0,0 +1,29 @@
import React from 'react';
import { FormattedMessage as T } from 'components';
import { Dialog, DialogSuspense } from 'components';
import withDialogRedux from 'components/DialogReduxConnect';
import { compose } from 'redux';
const BadDebtDialogContent = React.lazy(() => import('./BadDebtDialogContent'));
/**
* Bad debt dialog.
*/
function BadDebtDialog({ dialogName, payload: { invoiceId = null }, isOpen }) {
return (
<Dialog
name={dialogName}
title={<T id={'badDebt.label'} />}
isOpen={isOpen}
canEscapeJeyClose={true}
autoFocus={true}
className={'dialog--bad-debt'}
>
<DialogSuspense>
<BadDebtDialogContent dialogName={dialogName} invoice={invoiceId} />
</DialogSuspense>
</Dialog>
);
}
export default compose(withDialogRedux())(BadDebtDialog);

View File

@@ -0,0 +1,17 @@
import React from 'react';
import { Intent } from '@blueprintjs/core';
import { AppToaster } from 'components';
/**
* Transformes the response errors types.
*/
export const transformErrors = (errors, { setErrors }) => {
if (errors.some(({ type }) => type === 'SALE_INVOICE_ALREADY_WRITTEN_OFF')) {
AppToaster.show({
message: 'SALE_INVOICE_ALREADY_WRITTEN_OFF',
// message: intl.get(''),
intent: Intent.DANGER,
});
}
};