diff --git a/client/src/components/AccountsSelectList.js b/client/src/components/AccountsSelectList.js index e037a7aad..e1bc9de96 100644 --- a/client/src/components/AccountsSelectList.js +++ b/client/src/components/AccountsSelectList.js @@ -1,31 +1,36 @@ -import React, {useCallback, useState} from 'react'; -import { - MenuItem, - Button, -} from '@blueprintjs/core'; -import {Select} from '@blueprintjs/select'; +import React, { useCallback, useState } from 'react'; +import { MenuItem, Button } from '@blueprintjs/core'; +import { Select } from '@blueprintjs/select'; export default function AccountsSelectList({ accounts, onAccountSelected, - error, + error = [], initialAccount, - defautlSelectText = 'Select account' + defautlSelectText = 'Select account', }) { const [selectedAccount, setSelectedAccount] = useState( - initialAccount || null + initialAccount || null, ); // Account item of select accounts field. const accountItem = useCallback((item, { handleClick, modifiers, query }) => { return ( - + ); }, []); - const onAccountSelect = useCallback((account) => { - setSelectedAccount({ ...account }); - onAccountSelected && onAccountSelected(account); - }, [setSelectedAccount, onAccountSelected]); + const onAccountSelect = useCallback( + (account) => { + setSelectedAccount({ ...account }); + onAccountSelected && onAccountSelected(account); + }, + [setSelectedAccount, onAccountSelected], + ); // Filters accounts items. const filterAccountsPredicater = useCallback( @@ -47,15 +52,16 @@ export default function AccountsSelectList({ return ( ); -} \ No newline at end of file +} diff --git a/client/src/components/App.js b/client/src/components/App.js index 42095276b..d4e0add38 100644 --- a/client/src/components/App.js +++ b/client/src/components/App.js @@ -18,7 +18,7 @@ function App({ locale }) { const queryConfig = { queries: { - refetchOnWindowFocus: true, + refetchOnWindowFocus: false, } }; return ( diff --git a/client/src/containers/Customers/Customer.js b/client/src/containers/Customers/Customer.js index e4a8e237b..7d8b0f70f 100644 --- a/client/src/containers/Customers/Customer.js +++ b/client/src/containers/Customers/Customer.js @@ -25,15 +25,31 @@ function Customer({ requestFetchCustomers({}), ); - const fetchCustomerDatails =useQuery(id && ['customer-detail',id],()=>requestFetchCustomers()) + const fetchCustomerDatails = useQuery(id && ['customer-detail', id], () => + requestFetchCustomers(), + ); + + const handleFormSubmit = useCallback( + (payload) => { + payload.redirect && history.push('/customers'); + }, + [history], + ); + + const handleCancel = useCallback(() => { + history.goBack(); + }, [history]); return ( - + ); } diff --git a/client/src/containers/Customers/CustomerFooter.js b/client/src/containers/Customers/CustomerFooter.js new file mode 100644 index 000000000..f1c0aaf51 --- /dev/null +++ b/client/src/containers/Customers/CustomerFooter.js @@ -0,0 +1,57 @@ +import React from 'react'; +import { Intent, Button } from '@blueprintjs/core'; +import { FormattedMessage as T } from 'react-intl'; + +export default function CustomerFloatingFooter({ + formik: { isSubmitting, resetForm }, + onSubmitClick, + onCancelClick, + + customer, +}) { + return ( +
+ + + + + + + +
+ ); +} diff --git a/client/src/containers/Customers/CustomerForm.js b/client/src/containers/Customers/CustomerForm.js index 4de5b6b47..f535623ac 100644 --- a/client/src/containers/Customers/CustomerForm.js +++ b/client/src/containers/Customers/CustomerForm.js @@ -31,6 +31,7 @@ import withCustomers from 'containers/Customers//withCustomers'; import useMedia from 'hooks/useMedia'; import { compose } from 'utils'; +import CustomerFloatingFooter from './CustomerFooter'; function CustomerForm({ // #withDashboardActions @@ -40,7 +41,7 @@ function CustomerForm({ customers, //#withCustomerDetail - customerDetail, + customer, //#withCustomersActions requestSubmitCustomer, @@ -50,9 +51,13 @@ function CustomerForm({ // #withMediaActions requestSubmitMedia, requestDeleteMedia, + //#Props + onFormSubmit, + onCancelForm, }) { const { formatMessage } = useIntl(); const history = useHistory(); + const [payload, setPayload] = useState({}); const { setFiles, @@ -127,22 +132,30 @@ function CustomerForm({ const initialValues = useMemo( () => ({ - ...(customerDetail + ...(customer ? { - ...pick(customerDetail, Object.keys(defaultInitialValues)), + ...pick(customer, Object.keys(defaultInitialValues)), } : { ...defaultInitialValues, }), }), - [customerDetail, defaultInitialValues], + [customer, defaultInitialValues], + ); + + const saveInvokeSubmit = useCallback( + (payload) => { + onFormSubmit && onFormSubmit(payload); + }, + + [onFormSubmit], ); useEffect(() => { - customerDetail && customerDetail.id + customer && customer.id ? changePageTitle(formatMessage({ id: 'edit_customer_details' })) : changePageTitle(formatMessage({ id: 'new_customer' })); - }, [changePageTitle, customerDetail, formatMessage]); + }, [changePageTitle, customer, formatMessage]); const formik = useFormik({ enableReinitialize: true, @@ -152,9 +165,9 @@ function CustomerForm({ }, onSubmit: (values, { setSubmitting, resetForm, setErrors }) => { - const formValues = { ...values }; - if (customerDetail && customerDetail.id) { - requestEditCustomer(customerDetail.id, formValues) + const formValues = { ...values, status: payload.publish }; + if (customer && customer.id) { + requestEditCustomer(customer.id, formValues) .then((response) => { AppToaster.show({ message: formatMessage({ @@ -163,8 +176,9 @@ function CustomerForm({ intent: Intent.SUCCESS, }); setSubmitting(false); - history.push('/customers'); + // history.push('/customers'); resetForm(); + saveInvokeSubmit({ action: 'update', ...payload }); }) .catch((errors) => { setSubmitting(false); @@ -178,7 +192,9 @@ function CustomerForm({ }), intent: Intent.SUCCESS, }); - history.push('/customers'); + // history.push('/customers'); + setSubmitting(false); + saveInvokeSubmit({ action: 'new', ...payload }); }) .catch((errors) => { setSubmitting(false); @@ -202,8 +218,8 @@ function CustomerForm({ ); const initialAttachmentFiles = useMemo(() => { - return customerDetail && customerDetail.media - ? customerDetail.media.map((attach) => ({ + return customer && customer.media + ? customer.media.map((attach) => ({ preview: attach.attachment_file, upload: true, metadata: { ...attach }, @@ -224,11 +240,20 @@ function CustomerForm({ }, [setDeletedFiles, deletedFiles], ); + const handleSubmitClick = useCallback( + (payload) => { + setPayload(payload); + formik.handleSubmit(); + }, + [setPayload, formik], + ); - const handleCancelClickBtn = () => { - history.goBack(); - }; - + const handleCancelClick = useCallback( + (payload) => { + onCancelForm && onCancelForm(payload); + }, + [onCancelForm], + ); return (
@@ -396,34 +421,14 @@ function CustomerForm({ - - + +
); } diff --git a/client/src/containers/Customers/CustomersList.js b/client/src/containers/Customers/CustomersList.js index 7c2098c3c..9d47c649b 100644 --- a/client/src/containers/Customers/CustomersList.js +++ b/client/src/containers/Customers/CustomersList.js @@ -137,21 +137,21 @@ function CustomersList({ [fetchCustomers], ); - // Handle items bulk delete button click., + // Handle Customers bulk delete button click., const handleBulkDelete = useCallback( - (itemsIds) => { - setBulkDelete(itemsIds); + (customersIds) => { + setBulkDelete(customersIds); }, [setBulkDelete], ); - // Handle cancel accounts bulk delete. + // Handle cancel cusomters bulk delete. const handleCancelBulkDelete = useCallback(() => { setBulkDelete(false); }, []); - // Handle confirm items bulk delete. + // Handle confirm customers bulk delete. const handleConfirmBulkDelete = useCallback(() => { requestDeleteBulkCustomers(bulkDelete) .then(() => { @@ -163,7 +163,7 @@ function CustomersList({ intent: Intent.SUCCESS, }); }) - .catch((errors) => { + .catch((error) => { setBulkDelete(false); }); }, [requestDeleteBulkCustomers, bulkDelete, formatMessage]); diff --git a/client/src/containers/Customers/withCustomerDetail.js b/client/src/containers/Customers/withCustomerDetail.js index 755c2fc1a..bd7cc8a65 100644 --- a/client/src/containers/Customers/withCustomerDetail.js +++ b/client/src/containers/Customers/withCustomerDetail.js @@ -2,7 +2,7 @@ import { connect } from 'react-redux'; import { getCustomerById } from 'store/customers/customers.reducer'; const mapStateToProps = (state, props) => ({ - customerDetail: getCustomerById(state, props.customerId), + customer: getCustomerById(state, props.customerId), }); export default connect(mapStateToProps); diff --git a/client/src/containers/Customers/withCustomersActions.js b/client/src/containers/Customers/withCustomersActions.js index 66900f4f8..590004dd3 100644 --- a/client/src/containers/Customers/withCustomersActions.js +++ b/client/src/containers/Customers/withCustomersActions.js @@ -4,13 +4,14 @@ import { submitCustomer, editCustomer, deleteCustomer, + deleteBulkCustomers } from 'store/customers/customers.actions'; import t from 'store/types'; export const mapDispatchToProps = (dispatch) => ({ requestFetchCustomers: (query) => dispatch(fetchCustomers({ query })), requestDeleteCustomer: (id) => dispatch(deleteCustomer({ id })), - // requestDeleteBulkCustomers:(ids)=>dispatch(deleteBulkCustomers({ids})), + requestDeleteBulkCustomers:(ids)=>dispatch(deleteBulkCustomers({ids})), requestSubmitCustomer: (form) => dispatch(submitCustomer({ form })), requestEditCustomer: (id, form) => dispatch(editCustomer({ id, form })), diff --git a/client/src/containers/Expenses/ExpenseDataTable.js b/client/src/containers/Expenses/ExpenseDataTable.js index 99b7675f9..721ec1922 100644 --- a/client/src/containers/Expenses/ExpenseDataTable.js +++ b/client/src/containers/Expenses/ExpenseDataTable.js @@ -39,10 +39,10 @@ function ExpensesDataTable({ changeCurrentView, changePageSubtitle, setTopbarEditView, - + // #withView viewMeta, - + // #ownProps loading, onFetchData, @@ -99,10 +99,9 @@ function ExpensesDataTable({ const actionMenuList = useCallback( (expense) => ( - + - + ), - [handleEditExpense, handleDeleteExpense, handlePublishExpense, formatMessage], + [ + handleEditExpense, + handleDeleteExpense, + handlePublishExpense, + formatMessage, + ], ); - const onRowContextMenu = useCallback((cell) => { - return actionMenuList(cell.row.original); - }, [actionMenuList]); + const onRowContextMenu = useCallback( + (cell) => { + return actionMenuList(cell.row.original); + }, + [actionMenuList], + ); - const expenseAccountAccessor = (expense) => { - if (expense.categories.length === 1) { - return expense.categories[0].expense_account.name; - } else if (expense.categories.length > 1) { - const mutliCategories = expense.categories.map(category => - (
- {category.expense_account.name} ${ category.amount }
) + const expenseAccountAccessor = (_expense) => { + if (_expense.categories.length === 1) { + return _expense.categories[0].expense_account.name; + } else if (_expense.categories.length > 1) { + const mutliCategories = _expense.categories.map((category) => ( +
+ - {category.expense_account.name} ${category.amount} +
+ )); + return ( + {'- Multi Categories -'} ); - return { '- Multi Categories -' }; } - } + }; const columns = useMemo( () => [ @@ -180,7 +191,7 @@ function ExpensesDataTable({ id: 'publish', Header: formatMessage({ id: 'publish' }), accessor: (r) => { - return !r.published ? ( + return r.published ? ( @@ -279,5 +290,5 @@ export default compose( expensesLoading, expensesPagination, })), - withViewDetails, + withViewDetails(), )(ExpensesDataTable); diff --git a/client/src/containers/Expenses/ExpenseFooter.js b/client/src/containers/Expenses/ExpenseFooter.js index 8bd219518..d1506b9ff 100644 --- a/client/src/containers/Expenses/ExpenseFooter.js +++ b/client/src/containers/Expenses/ExpenseFooter.js @@ -2,42 +2,56 @@ import React from 'react'; import { Intent, Button } from '@blueprintjs/core'; import { FormattedMessage as T } from 'react-intl'; -export default function ExpenseFooter({ +export default function ExpenseFloatingFooter({ formik: { isSubmitting }, onSubmitClick, onCancelClick, + + expense, }) { return (
+ + +
); -}; +} diff --git a/client/src/containers/Expenses/ExpenseForm.js b/client/src/containers/Expenses/ExpenseForm.js index 9ea5a7b7f..dd4dba147 100644 --- a/client/src/containers/Expenses/ExpenseForm.js +++ b/client/src/containers/Expenses/ExpenseForm.js @@ -11,15 +11,13 @@ import moment from 'moment'; import { Intent, FormGroup, TextArea } from '@blueprintjs/core'; import { FormattedMessage as T, useIntl } from 'react-intl'; import { pick } from 'lodash'; -import { useQuery } from 'react-query'; -import { Col, Row } from 'react-grid-system'; import ExpenseFormHeader from './ExpenseFormHeader'; import ExpenseTable from './ExpenseTable'; import ExpenseFloatingFooter from './ExpenseFooter'; import withExpensesActions from 'containers/Expenses/withExpensesActions'; -import withExpneseDetail from 'containers/Expenses/withExpenseDetail'; +import withExpenseDetail from 'containers/Expenses/withExpenseDetail'; import withAccountsActions from 'containers/Accounts/withAccountsActions'; import withDashboardActions from 'containers/Dashboard/withDashboardActions'; import withMediaActions from 'containers/Media/withMediaActions'; @@ -44,13 +42,14 @@ function ExpenseForm({ changePageSubtitle, //#withExpenseDetail - // @todo expenseDetail to expense - expenseDetail, + expense, // #own Props expenseId, onFormSubmit, onCancelForm, + onClickAddNewRow, + onClickRemoveRow, }) { const { formatMessage } = useIntl(); const [payload, setPayload] = useState({}); @@ -75,18 +74,17 @@ function ExpenseForm({ }; useEffect(() => { - if (expenseDetail && expenseDetail.id) { + if (expense && expense.id) { changePageTitle(formatMessage({ id: 'edit_expense' })); // changePageSubtitle(`No. ${expenseDetail.payment_account_id}`); } else { changePageTitle(formatMessage({ id: 'new_expense' })); } // @todo not functions just states. - }, [changePageTitle, changePageSubtitle, expenseDetail, formatMessage]); + }, [changePageTitle, changePageSubtitle, expense, formatMessage]); const validationSchema = Yup.object().shape({ beneficiary: Yup.string() - // .required() .label(formatMessage({ id: 'beneficiary' })), payment_account_id: Yup.string() .required() @@ -104,8 +102,12 @@ function ExpenseForm({ Yup.object().shape({ index: Yup.number().nullable(), amount: Yup.number().nullable(), - // @todo expense_account_id is required. - expense_account_id: Yup.number().nullable(), + expense_account_id: Yup.number() + .nullable() + .when(['amount'], { + is: (amount) => amount, + then: Yup.number().required(), + }), description: Yup.string().nullable(), }), ), @@ -115,6 +117,7 @@ function ExpenseForm({ (payload) => { onFormSubmit && onFormSubmit(payload); }, + [onFormSubmit], ); @@ -146,32 +149,41 @@ function ExpenseForm({ [defaultCategory], ); + const orderingCategoriesIndex = (categories) => { + return categories.map((category, index) => ({ + ...category, + index: index + 1, + })); + }; + const initialValues = useMemo( () => ({ - ...(expenseDetail + ...(expense ? { - ...pick(expenseDetail, Object.keys(defaultInitialValues)), - categories: expenseDetail.categories.map((category) => ({ + ...pick(expense, Object.keys(defaultInitialValues)), + categories: expense.categories.map((category) => ({ ...pick(category, Object.keys(defaultCategory)), - }), - ), + })), } : { ...defaultInitialValues, + categories: orderingCategoriesIndex( + defaultInitialValues.categories, + ), }), }), - [expenseDetail, defaultInitialValues, defaultCategory], + [expense, defaultInitialValues, defaultCategory], ); const initialAttachmentFiles = useMemo(() => { - return expenseDetail && expenseDetail.media - ? expenseDetail.media.map((attach) => ({ + return expense && expense.media + ? expense.media.map((attach) => ({ preview: attach.attachment_file, uploaded: true, metadata: { ...attach }, })) : []; - }, [expenseDetail]); + }, [expense]); const formik = useFormik({ enableReinitialize: true, @@ -181,11 +193,12 @@ function ExpenseForm({ }, onSubmit: async (values, { setSubmitting, setErrors, resetForm }) => { const categories = values.categories.filter( - (category) => category.amount || category.index, + (category) => + category.amount && category.index && category.expense_account_id, ); const form = { ...values, - published: payload.publish, + publish: payload.publish, categories, }; @@ -193,8 +206,8 @@ function ExpenseForm({ new Promise((resolve, reject) => { const requestForm = { ...form, media_ids: mdeiaIds }; - if (expenseDetail && expenseDetail.id) { - requestEditExpense(expenseDetail.id, requestForm) + if (expense && expense.id) { + requestEditExpense(expense.id, requestForm) .then((response) => { AppToaster.show({ message: formatMessage( @@ -207,10 +220,8 @@ function ExpenseForm({ saveInvokeSubmit({ action: 'update', ...payload }); clearSavedMediaIds([]); resetForm(); - resolve(response); }) .catch((errors) => { - // @todo handle errors. if (errors.find((e) => e.type === 'TOTAL.AMOUNT.EQUALS.ZERO')) { } setErrors( @@ -234,10 +245,11 @@ function ExpenseForm({ intent: Intent.SUCCESS, }); setSubmitting(false); + formik.resetForm(); saveInvokeSubmit({ action: 'new', ...payload }); clearSavedMediaIds(); - resetForm(); - resolve(response); + + // resolve(response); }) .catch((errors) => { setSubmitting(false); @@ -260,6 +272,7 @@ function ExpenseForm({ const handleSubmitClick = useCallback( (payload) => { setPayload(payload); + formik.resetForm(); formik.handleSubmit(); }, [setPayload, formik], @@ -313,13 +326,13 @@ function ExpenseForm({ hint={'Attachments: Maxiumum size: 20MB'} /> - - + ); } @@ -329,5 +342,5 @@ export default compose( withAccountsActions, withDashboardActions, withMediaActions, - withExpneseDetail(), + withExpenseDetail(), )(ExpenseForm); diff --git a/client/src/containers/Expenses/ExpenseFormHeader.js b/client/src/containers/Expenses/ExpenseFormHeader.js index e82d65239..1afdea739 100644 --- a/client/src/containers/Expenses/ExpenseFormHeader.js +++ b/client/src/containers/Expenses/ExpenseFormHeader.js @@ -28,6 +28,7 @@ function ExpenseFormHeader({ formik: { errors, touched, setFieldValue, getFieldProps, values }, currenciesList, accounts, + accountsTypes, }) { const [selectedItems, setSelectedItems] = useState({}); @@ -102,7 +103,6 @@ function ExpenseFormHeader({ const onItemsSelect = useCallback( (filedName) => { return (filed) => { - // @todo @mohamed setSelectedItems({ ...selectedItems, [filedName]: filed, @@ -256,8 +256,9 @@ function ExpenseFormHeader({ } export default compose( - withAccounts(({ accounts }) => ({ + withAccounts(({ accounts, accountsTypes }) => ({ accounts, + accountsTypes, })), withCurrencies(({ currenciesList }) => ({ currenciesList, diff --git a/client/src/containers/Expenses/ExpenseTable.js b/client/src/containers/Expenses/ExpenseTable.js index a4d06926c..733ac2912 100644 --- a/client/src/containers/Expenses/ExpenseTable.js +++ b/client/src/containers/Expenses/ExpenseTable.js @@ -41,15 +41,20 @@ function ExpenseTable({ (rowIndex, columnId, value) => { const newRows = rows.map((row, index) => { if (index === rowIndex) { - return { ...rows[rowIndex], [columnId]: value }; + return { + ...rows[rowIndex], + [columnId]: value, + }; } return { ...row }; }); setRow(newRows); setFieldValue( 'categories', - newRows.map((row) => ({ + + newRows.map((row, index) => ({ ...omit(row, ['rowType']), + index: index + 1, })), ); }, @@ -60,15 +65,28 @@ function ExpenseTable({ const handleRemoveRow = useCallback( (rowIndex) => { const removeIndex = parseInt(rowIndex, 10); - const newRows = rows.filter((row, index) => index !== removeIndex); + const newRows = rows.filter((row, index) => index !== removeIndex); setRow([...newRows]); + setFieldValue( 'categories', + newRows .filter((row) => row.rowType === 'editor') - .map((row) => ({ ...omit(row, ['rowType']) })), + .map((row, index) => ({ + ...omit(row, ['rowType']), + index: index + 1, + })), ); + // const newRows = rows.filter((row, index) => index !== removeIndex); + // setRow([...newRows]); + // setFieldValue( + // 'categories', + // newRows + // .filter((row) => row.rowType === 'editor') + // .map((row) => ({ ...omit(row, ['rowType']) })), + // ); onClickRemoveRow && onClickRemoveRow(removeIndex); }, [rows, setFieldValue, onClickRemoveRow], @@ -82,7 +100,7 @@ function ExpenseTable({ data, payload, }) => { - if (data.length <= index + 2) { + if (data.length <= index + 1) { return ''; } const onClickRemoveRole = () => { @@ -104,7 +122,7 @@ function ExpenseTable({ // Total text cell renderer. const TotalExpenseCellRenderer = (chainedComponent) => (props) => { - if (props.data.length === props.row.index + 2) { + if (props.data.length <= props.row.index + 1) { return ( {formatMessage({ id: 'total_currency' }, { currency: 'USD' })} @@ -115,14 +133,14 @@ function ExpenseTable({ }; const NoteCellRenderer = (chainedComponent) => (props) => { - if (props.data.length === props.row.index + 2) { + if (props.data.length === props.row.index + 1) { return ''; } return chainedComponent(props); }; const TotalAmountCellRenderer = (chainedComponent, type) => (props) => { - if (props.data.length === props.row.index + 2) { + if (props.data.length === props.row.index + 1) { const total = props.data.reduce((total, entry) => { const amount = parseInt(entry[type], 10); const computed = amount ? total + amount : total; @@ -147,7 +165,12 @@ function ExpenseTable({ disableSortBy: true, }, { - Header: (<>{ formatMessage({ id: 'expense_category' }) }), + Header: ( + <> + {formatMessage({ id: 'expense_category' })} + + + ), id: 'expense_account_id', accessor: 'expense_account_id', Cell: TotalExpenseCellRenderer(AccountsListFieldCell), @@ -193,7 +216,7 @@ function ExpenseTable({ const rowClassNames = useCallback( (row) => ({ - 'row--total': rows.length === row.index + 2, + 'row--total': rows.length === row.index + 1, }), [rows], ); diff --git a/client/src/containers/Expenses/ExpenseViewTabs.js b/client/src/containers/Expenses/ExpenseViewTabs.js index 85bd248a9..41fe76ff7 100644 --- a/client/src/containers/Expenses/ExpenseViewTabs.js +++ b/client/src/containers/Expenses/ExpenseViewTabs.js @@ -96,11 +96,11 @@ const withExpensesViewTabs = connect(mapStateToProps); export default compose( withRouter, - withViewDetails(), withExpensesViewTabs, + withExpensesActions, + withDashboardActions, + withViewDetails(), withExpenses(({ expensesViews }) => ({ expensesViews, })), - withExpensesActions, - withDashboardActions, )(ExpenseViewTabs); diff --git a/client/src/containers/Expenses/Expenses.js b/client/src/containers/Expenses/Expenses.js index f372fa43e..11dedaf7a 100644 --- a/client/src/containers/Expenses/Expenses.js +++ b/client/src/containers/Expenses/Expenses.js @@ -14,6 +14,7 @@ import { compose } from 'utils'; function Expenses({ // #withwithAccountsActions requestFetchAccounts, + requestFetchAccountTypes, // #withExpensesActions requestFetchExpense, @@ -24,18 +25,17 @@ function Expenses({ const history = useHistory(); const { id } = useParams(); - // @todo - const fetchAccounts = useQuery('accounts-expense-list', (key) => + const fetchAccounts = useQuery('accounts-list', (key) => requestFetchAccounts(), ); - // @todo - const fetchExpense = useQuery(id && ['expense', id], (key, expense_Id) => - requestFetchExpense(expense_Id), + const fetchExpense = useQuery( + ['expense', id], + (key, _id) => requestFetchExpense(_id), + { enabled: !!id }, ); - // @todo - const fetchCurrencies = useQuery('currencies-expense-list', () => + const fetchCurrencies = useQuery('currencies', () => requestFetchCurrencies(), ); const handleFormSubmit = useCallback( @@ -46,7 +46,7 @@ function Expenses({ ); const handleCancel = useCallback(() => { - history.push('/expenses-list'); + history.goBack(); }, [history]); return ( @@ -56,6 +56,7 @@ function Expenses({ fetchAccounts.isFetching || fetchCurrencies.isFetching } + name={'expense-form'} > - {/* } confirmButtonText={ @@ -236,10 +235,10 @@ function ExpensesList({ >

-
*/} + ); @@ -248,7 +247,7 @@ function ExpensesList({ export default compose( withDashboardActions, withExpensesActions, - withExpenses(({ expensesTableQuery }) => ({ expensesTableQuery })), withViewsActions, - withResourceActions + withResourceActions, + withExpenses(({ expensesTableQuery }) => ({ expensesTableQuery })), )(ExpensesList); diff --git a/client/src/containers/Expenses/withExpenses.js b/client/src/containers/Expenses/withExpenses.js index 5ba6d1187..4eaeb6053 100644 --- a/client/src/containers/Expenses/withExpenses.js +++ b/client/src/containers/Expenses/withExpenses.js @@ -14,7 +14,6 @@ export default (mapState) => { const mapStateToProps = (state, props) => { const query = getExpensesTableQuery(state, props); - const mapped = { expensesCurrentPage: getExpensesItems(state, props, query), expensesViews: getResourceViews(state, props, 'expenses'), diff --git a/client/src/containers/Expenses/withExpensesActions.js b/client/src/containers/Expenses/withExpensesActions.js index e6592eda9..47ecc298b 100644 --- a/client/src/containers/Expenses/withExpensesActions.js +++ b/client/src/containers/Expenses/withExpensesActions.js @@ -10,7 +10,7 @@ import { } from 'store/expenses/expenses.actions'; import t from 'store/types'; -export const mapDispatchToProps = (dispatch) => ({ +const mapDispatchToProps = (dispatch) => ({ requestSubmitExpense: (form) => dispatch(submitExpense({ form })), requestFetchExpense: (id) => dispatch(fetchExpense({ id })), requestEditExpense: (id, form) => dispatch(editExpense({ id, form })), diff --git a/client/src/containers/Items/ItemCategoriesList.js b/client/src/containers/Items/ItemCategoriesList.js index 12f7bbd6d..c7d229d1c 100644 --- a/client/src/containers/Items/ItemCategoriesList.js +++ b/client/src/containers/Items/ItemCategoriesList.js @@ -49,7 +49,7 @@ const ItemCategoryList = ({ }, [id, changePageTitle, formatMessage]); const fetchCategories = useQuery( - ['items-categories-table', filter], + ['items-categories-list', filter], (key, query) => requestFetchItemCategories(query), ); @@ -183,7 +183,9 @@ const ItemCategoryList = ({ >

diff --git a/client/src/containers/Items/ItemFormPage.js b/client/src/containers/Items/ItemFormPage.js index 71b95cb9c..bd2bc7200 100644 --- a/client/src/containers/Items/ItemFormPage.js +++ b/client/src/containers/Items/ItemFormPage.js @@ -1,5 +1,5 @@ -import React, {useCallback } from 'react'; -import { useParams,useHistory } from 'react-router-dom'; +import React, { useCallback } from 'react'; +import { useParams, useHistory } from 'react-router-dom'; import { useQuery } from 'react-query'; import DashboardInsider from 'components/Dashboard/DashboardInsider'; @@ -12,7 +12,6 @@ import withItemsActions from './withItemsActions'; import { compose } from 'utils'; - const ItemFormContainer = ({ // #withDashboardActions changePageTitle, @@ -22,6 +21,7 @@ const ItemFormContainer = ({ // #withItemsActions requestFetchItems, + requestFetchItem, // #withItemCategoriesActions requestFetchItemCategories, @@ -29,35 +29,47 @@ const ItemFormContainer = ({ const { id } = useParams(); const history = useHistory(); - const fetchAccounts = useQuery('accounts-list', - (key) => requestFetchAccounts()); + const fetchAccounts = useQuery('accounts-list', (key) => + requestFetchAccounts(), + ); - const fetchCategories = useQuery('item-categories-list', - (key) => requestFetchItemCategories()); + const fetchCategories = useQuery('item-categories-list', (key) => + requestFetchItemCategories(), + ); - const fetchItemDetail = useQuery( - id && ['item-detail-list', id], - (key) => requestFetchItems()); + const fetchItemDetail = useQuery( + ['item', id], + (key, _id) => requestFetchItem(_id), + { + enabled: !!id, + }, + ); -const handleFormSubmit =useCallback((payload)=>{ + const handleFormSubmit = useCallback( + (payload) => { + payload.redirect && history.push('/items'); + }, + [history], + ); - payload.redirect && history.push('/items/new'); - -},[history]) - -const handleCancel =useCallback(()=>{ - - history.push('/items/new'); -},[history]) + const handleCancel = useCallback(() => { + // history.push('/items'); + history.goBack(); + }, [history]); return ( - + ); @@ -67,5 +79,5 @@ export default compose( withDashboardActions, withAccountsActions, withItemCategoriesActions, - withItemsActions + withItemsActions, )(ItemFormContainer); diff --git a/client/src/containers/Items/ItemsFooter.js b/client/src/containers/Items/ItemsFooter.js new file mode 100644 index 000000000..57b0e9386 --- /dev/null +++ b/client/src/containers/Items/ItemsFooter.js @@ -0,0 +1,56 @@ +import React from 'react'; +import { Intent, Button } from '@blueprintjs/core'; +import { FormattedMessage as T } from 'react-intl'; + +export default function ItemFloatingFooter({ + formik: { isSubmitting }, + onSubmitClick, + onCancelClick, + itemDetail, +}) { + return ( + + ); +} diff --git a/client/src/containers/Items/withItemsActions.js b/client/src/containers/Items/withItemsActions.js index fc8cafe94..eb95e8cff 100644 --- a/client/src/containers/Items/withItemsActions.js +++ b/client/src/containers/Items/withItemsActions.js @@ -1,6 +1,7 @@ import {connect} from 'react-redux'; import { fetchItems, + fetchItem, deleteItem, submitItem, editItem, @@ -10,6 +11,7 @@ import t from 'store/types'; export const mapDispatchToProps = (dispatch) => ({ requestFetchItems: (query) => dispatch(fetchItems({ query })), + requestFetchItem: (id) => dispatch(fetchItem({ id })), requestDeleteItem: (id) => dispatch(deleteItem({ id })), requestDeleteBulkItems:(ids)=>dispatch(deleteBulkItems({ids})), requestSubmitItem: (form) => dispatch(submitItem({ form })), diff --git a/client/src/lang/en/index.js b/client/src/lang/en/index.js index 3d8fb8f52..8e44a0462 100644 --- a/client/src/lang/en/index.js +++ b/client/src/lang/en/index.js @@ -438,6 +438,9 @@ export default { 'The expense has been successfully deleted', the_expenses_has_been_successfully_deleted: 'The expenses has been successfully deleted', + once_delete_these_expenses_you_will_not_able_restore_them: + "Once you delete these expenses, you won't be able to retrieve them later. Are you sure you want to delete them?", + the_expense_id_has_been_published: 'The expense id has been published', select_beneficiary_account: 'Select Beneficiary Account', total_amount_equals_zero: 'Total amount equals zero', diff --git a/client/src/lang/en/locale.js b/client/src/lang/en/locale.js index 47ba9bba1..395a78ccc 100644 --- a/client/src/lang/en/locale.js +++ b/client/src/lang/en/locale.js @@ -8,7 +8,7 @@ export const locale = { notType: ({ path, type, value, originalValue }) => { let isCast = originalValue != null && originalValue !== value; let msg = - `${path} must beeeeee a \`${type}\` type, ` + + `${path} must be a \`${type}\` type, ` + `but the final value was: \`${printValue(value, true)}\`` + (isCast ? ` (cast from the value \`${printValue(originalValue, true)}\`).` diff --git a/client/src/store/customers/customers.actions.js b/client/src/store/customers/customers.actions.js index ab705bc92..6e2a131a5 100644 --- a/client/src/store/customers/customers.actions.js +++ b/client/src/store/customers/customers.actions.js @@ -107,3 +107,19 @@ export const deleteCustomer = ({ id }) => { }); }; +export const deleteBulkCustomers = ({ ids }) => { + return (dispatch) => + new Promise((resolve, reject) => { + ApiService.delete('customers', { params: { ids } }) + .then((response) => { + dispatch({ + type: t.CUSTOMERS_BULK_DELETE, + payload: { ids }, + }); + resolve(response); + }) + .catch((error) => { + reject(error.response.data.errors || []); + }); + }); +}; diff --git a/client/src/store/customers/customers.reducer.js b/client/src/store/customers/customers.reducer.js index 86d198974..a0ea60a8e 100644 --- a/client/src/store/customers/customers.reducer.js +++ b/client/src/store/customers/customers.reducer.js @@ -40,6 +40,17 @@ const customersReducer = createReducer(initialState, { const { loading } = action.payload; state.loading = !!loading; }, + [t.CUSTOMERS_BULK_DELETE]: (state, action) => { + const { ids } = action.payload; + const items = { ...state.items }; + + ids.forEach((id) => { + if (typeof items[id] !== 'undefined') { + delete items[id]; + } + }); + state.items = items; + }, }); export default createTableQueryReducers('customers', customersReducer); diff --git a/client/src/store/customers/customers.type.js b/client/src/store/customers/customers.type.js index c63620434..81b12d864 100644 --- a/client/src/store/customers/customers.type.js +++ b/client/src/store/customers/customers.type.js @@ -4,5 +4,6 @@ export default { CUSTOMERS_PAGE_SET: 'CUSTOMERS_PAGE_SET', CUSTOMERS_TABLE_LOADING: 'CUSTOMERS_TABLE_LOADING', CUSTOMERS_TABLE_QUERIES_ADD: 'CUSTOMERS_TABLE_QUERIES_ADD', - CUSTOMER_DELETE:'CUSTOMER_DELETE' + CUSTOMER_DELETE:'CUSTOMER_DELETE', + CUSTOMERS_BULK_DELETE:'CUSTOMERS_BULK_DELETE' }; diff --git a/client/src/store/expenses/expenses.actions.js b/client/src/store/expenses/expenses.actions.js index 1adad7578..4f27a56fe 100644 --- a/client/src/store/expenses/expenses.actions.js +++ b/client/src/store/expenses/expenses.actions.js @@ -122,7 +122,7 @@ export const deleteExpense = ({ id }) => { export const deleteBulkExpenses = ({ ids }) => { return (dispatch) => new Promise((resolve, reject) => { - ApiService.delete('expenses/bulk', { params: { ids } }) + ApiService.delete('expenses', { params: { ids } }) .then((response) => { dispatch({ type: t.EXPENSES_BULK_DELETE, diff --git a/client/src/store/items/items.actions.js b/client/src/store/items/items.actions.js index 461bd949b..963890492 100644 --- a/client/src/store/items/items.actions.js +++ b/client/src/store/items/items.actions.js @@ -2,93 +2,101 @@ import ApiService from 'services/ApiService'; import t from 'store/types'; export const submitItem = ({ form }) => { - return dispatch => ApiService.post(`items`, form); + return (dispatch) => ApiService.post(`items`, form); }; export const editItem = ({ id, form }) => { - return dispatch => ApiService.post(`items/${id}`, form); + return (dispatch) => ApiService.post(`items/${id}`, form); }; export const fetchItems = ({ query }) => { - return (dispatch, getState) => new Promise((resolve, reject) => { - const pageQuery = getState().items.tableQuery; + return (dispatch, getState) => + new Promise((resolve, reject) => { + const pageQuery = getState().items.tableQuery; - dispatch({ - type: t.ITEMS_TABLE_LOADING, - payload: { loading: true }, - }); - dispatch({ - type: t.SET_DASHBOARD_REQUEST_LOADING, - }); - ApiService.get(`items`, { params: { ...pageQuery, ...query } }).then(response => { - dispatch({ - type: t.ITEMS_SET, - items: response.data.items.results, - }); - dispatch({ - type: t.ITEMS_PAGE_SET, - items: response.data.items.results, - customViewId: response.data.customViewId, - paginationMeta: response.data.items.pagination, - }); dispatch({ type: t.ITEMS_TABLE_LOADING, - payload: { loading: false }, + payload: { loading: true }, }); dispatch({ - type: t.SET_DASHBOARD_REQUEST_COMPLETED, + type: t.SET_DASHBOARD_REQUEST_LOADING, }); - resolve(response); - }).catch((error) => { - dispatch({ - type: t.SET_DASHBOARD_REQUEST_COMPLETED, - }); - reject(error); + ApiService.get(`items`, { params: { ...pageQuery, ...query } }) + .then((response) => { + dispatch({ + type: t.ITEMS_SET, + items: response.data.items.results, + }); + dispatch({ + type: t.ITEMS_PAGE_SET, + items: response.data.items.results, + customViewId: response.data.customViewId, + paginationMeta: response.data.items.pagination, + }); + dispatch({ + type: t.ITEMS_TABLE_LOADING, + payload: { loading: false }, + }); + dispatch({ + type: t.SET_DASHBOARD_REQUEST_COMPLETED, + }); + resolve(response); + }) + .catch((error) => { + dispatch({ + type: t.SET_DASHBOARD_REQUEST_COMPLETED, + }); + reject(error); + }); }); - }); }; export const fetchItem = ({ id }) => { - return dispatch => + return (dispatch) => new Promise((resolve, reject) => { ApiService.get(`items/${id}`) - .then(response => { + .then((response) => { dispatch({ type: t.ITEM_SET, - item: response.data.item + item: response.data.item, }); }) - .catch(error => { + .catch((error) => { reject(error); }); }); }; export const deleteItem = ({ id }) => { - return dispatch => new Promise((resolve, reject) => { - ApiService.delete(`items/${id}`) - .then((response) => { - dispatch({ - type: t.ITEM_DELETE, - payload: { id }, + return (dispatch) => + new Promise((resolve, reject) => { + ApiService.delete(`items/${id}`) + .then((response) => { + dispatch({ + type: t.ITEM_DELETE, + payload: { id }, + }); + resolve(response); + }) + .catch((error) => { + reject(error); }); - resolve(response); - }).catch((error) => { reject(error); }); - }); + }); }; - - export const deleteBulkItems = ({ ids }) => { - return dispatch => new Promise((resolve, reject) => { - ApiService.delete(`items`, { params: { ids }}).then((response) => { - dispatch({ - type: t.ITEMS_BULK_DELETE, - payload: { ids } - }); - resolve(response); - }).catch((error) => { - reject(error); + return (dispatch) => + new Promise((resolve, reject) => { + ApiService.delete('items', { params: { ids } }) + .then((response) => { + dispatch({ + type: t.ITEMS_BULK_DELETE, + payload: { ids }, + }); + resolve(response); + }) + .catch((error) => { + reject(error.response.data.errors || []); + }); }); - }); }; diff --git a/client/src/store/items/items.reducer.js b/client/src/store/items/items.reducer.js index 25fecd60e..b52b7966c 100644 --- a/client/src/store/items/items.reducer.js +++ b/client/src/store/items/items.reducer.js @@ -1,8 +1,6 @@ import t from 'store/types'; import { createReducer } from '@reduxjs/toolkit'; -import { - getItemsViewPages, -} from 'store/items/items.selectors'; +import { getItemsViewPages } from 'store/items/items.selectors'; import { createTableQueryReducers } from 'store/queryReducers'; const initialState = { @@ -20,7 +18,7 @@ const itemsReducer = createReducer(initialState, { [t.ITEMS_SET]: (state, action) => { const _items = {}; - action.items.forEach(item => { + action.items.forEach((item) => { _items[item.id] = item; }); state.items = { @@ -43,11 +41,11 @@ const itemsReducer = createReducer(initialState, { if (typeof itemRelation === 'undefined') { state.itemsRelation[item.id] = []; } - const filteredRelation = state.itemsRelation[item.id] - .filter((relation) => ( + const filteredRelation = state.itemsRelation[item.id].filter( + (relation) => ( relation.viewId === viewId && relation.pageNumber === paginationMeta.page - )); + )); filteredRelation.push({ viewId, @@ -61,10 +59,10 @@ const itemsReducer = createReducer(initialState, { pages: { ...viewPages, [paginationMeta.page]: { - ids: items.map(i => i.id), + ids: items.map((i) => i.id), meta: paginationMeta, }, - }, + }, }; }, @@ -93,9 +91,21 @@ const itemsReducer = createReducer(initialState, { state.loading = !!loading; }, - [t.ITEMS_SET_CURRENT_VIEW]: (state, action) => { + [t.ITEMS_SET_CURRENT_VIEW]: (state, action) => { state.currentViewId = action.currentViewId; }, + + [t.ITEMS_BULK_DELETE]: (state, action) => { + const { ids } = action.payload; + const items = { ...state.items }; + + ids.forEach((id) => { + if (typeof items[id] !== 'undefined') { + delete items[id]; + } + }); + state.items = items; + }, }); export default createTableQueryReducers('items', itemsReducer);