diff --git a/src/components/Dialog/DialogFooter.js b/src/components/Dialog/DialogFooter.js new file mode 100644 index 000000000..604015683 --- /dev/null +++ b/src/components/Dialog/DialogFooter.js @@ -0,0 +1,15 @@ +import React from 'react'; +import styled from 'styled-components'; +import { Classes } from '@blueprintjs/core'; + +export function DialogFooter({ children }) { + return ( + + {children} + + ); +} + +const DialogFooterRoot = styled.div` + display: flex; +`; diff --git a/src/components/Dialog/DialogFooterActions.js b/src/components/Dialog/DialogFooterActions.js index 91c856e9a..e24461203 100644 --- a/src/components/Dialog/DialogFooterActions.js +++ b/src/components/Dialog/DialogFooterActions.js @@ -14,13 +14,11 @@ export function DialogFooterActions({ alignment = 'right', children }) { } const DialogFooterActionsRoot = styled.div` - margin-left: -10px; - margin-right: -10px; - justify-content: ${(props) => - props.alignment === 'right' ? 'flex-end' : 'flex-start'}; + ${(props) => + props.alignment === 'right' ? 'margin-left: auto;' : 'margin-right: auto;'}; .bp3-button { - margin-left: 10px; - margin-left: 10px; + margin-left: 5px; + margin-left: 5px; } `; diff --git a/src/components/Dialog/index.js b/src/components/Dialog/index.js index c7c0982fb..58890b836 100644 --- a/src/components/Dialog/index.js +++ b/src/components/Dialog/index.js @@ -3,4 +3,5 @@ export * from './Dialog'; export * from './DialogFooterActions'; export * from './DialogSuspense'; -export * from './DialogContent'; \ No newline at end of file +export * from './DialogContent'; +export * from './DialogFooter'; \ No newline at end of file diff --git a/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostDialogProvider.js b/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostDialogProvider.js index 780ea0621..adf48ca5b 100644 --- a/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostDialogProvider.js +++ b/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostDialogProvider.js @@ -1,6 +1,16 @@ import React from 'react'; +import { defaultTo, get } from 'lodash'; import { DialogContent } from 'components'; -import { useBill, useCreateLandedCost } from 'hooks/query'; +import { + useBill, + useCreateLandedCost, + useLandedCostTransaction, +} from 'hooks/query'; +import { + getEntriesByTransactionId, + getCostTransactionById, + getTransactionEntryById, +} from './utils'; const AllocateLandedCostDialogContext = React.createContext(); @@ -13,22 +23,79 @@ function AllocateLandedCostDialogProvider({ dialogName, ...props }) { + const [transactionsType, setTransactionsType] = React.useState(null); + const [transactionId, setTransactionId] = React.useState(null); + const [transactionEntryId, setTransactionEntryId] = React.useState(null); + // Handle fetch bill details. const { isLoading: isBillLoading, data: bill } = useBill(billId, { enabled: !!billId, }); - + // Retrieve the landed cost transactions based on the given transactions type. + const { + data: { transactions: landedCostTransactions }, + } = useLandedCostTransaction(transactionsType, { + enabled: !!transactionsType, + }); + // Landed cost selected transaction. + const costTransaction = React.useMemo( + () => + transactionId + ? getCostTransactionById(transactionId, landedCostTransactions) + : null, + [transactionId, landedCostTransactions], + ); + // Retrieve the cost transaction entry. + const costTransactionEntry = React.useMemo( + () => + costTransaction && transactionEntryId + ? getTransactionEntryById(costTransaction, transactionEntryId) + : null, + [costTransaction, transactionEntryId], + ); + // Retrieve entries of the given transaction id. + const costTransactionEntries = React.useMemo( + () => + transactionId + ? getEntriesByTransactionId(landedCostTransactions, transactionId) + : [], + [landedCostTransactions, transactionId], + ); // Create landed cost mutations. const { mutateAsync: createLandedCostMutate } = useCreateLandedCost(); - // provider payload. + // Retrieve the unallocate cost amount of cost transaction. + const unallocatedCostAmount = defaultTo( + get(costTransactionEntry, 'unallocated_cost_amount'), + 0, + ); + + // Retrieve the unallocate cost amount of cost transaction. + const formattedUnallocatedCostAmount = defaultTo( + get(costTransactionEntry, 'formatted_unallocated_cost_amount'), + 0, + ); + + // Provider payload. const provider = { isBillLoading, bill, dialogName, query, createLandedCostMutate, + costTransaction, + costTransactionEntries, + transactionsType, + landedCostTransactions, + setTransactionsType, + setTransactionId, + setTransactionEntryId, + costTransactionEntry, + transactionEntryId, + transactionId, billId, + unallocatedCostAmount, + formattedUnallocatedCostAmount, }; return ( diff --git a/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFloatingActions.js b/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFloatingActions.js index 2b4cded08..de5b7e16e 100644 --- a/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFloatingActions.js +++ b/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFloatingActions.js @@ -1,8 +1,13 @@ import React from 'react'; -import { Intent, Button, Classes } from '@blueprintjs/core'; -import { FormattedMessage as T } from 'components'; - +import { Intent, Button } from '@blueprintjs/core'; import { useFormikContext } from 'formik'; +import styled from 'styled-components'; +import { + DialogFooter, + DialogFooterActions, + FormattedMessage as T, +} from 'components'; + import { useAllocateLandedConstDialogContext } from './AllocateLandedCostDialogProvider'; import withDialogActions from 'containers/Dialog/withDialogActions'; import { compose } from 'utils'; @@ -13,7 +18,8 @@ function AllocateLandedCostFloatingActions({ }) { // Formik context. const { isSubmitting } = useFormikContext(); - const { dialogName } = useAllocateLandedConstDialogContext(); + const { dialogName, costTransactionEntry, formattedUnallocatedCostAmount } = + useAllocateLandedConstDialogContext(); // Handle cancel button click. const handleCancelBtnClick = (event) => { @@ -21,22 +27,41 @@ function AllocateLandedCostFloatingActions({ }; return ( -
-
+ + + {costTransactionEntry && ( + + Unallocated cost Amount:{' '} + {formattedUnallocatedCostAmount} + + )} + + + -
-
+ + ); } export default compose(withDialogActions)(AllocateLandedCostFloatingActions); + +const UnallocatedAmount = styled.div` + color: #3f5278; + align-self: center; + + strong { + color: #353535; + padding-left: 4px; + } +`; diff --git a/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.js b/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.js index 3b6ded70a..5cb1a2fa2 100644 --- a/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.js +++ b/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.js @@ -2,8 +2,6 @@ import React from 'react'; import { Formik } from 'formik'; import { Intent } from '@blueprintjs/core'; import intl from 'react-intl-universal'; -import moment from 'moment'; -import { sumBy } from 'lodash'; import 'style/pages/AllocateLandedCost/AllocateLandedCostForm.scss'; @@ -14,20 +12,19 @@ import AllocateLandedCostFormContent from './AllocateLandedCostFormContent'; import withDialogActions from 'containers/Dialog/withDialogActions'; import { compose, transformToForm } from 'utils'; +const defaultInitialItem = { + entry_id: '', + cost: '', +}; + // Default form initial values. const defaultInitialValues = { transaction_type: 'Bill', - transaction_date: moment(new Date()).format('YYYY-MM-DD'), transaction_id: '', transaction_entry_id: '', amount: '', allocation_method: 'quantity', - items: [ - { - entry_id: '', - cost: '', - }, - ], + items: [defaultInitialItem], }; /** @@ -37,8 +34,13 @@ function AllocateLandedCostForm({ // #withDialogActions closeDialog, }) { - const { dialogName, bill, billId, createLandedCostMutate } = - useAllocateLandedConstDialogContext(); + const { + dialogName, + bill, + billId, + createLandedCostMutate, + unallocatedCostAmount, + } = useAllocateLandedConstDialogContext(); // Initial form values. const initialValues = { @@ -49,11 +51,10 @@ function AllocateLandedCostForm({ cost: '', })), }; - const amount = sumBy(initialValues.items, 'amount'); // Handle form submit. const handleFormSubmit = (values, { setSubmitting }) => { - setSubmitting(false); + setSubmitting(true); // Filters the entries has no cost. const entries = values.items @@ -81,13 +82,16 @@ function AllocateLandedCostForm({ // Handle the request error. const onError = () => { setSubmitting(false); - AppToaster.show({ message: 'Something went wrong!', intent: Intent.DANGER }); + AppToaster.show({ + message: 'Something went wrong!', + intent: Intent.DANGER, + }); }; createLandedCostMutate([billId, form]).then(onSuccess).catch(onError); }; // Computed validation schema. - const validationSchema = AllocateLandedCostFormSchema(amount); + const validationSchema = AllocateLandedCostFormSchema(unallocatedCostAmount); return ( +export const AllocateLandedCostFormSchema = (maxAmount) => 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().max(minAmount).label(intl.get('amount')), - allocation_method: Yup.string().trim(), + transaction_type: Yup.string() + .required() + .label(intl.get('transaction_type')), + transaction_id: Yup.string() + .required() + .label(intl.get('transaction_number')), + transaction_entry_id: Yup.string() + .required() + .label(intl.get('transaction_line')), + amount: Yup.number().max(maxAmount).label(intl.get('amount')), + allocation_method: Yup.string().required().trim(), items: Yup.array().of( Yup.object().shape({ entry_id: Yup.number().nullable(), diff --git a/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormContent.js b/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormContent.js index e1f5f08c0..51c22052a 100644 --- a/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormContent.js +++ b/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormContent.js @@ -1,16 +1,37 @@ import React from 'react'; -import { Form } from 'formik'; +import { Form, useFormikContext } from 'formik'; +import { FormObserver } from 'components'; import AllocateLandedCostFormFields from './AllocateLandedCostFormFields'; +import { useAllocateLandedConstDialogContext } from './AllocateLandedCostDialogProvider'; import AllocateLandedCostFloatingActions from './AllocateLandedCostFloatingActions'; /** * Allocate landed cost form content. */ export default function AllocateLandedCostFormContent() { + const { values } = useFormikContext(); + + // Allocate landed cost dialog context. + const { setTransactionsType, setTransactionId, setTransactionEntryId } = + useAllocateLandedConstDialogContext(); + + // Handle the form change. + const handleFormChange = (values) => { + if (values.transaction_type) { + setTransactionsType(values.transaction_type); + } + if (values.transaction_id) { + setTransactionId(values.transaction_id); + } + if (values.transaction_entry_id) { + setTransactionEntryId(values.transaction_entry_id); + } + }; return (
+ ); } diff --git a/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormFields.js b/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormFields.js index 4861a65eb..d71ebab2c 100644 --- a/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormFields.js +++ b/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormFields.js @@ -1,5 +1,5 @@ import React from 'react'; -import { FastField, Field, ErrorMessage, useFormikContext } from 'formik'; +import { FastField, Field, ErrorMessage } from 'formik'; import { Classes, FormGroup, @@ -14,31 +14,31 @@ import { inputIntent, handleStringChange } from 'utils'; import { FieldRequiredHint, ListSelect } from 'components'; import { CLASSES } from 'common/classes'; import allocateLandedCostType from 'common/allocateLandedCostType'; -import { useLandedCostTransaction } from 'hooks/query'; import AllocateLandedCostFormBody from './AllocateLandedCostFormBody'; -import { getEntriesByTransactionId, allocateCostToEntries } from './utils'; +import { + transactionsSelectShouldUpdate, + allocateCostToEntries, + resetAllocatedCostEntries, +} from './utils'; +import { useAllocateLandedConstDialogContext } from './AllocateLandedCostDialogProvider'; /** * Allocate landed cost form fields. */ export default function AllocateLandedCostFormFields() { - const { values } = useFormikContext(); - - const { - data: { transactions }, - } = useLandedCostTransaction(values.transaction_type); - - // Retrieve entries of the given transaction id. - const transactionEntries = React.useMemo( - () => getEntriesByTransactionId(transactions, values.transaction_id), - [transactions, values.transaction_id], - ); + // Allocated landed cost dialog. + const { costTransactionEntries, landedCostTransactions } = + useAllocateLandedConstDialogContext(); return (
{/*------------Transaction type -----------*/} - + {({ form: { values, setFieldValue }, field: { value }, @@ -55,9 +55,14 @@ export default function AllocateLandedCostFormFields() { { + const { items } = values; + setFieldValue('transaction_type', type.value); setFieldValue('transaction_id', ''); setFieldValue('transaction_entry_id', ''); + + setFieldValue('amount', ''); + setFieldValue('items', resetAllocatedCostEntries(items)); }} filterable={false} selectedItem={value} @@ -70,7 +75,11 @@ export default function AllocateLandedCostFormFields() { {/*------------ Transaction -----------*/} - + {({ form, field: { value }, meta: { error, touched } }) => ( } @@ -81,10 +90,14 @@ export default function AllocateLandedCostFormFields() { inline={true} > { + const { items } = form.values; form.setFieldValue('transaction_id', id); form.setFieldValue('transaction_entry_id', ''); + + form.setFieldValue('amount', ''); + form.setFieldValue('items', resetAllocatedCostEntries(items)); }} filterable={false} selectedItem={value} @@ -99,8 +112,12 @@ export default function AllocateLandedCostFormFields() { {/*------------ Transaction line -----------*/} - 0}> - + 0}> + {({ form, field: { value }, meta: { error, touched } }) => ( } @@ -113,16 +130,20 @@ export default function AllocateLandedCostFormFields() { inline={true} > { + items={costTransactionEntries} + onItemSelect={({ id, unallocated_cost_amount }) => { const { items, allocation_method } = form.values; - form.setFieldValue('amount', amount); form.setFieldValue('transaction_entry_id', id); + form.setFieldValue('amount', unallocated_cost_amount); form.setFieldValue( 'items', - allocateCostToEntries(amount, allocation_method, items), + allocateCostToEntries( + unallocated_cost_amount, + allocation_method, + items, + ), ); }} filterable={false} @@ -177,12 +198,12 @@ export default function AllocateLandedCostFormFields() { > { - const { amount, items, allocation_method } = form.values; + const { amount, items } = form.values; form.setFieldValue('allocation_method', _value); form.setFieldValue( 'items', - allocateCostToEntries(amount, allocation_method, items), + allocateCostToEntries(amount, _value, items), ); })} selectedValue={value} diff --git a/src/containers/Dialogs/AllocateLandedCostDialog/utils.js b/src/containers/Dialogs/AllocateLandedCostDialog/utils.js index 21d5fa076..a19fff723 100644 --- a/src/containers/Dialogs/AllocateLandedCostDialog/utils.js +++ b/src/containers/Dialogs/AllocateLandedCostDialog/utils.js @@ -1,5 +1,14 @@ import { sumBy, round } from 'lodash'; import * as R from 'ramda'; +import { defaultFastFieldShouldUpdate } from 'utils'; + +/** + * Retrieve the landed cost transaction by the given id. + */ +export function getCostTransactionById(id, transactions) { + return transactions.find((trans) => trans.id === id); +} + /** * Retrieve transaction entries of the given transaction id. */ @@ -8,6 +17,10 @@ export function getEntriesByTransactionId(transactions, id) { return transaction ? transaction.entries : []; } +export function getTransactionEntryById(transaction, transactionEntryId) { + return transaction.entries.find((entry) => entry.id === transactionEntryId); +} + export function allocateCostToEntries(total, allocateType, entries) { return R.compose( R.when( @@ -60,3 +73,18 @@ export function allocateCostByQuantity(total, entries) { cost: round(entry.percentageOfQuantity * total, 2), })); } + +/** + * Detarmines the transactions selet field when should update. + */ +export function transactionsSelectShouldUpdate(newProps, oldProps) { + return ( + newProps.transactions !== oldProps.transactions || + defaultFastFieldShouldUpdate(newProps, oldProps) + ); +} + + +export function resetAllocatedCostEntries(entries) { + return entries.map((entry) => ({ ...entry, cost: 0 })); +} \ No newline at end of file diff --git a/src/containers/Dialogs/SMSMessageDialog/SMSMessageFormFloatingActions.js b/src/containers/Dialogs/SMSMessageDialog/SMSMessageFormFloatingActions.js index e823fba9d..c579f0767 100644 --- a/src/containers/Dialogs/SMSMessageDialog/SMSMessageFormFloatingActions.js +++ b/src/containers/Dialogs/SMSMessageDialog/SMSMessageFormFloatingActions.js @@ -1,9 +1,12 @@ import React from 'react'; -import { Intent, Button, Classes } from '@blueprintjs/core'; +import { Intent, Button } from '@blueprintjs/core'; import { useFormikContext } from 'formik'; -import { DialogFooterActions, FormattedMessage as T } from 'components'; - +import { + DialogFooter, + DialogFooterActions, + FormattedMessage as T, +} from 'components'; import { useSMSMessageDialogContext } from './SMSMessageDialogProvider'; import withDialogActions from 'containers/Dialog/withDialogActions'; @@ -28,7 +31,7 @@ function SMSMessageFormFloatingActions({ }; return ( -
+ -
+ ); } diff --git a/src/containers/NotifyViaSMS/NotifyViaSMSFormFloatingActions.js b/src/containers/NotifyViaSMS/NotifyViaSMSFormFloatingActions.js index dbe701aaf..e79e69b21 100644 --- a/src/containers/NotifyViaSMS/NotifyViaSMSFormFloatingActions.js +++ b/src/containers/NotifyViaSMS/NotifyViaSMSFormFloatingActions.js @@ -1,8 +1,12 @@ import React from 'react'; import { useFormikContext } from 'formik'; -import { Intent, Button, Classes } from '@blueprintjs/core'; +import { Intent, Button } from '@blueprintjs/core'; -import { DialogFooterActions, FormattedMessage as T } from 'components'; +import { + DialogFooter, + DialogFooterActions, + FormattedMessage as T, +} from 'components'; /** * @@ -17,7 +21,7 @@ export default function NotifyViaSMSFormFloatingActions({ onCancel }) { }; return ( -
+ -
+ ); } diff --git a/src/hooks/query/bills.js b/src/hooks/query/bills.js index d46225079..2ee4b366e 100644 --- a/src/hooks/query/bills.js +++ b/src/hooks/query/bills.js @@ -22,6 +22,10 @@ const commonInvalidateQueries = (queryClient) => { // Invalidate financial reports. queryClient.invalidateQueries(t.FINANCIAL_REPORT); + + // Invalidate landed cost. + queryClient.invalidateQueries(t.LANDED_COST); + queryClient.invalidateQueries(t.LANDED_COST_TRANSACTION); }; /** diff --git a/src/hooks/query/expenses.js b/src/hooks/query/expenses.js index df5000d13..152af27f2 100644 --- a/src/hooks/query/expenses.js +++ b/src/hooks/query/expenses.js @@ -25,6 +25,10 @@ const commonInvalidateQueries = (queryClient) => { // Invalidate the cashflow transactions. queryClient.invalidateQueries(t.CASH_FLOW_TRANSACTIONS); queryClient.invalidateQueries(t.CASHFLOW_ACCOUNT_TRANSACTIONS_INFINITY); + + // Invalidate landed cost. + queryClient.invalidateQueries(t.LANDED_COST); + queryClient.invalidateQueries(t.LANDED_COST_TRANSACTION); }; const transformExpenses = (response) => ({ diff --git a/src/hooks/query/landedCost.js b/src/hooks/query/landedCost.js index 6249a6061..57dbcfe17 100644 --- a/src/hooks/query/landedCost.js +++ b/src/hooks/query/landedCost.js @@ -39,6 +39,7 @@ export function useCreateLandedCost(props) { export function useDeleteLandedCost(props) { const queryClient = useQueryClient(); const apiRequest = useApiRequest(); + return useMutation( (landedCostId) => apiRequest.delete(`purchases/landed-cost/${landedCostId}`), @@ -65,7 +66,6 @@ export function useLandedCostTransaction(query, props) { }, { select: (res) => res.data, - defaultData: { transactions: [], },