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/components/Abilities.js b/src/components/Abilities.js new file mode 100644 index 000000000..152ea01b0 --- /dev/null +++ b/src/components/Abilities.js @@ -0,0 +1,10 @@ +import { AbilityBuilder, defineAbility } from '@casl/ability'; +import { createContextualCan } from '@casl/react'; +import { createContext } from 'react'; + +export const AbilityContext = createContext(); +export const Can = createContextualCan(AbilityContext.Consumer); + +export const ability = defineAbility((can, cannot) => { + cannot('Item', 'create'); +}); diff --git a/src/components/Can.js b/src/components/Can.js new file mode 100644 index 000000000..9ba7fe434 --- /dev/null +++ b/src/components/Can.js @@ -0,0 +1,4 @@ +import { createCanBoundTo } from '@casl/react'; +import ability from '../components/Config/ability'; + +export default createCanBoundTo(ability); \ No newline at end of file diff --git a/src/components/Config/ability.js b/src/components/Config/ability.js new file mode 100644 index 000000000..72342959f --- /dev/null +++ b/src/components/Config/ability.js @@ -0,0 +1,10 @@ +import { AbilityBuilder } from '@casl/ability'; +// import { AbilitySubject, ItemAbility } from '../../common/abilityOption'; + +export function defineAbilitiesFor(role) { + const { rules, can } = new AbilityBuilder(); + + can('create', 'Item'); + + return new Ability(rules); +} 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/config/sidebarMenu.js b/src/config/sidebarMenu.js index ddd6799b3..bb02d9100 100644 --- a/src/config/sidebarMenu.js +++ b/src/config/sidebarMenu.js @@ -172,6 +172,10 @@ export default [ text: , href: '/manual-journals', }, + { + text: , + href: '/transactions-locking', + }, { text: , href: '/exchange-rates', diff --git a/src/containers/Alerts/Roles/RoleDeleteAlert.js b/src/containers/Alerts/Roles/RoleDeleteAlert.js new file mode 100644 index 000000000..c3b5e1946 --- /dev/null +++ b/src/containers/Alerts/Roles/RoleDeleteAlert.js @@ -0,0 +1,80 @@ +import React from 'react'; +import intl from 'react-intl-universal'; +import { FormattedMessage as T, FormattedHTMLMessage } from 'components'; +import { Intent, Alert } from '@blueprintjs/core'; +import { AppToaster } from 'components'; + +import { useDeleteRole } from 'hooks/query'; + +import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect'; +import withAlertActions from 'containers/Alert/withAlertActions'; + +import { compose } from 'utils'; + +/** + * Role delete alert. + */ +function RoleDeleteAlert({ + name, + + // #withAlertStoreConnect + isOpen, + payload: { roleId }, + + // #withAlertActions + closeAlert, +}) { + const { mutateAsync: deleteRole, isLoading } = useDeleteRole(); + + // Handle cancel delete role alert. + const handleCancelDelete = () => { + closeAlert(name); + }; + + // Handle confirm delete role. + const handleConfirmDeleteRole = () => { + deleteRole(roleId) + .then(() => { + AppToaster.show({ + message: intl.get('roles.permission_schema.delete.alert_message'), + intent: Intent.SUCCESS, + }); + }) + .catch( + ({ + response: { + data: { errors }, + }, + }) => {}, + ) + .finally(() => { + closeAlert(name); + }); + }; + + return ( + } + confirmButtonText={} + icon="trash" + intent={Intent.DANGER} + isOpen={isOpen} + onCancel={handleCancelDelete} + onConfirm={handleConfirmDeleteRole} + loading={isLoading} + > +

+ +

+
+ ); +} + +export default compose( + withAlertStoreConnect(), + withAlertActions, +)(RoleDeleteAlert); diff --git a/src/containers/AlertsContainer/registered.js b/src/containers/AlertsContainer/registered.js index 898c583d3..d0e77fcfb 100644 --- a/src/containers/AlertsContainer/registered.js +++ b/src/containers/AlertsContainer/registered.js @@ -16,6 +16,7 @@ import ExpensesAlerts from '../Expenses/ExpensesAlerts'; import AccountTransactionsAlerts from '../CashFlow/AccountTransactions/AccountTransactionsAlerts'; import UsersAlerts from '../Preferences/Users/UsersAlerts'; import CurrenciesAlerts from '../Preferences/Currencies/CurrenciesAlerts'; +import RolesAlerts from '../Preferences/Users/Roles/RolesAlerts'; export default [ ...AccountsAlerts, @@ -36,4 +37,5 @@ export default [ ...AccountTransactionsAlerts, ...UsersAlerts, ...CurrenciesAlerts, + ...RolesAlerts, ]; 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={} + > +