From 2b5d00ed600b709e5ea9c8797b92bf1934b398ab Mon Sep 17 00:00:00 2001 From: elforjani3 Date: Wed, 21 Jul 2021 23:47:40 +0200 Subject: [PATCH] feature/ allocate landed cost. --- client/src/common/allocateLandedCostType.js | 6 + client/src/components/DialogsContainer.js | 2 + client/src/components/index.js | 2 + .../AllocateLandedCostDialogContent.js | 18 ++ .../AllocateLandedCostDialogProvider.js | 38 ++++ .../AllocateLandedCostEntriesTable.js | 72 ++++++++ .../AllocateLandedCostFloatingActions.js | 43 +++++ .../AllocateLandedCostForm.js | 57 ++++++ .../AllocateLandedCostForm.schema.js | 19 ++ .../AllocateLandedCostFormBody.js | 26 +++ .../AllocateLandedCostFormContent.js | 15 ++ .../AllocateLandedCostFormFields.js | 171 ++++++++++++++++++ .../Dialogs/AllocateLandedCostDialog/index.js | 36 ++++ .../Bills/BillsLanding/BillsTable.js | 15 +- .../Bills/BillsLanding/components.js | 8 +- client/src/lang/en/index.json | 13 +- .../AllocateLandedCostForm.scss | 42 +++++ 17 files changed, 572 insertions(+), 11 deletions(-) create mode 100644 client/src/common/allocateLandedCostType.js create mode 100644 client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostDialogContent.js create mode 100644 client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostDialogProvider.js create mode 100644 client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostEntriesTable.js create mode 100644 client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFloatingActions.js create mode 100644 client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.js create mode 100644 client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.schema.js create mode 100644 client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormBody.js create mode 100644 client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormContent.js create mode 100644 client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormFields.js create mode 100644 client/src/containers/Dialogs/AllocateLandedCostDialog/index.js create mode 100644 client/src/style/pages/AllocateLandedCost/AllocateLandedCostForm.scss diff --git a/client/src/common/allocateLandedCostType.js b/client/src/common/allocateLandedCostType.js new file mode 100644 index 000000000..4e915bfec --- /dev/null +++ b/client/src/common/allocateLandedCostType.js @@ -0,0 +1,6 @@ +import intl from 'react-intl-universal'; + +export default [ + { name: intl.get('bills'), value: 'bills' }, + { name: intl.get('expenses'), value: 'expenses' }, +] \ No newline at end of file diff --git a/client/src/components/DialogsContainer.js b/client/src/components/DialogsContainer.js index 1419f7638..203a769b6 100644 --- a/client/src/components/DialogsContainer.js +++ b/client/src/components/DialogsContainer.js @@ -13,6 +13,7 @@ import KeyboardShortcutsDialog from 'containers/Dialogs/keyboardShortcutsDialog' import ContactDuplicateDialog from 'containers/Dialogs/ContactDuplicateDialog'; import QuickPaymentReceiveFormDialog from 'containers/Dialogs/QuickPaymentReceiveFormDialog'; import QuickPaymentMadeFormDialog from 'containers/Dialogs/QuickPaymentMadeFormDialog'; +import AllocateLandedCostDialog from 'containers/Dialogs/AllocateLandedCostDialog'; /** * Dialogs container. @@ -32,6 +33,7 @@ export default function DialogsContainer() { + ); } diff --git a/client/src/components/index.js b/client/src/components/index.js index c8d56dfe9..8567a384b 100644 --- a/client/src/components/index.js +++ b/client/src/components/index.js @@ -56,6 +56,7 @@ import DrawerHeaderContent from './Drawer/DrawerHeaderContent'; import Postbox from './Postbox'; import AccountsSuggestField from './AccountsSuggestField'; import MaterialProgressBar from './MaterialProgressBar'; +import { MoneyFieldCell } from './DataTableCells'; const Hint = FieldHint; @@ -123,4 +124,5 @@ export { Postbox, AccountsSuggestField, MaterialProgressBar, + MoneyFieldCell, }; diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostDialogContent.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostDialogContent.js new file mode 100644 index 000000000..1f574d9fa --- /dev/null +++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostDialogContent.js @@ -0,0 +1,18 @@ +import React from 'react'; +import { AllocateLandedCostDialogProvider } from './AllocateLandedCostDialogProvider'; +import AllocateLandedCostForm from './AllocateLandedCostForm'; + +/** + * Allocate landed cost dialog content. + */ +export default function AllocateLandedCostDialogContent({ + // #ownProps + dialogName, + bill, +}) { + return ( + + + + ); +} diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostDialogProvider.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostDialogProvider.js new file mode 100644 index 000000000..aec4f85eb --- /dev/null +++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostDialogProvider.js @@ -0,0 +1,38 @@ +import React from 'react'; +import { DialogContent } from 'components'; +import { useBill } from 'hooks/query'; + +import { pick } from 'lodash'; + +const AllocateLandedCostDialogContext = React.createContext(); + +/** + * Allocate landed cost provider. + */ +function AllocateLandedCostDialogProvider({ billId, dialogName, ...props }) { + // Handle fetch bill details. + const { isLoading: isBillLoading, data: bill } = useBill(billId, { + enabled: !!billId, + }); + + // provider payload. + const provider = { + bill: { + ...pick(bill, ['entries']), + }, + dialogName, + }; + return ( + + + + ); +} + +const useAllocateLandedConstDialogContext = () => + React.useContext(AllocateLandedCostDialogContext); + +export { + AllocateLandedCostDialogProvider, + useAllocateLandedConstDialogContext, +}; diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostEntriesTable.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostEntriesTable.js new file mode 100644 index 000000000..2275883ad --- /dev/null +++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostEntriesTable.js @@ -0,0 +1,72 @@ +import React from 'react'; +import intl from 'react-intl-universal'; +import { DataTable, MoneyFieldCell, DataTableEditable } from 'components'; +import { compose, updateTableRow } from 'utils'; + +/** + * Allocate landed cost entries table. + */ +export default function AllocateLandedCostEntriesTable({ + onUpdateData, + entries, +}) { + // allocate landed cost entries table columns. + const columns = React.useMemo( + () => [ + { + Header: intl.get('item'), + accessor: 'item_id', + disableSortBy: true, + width: '150', + }, + { + Header: intl.get('quantity'), + accessor: 'quantity', + disableSortBy: true, + width: '100', + }, + { + Header: intl.get('rate'), + accessor: 'rate', + disableSortBy: true, + width: '100', + }, + { + Header: intl.get('amount'), + accessor: 'amount', + disableSortBy: true, + width: '100', + }, + { + Header: intl.get('cost'), + accessor: 'cost', + width: '150', + Cell: MoneyFieldCell, + disableSortBy: true, + }, + ], + [], + ); + + // Handle update data. + const handleUpdateData = React.useCallback( + (rowIndex, columnId, value) => { + const newRows = compose(updateTableRow(rowIndex, columnId, value))( + entries, + ); + onUpdateData(newRows); + }, + [onUpdateData, entries], + ); + + const LL = [ + { + item_id: 'ITEM', + quantity: '30000', + rate: '100000', + amount: '400', + }, + ]; + + return ; +} diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFloatingActions.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFloatingActions.js new file mode 100644 index 000000000..ddc400903 --- /dev/null +++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFloatingActions.js @@ -0,0 +1,43 @@ +import React from 'react'; +import { Intent, Button, Classes } from '@blueprintjs/core'; +import { FormattedMessage as T } from 'components'; + +import { useFormikContext } from 'formik'; +import { useAllocateLandedConstDialogContext } from './AllocateLandedCostDialogProvider'; +import withDialogActions from 'containers/Dialog/withDialogActions'; +import { compose } from 'utils'; + +function AllocateLandedCostFloatingActions({ + // #withDialogActions + closeDialog, +}) { + // Formik context. + const { isSubmitting } = useFormikContext(); + + const { dialogName } = useAllocateLandedConstDialogContext(); + + // Handle cancel button click. + const handleCancelBtnClick = (event) => { + closeDialog(dialogName); + }; + + return ( +
+
+ + +
+
+ ); +} + +export default compose(withDialogActions)(AllocateLandedCostFloatingActions); diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.js new file mode 100644 index 000000000..5ccffca48 --- /dev/null +++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.js @@ -0,0 +1,57 @@ +import React from 'react'; +import { Formik } from 'formik'; +import moment from 'moment'; + +import 'style/pages/AllocateLandedCost/AllocateLandedCostForm.scss'; + +import { AllocateLandedCostFormSchema } from './AllocateLandedCostForm.schema'; +import { useAllocateLandedConstDialogContext } from './AllocateLandedCostDialogProvider'; +import AllocateLandedCostFormContent from './AllocateLandedCostFormContent'; +import withDialogActions from 'containers/Dialog/withDialogActions'; + +import { compose } from 'utils'; + +const defaultInitialValues = { + transaction_type: 'bills', + transaction_date: moment(new Date()).format('YYYY-MM-DD'), + transaction_id: '', + transaction_entry_id: '', + amount: '', + allocation_method: 'quantity', + entries: { + entry_id: '', + cost: '', + }, +}; + +/** + * Allocate landed cost form. + */ +function AllocateLandedCostForm({ + // #withDialogActions + closeDialog, +}) { + const { bill, dialogName } = useAllocateLandedConstDialogContext(); + + // Initial form values. + const initialValues = { + ...defaultInitialValues, + ...bill, + }; + + + // Handle form submit. + const handleFormSubmit = (values, { setSubmitting }) => {}; + + return ( + + + + ); +} + +export default compose(withDialogActions)(AllocateLandedCostForm); diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.schema.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.schema.js new file mode 100644 index 000000000..176514808 --- /dev/null +++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.schema.js @@ -0,0 +1,19 @@ +import * as Yup from 'yup'; +import intl from 'react-intl-universal'; + +const Schema = Yup.object().shape({ + transaction_type: Yup.string().label(intl.get('transaction_type')), + transaction_date: Yup.date().label(intl.get('transaction_date')), + transaction_id: Yup.string().label(intl.get('transaction_number')), + transaction_entry_id: Yup.string().label(intl.get('transaction_line')), + amount: Yup.number().label(intl.get('amount')), + allocation_method: Yup.string().trim(), + entries: Yup.array().of( + Yup.object().shape({ + entry_id: Yup.number().nullable(), + cost: Yup.number().nullable(), + }), + ), +}); + +export const AllocateLandedCostFormSchema = Schema; diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormBody.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormBody.js new file mode 100644 index 000000000..13029efd5 --- /dev/null +++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormBody.js @@ -0,0 +1,26 @@ +import React from 'react'; +import { FastField } from 'formik'; +import classNames from 'classnames'; +import { CLASSES } from 'common/classes'; +import AllocateLandedCostEntriesTable from './AllocateLandedCostEntriesTable'; + +export default function AllocateLandedCostFormBody() { + return ( +
+ + {({ + form: { setFieldValue, values }, + field: { value }, + meta: { error, touched }, + }) => ( + { + setFieldValue('entries', newEntries); + }} + /> + )} + +
+ ); +} diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormContent.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormContent.js new file mode 100644 index 000000000..c06d05a67 --- /dev/null +++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormContent.js @@ -0,0 +1,15 @@ +import React from 'react'; +import { Form } from 'formik'; +import AllocateLandedCostFormFields from './AllocateLandedCostFormFields'; +import AllocateLandedCostFloatingActions from './AllocateLandedCostFloatingActions'; +/** + * Allocate landed cost form content. + */ +export default function AllocateLandedCostFormContent() { + return ( +
+ + + + ); +} diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormFields.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormFields.js new file mode 100644 index 000000000..a1639ca96 --- /dev/null +++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormFields.js @@ -0,0 +1,171 @@ +import React from 'react'; +import { FastField, ErrorMessage } from 'formik'; +import { + Classes, + FormGroup, + RadioGroup, + Radio, + InputGroup, + Position, +} from '@blueprintjs/core'; +import { DateInput } from '@blueprintjs/datetime'; +import classNames from 'classnames'; +import { FormattedMessage as T } from 'components'; +import intl from 'react-intl-universal'; +import { + inputIntent, + momentFormatter, + tansformDateValue, + handleDateChange, + handleStringChange, +} from 'utils'; +import { FieldRequiredHint, ListSelect } from 'components'; +import { CLASSES } from 'common/classes'; +import allocateLandedCostType from 'common/allocateLandedCostType'; +import AccountsSuggestField from 'components/AccountsSuggestField'; +import AllocateLandedCostFormBody from './AllocateLandedCostFormBody'; + +/** + * Allocate landed cost form fields. + */ +export default function AllocateLandedCostFormFields() { + return ( +
+ {/*------------Transaction type -----------*/} + + {({ + form: { values, setFieldValue }, + field: { value }, + meta: { error, touched }, + }) => ( + } + labelInfo={} + helperText={} + intent={inputIntent({ error, touched })} + inline={true} + className={classNames(CLASSES.FILL, 'form-group--transaction_type')} + > + { + setFieldValue('transaction_type', type.value); + }} + filterable={false} + selectedItem={value} + selectedItemProp={'value'} + textProp={'name'} + popoverProps={{ minimal: true }} + /> + + )} + + + {/*------------Transaction date -----------*/} + + {({ form, field: { value }, meta: { error, touched } }) => ( + } + // labelInfo={} + intent={inputIntent({ error, touched })} + helperText={} + minimal={true} + className={classNames(CLASSES.FILL, 'form-group--transaction_date')} + inline={true} + > + { + form.setFieldValue('transaction_date', formattedDate); + })} + value={tansformDateValue(value)} + popoverProps={{ + position: Position.BOTTOM, + minimal: true, + }} + /> + + )} + + {/*------------ Transaction -----------*/} + + {({ form, field, meta: { error, touched } }) => ( + } + // labelInfo={} + intent={inputIntent({ error, touched })} + helperText={} + className={'form-group--transaction_id'} + inline={true} + > + + form.setFieldValue('transaction_id', id) + } + inputProps={{ + placeholder: intl.get('select_transaction'), + }} + /> + + )} + + {/*------------ Transaction line -----------*/} + + {({ form, field, meta: { error, touched } }) => ( + } + intent={inputIntent({ error, touched })} + helperText={} + className={'form-group--transaction_entry_id'} + inline={true} + > + + + )} + + {/*------------ Amount -----------*/} + + {({ form, field, meta: { error, touched } }) => ( + } + intent={inputIntent({ error, touched })} + helperText={} + className={'form-group--amount'} + inline={true} + > + + + )} + + {/*------------ Allocation method -----------*/} + + {({ form, field: { value }, meta: { touched, error } }) => ( + } + labelInfo={} + className={'form-group--allocation_method'} + intent={inputIntent({ error, touched })} + helperText={} + inline={true} + > + { + form.setFieldValue('allocation_method', _value); + })} + selectedValue={value} + inline={true} + > + } value="quantity" /> + } value="valuation" /> + + + )} + + + {/*------------ Allocate Landed cost Table -----------*/} + +
+ ); +} diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/index.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/index.js new file mode 100644 index 000000000..5798a5aba --- /dev/null +++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/index.js @@ -0,0 +1,36 @@ +import React, { lazy } from 'react'; +import { FormattedMessage as T, Dialog, DialogSuspense } from 'components'; +import withDialogRedux from 'components/DialogReduxConnect'; +import { compose } from 'utils'; + +const AllocateLandedCostDialogContent = lazy(() => + import('./AllocateLandedCostDialogContent'), +); + +/** + * Allocate landed cost dialog. + */ +function AllocateLandedCostDialog({ + dialogName, + payload = { billId: null }, + isOpen, +}) { + return ( + } + canEscapeKeyClose={true} + isOpen={isOpen} + className="dialog--allocate-landed-cost-form" + > + + + + + ); +} + +export default compose(withDialogRedux())(AllocateLandedCostDialog); diff --git a/client/src/containers/Purchases/Bills/BillsLanding/BillsTable.js b/client/src/containers/Purchases/Bills/BillsLanding/BillsTable.js index cbf514012..03f49bb09 100644 --- a/client/src/containers/Purchases/Bills/BillsLanding/BillsTable.js +++ b/client/src/containers/Purchases/Bills/BillsLanding/BillsTable.js @@ -34,13 +34,8 @@ function BillsDataTable({ openDialog, }) { // Bills list context. - const { - bills, - pagination, - isBillsLoading, - isBillsFetching, - isEmptyStatus, - } = useBillsListContext(); + const { bills, pagination, isBillsLoading, isBillsFetching, isEmptyStatus } = + useBillsListContext(); const history = useHistory(); @@ -78,6 +73,11 @@ function BillsDataTable({ openDialog('quick-payment-made', { billId: id }); }; + // handle allocate landed cost. + const handleAllocateLandedCost = ({ id }) => { + openDialog('allocate-landed-cost', { billId: id }); + }; + if (isEmptyStatus) { return ; } @@ -105,6 +105,7 @@ function BillsDataTable({ onEdit: handleEditBill, onOpen: handleOpenBill, onQuick: handleQuickPaymentMade, + onAllocateLandedCost: handleAllocateLandedCost, }} /> ); diff --git a/client/src/containers/Purchases/Bills/BillsLanding/components.js b/client/src/containers/Purchases/Bills/BillsLanding/components.js index 34d299000..fa4c2a76b 100644 --- a/client/src/containers/Purchases/Bills/BillsLanding/components.js +++ b/client/src/containers/Purchases/Bills/BillsLanding/components.js @@ -20,7 +20,7 @@ import moment from 'moment'; * Actions menu. */ export function ActionsMenu({ - payload: { onEdit, onOpen, onDelete, onQuick }, + payload: { onEdit, onOpen, onDelete, onQuick, onAllocateLandedCost }, row: { original }, }) { return ( @@ -50,7 +50,11 @@ export function ActionsMenu({ onClick={safeCallback(onQuick, original)} /> - + } + text={intl.get('allocate_landed_coast')} + onClick={safeCallback(onAllocateLandedCost, original)} + />