diff --git a/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceDialogContent.js b/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceDialogContent.js new file mode 100644 index 000000000..6109cc66e --- /dev/null +++ b/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceDialogContent.js @@ -0,0 +1,25 @@ +import React from 'react'; + +import 'style/pages/CustomerOpeningBalance/CustomerOpeningBalance.scss'; + +import CustomerOpeningBalanceForm from './CustomerOpeningBalanceForm'; +import { CustomerOpeningBalanceFormProvider } from './CustomerOpeningBalanceFormProvider'; + +/** + * Customer opening balance dialog content. + * @returns + */ +export default function CustomerOpeningBalanceDialogContent({ + // #ownProps + dialogName, + customerId, +}) { + return ( + + + + ); +} diff --git a/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceFields.js b/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceFields.js new file mode 100644 index 000000000..e79bd4d37 --- /dev/null +++ b/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceFields.js @@ -0,0 +1,115 @@ +import React from 'react'; +import { Classes, Position, FormGroup, ControlGroup } from '@blueprintjs/core'; +import { DateInput } from '@blueprintjs/datetime'; +import { isEqual } from 'lodash'; +import { FastField, useFormikContext } from 'formik'; +import { momentFormatter, tansformDateValue, handleDateChange } from 'utils'; +import { Features } from 'common'; +import classNames from 'classnames'; + +import { + If, + Icon, + FormattedMessage as T, + ExchangeRateMutedField, + BranchSelect, + BranchSelectButton, + FeatureCan, + InputPrependText, +} from 'components'; +import { FMoneyInputGroup, FFormGroup } from '../../../components/Forms'; + +import { useCustomerOpeningBalanceContext } from './CustomerOpeningBalanceFormProvider'; +import { useSetPrimaryBranchToForm } from './utils'; + +import withCurrentOrganization from 'containers/Organization/withCurrentOrganization'; +import { compose } from 'utils'; + +/** + * Customer Opening balance fields. + * @returns + */ +function CustomerOpeningBalanceFields({ + // #withCurrentOrganization + organization: { base_currency }, +}) { + // Formik context. + const { values } = useFormikContext(); + + const { branches, customer } = useCustomerOpeningBalanceContext(); + + // Sets the primary branch to form. + useSetPrimaryBranchToForm(); + + return ( +
+ {/*------------ Opening balance -----------*/} + } + > + + + + + + + {/*------------ Opening balance at -----------*/} + + {({ form, field: { value } }) => ( + + } + className={Classes.FILL} + > + { + form.setFieldValue('opening_balance_at', formattedDate); + })} + value={tansformDateValue(value)} + popoverProps={{ position: Position.BOTTOM, minimal: true }} + inputProps={{ + leftIcon: , + }} + /> + + )} + + + + {/*------------ Opening balance exchange rate -----------*/} + + + + {/*------------ Opening balance branch id -----------*/} + + } + name={'opening_balance_branch_id'} + className={classNames('form-group--select-list', Classes.FILL)} + > + + + +
+ ); +} +export default compose(withCurrentOrganization())(CustomerOpeningBalanceFields); diff --git a/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceForm.js b/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceForm.js new file mode 100644 index 000000000..2e9715b26 --- /dev/null +++ b/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceForm.js @@ -0,0 +1,81 @@ +import React from 'react'; +import moment from 'moment'; +import intl from 'react-intl-universal'; +import { Formik } from 'formik'; +import { Intent } from '@blueprintjs/core'; + +import { AppToaster } from 'components'; +import { CreateCustomerOpeningBalanceFormSchema } from './CustomerOpeningBalanceForm.schema'; +import { useCustomerOpeningBalanceContext } from './CustomerOpeningBalanceFormProvider'; + +import CustomerOpeningBalanceFormContent from './CustomerOpeningBalanceFormContent'; +import withDialogActions from 'containers/Dialog/withDialogActions'; + +import { compose } from 'utils'; + +const defaultInitialValues = { + opening_balance: '0', + opening_balance_branch_id: '', + opening_balance_exchange_rate: 1, + opening_balance_at: moment(new Date()).format('YYYY-MM-DD'), +}; + +/** + * Customer Opening balance form. + * @returns + */ +function CustomerOpeningBalanceForm({ + // #withDialogActions + closeDialog, +}) { + const { dialogName, customer, editCustomerOpeningBalanceMutate } = + useCustomerOpeningBalanceContext(); + + // Initial form values + const initialValues = { + ...defaultInitialValues, + ...customer, + }; + + // Handles the form submit. + const handleFormSubmit = (values, { setSubmitting, setErrors }) => { + const formValues = { + ...values, + }; + + // Handle request response success. + const onSuccess = (response) => { + AppToaster.show({ + message: intl.get('customer_opening_balance.success_message'), + intent: Intent.SUCCESS, + }); + closeDialog(dialogName); + }; + + // Handle request response errors. + const onError = ({ + response: { + data: { errors }, + }, + }) => { + if (errors) { + } + setSubmitting(false); + }; + + editCustomerOpeningBalanceMutate([customer.id, formValues]) + .then(onSuccess) + .catch(onError); + }; + + return ( + + ); +} + +export default compose(withDialogActions)(CustomerOpeningBalanceForm); diff --git a/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceForm.schema.js b/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceForm.schema.js new file mode 100644 index 000000000..a2f608eb0 --- /dev/null +++ b/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceForm.schema.js @@ -0,0 +1,10 @@ +import * as Yup from 'yup'; + +const Schema = Yup.object().shape({ + opening_balance_branch_id: Yup.string(), + opening_balance: Yup.number().nullable(), + opening_balance_at: Yup.date(), + opening_balance_exchange_rate: Yup.number(), +}); + +export const CreateCustomerOpeningBalanceFormSchema = Schema; diff --git a/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceFormContent.js b/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceFormContent.js new file mode 100644 index 000000000..fb1318af6 --- /dev/null +++ b/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceFormContent.js @@ -0,0 +1,18 @@ +import React from 'react'; +import { Form } from 'formik'; + +import CustomerOpeningBalanceFields from './CustomerOpeningBalanceFields'; +import CustomerOpeningBalanceFormFloatingActions from './CustomerOpeningBalanceFormFloatingActions'; + +/** + * Customer Opening balance form content. + * @returns + */ +export default function CustomerOpeningBalanceFormContent() { + return ( +
+ + + + ); +} diff --git a/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceFormFloatingActions.js b/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceFormFloatingActions.js new file mode 100644 index 000000000..12514d7b6 --- /dev/null +++ b/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceFormFloatingActions.js @@ -0,0 +1,50 @@ +import React from 'react'; +import { Intent, Button, Classes } from '@blueprintjs/core'; +import { useFormikContext } from 'formik'; +import { FormattedMessage as T } from 'components'; + +import { useCustomerOpeningBalanceContext } from './CustomerOpeningBalanceFormProvider'; +import withDialogActions from 'containers/Dialog/withDialogActions'; +import { compose } from 'utils'; + + +/** + * Customer Opening balance floating actions. + * @returns + */ +function CustomerOpeningBalanceFormFloatingActions({ + // #withDialogActions + closeDialog, +}) { + // dialog context. + const { dialogName } = useCustomerOpeningBalanceContext(); + + // Formik context. + const { isSubmitting } = useFormikContext(); + + // Handle close button click. + const handleCancelBtnClick = () => { + closeDialog(dialogName); + }; + + return ( +
+
+ + +
+
+ ); +} +export default compose(withDialogActions)( + CustomerOpeningBalanceFormFloatingActions, +); diff --git a/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceFormProvider.js b/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceFormProvider.js new file mode 100644 index 000000000..62d070245 --- /dev/null +++ b/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceFormProvider.js @@ -0,0 +1,73 @@ +import React from 'react'; +import { DialogContent } from 'components'; +import { + useBranches, + useCustomer, + useEditCustomerOpeningBalance, +} from 'hooks/query'; +import { useFeatureCan } from 'hooks/state'; +import { Features } from 'common'; +import { pick } from 'lodash'; + +const CustomerOpeningBalanceContext = React.createContext(); + +/** + * Customer opening balance provider. + * @returns + */ +function CustomerOpeningBalanceFormProvider({ + query, + customerId, + dialogName, + ...props +}) { + // Features guard. + const { featureCan } = useFeatureCan(); + const isBranchFeatureCan = featureCan(Features.Branches); + + const { mutateAsync: editCustomerOpeningBalanceMutate } = + useEditCustomerOpeningBalance(); + + // Fetches the branches list. + const { + data: branches, + isLoading: isBranchesLoading, + isSuccess: isBranchesSuccess, + } = useBranches(query, { enabled: isBranchFeatureCan }); + + // Handle fetch customer details. + const { data: customer, isLoading: isCustomerLoading } = useCustomer( + customerId, + { enabled: !!customerId }, + ); + + // State provider. + const provider = { + branches, + customer: { + ...pick(customer, [ + 'id', + 'opening_balance', + 'opening_balance_exchange_rate', + 'currency_code', + ]), + // opening_balance_at: customer.formatted_opening_balance_at, + }, + + isBranchesSuccess, + isBranchesLoading, + dialogName, + editCustomerOpeningBalanceMutate, + }; + + return ( + + + + ); +} + +const useCustomerOpeningBalanceContext = () => + React.useContext(CustomerOpeningBalanceContext); + +export { CustomerOpeningBalanceFormProvider, useCustomerOpeningBalanceContext }; diff --git a/src/containers/Dialogs/CustomerOpeningBalanceDialog/index.js b/src/containers/Dialogs/CustomerOpeningBalanceDialog/index.js new file mode 100644 index 000000000..fd68d49c2 --- /dev/null +++ b/src/containers/Dialogs/CustomerOpeningBalanceDialog/index.js @@ -0,0 +1,40 @@ +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 CustomerOpeningBalanceDialogContent = React.lazy(() => + import('./CustomerOpeningBalanceDialogContent'), +); + +/** + * Customer opening balance dialog. + * @returns + */ +function CustomerOpeningBalanceDialog({ + dialogName, + payload: { customerId }, + isOpen, +}) { + return ( + } + isOpen={isOpen} + canEscapeJeyClose={true} + autoFocus={true} + className={'dialog--customer-opening-balance'} + > + + + + + ); +} + +export default compose(withDialogRedux())(CustomerOpeningBalanceDialog); diff --git a/src/containers/Dialogs/CustomerOpeningBalanceDialog/utils.js b/src/containers/Dialogs/CustomerOpeningBalanceDialog/utils.js new file mode 100644 index 000000000..206545395 --- /dev/null +++ b/src/containers/Dialogs/CustomerOpeningBalanceDialog/utils.js @@ -0,0 +1,20 @@ +import React from 'react'; +import { useFormikContext } from 'formik'; +import { first } from 'lodash'; + +import { useCustomerOpeningBalanceContext } from './CustomerOpeningBalanceFormProvider'; + +export const useSetPrimaryBranchToForm = () => { + const { setFieldValue } = useFormikContext(); + const { branches, isBranchesSuccess } = useCustomerOpeningBalanceContext(); + + React.useEffect(() => { + if (isBranchesSuccess) { + const primaryBranch = branches.find((b) => b.primary) || first(branches); + + if (primaryBranch) { + setFieldValue('opening_balance_branch_id', primaryBranch.id); + } + } + }, [isBranchesSuccess, setFieldValue, branches]); +}; diff --git a/src/containers/Drawers/CustomerDetailsDrawer/CustomerDetailsActionsBar.js b/src/containers/Drawers/CustomerDetailsDrawer/CustomerDetailsActionsBar.js index 48f4ac7ff..226927dba 100644 --- a/src/containers/Drawers/CustomerDetailsDrawer/CustomerDetailsActionsBar.js +++ b/src/containers/Drawers/CustomerDetailsDrawer/CustomerDetailsActionsBar.js @@ -19,9 +19,11 @@ import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar'; import { useCustomerDetailsDrawerContext } from './CustomerDetailsDrawerProvider'; import withAlertsActions from 'containers/Alert/withAlertActions'; +import withDialogActions from 'containers/Dialog/withDialogActions'; import withDrawerActions from 'containers/Drawer/withDrawerActions'; import { Can, Icon, FormattedMessage as T } from 'components'; +import { CustomerMoreMenuItem } from './utils'; import { AbilitySubject, SaleInvoiceAction, @@ -36,6 +38,9 @@ import { compose } from 'utils'; * Customer details actions bar. */ function CustomerDetailsActionsBar({ + // #withDialogActions + openDialog, + // #withAlertsActions openAlert, @@ -75,6 +80,10 @@ function CustomerDetailsActionsBar({ closeDrawer('customer-details-drawer'); }; + const handleEditOpeningBalance = () => { + openDialog('customer-opening-balance', { customerId }); + }; + return ( @@ -141,6 +150,12 @@ function CustomerDetailsActionsBar({ onClick={handleDeleteCustomer} /> + + ); @@ -149,4 +164,5 @@ function CustomerDetailsActionsBar({ export default compose( withDrawerActions, withAlertsActions, + withDialogActions, )(CustomerDetailsActionsBar); diff --git a/src/containers/Drawers/CustomerDetailsDrawer/utils.js b/src/containers/Drawers/CustomerDetailsDrawer/utils.js new file mode 100644 index 000000000..6fa7b0d59 --- /dev/null +++ b/src/containers/Drawers/CustomerDetailsDrawer/utils.js @@ -0,0 +1,37 @@ +import React from 'react'; +import { + Button, + Popover, + PopoverInteractionKind, + Position, + MenuItem, + Menu, +} from '@blueprintjs/core'; +import { Icon, FormattedMessage as T } from 'components'; + +/** + * Customer more actions menu items. + * @returns + */ +export function CustomerMoreMenuItem({ payload: { onEditOpeningBalance } }) { + return ( + + } + onClick={onEditOpeningBalance} + /> + + } + > +