diff --git a/client/src/components/AccountsMultiSelect.js b/client/src/components/AccountsMultiSelect.js index 5fcfa0b24..0e00a40e4 100644 --- a/client/src/components/AccountsMultiSelect.js +++ b/client/src/components/AccountsMultiSelect.js @@ -6,6 +6,7 @@ import { } from '@blueprintjs/core'; // import {Select} from '@blueprintjs/select'; import MultiSelect from 'components/MultiSelect'; +import { FormattedMessage as T, useIntl } from 'react-intl'; export default function AccountsMultiSelect({ accounts, @@ -58,7 +59,7 @@ export default function AccountsMultiSelect({ - - + + diff --git a/client/src/components/Expenses/ExpensesActionsBar.js b/client/src/components/Expenses/ExpensesActionsBar.js index 049bd4911..79d02e13a 100644 --- a/client/src/components/Expenses/ExpensesActionsBar.js +++ b/client/src/components/Expenses/ExpensesActionsBar.js @@ -16,6 +16,7 @@ import { useRouteMatch } from 'react-router-dom' import classNames from 'classnames'; import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar'; import Icon from 'components/Icon'; +import { FormattedMessage as T, useIntl } from 'react-intl'; export default function ExpensesActionsBar({ @@ -39,7 +40,7 @@ export default function ExpensesActionsBar({ diff --git a/client/src/components/index.js b/client/src/components/index.js index 9cedc0cd1..08fdca144 100644 --- a/client/src/components/index.js +++ b/client/src/components/index.js @@ -1,9 +1,11 @@ import If from './Utils/If'; +import Money from './Money'; // import Choose from './Utils/Choose'; // import For from './Utils/For'; export { If, + Money, // Choose, // For, }; \ No newline at end of file diff --git a/client/src/config/sidebarMenu.js b/client/src/config/sidebarMenu.js index f1eb8028a..8798155f2 100644 --- a/client/src/config/sidebarMenu.js +++ b/client/src/config/sidebarMenu.js @@ -1,17 +1,17 @@ export default [ { - divider: true + divider: true, }, { icon: 'homepage', iconSize: 20, text: 'Homepage', disabled: false, - href: '/dashboard/homepage' + href: '/dashboard/homepage', }, { - divider: true + divider: true, }, { icon: 'homepage', @@ -20,20 +20,20 @@ export default [ children: [ { text: 'Items List', - href: '/dashboard/items' + href: '/dashboard/items', }, { text: 'New Item', - href: '/dashboard/items/new' + href: '/dashboard/items/new', }, { text: 'Category List', - href: '/dashboard/items/categories' + href: '/dashboard/items/categories', }, - ] + ], }, { - divider: true + divider: true, }, { icon: 'balance-scale', @@ -42,29 +42,33 @@ export default [ children: [ { text: 'Accounts Chart', - href: '/dashboard/accounts' + href: '/dashboard/accounts', }, { text: 'Manual Journal', - href: '/dashboard/accounting/manual-journals' + href: '/dashboard/accounting/manual-journals', }, { text: 'Make Journal', - href: '/dashboard/accounting/make-journal-entry' + href: '/dashboard/accounting/make-journal-entry', }, - ] + { + text: 'Exchange Rate', + href: '/dashboard/ExchangeRates', + }, + ], }, { icon: 'university', iconSize: 20, text: 'Banking', - children: [] + children: [], }, { icon: 'shopping-cart', iconSize: 20, text: 'Sales', - children: [] + children: [], }, { icon: 'balance-scale', @@ -75,9 +79,9 @@ export default [ icon: 'cut', text: 'cut', label: '⌘C', - disabled: false - } - ] + disabled: false, + }, + ], }, { icon: 'analytics', @@ -86,25 +90,25 @@ export default [ children: [ { text: 'Balance Sheet', - href: '/dashboard/accounting/balance-sheet' + href: '/dashboard/accounting/balance-sheet', }, { text: 'Trial Balance Sheet', - href: '/dashboard/accounting/trial-balance-sheet' + href: '/dashboard/accounting/trial-balance-sheet', }, { text: 'Journal', - href: '/dashboard/accounting/journal-sheet' + href: '/dashboard/accounting/journal-sheet', }, { text: 'General Ledger', - href: '/dashboard/accounting/general-ledger' + href: '/dashboard/accounting/general-ledger', }, { text: 'Profit Loss Sheet', - href: '/dashboard/accounting/profit-loss-sheet' - } - ] + href: '/dashboard/accounting/profit-loss-sheet', + }, + ], }, { text: 'Expenses', @@ -113,23 +117,23 @@ export default [ children: [ { text: 'Expenses List', - href: '/dashboard/expenses' + href: '/dashboard/expenses', }, { text: 'New Expenses', - href: '/dashboard/expenses/new' - } - ] + href: '/dashboard/expenses/new', + }, + ], }, { - divider: true + divider: true, }, { text: 'Preferences', - href: '/dashboard/preferences' + href: '/dashboard/preferences', }, { text: 'Auditing System', - href: '/dashboard/auditing/list' - } + href: '/dashboard/auditing/list', + }, ]; diff --git a/client/src/containers/Accounting/MakeJournalEntriesFooter.js b/client/src/containers/Accounting/MakeJournalEntriesFooter.js index bd2549ace..0b90ed418 100644 --- a/client/src/containers/Accounting/MakeJournalEntriesFooter.js +++ b/client/src/containers/Accounting/MakeJournalEntriesFooter.js @@ -1,9 +1,6 @@ -import React, {useMemo} from 'react'; -import { - Intent, - Button, -} from '@blueprintjs/core'; -import { FormattedList } from 'react-intl'; +import React, { useMemo } from 'react'; +import { Intent, Button } from '@blueprintjs/core'; +import { FormattedMessage as T, useIntl } from 'react-intl'; export default function MakeJournalEntriesFooter({ formik: { isSubmitting }, @@ -12,15 +9,16 @@ export default function MakeJournalEntriesFooter({ }) { return (
- ); -} \ No newline at end of file +} diff --git a/client/src/containers/Accounting/MakeJournalEntriesForm.js b/client/src/containers/Accounting/MakeJournalEntriesForm.js index 3754710f7..3115e8418 100644 --- a/client/src/containers/Accounting/MakeJournalEntriesForm.js +++ b/client/src/containers/Accounting/MakeJournalEntriesForm.js @@ -1,9 +1,10 @@ import React, {useMemo, useState, useEffect, useRef, useCallback} from 'react'; import * as Yup from 'yup'; -import {useFormik} from "formik"; +import { useFormik } from "formik"; import moment from 'moment'; import { Intent } from '@blueprintjs/core'; -import { useIntl } from 'react-intl'; +import { FormattedMessage as T, useIntl } from 'react-intl'; +import { pick } from 'lodash'; import MakeJournalEntriesHeader from './MakeJournalEntriesHeader'; import MakeJournalEntriesFooter from './MakeJournalEntriesFooter'; @@ -15,7 +16,6 @@ import withAccountsActions from 'containers/Accounts/withAccountsActions'; import withDashboardActions from 'containers/Dashboard/withDashboard'; import AppToaster from 'components/AppToaster'; -import {pick} from 'lodash'; import Dragzone from 'components/Dragzone'; import MediaConnect from 'connectors/Media.connect'; @@ -55,10 +55,10 @@ function MakeJournalEntriesForm({ useEffect(() => { if (manualJournal && manualJournal.id) { - changePageTitle('Edit Journal'); + changePageTitle(formatMessage({id:'edit_journal'})); changePageSubtitle(`No. ${manualJournal.journal_number}`); } else { - changePageTitle('New Journal'); + changePageTitle(formatMessage({id:'new_journal'})); } }, [changePageTitle, changePageSubtitle, manualJournal]); diff --git a/client/src/containers/Accounting/MakeJournalEntriesHeader.js b/client/src/containers/Accounting/MakeJournalEntriesHeader.js index 94962a562..8a766f018 100644 --- a/client/src/containers/Accounting/MakeJournalEntriesHeader.js +++ b/client/src/containers/Accounting/MakeJournalEntriesHeader.js @@ -6,7 +6,7 @@ import { Position, } from '@blueprintjs/core'; import {DateInput} from '@blueprintjs/datetime'; -import {useIntl} from 'react-intl'; +import { FormattedMessage as T, useIntl } from 'react-intl'; import {Row, Col} from 'react-grid-system'; import moment from 'moment'; import {momentFormatter} from 'utils'; @@ -17,7 +17,7 @@ import ErrorMessage from 'components/ErrorMessage'; export default function MakeJournalEntriesHeader({ formik: { errors, touched, setFieldValue, getFieldProps } }) { - const intl = useIntl(); + const {formatMessage} = useIntl(); const handleDateChange = useCallback((date) => { const formatted = moment(date).format('YYYY-MM-DD'); @@ -32,7 +32,7 @@ export default function MakeJournalEntriesHeader({ } labelInfo={infoIcon} className={'form-group--journal-number'} intent={(errors.journal_number && touched.journal_number) && Intent.DANGER} @@ -48,7 +48,7 @@ export default function MakeJournalEntriesHeader({ } intent={(errors.date && touched.date) && Intent.DANGER} helperText={} minimal={true}> @@ -63,7 +63,7 @@ export default function MakeJournalEntriesHeader({ } className={'form-group--description'} intent={(errors.name && touched.name) && Intent.DANGER} helperText={} @@ -80,7 +80,7 @@ export default function MakeJournalEntriesHeader({ } labelInfo={infoIcon} className={'form-group--reference'} intent={(errors.reference && touched.reference) && Intent.DANGER} diff --git a/client/src/containers/Accounting/MakeJournalEntriesTable.js b/client/src/containers/Accounting/MakeJournalEntriesTable.js index 54ec3cd48..55aceaab7 100644 --- a/client/src/containers/Accounting/MakeJournalEntriesTable.js +++ b/client/src/containers/Accounting/MakeJournalEntriesTable.js @@ -1,11 +1,8 @@ -import React, {useState, useMemo, useEffect, useCallback} from 'react'; -import { - Button, - Intent, -} from '@blueprintjs/core'; +import React, { useState, useMemo, useEffect, useCallback } from 'react'; +import { Button, Intent } from '@blueprintjs/core'; import DataTable from 'components/DataTable'; import Icon from 'components/Icon'; -import { compose, formattedAmount} from 'utils'; +import { compose, formattedAmount } from 'utils'; import { AccountsListFieldCell, MoneyFieldCell, @@ -14,7 +11,7 @@ import { import { omit } from 'lodash'; import withAccounts from 'containers/Accounts/withAccounts'; - +import { FormattedMessage as T, useIntl } from 'react-intl'; // Actions cell renderer. const ActionsCellRenderer = ({ @@ -24,55 +21,54 @@ const ActionsCellRenderer = ({ data, payload, }) => { - if (data.length <= (index + 2)) { + if (data.length <= index + 2) { return ''; } const onClickRemoveRole = () => { payload.removeRow(index); }; - return ( -
@@ -232,4 +243,4 @@ export default compose( withAccounts(({ accounts }) => ({ accounts, })), -)(MakeJournalEntriesTable); \ No newline at end of file +)(MakeJournalEntriesTable); diff --git a/client/src/containers/Accounting/ManualJournalActionsBar.js b/client/src/containers/Accounting/ManualJournalActionsBar.js index 43fe4a648..28423351b 100644 --- a/client/src/containers/Accounting/ManualJournalActionsBar.js +++ b/client/src/containers/Accounting/ManualJournalActionsBar.js @@ -25,6 +25,7 @@ import withResourceDetail from 'containers/Resources/withResourceDetails'; import withManualJournals from 'containers/Accounting/withManualJournals'; import withManualJournalsActions from 'containers/Accounting/withManualJournalsActions'; +import { FormattedMessage as T, useIntl } from 'react-intl'; function ManualJournalActionsBar({ // #withResourceDetail @@ -43,6 +44,7 @@ function ManualJournalActionsBar({ }) { const { path } = useRouteMatch(); const history = useHistory(); + const {formatMessage} = useIntl(); const viewsMenuItems = manualJournalsViews.map(view => { return ( @@ -82,7 +84,7 @@ function ManualJournalActionsBar({ - { inviteMeta.pending && ( -
+ {inviteMeta.pending && ( +
)} @@ -225,6 +269,4 @@ function Invite({ ); } -export default compose( - withAuthenticationActions, -)(Invite); +export default compose(withAuthenticationActions)(Invite); diff --git a/client/src/containers/Authentication/Login.js b/client/src/containers/Authentication/Login.js index 2c5f30723..bc6f1925b 100644 --- a/client/src/containers/Authentication/Login.js +++ b/client/src/containers/Authentication/Login.js @@ -69,7 +69,7 @@ function Login({ const toastBuilders = []; if (errors.find((e) => e.type === ERRORS_TYPES.INVALID_DETAILS)) { toastBuilders.push({ - message: formatMessage('email_and_password_entered_did_not_match'), + message: formatMessage({id:'email_and_password_entered_did_not_match'}), intent: Intent.DANGER, }); } @@ -102,7 +102,7 @@ function Login({

- +
diff --git a/client/src/containers/Authentication/Register.js b/client/src/containers/Authentication/Register.js index 384074544..fc7d9233c 100644 --- a/client/src/containers/Authentication/Register.js +++ b/client/src/containers/Authentication/Register.js @@ -1,13 +1,14 @@ import React, { useMemo, useState, useCallback } from 'react'; import * as Yup from 'yup'; import { useFormik } from 'formik'; -import { useIntl } from 'react-intl'; +import { FormattedMessage as T, useIntl } from 'react-intl'; + import { Button, InputGroup, Intent, FormGroup, - Spinner + Spinner, } from '@blueprintjs/core'; import { Row, Col } from 'react-grid-system'; import { Link, useHistory } from 'react-router-dom'; @@ -19,35 +20,40 @@ import { compose } from 'utils'; import Icon from 'components/Icon'; import { If } from 'components'; -function Register({ - requestRegister, -}) { - const intl = useIntl(); +function Register({ requestRegister }) { + const { formatMessage } = useIntl(); const history = useHistory(); const [shown, setShown] = useState(false); - const passwordRevealer = useCallback(() => { setShown(!shown); }, [shown]); + const passwordRevealer = useCallback(() => { + setShown(!shown); + }, [shown]); const ValidationSchema = Yup.object().shape({ - organization_name: Yup.string().required(), - first_name: Yup.string().required(), - last_name: Yup.string().required(), - email: Yup.string().email().required(), + organization_name: Yup.string().required(formatMessage({ id: 'required' })), + first_name: Yup.string().required(formatMessage({ id: 'required' })), + last_name: Yup.string().required(formatMessage({ id: 'required' })), + email: Yup.string() + .email() + .required(formatMessage({ id: 'required' })), phone_number: Yup.string() .matches() - .required(intl.formatMessage({ id: 'required' })), + .required(formatMessage({ id: 'required' })), password: Yup.string() .min(4, 'Password has to be longer than 8 characters!') .required('Password is required!'), }); - const initialValues = useMemo(() => ({ - organization_name: '', - first_name: '', - last_name: '', - email: '', - phone_number: '', - password: '', - }), []); + const initialValues = useMemo( + () => ({ + organization_name: '', + first_name: '', + last_name: '', + email: '', + phone_number: '', + password: '', + }), + [] + ); const { errors, @@ -62,26 +68,27 @@ function Register({ validationSchema: ValidationSchema, initialValues: { ...initialValues, - country: 'libya' + country: 'libya', }, onSubmit: (values, { setSubmitting, setErrors }) => { requestRegister(values) .then((response) => { AppToaster.show({ - message: 'success', + message: formatMessage({ id: 'success' }), }); setSubmitting(false); history.push('/auth/login'); }) .catch((errors) => { - if (errors.some(e => e.type === 'PHONE_NUMBER_EXISTS')) { + if (errors.some((e) => e.type === 'PHONE_NUMBER_EXISTS')) { setErrors({ - phone_number: 'The phone number is already used in another account.' + phone_number: + 'The phone number is already used in another account.', }); } - if (errors.some(e => e.type === 'EMAIL_EXISTS')) { + if (errors.some((e) => e.type === 'EMAIL_EXISTS')) { setErrors({ - email: 'The email is already used in another account.' + email: 'The email is already used in another account.', }); } setSubmitting(false); @@ -89,34 +96,66 @@ function Register({ }, }); - const passwordRevealerTmp = useMemo(() => ( - passwordRevealer()}> - {(shown) ? ( - <> Hide - ) : ( - <> Show - )} - ), [shown, passwordRevealer]); + const passwordRevealerTmp = useMemo( + () => ( + passwordRevealer()}> + + <> + {' '} + + + + + + + <> + {' '} + + + + + + + ), + [shown, passwordRevealer] + ); return (

- Register a New
Organization. +

- You have a bigcapital account ? Login + + + {' '} + +
} className={'form-group--name'} - intent={(errors.organization_name && touched.organization_name) && Intent.DANGER} - helperText={} + intent={ + errors.organization_name && + touched.organization_name && + Intent.DANGER + } + helperText={ + + } > @@ -124,13 +163,19 @@ function Register({ } + label={} + intent={ + errors.first_name && touched.first_name && Intent.DANGER + } + helperText={ + + } className={'form-group--first-name'} > @@ -138,65 +183,83 @@ function Register({ } + label={} + intent={errors.last_name && touched.last_name && Intent.DANGER} + helperText={ + + } className={'form-group--last-name'} > - + } + label={} + intent={ + errors.phone_number && touched.phone_number && Intent.DANGER + } + helperText={ + + } className={'form-group--phone-number'} > - + } + label={} + intent={errors.email && touched.email && Intent.DANGER} + helperText={ + + } className={'form-group--email'} > - + } labelInfo={passwordRevealerTmp} - intent={(errors.password && touched.password) && Intent.DANGER} - helperText={} + intent={errors.password && touched.password && Intent.DANGER} + helperText={ + + } className={'form-group--password has-password-revealer'} > - +
-

- By signing in or creating an account, you agree with our
- Terms & Conditions and Privacy Statement +

+
+ + + {' '} + + + {' '} + +

@@ -208,13 +271,13 @@ function Register({ fill={true} loading={isSubmitting} > - Register +
-
+
@@ -223,6 +286,4 @@ function Register({ ); } -export default compose( - withAuthenticationActions, -)(Register); +export default compose(withAuthenticationActions)(Register); diff --git a/client/src/containers/Authentication/ResetPassword.js b/client/src/containers/Authentication/ResetPassword.js index 7da28b8b0..43d5d3f75 100644 --- a/client/src/containers/Authentication/ResetPassword.js +++ b/client/src/containers/Authentication/ResetPassword.js @@ -1,7 +1,7 @@ import React, { useEffect, useMemo } from 'react'; import * as Yup from 'yup'; import { useFormik } from 'formik'; -import { useIntl } from 'react-intl'; + import { Button, InputGroup, @@ -15,12 +15,10 @@ import AppToaster from 'components/AppToaster'; import { compose } from 'utils'; import withAuthenticationActions from './withAuthenticationActions'; import AuthInsider from 'containers/Authentication/AuthInsider'; +import { FormattedMessage as T, useIntl } from 'react-intl'; - -function ResetPassword({ - requestResetPassword, -}) { - const intl = useIntl(); +function ResetPassword({ requestResetPassword }) { + const { formatMessage } = useIntl(); const { token } = useParams(); const history = useHistory(); @@ -33,10 +31,13 @@ function ResetPassword({ .required('Confirm Password is required'), }); - const initialValues = useMemo(() => ({ - password: '', - confirm_password: '', - }), []); + const initialValues = useMemo( + () => ({ + password: '', + confirm_password: '', + }), + [] + ); const { touched, @@ -56,7 +57,7 @@ function ResetPassword({ requestResetPassword(values, token) .then((response) => { AppToaster.show({ - message: 'The password for your account was successfully updated.', + message: formatMessage('password_successfully_updated'), intent: Intent.DANGER, position: Position.BOTTOM, }); @@ -64,9 +65,9 @@ function ResetPassword({ setSubmitting(false); }) .catch((errors) => { - if (errors.find(e => e.type === 'TOKEN_INVALID')) { + if (errors.find((e) => e.type === 'TOKEN_INVALID')) { AppToaster.show({ - message: 'An unexpected error occurred', + message: formatMessage('an_unexpected_error_occurred'), intent: Intent.DANGER, position: Position.BOTTOM, }); @@ -79,17 +80,24 @@ function ResetPassword({ return ( -
+
-

Choose a new password

- You remembered your password ? Login +

+ +

+ {' '} + + +
} + label={} + intent={errors.password && touched.password && Intent.DANGER} + helperText={ + + } className={'form-group--password'} > - + } labelInfo={'(again):'} - intent={(errors.confirm_password && touched.confirm_password) && Intent.DANGER} - helperText={} + intent={ + errors.confirm_password && + touched.confirm_password && + Intent.DANGER + } + helperText={ + + } className={'form-group--confirm-password'} > @@ -121,8 +142,9 @@ function ResetPassword({ className={'btn-new'} intent={Intent.PRIMARY} type='submit' - loading={isSubmitting}> - Submit new password + loading={isSubmitting} + > +
@@ -131,6 +153,4 @@ function ResetPassword({ ); } -export default compose( - withAuthenticationActions, -)(ResetPassword); +export default compose(withAuthenticationActions)(ResetPassword); diff --git a/client/src/containers/Authentication/SendResetPassword.js b/client/src/containers/Authentication/SendResetPassword.js index f33accec8..ee85ed052 100644 --- a/client/src/containers/Authentication/SendResetPassword.js +++ b/client/src/containers/Authentication/SendResetPassword.js @@ -1,7 +1,7 @@ import React, { useMemo } from 'react'; import * as Yup from 'yup'; import { useFormik } from 'formik'; -import { useIntl } from 'react-intl'; +import { FormattedMessage as T, useIntl } from 'react-intl'; import { Link, useHistory } from 'react-router-dom'; import { Button, InputGroup, Intent, FormGroup } from '@blueprintjs/core'; import { FormattedMessage } from 'react-intl'; @@ -15,23 +15,23 @@ import AuthInsider from 'containers/Authentication/AuthInsider'; import withAuthenticationActions from './withAuthenticationActions'; - -function SendResetPassword({ - requestSendResetPassword, -}) { - const intl = useIntl(); +function SendResetPassword({ requestSendResetPassword }) { + const { formatMessage } = useIntl(); const history = useHistory(); // Validation schema. const ValidationSchema = Yup.object().shape({ crediential: Yup.string('') - .required(intl.formatMessage({ id: 'required' })) - .email(intl.formatMessage({ id: 'invalid_email_or_phone_numner' })), + .required(formatMessage({ id: 'required' })) + .email(formatMessage({ id: 'invalid_email_or_phone_numner' })), }); - const initialValues = useMemo(() => ({ - crediential: '', - }), []); + const initialValues = useMemo( + () => ({ + crediential: '', + }), + [] + ); // Formik validation const { @@ -60,9 +60,9 @@ function SendResetPassword({ setSubmitting(false); }) .catch((errors) => { - if (errors.find(e => e.type === 'EMAIL.NOT.REGISTERED')){ + if (errors.find((e) => e.type === 'EMAIL.NOT.REGISTERED')) { AppToaster.show({ - message: 'We couldn\'t find your account with that email', + message: "We couldn't find your account with that email", intent: Intent.DANGER, }); } @@ -73,21 +73,29 @@ function SendResetPassword({ return ( -
+
-

Reset Your Password

-

Enter your email address and we’ll send you a link to reset your password.

+

+ +

+

+ +

} + intent={errors.crediential && touched.crediential && Intent.DANGER} + helperText={ + + } className={'form-group--crediential'} > @@ -100,14 +108,14 @@ function SendResetPassword({ fill={true} loading={isSubmitting} > - {intl.formatMessage({ id: 'Send password reset link' })} +
@@ -115,6 +123,4 @@ function SendResetPassword({ ); } -export default compose( - withAuthenticationActions, -)(SendResetPassword); +export default compose(withAuthenticationActions)(SendResetPassword); diff --git a/client/src/containers/Dialogs/AccountFormDialog.js b/client/src/containers/Dialogs/AccountFormDialog.js index 1f1263a65..1ed18e91f 100644 --- a/client/src/containers/Dialogs/AccountFormDialog.js +++ b/client/src/containers/Dialogs/AccountFormDialog.js @@ -8,12 +8,13 @@ import { TextArea, MenuItem, Checkbox, - Position + Position, } from '@blueprintjs/core'; import { Select } from '@blueprintjs/select'; import * as Yup from 'yup'; import { useFormik } from 'formik'; -import { useIntl } from 'react-intl'; +import { FormattedMessage as T, useIntl } from 'react-intl'; + import { omit } from 'lodash'; import { useQuery, queryCache } from 'react-query'; @@ -27,7 +28,6 @@ import Icon from 'components/Icon'; import ErrorMessage from 'components/ErrorMessage'; import { fetchAccountTypes } from 'store/accounts/accounts.actions'; - function AccountFormDialog({ name, payload, @@ -60,22 +60,26 @@ function AccountFormDialog({ description: Yup.string().trim() }); - const initialValues = useMemo(() => ({ - account_type_id: null, - name: '', - description: '', - }), []); + const initialValues = useMemo( + () => ({ + account_type_id: null, + name: '', + description: '', + }), + [] + ); const [selectedAccountType, setSelectedAccountType] = useState(null); const [selectedSubaccount, setSelectedSubaccount] = useState( - payload.action === 'new_child' ? - accounts.find(a => a.id === payload.id) : null, + payload.action === 'new_child' + ? accounts.find((a) => a.id === payload.id) + : null ); const transformApiErrors = (errors) => { const fields = {}; - if (errors.find(e => e.type === 'NOT_UNIQUE_CODE')) { - fields.code = 'Account code is not unqiue.' + if (errors.find((e) => e.type === 'NOT_UNIQUE_CODE')) { + fields.code = 'Account code is not unqiue.'; } return fields; }; @@ -84,7 +88,7 @@ function AccountFormDialog({ const formik = useFormik({ enableReinitialize: true, initialValues: { - ...(payload.action === 'edit' && account) ? account : initialValues, + ...(payload.action === 'edit' && account ? account : initialValues), }, validationSchema: accountFormValidationSchema, onSubmit: (values, { setSubmitting, setErrors }) => { @@ -106,11 +110,6 @@ function AccountFormDialog({ }), intent: Intent.SUCCESS, }); - setSubmitting(false); - queryCache.refetchQueries('accounts-table', { force: true }); - }).catch((errors) => { - setSubmitting(false); - setErrors(transformApiErrors(errors)); }); } else { requestSubmitAccount({ form: { ...omit(values, exclude) } }).then((response) => { @@ -125,22 +124,18 @@ function AccountFormDialog({ intent: Intent.SUCCESS, position: Position.BOTTOM, }); - setSubmitting(false); - queryCache.refetchQueries('accounts-table', { force: true }); - }).catch((errors) => { - setSubmitting(false); - setErrors(transformApiErrors(errors)); }); } - } + }, }); - const { errors, values, touched } = useMemo(() => (formik), [formik]); + const { errors, values, touched } = useMemo(() => formik, [formik]); // Set default account type. useEffect(() => { if (account && account.account_type_id) { - const defaultType = accountsTypes.find((t) => - t.id === account.account_type_id); + const defaultType = accountsTypes.find( + (t) => t.id === account.account_type_id + ); defaultType && setSelectedAccountType(defaultType); } @@ -166,44 +161,64 @@ function AccountFormDialog({ // Account item of select accounts field. const accountItem = (item, { handleClick, modifiers, query }) => { return ( - + ); }; // Filters accounts items. - const filterAccountsPredicater = useCallback((query, account, _index, exactMatch) => { - const normalizedTitle = account.name.toLowerCase(); - const normalizedQuery = query.toLowerCase(); + const filterAccountsPredicater = useCallback( + (query, account, _index, exactMatch) => { + const normalizedTitle = account.name.toLowerCase(); + const normalizedQuery = query.toLowerCase(); - if (exactMatch) { - return normalizedTitle === normalizedQuery; - } else { - return `${account.code} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0; - } - }, []); + if (exactMatch) { + return normalizedTitle === normalizedQuery; + } else { + return ( + `${account.code} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0 + ); + } + }, + [] + ); // Handles dialog close. - const handleClose = useCallback(() => { closeDialog(name); }, [closeDialog, name]); + const handleClose = useCallback(() => { + closeDialog(name); + }, [closeDialog, name]); // Fetches accounts list. - const fetchAccountsList = useQuery('accounts-list', - () => requestFetchAccounts(), { manual: true }); + const fetchAccountsList = useQuery( + 'accounts-list', + () => requestFetchAccounts(), + { manual: true } + ); // Fetches accounts types. - const fetchAccountsTypes = useQuery('accounts-types-list', async () => { - await requestFetchAccountTypes(); - }, { manual: true }); + const fetchAccountsTypes = useQuery( + 'accounts-types-list', + async () => { + await requestFetchAccountTypes(); + }, + { manual: true } + ); // Fetch the given account id on edit mode. const fetchAccount = useQuery( payload.action === 'edit' && ['account', payload.id], (key, id) => requestFetchAccount(id), - { manual: true }); + { manual: true } + ); - const isFetching = ( - fetchAccountsList.isFetching || - fetchAccountTypes.isFetching || - fetchAccount.isFetching); + const isFetching = + fetchAccountsList.isFetching || + fetchAccountTypes.isFetching || + fetchAccount.isFetching; // Fetch requests on dialog opening. const onDialogOpening = useCallback(() => { @@ -212,16 +227,22 @@ function AccountFormDialog({ fetchAccount.refetch(); }, []); - const onChangeAccountType = useCallback((accountType) => { - setSelectedAccountType(accountType); - formik.setFieldValue('account_type_id', accountType.id); - }, [setSelectedAccountType, formik]); + const onChangeAccountType = useCallback( + (accountType) => { + setSelectedAccountType(accountType); + formik.setFieldValue('account_type_id', accountType.id); + }, + [setSelectedAccountType, formik] + ); // Handles change sub-account. - const onChangeSubaccount = useCallback((account) => { - setSelectedSubaccount(account); - formik.setFieldValue('parent_account_id', account.id); - }, [setSelectedSubaccount, formik]); + const onChangeSubaccount = useCallback( + (account) => { + setSelectedSubaccount(account); + formik.setFieldValue('parent_account_id', account.id); + }, + [setSelectedSubaccount, formik] + ); const onDialogClosed = useCallback(() => { formik.resetForm(); @@ -229,21 +250,25 @@ function AccountFormDialog({ setSelectedAccountType(null); }, [formik]); - const infoIcon = useMemo(() => (), []); + const infoIcon = useMemo(() => , []); const subAccountLabel = useMemo(() => { - return ({'Sub account?'} ); + return ( + + + + ); }, []); - const requiredSpan = useMemo(() => (*), []); + const requiredSpan = useMemo(() => *, []); return ( : } className={{ 'dialog--loading': isFetching, - 'dialog--account-form': true + 'dialog--account-form': true, }} autoFocus={true} canEscapeKeyClose={true} @@ -256,15 +281,18 @@ function AccountFormDialog({
} labelInfo={requiredSpan} className={classNames( 'form-group--account-type', 'form-group--select-list', - Classes.FILL)} + Classes.FILL + )} inline={true} - helperText={} - intent={(errors.account_type_id && touched.account_type_id) && Intent.DANGER} + helperText={} + intent={ + errors.account_type_id && touched.account_type_id && Intent.DANGER + } > } labelInfo={requiredSpan} className={'form-group--account-name'} - intent={(errors.name && touched.name) && Intent.DANGER} - helperText={} + intent={errors.name && touched.name && Intent.DANGER} + helperText={} inline={true} > } className={'form-group--account-code'} - intent={(errors.code && touched.code) && Intent.DANGER} - helperText={} + intent={errors.code && touched.code && Intent.DANGER} + helperText={} inline={true} labelInfo={infoIcon} > @@ -327,11 +353,12 @@ function AccountFormDialog({ {values.subaccount && ( } className={classNames( 'form-group--parent-account', 'form-group--select-list', - Classes.FILL)} + Classes.FILL + )} inline={true} > @@ -354,7 +383,7 @@ function AccountFormDialog({ )} } className={'form-group--description'} intent={formik.errors.description && Intent.DANGER} helperText={formik.errors.description && formik.errors.credential} @@ -370,9 +399,13 @@ function AccountFormDialog({
- - +
@@ -381,6 +414,4 @@ function AccountFormDialog({ ); } -export default AccountFormDialogContainer( - AccountFormDialog, -); \ No newline at end of file +export default AccountFormDialogContainer(AccountFormDialog); diff --git a/client/src/containers/Dialogs/CurrencyDialog.js b/client/src/containers/Dialogs/CurrencyDialog.js index 5688b46e2..bb7872aac 100644 --- a/client/src/containers/Dialogs/CurrencyDialog.js +++ b/client/src/containers/Dialogs/CurrencyDialog.js @@ -7,7 +7,7 @@ import { Intent, } from '@blueprintjs/core'; import * as Yup from 'yup'; -import { useIntl } from 'react-intl'; +import { FormattedMessage as T, useIntl } from 'react-intl'; import { useFormik } from 'formik'; import { useQuery } from 'react-query'; import { connect } from 'react-redux'; @@ -44,15 +44,15 @@ function CurrencyDialog({ requestSubmitCurrencies, requestEditCurrency, }) { - const intl = useIntl(); + const {formatMessage} = useIntl(); const ValidationSchema = Yup.object().shape({ currency_name: Yup.string().required( - intl.formatMessage({ id: 'required' }) + formatMessage({ id: 'required' }) ), currency_code: Yup.string() .max(4) - .required(intl.formatMessage({ id: 'required' })), + .required(formatMessage({ id: 'required' })), }); const initialValues = useMemo(() => ({ currency_name: '', @@ -126,7 +126,7 @@ function CurrencyDialog({ return ( : } className={classNames( { 'dialog--loading': fetchCurrencies.isFetching, @@ -142,7 +142,7 @@ function CurrencyDialog({
} labelInfo={requiredSpan} className={'form-group--currency-name'} intent={(errors.currency_name && touched.currency_name) && Intent.DANGER} @@ -157,7 +157,7 @@ function CurrencyDialog({ } labelInfo={requiredSpan} className={'form-group--currency-code'} intent={(errors.currency_code && touched.currency_code) && Intent.DANGER} @@ -174,9 +174,9 @@ function CurrencyDialog({
- +
diff --git a/client/src/containers/Dialogs/ExchangeRateDialog.container.js b/client/src/containers/Dialogs/ExchangeRateDialog.container.js new file mode 100644 index 000000000..2bbed9778 --- /dev/null +++ b/client/src/containers/Dialogs/ExchangeRateDialog.container.js @@ -0,0 +1,35 @@ +import { connect } from 'react-redux'; +import { compose } from 'utils'; +import { getDialogPayload } from 'store/dashboard/dashboard.reducer'; + +import DialogConnect from 'connectors/Dialog.connector'; +import DialogReduxConnect from 'components/DialogReduxConnect'; + +import withExchangeRatesActions from 'containers/ExchangeRates/withExchangeRatesActions'; +import withExchangeRates from 'containers/ExchangeRates/withExchangeRates'; +import withCurrencies from 'containers/Currencies/withCurrencies'; + + +const mapStateToProps = (state, props) => { + const dialogPayload = getDialogPayload(state, 'exchangeRate-form'); + + return { + name: 'exchangeRate-form', + payload: { action: 'new', id: null, ...dialogPayload }, + }; +}; + +const withExchangeRateDialog = connect(mapStateToProps); + +export default compose( + withExchangeRateDialog, + withCurrencies(({ currenciesList }) => ({ + currenciesList, + })), + withExchangeRatesActions, + withExchangeRates(({ exchangeRatesList }) => ({ + exchangeRatesList, + })), + DialogReduxConnect, + DialogConnect, +); \ No newline at end of file diff --git a/client/src/containers/Dialogs/ExchangeRateDialog.js b/client/src/containers/Dialogs/ExchangeRateDialog.js new file mode 100644 index 000000000..5f89197ad --- /dev/null +++ b/client/src/containers/Dialogs/ExchangeRateDialog.js @@ -0,0 +1,267 @@ +import React, { useState, useMemo, useCallback, useEffect } from 'react'; +import { + Button, + Classes, + FormGroup, + InputGroup, + Intent, + Position, + MenuItem, +} from '@blueprintjs/core'; +import { pick } from 'lodash'; +import * as Yup from 'yup'; +import { FormattedMessage as T, useIntl } from 'react-intl'; +import { useFormik } from 'formik'; +import Dialog from 'components/Dialog'; +import AppToaster from 'components/AppToaster'; + +import { useQuery, queryCache } from 'react-query'; +import ErrorMessage from 'components/ErrorMessage'; +import classNames from 'classnames'; +import { Select } from '@blueprintjs/select'; +import moment from 'moment'; +import { DateInput } from '@blueprintjs/datetime'; +import { momentFormatter } from 'utils'; + +import withExchangeRatesDialog from './ExchangeRateDialog.container'; + + +function ExchangeRateDialog({ + name, + payload, + isOpen, + + // #withDialog + closeDialog, + + // #withCurrencies + currenciesList, + + // #withExchangeRatesActions + requestSubmitExchangeRate, + requestFetchExchangeRates, + requestEditExchangeRate, + requestFetchCurrencies, + editExchangeRate, + +}) { + const { formatMessage } = useIntl(); + const [selectedItems, setSelectedItems] = useState({}); + + const validationSchema = Yup.object().shape({ + exchange_rate: Yup.number().required(), + currency_code: Yup.string().max(3).required(), + date: Yup.date().required(), + }); + + const initialValues = useMemo(() => ({ + exchange_rate: '', + currency_code: '', + date: moment(new Date()).format('YYYY-MM-DD'), + }), []); + + const { + values, + touched, + errors, + isSubmitting, + handleSubmit, + getFieldProps, + setFieldValue, + resetForm, + } = useFormik({ + enableReinitialize: true, + validationSchema, + initialValues: { + ...(payload.action === 'edit' && + pick(editExchangeRate, Object.keys(initialValues))), + }, + onSubmit: (values, { setSubmitting }) => { + if (payload.action === 'edit') { + requestEditExchangeRate(payload.id, values) + .then((response) => { + closeDialog(name); + AppToaster.show({ + message: 'the_exchange_rate_has_been_edited', + }); + setSubmitting(false); + }) + .catch((error) => { + setSubmitting(false); + }); + } else { + requestSubmitExchangeRate(values) + .then((response) => { + closeDialog(name); + AppToaster.show({ + message: 'the_exchangeRate_has_been_submit', + }); + setSubmitting(false); + }) + .catch((error) => { + setSubmitting(false); + }); + } + }, + }); + + const requiredSpan = useMemo(() => *, []); + + const handleClose = useCallback(() => { + closeDialog(name); + }, [name, closeDialog]); + + const fetchExchangeRatesDialog = useQuery('exchange-rates-dialog', + () => requestFetchExchangeRates()); + + const onDialogClosed = useCallback(() => { + resetForm(); + closeDialog(name); + }, [closeDialog, name]); + + const onDialogOpening = useCallback(() => { + fetchExchangeRatesDialog.refetch(); + }, [fetchExchangeRatesDialog]); + + const handleDateChange = useCallback( + (date) => { + const formatted = moment(date).format('YYYY-MM-DD'); + setFieldValue('date', formatted); + }, + [setFieldValue] + ); + + const onItemsSelect = useCallback( + (filedName) => { + return (filed) => { + setSelectedItems({ + ...selectedItems, + [filedName]: filed, + }); + setFieldValue(filedName, filed.currency_code); + }; + }, + [setFieldValue, selectedItems] + ); + + const filterCurrencyCode = (query, currency_code, _index, exactMatch) => { + const normalizedTitle = currency_code.currency_code.toLowerCase(); + const normalizedQuery = query.toLowerCase(); + + if (exactMatch) { + return normalizedTitle === normalizedQuery; + } else { + return ( + `${currency_code.currency_code} ${normalizedTitle}`.indexOf( + normalizedQuery + ) >= 0 + ); + } + }; + + const currencyCodeRenderer = useCallback((CurrencyCode, { handleClick }) => { + return ( + + ); + }, []); + + const getSelectedItemLabel = useCallback((fieldName, defaultLabel) => { + return typeof selectedItems[fieldName] !== 'undefined' + ? selectedItems[fieldName].currency_code + : defaultLabel; + }, [selectedItems]); + + return ( + : } + className={classNames( + {'dialog--loading': fetchExchangeRatesDialog.isFetching}, + 'dialog--exchangeRate-form' + )} + isOpen={isOpen} + onClosed={onDialogClosed} + onOpening={onDialogOpening} + isLoading={fetchExchangeRatesDialog.isFetching} + onClose={handleClose} + > + +
+ } + inline={true} + labelInfo={requiredSpan} + intent={errors.date && touched.date && Intent.DANGER} + helperText={} + > + + + + } + labelInfo={requiredSpan} + intent={errors.exchange_rate && touched.exchange_rate && Intent.DANGER} + helperText={} + inline={true} + > + + + + } + labelInfo={requiredSpan} + className={classNames('form-group--select-list', Classes.FILL)} + inline={true} + intent={(errors.currency_code && touched.currency_code) && Intent.DANGER} + helperText={} + > + + +
+
+
+ + +
+
+ +
+ ); +} + +export default withExchangeRatesDialog(ExchangeRateDialog); diff --git a/client/src/containers/Dialogs/InviteUserDialog.js b/client/src/containers/Dialogs/InviteUserDialog.js index 862388364..01e03b8a6 100644 --- a/client/src/containers/Dialogs/InviteUserDialog.js +++ b/client/src/containers/Dialogs/InviteUserDialog.js @@ -1,5 +1,5 @@ import React, { useMemo, useCallback } from 'react'; -import { useIntl } from 'react-intl'; +import { FormattedMessage as T, useIntl } from 'react-intl'; import { useFormik } from 'formik'; import * as Yup from 'yup'; import { @@ -28,7 +28,7 @@ function InviteUserDialog({ requestFetchUser, requestEditUser, }) { - const intl = useIntl(); + const { formatMessage } = useIntl(); const fetchHook = useAsync(async () => { await Promise.all([ @@ -37,12 +37,12 @@ function InviteUserDialog({ }, false); const validationSchema = Yup.object().shape({ - first_name: Yup.string().required(intl.formatMessage({ id: 'required' })), - last_name: Yup.string().required(intl.formatMessage({ id: 'required' })), + first_name: Yup.string().required(formatMessage({ id: 'required' })), + last_name: Yup.string().required(formatMessage({ id: 'required' })), email: Yup.string() .email() - .required(intl.formatMessage({ id: 'required' })), - phone_number: Yup.number().required(intl.formatMessage({ id: 'required' })), + .required(formatMessage({ id: 'required' })), + phone_number: Yup.number().required(formatMessage({ id: 'required' })), }); const initialValues = useMemo( @@ -101,7 +101,7 @@ function InviteUserDialog({ return ( : ''} className={classNames({ 'dialog--loading': fetchHook.pending, 'dialog--invite-user': true, @@ -116,7 +116,7 @@ function InviteUserDialog({
} className={'form-group--first-name'} intent={errors.first_name && touched.first_name && Intent.DANGER} helperText={} @@ -129,7 +129,7 @@ function InviteUserDialog({ } className={'form-group--last-name'} intent={errors.last_name && touched.last_name && Intent.DANGER} helperText={} @@ -142,7 +142,7 @@ function InviteUserDialog({ } className={'form-group--email'} intent={errors.email && touched.email && Intent.DANGER} helperText={} @@ -156,7 +156,7 @@ function InviteUserDialog({ } className={'form-group--phone-number'} intent={ errors.phone_number && touched.phone_number && Intent.DANGER @@ -175,9 +175,9 @@ function InviteUserDialog({
- +
diff --git a/client/src/containers/Dialogs/ItemCategoryDialog.js b/client/src/containers/Dialogs/ItemCategoryDialog.js index 688a698f2..47c8c2b07 100644 --- a/client/src/containers/Dialogs/ItemCategoryDialog.js +++ b/client/src/containers/Dialogs/ItemCategoryDialog.js @@ -11,7 +11,7 @@ import { import { Select } from '@blueprintjs/select'; import { pick } from 'lodash'; import * as Yup from 'yup'; -import { useIntl } from 'react-intl'; +import { FormattedMessage as T, useIntl } from 'react-intl'; import { useFormik } from 'formik'; import { compose } from 'utils'; import { useQuery, queryCache } from 'react-query'; @@ -165,7 +165,7 @@ function ItemCategoryDialog({ return ( : } className={classNames({ 'dialog--loading': fetchList.isFetching, }, @@ -180,7 +180,7 @@ function ItemCategoryDialog({
} labelInfo={requiredSpan} className={'form-group--category-name'} intent={(errors.name && touched.name) && Intent.DANGER} @@ -195,7 +195,7 @@ function ItemCategoryDialog({ } labelInfo={infoIcon} className={classNames( 'form-group--select-list', @@ -215,7 +215,6 @@ function ItemCategoryDialog({ onItemSelect={onChangeParentCategory} > +
diff --git a/client/src/containers/Dialogs/ItemFromDialog.js b/client/src/containers/Dialogs/ItemFromDialog.js index e7b6c28c3..d110e1edb 100644 --- a/client/src/containers/Dialogs/ItemFromDialog.js +++ b/client/src/containers/Dialogs/ItemFromDialog.js @@ -5,10 +5,10 @@ import { FormGroup, InputGroup, Intent, - TextArea + TextArea, } from '@blueprintjs/core'; import * as Yup from 'yup'; -import { useIntl } from 'react-intl'; +import { FormattedMessage as T, useIntl } from 'react-intl'; import { useFormik } from 'formik'; import { compose } from 'utils'; import Dialog from 'components/Dialog'; @@ -25,30 +25,30 @@ function ItemFromDialog({ submitItemCategory, fetchCategory, openDialog, - closeDialog + closeDialog, }) { const [state, setState] = useState({}); - const intl = useIntl(); + const { formatMessage } = useIntl(); const ValidationSchema = Yup.object().shape({ - name: Yup.string().required(intl.formatMessage({ id: 'required' })), - description: Yup.string().trim() + name: Yup.string().required(formatMessage({ id: 'required' })), + description: Yup.string().trim(), }); const formik = useFormik({ enableReinitialize: true, initialValues: {}, validationSchema: ValidationSchema, - onSubmit: values => { + onSubmit: (values) => { submitItemCategory({ values }) - .then(response => { + .then((response) => { AppToaster.show({ - message: 'the_category_has_been_submit' + message: 'the_category_has_been_submit', }); }) - .catch(error => { + .catch((error) => { alert(error.message); }); - } + }, }); const fetchHook = useAsync(async () => { @@ -71,10 +71,12 @@ function ItemFromDialog({ return ( : + } className={{ 'dialog--loading': state.isLoading, - 'dialog--item-form': true + 'dialog--item-form': true, }} isOpen={isOpen} onClosed={onDialogClosed} @@ -84,7 +86,7 @@ function ItemFromDialog({
} className={'form-group--category-name'} intent={formik.errors.name && Intent.DANGER} helperText={formik.errors.name && formik.errors.name} @@ -97,7 +99,7 @@ function ItemFromDialog({ /> } className={'form-group--description'} intent={formik.errors.description && Intent.DANGER} helperText={formik.errors.description && formik.errors.credential} @@ -112,9 +114,15 @@ function ItemFromDialog({
- +
diff --git a/client/src/containers/Dialogs/UserFormDialog.js b/client/src/containers/Dialogs/UserFormDialog.js index 9343b1860..506426eef 100644 --- a/client/src/containers/Dialogs/UserFormDialog.js +++ b/client/src/containers/Dialogs/UserFormDialog.js @@ -1,5 +1,5 @@ import React, { useMemo, useCallback } from 'react'; -import { useIntl } from 'react-intl'; +import { FormattedMessage as T, useIntl } from 'react-intl'; import { useFormik } from 'formik'; import * as Yup from 'yup'; import { @@ -87,7 +87,13 @@ function UserFormDialog({ return ( + ) : ( + + ) + } className={classNames({ 'dialog--loading': fetchHook.pending, 'dialog--invite-form': true, @@ -101,18 +107,20 @@ function UserFormDialog({ >
-

Your teammate will get an email that gives them access to your team.

+

+ +

} className={classNames('form-group--email', Classes.FILL)} - intent={(errors.email && touched.email) && Intent.DANGER} - helperText={} + intent={errors.email && touched.email && Intent.DANGER} + helperText={} inline={true} > @@ -120,9 +128,9 @@ function UserFormDialog({
- +
diff --git a/client/src/containers/ExchangeRates/ExchangeRate.js b/client/src/containers/ExchangeRates/ExchangeRate.js new file mode 100644 index 000000000..f2e0d5847 --- /dev/null +++ b/client/src/containers/ExchangeRates/ExchangeRate.js @@ -0,0 +1,132 @@ +import React, { useEffect, useState, useCallback } from 'react'; +import { useQuery } from 'react-query'; +import { useParams } from 'react-router-dom'; +import { Alert, Intent } from '@blueprintjs/core'; +import AppToaster from 'components/AppToaster'; + +import DashboardPageContent from 'components/Dashboard/DashboardPageContent'; +import DashboardInsider from 'components/Dashboard/DashboardInsider'; +import ExchangeRateTable from './ExchangeRateTable'; +import ExchangeRateActionsBar from './ExchangeRateActionsBar'; + +import withDashboardActions from 'containers/Dashboard/withDashboard'; +import withResourceActions from 'containers/Resources/withResourcesActions'; +import withExchangeRatesActions from 'containers/ExchangeRates/withExchangeRatesActions'; + +import { compose } from 'utils'; + +import { FormattedMessage as T, useIntl } from 'react-intl'; + +function ExchangeRate({ + // #withDashboard + changePageTitle, + + //#withResourceActions + requestFetchResourceFields, + + // #withExchangeRatesActions + requestFetchExchangeRates, + requestDeleteExchangeRate, + addExchangeRatesTableQueries, +}) { + const { id } = useParams(); + const [deleteExchangeRate, setDeleteExchangeRate] = useState(false); + const [selectedRows, setSelectedRows] = useState([]); + const { formatMessage } = useIntl(); + + // const fetchExchangeRates = useQuery('exchange-rates-table', () => { + // return Promise.all([requestFetchExchangeRates()]); + // }); + + const fetchExchangeRates = useQuery('exchange-rates-table', + () => requestFetchExchangeRates(), + { refetchInterval: 3000 }); + + + useEffect(() => { + id + ? changePageTitle(formatMessage({id:'exchange_rate_details'})) + : changePageTitle(formatMessage({id:'exchange_rate_list'})); + }, [id, changePageTitle]); + + const handelDeleteExchangeRate = useCallback( + (exchange_rate) => { + setDeleteExchangeRate(exchange_rate); + }, + [setDeleteExchangeRate] + ); + + const handelEditExchangeRate = (exchange_rate) => {}; + + const handelCancelExchangeRateDelete = useCallback(() => { + setDeleteExchangeRate(false); + }, [setDeleteExchangeRate]); + + const handelConfirmExchangeRateDelete = useCallback(() => { + requestDeleteExchangeRate(deleteExchangeRate.id).then(() => { + setDeleteExchangeRate(false); + AppToaster.show({ + message: 'the_exchange_rate_has_been_delete', + }); + }); + }, [deleteExchangeRate, requestDeleteExchangeRate]); + + // Handle fetch data of Exchange_rates datatable. + const handleFetchData = useCallback( + ({ pageIndex, pageSize, sortBy }) => { + addExchangeRatesTableQueries({ + ...(sortBy.length > 0 + ? { + column_sort_by: sortBy[0].id, + sort_order: sortBy[0].desc ? 'desc' : 'asc', + } + : {}), + }); + }, + [addExchangeRatesTableQueries] + ); + + const handleSelectedRowsChange = useCallback( + (exchange_rates) => { + setSelectedRows(exchange_rates); + }, + [setSelectedRows] + ); + + return ( + + + + + } + confirmButtonText={} + icon='trash' + intent={Intent.DANGER} + isOpen={deleteExchangeRate} + onCancel={handelCancelExchangeRateDelete} + onConfirm={handelConfirmExchangeRateDelete} + > +

+ Are you sure you want to move filename to Trash? You will be + able to restore it later, but it will become private to you. +

+
+
+
+ ); +} + +export default compose( + withExchangeRatesActions, + withResourceActions, + withDashboardActions +)(ExchangeRate); diff --git a/client/src/containers/ExchangeRates/ExchangeRateActionsBar.js b/client/src/containers/ExchangeRates/ExchangeRateActionsBar.js new file mode 100644 index 000000000..af432a388 --- /dev/null +++ b/client/src/containers/ExchangeRates/ExchangeRateActionsBar.js @@ -0,0 +1,125 @@ +import React, { useCallback, useState, useMemo } from 'react'; +import { + NavbarGroup, + Button, + Classes, + Intent, + Popover, + Position, + PopoverInteractionKind, +} from '@blueprintjs/core'; +import classNames from 'classnames'; +import Icon from 'components/Icon'; +import { connect } from 'react-redux'; + +import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar'; +import DialogConnect from 'connectors/Dialog.connector'; + +import FilterDropdown from 'components/FilterDropdown'; +import withResourceDetail from 'containers/Resources/withResourceDetails'; + +import { compose } from 'utils'; +import { FormattedMessage as T } from 'react-intl'; + + +function ExchangeRateActionsBar({ + // #withDialog. + openDialog, + + // #withResourceDetail + resourceFields, + + selectedRows = [], + onDeleteExchangeRate, + onFilterChanged, +}) { + const [filterCount, setFilterCount] = useState(0); + + const onClickNewExchangeRate = () => { + openDialog('exchangeRate-form', {}); + }; + + const filterDropdown = FilterDropdown({ + fields: resourceFields, + onFilterChange: (filterConditions) => { + setFilterCount(filterConditions.length || 0); + + onFilterChanged && onFilterChanged(filterConditions); + }, + }); + + const handelDeleteExchangeRate = useCallback( + (exchangeRate) => { + onDeleteExchangeRate(exchangeRate); + }, + [selectedRows, onDeleteExchangeRate] + ); + + const hasSelectedRows = useMemo(() => selectedRows.length > 0, [ + selectedRows, + ]); + + return ( + + + - - + + +
@@ -465,5 +550,5 @@ export default compose( AccountsConnect, ItemsConnect, ItemCategoryConnect, - MediaConnect, -)(ItemForm); \ No newline at end of file + MediaConnect +)(ItemForm); diff --git a/client/src/containers/Items/ItemFormPage.js b/client/src/containers/Items/ItemFormPage.js index 9927ef219..fe55a3a28 100644 --- a/client/src/containers/Items/ItemFormPage.js +++ b/client/src/containers/Items/ItemFormPage.js @@ -10,6 +10,7 @@ import withAccountsActions from 'containers/Accounts/withAccountsActions'; import withItemCategoriesActions from 'containers/Items/withItemCategoriesActions'; import { compose } from 'utils'; +import { FormattedMessage as T, useIntl } from 'react-intl'; const ItemFormContainer = ({ @@ -23,11 +24,11 @@ const ItemFormContainer = ({ requestFetchItemCategories, }) => { const { id } = useParams(); - + const {formatMessage} =useIntl() useEffect(() => { id ? - changePageTitle('Edit Item Details') : - changePageTitle('New Item'); + changePageTitle(formatMessage({id:'edit_item_details'})) : + changePageTitle(formatMessage({id:'new_item'})); }, [id, changePageTitle]); const fetchAccounts = useQuery('accounts-list', diff --git a/client/src/containers/Items/ItemsActionsBar.js b/client/src/containers/Items/ItemsActionsBar.js index 107b64ea2..9d4a62a4c 100644 --- a/client/src/containers/Items/ItemsActionsBar.js +++ b/client/src/containers/Items/ItemsActionsBar.js @@ -21,6 +21,7 @@ import DialogConnect from 'connectors/Dialog.connector'; import withResourceDetail from 'containers/Resources/withResourceDetails'; import withItems from 'containers/Items/withItems'; import { If } from 'components'; +import { FormattedMessage as T, useIntl } from 'react-intl'; const ItemsActionsBar = ({ openDialog, @@ -70,7 +71,7 @@ const ItemsActionsBar = ({ -
+
+ +
-
- - - } - inline={true} - fill={true}> +
+ + + + } + inline={true} + fill={true} + > + + + + +
+ +
Columns Preferences
+ +
+ + +
Available Columns
- - -
-
+ placeholder={intl.formatMessage({ id: 'search' })} + leftIcon='search' + /> -
Columns Preferences
- -
- - -
Available Columns
+
+ + + {availableColumns.map((field) => ( + + ))} + + +
+ - + +
+
+ +
+
+ +
+
+ -
- - - {availableColumns.map((field) => ( - - ))} - - -
- + +
Selected Columns
+ - -
-
-
-
- +
+ + + {draggedColumns.map((field) => ( + + ))} + + +
+ +
+
- -
Selected Columns
- - -
- - - {draggedColumns.map((field) => ( - - ))} - - -
- -
-
- - - -
+ + + + + + +
+ +
); } -export default ViewFormContainer(ViewForm); \ No newline at end of file +export default ViewFormContainer(ViewForm); diff --git a/client/src/containers/Views/ViewFormPage.js b/client/src/containers/Views/ViewFormPage.js index 50812b89d..c40a46e55 100644 --- a/client/src/containers/Views/ViewFormPage.js +++ b/client/src/containers/Views/ViewFormPage.js @@ -2,7 +2,7 @@ import React, {useEffect, useState, useMemo, useCallback} from 'react'; import { useAsync } from 'react-use'; import { useParams } from 'react-router-dom'; import { Intent, Alert } from '@blueprintjs/core'; -import { FormattedHTMLMessage, useIntl } from 'react-intl'; +import { FormattedMessage as T, FormattedHTMLMessage, useIntl } from 'react-intl'; import DashboardInsider from 'components/Dashboard/DashboardInsider'; import DashboardPageContent from 'components/Dashboard/DashboardPageContent'; @@ -50,23 +50,27 @@ function ViewFormPage({ useEffect(() => { if (viewId) { - changePageTitle('Edit Custom View'); + changePageTitle(formatMessage({id:'edit_custom_view'})); } else { - changePageTitle('New Custom View'); + changePageTitle(formatMessage({id:'new_custom_view'})); } return () => { changePageTitle(''); }; }, [viewId, changePageTitle]); + + // Handle delete view button click. const handleDeleteView = useCallback((view) => { setStateDeleteView(view); }, []); + // Handle cancel delete button click. const handleCancelDeleteView = useCallback(() => { setStateDeleteView(null); }, []); + // Handle confirm delete custom view. const handleConfirmDeleteView = useCallback(() => { requestDeleteView(stateDeleteView.id).then((response) => { setStateDeleteView(null); @@ -92,8 +96,8 @@ function ViewFormPage({ onDelete={handleDeleteView} /> } + confirmButtonText={} icon="trash" intent={Intent.DANGER} isOpen={stateDeleteView} @@ -107,7 +111,7 @@ function ViewFormPage({ -

Something wrong

+

diff --git a/client/src/lang/en/index.js b/client/src/lang/en/index.js index 00c688cbe..fcff12224 100644 --- a/client/src/lang/en/index.js +++ b/client/src/lang/en/index.js @@ -1,32 +1,169 @@ - - export default { - 'hello_world': 'Hello World', - 'email_or_phone_number': 'Email or phone number', - 'password': 'Password', - 'login': 'Login', - 'invalid_email_or_phone_numner': 'Invalid email or phone number.', - 'required': 'Required', - 'reset_password': 'Reset Password', - 'the_user_has_been_suspended_from_admin': 'The user has been suspended from the administrator.', - 'email_and_password_entered_did_not_match': 'The email and password you entered did not match our records.', - 'field_name_must_be_number': 'field_name_must_be_number', - 'name': 'Name', - "search": "Search", - 'reference': 'Reference', - 'date': 'Date', - 'description': 'Description', - 'from_date': 'From date', - 'to_date': 'To date', - 'accounting_basis': 'Accounting basis', - 'report_date_range': 'Report date range', - 'log_in': 'Log in', - 'forget_my_password': 'Forget my password', - 'keep_me_logged_in': 'Keep me logged in', - 'create_an_account': 'Create an account', - 'need_bigcapital_account?': 'Need a Bigcapital account ?', - 'show': 'Show', - 'hide': 'Hide', + hello_world: 'Hello World', + email_or_phone_number: 'Email or phone number', + password: 'Password', + login: 'Login', + invalid_email_or_phone_numner: 'Invalid email or phone number.', + required: 'Required', + reset_password: 'Reset Password', + the_user_has_been_suspended_from_admin: 'The user has been suspended from the administrator.', + email_and_password_entered_did_not_match: + 'The email and password you entered did not match our records.', + field_name_must_be_number: 'field_name_must_be_number', + name: 'Name', + search: 'Search', + reference: 'Reference', + date: 'Date', + description: 'Description', + from_date: 'From date', + to_date: 'To date', + accounting_basis: 'Accounting basis', + report_date_range: 'Report date range', + log_in: 'Log in', + forget_my_password: 'Forget my password', + keep_me_logged_in: 'Keep me logged in', + create_an_account: 'Create an account', + need_bigcapital_account: 'Need a Bigcapital account ?', + show: 'Show', + hide: 'Hide', + an_unexpected_error_occurred: 'An unexpected error occurred', + welcome_to_bigcapital: 'Welcome to Bigcapital', + enter_your_personal_information: ' Enter your personal information', + first_name: 'First Name', + last_name: 'Last Name', + phone_number: 'Phone Number', + you_email_address_is: 'You email address is', + you_will_use_this_address_to_sign_in_to_bigcapital: + 'You will use this address to sign in to Bigcapital.', + signing_in_or_creating: + 'By signing in or creating an account, you agree with our', + terms_conditions: 'Terms & Conditions', + and: 'and', + privacy_statement: 'Privacy Statement', + create_account: 'Create Account', + success: 'Success', + register_a_new_organization: 'Register a New Organization.', + you_have_a_bigcapital_account: 'You have a bigcapital account ?', + organization_name: 'Organization Name', + email: 'Email', + register: 'Register', + password_successfully_updated: 'The Password for your account was successfully updated.', + choose_a_new_password: 'Choose a new password', + you_remembered_your_password: 'You remembered your password ?', + new_password: 'New Password', + submit_new_password: 'Submit new password', + reset_your_password: 'Reset Your Password', + we_ll_send_you_a_link_to_reset_your_password: 'Enter your email address and we’ll send you a link to reset your password.', + send_password_reset_link: 'Send password reset link', + return_to_log_in: 'Return to log in', + sub_account: 'Sub account?', + account_type: 'Account Type', + account_name: 'Account Name', + account_code: 'Account Code', + parent_account: 'Parent Account', + edit: 'Edit', + submit: 'Submit', + close: 'Close', + edit_account: 'Edit Account', + new_account: 'New Account', + edit_currency: 'Edit Currency', + new_currency: 'New Currency', + currency_name: 'Currency Name', + currency_code: 'Currency Code', + edit_exchange_rate: 'Edit Exchange Rate', + new_exchange_rate: 'New Exchange Rate', + delete_exchange_rate: 'Delete Exchange Rate', + exchange_rate: 'Exchange Rate', + currency_code: 'Currency Code', + edit_invite: 'Edit invite', + edit_category: 'Edit Category', + delete_category: 'Delete Category', + new_category: 'New Category', + category_name: 'Category Name', + parent_category: 'Parent Category', + new: 'New', + new_category: 'New Category', + invite_user: 'invite User', + your_access_to_your_team: 'Your teammate will get an email that gives them access to your team.', + invite: 'invite', + count: 'Count', + item_type: 'Item Type', + item_name: 'Item Name', + sku: 'SKU', + category: 'Category', + account: 'Account', + sales_information: 'Sales Information', + purchase_information: 'Purchase Information', + selling_price: 'Selling Price', + cost_price: 'Cost Price', + inventory_information: 'Inventory Information', + inventory_account: 'Inventory Account', + opening_stock: 'Opening Stock', + save: 'Save', + save_as_draft: 'Save as Draft', + active: 'Active', + new_item: 'New Item', + table_views: 'Table Views', + delete: 'Delete', + import: 'Import', + export: 'Export', + filter: 'Filter', + view_details: 'View Details', + edit_item: 'Edit Item', + delete_item: 'Delete Item', + sell_price: 'Sell Price', + cancel: 'Cancel', + move_to_trash: 'Move to Trash', + save_new: 'Save & New', + journal_number: 'Journal number', + credit_currency: 'Credit ({currency})', + debit_currency: 'Debit ({currency})', + note: 'Note', + new_lines: 'New lines', + clear_all_lines: 'Clear all lines', + new_journal: 'New Journal', + publish_journal: 'Publish Journal', + edit_journal: 'Edit Journal', + delete_journal: 'Delete Journal', + amount: 'Amount', + journal_no: 'Journal No.', + status: 'Status', + transaction_type: 'Transaction type', + created_at: 'Created At', + archive: 'Archive', + inactivate: 'Inactivate', + activate: 'Activate', + inactivate_account: 'Inactivate Account', + delete_account: 'Delete Account', + code: 'Code', + type: 'Type', + normal: 'Normal', + balance: 'Balance', + something_wrong: 'Something wrong', + filters: 'Filters', + add_order: 'Add order', + expense_account: 'Expense Account', + payment_account: 'Payment Account', + new_expense: 'New Expense', + bulk_update: 'Bulk Update', + all_accounts: 'All accounts', + go_to_bigcapital_com: '← Go to bigcapital.com', + currency: 'Currency', + new_conditional: '+ New Conditional', + chart_of_accounts: 'Chart of Accounts', + exchange_rate_details: 'Exchange Rate Details', + exchange_rate_list: 'Exchange Rate List', + manual_journals: 'Manual Journals', + edit_expense_details: 'Edit Expense Details', + expenses_list: 'Expenses List', + edit_category_details: 'Edit Category Details', + category_list: 'Category List', + edit_item_details: 'Edit Item Details', + items_list: 'Items List', + edit_custom_view: 'Edit Custom View', + new_custom_view: 'New Custom View', + view_name: 'View Name', + new_conditional: 'New Conditional', 'item': 'Item', 'account': 'Account', 'service_has_been_successful_created': '{service} {name} has been successfully created.', @@ -42,22 +179,17 @@ export default { 'the_accounts_has_been_successfully_deleted': 'The accounts have been successfully deleted.', 'are_sure_to_inactive_this_account': 'Are you sure you want to inactive this account? You will be able to activate it later', 'are_sure_to_activate_this_account': 'Are you sure you want to activate this account? You will be able to inactivate it later', - 'once_delete_this_account_you_will_able_to_restore_it': `Once you delete this account, you won\'t be able to restore it later. Are you sure you want to delete this account?

If you're not sure, you can inactivate this account instead.`, 'the_journal_has_been_successfully_created': 'The journal #{number} has been successfully created.', 'the_journal_has_been_successfully_edited': 'The journal #{number} has been successfully edited.', - 'credit': 'Credit', 'debit': 'Debit', - 'once_delete_this_item_you_will_able_to_restore_it': `Once you delete this item, you won\'t be able to restore the item later. Are you sure you want to delete ?

If you're not sure, you can inactivate it instead.`, 'the_item_has_been_successfully_deleted': 'The item has been successfully deleted.', - 'the_item_category_has_been_successfully_created': 'The item category has been successfully created.', 'the_item_category_has_been_successfully_edited': 'The item category has been successfully edited.', - 'once_delete_these_views_you_will_not_able_restore_them': 'Once you delete the custom view, you won\'t be able to restore it later. Are you sure you want to delete this view?', 'the_custom_view_has_been_successfully_deleted': 'The custom view has been successfully deleted.', - - 'teammate_invited_to_organization_account': 'Your teammate has been invited to the organization account.' -}; \ No newline at end of file + 'teammate_invited_to_organization_account': 'Your teammate has been invited to the organization account.', + 'select_account_type': 'Select account type', +}; diff --git a/client/src/routes/dashboard.js b/client/src/routes/dashboard.js index 3297aa8f0..b558fa2ac 100644 --- a/client/src/routes/dashboard.js +++ b/client/src/routes/dashboard.js @@ -7,7 +7,7 @@ export default [ { path: `${BASE_URL}/homepage`, component: LazyLoader({ - loader: () => import('containers/Homepage/Homepage') + loader: () => import('containers/Homepage/Homepage'), }), }, @@ -15,79 +15,76 @@ export default [ { path: `${BASE_URL}/accounts`, component: LazyLoader({ - loader: () => import('containers/Accounts/AccountsChart') - }) + loader: () => import('containers/Accounts/AccountsChart'), + }), }, // Custom views. { path: `${BASE_URL}/custom_views/:resource_slug/new`, component: LazyLoader({ - loader: () => import('containers/Views/ViewFormPage') - }) + loader: () => import('containers/Views/ViewFormPage'), + }), }, { path: `${BASE_URL}/custom_views/:view_id/edit`, component: LazyLoader({ - loader: () => import('containers/Views/ViewFormPage') - }) + loader: () => import('containers/Views/ViewFormPage'), + }), }, // Expenses. { path: `${BASE_URL}/expenses/new`, component: LazyLoader({ - loader: () => import('containers/Expenses/ExpenseForm') + loader: () => import('containers/Expenses/ExpenseForm'), }), }, { path: `${BASE_URL}/expenses`, component: LazyLoader({ - loader: () => import('containers/Expenses/ExpensesList') - }) + loader: () => import('containers/Expenses/ExpensesList'), + }), }, // Accounting { path: `${BASE_URL}/accounting/make-journal-entry`, component: LazyLoader({ - loader: () => - import('containers/Accounting/MakeJournalEntriesPage') + loader: () => import('containers/Accounting/MakeJournalEntriesPage'), }), }, { path: `${BASE_URL}/accounting/manual-journals/:id/edit`, component: LazyLoader({ - loader: () => - import('containers/Accounting/MakeJournalEntriesPage') + loader: () => import('containers/Accounting/MakeJournalEntriesPage'), }), }, { path: `${BASE_URL}/accounting/manual-journals`, component: LazyLoader({ - loader: () => - import('containers/Accounting/ManualJournalsList') + loader: () => import('containers/Accounting/ManualJournalsList'), }), }, { path: `${BASE_URL}/items/categories`, component: LazyLoader({ - loader: () => import('containers/Items/ItemCategoriesList') - }) - }, + loader: () => import('containers/Items/ItemCategoriesList'), + }), + }, { path: `${BASE_URL}/items/new`, component: LazyLoader({ - loader: () => import('containers/Items/ItemFormPage') - }) + loader: () => import('containers/Items/ItemFormPage'), + }), }, // Items { path: `${BASE_URL}/items`, component: LazyLoader({ - loader: () => import('containers/Items/ItemsList') - }) + loader: () => import('containers/Items/ItemsList'), + }), }, // Financial Reports. @@ -95,19 +92,15 @@ export default [ path: `${BASE_URL}/accounting/general-ledger`, component: LazyLoader({ loader: () => - import( - 'containers/FinancialStatements/GeneralLedger/GeneralLedger' - ) - }) + import('containers/FinancialStatements/GeneralLedger/GeneralLedger'), + }), }, { path: `${BASE_URL}/accounting/balance-sheet`, component: LazyLoader({ loader: () => - import( - 'containers/FinancialStatements/BalanceSheet/BalanceSheet' - ) - }) + import('containers/FinancialStatements/BalanceSheet/BalanceSheet'), + }), }, { path: `${BASE_URL}/accounting/trial-balance-sheet`, @@ -115,8 +108,8 @@ export default [ loader: () => import( 'containers/FinancialStatements/TrialBalanceSheet/TrialBalanceSheet' - ) - }) + ), + }), }, { path: `${BASE_URL}/accounting/profit-loss-sheet`, @@ -124,14 +117,20 @@ export default [ loader: () => import( 'containers/FinancialStatements/ProfitLossSheet/ProfitLossSheet' - ) - }) + ), + }), }, { path: `${BASE_URL}/accounting/journal-sheet`, + component: LazyLoader({ + loader: () => import('containers/FinancialStatements/Journal/Journal'), + }), + }, + { + path: `${BASE_URL}/ExchangeRates`, component: LazyLoader({ loader: () => - import('containers/FinancialStatements/Journal/Journal') - }) + import('containers/ExchangeRates/ExchangeRate'), + }), }, ]; diff --git a/client/src/store/ExchangeRate/exchange.actions.js b/client/src/store/ExchangeRate/exchange.actions.js new file mode 100644 index 000000000..8bf497b39 --- /dev/null +++ b/client/src/store/ExchangeRate/exchange.actions.js @@ -0,0 +1,65 @@ +import ApiService from 'services/ApiService'; +import t from 'store/types'; + +export const fetchExchangeRates = () => { + return (dispatch) => + new Promise((resolve, reject) => { + dispatch({ + type: t.SET_DASHBOARD_REQUEST_LOADING, + }); + dispatch({ + type: t.EXCHANGE_RATE_TABLE_LOADING, + loading: true, + }); + ApiService.get('exchange_rates') + .then((response) => { + dispatch({ + type: t.EXCHANGE_RATE_LIST_SET, + exchange_rates: response.data.exchange_rates.results, + }); + dispatch({ + type: t.SET_DASHBOARD_REQUEST_COMPLETED, + }); + dispatch({ + type: t.EXCHANGE_RATE_TABLE_LOADING, + loading: false, + }); + resolve(response); + }) + .catch((error) => { + reject(error); + }); + }); +}; + +export const submitExchangeRate = ({ form }) => { + return (dispatch) => { + return ApiService.post('exchange_rates', form); + }; +}; + +export const deleteExchangeRate = (id) => { + return (dispatch) => ApiService.delete(`exchange_rates/${id}`); +}; + +export const editExchangeRate = (id, form) => { + return (dispatch) => + new Promise((resolve, reject) => { + ApiService.post(`exchange_rates/${id}`, form) + .then((response) => { + dispatch({ type: t.CLEAR_EXCHANGE_RATE_FORM_ERRORS }); + resolve(response); + }) + .catch((error) => { + const { response } = error; + const { data } = response; + const { errors } = data; + + dispatch({ type: t.CLEAR_EXCHANGE_RATE_FORM_ERRORS }); + if (errors) { + dispatch({ type: t.CLEAR_EXCHANGE_RATE_FORM_ERRORS, errors }); + } + reject(error); + }); + }); +}; diff --git a/client/src/store/ExchangeRate/exchange.reducer.js b/client/src/store/ExchangeRate/exchange.reducer.js new file mode 100644 index 000000000..efcc2c4ae --- /dev/null +++ b/client/src/store/ExchangeRate/exchange.reducer.js @@ -0,0 +1,23 @@ +import { createReducer } from '@reduxjs/toolkit'; +import t from 'store/types'; + +const initialState = { + exchangeRates: {}, +}; + +export default createReducer(initialState, { + [t.EXCHANGE_RATE_LIST_SET]: (state, action) => { + const _exchangeRates = {}; + action.exchange_rates.forEach((exchange_rate) => { + _exchangeRates[exchange_rate.id] = exchange_rate; + }); + + state.exchangeRates = { + ...state.exchangeRates, + ..._exchangeRates, + }; + }, + [t.EXCHANGE_RATE_TABLE_LOADING]: (state, action) => { + state.loading = action.loading; + }, +}); diff --git a/client/src/store/ExchangeRate/exchange.type.js b/client/src/store/ExchangeRate/exchange.type.js new file mode 100644 index 000000000..7e07ab04b --- /dev/null +++ b/client/src/store/ExchangeRate/exchange.type.js @@ -0,0 +1,8 @@ +export default { + EXCHANGE_RATE_DATA_TABLE: 'EXCHANGE_RATE_DATA_TABLE', + EXCHANGE_RATE_DELETE: 'EXCHANGE_RATE_DELETE', + EXCHANGE_RATE_LIST_SET: 'EXCHANGE_RATE_LIST_SET', + CLEAR_EXCHANGE_RATE_FORM_ERRORS: 'CLEAR_EXCHANGE_RATE_FORM_ERRORS', + ExchangeRates_TABLE_QUERIES_ADD: 'ExchangeRates_TABLE_QUERIES_ADD', + EXCHANGE_RATE_TABLE_LOADING:'EXCHANGE_RATE_TABLE_LOADING' +}; diff --git a/client/src/store/reducers.js b/client/src/store/reducers.js index d476baf29..326d421cd 100644 --- a/client/src/store/reducers.js +++ b/client/src/store/reducers.js @@ -15,6 +15,7 @@ import itemCategories from './itemCategories/itemsCategory.reducer'; import settings from './settings/settings.reducer'; import manualJournals from './manualJournals/manualJournals.reducers'; import globalSearch from './search/search.reducer'; +import exchangeRates from './ExchangeRate/exchange.reducer' export default combineReducers({ authentication, @@ -32,4 +33,6 @@ export default combineReducers({ itemCategories, settings, globalSearch, + exchangeRates + }); diff --git a/client/src/store/types.js b/client/src/store/types.js index ed4bbad1b..dab91bc24 100644 --- a/client/src/store/types.js +++ b/client/src/store/types.js @@ -15,6 +15,7 @@ import itemCategories from './itemCategories/itemsCategory.type'; import settings from './settings/settings.type'; import search from './search/search.type'; import register from './registers/register.type'; +import exchangeRate from './ExchangeRate/exchange.type'; export default { ...authentication, @@ -34,4 +35,6 @@ export default { ...accounting, ...search, ...register, + ...exchangeRate, + }; diff --git a/client/src/style/App.scss b/client/src/style/App.scss index e3c6cc10c..faec559fa 100644 --- a/client/src/style/App.scss +++ b/client/src/style/App.scss @@ -49,6 +49,7 @@ $pt-font-family: Noto Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, @import 'pages/invite-form.scss'; @import "pages/currency"; @import "pages/invite-user.scss"; +@import 'pages/exchange-rate.scss'; // Views @import 'views/filter-dropdown'; diff --git a/client/src/style/pages/exchange-rate.scss b/client/src/style/pages/exchange-rate.scss new file mode 100644 index 000000000..0b5f270ef --- /dev/null +++ b/client/src/style/pages/exchange-rate.scss @@ -0,0 +1,23 @@ +.exchangeRate{ + + + &-menu { + width: 240px; + } +} + +.dialog--exchangeRate-form { + + + + .bp3-dialog-body { + .bp3-form-group.bp3-inline { + .bp3-label { + min-width: 140px; + } + .bp3-form-content { + width: 250px; + } + } + } +} \ No newline at end of file diff --git a/server/src/models/ExchangeRate.js b/server/src/models/ExchangeRate.js index e6c205082..55487e4aa 100644 --- a/server/src/models/ExchangeRate.js +++ b/server/src/models/ExchangeRate.js @@ -2,7 +2,6 @@ import bcrypt from 'bcryptjs'; import { Model } from 'objection'; import TenantModel from '@/models/TenantModel'; - export default class ExchangeRate extends TenantModel { /** * Table name.