diff --git a/client/src/containers/Entries/ItemsEntriesTable.js b/client/src/containers/Entries/ItemsEntriesTable.js index 3167afbdb..491bced8b 100644 --- a/client/src/containers/Entries/ItemsEntriesTable.js +++ b/client/src/containers/Entries/ItemsEntriesTable.js @@ -35,10 +35,10 @@ function ItemsEntriesTable({ const [cellsLoading, setCellsLoading] = React.useState(null); // Fetches the item details. - const { data: item, isFetching: isItemFetching } = useItem( + const { data: item, isFetching: isItemFetching, isSuccess: isItemSuccess } = useItem( rowItem && rowItem.itemId, { - enabled: !!rowItem, + enabled: !!(rowItem && rowItem.itemId), }, ); @@ -58,7 +58,7 @@ function ItemsEntriesTable({ // Once the item selected and fetched set the initial details to the table. useEffect(() => { - if (item && rowItem) { + if (isItemSuccess && item && rowItem) { const { rowIndex } = rowItem; const price = itemType === ITEM_TYPE.PURCHASABLE @@ -82,7 +82,7 @@ function ItemsEntriesTable({ setRowItem(null); saveInvoke(onUpdateData, newRows); } - }, [item, rowItem, rows, itemType, onUpdateData]); + }, [item, rowItem, rows, itemType, onUpdateData, isItemSuccess]); // Allows to observes `entries` to make table rows outside controlled. useEffect(() => { diff --git a/client/src/containers/Items/ItemFormProvider.js b/client/src/containers/Items/ItemFormProvider.js index c7af88ce7..94a9fcf0d 100644 --- a/client/src/containers/Items/ItemFormProvider.js +++ b/client/src/containers/Items/ItemFormProvider.js @@ -20,17 +20,18 @@ function ItemFormProvider({ itemId, ...props }) { const { state } = useLocation(); const duplicateId = state?.action; + // Fetches the accounts list. - const { isFetching: isAccountsLoading, data: accounts } = useAccounts(); + const { isLoading: isAccountsLoading, data: accounts } = useAccounts(); // Fetches the items categories list. const { - isFetching: isItemsCategoriesLoading, + isLoading: isItemsCategoriesLoading, data: { itemsCategories }, } = useItemsCategories(); // Fetches the given item details. - const { isFetching: isItemLoading, data: item } = useItem( + const { isLoading: isItemLoading, data: item } = useItem( itemId || duplicateId, { enabled: !!itemId || !!duplicateId, diff --git a/client/src/containers/Purchases/PaymentMades/PaymentsLanding/PaymentMadesListProvider.js b/client/src/containers/Purchases/PaymentMades/PaymentsLanding/PaymentMadesListProvider.js index b037b87da..23050b8ab 100644 --- a/client/src/containers/Purchases/PaymentMades/PaymentsLanding/PaymentMadesListProvider.js +++ b/client/src/containers/Purchases/PaymentMades/PaymentsLanding/PaymentMadesListProvider.js @@ -16,13 +16,13 @@ function PaymentMadesListProvider({ query, ...props }) { // Fetch accounts resource views and fields. const { data: paymentMadesViews, - isFetching: isViewsLoading, + isLoading: isViewsLoading, } = useResourceViews('bill_payments'); // Fetch the accounts resource fields. const { data: paymentMadesFields, - isFetching: isFieldsLoading, + isLoading: isFieldsLoading, } = useResourceFields('bill_payments'); // Fetch accounts list according to the given custom view id. diff --git a/client/src/hooks/query/accounts.js b/client/src/hooks/query/accounts.js index 7eac66ef8..8acaebee1 100644 --- a/client/src/hooks/query/accounts.js +++ b/client/src/hooks/query/accounts.js @@ -1,5 +1,5 @@ import { useMutation, useQueryClient } from 'react-query'; -import { useQueryTenant } from '../useQueryTenant'; +import { useQueryTenant, useRequestQuery } from '../useQueryRequest'; import useApiRequest from '../useRequest'; import t from './types'; @@ -14,27 +14,20 @@ const commonInvalidateQueries = (query) => { // Invalidate financial reports. query.invalidateQueries(t.FINANCIAL_REPORT); -} +}; /** * Retrieve accounts list. */ export function useAccounts(query, props) { - const apiRequest = useApiRequest(); - - return useQueryTenant( + return useRequestQuery( [t.ACCOUNTS, query], - () => apiRequest.get('accounts', { params: query }), + { method: 'get', url: 'accounts', params: query }, { select: (response) => { return response.data.accounts; }, - initialDataUpdatedAt: 0, - initialData: { - data: { - accounts: [] - }, - }, + defaultData: [], ...props, }, ); @@ -45,17 +38,13 @@ export function useAccounts(query, props) { * @param {number} id - Account id. */ export function useAccount(id, props) { - const apiRequest = useApiRequest(); - - return useQueryTenant( + return useRequestQuery( [t.ACCOUNT, id], - () => apiRequest.get(`accounts/${id}`).then(transformAccount), + { method: 'get', url: `accounts/${id}` }, { - initialDataUpdatedAt: 0, - initialData: { - data: { account: {} } - }, - ...props + select: transformAccount, + defaultData: {}, + ...props, }, ); } @@ -64,19 +53,12 @@ export function useAccount(id, props) { * Retrieve accounts types list. */ export function useAccountsTypes(props) { - const apiRequest = useApiRequest(); - - return useQueryTenant( + return useRequestQuery( [t.ACCOUNTS_TYPES], - () => apiRequest.get('account_types'), + { method: 'get', url: 'account_types' }, { select: (res) => res.data.account_types, - initialData: { - data: { - account_types: [], - }, - }, - initialDataUpdatedAt: 0, + defaultData: [], ...props, }, ); diff --git a/client/src/hooks/query/bills.js b/client/src/hooks/query/bills.js index 68d302aaf..9e2fdc54e 100644 --- a/client/src/hooks/query/bills.js +++ b/client/src/hooks/query/bills.js @@ -1,6 +1,5 @@ import { useQueryClient, useMutation } from 'react-query'; -import { useQueryTenant } from '../useQueryTenant'; -import { defaultTo } from 'lodash'; +import { useRequestQuery } from '../useQueryRequest'; import { transformPagination } from 'utils'; import useApiRequest from '../useRequest'; import t from './types'; @@ -63,20 +62,17 @@ export function useEditBill(props) { /** * Marks the given bill as open. */ - export function useOpenBill(props) { +export function useOpenBill(props) { const queryClient = useQueryClient(); const apiRequest = useApiRequest(); - return useMutation( - (id) => apiRequest.post(`purchases/bills/${id}/open`), - { - onSuccess: (res, id) => { - // Common invalidate queries. - commonInvalidateQueries(queryClient); - }, - ...props, + return useMutation((id) => apiRequest.post(`purchases/bills/${id}/open`), { + onSuccess: (res, id) => { + // Common invalidate queries. + commonInvalidateQueries(queryClient); }, - ); + ...props, + }); } /** @@ -95,38 +91,37 @@ export function useDeleteBill(props) { }); } +const transformBillsResponse = (response) => ({ + bills: response.data.bills, + pagination: transformPagination(response.data.pagination), + filterMeta: response.data.filter_meta, +}); + /** * Retrieve sale invoices list with pagination meta. */ export function useBills(query, props) { - const apiRequest = useApiRequest(); - - const states = useQueryTenant( + return useRequestQuery( [t.BILLS, query], - () => - apiRequest.get('purchases/bills', { params: query }), { - select: (response) => ({ - bills: response.data.bills, - pagination: transformPagination(response.data.pagination), - filterMeta: response.data.filter_meta, - }), + method: 'get', + url: 'purchases/bills', + params: query, + }, + { + select: transformBillsResponse, + defaultData: { + bills: [], + pagination: { + page: 1, + page_size: 12, + total: 0, + }, + filterMeta: {}, + }, ...props, }, ); - - return { - ...states, - data: defaultTo(states.data, { - bills: [], - pagination: { - page: 1, - page_size: 12, - total: 0, - }, - filterMeta: {}, - }) - } } /** @@ -134,44 +129,33 @@ export function useBills(query, props) { * @param {number} id - Bill id. */ export function useBill(id, props) { - const apiRequest = useApiRequest(); - - const states = useQueryTenant( + return useRequestQuery( [t.BILL, id], - () => apiRequest.get(`/purchases/bills/${id}`), + { method: 'get', url: `/purchases/bills/${id}`, }, { select: (res) => res.data.bill, + defaultData: {}, ...props, - } + }, ); - - return { - ...states, - data: defaultTo(states.data, {}), - } } /** * Retrieve the due bills of the given vendor id. * @param {number} vendorId - */ - export function useDueBills(vendorId, props) { - const apiRequest = useApiRequest(); - - const states = useQueryTenant( +export function useDueBills(vendorId, props) { + return useRequestQuery( [t.BILLS, t.BILLS_DUE, vendorId], - () => - apiRequest.get(`purchases/bills/due`, { - params: { vendor_id: vendorId }, - }), + { + method: 'get', + url: 'purchases/bills/due', + params: { vendor_id: vendorId }, + }, { select: (res) => res.data.bills, + defaultData: [], ...props, }, ); - - return { - ...states, - data: defaultTo(states.data, []), - }; } diff --git a/client/src/hooks/query/contacts.js b/client/src/hooks/query/contacts.js index 0c56b1c5d..6652c5297 100644 --- a/client/src/hooks/query/contacts.js +++ b/client/src/hooks/query/contacts.js @@ -1,5 +1,5 @@ import useApiRequest from '../useRequest'; -import { useQueryTenant } from '../useQueryTenant'; +import { useQueryTenant } from '../useQueryRequest'; /** * Retrieve the contact duplicate. diff --git a/client/src/hooks/query/currencies.js b/client/src/hooks/query/currencies.js index 98277abbf..018c0c2ea 100644 --- a/client/src/hooks/query/currencies.js +++ b/client/src/hooks/query/currencies.js @@ -1,5 +1,5 @@ import { useMutation, useQueryClient } from 'react-query'; -import { useQueryTenant } from '../useQueryTenant'; +import { useRequestQuery } from '../useQueryRequest'; import useApiRequest from '../useRequest'; import t from './types'; @@ -62,14 +62,12 @@ export function useDeleteCurrency(props) { * Retrieve the currencies list. */ export function useCurrencies(props) { - const apiRequest = useApiRequest(); - - return useQueryTenant( + return useRequestQuery( [t.CURRENCIES], - () => apiRequest.get('currencies').then((res) => res.data.currencies), + { method: 'get', url: 'currencies' }, { - initialDataUpdatedAt: 0, - initialData: [], + select: (res) => res.data.currencies, + defaultData: [], ...props }, ); diff --git a/client/src/hooks/query/customers.js b/client/src/hooks/query/customers.js index eec674867..0126127b5 100644 --- a/client/src/hooks/query/customers.js +++ b/client/src/hooks/query/customers.js @@ -1,5 +1,5 @@ import { useMutation, useQueryClient } from 'react-query'; -import { useQueryTenant } from '../useQueryTenant'; +import { useRequestQuery } from '../useQueryRequest'; import { transformPagination } from 'utils'; import useApiRequest from '../useRequest'; import t from './types'; @@ -22,28 +22,26 @@ const commonInvalidateQueries = (queryClient) => { queryClient.invalidateQueries(t.FINANCIAL_REPORT); }; +// Customers response selector. +const customersSelector = (response) => ({ + customers: response.data.customers, + pagination: transformPagination(response.data.pagination), + filterMeta: response.data.filter_meta, +}); + /** * Retrieve customers list with pagination meta. */ export function useCustomers(query, props) { - const apiRequest = useApiRequest(); - - return useQueryTenant( + return useRequestQuery( [t.CUSTOMERS, query], - () => apiRequest.get(`customers`, { params: query }), + { method: 'get', url: `customers`, params: query }, { - select: (response) => ({ - customers: response.data.customers, - pagination: transformPagination(response.data.pagination), - filterMeta: response.data.filter_meta, - }), - initialDataUpdatedAt: 0, - initialData: { - data: { - customers: [], - pagination: defaultPagination, - filter_meta: {}, - } + select: customersSelector, + defaultData: { + customers: [], + pagination: defaultPagination, + filterMeta: {}, }, ...props, }, @@ -117,19 +115,12 @@ export function useCreateCustomer(props) { * Retrieve the customer details. */ export function useCustomer(id, props) { - const apiRequest = useApiRequest(); - - return useQueryTenant( + return useRequestQuery( [t.CUSTOMER, id], - () => apiRequest.get(`customers/${id}`), + { method: 'get', url: `customers/${id}` }, { select: (res) => res.data.customer, - initialDataUpdatedAt: 0, - initialData: { - data: { - customer: {} - } - }, + defaultData: {}, ...props }, ); diff --git a/client/src/hooks/query/estimates.js b/client/src/hooks/query/estimates.js index 68afdc961..dbeace29c 100644 --- a/client/src/hooks/query/estimates.js +++ b/client/src/hooks/query/estimates.js @@ -1,5 +1,5 @@ import { useQueryClient, useMutation } from 'react-query'; -import { useQueryTenant } from '../useQueryTenant'; +import { useRequestQuery } from '../useQueryRequest'; import useApiRequest from '../useRequest'; import { transformPagination } from 'utils'; import t from './types'; @@ -55,48 +55,40 @@ export function useEditEstimate(props) { * Retrieve sale estimate details. */ export function useEstimate(id, props) { - const apiRequest = useApiRequest(); - - return useQueryTenant( + return useRequestQuery( [t.SALE_ESTIMATE, id], - () => apiRequest.get(`sales/estimates/${id}`), + { method: 'get', url: `sales/estimates/${id}` }, { select: (res) => res.data.estimate, - initialDataUpdatedAt: 0, - initialData: { - data: { estimate: {} }, - }, + defaultData: {}, ...props, }, ); } +const transformEstimates = (res) => ({ + estimates: res.data.sales_estimates, + pagination: transformPagination(res.data.pagination), + filterMeta: res.data.filter_meta, +}); + /** * Retrieve sale invoices list with pagination meta. */ export function useEstimates(query, props) { - const apiRequest = useApiRequest(); - - return useQueryTenant( + return useRequestQuery( [t.SALE_ESTIMATES, query], - () => apiRequest.get('sales/estimates', { params: query }), + { method: 'get', url: 'sales/estimates', params: query }, { - select: (res) => ({ - estimates: res.data.sales_estimates, - pagination: transformPagination(res.data.pagination), - filterMeta: res.data.filter_meta, - }), - initialDataUpdatedAt: 0, - initialData: { - data:{ - sales_estimates: [], - pagination: { - page: 1, - pageSize: 12, - total: 0, - }, - filter_meta: {}, - } + select: transformEstimates, + defaultData: { + estimates: [], + pagination: { + page: 1, + pageSize: 12, + total: 0, + }, + filterMeta: {}, }, ...props, }, diff --git a/client/src/hooks/query/exchangeRates.js b/client/src/hooks/query/exchangeRates.js index 214affa13..0f7f61eb2 100644 --- a/client/src/hooks/query/exchangeRates.js +++ b/client/src/hooks/query/exchangeRates.js @@ -1,6 +1,6 @@ import { useMutation, useQueryClient } from 'react-query'; import { defaultTo } from 'lodash'; -import { useQueryTenant } from '../useQueryTenant'; +import { useQueryTenant } from '../useQueryRequest'; import { transformPagination } from 'utils'; import useApiRequest from '../useRequest'; diff --git a/client/src/hooks/query/expenses.js b/client/src/hooks/query/expenses.js index 2340f241e..d6c506c80 100644 --- a/client/src/hooks/query/expenses.js +++ b/client/src/hooks/query/expenses.js @@ -1,6 +1,6 @@ import { useMutation, useQueryClient } from 'react-query'; import useApiRequest from '../useRequest'; -import { useQueryTenant } from '../useQueryTenant'; +import { useRequestQuery } from '../useQueryRequest'; import { transformPagination } from 'utils'; import t from './types'; @@ -23,28 +23,29 @@ const commonInvalidateQueries = (queryClient) => { queryClient.invalidateQueries(t.FINANCIAL_REPORT); }; +const transformExpenses = (response) => ({ + expenses: response.data.expenses, + pagination: transformPagination(response.data.pagination), + filterMeta: response.data.filter_meta, +}); + /** * Retrieve the expenses list. */ export function useExpenses(query, props) { - const apiRequest = useApiRequest(); - - return useQueryTenant( + return useRequestQuery( [t.EXPENSES, query], - () => apiRequest.get(`expenses`, { params: { ...query } }), { - select: (response) => ({ - expenses: response.data.expenses, - pagination: transformPagination(response.data.pagination), - filterMeta: response.data.filter_meta, - }), - initialDataUpdatedAt: 0, - initialData: { - data: { - expenses: [], - pagination: defaultPagination, - filter_meta: {}, - }, + method: 'get', + url: `expenses`, + params: { ...query } + }, + { + select: transformExpenses, + defaultData: { + expenses: [], + pagination: defaultPagination, + filterMeta: {}, }, ...props, }, @@ -56,19 +57,15 @@ export function useExpenses(query, props) { * @param {number} id - Expense id. */ export function useExpense(id, props) { - const apiRequest = useApiRequest(); - - return useQueryTenant( + return useRequestQuery( [t.EXPENSE, id], - () => apiRequest.get(`expenses/${id}`), + { + method: 'get', + url: `expenses/${id}` + }, { select: (res) => res.data.expense, - initialDataUpdatedAt: 0, - initialData: { - data: { - expense: {}, - } - }, + defaultData: {}, ...props, }, ); diff --git a/client/src/hooks/query/financialReports.js b/client/src/hooks/query/financialReports.js index 988b340a4..2cfce8db2 100644 --- a/client/src/hooks/query/financialReports.js +++ b/client/src/hooks/query/financialReports.js @@ -1,5 +1,4 @@ -import { defaultTo } from 'lodash'; -import { useQueryTenant } from '../useQueryTenant'; +import { useRequestQuery } from '../useQueryRequest'; import { trialBalanceSheetReducer, balanceSheetRowsReducer, @@ -7,176 +6,150 @@ import { generalLedgerTableRowsReducer, journalTableRowsReducer, ARAgingSummaryTableRowsMapper, - APAgingSummaryTableRowsMapper + APAgingSummaryTableRowsMapper, } from 'containers/FinancialStatements/reducers'; -import useApiRequest from '../useRequest'; import t from './types'; /** * Retrieve balance sheet. */ export function useBalanceSheet(query, props) { - const apiRequest = useApiRequest(); - - const states = useQueryTenant( + return useRequestQuery( [t.FINANCIAL_REPORT, t.BALANCE_SHEET, query], - () => - apiRequest.get('/financial_statements/balance_sheet', { - params: query, - }), + { + method: 'get', + url: '/financial_statements/balance_sheet', + params: query, + }, { select: (res) => ({ tableRows: balanceSheetRowsReducer(res.data.data), ...res.data, }), + defaultData: { + data: [], + columns: [], + query: {}, + tableRows: [], + }, ...props, }, ); - - return { - ...states, - data: defaultTo(states.data, { - data: [], - columns: [], - query: {}, - tableRows: [], - }), - }; } /** * Retrieve trial balance sheet. */ export function useTrialBalanceSheet(query, props) { - const apiRequest = useApiRequest(); - - const states = useQueryTenant( + return useRequestQuery( [t.FINANCIAL_REPORT, t.TRIAL_BALANCE_SHEET, query], - () => - apiRequest.get('/financial_statements/trial_balance_sheet', { - params: query, - }), + { + method: 'get', + url: '/financial_statements/trial_balance_sheet', + params: query, + }, { select: (res) => ({ tableRows: trialBalanceSheetReducer(res.data.data), ...res.data, }), + defaultData: { + tableRows: [], + data: [], + query: {}, + }, ...props, }, ); - - return { - ...states, - data: defaultTo(states.data, { - tableRows: [], - data: [], - query: {}, - }), - }; } /** * Retrieve profit/loss (P&L) sheet. */ export function useProfitLossSheet(query, props) { - const apiRequest = useApiRequest(); - - const states = useQueryTenant( + return useRequestQuery( [t.FINANCIAL_REPORT, t.PROFIT_LOSS_SHEET, query], - () => - apiRequest.get('/financial_statements/profit_loss_sheet', { - params: query, - }), + { + method: 'get', + url: '/financial_statements/profit_loss_sheet', + params: query, + }, { select: (res) => ({ tableRows: profitLossSheetReducer(res.data.data), ...res.data, }), + defaultData: { + data: {}, + tableRows: [], + columns: [], + query: {}, + }, ...props, }, ); - return { - ...states, - data: defaultTo(states.data, { - data: {}, - tableRows: [], - columns: [], - query: {}, - }), - }; } /** * Retrieve general ledger (GL) sheet. */ export function useGeneralLedgerSheet(query, props) { - const apiRequest = useApiRequest(); - - const states = useQueryTenant( + + return useRequestQuery( [t.FINANCIAL_REPORT, t.GENERAL_LEDGER, query], - () => - apiRequest.get('/financial_statements/general_ledger', { + { + method: 'get', + url: '/financial_statements/general_ledger', params: query, - }), + }, { select: (res) => ({ tableRows: generalLedgerTableRowsReducer(res.data.data), ...res.data, }), + defaultData: { + tableRows: [], + data: {}, + query: {}, + }, ...props, }, ); - - return { - ...states, - data: defaultTo(states.data, { - tableRows: [], - data: {}, - query: {}, - }), - }; } /** * Retrieve journal sheet. */ export function useJournalSheet(query, props) { - const apiRequest = useApiRequest(); - - const states = useQueryTenant( + return useRequestQuery( [t.FINANCIAL_REPORT, t.JOURNAL, query], - () => apiRequest.get('/financial_statements/journal', { params: query }), + { method: 'get', url: '/financial_statements/journal', params: query }, { select: (res) => ({ tableRows: journalTableRowsReducer(res.data.data), ...res.data, }), + defaultData: { + data: {}, + tableRows: [], + query: {}, + }, ...props, }, ); - - return { - ...states, - data: defaultTo(states.data, { - data: {}, - tableRows: [], - query: {}, - }), - }; } /** * Retrieve A/R aging summary report. */ export function useARAgingSummaryReport(query, props) { - const apiRequest = useApiRequest(); - - return useQueryTenant( + return useRequestQuery( [t.FINANCIAL_REPORT, t.AR_AGING_SUMMARY, query], - () => - apiRequest.get('/financial_statements/receivable_aging_summary', { - params: query, - }), + { + method: 'get', + url: '/financial_statements/receivable_aging_summary', + params: query, + }, { select: (res) => ({ columns: res.data.columns, @@ -188,17 +161,14 @@ export function useARAgingSummaryReport(query, props) { columns: res.data.columns, }), }), - initialData: { + defaultData: { data: { - data: { - customers: [], - total: {}, - }, - columns: [], - tableRows: [], + customers: [], + total: {}, }, + columns: [], + tableRows: [], }, - initialDataUpdatedAt: 0, ...props, }, ); @@ -208,14 +178,13 @@ export function useARAgingSummaryReport(query, props) { * Retrieve A/P aging summary report. */ export function useAPAgingSummaryReport(query, props) { - const apiRequest = useApiRequest(); - - return useQueryTenant( + return useRequestQuery( [t.FINANCIAL_REPORT, t.AP_AGING_SUMMARY, query], - () => - apiRequest.get('/financial_statements/payable_aging_summary', { - params: query, - }), + { + method: 'get', + url: '/financial_statements/payable_aging_summary', + params: query, + }, { select: (res) => ({ columns: res.data.columns, @@ -227,17 +196,14 @@ export function useAPAgingSummaryReport(query, props) { columns: res.data.columns, }), }), - initialData: { + defaultData: { data: { - data: { - vendors: [], - total: {}, - }, - columns: [], - tableRows: [], + vendors: [], + total: {}, }, + columns: [], + tableRows: [], }, - initialDataUpdatedAt: 0, ...props, }, ); diff --git a/client/src/hooks/query/inventoryAdjustments.js b/client/src/hooks/query/inventoryAdjustments.js index 618fcd28b..2e8818ead 100644 --- a/client/src/hooks/query/inventoryAdjustments.js +++ b/client/src/hooks/query/inventoryAdjustments.js @@ -1,10 +1,9 @@ import { useMutation, useQueryClient } from 'react-query'; -import { useQueryTenant } from '../useQueryTenant'; +import { useRequestQuery } from '../useQueryRequest'; import { transformPagination } from 'utils'; import useApiRequest from '../useRequest'; import t from './types'; - const commonInvalidateQueries = (queryClient) => { // Invalidate inventory adjustments. queryClient.invalidateQueries(t.INVENTORY_ADJUSTMENTS); @@ -47,16 +46,13 @@ export function useDeleteInventoryAdjustment(props) { const queryClient = useQueryClient(); const apiRequest = useApiRequest(); - return useMutation( - (id) => apiRequest.delete(`inventory_adjustments/${id}`), - { - onSuccess: (res, id) => { - // Common invalidate queries. - commonInvalidateQueries(queryClient); - }, - ...props + return useMutation((id) => apiRequest.delete(`inventory_adjustments/${id}`), { + onSuccess: (res, id) => { + // Common invalidate queries. + commonInvalidateQueries(queryClient); }, - ); + ...props, + }); } const inventoryAdjustmentsTransformer = (response) => { @@ -64,32 +60,27 @@ const inventoryAdjustmentsTransformer = (response) => { transactions: response.data.inventoy_adjustments, pagination: transformPagination(response.data.pagination), }; -} +}; /** * Retrieve inventory adjustment list with pagination meta. */ export function useInventoryAdjustments(query, props) { - const apiRequest = useApiRequest(); - - return useQueryTenant( + return useRequestQuery( ['INVENTORY_ADJUSTMENTS', query], - () => apiRequest.get('inventory_adjustments', { params: query }) - .then(inventoryAdjustmentsTransformer), + { url: 'inventory_adjustments', params: query }, { - initialDataUpdatedAt: 0, - initialData: { - data: { - transactions: [], - pagination: { - page: 1, - pageSize: 12, - total: 0, - pagesCount: 0, - }, - } + select: inventoryAdjustmentsTransformer, + defaultData: { + transactions: [], + pagination: { + page: 1, + pageSize: 12, + total: 0, + pagesCount: 0, + }, }, - ...props + ...props, }, ); -} \ No newline at end of file +} diff --git a/client/src/hooks/query/invite.js b/client/src/hooks/query/invite.js index fbd1f5154..aab936bf5 100644 --- a/client/src/hooks/query/invite.js +++ b/client/src/hooks/query/invite.js @@ -1,5 +1,5 @@ import { useMutation } from 'react-query'; -import { useQueryTenant } from '../useQueryTenant'; +import { useRequestQuery } from '../useQueryRequest'; import useApiRequest from '../useRequest'; /** @@ -19,11 +19,9 @@ export const useAuthInviteAccept = (props) => { * @param {string} token - Token. */ export const useInviteMetaByToken = (token, props) => { - const apiRequest = useApiRequest(); - - return useQueryTenant( + return useRequestQuery( ['INVITE_META', token], - () => apiRequest.get(`invite/invited/${token}`), + { method: 'get', url: `invite/invited/${token}` }, { select: (res) => res.data, ...props diff --git a/client/src/hooks/query/invoices.js b/client/src/hooks/query/invoices.js index ceba88910..c4503b210 100644 --- a/client/src/hooks/query/invoices.js +++ b/client/src/hooks/query/invoices.js @@ -1,5 +1,5 @@ import { useQueryClient, useMutation } from 'react-query'; -import { useQueryTenant } from '../useQueryTenant'; +import { useRequestQuery } from '../useQueryRequest'; import { transformPagination } from 'utils'; import useApiRequest from '../useRequest'; import t from './types'; @@ -90,32 +90,29 @@ export function useDeleteInvoice(props) { }); } +const transformInvoices = (res) => ({ + invoices: res.data.sales_invoices, + pagination: transformPagination(res.data.pagination), + filterMeta: res.data.filter_meta, +}); + /** * Retrieve sale invoices list with pagination meta. */ export function useInvoices(query, props) { - const apiRequest = useApiRequest(); - - return useQueryTenant( + return useRequestQuery( [t.SALE_INVOICES, query], - () => apiRequest.get('sales/invoices', { params: query }), + { method: 'get', url: 'sales/invoices', params: query }, { - select: (res) => ({ - invoices: res.data.sales_invoices, - pagination: transformPagination(res.data.pagination), - filterMeta: res.data.filter_meta, - }), - initialDataUpdatedAt: 0, - initialData: { - data: { - sales_invoices: [], - pagination: { - page: 1, - pageSize: 12, - total: 0, - }, - filter_meta: {}, + select: transformInvoices, + defaultData: { + invoices: [], + pagination: { + page: 1, + pageSize: 12, + total: 0, }, + filterMeta: {}, }, ...props, }, @@ -149,19 +146,12 @@ export function useDeliverInvoice(props) { * @param {number} invoiceId - Invoice id. */ export function useInvoice(invoiceId, props) { - const apiRequest = useApiRequest(); - - return useQueryTenant( + return useRequestQuery( [t.SALE_INVOICE, invoiceId], - () => apiRequest.get(`sales/invoices/${invoiceId}`), + { method: 'get', url: `sales/invoices/${invoiceId}` }, { select: (res) => res.data.sale_invoice, - initialDataUpdatedAt: 0, - initialData: { - data: { - sale_invoice: {} - }, - }, + defaultData: {}, ...props, }, ); @@ -172,22 +162,16 @@ export function useInvoice(invoiceId, props) { * @param {number} customerId - Customer id. */ export function useDueInvoices(customerId, props) { - const apiRequest = useApiRequest(); - - return useQueryTenant( + return useRequestQuery( [t.SALE_INVOICES, t.SALE_INVOICES_DUE, customerId], - () => - apiRequest.get(`sales/invoices/payable`, { - params: { customer_id: customerId }, - }), + { + method: 'get', + url: `sales/invoices/payable`, + params: { customer_id: customerId }, + }, { select: (res) => res.data.sales_invoices, - initialDataUpdatedAt: 0, - initialData: { - data: { - sales_invoices: [], - }, - }, + defaultData: [], ...props, }, ); diff --git a/client/src/hooks/query/items.js b/client/src/hooks/query/items.js index 3e417a44a..965e1781b 100644 --- a/client/src/hooks/query/items.js +++ b/client/src/hooks/query/items.js @@ -1,6 +1,6 @@ import { useMutation, useQueryClient } from 'react-query'; import { transformPagination, transformResponse } from 'utils'; -import { useQueryTenant } from '../useQueryTenant'; +import { useRequestQuery } from '../useQueryRequest'; import useApiRequest from '../useRequest'; import t from './types'; @@ -114,9 +114,7 @@ export function useInactivateItem(props) { const transformItemsResponse = (response) => { return { items: response.data.items, - pagination: transformPagination( - transformResponse(response.data.pagination) - ), + pagination: transformPagination(transformResponse(response.data.pagination)), filterMeta: transformResponse(response.data.filter_meta), }; }; @@ -125,19 +123,21 @@ const transformItemsResponse = (response) => { * Retrieves items list. */ export function useItems(query, props) { - const apiRequest = useApiRequest(); - - return useQueryTenant( + return useRequestQuery( [t.ITEMS, query], - () => apiRequest.get(`items`, { params: query }).then(transformItemsResponse), { - initialDataUpdatedAt: 0, - initialData: { + method: 'get', + url: 'items', + params: { ...query }, + }, + { + select: transformItemsResponse, + defaultData: { items: [], pagination: DEFAULT_PAGINATION, filterMeta: {}, }, - ...props, + ...props } ); } @@ -147,14 +147,15 @@ export function useItems(query, props) { * @param {number} id - Item id. */ export function useItem(id, props) { - const apiRequest = useApiRequest(); - - return useQueryTenant( + return useRequestQuery( [t.ITEM, id], - () => apiRequest.get(`items/${id}`).then((response) => response.data.item), { - initialDataUpdatedAt: 0, - initialData: {}, + method: 'get', + url: `items/${id}`, + }, + { + select: (response) => response.data.item, + defaultData: {}, ...props }, ); diff --git a/client/src/hooks/query/itemsCategories.js b/client/src/hooks/query/itemsCategories.js index 0cdb52f5f..a31b52483 100644 --- a/client/src/hooks/query/itemsCategories.js +++ b/client/src/hooks/query/itemsCategories.js @@ -1,5 +1,5 @@ import { useMutation, useQueryClient } from 'react-query'; -import { useQueryTenant } from '../useQueryTenant'; +import { useRequestQuery } from '../useQueryRequest'; import useApiRequest from '../useRequest'; import t from './types'; @@ -68,26 +68,24 @@ export function useDeleteItemCategory(props) { }); } + +const transformCategories = (res) => ({ + itemsCategories: res.data.item_categories, + pagination: res.data.pagination, +}); + /** * Retrieve the items categories. */ export function useItemsCategories(query, props) { - const apiRequest = useApiRequest(); - - return useQueryTenant( + return useRequestQuery( [t.ITEMS_CATEGORIES, query], - () => apiRequest.get(`item_categories`, { params: query }), + { method: 'get', url: `item_categories`, params: query }, { - select: (response) => ({ - itemsCategories: response.data.item_categories, - pagination: response.data.pagination, - }), - initialDataUpdatedAt: 0, - initialData: { - data: { - item_categories: [], - pagination: {} - }, + select: transformCategories, + defaultData: { + itemsCategories: [], + pagination: {} }, ...props, }, @@ -99,15 +97,12 @@ export function useItemsCategories(query, props) { * @param {number} id - Item category. */ export function useItemCategory(id, props) { - const apiRequest = useApiRequest(); - - return useQueryTenant( + return useRequestQuery( [t.ITEM_CATEGORY, id], - () => - apiRequest.get(`item_categories/${id}`).then((res) => res.data.category), + { method: 'get', url: `item_categories/${id}` }, { - initialDataUpdatedAt: 0, - initialData: {}, + select: (res) => res.data.category, + defaultData: {}, ...props, }, ); diff --git a/client/src/hooks/query/manualJournals.js b/client/src/hooks/query/manualJournals.js index e79a99c9e..34deb84db 100644 --- a/client/src/hooks/query/manualJournals.js +++ b/client/src/hooks/query/manualJournals.js @@ -1,6 +1,5 @@ -import { defaultTo } from 'lodash'; import { useMutation, useQueryClient } from 'react-query'; -import { useQueryTenant } from '../useQueryTenant'; +import { useRequestQuery } from '../useQueryRequest'; import { transformPagination } from 'utils'; import useApiRequest from '../useRequest'; import t from './types'; @@ -108,46 +107,41 @@ export function usePublishJournal(props) { ); } +const transformJournals = (response) => ({ + manualJournals: response.data.manual_journals, + pagination: transformPagination(response.data.pagination), + filterMeta: response.data.filter_meta +}); + /** * Retrieve the manual journals with pagination meta. */ export function useJournals(query, props) { - const apiRequest = useApiRequest(); - - const states = useQueryTenant( + return useRequestQuery( [t.MANUAL_JOURNALS, query], - () => apiRequest.get('manual-journals', { params: query }), + { method: 'get', url: 'manual-journals', params: query }, { - select: (response) => ({ - manualJournals: response.data.manual_journals, - pagination: transformPagination(response.data.pagination), - filterMeta: response.data.filter_meta - }), + select: transformJournals, + defaultData: { + manualJournals: [], + pagination: {}, + filterMeta: {}, + }, ...props, }, ); - - return { - ...states, - data: defaultTo(states.data, { - manualJournals: [], - pagination: {}, - filterMeta: {}, - }), - }; } /** * Retrieve the manual journal details. */ export function useJournal(id, props) { - const apiRequest = useApiRequest(); - - return useQueryTenant( + return useRequestQuery( [t.MANUAL_JOURNAL, id], - () => apiRequest.get(`manual-journals/${id}`), + { method: 'get', url: `manual-journals/${id}` }, { select: (res) => res.data.manual_journal, + defaultData: {}, ...props, }, ); diff --git a/client/src/hooks/query/organization.js b/client/src/hooks/query/organization.js index 8feeaf259..60fb83fed 100644 --- a/client/src/hooks/query/organization.js +++ b/client/src/hooks/query/organization.js @@ -1,7 +1,7 @@ import { useMutation } from 'react-query'; import t from './types'; import useApiRequest from '../useRequest'; -import { useQueryTenant } from '../useQueryTenant'; +import { useRequestQuery } from '../useQueryRequest'; import { useEffect } from 'react'; import { useSetOrganizations, useSetSubscriptions } from '../state'; import { omit } from 'lodash'; @@ -10,11 +10,9 @@ import { omit } from 'lodash'; * Retrieve organizations of the authenticated user. */ export function useOrganizations(props) { - const apiRequest = useApiRequest(); - - return useQueryTenant( + return useRequestQuery( [t.ORGANIZATIONS], - () => apiRequest.get(`organization/all`), + { method: 'get', url: `organization/all` }, { select: (res) => res.data.organizations, initialDataUpdatedAt: 0, @@ -32,21 +30,15 @@ export function useOrganizations(props) { * Retrieve the current organization metadata. */ export function useCurrentOrganization(props) { - const apiRequest = useApiRequest(); const setOrganizations = useSetOrganizations(); const setSubscriptions = useSetSubscriptions(); - const query = useQueryTenant( + const query = useRequestQuery( [t.ORGANIZATION_CURRENT], - () => apiRequest.get(`organization/current`), + { method: 'get', url: `organization/current` }, { select: (res) => res.data.organization, - initialDataUpdatedAt: 0, - initialData: { - data: { - organization: {}, - }, - }, + defaultData: {}, ...props, }, ); diff --git a/client/src/hooks/query/paymentMades.js b/client/src/hooks/query/paymentMades.js index 8c30152f0..a104b3f6b 100644 --- a/client/src/hooks/query/paymentMades.js +++ b/client/src/hooks/query/paymentMades.js @@ -1,6 +1,5 @@ -import { defaultTo } from 'lodash'; import { useMutation, useQueryClient } from 'react-query'; -import { useQueryTenant } from '../useQueryTenant'; +import { useRequestQuery } from '../useQueryRequest'; import { transformPagination } from 'utils'; import useApiRequest from '../useRequest'; import t from './types'; @@ -34,29 +33,23 @@ const commonInvalidateQueries = (client) => { * Retrieve payment mades list. */ export function usePaymentMades(query, props) { - const apiRequest = useApiRequest(); - - const states = useQueryTenant( + return useRequestQuery( [t.PAYMENT_MADES, query], - () => apiRequest.get('purchases/bill_payments', { params: query }), + { url: 'purchases/bill_payments', params: query }, { select: (res) => ({ paymentMades: res.data.bill_payments, pagination: transformPagination(res.data.pagination), filterMeta: res.data.filter_meta, }), + defaultData: { + paymentMades: [], + pagination: {}, + filterMeta: {}, + }, ...props, }, ); - - return { - ...states, - data: defaultTo(states.data, { - paymentMades: [], - pagination: {}, - filterMeta: {}, - }), - }; } /** @@ -126,24 +119,24 @@ export function useDeletePaymentMade(props) { * Retrieve specific payment made. */ export function usePaymentMadeEditPage(id, props) { - const apiRequest = useApiRequest(); - - const states = useQueryTenant( + return useRequestQuery( [t.PAYMENT_MADE_EDIT_PAGE, id], - () => apiRequest.get(`purchases/bill_payments/${id}/edit-page`), + { + method: 'get', + url: `purchases/bill_payments/${id}/edit-page`, + }, { select: (res) => ({ paymentMade: res.data.bill_payment, entries: res.data.entries, }), + defaultData: { + paymentMade: {}, + entries: [], + }, ...props, }, ); - - return { - ...states, - data: defaultTo(states.data, {}), - }; } /** @@ -151,22 +144,16 @@ export function usePaymentMadeEditPage(id, props) { * @param {number} vendorId - */ export function usePaymentMadeNewPageEntries(vendorId, props) { - const apiRequest = useApiRequest(); - - return useQueryTenant( + return useRequestQuery( [t.PAYMENT_MADE_NEW_ENTRIES, vendorId], - () => - apiRequest.get(`purchases/bill_payments/new-page/entries`, { - params: { vendor_id: vendorId }, - }), + { + method: 'get', + url: `purchases/bill_payments/new-page/entries`, + params: { vendor_id: vendorId }, + }, { select: (res) => res.data.entries, - initialDataUpdatedAt: 0, - initialData: { - data: { - entries: [], - }, - }, + defaultData: [], ...props, }, ); diff --git a/client/src/hooks/query/paymentReceives.js b/client/src/hooks/query/paymentReceives.js index ccb4f47da..d522b2b9a 100644 --- a/client/src/hooks/query/paymentReceives.js +++ b/client/src/hooks/query/paymentReceives.js @@ -1,6 +1,5 @@ import { useMutation, useQueryClient } from 'react-query'; -import { useQueryTenant } from '../useQueryTenant'; -import { defaultTo } from 'lodash'; +import { useRequestQuery } from '../useQueryRequest'; import useApiRequest from '../useRequest'; import { transformPagination, saveInvoke } from 'utils'; import t from './types'; @@ -27,37 +26,30 @@ const commonInvalidateQueries = (client) => { client.invalidateQueries(t.CUSTOMER); }; +// Transform payment receives. +const transformPaymentReceives = (res) => ({ + paymentReceives: res.data.payment_receives, + pagination: transformPagination(res.data.pagination), + filterMeta: res.data.filter_meta, +}); + /** * Retrieve accounts list. */ export function usePaymentReceives(query, props) { - const apiRequest = useApiRequest(); - - const states = useQueryTenant( + return useRequestQuery( [t.PAYMENT_RECEIVES, query], - () => apiRequest.get('sales/payment_receives', { params: query }), + { method: 'get', url: 'sales/payment_receives', params: query }, { - select: (res) => ({ - paymentReceives: res.data.payment_receives, - pagination: transformPagination(res.data.pagination), - filterMeta: res.data.filter_meta, - }), + select: transformPaymentReceives, + defaultData: { + paymentReceives: [], + pagination: { page: 1, pageSize: 12, total: 0 }, + filterMeta: {}, + }, ...props, }, ); - - return { - ...states, - data: defaultTo(states.data, { - paymentReceives: [], - pagination: { - page: 1, - pageSize: 12, - total: 0, - }, - filterMeta: {}, - }), - }; } /** @@ -136,23 +128,15 @@ export function useDeletePaymentReceive(props) { * @param {number} id - Payment receive. */ export function usePaymentReceive(id, props) { - const apiRequest = useApiRequest(); - - const states = useQueryTenant( + return useRequestQuery( [t.PAYMENT_RECEIVE, id], - () => apiRequest.get(`sales/payment_receives/${id}`), + { method: 'get', url: `sales/payment_receives/${id}` }, { - select: (res) => ({ - paymentReceive: res.data.payment_receive, - }), + select: (res) => res.data.payment_receive, + defaultData: {}, ...props, }, ); - - return { - ...states, - data: defaultTo(states.data, {}), - }; } /** @@ -160,25 +144,19 @@ export function usePaymentReceive(id, props) { * @param {number} id - Payment receive id. */ export function usePaymentReceiveEditPage(id, props) { - const apiRequest = useApiRequest(); - - const states = useQueryTenant( + return useRequestQuery( [t.PAYMENT_RECEIVE_EDIT_PAGE, id], - () => apiRequest.get(`sales/payment_receives/${id}/edit-page`), + { method: 'get', url: `sales/payment_receives/${id}/edit-page` }, { select: (res) => ({ paymentReceive: res.data.payment_receive, entries: res.data.entries, }), + defaultData: { + paymentReceive: {}, + entries: [], + }, ...props, }, ); - - return { - ...states, - data: defaultTo(states.data, { - paymentReceive: {}, - entries: [], - }), - }; } diff --git a/client/src/hooks/query/receipts.js b/client/src/hooks/query/receipts.js index 0e8c43805..8befaf3ff 100644 --- a/client/src/hooks/query/receipts.js +++ b/client/src/hooks/query/receipts.js @@ -1,6 +1,5 @@ import { useQueryClient, useMutation } from 'react-query'; -import { useQueryTenant } from '../useQueryTenant'; -import { defaultTo } from 'lodash'; +import { useRequestQuery } from '../useQueryRequest'; import useApiRequest from '../useRequest'; import { transformPagination } from 'utils'; import t from './types'; @@ -99,56 +98,46 @@ export function useCloseReceipt(props) { }); } +const transformReceipts = (res) => ({ + receipts: res.data.sale_receipts, + pagination: transformPagination(res.data.pagination), + filterMeta: res.data.filter_meta, +}); + /** * Retrieve sale invoices list with pagination meta. */ export function useReceipts(query, props) { - const apiRequest = useApiRequest(); - - const states = useQueryTenant( + return useRequestQuery( ['SALE_RECEIPTS', query], - () => apiRequest.get('sales/receipts', { params: query }), + { method: 'get', url: 'sales/receipts', params: query }, { - select: (response) => ({ - receipts: response.data.sale_receipts, - pagination: transformPagination(response.data.pagination), - filterMeta: response.data.filter_meta, - }), + select: transformReceipts, + defaultData: { + receipts: [], + pagination: { + page: 1, + page_size: 12, + total: 0, + }, + filterMeta: {}, + }, ...props, }, ); - - return { - ...states, - data: defaultTo(states.data, { - receipts: [], - pagination: { - page: 1, - page_size: 12, - total: 0, - }, - filterMeta: {}, - }), - }; } /** * Retrieve sale invoices list with pagination meta. */ export function useReceipt(id, props) { - const apiRequest = useApiRequest(); - - const states = useQueryTenant( + return useRequestQuery( ['SALE_RECEIPT', id], - () => apiRequest.get(`sales/receipts/${id}`), + { method: 'get', url: `sales/receipts/${id}` }, { select: (res) => res.data.sale_receipt, + defaultData: {}, ...props, }, ); - - return { - ...states, - data: defaultTo(states.data, {}), - } } diff --git a/client/src/hooks/query/settings.js b/client/src/hooks/query/settings.js index 559052565..8520e4bab 100644 --- a/client/src/hooks/query/settings.js +++ b/client/src/hooks/query/settings.js @@ -1,6 +1,6 @@ import { useEffect } from 'react'; import { useMutation, useQueryClient } from 'react-query'; -import { useQueryTenant } from '../useQueryTenant'; +import { useRequestQuery } from '../useQueryRequest'; import useApiRequest from '../useRequest'; import { useSetSettings } from 'hooks/state'; import t from './types'; @@ -21,20 +21,14 @@ export function useSaveSettings(props) { } function useSettingsQuery(key, query, props) { - const apiRequest = useApiRequest(); const setSettings = useSetSettings(); - const state = useQueryTenant( + const state = useRequestQuery( key, - () => apiRequest.get('settings', { params: query }), + { method: 'get', url: 'settings', params: query }, { select: (res) => res.data.settings, - initialDataUpdatedAt: 0, - initialData: { - data: { - settings: [], - }, - }, + defaultData: [], ...props, }, ); diff --git a/client/src/hooks/query/users.js b/client/src/hooks/query/users.js index 713fd1393..9bb908730 100644 --- a/client/src/hooks/query/users.js +++ b/client/src/hooks/query/users.js @@ -1,6 +1,6 @@ import { useMutation, useQueryClient } from 'react-query'; import { defaultTo } from 'lodash'; -import { useQueryTenant } from '../useQueryTenant'; +import { useQueryTenant } from '../useQueryRequest'; import useApiRequest from '../useRequest'; import t from './types'; diff --git a/client/src/hooks/query/vendors.js b/client/src/hooks/query/vendors.js index 6ba25aff6..f245afef4 100644 --- a/client/src/hooks/query/vendors.js +++ b/client/src/hooks/query/vendors.js @@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from 'react-query'; import t from './types'; import { transformPagination } from 'utils'; import useApiRequest from '../useRequest'; -import { useQueryTenant } from '../useQueryTenant'; +import { useRequestQuery } from '../useQueryRequest'; // Common invalidate queries. const commonInvalidateQueries = (queryClient) => { @@ -17,28 +17,26 @@ const commonInvalidateQueries = (queryClient) => { queryClient.invalidateQueries(t.FINANCIAL_REPORT); }; +// Transformes vendors response. +const transformVendorsResponse = (res) => ({ + vendors: res.data.vendors, + pagination: transformPagination(res.data.pagination), + filterMeta: res.data.filter_meta, +}); + /** * Retrieve vendors list. */ export function useVendors(query, props) { - const apiRequest = useApiRequest(); - - return useQueryTenant( + return useRequestQuery( [t.VENDORS, query], - () => apiRequest.get(`vendors`, { params: query }), + { method: 'get', url: `vendors`, params: query }, { - select: (res) => ({ - vendors: res.data.vendors, - pagination: transformPagination(res.data.pagination), - filterMeta: res.data.filter_meta, - }), - initialDataUpdatedAt: 0, - initialData: { - data: { - vendors: [], - pagination: {}, - filter_meta: {}, - }, + select: transformVendorsResponse, + defaultData: { + vendors: [], + pagination: {}, + filterMeta: {}, }, ...props, }, @@ -106,14 +104,13 @@ export function useCreateVendor(props) { * Retrieve vendor details. */ export function useVendor(id, props) { - const apiRequest = useApiRequest(); - - return useQueryTenant([t.VENDOR, id], () => apiRequest.get(`vendors/${id}`), { - select: (res) => res.data.vendor, - initialDataUpdatedAt: 0, - initialData: { - data: { vendor: {} }, + return useRequestQuery( + [t.VENDOR, id], + { method: 'get', url: `vendors/${id}` }, + { + select: (res) => res.data.vendor, + defaultData: {}, + ...props, }, - ...props, - }); + ); } diff --git a/client/src/hooks/query/views.js b/client/src/hooks/query/views.js index cf9b52922..1f16e3918 100644 --- a/client/src/hooks/query/views.js +++ b/client/src/hooks/query/views.js @@ -1,46 +1,46 @@ -import { defaultTo } from 'lodash'; -import useApiRequest from '../useRequest'; -import { useQueryTenant } from '../useQueryTenant'; +import { useRequestQuery } from '../useQueryRequest'; +/** + * Retrieve the resource views. + * @param {string} resourceSlug - Resource slug. + */ export function useResourceViews(resourceSlug) { - const apiRequest = useApiRequest(); - - const states = useQueryTenant( + return useRequestQuery( ['RESOURCE_VIEW', resourceSlug], - () => apiRequest.get(`views/resource/${resourceSlug}`) - .then((response) => response.data.views), - ); - - return { - ...states, - data: defaultTo(states.data, []), - } -} - -export function useResourceColumns(resourceSlug) { - const apiRequest = useApiRequest(); - - return useQueryTenant( - ['RESOURCE_COLUMNS', resourceSlug], - () => apiRequest.get(`resources/${resourceSlug}/columns`), + { method: 'get', url: `views/resource/${resourceSlug}` }, { - initialData: [], + select: (response) => response.data.views, + defaultData: [], }, ); } -export function useResourceFields(resourceSlug, props) { - const apiRequest = useApiRequest(); +/** + * Retrieve the resource columns. + * @param {string} resourceSlug - Resource slug. + */ +export function useResourceColumns(resourceSlug) { + return useRequestQuery( + ['RESOURCE_COLUMNS', resourceSlug], + { method: 'get', url: `resources/${resourceSlug}/columns` }, + { + defaultData: [], + }, + ); +} - const states = useQueryTenant( +/** + * Retrieve the resource fields. + * @param {string} resourceSlug - Resource slug. + */ +export function useResourceFields(resourceSlug, props) { + return useRequestQuery( ['RESOURCE_FIELDS', resourceSlug], - () => apiRequest.get(`resources/${resourceSlug}/fields`) - .then((res) => res.data.resource_fields), + { method: 'get', url: `resources/${resourceSlug}/fields` }, + { + select: (res) => res.data.resource_fields, + defaultData: [], + }, props ); - - return { - ...states, - data: defaultTo(states.data, []), - } } diff --git a/client/src/hooks/state/authentication.js b/client/src/hooks/state/authentication.js index 5af99dfd5..ac2b9558d 100644 --- a/client/src/hooks/state/authentication.js +++ b/client/src/hooks/state/authentication.js @@ -1,7 +1,12 @@ import { useDispatch, useSelector } from 'react-redux'; import { useCallback } from 'react'; import { isAuthenticated } from 'store/authentication/authentication.reducer'; -import { setLogin, setLogout } from 'store/authentication/authentication.actions'; +import { + setLogin, + setLogout, + setStoreReset, +} from 'store/authentication/authentication.actions'; +import { purgePersistedState } from 'store/createStore'; import { useQueryClient } from 'react-query'; export const useAuthActions = () => { @@ -11,9 +16,8 @@ export const useAuthActions = () => { return { setLogin: useCallback((login) => dispatch(setLogin(login)), [dispatch]), setLogout: useCallback(() => { - - // Logout action. - dispatch(setLogout()); + // Resets store state. + dispatch(setStoreReset()); // Remove all cached queries. queryClient.removeQueries(); diff --git a/client/src/hooks/useQueryRequest.js b/client/src/hooks/useQueryRequest.js new file mode 100644 index 000000000..9b4c0a49a --- /dev/null +++ b/client/src/hooks/useQueryRequest.js @@ -0,0 +1,31 @@ +import { useQuery } from 'react-query'; +import { castArray, defaultTo } from 'lodash'; +import { useAuthOrganizationId } from './state'; +import useApiRequest from './useRequest'; +import { useRef } from 'react'; + +/** + * Query for tenant requests. + */ +export function useQueryTenant(query, callback, props) { + const organizationId = useAuthOrganizationId(); + + return useQuery([...castArray(query), organizationId], callback, props); +} + +export function useRequestQuery(query, axios, props) { + const apiRequest = useApiRequest(); + + const states = useQuery( + query, + () => apiRequest.http({ ...axios, url: `/api/${axios.url}` }), + props, + ); + // Momerize the default data. + const defaultData = useRef(props.defaultData || undefined); + + return { + ...states, + data: defaultTo(states.data, defaultData.current), + }; +} diff --git a/client/src/hooks/useQueryTenant.js b/client/src/hooks/useQueryTenant.js deleted file mode 100644 index dbd2192ca..000000000 --- a/client/src/hooks/useQueryTenant.js +++ /dev/null @@ -1,12 +0,0 @@ -import { useQuery } from 'react-query'; -import { castArray } from 'lodash'; -import { useAuthOrganizationId } from './state'; - -/** - * Query for tenant requests. - */ - export function useQueryTenant(query, callback, props) { - const organizationId = useAuthOrganizationId(); - - return useQuery([...castArray(query), organizationId], callback, props); -} diff --git a/client/src/hooks/useRequest.js b/client/src/hooks/useRequest.js index be5909ed0..b71020bfc 100644 --- a/client/src/hooks/useRequest.js +++ b/client/src/hooks/useRequest.js @@ -61,6 +61,8 @@ export default function useApiRequest() { }, [token, organizationId, setGlobalErrors, setLogout]); return { + http, + get(resource, params) { return http.get(`/api/${resource}`, params); }, diff --git a/client/src/store/Bills/bills.reducer.js b/client/src/store/Bills/bills.reducer.js index 70d4fd929..1e2b9a302 100644 --- a/client/src/store/Bills/bills.reducer.js +++ b/client/src/store/Bills/bills.reducer.js @@ -1,7 +1,7 @@ import { createReducer } from '@reduxjs/toolkit'; -import { - createTableStateReducers, -} from 'store/tableState.reducer'; +import { persistReducer } from 'redux-persist'; +import storage from 'redux-persist/lib/storage'; +import { createTableStateReducers } from 'store/tableState.reducer'; const initialState = { tableState: { @@ -10,6 +10,17 @@ const initialState = { }, }; -export default createReducer(initialState, { +const STORAGE_KEY = 'bigcapital:bills'; + +const reducerInstance = createReducer(initialState, { ...createTableStateReducers('BILLS'), -}); \ No newline at end of file +}); + +export default persistReducer( + { + key: STORAGE_KEY, + whitelist: ['tableState'], + storage, + }, + reducerInstance, +); \ No newline at end of file diff --git a/client/src/store/Estimate/estimates.reducer.js b/client/src/store/Estimate/estimates.reducer.js index e67eacbbc..bc26dea13 100644 --- a/client/src/store/Estimate/estimates.reducer.js +++ b/client/src/store/Estimate/estimates.reducer.js @@ -1,4 +1,6 @@ import { createReducer } from '@reduxjs/toolkit'; +import { persistReducer } from 'redux-persist'; +import storage from 'redux-persist/lib/storage'; import { createTableStateReducers, } from 'store/tableState.reducer'; @@ -10,6 +12,17 @@ const initialState = { }, }; -export default createReducer(initialState, { +const reducerInstance = createReducer(initialState, { ...createTableStateReducers('ESTIMATES'), }); + +const STORAGE_KEY = 'bigcapital:estimates'; + +export default persistReducer( + { + key: STORAGE_KEY, + whitelist: ['tableState'], + storage, + }, + reducerInstance, +); diff --git a/client/src/store/Invoice/invoices.reducer.js b/client/src/store/Invoice/invoices.reducer.js index 52f6e1015..da6fce26f 100644 --- a/client/src/store/Invoice/invoices.reducer.js +++ b/client/src/store/Invoice/invoices.reducer.js @@ -1,4 +1,6 @@ import { createReducer } from '@reduxjs/toolkit'; +import { persistReducer } from 'redux-persist'; +import storage from 'redux-persist/lib/storage'; import { createTableStateReducers, } from 'store/tableState.reducer'; @@ -10,6 +12,17 @@ const initialState = { }, }; -export default createReducer(initialState, { +const reducerInstance = createReducer(initialState, { ...createTableStateReducers('INVOICES'), }); + +const STORAGE_KEY = 'bigcapital:invoices'; + +export default persistReducer( + { + key: STORAGE_KEY, + whitelist: ['tableState'], + storage, + }, + reducerInstance, +); diff --git a/client/src/store/PaymentMades/paymentMades.reducer.js b/client/src/store/PaymentMades/paymentMades.reducer.js index c1660a313..9dbd113b7 100644 --- a/client/src/store/PaymentMades/paymentMades.reducer.js +++ b/client/src/store/PaymentMades/paymentMades.reducer.js @@ -1,4 +1,6 @@ import { createReducer } from '@reduxjs/toolkit'; +import { persistReducer } from 'redux-persist'; +import storage from 'redux-persist/lib/storage'; import { createTableStateReducers, } from 'store/tableState.reducer'; @@ -11,6 +13,17 @@ const initialState = { }, }; -export default createReducer(initialState, { +const reducerInstance = createReducer(initialState, { ...createTableStateReducers('PAYMENT_MADES'), }); + +const STORAGE_KEY = 'bigcapital:paymentMades'; + +export default persistReducer( + { + key: STORAGE_KEY, + whitelist: ['tableState'], + storage, + }, + reducerInstance, +); diff --git a/client/src/store/PaymentReceives/paymentReceives.reducer.js b/client/src/store/PaymentReceives/paymentReceives.reducer.js index 59b44a40b..fc2be7e6b 100644 --- a/client/src/store/PaymentReceives/paymentReceives.reducer.js +++ b/client/src/store/PaymentReceives/paymentReceives.reducer.js @@ -1,4 +1,6 @@ import { createReducer } from '@reduxjs/toolkit'; +import { persistReducer } from 'redux-persist'; +import storage from 'redux-persist/lib/storage'; import { createTableStateReducers, } from 'store/tableState.reducer'; @@ -10,6 +12,17 @@ const initialState = { }, }; -export default createReducer(initialState, { +const reducerInstance = createReducer(initialState, { ...createTableStateReducers('PAYMENT_RECEIVES'), }); + +const STORAGE_KEY = 'bigcapital:paymentReceives'; + +export default persistReducer( + { + key: STORAGE_KEY, + whitelist: ['tableState'], + storage, + }, + reducerInstance, +); diff --git a/client/src/store/accounts/accounts.reducer.js b/client/src/store/accounts/accounts.reducer.js index 149772c77..194b7719a 100644 --- a/client/src/store/accounts/accounts.reducer.js +++ b/client/src/store/accounts/accounts.reducer.js @@ -1,14 +1,25 @@ import { createReducer} from '@reduxjs/toolkit'; +import { persistReducer } from 'redux-persist'; +import storage from 'redux-persist/lib/storage'; import { createTableStateReducers, } from 'store/tableState.reducer'; const initialState = { - tableState: { - - }, + tableState: {}, }; -export default createReducer(initialState, { +const reducerInstance = createReducer(initialState, { ...createTableStateReducers('ACCOUNTS'), }); + +const STORAGE_KEY = 'bigcapital:accounts'; + +export default persistReducer( + { + key: STORAGE_KEY, + whitelist: ['tableState'], + storage, + }, + reducerInstance, +); diff --git a/client/src/store/authentication/authentication.actions.js b/client/src/store/authentication/authentication.actions.js index 9e6682b87..4e12a6f2b 100644 --- a/client/src/store/authentication/authentication.actions.js +++ b/client/src/store/authentication/authentication.actions.js @@ -2,13 +2,8 @@ import t from 'store/types'; export const setLogin = ({ user, token, tenant }) => ({ type: t.LOGIN_SUCCESS, - payload: { - user, - token, - tenant, - }, + payload: { user, token, tenant, }, }); -export const setLogout = () => ({ - type: t.LOGOUT, -}); +export const setLogout = () => ({ type: t.LOGOUT }); +export const setStoreReset = () => ({ type: t.RESET }); \ No newline at end of file diff --git a/client/src/store/authentication/authentication.reducer.js b/client/src/store/authentication/authentication.reducer.js index eb7e66097..abc814e47 100644 --- a/client/src/store/authentication/authentication.reducer.js +++ b/client/src/store/authentication/authentication.reducer.js @@ -1,4 +1,6 @@ import { createReducer } from '@reduxjs/toolkit'; +import { persistReducer } from 'redux-persist'; +import storage from 'redux-persist/lib/storage'; import t from 'store/types'; const initialState = { @@ -11,7 +13,9 @@ const initialState = { errors: [], }; -export default createReducer(initialState, { +const STORAGE_KEY = 'bigcapital:authentication'; + +const reducerInstance = createReducer(initialState, { [t.LOGIN_SUCCESS]: (state, action) => { const { token, user, tenant } = action.payload; state.token = token; @@ -25,19 +29,20 @@ export default createReducer(initialState, { state.errors = action.errors; }, - [t.LOGOUT]: (state) => { - state.token = ''; - state.user = {}; - state.organization = ''; - state.organizationId = null; - state.tenant = {}; - }, - [t.LOGIN_CLEAR_ERRORS]: (state) => { state.errors = []; }, }); +export default persistReducer( + { + key: STORAGE_KEY, + blacklist: ['errors'], + storage, + }, + reducerInstance, +); + export const isAuthenticated = (state) => !!state.authentication.token; export const hasErrorType = (state, errorType) => { return state.authentication.errors.find((e) => e.type === errorType); diff --git a/client/src/store/authentication/authentication.types.js b/client/src/store/authentication/authentication.types.js index bafd70909..416bd0716 100644 --- a/client/src/store/authentication/authentication.types.js +++ b/client/src/store/authentication/authentication.types.js @@ -5,4 +5,5 @@ export default { LOGIN_FAILURE: 'LOGIN_FAILURE', LOGOUT: 'LOGOUT', LOGIN_CLEAR_ERRORS: 'LOGIN_CLEAR_ERRORS', + RESET: 'RESET', }; \ No newline at end of file diff --git a/client/src/store/createStore.js b/client/src/store/createStore.js index b9f3acd25..0110455e8 100644 --- a/client/src/store/createStore.js +++ b/client/src/store/createStore.js @@ -4,7 +4,7 @@ import { compose, } from 'redux'; import thunkMiddleware from 'redux-thunk'; -import { persistStore, persistReducer } from 'redux-persist'; +import { persistStore, persistReducer, purgeStoredState } from 'redux-persist'; import storage from 'redux-persist/lib/storage'; import monitorReducerEnhancer from 'store/enhancers/monitorReducer'; import loggerMiddleware from 'middleware/logger'; @@ -44,7 +44,7 @@ const createStoreFactory = (initialState = {}) => { |-------------------------------------------------- */ const store = createReduxStore( - persistReducer(persistConfig, rootReducer), + rootReducer, initialState, composeEnhancers(applyMiddleware(...middleware), ...enhancers), ); diff --git a/client/src/store/customers/customers.reducer.js b/client/src/store/customers/customers.reducer.js index f05798ffb..060fc0774 100644 --- a/client/src/store/customers/customers.reducer.js +++ b/client/src/store/customers/customers.reducer.js @@ -1,4 +1,6 @@ import { createReducer } from '@reduxjs/toolkit'; +import { persistReducer } from 'redux-persist'; +import storage from 'redux-persist/lib/storage'; import { createTableStateReducers } from 'store/tableState.reducer'; const initialState = { @@ -8,6 +10,17 @@ const initialState = { }, }; -export default createReducer(initialState, { +const reducerInstance = createReducer(initialState, { ...createTableStateReducers('CUSTOMERS'), }); + +const STORAGE_KEY = 'bigcapital:estimates'; + +export default persistReducer( +{ + key: STORAGE_KEY, + whitelist: ['tableState'], + storage, +}, +reducerInstance, +); diff --git a/client/src/store/dashboard/dashboard.reducer.js b/client/src/store/dashboard/dashboard.reducer.js index 70131b29f..2f5bee3c3 100644 --- a/client/src/store/dashboard/dashboard.reducer.js +++ b/client/src/store/dashboard/dashboard.reducer.js @@ -1,7 +1,7 @@ import t from 'store/types'; import { createReducer } from '@reduxjs/toolkit'; import { persistReducer } from 'redux-persist'; -import storage from 'redux-persist/lib/storage'; // defaults to localStorage for web +import storage from 'redux-persist/lib/storage'; const initialState = { pageTitle: '', @@ -18,6 +18,8 @@ const initialState = { backLink: false, }; +const STORAGE_KEY = 'bigcapital:dashboard'; + const reducerInstance = createReducer(initialState, { [t.CHANGE_DASHBOARD_PAGE_TITLE]: (state, action) => { state.pageTitle = action.pageTitle; @@ -117,14 +119,10 @@ const reducerInstance = createReducer(initialState, { export default persistReducer( { - key: 'bigcapital:dashboard', - blacklist: [ - 'pageTitle', - 'pageSubtitle', - 'pageHint', - 'preferencesPageTitle', - 'topbarEditViewId', - 'backLink', + key: STORAGE_KEY, + whitelist: [ + 'sidebarExpended', + 'previousSidebarExpended', ], storage, }, diff --git a/client/src/store/expenses/expenses.reducer.js b/client/src/store/expenses/expenses.reducer.js index ac76e670a..7c9557ace 100644 --- a/client/src/store/expenses/expenses.reducer.js +++ b/client/src/store/expenses/expenses.reducer.js @@ -1,7 +1,7 @@ import { createReducer } from '@reduxjs/toolkit'; -import { - createTableStateReducers, -} from 'store/tableState.reducer'; +import { persistReducer } from 'redux-persist'; +import storage from 'redux-persist/lib/storage'; +import { createTableStateReducers } from 'store/tableState.reducer'; const initialState = { tableState: { @@ -10,6 +10,17 @@ const initialState = { }, }; -export default createReducer(initialState, { +const reducerInstance = createReducer(initialState, { ...createTableStateReducers('EXPENSES'), }); + +const STORAGE_KEY = 'bigcapital:expenses'; + +export default persistReducer( + { + key: STORAGE_KEY, + whitelist: ['tableState'], + storage, + }, + reducerInstance, +); diff --git a/client/src/store/inventoryAdjustments/inventoryAdjustment.reducer.js b/client/src/store/inventoryAdjustments/inventoryAdjustment.reducer.js index 71a1cb803..96a46b517 100644 --- a/client/src/store/inventoryAdjustments/inventoryAdjustment.reducer.js +++ b/client/src/store/inventoryAdjustments/inventoryAdjustment.reducer.js @@ -1,7 +1,7 @@ import { createReducer } from '@reduxjs/toolkit'; -import { - createTableStateReducers, -} from 'store/tableState.reducer'; +import { persistReducer } from 'redux-persist'; +import storage from 'redux-persist/lib/storage'; +import { createTableStateReducers } from 'store/tableState.reducer'; const initialState = { tableState: { @@ -12,6 +12,17 @@ const initialState = { selectedRows: [], }; -export default createReducer(initialState, { +const reducerInstance = createReducer(initialState, { ...createTableStateReducers('INVENTORY_ADJUSTMENTS'), }); + +const STORAGE_KEY = 'bigcapital:inventoryAdjustments'; + +export default persistReducer( + { + key: STORAGE_KEY, + whitelist: ['tableState'], + storage, + }, + reducerInstance, +); diff --git a/client/src/store/itemCategories/itemsCategory.reducer.js b/client/src/store/itemCategories/itemsCategory.reducer.js index b510439a2..bb15a35dc 100644 --- a/client/src/store/itemCategories/itemsCategory.reducer.js +++ b/client/src/store/itemCategories/itemsCategory.reducer.js @@ -1,4 +1,6 @@ import { createReducer } from '@reduxjs/toolkit'; +import { persistReducer } from 'redux-persist'; +import storage from 'redux-persist/lib/storage'; import { createTableStateReducers, } from 'store/tableState.reducer'; @@ -8,6 +10,18 @@ const initialState = { tableState: {}, }; -export default createReducer(initialState, { +const reducerInstance = createReducer(initialState, { ...createTableStateReducers('ITEMS_CATEGORIES'), }); + + +const STORAGE_KEY = 'bigcapital:itemCategories'; + +export default persistReducer( + { + key: STORAGE_KEY, + whitelist: ['tableState'], + storage, + }, + reducerInstance, +); diff --git a/client/src/store/items/items.reducer.js b/client/src/store/items/items.reducer.js index cadadc9d8..0c60a7c43 100644 --- a/client/src/store/items/items.reducer.js +++ b/client/src/store/items/items.reducer.js @@ -1,7 +1,7 @@ import { createReducer } from '@reduxjs/toolkit'; -import { - createTableStateReducers, -} from 'store/tableState.reducer'; +import { persistReducer } from 'redux-persist'; +import storage from 'redux-persist/lib/storage'; +import { createTableStateReducers } from 'store/tableState.reducer'; const initialState = { tableState: { @@ -12,6 +12,17 @@ const initialState = { selectedRows: [], }; -export default createReducer(initialState, { +const reducerInstance = createReducer(initialState, { ...createTableStateReducers('ITEMS'), -}); \ No newline at end of file +}); + +const STORAGE_KEY = 'bigcapital:items'; + +export default persistReducer( + { + key: STORAGE_KEY, + whitelist: ['tableState'], + storage, + }, + reducerInstance, +); \ No newline at end of file diff --git a/client/src/store/manualJournals/manualJournals.reducers.js b/client/src/store/manualJournals/manualJournals.reducers.js index 0dc016f00..2cfd009e0 100644 --- a/client/src/store/manualJournals/manualJournals.reducers.js +++ b/client/src/store/manualJournals/manualJournals.reducers.js @@ -1,7 +1,7 @@ import { createReducer } from '@reduxjs/toolkit'; -import { - createTableStateReducers, -} from 'store/tableState.reducer'; +import { persistReducer } from 'redux-persist'; +import storage from 'redux-persist/lib/storage'; +import { createTableStateReducers } from 'store/tableState.reducer'; const initialState = { tableState: { @@ -10,6 +10,17 @@ const initialState = { }, }; -export default createReducer(initialState, { +const reducerInstance = createReducer(initialState, { ...createTableStateReducers('MANUAL_JOURNALS'), -}); \ No newline at end of file +}); + +const STORAGE_KEY = 'bigcapital:manualJournals'; + +export default persistReducer( + { + key: STORAGE_KEY, + whitelist: ['tableState'], + storage, + }, + reducerInstance, +); diff --git a/client/src/store/receipts/receipts.reducer.js b/client/src/store/receipts/receipts.reducer.js index b4281556c..de3b218d3 100644 --- a/client/src/store/receipts/receipts.reducer.js +++ b/client/src/store/receipts/receipts.reducer.js @@ -1,4 +1,6 @@ import { createReducer } from '@reduxjs/toolkit'; +import { persistReducer } from 'redux-persist'; +import storage from 'redux-persist/lib/storage'; import { createTableStateReducers, } from 'store/tableState.reducer'; @@ -10,6 +12,17 @@ const initialState = { }, }; -export default createReducer(initialState, { +const reducerInstance = createReducer(initialState, { ...createTableStateReducers('RECEIPTS'), -}); \ No newline at end of file +}); + +const STORAGE_KEY = 'bigcapital:receipts'; + +export default persistReducer( + { + key: STORAGE_KEY, + whitelist: ['tableState'], + storage, + }, + reducerInstance, +); diff --git a/client/src/store/reducers.js b/client/src/store/reducers.js index 118be5e31..a72b69ea8 100644 --- a/client/src/store/reducers.js +++ b/client/src/store/reducers.js @@ -1,5 +1,7 @@ import { combineReducers } from 'redux'; +import types from './types'; + import authentication from './authentication/authentication.reducer'; import dashboard from './dashboard/dashboard.reducer'; import users from './users/users.reducer'; @@ -30,7 +32,7 @@ import subscriptions from './subscription/subscription.reducer'; import inventoryAdjustments from './inventoryAdjustments/inventoryAdjustment.reducer'; import plans from './plans/plans.reducer'; -export default combineReducers({ +const appReducer = combineReducers({ authentication, organizations, subscriptions, @@ -61,3 +63,13 @@ export default combineReducers({ inventoryAdjustments, plans }); + +// Reset the state of a redux store +const rootReducer = (state, action) => { + if (action.type === types.RESET) { + state = undefined; + } + return appReducer(state, action) +} + +export default rootReducer; \ No newline at end of file diff --git a/client/src/store/vendors/vendors.reducer.js b/client/src/store/vendors/vendors.reducer.js index cc4726c91..74412377e 100644 --- a/client/src/store/vendors/vendors.reducer.js +++ b/client/src/store/vendors/vendors.reducer.js @@ -1,4 +1,6 @@ import { createReducer } from '@reduxjs/toolkit'; +import { persistReducer } from 'redux-persist'; +import storage from 'redux-persist/lib/storage'; import { createTableStateReducers, } from 'store/tableState.reducer'; @@ -9,6 +11,17 @@ const initialState = { pageIndex: 0, }, }; -export default createReducer(initialState, { +const reducerInstance = createReducer(initialState, { ...createTableStateReducers('VENDORS'), }); + +const STORAGE_KEY = 'bigcapital:vendors'; + +export default persistReducer( + { + key: STORAGE_KEY, + whitelist: ['tableState'], + storage, + }, + reducerInstance, +); diff --git a/server/package.json b/server/package.json index dd5625f35..8a846205e 100644 --- a/server/package.json +++ b/server/package.json @@ -42,6 +42,7 @@ "helmet": "^3.21.0", "i18n": "^0.8.5", "is-my-json-valid": "^2.20.5", + "js-money": "^0.6.3", "jsonwebtoken": "^8.5.1", "knex": "^0.20.3", "knex-cleaner": "^1.3.0", diff --git a/server/src/api/controllers/Items.ts b/server/src/api/controllers/Items.ts index d6a083723..e96822a9b 100644 --- a/server/src/api/controllers/Items.ts +++ b/server/src/api/controllers/Items.ts @@ -547,6 +547,14 @@ export default class ItemsController extends BaseController { }], }); } + if (error.errorType === 'TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS') { + return res.status(400).send({ + errors: [{ + type: 'TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS', + message: 'Cannot change item type to inventory with item has associated transactions.', + }], + }); + } } next(error); } diff --git a/server/src/api/controllers/ManualJournals.ts b/server/src/api/controllers/ManualJournals.ts index 5bff2449c..9b38ed1b4 100644 --- a/server/src/api/controllers/ManualJournals.ts +++ b/server/src/api/controllers/ManualJournals.ts @@ -104,7 +104,7 @@ export default class ManualJournalsController extends BaseController { return [ check('date').exists().isISO8601(), check('journal_number') - .exists() + .optional() .isString() .trim() .escape() @@ -470,6 +470,15 @@ export default class ManualJournalsController extends BaseController { errors: [{ type: 'MANUAL_JOURNAL_ALREADY_PUBLISHED', code: 900 }], }); } + if (error.errorType === 'MANUAL_JOURNAL_NO_REQUIRED') { + return res.boom.badRequest('', { + errors: [{ + type: 'MANUAL_JOURNAL_NO_REQUIRED', + message: 'The manual journal number required.', + code: 1000 + }], + }); + } } next(error); } diff --git a/server/src/api/controllers/Organization.ts b/server/src/api/controllers/Organization.ts index e89de8750..3d50c06f5 100644 --- a/server/src/api/controllers/Organization.ts +++ b/server/src/api/controllers/Organization.ts @@ -67,7 +67,7 @@ export default class OrganizationController extends BaseController { if (error instanceof ServiceError) { if (error.errorType === 'tenant_not_found') { return res.status(400).send({ - errors: [{ type: 'TENANT.NOT.FOUND', code: 100 }], + // errors: [{ type: 'TENANT.NOT.FOUND', code: 100 }], }); } if (error.errorType === 'tenant_already_initialized') { diff --git a/server/src/api/controllers/Purchases/Bills.ts b/server/src/api/controllers/Purchases/Bills.ts index 9d9684d38..d8f8affde 100644 --- a/server/src/api/controllers/Purchases/Bills.ts +++ b/server/src/api/controllers/Purchases/Bills.ts @@ -31,62 +31,54 @@ export default class BillsController extends BaseController { const router = Router(); router.post( - '/', [ - ...this.billValidationSchema, - ], + '/', + [...this.billValidationSchema], this.validationResult, asyncMiddleware(this.newBill.bind(this)), - this.handleServiceError, + this.handleServiceError ); router.post( - '/:id/open', [ - ...this.specificBillValidationSchema, - ], + '/:id/open', + [...this.specificBillValidationSchema], this.validationResult, asyncMiddleware(this.openBill.bind(this)), - this.handleServiceError, + this.handleServiceError ); router.post( - '/:id', [ - ...this.billEditValidationSchema, - ...this.specificBillValidationSchema, - ], + '/:id', + [...this.billEditValidationSchema, ...this.specificBillValidationSchema], this.validationResult, asyncMiddleware(this.editBill.bind(this)), - this.handleServiceError, + this.handleServiceError ); router.get( - '/due', [ - ...this.dueBillsListingValidationSchema - ], + '/due', + [...this.dueBillsListingValidationSchema], this.validationResult, asyncMiddleware(this.getDueBills.bind(this)), - this.handleServiceError, - ) - router.get( - '/:id', [ - ...this.specificBillValidationSchema, - ], - this.validationResult, - asyncMiddleware(this.getBill.bind(this)), - this.handleServiceError, + this.handleServiceError ); router.get( - '/', [ - ...this.billsListingValidationSchema, - ], + '/:id', + [...this.specificBillValidationSchema], + this.validationResult, + asyncMiddleware(this.getBill.bind(this)), + this.handleServiceError + ); + router.get( + '/', + [...this.billsListingValidationSchema], this.validationResult, asyncMiddleware(this.billsList.bind(this)), this.handleServiceError, - this.dynamicListService.handlerErrorsToResponse, + this.dynamicListService.handlerErrorsToResponse ); router.delete( - '/:id', [ - ...this.specificBillValidationSchema - ], + '/:id', + [...this.specificBillValidationSchema], this.validationResult, asyncMiddleware(this.deleteBill.bind(this)), - this.handleServiceError, + this.handleServiceError ); return router; } @@ -110,8 +102,14 @@ export default class BillsController extends BaseController { check('entries.*.item_id').exists().isNumeric().toInt(), check('entries.*.rate').exists().isNumeric().toFloat(), check('entries.*.quantity').exists().isNumeric().toFloat(), - check('entries.*.discount').optional({ nullable: true }).isNumeric().toFloat(), - check('entries.*.description').optional({ nullable: true }).trim().escape(), + check('entries.*.discount') + .optional({ nullable: true }) + .isNumeric() + .toFloat(), + check('entries.*.description') + .optional({ nullable: true }) + .trim() + .escape(), ]; } @@ -135,8 +133,14 @@ export default class BillsController extends BaseController { check('entries.*.item_id').exists().isNumeric().toInt(), check('entries.*.rate').exists().isNumeric().toFloat(), check('entries.*.quantity').exists().isNumeric().toFloat(), - check('entries.*.discount').optional({ nullable: true }).isNumeric().toFloat(), - check('entries.*.description').optional({ nullable: true }).trim().escape(), + check('entries.*.discount') + .optional({ nullable: true }) + .isNumeric() + .toFloat(), + check('entries.*.description') + .optional({ nullable: true }) + .trim() + .escape(), ]; } @@ -167,7 +171,7 @@ export default class BillsController extends BaseController { query('payment_made_id').optional().trim().escape(), ]; } - + /** * Creates a new bill and records journal transactions. * @param {Request} req @@ -179,7 +183,11 @@ export default class BillsController extends BaseController { const billDTO: IBillDTO = this.matchedBodyData(req); try { - const storedBill = await this.billsService.createBill(tenantId, billDTO, user); + const storedBill = await this.billsService.createBill( + tenantId, + billDTO, + user + ); return res.status(200).send({ id: storedBill.id, @@ -293,7 +301,11 @@ export default class BillsController extends BaseController { filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles); } try { - const { bills, pagination, filterMeta } = await this.billsService.getBills(tenantId, filter); + const { + bills, + pagination, + filterMeta, + } = await this.billsService.getBills(tenantId, filter); return res.status(200).send({ bills, @@ -307,9 +319,9 @@ export default class BillsController extends BaseController { /** * Listing all due bills of the given vendor. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ public async getDueBills(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; @@ -325,12 +337,17 @@ export default class BillsController extends BaseController { /** * Handles service errors. - * @param {Error} error - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next + * @param {Error} error + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ - handleServiceError(error: Error, req: Request, res: Response, next: NextFunction) { + handleServiceError( + error: Error, + req: Request, + res: Response, + next: NextFunction + ) { if (error instanceof ServiceError) { if (error.errorType === 'BILL_NOT_FOUND') { return res.status(400).send({ @@ -349,8 +366,8 @@ export default class BillsController extends BaseController { } if (error.errorType === 'BILL_ITEMS_NOT_PURCHASABLE') { return res.status(400).send({ - errors: [{ type: 'BILL_ITEMS_NOT_PURCHASABLE', code: 700 }] - }) + errors: [{ type: 'BILL_ITEMS_NOT_PURCHASABLE', code: 700 }], + }); } if (error.errorType === 'NOT_PURCHASE_ABLE_ITEMS') { return res.status(400).send({ @@ -377,7 +394,23 @@ export default class BillsController extends BaseController { errors: [{ type: 'BILL_ALREADY_OPEN', code: 1100 }], }); } + if (error.errorType === 'contact_not_found') { + return res.boom.badRequest(null, { + errors: [ + { type: 'VENDOR_NOT_FOUND', message: 'Vendor not found.', code: 1200 }, + ], + }); + } + if (error.errorType === 'BILL_HAS_ASSOCIATED_PAYMENT_ENTRIES') { + return res.status(400).send({ + errors: [{ + type: 'BILL_HAS_ASSOCIATED_PAYMENT_ENTRIES', + message: 'Cannot delete bill that has associated payment transactions.', + code: 1200 + }], + }); + } } - next(error); + next(error); } } diff --git a/server/src/api/controllers/Purchases/BillsPayments.ts b/server/src/api/controllers/Purchases/BillsPayments.ts index d0e2f78be..23c0c6673 100644 --- a/server/src/api/controllers/Purchases/BillsPayments.ts +++ b/server/src/api/controllers/Purchases/BillsPayments.ts @@ -415,6 +415,15 @@ export default class BillsPayments extends BaseController { errors: [{ type: 'PAYMENT_NUMBER_SHOULD_NOT_MODIFY', code: 1100 }], }); } + if (error.errorType === 'BILLS_NOT_OPENED_YET') { + return res.status(400).send({ + errors: [{ + type: 'BILLS_NOT_OPENED_YET', + message: 'The given bills are not opened yet.', + code: 1200, + }], + }); + } } next(error); } diff --git a/server/src/api/controllers/Sales/PaymentReceives.ts b/server/src/api/controllers/Sales/PaymentReceives.ts index 6d6a14ac0..0d8f99973 100644 --- a/server/src/api/controllers/Sales/PaymentReceives.ts +++ b/server/src/api/controllers/Sales/PaymentReceives.ts @@ -170,7 +170,6 @@ export default class PaymentReceivesController extends BaseController { message: 'The payment receive has been created successfully.', }); } catch (error) { - console.log(error); next(error); } } diff --git a/server/src/api/controllers/Settings.ts b/server/src/api/controllers/Settings.ts index b849d5767..a50876730 100644 --- a/server/src/api/controllers/Settings.ts +++ b/server/src/api/controllers/Settings.ts @@ -34,52 +34,31 @@ export default class SettingsController extends BaseController{ /** * Save settings validation schema. */ - get saveSettingsValidationSchema() { + private get saveSettingsValidationSchema() { return [ body('options').isArray({ min: 1 }), - body('options.*.key').exists().trim().escape().isLength({ min: 1 }), - body('options.*.value').exists().trim().escape().isLength({ min: 1 }), - body('options.*.group').exists().trim().escape().isLength({ min: 1 }), + body('options.*.key').exists().trim().isLength({ min: 1 }), + body('options.*.value').exists().trim().isLength({ min: 1 }), + body('options.*.group').exists().trim().isLength({ min: 1 }), ]; } /** * Retrieve the application options from the storage. */ - get getSettingsSchema() { + private get getSettingsSchema() { return [ query('key').optional().trim().escape(), query('group').optional().trim().escape(), ]; } - /** - * Observes application configuration option, whether all options configured - * sets `app_configured` option. - * @param {Setting} settings - */ - observeAppConfigsComplete(settings) { - if (!settings.get('app_configured', false)) { - const definedOptions = getDefinedOptions(); - - const isNotConfigured = definedOptions.some((option) => { - const isDefined = isDefinedOptionConfigurable(option.key, option.group); - const hasStoredOption = settings.get({ key: option.key, group: option.group }); - - return (isDefined && !hasStoredOption); - }); - if (!isNotConfigured) { - settings.set('app_configured', true); - } - } - } - /** * Saves the given options to the storage. * @param {Request} req - * @param {Response} res - */ - async saveSettings(req: Request, res: Response) { + public async saveSettings(req: Request, res: Response, next) { const { Option } = req.models; const optionsDTO: IOptionsDTO = this.matchedBodyData(req); const { settings } = req; @@ -100,15 +79,17 @@ export default class SettingsController extends BaseController{ optionsDTO.options.forEach((option: IOptionDTO) => { settings.set({ ...option }); }); - this.observeAppConfigsComplete(settings); + try { + await settings.save(); - await settings.save(); - - return res.status(200).send({ - type: 'success', - code: 'OPTIONS.SAVED.SUCCESSFULLY', - message: 'Options have been saved successfully.', - }); + return res.status(200).send({ + type: 'success', + code: 'OPTIONS.SAVED.SUCCESSFULLY', + message: 'Options have been saved successfully.', + }); + } catch (error) { + next(error); + } } /** @@ -116,7 +97,7 @@ export default class SettingsController extends BaseController{ * @param {Request} req * @param {Response} res */ - getSettings(req: Request, res: Response) { + public getSettings(req: Request, res: Response) { const { settings } = req; const allSettings = settings.all(); diff --git a/server/src/api/controllers/Setup.ts b/server/src/api/controllers/Setup.ts new file mode 100644 index 000000000..eaf598d21 --- /dev/null +++ b/server/src/api/controllers/Setup.ts @@ -0,0 +1,102 @@ +import { Router, Request, Response, NextFunction } from 'express'; +import { check, ValidationChain } from 'express-validator'; +import BaseController from './BaseController'; +import SetupService from 'services/Setup/SetupService'; +import { Inject, Service } from 'typedi'; +import { IOrganizationSetupDTO } from 'interfaces'; +import { ServiceError } from 'exceptions'; +// Middlewares +import JWTAuth from 'api/middleware/jwtAuth'; +import AttachCurrentTenantUser from 'api/middleware/AttachCurrentTenantUser'; +import SubscriptionMiddleware from 'api/middleware/SubscriptionMiddleware'; +import TenancyMiddleware from 'api/middleware/TenancyMiddleware'; +import EnsureTenantIsInitialized from 'api/middleware/EnsureTenantIsInitialized'; +import SettingsMiddleware from 'api/middleware/SettingsMiddleware'; + +@Service() +export default class SetupController extends BaseController { + @Inject() + setupService: SetupService; + + router() { + const router = Router('/setup'); + + router.use(JWTAuth); + router.use(AttachCurrentTenantUser); + router.use(TenancyMiddleware); + router.use(SubscriptionMiddleware('main')); + router.use(EnsureTenantIsInitialized); + router.use(SettingsMiddleware); + router.post( + '/organization', + this.organizationSetupSchema, + this.validationResult, + this.asyncMiddleware(this.organizationSetup.bind(this)), + this.handleServiceErrors + ); + return router; + } + + /** + * Organization setup schema. + */ + private get organizationSetupSchema(): ValidationChain[] { + return [ + check('organization_name').exists().trim(), + check('base_currency').exists(), + check('time_zone').exists(), + check('fiscal_year').exists(), + check('industry').optional(), + ]; + } + + /** + * Organization setup. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + * @returns + */ + async organizationSetup(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; + const setupDTO: IOrganizationSetupDTO = this.matchedBodyData(req); + + try { + await this.setupService.organizationSetup(tenantId, setupDTO); + + return res.status(200).send({ + message: 'The setup settings set successfully.', + }); + } catch (error) { + next(error); + } + } + + /** + * Handles service errors. + * @param {Error} error + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + handleServiceErrors( + error: Error, + req: Request, + res: Response, + next: NextFunction + ) { + if (error instanceof ServiceError) { + if (error.errorType === 'TENANT_IS_ALREADY_SETUPED') { + return res.status(400).send({ + errors: [{ type: 'TENANT_IS_ALREADY_SETUPED', code: 1000 }], + }); + } + if (error.errorType === 'BASE_CURRENCY_INVALID') { + return res.status(400).send({ + errors: [{ type: 'BASE_CURRENCY_INVALID', code: 110 }], + }); + } + } + next(error); + } +} diff --git a/server/src/api/index.ts b/server/src/api/index.ts index 49e773abb..4d6cf32f4 100644 --- a/server/src/api/index.ts +++ b/server/src/api/index.ts @@ -40,6 +40,8 @@ import Subscription from 'api/controllers/Subscription'; import Licenses from 'api/controllers/Subscription/Licenses'; import InventoryAdjustments from 'api/controllers/Inventory/InventoryAdjustments'; +import Setup from 'api/controllers/Setup'; + export default () => { const app = Router(); @@ -53,22 +55,7 @@ export default () => { app.use('/subscription', Container.get(Subscription).router()); app.use('/organization', Container.get(Organization).router()); app.use('/ping', Container.get(Ping).router()); - - // - Settings routes. - // --------------------------- - const settings = Router(); - - settings.use(JWTAuth); - settings.use(AttachCurrentTenantUser); - settings.use(TenancyMiddleware); - settings.use(SubscriptionMiddleware('main')); - settings.use(EnsureTenantIsInitialized); - settings.use(SettingsMiddleware); - - settings.use('/', Container.get(Settings).router()); - - app.use('/settings', settings); - + app.use('/setup', Container.get(Setup).router()); // - Dashboard routes. // --------------------------- const dashboard = Router(); @@ -86,6 +73,7 @@ export default () => { dashboard.use('/users', Container.get(Users).router()); dashboard.use('/invite', Container.get(InviteUsers).authRouter()); dashboard.use('/currencies', Container.get(Currencies).router()); + dashboard.use('/settings', Container.get(Settings).router()); dashboard.use('/accounts', Container.get(Accounts).router()); dashboard.use('/account_types', Container.get(AccountTypes).router()); dashboard.use('/manual-journals', Container.get(ManualJournals).router()); diff --git a/server/src/data/options.js b/server/src/data/options.js index f905a7d4c..e00bb7538 100644 --- a/server/src/data/options.js +++ b/server/src/data/options.js @@ -46,7 +46,7 @@ export default { manual_journals: [ { key: "next_number", - type: "number", + type: "string", }, { key: "number_prefix", diff --git a/server/src/database/migrations/20200728161617_create_bill_payments_entries.js b/server/src/database/migrations/20200728161617_create_bill_payments_entries.js index 30af9d50f..166bb9087 100644 --- a/server/src/database/migrations/20200728161617_create_bill_payments_entries.js +++ b/server/src/database/migrations/20200728161617_create_bill_payments_entries.js @@ -4,7 +4,7 @@ exports.up = function(knex) { table.increments(); table.integer('bill_payment_id').unsigned().index().references('id').inTable('bills_payments'); - table.integer('bill_id').unsigned().index(); + table.integer('bill_id').unsigned().index().references('id').inTable('bills'); table.decimal('payment_amount', 13, 3).unsigned(); }) }; diff --git a/server/src/database/seeds/core/20200810121809_seed_settings.js b/server/src/database/seeds/core/20200810121809_seed_settings.js index f6578ba9f..62e36cfd7 100644 --- a/server/src/database/seeds/core/20200810121809_seed_settings.js +++ b/server/src/database/seeds/core/20200810121809_seed_settings.js @@ -5,14 +5,29 @@ exports.up = (knex) => { const tenancyService = Container.get(TenancyService); const settings = tenancyService.settings(knex.userParams.tenantId); - settings.set({ group: 'manual_journals', key: 'next_number', value: 1 }); - settings.set({ group: 'sales_invoices', key: 'next_number', value: 1 }); - settings.set({ group: 'sales_invoices', key: 'number_prefix', value: 'INV' }); - settings.set({ group: 'sales_receipts', key: 'next_number', value: 1 }); - settings.set({ group: 'sales_receipts', key: 'number_prefix', value: 'REC' }); - settings.set({ group: 'sales_estimates', key: 'next_number', value: 1 }); - settings.set({ group: 'sales_estimates', key: 'number_prefix', value: 'EST' }); - settings.set({ group: 'payment_receives', key: 'next_number', value: 1 }); + // Manual journals settings. + settings.set({ group: 'manual_journals', key: 'next_number', value: '00001' }); + settings.set({ group: 'manual_journals', key: 'auto_increment', value: true }); + + // Sale invoices settings. + settings.set({ group: 'sales_invoices', key: 'next_number', value: '00001' }); + settings.set({ group: 'sales_invoices', key: 'number_prefix', value: 'INV-' }); + settings.set({ group: 'sales_invoices', key: 'auto_increment', value: true }); + + // Sale receipts settings. + settings.set({ group: 'sales_receipts', key: 'next_number', value: '00001' }); + settings.set({ group: 'sales_receipts', key: 'number_prefix', value: 'REC-' }); + settings.set({ group: 'sales_receipts', key: 'auto_increment', value: true }); + + // Sale estimates settings. + settings.set({ group: 'sales_estimates', key: 'next_number', value: '00001' }); + settings.set({ group: 'sales_estimates', key: 'number_prefix', value: 'EST-' }); + settings.set({ group: 'sales_estimates', key: 'auto_increment', value: true }); + + // Payment receives settings. + settings.set({ group: 'payment_receives', key: 'number_prefix', value: 'PAY-' }); + settings.set({ group: 'payment_receives', key: 'next_number', value: '00001' }); + settings.set({ group: 'payment_receives', key: 'auto_increment', value: true }); return settings.save(); }; diff --git a/server/src/interfaces/ManualJournal.ts b/server/src/interfaces/ManualJournal.ts index 58a148c61..ebef7b511 100644 --- a/server/src/interfaces/ManualJournal.ts +++ b/server/src/interfaces/ManualJournal.ts @@ -3,9 +3,9 @@ import { IJournalEntry } from './Journal'; import { ISystemUser } from './User'; export interface IManualJournal { - id: number; + id?: number; date: Date | string; - journalNumber: number; + journalNumber: string; journalType: string; reference: string; amount: number; @@ -37,7 +37,7 @@ export interface IManualJournalEntryDTO { export interface IManualJournalDTO { date: Date; - journalNumber: number; + journalNumber: string; journalType: string; reference?: string; description?: string; diff --git a/server/src/interfaces/Setup.ts b/server/src/interfaces/Setup.ts new file mode 100644 index 000000000..2edf91495 --- /dev/null +++ b/server/src/interfaces/Setup.ts @@ -0,0 +1,10 @@ + + + +export interface IOrganizationSetupDTO{ + organizationName: string, + baseCurrency: string, + fiscalYear: string, + industry: string, + timeZone: string, +} \ No newline at end of file diff --git a/server/src/interfaces/index.ts b/server/src/interfaces/index.ts index da77e0ed8..444d9d852 100644 --- a/server/src/interfaces/index.ts +++ b/server/src/interfaces/index.ts @@ -39,4 +39,5 @@ export * from './AgingReport'; export * from './ARAgingSummaryReport'; export * from './APAgingSummaryReport'; export * from './Mailable'; -export * from './InventoryAdjustment'; \ No newline at end of file +export * from './InventoryAdjustment'; +export * from './Setup' \ No newline at end of file diff --git a/server/src/services/Currencies/CurrenciesService.ts b/server/src/services/Currencies/CurrenciesService.ts index dc986b742..18e7702ed 100644 --- a/server/src/services/Currencies/CurrenciesService.ts +++ b/server/src/services/Currencies/CurrenciesService.ts @@ -1,4 +1,5 @@ -import { Inject, Container, Service } from 'typedi'; +import { Inject, Service } from 'typedi'; +import Currencies from 'js-money/lib/currency'; import { ICurrencyEditDTO, ICurrencyDTO, @@ -15,6 +16,7 @@ import TenancyService from 'services/Tenancy/TenancyService'; const ERRORS = { CURRENCY_NOT_FOUND: 'currency_not_found', CURRENCY_CODE_EXISTS: 'currency_code_exists', + BASE_CURRENCY_INVALID: 'BASE_CURRENCY_INVALID' }; @Service() @@ -129,7 +131,6 @@ export default class CurrenciesService implements ICurrenciesService { tenantId, currencyDTO, }); - await this.validateCurrencyCodeUniquiness( tenantId, currencyDTO.currencyCode @@ -211,4 +212,25 @@ export default class CurrenciesService implements ICurrenciesService { }); return currencies; } + + /** + * Seeds the given base currency to the currencies list. + * @param {number} tenantId + * @param {string} baseCurrency + */ + public async seedBaseCurrency(tenantId: number, baseCurrency: string) { + const { Currency } = this.tenancy.models(tenantId); + const currencyMeta = Currencies[baseCurrency]; + + const foundBaseCurrency = await Currency.query().findOne( + 'currency_code', + baseCurrency + ); + if (!foundBaseCurrency) { + await Currency.query().insert({ + currency_code: currencyMeta.code, + currency_name: currencyMeta.name, + }); + } + } } diff --git a/server/src/services/Items/ItemsService.ts b/server/src/services/Items/ItemsService.ts index 87d9ee3dd..1ea28d375 100644 --- a/server/src/services/Items/ItemsService.ts +++ b/server/src/services/Items/ItemsService.ts @@ -244,11 +244,11 @@ export default class ItemsService implements IItemsService { } /** - * Validate item type in edit item mode, cannot change item inventory type. + * Validate edit item type from inventory to another type that not allowed. * @param {IItemDTO} itemDTO * @param {IItem} oldItem */ - private validateEditItemInventoryType(itemDTO: IItemDTO, oldItem: IItem) { + private validateEditItemFromInventory(itemDTO: IItemDTO, oldItem: IItem) { if ( itemDTO.type && oldItem.type === 'inventory' && @@ -258,6 +258,36 @@ export default class ItemsService implements IItemsService { } } + /** + * Validates edit item type from service/non-inventory to inventory. + * Should item has no any relations with accounts transactions. + * @param {number} tenantId - Tenant id. + * @param {number} itemId - Item id. + */ + private async validateEditItemTypeToInventory( + tenantId: number, + oldItem: IItem, + newItemDTO: IItemDTO + ) { + const { AccountTransaction } = this.tenancy.models(tenantId); + + // We have no problem in case the item type not modified. + if (newItemDTO.type === oldItem.type || oldItem.type === 'inventory') { + return; + } + // Retrieve all transactions that associated to the given item id. + const itemTransactionsCount = await AccountTransaction.query() + .where('item_id', oldItem.id) + .count('item_id', { as: 'transactions' }) + .first(); + + if (itemTransactionsCount.transactions > 0) { + throw new ServiceError( + ERRORS.TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS + ); + } + } + /** * Creates a new item. * @param {number} tenantId DTO @@ -319,7 +349,11 @@ export default class ItemsService implements IItemsService { // Validates the given item existance on the storage. const oldItem = await this.getItemOrThrowError(tenantId, itemId); - this.validateEditItemInventoryType(itemDTO, oldItem); + // Validate edit item type from inventory type. + this.validateEditItemFromInventory(itemDTO, oldItem); + + // Validate edit item type to inventory type. + await this.validateEditItemTypeToInventory(tenantId, oldItem, itemDTO); // Transform the edit item DTO to model. const itemModel = this.transformEditItemDTOToModel(itemDTO, oldItem); @@ -580,8 +614,7 @@ export default class ItemsService implements IItemsService { const { ItemEntry } = this.tenancy.models(tenantId); const ids = Array.isArray(itemId) ? itemId : [itemId]; - const foundItemEntries = await ItemEntry.query() - .whereIn('item_id', ids); + const foundItemEntries = await ItemEntry.query().whereIn('item_id', ids); if (foundItemEntries.length > 0) { throw new ServiceError( diff --git a/server/src/services/Items/constants.ts b/server/src/services/Items/constants.ts index 0bafbe384..59ba5313d 100644 --- a/server/src/services/Items/constants.ts +++ b/server/src/services/Items/constants.ts @@ -18,4 +18,5 @@ export const ERRORS = { ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT: 'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT', ITEM_CANNOT_CHANGE_INVENTORY_TYPE: 'ITEM_CANNOT_CHANGE_INVENTORY_TYPE', + TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS: 'TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS', }; diff --git a/server/src/services/ManualJournals/ManualJournalsService.ts b/server/src/services/ManualJournals/ManualJournalsService.ts index be7737b95..d70592222 100644 --- a/server/src/services/ManualJournals/ManualJournalsService.ts +++ b/server/src/services/ManualJournals/ManualJournalsService.ts @@ -21,6 +21,7 @@ import { import JournalPoster from 'services/Accounting/JournalPoster'; import JournalCommands from 'services/Accounting/JournalCommands'; import JournalPosterService from 'services/Sales/JournalPosterService'; +import AutoIncrementOrdersService from 'services/Sales/AutoIncrementOrdersService'; import { ERRORS } from './constants'; @Service() @@ -40,6 +41,9 @@ export default class ManualJournalsService implements IManualJournalsService { @EventDispatcher() eventDispatcher: EventDispatcherInterface; + @Inject() + autoIncrementOrdersService: AutoIncrementOrdersService; + /** * Validates the manual journal existance. * @param {number} tenantId @@ -157,18 +161,18 @@ export default class ManualJournalsService implements IManualJournalsService { */ private async validateManualJournalNoUnique( tenantId: number, - manualJournalDTO: IManualJournalDTO, + journalNumber: string, notId?: number ) { const { ManualJournal } = this.tenancy.models(tenantId); - const journalNumber = await ManualJournal.query() - .where('journal_number', manualJournalDTO.journalNumber) + const journals = await ManualJournal.query() + .where('journal_number', journalNumber) .onBuild((builder) => { if (notId) { builder.whereNot('id', notId); } }); - if (journalNumber.length > 0) { + if (journals.length > 0) { throw new ServiceError(ERRORS.JOURNAL_NUMBER_EXISTS); } } @@ -206,7 +210,7 @@ export default class ManualJournalsService implements IManualJournalsService { ); // Throw error in case one of entries that has invalid contact type. if (entriesNoContact.length > 0) { - const indexes = entriesNoContact.map(e => e.index); + const indexes = entriesNoContact.map((e) => e.index); throw new ServiceError(ERRORS.ENTRIES_SHOULD_ASSIGN_WITH_CONTACT, '', { accountSlug: accountBySlug, @@ -291,18 +295,58 @@ export default class ManualJournalsService implements IManualJournalsService { } } + /** + * Retrieve the next journal number. + */ + getNextJournalNumber(tenantId: number): string { + return this.autoIncrementOrdersService.getNextTransactionNumber( + tenantId, + 'manual_journals' + ); + } + + /** + * Increment the manual journal number. + * @param {number} tenantId + */ + incrementNextJournalNumber(tenantId: number) { + return this.autoIncrementOrdersService.incrementSettingsNextNumber( + tenantId, + 'manual_journals' + ); + } + + /** + * Validates the manual journal number require. + * @param {string} journalNumber + */ + private validateJournalNoRequire(journalNumber: string) { + if (!journalNumber) { + throw new ServiceError(ERRORS.MANUAL_JOURNAL_NO_REQUIRED); + } + } + /** * Transform the new manual journal DTO to upsert graph operation. * @param {IManualJournalDTO} manualJournalDTO - Manual jorunal DTO. * @param {ISystemUser} authorizedUser */ private transformNewDTOToModel( + tenantId, manualJournalDTO: IManualJournalDTO, authorizedUser: ISystemUser ) { const amount = sumBy(manualJournalDTO.entries, 'credit') || 0; const date = moment(manualJournalDTO.date).format('YYYY-MM-DD'); + // Retrieve the next manual journal number. + const autoNextNumber = this.getNextJournalNumber(tenantId); + + const journalNumber = manualJournalDTO.journalNumber || autoNextNumber; + + // Validate manual journal number require. + this.validateJournalNoRequire(journalNumber); + return { ...omit(manualJournalDTO, ['publish']), ...(manualJournalDTO.publish @@ -310,6 +354,7 @@ export default class ManualJournalsService implements IManualJournalsService { : {}), amount, date, + journalNumber, userId: authorizedUser.id, }; } @@ -350,6 +395,12 @@ export default class ManualJournalsService implements IManualJournalsService { ): Promise<{ manualJournal: IManualJournal }> { const { ManualJournal } = this.tenancy.models(tenantId); + // Transformes the next DTO to model. + const manualJournalObj = this.transformNewDTOToModel( + tenantId, + manualJournalDTO, + authorizedUser + ); // Validate the total credit should equals debit. this.valdiateCreditDebitTotalEquals(manualJournalDTO); @@ -360,8 +411,10 @@ export default class ManualJournalsService implements IManualJournalsService { await this.validateAccountsExistance(tenantId, manualJournalDTO); // Validate manual journal uniquiness on the storage. - await this.validateManualJournalNoUnique(tenantId, manualJournalDTO); - + await this.validateManualJournalNoUnique( + tenantId, + manualJournalObj.journalNumber + ); // Validate accounts with contact type from the given config. await this.dynamicValidateAccountsWithContactType( tenantId, @@ -371,10 +424,7 @@ export default class ManualJournalsService implements IManualJournalsService { '[manual_journal] trying to save manual journal to the storage.', { tenantId, manualJournalDTO } ); - const manualJournalObj = this.transformNewDTOToModel( - manualJournalDTO, - authorizedUser - ); + const manualJournal = await ManualJournal.query().upsertGraph({ ...manualJournalObj, }); @@ -415,6 +465,11 @@ export default class ManualJournalsService implements IManualJournalsService { tenantId, manualJournalId ); + // Transform manual journal DTO to model. + const manualJournalObj = this.transformEditDTOToModel( + manualJournalDTO, + oldManualJournal + ); // Validates the total credit and debit to be equals. this.valdiateCreditDebitTotalEquals(manualJournalDTO); @@ -425,21 +480,19 @@ export default class ManualJournalsService implements IManualJournalsService { await this.validateAccountsExistance(tenantId, manualJournalDTO); // Validates the manual journal number uniquiness. - await this.validateManualJournalNoUnique( - tenantId, - manualJournalDTO, - manualJournalId - ); + if (manualJournalDTO.journalNumber) { + await this.validateManualJournalNoUnique( + tenantId, + manualJournalDTO.journalNumber, + manualJournalId + ); + } // Validate accounts with contact type from the given config. await this.dynamicValidateAccountsWithContactType( tenantId, manualJournalDTO.entries ); - // Transform manual journal DTO to model. - const manualJournalObj = this.transformEditDTOToModel( - manualJournalDTO, - oldManualJournal - ); + await ManualJournal.query().upsertGraph({ ...manualJournalObj, }); diff --git a/server/src/services/ManualJournals/constants.ts b/server/src/services/ManualJournals/constants.ts index 788323e03..5386e20b4 100644 --- a/server/src/services/ManualJournals/constants.ts +++ b/server/src/services/ManualJournals/constants.ts @@ -8,6 +8,7 @@ export const ERRORS = { CONTACTS_NOT_FOUND: 'contacts_not_found', ENTRIES_CONTACTS_NOT_FOUND: 'ENTRIES_CONTACTS_NOT_FOUND', MANUAL_JOURNAL_ALREADY_PUBLISHED: 'MANUAL_JOURNAL_ALREADY_PUBLISHED', + MANUAL_JOURNAL_NO_REQUIRED: 'MANUAL_JOURNAL_NO_REQUIRED' }; export const CONTACTS_CONFIG = [ diff --git a/server/src/services/Purchases/BillPayments/BillPayments.ts b/server/src/services/Purchases/BillPayments/BillPayments.ts index d2201ceb3..3b8ada9b2 100644 --- a/server/src/services/Purchases/BillPayments/BillPayments.ts +++ b/server/src/services/Purchases/BillPayments/BillPayments.ts @@ -172,6 +172,15 @@ export default class BillPaymentsService { if (notFoundBillsIds.length > 0) { throw new ServiceError(ERRORS.BILL_ENTRIES_IDS_NOT_FOUND); } + + // Validate the not opened bills. + const notOpenedBills = storedBills.filter((bill) => !bill.openedAt); + + if (notOpenedBills.length > 0) { + throw new ServiceError(ERRORS.BILLS_NOT_OPENED_YET, null, { + notOpenedBills + }); + } } /** diff --git a/server/src/services/Purchases/BillPayments/constants.ts b/server/src/services/Purchases/BillPayments/constants.ts index b38962971..0cf82d0d2 100644 --- a/server/src/services/Purchases/BillPayments/constants.ts +++ b/server/src/services/Purchases/BillPayments/constants.ts @@ -9,4 +9,5 @@ export const ERRORS = { BILL_PAYMENT_ENTRIES_NOT_FOUND: 'BILL_PAYMENT_ENTRIES_NOT_FOUND', INVALID_BILL_PAYMENT_AMOUNT: 'INVALID_BILL_PAYMENT_AMOUNT', PAYMENT_NUMBER_SHOULD_NOT_MODIFY: 'PAYMENT_NUMBER_SHOULD_NOT_MODIFY', + BILLS_NOT_OPENED_YET: 'BILLS_NOT_OPENED_YET' }; diff --git a/server/src/services/Purchases/Bills.ts b/server/src/services/Purchases/Bills.ts index 5d2a716ff..6c239623d 100644 --- a/server/src/services/Purchases/Bills.ts +++ b/server/src/services/Purchases/Bills.ts @@ -139,6 +139,26 @@ export default class BillsService extends SalesInvoicesCost { } } + /** + * Validate the bill has no payment entries. + * @param {number} tenantId + * @param {number} billId - Bill id. + */ + private async validateBillHasNoEntries( + tenantId, + billId: number, + ) { + const { BillPaymentEntry } = this.tenancy.models(tenantId); + + // Retireve the bill associate payment made entries. + const entries = await BillPaymentEntry.query().where('bill_id', billId); + + if (entries.length > 0) { + throw new ServiceError(ERRORS.BILL_HAS_ASSOCIATED_PAYMENT_ENTRIES); + } + return entries; + } + /** * Validate the bill number require. * @param {string} billNo - @@ -354,6 +374,9 @@ export default class BillsService extends SalesInvoicesCost { // Retrieve the given bill or throw not found error. const oldBill = await this.getBillOrThrowError(tenantId, billId); + // Validate the purchase bill has no assocaited payments transactions. + await this.validateBillHasNoEntries(tenantId, billId); + // Delete all associated bill entries. const deleteBillEntriesOper = ItemEntry.query() .where('reference_type', 'Bill') diff --git a/server/src/services/Purchases/constants.ts b/server/src/services/Purchases/constants.ts index b251516be..b0d0d87e4 100644 --- a/server/src/services/Purchases/constants.ts +++ b/server/src/services/Purchases/constants.ts @@ -7,5 +7,6 @@ export const ERRORS = { BILL_ENTRIES_IDS_NOT_FOUND: 'BILL_ENTRIES_IDS_NOT_FOUND', NOT_PURCHASE_ABLE_ITEMS: 'NOT_PURCHASE_ABLE_ITEMS', BILL_ALREADY_OPEN: 'BILL_ALREADY_OPEN', - BILL_NO_IS_REQUIRED: 'BILL_NO_IS_REQUIRED' + BILL_NO_IS_REQUIRED: 'BILL_NO_IS_REQUIRED', + BILL_HAS_ASSOCIATED_PAYMENT_ENTRIES: 'BILL_HAS_ASSOCIATED_PAYMENT_ENTRIES' }; diff --git a/server/src/services/Sales/AutoIncrementOrdersService.ts b/server/src/services/Sales/AutoIncrementOrdersService.ts index 5ae86f8a9..ec2b3391a 100644 --- a/server/src/services/Sales/AutoIncrementOrdersService.ts +++ b/server/src/services/Sales/AutoIncrementOrdersService.ts @@ -23,6 +23,7 @@ export default class AutoIncrementOrdersService { // Settings service transaction number and prefix. const autoIncrement = settings.get({ group, key: 'auto_increment' }, false); + const settingNo = settings.get({ group, key: 'next_number' }, ''); const settingPrefix = settings.get({ group, key: 'number_prefix' }, ''); @@ -37,11 +38,12 @@ export default class AutoIncrementOrdersService { */ async incrementSettingsNextNumber(tenantId: number, group: string) { const settings = this.tenancy.settings(tenantId); + const settingNo = settings.get({ group, key: 'next_number' }); const autoIncrement = settings.get({ group, key: 'auto_increment' }); // Can't continue if the auto-increment of the service was disabled. - if (!autoIncrement) return; + if (!autoIncrement) { return; } settings.set( { group, key: 'next_number' }, diff --git a/server/src/services/Sales/PaymentReceives/PaymentsReceives.ts b/server/src/services/Sales/PaymentReceives/PaymentsReceives.ts index 868b320d7..07977f5fe 100644 --- a/server/src/services/Sales/PaymentReceives/PaymentsReceives.ts +++ b/server/src/services/Sales/PaymentReceives/PaymentsReceives.ts @@ -522,7 +522,7 @@ export default class PaymentReceiveService { public async deletePaymentReceive( tenantId: number, paymentReceiveId: number, - authorizedUser: ISystemUser + authorizedUser: ISystemUser, ) { const { PaymentReceive, PaymentReceiveEntry } = this.tenancy.models( tenantId @@ -541,6 +541,7 @@ export default class PaymentReceiveService { // Deletes the payment receive transaction. await PaymentReceive.query().findById(paymentReceiveId).delete(); + // Triggers `onPaymentReceiveDeleted` event. await this.eventDispatcher.dispatch(events.paymentReceive.onDeleted, { tenantId, paymentReceiveId, diff --git a/server/src/services/Setup/SetupService.ts b/server/src/services/Setup/SetupService.ts new file mode 100644 index 000000000..80c3683ae --- /dev/null +++ b/server/src/services/Setup/SetupService.ts @@ -0,0 +1,120 @@ +import { Service, Inject } from 'typedi'; +import Currencies from 'js-money/lib/currency'; +import HasTenancyService from 'services/Tenancy/TenancyService'; +import { IOrganizationSetupDTO, ITenant } from 'interfaces'; + +import CurrenciesService from 'services/Currencies/CurrenciesService'; +import TenantsManagerService from 'services/Tenancy/TenantsManager'; +import { ServiceError } from 'exceptions'; + +const ERRORS = { + TENANT_IS_ALREADY_SETUPED: 'TENANT_IS_ALREADY_SETUPED', + BASE_CURRENCY_INVALID: 'BASE_CURRENCY_INVALID', +}; + +@Service() +export default class SetupService { + @Inject() + tenancy: HasTenancyService; + + @Inject() + currenciesService: CurrenciesService; + + @Inject() + tenantsManager: TenantsManagerService; + + @Inject('repositories') + sysRepositories: any; + + /** + * Transformes the setup DTO to settings. + * @param {IOrganizationSetupDTO} setupDTO + * @returns + */ + private transformSetupDTOToOptions(setupDTO: IOrganizationSetupDTO) { + return [ + { key: 'name', value: setupDTO.organizationName }, + { key: 'base_currency', value: setupDTO.baseCurrency }, + { key: 'time_zone', value: setupDTO.timeZone }, + { key: 'industry', value: setupDTO.industry }, + ]; + } + + /** + * Sets organization setup settings. + * @param {number} tenantId + * @param {IOrganizationSetupDTO} organizationSetupDTO + */ + private setOrganizationSetupSettings( + tenantId: number, + organizationSetupDTO: IOrganizationSetupDTO + ) { + const settings = this.tenancy.settings(tenantId); + + // Can't continue if app is already configured. + if (settings.get('app_configured')) { + return; + } + settings.set([ + ...this.transformSetupDTOToOptions(organizationSetupDTO) + .filter((option) => typeof option.value !== 'undefined') + .map((option) => ({ + ...option, + group: 'organization', + })), + { key: 'app_configured', value: true }, + ]); + } + + /** + * Validates the base currency code. + * @param {string} baseCurrency + */ + public validateBaseCurrencyCode(baseCurrency: string) { + if (typeof Currencies[baseCurrency] === 'undefined') { + throw new ServiceError(ERRORS.BASE_CURRENCY_INVALID); + } + } + + /** + * Organization setup DTO. + * @param {IOrganizationSetupDTO} organizationSetupDTO + * @return {Promise} + */ + public async organizationSetup( + tenantId: number, + organizationSetupDTO: IOrganizationSetupDTO, + ): Promise { + const { tenantRepository } = this.sysRepositories; + + // Find tenant model by the given id. + const tenant = await tenantRepository.findOneById(tenantId); + + // Validate base currency code. + this.validateBaseCurrencyCode(organizationSetupDTO.baseCurrency); + + // Validate tenant not already seeded. + this.validateTenantNotSeeded(tenant); + + // Seeds the base currency to the currencies list. + this.currenciesService.seedBaseCurrency( + tenantId, + organizationSetupDTO.baseCurrency + ); + // Sets organization setup settings. + await this.setOrganizationSetupSettings(tenantId, organizationSetupDTO); + + // Seed tenant. + await this.tenantsManager.seedTenant(tenant); + } + + /** + * Validates tenant not seeded. + * @param {ITenant} tenant + */ + private validateTenantNotSeeded(tenant: ITenant) { + if (tenant.seededAt) { + throw new ServiceError(ERRORS.TENANT_IS_ALREADY_SETUPED); + } + } +} diff --git a/server/src/subscribers/manualJournals.ts b/server/src/subscribers/manualJournals.ts index 8a031f8d2..48603d17c 100644 --- a/server/src/subscribers/manualJournals.ts +++ b/server/src/subscribers/manualJournals.ts @@ -24,12 +24,20 @@ export class ManualJournalSubscriber { // Ingore writing manual journal journal entries in case was not published. if (manualJournal.publishedAt) { await this.manualJournalsService.writeJournalEntries( - tenantId, + tenantId, manualJournal ); } } + /** + * Handles the manual journal next number increment once the journal be created. + */ + @On(events.manualJournals.onCreated) + public async handleJournalNumberIncrement({ tenantId }) { + await this.manualJournalsService.incrementNextJournalNumber(tenantId); + } + /** * Handle manual journal edited event. */ @@ -107,16 +115,4 @@ export class ManualJournalSubscriber { manualJournalsIds ); } - - /** - * Handle increment next number of manual journal once be created. - */ - @On(events.manualJournals.onCreated) - public async handleJournalNextNumberIncrement({ tenantId }) { - const query = { - group: 'manual_journals', - key: 'next_number', - }; - await this.settingsService.incrementNextNumber(tenantId, query); - } }