diff --git a/package.json b/package.json index bd109512b..38afbca86 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ "@blueprintjs/select": "^3.11.2", "@blueprintjs/table": "^3.8.3", "@blueprintjs/timezone": "^3.6.2", + "@casl/ability": "^5.4.3", + "@casl/react": "^2.3.0", "@reduxjs/toolkit": "^1.2.5", "@sentry/react": "^6.13.2", "@sentry/tracing": "^6.13.2", diff --git a/src/common/abilityOption.js b/src/common/abilityOption.js new file mode 100644 index 000000000..1a5625ca3 --- /dev/null +++ b/src/common/abilityOption.js @@ -0,0 +1,161 @@ +export const AbilitySubject = { + Item: 'Item', + InventoryAdjustment: 'InventoryAdjustment', + Estimate: 'SaleEstimate', + Invoice: 'SaleInvoice', + Receipt: 'SaleReceipt', + PaymentReceive: 'PaymentReceive', + Bill: 'Bill', + PaymentMade: 'PaymentMade', + Customer: 'Customer', + Vendor: 'Vendor', + Account: 'Account', + ManualJournal: 'ManualJournal', + Expense: 'Expense', + Cashflow: 'Cashflow', + Report: 'Report', + Preferences: 'Preferences', + ExchangeRate: 'ExchangeRate', + SubscriptionBilling: 'SubscriptionBilling', +}; + +export const ItemAction = { + View: 'View', + Create: 'Create', + Edit: 'Edit', + Delete: 'Delete', +}; + +export const InventoryAdjustmentAction = { + Create: 'Create', + Edit: 'Edit', + View: 'View', + Delete: 'Delete', +}; + +export const SaleEstimateAction = { + View: 'View', + Create: 'Create', + Edit: 'Edit', + Delete: 'Delete', + NotifyBySms: 'NotifyBySms', +}; + +export const SaleInvoiceAction = { + View: 'View', + Create: 'Create', + Edit: 'Edit', + Delete: 'Delete', + Writeoff: 'Writeoff', + NotifyBySms: 'NotifyBySms', +}; + +export const SaleReceiptAction = { + View: 'View', + Create: 'Create', + Edit: 'Edit', + Delete: 'Delete', + NotifyBySms: 'NotifyBySms', +}; + +export const PaymentReceiveAction = { + View: 'View', + Create: 'Create', + Edit: 'Edit', + Delete: 'Delete', + NotifyBySms: 'NotifyBySms', +}; + +export const BillAction = { + View: 'View', + Create: 'Create', + Edit: 'Edit', + Delete: 'Delete', + NotifyBySms: 'NotifyBySms', +}; + +export const PaymentMadeAction = { + View: 'View', + Create: 'Create', + Edit: 'Edit', + Delete: 'Delete', +}; + +export const CustomerAction = { + View: 'View', + Create: 'Create', + Edit: 'Edit', + Delete: 'Delete', +}; + +export const VendorAction = { + View: 'View', + Create: 'Create', + Edit: 'Edit', + Delete: 'Delete', +}; + +export const AccountAction = { + View: 'View', + Create: 'Create', + Edit: 'Edit', + Delete: 'Delete', + TransactionsLocking: 'TransactionsLocking', +}; + +export const ManualJournalAction = { + View: 'View', + Create: 'Create', + Edit: 'Edit', + Delete: 'Delete', + TransactionLocking: 'TransactionLocking', +}; + +export const ExpenseAction = { + View: 'View', + Create: 'Create', + Edit: 'Edit', + Delete: 'Delete', +}; + +export const CashflowAction = { + View: 'View', + Create: 'Create', + Delete: 'Delete', +}; + +export const ReportsAction = { + ALL: 'all', + READ_BALANCE_SHEET: 'read-balance-sheet', + READ_TRIAL_BALANCE_SHEET: 'read-trial-balance-sheet', + READ_PROFIT_LOSS: 'read-profit-loss', + READ_JOURNAL: 'read-journal', + READ_GENERAL_LEDGET: 'read-general-ledger', + READ_CASHFLOW: 'read-cashflow', + READ_AR_AGING_SUMMARY: 'read-ar-aging-summary', + READ_AP_AGING_SUMMARY: 'read-ap-aging-summary', + READ_PURCHASES_BY_ITEMS: 'read-purchases-by-items', + READ_SALES_BY_ITEMS: 'read-sales-by-items', + READ_CUSTOMERS_TRANSACTIONS: 'read-customers-transactions', + READ_VENDORS_TRANSACTIONS: 'read-vendors-transactions', + READ_CUSTOMERS_SUMMARY_BALANCE: 'read-customers-summary-balance', + READ_VENDORS_SUMMARY_BALANCE: 'read-vendors-summary-balance', + READ_INVENTORY_VALUATION_SUMMARY: 'read-inventory-valuation-summary', + READ_INVENTORY_ITEM_DETAILS: 'read-inventory-item-details', + READ_CASHFLOW_ACCOUNT_TRANSACTION: 'read-cashflow-account-transactions', +}; + +export const PreferencesAbility = { + Mutate: 'Mutate', +}; + +export const ExchangeRateAbility = { + View: 'view', + Create: 'create', + Delete: 'delete', +}; + +export const SubscriptionBillingAbility = { + View: 'view', + Payment: 'payment', +}; diff --git a/src/common/classes.js b/src/common/classes.js index bea33a718..e1df0b33f 100644 --- a/src/common/classes.js +++ b/src/common/classes.js @@ -67,6 +67,7 @@ const CLASSES = { PREFERENCES_PAGE_INSIDE_CONTENT_CURRENCIES: 'preferences-page__inside-content--currencies', PREFERENCES_PAGE_INSIDE_CONTENT_ACCOUNTANT: 'preferences-page__inside-content--accountant', PREFERENCES_PAGE_INSIDE_CONTENT_SMS_INTEGRATION: 'preferences-page__inside-content--sms-integration', + PREFERENCES_PAGE_INSIDE_CONTENT_ROLES_FORM: 'preferences-page__inside-content--roles-form', FINANCIAL_REPORT_INSIDER: 'dashboard__insider--financial-report', diff --git a/src/common/homepageOptions.js b/src/common/homepageOptions.js index 82972db3a..beb9ab623 100644 --- a/src/common/homepageOptions.js +++ b/src/common/homepageOptions.js @@ -1,5 +1,21 @@ import React from 'react'; import { FormattedMessage as T } from 'components'; +import { + SaleInvoiceAction, + SaleEstimateAction, + AbilitySubject, + SaleReceiptAction, + CustomerAction, + PaymentReceiveAction, + BillAction, + VendorAction, + PaymentMadeAction, + AccountAction, + ManualJournalAction, + ExpenseAction, + ItemAction, + ReportsAction, +} from '../common/abilityOption'; export const accountsReceivable = [ { @@ -9,21 +25,29 @@ export const accountsReceivable = [ title: , description: , link: '/invoices', + subject: AbilitySubject.Invoice, + ability: SaleInvoiceAction.View, }, { title: , description: , link: '/estimates', + subject: AbilitySubject.Estimate, + ability: SaleEstimateAction.View, }, { title: , description: , link: '/receipts', + subject: AbilitySubject.Receipt, + ability: SaleReceiptAction.View, }, { title: , description: , link: '/customers', + subject: AbilitySubject.Customer, + ability: CustomerAction.View, }, { title: , @@ -31,6 +55,8 @@ export const accountsReceivable = [ ), link: '/payment-receives', + subject: AbilitySubject.PaymentReceive, + ability: PaymentReceiveAction.View, }, ], }, @@ -46,6 +72,8 @@ export const accountsPayable = [ ), link: '/bills', + subject: AbilitySubject.Bill, + ability: BillAction.View, }, { title: , @@ -53,11 +81,15 @@ export const accountsPayable = [ ), link: '/vendors', + subject: AbilitySubject.Vendor, + ability: VendorAction.View, }, { title: , description: , link: '/payment-mades', + subject: AbilitySubject.PaymentMade, + ability: PaymentMadeAction.View, }, ], }, @@ -77,21 +109,35 @@ export const financialAccounting = [ /> ), link: '/accounts', + subject: AbilitySubject.Account, + ability: AccountAction.View, }, { - title: , - description:, + title: , + description: ( + + ), link: '/manual-journals', + subject: AbilitySubject.ManualJournal, + ability: ManualJournalAction.View, }, { - title: , - description:, + title: , + description: ( + + ), link: '/expenses', + subject: AbilitySubject.Expense, + ability: ExpenseAction.View, }, { - title: , - description:, + title: , + description: ( + + ), link: '/financial-reports', + subject: AbilitySubject.Report, + ability: ReportsAction.ALL, }, ], }, @@ -102,19 +148,27 @@ export const productsServices = [ sectionTitle: , shortcuts: [ { - title: , - description:, + title: , + description: ( + + ), link: '/items', + subject: AbilitySubject.Item, + ability: ItemAction.View, }, { - title: , - description:, + title: , + description: , link: 'items/categories', }, { - title: , - description: , + title: , + description: ( + + ), link: '/inventory-adjustments', + subject: AbilitySubject.InventoryAdjustment, + ability: SaleInvoiceAction.View, }, ], }, diff --git a/src/common/keyboardShortcutsOptions.js b/src/common/keyboardShortcutsOptions.js index 9a904e09b..cd026dedf 100644 --- a/src/common/keyboardShortcutsOptions.js +++ b/src/common/keyboardShortcutsOptions.js @@ -1,110 +1,228 @@ -import React from 'react'; import intl from 'react-intl-universal'; +import { + AbilitySubject, + AccountAction, + BillAction, + CashflowAction, + CustomerAction, + ExpenseAction, + ItemAction, + ManualJournalAction, + ReportsAction, + SaleEstimateAction, + SaleInvoiceAction, + SaleReceiptAction, + VendorAction, +} from './abilityOption'; export default [ { shortcut_key: 'Shift + I', description: intl.get('jump_to_the_invoices'), + permission: { + ability: SaleInvoiceAction.View, + subject: AbilitySubject.Invoice, + }, }, { shortcut_key: 'Shift + E', description: intl.get('jump_to_the_estimates'), + permission: { + ability: SaleEstimateAction.View, + subject: AbilitySubject.Estimate, + }, }, { shortcut_key: 'Shift + R', description: intl.get('jump_to_the_receipts'), + permission: { + ability: SaleReceiptAction.View, + subject: AbilitySubject.Receipt, + }, }, { shortcut_key: 'Shift + X', description: intl.get('jump_to_the_expenses'), + permission: { + ability: ExpenseAction.View, + subject: AbilitySubject.Expense, + }, }, { shortcut_key: 'Shift + C', description: intl.get('jump_to_the_customers'), + permission: { + ability: CustomerAction.View, + subject: AbilitySubject.Customer, + }, }, { shortcut_key: 'Shift + V', description: intl.get('jump_to_the_vendors'), + permission: { + ability: VendorAction.View, + subject: AbilitySubject.Vendor, + }, }, { shortcut_key: 'Shift + A', description: intl.get('jump_to_the_chart_of_accounts'), + permission: { + ability: AccountAction.View, + subject: AbilitySubject.Account, + }, }, { shortcut_key: 'Shift + B', description: intl.get('jump_to_the_bills'), + permission: { + ability: BillAction.View, + subject: AbilitySubject.Bill, + }, }, { shortcut_key: 'Shift + M', description: intl.get('jump_to_the_manual_journals'), + permission: { + ability: ManualJournalAction.View, + subject: AbilitySubject.ManualJournal, + }, }, { shortcut_key: 'Shift + W', description: intl.get('jump_to_the_items'), + permission: { + ability: ItemAction.View, + subject: AbilitySubject.Item, + }, }, { shortcut_key: 'Shift + D', description: intl.get('jump_to_the_add_money_in'), + permission: { + ability: CashflowAction.Create, + subject: AbilitySubject.Cashflow, + }, }, { shortcut_key: 'Shift + Q', description: intl.get('jump_to_the_add_money_out'), + permission: { + ability: CashflowAction.Create, + subject: AbilitySubject.Cashflow, + }, }, { shortcut_key: 'Shift + 1', description: intl.get('jump_to_the_balance_sheet'), + permission: { + ability: ReportsAction.READ_BALANCE_SHEET, + subject: AbilitySubject.Report, + }, }, { shortcut_key: 'Shift + 2', description: intl.get('jump_to_the_profit_loss_sheet'), + permission: { + ability: ReportsAction.READ_PROFIT_LOSS, + subject: AbilitySubject.Report, + }, }, { shortcut_key: 'Shift + 3', description: intl.get('jump_to_the_journal_sheet'), + permission: { + ability: ReportsAction.READ_JOURNAL, + subject: AbilitySubject.Report, + }, }, { shortcut_key: 'Shift + 4', description: intl.get('jump_to_the_general_ledger_sheet'), + permission: { + ability: ReportsAction.READ_GENERAL_LEDGET, + subject: AbilitySubject.Report, + }, }, { shortcut_key: 'Shift + 5', description: intl.get('jump_to_the_trial_balance_sheet'), + permission: { + ability: ReportsAction.READ_TRIAL_BALANCE_SHEET, + subject: AbilitySubject.Report, + }, }, { shortcut_key: 'Ctrl + Shift + I ', description: intl.get('create_a_new_invoice'), + permission: { + ability: SaleInvoiceAction.Create, + subject: AbilitySubject.Invoice, + }, }, { shortcut_key: 'Ctrl + Shift + E ', description: intl.get('create_a_new_estimate'), + permission: { + ability: SaleEstimateAction.Create, + subject: AbilitySubject.Estimate, + }, }, { shortcut_key: 'Ctrl + Shift + R ', description: intl.get('create_a_new_receipt'), + permission: { + ability: SaleReceiptAction.Create, + subject: AbilitySubject.Receipt, + }, }, { shortcut_key: 'Ctrl + Shift + X ', description: intl.get('create_a_new_expense'), + permission: { + ability: ExpenseAction.Create, + subject: AbilitySubject.Expense, + }, }, { shortcut_key: 'Ctrl + Shift + C ', description: intl.get('create_a_new_customer'), + permission: { + ability: CustomerAction.Create, + subject: AbilitySubject.Customer, + }, }, { shortcut_key: 'Ctrl + Shift + V ', description: intl.get('create_a_new_vendor'), + permission: { + ability: VendorAction.Create, + subject: AbilitySubject.Vendor, + }, }, { shortcut_key: 'Ctrl + Shift + B ', description: intl.get('create_a_new_bill'), + permission: { + ability: BillAction.Create, + subject: AbilitySubject.Bill, + }, }, { shortcut_key: 'Ctrl + Shift + M ', description: intl.get('create_a_new_journal'), + permission: { + ability: ManualJournalAction.Create, + subject: AbilitySubject.ManualJournal, + }, }, { shortcut_key: 'Ctrl + Shift + W ', description: intl.get('create_a_new_item'), + permission: { + ability: ItemAction.Create, + subject: AbilitySubject.Item, + }, }, { shortcut_key: 'Ctrl + / ', diff --git a/src/common/quickNewOptions.js b/src/common/quickNewOptions.js index 74b9fece8..ba4f9604b 100644 --- a/src/common/quickNewOptions.js +++ b/src/common/quickNewOptions.js @@ -1,10 +1,71 @@ import intl from 'react-intl-universal'; +import { + AbilitySubject, + SaleInvoiceAction, + CustomerAction, + VendorAction, + ManualJournalAction, + ExpenseAction, +} from '../common/abilityOption'; +import { useAbilitiesFilter } from '../hooks'; export const getQuickNewActions = () => [ - { path: 'invoices/new', name: intl.get('sale_invoice') }, - { path: 'bills/new', name: intl.get('purchase_invoice') }, - { path: 'make-journal-entry', name: intl.get('manual_journal') }, - { path: 'expenses/new', name: intl.get('expense') }, - { path: 'customers/new', name: intl.get('customer') }, - { path: 'vendors/new', name: intl.get('vendor') }, + { + path: 'invoices/new', + name: intl.get('sale_invoice'), + permission: { + subject: AbilitySubject.Invoice, + ability: SaleInvoiceAction.Create, + }, + }, + { + path: 'bills/new', + name: intl.get('purchase_invoice'), + permission: { + subject: AbilitySubject.Invoice, + ability: SaleInvoiceAction.Create, + }, + }, + { + path: 'make-journal-entry', + name: intl.get('manual_journal'), + permission: { + subject: AbilitySubject.ManualJournal, + ability: ManualJournalAction.Create, + }, + }, + { + path: 'expenses/new', + name: intl.get('expense'), + permission: { + subject: AbilitySubject.Expense, + ability: ExpenseAction.Create, + }, + }, + { + path: 'customers/new', + name: intl.get('customer'), + permission: { + subject: AbilitySubject.Customer, + ability: CustomerAction.Create, + }, + }, + { + path: 'vendors/new', + name: intl.get('vendor'), + permission: { + subject: AbilitySubject.Vendor, + ability: VendorAction.Vendor, + }, + }, ]; + +/** + * Retrieve the dashboard quick new menu items. + */ +export const useGetQuickNewMenu = () => { + const quickNewMenu = getQuickNewActions(); + const abilitiesFilter = useAbilitiesFilter(); + + return abilitiesFilter(quickNewMenu); +}; diff --git a/src/components/AccountsSelectList.js b/src/components/AccountsSelectList.js index 9ed337284..354b80bb1 100644 --- a/src/components/AccountsSelectList.js +++ b/src/components/AccountsSelectList.js @@ -3,6 +3,7 @@ import { MenuItem, Button } from '@blueprintjs/core'; import { Select } from '@blueprintjs/select'; import * as R from 'ramda'; import classNames from 'classnames'; +import intl from 'react-intl-universal' import { MenuItemNestedText, FormattedMessage as T } from 'components'; import { filterAccountsByQuery } from './utils'; @@ -16,7 +17,7 @@ const createNewItemRenderer = (query, active, handleClick) => { return ( diff --git a/src/components/AccountsSuggestField.js b/src/components/AccountsSuggestField.js index 55697a4fe..c28739947 100644 --- a/src/components/AccountsSuggestField.js +++ b/src/components/AccountsSuggestField.js @@ -18,7 +18,7 @@ const createNewItemRenderer = (query, active, handleClick) => { return ( diff --git a/src/components/Contacts/utils.js b/src/components/Contacts/utils.js index 59d0a4a8c..58b453981 100644 --- a/src/components/Contacts/utils.js +++ b/src/components/Contacts/utils.js @@ -1,4 +1,5 @@ import React from 'react'; +import intl from 'react-intl-universal'; import { MenuItem } from '@blueprintjs/core'; // Filter Contact List @@ -34,7 +35,7 @@ export const createNewItemRenderer = (query, active, handleClick) => { return ( - ); -} - -export const AuthenticatedUser = withAuthentication( - ({ authenticatedUserId }) => ({ - authenticatedUserId, - }), -)(AuthenticatedUserComponent); - -export const useAuthenticatedUser = () => - React.useContext(AuthenticatedUserContext); diff --git a/src/components/Dashboard/DashboardAbilityProvider.js b/src/components/Dashboard/DashboardAbilityProvider.js new file mode 100644 index 000000000..37de0ced2 --- /dev/null +++ b/src/components/Dashboard/DashboardAbilityProvider.js @@ -0,0 +1,25 @@ +import React from 'react'; +import { Ability } from '@casl/ability'; +import { createContextualCan } from '@casl/react'; +import { useDashboardMeta } from '../../hooks/query'; + +export const AbilityContext = React.createContext(); +export const Can = createContextualCan(AbilityContext.Consumer); + +/** + * Dashboard ability provider. + */ +export function DashboardAbilityProvider({ children }) { + const { + data: { abilities }, + } = useDashboardMeta(); + + // Ability instance. + const ability = new Ability(abilities); + + return ( + + {children} + + ); +} diff --git a/src/components/Dashboard/DashboardBoot.js b/src/components/Dashboard/DashboardBoot.js index 81eadb658..095a972cc 100644 --- a/src/components/Dashboard/DashboardBoot.js +++ b/src/components/Dashboard/DashboardBoot.js @@ -1,18 +1,52 @@ import React from 'react'; -import * as R from 'ramda'; - -import { useUser, useCurrentOrganization } from '../../hooks/query'; +import { + useAuthenticatedAccount, + useCurrentOrganization, + useDashboardMeta, +} from '../../hooks/query'; import { useSplashLoading } from '../../hooks/state'; import { useWatch, useWatchImmediate, useWhen } from '../../hooks'; - -import withAuthentication from '../../containers/Authentication/withAuthentication'; - import { setCookie, getCookie } from '../../utils'; /** - * Dashboard async booting. + * Dashboard meta async booting. */ -function DashboardBootJSX({ authenticatedUserId }) { +export function useDashboardMetaBoot() { + const { + data: dashboardMeta, + isLoading: isDashboardMetaLoading, + isSuccess: isDashboardMetaSuccess, + } = useDashboardMeta(); + + const [startLoading, stopLoading] = useSplashLoading(); + + useWatchImmediate((value) => { + value && startLoading(); + }, isDashboardMetaLoading); + + useWatchImmediate(() => { + isDashboardMetaSuccess && stopLoading(); + }, isDashboardMetaSuccess); + + return { + isLoading: isDashboardMetaLoading, + }; +} + +/** + * Dashboard async booting. + * @returns {{ isLoading: boolean }} + */ +export function useDashboardBoot() { + const { isLoading } = useDashboardMetaBoot(); + + return { isLoading }; +} + +/** + * Application async booting. + */ +export function useApplicationBoot() { // Fetches the current user's organization. const { isSuccess: isCurrentOrganizationSuccess, @@ -22,7 +56,7 @@ function DashboardBootJSX({ authenticatedUserId }) { // Authenticated user. const { isSuccess: isAuthUserSuccess, isLoading: isAuthUserLoading } = - useUser(authenticatedUserId); + useAuthenticatedAccount(); // Initial locale cookie value. const localeCookie = getCookie('locale'); @@ -86,11 +120,8 @@ function DashboardBootJSX({ authenticatedUserId }) { isBooted.current = true; }, ); - return null; -} -export const DashboardBoot = R.compose( - withAuthentication(({ authenticatedUserId }) => ({ - authenticatedUserId, - })), -)(DashboardBootJSX); + return { + isLoading: isOrgLoading || isAuthUserLoading, + }; +} diff --git a/src/components/Dashboard/DashboardProvider.js b/src/components/Dashboard/DashboardProvider.js index 5a5fce966..b72160cf4 100644 --- a/src/components/Dashboard/DashboardProvider.js +++ b/src/components/Dashboard/DashboardProvider.js @@ -1,8 +1,16 @@ import React from 'react'; +import { DashboardAbilityProvider } from '../../components'; +import { useDashboardBoot } from './DashboardBoot'; /** * Dashboard provider. */ export default function DashboardProvider({ children }) { - return children; + const { isLoading } = useDashboardBoot(); + + return ( + + {isLoading ? null : children} + + ); } diff --git a/src/components/Dashboard/PrivatePagesProvider.js b/src/components/Dashboard/PrivatePagesProvider.js index 1f7098817..2fb0eef6a 100644 --- a/src/components/Dashboard/PrivatePagesProvider.js +++ b/src/components/Dashboard/PrivatePagesProvider.js @@ -1,31 +1,15 @@ import React from 'react'; -import * as R from 'ramda'; -import { AuthenticatedUser } from './AuthenticatedUser'; -import { DashboardBoot } from '../../components'; - -import withDashboard from '../../containers/Dashboard/withDashboard'; +import { useApplicationBoot } from '../../components'; /** * Private pages provider. */ -function PrivatePagesProviderComponent({ - splashScreenCompleted, - +export function PrivatePagesProvider({ // #ownProps children, }) { - return ( - - + const { isLoading } = useApplicationBoot(); - {splashScreenCompleted ? children : null} - - ); + return {!isLoading ? children : null}; } - -export const PrivatePagesProvider = R.compose( - withDashboard(({ splashScreenCompleted }) => ({ - splashScreenCompleted, - })), -)(PrivatePagesProviderComponent); diff --git a/src/components/Dashboard/TopbarUser.js b/src/components/Dashboard/TopbarUser.js index 80c0f0adb..4e97ce696 100644 --- a/src/components/Dashboard/TopbarUser.js +++ b/src/components/Dashboard/TopbarUser.js @@ -14,10 +14,14 @@ import { firstLettersArgs } from 'utils'; import { useAuthActions } from 'hooks/state'; import withDialogActions from 'containers/Dialog/withDialogActions'; -import { compose } from 'utils'; import withSubscriptions from '../../containers/Subscriptions/withSubscriptions'; -import { useAuthenticatedUser } from './AuthenticatedUser'; +import { useAuthenticatedAccount } from 'hooks/query' +import { compose } from 'utils'; + +/** + * Dashboard topbar user. + */ function DashboardTopbarUser({ openDialog, @@ -28,7 +32,7 @@ function DashboardTopbarUser({ const { setLogout } = useAuthActions(); // Retrieve authenticated user information. - const { user } = useAuthenticatedUser(); + const { data: user } = useAuthenticatedAccount(); const onClickLogout = () => { setLogout(); diff --git a/src/components/Dashboard/index.js b/src/components/Dashboard/index.js index fb97e3182..6da46b013 100644 --- a/src/components/Dashboard/index.js +++ b/src/components/Dashboard/index.js @@ -1,3 +1,4 @@ export * from './SplashScreen'; export * from './DashboardBoot'; export * from './DashboardThemeProvider'; +export * from './DashboardAbilityProvider'; \ No newline at end of file diff --git a/src/components/DialogsContainer.js b/src/components/DialogsContainer.js index db9f883c2..871e38a58 100644 --- a/src/components/DialogsContainer.js +++ b/src/components/DialogsContainer.js @@ -23,8 +23,9 @@ import BadDebtDialog from '../containers/Dialogs/BadDebtDialog'; import NotifyInvoiceViaSMSDialog from '../containers/Dialogs/NotifyInvoiceViaSMSDialog'; import NotifyReceiptViaSMSDialog from '../containers/Dialogs/NotifyReceiptViaSMSDialog'; import NotifyEstimateViaSMSDialog from '../containers/Dialogs/NotifyEstimateViaSMSDialog'; -import NotifyPaymentReceiveViaSMSDialog from '../containers/Dialogs/NotifyPaymentReceiveViaSMSDialog' +import NotifyPaymentReceiveViaSMSDialog from '../containers/Dialogs/NotifyPaymentReceiveViaSMSDialog'; import SMSMessageDialog from '../containers/Dialogs/SMSMessageDialog'; +import TransactionsLockingDialog from '../containers/Dialogs/TransactionsLockingDialog'; /** * Dialogs container. @@ -58,6 +59,7 @@ export default function DialogsContainer() { + ); } diff --git a/src/components/ItemsSuggestField.js b/src/components/ItemsSuggestField.js index 7473a6bb5..346784661 100644 --- a/src/components/ItemsSuggestField.js +++ b/src/components/ItemsSuggestField.js @@ -3,7 +3,7 @@ import { MenuItem } from '@blueprintjs/core'; import { Suggest } from '@blueprintjs/select'; import classNames from 'classnames'; import * as R from 'ramda'; - +import intl from 'react-intl-universal'; import { CLASSES } from 'common/classes'; import { FormattedMessage as T } from 'components'; @@ -24,7 +24,7 @@ const createNewItemRenderer = (query, active, handleClick) => { return (
- +
diff --git a/src/components/Sidebar/SidebarHead.js b/src/components/Sidebar/SidebarHead.js index bc5c11c22..25f6d390f 100644 --- a/src/components/Sidebar/SidebarHead.js +++ b/src/components/Sidebar/SidebarHead.js @@ -3,7 +3,7 @@ import { Button, Popover, Menu, Position } from '@blueprintjs/core'; import Icon from 'components/Icon'; import { compose, firstLettersArgs } from 'utils'; import withCurrentOrganization from '../../containers/Organization/withCurrentOrganization'; -import { useAuthenticatedUser } from '../Dashboard/AuthenticatedUser'; +import { useAuthenticatedAccount } from '../../hooks/query'; // Popover modifiers. const POPOVER_MODIFIERS = { @@ -18,7 +18,7 @@ function SidebarHead({ organization, }) { // Retrieve authenticated user information. - const { user } = useAuthenticatedUser(); + const { data: user } = useAuthenticatedAccount(); return (
diff --git a/src/components/Sidebar/SidebarMenu.js b/src/components/Sidebar/SidebarMenu.js index 56c667806..5a8d2bd82 100644 --- a/src/components/Sidebar/SidebarMenu.js +++ b/src/components/Sidebar/SidebarMenu.js @@ -1,7 +1,7 @@ import React from 'react'; import { Menu, MenuDivider } from '@blueprintjs/core'; import { useHistory, useLocation } from 'react-router-dom'; -import sidebarMenuList from 'config/sidebarMenu'; + import { Choose } from 'components'; import Icon from 'components/Icon'; import MenuItem from 'components/MenuItem'; @@ -24,7 +24,7 @@ function SidebarMenuItemSpace({ space }) { return
; } -function SidebarMenu({ isSubscriptionActive }) { +function SidebarMenu({ menu, isSubscriptionActive }) { const history = useHistory(); const location = useLocation(); @@ -93,7 +93,7 @@ function SidebarMenu({ isSubscriptionActive }) { }); }; - const filterItems = sidebarMenuList.filter( + const filterItems = menu.filter( (item) => isSubscriptionActive || item.enableBilling, ); const items = menuItemsMapper(filterItems); diff --git a/src/components/Sidebar/utils.js b/src/components/Sidebar/utils.js new file mode 100644 index 000000000..fcf056fac --- /dev/null +++ b/src/components/Sidebar/utils.js @@ -0,0 +1,48 @@ +import sidebarMenuList from 'config/sidebarMenu'; +import { isArray, isEmpty } from 'lodash'; +import { useAbilityContext } from 'hooks/utils'; + +export function useGetSidebarMenu() { + const ability = useAbilityContext(); + + return sidebarMenuList + .map((item) => { + const children = isArray(item.children) + ? item.children.filter((childItem) => { + return isArray(childItem.permission) + ? childItem.permission.some((perm) => + ability.can(perm.ability, perm.subject), + ) + : childItem?.permission?.ability && childItem?.permission?.subject + ? ability.can( + childItem.permission.ability, + childItem.permission.subject, + ) + : true; + }) + : []; + + return { + ...item, + ...(isArray(item.children) + ? { + children, + } + : {}), + }; + }) + .filter((item) => { + return isArray(item.permission) + ? item.permission.some((per) => + ability.can(per.ability, per.subject), + ) + : item?.permission?.ability && item?.permission?.subject + ? ability.can(item.permission.ability, item.permission.subject) + : true; + }) + .filter((item) => + isEmpty(item.children) && !item.href && !item.label && !item.divider + ? false + : true, + ); +} diff --git a/src/config/financialReportsMenu.js b/src/config/financialReportsMenu.js index 4c3226a4f..83e6ffd25 100644 --- a/src/config/financialReportsMenu.js +++ b/src/config/financialReportsMenu.js @@ -1,5 +1,6 @@ import React from 'react'; import { FormattedMessage as T } from 'components'; +import { ReportsAction, AbilitySubject } from '../common/abilityOption'; export const financialReportMenus = [ { @@ -11,6 +12,8 @@ export const financialReportMenus = [ ), link: '/financial-reports/balance-sheet', + subject: AbilitySubject.Report, + ability: ReportsAction.READ_BALANCE_SHEET, }, { title: , @@ -18,11 +21,15 @@ export const financialReportMenus = [ ), link: '/financial-reports/trial-balance-sheet', + subject: AbilitySubject.Report, + ability: ReportsAction.READ_TRIAL_BALANCE_SHEET, }, { title: , desc: , link: '/financial-reports/profit-loss-sheet', + subject: AbilitySubject.Report, + ability: ReportsAction.READ_PROFIT_LOSS, }, { title: , @@ -30,16 +37,22 @@ export const financialReportMenus = [ ), link: '/financial-reports/cash-flow', + subject: AbilitySubject.Report, + ability: ReportsAction.READ_CASHFLOW, }, { title: , desc: , link: '/financial-reports/journal-sheet', + subject: AbilitySubject.Report, + ability: ReportsAction.READ_JOURNAL, }, { title: , desc: , link: '/financial-reports/general-ledger', + subject: AbilitySubject.Report, + ability: ReportsAction.READ_GENERAL_LEDGET, }, { title: , @@ -47,11 +60,15 @@ export const financialReportMenus = [ ), link: '/financial-reports/receivable-aging-summary', + subject: AbilitySubject.Report, + ability: ReportsAction.READ_AR_AGING_SUMMARY, }, { title: , desc: , link: '/financial-reports/payable-aging-summary', + subject: AbilitySubject.Report, + ability: ReportsAction.READ_AP_AGING_SUMMARY, }, ], }, @@ -71,6 +88,8 @@ export const SalesAndPurchasesReportMenus = [ /> ), link: '/financial-reports/purchases-by-items', + subject: AbilitySubject.Report, + ability: ReportsAction.READ_PURCHASES_BY_ITEMS, }, { title: , @@ -82,6 +101,8 @@ export const SalesAndPurchasesReportMenus = [ /> ), link: '/financial-reports/sales-by-items', + subject: AbilitySubject.Report, + ability: ReportsAction.READ_SALES_BY_ITEMS, }, { title: , @@ -93,6 +114,8 @@ export const SalesAndPurchasesReportMenus = [ /> ), link: '/financial-reports/inventory-valuation', + subject: AbilitySubject.Report, + ability: ReportsAction.READ_INVENTORY_VALUATION_SUMMARY, }, { title: , @@ -104,6 +127,8 @@ export const SalesAndPurchasesReportMenus = [ /> ), link: '/financial-reports/customers-balance-summary', + subject: AbilitySubject.Report, + ability: ReportsAction.READ_CUSTOMERS_SUMMARY_BALANCE, }, { title: , @@ -111,6 +136,8 @@ export const SalesAndPurchasesReportMenus = [ ), link: '/financial-reports/vendors-balance-summary', + subject: AbilitySubject.Report, + ability: ReportsAction.READ_VENDORS_SUMMARY_BALANCE, }, { title: , @@ -120,6 +147,8 @@ export const SalesAndPurchasesReportMenus = [ /> ), link: '/financial-reports/transactions-by-customers', + subject: AbilitySubject.Report, + ability: ReportsAction.READ_CUSTOMERS_TRANSACTIONS, }, { title: , @@ -131,6 +160,8 @@ export const SalesAndPurchasesReportMenus = [ /> ), link: '/financial-reports/transactions-by-vendors', + subject: AbilitySubject.Report, + ability: ReportsAction.READ_VENDORS_TRANSACTIONS, }, { title: , @@ -138,6 +169,8 @@ export const SalesAndPurchasesReportMenus = [ ), link: '/financial-reports/inventory-item-details', + subject: AbilitySubject.Report, + ability: ReportsAction.READ_INVENTORY_ITEM_DETAILS, }, ], }, diff --git a/src/config/sidebarMenu.js b/src/config/sidebarMenu.js index ddd6799b3..a847f8e52 100644 --- a/src/config/sidebarMenu.js +++ b/src/config/sidebarMenu.js @@ -1,5 +1,26 @@ import React from 'react'; import { FormattedMessage as T } from 'components'; +import { + ReportsAction, + AbilitySubject, + ItemAction, + InventoryAdjustmentAction, + SaleEstimateAction, + SaleInvoiceAction, + SaleReceiptAction, + PaymentReceiveAction, + BillAction, + PaymentMadeAction, + CustomerAction, + VendorAction, + AccountAction, + ManualJournalAction, + ExpenseAction, + CashflowAction, + PreferencesAbility, + ExchangeRateAbility, + SubscriptionBillingAbility, +} from '../common/abilityOption'; export default [ { @@ -11,6 +32,32 @@ export default [ { text: , label: true, + permission: [ + { + subject: AbilitySubject.Item, + ability: ItemAction.View, + }, + { + subject: AbilitySubject.InventoryAdjustment, + ability: InventoryAdjustmentAction.View, + }, + { + subject: AbilitySubject.Estimate, + ability: SaleEstimateAction.View, + }, + { + subject: AbilitySubject.Invoice, + ability: SaleInvoiceAction.View, + }, + { + subject: AbilitySubject.Receipt, + ability: SaleReceiptAction.View, + }, + { + subject: AbilitySubject.PaymentReceive, + ability: PaymentReceiveAction.View, + }, + ], }, { text: , @@ -18,37 +65,70 @@ export default [ { text: , href: '/items', + permission: { + subject: AbilitySubject.Item, + ability: ItemAction.View, + }, }, { text: , href: '/inventory-adjustments', + permission: { + subject: AbilitySubject.InventoryAdjustment, + ability: InventoryAdjustmentAction.View, + }, }, { text: , href: '/items/categories', + permission: { + subject: AbilitySubject.Item, + ability: ItemAction.View, + }, }, { text: , label: true, + permission: [ + { + subject: AbilitySubject.Item, + ability: ItemAction.Create, + }, + ], }, { divider: true, + permission: [ + { + subject: AbilitySubject.Item, + ability: ItemAction.Create, + }, + ], }, { text: , href: '/items/new', + permission: { + subject: AbilitySubject.Item, + ability: ItemAction.Create, + }, }, { text: , href: '/items/new', + permission: { + subject: AbilitySubject.Item, + ability: ItemAction.Create, + }, }, { text: , href: '/items/categories/new', + permission: { + subject: AbilitySubject.Item, + ability: ItemAction.Create, + }, }, - // { - // text: , - // }, ], }, { @@ -57,43 +137,109 @@ export default [ { text: , href: '/estimates', - newTabHref: '/estimates/new', + permission: { + subject: AbilitySubject.Estimate, + ability: SaleEstimateAction.View, + }, }, { text: , href: '/invoices', - newTabHref: '/invoices/new', + permission: { + subject: AbilitySubject.Invoice, + ability: SaleInvoiceAction.View, + }, }, { text: , href: '/receipts', + permission: { + subject: AbilitySubject.Receipt, + ability: SaleReceiptAction.View, + }, }, { text: , href: '/payment-receives', + permission: { + subject: AbilitySubject.PaymentReceive, + ability: PaymentReceiveAction.View, + }, }, { text: , label: true, + permission: [ + { + subject: AbilitySubject.Estimate, + ability: SaleEstimateAction.Create, + }, + { + subject: AbilitySubject.Invoice, + ability: SaleInvoiceAction.Create, + }, + { + subject: AbilitySubject.Receipt, + ability: SaleReceiptAction.Create, + }, + { + subject: AbilitySubject.PaymentReceive, + ability: PaymentReceiveAction.Create, + }, + ], }, { divider: true, + permission: [ + { + subject: AbilitySubject.Estimate, + ability: SaleEstimateAction.Create, + }, + { + subject: AbilitySubject.Invoice, + ability: SaleInvoiceAction.Create, + }, + { + subject: AbilitySubject.Receipt, + ability: SaleReceiptAction.Create, + }, + { + subject: AbilitySubject.PaymentReceive, + ability: PaymentReceiveAction.Create, + }, + ], }, { text: , href: '/estimates/new', + permission: { + subject: AbilitySubject.Estimate, + ability: SaleEstimateAction.Create, + }, }, { text: , href: '/invoices/new', + permission: { + subject: AbilitySubject.Invoice, + ability: SaleInvoiceAction.Create, + }, }, { text: , href: '/receipts/new', + permission: { + subject: AbilitySubject.Receipt, + ability: SaleReceiptAction.Create, + }, }, { text: , href: '/payment-receives/new', + permission: { + subject: AbilitySubject.PaymentReceive, + ability: PaymentReceiveAction.Create, + }, }, ], }, @@ -103,27 +249,62 @@ export default [ { text: , href: '/bills', - newTabHref: '/bills/new', + permission: { + subject: AbilitySubject.Bill, + ability: BillAction.View, + }, }, { text: , href: '/payment-mades', newTabHref: '/payment-mades/new', + permission: { + subject: AbilitySubject.PaymentMade, + ability: PaymentMadeAction.View, + }, }, { text: , label: true, + permission: [ + { + subject: AbilitySubject.Bill, + ability: BillAction.Create, + }, + { + subject: AbilitySubject.PaymentMade, + ability: PaymentMadeAction.Create, + }, + ], }, { divider: true, + permission: [ + { + subject: AbilitySubject.Bill, + ability: BillAction.Create, + }, + { + subject: AbilitySubject.PaymentMade, + ability: PaymentMadeAction.Create, + }, + ], }, { text: , href: '/bills/new', + permission: { + subject: AbilitySubject.Bill, + ability: BillAction.Create, + }, }, { text: , href: '/payment-mades/new', + permission: { + subject: AbilitySubject.PaymentMade, + ability: PaymentMadeAction.Create, + }, }, ], }, @@ -133,33 +314,77 @@ export default [ { text: , href: '/customers', - newTabHref: '/customers/new', + permission: { + subject: AbilitySubject.Customer, + ability: CustomerAction.View, + }, }, { text: , href: '/vendors', - newTabHref: '/vendors/new', + permission: { + subject: AbilitySubject.Vendor, + ability: VendorAction.Create, + }, }, { text: , label: true, + permission: [ + { + subject: AbilitySubject.Customer, + ability: CustomerAction.View, + }, + { + subject: AbilitySubject.Vendor, + ability: VendorAction.View, + }, + ], }, { divider: true, + permission: [ + { + subject: AbilitySubject.Customer, + ability: CustomerAction.View, + }, + { + subject: AbilitySubject.Vendor, + ability: VendorAction.View, + }, + ], }, { text: , href: '/customers/new', + permission: { + subject: AbilitySubject.Customer, + ability: CustomerAction.View, + }, }, { text: , href: '/vendors/new', + permission: { + subject: AbilitySubject.Vendor, + ability: VendorAction.View, + }, }, ], }, { text: , label: true, + permission: [ + { + subject: AbilitySubject.Account, + ability: AccountAction.View, + }, + { + subject: AbilitySubject.ManualJournal, + ability: ManualJournalAction.View, + }, + ], }, { text: , @@ -167,25 +392,57 @@ export default [ { text: , href: '/accounts', + permission: { + subject: AbilitySubject.Account, + ability: AccountAction.View, + }, }, { text: , href: '/manual-journals', + permission: { + subject: AbilitySubject.ManualJournal, + ability: ManualJournalAction.View, + }, + }, + { + text: , + href: '/transactions-locking', + permission: { + subject: AbilitySubject.ManualJournal, + ability: ManualJournalAction.TransactionLocking, + }, }, { text: , href: '/exchange-rates', + permission: { + subject: AbilitySubject.ExchangeRate, + ability: ExchangeRateAbility.View, + }, }, { text: , label: true, + permission: { + subject: AbilitySubject.ManualJournal, + ability: ManualJournalAction.Create, + }, }, { divider: true, + permission: { + subject: AbilitySubject.ManualJournal, + ability: ManualJournalAction.Create, + }, }, { text: , href: '/make-journal-entry', + permission: { + subject: AbilitySubject.ManualJournal, + ability: ManualJournalAction.Create, + }, }, ], }, @@ -195,29 +452,61 @@ export default [ { text: , href: '/cashflow-accounts', + permission: { + subject: AbilitySubject.Cashflow, + ability: CashflowAction.View, + }, }, { text: , label: true, + permission: [ + { + subject: AbilitySubject.Cashflow, + ability: CashflowAction.Create, + }, + ], }, { divider: true, + permission: [ + { + subject: AbilitySubject.Cashflow, + ability: CashflowAction.Create, + }, + ], }, { text: , href: '/cashflow-accounts', + permission: { + subject: AbilitySubject.Cashflow, + ability: CashflowAction.Create, + }, }, { text: , href: '/cashflow-accounts', + permission: { + subject: AbilitySubject.Cashflow, + ability: CashflowAction.Create, + }, }, { text: , href: '/cashflow-accounts', + permission: { + subject: AbilitySubject.Cashflow, + ability: CashflowAction.Create, + }, }, { text: , href: '/cashflow-accounts', + permission: { + subject: AbilitySubject.Cashflow, + ability: CashflowAction.Create, + }, }, ], }, @@ -227,17 +516,33 @@ export default [ { text: , href: '/expenses', + permission: { + subject: AbilitySubject.Expense, + ability: ExpenseAction.View, + }, }, { text: , label: true, + permission: { + subject: AbilitySubject.Expense, + ability: ExpenseAction.Create, + }, }, { divider: true, + permission: { + subject: AbilitySubject.Expense, + ability: ExpenseAction.Create, + }, }, { text: , href: '/expenses/new', + permission: { + subject: AbilitySubject.Expense, + ability: ExpenseAction.Create, + }, }, ], }, @@ -247,80 +552,216 @@ export default [ { text: , href: '/financial-reports/balance-sheet', + permission: { + subject: AbilitySubject.Report, + ability: ReportsAction.READ_BALANCE_SHEET, + }, }, { text: , href: '/financial-reports/trial-balance-sheet', + permission: { + subject: AbilitySubject.Report, + ability: ReportsAction.READ_TRIAL_BALANCE_SHEET, + }, }, { text: , href: '/financial-reports/journal-sheet', + permission: { + subject: AbilitySubject.Report, + ability: ReportsAction.READ_JOURNAL, + }, }, { text: , href: '/financial-reports/general-ledger', + permission: { + subject: AbilitySubject.Report, + ability: ReportsAction.READ_GENERAL_LEDGET, + }, }, { text: , href: '/financial-reports/profit-loss-sheet', + permission: { + subject: AbilitySubject.Report, + ability: ReportsAction.READ_PROFIT_LOSS, + }, }, { text: , href: '/financial-reports/cash-flow', + permission: { + subject: AbilitySubject.Report, + ability: ReportsAction.READ_CASHFLOW_ACCOUNT_TRANSACTION, + }, }, { text: , href: '/financial-reports/receivable-aging-summary', + permission: { + subject: AbilitySubject.Report, + ability: ReportsAction.READ_AR_AGING_SUMMARY, + }, }, { text: , href: '/financial-reports/payable-aging-summary', + permission: { + subject: AbilitySubject.Report, + ability: ReportsAction.READ_AP_AGING_SUMMARY, + }, }, { text: , label: true, + permission: [ + { + subject: AbilitySubject.Report, + ability: ReportsAction.READ_PURCHASES_BY_ITEMS, + }, + { + subject: AbilitySubject.Report, + ability: ReportsAction.READ_SALES_BY_ITEMS, + }, + { + subject: AbilitySubject.Report, + ability: ReportsAction.READ_CUSTOMERS_TRANSACTIONS, + }, + { + subject: AbilitySubject.Report, + ability: ReportsAction.READ_VENDORS_TRANSACTIONS, + }, + { + subject: AbilitySubject.Report, + ability: ReportsAction.READ_CUSTOMERS_SUMMARY_BALANCE, + }, + { + subject: AbilitySubject.Report, + ability: ReportsAction.READ_VENDORS_SUMMARY_BALANCE, + }, + ], }, { divider: true, + permission: [ + { + subject: AbilitySubject.Report, + ability: ReportsAction.READ_PURCHASES_BY_ITEMS, + }, + { + subject: AbilitySubject.Report, + ability: ReportsAction.READ_SALES_BY_ITEMS, + }, + { + subject: AbilitySubject.Report, + ability: ReportsAction.READ_CUSTOMERS_TRANSACTIONS, + }, + { + subject: AbilitySubject.Report, + ability: ReportsAction.READ_VENDORS_TRANSACTIONS, + }, + { + subject: AbilitySubject.Report, + ability: ReportsAction.READ_CUSTOMERS_SUMMARY_BALANCE, + }, + { + subject: AbilitySubject.Report, + ability: ReportsAction.READ_VENDORS_SUMMARY_BALANCE, + }, + ], }, { text: , href: '/financial-reports/purchases-by-items', + permission: { + subject: AbilitySubject.Report, + ability: ReportsAction.READ_PURCHASES_BY_ITEMS, + }, }, { text: , href: '/financial-reports/sales-by-items', + permission: { + subject: AbilitySubject.Report, + ability: ReportsAction.READ_SALES_BY_ITEMS, + }, }, { text: , href: '/financial-reports/transactions-by-customers', + permission: { + subject: AbilitySubject.Report, + ability: ReportsAction.READ_CUSTOMERS_TRANSACTIONS, + }, }, { text: , href: '/financial-reports/transactions-by-vendors', + permission: { + subject: AbilitySubject.Report, + ability: ReportsAction.READ_VENDORS_TRANSACTIONS, + }, }, { text: , href: '/financial-reports/customers-balance-summary', + permission: { + subject: AbilitySubject.Report, + ability: ReportsAction.READ_CUSTOMERS_SUMMARY_BALANCE, + }, }, { text: , href: '/financial-reports/vendors-balance-summary', + permission: { + subject: AbilitySubject.Report, + ability: ReportsAction.READ_VENDORS_SUMMARY_BALANCE, + }, }, { text: , label: true, + permission: [ + { + subject: AbilitySubject.Report, + ability: ReportsAction.READ_INVENTORY_ITEM_DETAILS, + }, + { + subject: AbilitySubject.Report, + ability: ReportsAction.READ_INVENTORY_VALUATION_SUMMARY, + }, + ], }, { divider: true, + permission: [ + { + subject: AbilitySubject.Report, + ability: ReportsAction.READ_INVENTORY_ITEM_DETAILS, + }, + { + subject: AbilitySubject.Report, + ability: ReportsAction.READ_INVENTORY_VALUATION_SUMMARY, + }, + ], }, { text: , href: '/financial-reports/inventory-item-details', + permission: { + subject: AbilitySubject.Report, + ability: ReportsAction.READ_INVENTORY_ITEM_DETAILS, + }, }, { text: , href: '/financial-reports/inventory-valuation', + permission: { + subject: AbilitySubject.Report, + ability: ReportsAction.READ_INVENTORY_VALUATION_SUMMARY, + }, }, ], }, @@ -328,14 +769,32 @@ export default [ text: , enableBilling: true, label: true, + permission: [ + { + subject: AbilitySubject.Preferences, + ability: PreferencesAbility.Mutate, + }, + { + subject: AbilitySubject.SubscriptionBilling, + ability: SubscriptionBillingAbility.View, + }, + ], }, { text: , href: '/preferences', + permission: { + subject: AbilitySubject.Preferences, + ability: PreferencesAbility.Mutate, + }, }, { text: , href: '/billing', enableBilling: true, + permission: { + subject: AbilitySubject.SubscriptionBilling, + ability: SubscriptionBillingAbility.View, + }, }, ]; diff --git a/src/containers/Accounting/JournalsLanding/ManualJournalActionsBar.js b/src/containers/Accounting/JournalsLanding/ManualJournalActionsBar.js index a3f65be13..b90238018 100644 --- a/src/containers/Accounting/JournalsLanding/ManualJournalActionsBar.js +++ b/src/containers/Accounting/JournalsLanding/ManualJournalActionsBar.js @@ -26,8 +26,11 @@ import withManualJournals from './withManualJournals'; import withSettingsActions from '../../Settings/withSettingsActions'; import withSettings from '../../Settings/withSettings'; -import { If, DashboardActionViewsList } from 'components'; - +import { Can, If, DashboardActionViewsList } from 'components'; +import { + ManualJournalAction, + AbilitySubject, +} from '../../../common/abilityOption'; import { compose } from 'utils'; /** @@ -86,13 +89,14 @@ function ManualJournalActionsBar({ onChange={handleTabChange} /> - - + + - + + } /> diff --git a/src/containers/Accounting/JournalsLanding/components.js b/src/containers/Accounting/JournalsLanding/components.js index af986e522..fab711e72 100644 --- a/src/containers/Accounting/JournalsLanding/components.js +++ b/src/containers/Accounting/JournalsLanding/components.js @@ -13,7 +13,18 @@ import { } from '@blueprintjs/core'; import intl from 'react-intl-universal'; -import { FormattedMessage as T, Choose, Money, If, Icon } from 'components'; +import { + Can, + FormattedMessage as T, + Choose, + Money, + If, + Icon, +} from 'components'; +import { + ManualJournalAction, + AbilitySubject, +} from '../../../common/abilityOption'; import { safeCallback } from 'utils'; /** @@ -150,25 +161,31 @@ export const ActionsMenu = ({ text={intl.get('view_details')} onClick={safeCallback(onViewDetails, original)} /> - - + + + + } + text={intl.get('publish_journal')} + onClick={safeCallback(onPublish, original)} + /> + + + } - text={intl.get('publish_journal')} - onClick={safeCallback(onPublish, original)} + icon={} + text={intl.get('edit_journal')} + onClick={safeCallback(onEdit, original)} /> - - } - text={intl.get('edit_journal')} - onClick={safeCallback(onEdit, original)} - /> - } - intent={Intent.DANGER} - onClick={safeCallback(onDelete, original)} - /> + + + } + intent={Intent.DANGER} + onClick={safeCallback(onDelete, original)} + /> + ); }; diff --git a/src/containers/Accounts/AccountsActionsBar.js b/src/containers/Accounts/AccountsActionsBar.js index e96d69c8a..d77f309fc 100644 --- a/src/containers/Accounts/AccountsActionsBar.js +++ b/src/containers/Accounts/AccountsActionsBar.js @@ -15,6 +15,7 @@ import { FormattedMessage as T } from 'components'; import { AdvancedFilterPopover, If, + Can, DashboardActionViewsList, DashboardFilterButton, DashboardRowsHeightButton, @@ -30,6 +31,8 @@ import withAlertActions from 'containers/Alert/withAlertActions'; import withAccountsTableActions from './withAccountsTableActions'; import withSettings from '../Settings/withSettings'; import withSettingsActions from '../Settings/withSettingsActions'; +import { AccountAction, AbilitySubject } from '../../common/abilityOption'; + import { compose } from 'utils'; /** @@ -116,13 +119,14 @@ function AccountsActionsBar({ onChange={handleTabChange} /> - - + + - + + } /> diff --git a/src/containers/Customers/CustomersLanding/components.js b/src/containers/Customers/CustomersLanding/components.js index 0cce24139..9d878bbf4 100644 --- a/src/containers/Customers/CustomersLanding/components.js +++ b/src/containers/Customers/CustomersLanding/components.js @@ -4,7 +4,8 @@ import clsx from 'classnames'; import intl from 'react-intl-universal'; -import { Icon, Money, If, AvaterCell } from 'components'; +import { Can, Icon, Money, If, AvaterCell } from 'components'; +import { CustomerAction, AbilitySubject } from '../../../common/abilityOption'; import { safeCallback } from 'utils'; @@ -29,37 +30,46 @@ export function ActionsMenu({ text={intl.get('view_details')} onClick={safeCallback(onViewDetails, original)} /> - - } - text={intl.get('edit_customer')} - onClick={safeCallback(onEdit, original)} - /> - } - text={intl.get('duplicate')} - onClick={safeCallback(onDuplicate, original)} - /> - + + + } - onClick={safeCallback(onInactivate, original)} + icon={} + text={intl.get('edit_customer')} + onClick={safeCallback(onEdit, original)} /> - - + + } - onClick={safeCallback(onActivate, original)} + icon={} + text={intl.get('duplicate')} + onClick={safeCallback(onDuplicate, original)} /> - - } - text={intl.get('delete_customer')} - intent={Intent.DANGER} - onClick={safeCallback(onDelete, original)} - /> + + + + } + onClick={safeCallback(onInactivate, original)} + /> + + + } + onClick={safeCallback(onActivate, original)} + /> + + + + } + text={intl.get('delete_customer')} + intent={Intent.DANGER} + onClick={safeCallback(onDelete, original)} + /> + ); } diff --git a/src/containers/Dialogs/InviteUserDialog/InviteUserDialog.schema.js b/src/containers/Dialogs/InviteUserDialog/InviteUserDialog.schema.js index ee0c34702..057f75e25 100644 --- a/src/containers/Dialogs/InviteUserDialog/InviteUserDialog.schema.js +++ b/src/containers/Dialogs/InviteUserDialog/InviteUserDialog.schema.js @@ -2,10 +2,8 @@ import * as Yup from 'yup'; import intl from 'react-intl-universal'; const Schema = Yup.object().shape({ - email: Yup.string() - .email() - .required() - .label(intl.get('email')), + email: Yup.string().email().required().label(intl.get('email')), + role_id: Yup.string().required().label(intl.get('roles.label.role_name_')), }); -export const InviteUserFormSchema = Schema; \ No newline at end of file +export const InviteUserFormSchema = Schema; diff --git a/src/containers/Dialogs/InviteUserDialog/InviteUserFormContent.js b/src/containers/Dialogs/InviteUserDialog/InviteUserFormContent.js index bcc989cea..c66fe492f 100644 --- a/src/containers/Dialogs/InviteUserDialog/InviteUserFormContent.js +++ b/src/containers/Dialogs/InviteUserDialog/InviteUserFormContent.js @@ -1,7 +1,11 @@ import React from 'react'; import { FormGroup, InputGroup, Intent, Button } from '@blueprintjs/core'; import { FastField, Form, useFormikContext, ErrorMessage } from 'formik'; -import { FormattedMessage as T } from 'components'; +import { + ListSelect, + FieldRequiredHint, + FormattedMessage as T, +} from 'components'; import { CLASSES } from 'common/classes'; import classNames from 'classnames'; import { inputIntent } from 'utils'; @@ -15,23 +19,24 @@ function InviteUserFormContent({ closeDialog, }) { const { isSubmitting } = useFormikContext(); - const { isEditMode, dialogName } = useInviteUserFormContext(); + const { isEditMode, dialogName, roles } = useInviteUserFormContext(); const handleClose = () => { closeDialog(dialogName); }; - + console.log(roles, 'XX'); return (

- + {/* ----------- Email ----------- */} {({ field, meta: { error, touched } }) => ( } + labelInfo={} className={classNames('form-group--email', CLASSES.FILL)} intent={inputIntent({ error, touched })} helperText={} @@ -40,6 +45,31 @@ function InviteUserFormContent({ )} + {/* ----------- Role name ----------- */} + + {({ form, field: { value }, meta: { error, touched } }) => ( + } + labelInfo={} + helperText={} + className={classNames(CLASSES.FILL, 'form-group--role_name')} + intent={inputIntent({ error, touched })} + > + { + form.setFieldValue('role_id', id); + }} + selectedItem={value} + selectedItemProp={'id'} + textProp={'name'} + // labelProp={'id '} + popoverProps={{ minimal: true }} + intent={inputIntent({ error, touched })} + /> + + )} +
diff --git a/src/containers/Dialogs/InviteUserDialog/InviteUserFormProvider.js b/src/containers/Dialogs/InviteUserDialog/InviteUserFormProvider.js index aa093c209..dcaaad4cb 100644 --- a/src/containers/Dialogs/InviteUserDialog/InviteUserFormProvider.js +++ b/src/containers/Dialogs/InviteUserDialog/InviteUserFormProvider.js @@ -1,5 +1,5 @@ import React, { createContext } from 'react'; -import { useCreateInviteUser, useUsers } from 'hooks/query'; +import { useCreateInviteUser, useUsers, useRoles } from 'hooks/query'; import { DialogContent } from 'components'; const InviteUserFormContext = createContext(); @@ -14,6 +14,9 @@ function InviteUserFormProvider({ userId, isEditMode, dialogName, ...props }) { // fetch users list. const { isLoading: isUsersLoading } = useUsers(); + // fetch roles list. + const { data: roles, isLoading: isRolesLoading } = useRoles(); + // Provider state. const provider = { inviteUserMutate, @@ -21,10 +24,14 @@ function InviteUserFormProvider({ userId, isEditMode, dialogName, ...props }) { userId, isUsersLoading, isEditMode, + roles, }; return ( - + ); diff --git a/src/containers/Dialogs/TransactionsLockingDialog/TransactionsLockingDialogContent.js b/src/containers/Dialogs/TransactionsLockingDialog/TransactionsLockingDialogContent.js new file mode 100644 index 000000000..28d763962 --- /dev/null +++ b/src/containers/Dialogs/TransactionsLockingDialog/TransactionsLockingDialogContent.js @@ -0,0 +1,14 @@ +import React from 'react'; +import { TransactionsLockingFormProvider } from './TransactionsLockingFormProvider'; +import TransactionsLockingForm from './TransactionsLockingForm'; + +export default function TransactionsLockingDialogContent({ + // #ownProps + dialogName, +}) { + return ( + + + + ); +} diff --git a/src/containers/Dialogs/TransactionsLockingDialog/TransactionsLockingFloatingActions.js b/src/containers/Dialogs/TransactionsLockingDialog/TransactionsLockingFloatingActions.js new file mode 100644 index 000000000..51c29dedc --- /dev/null +++ b/src/containers/Dialogs/TransactionsLockingDialog/TransactionsLockingFloatingActions.js @@ -0,0 +1,50 @@ +import React from 'react'; +import { Intent, Button, Classes } from '@blueprintjs/core'; +import { useFormikContext } from 'formik'; +import { FormattedMessage as T } from 'components'; + +import { useTransactionLockingContext } from './TransactionsLockingFormProvider'; +import withDialogActions from 'containers/Dialog/withDialogActions'; +import { compose } from 'utils'; + +/** + * Transactions locking floating actions. + */ +function TransactionsLockingFloatingActions({ + // #withDialogActions + closeDialog, +}) { + // Formik context. + const { isSubmitting } = useFormikContext(); + + const { dialogName } = useTransactionLockingContext(); + + // Handle cancel button click. + const handleCancelBtnClick = (event) => { + closeDialog(dialogName); + }; + + return ( +
+
+ + +
+
+ ); +} + +export default compose(withDialogActions)(TransactionsLockingFloatingActions); diff --git a/src/containers/Dialogs/TransactionsLockingDialog/TransactionsLockingForm.js b/src/containers/Dialogs/TransactionsLockingDialog/TransactionsLockingForm.js new file mode 100644 index 000000000..9bba5437a --- /dev/null +++ b/src/containers/Dialogs/TransactionsLockingDialog/TransactionsLockingForm.js @@ -0,0 +1,48 @@ +import React from 'react'; +import moment from 'moment'; +import { Intent } from '@blueprintjs/core'; +import { Formik } from 'formik'; +import intl from 'react-intl-universal'; + +import '../../../style/pages/TransactionsLocking/TransactionsLockingDialog.scss' + +import { AppToaster } from 'components'; +import { CreateTransactionsLockingFormSchema } from './TransactionsLockingForm.schema'; + +import { useTransactionLockingContext } from './TransactionsLockingFormProvider'; +import TransactionsLockingFormContent from './TransactionsLockingFormContent'; + +import withDialogActions from 'containers/Dialog/withDialogActions'; +import { compose } from 'utils'; + +const defaultInitialValues = { + date: moment(new Date()).format('YYYY-MM-DD'), + reason: '', +}; + +/** + * Transactions Locking From. + */ +function TransactionsLockingForm({ + // #withDialogActions + closeDialog, +}) { + const { dialogName } = useTransactionLockingContext(); + // Initial form values. + const initialValues = { + ...defaultInitialValues, + }; + + // Handles the form submit. + const handleFormSubmit = (values, { setSubmitting, setErrors }) => {}; + + return ( + + ); +} +export default compose(withDialogActions)(TransactionsLockingForm); diff --git a/src/containers/Dialogs/TransactionsLockingDialog/TransactionsLockingForm.schema.js b/src/containers/Dialogs/TransactionsLockingDialog/TransactionsLockingForm.schema.js new file mode 100644 index 000000000..a74058f3f --- /dev/null +++ b/src/containers/Dialogs/TransactionsLockingDialog/TransactionsLockingForm.schema.js @@ -0,0 +1,13 @@ +import * as Yup from 'yup'; +import intl from 'react-intl-universal'; +import { DATATYPES_LENGTH } from 'common/dataTypes'; + +const Schema = Yup.object().shape({ + date: Yup.date().required().label(intl.get('date')), + reason: Yup.string() + .required() + .min(3) + .max(DATATYPES_LENGTH.TEXT) + .label(intl.get('reason')), +}); +export const CreateTransactionsLockingFormSchema = Schema; diff --git a/src/containers/Dialogs/TransactionsLockingDialog/TransactionsLockingFormContent.js b/src/containers/Dialogs/TransactionsLockingDialog/TransactionsLockingFormContent.js new file mode 100644 index 000000000..43a1e1dda --- /dev/null +++ b/src/containers/Dialogs/TransactionsLockingDialog/TransactionsLockingFormContent.js @@ -0,0 +1,17 @@ +import React from 'react'; +import { Form } from 'formik'; + +import TransactionsLockingFormFields from './TransactionsLockingFormFields'; +import TransactionsLockingFloatingActions from './TransactionsLockingFloatingActions'; + +/** + * Transactions locking form content. + */ +export default function TransactionsLockingFormContent() { + return ( + + + + + ); +} diff --git a/src/containers/Dialogs/TransactionsLockingDialog/TransactionsLockingFormFields.js b/src/containers/Dialogs/TransactionsLockingDialog/TransactionsLockingFormFields.js new file mode 100644 index 000000000..bede9d2fc --- /dev/null +++ b/src/containers/Dialogs/TransactionsLockingDialog/TransactionsLockingFormFields.js @@ -0,0 +1,72 @@ +import React from 'react'; +import { FastField, ErrorMessage } from 'formik'; +import { Classes, FormGroup, TextArea, Position } from '@blueprintjs/core'; +import { DateInput } from '@blueprintjs/datetime'; +import classNames from 'classnames'; +import { CLASSES } from 'common/classes'; +import { FieldRequiredHint, FormattedMessage as T } from 'components'; +import { useAutofocus } from 'hooks'; +import { + inputIntent, + momentFormatter, + tansformDateValue, + handleDateChange, +} from 'utils'; + +/** + * Transactions locking form fields. + */ +export default function TransactionsLockingFormFields() { + const dateFieldRef = useAutofocus(); + + return ( +
+ {/*------------ Date -----------*/} + + {({ form, field: { value }, meta: { error, touched } }) => ( + } + labelInfo={} + intent={inputIntent({ error, touched })} + helperText={} + minimal={true} + className={classNames(CLASSES.FILL, 'form-group--date')} + > + { + form.setFieldValue('date', formattedDate); + })} + value={tansformDateValue(value)} + popoverProps={{ + position: Position.BOTTOM, + minimal: true, + }} + intent={inputIntent({ error, touched })} + inputRef={(ref) => (dateFieldRef.current = ref)} + /> + + )} + + {/*------------ reasons -----------*/} + + {({ field, meta: { error, touched } }) => ( + } + labelInfo={} + className={'form-group--reason'} + intent={inputIntent({ error, touched })} + helperText={} + > +