From 2feb4c2e88f8aabcec59467b58d2cf87f17415e3 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Wed, 2 Dec 2020 17:21:54 +0200 Subject: [PATCH] feat: application preferences. --- client/src/common/classes.js | 20 + client/src/common/countries.js | 5 + client/src/common/currencies.js | 5 + client/src/common/dateFormatsOptions.js | 40 ++ client/src/common/fiscalYearOptions.js | 88 +++ client/src/common/languagesOptions.js | 6 + client/src/components/Dashboard/Dashboard.js | 6 +- client/src/components/DialogsContainer.js | 8 +- client/src/components/ListSelect.js | 6 + .../Preferences/PreferencesContent.js | 17 - .../components/Preferences/PreferencesPage.js | 28 +- .../Preferences/PreferencesSidebar.js | 39 +- .../PreferencesSidebarContainer.js | 21 + .../Preferences/PreferencesTopbar.js | 17 +- client/src/config/preferencesMenu.js | 5 - .../AccountFormDialogContent.js | 6 +- .../AccountFormDialogFields.js | 4 +- .../containers/Dialogs/InviteUserDialog.js | 218 ------- .../InviteUserDialog.schema.js | 11 + .../InviteUserDialogContent.js | 97 +++ .../InviteUserDialog/InviteUserDialogForm.js | 56 ++ .../index.js | 5 +- .../Dialogs/InviteUserDialog/utils.js | 10 + .../UserFormDialog/UserFormDialogContent.js | 162 ----- .../Preferences/Accountant/Accountant.js | 60 +- .../Accountant/Accountant.schema.js | 10 + .../Preferences/Accountant/AccountantForm.js | 105 ++++ .../Preferences/Currencies/Currencies.js | 28 +- .../Currencies/CurrenciesDataTable.js | 53 +- .../Preferences/Currencies/CurrenciesList.js | 69 +-- .../containers/Preferences/General/General.js | 575 ++---------------- .../Preferences/General/General.schema.js | 34 ++ .../Preferences/General/GeneralForm.js | 250 ++++++++ .../src/containers/Preferences/Users/Users.js | 24 +- .../Preferences/Users/UsersActions.js | 10 +- .../Preferences/Users/UsersDataTable.js | 53 +- .../containers/Preferences/Users/UsersList.js | 105 ++-- client/src/lang/en/index.js | 1 + client/src/routes/preferences.js | 8 +- .../store/currencies/currencies.reducer.js | 1 + client/src/store/users/users.actions.js | 11 +- client/src/style/App.scss | 12 + client/src/style/pages/dashboard.scss | 15 +- client/src/style/pages/preferences.scss | 308 +++++++--- server/src/api/controllers/ItemCategories.ts | 2 +- 45 files changed, 1321 insertions(+), 1293 deletions(-) create mode 100644 client/src/common/countries.js create mode 100644 client/src/common/currencies.js create mode 100644 client/src/common/dateFormatsOptions.js create mode 100644 client/src/common/fiscalYearOptions.js create mode 100644 client/src/common/languagesOptions.js delete mode 100644 client/src/components/Preferences/PreferencesContent.js create mode 100644 client/src/components/Preferences/PreferencesSidebarContainer.js delete mode 100644 client/src/containers/Dialogs/InviteUserDialog.js create mode 100644 client/src/containers/Dialogs/InviteUserDialog/InviteUserDialog.schema.js create mode 100644 client/src/containers/Dialogs/InviteUserDialog/InviteUserDialogContent.js create mode 100644 client/src/containers/Dialogs/InviteUserDialog/InviteUserDialogForm.js rename client/src/containers/Dialogs/{UserFormDialog => InviteUserDialog}/index.js (85%) create mode 100644 client/src/containers/Dialogs/InviteUserDialog/utils.js delete mode 100644 client/src/containers/Dialogs/UserFormDialog/UserFormDialogContent.js create mode 100644 client/src/containers/Preferences/Accountant/Accountant.schema.js create mode 100644 client/src/containers/Preferences/Accountant/AccountantForm.js create mode 100644 client/src/containers/Preferences/General/General.schema.js create mode 100644 client/src/containers/Preferences/General/GeneralForm.js diff --git a/client/src/common/classes.js b/client/src/common/classes.js index e1102405b..17edd9724 100644 --- a/client/src/common/classes.js +++ b/client/src/common/classes.js @@ -10,6 +10,9 @@ const CLASSES = { DATATABLE_EDITOR_ITEMS_ENTRIES: 'items-entries-table', DATATABLE_EDITOR_HAS_TOTAL_ROW: 'has-total-row', + DASHBOARD_CONTENT: 'dashboard-content', + DASHBOARD_CONTENT_PREFERENCES: 'dashboard-content--preferences', + PAGE_FORM: 'page-form', PAGE_FORM_HEADER: 'page-form__header', PAGE_FORM_HEADER_PRIMARY: 'page-form__primary-section', @@ -46,7 +49,24 @@ const CLASSES = { SELECT_LIST_FILL_POPOVER: 'select-list--fill-popover', + + PREFERENCES_PAGE: 'preferences-page', + PREFERENCES_PAGE_SIDEBAR: 'preferences-page__sidebar', + PREFERENCES_PAGE_TOPBAR: 'preferences-page__topbar', + PREFERENCES_PAGE_CONTENT: 'preferences-page__content', + PREFERENCES_PAGE_TABS: 'preferences-page__tabs', + + PREFERENCES_SIDEBAR: 'preferences-sidebar', + PREFERENCES_TOPBAR: 'preferences-topbar', + + PREFERENCES_PAGE_INSIDE_CONTENT: 'preferences-page__inside-content', + PREFERENCES_PAGE_INSIDE_CONTENT_GENERAL: 'preferences-page__inside-content--general', + PREFERENCES_PAGE_INSIDE_CONTENT_USERS: 'preferences-page__inside-content--users', + PREFERENCES_PAGE_INSIDE_CONTENT_CURRENCIES: 'preferences-page__inside-content--currencies', + PREFERENCES_PAGE_INSIDE_CONTENT_ACCOUNTANT: 'preferences-page__inside-content--accountant', + ...Classes, + CARD: 'card', }; export { diff --git a/client/src/common/countries.js b/client/src/common/countries.js new file mode 100644 index 000000000..cf08fcf48 --- /dev/null +++ b/client/src/common/countries.js @@ -0,0 +1,5 @@ + + +export default [ + { name: 'Libya', value: 'libya' }, +] \ No newline at end of file diff --git a/client/src/common/currencies.js b/client/src/common/currencies.js new file mode 100644 index 000000000..c0c390bf2 --- /dev/null +++ b/client/src/common/currencies.js @@ -0,0 +1,5 @@ +export default [ + { label: 'US Dollar', code: 'USD' }, + { label: 'Euro', code: 'EUR' }, + { label: 'Libyan Dinar ', code: 'LYD' }, +] \ No newline at end of file diff --git a/client/src/common/dateFormatsOptions.js b/client/src/common/dateFormatsOptions.js new file mode 100644 index 000000000..bed2cda21 --- /dev/null +++ b/client/src/common/dateFormatsOptions.js @@ -0,0 +1,40 @@ +import moment from 'moment'; + +export default [ + { + id: 1, + name: 'MM/DD/YY', + label: `${moment().format('MM/DD/YYYY')}`, + value: 'mm/dd/yy', + }, + { + id: 2, + name: 'DD/MM/YY', + label: `${moment().format('DD/MM/YYYY')}`, + value: 'dd/mm/yy', + }, + { + id: 3, + name: 'YY/MM/DD', + label: `${moment().format('YYYY/MM/DD')}`, + value: 'yy/mm/dd', + }, + { + id: 4, + name: 'MM-DD-YY', + label: `${moment().format('MM-DD-YYYY')}`, + value: 'mm-dd-yy', + }, + { + id: 5, + name: 'DD-MM-YY', + label: `${moment().format('DD-MM-YYYY')}`, + value: 'dd-mm-yy', + }, + { + id: 6, + name: 'YY-MM-DD', + label: `${moment().format('YYYY-MM-DD')}`, + value: 'yy-mm-dd', + }, +] \ No newline at end of file diff --git a/client/src/common/fiscalYearOptions.js b/client/src/common/fiscalYearOptions.js new file mode 100644 index 000000000..1a89aabb7 --- /dev/null +++ b/client/src/common/fiscalYearOptions.js @@ -0,0 +1,88 @@ +import { formatMessage } from 'services/intl'; + +export default [ + { + id: 0, + name: `${formatMessage({ id: 'january' })} - ${formatMessage({ + id: 'december', + })}`, + value: 'january', + }, + { + id: 1, + name: `${formatMessage({ id: 'february' })} - ${formatMessage({ + id: 'january', + })}`, + value: 'february', + }, + { + id: 2, + name: `${formatMessage({ id: 'march' })} - ${formatMessage({ + id: 'february', + })}`, + value: 'March', + }, + { + id: 3, + name: `${formatMessage({ id: 'april' })} - ${formatMessage({ + id: 'march', + })}`, + value: 'april', + }, + { + id: 4, + name: `${formatMessage({ id: 'may' })} - ${formatMessage({ + id: 'april', + })}`, + value: 'may', + }, + { + id: 5, + name: `${formatMessage({ id: 'june' })} - ${formatMessage({ + id: 'may', + })}`, + value: 'june', + }, + { + id: 6, + name: `${formatMessage({ id: 'july' })} - ${formatMessage({ + id: 'june', + })}`, + value: 'july', + }, + { + id: 7, + name: `${formatMessage({ id: 'august' })} - ${formatMessage({ + id: 'july', + })}`, + value: 'August', + }, + { + id: 8, + name: `${formatMessage({ id: 'september' })} - ${formatMessage({ + id: 'august', + })}`, + value: 'september', + }, + { + id: 9, + name: `${formatMessage({ id: 'october' })} - ${formatMessage({ + id: 'november', + })}`, + value: 'october', + }, + { + id: 10, + name: `${formatMessage({ id: 'november' })} - ${formatMessage({ + id: 'october', + })}`, + value: 'november', + }, + { + id: 11, + name: `${formatMessage({ id: 'december' })} - ${formatMessage({ + id: 'november', + })}`, + value: 'december', + }, +] \ No newline at end of file diff --git a/client/src/common/languagesOptions.js b/client/src/common/languagesOptions.js new file mode 100644 index 000000000..3de03d718 --- /dev/null +++ b/client/src/common/languagesOptions.js @@ -0,0 +1,6 @@ + + +export default [ + { name: 'English', value: 'EN' }, + { name: 'Arabic', value: 'AR' }, +]; \ No newline at end of file diff --git a/client/src/components/Dashboard/Dashboard.js b/client/src/components/Dashboard/Dashboard.js index dbfcdc9d9..c4fdb02b6 100644 --- a/client/src/components/Dashboard/Dashboard.js +++ b/client/src/components/Dashboard/Dashboard.js @@ -7,8 +7,7 @@ import DashboardLoadingIndicator from './DashboardLoadingIndicator'; import Sidebar from 'components/Sidebar/Sidebar'; import DashboardContent from 'components/Dashboard/DashboardContent'; import DialogsContainer from 'components/DialogsContainer'; -import PreferencesContent from 'components/Preferences/PreferencesContent'; -import PreferencesSidebar from 'components/Preferences/PreferencesSidebar'; +import PreferencesPage from 'components/Preferences/PreferencesPage'; import Search from 'containers/GeneralSearch/Search'; import DashboardSplitPane from 'components/Dashboard/DashboardSplitePane'; @@ -28,9 +27,8 @@ function Dashboard({ - + - diff --git a/client/src/components/DialogsContainer.js b/client/src/components/DialogsContainer.js index 36ab35432..de3b62a59 100644 --- a/client/src/components/DialogsContainer.js +++ b/client/src/components/DialogsContainer.js @@ -2,10 +2,9 @@ import React, { lazy } from 'react'; import AccountFormDialog from 'containers/Dialogs/AccountFormDialog'; -import UserFormDialog from 'containers/Dialogs/UserFormDialog'; +import InviteUserDialog from 'containers/Dialogs/InviteUserDialog'; import ItemCategoryDialog from 'containers/Dialogs/ItemCategoryDialog'; import CurrencyFormDialog from 'containers/Dialogs/CurrencyFormDialog'; -// import InviteUserDialog from 'containers/Dialogs/InviteUserDialog'; import ExchangeRateFormDialog from 'containers/Dialogs/ExchangeRateFormDialog'; import JournalNumberDialog from 'containers/Dialogs/JournalNumberDialog'; // import BillNumberDialog from 'containers/Dialogs/BillNumberDialog'; @@ -13,6 +12,7 @@ import PaymentReceiveNumberDialog from 'containers/Dialogs/PaymentReceiveNumberD import EstimateNumberDialog from 'containers/Dialogs/EstimateNumberDialog'; import ReceiptNumberDialog from 'containers/Dialogs/ReceiptNumberDialog'; import InvoiceNumberDialog from 'containers/Dialogs/InvoiceNumberDialog'; + export default function DialogsContainer() { return (
@@ -24,9 +24,9 @@ export default function DialogsContainer() { - +
); -} \ No newline at end of file +} diff --git a/client/src/components/ListSelect.js b/client/src/components/ListSelect.js index 4a9bbfca4..c0aedf686 100644 --- a/client/src/components/ListSelect.js +++ b/client/src/components/ListSelect.js @@ -2,6 +2,8 @@ import React, { useState, useMemo, useEffect } from 'react'; import { Button, MenuItem } from '@blueprintjs/core'; import { Select } from '@blueprintjs/select'; import { FormattedMessage as T } from 'react-intl'; +import classNames from 'classnames'; +import { CLASSES } from 'common/classes'; export default function ListSelect({ buttonProps, @@ -69,6 +71,10 @@ export default function ListSelect({ {...selectProps} noResults={noResults} disabled={disabled} + className={classNames( + CLASSES.FORM_GROUP_LIST_SELECT, + selectProps.className, + )} > - - - - - - ); -} - -export default compose( - withDialogActions, - UserListDialogConnect, - withUsersActions, - DialogReduxConnect, -)(InviteUserDialog); diff --git a/client/src/containers/Dialogs/InviteUserDialog/InviteUserDialog.schema.js b/client/src/containers/Dialogs/InviteUserDialog/InviteUserDialog.schema.js new file mode 100644 index 000000000..98c3b7f30 --- /dev/null +++ b/client/src/containers/Dialogs/InviteUserDialog/InviteUserDialog.schema.js @@ -0,0 +1,11 @@ +import * as Yup from 'yup'; +import { formatMessage } from 'services/intl'; + +const Schema = Yup.object().shape({ + email: Yup.string() + .email() + .required() + .label(formatMessage({ id: 'email' })), +}); + +export const InviteUserFormSchema = Schema; \ No newline at end of file diff --git a/client/src/containers/Dialogs/InviteUserDialog/InviteUserDialogContent.js b/client/src/containers/Dialogs/InviteUserDialog/InviteUserDialogContent.js new file mode 100644 index 000000000..20a6c95e4 --- /dev/null +++ b/client/src/containers/Dialogs/InviteUserDialog/InviteUserDialogContent.js @@ -0,0 +1,97 @@ +import React from 'react'; +import { Intent } from '@blueprintjs/core'; +import { pick, snakeCase } from 'lodash'; +import { queryCache, useQuery } from 'react-query'; +import { useIntl } from 'react-intl'; +import { Formik } from 'formik'; +import { AppToaster, DialogContent } from 'components'; + +import withUsersActions from 'containers/Users/withUsersActions'; +import withDialogActions from 'containers/Dialog/withDialogActions'; + +import { compose, objectKeysTransform } from 'utils'; +import { InviteUserFormSchema } from './InviteUserDialog.schema'; +import UserFormDialogForm from './InviteUserDialogForm'; + +import { transformApiErrors } from './utils'; + +// +function InviteUserDialogContent({ + // #wihtCurrenciesActions + requestFetchUser, + requestSubmitInvite, + + // #withDialogActions + closeDialog, + + // #ownProp + action, + userId, + dialogName, +}) { + const { formatMessage } = useIntl(); + + // Fetch user details. + const fetchHook = useQuery( + ['user', userId], + (key, id) => requestFetchUser(id), + { enabled: userId }, + ); + + const initialValues = { + status: 1, + ...(action === 'edit' && + pick( + objectKeysTransform(userId, snakeCase), + Object.keys(InviteUserFormSchema.fields), + )), + }; + + const handleSubmit = (values, { setSubmitting, setErrors }) => { + const form = { ...values }; + + requestSubmitInvite(form) + .then((response) => { + closeDialog(dialogName); + AppToaster.show({ + message: formatMessage({ + id: 'teammate_invited_to_organization_account', + }), + intent: Intent.SUCCESS, + }); + setSubmitting(false); + queryCache.invalidateQueries('users-table'); + }) + .catch((errors) => { + const errorsTransformed = transformApiErrors(errors); + + setErrors({ ...errorsTransformed }); + setSubmitting(false); + }); + }; + + const handleCancelBtnClick = () => { + closeDialog('invite-user'); + }; + + return ( + + + + + + ); +} + +export default compose( + // UserFormDialogConnect, + withDialogActions, + withUsersActions, +)(InviteUserDialogContent); diff --git a/client/src/containers/Dialogs/InviteUserDialog/InviteUserDialogForm.js b/client/src/containers/Dialogs/InviteUserDialog/InviteUserDialogForm.js new file mode 100644 index 000000000..a8efbbe1f --- /dev/null +++ b/client/src/containers/Dialogs/InviteUserDialog/InviteUserDialogForm.js @@ -0,0 +1,56 @@ +import React from 'react'; +import { + FormGroup, + InputGroup, + Intent, + Button, +} from '@blueprintjs/core'; +import { FastField, Form, useFormikContext, ErrorMessage } from 'formik'; +import { FormattedMessage as T } from 'react-intl'; +import { CLASSES } from 'common/classes'; +import classNames from 'classnames'; +import { inputIntent, saveInvoke } from 'utils'; + +export default function InviteUserDialogForm({ onCancelClick, action }) { + const { isSubmitting } = useFormikContext(); + + const handleCancelBtnClick = (event) => { + saveInvoke(onCancelClick, event); + }; + + return ( +
+
+

+ +

+ + + {({ field, meta: { error, touched } }) => ( + } + className={classNames('form-group--email', CLASSES.FILL)} + intent={inputIntent({ error, touched })} + helperText={} + inline={true} + > + + + )} + +
+ +
+
+ + + +
+
+
+ ); +} diff --git a/client/src/containers/Dialogs/UserFormDialog/index.js b/client/src/containers/Dialogs/InviteUserDialog/index.js similarity index 85% rename from client/src/containers/Dialogs/UserFormDialog/index.js rename to client/src/containers/Dialogs/InviteUserDialog/index.js index 09fa56aaf..e8765e9fd 100644 --- a/client/src/containers/Dialogs/UserFormDialog/index.js +++ b/client/src/containers/Dialogs/InviteUserDialog/index.js @@ -1,11 +1,12 @@ import React, { lazy } from 'react'; -import { FormattedMessage as T, useIntl } from 'react-intl'; +import { FormattedMessage as T } from 'react-intl'; import { Dialog, DialogSuspense } from 'components'; import withDialogRedux from 'components/DialogReduxConnect'; import { compose } from 'utils'; -const UserFormDialogContent = lazy(() => import('./UserFormDialogContent')); +const UserFormDialogContent = lazy(() => import('./InviteUserDialogContent')); +// User form dialog. function UserFormDialog({ dialogName, payload = { action: '', id: null }, diff --git a/client/src/containers/Dialogs/InviteUserDialog/utils.js b/client/src/containers/Dialogs/InviteUserDialog/utils.js new file mode 100644 index 000000000..c55e53a8a --- /dev/null +++ b/client/src/containers/Dialogs/InviteUserDialog/utils.js @@ -0,0 +1,10 @@ +import { formatMessage } from 'services/intl'; + +export const transformApiErrors = (errors) => { + const fields = {}; + + if (errors.find(error => error.type === 'EMAIL.ALREADY.INVITED')) { + fields.email = formatMessage({ id: 'email_is_already_used' }); + } + return fields; + } \ No newline at end of file diff --git a/client/src/containers/Dialogs/UserFormDialog/UserFormDialogContent.js b/client/src/containers/Dialogs/UserFormDialog/UserFormDialogContent.js deleted file mode 100644 index 44bf2ae77..000000000 --- a/client/src/containers/Dialogs/UserFormDialog/UserFormDialogContent.js +++ /dev/null @@ -1,162 +0,0 @@ -import React, { useCallback } from 'react'; -import { useFormik } from 'formik'; -import * as Yup from 'yup'; -import { - Button, - FormGroup, - InputGroup, - Intent, - Classes, -} from '@blueprintjs/core'; -import { pick, snakeCase } from 'lodash'; -import classNames from 'classnames'; -import { queryCache, useQuery } from 'react-query'; -import { FormattedMessage as T, useIntl } from 'react-intl'; -import { - If, - ErrorMessage, - AppToaster, - FieldRequiredHint, - DialogContent, -} from 'components'; - -import UserFormDialogConnect from 'containers/Dialogs/UserFormDialog.connector'; -import withUsersActions from 'containers/Users/withUsersActions'; -import withDialogActions from 'containers/Dialog/withDialogActions'; - -import { compose, objectKeysTransform } from 'utils'; - -function UserFormDialogContent({ - // #wihtCurrenciesActions - requestFetchUser, - requestSubmitInvite, - - // #withDialogActions - closeDialog, - - // #ownProp - action, - userId, - dialogName, -}) { - const { formatMessage } = useIntl(); - - const fetchHook = useQuery( - // action === 'edit' && ['user', action.user.id], - action === 'edit' && ['user', userId], - (key, id) => requestFetchUser(id), - { enabled: userId }, - ); - - const validationSchema = Yup.object().shape({ - email: Yup.string() - .email() - .required() - .label(formatMessage({ id: 'email' })), - }); - - const initialValues = { - status: 1, - ...(action === 'edit' && - pick( - objectKeysTransform(userId, snakeCase), - Object.keys(validationSchema.fields), - )), - }; - - const { - errors, - touched, - resetForm, - getFieldProps, - handleSubmit, - isSubmitting, - } = useFormik({ - enableReinitialize: true, - initialValues, - validationSchema, - onSubmit: (values, { setSubmitting }) => { - const form = { ...values }; - - requestSubmitInvite(form) - .then((response) => { - closeDialog(dialogName); - AppToaster.show({ - message: formatMessage({ - id: 'teammate_invited_to_organization_account', - }), - intent: Intent.SUCCESS, - }); - setSubmitting(false); - queryCache.invalidateQueries('users-table'); - }) - .catch((errors) => { - setSubmitting(false); - }); - }, - }); - - // Handles dialog close. - const handleClose = useCallback(() => { - closeDialog(dialogName); - }, [closeDialog, dialogName]); - - // Handle the dialog opening. - const onDialogOpening = useCallback(() => { - fetchHook.refetch(); - }, [fetchHook]); - - const onDialogClosed = useCallback(() => { - resetForm(); - closeDialog(dialogName); - }, [resetForm]); - - console.log(action, 'action'); - - return ( - -
-
-

- -

- - } - className={classNames('form-group--email', Classes.FILL)} - intent={errors.email && touched.email && Intent.DANGER} - helperText={} - inline={true} - > - - -
- -
-
- - -
-
-
-
- ); -} - -export default compose( - // UserFormDialogConnect, - withDialogActions, - withUsersActions, -)(UserFormDialogContent); diff --git a/client/src/containers/Preferences/Accountant/Accountant.js b/client/src/containers/Preferences/Accountant/Accountant.js index f5da86b9f..e7acd35c5 100644 --- a/client/src/containers/Preferences/Accountant/Accountant.js +++ b/client/src/containers/Preferences/Accountant/Accountant.js @@ -1,9 +1,59 @@ -import React from 'react'; +import React, { useEffect } from 'react'; +import classNames from 'classnames'; +import { Formik } from 'formik'; +import { useQuery } from 'react-query'; +import { CLASSES } from 'common/classes'; +import { LoadingIndicator } from 'components'; +import AccountantForm from './AccountantForm'; +import { AccountantSchema } from './Accountant.schema'; + +import withDashboardActions from 'containers/Dashboard/withDashboardActions'; +import withSettings from 'containers/Settings/withSettings'; +import withSettingsActions from 'containers/Settings/withSettingsActions'; +import withAccountsActions from 'containers/Accounts/withAccountsActions'; + +import { compose } from 'utils'; + +// Accountant preferences. +function AccountantPreferences({ + changePreferencesPageTitle, + + // #withAccountsActions + requestFetchAccounts, +}) { + const initialValues = {}; + + useEffect(() => { + changePreferencesPageTitle('Accountant'); + }, [changePreferencesPageTitle]); + + const fetchAccounts = useQuery('accounts-list', (key) => + requestFetchAccounts(), + ); -export default function AccountantPreferences() { return ( -
- +
+
+ + + +
); -} \ No newline at end of file +} + +export default compose( + withSettings(({ organizationSettings }) => ({ organizationSettings })), + withSettingsActions, + withDashboardActions, + withAccountsActions, +)(AccountantPreferences); diff --git a/client/src/containers/Preferences/Accountant/Accountant.schema.js b/client/src/containers/Preferences/Accountant/Accountant.schema.js new file mode 100644 index 000000000..699c6bf0a --- /dev/null +++ b/client/src/containers/Preferences/Accountant/Accountant.schema.js @@ -0,0 +1,10 @@ +import * as Yup from 'yup'; + +const Schema = Yup.object().shape({ + accounting_basis: Yup.string().required(), + account_code_required: Yup.boolean(), + customer_deposit_account: Yup.number().nullable(), + vendor_withdrawal_account: Yup.number().nullable(), +}); + +export const AccountantSchema = Schema; \ No newline at end of file diff --git a/client/src/containers/Preferences/Accountant/AccountantForm.js b/client/src/containers/Preferences/Accountant/AccountantForm.js new file mode 100644 index 000000000..e676d71b9 --- /dev/null +++ b/client/src/containers/Preferences/Accountant/AccountantForm.js @@ -0,0 +1,105 @@ +import React from 'react'; +import { Form } from 'formik'; +import { + FormGroup, + RadioGroup, + Radio, + Checkbox, + Button, + Intent, +} from '@blueprintjs/core'; +import { useHistory } from 'react-router-dom'; +import { AccountsSelectList } from 'components'; +import { + FieldRequiredHint, +} from 'components'; +import { FormattedMessage as T } from 'react-intl'; +import { compose } from 'utils'; +import withAccounts from 'containers/Accounts/withAccounts'; + +function AccountantForm({ + // #withAccounts + accountsList, +}) { + const history = useHistory(); + const handleCloseClick = () => { + history.go(-1); + }; + + return ( +
+ Accounts}> + + + + + } + label={Accounting Basis}> + + + + + + + Deposit customer account} + helperText={ + 'Select a preferred account to deposit into it after customer make payment.' + } + labelInfo={} + > + } + filterByTypes={['current_asset']} + /> + + + Withdrawal customer account} + helperText={ + 'Select a preferred account to deposit into it after customer make payment.' + } + labelInfo={} + > + } + filterByTypes={['current_asset']} + /> + + + Vendor advance deposit} + helperText={ + 'Select a preferred account to deposit into it vendor advanced deposits.' + } + labelInfo={} + > + } + filterByTypes={['current_asset', 'other_current_asset']} + /> + + +
+ + +
+
+ ); +} + +export default compose( + withAccounts(({ accountsList }) => ({ accountsList })), +)(AccountantForm); diff --git a/client/src/containers/Preferences/Currencies/Currencies.js b/client/src/containers/Preferences/Currencies/Currencies.js index b354b74b8..e110e72df 100644 --- a/client/src/containers/Preferences/Currencies/Currencies.js +++ b/client/src/containers/Preferences/Currencies/Currencies.js @@ -1,20 +1,20 @@ + + import React from 'react'; -import { Button, Intent } from '@blueprintjs/core'; -import { compose } from 'utils'; -import withDialogActions from 'containers/Dialog/withDialogActions'; +import classNames from 'classnames'; -function Currencies({ openDialog }) { - const onClickNewCurrency = () => { - openDialog('currency-form',{}); - }; +import { CLASSES } from 'common/classes'; +import CurrenciesList from './CurrenciesList'; +export default function PreferencesCurrenciesPage() { return ( -
-
- +
+
+
- ); -} - -export default compose(withDialogActions)(Currencies); + ) +} \ No newline at end of file diff --git a/client/src/containers/Preferences/Currencies/CurrenciesDataTable.js b/client/src/containers/Preferences/Currencies/CurrenciesDataTable.js index 232e6113f..071585af5 100644 --- a/client/src/containers/Preferences/Currencies/CurrenciesDataTable.js +++ b/client/src/containers/Preferences/Currencies/CurrenciesDataTable.js @@ -1,4 +1,4 @@ -import React, { useCallback, useState, useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { Intent, Button, @@ -8,10 +8,9 @@ import { Position, } from '@blueprintjs/core'; import { withRouter } from 'react-router'; -import { FormattedMessage as T, useIntl } from 'react-intl'; -import { compose } from 'utils'; -import { useUpdateEffect } from 'hooks'; -import LoadingIndicator from 'components/LoadingIndicator'; +import { useIntl } from 'react-intl'; +import { compose, saveInvoke } from 'utils'; + import { DataTable, Icon } from 'components'; import withDashboardActions from 'containers/Dashboard/withDashboardActions'; @@ -23,24 +22,15 @@ function CurrenciesDataTable({ currenciesList, currenciesLoading, - loading, + // #ownProps onFetchData, - onSelectedRowsChange, onDeleteCurrency, // #withDialog. openDialog, }) { - const [initialMount, setInitialMount] = useState(false); - const { formatMessage } = useIntl(); - useUpdateEffect(() => { - if (!currenciesLoading) { - setInitialMount(true); - } - }, [currenciesLoading, setInitialMount]); - const handleEditCurrency = useCallback( (currency) => { openDialog('currency-form', { @@ -59,7 +49,6 @@ function CurrenciesDataTable({ text={formatMessage({ id: 'edit_currency' })} onClick={() => handleEditCurrency(currency)} /> - } text={formatMessage({ id: 'delete_currency' })} @@ -116,31 +105,20 @@ function CurrenciesDataTable({ const handleDataTableFetchData = useCallback( (...args) => { - onFetchData && onFetchData(...args); + saveInvoke(onFetchData, ...args); }, [onFetchData], ); - const handleSelectedRowsChange = useCallback( - (selectedRows) => { - onSelectedRowsChange && - onSelectedRowsChange(selectedRows.map((s) => s.original)); - }, - [onSelectedRowsChange], - ); - return ( - - - + ); } @@ -148,7 +126,8 @@ export default compose( withRouter, withDashboardActions, withDialogActions, - withCurrencies(({ currenciesList }) => ({ + withCurrencies(({ currenciesList, currenciesLoading }) => ({ currenciesList, + currenciesLoading, })), )(CurrenciesDataTable); diff --git a/client/src/containers/Preferences/Currencies/CurrenciesList.js b/client/src/containers/Preferences/Currencies/CurrenciesList.js index e3c3d2764..00b2ebfa7 100644 --- a/client/src/containers/Preferences/Currencies/CurrenciesList.js +++ b/client/src/containers/Preferences/Currencies/CurrenciesList.js @@ -1,6 +1,6 @@ -import React, { useCallback, useState, useMemo, useEffect } from 'react'; +import React, { useCallback, useState, useEffect } from 'react'; import { Alert, Intent } from '@blueprintjs/core'; -import { useQuery, queryCache } from 'react-query'; +import { useQuery } from 'react-query'; import { FormattedMessage as T, FormattedHTMLMessage, @@ -8,33 +8,23 @@ import { } from 'react-intl'; import CurrenciesDataTable from './CurrenciesDataTable'; -import DashboardPageContent from 'components/Dashboard/DashboardPageContent'; -import DashboardInsider from 'components/Dashboard/DashboardInsider'; import AppToaster from 'components/AppToaster'; import withDashboardActions from 'containers/Dashboard/withDashboardActions'; import withCurrenciesActions from 'containers/Currencies/withCurrenciesActions'; -import withDialogActions from 'containers/Dialog/withDialogActions'; import { compose } from 'utils'; +// Currencies landing list page. function CurrenciesList({ - // #withCurrencies - currenciesList, - currenciesLoading, - // #withCurrenciesActions requestDeleteCurrency, requestFetchCurrencies, - // #withDialogActions - openDialog, - // #withDashboardActions changePreferencesPageTitle, }) { const [deleteCurrencyState, setDeleteCurrencyState] = useState(false); - const [selectedRows, setSelectedRows] = useState([]); const { formatMessage } = useIntl(); const fetchCurrencies = useQuery( @@ -52,7 +42,7 @@ function CurrenciesList({ // Handle click and cancel/confirm currency delete const handleDeleteCurrency = useCallback((currency) => { setDeleteCurrencyState(currency); - }, []); + }, [setDeleteCurrencyState]); // handle cancel delete currency alert. const handleCancelCurrencyDelete = () => { @@ -79,44 +69,29 @@ function CurrenciesList({ [deleteCurrencyState, requestDeleteCurrency, formatMessage], ); - // Handle selected rows change. - const handleSelectedRowsChange = useCallback( - (accounts) => { - setSelectedRows(accounts); - }, - [setSelectedRows], - ); - return ( - - - - } - confirmButtonText={} - icon="trash" - intent={Intent.DANGER} - isOpen={deleteCurrencyState} - onCancel={handleCancelCurrencyDelete} - onConfirm={handleConfirmCurrencyDelete} - > -

- -

-
-
-
+ <> + + } + confirmButtonText={} + intent={Intent.DANGER} + isOpen={deleteCurrencyState} + onCancel={handleCancelCurrencyDelete} + onConfirm={handleConfirmCurrencyDelete} + > +

+ Once you delete this currency, you won't be able to restore it later. Are you sure you want to delete ? +

+
+ ); } export default compose( withDashboardActions, withCurrenciesActions, - withDialogActions, )(CurrenciesList); diff --git a/client/src/containers/Preferences/General/General.js b/client/src/containers/Preferences/General/General.js index c8eed2ceb..0aa0d2c71 100644 --- a/client/src/containers/Preferences/General/General.js +++ b/client/src/containers/Preferences/General/General.js @@ -1,39 +1,17 @@ -import React, { useState, useCallback, useEffect } from 'react'; -import { useFormik } from 'formik'; +import React, { useEffect } from 'react'; +import { Formik } from 'formik'; import { mapKeys, snakeCase } from 'lodash'; -import * as Yup from 'yup'; -import { - Button, - FormGroup, - InputGroup, - Intent, - MenuItem, - Classes, - Spinner, - Position, -} from '@blueprintjs/core'; +import { Intent } from '@blueprintjs/core'; import classNames from 'classnames'; -import { TimezonePicker } from '@blueprintjs/timezone'; import { useQuery, queryCache } from 'react-query'; -import { FormattedMessage as T, useIntl } from 'react-intl'; -import { DateInput } from '@blueprintjs/datetime'; -import moment from 'moment'; -import { useHistory } from 'react-router-dom'; +import { useIntl } from 'react-intl'; +import { CLASSES } from 'common/classes'; -import { - compose, - optionsMapToArray, - tansformDateValue, - momentFormatter, -} from 'utils'; +import { compose, optionsMapToArray } from 'utils'; -import { - If, - FieldRequiredHint, - ListSelect, - ErrorMessage, - AppToaster, -} from 'components'; +import { AppToaster, LoadingIndicator } from 'components'; +import GeneralForm from './GeneralForm'; +import { PreferencesGeneralSchema } from './General.schema'; import withDashboardActions from 'containers/Dashboard/withDashboardActions'; import withSettings from 'containers/Settings/withSettings'; @@ -51,524 +29,59 @@ function GeneralPreferences({ requestFetchOptions, }) { const { formatMessage } = useIntl(); - const [selectedItems, setSelectedItems] = useState({}); - const history = useHistory(); - const fetchHook = useQuery(['settings'], () => { - requestFetchOptions(); - }); + const fetchSettings = useQuery(['settings'], () => requestFetchOptions()); useEffect(() => { changePreferencesPageTitle(formatMessage({ id: 'general' })); }, [changePreferencesPageTitle, formatMessage]); - const businessLocation = [{ id: 218, name: 'LIBYA', value: 'libya' }]; - const languagesDisplay = [ - { id: 0, name: 'English', value: 'en' }, - { id: 1, name: 'Arabic', value: 'ar' }, - ]; - const currencies = [ - { id: 0, name: 'US Dollar', value: 'USD' }, - { id: 1, name: 'Euro', value: 'EUR' }, - { id: 1, name: 'Libyan Dinar ', value: 'LYD' }, - ]; - - const fiscalYear = [ - { - id: 0, - name: `${formatMessage({ id: 'january' })} - ${formatMessage({ - id: 'december', - })}`, - value: 'january', - }, - { - id: 1, - name: `${formatMessage({ id: 'february' })} - ${formatMessage({ - id: 'january', - })}`, - value: 'february', - }, - { - id: 2, - name: `${formatMessage({ id: 'march' })} - ${formatMessage({ - id: 'february', - })}`, - value: 'March', - }, - { - id: 3, - name: `${formatMessage({ id: 'april' })} - ${formatMessage({ - id: 'march', - })}`, - value: 'april', - }, - { - id: 4, - name: `${formatMessage({ id: 'may' })} - ${formatMessage({ - id: 'april', - })}`, - value: 'may', - }, - { - id: 5, - name: `${formatMessage({ id: 'june' })} - ${formatMessage({ - id: 'may', - })}`, - value: 'june', - }, - { - id: 6, - name: `${formatMessage({ id: 'july' })} - ${formatMessage({ - id: 'june', - })}`, - value: 'july', - }, - { - id: 7, - name: `${formatMessage({ id: 'august' })} - ${formatMessage({ - id: 'july', - })}`, - value: 'August', - }, - { - id: 8, - name: `${formatMessage({ id: 'september' })} - ${formatMessage({ - id: 'august', - })}`, - value: 'september', - }, - { - id: 9, - name: `${formatMessage({ id: 'october' })} - ${formatMessage({ - id: 'november', - })}`, - value: 'october', - }, - { - id: 10, - name: `${formatMessage({ id: 'november' })} - ${formatMessage({ - id: 'october', - })}`, - value: 'november', - }, - { - id: 11, - name: `${formatMessage({ id: 'december' })} - ${formatMessage({ - id: 'november', - })}`, - value: 'december', - }, - ]; - const dateFormat = [ - { - id: 1, - name: 'MM/DD/YY', - label: `${moment().format('MM/DD/YYYY')}`, - value: 'mm/dd/yy', - }, - { - id: 2, - name: 'DD/MM/YY', - label: `${moment().format('DD/MM/YYYY')}`, - value: 'dd/mm/yy', - }, - { - id: 3, - name: 'YY/MM/DD', - label: `${moment().format('YYYY/MM/DD')}`, - value: 'yy/mm/dd', - }, - { - id: 4, - name: 'MM-DD-YY', - label: `${moment().format('MM-DD-YYYY')}`, - value: 'mm-dd-yy', - }, - { - id: 5, - name: 'DD-MM-YY', - label: `${moment().format('DD-MM-YYYY')}`, - value: 'dd-mm-yy', - }, - { - id: 6, - name: 'YY-MM-DD', - label: `${moment().format('YYYY-MM-DD')}`, - value: 'yy-mm-dd', - }, - ]; - - const validationSchema = Yup.object().shape({ - name: Yup.string() - .required() - .label(formatMessage({ id: 'organization_name_' })), - financial_date_start: Yup.date() - .required() - .label(formatMessage({ id: 'date_start_' })), - industry: Yup.string() - .required() - .label(formatMessage({ id: 'organization_industry_' })), - location: Yup.string() - .required() - .label(formatMessage({ id: 'location' })), - base_currency: Yup.string() - .required() - .label(formatMessage({ id: 'base_currency_' })), - fiscal_year: Yup.string() - .required() - .label(formatMessage({ id: 'fiscal_year_' })), - language: Yup.string() - .required() - .label(formatMessage({ id: 'language' })), - time_zone: Yup.string() - .required() - .label(formatMessage({ id: 'time_zone_' })), - date_format: Yup.string() - .required() - .label(formatMessage({ id: 'date_format_' })), - }); - - function snakeCaseChange(data) { + function transformGeneralSettings(data) { return mapKeys(data, (value, key) => snakeCase(key)); } - const initialValues = snakeCaseChange(organizationSettings); + const initialValues = { + ...transformGeneralSettings(organizationSettings), + }; - const { - values, - errors, - touched, - setFieldValue, - getFieldProps, - handleSubmit, - resetForm, - isSubmitting, - } = useFormik({ - enableReinitialize: true, - initialValues: { - ...initialValues, - }, - validationSchema, - onSubmit: (values, { setSubmitting }) => { - const options = optionsMapToArray(values).map((option) => { - return { key: option.key, ...option, group: 'organization' }; - }); - requestSubmitOptions({ options }) - .then((response) => { - AppToaster.show({ - message: formatMessage({ - id: 'the_options_has_been_successfully_created', - }), - intent: Intent.SUCCESS, - }); - setSubmitting(false); - resetForm(); - queryCache.invalidateQueries('settings'); - }) - .catch((error) => { - setSubmitting(false); + const handleFormSubmit = (values, { setSubmitting, resetForm }) => { + const options = optionsMapToArray(values).map((option) => { + return { key: option.key, ...option, group: 'organization' }; + }); + requestSubmitOptions({ options }) + .then((response) => { + AppToaster.show({ + message: formatMessage({ + id: 'the_options_has_been_successfully_created', + }), + intent: Intent.SUCCESS, }); - }, - }); - - const onItemRenderer = (item, { handleClick }) => ( - - ); - - const currencyItem = (item, { handleClick }) => ( - - ); - - const handleDateChange = useCallback( - (date) => { - const formatted = moment(date).format('YYYY-MM-DD'); - setFieldValue('financial_date_start', formatted); - }, - [setFieldValue], - ); - const date_format = (item, { handleClick }) => ( - - ); - - const onItemsSelect = (filedName) => { - return (filed) => { - setSelectedItems({ - ...selectedItems, - [filedName]: filed, + setSubmitting(false); + resetForm(); + queryCache.invalidateQueries('settings'); + }) + .catch((error) => { + setSubmitting(false); }); - setFieldValue(filedName, filed.value); - }; - }; - - const filterItems = (query, item, _index, exactMatch) => { - const normalizedTitle = item.name.toLowerCase(); - const normalizedQuery = query.toLowerCase(); - - if (exactMatch) { - return normalizedTitle === normalizedQuery; - } else { - return normalizedTitle.indexOf(normalizedQuery) >= 0; - } - }; - - const handleTimezoneChange = useCallback( - (timezone) => { - setFieldValue('time_zone', timezone); - }, - [setFieldValue], - ); - - const handleClose = () => { - history.goBack(); }; return (
-
- } - labelInfo={} - inline={true} - intent={errors.name && touched.name && Intent.DANGER} - helperText={} - className={'form-group--org-name'} - > - + + - - } - labelInfo={} - inline={true} - intent={ - errors.financial_date_start && - touched.financial_date_start && - Intent.DANGER - } - helperText={ - - } - className={classNames('form-group--select-list', Classes.FILL)} - > - - - - } - inline={true} - intent={errors.industry && touched.industry && Intent.DANGER} - helperText={} - className={'form-group--org-industry'} - > - - - - } - className={classNames( - 'form-group--business-location', - 'form-group--select-list', - Classes.FILL, - )} - inline={true} - helperText={} - intent={errors.location && touched.location && Intent.DANGER} - > - } - itemRenderer={onItemRenderer} - popoverProps={{ minimal: true }} - onItemSelect={onItemsSelect('location')} - selectedItem={values.location} - selectedItemProp={'value'} - defaultText={} - labelProp={'name'} - /> - - - } - labelInfo={} - className={classNames( - 'form-group--base-currency', - 'form-group--select-list', - Classes.LOADING, - Classes.FILL, - )} - inline={true} - helperText={ - - } - intent={ - errors.base_currency && touched.base_currency && Intent.DANGER - } - > - } - itemRenderer={currencyItem} - popoverProps={{ minimal: true }} - onItemSelect={onItemsSelect('base_currency')} - itemPredicate={filterItems} - selectedItem={values.base_currency} - selectedItemProp={'value'} - defaultText={} - labelProp={'name'} - /> - - - } - labelInfo={} - className={classNames( - 'form-group--fiscal-year', - 'form-group--select-list', - Classes.FILL, - )} - inline={true} - helperText={ - - } - intent={errors.fiscal_year && touched.fiscal_year && Intent.DANGER} - > - } - itemRenderer={onItemRenderer} - popoverProps={{ minimal: true }} - onItemSelect={onItemsSelect('fiscal_year')} - itemPredicate={filterItems} - selectedItem={values.fiscal_year} - selectedItemProp={'value'} - defaultText={} - labelProp={'name'} - /> - - - } - labelInfo={} - inline={true} - className={classNames( - 'form-group--language', - 'form-group--select-list', - Classes.FILL, - )} - intent={errors.language && touched.language && Intent.DANGER} - helperText={} - > - } - itemRenderer={onItemRenderer} - popoverProps={{ minimal: true }} - onItemSelect={onItemsSelect('language')} - itemPredicate={filterItems} - selectedItem={values.language} - selectedItemProp={'value'} - defaultText={} - labelProp={'name'} - /> - - } - labelInfo={} - inline={true} - className={classNames( - 'form-group--time-zone', - 'form-group--select-list', - Classes.FILL, - )} - intent={errors.time_zone && touched.time_zone && Intent.DANGER} - helperText={ - - } - > - } - /> - - } - labelInfo={} - inline={true} - className={classNames( - 'form-group--language', - 'form-group--select-list', - Classes.FILL, - )} - intent={errors.date_format && touched.date_format && Intent.DANGER} - helperText={ - - } - > - } - itemRenderer={date_format} - popoverProp={{ minimal: true }} - onItemSelect={onItemsSelect('date_format')} - itemPredicate={filterItems} - selectedItem={values.date_format} - selectedItemProp={'value'} - defaultText={} - labelProp={'name'} - /> - - -
- - -
-
- -
- -
-
+ +
); } diff --git a/client/src/containers/Preferences/General/General.schema.js b/client/src/containers/Preferences/General/General.schema.js new file mode 100644 index 000000000..48c893188 --- /dev/null +++ b/client/src/containers/Preferences/General/General.schema.js @@ -0,0 +1,34 @@ +import * as Yup from 'yup'; +import { formatMessage } from 'services/intl'; + +const Schema = Yup.object().shape({ + name: Yup.string() + .required() + .label(formatMessage({ id: 'organization_name_' })), + financial_date_start: Yup.date() + .required() + .label(formatMessage({ id: 'date_start_' })), + industry: Yup.string() + .nullable() + .label(formatMessage({ id: 'organization_industry_' })), + location: Yup.string() + .nullable() + .label(formatMessage({ id: 'location' })), + base_currency: Yup.string() + .required() + .label(formatMessage({ id: 'base_currency_' })), + fiscal_year: Yup.string() + .required() + .label(formatMessage({ id: 'fiscal_year_' })), + language: Yup.string() + .required() + .label(formatMessage({ id: 'language' })), + time_zone: Yup.string() + .required() + .label(formatMessage({ id: 'time_zone_' })), + date_format: Yup.string() + .required() + .label(formatMessage({ id: 'date_format_' })), +}); + +export const PreferencesGeneralSchema = Schema; diff --git a/client/src/containers/Preferences/General/GeneralForm.js b/client/src/containers/Preferences/General/GeneralForm.js new file mode 100644 index 000000000..ef1239cbd --- /dev/null +++ b/client/src/containers/Preferences/General/GeneralForm.js @@ -0,0 +1,250 @@ +import { Form } from 'formik'; +import React from 'react'; +import { + Button, + FormGroup, + InputGroup, + Intent, + Position, +} from '@blueprintjs/core'; +import classNames from 'classnames'; +import { TimezonePicker } from '@blueprintjs/timezone'; +import { ErrorMessage, FastField } from 'formik'; +import { FormattedMessage as T } from 'react-intl'; +import { DateInput } from '@blueprintjs/datetime'; +import { useHistory } from 'react-router-dom'; +import { ListSelect, FieldRequiredHint } from 'components'; +import { + inputIntent, + momentFormatter, + tansformDateValue, + handleDateChange, +} from 'utils'; +import { CLASSES } from 'common/classes'; +import countriesOptions from 'common/countries'; +import currencies from 'common/currencies'; +import fiscalYearOptions from 'common/fiscalYearOptions'; +import languages from 'common/languagesOptions'; +import dateFormatsOptions from 'common/dateFormatsOptions'; + +export default function PreferencesGeneralForm({}) { + const history = useHistory(); + + const handleCloseClick = () => { + history.go(-1); + }; + + return ( +
+ + {({ field, meta: { error, touched } }) => ( + } + labelInfo={} + inline={true} + intent={inputIntent({ error, touched })} + helperText={} + className={'form-group--org-name'} + > + + + )} + + + + {({ form, field: { value }, meta: { error, touched } }) => ( + } + labelInfo={} + inline={true} + intent={inputIntent({ error, touched })} + helperText={} + className={classNames('form-group--select-list', CLASSES.FILL)} + > + { + form.setFieldValue('financial_date_start', formattedDate); + })} + popoverProps={{ position: Position.BOTTOM, minimal: true }} + /> + + )} + + + + {({ field, meta: { error, touched } }) => ( + } + inline={true} + intent={inputIntent({ error, touched })} + helperText={} + className={'form-group--org-industry'} + > + + + )} + + + + {({ field: { value }, meta: { error, touched } }) => ( + } + className={classNames( + 'form-group--business-location', + CLASSES.FILL, + )} + inline={true} + helperText={} + intent={inputIntent({ error, touched })} + > + {}} + selectedItem={value} + selectedItemProp={'value'} + defaultText={} + labelProp={'name'} + popoverProps={{ minimal: true }} + /> + + )} + + + + {({ form, field: { value }, meta: { error, touched } }) => ( + } + labelInfo={} + className={classNames('form-group--base-currency', CLASSES.FILL)} + inline={true} + helperText={} + intent={inputIntent({ error, touched })} + > + { + form.setFieldValue('base_currency', currency.code); + }} + selectedItem={value} + selectedItemProp={'code'} + defaultText={} + labelProp={'label'} + popoverProps={{ minimal: true }} + /> + + )} + + + + {({ form, field: { value }, meta: { error, touched } }) => ( + } + labelInfo={} + className={classNames('form-group--fiscal-year', CLASSES.FILL)} + inline={true} + helperText={} + intent={inputIntent({ error, touched })} + > + } + selectedItem={value} + onItemSelect={(item) => {}} + popoverProps={{ minimal: true }} + /> + + )} + + + + {({ form, field: { value }, meta: { error, touched } }) => ( + } + labelInfo={} + inline={true} + className={classNames('form-group--language', CLASSES.FILL)} + intent={inputIntent({ error, touched })} + helperText={} + > + } + selectedItem={value} + onItemSelect={(item) => {}} + popoverProps={{ minimal: true }} + /> + + )} + + + + {({ form, field: { value }, meta: { error, touched } }) => ( + } + labelInfo={} + inline={true} + className={classNames( + 'form-group--time-zone', + CLASSES.FORM_GROUP_LIST_SELECT, + CLASSES.FILL, + )} + intent={inputIntent({ error, touched })} + helperText={} + > + { + form.setFieldValue('time_zone', timezone); + }} + valueDisplayFormat="composite" + placeholder={} + /> + + )} + + + + {({ form, field: { value }, meta: { error, touched } }) => ( + } + labelInfo={} + inline={true} + className={classNames('form-group--date-format', CLASSES.FILL)} + intent={inputIntent({ error, touched })} + helperText={} + > + { + form.setFieldValue('date_format', dateFormat); + }} + selectedItem={value} + selectedItemProp={'value'} + defaultText={} + labelProp={'name'} + popoverProps={{ minimal: true }} + /> + + )} + + +
+ + +
+
+ ); +} diff --git a/client/src/containers/Preferences/Users/Users.js b/client/src/containers/Preferences/Users/Users.js index a2973802c..ebb0405cf 100644 --- a/client/src/containers/Preferences/Users/Users.js +++ b/client/src/containers/Preferences/Users/Users.js @@ -1,20 +1,28 @@ import React from 'react'; import { Tabs, Tab } from '@blueprintjs/core'; +import classNames from 'classnames'; + +import { CLASSES } from 'common/classes'; import PreferencesSubContent from 'components/Preferences/PreferencesSubContent'; -import withUserPreferences from 'containers/Preferences/Users/withUserPreferences' +import withUserPreferences from 'containers/Preferences/Users/withUserPreferences'; function UsersPreferences({ openDialog }) { const onChangeTabs = (currentTabId) => {}; return ( -
-
- - - - +
+
+
+ + + + +
+
-
); } diff --git a/client/src/containers/Preferences/Users/UsersActions.js b/client/src/containers/Preferences/Users/UsersActions.js index 8cef6c941..9a5a5ef79 100644 --- a/client/src/containers/Preferences/Users/UsersActions.js +++ b/client/src/containers/Preferences/Users/UsersActions.js @@ -1,9 +1,9 @@ -import React, {useCallback} from 'react'; +import React from 'react'; import { Button, Intent, } from '@blueprintjs/core'; -import { FormattedMessage as T, useIntl } from 'react-intl'; +import { FormattedMessage as T } from 'react-intl'; import Icon from 'components/Icon'; import withDialogActions from 'containers/Dialog/withDialogActions'; @@ -13,9 +13,9 @@ function UsersActions({ openDialog, closeDialog, }) { - const onClickNewUser = useCallback(() => { - openDialog('user-form'); - }, [openDialog]); + const onClickNewUser = () => { + openDialog('invite-user'); + }; return (
diff --git a/client/src/containers/Preferences/Users/UsersDataTable.js b/client/src/containers/Preferences/Users/UsersDataTable.js index 13941d6da..a2c9f05d6 100644 --- a/client/src/containers/Preferences/Users/UsersDataTable.js +++ b/client/src/containers/Preferences/Users/UsersDataTable.js @@ -12,18 +12,18 @@ import { import { withRouter } from 'react-router'; import { snakeCase } from 'lodash'; -import { - FormattedMessage as T, - FormattedHTMLMessage, - useIntl, -} from 'react-intl'; -import { compose } from 'utils'; -import LoadingIndicator from 'components/LoadingIndicator'; -import { DataTable, Icon, If, AppToaster } from 'components'; +import { FormattedMessage as T, useIntl } from 'react-intl'; +import { compose, firstLettersArgs } from 'utils'; + +import { DataTable, Icon, If } from 'components'; import withDialogActions from 'containers/Dialog/withDialogActions'; import withUsers from 'containers/Users/withUsers'; +const AvatarCell = (row) => { + return { firstLettersArgs(row.email) }; +} + function UsersDataTable({ // #withDialogActions openDialog, @@ -39,7 +39,6 @@ function UsersDataTable({ onDeleteUser, onSelectedRowsChange, }) { - const [initialMount, setInitialMount] = useState(false); const { formatMessage } = useIntl(); const onEditUser = useCallback( @@ -80,7 +79,7 @@ function UsersDataTable({ /> ), - [onInactiveUser, onDeleteUser, onEditUser], + [onInactiveUser, onDeleteUser, onEditUser, formatMessage], ); const onRowContextMenu = useCallback( (cell) => { @@ -91,6 +90,12 @@ function UsersDataTable({ const columns = useMemo( () => [ + { + id: 'avatar', + Header: '', + accessor: AvatarCell, + width: 100, + }, { id: 'full_name', Header: formatMessage({ id: 'full_name' }), @@ -154,27 +159,15 @@ function UsersDataTable({ }, [onFetchData], ); - const handleSelectedRowsChange = useCallback( - (selectedRows) => { - onSelectedRowsChange && - onSelectedRowsChange(selectedRows.map((s) => s.original)); - }, - [onSelectedRowsChange], - ); - return ( - - - + ); } diff --git a/client/src/containers/Preferences/Users/UsersList.js b/client/src/containers/Preferences/Users/UsersList.js index 16ad43b01..e72bee679 100644 --- a/client/src/containers/Preferences/Users/UsersList.js +++ b/client/src/containers/Preferences/Users/UsersList.js @@ -1,47 +1,32 @@ -import React, { useState, useMemo, useCallback, useEffect } from 'react'; +import React, { useState, useCallback, useEffect } from 'react'; import { queryCache, useQuery } from 'react-query'; import { Alert, Intent } from '@blueprintjs/core'; -import withDialogActions from 'containers/Dialog/withDialogActions'; import withDashboardActions from 'containers/Dashboard/withDashboardActions'; -import withUsers from 'containers/Users/withUsers'; -import UsersDataTable from './UsersDataTable'; import withUsersActions from 'containers/Users/withUsersActions'; -import DashboardInsider from 'components/Dashboard/DashboardInsider'; -import DashboardPageContent from 'components/Dashboard/DashboardPageContent'; +import UsersDataTable from './UsersDataTable'; import { FormattedMessage as T, FormattedHTMLMessage, useIntl, } from 'react-intl'; -import { snakeCase } from 'lodash'; import AppToaster from 'components/AppToaster'; import { compose } from 'utils'; function UsersListPreferences({ - // #withDialog - openDialog, - // #withDashboardActions changePreferencesPageTitle, - // #withUsers - usersList, - // #withUsersActions requestDeleteUser, requestInactiveUser, requestFetchUsers, - - // #ownProps - onFetchData, }) { const [deleteUserState, setDeleteUserState] = useState(false); const [inactiveUserState, setInactiveUserState] = useState(false); - const [selectedRows, setSelectedRows] = useState([]); const { formatMessage } = useIntl(); @@ -91,9 +76,6 @@ function UsersListPreferences({ const handleEditUser = useCallback(() => {}, []); - - - // Handle confirm User delete const handleConfirmUserDelete = useCallback(() => { if (!deleteUserState) { @@ -115,63 +97,44 @@ function UsersListPreferences({ }); }, [deleteUserState, requestDeleteUser, formatMessage]); - // const handelDataTableFetchData = useCallback(() => { - // onFetchData && onFetchData(); - // }, [onFetchData]); - - // Handle selected rows change. - const handleSelectedRowsChange = useCallback( - (accounts) => { - setSelectedRows(accounts); - }, - [setSelectedRows], - ); return ( - - - - - } - confirmButtonText={} - icon="trash" - intent={Intent.DANGER} - isOpen={deleteUserState} - onCancel={handleCancelUserDelete} - onConfirm={handleConfirmUserDelete} - > -

- -

-
- } - confirmButtonText={} - intent={Intent.WARNING} - isOpen={inactiveUserState} - onCancel={handleCancelInactiveUser} - onConfirm={handleConfirmUserActive} - > -

- -

-
-
-
+ <> + + } + confirmButtonText={} + intent={Intent.DANGER} + isOpen={deleteUserState} + onCancel={handleCancelUserDelete} + onConfirm={handleConfirmUserDelete} + > +

+ Once you delete this user, you won't be able to restore it later. Are you sure you want to delete ? +

+
+ } + confirmButtonText={} + intent={Intent.WARNING} + isOpen={inactiveUserState} + onCancel={handleCancelInactiveUser} + onConfirm={handleConfirmUserActive} + > +

+ +

+
+ ); } export default compose( - withDialogActions, withDashboardActions, withUsersActions, )(UsersListPreferences); diff --git a/client/src/lang/en/index.js b/client/src/lang/en/index.js index 0ee9bb4ef..f141cf36c 100644 --- a/client/src/lang/en/index.js +++ b/client/src/lang/en/index.js @@ -854,4 +854,5 @@ export default { save_and_send: 'Save and Send', posting_date: 'Posting date', customer: 'Customer', + email_is_already_used: 'The email is already used.', }; diff --git a/client/src/routes/preferences.js b/client/src/routes/preferences.js index d751c0b40..7c6a5faf1 100644 --- a/client/src/routes/preferences.js +++ b/client/src/routes/preferences.js @@ -2,7 +2,7 @@ import General from 'containers/Preferences/General/General'; import Users from 'containers/Preferences/Users/Users'; import Accountant from 'containers/Preferences/Accountant/Accountant'; import Accounts from 'containers/Preferences/Accounts/Accounts'; -import CurrenciesList from 'containers/Preferences/Currencies/CurrenciesList' +import Currencies from 'containers/Preferences/Currencies/Currencies' const BASE_URL = '/preferences'; @@ -19,7 +19,7 @@ export default [ }, { path: `${BASE_URL}/currencies`, - component: CurrenciesList, + component: Currencies, exact: true, }, { @@ -27,8 +27,4 @@ export default [ component: Accountant, exact: true, }, - { - path: `${BASE_URL}/accounts`, - component: Accounts, - }, ]; diff --git a/client/src/store/currencies/currencies.reducer.js b/client/src/store/currencies/currencies.reducer.js index 92119e61a..d8aa7d108 100644 --- a/client/src/store/currencies/currencies.reducer.js +++ b/client/src/store/currencies/currencies.reducer.js @@ -3,6 +3,7 @@ import t from 'store/types'; const initialState = { data: {}, + loading: false, }; export default createReducer(initialState, { diff --git a/client/src/store/users/users.actions.js b/client/src/store/users/users.actions.js index 1e50a3b1f..729f81d62 100644 --- a/client/src/store/users/users.actions.js +++ b/client/src/store/users/users.actions.js @@ -49,7 +49,16 @@ export const deleteUser = ({ id }) => { }; export const submitInvite = ({ form }) => { - return (dispatch) => ApiService.post(`invite/send`, form); + return (dispatch) => new Promise((resolve, reject) => { + ApiService.post(`invite/send`, form) + .then((response) => { resolve(response); }) + .catch((error) => { + const { response } = error; + const { data } = response; + + reject(data?.errors); + }); + }); }; export const editUser = ({ form, id }) => { diff --git a/client/src/style/App.scss b/client/src/style/App.scss index 895aa9831..cafaad53f 100644 --- a/client/src/style/App.scss +++ b/client/src/style/App.scss @@ -587,4 +587,16 @@ body.authentication { .dropzone-container{ max-width: 250px; margin-left: auto; +} + +.card{ + background: #fff; + border: 1px solid #d2dce2; +} + + +.form-group--select-list{ + button{ + justify-content: start; + } } \ No newline at end of file diff --git a/client/src/style/pages/dashboard.scss b/client/src/style/pages/dashboard.scss index d45db8e6b..2d27483ce 100644 --- a/client/src/style/pages/dashboard.scss +++ b/client/src/style/pages/dashboard.scss @@ -355,20 +355,7 @@ } &__preferences-topbar{ - border-bottom: 1px solid #E5E5E5; - height: 65px; - padding: 0 0 0 22px; - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - - h2{ - font-size: 22px; - font-weight: 300; - margin: 0; - color: #555; - } + } &__footer{ diff --git a/client/src/style/pages/preferences.scss b/client/src/style/pages/preferences.scss index 55c14ab47..c0602e900 100644 --- a/client/src/style/pages/preferences.scss +++ b/client/src/style/pages/preferences.scss @@ -1,16 +1,37 @@ .dashboard-content--preferences { - // margin-left: 430px; - margin-left: 410px; - // width: 100%; - position: relative; + flex-direction: row; + flex: 1 0 0; + min-width: auto; + background-color: #fbfbfb; } - -.preferences { - &__inside-content--tabable { - margin-left: -25px; - margin-right: -25px; +.dashboard { + &__preferences-content { + flex: 1 0 0; + display: flex; + flex-direction: column; } +} +.preferences-page { + &__inside-content { + display: flex; + flex-direction: column; + height: 100%; + &--tabable { + margin-left: -25px; + margin-right: -25px; + } + + .card{ + margin: 15px; + } + .bigcapital-datatable{ + + .table .tbody .tbody-inner > .loading{ + padding: 30px 0; + } + } + } &__inside-content { .#{$ns}-tab-list { border-bottom: 1px solid #e5e5e5; @@ -29,8 +50,75 @@ &__tabs-extra-actions { margin-left: auto; } +} - &__topbar-actions { +.preferences-page { + display: flex; + flex: 1 0 0; + + &__content { + flex: 1 0 0; + display: flex; + flex-direction: column; + margin-left: 220px; + + .dashboard__card { + margin: 15px; + flex: 1 0 0; + } + } +} + +// Preferences topbar. +// ----------------------------- +.preferences-topbar { + border-bottom: 1px solid #d2dde2; + min-height: 60px; + flex: 60px 0 0; + padding: 0 0 0 22px; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + background-color: #fff; + + &__title { + h2 { + font-size: 22px; + font-weight: 400; + margin: 0; + color: #48485b; + } + } + + &__user { + display: flex; + align-items: center; + margin-right: 14px; + + .bp3-button:not([class*='bp3-intent-']):not(.bp3-minimal) { + padding: 0; + background-size: contain; + background-color: #eed1f2; + border-radius: 50%; + height: 35px; + width: 35px; + + .user-text { + font-size: 12px; + color: #804f87; + } + &, + &:hover, + &:focus { + background-color: #eed1f2; + border: 0; + box-shadow: none; + } + } + } + + &__actions { margin-left: auto; padding-right: 15px; margin-right: 15px; @@ -40,51 +128,60 @@ margin-left: 10px; } } - - &-button { - margin-right: 10px; - } - - &__floating-footer { - position: fixed; - bottom: 0; - left: 430px; - right: 0; - background: #fff; - padding: 10px 18px; - border-top: 1px solid #ececec; - } } -.preferences__sidebar { - background: #fdfdfd; - position: fixed; - left: 190px; - top: -5px; +// Preferences sidebar. +// ----------------------------- +.preferences-sidebar { + background: #e5eaee; + border-right: 1px solid #c6d0d9; min-width: 220px; max-width: 220px; height: 100%; + position: fixed; - .sidebar-wrapper { + &__wrapper { + height: 100%; } - &-head { + .ScrollbarsCustom-Track { + &.ScrollbarsCustom-TrackY, + &.ScrollbarsCustom-TrackX{ + background: rgba(0, 0, 0, 0); + } + } + .ScrollbarsCustom-Thumb{ + &.ScrollbarsCustom-ThumbX, + &.ScrollbarsCustom-ThumbY { + background: rgba(0, 0, 0, 0); + } + } + + &:hover { + .ScrollbarsCustom-Thumb{ + &.ScrollbarsCustom-ThumbX, + &.ScrollbarsCustom-ThumbY { + background: rgba(0, 0, 0, 0.15); + } + } + } + + &__head { display: flex; flex-direction: row; align-items: center; - border-bottom: 1px solid #e5e5e5; - height: 70px; + height: 60px; padding: 0 22px; h2 { font-size: 22px; - font-weight: 300; - color: #555; + color: #48485b; + font-weight: 400; margin: 0; } } - &-menu { + &__menu { padding: 0; background: transparent; @@ -96,69 +193,124 @@ &:hover, &.#{$ns}-active { - background-color: #ebf1f5; + background-color: rgba(255, 255, 255, 0.5); color: #333; } } } } -// Preference +// General page //--------------------------------- -.preferences__inside-content--general { - margin: 20px; - .bp3-form-group { - margin: 18px 18px; - .bp3-label { - min-width: 180px; - } - .bp3-form-content { - width: 50%; +.preferences-page__inside-content--general { + .card { + padding: 25px; + + .card__footer { + padding-top: 16px; + border-top: 1px solid #e0e7ea; + margin-top: 30px; + + .bp3-button { + min-width: 60px; + + + .bp3-button{ + margin-left: 10px; + } + } } } - .form-group--org-name, - .form-group--org-industry { + .bp3-form-group { + max-width: 550px; + + .bp3-label { + min-width: 180px; + } + .bp3-form-content { - position: relative; - width: 70%; - } - } - .form-group--time-zone { - .bp3-menu { - max-height: 300px; - max-width: 400px; - overflow: auto; - padding: 0; - } - .bp3-text-muted { - color: #000000; - } - .bp3-button:not([class*='bp3-intent-']):not(.bp3-minimal) { - background: #fff; - box-shadow: 0 0 0 transparent; - border: 1px solid #ced4da; - padding: 8px; - } - .bp3-button .bp3-icon:first-child:last-child, - .bp3-button .bp3-spinner + .bp3-icon:last-child { - position: absolute; - right: 15px; - display: inline-block; + width: 100%; } } } // Users/Roles List. // --------------------------------- -.preferences__inside-content--users-roles { +.preferences-page__inside-content--users{ + .bigcapital-datatable { - .table .th, - .table .td { - padding: 0.8rem 0.5rem; + + .td{ + .avatar{ + height: 28px; + width: 28px; + text-align: center; + background: #b7bfc6; + border-radius: 50%; + line-height: 28px; + color: #fff; + text-transform: uppercase; + margin-left: 10px; + } } .td.status { text-transform: uppercase; } + + .tr:last-child .td{ + border-bottom: 0; + } } } + +// Currencies List. +// --------------------------------- +.preferences-page__inside-content--currencies{ + + .bigcapital-datatable { + + .tr:last-child .td{ + border-bottom: 0; + } + } +} + +// Accountant. +// --------------------------------- +.preferences-page__inside-content--accountant { + .card { + padding: 25px; + + .card__footer { + padding-top: 16px; + border-top: 1px solid #e0e7ea; + margin-top: 30px; + + .bp3-button { + min-width: 60px; + + + .bp3-button{ + margin-left: 10px; + } + } + } + } + + .form-group--select-list{ + + button{ + min-width: 250px; + } + } + + .bp3-form-group { + + .bp3-form-helper-text{ + margin-top: 7px; + } + + label.bp3-label { + margin-bottom: 7px; + } + } +} \ No newline at end of file diff --git a/server/src/api/controllers/ItemCategories.ts b/server/src/api/controllers/ItemCategories.ts index 10c9255c0..d36d75643 100644 --- a/server/src/api/controllers/ItemCategories.ts +++ b/server/src/api/controllers/ItemCategories.ts @@ -89,7 +89,7 @@ export default class ItemsCategoriesController extends BaseController { .isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 }) .toInt(), check('description') - .optional() + .optional({ nullable: true }) .isString() .trim() .escape()