Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bad78b0d3 | ||
|
|
d94d28f709 | ||
|
|
94e6b64944 | ||
|
|
d8e9be0246 | ||
|
|
7ef72e8955 | ||
|
|
d76cc3d2a2 | ||
|
|
3102329ac0 | ||
|
|
fd09ea12ff | ||
|
|
7bd09e7326 | ||
|
|
cd3105b320 | ||
|
|
91b848f158 |
12
src/common/moreVertOptions.js
Normal file
12
src/common/moreVertOptions.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import intl from 'react-intl-universal';
|
||||
|
||||
export const moreVertOptions = [
|
||||
{
|
||||
name: intl.get('bad_debt.dialog.bad_debt'),
|
||||
value: 'bad debt',
|
||||
},
|
||||
{
|
||||
name: intl.get('bad_debt.dialog.cancel_bad_debt'),
|
||||
value: 'cancel bad debt',
|
||||
},
|
||||
];
|
||||
@@ -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() {
|
||||
<ReceiptPdfPreviewDialog dialogName={'receipt-pdf-preview'} />
|
||||
<MoneyInDialog dialogName={'money-in'} />
|
||||
<MoneyOutDialog dialogName={'money-out'} />
|
||||
<BadDebtDialog dialogName={'write-off-bad-debt'} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
46
src/components/MoreVertMenutItems.js
Normal file
46
src/components/MoreVertMenutItems.js
Normal file
@@ -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 }) => (
|
||||
<MenuItem text={item.name} label={item.label} onClick={handleClick} />
|
||||
);
|
||||
const handleMenuSelect = (type) => {
|
||||
onItemSelect && onItemSelect(type);
|
||||
};
|
||||
|
||||
return (
|
||||
<Select
|
||||
items={items}
|
||||
itemRenderer={itemsRenderer}
|
||||
onItemSelect={handleMenuSelect}
|
||||
popoverProps={{
|
||||
minimal: true,
|
||||
position: Position.BOTTOM_LEFT,
|
||||
interactionKind: PopoverInteractionKind.CLICK,
|
||||
modifiers: {
|
||||
offset: { offset: '0, 4' },
|
||||
},
|
||||
}}
|
||||
filterable={false}
|
||||
>
|
||||
<Button
|
||||
text={text}
|
||||
icon={<Icon icon={'more-vert'} iconSize={16} />}
|
||||
minimal={true}
|
||||
{...buttonProps}
|
||||
/>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
export default MoreVertMenutItems;
|
||||
@@ -8,7 +8,7 @@ import intl from 'react-intl-universal';
|
||||
export function FormatDate({ value, format = 'YYYY MMM DD' }) {
|
||||
const localizedFormat = intl.get(`date_formats.${format}`);
|
||||
|
||||
return moment().format(localizedFormat);
|
||||
return moment(value).format(localizedFormat);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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';
|
||||
@@ -85,6 +86,7 @@ export * from './BankAccounts';
|
||||
export * from './IntersectionObserver'
|
||||
export * from './Datatable/CellForceWidth';
|
||||
export * from './Button';
|
||||
export * from './IntersectionObserver';
|
||||
|
||||
const Hint = FieldHint;
|
||||
|
||||
@@ -156,4 +158,5 @@ export {
|
||||
ItemsMultiSelect,
|
||||
Card,
|
||||
AvaterCell,
|
||||
MoreVertMenutItems,
|
||||
};
|
||||
|
||||
@@ -205,19 +205,19 @@ export default [
|
||||
},
|
||||
{
|
||||
text: <T id={'cash_flow.label.add_money_in'} />,
|
||||
href: '/',
|
||||
href: '/cashflow-accounts',
|
||||
},
|
||||
{
|
||||
text: <T id={'cash_flow.label.add_money_out'} />,
|
||||
href: '/',
|
||||
href: '/cashflow-accounts',
|
||||
},
|
||||
{
|
||||
text: <T id={'cash_flow.label.add_cash_account'} />,
|
||||
href: '/',
|
||||
href: '/cashflow-accounts',
|
||||
},
|
||||
{
|
||||
text: <T id={'cash_flow.label.add_bank_account'} />,
|
||||
href: '/',
|
||||
href: '/cashflow-accounts',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
68
src/containers/Alerts/Invoices/CancelBadDebtAlert.js
Normal file
68
src/containers/Alerts/Invoices/CancelBadDebtAlert.js
Normal file
@@ -0,0 +1,68 @@
|
||||
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';
|
||||
|
||||
/**
|
||||
* Cancel bad debt alert.
|
||||
*/
|
||||
function CancelBadDebtAlert({
|
||||
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('bad_debt.cancel_alert.success_message'),
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
closeAlert(name);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Alert
|
||||
cancelButtonText={<T id={'cancel'} />}
|
||||
confirmButtonText={<T id={'save'} />}
|
||||
intent={Intent.WARNING}
|
||||
isOpen={isOpen}
|
||||
onCancel={handleCancel}
|
||||
onConfirm={handleConfirm}
|
||||
loading={isLoading}
|
||||
>
|
||||
<p>
|
||||
<T id={'bad_debt.cancel_alert.message'} />
|
||||
</p>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withAlertStoreConnect(),
|
||||
withAlertActions,
|
||||
)(CancelBadDebtAlert);
|
||||
@@ -23,7 +23,7 @@ function AccountSwitchButton() {
|
||||
<AccountSwitchButtonBase
|
||||
minimal={true}
|
||||
rightIcon={<Icon icon={'arrow-drop-down'} iconSize={24} />}
|
||||
>
|
||||
>
|
||||
<AccountSwitchText>{currentAccount.name}</AccountSwitchText>
|
||||
</AccountSwitchButtonBase>
|
||||
);
|
||||
|
||||
20
src/containers/Dialogs/BadDebtDialog/BadDebtDialogContent.js
Normal file
20
src/containers/Dialogs/BadDebtDialog/BadDebtDialogContent.js
Normal 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>
|
||||
);
|
||||
}
|
||||
86
src/containers/Dialogs/BadDebtDialog/BadDebtForm.js
Normal file
86
src/containers/Dialogs/BadDebtDialog/BadDebtForm.js
Normal 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('bad_debt.dialog.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);
|
||||
17
src/containers/Dialogs/BadDebtDialog/BadDebtForm.schema.js
Normal file
17
src/containers/Dialogs/BadDebtDialog/BadDebtForm.schema.js
Normal 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;
|
||||
17
src/containers/Dialogs/BadDebtDialog/BadDebtFormContent.js
Normal file
17
src/containers/Dialogs/BadDebtDialog/BadDebtFormContent.js
Normal 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>
|
||||
);
|
||||
}
|
||||
121
src/containers/Dialogs/BadDebtDialog/BadDebtFormFields.js
Normal file
121
src/containers/Dialogs/BadDebtDialog/BadDebtFormFields.js
Normal 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={'bad_debt.dialog.header_note'} />
|
||||
</p>
|
||||
</Callout>
|
||||
|
||||
{/*------------ Written-off amount -----------*/}
|
||||
<FastField name={'amount'}>
|
||||
{({
|
||||
form: { values, setFieldValue },
|
||||
field: { value },
|
||||
meta: { error, touched },
|
||||
}) => (
|
||||
<FormGroup
|
||||
label={<T id={'bad_debt.dialog.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;
|
||||
@@ -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);
|
||||
46
src/containers/Dialogs/BadDebtDialog/BadDebtFormProvider.js
Normal file
46
src/containers/Dialogs/BadDebtDialog/BadDebtFormProvider.js
Normal 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 };
|
||||
29
src/containers/Dialogs/BadDebtDialog/index.js
Normal file
29
src/containers/Dialogs/BadDebtDialog/index.js
Normal 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={'bad_debt.dialog.bad_debt'} />}
|
||||
isOpen={isOpen}
|
||||
canEscapeJeyClose={true}
|
||||
autoFocus={true}
|
||||
className={'dialog--bad-debt'}
|
||||
>
|
||||
<DialogSuspense>
|
||||
<BadDebtDialogContent dialogName={dialogName} invoice={invoiceId} />
|
||||
</DialogSuspense>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
export default compose(withDialogRedux())(BadDebtDialog);
|
||||
17
src/containers/Dialogs/BadDebtDialog/utils.js
Normal file
17
src/containers/Dialogs/BadDebtDialog/utils.js
Normal 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,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -15,7 +15,6 @@ function MoneyInDialog({
|
||||
payload = { account_type: null, account_id: null, account_name: '' },
|
||||
isOpen,
|
||||
}) {
|
||||
console.log(payload, 'EE');
|
||||
return (
|
||||
<Dialog
|
||||
name={dialogName}
|
||||
|
||||
@@ -20,8 +20,6 @@ function UserForm({
|
||||
// #withDialogActions
|
||||
closeDialog,
|
||||
}) {
|
||||
|
||||
|
||||
const {
|
||||
dialogName,
|
||||
user,
|
||||
@@ -29,7 +27,7 @@ function UserForm({
|
||||
isEditMode,
|
||||
EditUserMutate,
|
||||
} = useUserFormContext();
|
||||
console.log(user, 'EE');
|
||||
|
||||
const initialValues = {
|
||||
...(isEditMode &&
|
||||
pick(
|
||||
|
||||
@@ -38,6 +38,11 @@ export default function BillDrawerDetails() {
|
||||
id={'landed_cost'}
|
||||
panel={<LocatedLandedCostTable />}
|
||||
/>
|
||||
{/* <Tab
|
||||
title={intl.get('payment_transactions')}
|
||||
id={'payment_transactions'}
|
||||
// panel={}
|
||||
/> */}
|
||||
</DrawerMainTabs>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -30,6 +30,11 @@ export default function InvoiceDetail() {
|
||||
id={'journal_entries'}
|
||||
panel={<JournalEntriesTable transactions={transactions} />}
|
||||
/>
|
||||
{/* <Tab
|
||||
title={intl.get('payment_transactions')}
|
||||
id={'payment_transactions'}
|
||||
// panel={}
|
||||
/> */}
|
||||
</DrawerMainTabs>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -6,20 +6,33 @@ import {
|
||||
NavbarGroup,
|
||||
Classes,
|
||||
NavbarDivider,
|
||||
Popover,
|
||||
PopoverInteractionKind,
|
||||
Position,
|
||||
Intent,
|
||||
MenuItem,
|
||||
Menu,
|
||||
} from '@blueprintjs/core';
|
||||
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
|
||||
|
||||
import { useInvoiceDetailDrawerContext } from './InvoiceDetailDrawerProvider';
|
||||
import { moreVertOptions } from '../../../common/moreVertOptions';
|
||||
|
||||
import withDialogActions from 'containers/Dialog/withDialogActions';
|
||||
import withAlertsActions from 'containers/Alert/withAlertActions';
|
||||
import withDrawerActions from 'containers/Drawer/withDrawerActions';
|
||||
|
||||
import { If, Icon, FormattedMessage as T } from 'components';
|
||||
import {
|
||||
If,
|
||||
Icon,
|
||||
FormattedMessage as T,
|
||||
// MoreVertMenutItems,
|
||||
} from 'components';
|
||||
|
||||
import { compose } from 'utils';
|
||||
|
||||
import { BadDebtMenuItem } from './utils';
|
||||
|
||||
/**
|
||||
* Invoice details action bar.
|
||||
*/
|
||||
@@ -59,6 +72,16 @@ function InvoiceDetailActionsBar({
|
||||
openDialog('quick-payment-receive', { invoiceId });
|
||||
};
|
||||
|
||||
// Handle write-off invoice.
|
||||
const handleBadDebtInvoice = () => {
|
||||
openDialog('write-off-bad-debt', { invoiceId });
|
||||
};
|
||||
|
||||
// Handle cancele write-off invoice.
|
||||
const handleCancelBadDebtInvoice = () => {
|
||||
openAlert('cancel-bad-debt', { invoiceId });
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardActionsBar>
|
||||
<NavbarGroup>
|
||||
@@ -91,6 +114,12 @@ function InvoiceDetailActionsBar({
|
||||
intent={Intent.DANGER}
|
||||
onClick={handleDeleteInvoice}
|
||||
/>
|
||||
<NavbarDivider />
|
||||
<BadDebtMenuItem
|
||||
invoice={invoice}
|
||||
onAlert={handleCancelBadDebtInvoice}
|
||||
onDialog={handleBadDebtInvoice}
|
||||
/>
|
||||
</NavbarGroup>
|
||||
</DashboardActionsBar>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import {
|
||||
Button,
|
||||
Popover,
|
||||
PopoverInteractionKind,
|
||||
Position,
|
||||
MenuItem,
|
||||
Menu,
|
||||
} from '@blueprintjs/core';
|
||||
import { Icon, FormattedMessage as T, Choose } from 'components';
|
||||
import { FormatNumberCell } from '../../../components';
|
||||
|
||||
/**
|
||||
@@ -48,3 +57,36 @@ export const useInvoiceReadonlyEntriesColumns = () =>
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
export const BadDebtMenuItem = ({ invoice, onDialog, onAlert }) => {
|
||||
return (
|
||||
<Popover
|
||||
minimal={true}
|
||||
interactionKind={PopoverInteractionKind.CLICK}
|
||||
position={Position.BOTTOM_LEFT}
|
||||
modifiers={{
|
||||
offset: { offset: '0, 4' },
|
||||
}}
|
||||
content={
|
||||
<Menu>
|
||||
<Choose>
|
||||
<Choose.When condition={!invoice.is_writtenoff}>
|
||||
<MenuItem
|
||||
text={<T id={'bad_debt.dialog.bad_debt'} />}
|
||||
onClick={onDialog}
|
||||
/>
|
||||
</Choose.When>
|
||||
<Choose.When condition={invoice.is_writtenoff}>
|
||||
<MenuItem
|
||||
onClick={onAlert}
|
||||
text={<T id={'bad_debt.dialog.cancel_bad_debt'} />}
|
||||
/>
|
||||
</Choose.When>
|
||||
</Choose>
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<Button icon={<Icon icon="more-vert" iconSize={16} />} minimal={true} />
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -32,7 +32,7 @@ export default function CashFlowStatementTable({
|
||||
|
||||
const rowClassNames = (row) => {
|
||||
return [
|
||||
`row-type--${row.original.rowTypes}`,
|
||||
`row-type--${row.original.row_types}`,
|
||||
`row-type--${row.original.id}`,
|
||||
];
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ const columnsMapper = (data, index, column) => ({
|
||||
id: column.key,
|
||||
key: column.key,
|
||||
Header: column.label,
|
||||
Cell: CellForceWidth,
|
||||
// Cell: CellForceWidth,
|
||||
accessor: `cells[${index}].value`,
|
||||
forceWidthAccess: `cells[0].value`,
|
||||
className: column.key,
|
||||
|
||||
@@ -7,10 +7,15 @@ const InvoiceDeliverAlert = React.lazy(() =>
|
||||
import('../../Alerts/Invoices/InvoiceDeliverAlert'),
|
||||
);
|
||||
|
||||
const CancelBadDebtAlert = React.lazy(() =>
|
||||
import('../../Alerts/Invoices/CancelBadDebtAlert'),
|
||||
);
|
||||
|
||||
/**
|
||||
* Invoices alert.
|
||||
*/
|
||||
export default [
|
||||
{ name: 'invoice-delete', component: InvoiceDeleteAlert },
|
||||
{ name: 'invoice-deliver', component: InvoiceDeliverAlert },
|
||||
{ name: 'cancel-bad-debt', component: CancelBadDebtAlert },
|
||||
];
|
||||
|
||||
@@ -9,6 +9,7 @@ import t from './types';
|
||||
const commonInvalidateQueries = (queryClient) => {
|
||||
// Invalidate invoices.
|
||||
queryClient.invalidateQueries(t.SALE_INVOICES);
|
||||
queryClient.invalidateQueries(t.SALE_INVOICE);
|
||||
|
||||
// Invalidate customers.
|
||||
queryClient.invalidateQueries(t.CUSTOMERS);
|
||||
@@ -194,3 +195,38 @@ export function useRefreshInvoices() {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function useCreateBadDebt(props) {
|
||||
const queryClient = useQueryClient();
|
||||
const apiRequest = useApiRequest();
|
||||
|
||||
return useMutation(
|
||||
([id, values]) => apiRequest.post(`sales/invoices/${id}/writeoff`, values),
|
||||
{
|
||||
onSuccess: (res, [id, values]) => {
|
||||
// Invalidate
|
||||
queryClient.invalidateQueries([t.BAD_DEBT, id]);
|
||||
|
||||
// Common invalidate queries.
|
||||
commonInvalidateQueries(queryClient);
|
||||
},
|
||||
...props,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function useCancelBadDebt(props) {
|
||||
const queryClient = useQueryClient();
|
||||
const apiRequest = useApiRequest();
|
||||
|
||||
return useMutation((id) => apiRequest.post(`sales/invoices/${id}/writeoff/cancel`), {
|
||||
onSuccess: (res, id) => {
|
||||
// Invalidate
|
||||
queryClient.invalidateQueries([t.CANCEL_BAD_DEBT, id]);
|
||||
|
||||
// Common invalidate queries.
|
||||
commonInvalidateQueries(queryClient);
|
||||
},
|
||||
...props,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -85,6 +85,8 @@ const SALE_INVOICES = {
|
||||
SALE_INVOICES: 'SALE_INVOICES',
|
||||
SALE_INVOICE: 'SALE_INVOICE',
|
||||
SALE_INVOICES_DUE: 'SALE_INVOICES_DUE',
|
||||
BAD_DEBT: 'BAD_DEBT',
|
||||
CANCEL_BAD_DEBT: 'CANCEL_BAD_DEBT',
|
||||
};
|
||||
|
||||
const USERS = {
|
||||
@@ -137,7 +139,8 @@ const CASH_FLOW_ACCOUNTS = {
|
||||
CASH_FLOW_ACCOUNTS: 'CASH_FLOW_ACCOUNTS',
|
||||
CASH_FLOW_TRANSACTIONS: 'CASH_FLOW_TRANSACTIONS',
|
||||
CASH_FLOW_TRANSACTION: 'CASH_FLOW_TRANSACTION',
|
||||
CASHFLOW_ACCOUNT_TRANSACTIONS_INFINITY: 'CASHFLOW_ACCOUNT_TRANSACTIONS_INFINITY'
|
||||
CASHFLOW_ACCOUNT_TRANSACTIONS_INFINITY:
|
||||
'CASHFLOW_ACCOUNT_TRANSACTIONS_INFINITY',
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
@@ -1012,6 +1012,8 @@
|
||||
"jump_to_the_bills": "انتقل إلي فواتير الشراء.",
|
||||
"jump_to_the_manual_journals": "انتقل إلي القيود اليدوية.",
|
||||
"jump_to_the_items": "انتقل إلي المنتجات.",
|
||||
"jump_to_the_add_money_in": "انتقل إلي إيداع إلي الحساب.",
|
||||
"jump_to_the_add_money_out": "انتقل إلي سحب من الحساب.",
|
||||
"jump_to_the_balance_sheet": "انتقل إلي تقرير الميزانية العمومية.",
|
||||
"jump_to_the_profit_loss_sheet": "انتقل إلي تقرير قائمة الدخل.",
|
||||
"jump_to_the_journal_sheet": "انتقل إلي دفتر اليومية العامة.",
|
||||
@@ -1371,7 +1373,6 @@
|
||||
"payment_made.empty_status.title": "المنشأة لم تدفع اي اموال إلي الموردين ، إلي حد الأن!.",
|
||||
"estimate.delete.error.estimate_converted_to_invoice": "لا يمكن حذف عملية عرض اسعار الذي تم تحويلها إلي فاتورة بيع.",
|
||||
"landed_cost.action.delete.success_message": "The landed cost has been deleted successfully.",
|
||||
|
||||
"items.option.only_active": "Only active",
|
||||
"items.option_all_items.hint": "جميع الاصناف ، بما في ذلك تلك الاصناف لديها رصيد صفر.",
|
||||
"items.option_with_transactions": "الاصناف مع معاملات",
|
||||
@@ -1379,22 +1380,18 @@
|
||||
"items.option_without_zero_balance": "الاصناف ذات رصيد صفر",
|
||||
"items.option_without_zero_balance.hint": "قم بتضمين الاصناف واستبعاد تلك التي لديها رصيد صفري.",
|
||||
"items.label_filter_items": "تصفية الاصناف",
|
||||
|
||||
"customers.option_all_customers.hint": "All customers, including that ones have zero-balance.",
|
||||
"customers.option_without_zero_balance": "Customers without zero-balance",
|
||||
"customers.option_without_zero_balance.hint": "Include customers and exclude that ones have zero-balance.",
|
||||
"customers.option_with_transactions": "Customers with transactions",
|
||||
"customers.option_with_transactions.hint": "Include customers that onces have transactions on the given date period only.",
|
||||
"customers.label_filter_customers": "Filter customers",
|
||||
|
||||
|
||||
"vendors.option_all_vendors.hint": "All vendors, including that ones have zero-balance.",
|
||||
"vendors.label_filter_vendors": "Filter Vendors",
|
||||
"vendors.option_without_zero_balance": "Vendors without zero-balance",
|
||||
"vendors.option_without_zero_balance.hint": "Include vendors and exclude that ones have zero-balance.",
|
||||
"vendors.option_with_transactions": "Vendors with transactions",
|
||||
"vendors.option_with_transactions.hint": "Include vendors that onces have transactions on the given date period only.",
|
||||
|
||||
"siebar.cashflow": "التدفق النقدي",
|
||||
"siebar.cashflow.label_cash_and_bank_accounts": "حسابات نقدية والمصارف ",
|
||||
"cash_flow.label_account_transcations": "معاملات الحساب",
|
||||
@@ -1405,7 +1402,6 @@
|
||||
"cash_flow.label.add_bank_account": "اضافة حساب مصرف",
|
||||
"cash_flow.label.add_money_in": "إيداع إلي الحساب",
|
||||
"cash_flow.label.add_money_out": "سحب من الحساب",
|
||||
|
||||
"cash_flow.owner_contribution": "زيادة رأس المال",
|
||||
"cash_flow.other_income": "إيراد أخر",
|
||||
"cash_flow.owner_drawings": "سحب رأس المال",
|
||||
@@ -1436,5 +1432,12 @@
|
||||
"cash_flow_money_in": "إيداع",
|
||||
"cash_flow_money_out": "سحب",
|
||||
"cash_flow_transaction.switch_item": " {value} معاملة علي حساب",
|
||||
"cash_flow_transaction.balance_in_bigcapital": "الرصيد في Bigcapital"
|
||||
"cash_flow_transaction.balance_in_bigcapital": "الرصيد في Bigcapital",
|
||||
"bad_debt.dialog.written_off_amount": "المبلغ المشطوب",
|
||||
"bad_debt.dialog.bad_debt": "الديون المعدومة",
|
||||
"bad_debt.dialog.cancel_bad_debt": "إلغاء الديون المعدومة",
|
||||
"bad_debt.dialog.header_note": "يمكن للبائع تحميل مبلغ الفاتورة على حساب مصروفات الديون المعدومة عندما يكون من المؤكد أن الفاتورة لن يتم دفعها.",
|
||||
"bad_debt.dialog.success_message":"تم شطب فاتورة البيع المقدمة بنجاح.",
|
||||
"bad_debt.cancel_alert.success_message":"تم إلغاء شطب فاتورة البيع المقدمة بنجاح.",
|
||||
"bad_debt.cancel_alert.message": "هل أنت متأكد أنك تريد شطب هذه الفاتورة؟ "
|
||||
}
|
||||
@@ -756,7 +756,7 @@
|
||||
"category_name_exists": "Category name exists",
|
||||
"some_customers_have_sales_invoices": "Some customers have sales invoices",
|
||||
"inventory_adjustments": "Inventory adjustments",
|
||||
"make_adjustment": "Make a adjustment",
|
||||
"make_adjustment": "Make an adjustment",
|
||||
"adjustment_type": "Adjustment type",
|
||||
"decrement": "Decrement",
|
||||
"new_quantity": "New quantity",
|
||||
@@ -1420,5 +1420,13 @@
|
||||
"AP_aging_summary.filter_options.label": "Filter vendors",
|
||||
"item.error.type_cannot_change_with_item_has_transactions": "Cannot change item type to inventory with item has associated transactions.",
|
||||
"item.error.cannot_change_inventory_account": "Cannot change item inventory account while the item has transactions.",
|
||||
"customer.link.customer_details": "Customer details ({amount})"
|
||||
}
|
||||
"customer.link.customer_details": "Customer details ({amount})",
|
||||
"bad_debt.dialog.written_off_amount": "Written-off amount",
|
||||
"bad_debt.dialog.bad_debt": "Bad debt",
|
||||
"bad_debt.dialog.cancel_bad_debt": "Cancel bad debt",
|
||||
"bad_debt.dialog.header_note": "The seller can charge the amount of an invoice to the bad debt expense account when it is certain that the invoice will not be paid.",
|
||||
"bad_debt.dialog.success_message":"The given sale invoice has been writte-off successfully.",
|
||||
"bad_debt.cancel_alert.success_message":"The given sale invoice has been canceled write-off successfully.",
|
||||
"bad_debt.cancel_alert.message": "Are you sure you want to write off this invoice?"
|
||||
|
||||
}
|
||||
|
||||
@@ -502,5 +502,11 @@ export default {
|
||||
'arrow-upward': {
|
||||
path: ['M4 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z'],
|
||||
viewBox: '0 0 24 24',
|
||||
}
|
||||
},
|
||||
'more-vert': {
|
||||
path: [
|
||||
'M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z',
|
||||
],
|
||||
viewBox: '0 0 24 24',
|
||||
},
|
||||
};
|
||||
|
||||
34
src/style/pages/BadDebt/BadDebtDialog.scss
Normal file
34
src/style/pages/BadDebt/BadDebtDialog.scss
Normal file
@@ -0,0 +1,34 @@
|
||||
.dialog--bad-debt {
|
||||
max-width: 400px;
|
||||
|
||||
.bp3-dialog-body {
|
||||
.bp3-form-group {
|
||||
margin-bottom: 15px;
|
||||
margin-top: 15px;
|
||||
|
||||
label.bp3-label {
|
||||
margin-bottom: 3px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.bp3-callout {
|
||||
font-size: 13px;
|
||||
}
|
||||
.form-group {
|
||||
&--reason {
|
||||
.bp3-form-content {
|
||||
textarea {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bp3-dialog-footer {
|
||||
padding-top: 10px;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user